mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-06-27 14:22:10 +00:00
Compare commits
30 Commits
5a4b6c69a5
...
2.12.2
Author | SHA1 | Date | |
---|---|---|---|
b050c409cf | |||
d77cf7c4bb | |||
e03fcbfab5 | |||
62dd77317d | |||
95056cfbe7 | |||
2d31a415ce | |||
5cbeec40f8 | |||
4a644fc80f | |||
d1aeeda975 | |||
53aedbb9aa | |||
c941226a9b | |||
69cbbfce65 | |||
52d69fa9f6 | |||
279ac785a1 | |||
f51b8e2358 | |||
b116e6fa07 | |||
7b667c8157 | |||
c51463d36b | |||
79947af013 | |||
e6d6cb006b | |||
f553cc059a | |||
c99d4f6f0f | |||
554827cc57 | |||
8524f1eb20 | |||
4b6feb9ae6 | |||
ad901067d8 | |||
4c20897e14 | |||
f8cbd07259 | |||
16ad96d8c6 | |||
6bd1636bfa |
@ -67,7 +67,7 @@ ignored-modules=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
init-hook='import sys; sys.path.append("pylint_plugins")'
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use, and will cap the count on Windows to
|
||||
@ -81,7 +81,8 @@ limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python module names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
load-plugins=pylint.extensions.docparams,
|
||||
definition_order,
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
@ -227,7 +228,7 @@ name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
no-docstring-rgx=
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
|
@ -26,7 +26,7 @@ In order to resolve all difficult cases the `autopep8` is used. You can perform
|
||||
Again, the most checks can be performed by `make check` command, though some additional guidelines must be applied:
|
||||
|
||||
* Every class, every function (including private and protected), every attribute must be documented. The project follows [Google style documentation](https://google.github.io/styleguide/pyguide.html). The only exception is local functions.
|
||||
* Correct way to document function, if section is empty, e.g. no notes or there are no args, it should be omitted:
|
||||
* Correct way to document function (if a section is empty, e.g. no notes or there are no args, it should be omitted) is the following:
|
||||
|
||||
```python
|
||||
def foo(argument: str, *, flag: bool = False) -> int:
|
||||
@ -43,7 +43,8 @@ Again, the most checks can be performed by `make check` command, though some add
|
||||
flag(bool, optional): a flag (Default value = False)
|
||||
|
||||
Returns:
|
||||
int: result
|
||||
int: result with
|
||||
long description
|
||||
|
||||
Raises:
|
||||
RuntimeError: a local function error occurs
|
||||
@ -64,7 +65,7 @@ Again, the most checks can be performed by `make check` command, though some add
|
||||
```python
|
||||
class Clazz(BaseClazz):
|
||||
"""
|
||||
brand-new implementation of ``BaseClazz``
|
||||
brand-new implementation of :class:`BaseClazz`
|
||||
|
||||
Attributes:
|
||||
CLAZZ_ATTRIBUTE(int): (class attribute) a brand-new class attribute
|
||||
@ -92,7 +93,7 @@ Again, the most checks can be performed by `make check` command, though some add
|
||||
|
||||
* Type annotations are the must, even for local functions. For the function argument `self` (for instance methods) and `cls` (for class methods) should not be annotated.
|
||||
* For collection types built-in classes must be used if possible (e.g. `dict` instead of `typing.Dict`, `tuple` instead of `typing.Tuple`). In case if built-in type is not available, but `collections.abc` provides interface, it must be used (e.g. `collections.abc.Awaitable` instead of `typing.Awaitable`, `collections.abc.Iterable` instead of `typing.Iterable`). For union classes, the bar operator (`|`) must be used (e.g. `float | int` instead of `typing.Union[float, int]`), which also includes `typinng.Optional` (e.g. `str | None` instead of `Optional[str]`).
|
||||
* `classmethod` should always return `Self`. In case of mypy warning (e.g. if there is a branch in which function doesn't return the instance of `cls`) consider using `staticmethod` instead.
|
||||
* `classmethod` should (almost) always return `Self`. In case of mypy warning (e.g. if there is a branch in which function doesn't return the instance of `cls`) consider using `staticmethod` instead.
|
||||
* Recommended order of function definitions in class:
|
||||
|
||||
```python
|
||||
@ -121,10 +122,12 @@ Again, the most checks can be performed by `make check` command, though some add
|
||||
def __hash__(self) -> int: ... # basically any magic (or look-alike) method
|
||||
```
|
||||
|
||||
Methods inside one group should be ordered alphabetically, the only exception is `__init__` method (`__post__init__` for dataclasses) which should be defined first. For test methods it is recommended to follow the order in which functions are defined.
|
||||
Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses), `__new__` and `__del__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined.
|
||||
|
||||
Though, we would like to highlight abstract methods (i.e. ones which raise `NotImplementedError`), we still keep in global order at the moment.
|
||||
|
||||
For the most cases there is custom `pylint` plugin, which performs checks automatically.
|
||||
|
||||
* Abstract methods must raise `NotImplementedError` instead of using `abc.abstractmethod`. The reason behind this restriction is the fact that we have class/static abstract methods for those we need to define their attribute first making the code harder to read.
|
||||
* For any path interactions `pathlib.Path` must be used.
|
||||
* Configuration interactions must go through `ahriman.core.configuration.Configuration` class instance.
|
||||
@ -167,7 +170,7 @@ Again, the most checks can be performed by `make check` command, though some add
|
||||
* 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):
|
||||
* Web API methods must be documented by using `aiohttp_apispec` library. The schema testing mostly should be implemented in related view class tests. Recommended example for documentation (excluding comments):
|
||||
|
||||
```python
|
||||
import aiohttp_apispec
|
||||
@ -191,6 +194,7 @@ Again, the most checks can be performed by `make check` command, though some add
|
||||
class Foo(BaseView):
|
||||
|
||||
POST_PERMISSION = ...
|
||||
ROUTES = ...
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Tag"],
|
||||
@ -202,6 +206,7 @@ Again, the most checks can be performed by `make check` command, though some add
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema}, # exception raised by this method
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema}, # should be always presented
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema}, # should be always presented
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema}, # include if BaseView.service() method is called
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema}, # should be always presented
|
||||
},
|
||||
security=[{"token": [POST_PERMISSION]}],
|
||||
@ -215,7 +220,30 @@ Again, the most checks can be performed by `make check` command, though some add
|
||||
|
||||
* It is allowed to change web API to add new fields or remove optional ones. However, in case of model changes, new API version must be introduced.
|
||||
* On the other hand, it is allowed to change method signatures, however, it is recommended to add new parameters as optional if possible. Deprecated API can be dropped during major release.
|
||||
* Enumerations (`Enum` classes) are allowed and recommended. However, it is recommended to use `StrEnum` class if there are from/to string conversions and `IntEnum` otherwise.
|
||||
|
||||
### Other checks
|
||||
|
||||
The projects also uses typing checks (provided by `mypy`) and some linter checks provided by `pylint` and `bandit`. Those checks must be passed successfully for any open pull requests.
|
||||
|
||||
## Developers how to
|
||||
|
||||
### Run automated checks
|
||||
|
||||
```shell
|
||||
make check tests
|
||||
```
|
||||
|
||||
### Generate documentation templates
|
||||
|
||||
```shell
|
||||
make specification
|
||||
```
|
||||
|
||||
### Create release
|
||||
|
||||
```shell
|
||||
make VERSION=x.y.z check tests release
|
||||
```
|
||||
|
||||
The command above will also run checks first and will generate documentation, tags, etc., and will push them to GitHub. Other things will be handled by GitHub workflows automatically.
|
||||
|
@ -10,6 +10,8 @@ ENV AHRIMAN_OUTPUT=""
|
||||
ENV AHRIMAN_PACKAGER="ahriman bot <ahriman@example.com>"
|
||||
ENV AHRIMAN_PACMAN_MIRROR=""
|
||||
ENV AHRIMAN_PORT=""
|
||||
ENV AHRIMAN_POSTSETUP_COMMAND=""
|
||||
ENV AHRIMAN_PRESETUP_COMMAND=""
|
||||
ENV AHRIMAN_REPOSITORY="aur-clone"
|
||||
ENV AHRIMAN_REPOSITORY_SERVER=""
|
||||
ENV AHRIMAN_REPOSITORY_ROOT="/var/lib/ahriman/ahriman"
|
||||
|
6
Makefile
6
Makefile
@ -1,9 +1,9 @@
|
||||
.PHONY: archive archlinux check clean directory html push specification tests version
|
||||
.PHONY: archive archlinux check clean directory html release specification tests version
|
||||
.DEFAULT_GOAL := archlinux
|
||||
|
||||
PROJECT := ahriman
|
||||
|
||||
FILES := AUTHORS CONTRIBUTING.md COPYING Makefile README.md SECURITY.md docs package pyproject.toml src tox.ini web.png
|
||||
FILES := AUTHORS CONTRIBUTING.md COPYING README.md SECURITY.md package pyproject.toml src web.png
|
||||
TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES))
|
||||
IGNORE_FILES := package/archlinux src/.mypy_cache
|
||||
|
||||
@ -37,7 +37,7 @@ html: specification
|
||||
rm -rf docs/html
|
||||
tox -e docs-html
|
||||
|
||||
push: specification archlinux
|
||||
release: specification archlinux
|
||||
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)"
|
||||
|
14
README.md
14
README.md
@ -4,39 +4,39 @@
|
||||
[](https://github.com/arcan1s/ahriman/actions/workflows/setup.yml)
|
||||
[](https://hub.docker.com/r/arcan1s/ahriman)
|
||||
[](https://www.codefactor.io/repository/github/arcan1s/ahriman)
|
||||
[](https://ahriman.readthedocs.io/?badge=latest)
|
||||
[](https://ahriman.readthedocs.io)
|
||||
|
||||
Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
|
||||
|
||||
## Features
|
||||
|
||||
* Install-configure-forget manager for the very own repository.
|
||||
* Multi-architecture support.
|
||||
* Multi architecture and repository support.
|
||||
* Dependency manager.
|
||||
* 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).
|
||||
* 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:
|
||||
|
||||

|
||||
|
||||
## Installation and run
|
||||
|
||||
For installation details kindly refer to the [documentation](https://ahriman.readthedocs.io/en/latest/setup.html). For application commands it is possible to get information by using `--help`/`help` command or by using man page ([web version](https://ahriman.readthedocs.io/en/latest/command-line.html)).
|
||||
For installation details kindly refer to the [documentation](https://ahriman.readthedocs.io/en/stable/setup.html). For application commands it is possible to get information by using `--help`/`help` command or by using man page ([web version](https://ahriman.readthedocs.io/en/stable/command-line.html)).
|
||||
|
||||
## Configuration
|
||||
|
||||
Every available option is described in the [documentation](https://ahriman.readthedocs.io/en/latest/configuration.html).
|
||||
Every available option is described in the [documentation](https://ahriman.readthedocs.io/en/stable/configuration.html).
|
||||
|
||||
The application provides reasonable defaults which allow to use it out-of-box; however additional steps (like configuring build toolchain and sudoers) are recommended and can be easily achieved by following install instructions.
|
||||
|
||||
## [FAQ](https://ahriman.readthedocs.io/en/latest/faq.html)
|
||||
## [FAQ](https://ahriman.readthedocs.io/en/stable/faq.html)
|
||||
|
||||
## Live demos
|
||||
|
||||
* [Build status page](https://ahriman-demo.arcanis.me). You can log in as `demo` user by using `demo` password. However, you will not be able to run tasks. [HTTP API documentation](https://ahriman-demo.arcanis.me/api-docs) is also available.
|
||||
* [Repository index](http://repo.arcanis.me/arcanisrepo/x86_64/).
|
||||
* [Repository index](https://repo.arcanis.me/arcanisrepo/x86_64/).
|
||||
* [Telegram feed](https://t.me/arcanisrepo).
|
||||
|
@ -49,7 +49,10 @@ fi
|
||||
if [ -n "$AHRIMAN_UNIX_SOCKET" ]; then
|
||||
AHRIMAN_SETUP_ARGS+=("--web-unix-socket" "$AHRIMAN_UNIX_SOCKET")
|
||||
fi
|
||||
|
||||
[ -n "$AHRIMAN_PRESETUP_COMMAND" ] && eval "$AHRIMAN_PRESETUP_COMMAND"
|
||||
ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" service-setup "${AHRIMAN_SETUP_ARGS[@]}"
|
||||
[ -n "$AHRIMAN_POSTSETUP_COMMAND" ] && eval "$AHRIMAN_POSTSETUP_COMMAND"
|
||||
|
||||
# validate configuration if set
|
||||
[ -n "$AHRIMAN_VALIDATE_CONFIGURATION" ] && ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" service-config-validate --exit-code
|
||||
|
File diff suppressed because it is too large
Load Diff
Before Width: | Height: | Size: 839 KiB After Width: | Height: | Size: 993 KiB |
@ -116,6 +116,14 @@ ahriman.application.handlers.restore module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.application.handlers.run module
|
||||
---------------------------------------
|
||||
|
||||
.. automodule:: ahriman.application.handlers.run
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.application.handlers.search module
|
||||
------------------------------------------
|
||||
|
||||
|
@ -100,6 +100,14 @@ ahriman.web.schemas.package\_names\_schema module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.schemas.package\_patch\_schema module
|
||||
-------------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.web.schemas.package_patch_schema
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.schemas.package\_properties\_schema module
|
||||
------------------------------------------------------
|
||||
|
||||
@ -132,6 +140,22 @@ ahriman.web.schemas.pagination\_schema module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.schemas.patch\_name\_schema module
|
||||
----------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.web.schemas.patch_name_schema
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.schemas.patch\_schema module
|
||||
----------------------------------------
|
||||
|
||||
.. automodule:: ahriman.web.schemas.patch_schema
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.schemas.pgp\_key\_id\_schema module
|
||||
-----------------------------------------------
|
||||
|
||||
@ -204,6 +228,14 @@ ahriman.web.schemas.update\_flags\_schema module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.schemas.versioned\_log\_schema module
|
||||
-------------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.web.schemas.versioned_log_schema
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
|
@ -30,6 +30,14 @@ ahriman.web.views.index module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.views.static module
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: ahriman.web.views.static
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
|
@ -28,6 +28,22 @@ ahriman.web.views.v1.status.packages module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.views.v1.status.patch module
|
||||
----------------------------------------
|
||||
|
||||
.. automodule:: ahriman.web.views.v1.status.patch
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.views.v1.status.patches module
|
||||
------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.web.views.v1.status.patches
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.views.v1.status.repositories module
|
||||
-----------------------------------------------
|
||||
|
||||
|
@ -41,9 +41,8 @@ Base configuration settings.
|
||||
|
||||
* ``apply_migrations`` - perform migrations on application start, boolean, optional, default ``yes``. Useful if you are using git version. Note, however, that this option must be changed only if you know what to do and going to handle migrations automatically.
|
||||
* ``database`` - path to SQLite database, string, required.
|
||||
* ``include`` - path to directory with configuration files overrides, string, optional. Note, however, that the application will also load configuration files from the repository root, which is used, in particular, by setup subcommand.
|
||||
* ``include`` - path to directory with configuration files overrides, string, optional.
|
||||
* ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference.
|
||||
* ``suppress_http_log_errors`` - suppress http log errors, boolean, optional, default ``no``. If set to ``yes``, any http log errors (e.g. if web server is not available, but http logging is enabled) will be suppressed.
|
||||
|
||||
``alpm:*`` groups
|
||||
-----------------
|
||||
@ -86,7 +85,7 @@ Build related configuration. Group name can refer to architecture, e.g. ``build:
|
||||
* ``makechrootpkg_flags`` - additional flags passed to ``makechrootpkg`` command, space separated list of strings, optional.
|
||||
* ``triggers`` - list of ``ahriman.core.triggers.Trigger`` class implementation (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of mention.
|
||||
* ``triggers_known`` - optional list of ``ahriman.core.triggers.Trigger`` class implementations which are not run automatically and used only for trigger discovery and configuration validation.
|
||||
* ``vcs_allowed_age`` - maximal age in seconds of the VCS packages before their version will be updated with its remote source, int, optional, default ``604800``.
|
||||
* ``vcs_allowed_age`` - maximal age in seconds of the VCS packages before their version will be updated with its remote source, integer, optional, default ``604800``.
|
||||
|
||||
``repository`` group
|
||||
--------------------
|
||||
@ -103,6 +102,17 @@ Settings for signing packages or repository. Group name can refer to architectur
|
||||
* ``target`` - configuration flag to enable signing, space separated list of strings, required. Allowed values are ``package`` (sign each package separately), ``repository`` (sign repository database file).
|
||||
* ``key`` - default PGP key, string, required. This key will also be used for database signing if enabled.
|
||||
|
||||
``status`` group
|
||||
----------------
|
||||
|
||||
Reporting to web service related settings. In most cases there is fallback to web section settings.
|
||||
|
||||
* ``enabled`` - enable reporting to web service, boolean, optional, default ``yes`` for backward compatibility.
|
||||
* ``address`` - remote web service address with protocol, string, optional. In case of websocket, the ``http+unix`` scheme and url encoded address (e.g. ``%2Fvar%2Flib%2Fahriman`` for ``/var/lib/ahriman``) must be used, e.g. ``http+unix://%2Fvar%2Flib%2Fahriman%2Fsocket``. In case if none set, it will be guessed from ``web`` section.
|
||||
* ``password`` - password to authorize in web service in order to update service status, string, required in case if authorization enabled.
|
||||
* ``suppress_http_log_errors`` - suppress http log errors, boolean, optional, default ``no``. If set to ``yes``, any http log errors (e.g. if web server is not available, but http logging is enabled) will be suppressed.
|
||||
* ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled.
|
||||
|
||||
``web`` group
|
||||
-------------
|
||||
|
||||
@ -116,15 +126,13 @@ Web server settings. If any of ``host``/``port`` is not set, web integration wil
|
||||
* ``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.
|
||||
* ``port`` - port to bind, integer, optional.
|
||||
* ``static_path`` - path to directory with static files, string, required.
|
||||
* ``templates`` - path to templates directories, space separated list of strings, required.
|
||||
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
|
||||
* ``timeout`` - HTTP request timeout in seconds, integer, 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.
|
||||
* ``wait_timeout`` - wait timeout in seconds, maximum amount of time to be waited before lock will be free, integer, optional.
|
||||
|
||||
``keyring`` group
|
||||
--------------------
|
||||
@ -237,7 +245,7 @@ Section name must be either ``email`` (plus optional architecture name, e.g. ``e
|
||||
* ``link_path`` - prefix for HTML links, string, required.
|
||||
* ``no_empty_report`` - skip report generation for empty packages list, boolean, optional, default ``yes``.
|
||||
* ``password`` - SMTP password to authenticate, string, optional.
|
||||
* ``port`` - SMTP port for sending emails, int, required.
|
||||
* ``port`` - SMTP port for sending emails, integer, required.
|
||||
* ``receivers`` - SMTP receiver addresses, space separated list of strings, required.
|
||||
* ``sender`` - SMTP sender address, string, required.
|
||||
* ``ssl`` - SSL mode for SMTP connection, one of ``ssl``, ``starttls``, ``disabled``, optional, default ``disabled``.
|
||||
@ -267,7 +275,7 @@ Section name must be either ``remote-call`` (plus optional architecture name, e.
|
||||
* ``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``.
|
||||
* ``wait_timeout`` - maximum amount of time in seconds to be waited before remote process will be terminated, integer, optional, default ``-1``.
|
||||
|
||||
``telegram`` type
|
||||
^^^^^^^^^^^^^^^^^
|
||||
@ -282,7 +290,7 @@ Section name must be either ``telegram`` (plus optional architecture name, e.g.
|
||||
* ``template`` - Jinja2 template name, string, required.
|
||||
* ``template_type`` - ``parse_mode`` to be passed to telegram API, one of ``MarkdownV2``, ``HTML``, ``Markdown``, string, optional, default ``HTML``.
|
||||
* ``templates`` - path to templates directories, space separated list of strings, required.
|
||||
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
|
||||
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
|
||||
|
||||
``upload`` group
|
||||
----------------
|
||||
@ -312,7 +320,7 @@ This feature requires GitHub key creation (see below). Section name must be eith
|
||||
#. Generate new token. Required scope is ``public_repo`` (or ``repo`` for private repository support).
|
||||
|
||||
* ``repository`` - GitHub repository name, string, required. Repository must be created before any action and must have active branch (e.g. with readme).
|
||||
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
|
||||
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
|
||||
* ``use_full_release_name`` - if set to ``yes``, the release will contain both repository name and architecture, and only architecture otherwise, boolean, optional, default ``no`` (legacy behavior).
|
||||
* ``username`` - GitHub authorization user, string, required. Basically the same as ``owner``.
|
||||
|
||||
@ -322,7 +330,7 @@ This feature requires GitHub key creation (see below). Section name must be eith
|
||||
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``.
|
||||
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
|
||||
|
||||
``rsync`` type
|
||||
^^^^^^^^^^^^^^
|
||||
@ -341,7 +349,7 @@ Requires ``boto3`` library to be installed. Section name must be either ``s3`` (
|
||||
* ``type`` - type of the upload, string, optional, must be set to ``s3`` if exists.
|
||||
* ``access_key`` - AWS access key ID, string, required.
|
||||
* ``bucket`` - bucket name (e.g. ``bucket``), string, required.
|
||||
* ``chunk_size`` - chunk size for calculating entity tags, int, optional, default 8 * 1024 * 1024.
|
||||
* ``chunk_size`` - chunk size for calculating entity tags, integer, optional, default 8 * 1024 * 1024.
|
||||
* ``object_path`` - path prefix for stored objects, string, optional. If none set, the prefix as in repository tree will be used.
|
||||
* ``region`` - bucket region (e.g. ``eu-central-1``), string, required.
|
||||
* ``secret_key`` - AWS secret access key, string, required.
|
36
docs/faq.rst
36
docs/faq.rst
@ -200,6 +200,14 @@ Alternatively you can create full-diff patches, which are calculated by using ``
|
||||
|
||||
The last command will calculate diff from current tree to the ``HEAD`` and will store it locally. Patches will be applied on any package actions (e.g. it can be used for dependency management).
|
||||
|
||||
It is also possible to create simple patch during package addition, e.g.:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
sudo -u ahriman ahriman package-add ahriman --variable PKGEXT=.pkg.tar.xz
|
||||
|
||||
The ``--variable`` argument accepts variables in shell like format: quotation and lists are supported as usual, but functions are not. This feature is useful in particular in order to override specific makepkg variables during build.
|
||||
|
||||
How to build package from official repository
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@ -402,6 +410,8 @@ The following environment variables are supported:
|
||||
* ``AHRIMAN_PACKAGER`` - packager name from which packages will be built, default is ``ahriman bot <ahriman@example.com>``.
|
||||
* ``AHRIMAN_PACMAN_MIRROR`` - override pacman mirror server if set.
|
||||
* ``AHRIMAN_PORT`` - HTTP server port if any, default is empty.
|
||||
* ``AHRIMAN_POSTSETUP_COMMAND`` - if set, the command which will be called (as root) after the setup command, but before any other actions.
|
||||
* ``AHRIMAN_PRESETUP_COMMAND`` - if set, the command which will be called (as root) right before the setup command.
|
||||
* ``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.
|
||||
@ -429,7 +439,7 @@ This command uses same rules as ``repo-update``, thus, e.g. requires ``--privile
|
||||
Web service setup
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Well for that you would need to have web container instance running forever; it can be achieved by the following command:
|
||||
For that you would need to have web container instance running forever; it can be achieved by the following command:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
@ -451,6 +461,20 @@ Otherwise, you would need to pass ``AHRIMAN_PORT`` and mount container network t
|
||||
|
||||
docker run --privileged --net=host -e AHRIMAN_PORT=8080 -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest
|
||||
|
||||
Mutli-repository web service
|
||||
""""""""""""""""""""""""""""
|
||||
|
||||
Idea is pretty same as to just run web service. However, it is required to run setup commands for each repository, except for one which is specified by ``AHRIMAN_REPOSITORY`` and ``AHRIMAN_ARCHITECTURE`` variables.
|
||||
|
||||
In order to create configuration for additional repositories, the ``AHRIMAN_POSTSETUP_COMMAND`` variable should be used, e.g.:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
docker run --privileged -p 8080:8080 -e AHRIMAN_PORT=8080 -e AHRIMAN_UNIX_SOCKET=/var/lib/ahriman/ahriman/ahriman-web.sock -e AHRIMAN_POSTSETUP_COMMAND="ahriman --architecture x86_64 --repository aur-clone-v2 service-setup --build-as-user ahriman --packager 'ahriman bot <ahriman@example.com>'" -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest
|
||||
|
||||
The command above will also create configuration for the repository named ``aur-clone-v2``.
|
||||
|
||||
Note, however, that the command above is only required in case if the service is going to be used to run subprocesses. Otherwise, everything else (web interface, status, etc) will be handled as usual.
|
||||
|
||||
Non-x86_64 architecture setup
|
||||
-----------------------------
|
||||
@ -845,12 +869,12 @@ Worker nodes configuration
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[web]
|
||||
address = master.example.com
|
||||
[status]
|
||||
address = https://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.
|
||||
As it has been mentioned above, ``status.address`` must be available for workers. In case if unix socket is used, it can be passed in the same option as usual. Optional ``status.username``/``status.password`` can be supplied in case if authentication was enabled on master node.
|
||||
|
||||
#.
|
||||
Each worker must call master node on success:
|
||||
@ -934,7 +958,7 @@ The user ``worker-user`` has been created additionally. Worker node config (``wo
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[web]
|
||||
[status]
|
||||
address = http://172.17.0.1:8080
|
||||
username = worker-user
|
||||
password = very-secure-password
|
||||
@ -1118,7 +1142,7 @@ How to enable basic authorization
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[web]
|
||||
[status]
|
||||
username = api
|
||||
password = pa55w0rd
|
||||
|
||||
|
@ -7,21 +7,21 @@ Features
|
||||
--------
|
||||
|
||||
* Install-configure-forget manager for the very own repository.
|
||||
* Multi-architecture support.
|
||||
* Multi architecture and repository support.
|
||||
* Dependency manager.
|
||||
* 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).
|
||||
* 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.
|
||||
|
||||
Live demos
|
||||
----------
|
||||
|
||||
* `Build status page <https://ahriman-demo.arcanis.me>`_. You can login as ``demo`` user by using ``demo`` password. Note, however, you will not be able to run tasks. `HTTP API documentation <https://ahriman-demo.arcanis.me/api-docs>`_ is also available.
|
||||
* `Repository index <http://repo.arcanis.me/x86_64/index.html>`_.
|
||||
* `Repository index <https://repo.arcanis.me/x86_64/index.html>`_.
|
||||
* `Telegram feed <https://t.me/arcanisrepo>`_.
|
||||
|
||||
Contents
|
||||
|
@ -1,7 +1,7 @@
|
||||
Manual migrations
|
||||
=================
|
||||
|
||||
Normally most of migrations are handled automatically after application start. However, some upgrades require manual interventions; this document describes them.
|
||||
Normally the most of migrations are handled automatically after application start, however, some upgrades require manual interventions; this document describes them.
|
||||
|
||||
Upgrades to breakpoints
|
||||
-----------------------
|
||||
@ -13,8 +13,8 @@ This release includes major upgrade for the newest devtools and archlinux reposi
|
||||
|
||||
#. Upgrade to the latest major release of python (3.11) (required by other changes).
|
||||
#. Upgrade devtools to the latest release.
|
||||
#. Backup your settings, ``/etc/ahriman.ini.d/00-setup-overrides.ini`` by default.
|
||||
#. Run setup command (i.e. ``ahriman service-setup``) again with the same arguments as you used before. This step can be done manually by moving ``devtools`` configuration (something like ``/usr/share/devtools/pacman-ahriman*.conf``) to new location ``/usr/share/devtools/pacman.conf.d/`` under name ``ahriman.conf``. After that make sure to remove any ``community`` mentions from configurations (e.g. ``/usr/share/devtools/pacman.conf.d/ahriman.conf``, ``/etc/ahriman.ini``) if there were any. The only thing which will change is ``devtools`` configuration.
|
||||
#. Backup local settings, ``/etc/ahriman.ini.d/00-setup-overrides.ini`` by default.
|
||||
#. Run setup command (i.e. ``ahriman service-setup``) again with the same arguments as used before. This step can be done manually by moving ``devtools`` configuration (something like ``/usr/share/devtools/pacman-ahriman*.conf``) to new location ``/usr/share/devtools/pacman.conf.d/`` under name ``ahriman.conf``. After that make sure to remove any ``community`` mentions from configurations (e.g. ``/usr/share/devtools/pacman.conf.d/ahriman.conf``, ``/etc/ahriman.ini``) if there were any. The only thing which will change is ``devtools`` configuration.
|
||||
#. Remove build chroot as it is incompatible, e.g. ``sudo ahriman service-clean --chroot``.
|
||||
#. Run ``sudo -u ahriman ahriman update --no-aur --no-local --no-manual -yy`` in order to update local databases.
|
||||
|
||||
@ -27,7 +27,7 @@ This release includes paths migration. Unlike usual case, no automatic migration
|
||||
* Path to pacman databases now includes repository name too, it has been changed from ``/var/lib/ahriman/pacman/x86_64`` to ``/var/lib/ahriman/pacman/aur-clone/x86_64``.
|
||||
* Path to repository itself also includes repository name, from ``/var/lib/ahriman/repository/x86_64`` to ``/var/lib/ahriman/repository/aur-clone/x86_64``.
|
||||
|
||||
In order to migrate to new filesystem tree the following actions are required:
|
||||
In order to migrate to the new filesystem tree the following actions are required:
|
||||
|
||||
#.
|
||||
Stop and disable all services, e.g. timer and web service:
|
||||
@ -38,13 +38,13 @@ In order to migrate to new filesystem tree the following actions are required:
|
||||
sudo systemctl disable --now ahriman-web@x86_64
|
||||
|
||||
#.
|
||||
Create directory tree. It can be done by running ``ahriman service-tree-migrate`` subcommand. It performs copying between the old repository tree and the new one. Alternatively you can copy directories by hands.
|
||||
Create directory tree. It can be done by running ``ahriman service-tree-migrate`` subcommand. It performs copying between the old repository tree and the new one. Alternatively directories can be copied by hands.
|
||||
|
||||
#.
|
||||
Edit configuration in case if anything is pointing to the old path, e.g. HTML report generation, in the way in which it will be pointed to directory inside repository specific one, e.g. ``/var/lib/ahriman/repository/x86_64`` to ``/var/lib/ahriman/repository/aur-clone/x86_64``.
|
||||
Edit configuration in case if anything is pointing to the old path, e.g. HTML report generation, in the way in which it will point to the directory inside repository specific one, e.g. ``/var/lib/ahriman/repository/x86_64`` to ``/var/lib/ahriman/repository/aur-clone/x86_64``.
|
||||
|
||||
#.
|
||||
Edit devtools pacman configuration (``/usr/share/devtools/pacman.conf.d/ahriman-x86_64.conf`` by default) replacing ``Server`` with path to your repository, e.g.:
|
||||
Run setup command (i.e. ``ahriman service-setup``) again with the same arguments as used before. This step can be done manually by editing devtools pacman configuration (``/usr/share/devtools/pacman.conf.d/ahriman-x86_64.conf`` by default) replacing ``Server`` with path to the repository, e.g.:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
@ -52,15 +52,13 @@ In order to migrate to new filesystem tree the following actions are required:
|
||||
SigLevel = Optional TrustAll
|
||||
Server = file:///var/lib/ahriman/repository/aur-clone/x86_64
|
||||
|
||||
Alternatively it can be done by running ``service-setup`` command again.
|
||||
|
||||
#. If you didn't run setup command on the previous step, make sure to remove architecture reference from ``web`` section (if any).
|
||||
In case of manual interventions make sure to remove architecture reference from ``web`` sections (if any) to avoid ambiguity.
|
||||
|
||||
#.
|
||||
Make sure to update remote synchronization services if any. Almost all of them rely on current repository tree by default, so you need to setup either redirects or configure to synchronize to the old locations (e.g. ``object_path`` option for S3 synchronization).
|
||||
Make sure to update remote synchronization services if any. Almost all of them rely on current repository tree by default, so it is required to setup either redirects or configure to synchronize to the old locations (e.g. ``object_path`` option for S3 synchronization).
|
||||
|
||||
#.
|
||||
Enable and start services again. Unit template parameter should include both repository architecture and name, dash separated, e.g. ``x86_64-aur-clone``:
|
||||
Enable and start services again. Unit template parameter should include both repository architecture and name, dash separated, e.g. ``x86_64-aur-clone``, where ``x86_64`` is the repository architecture and ``aur-clone`` is the repository name:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Maintainer: Evgeniy Alekseev
|
||||
|
||||
pkgname='ahriman'
|
||||
pkgver=2.11.0
|
||||
pkgver=2.12.2
|
||||
pkgrel=1
|
||||
pkgdesc="ArcH linux ReposItory MANager"
|
||||
arch=('any')
|
||||
|
@ -13,30 +13,30 @@ post_upgrade() {
|
||||
|
||||
_2_9_0_1_changes() {
|
||||
cat << EOF
|
||||
It was found that you were upgrading from old-devtools package to the new one, which requires manual intervention:
|
||||
It was found that there was an upgrade from old devtools package to the new one, which requires manual intervention:
|
||||
|
||||
* make sure that devtools are upgraded to the latest release;
|
||||
* merge service configuration if required;
|
||||
* run setup command (i.e. sudo ahriman service-setup) with the same arguments as you did before;
|
||||
* remove build chroot: sudo rm -r /var/lib/ahriman/chroot/ahriman-x86_64/;
|
||||
* update local databases: sudo -u ahriman ahriman update --no-aur --no-local --no-manual -yy;
|
||||
* merge upstream configuration into local configuration (e.g. by using pacdiff);
|
||||
* run setup command (i.e. ahriman service-setup) with the same arguments as did before;
|
||||
* remove build chroot, e.g.: ahriman service-clean --chroot;
|
||||
* update local databases: ahriman update --no-aur --no-local --no-manual -yy.
|
||||
|
||||
For more information kindly refer to migration notes https://ahriman.readthedocs.io/en/stable/migration.html
|
||||
For more information kindly refer to migration notes https://ahriman.readthedocs.io/en/stable/migration.html.
|
||||
EOF
|
||||
}
|
||||
|
||||
_2_12_0_1_changes() {
|
||||
cat << EOF
|
||||
Whereas old tree is still supported it is highly recommended to migrate to the new one:
|
||||
Whereas old local tree is still supported it is highly recommended to migrate to the new one:
|
||||
|
||||
* stop and disable all services;
|
||||
* run service-tree-migrate as ahriman user;
|
||||
* edit configuration to avoid pointing to the old paths;
|
||||
* update devtools pacman configuration (e.g. /usr/share/devtools/pacman.conf.d/ahriman-x86_64.conf) with new repository path;
|
||||
* update synchronization services in order to support new paths (or setup redirects);
|
||||
* enable web and timer services again by using x86_64-aur-clone suffix, where x86_64 is your architecture and
|
||||
aur-clone is repository name;
|
||||
* edit local configuration to avoid pointing to the old paths;
|
||||
* run setup command (i.e. ahriman service-setup) with the same arguments as did before;
|
||||
* update remote services in order to support new paths (or setup redirects) if any;
|
||||
* enable web and timer services again by using x86_64-aur-clone suffix,
|
||||
where x86_64 is the repository architecture and aur-clone is the repository name.
|
||||
|
||||
For more information kindly refer to migration notes https://ahriman.readthedocs.io/en/stable/migration.html
|
||||
For more information kindly refer to migration notes https://ahriman.readthedocs.io/en/stable/migration.html.
|
||||
EOF
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ include = ahriman.ini.d
|
||||
logging = ahriman.ini.d/logging.ini
|
||||
apply_migrations = yes
|
||||
database = /var/lib/ahriman/ahriman.db
|
||||
suppress_http_log_errors = yes
|
||||
|
||||
[alpm]
|
||||
database = /var/lib/pacman
|
||||
@ -62,6 +61,10 @@ ssl = disabled
|
||||
template = repo-index.jinja2
|
||||
templates = /usr/share/ahriman/templates
|
||||
|
||||
[status]
|
||||
enabled = yes
|
||||
suppress_http_log_errors = yes
|
||||
|
||||
[telegram]
|
||||
template = telegram-index.jinja2
|
||||
templates = /usr/share/ahriman/templates
|
||||
|
@ -6,10 +6,8 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<!-- Embed elements Elements via Web Component -->
|
||||
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css" type="text/css">
|
||||
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
<script src="https://cdn.jsdelivr.net/npm/@stoplight/elements@7.13.7/web-components.min.js" integrity="sha384-aKMPitODat9Dqj3Mva9Rs9jS5Z3KPSW0sFlAOazuULJMFYhAfmORI5SlH9aWIst8" crossorigin="anonymous" type="application/javascript"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@stoplight/elements@7.13.7/styles.min.css" integrity="sha384-wPzTs1aFAoGq9gqp9NAs2YVTkFXcU2d6Bx11aKRFhVw2B7o1bCwaV9pGHTlUfD2+" crossorigin="anonymous" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
@ -5,8 +5,6 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
{% include "utils/style.jinja2" %}
|
||||
{% include "user-style.jinja2" ignore missing %}
|
||||
</head>
|
||||
@ -17,16 +15,16 @@
|
||||
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<a class="navbar-brand" href="#"><img src="/static/logo.svg" width="30" height="30" alt=""> ahriman</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-supported-content" aria-controls="navbar-supported-content" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<div class="navbar-brand"><img src="/static/logo.svg" width="30" height="30" alt=""> ahriman</div>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#repositories-navbar-supported-content" aria-controls="repositories-navbar-supported-content" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div id="navbar-supported-content" class="collapse navbar-collapse">
|
||||
<div id="repositories-navbar-supported-content" class="collapse navbar-collapse">
|
||||
<ul id="repositories" class="nav nav-tabs">
|
||||
{% for repository in repositories %}
|
||||
<li class="nav-item">
|
||||
<a id="{{ repository.id }}-lnk" class="nav-link" href="#{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</a>
|
||||
<a id="{{ repository.id }}-link" class="nav-link" href="#{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@ -42,7 +40,7 @@
|
||||
|
||||
{% if not auth.enabled or auth.username is not none %}
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-box"></i> packages
|
||||
<i class="bi bi-box"></i><span class="d-none d-sm-inline"> packages</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
@ -51,7 +49,7 @@
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button id="package-update-button" class="btn dropdown-item" onclick="updatePackages()" hidden>
|
||||
<button id="package-update-button" class="btn dropdown-item" onclick="packagesUpdate()" hidden>
|
||||
<i class="bi bi-play"></i> update
|
||||
</button>
|
||||
</li>
|
||||
@ -61,23 +59,24 @@
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button id="package-remove-button" class="btn dropdown-item" onclick="removePackages()" disabled hidden>
|
||||
<button id="package-remove-button" class="btn dropdown-item" onclick="packagesRemove()" disabled hidden>
|
||||
<i class="bi bi-trash"></i> remove
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button id="key-import-button" type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#key-import-modal" hidden>
|
||||
<i class="bi bi-key"></i> import key
|
||||
<i class="bi bi-key"></i><span class="d-none d-sm-inline"> import key</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button type="button" class="btn btn-secondary" onclick="reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> reload
|
||||
<i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table id="packages" class="table table-striped table-hover"
|
||||
<table id="packages"
|
||||
data-classes="table table-hover"
|
||||
data-export-options='{"fileName": "packages"}'
|
||||
data-filter-control="true"
|
||||
data-filter-control-visible="false"
|
||||
@ -102,13 +101,13 @@
|
||||
<tr>
|
||||
<th data-checkbox="true"></th>
|
||||
<th data-sortable="true" data-switchable="false" data-field="base" data-filter-control="input" data-filter-control-placeholder="(any base)">package base</th>
|
||||
<th data-sortable="true" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th>
|
||||
<th data-sortable="true" data-align="right" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th>
|
||||
<th data-sortable="true" data-field="packages" data-filter-control="input" data-filter-control-placeholder="(any package)">packages</th>
|
||||
<th data-sortable="true" data-visible="false" data-field="groups" data-filter-control="select" data-filter-data="func:filterListGroups" data-filter-custom-search="filterList" data-filter-control-placeholder="(any group)">groups</th>
|
||||
<th data-sortable="true" data-visible="false" data-field="licenses" data-filter-control="select" data-filter-data="func:filterListLicenses" data-filter-custom-search="filterList" data-filter-control-placeholder="(any license)">licenses</th>
|
||||
<th data-sortable="true" data-visible="false" data-field="packager" data-filter-control="select" data-filter-custom-search="filterContains" data-filter-control-placeholder="(any packager)">packager</th>
|
||||
<th data-sortable="true" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">last update</th>
|
||||
<th data-sortable="true" data-cell-style="statusFormat" data-field="status" data-filter-control="select" data-filter-control-placeholder="(any status)">status</th>
|
||||
<th data-sortable="true" data-align="right" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">last update</th>
|
||||
<th data-sortable="true" data-align="center" data-cell-style="statusFormat" data-field="status" data-filter-control="select" data-filter-control-placeholder="(any status)">status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
|
@ -8,20 +8,20 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label for="key-import-fingerprint-input" class="col-sm-2 col-form-label">fingerprint</label>
|
||||
<div class="col-sm-10">
|
||||
<label for="key-import-fingerprint-input" class="col-2 col-form-label">fingerprint</label>
|
||||
<div class="col-10">
|
||||
<input id="key-import-fingerprint-input" type="text" class="form-control" placeholder="PGP key fingerprint" name="key" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="key-import-server-input" class="col-sm-2 col-form-label">key server</label>
|
||||
<div class="col-sm-10">
|
||||
<label for="key-import-server-input" class="col-2 col-form-label">key server</label>
|
||||
<div class="col-10">
|
||||
<input id="key-import-server-input" type="text" class="form-control" placeholder="PGP key server" name="server" value="keyserver.ubuntu.com" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-2"></div>
|
||||
<div class="col-sm-10">
|
||||
<div class="col-2"></div>
|
||||
<div class="col-10">
|
||||
<pre class="language-less"><samp id="key-import-body-input" class="pre-scrollable language-less"></samp><button id="key-import-copy-button" type="button" class="btn language-less" onclick="copyPgpKey()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,14 +8,14 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label for="login-username" class="col-sm-4 col-form-label">username</label>
|
||||
<div class="col-sm-8">
|
||||
<label for="login-username" class="col-4 col-form-label">username</label>
|
||||
<div class="col-8">
|
||||
<input id="login-username" type="text" class="form-control" placeholder="enter username" name="username" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="login-password" class="col-sm-4 col-form-label">password</label>
|
||||
<div class="col-sm-8">
|
||||
<label for="login-password" class="col-4 col-form-label">password</label>
|
||||
<div class="col-8">
|
||||
<div class="input-group">
|
||||
<input id="login-password" type="password" class="form-control" placeholder="enter password" name="password" required>
|
||||
<div class="input-group-append">
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div id="package-add-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<form id="package-add-form" onsubmit="return false">
|
||||
<div class="modal-header">
|
||||
@ -8,9 +8,9 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label for="package-add-repository-input" class="col-sm-4 col-form-label">repository</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="package-add-repository-input" class="form-control" name="repository" required>
|
||||
<label for="package-add-repository-input" class="col-3 col-form-label">repository</label>
|
||||
<div class="col-9">
|
||||
<select id="package-add-repository-input" class="form-control" required>
|
||||
{% for repository in repositories %}
|
||||
<option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option>
|
||||
{% endfor %}
|
||||
@ -18,12 +18,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="package-add-input" class="col-sm-4 col-form-label">package</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="package-add-input" type="text" list="known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" name="package" required>
|
||||
<label for="package-add-input" class="col-3 col-form-label">package</label>
|
||||
<div class="col-9">
|
||||
<input id="package-add-input" type="text" list="package-add-known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" required>
|
||||
<datalist id="package-add-known-packages-dlist"></datalist>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-12">
|
||||
<button id="package-add-variable-button" type="button" class="form-control btn btn-light rounded" onclick="packageAddVariableInputCreate()"><i class="bi bi-plus"></i> add environment variable </button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="package-add-variables-div" class="form-group row"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary" onclick="packagesAdd()"><i class="bi bi-play"></i> add</button>
|
||||
@ -39,9 +45,11 @@
|
||||
const packageAddForm = $("#package-add-form");
|
||||
packageAddModal.on("shown.bs.modal", () => {
|
||||
$(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
|
||||
|
||||
});
|
||||
packageAddModal.on("hidden.bs.modal", () => { packageAddForm.trigger("reset"); });
|
||||
packageAddModal.on("hidden.bs.modal", () => {
|
||||
packageAddVariablesDiv.empty();
|
||||
packageAddForm.trigger("reset");
|
||||
});
|
||||
|
||||
const packageAddInput = $("#package-add-input");
|
||||
const packageAddRepositoryInput = $("#package-add-repository-input");
|
||||
@ -71,25 +79,81 @@
|
||||
}, this), 500));
|
||||
});
|
||||
|
||||
function packagesAdd() {
|
||||
const packages = packageAddInput.val();
|
||||
const repository = getRepositorySelector(packageAddRepositoryInput);
|
||||
const packageAddVariablesDiv = $("#package-add-variables-div");
|
||||
|
||||
function packageAddVariableInputCreate() {
|
||||
const variableInput = document.createElement("div");
|
||||
variableInput.classList.add("input-group");
|
||||
variableInput.classList.add("package-add-variable");
|
||||
|
||||
const variableNameInput = document.createElement("input");
|
||||
variableNameInput.type = "text";
|
||||
variableNameInput.classList.add("form-control");
|
||||
variableNameInput.classList.add("package-add-variable-name");
|
||||
variableNameInput.placeholder = "name";
|
||||
variableNameInput.ariaLabel = "variable name";
|
||||
|
||||
const variableSeparator = document.createElement("span");
|
||||
variableSeparator.classList.add("input-group-text")
|
||||
variableSeparator.textContent = "=";
|
||||
|
||||
const variableValueInput = document.createElement("input");
|
||||
variableValueInput.type = "text";
|
||||
variableValueInput.classList.add("form-control");
|
||||
variableValueInput.classList.add("package-add-variable-value");
|
||||
variableValueInput.placeholder = "value";
|
||||
variableValueInput.ariaLabel = "variable value";
|
||||
|
||||
const variableButtonRemove = document.createElement("button");
|
||||
variableButtonRemove.type = "button";
|
||||
variableButtonRemove.classList.add("btn");
|
||||
variableButtonRemove.classList.add("btn-outline-danger");
|
||||
variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>";
|
||||
variableButtonRemove.onclick = _ => { return variableInput.remove(); };
|
||||
|
||||
// bring them together
|
||||
variableInput.appendChild(variableNameInput);
|
||||
variableInput.appendChild(variableSeparator);
|
||||
variableInput.appendChild(variableValueInput);
|
||||
variableInput.appendChild(variableButtonRemove);
|
||||
|
||||
packageAddVariablesDiv.append(variableInput);
|
||||
}
|
||||
|
||||
function patchesParse() {
|
||||
const patches = packageAddVariablesDiv.find(".package-add-variable").map((_, element) => {
|
||||
const richElement = $(element);
|
||||
return {
|
||||
key: richElement.find(".package-add-variable-name").val(),
|
||||
value: richElement.find(".package-add-variable-value").val(),
|
||||
};
|
||||
}).filter((_, patch) => patch.key).get();
|
||||
return {patches: patches};
|
||||
}
|
||||
|
||||
function packagesAdd(packages, patches, repository) {
|
||||
packages = packages ?? packageAddInput.val();
|
||||
patches = patches ?? patchesParse();
|
||||
repository = repository ?? getRepositorySelector(packageAddRepositoryInput);
|
||||
|
||||
if (packages) {
|
||||
packageAddModal.modal("hide");
|
||||
const onSuccess = update => `Packages ${update} have been added`;
|
||||
const onFailure = error => `Package addition failed: ${error}`;
|
||||
doPackageAction("/api/v1/service/add", [packages], repository, onSuccess, onFailure);
|
||||
doPackageAction("/api/v1/service/add", [packages], repository, onSuccess, onFailure, patches);
|
||||
}
|
||||
}
|
||||
|
||||
function packagesRequest() {
|
||||
const packages = packageAddInput.val();
|
||||
function packagesRequest(packages, patches) {
|
||||
packages = packages ?? packageAddInput.val();
|
||||
patches = patches ?? patchesParse();
|
||||
const repository = getRepositorySelector(packageAddRepositoryInput);
|
||||
|
||||
if (packages) {
|
||||
packageAddModal.modal("hide");
|
||||
const onSuccess = update => `Packages ${update} have been requested`;
|
||||
const onFailure = error => `Package request failed: ${error}`;
|
||||
doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure);
|
||||
doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure, patches);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -6,11 +6,49 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group row mt-2">
|
||||
<div class="col-4 col-lg-1" style="text-align: right">version</div>
|
||||
<div id="package-info-version" class="col-8 col-lg-5"></div>
|
||||
<div class="col-4 col-lg-1" style="text-align: right">packager</div>
|
||||
<div id="package-info-packager" class="col-8 col-lg-5"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-2">
|
||||
<div class="col-4 col-lg-1" style="text-align: right">groups</div>
|
||||
<div id="package-info-groups" class="col-8 col-lg-5"></div>
|
||||
<div class="col-4 col-lg-1" style="text-align: right">licenses</div>
|
||||
<div id="package-info-licenses" class="col-8 col-lg-5"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-2">
|
||||
<div class="col-4 col-lg-1" style="text-align: right">upstream</div>
|
||||
<div id="package-info-upstream-url" class="col-8 col-lg-5"></div>
|
||||
<div class="col-4 col-lg-1" style="text-align: right">AUR</div>
|
||||
<div id="package-info-aur-url" class="col-8 col-lg-5"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-2">
|
||||
<div class="col-4 col-lg-1" style="text-align: right">packages</div>
|
||||
<div id="package-info-packages" class="col-8 col-lg-5"></div>
|
||||
<div class="col-4 col-lg-1" style="text-align: right">depends</div>
|
||||
<div id="package-info-depends" class="col-8 col-lg-5"></div>
|
||||
</div>
|
||||
|
||||
<hr class="col-12">
|
||||
|
||||
<h3>Environment variables</h3>
|
||||
<div id="package-info-variables-div" class="form-group row"></div>
|
||||
|
||||
<hr class="col-12">
|
||||
|
||||
<h3>Build logs</h3>
|
||||
<pre class="language-logs"><samp id="package-info-logs-input" class="pre-scrollable language-logs"></samp><button id="package-info-logs-copy-button" type="button" class="btn language-logs" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="showLogs()"><i class="bi bi-arrow-clockwise"></i> reload</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i> close</button>
|
||||
<button id="package-info-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal" hidden><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button>
|
||||
<button id="package-info-remove-button" type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal" hidden><i class="bi bi-trash"></i><span class="d-none d-sm-inline"> remove</span></button>
|
||||
<button type="button" class="btn btn-secondary" onclick="showPackageInfo()"><i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span></button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i><span class="d-none d-sm-inline"> close</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -20,30 +58,91 @@
|
||||
const packageInfoModal = $("#package-info-modal");
|
||||
const packageInfoModalHeader = $("#package-info-modal-header");
|
||||
const packageInfo = $("#package-info");
|
||||
packageInfoModal.on("hidden.bs.modal", () => {
|
||||
packageInfoAurUrl.empty();
|
||||
packageInfoDepends.empty();
|
||||
packageInfoGroups.empty();
|
||||
packageInfoLicenses.empty();
|
||||
packageInfoPackager.empty();
|
||||
packageInfoPackages.empty();
|
||||
packageInfoUpstreamUrl.empty();
|
||||
packageInfoVersion.empty();
|
||||
|
||||
packageInfoVariablesDiv.empty();
|
||||
|
||||
packageInfoLogsInput.empty();
|
||||
|
||||
packageInfoModal.trigger("reset");
|
||||
|
||||
hideInfoControls(true);
|
||||
});
|
||||
|
||||
const packageInfoLogsInput = $("#package-info-logs-input");
|
||||
const packageInfoLogsCopyButton = $("#package-info-logs-copy-button");
|
||||
|
||||
const packageInfoAurUrl = $("#package-info-aur-url");
|
||||
const packageInfoDepends = $("#package-info-depends");
|
||||
const packageInfoGroups = $("#package-info-groups");
|
||||
const packageInfoLicenses = $("#package-info-licenses");
|
||||
const packageInfoPackager = $("#package-info-packager");
|
||||
const packageInfoPackages = $("#package-info-packages");
|
||||
const packageInfoUpstreamUrl = $("#package-info-upstream-url");
|
||||
const packageInfoVersion = $("#package-info-version");
|
||||
|
||||
const packageInfoVariablesDiv = $("#package-info-variables-div");
|
||||
|
||||
async function copyLogs() {
|
||||
const logs = packageInfoLogsInput.text();
|
||||
await copyToClipboard(logs, packageInfoLogsCopyButton);
|
||||
}
|
||||
|
||||
function showLogs(packageBase) {
|
||||
const isPackageBaseSet = packageBase !== undefined;
|
||||
if (isPackageBaseSet)
|
||||
packageInfoModal.data("package", packageBase); // set package base as currently used
|
||||
else
|
||||
packageBase = packageInfoModal.data("package"); // read package base from the current window attribute
|
||||
function hideInfoControls(hidden) {
|
||||
packageInfoRemoveButton.attr("hidden", hidden);
|
||||
packageInfoUpdateButton.attr("hidden", hidden);
|
||||
}
|
||||
|
||||
const headerClass = status => {
|
||||
if (status === "pending") return ["bg-warning"];
|
||||
if (status === "building") return ["bg-warning"];
|
||||
if (status === "failed") return ["bg-danger", "text-white"];
|
||||
if (status === "success") return ["bg-success", "text-white"];
|
||||
return ["bg-secondary", "text-white"];
|
||||
function insertVariable(packageBase, variable) {
|
||||
const variableInput = document.createElement("div");
|
||||
variableInput.classList.add("input-group");
|
||||
|
||||
const variableNameInput = document.createElement("input");
|
||||
variableNameInput.classList.add("form-control");
|
||||
variableNameInput.readOnly = true;
|
||||
variableNameInput.value = variable.key;
|
||||
|
||||
const variableSeparator = document.createElement("span");
|
||||
variableSeparator.classList.add("input-group-text")
|
||||
variableSeparator.textContent = "=";
|
||||
|
||||
const variableValueInput = document.createElement("input");
|
||||
variableValueInput.classList.add("form-control");
|
||||
variableValueInput.readOnly = true;
|
||||
variableValueInput.value = variable.value;
|
||||
|
||||
const variableButtonRemove = document.createElement("button");
|
||||
variableButtonRemove.type = "button";
|
||||
variableButtonRemove.classList.add("btn");
|
||||
variableButtonRemove.classList.add("btn-outline-danger");
|
||||
variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>";
|
||||
variableButtonRemove.onclick = _ => {
|
||||
$.ajax({
|
||||
url: `/api/v1/packages/${packageBase}/patches/${variable.key}`,
|
||||
type: "DELETE",
|
||||
dataType: "json",
|
||||
success: _ => variableInput.remove(),
|
||||
});
|
||||
};
|
||||
|
||||
// bring them together
|
||||
variableInput.appendChild(variableNameInput);
|
||||
variableInput.appendChild(variableSeparator);
|
||||
variableInput.appendChild(variableValueInput);
|
||||
variableInput.appendChild(variableButtonRemove);
|
||||
|
||||
packageInfoVariablesDiv.append(variableInput);
|
||||
}
|
||||
|
||||
function loadLogs(packageBase, onFailure) {
|
||||
$.ajax({
|
||||
url: `/api/v2/packages/${packageBase}/logs`,
|
||||
data: {
|
||||
@ -53,26 +152,115 @@
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: response => {
|
||||
packageInfo.text(`${response.package_base} ${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`);
|
||||
const logs = response.logs.map(log_record => {
|
||||
const [timestamp, record] = log_record;
|
||||
return `[${new Date(1000 * timestamp).toISOString()}] ${record}`;
|
||||
const logs = response.map(log_record => {
|
||||
return `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`;
|
||||
});
|
||||
packageInfoLogsInput.text(logs.join("\n"));
|
||||
},
|
||||
error: onFailure,
|
||||
});
|
||||
}
|
||||
|
||||
function loadPackage(packageBase, onFailure) {
|
||||
const headerClass = status => {
|
||||
if (status === "pending") return ["bg-warning"];
|
||||
if (status === "building") return ["bg-warning"];
|
||||
if (status === "failed") return ["bg-danger", "text-white"];
|
||||
if (status === "success") return ["bg-success", "text-white"];
|
||||
return ["bg-secondary", "text-white"];
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: `/api/v1/packages/${packageBase}`,
|
||||
data: {
|
||||
architecture: repository.architecture,
|
||||
repository: repository.repository,
|
||||
},
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: response => {
|
||||
const description = response.find(Boolean);
|
||||
const packages = Object.keys(description.package.packages);
|
||||
const aurUrl = description.package.remote.web_url;
|
||||
const upstreamUrls = Array.from(
|
||||
new Set(
|
||||
Object.values(description.package.packages)
|
||||
.map(single => single.url)
|
||||
)
|
||||
).sort();
|
||||
|
||||
packageInfo.text(`${description.package.base} ${description.status.status} at ${new Date(1000 * description.status.timestamp).toISOStringShort()}`);
|
||||
|
||||
packageInfoModalHeader.removeClass();
|
||||
packageInfoModalHeader.addClass("modal-header");
|
||||
headerClass(response.status.status).forEach((clz) => packageInfoModalHeader.addClass(clz));
|
||||
headerClass(description.status.status).forEach(clz => packageInfoModalHeader.addClass(clz));
|
||||
|
||||
if (isPackageBaseSet) packageInfoModal.modal("show"); // we don't need to show window again
|
||||
packageInfoAurUrl.html(aurUrl ? safeLink(aurUrl, aurUrl, "AUR link").outerHTML : "");
|
||||
packageInfoDepends.html(listToTable(
|
||||
Object.values(description.package.packages)
|
||||
.reduce((accumulator, currentValue) => {
|
||||
return accumulator.concat(currentValue.depends.filter(v => packages.indexOf(v) === -1))
|
||||
.concat(currentValue.make_depends.filter(v => packages.indexOf(v) === -1).map(v => `${v} (make)`))
|
||||
.concat(currentValue.opt_depends.filter(v => packages.indexOf(v) === -1).map(v => `${v} (optional)`));
|
||||
}, [])
|
||||
));
|
||||
packageInfoGroups.html(listToTable(extractListProperties(description.package, "groups")));
|
||||
packageInfoLicenses.html(listToTable(extractListProperties(description.package, "licenses")));
|
||||
packageInfoPackager.text(description.package.packager);
|
||||
packageInfoPackages.html(listToTable(packages));
|
||||
packageInfoUpstreamUrl.html(upstreamUrls.map(url => safeLink(url, url, "upstream link").outerHTML).join("<br>"));
|
||||
packageInfoVersion.text(description.package.version);
|
||||
|
||||
hideInfoControls(false);
|
||||
},
|
||||
error: (jqXHR, _, errorThrown) => {
|
||||
// show failed modal in case if first time loading
|
||||
if (isPackageBaseSet) {
|
||||
const message = error => `Could not load package ${packageBase} logs: ${error}`;
|
||||
showFailure("Load failure", message, jqXHR, errorThrown);
|
||||
}
|
||||
hideInfoControls(true);
|
||||
onFailure(jqXHR, null, errorThrown);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function loadPatches(packageBase, onFailure) {
|
||||
$.ajax({
|
||||
url: `/api/v1/packages/${packageBase}/patches`,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: response => {
|
||||
packageInfoVariablesDiv.empty();
|
||||
response.map(patch => insertVariable(packageBase, patch));
|
||||
},
|
||||
error: onFailure,
|
||||
});
|
||||
}
|
||||
|
||||
function packageInfoRemove() {
|
||||
const packageBase = packageInfoModal.data("package");
|
||||
if (packageBase) return packagesRemove([packageBase]);
|
||||
}
|
||||
|
||||
function packageInfoUpdate() {
|
||||
const packageBase = packageInfoModal.data("package");
|
||||
if (packageBase) return packagesAdd(packageBase, [], repository);
|
||||
}
|
||||
|
||||
function showPackageInfo(packageBase) {
|
||||
const isPackageBaseSet = packageBase !== undefined;
|
||||
if (isPackageBaseSet)
|
||||
packageInfoModal.data("package", packageBase); // set package base as currently used
|
||||
else
|
||||
packageBase = packageInfoModal.data("package"); // read package base from the current window attribute
|
||||
|
||||
const onFailure = (jqXHR, _, errorThrown) => {
|
||||
if (isPackageBaseSet) {
|
||||
const message = error => `Could not load package ${packageBase} info: ${error}`;
|
||||
showFailure("Load failure", message, jqXHR, errorThrown);
|
||||
}
|
||||
};
|
||||
|
||||
loadPackage(packageBase, onFailure);
|
||||
loadPatches(packageBase, onFailure);
|
||||
loadLogs(packageBase, onFailure);
|
||||
|
||||
if (isPackageBaseSet) packageInfoModal.modal("show");
|
||||
}
|
||||
</script>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div id="package-rebuild-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<form id="package-rebuild-form" onsubmit="return false">
|
||||
<div class="modal-header">
|
||||
@ -8,8 +8,8 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label for="package-rebuild-repository-input" class="col-sm-4 col-form-label">repository</label>
|
||||
<div class="col-sm-8">
|
||||
<label for="package-rebuild-repository-input" class="col-3 col-form-label">repository</label>
|
||||
<div class="col-9">
|
||||
<select id="package-rebuild-repository-input" class="form-control" name="repository" required>
|
||||
{% for repository in repositories %}
|
||||
<option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option>
|
||||
@ -18,8 +18,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="package-rebuild-dependency-input" class="col-sm-4 col-form-label">dependency</label>
|
||||
<div class="col-sm-8">
|
||||
<label for="package-rebuild-dependency-input" class="col-3 col-form-label">dependency</label>
|
||||
<div class="col-9">
|
||||
<input id="package-rebuild-dependency-input" type="text" class="form-control" placeholder="packages dependency" name="package" required>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,6 +5,9 @@
|
||||
const packageRemoveButton = $("#package-remove-button");
|
||||
const packageUpdateButton = $("#package-update-button");
|
||||
|
||||
const packageInfoRemoveButton = $("#package-info-remove-button");
|
||||
const packageInfoUpdateButton = $("#package-info-update-button");
|
||||
|
||||
let repository = null;
|
||||
$("#repositories a").on("click", (event) => {
|
||||
const element = event.target;
|
||||
@ -12,7 +15,7 @@
|
||||
architecture: element.dataset.architecture,
|
||||
repository: element.dataset.repository,
|
||||
};
|
||||
packageUpdateButton.html(`<i class="bi bi-play"></i> update ${safe(repository.repository)} (${safe(repository.architecture)})`);
|
||||
packageUpdateButton.html(`<i class="bi bi-play"></i> update<span class="d-none d-sm-inline"> ${safe(repository.repository)} (${safe(repository.architecture)})</span>`);
|
||||
$(`#${element.id}`).tab("show");
|
||||
reload();
|
||||
});
|
||||
@ -25,7 +28,7 @@
|
||||
if (0 === cell || "base" === cell) {
|
||||
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
|
||||
table.bootstrapTable(method, {field: "id", values: [data.id]});
|
||||
} else showLogs(data.id);
|
||||
} else showPackageInfo(data.id);
|
||||
});
|
||||
table.on("created-controls.bs.table", () => {
|
||||
const pickerInput = $(".bootstrap-table-filter-control-timestamp");
|
||||
@ -50,7 +53,7 @@
|
||||
const statusBadge = $("#badge-status");
|
||||
const versionBadge = $("#badge-version");
|
||||
|
||||
function doPackageAction(uri, packages, repository, successText, failureText) {
|
||||
function doPackageAction(uri, packages, repository, successText, failureText, data) {
|
||||
const queryParams = $.param({
|
||||
architecture: repository.architecture,
|
||||
repository: repository.repository,
|
||||
@ -58,7 +61,7 @@
|
||||
|
||||
$.ajax({
|
||||
url: `${uri}?${queryParams}`,
|
||||
data: JSON.stringify({packages: packages}),
|
||||
data: JSON.stringify(Object.assign({}, {packages: packages}, data || {})),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: _ => {
|
||||
@ -71,31 +74,30 @@
|
||||
});
|
||||
}
|
||||
|
||||
function filterListGroups() {
|
||||
return extractDataList(table.bootstrapTable("getData"), "groups");
|
||||
}
|
||||
|
||||
function filterListLicenses() {
|
||||
return extractDataList(table.bootstrapTable("getData"), "licenses");
|
||||
}
|
||||
|
||||
function filterListPackagers() {
|
||||
return extractDataList(table.bootstrapTable("getData"), "packager");
|
||||
}
|
||||
|
||||
function getRepositorySelector(selector) {
|
||||
const selected = selector.find(":selected");
|
||||
return {
|
||||
architecture: selected.data("architecture"),
|
||||
repository: selected.data("repository"),
|
||||
};
|
||||
}
|
||||
|
||||
function getSelection() {
|
||||
return table.bootstrapTable("getSelections").map(row => row.id);
|
||||
}
|
||||
|
||||
function removePackages() {
|
||||
const onSuccess = update => `Packages ${update} have been removed`;
|
||||
const onFailure = error => `Could not remove packages: ${error}`;
|
||||
doPackageAction("/api/v1/service/remove", getSelection(), repository, onSuccess, onFailure);
|
||||
}
|
||||
|
||||
function selectRepository() {
|
||||
const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}";
|
||||
const element = $(`#${fragment}-lnk`);
|
||||
element.click();
|
||||
}
|
||||
|
||||
function updatePackages() {
|
||||
const currentSelection = getSelection();
|
||||
const [url, onSuccess] = currentSelection.length === 0
|
||||
? ["/api/v1/service/update", _ => `Repository update has been run`]
|
||||
: ["/api/v1/service/add", update => `Run update for packages ${update}`];
|
||||
const onFailure = error => `Packages update failed: ${error}`;
|
||||
doPackageAction(url, currentSelection, repository, onSuccess, onFailure);
|
||||
}
|
||||
|
||||
function hideControls(hidden) {
|
||||
keyImportButton.attr("hidden", hidden);
|
||||
packageAddButton.attr("hidden", hidden);
|
||||
@ -104,6 +106,24 @@
|
||||
packageUpdateButton.attr("hidden", hidden);
|
||||
}
|
||||
|
||||
function packagesRemove(packages) {
|
||||
packages = packages ?? getSelection();
|
||||
const onSuccess = update => `Packages ${update} have been removed`;
|
||||
const onFailure = error => `Could not remove packages: ${error}`;
|
||||
|
||||
doPackageAction("/api/v1/service/remove", packages, repository, onSuccess, onFailure);
|
||||
}
|
||||
|
||||
function packagesUpdate() {
|
||||
const currentSelection = getSelection();
|
||||
const [url, onSuccess] = currentSelection.length === 0
|
||||
? ["/api/v1/service/update", _ => `Repository update has been run`]
|
||||
: ["/api/v1/service/add", update => `Run update for packages ${update}`];
|
||||
const onFailure = error => `Packages update failed: ${error}`;
|
||||
|
||||
doPackageAction(url, currentSelection, repository, onSuccess, onFailure);
|
||||
}
|
||||
|
||||
function reload() {
|
||||
table.bootstrapTable("showLoading");
|
||||
|
||||
@ -124,30 +144,18 @@
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: response => {
|
||||
const extractListProperties = (description, property) => {
|
||||
return Object.values(description.packages)
|
||||
.map(pkg => pkg[property])
|
||||
.reduce((left, right) => left.concat(right), []);
|
||||
};
|
||||
const listToTable = data => {
|
||||
return Array.from(new Set(data))
|
||||
.sort()
|
||||
.map(entry => safe(entry))
|
||||
.join("<br>");
|
||||
};
|
||||
|
||||
const payload = response.map(description => {
|
||||
const package_base = description.package.base;
|
||||
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),
|
||||
base: web_url ? safeLink(web_url, package_base, package_base).outerHTML : safe(package_base),
|
||||
version: safe(description.package.version),
|
||||
packager: description.package.packager ? safe(description.package.packager) : "",
|
||||
packages: listToTable(Object.keys(description.package.packages)),
|
||||
groups: listToTable(extractListProperties(description.package, "groups")),
|
||||
licenses: listToTable(extractListProperties(description.package, "licenses")),
|
||||
timestamp: new Date(1000 * description.status.timestamp).toISOString(),
|
||||
timestamp: new Date(1000 * description.status.timestamp).toISOStringShort(),
|
||||
status: description.status.status,
|
||||
};
|
||||
});
|
||||
@ -186,7 +194,7 @@
|
||||
|
||||
statusBadge
|
||||
.popover("dispose")
|
||||
.attr("data-bs-content", `${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`)
|
||||
.attr("data-bs-content", `${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOStringShort()}`)
|
||||
.popover();
|
||||
statusBadge.removeClass();
|
||||
statusBadge.addClass("btn");
|
||||
@ -195,6 +203,12 @@
|
||||
});
|
||||
}
|
||||
|
||||
function selectRepository() {
|
||||
const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}";
|
||||
const element = $(`#${fragment}-link`);
|
||||
element.click();
|
||||
}
|
||||
|
||||
function statusFormat(value) {
|
||||
const cellClass = status => {
|
||||
if (status === "pending") return "table-warning";
|
||||
@ -206,26 +220,6 @@
|
||||
return {classes: cellClass(value)};
|
||||
}
|
||||
|
||||
function filterListGroups() {
|
||||
return extractDataList(table.bootstrapTable("getData"), "groups");
|
||||
}
|
||||
|
||||
function filterListLicenses() {
|
||||
return extractDataList(table.bootstrapTable("getData"), "licenses");
|
||||
}
|
||||
|
||||
function filterListPackagers() {
|
||||
return extractDataList(table.bootstrapTable("getData"), "packager");
|
||||
}
|
||||
|
||||
function getRepositorySelector(selector) {
|
||||
const selected = selector.find(":selected");
|
||||
return {
|
||||
architecture: selected.data("architecture"),
|
||||
repository: selected.data("repository"),
|
||||
};
|
||||
}
|
||||
|
||||
$(() => {
|
||||
table.bootstrapTable({});
|
||||
statusBadge.popover();
|
||||
|
@ -5,8 +5,6 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
|
||||
{% include "utils/style.jinja2" %}
|
||||
{% include "user-style.jinja2" ignore missing %}
|
||||
</head>
|
||||
|
@ -30,7 +30,8 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<table id="packages" class="table table-striped table-hover"
|
||||
<table id="packages"
|
||||
data-classes="table table-hover"
|
||||
data-export-options='{"fileName": "packages"}'
|
||||
data-filter-control="true"
|
||||
data-filter-control-visible="false"
|
||||
@ -53,16 +54,16 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th data-sortable="true" data-switchable="false" data-field="name" data-filter-control="input" data-filter-control-placeholder="(any package)">package</th>
|
||||
<th data-sortable="true" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th>
|
||||
<th data-sortable="true" data-align="right" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th>
|
||||
<th data-sortable="true" data-visible="false" data-field="architecture" data-filter-control="select" data-filter-control-placeholder="(any arch)">architecture</th>
|
||||
<th data-sortable="true" data-visible="false" data-field="description" data-filter-control="input" data-filter-control-placeholder="(any description)">description</th>
|
||||
<th data-sortable="true" data-visible="false" data-field="url">upstream url</th>
|
||||
<th data-sortable="true" data-visible="false" data-field="licenses" data-filter-control="select" data-filter-data="func:filterListLicenses" data-filter-custom-search="filterList" data-filter-control-placeholder="(any license)">licenses</th>
|
||||
<th data-sortable="true" data-visible="false" data-field="groups" data-filter-control="select" data-filter-data="func:filterListGroups" data-filter-custom-search="filterList" data-filter-control-placeholder="(any group)">groups</th>
|
||||
<th data-sortable="true" data-visible="false" data-field="depends" data-filter-control="select" data-filter-data="func:filterListDepends" data-filter-custom-search="filterList" data-filter-control-placeholder="(any depends)">depends</th>
|
||||
<th data-sortable="true" data-field="archive_size">archive size</th>
|
||||
<th data-sortable="true" data-field="installed_size">installed size</th>
|
||||
<th data-sortable="true" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">build date</th>
|
||||
<th data-sortable="true" data-align="right" data-field="archive_size">archive size</th>
|
||||
<th data-sortable="true" data-align="right" data-field="installed_size">installed size</th>
|
||||
<th data-sortable="true" data-align="right" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">build date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
{#simplified version of full report#}
|
||||
<b>{{ repository }} update</b>
|
||||
{% for package in packages %}
|
||||
<a href="{{ link_path }}/{{ package.filename }}">{{ package.name }}</a> {{ package.version }}{% endfor %}
|
||||
<a href="{{ link_path }}/{{ package.filename }}">{{ package.name }}</a> {{ package.version }}
|
||||
{% endfor %}
|
@ -1,4 +1,4 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.0/dist/jquery.min.js" integrity="sha384-NXgwF8Kv9SSAr+jemKKcbvQsz+teULH/a5UNJvZc6kP47hZgl62M1vGnw6gHQhb1" crossorigin="anonymous" type="application/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" integrity="sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs" crossorigin="anonymous" type="application/javascript"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js" integrity="sha384-8hHkOkbWN1TLWwet/jpbJ0zbx3FJDeYJgQ8dX1mRrv/vfCfHCqFSFZYCgaMML3z9" crossorigin="anonymous" type="application/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.min.js" integrity="sha384-u4eJN1VWrTf/FnYYQJo2kqJyVxEQf5UmWY4iUcNAoLenOEtEuCkfwc5bKvZOWBi5" crossorigin="anonymous" type="application/javascript"></script>
|
||||
@ -8,14 +8,12 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" integrity="sha384-IazMVNyYoUNx6357fWJoqtHYUWWCNHIXxFVtbpVgvImQNWuRP2WbHPaIb3QF8j97" crossorigin="anonymous" type="application/javascript"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous" type="application/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js" integrity="sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V" crossorigin="anonymous" type="application/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js" integrity="sha384-BBtl+eGJRgqQAUMxJ7pMwbEyER4l1g+O15P+16Ep7Q9Q+zqX6gSbd85u4mG4QzX+" crossorigin="anonymous" type="application/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/bootstrap-table.min.js" integrity="sha384-GVLHfbEvuGA/RFiQ3MK2ClEJkWYJXABg55t9LpoDPZFGIsSq8xhFlQydm5poV2jW" crossorigin="anonymous" type="application/javascript"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.21.4/dist/extensions/export/bootstrap-table-export.min.js" integrity="sha384-jeldDadm+qM2RwGER3qVqxFgWVpAEJ7Jie+0rlYj8ni3KkQA654T8TSXDtol022X" crossorigin="anonymous" type="application/javascript"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.21.4/dist/extensions/resizable/bootstrap-table-resizable.js" integrity="sha384-wd8Vc6Febikdnsnk9vthRWRvMwffw246vhqiqNO3aSNe1maTEA07Vh3zAQiSyDji" crossorigin="anonymous" type="application/javascript"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.21.4/dist/extensions/filter-control/bootstrap-table-filter-control.js" integrity="sha384-B6xNXlSOaOFxjlKo9OW3htbox+9/DcaEcjPPEi1+pTMwH5Tzc/s2wNTYriHz7Tb8" crossorigin="anonymous" type="application/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/export/bootstrap-table-export.min.js" integrity="sha384-g9OAB1Moamcy8+l1Q/tajHlMf6NTkS79ehKLTYbA80aQRbRhFCjrSuezv+FE2Kwe" crossorigin="anonymous" type="application/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/resizable/bootstrap-table-resizable.js" integrity="sha384-wd8Vc6Febikdnsnk9vthRWRvMwffw246vhqiqNO3aSNe1maTEA07Vh3zAQiSyDji" crossorigin="anonymous" type="application/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/filter-control/bootstrap-table-filter-control.js" integrity="sha384-NIqcjpr/3eZI1iNzz7hgT5rgp70qFUzkZffeCgVva9gi80B5vqcm7gn+8QvlWxko" crossorigin="anonymous" type="application/javascript"></script>
|
||||
|
||||
<script>
|
||||
async function copyToClipboard(text, button) {
|
||||
@ -36,19 +34,17 @@
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function safe(string) {
|
||||
return String(string)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function extractDataList(data, column) {
|
||||
const elements = data.flatMap(row => row[column].split("<br>")).filter(v => v); // remove empty elements from array
|
||||
return Array.from(new Set(elements)).sort();
|
||||
}
|
||||
|
||||
function extractListProperties(description, property) {
|
||||
return Object.values(description.packages)
|
||||
.map(pkg => pkg[property])
|
||||
.reduce((left, right) => left.concat(right), []);
|
||||
}
|
||||
|
||||
function filterContains(text, value) {
|
||||
return value.includes(text.toLowerCase().trim());
|
||||
}
|
||||
@ -67,4 +63,33 @@
|
||||
// the library removes all symbols from string, so it is just string
|
||||
return value.includes(dataList[index].toLowerCase());
|
||||
}
|
||||
|
||||
function listToTable(data) {
|
||||
return Array.from(new Set(data))
|
||||
.sort()
|
||||
.map(entry => safe(entry))
|
||||
.join("<br>");
|
||||
}
|
||||
|
||||
function safe(string) {
|
||||
return String(string)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
// because I'm tired of safe element generation
|
||||
function safeLink(url, text, title) {
|
||||
const element = document.createElement("a");
|
||||
element.href = url;
|
||||
element.innerText = text;
|
||||
if (title) element.title = title;
|
||||
return element;
|
||||
}
|
||||
|
||||
Date.prototype.toISOStringShort = function() {
|
||||
const pad = number => String(number).padStart(2, "0");
|
||||
return `${this.getFullYear()}-${pad(this.getMonth() + 1)}-${pad(this.getDate())} ${pad(this.getHours())}:${pad(this.getMinutes())}:${pad(this.getSeconds())}`;
|
||||
}
|
||||
</script>
|
||||
|
@ -1,13 +1,13 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.4/font/bootstrap-icons.css" integrity="sha384-LrVLJJYk9OiJmjNDakUBU7kS9qCT8wk1j2OU7ncpsfB3QS37UPdkCuq3ZD1MugNY" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" integrity="sha384-4LISF5TTJX/fLmGSxO53rV4miRxdg84mZsxmO8Rx5jGtp/LbrixFETvWa5a6sESd" crossorigin="anonymous" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.21.4/dist/bootstrap-table.min.css" integrity="sha384-pTEAhytv7JmEG2D7qiW5gY0lI5EKZ9n3CNmj6Qp+U3qhnmH2qnnN9KJbVwbtMHN0" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/bootstrap-table.min.css" integrity="sha384-sN3NwxbjH33ZidqZnPmX+nQ5IF+LoiI7HvZSoZj5wGacmu0/q4RJfsN0xqN+LIa5" crossorigin="anonymous" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" integrity="sha384-1sLxvR8mXzjhvFY9f8mzXl97DNLepeZ0PnRiMMdm/rQsKjsrPZPJxYle2wwT2PMg" crossorigin="anonymous" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.21.4/dist/extensions/filter-control/bootstrap-table-filter-control.css" integrity="sha384-4Glx18jZ0Un+yDG6KUpYJ/af8hkssJ02jRASuFv23gfCl0mTXaVMPI9cB4cn3GvE" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/filter-control/bootstrap-table-filter-control.css" integrity="sha384-4Glx18jZ0Un+yDG6KUpYJ/af8hkssJ02jRASuFv23gfCl0mTXaVMPI9cB4cn3GvE" crossorigin="anonymous" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.2.3/dist/cosmo/bootstrap.min.css" integrity="sha384-P1PBFVifKf1Ww0gS5B8A0siIeDpcFd4uU7S68LA1XMdE0R+y1WN3DR4HcLc9csRC" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.2/dist/cosmo/bootstrap.min.css" integrity="sha384-RfV5VNj9uqyOdZbN0hFNmoq56291KK2Y4iKhoRAbcfBfjYlpasjxK6TefPjxiAiN" crossorigin="anonymous" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.css" integrity="sha384-zLkQsiLfAQqGeIJeKLC+rcCR1YoYaQFLCL7cLDUoKE1ajKJzySpjzWGfYS2vjSG+" crossorigin="anonymous" type="text/css">
|
||||
|
||||
|
@ -1,18 +1,18 @@
|
||||
# AUTOMATICALLY GENERATED by `shtab`
|
||||
|
||||
_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-repositories' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'service-tree-migrate' 'user-add' 'user-list' 'user-remove' 'web')
|
||||
_shtab_ahriman_subparsers=('aur-search' 'search' 'help-commands-unsafe' 'help' '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-repositories' 'service-run' 'run' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'service-tree-migrate' 'user-add' 'user-list' 'user-remove' 'web')
|
||||
|
||||
_shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--configuration' '--force' '-l' '--lock' '--log-handler' '-q' '--quiet' '--report' '--no-report' '-r' '--repository' '--unsafe' '--wait-timeout' '-V' '--version')
|
||||
_shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--configuration' '--force' '-l' '--lock' '--log-handler' '-q' '--quiet' '--report' '--no-report' '-r' '--repository' '--unsafe' '-V' '--version' '--wait-timeout')
|
||||
_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')
|
||||
_shtab_ahriman_help_commands_unsafe_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_help_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' '--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_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username' '-v' '--variable')
|
||||
_shtab_ahriman_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username' '-v' '--variable')
|
||||
_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' '-v' '--variable')
|
||||
_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')
|
||||
@ -59,6 +59,8 @@ _shtab_ahriman_repo_config_validate_option_strings=('-h' '--help' '-e' '--exit-c
|
||||
_shtab_ahriman_service_key_import_option_strings=('-h' '--help' '--key-server')
|
||||
_shtab_ahriman_key_import_option_strings=('-h' '--help' '--key-server')
|
||||
_shtab_ahriman_service_repositories_option_strings=('-h' '--help' '--id-only' '--no-id-only')
|
||||
_shtab_ahriman_service_run_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_run_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_service_setup_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
|
||||
_shtab_ahriman_init_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
|
||||
_shtab_ahriman_repo_init_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
|
||||
@ -74,7 +76,7 @@ _shtab_ahriman_web_option_strings=('-h' '--help')
|
||||
|
||||
|
||||
|
||||
_shtab_ahriman_pos_0_choices=('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-repositories' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'service-tree-migrate' 'user-add' 'user-list' 'user-remove' 'web')
|
||||
_shtab_ahriman_pos_0_choices=('aur-search' 'search' 'help-commands-unsafe' 'help' '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-repositories' 'service-run' 'run' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'service-tree-migrate' 'user-add' 'user-list' 'user-remove' 'web')
|
||||
_shtab_ahriman___log_handler_choices=('console' 'syslog' 'journald')
|
||||
_shtab_ahriman_aur_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'submitter' 'url' 'url_path' 'version')
|
||||
_shtab_ahriman_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'submitter' 'url' 'url_path' 'version')
|
||||
@ -133,11 +135,11 @@ _shtab_ahriman_search__e_nargs=0
|
||||
_shtab_ahriman_search___exit_code_nargs=0
|
||||
_shtab_ahriman_search___info_nargs=0
|
||||
_shtab_ahriman_search___no_info_nargs=0
|
||||
_shtab_ahriman_help__h_nargs=0
|
||||
_shtab_ahriman_help___help_nargs=0
|
||||
_shtab_ahriman_help_commands_unsafe_pos_0_nargs=*
|
||||
_shtab_ahriman_help_commands_unsafe__h_nargs=0
|
||||
_shtab_ahriman_help_commands_unsafe___help_nargs=0
|
||||
_shtab_ahriman_help__h_nargs=0
|
||||
_shtab_ahriman_help___help_nargs=0
|
||||
_shtab_ahriman_help_updates__h_nargs=0
|
||||
_shtab_ahriman_help_updates___help_nargs=0
|
||||
_shtab_ahriman_help_updates__e_nargs=0
|
||||
@ -437,6 +439,12 @@ _shtab_ahriman_service_repositories__h_nargs=0
|
||||
_shtab_ahriman_service_repositories___help_nargs=0
|
||||
_shtab_ahriman_service_repositories___id_only_nargs=0
|
||||
_shtab_ahriman_service_repositories___no_id_only_nargs=0
|
||||
_shtab_ahriman_service_run_pos_0_nargs=+
|
||||
_shtab_ahriman_service_run__h_nargs=0
|
||||
_shtab_ahriman_service_run___help_nargs=0
|
||||
_shtab_ahriman_run_pos_0_nargs=+
|
||||
_shtab_ahriman_run__h_nargs=0
|
||||
_shtab_ahriman_run___help_nargs=0
|
||||
_shtab_ahriman_service_setup__h_nargs=0
|
||||
_shtab_ahriman_service_setup___help_nargs=0
|
||||
_shtab_ahriman_service_setup___generate_salt_nargs=0
|
||||
|
@ -1,9 +1,9 @@
|
||||
.TH AHRIMAN "1" "2023\-10\-09" "ahriman" "Generated Python Manual"
|
||||
.TH AHRIMAN "1" "2023\-11\-13" "ahriman" "Generated Python Manual"
|
||||
.SH NAME
|
||||
ahriman
|
||||
.SH SYNOPSIS
|
||||
.B ahriman
|
||||
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--log-handler {console,syslog,journald}] [-q] [--report | --no-report] [-r REPOSITORY] [--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-repositories,service-setup,init,repo-init,repo-setup,setup,service-shell,shell,service-tree-migrate,user-add,user-list,user-remove,web} ...
|
||||
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--log-handler {console,syslog,journald}] [-q] [--report | --no-report] [-r REPOSITORY] [--unsafe] [-V] [--wait-timeout WAIT_TIMEOUT] {aur-search,search,help-commands-unsafe,help,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-repositories,service-run,run,service-setup,init,repo-init,repo-setup,setup,service-shell,shell,service-tree-migrate,user-add,user-list,user-remove,web} ...
|
||||
.SH DESCRIPTION
|
||||
ArcH linux ReposItory MANager
|
||||
|
||||
@ -44,27 +44,27 @@ filter by target repository
|
||||
\fB\-\-unsafe\fR
|
||||
allow to run ahriman as non\-ahriman user. Some actions might be unavailable
|
||||
|
||||
.TP
|
||||
\fB\-V\fR, \fB\-\-version\fR
|
||||
show program's version number and exit
|
||||
|
||||
.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
|
||||
|
||||
.SH
|
||||
COMMAND
|
||||
.TP
|
||||
\fBahriman\fR \fI\,aur\-search\/\fR
|
||||
search for package
|
||||
.TP
|
||||
\fBahriman\fR \fI\,help\/\fR
|
||||
show help message
|
||||
.TP
|
||||
\fBahriman\fR \fI\,help\-commands\-unsafe\/\fR
|
||||
list unsafe commands
|
||||
.TP
|
||||
\fBahriman\fR \fI\,help\/\fR
|
||||
show help message
|
||||
.TP
|
||||
\fBahriman\fR \fI\,help\-updates\/\fR
|
||||
check for service updates
|
||||
.TP
|
||||
@ -158,6 +158,9 @@ import PGP key
|
||||
\fBahriman\fR \fI\,service\-repositories\/\fR
|
||||
show repositories
|
||||
.TP
|
||||
\fBahriman\fR \fI\,service\-run\/\fR
|
||||
run multiple commands
|
||||
.TP
|
||||
\fBahriman\fR \fI\,service\-setup\/\fR
|
||||
initial service configuration
|
||||
.TP
|
||||
@ -204,25 +207,25 @@ show additional package information
|
||||
sort field by this field. In case if two packages have the same value of the specified field, they will be always sorted
|
||||
by name
|
||||
|
||||
.SH COMMAND \fI\,'ahriman help'\/\fR
|
||||
usage: ahriman help [\-h] [command]
|
||||
|
||||
show help message for application or command and exit
|
||||
|
||||
.TP
|
||||
\fBcommand\fR
|
||||
show help message for specific command
|
||||
|
||||
.SH COMMAND \fI\,'ahriman help\-commands\-unsafe'\/\fR
|
||||
usage: ahriman help\-commands\-unsafe [\-h] [command ...]
|
||||
usage: ahriman help\-commands\-unsafe [\-h] [subcommand ...]
|
||||
|
||||
list unsafe commands as defined in default args
|
||||
|
||||
.TP
|
||||
\fBcommand\fR
|
||||
\fBsubcommand\fR
|
||||
instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1
|
||||
otherwise
|
||||
|
||||
.SH COMMAND \fI\,'ahriman help'\/\fR
|
||||
usage: ahriman help [\-h] [subcommand]
|
||||
|
||||
show help message for application or command and exit
|
||||
|
||||
.TP
|
||||
\fBsubcommand\fR
|
||||
show help message for specific command
|
||||
|
||||
.SH COMMAND \fI\,'ahriman help\-updates'\/\fR
|
||||
usage: ahriman help\-updates [\-h] [\-e]
|
||||
|
||||
@ -240,7 +243,7 @@ print application and its dependencies versions
|
||||
|
||||
.SH COMMAND \fI\,'ahriman package\-add'\/\fR
|
||||
usage: ahriman package\-add [\-h] [\-\-dependencies | \-\-no\-dependencies] [\-e] [\-\-increment | \-\-no\-increment] [\-n] [\-y]
|
||||
[\-s {auto,archive,aur,directory,local,remote,repository}] [\-u USERNAME]
|
||||
[\-s {auto,archive,aur,directory,local,remote,repository}] [\-u USERNAME] [\-v VARIABLE]
|
||||
package [package ...]
|
||||
|
||||
add existing or new package to the build queue
|
||||
@ -278,6 +281,10 @@ explicitly specify the package source for this command
|
||||
\fB\-u\fR \fI\,USERNAME\/\fR, \fB\-\-username\fR \fI\,USERNAME\/\fR
|
||||
build as user
|
||||
|
||||
.TP
|
||||
\fB\-v\fR \fI\,VARIABLE\/\fR, \fB\-\-variable\fR \fI\,VARIABLE\/\fR
|
||||
apply specified makepkg variables to the next build
|
||||
|
||||
.SH COMMAND \fI\,'ahriman package\-remove'\/\fR
|
||||
usage: ahriman package\-remove [\-h] package [package ...]
|
||||
|
||||
@ -722,6 +729,15 @@ list all available repositories
|
||||
\fB\-\-id\-only\fR, \fB\-\-no\-id\-only\fR
|
||||
show machine readable identifier instead
|
||||
|
||||
.SH COMMAND \fI\,'ahriman service\-run'\/\fR
|
||||
usage: ahriman service\-run [\-h] command [command ...]
|
||||
|
||||
run multiple commands on success run of the previous command
|
||||
|
||||
.TP
|
||||
\fBcommand\fR
|
||||
command to be run (quoted) without ``ahriman``
|
||||
|
||||
.SH COMMAND \fI\,'ahriman service\-setup'\/\fR
|
||||
usage: ahriman service\-setup [\-h] [\-\-build\-as\-user BUILD_AS_USER] [\-\-from\-configuration FROM_CONFIGURATION]
|
||||
[\-\-generate\-salt | \-\-no\-generate\-salt] [\-\-makeflags\-jobs | \-\-no\-makeflags\-jobs]
|
||||
|
@ -52,12 +52,14 @@ _shtab_ahriman_commands() {
|
||||
"repo-triggers:run triggers on empty build result as configured by settings"
|
||||
"repo-update:check for packages updates and run build process if requested"
|
||||
"report:generate repository report according to current settings"
|
||||
"run:run multiple commands on success run of the previous command"
|
||||
"search:search for package in AUR using API"
|
||||
"service-clean:remove local caches"
|
||||
"service-config:dump configuration for the specified architecture"
|
||||
"service-config-validate:validate configuration and print found errors"
|
||||
"service-key-import:import PGP key from public sources to the repository user"
|
||||
"service-repositories:list all available repositories"
|
||||
"service-run:run multiple commands on success run of the previous command"
|
||||
"service-setup:create initial service configuration, requires root"
|
||||
"service-shell:drop into python shell"
|
||||
"service-tree-migrate:migrate repository tree between versions"
|
||||
@ -88,8 +90,8 @@ _shtab_ahriman_options=(
|
||||
{--report,--no-report}"[force enable or disable reporting to web service (default\: True)]:report:"
|
||||
{-r,--repository}"[filter by target repository (default\: None)]:repository:"
|
||||
"--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]"
|
||||
"--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:"
|
||||
)
|
||||
|
||||
_shtab_ahriman_add_options=(
|
||||
@ -99,8 +101,9 @@ _shtab_ahriman_add_options=(
|
||||
{--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)"
|
||||
{-s,--source}"[explicitly specify the package source for this command (default\: auto)]:source:(auto archive aur directory local remote repository)"
|
||||
{-u,--username}"[build as user (default\: None)]:username:"
|
||||
"*"{-v,--variable}"[apply specified makepkg variables to the next build (default\: None)]:variable:"
|
||||
"(*):package source (base name, path to local files, remote URL):"
|
||||
)
|
||||
|
||||
@ -201,8 +204,9 @@ _shtab_ahriman_package_add_options=(
|
||||
{--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)"
|
||||
{-s,--source}"[explicitly specify the package source for this command (default\: auto)]:source:(auto archive aur directory local remote repository)"
|
||||
{-u,--username}"[build as user (default\: None)]:username:"
|
||||
"*"{-v,--variable}"[apply specified makepkg variables to the next build (default\: None)]:variable:"
|
||||
"(*):package source (base name, path to local files, remote URL):"
|
||||
)
|
||||
|
||||
@ -227,7 +231,7 @@ _shtab_ahriman_package_status_remove_options=(
|
||||
|
||||
_shtab_ahriman_package_status_update_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-s,--status}"[new package build status (default\: BuildStatusEnum.Success)]:status:(unknown pending building failed success)"
|
||||
{-s,--status}"[new package build status (default\: success)]:status:(unknown pending building failed success)"
|
||||
"(*)::set status for specified packages. If no packages supplied, service status will be updated (default\: None):"
|
||||
)
|
||||
|
||||
@ -238,8 +242,9 @@ _shtab_ahriman_package_update_options=(
|
||||
{--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)"
|
||||
{-s,--source}"[explicitly specify the package source for this command (default\: auto)]:source:(auto archive aur directory local remote repository)"
|
||||
{-u,--username}"[build as user (default\: None)]:username:"
|
||||
"*"{-v,--variable}"[apply specified makepkg variables to the next build (default\: None)]:variable:"
|
||||
"(*):package source (base name, path to local files, remote URL):"
|
||||
)
|
||||
|
||||
@ -409,7 +414,7 @@ _shtab_ahriman_repo_sign_options=(
|
||||
|
||||
_shtab_ahriman_repo_status_update_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-s,--status}"[new status (default\: BuildStatusEnum.Success)]:status:(unknown pending building failed success)"
|
||||
{-s,--status}"[new status (default\: success)]:status:(unknown pending building failed success)"
|
||||
)
|
||||
|
||||
_shtab_ahriman_repo_sync_options=(
|
||||
@ -445,6 +450,11 @@ _shtab_ahriman_report_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
)
|
||||
|
||||
_shtab_ahriman_run_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"(*):command to be run (quoted) without \`\`ahriman\`\`:"
|
||||
)
|
||||
|
||||
_shtab_ahriman_search_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
|
||||
@ -486,6 +496,11 @@ _shtab_ahriman_service_repositories_options=(
|
||||
{--id-only,--no-id-only}"[show machine readable identifier instead (default\: False)]:id_only:"
|
||||
)
|
||||
|
||||
_shtab_ahriman_service_run_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"(*):command to be run (quoted) without \`\`ahriman\`\`:"
|
||||
)
|
||||
|
||||
_shtab_ahriman_service_setup_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"--build-as-user[force makepkg user to the specific one (default\: None)]:build_as_user:"
|
||||
@ -548,7 +563,7 @@ _shtab_ahriman_status_options=(
|
||||
|
||||
_shtab_ahriman_status_update_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-s,--status}"[new package build status (default\: BuildStatusEnum.Success)]:status:(unknown pending building failed success)"
|
||||
{-s,--status}"[new package build status (default\: success)]:status:(unknown pending building failed success)"
|
||||
"(*)::set status for specified packages. If no packages supplied, service status will be updated (default\: None):"
|
||||
)
|
||||
|
||||
@ -576,7 +591,7 @@ _shtab_ahriman_user_add_options=(
|
||||
"--key[optional PGP key used by this user. The private key must be imported (default\: None)]:key:"
|
||||
"--packager[optional packager id used for build process in form of \`Name Surname \<mail\@example.com\>\` (default\: None)]:packager:"
|
||||
{-p,--password}"[user password. Blank password will be treated as empty password, which is in particular must be used for OAuth2 authorization type. (default\: None)]:password:"
|
||||
{-R,--role}"[user access level (default\: UserAccess.Read)]:role:(unauthorized read reporter full)"
|
||||
{-R,--role}"[user access level (default\: read)]:role:(unauthorized read reporter full)"
|
||||
":username for web service:"
|
||||
)
|
||||
|
||||
@ -662,12 +677,14 @@ _shtab_ahriman() {
|
||||
repo-triggers) _arguments -C -s $_shtab_ahriman_repo_triggers_options ;;
|
||||
repo-update) _arguments -C -s $_shtab_ahriman_repo_update_options ;;
|
||||
report) _arguments -C -s $_shtab_ahriman_report_options ;;
|
||||
run) _arguments -C -s $_shtab_ahriman_run_options ;;
|
||||
search) _arguments -C -s $_shtab_ahriman_search_options ;;
|
||||
service-clean) _arguments -C -s $_shtab_ahriman_service_clean_options ;;
|
||||
service-config) _arguments -C -s $_shtab_ahriman_service_config_options ;;
|
||||
service-config-validate) _arguments -C -s $_shtab_ahriman_service_config_validate_options ;;
|
||||
service-key-import) _arguments -C -s $_shtab_ahriman_service_key_import_options ;;
|
||||
service-repositories) _arguments -C -s $_shtab_ahriman_service_repositories_options ;;
|
||||
service-run) _arguments -C -s $_shtab_ahriman_service_run_options ;;
|
||||
service-setup) _arguments -C -s $_shtab_ahriman_service_setup_options ;;
|
||||
service-shell) _arguments -C -s $_shtab_ahriman_service_shell_options ;;
|
||||
service-tree-migrate) _arguments -C -s $_shtab_ahriman_service_tree_migrate_options ;;
|
||||
|
212
pylint_plugins/definition_order.py
Normal file
212
pylint_plugins/definition_order.py
Normal file
@ -0,0 +1,212 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
from astroid import nodes
|
||||
from collections.abc import Iterable
|
||||
from enum import StrEnum
|
||||
from pylint.checkers import BaseRawFileChecker
|
||||
from pylint.lint import PyLinter
|
||||
from typing import Any
|
||||
|
||||
|
||||
class MethodTypeOrder(StrEnum):
|
||||
"""
|
||||
method type enumeration
|
||||
|
||||
Attributes:
|
||||
Class(MethodTypeOrder): (class attribute) class method
|
||||
Delete(MethodTypeOrder): (class attribute) destructor-like methods
|
||||
Init(MethodTypeOrder): (class attribute) initialization method
|
||||
Magic(MethodTypeOrder): (class attribute) other magical methods
|
||||
New(MethodTypeOrder): (class attribute) constructor method
|
||||
Normal(MethodTypeOrder): (class attribute) usual method
|
||||
Property(MethodTypeOrder): (class attribute) property method
|
||||
Static(MethodTypeOrder): (class attribute) static method
|
||||
"""
|
||||
|
||||
Class = "classmethod"
|
||||
Delete = "del"
|
||||
Init = "init"
|
||||
Magic = "magic"
|
||||
New = "new"
|
||||
Normal = "regular"
|
||||
Property = "property"
|
||||
Static = "staticmethod"
|
||||
|
||||
|
||||
class DefinitionOrder(BaseRawFileChecker):
|
||||
"""
|
||||
check if methods are defined in recommended order
|
||||
|
||||
Attributes:
|
||||
DECORATED_METHODS_ORDER(list[str]): (class attribute) predefined list of known function decorators
|
||||
"""
|
||||
|
||||
DECORATED_METHODS_ORDER = {
|
||||
"cached_property": MethodTypeOrder.Property,
|
||||
"classmethod": MethodTypeOrder.Class,
|
||||
"property": MethodTypeOrder.Property,
|
||||
"staticmethod": MethodTypeOrder.Static,
|
||||
}
|
||||
|
||||
name = "method-ordering"
|
||||
msgs = {
|
||||
"W6001": (
|
||||
"Invalid method order %s, expected %s",
|
||||
"methods-out-of-order",
|
||||
"Methods are defined out of recommended order.",
|
||||
)
|
||||
}
|
||||
options = (
|
||||
(
|
||||
"method-type-order",
|
||||
{
|
||||
"default": [
|
||||
"init",
|
||||
"new",
|
||||
"del",
|
||||
"property",
|
||||
"classmethod",
|
||||
"staticmethod",
|
||||
"regular",
|
||||
"magic",
|
||||
],
|
||||
"type": "csv",
|
||||
"metavar": "<comma-separated types>",
|
||||
"help": "Method types order to check.",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def methods(source: Iterable[Any], start_lineno: int = 0) -> list[nodes.FunctionDef]:
|
||||
"""
|
||||
extract function nodes from list of raw nodes
|
||||
|
||||
Args:
|
||||
source(Iterable[Any]): all available nodes
|
||||
start_lineno(int, optional): minimal allowed line number (Default value = 0)
|
||||
|
||||
Returns:
|
||||
list[nodes.FunctionDef]: list of function nodes
|
||||
"""
|
||||
def is_defined_function(function: Any) -> bool:
|
||||
return isinstance(function, nodes.FunctionDef) \
|
||||
and function.lineno is not None \
|
||||
and function.lineno >= start_lineno
|
||||
|
||||
return list(filter(is_defined_function, source))
|
||||
|
||||
@staticmethod
|
||||
def resolve_type(function: nodes.FunctionDef) -> MethodTypeOrder:
|
||||
"""
|
||||
resolve type of the function
|
||||
|
||||
Args:
|
||||
function(nodes.FunctionDef): function definition
|
||||
|
||||
Returns:
|
||||
MethodTypeOrder: resolved function type
|
||||
"""
|
||||
# init methods
|
||||
if function.name in ("__init__", "__post_init__"):
|
||||
return MethodTypeOrder.Init
|
||||
if function.name in ("__new__",):
|
||||
return MethodTypeOrder.New
|
||||
if function.name in ("__del__",):
|
||||
return MethodTypeOrder.Delete
|
||||
|
||||
# decorated methods
|
||||
decorators = []
|
||||
if function.decorators is not None:
|
||||
decorators = [getattr(decorator, "name", None) for decorator in function.decorators.get_children()]
|
||||
for decorator in decorators:
|
||||
if decorator in DefinitionOrder.DECORATED_METHODS_ORDER:
|
||||
return DefinitionOrder.DECORATED_METHODS_ORDER[decorator]
|
||||
|
||||
# magic methods
|
||||
if function.name.startswith("__") and function.name.endswith("__"):
|
||||
return MethodTypeOrder.Magic
|
||||
|
||||
# normal method
|
||||
return MethodTypeOrder.Normal
|
||||
|
||||
def check_class(self, clazz: nodes.ClassDef) -> None:
|
||||
"""
|
||||
check class functions ordering
|
||||
|
||||
Args:
|
||||
clazz(nodes.ClassDef): class definition
|
||||
"""
|
||||
methods = self.methods(clazz.values(), clazz.lineno)
|
||||
self.check_functions(methods)
|
||||
|
||||
def check_functions(self, functions: list[nodes.FunctionDef]) -> None:
|
||||
"""
|
||||
check global functions ordering
|
||||
|
||||
Args:
|
||||
functions(list[nodes.FunctionDef]): list of functions in their defined order
|
||||
"""
|
||||
for real, expected in zip(functions, sorted(functions, key=self.comparator)):
|
||||
if real == expected:
|
||||
continue
|
||||
self.add_message("methods-out-of-order", line=real.lineno, args=(real.name, expected.name))
|
||||
break
|
||||
|
||||
def comparator(self, function: nodes.FunctionDef) -> tuple[int, str]:
|
||||
"""
|
||||
compare key for sorting function
|
||||
|
||||
Args:
|
||||
function(nodes.FunctionDef): function definition
|
||||
|
||||
Returns:
|
||||
tuple[int, str]: comparison key for the specified function definition
|
||||
"""
|
||||
function_type = self.resolve_type(function)
|
||||
try:
|
||||
function_type_index = self.linter.config.method_type_order.index(function_type)
|
||||
except ValueError:
|
||||
function_type_index = 10 # not in the list
|
||||
|
||||
return function_type_index, function.name
|
||||
|
||||
def process_module(self, node: nodes.Module) -> None:
|
||||
"""
|
||||
process module
|
||||
|
||||
Args:
|
||||
node(nodes.Module): module node to check
|
||||
"""
|
||||
# check global methods
|
||||
self.check_functions(self.methods(node.values()))
|
||||
# check class definitions
|
||||
for clazz in filter(lambda method: isinstance(method, nodes.ClassDef), node.values()):
|
||||
self.check_class(clazz)
|
||||
|
||||
|
||||
def register(linter: PyLinter) -> None:
|
||||
"""
|
||||
register custom checker
|
||||
|
||||
Args:
|
||||
linter(PyLinter): linter in which checker should be registered
|
||||
"""
|
||||
linter.register_checker(DefinitionOrder(linter))
|
@ -40,7 +40,7 @@ check = [
|
||||
"mypy",
|
||||
"pylint",
|
||||
]
|
||||
docs = [
|
||||
docs = [
|
||||
"Sphinx",
|
||||
"argparse-manpage",
|
||||
"pydeps",
|
||||
|
@ -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.11.0"
|
||||
__version__ = "2.12.2"
|
||||
|
@ -87,17 +87,17 @@ def _parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument("--repository-id", help=argparse.SUPPRESS)
|
||||
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__)
|
||||
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")
|
||||
|
||||
_set_aur_search_parser(subparsers)
|
||||
_set_help_parser(subparsers)
|
||||
_set_help_commands_unsafe_parser(subparsers)
|
||||
_set_help_parser(subparsers)
|
||||
_set_help_updates_parser(subparsers)
|
||||
_set_help_version_parser(subparsers)
|
||||
_set_package_add_parser(subparsers)
|
||||
@ -129,6 +129,7 @@ def _parser() -> argparse.ArgumentParser:
|
||||
_set_service_config_validate_parser(subparsers)
|
||||
_set_service_key_import_parser(subparsers)
|
||||
_set_service_repositories(subparsers)
|
||||
_set_service_run(subparsers)
|
||||
_set_service_setup_parser(subparsers)
|
||||
_set_service_shell_parser(subparsers)
|
||||
_set_service_tree_migrate_parser(subparsers)
|
||||
@ -165,6 +166,25 @@ def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
return parser
|
||||
|
||||
|
||||
def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for listing unsafe commands
|
||||
|
||||
Args:
|
||||
root(SubParserAction): subparsers for the commands
|
||||
|
||||
Returns:
|
||||
argparse.ArgumentParser: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("help-commands-unsafe", help="list unsafe commands",
|
||||
description="list unsafe commands as defined in default args", formatter_class=_formatter)
|
||||
parser.add_argument("subcommand", help="instead of showing commands, just test command line for unsafe subcommand "
|
||||
"and return 0 in case if command is safe and 1 otherwise", nargs="*")
|
||||
parser.set_defaults(handler=handlers.UnsafeCommands, architecture="", lock=None, quiet=True, report=False,
|
||||
repository="", unsafe=True, parser=_parser)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for listing help subcommand
|
||||
@ -178,31 +198,12 @@ def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
parser = root.add_parser("help", help="show help message",
|
||||
description="show help message for application or command and exit",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("command", help="show help message for specific command", nargs="?")
|
||||
parser.add_argument("subcommand", help="show help message for specific command", nargs="?")
|
||||
parser.set_defaults(handler=handlers.Help, architecture="", lock=None, quiet=True, report=False, repository="",
|
||||
unsafe=True, parser=_parser)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for listing unsafe commands
|
||||
|
||||
Args:
|
||||
root(SubParserAction): subparsers for the commands
|
||||
|
||||
Returns:
|
||||
argparse.ArgumentParser: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("help-commands-unsafe", help="list unsafe commands",
|
||||
description="list unsafe commands as defined in default args", formatter_class=_formatter)
|
||||
parser.add_argument("command", help="instead of showing commands, just test command line for unsafe subcommand "
|
||||
"and return 0 in case if command is safe and 1 otherwise", nargs="*")
|
||||
parser.set_defaults(handler=handlers.UnsafeCommands, architecture="", lock=None, quiet=True, report=False,
|
||||
repository="", unsafe=True, parser=_parser)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_help_updates_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for service update check subcommand
|
||||
@ -275,6 +276,7 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
|
||||
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
|
||||
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
||||
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build", action="append")
|
||||
parser.set_defaults(handler=handlers.Add)
|
||||
return parser
|
||||
|
||||
@ -902,6 +904,27 @@ def _set_service_repositories(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
return parser
|
||||
|
||||
|
||||
def _set_service_run(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for multicommand
|
||||
|
||||
Args:
|
||||
root(SubParserAction): subparsers for the commands
|
||||
|
||||
Returns:
|
||||
argparse.ArgumentParser: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("service-run", aliases=["run"], help="run multiple commands",
|
||||
description="run multiple commands on success run of the previous command",
|
||||
epilog="Commands must be quoted by using usual bash rules. Processes will be spawned "
|
||||
"under the same user as this command",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("command", help="command to be run (quoted) without ``ahriman``", nargs="+")
|
||||
parser.set_defaults(handler=handlers.Run, architecture="", lock=None, report=False, repository="",
|
||||
unsafe=True, parser=_parser)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_service_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for setup subcommand
|
||||
|
@ -32,8 +32,8 @@ class Application(ApplicationPackages, ApplicationRepository):
|
||||
base application class
|
||||
|
||||
Examples:
|
||||
This class groups ``Repository`` methods into specific method which process all supposed actions caused by
|
||||
underlying action. E.g.::
|
||||
This class groups :class:`ahriman.core.repository.repository.Repository` methods into specific method which
|
||||
process all supposed actions caused by underlying action. E.g.::
|
||||
|
||||
>>> from ahriman.core.configuration import Configuration
|
||||
>>> from ahriman.models.package_source import PackageSource
|
||||
@ -49,8 +49,8 @@ class Application(ApplicationPackages, ApplicationRepository):
|
||||
>>> # updates for specified packages
|
||||
>>> application.update(updates)
|
||||
|
||||
In case if specific actions or their order are required, the direct access to ``Repository`` must
|
||||
be used instead.
|
||||
In case if specific actions or their order are required, the direct access to
|
||||
:class:`ahriman.core.repository.repository.Repository` must be used instead.
|
||||
"""
|
||||
|
||||
def _known_packages(self) -> set[str]:
|
||||
@ -117,7 +117,7 @@ class Application(ApplicationPackages, ApplicationRepository):
|
||||
|
||||
Returns:
|
||||
list[Package]: updated packages list. Packager for dependencies will be copied from
|
||||
original package
|
||||
original package
|
||||
"""
|
||||
def missing_dependencies(source: Iterable[Package]) -> dict[str, str | None]:
|
||||
# append list of known packages with packages which are in current sources
|
||||
|
@ -167,12 +167,16 @@ class ApplicationPackages(ApplicationProperties):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def remove(self, names: Iterable[str]) -> None:
|
||||
def remove(self, names: Iterable[str]) -> Result:
|
||||
"""
|
||||
remove packages from repository
|
||||
|
||||
Args:
|
||||
names(Iterable[str]): list of packages (either base or name) to remove
|
||||
|
||||
Returns:
|
||||
Result: removal result
|
||||
"""
|
||||
self.repository.process_remove(names)
|
||||
self.on_result(Result())
|
||||
result = self.repository.process_remove(names)
|
||||
self.on_result(result)
|
||||
return result
|
||||
|
@ -32,6 +32,7 @@ from ahriman.application.handlers.remove import Remove
|
||||
from ahriman.application.handlers.remove_unknown import RemoveUnknown
|
||||
from ahriman.application.handlers.repositories import Repositories
|
||||
from ahriman.application.handlers.restore import Restore
|
||||
from ahriman.application.handlers.run import Run
|
||||
from ahriman.application.handlers.search import Search
|
||||
from ahriman.application.handlers.service_updates import ServiceUpdates
|
||||
from ahriman.application.handlers.setup import Setup
|
||||
|
@ -23,6 +23,7 @@ from ahriman.application.application import Application
|
||||
from ahriman.application.handlers import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.packagers import Packagers
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
@ -45,7 +46,12 @@ class Add(Handler):
|
||||
"""
|
||||
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
||||
application.on_start()
|
||||
|
||||
application.add(args.package, args.source, args.username)
|
||||
patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else []
|
||||
for package in args.package: # for each requested package insert patch
|
||||
application.database.patches_insert(package, patches)
|
||||
|
||||
if not args.now:
|
||||
return
|
||||
|
||||
|
@ -39,8 +39,8 @@ class Handler:
|
||||
ALLOW_MULTI_ARCHITECTURE_RUN(bool): (class attribute) allow running with multiple architectures
|
||||
|
||||
Examples:
|
||||
Wrapper for all command line actions, though each derived class implements ``run`` method, it usually must not
|
||||
be called directly. The recommended way is to call ``execute`` class method, e.g.::
|
||||
Wrapper for all command line actions, though each derived class implements :func:`run()` method, it usually
|
||||
must not be called directly. The recommended way is to call :func:`execute()` class method, e.g.::
|
||||
|
||||
>>> from ahriman.application.handlers import Add
|
||||
>>>
|
||||
@ -162,7 +162,7 @@ class Handler:
|
||||
if args.repository_id is not None:
|
||||
separator = "/" if "/" in args.repository_id else "-" # systemd and non-systemd identifiers
|
||||
# repository parts is optional for backward compatibility
|
||||
architecture, *repository_parts = args.repository_id.split(separator)
|
||||
architecture, *repository_parts = args.repository_id.split(separator) # maxsplit isn't used intentionally
|
||||
args.architecture = architecture
|
||||
if repository_parts:
|
||||
args.repository = "-".join(repository_parts) # replace slash with dash
|
||||
|
@ -44,7 +44,7 @@ class Help(Handler):
|
||||
report(bool): force enable or disable reporting
|
||||
"""
|
||||
parser: argparse.ArgumentParser = args.parser()
|
||||
if args.command is None:
|
||||
if args.subcommand is None:
|
||||
parser.parse_args(["--help"])
|
||||
else:
|
||||
parser.parse_args([args.command, "--help"])
|
||||
parser.parse_args([args.subcommand, "--help"])
|
||||
|
@ -115,7 +115,7 @@ class Patch(Handler):
|
||||
package_base(str): package base
|
||||
patch(PkgbuildPatch): patch descriptor
|
||||
"""
|
||||
application.database.patches_insert(package_base, patch)
|
||||
application.database.patches_insert(package_base, [patch])
|
||||
|
||||
@staticmethod
|
||||
def patch_set_list(application: Application, package_base: str | None, variables: list[str] | None,
|
||||
|
@ -24,6 +24,7 @@ from ahriman.application.handlers import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.packagers import Packagers
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
@ -55,7 +56,7 @@ class Rebuild(Handler):
|
||||
application.print_updates(updates, log_fn=print)
|
||||
return
|
||||
|
||||
result = application.update(updates, args.username, bump_pkgrel=args.increment)
|
||||
result = application.update(updates, Packagers(args.username), bump_pkgrel=args.increment)
|
||||
Rebuild.check_if_empty(args.exit_code, result.is_empty)
|
||||
|
||||
@staticmethod
|
||||
|
66
src/ahriman/application/handlers/run.py
Normal file
66
src/ahriman/application/handlers/run.py
Normal file
@ -0,0 +1,66 @@
|
||||
#
|
||||
# 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 argparse
|
||||
import shlex
|
||||
|
||||
from ahriman.application.handlers import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class Run(Handler):
|
||||
"""
|
||||
multicommand handler
|
||||
"""
|
||||
|
||||
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||
|
||||
@classmethod
|
||||
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||
report: bool) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
|
||||
Args:
|
||||
args(argparse.Namespace): command line args
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
report(bool): force enable or disable reporting
|
||||
"""
|
||||
parser = args.parser()
|
||||
for command in args.command:
|
||||
status = Run.run_command(shlex.split(command), parser)
|
||||
Run.check_if_empty(True, not status)
|
||||
|
||||
@staticmethod
|
||||
def run_command(command: list[str], parser: argparse.ArgumentParser) -> bool:
|
||||
"""
|
||||
run command specified by the argument
|
||||
|
||||
Args:
|
||||
command(list[str]): command to run
|
||||
parser(argparse.ArgumentParser): generated argument parser
|
||||
|
||||
Returns:
|
||||
bool: status of the command
|
||||
"""
|
||||
args = parser.parse_args(command)
|
||||
handler: Handler = args.handler
|
||||
return handler.execute(args) == 0
|
@ -21,6 +21,7 @@ import argparse
|
||||
|
||||
from pathlib import Path
|
||||
from pwd import getpwuid
|
||||
from urllib.parse import quote_plus as urlencode
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers import Handler
|
||||
@ -128,14 +129,18 @@ class Setup(Handler):
|
||||
|
||||
if args.web_port is not None:
|
||||
configuration.set_option("web", "port", str(args.web_port))
|
||||
if (host := root.get("web", "host", fallback=None)) is not None:
|
||||
configuration.set_option("status", "address", f"http://{host}:{args.web_port}")
|
||||
if args.web_unix_socket is not None:
|
||||
configuration.set_option("web", "unix_socket", str(args.web_unix_socket))
|
||||
unix_socket = str(args.web_unix_socket)
|
||||
configuration.set_option("web", "unix_socket", unix_socket)
|
||||
configuration.set_option("status", "address", f"http+unix://{urlencode(unix_socket)}")
|
||||
|
||||
if args.generate_salt:
|
||||
configuration.set_option("auth", "salt", User.generate_password(20))
|
||||
|
||||
(root.include / "00-setup-overrides.ini").unlink(missing_ok=True) # remove old-style configuration
|
||||
target = root.repository_paths.root / f"00-setup-overrides-{repository_id.id}.ini"
|
||||
target = root.include / f"00-setup-overrides-{repository_id.id}.ini"
|
||||
with target.open("w") as ahriman_configuration:
|
||||
configuration.write(ahriman_configuration)
|
||||
|
||||
|
@ -46,8 +46,8 @@ class UnsafeCommands(Handler):
|
||||
"""
|
||||
parser = args.parser()
|
||||
unsafe_commands = UnsafeCommands.get_unsafe_commands(parser)
|
||||
if args.command:
|
||||
UnsafeCommands.check_unsafe(args.command, unsafe_commands, parser)
|
||||
if args.subcommand:
|
||||
UnsafeCommands.check_unsafe(args.subcommand, unsafe_commands, parser)
|
||||
else:
|
||||
for command in unsafe_commands:
|
||||
StringPrinter(command)(verbose=True)
|
||||
|
@ -67,8 +67,8 @@ class Versions(Handler):
|
||||
Args:
|
||||
root(str): root package name
|
||||
|
||||
Returns:
|
||||
Generator[tuple[str, str], None, None]: map of installed dependency to its version
|
||||
Yields:
|
||||
tuple[str, str]: map of installed dependency to its version
|
||||
"""
|
||||
def dependencies_by_key(key: str) -> Generator[str, None, None]:
|
||||
# in importlib it returns requires in the following format
|
||||
|
@ -76,8 +76,8 @@ class Web(Handler):
|
||||
args(argparse.Namespace): command line args
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
Generator[str, None, None]: command line arguments which were used for this specific command
|
||||
Yields:
|
||||
str: command line arguments which were used for this specific command
|
||||
"""
|
||||
# read configuration path from current settings
|
||||
if (configuration_path := configuration.path) is not None:
|
||||
|
@ -70,9 +70,8 @@ class Lock(LazyLogging):
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
lock_suffix = f"{repository_id.name}_{repository_id.architecture}"
|
||||
self.path: Path | None = \
|
||||
args.lock.with_stem(f"{args.lock.stem}_{lock_suffix}") if args.lock is not None else None
|
||||
args.lock.with_stem(f"{args.lock.stem}_{repository_id.id}") if args.lock is not None else None
|
||||
|
||||
self.force: bool = args.force
|
||||
self.unsafe: bool = args.unsafe
|
||||
@ -81,6 +80,13 @@ class Lock(LazyLogging):
|
||||
self.paths = configuration.repository_paths
|
||||
self.reporter = Client.load(repository_id, configuration, report=args.report)
|
||||
|
||||
def check_user(self) -> None:
|
||||
"""
|
||||
check if current user is actually owner of ahriman root
|
||||
"""
|
||||
check_user(self.paths, unsafe=self.unsafe)
|
||||
self.paths.tree_create()
|
||||
|
||||
def check_version(self) -> None:
|
||||
"""
|
||||
check web server version
|
||||
@ -90,13 +96,6 @@ class Lock(LazyLogging):
|
||||
self.logger.warning("status watcher version mismatch, our %s, their %s",
|
||||
__version__, status.version)
|
||||
|
||||
def check_user(self) -> None:
|
||||
"""
|
||||
check if current user is actually owner of ahriman root
|
||||
"""
|
||||
check_user(self.paths, unsafe=self.unsafe)
|
||||
self.paths.tree_create()
|
||||
|
||||
def clear(self) -> None:
|
||||
"""
|
||||
remove lock file
|
||||
|
@ -28,15 +28,16 @@ class Remote(SyncHttpClient):
|
||||
|
||||
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::
|
||||
provided: :func:`info()`, :func:`multisearch()` and :func:`search()`. Thus, the basic flow is the following::
|
||||
|
||||
>>> from ahriman.core.alpm.remote import AUR, Official
|
||||
>>>
|
||||
>>> package = AUR.info("ahriman", pacman=pacman)
|
||||
>>> search_result = Official.multisearch("pacman", "manager", pacman=pacman)
|
||||
|
||||
Differnece between ``search`` and ``multisearch`` is that ``search`` passes all arguments to underlying wrapper
|
||||
directly, whereas ``multisearch`` splits search one by one and finds intersection between search results.
|
||||
Differnece between :func:`search()` and :func:`multisearch()` is that :func:`search()` passes all arguments to
|
||||
underlying wrapper directly, whereas :func:`multisearch()` splits search one by one and finds intersection
|
||||
between search results.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
|
@ -36,8 +36,6 @@ class Repo(LazyLogging):
|
||||
uid(int): uid of the repository owner user
|
||||
"""
|
||||
|
||||
_check_output = check_output
|
||||
|
||||
def __init__(self, name: str, paths: RepositoryPaths, sign_args: list[str]) -> None:
|
||||
"""
|
||||
default constructor
|
||||
@ -69,7 +67,7 @@ class Repo(LazyLogging):
|
||||
Args:
|
||||
path(Path): path to archive to add
|
||||
"""
|
||||
Repo._check_output(
|
||||
check_output(
|
||||
"repo-add", *self.sign_args, "-R", str(self.repo_path), str(path),
|
||||
exception=BuildError.from_process(path.name),
|
||||
cwd=self.paths.repository,
|
||||
@ -80,8 +78,8 @@ class Repo(LazyLogging):
|
||||
"""
|
||||
create empty repository database
|
||||
"""
|
||||
Repo._check_output("repo-add", *self.sign_args, str(self.repo_path),
|
||||
cwd=self.paths.repository, logger=self.logger, user=self.uid)
|
||||
check_output("repo-add", *self.sign_args, str(self.repo_path),
|
||||
cwd=self.paths.repository, logger=self.logger, user=self.uid)
|
||||
|
||||
def remove(self, package: str, filename: Path) -> None:
|
||||
"""
|
||||
@ -96,7 +94,7 @@ class Repo(LazyLogging):
|
||||
full_path.unlink()
|
||||
|
||||
# remove package from registry
|
||||
Repo._check_output(
|
||||
check_output(
|
||||
"repo-remove", *self.sign_args, str(self.repo_path), package,
|
||||
exception=BuildError.from_process(package),
|
||||
cwd=self.paths.repository,
|
||||
|
@ -42,8 +42,6 @@ class Sources(LazyLogging):
|
||||
DEFAULT_BRANCH = "master" # default fallback branch
|
||||
DEFAULT_COMMIT_AUTHOR = ("ahriman", "ahriman@localhost")
|
||||
|
||||
_check_output = check_output
|
||||
|
||||
@staticmethod
|
||||
def extend_architectures(sources_dir: Path, architecture: str) -> list[PkgbuildPatch]:
|
||||
"""
|
||||
@ -82,20 +80,20 @@ class Sources(LazyLogging):
|
||||
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", "--quiet", "--depth", "1", "origin", branch,
|
||||
cwd=sources_dir, logger=instance.logger)
|
||||
check_output("git", "fetch", "--quiet", "--depth", "1", "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", "--quiet", "--depth", "1", "--branch", branch, "--single-branch",
|
||||
remote.git_url, str(sources_dir), cwd=sources_dir.parent, logger=instance.logger)
|
||||
check_output("git", "clone", "--quiet", "--depth", "1", "--branch", branch, "--single-branch",
|
||||
remote.git_url, str(sources_dir), cwd=sources_dir.parent, logger=instance.logger)
|
||||
else:
|
||||
# it will cause an exception later
|
||||
instance.logger.error("%s is not initialized, but no remote provided", sources_dir)
|
||||
|
||||
# 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", "--quiet", "--hard", f"origin/{branch}",
|
||||
cwd=sources_dir, logger=instance.logger)
|
||||
check_output("git", "checkout", "--force", branch, cwd=sources_dir, logger=instance.logger)
|
||||
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
|
||||
@ -114,7 +112,7 @@ class Sources(LazyLogging):
|
||||
bool: True in case if there is any remote and false otherwise
|
||||
"""
|
||||
instance = Sources()
|
||||
remotes = Sources._check_output("git", "remote", cwd=sources_dir, logger=instance.logger)
|
||||
remotes = check_output("git", "remote", cwd=sources_dir, logger=instance.logger)
|
||||
return bool(remotes)
|
||||
|
||||
@staticmethod
|
||||
@ -128,8 +126,8 @@ class Sources(LazyLogging):
|
||||
instance = Sources()
|
||||
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)
|
||||
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)]
|
||||
@ -193,7 +191,7 @@ class Sources(LazyLogging):
|
||||
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)
|
||||
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:
|
||||
"""
|
||||
@ -214,8 +212,8 @@ class Sources(LazyLogging):
|
||||
self.logger.info("found matching files %s", found_files)
|
||||
# add them to index
|
||||
args = ["--intent-to-add"] if intent_to_add else []
|
||||
Sources._check_output("git", "add", *args, *[str(fn.relative_to(sources_dir)) for fn in found_files],
|
||||
cwd=sources_dir, logger=self.logger)
|
||||
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,
|
||||
commit_author: tuple[str, str] | None = None) -> bool:
|
||||
@ -245,8 +243,7 @@ class Sources(LazyLogging):
|
||||
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)
|
||||
check_output("git", "commit", "--quiet", *args, cwd=sources_dir, logger=self.logger, environment=environment)
|
||||
|
||||
return True
|
||||
|
||||
@ -260,7 +257,7 @@ class Sources(LazyLogging):
|
||||
Returns:
|
||||
str: patch as plain string
|
||||
"""
|
||||
return Sources._check_output("git", "diff", cwd=sources_dir, logger=self.logger)
|
||||
return check_output("git", "diff", cwd=sources_dir, logger=self.logger)
|
||||
|
||||
def has_changes(self, sources_dir: Path) -> bool:
|
||||
"""
|
||||
@ -273,7 +270,7 @@ class Sources(LazyLogging):
|
||||
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)
|
||||
changes = 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:
|
||||
@ -302,7 +299,7 @@ class Sources(LazyLogging):
|
||||
# create patch
|
||||
self.logger.info("apply patch %s from database at %s", patch.key, sources_dir)
|
||||
if patch.is_plain_diff:
|
||||
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
|
||||
cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
|
||||
check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
|
||||
cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
|
||||
else:
|
||||
patch.write(sources_dir / "PKGBUILD")
|
||||
|
@ -45,8 +45,6 @@ class Task(LazyLogging):
|
||||
uid(int): uid of the repository owner user
|
||||
"""
|
||||
|
||||
_check_output = check_output
|
||||
|
||||
def __init__(self, package: Package, configuration: Configuration, architecture: str,
|
||||
paths: RepositoryPaths) -> None:
|
||||
"""
|
||||
@ -68,13 +66,13 @@ class Task(LazyLogging):
|
||||
self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
|
||||
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])
|
||||
|
||||
def build(self, sources_dir: Path, packager: str | None = None) -> list[Path]:
|
||||
def build(self, sources_dir: Path, **kwargs: str | None) -> list[Path]:
|
||||
"""
|
||||
run package build
|
||||
|
||||
Args:
|
||||
sources_dir(Path): path to where sources are
|
||||
packager(str | None, optional): optional packager override (Default value = None)
|
||||
**kwargs(str | None): environment variables to be passed to build processes
|
||||
|
||||
Returns:
|
||||
list[Path]: paths of produced packages
|
||||
@ -85,25 +83,29 @@ class Task(LazyLogging):
|
||||
command.extend(["--"] + self.makepkg_flags)
|
||||
self.logger.info("using %s for %s", command, self.package.base)
|
||||
|
||||
environment: dict[str, str] = {}
|
||||
if packager is not None:
|
||||
environment["PACKAGER"] = packager
|
||||
environment: dict[str, str] = {
|
||||
key: value
|
||||
for key, value in kwargs.items()
|
||||
if value is not None
|
||||
}
|
||||
self.logger.info("using environment variables %s", environment)
|
||||
|
||||
Task._check_output(
|
||||
check_output(
|
||||
*command,
|
||||
exception=BuildError.from_process(self.package.base),
|
||||
cwd=sources_dir,
|
||||
logger=self.logger,
|
||||
user=self.uid,
|
||||
environment=environment)
|
||||
environment=environment,
|
||||
)
|
||||
|
||||
# well it is not actually correct, but we can deal with it
|
||||
packages = Task._check_output(
|
||||
packages = check_output(
|
||||
"makepkg", "--packagelist",
|
||||
exception=BuildError.from_process(self.package.base),
|
||||
cwd=sources_dir,
|
||||
logger=self.logger
|
||||
logger=self.logger,
|
||||
environment=environment,
|
||||
).splitlines()
|
||||
return [Path(package) for package in packages]
|
||||
|
||||
|
@ -45,8 +45,8 @@ class Configuration(configparser.RawConfigParser):
|
||||
|
||||
Examples:
|
||||
Configuration class provides additional method in order to handle application configuration. Since this class is
|
||||
derived from built-in ``configparser.RawConfigParser`` class, the same flow is applicable here. Nevertheless,
|
||||
it is recommended to use ``from_path`` class method which also calls initialization methods::
|
||||
derived from built-in :class:`configparser.RawConfigParser` class, the same flow is applicable here.
|
||||
Nevertheless, it is recommended to use :func:`from_path` class method which also calls initialization methods::
|
||||
|
||||
>>> from pathlib import Path
|
||||
>>>
|
||||
@ -57,12 +57,13 @@ class Configuration(configparser.RawConfigParser):
|
||||
The configuration instance loaded in this way will contain only sections which are defined for the specified
|
||||
architecture according to the merge rules. Moreover, the architecture names will be removed from section names.
|
||||
|
||||
In order to get current settings, the ``check_loaded`` method can be used. This method will raise an
|
||||
``InitializeError`` in case if configuration was not yet loaded::
|
||||
In order to get current settings, the :func:`check_loaded` method can be used. This method will raise an
|
||||
:exc:`ahriman.core.exceptions.InitializeError` in case if configuration was not yet loaded::
|
||||
|
||||
>>> path, repository_id = configuration.check_loaded()
|
||||
"""
|
||||
|
||||
_LEGACY_ARCHITECTURE_SPECIFIC_SECTIONS = ["web"]
|
||||
ARCHITECTURE_SPECIFIC_SECTIONS = ["alpm", "build", "sign"]
|
||||
SYSTEM_CONFIGURATION_PATH = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini"
|
||||
converters: dict[str, Callable[[str], Any]] # typing guard
|
||||
@ -72,7 +73,7 @@ class Configuration(configparser.RawConfigParser):
|
||||
default constructor. In the most cases must not be called directly
|
||||
|
||||
Args:
|
||||
allow_no_value(bool, optional): copies ``configparser.RawConfigParser`` behaviour. In case if it is set
|
||||
allow_no_value(bool, optional): copies :class:`configparser.RawConfigParser` behaviour. In case if it is set
|
||||
to ``True``, the keys without values will be allowed (Default value = False)
|
||||
"""
|
||||
configparser.RawConfigParser.__init__(
|
||||
@ -160,25 +161,6 @@ class Configuration(configparser.RawConfigParser):
|
||||
configuration.merge_sections(repository_id)
|
||||
return configuration
|
||||
|
||||
@staticmethod
|
||||
def override_sections(section: str, repository_id: RepositoryId) -> list[str]:
|
||||
"""
|
||||
extract override sections
|
||||
|
||||
Args:
|
||||
section(str): section name
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
|
||||
Returns:
|
||||
list[str]: architecture and repository specific sections in correct order
|
||||
"""
|
||||
# the valid order is global < per architecture < per repository < per repository and architecture
|
||||
return [
|
||||
Configuration.section_name(section, repository_id.architecture), # architecture specific override
|
||||
Configuration.section_name(section, repository_id.name), # override with repository name
|
||||
Configuration.section_name(section, repository_id.name, repository_id.architecture), # both
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def section_name(section: str, *suffixes: str | None) -> str:
|
||||
"""
|
||||
@ -284,18 +266,18 @@ class Configuration(configparser.RawConfigParser):
|
||||
self.path = path
|
||||
|
||||
self.read(self.path)
|
||||
|
||||
self.includes = [] # reset state
|
||||
self.load_includes() # basic includes
|
||||
self.load_includes(self.getpath("repository", "root")) # home directory includes
|
||||
self.load_includes() # load includes
|
||||
|
||||
def load_includes(self, path: Path | None = None) -> None:
|
||||
"""
|
||||
load configuration includes from specified path
|
||||
|
||||
Args:
|
||||
path(Path | None, optional): path to directory with include files (Default value = None)
|
||||
path(Path | None, optional): path to directory with include files. If none set, the default path will be
|
||||
used (Default value = None)
|
||||
"""
|
||||
self.includes = [] # reset state
|
||||
|
||||
try:
|
||||
path = path or self.include
|
||||
for include in sorted(path.glob("*.ini")):
|
||||
@ -315,7 +297,7 @@ class Configuration(configparser.RawConfigParser):
|
||||
"""
|
||||
self.repository_id = repository_id
|
||||
|
||||
for section in self.ARCHITECTURE_SPECIFIC_SECTIONS:
|
||||
for section in self.ARCHITECTURE_SPECIFIC_SECTIONS + self._LEGACY_ARCHITECTURE_SPECIFIC_SECTIONS:
|
||||
for specific in self.override_sections(section, repository_id):
|
||||
if self.has_section(specific):
|
||||
# if there is no such section it means that there is no overrides for this arch,
|
||||
@ -330,6 +312,26 @@ class Configuration(configparser.RawConfigParser):
|
||||
continue
|
||||
self.remove_section(foreign)
|
||||
|
||||
def override_sections(self, section: str, repository_id: RepositoryId) -> list[str]:
|
||||
"""
|
||||
extract override sections
|
||||
|
||||
Args:
|
||||
section(str): section name
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
|
||||
Returns:
|
||||
list[str]: architecture and repository specific sections in correct order
|
||||
"""
|
||||
if repository_id.is_empty: # special case, guess sections from configuration
|
||||
return sorted(specific for specific in self.sections() if specific.startswith(f"{section}:"))
|
||||
# the valid order is global < per architecture < per repository < per repository and architecture
|
||||
return [
|
||||
Configuration.section_name(section, repository_id.architecture), # architecture specific override
|
||||
Configuration.section_name(section, repository_id.name), # override with repository name
|
||||
Configuration.section_name(section, repository_id.name, repository_id.architecture), # both
|
||||
]
|
||||
|
||||
def reload(self) -> None:
|
||||
"""
|
||||
reload configuration if possible or raise exception otherwise
|
||||
@ -342,7 +344,7 @@ class Configuration(configparser.RawConfigParser):
|
||||
|
||||
def set_option(self, section: str, option: str, value: str) -> None:
|
||||
"""
|
||||
set option. Unlike default ``configparser.RawConfigParser.set`` it also creates section if it does not exist
|
||||
set option. Unlike default :func:`configparser.RawConfigParser.set` it also creates section if it does not exist
|
||||
|
||||
Args:
|
||||
section(str): section name
|
||||
|
@ -249,6 +249,32 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"status": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
},
|
||||
"address": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
"is_url": [],
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
"suppress_http_log_errors": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
"web": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
|
@ -46,6 +46,6 @@ class ShellInterpolator(configparser.Interpolation):
|
||||
"""
|
||||
# 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
|
||||
# In addition, we are using shell-like variables in some cases (see :attr:`alpm.mirror` option),
|
||||
# thus we would like to keep them alive
|
||||
return Template(value).safe_substitute(os.environ)
|
||||
|
@ -21,6 +21,7 @@ from sqlite3 import Connection
|
||||
|
||||
from ahriman.core.database.operations import Operations
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class BuildOperations(Operations):
|
||||
@ -28,13 +29,16 @@ class BuildOperations(Operations):
|
||||
operations for build queue functions
|
||||
"""
|
||||
|
||||
def build_queue_clear(self, package_base: str | None) -> None:
|
||||
def build_queue_clear(self, package_base: str | None, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
remove packages from build queue
|
||||
|
||||
Args:
|
||||
package_base(str | None): optional filter by package base
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
"""
|
||||
@ -43,36 +47,44 @@ class BuildOperations(Operations):
|
||||
""",
|
||||
{
|
||||
"package_base": package_base,
|
||||
"repository": self._repository_id.id,
|
||||
"repository": repository_id.id,
|
||||
})
|
||||
|
||||
return self.with_connection(run, commit=True)
|
||||
|
||||
def build_queue_get(self) -> list[Package]:
|
||||
def build_queue_get(self, repository_id: RepositoryId | None = None) -> list[Package]:
|
||||
"""
|
||||
retrieve packages from build queue
|
||||
|
||||
Args:
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
|
||||
Return:
|
||||
list[Package]: list of packages to be built
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> list[Package]:
|
||||
return [
|
||||
Package.from_json(row["properties"])
|
||||
for row in connection.execute(
|
||||
"""select properties from build_queue where repository = :repository""",
|
||||
{"repository": self._repository_id.id}
|
||||
{"repository": repository_id.id}
|
||||
)
|
||||
]
|
||||
|
||||
return self.with_connection(run)
|
||||
|
||||
def build_queue_insert(self, package: Package) -> None:
|
||||
def build_queue_insert(self, package: Package, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
insert packages to build queue
|
||||
|
||||
Args:
|
||||
package(Package): package to be inserted
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
"""
|
||||
@ -86,7 +98,7 @@ class BuildOperations(Operations):
|
||||
{
|
||||
"package_base": package.base,
|
||||
"properties": package.view(),
|
||||
"repository": self._repository_id.id,
|
||||
"repository": repository_id.id,
|
||||
})
|
||||
|
||||
return self.with_connection(run, commit=True)
|
||||
|
@ -33,8 +33,8 @@ class PackageOperations(Operations):
|
||||
package operations
|
||||
"""
|
||||
|
||||
def _package_remove_package_base(self, connection: Connection, package_base: str,
|
||||
repository_id: RepositoryId) -> None:
|
||||
@staticmethod
|
||||
def _package_remove_package_base(connection: Connection, package_base: str, repository_id: RepositoryId) -> None:
|
||||
"""
|
||||
remove package base information
|
||||
|
||||
@ -50,8 +50,9 @@ class PackageOperations(Operations):
|
||||
"""delete from package_bases where package_base = :package_base and repository = :repository""",
|
||||
{"package_base": package_base, "repository": repository_id.id})
|
||||
|
||||
def _package_remove_packages(self, connection: Connection, package_base: str,
|
||||
current_packages: Iterable[str], repository_id: RepositoryId) -> None:
|
||||
@staticmethod
|
||||
def _package_remove_packages(connection: Connection, package_base: str, current_packages: Iterable[str],
|
||||
repository_id: RepositoryId) -> None:
|
||||
"""
|
||||
remove packages belong to the package base
|
||||
|
||||
@ -74,8 +75,8 @@ class PackageOperations(Operations):
|
||||
"""delete from packages where package = :package and repository = :repository""",
|
||||
packages)
|
||||
|
||||
def _package_update_insert_base(self, connection: Connection, package: Package,
|
||||
repository_id: RepositoryId) -> None:
|
||||
@staticmethod
|
||||
def _package_update_insert_base(connection: Connection, package: Package, repository_id: RepositoryId) -> None:
|
||||
"""
|
||||
insert base package into table
|
||||
|
||||
@ -107,8 +108,8 @@ class PackageOperations(Operations):
|
||||
}
|
||||
)
|
||||
|
||||
def _package_update_insert_packages(self, connection: Connection, package: Package,
|
||||
repository_id: RepositoryId) -> None:
|
||||
@staticmethod
|
||||
def _package_update_insert_packages(connection: Connection, package: Package, repository_id: RepositoryId) -> None:
|
||||
"""
|
||||
insert packages into table
|
||||
|
||||
@ -149,7 +150,8 @@ class PackageOperations(Operations):
|
||||
""",
|
||||
package_list)
|
||||
|
||||
def _package_update_insert_status(self, connection: Connection, package_base: str, status: BuildStatus,
|
||||
@staticmethod
|
||||
def _package_update_insert_status(connection: Connection, package_base: str, status: BuildStatus,
|
||||
repository_id: RepositoryId) -> None:
|
||||
"""
|
||||
insert base package status into table
|
||||
@ -176,8 +178,8 @@ class PackageOperations(Operations):
|
||||
"repository": repository_id.id,
|
||||
})
|
||||
|
||||
def _packages_get_select_package_bases(self, connection: Connection,
|
||||
repository_id: RepositoryId) -> dict[str, Package]:
|
||||
@staticmethod
|
||||
def _packages_get_select_package_bases(connection: Connection, repository_id: RepositoryId) -> dict[str, Package]:
|
||||
"""
|
||||
select package bases from the table
|
||||
|
||||
@ -201,7 +203,8 @@ class PackageOperations(Operations):
|
||||
)
|
||||
}
|
||||
|
||||
def _packages_get_select_packages(self, connection: Connection, packages: dict[str, Package],
|
||||
@staticmethod
|
||||
def _packages_get_select_packages(connection: Connection, packages: dict[str, Package],
|
||||
repository_id: RepositoryId) -> dict[str, Package]:
|
||||
"""
|
||||
select packages from the table
|
||||
@ -223,8 +226,8 @@ class PackageOperations(Operations):
|
||||
packages[row["package_base"]].packages[row["package"]] = PackageDescription.from_json(row)
|
||||
return packages
|
||||
|
||||
def _packages_get_select_statuses(self, connection: Connection,
|
||||
repository_id: RepositoryId) -> dict[str, BuildStatus]:
|
||||
@staticmethod
|
||||
def _packages_get_select_statuses(connection: Connection, repository_id: RepositoryId) -> dict[str, BuildStatus]:
|
||||
"""
|
||||
select package build statuses from the table
|
||||
|
||||
@ -299,26 +302,35 @@ class PackageOperations(Operations):
|
||||
|
||||
return self.with_connection(lambda connection: list(run(connection)))
|
||||
|
||||
def remote_update(self, package: Package) -> None:
|
||||
def remote_update(self, package: Package, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
update package remote source
|
||||
|
||||
Args:
|
||||
package(Package): package properties
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
return self.with_connection(
|
||||
lambda connection: self._package_update_insert_base(connection, package, self._repository_id),
|
||||
commit=True)
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def remotes_get(self) -> dict[str, RemoteSource]:
|
||||
def run(connection: Connection) -> None:
|
||||
self._package_update_insert_base(connection, package, repository_id)
|
||||
|
||||
return self.with_connection(run, commit=True)
|
||||
|
||||
def remotes_get(self, repository_id: RepositoryId | None = None) -> dict[str, RemoteSource]:
|
||||
"""
|
||||
get packages remotes based on current settings
|
||||
|
||||
Args:
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
|
||||
Returns:
|
||||
dict[str, RemoteSource]: map of package base to its remote sources
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> dict[str, Package]:
|
||||
return self._packages_get_select_package_bases(connection, self._repository_id)
|
||||
return self._packages_get_select_package_bases(connection, repository_id)
|
||||
|
||||
return {
|
||||
package_base: package.remote
|
||||
|
@ -18,7 +18,6 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from collections import defaultdict
|
||||
|
||||
from sqlite3 import Connection
|
||||
|
||||
from ahriman.core.database.operations import Operations
|
||||
@ -42,16 +41,16 @@ class PatchOperations(Operations):
|
||||
"""
|
||||
return self.patches_list(package_base, None).get(package_base, [])
|
||||
|
||||
def patches_insert(self, package_base: str, patch: PkgbuildPatch) -> None:
|
||||
def patches_insert(self, package_base: str, patches: list[PkgbuildPatch]) -> None:
|
||||
"""
|
||||
insert or update patch in database
|
||||
|
||||
Args:
|
||||
package_base(str): package base to insert
|
||||
patch(PkgbuildPatch): patch content
|
||||
patches(list[PkgbuildPatch]): patch content
|
||||
"""
|
||||
def run(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
connection.executemany(
|
||||
"""
|
||||
insert into patches
|
||||
(package_base, variable, patch)
|
||||
@ -60,7 +59,14 @@ class PatchOperations(Operations):
|
||||
on conflict (package_base, coalesce(variable, '')) do update set
|
||||
patch = :patch
|
||||
""",
|
||||
{"package_base": package_base, "variable": patch.key, "patch": patch.value})
|
||||
[
|
||||
{
|
||||
"package_base": package_base,
|
||||
"variable": patch.key,
|
||||
"patch": patch.value,
|
||||
} for patch in patches
|
||||
]
|
||||
)
|
||||
|
||||
return self.with_connection(run, commit=True)
|
||||
|
||||
@ -89,7 +95,7 @@ class PatchOperations(Operations):
|
||||
if variables is not None and patch.key not in variables:
|
||||
continue
|
||||
patches[package].append(patch)
|
||||
return dict(patches)
|
||||
return patches
|
||||
|
||||
def patches_remove(self, package_base: str, variables: list[str] | None) -> None:
|
||||
"""
|
||||
@ -102,12 +108,21 @@ class PatchOperations(Operations):
|
||||
def run_many(connection: Connection) -> None:
|
||||
patches = variables or [] # suppress mypy warning
|
||||
connection.executemany(
|
||||
"""delete from patches where package_base = :package_base and variable = :variable""",
|
||||
[{"package_base": package_base, "variable": variable} for variable in patches])
|
||||
"""
|
||||
delete from patches where package_base = :package_base and variable = :variable
|
||||
""",
|
||||
[
|
||||
{
|
||||
"package_base": package_base,
|
||||
"variable": variable,
|
||||
} for variable in patches
|
||||
])
|
||||
|
||||
def run(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
"""delete from patches where package_base = :package_base""",
|
||||
"""
|
||||
delete from patches where package_base = :package_base
|
||||
""",
|
||||
{"package_base": package_base})
|
||||
|
||||
if variables is not None:
|
||||
|
@ -35,7 +35,7 @@ class SQLite(AuthOperations, BuildOperations, LogsOperations, PackageOperations,
|
||||
|
||||
Examples:
|
||||
Database wrapper must be used together with migration and SQLite3 setup. In order to perform it there is
|
||||
``load`` class method::
|
||||
:func:`load()` class method::
|
||||
|
||||
>>> from ahriman.core.configuration import Configuration
|
||||
>>>
|
||||
|
@ -62,7 +62,7 @@ class BuildError(RuntimeError):
|
||||
|
||||
class CalledProcessError(subprocess.CalledProcessError):
|
||||
"""
|
||||
like ``subprocess.CalledProcessError``, but better
|
||||
like :exc:`subprocess.CalledProcessError`, but better
|
||||
"""
|
||||
|
||||
def __init__(self, status_code: int, process: list[str], stderr: str) -> None:
|
||||
@ -316,21 +316,6 @@ class UnknownPackageError(ValueError):
|
||||
ValueError.__init__(self, f"Package base {package_base} is unknown")
|
||||
|
||||
|
||||
class UnprocessedPackageStatusError(ValueError):
|
||||
"""
|
||||
exception for merging invalid statues
|
||||
"""
|
||||
|
||||
def __init__(self, package_base: str) -> None:
|
||||
"""
|
||||
default constructor
|
||||
|
||||
Args:
|
||||
package_base(str): package base name
|
||||
"""
|
||||
ValueError.__init__(self, f"Package base {package_base} had status failed, but new status is success")
|
||||
|
||||
|
||||
class UnsafeRunError(RuntimeError):
|
||||
"""
|
||||
exception which will be raised in case if user is not owner of repository
|
||||
|
@ -57,6 +57,7 @@ class Printer:
|
||||
"""
|
||||
return []
|
||||
|
||||
# pylint: disable=redundant-returns-doc
|
||||
def title(self) -> str | None:
|
||||
"""
|
||||
generate entry title from content
|
||||
@ -68,7 +69,7 @@ class Printer:
|
||||
|
||||
def __call__(self, *, verbose: bool, log_fn: Callable[[str], None] = _print, separator: str = ": ") -> None:
|
||||
"""
|
||||
print content. Shortcut for ``Printer.print``
|
||||
print content. Shortcut for :func:`print()`
|
||||
|
||||
Args:
|
||||
verbose(bool): print all fields
|
||||
|
@ -67,7 +67,7 @@ class FilteredAccessLogger(AccessLogger):
|
||||
Args:
|
||||
request(BaseRequest): http reqeust descriptor
|
||||
response(StreamResponse): streaming response object
|
||||
time(float):
|
||||
time(float): log record timestamp
|
||||
"""
|
||||
if self.is_logs_post(request) \
|
||||
or self.is_process_get(request):
|
||||
|
@ -27,8 +27,8 @@ from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
class HttpLogHandler(logging.Handler):
|
||||
"""
|
||||
handler for the http logging. Because default ``logging.handlers.HTTPHandler`` does not support cookies
|
||||
authorization, we have to implement own handler which overrides the ``logging.handlers.HTTPHandler.emit`` method
|
||||
handler for the http logging. Because default :class:`logging.handlers.HTTPHandler` does not support cookies
|
||||
authorization, we have to implement own handler which overrides the :func:`logging.handlers.HTTPHandler.emit` method
|
||||
|
||||
Attributes:
|
||||
reporter(Client): build status reporter instance
|
||||
@ -72,7 +72,9 @@ class HttpLogHandler(logging.Handler):
|
||||
if (handler := next((handler for handler in root.handlers if isinstance(handler, cls)), None)) is not None:
|
||||
return handler # there is already registered instance
|
||||
|
||||
suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False)
|
||||
suppress_errors = configuration.getboolean( # read old-style first and then fallback to new style
|
||||
"settings", "suppress_http_log_errors",
|
||||
fallback=configuration.getboolean("status", "suppress_http_log_errors", fallback=False))
|
||||
handler = cls(repository_id, configuration, report=report, suppress_errors=suppress_errors)
|
||||
root.addHandler(handler)
|
||||
|
||||
|
@ -46,9 +46,11 @@ class LogLoader:
|
||||
def handler(selected: LogHandler | None) -> LogHandler:
|
||||
"""
|
||||
try to guess default log handler. In case if ``selected`` is set, it will return specified value with appended
|
||||
_handler suffix. Otherwise, it will try to import journald handler and returns ``journald_handler`` if library
|
||||
is available. Otherwise, it will check if there is ``/dev/log`` device and returns ``syslog_handler`` in this
|
||||
case. And, finally, it will fall back to ``console_handler`` if none were found
|
||||
_handler suffix. Otherwise, it will try to import journald handler and returns
|
||||
:attr:`ahriman.models.log_handler.LogHandler.Journald` if library is available. Otherwise, it will check if
|
||||
there is ``/dev/log`` device and returns :attr:`ahriman.models.log_handler.LogHandler.Syslog` in this
|
||||
case. And, finally, it will fall back to :attr:`ahriman.models.log_handler.LogHandler.Console`
|
||||
if none were found
|
||||
|
||||
Args:
|
||||
selected(LogHandler | None): user specified handler if any
|
||||
|
@ -120,6 +120,6 @@ class Email(Report, JinjaTemplate):
|
||||
text = self.make_html(result, self.template)
|
||||
attachments = {}
|
||||
if self.template_full is not None:
|
||||
attachments["index.html"] = self.make_html(Result(success=packages), self.template_full)
|
||||
attachments["index.html"] = self.make_html(Result(updated=packages), self.template_full)
|
||||
|
||||
self._send(text, attachments)
|
||||
|
@ -58,5 +58,5 @@ class HTML(Report, JinjaTemplate):
|
||||
packages(list[Package]): list of packages to generate report
|
||||
result(Result): build result
|
||||
"""
|
||||
html = self.make_html(Result(success=packages), self.template)
|
||||
html = self.make_html(Result(updated=packages), self.template)
|
||||
self.report_path.write_text(html, encoding="utf8")
|
||||
|
@ -37,14 +37,14 @@ class Report(LazyLogging):
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
|
||||
Examples:
|
||||
``Report`` classes provide several method in order to operate with the report generation and additional class
|
||||
method ``load`` which can be used in order to determine right report instance::
|
||||
:class:`Report` subclasses provide several method in order to operate with the report generation and additional
|
||||
class method :func:`load()` which can be used in order to determine right report instance::
|
||||
|
||||
>>> configuration = Configuration()
|
||||
>>> report = Report.load(RepositoryId("x86_64", "aur-clone"), configuration, "email")
|
||||
|
||||
The ``generate`` method can be used in order to perform the report itself, whereas ``run`` method handles
|
||||
exception and raises ``ReportFailed`` instead::
|
||||
The :func:`generate()` method can be used in order to perform the report itself, whereas :func:`run()` method
|
||||
handles exception and raises :exc:`ahriman.core.exceptions.ReportError` instead::
|
||||
|
||||
>>> try:
|
||||
>>> report.generate([], Result())
|
||||
|
@ -83,7 +83,7 @@ class Executor(Cleaner):
|
||||
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)
|
||||
built = task.build(local_path, PACKAGER=packager_id)
|
||||
for src in built:
|
||||
dst = self.paths.packages / src.name
|
||||
shutil.move(src, dst)
|
||||
@ -98,7 +98,7 @@ class Executor(Cleaner):
|
||||
try:
|
||||
packager = self.packager(packagers, single.base)
|
||||
build_single(single, Path(dir_name), packager.packager_id)
|
||||
result.add_success(single)
|
||||
result.add_updated(single)
|
||||
except Exception:
|
||||
self.reporter.set_failed(single.base)
|
||||
result.add_failed(single)
|
||||
@ -106,7 +106,7 @@ class Executor(Cleaner):
|
||||
|
||||
return result
|
||||
|
||||
def process_remove(self, packages: Iterable[str]) -> Path:
|
||||
def process_remove(self, packages: Iterable[str]) -> Result:
|
||||
"""
|
||||
remove packages from list
|
||||
|
||||
@ -114,7 +114,7 @@ class Executor(Cleaner):
|
||||
packages(Iterable[str]): list of package names or bases to remove
|
||||
|
||||
Returns:
|
||||
Path: path to repository database
|
||||
Result: remove result
|
||||
"""
|
||||
def remove_base(package_base: str) -> None:
|
||||
try:
|
||||
@ -126,9 +126,9 @@ class Executor(Cleaner):
|
||||
except Exception:
|
||||
self.logger.exception("could not remove base %s", package_base)
|
||||
|
||||
def remove_package(package: str, fn: Path) -> None:
|
||||
def remove_package(package: str, archive_path: Path) -> None:
|
||||
try:
|
||||
self.repo.remove(package, fn) # remove the package itself
|
||||
self.repo.remove(package, archive_path) # remove the package itself
|
||||
except Exception:
|
||||
self.logger.exception("could not remove %s", package)
|
||||
|
||||
@ -136,6 +136,7 @@ class Executor(Cleaner):
|
||||
bases_to_remove: list[str] = []
|
||||
|
||||
# build package list based on user input
|
||||
result = Result()
|
||||
requested = set(packages)
|
||||
for local in self.packages():
|
||||
if local.base in packages or all(package in requested for package in local.packages):
|
||||
@ -145,6 +146,7 @@ class Executor(Cleaner):
|
||||
if properties.filepath is not None
|
||||
})
|
||||
bases_to_remove.append(local.base)
|
||||
result.add_removed(local)
|
||||
elif requested.intersection(local.packages.keys()):
|
||||
packages_to_remove.update({
|
||||
package: properties.filepath
|
||||
@ -167,7 +169,7 @@ class Executor(Cleaner):
|
||||
for package in bases_to_remove:
|
||||
remove_base(package)
|
||||
|
||||
return self.repo.repo_path
|
||||
return result
|
||||
|
||||
def process_update(self, packages: Iterable[Path], packagers: Packagers | None = None) -> Result:
|
||||
"""
|
||||
@ -219,7 +221,7 @@ class Executor(Cleaner):
|
||||
rename(description, local.base)
|
||||
update_single(description.filename, local.base, packager.key)
|
||||
self.reporter.set_success(local)
|
||||
result.add_success(local)
|
||||
result.add_updated(local)
|
||||
|
||||
current_package_archives: set[str] = set()
|
||||
if local.base in current_packages:
|
||||
|
@ -123,11 +123,11 @@ class UpdateHandler(Cleaner):
|
||||
|
||||
local = packages.get(remote.base)
|
||||
if local is None:
|
||||
self.reporter.set_unknown(remote)
|
||||
result.append(remote)
|
||||
elif local.is_outdated(remote, self.paths,
|
||||
vcs_allowed_age=self.vcs_allowed_age,
|
||||
calculate_version=vcs):
|
||||
continue # we don't add packages automatically
|
||||
|
||||
if local.is_outdated(remote, self.paths,
|
||||
vcs_allowed_age=self.vcs_allowed_age,
|
||||
calculate_version=vcs):
|
||||
self.reporter.set_pending(local.base)
|
||||
result.append(remote)
|
||||
except Exception:
|
||||
|
@ -36,8 +36,6 @@ class GPG(SyncHttpClient):
|
||||
targets(set[SignSettings]): list of targets to sign (repository, package etc.)
|
||||
"""
|
||||
|
||||
_check_output = check_output
|
||||
|
||||
def __init__(self, configuration: Configuration) -> None:
|
||||
"""
|
||||
default constructor
|
||||
@ -140,7 +138,7 @@ class GPG(SyncHttpClient):
|
||||
Returns:
|
||||
str: PGP key in .asc format
|
||||
"""
|
||||
return GPG._check_output("gpg", "--armor", "--no-emit-version", "--export", key, logger=self.logger)
|
||||
return check_output("gpg", "--armor", "--no-emit-version", "--export", key, logger=self.logger)
|
||||
|
||||
def key_fingerprint(self, key: str) -> str:
|
||||
"""
|
||||
@ -152,7 +150,7 @@ class GPG(SyncHttpClient):
|
||||
Returns:
|
||||
str: full PGP key fingerprint
|
||||
"""
|
||||
metadata = GPG._check_output("gpg", "--with-colons", "--fingerprint", key, logger=self.logger)
|
||||
metadata = check_output("gpg", "--with-colons", "--fingerprint", key, logger=self.logger)
|
||||
# fingerprint line will be like
|
||||
# fpr:::::::::43A663569A07EE1E4ECC55CC7E3A4240CE3C45C2:
|
||||
fingerprint = next(filter(lambda line: line[:3] == "fpr", metadata.splitlines()))
|
||||
@ -167,7 +165,7 @@ class GPG(SyncHttpClient):
|
||||
key(str): key ID to import
|
||||
"""
|
||||
key_body = self.key_download(server, key)
|
||||
GPG._check_output("gpg", "--import", input_data=key_body, logger=self.logger)
|
||||
check_output("gpg", "--import", input_data=key_body, logger=self.logger)
|
||||
|
||||
def process(self, path: Path, key: str) -> list[Path]:
|
||||
"""
|
||||
@ -180,7 +178,7 @@ class GPG(SyncHttpClient):
|
||||
Returns:
|
||||
list[Path]: list of generated files including original file
|
||||
"""
|
||||
GPG._check_output(
|
||||
check_output(
|
||||
*GPG.sign_command(path, key),
|
||||
exception=BuildError.from_process(path.name),
|
||||
logger=self.logger)
|
||||
@ -215,7 +213,7 @@ class GPG(SyncHttpClient):
|
||||
sign repository if required by configuration
|
||||
|
||||
Notes:
|
||||
More likely you just want to pass ``repository_sign_args`` to repo wrapper
|
||||
More likely you just want to pass :attr:`repository_sign_args` to repo wrapper
|
||||
|
||||
Args:
|
||||
path(Path): path to repository database
|
||||
|
@ -28,6 +28,7 @@ from multiprocessing import Process, Queue
|
||||
from threading import Lock, Thread
|
||||
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.process_status import ProcessStatus
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
@ -82,7 +83,8 @@ class Spawn(Thread, LazyLogging):
|
||||
helper to run external process
|
||||
|
||||
Args:
|
||||
callback(Callable[[argparse.Namespace, str], bool]): application run function (i.e. ``Handler.call`` method)
|
||||
callback(Callable[[argparse.Namespace, str], bool]): application run function
|
||||
(i.e. :func:`ahriman.application.handlers.handler.Handler.call()` method)
|
||||
args(argparse.Namespace): command line arguments
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
process_id(str): process unique identifier
|
||||
@ -96,7 +98,8 @@ class Spawn(Thread, LazyLogging):
|
||||
|
||||
queue.put(ProcessStatus(process_id, result, consumed_time))
|
||||
|
||||
def _spawn_process(self, repository_id: RepositoryId, command: str, *args: str, **kwargs: str | None) -> str:
|
||||
def _spawn_process(self, repository_id: RepositoryId, command: str, *args: str,
|
||||
**kwargs: str | list[str] | None) -> str:
|
||||
"""
|
||||
spawn external ahriman process with supplied arguments
|
||||
|
||||
@ -104,7 +107,7 @@ class Spawn(Thread, LazyLogging):
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
command(str): subcommand to run
|
||||
*args(str): positional command arguments
|
||||
**kwargs(str): named command arguments
|
||||
**kwargs(str | list[str] | None): named command arguments
|
||||
|
||||
Returns:
|
||||
str: spawned process identifier
|
||||
@ -118,9 +121,13 @@ class Spawn(Thread, LazyLogging):
|
||||
for argument, value in kwargs.items():
|
||||
if value is None:
|
||||
continue # skip null values
|
||||
arguments.append(f"--{argument}")
|
||||
if value:
|
||||
arguments.append(value)
|
||||
flag = f"--{argument}"
|
||||
if isinstance(value, list):
|
||||
arguments.extend(list(sum(((flag, v) for v in value), ())))
|
||||
elif value:
|
||||
arguments.extend([flag, value])
|
||||
else:
|
||||
arguments.append(flag) # boolean argument
|
||||
|
||||
process_id = str(uuid.uuid4())
|
||||
self.logger.info("full command line arguments of %s are %s using repository %s",
|
||||
@ -143,7 +150,7 @@ class Spawn(Thread, LazyLogging):
|
||||
check if given process is alive
|
||||
|
||||
Args:
|
||||
process_id(str): process id to be checked as returned by ``Spawn._spawn_process``
|
||||
process_id(str): process id to be checked as returned by :func:`_spawn_process()`
|
||||
|
||||
Returns:
|
||||
bool: True in case if process still counts as active and False otherwise
|
||||
@ -167,7 +174,7 @@ class Spawn(Thread, LazyLogging):
|
||||
return self._spawn_process(repository_id, "service-key-import", key, **kwargs)
|
||||
|
||||
def packages_add(self, repository_id: RepositoryId, packages: Iterable[str], username: str | None, *,
|
||||
now: bool) -> str:
|
||||
patches: list[PkgbuildPatch], now: bool) -> str:
|
||||
"""
|
||||
add packages
|
||||
|
||||
@ -175,14 +182,18 @@ class Spawn(Thread, LazyLogging):
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
packages(Iterable[str]): packages list to add
|
||||
username(str | None): optional override of username for build process
|
||||
patches(list[PkgbuildPatch]): list of patches to be passed
|
||||
now(bool): build packages now
|
||||
|
||||
Returns:
|
||||
str: spawned process identifier
|
||||
"""
|
||||
kwargs = {"username": username}
|
||||
kwargs: dict[str, str | list[str] | None] = {"username": username}
|
||||
if now:
|
||||
kwargs["now"] = ""
|
||||
if patches:
|
||||
kwargs["variable"] = [patch.serialize() for patch in patches]
|
||||
|
||||
return self._spawn_process(repository_id, "package-add", *packages, **kwargs)
|
||||
|
||||
def packages_rebuild(self, repository_id: RepositoryId, depends_on: str, username: str | None) -> str:
|
||||
|
@ -49,16 +49,19 @@ class Client:
|
||||
"""
|
||||
if not report:
|
||||
return Client()
|
||||
if not configuration.getboolean("status", "enabled", fallback=True): # global switch
|
||||
return Client()
|
||||
|
||||
address = configuration.get("web", "address", fallback=None)
|
||||
# new-style section
|
||||
address = configuration.get("status", "address", fallback=None)
|
||||
# old-style section
|
||||
legacy_address = configuration.get("web", "address", fallback=None)
|
||||
host = configuration.get("web", "host", fallback=None)
|
||||
port = configuration.getint("web", "port", fallback=None)
|
||||
socket = configuration.get("web", "unix_socket", fallback=None)
|
||||
|
||||
# basically we just check if there is something we can use for interaction with remote server
|
||||
# at the moment (end of 2022) I think it would be much better idea to introduce flag like `enabled`,
|
||||
# but it will totally break used experience
|
||||
if address or (host and port) or socket:
|
||||
if address or legacy_address or (host and port) or socket:
|
||||
from ahriman.core.status.web_client import WebClient
|
||||
return WebClient(repository_id, configuration)
|
||||
return Client()
|
||||
@ -104,7 +107,7 @@ class Client:
|
||||
|
||||
def package_update(self, package_base: str, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
update package build status. Unlike ``add`` it does not update package properties
|
||||
update package build status. Unlike :func:`package_add()` it does not update package properties
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
|
@ -23,6 +23,7 @@ from ahriman.core.log import LazyLogging
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
@ -32,8 +33,8 @@ class Watcher(LazyLogging):
|
||||
|
||||
Attributes:
|
||||
database(SQLite): database instance
|
||||
known(dict[str, tuple[Package, BuildStatus]]): list of known packages. For the most cases ``packages`` should
|
||||
be used instead
|
||||
known(dict[str, tuple[Package, BuildStatus]]): list of known packages. For the most cases :attr:`packages`
|
||||
should be used instead
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
status(BuildStatus): daemon status
|
||||
"""
|
||||
@ -162,6 +163,40 @@ class Watcher(LazyLogging):
|
||||
self.known[package_base] = (package, full_status)
|
||||
self.database.package_update(package, full_status, self.repository_id)
|
||||
|
||||
def patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
|
||||
"""
|
||||
get patches for the package
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
variable(str | None): patch variable name if any
|
||||
|
||||
Returns:
|
||||
list[PkgbuildPatch]: list of patches which are stored for the package
|
||||
"""
|
||||
variables = [variable] if variable is not None else None
|
||||
return self.database.patches_list(package_base, variables).get(package_base, [])
|
||||
|
||||
def patches_remove(self, package_base: str, variable: str) -> None:
|
||||
"""
|
||||
remove package patch
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
variable(str): patch variable name
|
||||
"""
|
||||
self.database.patches_remove(package_base, [variable])
|
||||
|
||||
def patches_update(self, package_base: str, patch: PkgbuildPatch) -> None:
|
||||
"""
|
||||
update package patch
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
patch(PkgbuildPatch): package patch
|
||||
"""
|
||||
self.database.patches_insert(package_base, [patch])
|
||||
|
||||
def status_update(self, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
update service status
|
||||
|
@ -22,7 +22,7 @@ import logging
|
||||
import requests
|
||||
|
||||
from functools import cached_property
|
||||
from urllib.parse import quote_plus as urlencode
|
||||
from urllib.parse import quote_plus as urlencode, urlparse
|
||||
|
||||
from ahriman import __version__
|
||||
from ahriman.core.configuration import Configuration
|
||||
@ -42,7 +42,6 @@ class WebClient(Client, SyncHttpClient):
|
||||
Attributes:
|
||||
address(str): address of the web service
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
use_unix_socket(bool): use websocket or not
|
||||
"""
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
@ -53,11 +52,13 @@ class WebClient(Client, SyncHttpClient):
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False)
|
||||
SyncHttpClient.__init__(self, configuration, "web", suppress_errors=suppress_errors)
|
||||
section, self.address = self.parse_address(configuration)
|
||||
suppress_errors = configuration.getboolean( # read old-style first and then fallback to new style
|
||||
"settings", "suppress_http_log_errors",
|
||||
fallback=configuration.getboolean("status", "suppress_http_log_errors", fallback=False))
|
||||
SyncHttpClient.__init__(self, configuration, section, suppress_errors=suppress_errors)
|
||||
|
||||
self.repository_id = repository_id
|
||||
self.address, self.use_unix_socket = self.parse_address(configuration)
|
||||
|
||||
@cached_property
|
||||
def session(self) -> requests.Session:
|
||||
@ -67,41 +68,7 @@ class WebClient(Client, SyncHttpClient):
|
||||
Returns:
|
||||
request.Session: created session object
|
||||
"""
|
||||
return self._create_session(use_unix_socket=self.use_unix_socket)
|
||||
|
||||
@staticmethod
|
||||
def parse_address(configuration: Configuration) -> tuple[str, bool]:
|
||||
"""
|
||||
parse address from configuration
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
tuple[str, bool]: tuple of server address and socket flag (True in case if unix socket must be used)
|
||||
"""
|
||||
if (unix_socket := configuration.get("web", "unix_socket", fallback=None)) is not None:
|
||||
# special pseudo-protocol which is used for unix sockets
|
||||
return f"http+unix://{urlencode(unix_socket)}", True
|
||||
address = configuration.get("web", "address", fallback=None)
|
||||
if not address:
|
||||
# build address from host and port directly
|
||||
host = configuration.get("web", "host")
|
||||
port = configuration.getint("web", "port")
|
||||
address = f"http://{host}:{port}"
|
||||
return address, False
|
||||
|
||||
def _create_session(self, *, use_unix_socket: bool) -> requests.Session:
|
||||
"""
|
||||
generate new request session
|
||||
|
||||
Args:
|
||||
use_unix_socket(bool): if set to True then unix socket session will be generated instead of native requests
|
||||
|
||||
Returns:
|
||||
requests.Session: generated session object
|
||||
"""
|
||||
if use_unix_socket:
|
||||
if urlparse(self.address).scheme == "http+unix":
|
||||
import requests_unixsocket # type: ignore[import-untyped]
|
||||
session: requests.Session = requests_unixsocket.Session()
|
||||
session.headers["User-Agent"] = f"ahriman/{__version__}"
|
||||
@ -113,6 +80,33 @@ class WebClient(Client, SyncHttpClient):
|
||||
|
||||
return session
|
||||
|
||||
@staticmethod
|
||||
def parse_address(configuration: Configuration) -> tuple[str, str]:
|
||||
"""
|
||||
parse address from legacy configuration
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: tuple of section name and server address
|
||||
"""
|
||||
# new-style section
|
||||
if (address := configuration.get("status", "address", fallback=None)) is not None:
|
||||
return "status", address
|
||||
|
||||
# legacy-style section
|
||||
if (unix_socket := configuration.get("web", "unix_socket", fallback=None)) is not None:
|
||||
# special pseudo-protocol which is used for unix sockets
|
||||
return "web", f"http+unix://{urlencode(unix_socket)}"
|
||||
address = configuration.get("web", "address", fallback=None)
|
||||
if not address:
|
||||
# build address from host and port directly
|
||||
host = configuration.get("web", "host")
|
||||
port = configuration.getint("web", "port")
|
||||
address = f"http://{host}:{port}"
|
||||
return "web", address
|
||||
|
||||
def _login(self, session: requests.Session) -> None:
|
||||
"""
|
||||
process login to the service
|
||||
@ -245,7 +239,7 @@ class WebClient(Client, SyncHttpClient):
|
||||
|
||||
def package_update(self, package_base: str, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
update package build status. Unlike ``add`` it does not update package properties
|
||||
update package build status. Unlike :func:`package_add()` it does not update package properties
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
|
@ -182,7 +182,7 @@ class Tree:
|
||||
|
||||
Returns:
|
||||
list[list[Package]]: list of packages lists based on their dependencies. The amount of elements in each
|
||||
sublist is less or equal to ``count``
|
||||
sublist is less or equal to ``count``
|
||||
|
||||
Raises:
|
||||
PartitionError: in case if it is impossible to divide tree by specified amount of partitions
|
||||
|
@ -65,6 +65,14 @@ class TriggerLoader(LazyLogging):
|
||||
self._on_stop_requested = False
|
||||
self.triggers: list[Trigger] = []
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""
|
||||
custom destructor object which calls on_stop in case if it was requested
|
||||
"""
|
||||
if not self._on_stop_requested:
|
||||
return
|
||||
self.on_stop()
|
||||
|
||||
@classmethod
|
||||
def load(cls, repository_id: RepositoryId, configuration: Configuration) -> Self:
|
||||
"""
|
||||
@ -88,8 +96,8 @@ class TriggerLoader(LazyLogging):
|
||||
@staticmethod
|
||||
def known_triggers(configuration: Configuration) -> list[str]:
|
||||
"""
|
||||
read configuration and return list of known triggers. Unlike ``selected_triggers`` this option is used mainly
|
||||
for configuration and validation and mentioned triggers are not being executed automatically
|
||||
read configuration and return list of known triggers. Unlike :func:`selected_triggers()` this option is used
|
||||
mainly for configuration and validation and mentioned triggers are not being executed automatically
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
@ -141,7 +149,7 @@ class TriggerLoader(LazyLogging):
|
||||
"""
|
||||
self.logger.info("load module %s from path %s", implementation, module_path)
|
||||
# basically this method is called only if ``module_path`` exists and is file.
|
||||
# Thus, this method should never throw ``FileNotFoundError`` exception
|
||||
# Thus, this method should never throw :exc:`FileNotFoundError` exception
|
||||
loader = machinery.SourceFileLoader(implementation, module_path)
|
||||
module = ModuleType(loader.name)
|
||||
loader.exec_module(module)
|
||||
@ -257,11 +265,3 @@ class TriggerLoader(LazyLogging):
|
||||
for trigger in self.triggers:
|
||||
with self.__execute_trigger(trigger):
|
||||
trigger.on_stop()
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""
|
||||
custom destructor object which calls on_stop in case if it was requested
|
||||
"""
|
||||
if not self._on_stop_requested:
|
||||
return
|
||||
self.on_stop()
|
||||
|
@ -95,21 +95,6 @@ class GitHub(Upload, HttpUpload):
|
||||
with path.open("rb") as archive:
|
||||
self.make_request("POST", url, params=[("name", path.name)], data=archive, headers=headers)
|
||||
|
||||
def get_local_files(self, path: Path) -> dict[Path, str]:
|
||||
"""
|
||||
get all local files and their calculated checksums
|
||||
|
||||
Args:
|
||||
path(Path): local path to sync
|
||||
|
||||
Returns:
|
||||
dict[Path, str]: map of path objects to its checksum
|
||||
"""
|
||||
return {
|
||||
local_file: self.calculate_hash(local_file)
|
||||
for local_file in walk(path)
|
||||
}
|
||||
|
||||
def files_remove(self, release: dict[str, Any], local_files: dict[Path, str], remote_files: dict[str, str]) -> None:
|
||||
"""
|
||||
remove files from GitHub
|
||||
@ -140,6 +125,21 @@ class GitHub(Upload, HttpUpload):
|
||||
continue
|
||||
self.asset_upload(release, local_file)
|
||||
|
||||
def get_local_files(self, path: Path) -> dict[Path, str]:
|
||||
"""
|
||||
get all local files and their calculated checksums
|
||||
|
||||
Args:
|
||||
path(Path): local path to sync
|
||||
|
||||
Returns:
|
||||
dict[Path, str]: map of path objects to its checksum
|
||||
"""
|
||||
return {
|
||||
local_file: self.calculate_hash(local_file)
|
||||
for local_file in walk(path)
|
||||
}
|
||||
|
||||
def release_create(self) -> dict[str, Any]:
|
||||
"""
|
||||
create empty release
|
||||
|
@ -35,8 +35,6 @@ class Rsync(Upload):
|
||||
remote(str): remote address to sync
|
||||
"""
|
||||
|
||||
_check_output = check_output
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None:
|
||||
"""
|
||||
default constructor
|
||||
@ -58,4 +56,4 @@ class Rsync(Upload):
|
||||
path(Path): local path to sync
|
||||
built_packages(list[Package]): list of packages which has just been built
|
||||
"""
|
||||
Rsync._check_output(*self.command, str(path), self.remote, logger=self.logger)
|
||||
check_output(*self.command, str(path), self.remote, logger=self.logger)
|
||||
|
@ -87,6 +87,20 @@ class S3(Upload):
|
||||
suffix = f"-{len(md5s)}" if len(md5s) > 1 else ""
|
||||
return f"{checksum.hexdigest()}{suffix}"
|
||||
|
||||
@staticmethod
|
||||
def files_remove(local_files: dict[Path, str], remote_objects: dict[Path, Any]) -> None:
|
||||
"""
|
||||
remove files which have been removed locally
|
||||
|
||||
Args:
|
||||
local_files(dict[Path, str]): map of local path object to its checksum
|
||||
remote_objects(dict[Path, Any]): map of remote path object to the remote s3 object
|
||||
"""
|
||||
for local_file, remote_object in remote_objects.items():
|
||||
if local_file in local_files:
|
||||
continue
|
||||
remote_object.delete()
|
||||
|
||||
@staticmethod
|
||||
def get_bucket(configuration: Configuration, section: str) -> Any:
|
||||
"""
|
||||
@ -105,20 +119,6 @@ class S3(Upload):
|
||||
aws_secret_access_key=configuration.get(section, "secret_key"))
|
||||
return client.Bucket(configuration.get(section, "bucket"))
|
||||
|
||||
@staticmethod
|
||||
def files_remove(local_files: dict[Path, str], remote_objects: dict[Path, Any]) -> None:
|
||||
"""
|
||||
remove files which have been removed locally
|
||||
|
||||
Args:
|
||||
local_files(dict[Path, str]): map of local path object to its checksum
|
||||
remote_objects(dict[Path, Any]): map of remote path object to the remote s3 object
|
||||
"""
|
||||
for local_file, remote_object in remote_objects.items():
|
||||
if local_file in local_files:
|
||||
continue
|
||||
remote_object.delete()
|
||||
|
||||
def files_upload(self, path: Path, local_files: dict[Path, str], remote_objects: dict[Path, Any]) -> None:
|
||||
"""
|
||||
upload changed files to s3
|
||||
|
@ -39,14 +39,14 @@ class Upload(LazyLogging):
|
||||
|
||||
Examples:
|
||||
These classes provide the way to upload packages to remote sources as it is described in their implementations.
|
||||
Basic flow includes class instantiating by using the ``load`` method and then calling the ``run`` method which
|
||||
wraps any internal exceptions into the ``SyncFailed`` exception::
|
||||
Basic flow includes class instantiating by using the :func:`load` method and then calling the :func:`run` method
|
||||
which wraps any internal exceptions into the :exc:`ahriman.core.exceptions.SynchronizationError` exception::
|
||||
|
||||
>>> configuration = Configuration()
|
||||
>>> upload = Upload.load(RepositoryId("x86_64", "aur-clone"), configuration, "s3")
|
||||
>>> upload.run(configuration.repository_paths.repository, [])
|
||||
|
||||
Or in case if direct access to exception is required, the ``sync`` method can be used::
|
||||
Or in case if direct access to exception is required, the :func:`sync` method can be used::
|
||||
|
||||
>>> try:
|
||||
>>> upload.sync(configuration.repository_paths.repository, [])
|
||||
|
@ -56,6 +56,7 @@ __all__ = [
|
||||
"srcinfo_property",
|
||||
"srcinfo_property_list",
|
||||
"trim_package",
|
||||
"unquote",
|
||||
"utcnow",
|
||||
"walk",
|
||||
]
|
||||
@ -75,8 +76,8 @@ def check_output(*args: str, exception: Exception | Callable[[int, list[str], st
|
||||
Args:
|
||||
*args(str): command line arguments
|
||||
exception(Exception | Callable[[int, list[str], str, str]] | None, optional): exception which has to be raised
|
||||
instead of default subprocess exception. If callable us is supplied, the ``subprocess.CalledProcessError``
|
||||
arguments will be passed (Default value = None)
|
||||
instead of default subprocess exception. If callable us is supplied, the
|
||||
:exc:`subprocess.CalledProcessError` arguments will be passed (Default value = None)
|
||||
cwd(Path | None, optional): current working directory (Default value = None)
|
||||
input_data(str | None, optional): data which will be written to command stdin (Default value = None)
|
||||
logger(logging.Logger | None, optional): logger to log command result if required (Default value = None)
|
||||
@ -411,7 +412,7 @@ def srcinfo_property(key: str, srcinfo: dict[str, Any], package_srcinfo: dict[st
|
||||
default: Any = None) -> Any:
|
||||
"""
|
||||
extract property from SRCINFO. This method extracts property from package if this property is presented in
|
||||
``package``. Otherwise, it looks for the same property in root srcinfo. If none found, the default value will be
|
||||
``srcinfo``. Otherwise, it looks for the same property in root srcinfo. If none found, the default value will be
|
||||
returned
|
||||
|
||||
Args:
|
||||
@ -429,9 +430,9 @@ def srcinfo_property(key: str, srcinfo: dict[str, Any], package_srcinfo: dict[st
|
||||
def srcinfo_property_list(key: str, srcinfo: dict[str, Any], package_srcinfo: dict[str, Any], *,
|
||||
architecture: str | None = None) -> list[Any]:
|
||||
"""
|
||||
extract list property from SRCINFO. Unlike ``srcinfo_property`` it supposes that default return value is always
|
||||
empty list. If ``architecture`` is supplied, then it will try to lookup for architecture specific values and will
|
||||
append it at the end of result
|
||||
extract list property from SRCINFO. Unlike :func:`srcinfo_property()` it supposes that default return value is
|
||||
always empty list. If ``architecture`` is supplied, then it will try to lookup for architecture specific values and
|
||||
will append it at the end of result
|
||||
|
||||
Args:
|
||||
key(str): key to extract
|
||||
@ -465,6 +466,38 @@ def trim_package(package_name: str) -> str:
|
||||
return package_name
|
||||
|
||||
|
||||
def unquote(source: str) -> str:
|
||||
"""
|
||||
like :func:`shlex.quote()`, but opposite
|
||||
|
||||
Args:
|
||||
source(str): source string to remove quotes
|
||||
|
||||
Returns:
|
||||
str: string with quotes removed
|
||||
|
||||
Raises:
|
||||
ValueError: if no closing quotation
|
||||
"""
|
||||
def generator() -> Generator[str, None, None]:
|
||||
token = None
|
||||
for char in source:
|
||||
if token is not None:
|
||||
if char == token:
|
||||
token = None # closed quote
|
||||
else:
|
||||
yield char # character inside quotes
|
||||
elif char in ("'", "\""):
|
||||
token = char # first quote found
|
||||
else:
|
||||
yield char # normal character
|
||||
|
||||
if token is not None:
|
||||
raise ValueError("No closing quotation")
|
||||
|
||||
return "".join(generator())
|
||||
|
||||
|
||||
def utcnow() -> datetime.datetime:
|
||||
"""
|
||||
get current time
|
||||
@ -487,7 +520,8 @@ def walk(directory_path: Path) -> Generator[Path, None, None]:
|
||||
Path: all found files in given directory with full path
|
||||
|
||||
Examples:
|
||||
Since the ``pathlib`` module does not provide an alternative to ``os.walk``, this wrapper can be used instead::
|
||||
Since the :mod:`pathlib` module does not provide an alternative to :func:`os.walk()`, this wrapper
|
||||
can be used instead::
|
||||
|
||||
>>> from pathlib import Path
|
||||
>>>
|
||||
|
@ -17,10 +17,10 @@
|
||||
# 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 enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class Action(str, Enum):
|
||||
class Action(StrEnum):
|
||||
"""
|
||||
base action enumeration
|
||||
|
||||
|
@ -59,7 +59,7 @@ class AURPackage:
|
||||
keywords(list[str]): list of package keywords
|
||||
|
||||
Examples:
|
||||
Mainly this class must be used from class methods instead of default ``__init__``::
|
||||
Mainly this class must be used from class methods instead of default :func:`__init__()`::
|
||||
|
||||
>>> package = AURPackage.from_json(metadata) # load package from json dump
|
||||
>>> # ...or alternatively...
|
||||
|
@ -19,10 +19,10 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class AuthSettings(str, Enum):
|
||||
class AuthSettings(StrEnum):
|
||||
"""
|
||||
web authorization type
|
||||
|
||||
|
@ -18,13 +18,13 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from dataclasses import dataclass, field, fields
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
from typing import Any, Self
|
||||
|
||||
from ahriman.core.util import filter_json, pretty_datetime, utcnow
|
||||
|
||||
|
||||
class BuildStatusEnum(str, Enum):
|
||||
class BuildStatusEnum(StrEnum):
|
||||
"""
|
||||
build status enumeration
|
||||
|
||||
|
@ -17,10 +17,10 @@
|
||||
# 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 enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class LogHandler(str, Enum):
|
||||
class LogHandler(StrEnum):
|
||||
"""
|
||||
log handler as described by default configuration
|
||||
|
||||
|
@ -66,11 +66,13 @@ class Package(LazyLogging):
|
||||
it will probably miss file descriptions (in case if there are multiple packages which belong to the base).
|
||||
|
||||
The specific class load method must be defined based on the source provided. The following methods (mostly) must
|
||||
be used: ``from_archive``, ``from_aur``, ``from_build``, ``from_official`` for sources
|
||||
``PackageSource.Archive``, ``PackageSource.AUR``, ``PackageSource.Local`` and ``PackageSource.Repository``
|
||||
repsectively:
|
||||
be used: :func:`from_archive()`, :func:`from_aur()`, :func:`from_build()`, :func:`from_official()` for sources
|
||||
:attr:`ahriman.models.package_source.PackageSource.Archive`,
|
||||
:attr:`ahriman.models.package_source.PackageSource.AUR`,
|
||||
:attr:`ahriman.models.package_source.PackageSource.Local` and
|
||||
:attr:`ahriman.models.package_source.PackageSource.Repository` repsectively:
|
||||
|
||||
>>> ahriman_package = Package.from_aur("ahriman", pacman)
|
||||
>>> ahriman_package = Package.from_aur("ahriman")
|
||||
>>> pacman_package = Package.from_official("pacman", pacman)
|
||||
"""
|
||||
|
||||
@ -80,8 +82,6 @@ class Package(LazyLogging):
|
||||
packages: dict[str, PackageDescription]
|
||||
packager: str | None = None
|
||||
|
||||
_check_output = check_output
|
||||
|
||||
@property
|
||||
def depends(self) -> list[str]:
|
||||
"""
|
||||
@ -259,7 +259,7 @@ class Package(LazyLogging):
|
||||
Raises:
|
||||
PackageInfoError: if there are parsing errors
|
||||
"""
|
||||
srcinfo_source = Package._check_output("makepkg", "--printsrcinfo", cwd=path)
|
||||
srcinfo_source = check_output("makepkg", "--printsrcinfo", cwd=path)
|
||||
srcinfo, errors = parse_srcinfo(srcinfo_source)
|
||||
if errors:
|
||||
raise PackageInfoError(errors)
|
||||
@ -356,14 +356,14 @@ class Package(LazyLogging):
|
||||
Args:
|
||||
path(Path): path to package sources directory
|
||||
|
||||
Returns:
|
||||
Generator[Path, None, None]: list of paths of files which belong to the package and distributed together
|
||||
with this tarball. All paths are relative to the ``path``
|
||||
Yields:
|
||||
Path: list of paths of files which belong to the package and distributed together with this tarball.
|
||||
All paths are relative to the ``path``
|
||||
|
||||
Raises:
|
||||
PackageInfoError: if there are parsing errors
|
||||
"""
|
||||
srcinfo_source = Package._check_output("makepkg", "--printsrcinfo", cwd=path)
|
||||
srcinfo_source = check_output("makepkg", "--printsrcinfo", cwd=path)
|
||||
srcinfo, errors = parse_srcinfo(srcinfo_source)
|
||||
if errors:
|
||||
raise PackageInfoError(errors)
|
||||
@ -400,7 +400,7 @@ class Package(LazyLogging):
|
||||
Raises:
|
||||
PackageInfoError: if there are parsing errors
|
||||
"""
|
||||
srcinfo_source = Package._check_output("makepkg", "--printsrcinfo", cwd=path)
|
||||
srcinfo_source = check_output("makepkg", "--printsrcinfo", cwd=path)
|
||||
srcinfo, errors = parse_srcinfo(srcinfo_source)
|
||||
if errors:
|
||||
raise PackageInfoError(errors)
|
||||
@ -448,11 +448,10 @@ class Package(LazyLogging):
|
||||
|
||||
try:
|
||||
# update pkgver first
|
||||
Package._check_output("makepkg", "--nodeps", "--nobuild",
|
||||
cwd=paths.cache_for(self.base), logger=self.logger)
|
||||
check_output("makepkg", "--nodeps", "--nobuild", cwd=paths.cache_for(self.base), logger=self.logger)
|
||||
# generate new .SRCINFO and put it to parser
|
||||
srcinfo_source = Package._check_output("makepkg", "--printsrcinfo",
|
||||
cwd=paths.cache_for(self.base), logger=self.logger)
|
||||
srcinfo_source = check_output("makepkg", "--printsrcinfo",
|
||||
cwd=paths.cache_for(self.base), logger=self.logger)
|
||||
srcinfo, errors = parse_srcinfo(srcinfo_source)
|
||||
if errors:
|
||||
raise PackageInfoError(errors)
|
||||
@ -543,7 +542,7 @@ class Package(LazyLogging):
|
||||
def next_pkgrel(self, local_version: str) -> str | None:
|
||||
"""
|
||||
generate next pkgrel variable. The package release will be incremented if ``local_version`` is more or equal to
|
||||
the ``Package.version``; in this case the function will return new pkgrel value, otherwise ``None`` will be
|
||||
the :attr:`version`; in this case the function will return new pkgrel value, otherwise ``None`` will be
|
||||
returned
|
||||
|
||||
Args:
|
||||
@ -551,8 +550,8 @@ class Package(LazyLogging):
|
||||
|
||||
Returns:
|
||||
str | None: new generated package release version if any. In case if the release contains dot (e.g. 1.2),
|
||||
the minor part will be incremented by 1. If the release does not contain major.minor notation, the minor version
|
||||
equals to 1 will be appended
|
||||
the minor part will be incremented by 1. If the release does not contain major.minor notation, the minor
|
||||
version equals to 1 will be appended
|
||||
"""
|
||||
epoch, pkgver, _ = parse_version(self.version)
|
||||
local_epoch, local_pkgver, local_pkgrel = parse_version(local_version)
|
||||
|
@ -48,8 +48,9 @@ class PackageDescription:
|
||||
url(str | None): package url
|
||||
|
||||
Examples:
|
||||
Unlike the ``Package`` class, this implementation only holds properties. The recommended way to deal with it is
|
||||
to read data based on the source type - either json or ``pyalpm.Package`` instance::
|
||||
Unlike the :class:`ahriman.models.package.Package` class, this implementation only holds properties.
|
||||
The recommended way to deal with it is to read data based on the source type - either json or
|
||||
:class:`pyalpm.Package` instance::
|
||||
|
||||
>>> description = PackageDescription.from_json(dump)
|
||||
>>>
|
||||
|
@ -19,7 +19,7 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@ -27,7 +27,7 @@ from ahriman.core.util import package_like
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class PackageSource(str, Enum):
|
||||
class PackageSource(StrEnum):
|
||||
"""
|
||||
package source for addition enumeration
|
||||
|
||||
@ -41,11 +41,13 @@ class PackageSource(str, Enum):
|
||||
Repository(PackageSource): (class attribute) source is official repository
|
||||
|
||||
Examples:
|
||||
In case if source is unknown the ``resolve()`` and the source descriptor is available method must be used::
|
||||
In case if source is unknown the :func:`resolve()` and the source
|
||||
descriptor is available method must be used::
|
||||
|
||||
>>> real_source = PackageSource.Auto.resolve("ahriman", configuration.repository_paths)
|
||||
|
||||
the code above will ensure that the presudo-source ``PackageSource.Auto`` will not be processed later.
|
||||
the code above will ensure that the presudo-source :attr:`Auto`
|
||||
will not be processed later.
|
||||
"""
|
||||
|
||||
Auto = "auto"
|
||||
|
@ -17,10 +17,10 @@
|
||||
# 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 enum import Enum
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class PacmanSynchronization(int, Enum):
|
||||
class PacmanSynchronization(IntEnum):
|
||||
"""
|
||||
pacman database synchronization flag
|
||||
|
||||
|
@ -19,8 +19,11 @@
|
||||
#
|
||||
import shlex
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Self
|
||||
|
||||
from ahriman.core.util import dataclass_view, unquote
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -33,12 +36,12 @@ class PkgbuildPatch:
|
||||
considered as full PKGBUILD diffs
|
||||
value(str | list[str]): value of the stored PKGBUILD property. It must be either string or list of string
|
||||
values
|
||||
unsafe(bool): if set, value will be not quoted, might break PKGBUILD
|
||||
"""
|
||||
|
||||
key: str | None
|
||||
value: str | list[str]
|
||||
unsafe: bool = field(default=False, kw_only=True)
|
||||
|
||||
quote = shlex.quote
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""
|
||||
@ -66,17 +69,26 @@ class PkgbuildPatch:
|
||||
"""
|
||||
return self.key is None
|
||||
|
||||
def quote(self, value: str) -> str:
|
||||
@classmethod
|
||||
def from_env(cls, variable: str) -> Self:
|
||||
"""
|
||||
quote value according to the unsafe flag
|
||||
construct patch from environment variable. Functions are not supported
|
||||
|
||||
Args:
|
||||
value(str): value to be quoted
|
||||
variable(str): variable in bash form, i.e. KEY=VALUE
|
||||
|
||||
Returns:
|
||||
str: quoted string in case if unsafe is False and as is otherwise
|
||||
Self: package properties
|
||||
"""
|
||||
return value if self.unsafe else shlex.quote(value)
|
||||
key, *value_parts = variable.split("=", maxsplit=1)
|
||||
|
||||
raw_value = next(iter(value_parts), "") # extract raw value
|
||||
if raw_value.startswith("(") and raw_value.endswith(")"):
|
||||
value: str | list[str] = shlex.split(raw_value[1:-1]) # arrays for poor
|
||||
else:
|
||||
value = unquote(raw_value)
|
||||
|
||||
return cls(key, value)
|
||||
|
||||
def serialize(self) -> str:
|
||||
"""
|
||||
@ -88,14 +100,23 @@ class PkgbuildPatch:
|
||||
str: serialized key-value pair, print-friendly
|
||||
"""
|
||||
if isinstance(self.value, list): # list like
|
||||
value = " ".join(map(self.quote, self.value))
|
||||
value = " ".join(map(PkgbuildPatch.quote, self.value))
|
||||
return f"""{self.key}=({value})"""
|
||||
if self.is_plain_diff: # no additional logic for plain diffs
|
||||
return self.value
|
||||
# we suppose that function values are only supported in string-like values
|
||||
if self.is_function:
|
||||
return f"{self.key} {self.value}" # no quoting enabled here
|
||||
return f"""{self.key}={self.quote(self.value)}"""
|
||||
return f"""{self.key}={PkgbuildPatch.quote(self.value)}"""
|
||||
|
||||
def view(self) -> dict[str, Any]:
|
||||
"""
|
||||
generate json patch view
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: json-friendly dictionary
|
||||
"""
|
||||
return dataclass_view(self)
|
||||
|
||||
def write(self, pkgbuild_path: Path) -> None:
|
||||
"""
|
||||
|
@ -19,10 +19,10 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class ReportSettings(str, Enum):
|
||||
class ReportSettings(StrEnum):
|
||||
"""
|
||||
report targets enumeration
|
||||
|
||||
|
@ -34,6 +34,18 @@ class RepositoryId:
|
||||
architecture: str
|
||||
name: str
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
"""
|
||||
get repository id to be used for databases
|
||||
|
||||
Returns:
|
||||
str: unique id for this repository
|
||||
"""
|
||||
if self.is_empty:
|
||||
return ""
|
||||
return f"{self.architecture}-{self.name}" # basically the same as used for command line
|
||||
|
||||
@property
|
||||
def is_empty(self) -> bool:
|
||||
"""
|
||||
@ -44,16 +56,6 @@ class RepositoryId:
|
||||
"""
|
||||
return not self.architecture or not self.name
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
"""
|
||||
get repository id to be used for databases
|
||||
|
||||
Returns:
|
||||
str: unique id for this repository
|
||||
"""
|
||||
return f"{self.architecture}-{self.name}" # basically the same as used for command line
|
||||
|
||||
def query(self) -> list[tuple[str, str]]:
|
||||
"""
|
||||
generate query parameters
|
||||
|
@ -75,7 +75,7 @@ class RepositoryPaths(LazyLogging):
|
||||
|
||||
Returns:
|
||||
Path: relative path which contains only architecture segment in case if legacy tree is used and repository
|
||||
name and architecture otherwise
|
||||
name and architecture otherwise
|
||||
"""
|
||||
if not self._force_current_tree:
|
||||
if (self._repository_root / self.repository_id.architecture).is_dir():
|
||||
@ -181,7 +181,7 @@ class RepositoryPaths(LazyLogging):
|
||||
|
||||
Returns:
|
||||
set[str]: list of repository names for which there is created tree. Returns empty set in case if repository
|
||||
is loaded in legacy mode
|
||||
is loaded in legacy mode
|
||||
"""
|
||||
# simply walk through the root. In case if there are subdirectories, emit the name
|
||||
def walk(paths: RepositoryPaths) -> Generator[str, None, None]:
|
||||
|
@ -19,28 +19,50 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
from collections.abc import Iterable, Callable
|
||||
from typing import Any, Self
|
||||
|
||||
from ahriman.core.exceptions import UnprocessedPackageStatusError
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class Result:
|
||||
"""
|
||||
build result class holder
|
||||
|
||||
Attributes:
|
||||
STATUS_PRIORITIES(list[str]): (class attribute) list of statues according to their priorities
|
||||
"""
|
||||
|
||||
def __init__(self, success: Iterable[Package] | None = None, failed: Iterable[Package] | None = None) -> None:
|
||||
STATUS_PRIORITIES = [
|
||||
"failed",
|
||||
"removed",
|
||||
"updated",
|
||||
"added",
|
||||
]
|
||||
|
||||
def __init__(self, *, added: Iterable[Package] | None = None, updated: Iterable[Package] | None = None,
|
||||
removed: Iterable[Package] | None = None, failed: Iterable[Package] | None = None) -> None:
|
||||
"""
|
||||
default constructor
|
||||
|
||||
Args:
|
||||
success(Iterable[Package] | None, optional): initial list of successes packages (Default value = None)
|
||||
addded(Iterable[Package] | None, optional): initial list of successfully added packages
|
||||
(Default value = None)
|
||||
updated(Iterable[Package] | None, optional): initial list of successfully updated packages
|
||||
(Default value = None)
|
||||
removed(Iterable[Package] | None, optional): initial list of successfully removed packages
|
||||
(Default value = None)
|
||||
failed(Iterable[Package] | None, optional): initial list of failed packages (Default value = None)
|
||||
"""
|
||||
success = success or []
|
||||
self._success = {package.base: package for package in success}
|
||||
added = added or []
|
||||
self._added = {package.base: package for package in added}
|
||||
|
||||
updated = updated or []
|
||||
self._updated = {package.base: package for package in updated}
|
||||
|
||||
removed = removed or []
|
||||
self._removed = {package.base: package for package in removed}
|
||||
|
||||
failed = failed or []
|
||||
self._failed = {package.base: package for package in failed}
|
||||
|
||||
@ -62,7 +84,17 @@ class Result:
|
||||
Returns:
|
||||
bool: True in case if success list is empty and False otherwise
|
||||
"""
|
||||
return not bool(self._success)
|
||||
return not self._added and not self._updated
|
||||
|
||||
@property
|
||||
def removed(self) -> list[Package]:
|
||||
"""
|
||||
get list of removed packages
|
||||
|
||||
Returns:
|
||||
list[Package]: list of packages successfully removed
|
||||
"""
|
||||
return list(self._removed.values())
|
||||
|
||||
@property
|
||||
def success(self) -> list[Package]:
|
||||
@ -72,7 +104,16 @@ class Result:
|
||||
Returns:
|
||||
list[Package]: list of packages with success result
|
||||
"""
|
||||
return list(self._success.values())
|
||||
return list(self._added.values()) + list(self._updated.values())
|
||||
|
||||
def add_added(self, package: Package) -> None:
|
||||
"""
|
||||
add new package to new packages list
|
||||
|
||||
Args:
|
||||
package(Package): package removed
|
||||
"""
|
||||
self._added[package.base] = package
|
||||
|
||||
def add_failed(self, package: Package) -> None:
|
||||
"""
|
||||
@ -83,17 +124,26 @@ class Result:
|
||||
"""
|
||||
self._failed[package.base] = package
|
||||
|
||||
def add_success(self, package: Package) -> None:
|
||||
def add_removed(self, package: Package) -> None:
|
||||
"""
|
||||
add new package to removed list
|
||||
|
||||
Args:
|
||||
package(Package): package removed
|
||||
"""
|
||||
self._removed[package.base] = package
|
||||
|
||||
def add_updated(self, package: Package) -> None:
|
||||
"""
|
||||
add new package to success built
|
||||
|
||||
Args:
|
||||
package(Package): package built
|
||||
"""
|
||||
self._success[package.base] = package
|
||||
self._updated[package.base] = package
|
||||
|
||||
# pylint: disable=protected-access
|
||||
def merge(self, other: Result) -> Result:
|
||||
def merge(self, other: Result) -> Self:
|
||||
"""
|
||||
merge other result into this one. This method assumes that other has fresh info about status and override it
|
||||
|
||||
@ -101,19 +151,35 @@ class Result:
|
||||
other(Result): instance of the newest result
|
||||
|
||||
Returns:
|
||||
Result: updated instance
|
||||
|
||||
Raises:
|
||||
UnprocessedPackageStatusError: if there is previously failed package which is masked as success
|
||||
Self: updated instance
|
||||
"""
|
||||
for base, package in other._failed.items():
|
||||
if base in self._success:
|
||||
del self._success[base]
|
||||
self.add_failed(package)
|
||||
for base, package in other._success.items():
|
||||
if base in self._failed:
|
||||
raise UnprocessedPackageStatusError(base)
|
||||
self.add_success(package)
|
||||
for status in self.STATUS_PRIORITIES:
|
||||
new_packages: Iterable[Package] = getattr(other, f"_{status}", {}).values()
|
||||
insert_package: Callable[[Package], None] = getattr(self, f"add_{status}")
|
||||
for package in new_packages:
|
||||
insert_package(package)
|
||||
|
||||
return self.refine()
|
||||
|
||||
def refine(self) -> Self:
|
||||
"""
|
||||
merge packages between different results (e.g. remove failed from added, etc.) removing duplicates
|
||||
|
||||
Returns:
|
||||
Self: updated instance
|
||||
"""
|
||||
for index, base_status in enumerate(self.STATUS_PRIORITIES):
|
||||
# extract top-level packages
|
||||
base_packages: Iterable[str] = getattr(self, f"_{base_status}", {}).keys()
|
||||
# extract packages for each bottom-level
|
||||
for status in self.STATUS_PRIORITIES[index + 1:]:
|
||||
packages: dict[str, Package] = getattr(self, f"_{status}", {})
|
||||
|
||||
# if there is top-level package in bottom-level, then remove it
|
||||
for base in base_packages:
|
||||
if base in packages:
|
||||
del packages[base]
|
||||
|
||||
return self
|
||||
|
||||
# required for tests at least
|
||||
@ -129,4 +195,7 @@ class Result:
|
||||
"""
|
||||
if not isinstance(other, Result):
|
||||
return False
|
||||
return self.success == other.success and self.failed == other.failed
|
||||
return self._added == other._added \
|
||||
and self._removed == other._removed \
|
||||
and self._updated == other._updated \
|
||||
and self._failed == other._failed
|
||||
|
@ -19,10 +19,10 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class SignSettings(str, Enum):
|
||||
class SignSettings(StrEnum):
|
||||
"""
|
||||
sign targets enumeration
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user