Compare commits

..

23 Commits

Author SHA1 Message Date
4a644fc80f Release 2.12.1 2023-11-06 16:31:56 +02:00
d1aeeda975 fix: show unique upstream urls only 2023-11-06 16:29:18 +02:00
53aedbb9aa Release 2.12.0 2023-11-06 15:50:11 +02:00
c941226a9b feat: add upstream and aur urls to package info modal 2023-11-06 15:46:32 +02:00
69cbbfce65 feat: serve favicon in root 2023-11-06 15:46:32 +02:00
52d69fa9f6 build: remove spaces from names 2023-11-06 14:04:15 +02:00
279ac785a1 docs: correct crossreference in docs 2023-11-06 14:04:15 +02:00
f51b8e2358 refactor: replace enum with intenum and strenum 2023-11-05 13:26:11 +02:00
b116e6fa07 feat: allow to pass repository identifier to all database methods 2023-11-05 13:26:11 +02:00
7b667c8157 build: remove unused files from tarball 2023-11-05 13:26:11 +02:00
c51463d36b chore: add metthod definition order plugin to pylint
Also reorder some methods to fix errors
2023-11-05 13:26:11 +02:00
79947af013 feat: read old web sections 2023-11-04 13:34:53 +02:00
e6d6cb006b feat: improve small screens views
also fix case when invalid repository id has been sent
2023-11-01 16:38:53 +02:00
f553cc059a fix: reset logs window manually 2023-10-31 12:33:57 +02:00
c99d4f6f0f fix: correct month serialization 2023-10-31 10:13:25 +02:00
554827cc57 feat: add patch controls to web, review web, enrich info tab (#115)
* add ability to specify one-time patch on package addition

* support vars in interface
2023-10-29 23:41:20 +02:00
8524f1eb20 refactor: drop _check_output class attribute 2023-10-23 02:24:53 +03:00
4b6feb9ae6 refactor: add ability to pass anything as environment variable to build task 2023-10-23 02:24:41 +03:00
ad901067d8 fix: make toggle button works on small displays 2023-10-21 15:18:09 +03:00
4c20897e14 fix: write new line for telegram index 2023-10-21 03:56:48 +03:00
f8cbd07259 fix: do not automatically add unknown local packages
Instead of automatic package addition now it is required to add package
manually after clone. Less magic, plus would allow to use caches for
multi-repo setup (see #109)
2023-10-21 01:33:12 +03:00
16ad96d8c6 feat: add ability to run multiple commands on success 2023-10-19 04:34:49 +03:00
6bd1636bfa feat: allow to use single web instance for all repositories (#114)
* Allow to use single web instance for any repository

* some improvements

* drop includes from user home directory, introduce new variables to docker

The old solution didn't actually work as expected, because devtools
configuration belongs to filesystem (as well as sudo one), so it was
still required to run setup command.

In order to handle additional repositories, the POSTSETUP and PRESETUP
commands variables have been introduced. FAQ has been updated as well

* raise 404 in case if repository is unknown
2023-10-17 03:58:50 +03:00
156 changed files with 10785 additions and 7767 deletions

View File

@ -67,7 +67,7 @@ ignored-modules=
# Python code to execute, usually for sys.path manipulation such as # Python code to execute, usually for sys.path manipulation such as
# pygtk.require(). # 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 # 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 # 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, # List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers. # usually to register additional checkers.
load-plugins= load-plugins=pylint.extensions.docparams,
definition_order,
# Pickle collected data for later comparisons. # Pickle collected data for later comparisons.
persistent=yes persistent=yes
@ -227,7 +228,7 @@ name-group=
# Regular expression which should only match function or class names that do # Regular expression which should only match function or class names that do
# not require a docstring. # not require a docstring.
no-docstring-rgx=^_ no-docstring-rgx=
# List of decorators that produce properties, such as abc.abstractproperty. Add # List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties. # to this list to register other decorators that produce valid properties.

View File

@ -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: 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. * 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 ```python
def foo(argument: str, *, flag: bool = False) -> int: 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) flag(bool, optional): a flag (Default value = False)
Returns: Returns:
int: result int: result with
long description
Raises: Raises:
RuntimeError: a local function error occurs 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 ```python
class Clazz(BaseClazz): class Clazz(BaseClazz):
""" """
brand-new implementation of ``BaseClazz`` brand-new implementation of :class:`BaseClazz`
Attributes: Attributes:
CLAZZ_ATTRIBUTE(int): (class attribute) a brand-new class attribute 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. * 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]`). * 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: * Recommended order of function definitions in class:
```python ```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 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) and `__new__` 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. 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. * 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. * For any path interactions `pathlib.Path` must be used.
* Configuration interactions must go through `ahriman.core.configuration.Configuration` class instance. * 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. * 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. * 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. * 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 ```python
import aiohttp_apispec import aiohttp_apispec
@ -191,6 +194,7 @@ Again, the most checks can be performed by `make check` command, though some add
class Foo(BaseView): class Foo(BaseView):
POST_PERMISSION = ... POST_PERMISSION = ...
ROUTES = ...
@aiohttp_apispec.docs( @aiohttp_apispec.docs(
tags=["Tag"], 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 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, # exception raised by this method
401: {"description": "Authorization required", "schema": ErrorSchema}, # should be always presented 401: {"description": "Authorization required", "schema": ErrorSchema}, # should be always presented
403: {"description": "Access is forbidden", "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 500: {"description": "Internal server error", "schema": ErrorSchema}, # should be always presented
}, },
security=[{"token": [POST_PERMISSION]}], security=[{"token": [POST_PERMISSION]}],
@ -215,6 +220,7 @@ 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. * 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. * 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 ### Other checks

View File

@ -10,6 +10,8 @@ ENV AHRIMAN_OUTPUT=""
ENV AHRIMAN_PACKAGER="ahriman bot <ahriman@example.com>" ENV AHRIMAN_PACKAGER="ahriman bot <ahriman@example.com>"
ENV AHRIMAN_PACMAN_MIRROR="" ENV AHRIMAN_PACMAN_MIRROR=""
ENV AHRIMAN_PORT="" ENV AHRIMAN_PORT=""
ENV AHRIMAN_POSTSETUP_COMMAND=""
ENV AHRIMAN_PRESETUP_COMMAND=""
ENV AHRIMAN_REPOSITORY="aur-clone" ENV AHRIMAN_REPOSITORY="aur-clone"
ENV AHRIMAN_REPOSITORY_SERVER="" ENV AHRIMAN_REPOSITORY_SERVER=""
ENV AHRIMAN_REPOSITORY_ROOT="/var/lib/ahriman/ahriman" ENV AHRIMAN_REPOSITORY_ROOT="/var/lib/ahriman/ahriman"

View File

@ -3,7 +3,7 @@
PROJECT := ahriman 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)) TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES))
IGNORE_FILES := package/archlinux src/.mypy_cache IGNORE_FILES := package/archlinux src/.mypy_cache

View File

@ -4,39 +4,39 @@
[![setup status](https://github.com/arcan1s/ahriman/actions/workflows/setup.yml/badge.svg)](https://github.com/arcan1s/ahriman/actions/workflows/setup.yml) [![setup status](https://github.com/arcan1s/ahriman/actions/workflows/setup.yml/badge.svg)](https://github.com/arcan1s/ahriman/actions/workflows/setup.yml)
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/arcan1s/ahriman?label=Docker%20image)](https://hub.docker.com/r/arcan1s/ahriman) [![Docker Image Version (latest semver)](https://img.shields.io/docker/v/arcan1s/ahriman?label=Docker%20image)](https://hub.docker.com/r/arcan1s/ahriman)
[![CodeFactor](https://www.codefactor.io/repository/github/arcan1s/ahriman/badge)](https://www.codefactor.io/repository/github/arcan1s/ahriman) [![CodeFactor](https://www.codefactor.io/repository/github/arcan1s/ahriman/badge)](https://www.codefactor.io/repository/github/arcan1s/ahriman)
[![Documentation Status](https://readthedocs.org/projects/ahriman/badge/?version=latest)](https://ahriman.readthedocs.io/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/ahriman/badge/?version=latest)](https://ahriman.readthedocs.io)
Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts). Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
## Features ## Features
* Install-configure-forget manager for the very own repository. * Install-configure-forget manager for the very own repository.
* Multi-architecture support. * Multi architecture and repository support.
* Dependency manager. * Dependency manager.
* VCS packages support. * VCS packages support.
* Official repository support. * Official repository support.
* Ability to patch AUR packages and even create package from local PKGBUILDs. * Ability to patch AUR packages and even create package from local PKGBUILDs.
* Various rebuild options with ability to automatically bump package version. * Various rebuild options with ability to automatically bump package version.
* Sign support with gpg (repository, package), multiple packagers support. * 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: * Repository status interface with optional authorization and control options:
![web interface](web.png) ![web interface](web.png)
## Installation and run ## 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 ## 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. 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 ## 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. * [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). * [Telegram feed](https://t.me/arcanisrepo).

View File

@ -49,7 +49,10 @@ fi
if [ -n "$AHRIMAN_UNIX_SOCKET" ]; then if [ -n "$AHRIMAN_UNIX_SOCKET" ]; then
AHRIMAN_SETUP_ARGS+=("--web-unix-socket" "$AHRIMAN_UNIX_SOCKET") AHRIMAN_SETUP_ARGS+=("--web-unix-socket" "$AHRIMAN_UNIX_SOCKET")
fi fi
[ -n "$AHRIMAN_PRESETUP_COMMAND" ] && eval "$AHRIMAN_PRESETUP_COMMAND"
ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" service-setup "${AHRIMAN_SETUP_ARGS[@]}" ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" service-setup "${AHRIMAN_SETUP_ARGS[@]}"
[ -n "$AHRIMAN_POSTSETUP_COMMAND" ] && eval "$AHRIMAN_POSTSETUP_COMMAND"
# validate configuration if set # validate configuration if set
[ -n "$AHRIMAN_VALIDATE_CONFIGURATION" ] && ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" service-config-validate --exit-code [ -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: 1.0 MiB

View File

@ -116,6 +116,14 @@ ahriman.application.handlers.restore module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.application.handlers.run module
---------------------------------------
.. automodule:: ahriman.application.handlers.run
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.search module ahriman.application.handlers.search module
------------------------------------------ ------------------------------------------

View File

@ -100,6 +100,14 @@ ahriman.web.schemas.package\_names\_schema module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :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 ahriman.web.schemas.package\_properties\_schema module
------------------------------------------------------ ------------------------------------------------------
@ -132,6 +140,22 @@ ahriman.web.schemas.pagination\_schema module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :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 ahriman.web.schemas.pgp\_key\_id\_schema module
----------------------------------------------- -----------------------------------------------
@ -204,6 +228,14 @@ ahriman.web.schemas.update\_flags\_schema module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.web.schemas.versioned\_log\_schema module
-------------------------------------------------
.. automodule:: ahriman.web.schemas.versioned_log_schema
:members:
:no-undoc-members:
:show-inheritance:
Module contents Module contents
--------------- ---------------

View File

@ -30,6 +30,14 @@ ahriman.web.views.index module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.web.views.static module
-------------------------------
.. automodule:: ahriman.web.views.static
:members:
:no-undoc-members:
:show-inheritance:
Module contents Module contents
--------------- ---------------

View File

@ -28,6 +28,22 @@ ahriman.web.views.v1.status.packages module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :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 ahriman.web.views.v1.status.repositories module
----------------------------------------------- -----------------------------------------------

View File

@ -41,7 +41,7 @@ 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. * ``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. * ``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. * ``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. * ``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.

View File

@ -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). 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 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_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_PACMAN_MIRROR`` - override pacman mirror server if set.
* ``AHRIMAN_PORT`` - HTTP server port if any, default is empty. * ``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`` - 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_SERVER`` - optional override for the repository url. Useful if you would like to download packages from remote instead of local filesystem.
* ``AHRIMAN_REPOSITORY_ROOT`` - repository root. Because of filesystem rights it is required to override default repository root. By default, it uses ``ahriman`` directory inside ahriman's home, which can be passed as mount volume. * ``AHRIMAN_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 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 .. 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 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 Non-x86_64 architecture setup
----------------------------- -----------------------------

View File

@ -7,21 +7,21 @@ Features
-------- --------
* Install-configure-forget manager for the very own repository. * Install-configure-forget manager for the very own repository.
* Multi-architecture support. * Multi architecture and repository support.
* Dependency manager. * Dependency manager.
* VCS packages support. * VCS packages support.
* Official repository support. * Official repository support.
* Ability to patch AUR packages and even create package from local PKGBUILDs. * Ability to patch AUR packages and even create package from local PKGBUILDs.
* Various rebuild options with ability to automatically bump package version. * Various rebuild options with ability to automatically bump package version.
* Sign support with gpg (repository, package), multiple packagers support. * 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. * Repository status interface with optional authorization and control options.
Live demos 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. * `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>`_. * `Telegram feed <https://t.me/arcanisrepo>`_.
Contents Contents

View File

@ -1,7 +1,7 @@
Manual migrations 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 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 to the latest major release of python (3.11) (required by other changes).
#. Upgrade devtools to the latest release. #. Upgrade devtools to the latest release.
#. Backup your settings, ``/etc/ahriman.ini.d/00-setup-overrides.ini`` by default. #. 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 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. #. 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``. #. 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. #. 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 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``. * 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: 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 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 .. code-block:: ini
@ -52,15 +52,13 @@ In order to migrate to new filesystem tree the following actions are required:
SigLevel = Optional TrustAll SigLevel = Optional TrustAll
Server = file:///var/lib/ahriman/repository/aur-clone/x86_64 Server = file:///var/lib/ahriman/repository/aur-clone/x86_64
Alternatively it can be done by running ``service-setup`` command again. In case of manual interventions make sure to remove architecture reference from ``web`` sections (if any) to avoid ambiguity.
#. If you didn't run setup command on the previous step, make sure to remove architecture reference from ``web`` section (if any).
#. #.
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 .. code-block:: shell

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev # Maintainer: Evgeniy Alekseev
pkgname='ahriman' pkgname='ahriman'
pkgver=2.11.0 pkgver=2.12.1
pkgrel=1 pkgrel=1
pkgdesc="ArcH linux ReposItory MANager" pkgdesc="ArcH linux ReposItory MANager"
arch=('any') arch=('any')

View File

@ -13,30 +13,30 @@ post_upgrade() {
_2_9_0_1_changes() { _2_9_0_1_changes() {
cat << EOF 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; * make sure that devtools are upgraded to the latest release;
* merge service configuration if required; * merge upstream configuration into local configuration (e.g. by using pacdiff);
* run setup command (i.e. sudo ahriman service-setup) with the same arguments as you did before; * run setup command (i.e. ahriman service-setup) with the same arguments as did before;
* remove build chroot: sudo rm -r /var/lib/ahriman/chroot/ahriman-x86_64/; * remove build chroot, e.g.: ahriman service-clean --chroot;
* update local databases: sudo -u ahriman ahriman update --no-aur --no-local --no-manual -yy; * 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 EOF
} }
_2_12_0_1_changes() { _2_12_0_1_changes() {
cat << EOF 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; * stop and disable all services;
* run service-tree-migrate as ahriman user; * run service-tree-migrate as ahriman user;
* edit configuration to avoid pointing to the old paths; * edit local 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; * run setup command (i.e. ahriman service-setup) with the same arguments as did before;
* update synchronization services in order to support new paths (or setup redirects); * 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 your architecture and * enable web and timer services again by using x86_64-aur-clone suffix,
aur-clone is repository name; 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 EOF
} }

View File

@ -6,10 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Embed elements Elements via Web Component --> <!-- Embed elements Elements via Web Component -->
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script> <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://unpkg.com/@stoplight/elements/styles.min.css" type="text/css"> <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">
<link rel="shortcut icon" href="/static/favicon.ico">
</head> </head>
<body> <body>

View File

@ -5,8 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/static/favicon.ico">
{% include "utils/style.jinja2" %} {% include "utils/style.jinja2" %}
{% include "user-style.jinja2" ignore missing %} {% include "user-style.jinja2" ignore missing %}
</head> </head>
@ -17,16 +15,16 @@
<div class="container"> <div class="container">
<nav class="navbar navbar-expand-lg"> <nav class="navbar navbar-expand-lg">
<a class="navbar-brand" href="#"><img src="/static/logo.svg" width="30" height="30" alt=""> ahriman</a> <div class="navbar-brand"><img src="/static/logo.svg" width="30" height="30" alt=""> ahriman</div>
<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"> <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> <span class="navbar-toggler-icon"></span>
</button> </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"> <ul id="repositories" class="nav nav-tabs">
{% for repository in repositories %} {% for repository in repositories %}
<li class="nav-item"> <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> </li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -42,7 +40,7 @@
{% if not auth.enabled or auth.username is not none %} {% 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"> <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> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
@ -51,7 +49,7 @@
</button> </button>
</li> </li>
<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 <i class="bi bi-play"></i> update
</button> </button>
</li> </li>
@ -61,23 +59,24 @@
</button> </button>
</li> </li>
<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 <i class="bi bi-trash"></i> remove
</button> </button>
</li> </li>
</ul> </ul>
<button id="key-import-button" type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#key-import-modal" hidden> <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> </button>
{% endif %} {% endif %}
<button type="button" class="btn btn-secondary" onclick="reload()"> <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> </button>
</div> </div>
<table id="packages" class="table table-striped table-hover" <table id="packages"
data-classes="table table-hover"
data-export-options='{"fileName": "packages"}' data-export-options='{"fileName": "packages"}'
data-filter-control="true" data-filter-control="true"
data-filter-control-visible="false" data-filter-control-visible="false"
@ -102,13 +101,13 @@
<tr> <tr>
<th data-checkbox="true"></th> <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-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-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="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="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-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-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-cell-style="statusFormat" data-field="status" data-filter-control="select" data-filter-control-placeholder="(any status)">status</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> </tr>
</thead> </thead>
</table> </table>

View File

@ -8,20 +8,20 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group row"> <div class="form-group row">
<label for="key-import-fingerprint-input" class="col-sm-2 col-form-label">fingerprint</label> <label for="key-import-fingerprint-input" class="col-2 col-form-label">fingerprint</label>
<div class="col-sm-10"> <div class="col-10">
<input id="key-import-fingerprint-input" type="text" class="form-control" placeholder="PGP key fingerprint" name="key" required> <input id="key-import-fingerprint-input" type="text" class="form-control" placeholder="PGP key fingerprint" name="key" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="key-import-server-input" class="col-sm-2 col-form-label">key server</label> <label for="key-import-server-input" class="col-2 col-form-label">key server</label>
<div class="col-sm-10"> <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> <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> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-2"></div> <div class="col-2"></div>
<div class="col-sm-10"> <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> <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>
</div> </div>

View File

@ -8,14 +8,14 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group row"> <div class="form-group row">
<label for="login-username" class="col-sm-4 col-form-label">username</label> <label for="login-username" class="col-4 col-form-label">username</label>
<div class="col-sm-8"> <div class="col-8">
<input id="login-username" type="text" class="form-control" placeholder="enter username" name="username" required> <input id="login-username" type="text" class="form-control" placeholder="enter username" name="username" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="login-password" class="col-sm-4 col-form-label">password</label> <label for="login-password" class="col-4 col-form-label">password</label>
<div class="col-sm-8"> <div class="col-8">
<div class="input-group"> <div class="input-group">
<input id="login-password" type="password" class="form-control" placeholder="enter password" name="password" required> <input id="login-password" type="password" class="form-control" placeholder="enter password" name="password" required>
<div class="input-group-append"> <div class="input-group-append">

View File

@ -1,5 +1,5 @@
<div id="package-add-modal" tabindex="-1" role="dialog" class="modal fade"> <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"> <div class="modal-content">
<form id="package-add-form" onsubmit="return false"> <form id="package-add-form" onsubmit="return false">
<div class="modal-header"> <div class="modal-header">
@ -8,9 +8,9 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group row"> <div class="form-group row">
<label for="package-add-repository-input" class="col-sm-4 col-form-label">repository</label> <label for="package-add-repository-input" class="col-3 col-form-label">repository</label>
<div class="col-sm-8"> <div class="col-9">
<select id="package-add-repository-input" class="form-control" name="repository" required> <select id="package-add-repository-input" class="form-control" required>
{% for repository in repositories %} {% for repository in repositories %}
<option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option> <option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option>
{% endfor %} {% endfor %}
@ -18,12 +18,18 @@
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="package-add-input" class="col-sm-4 col-form-label">package</label> <label for="package-add-input" class="col-3 col-form-label">package</label>
<div class="col-sm-8"> <div class="col-9">
<input id="package-add-input" type="text" list="known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" name="package" required> <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> <datalist id="package-add-known-packages-dlist"></datalist>
</div> </div>
</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>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" class="btn btn-primary" onclick="packagesAdd()"><i class="bi bi-play"></i> add</button> <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"); const packageAddForm = $("#package-add-form");
packageAddModal.on("shown.bs.modal", () => { packageAddModal.on("shown.bs.modal", () => {
$(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true); $(`#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 packageAddInput = $("#package-add-input");
const packageAddRepositoryInput = $("#package-add-repository-input"); const packageAddRepositoryInput = $("#package-add-repository-input");
@ -71,25 +79,81 @@
}, this), 500)); }, this), 500));
}); });
function packagesAdd() { const packageAddVariablesDiv = $("#package-add-variables-div");
const packages = packageAddInput.val();
const repository = getRepositorySelector(packageAddRepositoryInput); 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) { if (packages) {
packageAddModal.modal("hide"); packageAddModal.modal("hide");
const onSuccess = update => `Packages ${update} have been added`; const onSuccess = update => `Packages ${update} have been added`;
const onFailure = error => `Package addition failed: ${error}`; 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() { function packagesRequest(packages, patches) {
const packages = packageAddInput.val(); packages = packages ?? packageAddInput.val();
patches = patches ?? patchesParse();
const repository = getRepositorySelector(packageAddRepositoryInput); const repository = getRepositorySelector(packageAddRepositoryInput);
if (packages) { if (packages) {
packageAddModal.modal("hide"); packageAddModal.modal("hide");
const onSuccess = update => `Packages ${update} have been requested`; const onSuccess = update => `Packages ${update} have been requested`;
const onFailure = error => `Package request failed: ${error}`; 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> </script>

View File

@ -6,11 +6,49 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div> </div>
<div class="modal-body"> <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> <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>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="showLogs()"><i class="bi bi-arrow-clockwise"></i> reload</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 type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i> close</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> </div>
</div> </div>
@ -20,30 +58,91 @@
const packageInfoModal = $("#package-info-modal"); const packageInfoModal = $("#package-info-modal");
const packageInfoModalHeader = $("#package-info-modal-header"); const packageInfoModalHeader = $("#package-info-modal-header");
const packageInfo = $("#package-info"); 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 packageInfoLogsInput = $("#package-info-logs-input");
const packageInfoLogsCopyButton = $("#package-info-logs-copy-button"); 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() { async function copyLogs() {
const logs = packageInfoLogsInput.text(); const logs = packageInfoLogsInput.text();
await copyToClipboard(logs, packageInfoLogsCopyButton); await copyToClipboard(logs, packageInfoLogsCopyButton);
} }
function showLogs(packageBase) { function hideInfoControls(hidden) {
const isPackageBaseSet = packageBase !== undefined; packageInfoRemoveButton.attr("hidden", hidden);
if (isPackageBaseSet) packageInfoUpdateButton.attr("hidden", hidden);
packageInfoModal.data("package", packageBase); // set package base as currently used }
else
packageBase = packageInfoModal.data("package"); // read package base from the current window attribute
const headerClass = status => { function insertVariable(packageBase, variable) {
if (status === "pending") return ["bg-warning"]; const variableInput = document.createElement("div");
if (status === "building") return ["bg-warning"]; variableInput.classList.add("input-group");
if (status === "failed") return ["bg-danger", "text-white"];
if (status === "success") return ["bg-success", "text-white"]; const variableNameInput = document.createElement("input");
return ["bg-secondary", "text-white"]; 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({ $.ajax({
url: `/api/v2/packages/${packageBase}/logs`, url: `/api/v2/packages/${packageBase}/logs`,
data: { data: {
@ -53,26 +152,115 @@
type: "GET", type: "GET",
dataType: "json", dataType: "json",
success: response => { success: response => {
packageInfo.text(`${response.package_base} ${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`); const logs = response.map(log_record => {
const logs = response.logs.map(log_record => { return `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`;
const [timestamp, record] = log_record;
return `[${new Date(1000 * timestamp).toISOString()}] ${record}`;
}); });
packageInfoLogsInput.text(logs.join("\n")); 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.removeClass();
packageInfoModalHeader.addClass("modal-header"); 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) => { error: (jqXHR, _, errorThrown) => {
// show failed modal in case if first time loading hideInfoControls(true);
if (isPackageBaseSet) { onFailure(jqXHR, null, errorThrown);
const message = error => `Could not load package ${packageBase} logs: ${error}`;
showFailure("Load failure", message, jqXHR, 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> </script>

View File

@ -1,5 +1,5 @@
<div id="package-rebuild-modal" tabindex="-1" role="dialog" class="modal fade"> <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"> <div class="modal-content">
<form id="package-rebuild-form" onsubmit="return false"> <form id="package-rebuild-form" onsubmit="return false">
<div class="modal-header"> <div class="modal-header">
@ -8,8 +8,8 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group row"> <div class="form-group row">
<label for="package-rebuild-repository-input" class="col-sm-4 col-form-label">repository</label> <label for="package-rebuild-repository-input" class="col-3 col-form-label">repository</label>
<div class="col-sm-8"> <div class="col-9">
<select id="package-rebuild-repository-input" class="form-control" name="repository" required> <select id="package-rebuild-repository-input" class="form-control" name="repository" required>
{% for repository in repositories %} {% for repository in repositories %}
<option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option> <option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option>
@ -18,8 +18,8 @@
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="package-rebuild-dependency-input" class="col-sm-4 col-form-label">dependency</label> <label for="package-rebuild-dependency-input" class="col-3 col-form-label">dependency</label>
<div class="col-sm-8"> <div class="col-9">
<input id="package-rebuild-dependency-input" type="text" class="form-control" placeholder="packages dependency" name="package" required> <input id="package-rebuild-dependency-input" type="text" class="form-control" placeholder="packages dependency" name="package" required>
</div> </div>
</div> </div>

View File

@ -5,6 +5,9 @@
const packageRemoveButton = $("#package-remove-button"); const packageRemoveButton = $("#package-remove-button");
const packageUpdateButton = $("#package-update-button"); const packageUpdateButton = $("#package-update-button");
const packageInfoRemoveButton = $("#package-info-remove-button");
const packageInfoUpdateButton = $("#package-info-update-button");
let repository = null; let repository = null;
$("#repositories a").on("click", (event) => { $("#repositories a").on("click", (event) => {
const element = event.target; const element = event.target;
@ -12,7 +15,7 @@
architecture: element.dataset.architecture, architecture: element.dataset.architecture,
repository: element.dataset.repository, 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"); $(`#${element.id}`).tab("show");
reload(); reload();
}); });
@ -25,7 +28,7 @@
if (0 === cell || "base" === cell) { if (0 === cell || "base" === cell) {
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
table.bootstrapTable(method, {field: "id", values: [data.id]}); table.bootstrapTable(method, {field: "id", values: [data.id]});
} else showLogs(data.id); } else showPackageInfo(data.id);
}); });
table.on("created-controls.bs.table", () => { table.on("created-controls.bs.table", () => {
const pickerInput = $(".bootstrap-table-filter-control-timestamp"); const pickerInput = $(".bootstrap-table-filter-control-timestamp");
@ -50,7 +53,7 @@
const statusBadge = $("#badge-status"); const statusBadge = $("#badge-status");
const versionBadge = $("#badge-version"); const versionBadge = $("#badge-version");
function doPackageAction(uri, packages, repository, successText, failureText) { function doPackageAction(uri, packages, repository, successText, failureText, data) {
const queryParams = $.param({ const queryParams = $.param({
architecture: repository.architecture, architecture: repository.architecture,
repository: repository.repository, repository: repository.repository,
@ -58,7 +61,7 @@
$.ajax({ $.ajax({
url: `${uri}?${queryParams}`, url: `${uri}?${queryParams}`,
data: JSON.stringify({packages: packages}), data: JSON.stringify(Object.assign({}, {packages: packages}, data || {})),
type: "POST", type: "POST",
contentType: "application/json", contentType: "application/json",
success: _ => { 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() { function getSelection() {
return table.bootstrapTable("getSelections").map(row => row.id); 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) { function hideControls(hidden) {
keyImportButton.attr("hidden", hidden); keyImportButton.attr("hidden", hidden);
packageAddButton.attr("hidden", hidden); packageAddButton.attr("hidden", hidden);
@ -104,6 +106,24 @@
packageUpdateButton.attr("hidden", hidden); 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() { function reload() {
table.bootstrapTable("showLoading"); table.bootstrapTable("showLoading");
@ -124,30 +144,18 @@
type: "GET", type: "GET",
dataType: "json", dataType: "json",
success: response => { 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 payload = response.map(description => {
const package_base = description.package.base; const package_base = description.package.base;
const web_url = description.package.remote.web_url; const web_url = description.package.remote.web_url;
return { return {
id: package_base, 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), version: safe(description.package.version),
packager: description.package.packager ? safe(description.package.packager) : "", packager: description.package.packager ? safe(description.package.packager) : "",
packages: listToTable(Object.keys(description.package.packages)), packages: listToTable(Object.keys(description.package.packages)),
groups: listToTable(extractListProperties(description.package, "groups")), groups: listToTable(extractListProperties(description.package, "groups")),
licenses: listToTable(extractListProperties(description.package, "licenses")), 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, status: description.status.status,
}; };
}); });
@ -186,7 +194,7 @@
statusBadge statusBadge
.popover("dispose") .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(); .popover();
statusBadge.removeClass(); statusBadge.removeClass();
statusBadge.addClass("btn"); 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) { function statusFormat(value) {
const cellClass = status => { const cellClass = status => {
if (status === "pending") return "table-warning"; if (status === "pending") return "table-warning";
@ -206,26 +220,6 @@
return {classes: cellClass(value)}; 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({}); table.bootstrapTable({});
statusBadge.popover(); statusBadge.popover();

View File

@ -5,8 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/static/favicon.ico">
{% include "utils/style.jinja2" %} {% include "utils/style.jinja2" %}
{% include "user-style.jinja2" ignore missing %} {% include "user-style.jinja2" ignore missing %}
</head> </head>

View File

@ -30,7 +30,8 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
</div> </div>
<div class="container"> <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-export-options='{"fileName": "packages"}'
data-filter-control="true" data-filter-control="true"
data-filter-control-visible="false" data-filter-control-visible="false"
@ -53,16 +54,16 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
<thead class="table-primary"> <thead class="table-primary">
<tr> <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-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="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="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="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="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="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-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-align="right" data-field="archive_size">archive size</th>
<th data-sortable="true" data-field="installed_size">installed size</th> <th data-sortable="true" data-align="right" 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="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">build date</th>
</tr> </tr>
</thead> </thead>

View File

@ -1,4 +1,5 @@
{#simplified version of full report#} {#simplified version of full report#}
<b>{{ repository }} update</b> <b>{{ repository }} update</b>
{% for package in packages %} {% 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 %}

View File

@ -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/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> <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/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/@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.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.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.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.22.1/dist/extensions/filter-control/bootstrap-table-filter-control.js" integrity="sha384-NIqcjpr/3eZI1iNzz7hgT5rgp70qFUzkZffeCgVva9gi80B5vqcm7gn+8QvlWxko" 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> <script>
async function copyToClipboard(text, button) { async function copyToClipboard(text, button) {
@ -36,19 +34,17 @@
}, 2000); }, 2000);
} }
function safe(string) {
return String(string)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function extractDataList(data, column) { function extractDataList(data, column) {
const elements = data.flatMap(row => row[column].split("<br>")).filter(v => v); // remove empty elements from array const elements = data.flatMap(row => row[column].split("<br>")).filter(v => v); // remove empty elements from array
return Array.from(new Set(elements)).sort(); 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) { function filterContains(text, value) {
return value.includes(text.toLowerCase().trim()); return value.includes(text.toLowerCase().trim());
} }
@ -67,4 +63,33 @@
// the library removes all symbols from string, so it is just string // the library removes all symbols from string, so it is just string
return value.includes(dataList[index].toLowerCase()); 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
// 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> </script>

View File

@ -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@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.10.4/font/bootstrap-icons.css" integrity="sha384-LrVLJJYk9OiJmjNDakUBU7kS9qCT8wk1j2OU7ncpsfB3QS37UPdkCuq3ZD1MugNY" 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/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"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.css" integrity="sha384-zLkQsiLfAQqGeIJeKLC+rcCR1YoYaQFLCL7cLDUoKE1ajKJzySpjzWGfYS2vjSG+" crossorigin="anonymous" type="text/css">

View File

@ -1,18 +1,18 @@
# AUTOMATICALLY GENERATED by `shtab` # 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' '--wait-timeout' '-V' '--version')
_shtab_ahriman_aur_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by') _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_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_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_updates_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_help_version_option_strings=('-h' '--help') _shtab_ahriman_help_version_option_strings=('-h' '--help')
_shtab_ahriman_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_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') _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') _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_package_remove_option_strings=('-h' '--help')
_shtab_ahriman_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') _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_service_key_import_option_strings=('-h' '--help' '--key-server')
_shtab_ahriman_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_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_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_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') _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___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_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') _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___exit_code_nargs=0
_shtab_ahriman_search___info_nargs=0 _shtab_ahriman_search___info_nargs=0
_shtab_ahriman_search___no_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_pos_0_nargs=*
_shtab_ahriman_help_commands_unsafe__h_nargs=0 _shtab_ahriman_help_commands_unsafe__h_nargs=0
_shtab_ahriman_help_commands_unsafe___help_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__h_nargs=0
_shtab_ahriman_help_updates___help_nargs=0 _shtab_ahriman_help_updates___help_nargs=0
_shtab_ahriman_help_updates__e_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___help_nargs=0
_shtab_ahriman_service_repositories___id_only_nargs=0 _shtab_ahriman_service_repositories___id_only_nargs=0
_shtab_ahriman_service_repositories___no_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__h_nargs=0
_shtab_ahriman_service_setup___help_nargs=0 _shtab_ahriman_service_setup___help_nargs=0
_shtab_ahriman_service_setup___generate_salt_nargs=0 _shtab_ahriman_service_setup___generate_salt_nargs=0

View File

@ -1,9 +1,9 @@
.TH AHRIMAN "1" "2023\-10\-09" "ahriman" "Generated Python Manual" .TH AHRIMAN "1" "2023\-11\-06" "ahriman" "Generated Python Manual"
.SH NAME .SH NAME
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS
.B ahriman .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] [--wait-timeout WAIT_TIMEOUT] [-V] {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 .SH DESCRIPTION
ArcH linux ReposItory MANager ArcH linux ReposItory MANager
@ -59,12 +59,12 @@ COMMAND
\fBahriman\fR \fI\,aur\-search\/\fR \fBahriman\fR \fI\,aur\-search\/\fR
search for package search for package
.TP .TP
\fBahriman\fR \fI\,help\/\fR
show help message
.TP
\fBahriman\fR \fI\,help\-commands\-unsafe\/\fR \fBahriman\fR \fI\,help\-commands\-unsafe\/\fR
list unsafe commands list unsafe commands
.TP .TP
\fBahriman\fR \fI\,help\/\fR
show help message
.TP
\fBahriman\fR \fI\,help\-updates\/\fR \fBahriman\fR \fI\,help\-updates\/\fR
check for service updates check for service updates
.TP .TP
@ -158,6 +158,9 @@ import PGP key
\fBahriman\fR \fI\,service\-repositories\/\fR \fBahriman\fR \fI\,service\-repositories\/\fR
show repositories show repositories
.TP .TP
\fBahriman\fR \fI\,service\-run\/\fR
run multiple commands
.TP
\fBahriman\fR \fI\,service\-setup\/\fR \fBahriman\fR \fI\,service\-setup\/\fR
initial service configuration initial service configuration
.TP .TP
@ -204,15 +207,6 @@ 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 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 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 .SH COMMAND \fI\,'ahriman help\-commands\-unsafe'\/\fR
usage: ahriman help\-commands\-unsafe [\-h] [command ...] usage: ahriman help\-commands\-unsafe [\-h] [command ...]
@ -223,6 +217,15 @@ list unsafe commands as defined in default args
instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1 instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1
otherwise otherwise
.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\-updates'\/\fR .SH COMMAND \fI\,'ahriman help\-updates'\/\fR
usage: ahriman help\-updates [\-h] [\-e] usage: ahriman help\-updates [\-h] [\-e]
@ -240,7 +243,7 @@ print application and its dependencies versions
.SH COMMAND \fI\,'ahriman package\-add'\/\fR .SH COMMAND \fI\,'ahriman package\-add'\/\fR
usage: ahriman package\-add [\-h] [\-\-dependencies | \-\-no\-dependencies] [\-e] [\-\-increment | \-\-no\-increment] [\-n] [\-y] usage: ahriman package\-add [\-h] [\-\-dependencies | \-\-no\-dependencies] [\-e] [\-\-increment | \-\-no\-increment] [\-n] [\-y]
[\-s {auto,archive,aur,directory,local,remote,repository}] [\-u USERNAME] [\-s {auto,archive,aur,directory,local,remote,repository}] [\-u USERNAME] [\-v VARIABLE]
package [package ...] package [package ...]
add existing or new package to the build queue 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 \fB\-u\fR \fI\,USERNAME\/\fR, \fB\-\-username\fR \fI\,USERNAME\/\fR
build as user 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 .SH COMMAND \fI\,'ahriman package\-remove'\/\fR
usage: ahriman package\-remove [\-h] package [package ...] usage: ahriman package\-remove [\-h] package [package ...]
@ -722,6 +729,15 @@ list all available repositories
\fB\-\-id\-only\fR, \fB\-\-no\-id\-only\fR \fB\-\-id\-only\fR, \fB\-\-no\-id\-only\fR
show machine readable identifier instead 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 .SH COMMAND \fI\,'ahriman service\-setup'\/\fR
usage: ahriman service\-setup [\-h] [\-\-build\-as\-user BUILD_AS_USER] [\-\-from\-configuration FROM_CONFIGURATION] 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] [\-\-generate\-salt | \-\-no\-generate\-salt] [\-\-makeflags\-jobs | \-\-no\-makeflags\-jobs]

View File

@ -52,12 +52,14 @@ _shtab_ahriman_commands() {
"repo-triggers:run triggers on empty build result as configured by settings" "repo-triggers:run triggers on empty build result as configured by settings"
"repo-update:check for packages updates and run build process if requested" "repo-update:check for packages updates and run build process if requested"
"report:generate repository report according to current settings" "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" "search:search for package in AUR using API"
"service-clean:remove local caches" "service-clean:remove local caches"
"service-config:dump configuration for the specified architecture" "service-config:dump configuration for the specified architecture"
"service-config-validate:validate configuration and print found errors" "service-config-validate:validate configuration and print found errors"
"service-key-import:import PGP key from public sources to the repository user" "service-key-import:import PGP key from public sources to the repository user"
"service-repositories:list all available repositories" "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-setup:create initial service configuration, requires root"
"service-shell:drop into python shell" "service-shell:drop into python shell"
"service-tree-migrate:migrate repository tree between versions" "service-tree-migrate:migrate repository tree between versions"
@ -99,8 +101,9 @@ _shtab_ahriman_add_options=(
{--increment,--no-increment}"[increment package release (pkgrel) version on duplicate (default\: True)]:increment:" {--increment,--no-increment}"[increment package release (pkgrel) version on duplicate (default\: True)]:increment:"
{-n,--now}"[run update function after (default\: False)]" {-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)]" "*"{-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:" {-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):" "(*):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:" {--increment,--no-increment}"[increment package release (pkgrel) version on duplicate (default\: True)]:increment:"
{-n,--now}"[run update function after (default\: False)]" {-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)]" "*"{-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:" {-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):" "(*):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=( _shtab_ahriman_package_status_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-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):" "(*)::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:" {--increment,--no-increment}"[increment package release (pkgrel) version on duplicate (default\: True)]:increment:"
{-n,--now}"[run update function after (default\: False)]" {-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)]" "*"{-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:" {-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):" "(*):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=( _shtab_ahriman_repo_status_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-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=( _shtab_ahriman_repo_sync_options=(
@ -445,6 +450,11 @@ _shtab_ahriman_report_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-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=( _shtab_ahriman_search_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]" {-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:" {--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=( _shtab_ahriman_service_setup_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
"--build-as-user[force makepkg user to the specific one (default\: None)]:build_as_user:" "--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=( _shtab_ahriman_status_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-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):" "(*)::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:" "--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:" "--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:" {-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:" ":username for web service:"
) )
@ -662,12 +677,14 @@ _shtab_ahriman() {
repo-triggers) _arguments -C -s $_shtab_ahriman_repo_triggers_options ;; repo-triggers) _arguments -C -s $_shtab_ahriman_repo_triggers_options ;;
repo-update) _arguments -C -s $_shtab_ahriman_repo_update_options ;; repo-update) _arguments -C -s $_shtab_ahriman_repo_update_options ;;
report) _arguments -C -s $_shtab_ahriman_report_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 ;; search) _arguments -C -s $_shtab_ahriman_search_options ;;
service-clean) _arguments -C -s $_shtab_ahriman_service_clean_options ;; service-clean) _arguments -C -s $_shtab_ahriman_service_clean_options ;;
service-config) _arguments -C -s $_shtab_ahriman_service_config_options ;; service-config) _arguments -C -s $_shtab_ahriman_service_config_options ;;
service-config-validate) _arguments -C -s $_shtab_ahriman_service_config_validate_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-key-import) _arguments -C -s $_shtab_ahriman_service_key_import_options ;;
service-repositories) _arguments -C -s $_shtab_ahriman_service_repositories_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-setup) _arguments -C -s $_shtab_ahriman_service_setup_options ;;
service-shell) _arguments -C -s $_shtab_ahriman_service_shell_options ;; service-shell) _arguments -C -s $_shtab_ahriman_service_shell_options ;;
service-tree-migrate) _arguments -C -s $_shtab_ahriman_service_tree_migrate_options ;; service-tree-migrate) _arguments -C -s $_shtab_ahriman_service_tree_migrate_options ;;

View File

@ -0,0 +1,207 @@
#
# 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
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"
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": [
"new",
"init",
"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 ("__new__",):
return MethodTypeOrder.New
if function.name in ("__init__", "__post_init__"):
return MethodTypeOrder.Init
# 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))

View File

@ -40,7 +40,7 @@ check = [
"mypy", "mypy",
"pylint", "pylint",
] ]
docs = [ docs = [
"Sphinx", "Sphinx",
"argparse-manpage", "argparse-manpage",
"pydeps", "pydeps",

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
__version__ = "2.11.0" __version__ = "2.12.1"

View File

@ -96,8 +96,8 @@ def _parser() -> argparse.ArgumentParser:
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command") subparsers = parser.add_subparsers(title="command", help="command to run", dest="command")
_set_aur_search_parser(subparsers) _set_aur_search_parser(subparsers)
_set_help_parser(subparsers)
_set_help_commands_unsafe_parser(subparsers) _set_help_commands_unsafe_parser(subparsers)
_set_help_parser(subparsers)
_set_help_updates_parser(subparsers) _set_help_updates_parser(subparsers)
_set_help_version_parser(subparsers) _set_help_version_parser(subparsers)
_set_package_add_parser(subparsers) _set_package_add_parser(subparsers)
@ -129,6 +129,7 @@ def _parser() -> argparse.ArgumentParser:
_set_service_config_validate_parser(subparsers) _set_service_config_validate_parser(subparsers)
_set_service_key_import_parser(subparsers) _set_service_key_import_parser(subparsers)
_set_service_repositories(subparsers) _set_service_repositories(subparsers)
_set_service_run(subparsers)
_set_service_setup_parser(subparsers) _set_service_setup_parser(subparsers)
_set_service_shell_parser(subparsers) _set_service_shell_parser(subparsers)
_set_service_tree_migrate_parser(subparsers) _set_service_tree_migrate_parser(subparsers)
@ -165,25 +166,6 @@ def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser return parser
def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for listing help subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
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.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: def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for listing unsafe commands add parser for listing unsafe commands
@ -203,6 +185,25 @@ def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.Argument
return parser return parser
def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for listing help subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
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.set_defaults(handler=handlers.Help, architecture="", lock=None, quiet=True, report=False, repository="",
unsafe=True, parser=_parser)
return parser
def _set_help_updates_parser(root: SubParserAction) -> argparse.ArgumentParser: def _set_help_updates_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for service update check subcommand 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", parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto) type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
parser.add_argument("-u", "--username", help="build as user", default=extract_user()) parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build", action="append")
parser.set_defaults(handler=handlers.Add) parser.set_defaults(handler=handlers.Add)
return parser return parser
@ -902,6 +904,27 @@ def _set_service_repositories(root: SubParserAction) -> argparse.ArgumentParser:
return parser 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: def _set_service_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for setup subcommand add parser for setup subcommand

View File

@ -32,8 +32,8 @@ class Application(ApplicationPackages, ApplicationRepository):
base application class base application class
Examples: Examples:
This class groups ``Repository`` methods into specific method which process all supposed actions caused by This class groups :class:`ahriman.core.repository.repository.Repository` methods into specific method which
underlying action. E.g.:: process all supposed actions caused by underlying action. E.g.::
>>> from ahriman.core.configuration import Configuration >>> from ahriman.core.configuration import Configuration
>>> from ahriman.models.package_source import PackageSource >>> from ahriman.models.package_source import PackageSource
@ -49,8 +49,8 @@ class Application(ApplicationPackages, ApplicationRepository):
>>> # updates for specified packages >>> # updates for specified packages
>>> application.update(updates) >>> application.update(updates)
In case if specific actions or their order are required, the direct access to ``Repository`` must In case if specific actions or their order are required, the direct access to
be used instead. :class:`ahriman.core.repository.repository.Repository` must be used instead.
""" """
def _known_packages(self) -> set[str]: def _known_packages(self) -> set[str]:
@ -117,7 +117,7 @@ class Application(ApplicationPackages, ApplicationRepository):
Returns: Returns:
list[Package]: updated packages list. Packager for dependencies will be copied from 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]: def missing_dependencies(source: Iterable[Package]) -> dict[str, str | None]:
# append list of known packages with packages which are in current sources # append list of known packages with packages which are in current sources

View File

@ -32,6 +32,7 @@ from ahriman.application.handlers.remove import Remove
from ahriman.application.handlers.remove_unknown import RemoveUnknown from ahriman.application.handlers.remove_unknown import RemoveUnknown
from ahriman.application.handlers.repositories import Repositories from ahriman.application.handlers.repositories import Repositories
from ahriman.application.handlers.restore import Restore 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.search import Search
from ahriman.application.handlers.service_updates import ServiceUpdates from ahriman.application.handlers.service_updates import ServiceUpdates
from ahriman.application.handlers.setup import Setup from ahriman.application.handlers.setup import Setup

View File

@ -23,6 +23,7 @@ from ahriman.application.application import Application
from ahriman.application.handlers import Handler from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.packagers import Packagers from ahriman.models.packagers import Packagers
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId 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 = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
application.on_start() application.on_start()
application.add(args.package, args.source, args.username) 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: if not args.now:
return return

View File

@ -39,8 +39,8 @@ class Handler:
ALLOW_MULTI_ARCHITECTURE_RUN(bool): (class attribute) allow running with multiple architectures ALLOW_MULTI_ARCHITECTURE_RUN(bool): (class attribute) allow running with multiple architectures
Examples: Examples:
Wrapper for all command line actions, though each derived class implements ``run`` method, it usually must not Wrapper for all command line actions, though each derived class implements :func:`run()` method, it usually
be called directly. The recommended way is to call ``execute`` class method, e.g.:: must not be called directly. The recommended way is to call :func:`execute()` class method, e.g.::
>>> from ahriman.application.handlers import Add >>> from ahriman.application.handlers import Add
>>> >>>
@ -162,7 +162,7 @@ class Handler:
if args.repository_id is not None: if args.repository_id is not None:
separator = "/" if "/" in args.repository_id else "-" # systemd and non-systemd identifiers separator = "/" if "/" in args.repository_id else "-" # systemd and non-systemd identifiers
# repository parts is optional for backward compatibility # 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 args.architecture = architecture
if repository_parts: if repository_parts:
args.repository = "-".join(repository_parts) # replace slash with dash args.repository = "-".join(repository_parts) # replace slash with dash

View File

@ -115,7 +115,7 @@ class Patch(Handler):
package_base(str): package base package_base(str): package base
patch(PkgbuildPatch): patch descriptor patch(PkgbuildPatch): patch descriptor
""" """
application.database.patches_insert(package_base, patch) application.database.patches_insert(package_base, [patch])
@staticmethod @staticmethod
def patch_set_list(application: Application, package_base: str | None, variables: list[str] | None, def patch_set_list(application: Application, package_base: str | None, variables: list[str] | None,

View 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

View File

@ -135,7 +135,7 @@ class Setup(Handler):
configuration.set_option("auth", "salt", User.generate_password(20)) configuration.set_option("auth", "salt", User.generate_password(20))
(root.include / "00-setup-overrides.ini").unlink(missing_ok=True) # remove old-style configuration (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: with target.open("w") as ahriman_configuration:
configuration.write(ahriman_configuration) configuration.write(ahriman_configuration)

View File

@ -67,8 +67,8 @@ class Versions(Handler):
Args: Args:
root(str): root package name root(str): root package name
Returns: Yields:
Generator[tuple[str, str], None, None]: map of installed dependency to its version tuple[str, str]: map of installed dependency to its version
""" """
def dependencies_by_key(key: str) -> Generator[str, None, None]: def dependencies_by_key(key: str) -> Generator[str, None, None]:
# in importlib it returns requires in the following format # in importlib it returns requires in the following format

View File

@ -76,8 +76,8 @@ class Web(Handler):
args(argparse.Namespace): command line args args(argparse.Namespace): command line args
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
Returns: Yields:
Generator[str, None, None]: command line arguments which were used for this specific command str: command line arguments which were used for this specific command
""" """
# read configuration path from current settings # read configuration path from current settings
if (configuration_path := configuration.path) is not None: if (configuration_path := configuration.path) is not None:

View File

@ -70,9 +70,8 @@ class Lock(LazyLogging):
repository_id(RepositoryId): repository unique identifier repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
""" """
lock_suffix = f"{repository_id.name}_{repository_id.architecture}"
self.path: Path | None = \ 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.force: bool = args.force
self.unsafe: bool = args.unsafe self.unsafe: bool = args.unsafe
@ -81,6 +80,13 @@ class Lock(LazyLogging):
self.paths = configuration.repository_paths self.paths = configuration.repository_paths
self.reporter = Client.load(repository_id, configuration, report=args.report) 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: def check_version(self) -> None:
""" """
check web server version check web server version
@ -90,13 +96,6 @@ class Lock(LazyLogging):
self.logger.warning("status watcher version mismatch, our %s, their %s", self.logger.warning("status watcher version mismatch, our %s, their %s",
__version__, status.version) __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: def clear(self) -> None:
""" """
remove lock file remove lock file

View File

@ -28,15 +28,16 @@ class Remote(SyncHttpClient):
Examples: Examples:
These classes are designed to be used without instancing. In order to achieve it several class methods are 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 >>> from ahriman.core.alpm.remote import AUR, Official
>>> >>>
>>> package = AUR.info("ahriman", pacman=pacman) >>> package = AUR.info("ahriman", pacman=pacman)
>>> search_result = Official.multisearch("pacman", "manager", pacman=pacman) >>> search_result = Official.multisearch("pacman", "manager", pacman=pacman)
Differnece between ``search`` and ``multisearch`` is that ``search`` passes all arguments to underlying wrapper Differnece between :func:`search()` and :func:`multisearch()` is that :func:`search()` passes all arguments to
directly, whereas ``multisearch`` splits search one by one and finds intersection between search results. underlying wrapper directly, whereas :func:`multisearch()` splits search one by one and finds intersection
between search results.
""" """
@classmethod @classmethod

View File

@ -36,8 +36,6 @@ class Repo(LazyLogging):
uid(int): uid of the repository owner user uid(int): uid of the repository owner user
""" """
_check_output = check_output
def __init__(self, name: str, paths: RepositoryPaths, sign_args: list[str]) -> None: def __init__(self, name: str, paths: RepositoryPaths, sign_args: list[str]) -> None:
""" """
default constructor default constructor
@ -69,7 +67,7 @@ class Repo(LazyLogging):
Args: Args:
path(Path): path to archive to add path(Path): path to archive to add
""" """
Repo._check_output( check_output(
"repo-add", *self.sign_args, "-R", str(self.repo_path), str(path), "repo-add", *self.sign_args, "-R", str(self.repo_path), str(path),
exception=BuildError.from_process(path.name), exception=BuildError.from_process(path.name),
cwd=self.paths.repository, cwd=self.paths.repository,
@ -80,8 +78,8 @@ class Repo(LazyLogging):
""" """
create empty repository database create empty repository database
""" """
Repo._check_output("repo-add", *self.sign_args, str(self.repo_path), check_output("repo-add", *self.sign_args, str(self.repo_path),
cwd=self.paths.repository, logger=self.logger, user=self.uid) cwd=self.paths.repository, logger=self.logger, user=self.uid)
def remove(self, package: str, filename: Path) -> None: def remove(self, package: str, filename: Path) -> None:
""" """
@ -96,7 +94,7 @@ class Repo(LazyLogging):
full_path.unlink() full_path.unlink()
# remove package from registry # remove package from registry
Repo._check_output( check_output(
"repo-remove", *self.sign_args, str(self.repo_path), package, "repo-remove", *self.sign_args, str(self.repo_path), package,
exception=BuildError.from_process(package), exception=BuildError.from_process(package),
cwd=self.paths.repository, cwd=self.paths.repository,

View File

@ -42,8 +42,6 @@ class Sources(LazyLogging):
DEFAULT_BRANCH = "master" # default fallback branch DEFAULT_BRANCH = "master" # default fallback branch
DEFAULT_COMMIT_AUTHOR = ("ahriman", "ahriman@localhost") DEFAULT_COMMIT_AUTHOR = ("ahriman", "ahriman@localhost")
_check_output = check_output
@staticmethod @staticmethod
def extend_architectures(sources_dir: Path, architecture: str) -> list[PkgbuildPatch]: def extend_architectures(sources_dir: Path, architecture: str) -> list[PkgbuildPatch]:
""" """
@ -82,20 +80,20 @@ class Sources(LazyLogging):
branch = remote.branch or instance.DEFAULT_BRANCH branch = remote.branch or instance.DEFAULT_BRANCH
if is_initialized_git: if is_initialized_git:
instance.logger.info("update HEAD to remote at %s using branch %s", sources_dir, branch) 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, check_output("git", "fetch", "--quiet", "--depth", "1", "origin", branch,
cwd=sources_dir, logger=instance.logger) cwd=sources_dir, logger=instance.logger)
elif remote.git_url is not None: elif remote.git_url is not None:
instance.logger.info("clone remote %s to %s using branch %s", remote.git_url, sources_dir, branch) 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", check_output("git", "clone", "--quiet", "--depth", "1", "--branch", branch, "--single-branch",
remote.git_url, str(sources_dir), cwd=sources_dir.parent, logger=instance.logger) remote.git_url, str(sources_dir), cwd=sources_dir.parent, logger=instance.logger)
else: else:
# it will cause an exception later # it will cause an exception later
instance.logger.error("%s is not initialized, but no remote provided", sources_dir) instance.logger.error("%s is not initialized, but no remote provided", sources_dir)
# and now force reset to our branch # and now force reset to our branch
Sources._check_output("git", "checkout", "--force", branch, cwd=sources_dir, logger=instance.logger) check_output("git", "checkout", "--force", branch, cwd=sources_dir, logger=instance.logger)
Sources._check_output("git", "reset", "--quiet", "--hard", f"origin/{branch}", check_output("git", "reset", "--quiet", "--hard", f"origin/{branch}",
cwd=sources_dir, logger=instance.logger) cwd=sources_dir, logger=instance.logger)
# move content if required # move content if required
# we are using full path to source directory in order to make append possible # 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 bool: True in case if there is any remote and false otherwise
""" """
instance = Sources() 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) return bool(remotes)
@staticmethod @staticmethod
@ -128,8 +126,8 @@ class Sources(LazyLogging):
instance = Sources() instance = Sources()
if not (sources_dir / ".git").is_dir(): if not (sources_dir / ".git").is_dir():
# skip initializing in case if it was already # skip initializing in case if it was already
Sources._check_output("git", "init", "--quiet", "--initial-branch", instance.DEFAULT_BRANCH, check_output("git", "init", "--quiet", "--initial-branch", instance.DEFAULT_BRANCH,
cwd=sources_dir, logger=instance.logger) cwd=sources_dir, logger=instance.logger)
# extract local files... # extract local files...
files = ["PKGBUILD", ".SRCINFO"] + [str(path) for path in Package.local_files(sources_dir)] 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 return # no changes to push, just skip action
git_url, branch = remote.git_source() 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: 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) self.logger.info("found matching files %s", found_files)
# add them to index # add them to index
args = ["--intent-to-add"] if intent_to_add else [] 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], check_output("git", "add", *args, *[str(fn.relative_to(sources_dir)) for fn in found_files],
cwd=sources_dir, logger=self.logger) cwd=sources_dir, logger=self.logger)
def commit(self, sources_dir: Path, message: str | None = None, def commit(self, sources_dir: Path, message: str | None = None,
commit_author: tuple[str, str] | None = None) -> bool: 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_NAME"] = environment["GIT_COMMITTER_NAME"] = user
environment["GIT_AUTHOR_EMAIL"] = environment["GIT_COMMITTER_EMAIL"] = email environment["GIT_AUTHOR_EMAIL"] = environment["GIT_COMMITTER_EMAIL"] = email
Sources._check_output("git", "commit", "--quiet", *args, check_output("git", "commit", "--quiet", *args, cwd=sources_dir, logger=self.logger, environment=environment)
cwd=sources_dir, logger=self.logger, environment=environment)
return True return True
@ -260,7 +257,7 @@ class Sources(LazyLogging):
Returns: Returns:
str: patch as plain string 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: 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 bool: True if there are uncommitted changes and False otherwise
""" """
# there is --exit-code argument to diff, however, there might be other process errors # 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) return bool(changes)
def move(self, pkgbuild_dir: Path, sources_dir: Path) -> None: def move(self, pkgbuild_dir: Path, sources_dir: Path) -> None:
@ -302,7 +299,7 @@ class Sources(LazyLogging):
# create patch # create patch
self.logger.info("apply patch %s from database at %s", patch.key, sources_dir) self.logger.info("apply patch %s from database at %s", patch.key, sources_dir)
if patch.is_plain_diff: if patch.is_plain_diff:
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace", check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
cwd=sources_dir, input_data=patch.serialize(), logger=self.logger) cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
else: else:
patch.write(sources_dir / "PKGBUILD") patch.write(sources_dir / "PKGBUILD")

View File

@ -45,8 +45,6 @@ class Task(LazyLogging):
uid(int): uid of the repository owner user uid(int): uid of the repository owner user
""" """
_check_output = check_output
def __init__(self, package: Package, configuration: Configuration, architecture: str, def __init__(self, package: Package, configuration: Configuration, architecture: str,
paths: RepositoryPaths) -> None: paths: RepositoryPaths) -> None:
""" """
@ -68,13 +66,13 @@ class Task(LazyLogging):
self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[]) self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_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 run package build
Args: Args:
sources_dir(Path): path to where sources are 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: Returns:
list[Path]: paths of produced packages list[Path]: paths of produced packages
@ -85,25 +83,29 @@ class Task(LazyLogging):
command.extend(["--"] + self.makepkg_flags) command.extend(["--"] + self.makepkg_flags)
self.logger.info("using %s for %s", command, self.package.base) self.logger.info("using %s for %s", command, self.package.base)
environment: dict[str, str] = {} environment: dict[str, str] = {
if packager is not None: key: value
environment["PACKAGER"] = packager for key, value in kwargs.items()
if value is not None
}
self.logger.info("using environment variables %s", environment) self.logger.info("using environment variables %s", environment)
Task._check_output( check_output(
*command, *command,
exception=BuildError.from_process(self.package.base), exception=BuildError.from_process(self.package.base),
cwd=sources_dir, cwd=sources_dir,
logger=self.logger, logger=self.logger,
user=self.uid, user=self.uid,
environment=environment) environment=environment,
)
# well it is not actually correct, but we can deal with it # well it is not actually correct, but we can deal with it
packages = Task._check_output( packages = check_output(
"makepkg", "--packagelist", "makepkg", "--packagelist",
exception=BuildError.from_process(self.package.base), exception=BuildError.from_process(self.package.base),
cwd=sources_dir, cwd=sources_dir,
logger=self.logger logger=self.logger,
environment=environment,
).splitlines() ).splitlines()
return [Path(package) for package in packages] return [Path(package) for package in packages]

View File

@ -45,8 +45,8 @@ class Configuration(configparser.RawConfigParser):
Examples: Examples:
Configuration class provides additional method in order to handle application configuration. Since this class is 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, derived from built-in :class:`configparser.RawConfigParser` class, the same flow is applicable here.
it is recommended to use ``from_path`` class method which also calls initialization methods:: Nevertheless, it is recommended to use :func:`from_path` class method which also calls initialization methods::
>>> from pathlib import Path >>> 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 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. 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 In order to get current settings, the :func:`check_loaded` method can be used. This method will raise an
``InitializeError`` in case if configuration was not yet loaded:: :exc:`ahriman.core.exceptions.InitializeError` in case if configuration was not yet loaded::
>>> path, repository_id = configuration.check_loaded() >>> path, repository_id = configuration.check_loaded()
""" """
_LEGACY_ARCHITECTURE_SPECIFIC_SECTIONS = ["web"]
ARCHITECTURE_SPECIFIC_SECTIONS = ["alpm", "build", "sign"] ARCHITECTURE_SPECIFIC_SECTIONS = ["alpm", "build", "sign"]
SYSTEM_CONFIGURATION_PATH = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini" SYSTEM_CONFIGURATION_PATH = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini"
converters: dict[str, Callable[[str], Any]] # typing guard 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 default constructor. In the most cases must not be called directly
Args: 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) to ``True``, the keys without values will be allowed (Default value = False)
""" """
configparser.RawConfigParser.__init__( configparser.RawConfigParser.__init__(
@ -160,25 +161,6 @@ class Configuration(configparser.RawConfigParser):
configuration.merge_sections(repository_id) configuration.merge_sections(repository_id)
return configuration 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 @staticmethod
def section_name(section: str, *suffixes: str | None) -> str: def section_name(section: str, *suffixes: str | None) -> str:
""" """
@ -284,18 +266,18 @@ class Configuration(configparser.RawConfigParser):
self.path = path self.path = path
self.read(self.path) self.read(self.path)
self.load_includes() # load includes
self.includes = [] # reset state
self.load_includes() # basic includes
self.load_includes(self.getpath("repository", "root")) # home directory includes
def load_includes(self, path: Path | None = None) -> None: def load_includes(self, path: Path | None = None) -> None:
""" """
load configuration includes from specified path load configuration includes from specified path
Args: 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: try:
path = path or self.include path = path or self.include
for include in sorted(path.glob("*.ini")): for include in sorted(path.glob("*.ini")):
@ -315,7 +297,7 @@ class Configuration(configparser.RawConfigParser):
""" """
self.repository_id = repository_id 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): for specific in self.override_sections(section, repository_id):
if self.has_section(specific): if self.has_section(specific):
# if there is no such section it means that there is no overrides for this arch, # 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 continue
self.remove_section(foreign) 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: def reload(self) -> None:
""" """
reload configuration if possible or raise exception otherwise 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: 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: Args:
section(str): section name section(str): section name

View File

@ -46,6 +46,6 @@ class ShellInterpolator(configparser.Interpolation):
""" """
# At the moment it seems that it is the most legit way to handle environment variables # At the moment it seems that it is the most legit way to handle environment variables
# Template behaviour is literally the same as shell # 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 # In addition, we are using shell-like variables in some cases (see :attr:`alpm.mirror` option),
# to keep them alive # thus we would like to keep them alive
return Template(value).safe_substitute(os.environ) return Template(value).safe_substitute(os.environ)

View File

@ -21,6 +21,7 @@ from sqlite3 import Connection
from ahriman.core.database.operations import Operations from ahriman.core.database.operations import Operations
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId
class BuildOperations(Operations): class BuildOperations(Operations):
@ -28,13 +29,16 @@ class BuildOperations(Operations):
operations for build queue functions 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 remove packages from build queue
Args: Args:
package_base(str | None): optional filter by package base 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: def run(connection: Connection) -> None:
connection.execute( connection.execute(
""" """
@ -43,36 +47,44 @@ class BuildOperations(Operations):
""", """,
{ {
"package_base": package_base, "package_base": package_base,
"repository": self._repository_id.id, "repository": repository_id.id,
}) })
return self.with_connection(run, commit=True) 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 retrieve packages from build queue
Args:
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
Return: Return:
list[Package]: list of packages to be built list[Package]: list of packages to be built
""" """
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> list[Package]: def run(connection: Connection) -> list[Package]:
return [ return [
Package.from_json(row["properties"]) Package.from_json(row["properties"])
for row in connection.execute( for row in connection.execute(
"""select properties from build_queue where repository = :repository""", """select properties from build_queue where repository = :repository""",
{"repository": self._repository_id.id} {"repository": repository_id.id}
) )
] ]
return self.with_connection(run) 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 insert packages to build queue
Args: Args:
package(Package): package to be inserted 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: def run(connection: Connection) -> None:
connection.execute( connection.execute(
""" """
@ -86,7 +98,7 @@ class BuildOperations(Operations):
{ {
"package_base": package.base, "package_base": package.base,
"properties": package.view(), "properties": package.view(),
"repository": self._repository_id.id, "repository": repository_id.id,
}) })
return self.with_connection(run, commit=True) return self.with_connection(run, commit=True)

View File

@ -33,8 +33,8 @@ class PackageOperations(Operations):
package operations package operations
""" """
def _package_remove_package_base(self, connection: Connection, package_base: str, @staticmethod
repository_id: RepositoryId) -> None: def _package_remove_package_base(connection: Connection, package_base: str, repository_id: RepositoryId) -> None:
""" """
remove package base information remove package base information
@ -50,8 +50,9 @@ class PackageOperations(Operations):
"""delete from package_bases where package_base = :package_base and repository = :repository""", """delete from package_bases where package_base = :package_base and repository = :repository""",
{"package_base": package_base, "repository": repository_id.id}) {"package_base": package_base, "repository": repository_id.id})
def _package_remove_packages(self, connection: Connection, package_base: str, @staticmethod
current_packages: Iterable[str], repository_id: RepositoryId) -> None: def _package_remove_packages(connection: Connection, package_base: str, current_packages: Iterable[str],
repository_id: RepositoryId) -> None:
""" """
remove packages belong to the package base remove packages belong to the package base
@ -74,8 +75,8 @@ class PackageOperations(Operations):
"""delete from packages where package = :package and repository = :repository""", """delete from packages where package = :package and repository = :repository""",
packages) packages)
def _package_update_insert_base(self, connection: Connection, package: Package, @staticmethod
repository_id: RepositoryId) -> None: def _package_update_insert_base(connection: Connection, package: Package, repository_id: RepositoryId) -> None:
""" """
insert base package into table insert base package into table
@ -107,8 +108,8 @@ class PackageOperations(Operations):
} }
) )
def _package_update_insert_packages(self, connection: Connection, package: Package, @staticmethod
repository_id: RepositoryId) -> None: def _package_update_insert_packages(connection: Connection, package: Package, repository_id: RepositoryId) -> None:
""" """
insert packages into table insert packages into table
@ -149,7 +150,8 @@ class PackageOperations(Operations):
""", """,
package_list) 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: repository_id: RepositoryId) -> None:
""" """
insert base package status into table insert base package status into table
@ -176,8 +178,8 @@ class PackageOperations(Operations):
"repository": repository_id.id, "repository": repository_id.id,
}) })
def _packages_get_select_package_bases(self, connection: Connection, @staticmethod
repository_id: RepositoryId) -> dict[str, Package]: def _packages_get_select_package_bases(connection: Connection, repository_id: RepositoryId) -> dict[str, Package]:
""" """
select package bases from the table 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]: repository_id: RepositoryId) -> dict[str, Package]:
""" """
select packages from the table select packages from the table
@ -223,8 +226,8 @@ class PackageOperations(Operations):
packages[row["package_base"]].packages[row["package"]] = PackageDescription.from_json(row) packages[row["package_base"]].packages[row["package"]] = PackageDescription.from_json(row)
return packages return packages
def _packages_get_select_statuses(self, connection: Connection, @staticmethod
repository_id: RepositoryId) -> dict[str, BuildStatus]: def _packages_get_select_statuses(connection: Connection, repository_id: RepositoryId) -> dict[str, BuildStatus]:
""" """
select package build statuses from the table select package build statuses from the table
@ -299,26 +302,35 @@ class PackageOperations(Operations):
return self.with_connection(lambda connection: list(run(connection))) 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 update package remote source
Args: Args:
package(Package): package properties package(Package): package properties
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
""" """
return self.with_connection( repository_id = repository_id or self._repository_id
lambda connection: self._package_update_insert_base(connection, package, self._repository_id),
commit=True)
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 get packages remotes based on current settings
Args:
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
Returns: Returns:
dict[str, RemoteSource]: map of package base to its remote sources 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]: 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 { return {
package_base: package.remote package_base: package.remote

View File

@ -18,7 +18,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from collections import defaultdict from collections import defaultdict
from sqlite3 import Connection from sqlite3 import Connection
from ahriman.core.database.operations import Operations from ahriman.core.database.operations import Operations
@ -42,16 +41,16 @@ class PatchOperations(Operations):
""" """
return self.patches_list(package_base, None).get(package_base, []) 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 insert or update patch in database
Args: Args:
package_base(str): package base to insert package_base(str): package base to insert
patch(PkgbuildPatch): patch content patches(list[PkgbuildPatch]): patch content
""" """
def run(connection: Connection) -> None: def run(connection: Connection) -> None:
connection.execute( connection.executemany(
""" """
insert into patches insert into patches
(package_base, variable, patch) (package_base, variable, patch)
@ -60,7 +59,14 @@ class PatchOperations(Operations):
on conflict (package_base, coalesce(variable, '')) do update set on conflict (package_base, coalesce(variable, '')) do update set
patch = :patch 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) 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: if variables is not None and patch.key not in variables:
continue continue
patches[package].append(patch) patches[package].append(patch)
return dict(patches) return patches
def patches_remove(self, package_base: str, variables: list[str] | None) -> None: 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: def run_many(connection: Connection) -> None:
patches = variables or [] # suppress mypy warning patches = variables or [] # suppress mypy warning
connection.executemany( 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: def run(connection: Connection) -> None:
connection.execute( connection.execute(
"""delete from patches where package_base = :package_base""", """
delete from patches where package_base = :package_base
""",
{"package_base": package_base}) {"package_base": package_base})
if variables is not None: if variables is not None:

View File

@ -35,7 +35,7 @@ class SQLite(AuthOperations, BuildOperations, LogsOperations, PackageOperations,
Examples: Examples:
Database wrapper must be used together with migration and SQLite3 setup. In order to perform it there is 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 >>> from ahriman.core.configuration import Configuration
>>> >>>

View File

@ -62,7 +62,7 @@ class BuildError(RuntimeError):
class CalledProcessError(subprocess.CalledProcessError): 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: def __init__(self, status_code: int, process: list[str], stderr: str) -> None:

View File

@ -57,6 +57,7 @@ class Printer:
""" """
return [] return []
# pylint: disable=redundant-returns-doc
def title(self) -> str | None: def title(self) -> str | None:
""" """
generate entry title from content 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: 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: Args:
verbose(bool): print all fields verbose(bool): print all fields

View File

@ -67,7 +67,7 @@ class FilteredAccessLogger(AccessLogger):
Args: Args:
request(BaseRequest): http reqeust descriptor request(BaseRequest): http reqeust descriptor
response(StreamResponse): streaming response object response(StreamResponse): streaming response object
time(float): time(float): log record timestamp
""" """
if self.is_logs_post(request) \ if self.is_logs_post(request) \
or self.is_process_get(request): or self.is_process_get(request):

View File

@ -27,8 +27,8 @@ from ahriman.models.repository_id import RepositoryId
class HttpLogHandler(logging.Handler): class HttpLogHandler(logging.Handler):
""" """
handler for the http logging. Because default ``logging.handlers.HTTPHandler`` does not support cookies 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 ``logging.handlers.HTTPHandler.emit`` method authorization, we have to implement own handler which overrides the :func:`logging.handlers.HTTPHandler.emit` method
Attributes: Attributes:
reporter(Client): build status reporter instance reporter(Client): build status reporter instance

View File

@ -46,9 +46,11 @@ class LogLoader:
def handler(selected: LogHandler | None) -> LogHandler: 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 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 _handler suffix. Otherwise, it will try to import journald handler and returns
is available. Otherwise, it will check if there is ``/dev/log`` device and returns ``syslog_handler`` in this :attr:`ahriman.models.log_handler.LogHandler.Journald` if library is available. Otherwise, it will check if
case. And, finally, it will fall back to ``console_handler`` if none were found 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: Args:
selected(LogHandler | None): user specified handler if any selected(LogHandler | None): user specified handler if any

View File

@ -37,14 +37,14 @@ class Report(LazyLogging):
repository_id(RepositoryId): repository unique identifier repository_id(RepositoryId): repository unique identifier
Examples: Examples:
``Report`` classes provide several method in order to operate with the report generation and additional class :class:`Report` subclasses provide several method in order to operate with the report generation and additional
method ``load`` which can be used in order to determine right report instance:: class method :func:`load()` which can be used in order to determine right report instance::
>>> configuration = Configuration() >>> configuration = Configuration()
>>> report = Report.load(RepositoryId("x86_64", "aur-clone"), configuration, "email") >>> 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 The :func:`generate()` method can be used in order to perform the report itself, whereas :func:`run()` method
exception and raises ``ReportFailed`` instead:: handles exception and raises :exc:`ahriman.core.exceptions.ReportError` instead::
>>> try: >>> try:
>>> report.generate([], Result()) >>> report.generate([], Result())

View File

@ -83,7 +83,7 @@ class Executor(Cleaner):
task = Task(package, self.configuration, self.architecture, self.paths) task = Task(package, self.configuration, self.architecture, self.paths)
local_version = local_versions.get(package.base) if bump_pkgrel else None local_version = local_versions.get(package.base) if bump_pkgrel else None
task.init(local_path, self.database, local_version) 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: for src in built:
dst = self.paths.packages / src.name dst = self.paths.packages / src.name
shutil.move(src, dst) shutil.move(src, dst)

View File

@ -123,11 +123,11 @@ class UpdateHandler(Cleaner):
local = packages.get(remote.base) local = packages.get(remote.base)
if local is None: if local is None:
self.reporter.set_unknown(remote) continue # we don't add packages automatically
result.append(remote)
elif local.is_outdated(remote, self.paths, if local.is_outdated(remote, self.paths,
vcs_allowed_age=self.vcs_allowed_age, vcs_allowed_age=self.vcs_allowed_age,
calculate_version=vcs): calculate_version=vcs):
self.reporter.set_pending(local.base) self.reporter.set_pending(local.base)
result.append(remote) result.append(remote)
except Exception: except Exception:

View File

@ -36,8 +36,6 @@ class GPG(SyncHttpClient):
targets(set[SignSettings]): list of targets to sign (repository, package etc.) targets(set[SignSettings]): list of targets to sign (repository, package etc.)
""" """
_check_output = check_output
def __init__(self, configuration: Configuration) -> None: def __init__(self, configuration: Configuration) -> None:
""" """
default constructor default constructor
@ -140,7 +138,7 @@ class GPG(SyncHttpClient):
Returns: Returns:
str: PGP key in .asc format 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: def key_fingerprint(self, key: str) -> str:
""" """
@ -152,7 +150,7 @@ class GPG(SyncHttpClient):
Returns: Returns:
str: full PGP key fingerprint 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 # fingerprint line will be like
# fpr:::::::::43A663569A07EE1E4ECC55CC7E3A4240CE3C45C2: # fpr:::::::::43A663569A07EE1E4ECC55CC7E3A4240CE3C45C2:
fingerprint = next(filter(lambda line: line[:3] == "fpr", metadata.splitlines())) fingerprint = next(filter(lambda line: line[:3] == "fpr", metadata.splitlines()))
@ -167,7 +165,7 @@ class GPG(SyncHttpClient):
key(str): key ID to import key(str): key ID to import
""" """
key_body = self.key_download(server, key) 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]: def process(self, path: Path, key: str) -> list[Path]:
""" """
@ -180,7 +178,7 @@ class GPG(SyncHttpClient):
Returns: Returns:
list[Path]: list of generated files including original file list[Path]: list of generated files including original file
""" """
GPG._check_output( check_output(
*GPG.sign_command(path, key), *GPG.sign_command(path, key),
exception=BuildError.from_process(path.name), exception=BuildError.from_process(path.name),
logger=self.logger) logger=self.logger)
@ -215,7 +213,7 @@ class GPG(SyncHttpClient):
sign repository if required by configuration sign repository if required by configuration
Notes: 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: Args:
path(Path): path to repository database path(Path): path to repository database

View File

@ -28,6 +28,7 @@ from multiprocessing import Process, Queue
from threading import Lock, Thread from threading import Lock, Thread
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.process_status import ProcessStatus from ahriman.models.process_status import ProcessStatus
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
@ -82,7 +83,8 @@ class Spawn(Thread, LazyLogging):
helper to run external process helper to run external process
Args: 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 args(argparse.Namespace): command line arguments
repository_id(RepositoryId): repository unique identifier repository_id(RepositoryId): repository unique identifier
process_id(str): process unique identifier process_id(str): process unique identifier
@ -96,7 +98,8 @@ class Spawn(Thread, LazyLogging):
queue.put(ProcessStatus(process_id, result, consumed_time)) 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 spawn external ahriman process with supplied arguments
@ -104,7 +107,7 @@ class Spawn(Thread, LazyLogging):
repository_id(RepositoryId): repository unique identifier repository_id(RepositoryId): repository unique identifier
command(str): subcommand to run command(str): subcommand to run
*args(str): positional command arguments *args(str): positional command arguments
**kwargs(str): named command arguments **kwargs(str | list[str] | None): named command arguments
Returns: Returns:
str: spawned process identifier str: spawned process identifier
@ -118,9 +121,13 @@ class Spawn(Thread, LazyLogging):
for argument, value in kwargs.items(): for argument, value in kwargs.items():
if value is None: if value is None:
continue # skip null values continue # skip null values
arguments.append(f"--{argument}") flag = f"--{argument}"
if value: if isinstance(value, list):
arguments.append(value) 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()) process_id = str(uuid.uuid4())
self.logger.info("full command line arguments of %s are %s using repository %s", 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 check if given process is alive
Args: 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: Returns:
bool: True in case if process still counts as active and False otherwise 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) return self._spawn_process(repository_id, "service-key-import", key, **kwargs)
def packages_add(self, repository_id: RepositoryId, packages: Iterable[str], username: str | None, *, def packages_add(self, repository_id: RepositoryId, packages: Iterable[str], username: str | None, *,
now: bool) -> str: patches: list[PkgbuildPatch], now: bool) -> str:
""" """
add packages add packages
@ -175,14 +182,18 @@ class Spawn(Thread, LazyLogging):
repository_id(RepositoryId): repository unique identifier repository_id(RepositoryId): repository unique identifier
packages(Iterable[str]): packages list to add packages(Iterable[str]): packages list to add
username(str | None): optional override of username for build process username(str | None): optional override of username for build process
patches(list[PkgbuildPatch]): list of patches to be passed
now(bool): build packages now now(bool): build packages now
Returns: Returns:
str: spawned process identifier str: spawned process identifier
""" """
kwargs = {"username": username} kwargs: dict[str, str | list[str] | None] = {"username": username}
if now: if now:
kwargs["now"] = "" kwargs["now"] = ""
if patches:
kwargs["variable"] = [patch.serialize() for patch in patches]
return self._spawn_process(repository_id, "package-add", *packages, **kwargs) return self._spawn_process(repository_id, "package-add", *packages, **kwargs)
def packages_rebuild(self, repository_id: RepositoryId, depends_on: str, username: str | None) -> str: def packages_rebuild(self, repository_id: RepositoryId, depends_on: str, username: str | None) -> str:

View File

@ -104,7 +104,7 @@ class Client:
def package_update(self, package_base: str, status: BuildStatusEnum) -> None: def package_update(self, package_base: str, status: BuildStatusEnum) -> None:
""" """
update package build status. Unlike ``add`` it does not update package properties update package build status. Unlike :func:`package_add()` it does not update package properties
Args: Args:
package_base(str): package base to update package_base(str): package base to update

View File

@ -23,6 +23,7 @@ from ahriman.core.log import LazyLogging
from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.log_record_id import LogRecordId from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
@ -32,8 +33,8 @@ class Watcher(LazyLogging):
Attributes: Attributes:
database(SQLite): database instance database(SQLite): database instance
known(dict[str, tuple[Package, BuildStatus]]): list of known packages. For the most cases ``packages`` should known(dict[str, tuple[Package, BuildStatus]]): list of known packages. For the most cases :attr:`packages`
be used instead should be used instead
repository_id(RepositoryId): repository unique identifier repository_id(RepositoryId): repository unique identifier
status(BuildStatus): daemon status status(BuildStatus): daemon status
""" """
@ -162,6 +163,40 @@ class Watcher(LazyLogging):
self.known[package_base] = (package, full_status) self.known[package_base] = (package, full_status)
self.database.package_update(package, full_status, self.repository_id) 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: def status_update(self, status: BuildStatusEnum) -> None:
""" """
update service status update service status

View File

@ -245,7 +245,7 @@ class WebClient(Client, SyncHttpClient):
def package_update(self, package_base: str, status: BuildStatusEnum) -> None: def package_update(self, package_base: str, status: BuildStatusEnum) -> None:
""" """
update package build status. Unlike ``add`` it does not update package properties update package build status. Unlike :func:`package_add()` it does not update package properties
Args: Args:
package_base(str): package base to update package_base(str): package base to update

View File

@ -182,7 +182,7 @@ class Tree:
Returns: Returns:
list[list[Package]]: list of packages lists based on their dependencies. The amount of elements in each 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: Raises:
PartitionError: in case if it is impossible to divide tree by specified amount of partitions PartitionError: in case if it is impossible to divide tree by specified amount of partitions

View File

@ -88,8 +88,8 @@ class TriggerLoader(LazyLogging):
@staticmethod @staticmethod
def known_triggers(configuration: Configuration) -> list[str]: def known_triggers(configuration: Configuration) -> list[str]:
""" """
read configuration and return list of known triggers. Unlike ``selected_triggers`` this option is used mainly read configuration and return list of known triggers. Unlike :func:`selected_triggers()` this option is used
for configuration and validation and mentioned triggers are not being executed automatically mainly for configuration and validation and mentioned triggers are not being executed automatically
Args: Args:
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
@ -141,7 +141,7 @@ class TriggerLoader(LazyLogging):
""" """
self.logger.info("load module %s from path %s", implementation, module_path) 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. # 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) loader = machinery.SourceFileLoader(implementation, module_path)
module = ModuleType(loader.name) module = ModuleType(loader.name)
loader.exec_module(module) loader.exec_module(module)

View File

@ -95,21 +95,6 @@ class GitHub(Upload, HttpUpload):
with path.open("rb") as archive: with path.open("rb") as archive:
self.make_request("POST", url, params=[("name", path.name)], data=archive, headers=headers) 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: def files_remove(self, release: dict[str, Any], local_files: dict[Path, str], remote_files: dict[str, str]) -> None:
""" """
remove files from GitHub remove files from GitHub
@ -140,6 +125,21 @@ class GitHub(Upload, HttpUpload):
continue continue
self.asset_upload(release, local_file) 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]: def release_create(self) -> dict[str, Any]:
""" """
create empty release create empty release

View File

@ -35,8 +35,6 @@ class Rsync(Upload):
remote(str): remote address to sync remote(str): remote address to sync
""" """
_check_output = check_output
def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None:
""" """
default constructor default constructor
@ -58,4 +56,4 @@ class Rsync(Upload):
path(Path): local path to sync path(Path): local path to sync
built_packages(list[Package]): list of packages which has just been built 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)

View File

@ -87,6 +87,20 @@ class S3(Upload):
suffix = f"-{len(md5s)}" if len(md5s) > 1 else "" suffix = f"-{len(md5s)}" if len(md5s) > 1 else ""
return f"{checksum.hexdigest()}{suffix}" 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 @staticmethod
def get_bucket(configuration: Configuration, section: str) -> Any: def get_bucket(configuration: Configuration, section: str) -> Any:
""" """
@ -105,20 +119,6 @@ class S3(Upload):
aws_secret_access_key=configuration.get(section, "secret_key")) aws_secret_access_key=configuration.get(section, "secret_key"))
return client.Bucket(configuration.get(section, "bucket")) 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: def files_upload(self, path: Path, local_files: dict[Path, str], remote_objects: dict[Path, Any]) -> None:
""" """
upload changed files to s3 upload changed files to s3

View File

@ -39,14 +39,14 @@ class Upload(LazyLogging):
Examples: Examples:
These classes provide the way to upload packages to remote sources as it is described in their implementations. 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 Basic flow includes class instantiating by using the :func:`load` method and then calling the :func:`run` method
wraps any internal exceptions into the ``SyncFailed`` exception:: which wraps any internal exceptions into the :exc:`ahriman.core.exceptions.SynchronizationError` exception::
>>> configuration = Configuration() >>> configuration = Configuration()
>>> upload = Upload.load(RepositoryId("x86_64", "aur-clone"), configuration, "s3") >>> upload = Upload.load(RepositoryId("x86_64", "aur-clone"), configuration, "s3")
>>> upload.run(configuration.repository_paths.repository, []) >>> 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: >>> try:
>>> upload.sync(configuration.repository_paths.repository, []) >>> upload.sync(configuration.repository_paths.repository, [])

View File

@ -56,6 +56,7 @@ __all__ = [
"srcinfo_property", "srcinfo_property",
"srcinfo_property_list", "srcinfo_property_list",
"trim_package", "trim_package",
"unquote",
"utcnow", "utcnow",
"walk", "walk",
] ]
@ -75,8 +76,8 @@ def check_output(*args: str, exception: Exception | Callable[[int, list[str], st
Args: Args:
*args(str): command line arguments *args(str): command line arguments
exception(Exception | Callable[[int, list[str], str, str]] | None, optional): exception which has to be raised 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`` instead of default subprocess exception. If callable us is supplied, the
arguments will be passed (Default value = None) :exc:`subprocess.CalledProcessError` arguments will be passed (Default value = None)
cwd(Path | None, optional): current working directory (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) 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) 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: default: Any = None) -> Any:
""" """
extract property from SRCINFO. This method extracts property from package if this property is presented in 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 returned
Args: 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], *, def srcinfo_property_list(key: str, srcinfo: dict[str, Any], package_srcinfo: dict[str, Any], *,
architecture: str | None = None) -> list[Any]: architecture: str | None = None) -> list[Any]:
""" """
extract list property from SRCINFO. Unlike ``srcinfo_property`` it supposes that default return value is always extract list property from SRCINFO. Unlike :func:`srcinfo_property()` it supposes that default return value is
empty list. If ``architecture`` is supplied, then it will try to lookup for architecture specific values and will always empty list. If ``architecture`` is supplied, then it will try to lookup for architecture specific values and
append it at the end of result will append it at the end of result
Args: Args:
key(str): key to extract key(str): key to extract
@ -465,6 +466,38 @@ def trim_package(package_name: str) -> str:
return package_name 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: def utcnow() -> datetime.datetime:
""" """
get current time 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 Path: all found files in given directory with full path
Examples: 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 >>> from pathlib import Path
>>> >>>

View File

@ -17,10 +17,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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 base action enumeration

View File

@ -59,7 +59,7 @@ class AURPackage:
keywords(list[str]): list of package keywords keywords(list[str]): list of package keywords
Examples: 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 >>> package = AURPackage.from_json(metadata) # load package from json dump
>>> # ...or alternatively... >>> # ...or alternatively...

View File

@ -19,10 +19,10 @@
# #
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import StrEnum
class AuthSettings(str, Enum): class AuthSettings(StrEnum):
""" """
web authorization type web authorization type

View File

@ -18,13 +18,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from dataclasses import dataclass, field, fields from dataclasses import dataclass, field, fields
from enum import Enum from enum import StrEnum
from typing import Any, Self from typing import Any, Self
from ahriman.core.util import filter_json, pretty_datetime, utcnow from ahriman.core.util import filter_json, pretty_datetime, utcnow
class BuildStatusEnum(str, Enum): class BuildStatusEnum(StrEnum):
""" """
build status enumeration build status enumeration

View File

@ -17,10 +17,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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 log handler as described by default configuration

View File

@ -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). 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 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 be used: :func:`from_archive()`, :func:`from_aur()`, :func:`from_build()`, :func:`from_official()` for sources
``PackageSource.Archive``, ``PackageSource.AUR``, ``PackageSource.Local`` and ``PackageSource.Repository`` :attr:`ahriman.models.package_source.PackageSource.Archive`,
repsectively: :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) >>> pacman_package = Package.from_official("pacman", pacman)
""" """
@ -80,8 +82,6 @@ class Package(LazyLogging):
packages: dict[str, PackageDescription] packages: dict[str, PackageDescription]
packager: str | None = None packager: str | None = None
_check_output = check_output
@property @property
def depends(self) -> list[str]: def depends(self) -> list[str]:
""" """
@ -259,7 +259,7 @@ class Package(LazyLogging):
Raises: Raises:
PackageInfoError: if there are parsing errors 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) srcinfo, errors = parse_srcinfo(srcinfo_source)
if errors: if errors:
raise PackageInfoError(errors) raise PackageInfoError(errors)
@ -356,14 +356,14 @@ class Package(LazyLogging):
Args: Args:
path(Path): path to package sources directory path(Path): path to package sources directory
Returns: Yields:
Generator[Path, None, None]: list of paths of files which belong to the package and distributed together Path: list of paths of files which belong to the package and distributed together with this tarball.
with this tarball. All paths are relative to the ``path`` All paths are relative to the ``path``
Raises: Raises:
PackageInfoError: if there are parsing errors 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) srcinfo, errors = parse_srcinfo(srcinfo_source)
if errors: if errors:
raise PackageInfoError(errors) raise PackageInfoError(errors)
@ -400,7 +400,7 @@ class Package(LazyLogging):
Raises: Raises:
PackageInfoError: if there are parsing errors 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) srcinfo, errors = parse_srcinfo(srcinfo_source)
if errors: if errors:
raise PackageInfoError(errors) raise PackageInfoError(errors)
@ -448,11 +448,10 @@ class Package(LazyLogging):
try: try:
# update pkgver first # update pkgver first
Package._check_output("makepkg", "--nodeps", "--nobuild", check_output("makepkg", "--nodeps", "--nobuild", cwd=paths.cache_for(self.base), logger=self.logger)
cwd=paths.cache_for(self.base), logger=self.logger)
# generate new .SRCINFO and put it to parser # generate new .SRCINFO and put it to parser
srcinfo_source = Package._check_output("makepkg", "--printsrcinfo", srcinfo_source = check_output("makepkg", "--printsrcinfo",
cwd=paths.cache_for(self.base), logger=self.logger) cwd=paths.cache_for(self.base), logger=self.logger)
srcinfo, errors = parse_srcinfo(srcinfo_source) srcinfo, errors = parse_srcinfo(srcinfo_source)
if errors: if errors:
raise PackageInfoError(errors) raise PackageInfoError(errors)
@ -543,7 +542,7 @@ class Package(LazyLogging):
def next_pkgrel(self, local_version: str) -> str | None: 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 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 returned
Args: Args:
@ -551,8 +550,8 @@ class Package(LazyLogging):
Returns: Returns:
str | None: new generated package release version if any. In case if the release contains dot (e.g. 1.2), 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 the minor part will be incremented by 1. If the release does not contain major.minor notation, the minor
equals to 1 will be appended version equals to 1 will be appended
""" """
epoch, pkgver, _ = parse_version(self.version) epoch, pkgver, _ = parse_version(self.version)
local_epoch, local_pkgver, local_pkgrel = parse_version(local_version) local_epoch, local_pkgver, local_pkgrel = parse_version(local_version)

View File

@ -48,8 +48,9 @@ class PackageDescription:
url(str | None): package url url(str | None): package url
Examples: Examples:
Unlike the ``Package`` class, this implementation only holds properties. The recommended way to deal with it is Unlike the :class:`ahriman.models.package.Package` class, this implementation only holds properties.
to read data based on the source type - either json or ``pyalpm.Package`` instance:: 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) >>> description = PackageDescription.from_json(dump)
>>> >>>

View File

@ -19,7 +19,7 @@
# #
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import StrEnum
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse
@ -27,7 +27,7 @@ from ahriman.core.util import package_like
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
class PackageSource(str, Enum): class PackageSource(StrEnum):
""" """
package source for addition enumeration package source for addition enumeration
@ -41,11 +41,13 @@ class PackageSource(str, Enum):
Repository(PackageSource): (class attribute) source is official repository Repository(PackageSource): (class attribute) source is official repository
Examples: 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) >>> 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" Auto = "auto"

View File

@ -17,10 +17,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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 pacman database synchronization flag

View File

@ -19,8 +19,11 @@
# #
import shlex import shlex
from dataclasses import dataclass, field from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Self
from ahriman.core.util import dataclass_view, unquote
@dataclass(frozen=True) @dataclass(frozen=True)
@ -33,12 +36,12 @@ class PkgbuildPatch:
considered as full PKGBUILD diffs considered as full PKGBUILD diffs
value(str | list[str]): value of the stored PKGBUILD property. It must be either string or list of string value(str | list[str]): value of the stored PKGBUILD property. It must be either string or list of string
values values
unsafe(bool): if set, value will be not quoted, might break PKGBUILD
""" """
key: str | None key: str | None
value: str | list[str] value: str | list[str]
unsafe: bool = field(default=False, kw_only=True)
quote = shlex.quote
def __post_init__(self) -> None: def __post_init__(self) -> None:
""" """
@ -66,17 +69,26 @@ class PkgbuildPatch:
""" """
return self.key is None 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: Args:
value(str): value to be quoted variable(str): variable in bash form, i.e. KEY=VALUE
Returns: 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: def serialize(self) -> str:
""" """
@ -88,14 +100,23 @@ class PkgbuildPatch:
str: serialized key-value pair, print-friendly str: serialized key-value pair, print-friendly
""" """
if isinstance(self.value, list): # list like 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})""" return f"""{self.key}=({value})"""
if self.is_plain_diff: # no additional logic for plain diffs if self.is_plain_diff: # no additional logic for plain diffs
return self.value return self.value
# we suppose that function values are only supported in string-like values # we suppose that function values are only supported in string-like values
if self.is_function: if self.is_function:
return f"{self.key} {self.value}" # no quoting enabled here 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: def write(self, pkgbuild_path: Path) -> None:
""" """

View File

@ -19,10 +19,10 @@
# #
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import StrEnum
class ReportSettings(str, Enum): class ReportSettings(StrEnum):
""" """
report targets enumeration report targets enumeration

View File

@ -34,6 +34,18 @@ class RepositoryId:
architecture: str architecture: str
name: 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 @property
def is_empty(self) -> bool: def is_empty(self) -> bool:
""" """
@ -44,16 +56,6 @@ class RepositoryId:
""" """
return not self.architecture or not self.name 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]]: def query(self) -> list[tuple[str, str]]:
""" """
generate query parameters generate query parameters

View File

@ -75,7 +75,7 @@ class RepositoryPaths(LazyLogging):
Returns: Returns:
Path: relative path which contains only architecture segment in case if legacy tree is used and repository 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 not self._force_current_tree:
if (self._repository_root / self.repository_id.architecture).is_dir(): if (self._repository_root / self.repository_id.architecture).is_dir():
@ -181,7 +181,7 @@ class RepositoryPaths(LazyLogging):
Returns: Returns:
set[str]: list of repository names for which there is created tree. Returns empty set in case if repository 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 # simply walk through the root. In case if there are subdirectories, emit the name
def walk(paths: RepositoryPaths) -> Generator[str, None, None]: def walk(paths: RepositoryPaths) -> Generator[str, None, None]:

View File

@ -19,10 +19,10 @@
# #
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import StrEnum
class SignSettings(str, Enum): class SignSettings(StrEnum):
""" """
sign targets enumeration sign targets enumeration

View File

@ -19,10 +19,10 @@
# #
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import StrEnum
class SmtpSSLSettings(str, Enum): class SmtpSSLSettings(StrEnum):
""" """
SMTP SSL mode enumeration SMTP SSL mode enumeration

View File

@ -19,10 +19,10 @@
# #
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import StrEnum
class UploadSettings(str, Enum): class UploadSettings(StrEnum):
""" """
remote synchronization targets enumeration remote synchronization targets enumeration

View File

@ -43,7 +43,8 @@ class User:
>>> password = User.generate_password(24) >>> password = User.generate_password(24)
>>> user = User(username="ahriman", password=password, access=UserAccess.Full) >>> user = User(username="ahriman", password=password, access=UserAccess.Full)
Since the password supplied may be plain text, the ``hash_password`` method can be used to hash the password:: Since the password supplied may be plain text, the :func:`hash_password()` method can be used to hash
the password::
>>> user = user.hash_password("salt") >>> user = user.hash_password("salt")

View File

@ -19,10 +19,10 @@
# #
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import StrEnum
class UserAccess(str, Enum): class UserAccess(StrEnum):
""" """
web user access enumeration web user access enumeration

View File

@ -48,8 +48,8 @@ class Waiter:
check if timer is out check if timer is out
Returns: Returns:
bool: True in case current monotonic time is more than ``Waiter.start_time`` and bool: True in case current monotonic time is more than :attr:`start_time` and :attr:`wait_timeout`
``Waiter.wait_timeout`` doesn't equal to 0 doesn't equal to 0
""" """
since_start: float = time.monotonic() - self.start_time since_start: float = time.monotonic() - self.start_time
return self.wait_timeout != 0 and since_start > self.wait_timeout return self.wait_timeout != 0 and since_start > self.wait_timeout

View File

@ -41,8 +41,8 @@ def _dynamic_routes(module_root: Path) -> dict[str, Type[View]]:
Returns: Returns:
dict[str, Type[View]]: map of the route to its view dict[str, Type[View]]: map of the route to its view
""" """
def is_base_view(clz: Any) -> TypeGuard[Type[BaseView]]: def is_base_view(clazz: Any) -> TypeGuard[Type[BaseView]]:
return isinstance(clz, type) and issubclass(clz, BaseView) return isinstance(clazz, type) and issubclass(clazz, BaseView)
routes: dict[str, Type[View]] = {} routes: dict[str, Type[View]] = {}
for module_info in _modules(module_root): for module_info in _modules(module_root):
@ -68,7 +68,7 @@ def _module(module_info: ModuleInfo) -> ModuleType:
ModuleType: loaded module ModuleType: loaded module
Raises: Raises:
ValueError: if loader is not an instance of ``SourceFileLoader`` ValueError: if loader is not an instance of :class:`importlib.machinery.SourceFileLoader`
""" """
module_spec = module_info.module_finder.find_spec(module_info.name, None) module_spec = module_info.module_finder.find_spec(module_info.name, None)
if module_spec is None: if module_spec is None:

View File

@ -25,14 +25,17 @@ from ahriman.web.schemas.file_schema import FileSchema
from ahriman.web.schemas.internal_status_schema import InternalStatusSchema from ahriman.web.schemas.internal_status_schema import InternalStatusSchema
from ahriman.web.schemas.log_schema import LogSchema from ahriman.web.schemas.log_schema import LogSchema
from ahriman.web.schemas.login_schema import LoginSchema from ahriman.web.schemas.login_schema import LoginSchema
from ahriman.web.schemas.logs_schema import LogsSchema, LogsSchemaV2 from ahriman.web.schemas.logs_schema import LogsSchema
from ahriman.web.schemas.oauth2_schema import OAuth2Schema from ahriman.web.schemas.oauth2_schema import OAuth2Schema
from ahriman.web.schemas.package_name_schema import PackageNameSchema from ahriman.web.schemas.package_name_schema import PackageNameSchema
from ahriman.web.schemas.package_names_schema import PackageNamesSchema from ahriman.web.schemas.package_names_schema import PackageNamesSchema
from ahriman.web.schemas.package_patch_schema import PackagePatchSchema
from ahriman.web.schemas.package_properties_schema import PackagePropertiesSchema from ahriman.web.schemas.package_properties_schema import PackagePropertiesSchema
from ahriman.web.schemas.package_schema import PackageSchema from ahriman.web.schemas.package_schema import PackageSchema
from ahriman.web.schemas.package_status_schema import PackageStatusSimplifiedSchema, PackageStatusSchema from ahriman.web.schemas.package_status_schema import PackageStatusSimplifiedSchema, PackageStatusSchema
from ahriman.web.schemas.pagination_schema import PaginationSchema from ahriman.web.schemas.pagination_schema import PaginationSchema
from ahriman.web.schemas.patch_name_schema import PatchNameSchema
from ahriman.web.schemas.patch_schema import PatchSchema
from ahriman.web.schemas.pgp_key_id_schema import PGPKeyIdSchema from ahriman.web.schemas.pgp_key_id_schema import PGPKeyIdSchema
from ahriman.web.schemas.pgp_key_schema import PGPKeySchema from ahriman.web.schemas.pgp_key_schema import PGPKeySchema
from ahriman.web.schemas.process_id_schema import ProcessIdSchema from ahriman.web.schemas.process_id_schema import ProcessIdSchema
@ -41,4 +44,5 @@ from ahriman.web.schemas.remote_schema import RemoteSchema
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
from ahriman.web.schemas.search_schema import SearchSchema from ahriman.web.schemas.search_schema import SearchSchema
from ahriman.web.schemas.status_schema import StatusSchema from ahriman.web.schemas.status_schema import StatusSchema
from ahriman.web.schemas.versioned_log_schema import VersionedLogSchema
from ahriman.web.schemas.update_flags_schema import UpdateFlagsSchema from ahriman.web.schemas.update_flags_schema import UpdateFlagsSchema

View File

@ -17,13 +17,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from marshmallow import fields from marshmallow import Schema, fields
from ahriman import __version__
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
class LogSchema(RepositoryIdSchema): class LogSchema(Schema):
""" """
request package log schema request package log schema
""" """
@ -32,10 +29,6 @@ class LogSchema(RepositoryIdSchema):
"description": "Log record timestamp", "description": "Log record timestamp",
"example": 1680537091.233495, "example": 1680537091.233495,
}) })
version = fields.Integer(required=True, metadata={
"description": "Package version to tag",
"example": __version__,
})
message = fields.String(required=True, metadata={ message = fields.String(required=True, metadata={
"description": "Log message", "description": "Log message",
}) })

View File

@ -27,23 +27,9 @@ class LogsSchema(Schema):
response package logs schema response package logs schema
""" """
package_base = fields.String(required=True, metadata={
"description": "Package base name",
"example": "ahriman",
})
status = fields.Nested(StatusSchema(), required=True, metadata={
"description": "Last package status",
})
logs = fields.String(required=True, metadata={ logs = fields.String(required=True, metadata={
"description": "Full package log from the last build", "description": "Full package log from the last build",
}) })
class LogsSchemaV2(Schema):
"""
response package logs api v2 schema
"""
package_base = fields.String(required=True, metadata={ package_base = fields.String(required=True, metadata={
"description": "Package base name", "description": "Package base name",
"example": "ahriman", "example": "ahriman",
@ -51,7 +37,3 @@ class LogsSchemaV2(Schema):
status = fields.Nested(StatusSchema(), required=True, metadata={ status = fields.Nested(StatusSchema(), required=True, metadata={
"description": "Last package status", "description": "Last package status",
}) })
logs = fields.List(fields.Tuple([fields.Float(), fields.String()]), required=True, metadata={ # type: ignore[no-untyped-call]
"description": "Package log records timestamp and message",
"example": [(1680537091.233495, "log record")]
})

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