mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-18 08:19:56 +00:00
Compare commits
23 Commits
2.14.1
...
3bdd2b618f
Author | SHA1 | Date | |
---|---|---|---|
3bdd2b618f | |||
a075606330 | |||
547357a705 | |||
50cd71b954 | |||
f81ebe6c3c | |||
1d85a61cc4 | |||
689de82139 | |||
5b9f35220f | |||
8fc4d7b4a5 | |||
cedf18ac7a | |||
164b6d7956 | |||
27e595cdf4 | |||
020560d341 | |||
cdef67986b | |||
dddcd0bfce | |||
a0784b7af1 | |||
4c4c9b2bfd | |||
5c34c051cb | |||
4fa44b0532 | |||
f167ce7d3b | |||
950b9e4289 | |||
264aeb7150 | |||
be7169c5df |
16
.github/workflows/setup.sh
vendored
16
.github/workflows/setup.sh
vendored
@ -8,19 +8,19 @@ set -ex
|
|||||||
# install dependencies
|
# install dependencies
|
||||||
echo -e '[arcanisrepo]\nServer = https://repo.arcanis.me/$arch\nSigLevel = Never' | tee -a /etc/pacman.conf
|
echo -e '[arcanisrepo]\nServer = https://repo.arcanis.me/$arch\nSigLevel = Never' | tee -a /etc/pacman.conf
|
||||||
# refresh the image
|
# refresh the image
|
||||||
pacman -Syu --noconfirm
|
pacman -Syyu --noconfirm
|
||||||
# main dependencies
|
# main dependencies
|
||||||
pacman -Sy --noconfirm devtools git pyalpm python-cerberus python-inflection python-passlib python-pyelftools python-requests python-srcinfo python-systemd sudo
|
pacman -S --noconfirm devtools git pyalpm python-inflection python-passlib python-pyelftools python-requests python-srcinfo python-systemd sudo
|
||||||
# make dependencies
|
# make dependencies
|
||||||
pacman -Sy --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel
|
pacman -S --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel
|
||||||
# optional dependencies
|
# optional dependencies
|
||||||
if [[ -z $MINIMAL_INSTALL ]]; then
|
if [[ -z $MINIMAL_INSTALL ]]; then
|
||||||
# VCS support
|
# VCS support
|
||||||
pacman -Sy --noconfirm breezy darcs mercurial subversion
|
pacman -S --noconfirm breezy darcs mercurial subversion
|
||||||
# web server
|
# web server
|
||||||
pacman -Sy --noconfirm python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja
|
pacman -S --noconfirm python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja
|
||||||
# additional features
|
# additional features
|
||||||
pacman -Sy --noconfirm gnupg python-boto3 rsync
|
pacman -S --noconfirm gnupg python-boto3 python-cerberus python-matplotlib rsync
|
||||||
fi
|
fi
|
||||||
# FIXME since 1.0.4 devtools requires dbus to be run, which doesn't work now in container
|
# FIXME since 1.0.4 devtools requires dbus to be run, which doesn't work now in container
|
||||||
cp "docker/systemd-nspawn.sh" "/usr/local/bin/systemd-nspawn"
|
cp "docker/systemd-nspawn.sh" "/usr/local/bin/systemd-nspawn"
|
||||||
@ -42,12 +42,12 @@ pacman -Qdtq | pacman -Rscn --noconfirm -
|
|||||||
# initial setup command as root
|
# initial setup command as root
|
||||||
[[ -z $MINIMAL_INSTALL ]] && WEB_ARGS=("--web-port" "8080")
|
[[ -z $MINIMAL_INSTALL ]] && WEB_ARGS=("--web-port" "8080")
|
||||||
ahriman -a x86_64 -r "github" service-setup --packager "ahriman bot <ahriman@example.com>" "${WEB_ARGS[@]}"
|
ahriman -a x86_64 -r "github" service-setup --packager "ahriman bot <ahriman@example.com>" "${WEB_ARGS[@]}"
|
||||||
# validate configuration
|
|
||||||
ahriman service-config-validate --exit-code
|
|
||||||
# enable services
|
# enable services
|
||||||
systemctl enable ahriman-web
|
systemctl enable ahriman-web
|
||||||
systemctl enable ahriman@x86_64-github.timer
|
systemctl enable ahriman@x86_64-github.timer
|
||||||
if [[ -z $MINIMAL_INSTALL ]]; then
|
if [[ -z $MINIMAL_INSTALL ]]; then
|
||||||
|
# validate configuration
|
||||||
|
ahriman service-config-validate --exit-code
|
||||||
# run web service (detached)
|
# run web service (detached)
|
||||||
sudo -u ahriman -- ahriman web &
|
sudo -u ahriman -- ahriman web &
|
||||||
WEB_PID=$!
|
WEB_PID=$!
|
||||||
|
2
.github/workflows/tests.sh
vendored
2
.github/workflows/tests.sh
vendored
@ -4,7 +4,7 @@
|
|||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
# install dependencies
|
# install dependencies
|
||||||
pacman --noconfirm -Syu base-devel python-tox
|
pacman --noconfirm -Syyu base-devel python-tox
|
||||||
|
|
||||||
# run test and check targets
|
# run test and check targets
|
||||||
tox
|
tox
|
||||||
|
@ -12,6 +12,7 @@ python:
|
|||||||
extra_requirements:
|
extra_requirements:
|
||||||
- docs
|
- docs
|
||||||
- s3
|
- s3
|
||||||
|
- validator
|
||||||
- web
|
- web
|
||||||
|
|
||||||
formats:
|
formats:
|
||||||
|
@ -36,6 +36,7 @@ Again, the most checks can be performed by `tox` command, though some additional
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
Very important note about this function
|
Very important note about this function
|
||||||
|
Probably multi-line
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
argument(str): an argument. This argument has
|
argument(str): an argument. This argument has
|
||||||
@ -70,6 +71,7 @@ Again, the most checks can be performed by `tox` command, though some additional
|
|||||||
Attributes:
|
Attributes:
|
||||||
CLAZZ_ATTRIBUTE(int): (class attribute) a brand-new class attribute
|
CLAZZ_ATTRIBUTE(int): (class attribute) a brand-new class attribute
|
||||||
instance_attribute(str): an instance attribute
|
instance_attribute(str): an instance attribute
|
||||||
|
with the long description
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
Very informative class usage example, e.g.::
|
Very informative class usage example, e.g.::
|
||||||
@ -82,8 +84,6 @@ Again, the most checks can be performed by `tox` command, though some additional
|
|||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
*args(Any): positional arguments
|
*args(Any): positional arguments
|
||||||
**kwargs(Any): keyword arguments
|
**kwargs(Any): keyword arguments
|
||||||
@ -91,8 +91,10 @@ Again, the most checks can be performed by `tox` command, though some additional
|
|||||||
self.instance_attribute = ""
|
self.instance_attribute = ""
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note missing comment for the `__init__` method, which is the special case.
|
||||||
|
|
||||||
* 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 `typing.Optional` (e.g. `str | None` instead of `Optional[str]`).
|
||||||
* `classmethod` should (almost) always return `Self`. In case of mypy warning (e.g. if there is a branch in which function doesn't return the instance of `cls`) consider using `staticmethod` instead.
|
* `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:
|
||||||
|
|
||||||
|
45
Dockerfile
45
Dockerfile
@ -32,11 +32,46 @@ RUN useradd -m -d "/home/build" -s "/usr/bin/nologin" build && \
|
|||||||
COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
|
COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
|
||||||
## install package dependencies
|
## install package dependencies
|
||||||
## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size
|
## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size
|
||||||
RUN pacman -Sy --noconfirm --asdeps devtools git pyalpm python-cerberus python-inflection python-passlib python-pyelftools python-requests python-srcinfo && \
|
RUN pacman -Sy --noconfirm --asdeps \
|
||||||
pacman -Sy --noconfirm --asdeps base-devel python-build python-flit python-installer python-wheel && \
|
devtools \
|
||||||
pacman -Sy --noconfirm --asdeps breezy git mercurial python-aiohttp python-boto3 python-cryptography python-jinja python-systemd rsync subversion && \
|
git \
|
||||||
runuser -u build -- install-aur-package python-aioauth-client python-webargs python-aiohttp-apispec-git python-aiohttp-cors \
|
pyalpm \
|
||||||
python-aiohttp-jinja2 python-aiohttp-session python-aiohttp-security python-requests-unixsocket2
|
python-inflection \
|
||||||
|
python-passlib \
|
||||||
|
python-pyelftools \
|
||||||
|
python-requests \
|
||||||
|
python-srcinfo \
|
||||||
|
&& \
|
||||||
|
pacman -Sy --noconfirm --asdeps \
|
||||||
|
base-devel \
|
||||||
|
python-build \
|
||||||
|
python-flit \
|
||||||
|
python-installer \
|
||||||
|
python-wheel \
|
||||||
|
&& \
|
||||||
|
pacman -Sy --noconfirm --asdeps \
|
||||||
|
breezy \
|
||||||
|
git \
|
||||||
|
mercurial \
|
||||||
|
python-aiohttp \
|
||||||
|
python-boto3 \
|
||||||
|
python-cerberus \
|
||||||
|
python-cryptography \
|
||||||
|
python-jinja \
|
||||||
|
python-matplotlib \
|
||||||
|
python-systemd \
|
||||||
|
rsync \
|
||||||
|
subversion \
|
||||||
|
&& \
|
||||||
|
runuser -u build -- install-aur-package \
|
||||||
|
python-aioauth-client \
|
||||||
|
python-webargs \
|
||||||
|
python-aiohttp-apispec-git \
|
||||||
|
python-aiohttp-cors \
|
||||||
|
python-aiohttp-jinja2 \
|
||||||
|
python-aiohttp-session \
|
||||||
|
python-aiohttp-security \
|
||||||
|
python-requests-unixsocket2
|
||||||
|
|
||||||
## FIXME since 1.0.4 devtools requires dbus to be run, which doesn't work now in container
|
## FIXME since 1.0.4 devtools requires dbus to be run, which doesn't work now in container
|
||||||
COPY "docker/systemd-nspawn.sh" "/usr/local/bin/systemd-nspawn"
|
COPY "docker/systemd-nspawn.sh" "/usr/local/bin/systemd-nspawn"
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[](https://github.com/arcan1s/ahriman/actions/workflows/tests.yml)
|
[](https://github.com/arcan1s/ahriman/actions/workflows/tests.yml)
|
||||||
[](https://github.com/arcan1s/ahriman/actions/workflows/setup.yml)
|
[](https://github.com/arcan1s/ahriman/actions/workflows/setup.yml)
|
||||||
[](https://hub.docker.com/r/arcan1s/ahriman)
|
[](https://hub.docker.com/r/arcan1s/ahriman)
|
||||||
[](https://www.codefactor.io/repository/github/arcan1s/ahriman)
|
[](https://www.codefactor.io/repository/github/arcan1s/ahriman)
|
||||||
[](https://ahriman.readthedocs.io)
|
[](https://ahriman.readthedocs.io)
|
||||||
|
|
||||||
|
@ -8,9 +8,6 @@ cat <<EOF > "/etc/ahriman.ini.d/00-docker.ini"
|
|||||||
[repository]
|
[repository]
|
||||||
root = $AHRIMAN_REPOSITORY_ROOT
|
root = $AHRIMAN_REPOSITORY_ROOT
|
||||||
|
|
||||||
[settings]
|
|
||||||
database = $AHRIMAN_REPOSITORY_ROOT/ahriman.db
|
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
host = $AHRIMAN_HOST
|
host = $AHRIMAN_HOST
|
||||||
|
|
||||||
|
@ -172,6 +172,14 @@ ahriman.application.handlers.sign module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.application.handlers.statistics module
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.application.handlers.statistics
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.application.handlers.status module
|
ahriman.application.handlers.status module
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
|
||||||
|
@ -4,6 +4,14 @@ ahriman.core.build\_tools package
|
|||||||
Submodules
|
Submodules
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
ahriman.core.build\_tools.package\_archive module
|
||||||
|
-------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.build_tools.package_archive
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.build\_tools.sources module
|
ahriman.core.build\_tools.sources module
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
|
@ -116,6 +116,14 @@ ahriman.core.database.migrations.m013\_dependencies module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.database.migrations.m014\_auditlog module
|
||||||
|
------------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.database.migrations.m014_auditlog
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
Module contents
|
Module contents
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
@ -36,6 +36,14 @@ ahriman.core.database.operations.dependencies\_operations module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.database.operations.event\_operations module
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.database.operations.event_operations
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.database.operations.logs\_operations module
|
ahriman.core.database.operations.logs\_operations module
|
||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
|
|
||||||
|
@ -44,6 +44,14 @@ ahriman.core.formatters.configuration\_printer module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.formatters.event\_stats\_printer module
|
||||||
|
----------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.formatters.event_stats_printer
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.formatters.package\_printer module
|
ahriman.core.formatters.package\_printer module
|
||||||
-----------------------------------------------
|
-----------------------------------------------
|
||||||
|
|
||||||
@ -52,6 +60,14 @@ ahriman.core.formatters.package\_printer module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.formatters.package\_stats\_printer module
|
||||||
|
------------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.formatters.package_stats_printer
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.formatters.patch\_printer module
|
ahriman.core.formatters.patch\_printer module
|
||||||
---------------------------------------------
|
---------------------------------------------
|
||||||
|
|
||||||
|
@ -60,6 +60,14 @@ ahriman.core.report.report\_trigger module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.report.rss module
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.report.rss
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.report.telegram module
|
ahriman.core.report.telegram module
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
|
@ -12,6 +12,14 @@ ahriman.core.repository.cleaner module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.repository.event\_logger module
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.repository.event_logger
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.repository.executor module
|
ahriman.core.repository.executor module
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
|
@ -68,6 +68,14 @@ ahriman.models.dependencies module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.models.event module
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.models.event
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.models.filesystem\_package module
|
ahriman.models.filesystem\_package module
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
|
|
||||||
@ -100,6 +108,14 @@ ahriman.models.log\_record\_id module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.models.metrics\_timer module
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.models.metrics_timer
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.models.migration module
|
ahriman.models.migration module
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
@ -124,14 +140,6 @@ ahriman.models.package module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.models.package\_archive module
|
|
||||||
--------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: ahriman.models.package_archive
|
|
||||||
:members:
|
|
||||||
:no-undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
ahriman.models.package\_description module
|
ahriman.models.package\_description module
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
|
||||||
|
@ -60,6 +60,22 @@ ahriman.web.schemas.error\_schema module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.schemas.event\_schema module
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.schemas.event_schema
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.schemas.event\_search\_schema module
|
||||||
|
------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.schemas.event_search_schema
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.schemas.file\_schema module
|
ahriman.web.schemas.file\_schema module
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
|
21
docs/ahriman.web.views.v1.auditlog.rst
Normal file
21
docs/ahriman.web.views.v1.auditlog.rst
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
ahriman.web.views.v1.auditlog package
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
ahriman.web.views.v1.auditlog.events module
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.views.v1.auditlog.events
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.views.v1.auditlog
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
@ -7,6 +7,7 @@ Subpackages
|
|||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 4
|
:maxdepth: 4
|
||||||
|
|
||||||
|
ahriman.web.views.v1.auditlog
|
||||||
ahriman.web.views.v1.distributed
|
ahriman.web.views.v1.distributed
|
||||||
ahriman.web.views.v1.packages
|
ahriman.web.views.v1.packages
|
||||||
ahriman.web.views.v1.service
|
ahriman.web.views.v1.service
|
||||||
|
@ -17,14 +17,33 @@ There are two variable types which have been added to default ones, they are pat
|
|||||||
|
|
||||||
Path values, except for casting to ``pathlib.Path`` type, will be also expanded to absolute paths relative to the configuration path. E.g. if path is set to ``ahriman.ini.d/logging.ini`` and root configuration path is ``/etc/ahriman.ini``, the value will be expanded to ``/etc/ahriman.ini.d/logging.ini``. In order to disable path expand, use the full path, e.g. ``/etc/ahriman.ini.d/logging.ini``.
|
Path values, except for casting to ``pathlib.Path`` type, will be also expanded to absolute paths relative to the configuration path. E.g. if path is set to ``ahriman.ini.d/logging.ini`` and root configuration path is ``/etc/ahriman.ini``, the value will be expanded to ``/etc/ahriman.ini.d/logging.ini``. In order to disable path expand, use the full path, e.g. ``/etc/ahriman.ini.d/logging.ini``.
|
||||||
|
|
||||||
Configuration allows string interpolation from environment variables, e.g.:
|
Configuration allows string interpolation from the same configuration file, e.g.:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[section]
|
||||||
|
key = ${anoher_key}
|
||||||
|
another_key = value
|
||||||
|
|
||||||
|
will read value for the ``section.key`` option from ``section.another_key``. In case if the cross-section reference is required, the ``${section:another_key}`` notation must be used. It also allows string interpolation from environment variables, e.g.:
|
||||||
|
|
||||||
.. code-block:: ini
|
.. code-block:: ini
|
||||||
|
|
||||||
[section]
|
[section]
|
||||||
key = $SECRET
|
key = $SECRET
|
||||||
|
|
||||||
will try to read value from ``SECRET`` environment variable. In case if the required environment variable wasn't found, it will keep original value (i.e. ``$SECRET`` in the example). Dollar sign can be set as ``$$``.
|
will try to read value from ``SECRET`` environment variable. In case if the required environment variable wasn't found, it will keep original value (i.e. ``$SECRET`` in the example). Dollar sign can be set as ``$$``. All those interpolations will be applied in succession and - expected to be - recursively, e.g. the following configuration:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[section1]
|
||||||
|
key = ${section2:key}
|
||||||
|
|
||||||
|
[section2]
|
||||||
|
key = ${home}
|
||||||
|
home = $HOME
|
||||||
|
|
||||||
|
will eventually lead ``section1.key`` option to be set to the value of ``HOME`` environment variable (if available).
|
||||||
|
|
||||||
There is also additional subcommand which will allow to validate configuration and print found errors. In order to do so, run ``service-config-validate`` subcommand, e.g.:
|
There is also additional subcommand which will allow to validate configuration and print found errors. In order to do so, run ``service-config-validate`` subcommand, e.g.:
|
||||||
|
|
||||||
@ -81,14 +100,13 @@ Authorized users are stored inside internal database, if any of external provide
|
|||||||
|
|
||||||
Build related configuration. Group name can refer to architecture, e.g. ``build:x86_64`` can be used for x86_64 architecture specific settings.
|
Build related configuration. Group name can refer to architecture, e.g. ``build:x86_64`` can be used for x86_64 architecture specific settings.
|
||||||
|
|
||||||
* ``allowed_scan_paths`` - paths to be used for implicit dependencies scan, scape separated list of paths, optional.
|
|
||||||
* ``archbuild_flags`` - additional flags passed to ``archbuild`` command, space separated list of strings, optional.
|
* ``archbuild_flags`` - additional flags passed to ``archbuild`` command, space separated list of strings, optional.
|
||||||
* ``blacklisted_scan_paths`` - paths to be excluded for implicit dependencies scan, scape separated list of paths, optional. Normally all elements of this option must be child paths of any of ``allowed_scan_paths`` element.
|
|
||||||
* ``build_command`` - default build command, string, required.
|
* ``build_command`` - default build command, string, required.
|
||||||
* ``ignore_packages`` - list packages to ignore during a regular update (manual update will still work), space separated list of strings, optional.
|
* ``ignore_packages`` - list packages to ignore during a regular update (manual update will still work), space separated list of strings, optional.
|
||||||
* ``include_debug_packages`` - distribute debug packages, boolean, optional, default ``yes``.
|
* ``include_debug_packages`` - distribute debug packages, boolean, optional, default ``yes``.
|
||||||
* ``makepkg_flags`` - additional flags passed to ``makepkg`` command, space separated list of strings, optional.
|
* ``makepkg_flags`` - additional flags passed to ``makepkg`` command, space separated list of strings, optional.
|
||||||
* ``makechrootpkg_flags`` - additional flags passed to ``makechrootpkg`` command, space separated list of strings, optional.
|
* ``makechrootpkg_flags`` - additional flags passed to ``makechrootpkg`` command, space separated list of strings, optional.
|
||||||
|
* ``scan_paths`` - paths to be used for implicit dependencies scan, space separated list of strings, optional. If any of those paths is matched against the path, it will be added to the allowed list.
|
||||||
* ``triggers`` - list of ``ahriman.core.triggers.Trigger`` class implementation (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of definition.
|
* ``triggers`` - list of ``ahriman.core.triggers.Trigger`` class implementation (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of definition.
|
||||||
* ``triggers_known`` - optional list of ``ahriman.core.triggers.Trigger`` class implementations which are not run automatically and used only for trigger discovery and configuration validation.
|
* ``triggers_known`` - optional list of ``ahriman.core.triggers.Trigger`` class implementations which are not run automatically and used only for trigger discovery and configuration validation.
|
||||||
* ``vcs_allowed_age`` - maximal age in seconds of the VCS packages before their version will be updated with its remote source, integer, optional, default is 7 days.
|
* ``vcs_allowed_age`` - maximal age in seconds of the VCS packages before their version will be updated with its remote source, integer, optional, default is 7 days.
|
||||||
@ -252,6 +270,7 @@ Section name must be either ``email`` (plus optional architecture name, e.g. ``e
|
|||||||
* ``password`` - SMTP password to authenticate, string, optional.
|
* ``password`` - SMTP password to authenticate, string, optional.
|
||||||
* ``port`` - SMTP port for sending emails, integer, required.
|
* ``port`` - SMTP port for sending emails, integer, required.
|
||||||
* ``receivers`` - SMTP receiver addresses, space separated list of strings, required.
|
* ``receivers`` - SMTP receiver addresses, space separated list of strings, required.
|
||||||
|
* ``rss_url`` - link to RSS feed, string, optional.
|
||||||
* ``sender`` - SMTP sender address, string, required.
|
* ``sender`` - SMTP sender address, string, required.
|
||||||
* ``ssl`` - SSL mode for SMTP connection, one of ``ssl``, ``starttls``, ``disabled``, optional, default ``disabled``.
|
* ``ssl`` - SSL mode for SMTP connection, one of ``ssl``, ``starttls``, ``disabled``, optional, default ``disabled``.
|
||||||
* ``template`` - Jinja2 template name, string, required.
|
* ``template`` - Jinja2 template name, string, required.
|
||||||
@ -267,7 +286,8 @@ Section name must be either ``html`` (plus optional architecture name, e.g. ``ht
|
|||||||
* ``type`` - type of the report, string, optional, must be set to ``html`` if exists.
|
* ``type`` - type of the report, string, optional, must be set to ``html`` if exists.
|
||||||
* ``homepage`` - link to homepage, string, optional.
|
* ``homepage`` - link to homepage, string, optional.
|
||||||
* ``link_path`` - prefix for HTML links, string, required.
|
* ``link_path`` - prefix for HTML links, string, required.
|
||||||
* ``path`` - path to html report file, string, required.
|
* ``path`` - path to HTML report file, string, required.
|
||||||
|
* ``rss_url`` - link to RSS feed, string, optional.
|
||||||
* ``template`` - Jinja2 template name, string, required.
|
* ``template`` - Jinja2 template name, string, required.
|
||||||
* ``templates`` - path to templates directories, space separated list of paths, required.
|
* ``templates`` - path to templates directories, space separated list of paths, required.
|
||||||
|
|
||||||
@ -282,6 +302,20 @@ Section name must be either ``remote-call`` (plus optional architecture name, e.
|
|||||||
* ``manual`` - update manually built packages, boolean, optional, default ``no``.
|
* ``manual`` - update manually built packages, boolean, optional, default ``no``.
|
||||||
* ``wait_timeout`` - maximum amount of time in seconds to be waited before remote process will be terminated, integer, optional, default ``-1``.
|
* ``wait_timeout`` - maximum amount of time in seconds to be waited before remote process will be terminated, integer, optional, default ``-1``.
|
||||||
|
|
||||||
|
``rss`` type
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Section name must be either ``rss`` (plus optional architecture name, e.g. ``rss:x86_64``) or random name with ``type`` set.
|
||||||
|
|
||||||
|
* ``type`` - type of the report, string, optional, must be set to ``rss`` if exists.
|
||||||
|
* ``homepage`` - link to homepage, string, optional.
|
||||||
|
* ``link_path`` - prefix for HTML links, string, required.
|
||||||
|
* ``max_entries`` - maximal amount of entries to be included to the report, negative means no limit, integer, optional, default ``-1``.
|
||||||
|
* ``path`` - path to generated RSS file, string, required.
|
||||||
|
* ``rss_url`` - link to RSS feed, string, optional.
|
||||||
|
* ``template`` - Jinja2 template name, string, required.
|
||||||
|
* ``templates`` - path to templates directories, space separated list of paths, required.
|
||||||
|
|
||||||
``telegram`` type
|
``telegram`` type
|
||||||
^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
@ -292,6 +326,7 @@ Section name must be either ``telegram`` (plus optional architecture name, e.g.
|
|||||||
* ``chat_id`` - telegram chat id, either string with ``@`` or integer value, required.
|
* ``chat_id`` - telegram chat id, either string with ``@`` or integer value, required.
|
||||||
* ``homepage`` - link to homepage, string, optional.
|
* ``homepage`` - link to homepage, string, optional.
|
||||||
* ``link_path`` - prefix for HTML links, string, required.
|
* ``link_path`` - prefix for HTML links, string, required.
|
||||||
|
* ``rss_url`` - link to RSS feed, string, optional.
|
||||||
* ``template`` - Jinja2 template name, string, required.
|
* ``template`` - Jinja2 template name, string, required.
|
||||||
* ``template_type`` - ``parse_mode`` to be passed to telegram API, one of ``MarkdownV2``, ``HTML``, ``Markdown``, string, optional, default ``HTML``.
|
* ``template_type`` - ``parse_mode`` to be passed to telegram API, one of ``MarkdownV2``, ``HTML``, ``Markdown``, string, optional, default ``HTML``.
|
||||||
* ``templates`` - path to templates directories, space separated list of paths, required.
|
* ``templates`` - path to templates directories, space separated list of paths, required.
|
||||||
|
@ -292,7 +292,7 @@ Worker nodes (applicable for all workers) config (``worker.ini``) as:
|
|||||||
|
|
||||||
Command to run worker nodes (considering there will be two workers, one is on ``8081`` port and other is on ``8082``):
|
Command to run worker nodes (considering there will be two workers, one is on ``8081`` port and other is on ``8082``):
|
||||||
|
|
||||||
.. code-block:: ini
|
.. code-block:: shell
|
||||||
|
|
||||||
docker run --privileged -p 8081:8081 -e AHRIMAN_PORT=8081 -v worker.ini:/etc/ahriman.ini.d/overrides.ini arcan1s/ahriman:latest web
|
docker run --privileged -p 8081:8081 -e AHRIMAN_PORT=8081 -v worker.ini:/etc/ahriman.ini.d/overrides.ini arcan1s/ahriman:latest web
|
||||||
docker run --privileged -p 8082:8082 -e AHRIMAN_PORT=8082 -v worker.ini:/etc/ahriman.ini.d/overrides.ini arcan1s/ahriman:latest web
|
docker run --privileged -p 8082:8082 -e AHRIMAN_PORT=8082 -v worker.ini:/etc/ahriman.ini.d/overrides.ini arcan1s/ahriman:latest web
|
||||||
|
@ -379,7 +379,7 @@ After the success build the application extracts all linked libraries and used d
|
|||||||
|
|
||||||
In order to disable this check completely, the ``--no-check-files`` flag can be used.
|
In order to disable this check completely, the ``--no-check-files`` flag can be used.
|
||||||
|
|
||||||
In addition, there is possibility to control paths which will be used for checking, by using options ``build.allowed_scan_paths`` and ``build.blacklisted_scan_paths``. Leaving ``build.allowed_scan_paths`` blank will effectively disable any check too.
|
In addition, there is possibility to control paths which will be used for checking, by using option ``build.scan_paths``, which supports regular expressions. Leaving ``build.scan_paths`` blank will effectively disable any check too.
|
||||||
|
|
||||||
How to install built packages
|
How to install built packages
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
@ -28,8 +28,8 @@ How to report by email
|
|||||||
sender = me@example.com
|
sender = me@example.com
|
||||||
user = me@example.com
|
user = me@example.com
|
||||||
|
|
||||||
How to generate index page for S3
|
How to generate index page
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
#.
|
#.
|
||||||
Install dependencies:
|
Install dependencies:
|
||||||
@ -47,10 +47,30 @@ How to generate index page for S3
|
|||||||
target = html
|
target = html
|
||||||
|
|
||||||
[html]
|
[html]
|
||||||
path = /var/lib/ahriman/repository/aur-clone/x86_64/index.html
|
path = ${repository:root}/repository/aur-clone/x86_64/index.html
|
||||||
link_path = http://example.com/aur-clone/x86_64
|
link_path = http://example.com/aur-clone/x86_64
|
||||||
|
|
||||||
After these steps ``index.html`` file will be automatically synced to S3.
|
Having this configuration, the generated ``index.html`` will be also automatically synced to remote services (e.g. S3).
|
||||||
|
|
||||||
|
How to generate RSS feed for index page
|
||||||
|
"""""""""""""""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
In addition to previous steps, the following configuration is required:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[report]
|
||||||
|
target = html rss
|
||||||
|
|
||||||
|
[html]
|
||||||
|
rss_url = ${html:link_path}/rss.xml
|
||||||
|
|
||||||
|
[rss]
|
||||||
|
link_path = ${html:link_path}
|
||||||
|
path = ${repository:root}/repository/ahriman-demo/x86_64/rss.xml
|
||||||
|
rss_url = ${html:link_path}/rss.xml
|
||||||
|
|
||||||
|
With the appended configuration, the service fill also generate ``rss.xml``, link it to generated ``index.html`` and put it together.
|
||||||
|
|
||||||
How to post build report to telegram
|
How to post build report to telegram
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
@ -7,7 +7,7 @@ pkgdesc="ArcH linux ReposItory MANager"
|
|||||||
arch=('any')
|
arch=('any')
|
||||||
url="https://github.com/arcan1s/ahriman"
|
url="https://github.com/arcan1s/ahriman"
|
||||||
license=('GPL3')
|
license=('GPL3')
|
||||||
depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-cerberus' 'python-inflection' 'python-passlib' 'python-pyelftools' 'python-requests' 'python-srcinfo')
|
depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-inflection' 'python-passlib' 'python-pyelftools' 'python-requests' 'python-srcinfo')
|
||||||
makedepends=('python-build' 'python-flit' 'python-installer' 'python-wheel')
|
makedepends=('python-build' 'python-flit' 'python-installer' 'python-wheel')
|
||||||
optdepends=('breezy: -bzr packages support'
|
optdepends=('breezy: -bzr packages support'
|
||||||
'darcs: -darcs packages support'
|
'darcs: -darcs packages support'
|
||||||
@ -20,7 +20,9 @@ optdepends=('breezy: -bzr packages support'
|
|||||||
'python-aiohttp-security: web server with authorization'
|
'python-aiohttp-security: web server with authorization'
|
||||||
'python-aiohttp-session: web server with authorization'
|
'python-aiohttp-session: web server with authorization'
|
||||||
'python-boto3: sync to s3'
|
'python-boto3: sync to s3'
|
||||||
|
'python-cerberus: configuration validator'
|
||||||
'python-cryptography: web server with authorization'
|
'python-cryptography: web server with authorization'
|
||||||
|
'python-matplotlib: usage statistics chart'
|
||||||
'python-requests-unixsocket2: client report to web server by unix socket'
|
'python-requests-unixsocket2: client report to web server by unix socket'
|
||||||
'python-jinja: html report generation'
|
'python-jinja: html report generation'
|
||||||
'python-systemd: journal support'
|
'python-systemd: journal support'
|
||||||
|
@ -6,7 +6,7 @@ logging = ahriman.ini.d/logging.ini
|
|||||||
; Perform database migrations on the application start. Do not touch this option unless you know what are you doing.
|
; Perform database migrations on the application start. Do not touch this option unless you know what are you doing.
|
||||||
;apply_migrations = yes
|
;apply_migrations = yes
|
||||||
; Path to the application SQLite database.
|
; Path to the application SQLite database.
|
||||||
database = /var/lib/ahriman/ahriman.db
|
database = ${repository:root}/ahriman.db
|
||||||
|
|
||||||
[alpm]
|
[alpm]
|
||||||
; Path to pacman system database cache.
|
; Path to pacman system database cache.
|
||||||
@ -17,7 +17,7 @@ mirror = https://geo.mirror.pkgbuild.com/$repo/os/$arch
|
|||||||
repositories = core extra multilib
|
repositories = core extra multilib
|
||||||
; Pacman's root directory. In the most cases it must point to the system root.
|
; Pacman's root directory. In the most cases it must point to the system root.
|
||||||
root = /
|
root = /
|
||||||
; Sync files databases too, which is required by deep dependencies check
|
; Sync files databases too, which is required by deep dependencies check.
|
||||||
sync_files_database = yes
|
sync_files_database = yes
|
||||||
; Use local packages cache. If this option is enabled, the service will be able to synchronize databases (available
|
; Use local packages cache. If this option is enabled, the service will be able to synchronize databases (available
|
||||||
; as additional option for some subcommands). If set to no, databases must be synchronized manually.
|
; as additional option for some subcommands). If set to no, databases must be synchronized manually.
|
||||||
@ -50,22 +50,20 @@ allow_read_only = yes
|
|||||||
;salt =
|
;salt =
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
; List of paths to be used for implicit dependency scan
|
|
||||||
allowed_scan_paths = /usr/lib
|
|
||||||
; List of additional flags passed to archbuild command.
|
; List of additional flags passed to archbuild command.
|
||||||
;archbuild_flags =
|
;archbuild_flags =
|
||||||
; List of paths to be excluded for implicit dependency scan. Usually they should be subpaths of allowed_scan_paths
|
; Path to build command.
|
||||||
blacklisted_scan_paths = /usr/lib/cmake
|
|
||||||
; Path to build command
|
|
||||||
;build_command =
|
;build_command =
|
||||||
; List of packages to be ignored during automatic updates.
|
; List of packages to be ignored during automatic updates.
|
||||||
;ignore_packages =
|
;ignore_packages =
|
||||||
; Include debug packages
|
; Include debug packages.
|
||||||
;include_debug_packages = yes
|
;include_debug_packages = yes
|
||||||
; List of additional flags passed to makechrootpkg command.
|
; List of additional flags passed to makechrootpkg command.
|
||||||
;makechrootpkg_flags =
|
;makechrootpkg_flags =
|
||||||
; List of additional flags passed to makepkg command.
|
; List of additional flags passed to makepkg command.
|
||||||
makepkg_flags = --nocolor --ignorearch
|
makepkg_flags = --nocolor --ignorearch
|
||||||
|
; List of paths to be used for implicit dependency scan. Regular expressions are supported.
|
||||||
|
scan_paths = ^usr/lib(?!/cmake).*$
|
||||||
; List of enabled triggers in the order of calls.
|
; List of enabled triggers in the order of calls.
|
||||||
triggers = ahriman.core.gitremote.RemotePullTrigger ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger ahriman.core.gitremote.RemotePushTrigger
|
triggers = ahriman.core.gitremote.RemotePullTrigger ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger ahriman.core.gitremote.RemotePushTrigger
|
||||||
; List of well-known triggers. Used only for configuration purposes.
|
; List of well-known triggers. Used only for configuration purposes.
|
||||||
@ -121,9 +119,9 @@ host = 127.0.0.1
|
|||||||
; Disable status (e.g. package status, logs, etc) endpoints. Useful for build only modes.
|
; Disable status (e.g. package status, logs, etc) endpoints. Useful for build only modes.
|
||||||
;service_only = no
|
;service_only = no
|
||||||
; Path to directory with static files.
|
; Path to directory with static files.
|
||||||
static_path = /usr/share/ahriman/templates/static
|
static_path = ${templates}/static
|
||||||
; List of directories with templates.
|
; List of directories with templates.
|
||||||
templates = /usr/share/ahriman/templates
|
templates = ${prefix}/share/ahriman/templates
|
||||||
; Path to unix socket. If none set, unix socket will be disabled.
|
; Path to unix socket. If none set, unix socket will be disabled.
|
||||||
;unix_socket =
|
;unix_socket =
|
||||||
; Allow unix socket to be world readable.
|
; Allow unix socket to be world readable.
|
||||||
@ -214,14 +212,14 @@ target = console
|
|||||||
|
|
||||||
; Console reporting trigger configuration sample.
|
; Console reporting trigger configuration sample.
|
||||||
[console]
|
[console]
|
||||||
; Trigger type name
|
; Trigger type name.
|
||||||
;type = console
|
;type = console
|
||||||
; Use utf8 symbols in output.
|
; Use utf8 symbols in output.
|
||||||
use_utf = yes
|
use_utf = yes
|
||||||
|
|
||||||
; Email reporting trigger configuration sample.
|
; Email reporting trigger configuration sample.
|
||||||
[email]
|
[email]
|
||||||
; Trigger type name
|
; Trigger type name.
|
||||||
;type = email
|
;type = email
|
||||||
; Optional URL to the repository homepage.
|
; Optional URL to the repository homepage.
|
||||||
;homepage=
|
;homepage=
|
||||||
@ -237,6 +235,8 @@ use_utf = yes
|
|||||||
;port =
|
;port =
|
||||||
; List of emails to receive the reports.
|
; List of emails to receive the reports.
|
||||||
;receivers =
|
;receivers =
|
||||||
|
; Optional link to the RSS feed.
|
||||||
|
;rss_url =
|
||||||
; Sender email.
|
; Sender email.
|
||||||
;sender =
|
;sender =
|
||||||
; SMTP server SSL mode, one of ssl, starttls, disabled.
|
; SMTP server SSL mode, one of ssl, starttls, disabled.
|
||||||
@ -246,13 +246,13 @@ template = email-index.jinja2
|
|||||||
; Template name to be used for full packages list generation (same as HTML report).
|
; Template name to be used for full packages list generation (same as HTML report).
|
||||||
;template_full =
|
;template_full =
|
||||||
; List of directories with templates.
|
; List of directories with templates.
|
||||||
templates = /usr/share/ahriman/templates
|
templates = ${prefix}/share/ahriman/templates
|
||||||
; SMTP user.
|
; SMTP user.
|
||||||
;user =
|
;user =
|
||||||
|
|
||||||
; HTML reporting trigger configuration sample.
|
; HTML reporting trigger configuration sample.
|
||||||
[html]
|
[html]
|
||||||
; Trigger type name
|
; Trigger type name.
|
||||||
;type = html
|
;type = html
|
||||||
; Optional URL to the repository homepage.
|
; Optional URL to the repository homepage.
|
||||||
;homepage=
|
;homepage=
|
||||||
@ -260,14 +260,16 @@ templates = /usr/share/ahriman/templates
|
|||||||
;link_path =
|
;link_path =
|
||||||
; Output path for the HTML report.
|
; Output path for the HTML report.
|
||||||
;path =
|
;path =
|
||||||
|
; Optional link to the RSS feed.
|
||||||
|
;rss_url =
|
||||||
; Template name to be used.
|
; Template name to be used.
|
||||||
template = repo-index.jinja2
|
template = repo-index.jinja2
|
||||||
; List of directories with templates.
|
; List of directories with templates.
|
||||||
templates = /usr/share/ahriman/templates
|
templates = ${prefix}/share/ahriman/templates
|
||||||
|
|
||||||
; Remote service callback trigger configuration sample.
|
; Remote service callback trigger configuration sample.
|
||||||
[remote-call]
|
[remote-call]
|
||||||
; Trigger type name
|
; Trigger type name.
|
||||||
;type = remote-call
|
;type = remote-call
|
||||||
; Call for AUR packages update.
|
; Call for AUR packages update.
|
||||||
;aur = no
|
;aur = no
|
||||||
@ -278,9 +280,26 @@ templates = /usr/share/ahriman/templates
|
|||||||
; Wait until remote process will be terminated in seconds.
|
; Wait until remote process will be terminated in seconds.
|
||||||
;wait_timeout = -1
|
;wait_timeout = -1
|
||||||
|
|
||||||
|
; RSS reporting trigger configuration sample.
|
||||||
|
[rss]
|
||||||
|
; Trigger type name.
|
||||||
|
;type = rss
|
||||||
|
; Optional URL to the repository homepage.
|
||||||
|
;homepage=
|
||||||
|
; Prefix for packages links. Link to a package will be formed as link_path / filename.
|
||||||
|
;link_path =
|
||||||
|
; Output path for the RSS report.
|
||||||
|
;path =
|
||||||
|
; Optional link to the RSS feed.
|
||||||
|
;rss_url =
|
||||||
|
; Template name to be used.
|
||||||
|
template = rss.jinja2
|
||||||
|
; List of directories with templates.
|
||||||
|
templates = ${prefix}/share/ahriman/templates
|
||||||
|
|
||||||
; Telegram reporting trigger configuration sample.
|
; Telegram reporting trigger configuration sample.
|
||||||
[telegram]
|
[telegram]
|
||||||
; Trigger type name
|
; Trigger type name.
|
||||||
;type = telegram
|
;type = telegram
|
||||||
; Telegram bot API key.
|
; Telegram bot API key.
|
||||||
;api_key =
|
;api_key =
|
||||||
@ -290,12 +309,14 @@ templates = /usr/share/ahriman/templates
|
|||||||
;homepage=
|
;homepage=
|
||||||
; Prefix for packages links. Link to a package will be formed as link_path / filename.
|
; Prefix for packages links. Link to a package will be formed as link_path / filename.
|
||||||
;link_path =
|
;link_path =
|
||||||
|
; Optional link to the RSS feed.
|
||||||
|
;rss_url =
|
||||||
; Template name to be used.
|
; Template name to be used.
|
||||||
template = telegram-index.jinja2
|
template = telegram-index.jinja2
|
||||||
; Telegram specific template mode, one of MarkdownV2, HTML or Markdown.
|
; Telegram specific template mode, one of MarkdownV2, HTML or Markdown.
|
||||||
;template_type = HTML
|
;template_type = HTML
|
||||||
; List of directories with templates.
|
; List of directories with templates.
|
||||||
templates = /usr/share/ahriman/templates
|
templates = ${prefix}/share/ahriman/templates
|
||||||
; HTTP request timeout in seconds.
|
; HTTP request timeout in seconds.
|
||||||
;timeout = 30
|
;timeout = 30
|
||||||
|
|
||||||
@ -306,7 +327,7 @@ target =
|
|||||||
|
|
||||||
; GitHub upload trigger configuration sample.
|
; GitHub upload trigger configuration sample.
|
||||||
[github]
|
[github]
|
||||||
; Trigger type name
|
; Trigger type name.
|
||||||
;type = github
|
;type = github
|
||||||
; GitHub repository owner username.
|
; GitHub repository owner username.
|
||||||
;owner =
|
;owner =
|
||||||
@ -323,14 +344,14 @@ target =
|
|||||||
|
|
||||||
; Remote instance upload trigger configuration sample.
|
; Remote instance upload trigger configuration sample.
|
||||||
[remote-service]
|
[remote-service]
|
||||||
; Trigger type name
|
; Trigger type name.
|
||||||
;type = remote-service
|
;type = remote-service
|
||||||
; HTTP request timeout in seconds.
|
; HTTP request timeout in seconds.
|
||||||
;timeout = 30
|
;timeout = 30
|
||||||
|
|
||||||
; rsync upload trigger configuration sample.
|
; rsync upload trigger configuration sample.
|
||||||
[rsync]
|
[rsync]
|
||||||
; Trigger type name
|
; Trigger type name.
|
||||||
;type = rsync
|
;type = rsync
|
||||||
; rsync command to run.
|
; rsync command to run.
|
||||||
command = rsync --archive --compress --partial --delete
|
command = rsync --archive --compress --partial --delete
|
||||||
@ -340,7 +361,7 @@ command = rsync --archive --compress --partial --delete
|
|||||||
|
|
||||||
; S3 upload trigger configuration sample.
|
; S3 upload trigger configuration sample.
|
||||||
[s3]
|
[s3]
|
||||||
; Trigger type name
|
; Trigger type name.
|
||||||
;type = s3
|
;type = s3
|
||||||
; AWS services access key.
|
; AWS services access key.
|
||||||
;access_key =
|
;access_key =
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[loggers]
|
[loggers]
|
||||||
keys = root,http,stderr,boto3,botocore,nose,s3transfer
|
keys = root,http,stderr,boto3,botocore,nose,s3transfer,sql
|
||||||
|
|
||||||
[handlers]
|
[handlers]
|
||||||
keys = console_handler,journald_handler,syslog_handler
|
keys = console_handler,journald_handler,syslog_handler
|
||||||
@ -64,3 +64,8 @@ propagate = 0
|
|||||||
level = INFO
|
level = INFO
|
||||||
qualname = s3transfer
|
qualname = s3transfer
|
||||||
propagate = 0
|
propagate = 0
|
||||||
|
|
||||||
|
[logger_sql]
|
||||||
|
level = INFO
|
||||||
|
qualname = sql
|
||||||
|
propagate = 0
|
||||||
|
@ -44,28 +44,28 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li>
|
<li>
|
||||||
<button id="package-add-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-add-modal" hidden>
|
<button id="package-add-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-add-modal">
|
||||||
<i class="bi bi-plus"></i> add
|
<i class="bi bi-plus"></i> add
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button id="package-update-button" class="btn dropdown-item" onclick="packagesUpdate()" hidden>
|
<button id="package-update-button" class="btn dropdown-item" onclick="packagesUpdate()">
|
||||||
<i class="bi bi-play"></i> update
|
<i class="bi bi-play"></i> update
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button id="package-rebuild-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-rebuild-modal" hidden>
|
<button id="package-rebuild-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-rebuild-modal">
|
||||||
<i class="bi bi-arrow-clockwise"></i> rebuild
|
<i class="bi bi-arrow-clockwise"></i> rebuild
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button id="package-remove-button" class="btn dropdown-item" onclick="packagesRemove()" disabled hidden>
|
<button id="package-remove-button" class="btn dropdown-item" onclick="packagesRemove()" disabled>
|
||||||
<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">
|
||||||
<i class="bi bi-key"></i><span class="d-none d-sm-inline"> import key</span>
|
<i class="bi bi-key"></i><span class="d-none d-sm-inline"> import key</span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
const alertPlaceholder = $("#alert-placeholder");
|
const alertPlaceholder = document.getElementById("alert-placeholder");
|
||||||
|
|
||||||
|
function createAlert(title, message, clz, action, id) {
|
||||||
|
id ??= md5(title + message); // MD5 id from the content
|
||||||
|
if (alertPlaceholder.querySelector(`#alert-${id}`)) return; // check if there are duplicates
|
||||||
|
|
||||||
function createAlert(title, message, clz, action) {
|
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.id = `alert-${id}`;
|
||||||
wrapper.classList.add("toast", clz);
|
wrapper.classList.add("toast", clz);
|
||||||
wrapper.role = "alert";
|
wrapper.role = "alert";
|
||||||
wrapper.ariaLive = "assertive";
|
wrapper.ariaLive = "assertive";
|
||||||
@ -19,7 +23,7 @@
|
|||||||
body.innerText = message;
|
body.innerText = message;
|
||||||
wrapper.appendChild(body);
|
wrapper.appendChild(body);
|
||||||
|
|
||||||
alertPlaceholder.append(wrapper);
|
alertPlaceholder.appendChild(wrapper);
|
||||||
const toast = new bootstrap.Toast(wrapper);
|
const toast = new bootstrap.Toast(wrapper);
|
||||||
wrapper.addEventListener("hidden.bs.toast", _ => {
|
wrapper.addEventListener("hidden.bs.toast", _ => {
|
||||||
wrapper.remove(); // bootstrap doesn't remove elements
|
wrapper.remove(); // bootstrap doesn't remove elements
|
||||||
@ -28,12 +32,12 @@
|
|||||||
toast.show();
|
toast.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showFailure(title, description, jqXHR, errorThrown) {
|
function showFailure(title, description, error) {
|
||||||
let details;
|
let details;
|
||||||
try {
|
try {
|
||||||
details = $.parseJSON(jqXHR.responseText).error; // execution handler json error response
|
details = JSON.parse(error.text).error; // execution handler json error response
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
details = errorThrown;
|
details = error.text ?? error.message ?? error;
|
||||||
}
|
}
|
||||||
createAlert(title, description(details), "text-bg-danger");
|
createAlert(title, description(details), "text-bg-danger");
|
||||||
}
|
}
|
||||||
|
@ -36,61 +36,69 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const keyImportModal = $("#key-import-modal");
|
const keyImportModal = document.getElementById("key-import-modal");
|
||||||
const keyImportForm = $("#key-import-form");
|
const keyImportForm = document.getElementById("key-import-form");
|
||||||
|
|
||||||
const keyImportBodyInput = $("#key-import-body-input");
|
const keyImportBodyInput = document.getElementById("key-import-body-input");
|
||||||
const keyImportCopyButton = $("#key-import-copy-button");
|
const keyImportCopyButton = document.getElementById("key-import-copy-button");
|
||||||
|
|
||||||
const keyImportFingerprintInput = $("#key-import-fingerprint-input");
|
const keyImportFingerprintInput = document.getElementById("key-import-fingerprint-input");
|
||||||
const keyImportServerInput = $("#key-import-server-input");
|
const keyImportServerInput = document.getElementById("key-import-server-input");
|
||||||
|
|
||||||
async function copyPgpKey() {
|
async function copyPgpKey() {
|
||||||
const logs = keyImportBodyInput.text();
|
const key = keyImportBodyInput.textContent;
|
||||||
await copyToClipboard(logs, keyImportCopyButton);
|
await copyToClipboard(key, keyImportCopyButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchPgpKey() {
|
function fetchPgpKey() {
|
||||||
const key = keyImportFingerprintInput.val();
|
const key = keyImportFingerprintInput.value;
|
||||||
const server = keyImportServerInput.val();
|
const server = keyImportServerInput.value;
|
||||||
|
|
||||||
if (key && server) {
|
if (key && server) {
|
||||||
$.ajax({
|
makeRequest(
|
||||||
url: "/api/v1/service/pgp",
|
"/api/v1/service/pgp",
|
||||||
data: {"key": key, "server": server},
|
{
|
||||||
type: "GET",
|
query: {
|
||||||
dataType: "json",
|
key: key,
|
||||||
success: response => { keyImportBodyInput.text(response.key); },
|
server: server,
|
||||||
});
|
},
|
||||||
|
convert: response => response.json(),
|
||||||
|
},
|
||||||
|
data => { keyImportBodyInput.textContent = data.key; },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function importPgpKey() {
|
function importPgpKey() {
|
||||||
const key = keyImportFingerprintInput.val();
|
const key = keyImportFingerprintInput.value;
|
||||||
const server = keyImportServerInput.val();
|
const server = keyImportServerInput.value;
|
||||||
|
|
||||||
if (key && server) {
|
if (key && server) {
|
||||||
$.ajax({
|
makeRequest(
|
||||||
url: "/api/v1/service/pgp",
|
"/api/v1/service/pgp",
|
||||||
data: JSON.stringify({key: key, server: server}),
|
{
|
||||||
type: "POST",
|
method: "POST",
|
||||||
contentType: "application/json",
|
json: {
|
||||||
success: _ => {
|
key: key,
|
||||||
keyImportModal.modal("hide");
|
server: server,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
bootstrap.Modal.getOrCreateInstance(keyImportModal).hide();
|
||||||
showSuccess("Success", `Key ${key} has been imported`);
|
showSuccess("Success", `Key ${key} has been imported`);
|
||||||
},
|
},
|
||||||
error: (jqXHR, _, errorThrown) => {
|
error => {
|
||||||
const message = _ => `Could not import key ${key} from ${server}`;
|
const message = _ => `Could not import key ${key} from ${server}`;
|
||||||
showFailure("Action failed", message, jqXHR, errorThrown);
|
showFailure("Action failed", message, error);
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(_ => {
|
ready(_ => {
|
||||||
keyImportModal.on("hidden.bs.modal", _ => {
|
keyImportModal.addEventListener("hidden.bs.modal", _ => {
|
||||||
keyImportBodyInput.text("");
|
keyImportBodyInput.textContent = "";
|
||||||
keyImportForm.trigger("reset");
|
keyImportForm.reset();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -34,53 +34,57 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const loginModal = $("#login-modal");
|
const loginModal = document.getElementById("login-modal");
|
||||||
const loginForm = $("#login-form");
|
const loginForm = document.getElementById("login-form");
|
||||||
|
|
||||||
const loginPasswordInput = $("#login-password");
|
const loginPasswordInput = document.getElementById("login-password");
|
||||||
const loginUsernameInput = $("#login-username");
|
const loginUsernameInput = document.getElementById("login-username");
|
||||||
const showHidePasswordButton = $("#login-show-hide-password-button");
|
const showHidePasswordButton = document.getElementById("login-show-hide-password-button");
|
||||||
|
|
||||||
function login() {
|
function login() {
|
||||||
const password = loginPasswordInput.val();
|
const password = loginPasswordInput.value;
|
||||||
const username = loginUsernameInput.val();
|
const username = loginUsernameInput.value;
|
||||||
|
|
||||||
if (username && password) {
|
if (username && password) {
|
||||||
$.ajax({
|
makeRequest(
|
||||||
url: "/api/v1/login",
|
"/api/v1/login",
|
||||||
data: JSON.stringify({username: username, password: password}),
|
{
|
||||||
type: "POST",
|
method: "POST",
|
||||||
contentType: "application/json",
|
json: {
|
||||||
success: _ => {
|
username: username,
|
||||||
loginModal.modal("hide");
|
password: password,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
bootstrap.Modal.getOrCreateInstance(loginModal).hide();
|
||||||
showSuccess("Logged in", `Successfully logged in as ${username}`, _ => location.href = "/");
|
showSuccess("Logged in", `Successfully logged in as ${username}`, _ => location.href = "/");
|
||||||
},
|
},
|
||||||
error: (jqXHR, _, errorThrown) => {
|
error => {
|
||||||
const message = _ =>
|
const message = _ =>
|
||||||
username === "admin" && password === "admin"
|
username === "admin" && password === "admin"
|
||||||
? "You've entered a password for user \"root\", did you make a typo in username?"
|
? "You've entered a password for user \"root\", did you make a typo in username?"
|
||||||
: `Could not login as ${username}`;
|
: `Could not login as ${username}`;
|
||||||
showFailure("Login error", message, jqXHR, errorThrown);
|
showFailure("Login error", message, error);
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showPassword() {
|
function showPassword() {
|
||||||
if (loginPasswordInput.attr("type") === "password") {
|
if (loginPasswordInput.getAttribute("type") === "password") {
|
||||||
loginPasswordInput.attr("type", "text");
|
loginPasswordInput.setAttribute("type", "text");
|
||||||
showHidePasswordButton.removeClass("bi-eye");
|
showHidePasswordButton.classList.remove("bi-eye");
|
||||||
showHidePasswordButton.addClass("bi-eye-slash");
|
showHidePasswordButton.classList.add("bi-eye-slash");
|
||||||
} else {
|
} else {
|
||||||
loginPasswordInput.attr("type", "password");
|
loginPasswordInput.setAttribute("type", "password");
|
||||||
showHidePasswordButton.removeClass("bi-eye-slash");
|
showHidePasswordButton.classList.remove("bi-eye-slash");
|
||||||
showHidePasswordButton.addClass("bi-eye");
|
showHidePasswordButton.classList.add("bi-eye");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(_ => {
|
ready(_ => {
|
||||||
loginModal.on("hidden.bs.modal", _ => {
|
loginModal.addEventListener("hidden.bs.modal", _ => {
|
||||||
loginForm.trigger("reset");
|
loginForm.reset();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -41,14 +41,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const packageAddModal = $("#package-add-modal");
|
const packageAddModal = document.getElementById("package-add-modal");
|
||||||
const packageAddForm = $("#package-add-form");
|
const packageAddForm = document.getElementById("package-add-form");
|
||||||
|
|
||||||
const packageAddInput = $("#package-add-input");
|
const packageAddInput = document.getElementById("package-add-input");
|
||||||
const packageAddRepositoryInput = $("#package-add-repository-input");
|
const packageAddRepositoryInput = document.getElementById("package-add-repository-input");
|
||||||
const packageAddKnownPackagesList = $("#package-add-known-packages-dlist");
|
const packageAddKnownPackagesList = document.getElementById("package-add-known-packages-dlist");
|
||||||
|
|
||||||
const packageAddVariablesDiv = $("#package-add-variables-div");
|
const packageAddVariablesDiv = document.getElementById("package-add-variables-div");
|
||||||
|
|
||||||
function packageAddVariableInputCreate() {
|
function packageAddVariableInputCreate() {
|
||||||
const variableInput = document.createElement("div");
|
const variableInput = document.createElement("div");
|
||||||
@ -78,7 +78,7 @@
|
|||||||
variableButtonRemove.classList.add("btn");
|
variableButtonRemove.classList.add("btn");
|
||||||
variableButtonRemove.classList.add("btn-outline-danger");
|
variableButtonRemove.classList.add("btn-outline-danger");
|
||||||
variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>";
|
variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>";
|
||||||
variableButtonRemove.onclick = _ => { return variableInput.remove(); };
|
variableButtonRemove.onclick = _ => { variableInput.remove(); };
|
||||||
|
|
||||||
// bring them together
|
// bring them together
|
||||||
variableInput.appendChild(variableNameInput);
|
variableInput.appendChild(variableNameInput);
|
||||||
@ -86,27 +86,26 @@
|
|||||||
variableInput.appendChild(variableValueInput);
|
variableInput.appendChild(variableValueInput);
|
||||||
variableInput.appendChild(variableButtonRemove);
|
variableInput.appendChild(variableButtonRemove);
|
||||||
|
|
||||||
packageAddVariablesDiv.append(variableInput);
|
packageAddVariablesDiv.appendChild(variableInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchesParse() {
|
function patchesParse() {
|
||||||
const patches = packageAddVariablesDiv.find(".package-add-variable").map((_, element) => {
|
const patches = Array.from(packageAddVariablesDiv.getElementsByClassName("package-add-variable")).map(element => {
|
||||||
const richElement = $(element);
|
|
||||||
return {
|
return {
|
||||||
key: richElement.find(".package-add-variable-name").val(),
|
key: element.querySelector(".package-add-variable-name").value,
|
||||||
value: richElement.find(".package-add-variable-value").val(),
|
value: element.querySelector(".package-add-variable-value").value,
|
||||||
};
|
};
|
||||||
}).filter((_, patch) => patch.key).get();
|
}).filter(patch => patch.key);
|
||||||
return {patches: patches};
|
return {patches: patches};
|
||||||
}
|
}
|
||||||
|
|
||||||
function packagesAdd(packages, patches, repository) {
|
function packagesAdd(packages, patches, repository) {
|
||||||
packages = packages ?? packageAddInput.val();
|
packages = packages ?? packageAddInput.value;
|
||||||
patches = patches ?? patchesParse();
|
patches = patches ?? patchesParse();
|
||||||
repository = repository ?? getRepositorySelector(packageAddRepositoryInput);
|
repository = repository ?? getRepositorySelector(packageAddRepositoryInput);
|
||||||
|
|
||||||
if (packages) {
|
if (packages) {
|
||||||
packageAddModal.modal("hide");
|
bootstrap.Modal.getOrCreateInstance(packageAddModal).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, patches);
|
doPackageAction("/api/v1/service/add", [packages], repository, onSuccess, onFailure, patches);
|
||||||
@ -114,50 +113,54 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function packagesRequest(packages, patches) {
|
function packagesRequest(packages, patches) {
|
||||||
packages = packages ?? packageAddInput.val();
|
packages = packages ?? packageAddInput.value;
|
||||||
patches = patches ?? patchesParse();
|
patches = patches ?? patchesParse();
|
||||||
const repository = getRepositorySelector(packageAddRepositoryInput);
|
const repository = getRepositorySelector(packageAddRepositoryInput);
|
||||||
|
|
||||||
if (packages) {
|
if (packages) {
|
||||||
packageAddModal.modal("hide");
|
bootstrap.Modal.getOrCreateInstance(packageAddModal).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, patches);
|
doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure, patches);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(_ => {
|
ready(_ => {
|
||||||
packageAddModal.on("shown.bs.modal", _ => {
|
packageAddModal.addEventListener("shown.bs.modal", _ => {
|
||||||
$(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
|
const option = packageAddRepositoryInput.querySelector(`option[value="${repository.architecture}-${repository.repository}"]`);
|
||||||
|
option.selected = "selected";
|
||||||
});
|
});
|
||||||
packageAddModal.on("hidden.bs.modal", _ => {
|
packageAddModal.addEventListener("hidden.bs.modal", _ => {
|
||||||
packageAddVariablesDiv.empty();
|
packageAddVariablesDiv.replaceChildren();
|
||||||
packageAddForm.trigger("reset");
|
packageAddForm.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
packageAddInput.keyup(_ => {
|
packageAddInput.addEventListener("keyup", _ => {
|
||||||
clearTimeout(packageAddInput.data("timeout"));
|
clearTimeout(packageAddInput.requestTimeout);
|
||||||
packageAddInput.data("timeout", setTimeout($.proxy(_ => {
|
packageAddInput.requestTimeout = setTimeout(_ => {
|
||||||
const value = packageAddInput.val();
|
const value = packageAddInput.value;
|
||||||
|
|
||||||
if (value.length >= 3) {
|
if (value.length >= 3) {
|
||||||
$.ajax({
|
makeRequest(
|
||||||
url: "/api/v1/service/search",
|
"/api/v1/service/search",
|
||||||
data: {"for": value},
|
{
|
||||||
type: "GET",
|
query: {
|
||||||
dataType: "json",
|
for: value,
|
||||||
success: response => {
|
},
|
||||||
const options = response.map(pkg => {
|
convert: response => response.json(),
|
||||||
|
},
|
||||||
|
data => {
|
||||||
|
const options = data.map(pkg => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = pkg.package;
|
option.value = pkg.package;
|
||||||
option.innerText = `${pkg.package} (${pkg.description})`;
|
option.innerText = `${pkg.package} (${pkg.description})`;
|
||||||
return option;
|
return option;
|
||||||
});
|
});
|
||||||
packageAddKnownPackagesList.empty().append(options);
|
packageAddKnownPackagesList.replaceChildren(...options);
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}, this), 500));
|
}, 500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -45,8 +45,9 @@
|
|||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<div class="nav nav-tabs" role="tablist">
|
<div class="nav nav-tabs" role="tablist">
|
||||||
<button id="package-info-logs-button" class="nav-link active" data-bs-toggle="tab" data-bs-target="#package-info-logs" type="button" role="tab" aria-controls="package-info-logs" aria-selected="true"><h3>Build logs</h3></button>
|
<button id="package-info-logs-button" class="nav-link active" data-bs-toggle="tab" data-bs-target="#package-info-logs" type="button" role="tab" aria-controls="package-info-logs" aria-selected="true">Build logs</button>
|
||||||
<button id="package-info-changes-button" class="nav-link" data-bs-toggle="tab" data-bs-target="#package-info-changes" type="button" role="tab" aria-controls="package-info-changes" aria-selected="false"><h3>Changes</h3></button>
|
<button id="package-info-changes-button" class="nav-link" data-bs-toggle="tab" data-bs-target="#package-info-changes" type="button" role="tab" aria-controls="package-info-changes" aria-selected="false">Changes</button>
|
||||||
|
<button id="package-info-events-button" class="nav-link" data-bs-toggle="tab" data-bs-target="#package-info-events" type="button" role="tab" aria-controls="package-info-events" aria-selected="false">Events</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="tab-content" id="nav-tabContent">
|
<div class="tab-content" id="nav-tabContent">
|
||||||
@ -56,11 +57,30 @@
|
|||||||
<div id="package-info-changes" class="tab-pane fade" role="tabpanel" aria-labelledby="package-info-changes-button" tabindex="0">
|
<div id="package-info-changes" class="tab-pane fade" role="tabpanel" aria-labelledby="package-info-changes-button" tabindex="0">
|
||||||
<pre class="language-diff"><code id="package-info-changes-input" class="pre-scrollable language-diff"></code><button id="package-info-changes-copy-button" type="button" class="btn language-diff" onclick="copyChanges()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
<pre class="language-diff"><code id="package-info-changes-input" class="pre-scrollable language-diff"></code><button id="package-info-changes-copy-button" type="button" class="btn language-diff" onclick="copyChanges()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="package-info-events" class="tab-pane fade" role="tabpanel" aria-labelledby="package-info-events-button" tabindex="0">
|
||||||
|
<canvas id="package-info-events-update-chart" hidden></canvas>
|
||||||
|
<table id="package-info-events-table"
|
||||||
|
data-classes="table table-hover"
|
||||||
|
data-sortable="true"
|
||||||
|
data-sort-name="timestamp"
|
||||||
|
data-sort-order="desc"
|
||||||
|
data-toggle="table">
|
||||||
|
<thead class="table-primary">
|
||||||
|
<tr>
|
||||||
|
<th data-align="right" data-field="timestamp">date</th>
|
||||||
|
<th data-field="event">event</th>
|
||||||
|
<th data-field="message">description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<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>
|
{% if not auth.enabled or auth.username is not none %}
|
||||||
<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 id="package-info-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal"><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button>
|
||||||
|
<button id="package-info-remove-button" type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal"><i class="bi bi-trash"></i><span class="d-none d-sm-inline"> remove</span></button>
|
||||||
|
{% endif %}
|
||||||
<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-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>
|
<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>
|
||||||
@ -69,41 +89,54 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const packageInfoModal = $("#package-info-modal");
|
const packageInfoModal = document.getElementById("package-info-modal");
|
||||||
const packageInfoModalHeader = $("#package-info-modal-header");
|
const packageInfoModalHeader = document.getElementById("package-info-modal-header");
|
||||||
const packageInfo = $("#package-info");
|
const packageInfo = document.getElementById("package-info");
|
||||||
|
|
||||||
const packageInfoLogsInput = $("#package-info-logs-input");
|
const packageInfoLogsInput = document.getElementById("package-info-logs-input");
|
||||||
const packageInfoLogsCopyButton = $("#package-info-logs-copy-button");
|
const packageInfoLogsCopyButton = document.getElementById("package-info-logs-copy-button");
|
||||||
|
|
||||||
const packageInfoChangesInput = $("#package-info-changes-input");
|
const packageInfoChangesInput = document.getElementById("package-info-changes-input");
|
||||||
const packageInfoChangesCopyButton = $("#package-info-changes-copy-button");
|
const packageInfoChangesCopyButton = document.getElementById("package-info-changes-copy-button");
|
||||||
|
|
||||||
const packageInfoAurUrl = $("#package-info-aur-url");
|
// so far bootstrap-table only operates with jquery elements
|
||||||
const packageInfoDepends = $("#package-info-depends");
|
const packageInfoEventsTable = $(document.getElementById("package-info-events-table"));
|
||||||
const packageInfoGroups = $("#package-info-groups");
|
const packageInfoEventsUpdateChartCanvas = document.getElementById("package-info-events-update-chart");
|
||||||
const packageInfoLicenses = $("#package-info-licenses");
|
let packageInfoEventsUpdateChart = null;
|
||||||
const packageInfoPackager = $("#package-info-packager");
|
|
||||||
const packageInfoPackages = $("#package-info-packages");
|
|
||||||
const packageInfoUpstreamUrl = $("#package-info-upstream-url");
|
|
||||||
const packageInfoVersion = $("#package-info-version");
|
|
||||||
|
|
||||||
const packageInfoVariablesBlock = $("#package-info-variables-block");
|
const packageInfoAurUrl = document.getElementById("package-info-aur-url");
|
||||||
const packageInfoVariablesDiv = $("#package-info-variables-div");
|
const packageInfoDepends = document.getElementById("package-info-depends");
|
||||||
|
const packageInfoGroups = document.getElementById("package-info-groups");
|
||||||
|
const packageInfoLicenses = document.getElementById("package-info-licenses");
|
||||||
|
const packageInfoPackager = document.getElementById("package-info-packager");
|
||||||
|
const packageInfoPackages = document.getElementById("package-info-packages");
|
||||||
|
const packageInfoUpstreamUrl = document.getElementById("package-info-upstream-url");
|
||||||
|
const packageInfoVersion = document.getElementById("package-info-version");
|
||||||
|
|
||||||
|
const packageInfoVariablesBlock = document.getElementById("package-info-variables-block");
|
||||||
|
const packageInfoVariablesDiv = document.getElementById("package-info-variables-div");
|
||||||
|
|
||||||
|
function clearChart() {
|
||||||
|
packageInfoEventsUpdateChartCanvas.hidden = true;
|
||||||
|
if (packageInfoEventsUpdateChart) {
|
||||||
|
packageInfoEventsUpdateChart.data = {};
|
||||||
|
packageInfoEventsUpdateChart.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function copyChanges() {
|
async function copyChanges() {
|
||||||
const changes = packageInfoChangesInput.text();
|
const changes = packageInfoChangesInput.textContent;
|
||||||
await copyToClipboard(changes, packageInfoChangesCopyButton);
|
await copyToClipboard(changes, packageInfoChangesCopyButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyLogs() {
|
async function copyLogs() {
|
||||||
const logs = packageInfoLogsInput.text();
|
const logs = packageInfoLogsInput.textContent;
|
||||||
await copyToClipboard(logs, packageInfoLogsCopyButton);
|
await copyToClipboard(logs, packageInfoLogsCopyButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideInfoControls(hidden) {
|
function highlight(element) {
|
||||||
packageInfoRemoveButton.attr("hidden", hidden);
|
delete element.dataset.highlighted;
|
||||||
packageInfoUpdateButton.attr("hidden", hidden);
|
hljs.highlightElement(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertVariable(packageBase, variable) {
|
function insertVariable(packageBase, variable) {
|
||||||
@ -130,12 +163,13 @@
|
|||||||
variableButtonRemove.classList.add("btn-outline-danger");
|
variableButtonRemove.classList.add("btn-outline-danger");
|
||||||
variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>";
|
variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>";
|
||||||
variableButtonRemove.onclick = _ => {
|
variableButtonRemove.onclick = _ => {
|
||||||
$.ajax({
|
makeRequest(
|
||||||
url: `/api/v1/packages/${packageBase}/patches/${variable.key}`,
|
`/api/v1/packages/${packageBase}/patches/${variable.key}`,
|
||||||
type: "DELETE",
|
{
|
||||||
dataType: "json",
|
method: "DELETE",
|
||||||
success: _ => variableInput.remove(),
|
},
|
||||||
});
|
_ => variableInput.remove(),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// bring them together
|
// bring them together
|
||||||
@ -144,45 +178,93 @@
|
|||||||
variableInput.appendChild(variableValueInput);
|
variableInput.appendChild(variableValueInput);
|
||||||
variableInput.appendChild(variableButtonRemove);
|
variableInput.appendChild(variableButtonRemove);
|
||||||
|
|
||||||
packageInfoVariablesDiv.append(variableInput);
|
packageInfoVariablesDiv.appendChild(variableInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadChanges(packageBase, onFailure) {
|
function loadChanges(packageBase, onFailure) {
|
||||||
$.ajax({
|
makeRequest(
|
||||||
url: `/api/v1/packages/${packageBase}/changes`,
|
`/api/v1/packages/${packageBase}/changes`,
|
||||||
data: {
|
{
|
||||||
architecture: repository.architecture,
|
query: {
|
||||||
repository: repository.repository,
|
architecture: repository.architecture,
|
||||||
|
repository: repository.repository,
|
||||||
|
},
|
||||||
|
convert: response => response.json(),
|
||||||
},
|
},
|
||||||
type: "GET",
|
data => {
|
||||||
dataType: "json",
|
const changes = data.changes;
|
||||||
success: response => {
|
packageInfoChangesInput.textContent = changes ?? "";
|
||||||
const changes = response.changes;
|
highlight(packageInfoChangesInput);
|
||||||
packageInfoChangesInput.text(changes || "");
|
|
||||||
packageInfoChangesInput.map((_, el) => hljs.highlightElement(el));
|
|
||||||
},
|
},
|
||||||
error: onFailure,
|
onFailure,
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadEvents(packageBase, onFailure) {
|
||||||
|
packageInfoEventsTable.bootstrapTable("showLoading");
|
||||||
|
clearChart();
|
||||||
|
|
||||||
|
makeRequest(
|
||||||
|
"/api/v1/events",
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
architecture: repository.architecture,
|
||||||
|
repository: repository.repository,
|
||||||
|
object_id: packageBase,
|
||||||
|
limit: 30,
|
||||||
|
},
|
||||||
|
convert: response => response.json(),
|
||||||
|
},
|
||||||
|
data => {
|
||||||
|
const events = data.map(event => {
|
||||||
|
return {
|
||||||
|
timestamp: new Date(1000 * event.created).toISOStringShort(),
|
||||||
|
event: event.event,
|
||||||
|
message: event.message || "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const chart = data.filter(event => event.event === "package-updated");
|
||||||
|
|
||||||
|
packageInfoEventsTable.bootstrapTable("load", events);
|
||||||
|
packageInfoEventsTable.bootstrapTable("hideLoading");
|
||||||
|
|
||||||
|
if (packageInfoEventsUpdateChart) {
|
||||||
|
packageInfoEventsUpdateChart.config.data = {
|
||||||
|
labels: chart.map(event => new Date(1000 * event.created).toISOStringShort()),
|
||||||
|
datasets: [{
|
||||||
|
label: "update duration, s",
|
||||||
|
data: chart.map(event => event.data.took),
|
||||||
|
cubicInterpolationMode: "monotone",
|
||||||
|
tension: 0.4,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
packageInfoEventsUpdateChart.update();
|
||||||
|
}
|
||||||
|
packageInfoEventsUpdateChartCanvas.hidden = !chart.length;
|
||||||
|
},
|
||||||
|
onFailure,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadLogs(packageBase, onFailure) {
|
function loadLogs(packageBase, onFailure) {
|
||||||
$.ajax({
|
makeRequest(
|
||||||
url: `/api/v2/packages/${packageBase}/logs`,
|
`/api/v2/packages/${packageBase}/logs`,
|
||||||
data: {
|
{
|
||||||
architecture: repository.architecture,
|
query: {
|
||||||
repository: repository.repository,
|
architecture: repository.architecture,
|
||||||
|
repository: repository.repository,
|
||||||
|
},
|
||||||
|
convert: response => response.json(),
|
||||||
},
|
},
|
||||||
type: "GET",
|
data => {
|
||||||
dataType: "json",
|
const logs = data.map(log_record => {
|
||||||
success: response => {
|
|
||||||
const logs = response.map(log_record => {
|
|
||||||
return `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`;
|
return `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`;
|
||||||
});
|
});
|
||||||
packageInfoLogsInput.text(logs.join("\n"));
|
packageInfoLogsInput.textContent = logs.join("\n");
|
||||||
packageInfoLogsInput.map((_, el) => hljs.highlightElement(el));
|
highlight(packageInfoLogsInput);
|
||||||
},
|
},
|
||||||
error: onFailure,
|
onFailure,
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPackage(packageBase, onFailure) {
|
function loadPackage(packageBase, onFailure) {
|
||||||
@ -194,16 +276,17 @@
|
|||||||
return ["bg-secondary", "text-white"];
|
return ["bg-secondary", "text-white"];
|
||||||
};
|
};
|
||||||
|
|
||||||
$.ajax({
|
makeRequest(
|
||||||
url: `/api/v1/packages/${packageBase}`,
|
`/api/v1/packages/${packageBase}`,
|
||||||
data: {
|
{
|
||||||
architecture: repository.architecture,
|
query: {
|
||||||
repository: repository.repository,
|
architecture: repository.architecture,
|
||||||
|
repository: repository.repository,
|
||||||
|
},
|
||||||
|
convert: response => response.json(),
|
||||||
},
|
},
|
||||||
type: "GET",
|
data => {
|
||||||
dataType: "json",
|
const description = data.find(Boolean);
|
||||||
success: response => {
|
|
||||||
const description = response.find(Boolean);
|
|
||||||
const packages = Object.keys(description.package.packages);
|
const packages = Object.keys(description.package.packages);
|
||||||
const aurUrl = description.package.remote.web_url;
|
const aurUrl = description.package.remote.web_url;
|
||||||
const upstreamUrls = Array.from(
|
const upstreamUrls = Array.from(
|
||||||
@ -213,103 +296,111 @@
|
|||||||
)
|
)
|
||||||
).sort();
|
).sort();
|
||||||
|
|
||||||
packageInfo.text(`${description.package.base} ${description.status.status} at ${new Date(1000 * description.status.timestamp).toISOStringShort()}`);
|
packageInfo.textContent = `${description.package.base} ${description.status.status} at ${new Date(1000 * description.status.timestamp).toISOStringShort()}`;
|
||||||
|
|
||||||
packageInfoModalHeader.removeClass();
|
packageInfoModalHeader.classList.remove(...packageInfoModalHeader.classList);
|
||||||
packageInfoModalHeader.addClass("modal-header");
|
packageInfoModalHeader.classList.add("modal-header");
|
||||||
headerClass(description.status.status).forEach(clz => packageInfoModalHeader.addClass(clz));
|
headerClass(description.status.status).forEach(clz => packageInfoModalHeader.classList.add(clz));
|
||||||
|
|
||||||
packageInfoAurUrl.html(aurUrl ? safeLink(aurUrl, aurUrl, "AUR link").outerHTML : "");
|
packageInfoAurUrl.innerHTML = aurUrl ? safeLink(aurUrl, aurUrl, "AUR link").outerHTML : "";
|
||||||
packageInfoDepends.html(listToTable(
|
packageInfoDepends.innerHTML = listToTable(
|
||||||
Object.values(description.package.packages)
|
Object.values(description.package.packages)
|
||||||
.reduce((accumulator, currentValue) => {
|
.reduce((accumulator, currentValue) => {
|
||||||
return accumulator.concat(currentValue.depends.filter(v => packages.indexOf(v) === -1))
|
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.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)`));
|
.concat(currentValue.opt_depends.filter(v => packages.indexOf(v) === -1).map(v => `${v} (optional)`));
|
||||||
}, [])
|
}, [])
|
||||||
));
|
);
|
||||||
packageInfoGroups.html(listToTable(extractListProperties(description.package, "groups")));
|
packageInfoGroups.innerHTML = listToTable(extractListProperties(description.package, "groups"));
|
||||||
packageInfoLicenses.html(listToTable(extractListProperties(description.package, "licenses")));
|
packageInfoLicenses.innerHTML = listToTable(extractListProperties(description.package, "licenses"));
|
||||||
packageInfoPackager.text(description.package.packager);
|
packageInfoPackager.textContent = description.package.packager;
|
||||||
packageInfoPackages.html(listToTable(packages));
|
packageInfoPackages.innerHTML = listToTable(packages);
|
||||||
packageInfoUpstreamUrl.html(upstreamUrls.map(url => safeLink(url, url, "upstream link").outerHTML).join("<br>"));
|
packageInfoUpstreamUrl.innerHTML = upstreamUrls.map(url => safeLink(url, url, "upstream link").outerHTML).join("<br>");
|
||||||
packageInfoVersion.text(description.package.version);
|
packageInfoVersion.textContent = description.package.version;
|
||||||
|
|
||||||
hideInfoControls(false);
|
|
||||||
},
|
},
|
||||||
error: (jqXHR, _, errorThrown) => {
|
onFailure,
|
||||||
hideInfoControls(true);
|
);
|
||||||
onFailure(jqXHR, null, errorThrown);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPatches(packageBase, onFailure) {
|
function loadPatches(packageBase, onFailure) {
|
||||||
$.ajax({
|
makeRequest(
|
||||||
url: `/api/v1/packages/${packageBase}/patches`,
|
`/api/v1/packages/${packageBase}/patches`,
|
||||||
type: "GET",
|
{
|
||||||
dataType: "json",
|
convert: response => response.json(),
|
||||||
success: response => {
|
|
||||||
packageInfoVariablesDiv.empty();
|
|
||||||
response.map(patch => insertVariable(packageBase, patch));
|
|
||||||
packageInfoVariablesBlock.attr("hidden", response.length === 0);
|
|
||||||
},
|
},
|
||||||
error: onFailure,
|
data => {
|
||||||
});
|
packageInfoVariablesDiv.replaceChildren();
|
||||||
|
data.map(patch => insertVariable(packageBase, patch));
|
||||||
|
packageInfoVariablesBlock.hidden = !data.length;
|
||||||
|
},
|
||||||
|
onFailure,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function packageInfoRemove() {
|
function packageInfoRemove() {
|
||||||
const packageBase = packageInfoModal.data("package");
|
const packageBase = packageInfoModal.package;
|
||||||
if (packageBase) return packagesRemove([packageBase]);
|
packagesRemove([packageBase]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function packageInfoUpdate() {
|
function packageInfoUpdate() {
|
||||||
const packageBase = packageInfoModal.data("package");
|
const packageBase = packageInfoModal.package;
|
||||||
if (packageBase) return packagesAdd(packageBase, [], repository);
|
packagesAdd(packageBase, [], repository);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showPackageInfo(packageBase) {
|
function showPackageInfo(packageBase) {
|
||||||
const isPackageBaseSet = packageBase !== undefined;
|
const isPackageBaseSet = packageBase !== undefined;
|
||||||
if (isPackageBaseSet)
|
if (isPackageBaseSet) {
|
||||||
packageInfoModal.data("package", packageBase); // set package base as currently used
|
// set package base as currently used
|
||||||
else
|
packageInfoModal.package = packageBase;
|
||||||
packageBase = packageInfoModal.data("package"); // read package base from the current window attribute
|
} else {
|
||||||
|
// read package base from the current window attribute
|
||||||
|
packageBase = packageInfoModal.package;
|
||||||
|
}
|
||||||
|
|
||||||
const onFailure = (jqXHR, _, errorThrown) => {
|
const onFailure = error => {
|
||||||
if (isPackageBaseSet) {
|
if (isPackageBaseSet) {
|
||||||
const message = error => `Could not load package ${packageBase} info: ${error}`;
|
const message = details => `Could not load package ${packageBase} info: ${details}`;
|
||||||
showFailure("Load failure", message, jqXHR, errorThrown);
|
showFailure("Load failure", message, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadPackage(packageBase, onFailure);
|
loadPackage(packageBase, onFailure);
|
||||||
loadPatches(packageBase, onFailure);
|
loadPatches(packageBase, onFailure);
|
||||||
loadLogs(packageBase, onFailure);
|
loadLogs(packageBase, onFailure);
|
||||||
loadChanges(packageBase, onFailure)
|
loadChanges(packageBase, onFailure);
|
||||||
|
loadEvents(packageBase, onFailure);
|
||||||
|
|
||||||
if (isPackageBaseSet) packageInfoModal.modal("show");
|
if (isPackageBaseSet) {
|
||||||
|
bootstrap.Modal.getOrCreateInstance(packageInfoModal).show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(_ => {
|
ready(_ => {
|
||||||
packageInfoModal.on("hidden.bs.modal", _ => {
|
packageInfoEventsUpdateChart = new Chart(packageInfoEventsUpdateChartCanvas, {
|
||||||
packageInfoAurUrl.empty();
|
type: "line",
|
||||||
packageInfoDepends.empty();
|
data: {},
|
||||||
packageInfoGroups.empty();
|
options: {
|
||||||
packageInfoLicenses.empty();
|
responsive: true,
|
||||||
packageInfoPackager.empty();
|
},
|
||||||
packageInfoPackages.empty();
|
});
|
||||||
packageInfoUpstreamUrl.empty();
|
|
||||||
packageInfoVersion.empty();
|
|
||||||
|
|
||||||
packageInfoVariablesBlock.attr("hidden", true);
|
packageInfoModal.addEventListener("hidden.bs.modal", _ => {
|
||||||
packageInfoVariablesDiv.empty();
|
packageInfoAurUrl.textContent = "";
|
||||||
|
packageInfoDepends.textContent = "";
|
||||||
|
packageInfoGroups.textContent = "";
|
||||||
|
packageInfoLicenses.textContent = "";
|
||||||
|
packageInfoPackager.textContent = "";
|
||||||
|
packageInfoPackages.textContent = "";
|
||||||
|
packageInfoUpstreamUrl.textContent = "";
|
||||||
|
packageInfoVersion.textContent = "";
|
||||||
|
|
||||||
packageInfoLogsInput.empty();
|
packageInfoVariablesBlock.hidden = true;
|
||||||
packageInfoChangesInput.empty();
|
packageInfoVariablesDiv.replaceChildren();
|
||||||
|
|
||||||
packageInfoModal.trigger("reset");
|
packageInfoLogsInput.textContent = "";
|
||||||
|
packageInfoChangesInput.textContent = "";
|
||||||
hideInfoControls(true);
|
packageInfoEventsTable.bootstrapTable("load", []);
|
||||||
|
clearChart();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -33,28 +33,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const packageRebuildModal = $("#package-rebuild-modal");
|
const packageRebuildModal = document.getElementById("package-rebuild-modal");
|
||||||
const packageRebuildForm = $("#package-rebuild-form");
|
const packageRebuildForm = document.getElementById("package-rebuild-form");
|
||||||
|
|
||||||
const packageRebuildDependencyInput = $("#package-rebuild-dependency-input");
|
const packageRebuildDependencyInput = document.getElementById("package-rebuild-dependency-input");
|
||||||
const packageRebuildRepositoryInput = $("#package-rebuild-repository-input");
|
const packageRebuildRepositoryInput = document.getElementById("package-rebuild-repository-input");
|
||||||
|
|
||||||
function packagesRebuild() {
|
function packagesRebuild() {
|
||||||
const packages = packageRebuildDependencyInput.val();
|
const packages = packageRebuildDependencyInput.value;
|
||||||
const repository = getRepositorySelector(packageRebuildRepositoryInput);
|
const repository = getRepositorySelector(packageRebuildRepositoryInput);
|
||||||
if (packages) {
|
if (packages) {
|
||||||
packageRebuildModal.modal("hide");
|
bootstrap.Modal.getOrCreateInstance(packageRebuildModal).hide();
|
||||||
const onSuccess = update => `Repository rebuild has been run for packages which depend on ${update}`;
|
const onSuccess = update => `Repository rebuild has been run for packages which depend on ${update}`;
|
||||||
const onFailure = error => `Repository rebuild failed: ${error}`;
|
const onFailure = error => `Repository rebuild failed: ${error}`;
|
||||||
doPackageAction("/api/v1/service/rebuild", [packages], repository, onSuccess, onFailure);
|
doPackageAction("/api/v1/service/rebuild", [packages], repository, onSuccess, onFailure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(_ => {
|
ready(_ => {
|
||||||
packageRebuildModal.on("shown.bs.modal", _ => {
|
packageRebuildModal.addEventListener("shown.bs.modal", _ => {
|
||||||
$(`#package-rebuild-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
|
const option = packageRebuildRepositoryInput.querySelector(`option[value="${repository.architecture}-${repository.repository}"]`);
|
||||||
|
option.selected = "selected";
|
||||||
|
|
||||||
});
|
});
|
||||||
packageRebuildModal.on("hidden.bs.modal", _ => { packageRebuildForm.trigger("reset"); });
|
packageRebuildModal.addEventListener("hidden.bs.modal", _ => {
|
||||||
|
packageRebuildForm.reset();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,39 +1,34 @@
|
|||||||
<script>
|
<script>
|
||||||
const keyImportButton = $("#key-import-button");
|
const packageRemoveButton = document.getElementById("package-remove-button");
|
||||||
const packageAddButton = $("#package-add-button");
|
const packageUpdateButton = document.getElementById("package-update-button");
|
||||||
const packageRebuildButton = $("#package-rebuild-button");
|
|
||||||
const packageRemoveButton = $("#package-remove-button");
|
|
||||||
const packageUpdateButton = $("#package-update-button");
|
|
||||||
|
|
||||||
const packageInfoRemoveButton = $("#package-info-remove-button");
|
|
||||||
const packageInfoUpdateButton = $("#package-info-update-button");
|
|
||||||
|
|
||||||
let repository = null;
|
let repository = null;
|
||||||
|
|
||||||
const table = $("#packages");
|
// so far bootstrap-table only operates with jquery elements
|
||||||
|
const table = $(document.getElementById("packages"));
|
||||||
|
|
||||||
const statusBadge = $("#badge-status");
|
const statusBadge = document.getElementById("badge-status");
|
||||||
const versionBadge = $("#badge-version");
|
const versionBadge = document.getElementById("badge-version");
|
||||||
|
|
||||||
function doPackageAction(uri, packages, repository, successText, failureText, data) {
|
function doPackageAction(uri, packages, repository, successText, failureText, data) {
|
||||||
const queryParams = $.param({
|
makeRequest(
|
||||||
architecture: repository.architecture,
|
uri,
|
||||||
repository: repository.repository,
|
{
|
||||||
}); // it will never be empty btw
|
method: "POST",
|
||||||
|
query: {
|
||||||
$.ajax({
|
architecture: repository.architecture,
|
||||||
url: `${uri}?${queryParams}`,
|
repository: repository.repository,
|
||||||
data: JSON.stringify(Object.assign({}, {packages: packages}, data || {})),
|
},
|
||||||
type: "POST",
|
json: Object.assign({}, {packages: packages}, data || {}),
|
||||||
contentType: "application/json",
|
},
|
||||||
success: _ => {
|
_ => {
|
||||||
const message = successText(packages.join(", "));
|
const message = successText(packages.join(", "));
|
||||||
showSuccess("Success", message);
|
showSuccess("Success", message);
|
||||||
},
|
},
|
||||||
error: (jqXHR, _, errorThrown) => {
|
error => {
|
||||||
showFailure("Action failed", failureText, jqXHR, errorThrown);
|
showFailure("Action failed", failureText, error);
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterListGroups() {
|
function filterListGroups() {
|
||||||
@ -49,10 +44,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getRepositorySelector(selector) {
|
function getRepositorySelector(selector) {
|
||||||
const selected = selector.find(":selected");
|
const selected = selector.options[selector.selectedIndex];
|
||||||
return {
|
return {
|
||||||
architecture: selected.data("architecture"),
|
architecture: selected.getAttribute("data-architecture"),
|
||||||
repository: selected.data("repository"),
|
repository: selected.getAttribute("data-repository"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,14 +55,6 @@
|
|||||||
return table.bootstrapTable("getSelections").map(row => row.id);
|
return table.bootstrapTable("getSelections").map(row => row.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideControls(hidden) {
|
|
||||||
keyImportButton.attr("hidden", hidden);
|
|
||||||
packageAddButton.attr("hidden", hidden);
|
|
||||||
packageRebuildButton.attr("hidden", hidden);
|
|
||||||
packageRemoveButton.attr("hidden", hidden);
|
|
||||||
packageUpdateButton.attr("hidden", hidden);
|
|
||||||
}
|
|
||||||
|
|
||||||
function packagesRemove(packages) {
|
function packagesRemove(packages) {
|
||||||
packages = packages ?? getSelection();
|
packages = packages ?? getSelection();
|
||||||
const onSuccess = update => `Packages ${update} have been removed`;
|
const onSuccess = update => `Packages ${update} have been removed`;
|
||||||
@ -97,16 +84,17 @@
|
|||||||
return "btn-outline-secondary";
|
return "btn-outline-secondary";
|
||||||
};
|
};
|
||||||
|
|
||||||
$.ajax({
|
makeRequest(
|
||||||
url: "/api/v1/packages",
|
"/api/v1/packages",
|
||||||
data: {
|
{
|
||||||
architecture: repository.architecture,
|
query: {
|
||||||
repository: repository.repository,
|
architecture: repository.architecture,
|
||||||
|
repository: repository.repository,
|
||||||
|
},
|
||||||
|
convert: response => response.json(),
|
||||||
},
|
},
|
||||||
type: "GET",
|
data => {
|
||||||
dataType: "json",
|
const payload = data.map(description => {
|
||||||
success: response => {
|
|
||||||
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 {
|
||||||
@ -125,10 +113,9 @@
|
|||||||
table.bootstrapTable("load", payload);
|
table.bootstrapTable("load", payload);
|
||||||
table.bootstrapTable("uncheckAll");
|
table.bootstrapTable("uncheckAll");
|
||||||
table.bootstrapTable("hideLoading");
|
table.bootstrapTable("hideLoading");
|
||||||
hideControls(false);
|
|
||||||
},
|
},
|
||||||
error: (jqXHR, _, errorThrown) => {
|
error => {
|
||||||
if ((jqXHR.status === 401) || (jqXHR.status === 403)) {
|
if ((error.status === 401) || (error.status === 403)) {
|
||||||
// authorization error
|
// authorization error
|
||||||
const text = "In order to see statuses you must login first.";
|
const text = "In order to see statuses you must login first.";
|
||||||
table.find("tr.unauthorized").remove();
|
table.find("tr.unauthorized").remove();
|
||||||
@ -136,39 +123,39 @@
|
|||||||
table.bootstrapTable("hideLoading");
|
table.bootstrapTable("hideLoading");
|
||||||
} else {
|
} else {
|
||||||
// other errors
|
// other errors
|
||||||
const message = error => `Could not load list of packages: ${error}`;
|
const message = details => `Could not load list of packages: ${details}`;
|
||||||
showFailure("Load failure", message, jqXHR, errorThrown);
|
showFailure("Load failure", message, error);
|
||||||
}
|
}
|
||||||
hideControls(true);
|
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
$.ajax({
|
makeRequest(
|
||||||
url: "/api/v1/status",
|
"/api/v1/status",
|
||||||
data: {
|
{
|
||||||
architecture: repository.architecture,
|
query: {
|
||||||
repository: repository.repository,
|
architecture: repository.architecture,
|
||||||
|
repository: repository.repository,
|
||||||
|
},
|
||||||
|
convert: response => response.json(),
|
||||||
},
|
},
|
||||||
type: "GET",
|
data => {
|
||||||
dataType: "json",
|
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`;
|
||||||
success: response => {
|
|
||||||
versionBadge.html(`<i class="bi bi-github"></i> ahriman ${safe(response.version)}`);
|
|
||||||
|
|
||||||
statusBadge
|
statusBadge.classList.remove(...statusBadge.classList);
|
||||||
.popover("dispose")
|
statusBadge.classList.add("btn");
|
||||||
.attr("data-bs-content", `${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOStringShort()}`)
|
statusBadge.classList.add(badgeClass(data.status.status));
|
||||||
.popover();
|
|
||||||
statusBadge.removeClass();
|
const popover = bootstrap.Popover.getOrCreateInstance(statusBadge);
|
||||||
statusBadge.addClass("btn");
|
popover.dispose();
|
||||||
statusBadge.addClass(badgeClass(response.status.status));
|
statusBadge.dataset.bsContent = `${data.status.status} at ${new Date(1000 * data.status.timestamp).toISOStringShort()}`;
|
||||||
|
bootstrap.Popover.getOrCreateInstance(statusBadge);
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectRepository() {
|
function selectRepository() {
|
||||||
const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}";
|
const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}";
|
||||||
const element = $(`#${fragment}-link`);
|
document.getElementById(`${fragment}-link`).click();
|
||||||
element.click();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusFormat(value) {
|
function statusFormat(value) {
|
||||||
@ -182,20 +169,25 @@
|
|||||||
return {classes: cellClass(value)};
|
return {classes: cellClass(value)};
|
||||||
}
|
}
|
||||||
|
|
||||||
$(_ => {
|
ready(_ => {
|
||||||
$("#repositories a").on("click", event => {
|
document.querySelectorAll("#repositories a").forEach(element => {
|
||||||
const element = event.target;
|
element.onclick = _ => {
|
||||||
repository = {
|
repository = {
|
||||||
architecture: element.dataset.architecture,
|
architecture: element.dataset.architecture,
|
||||||
repository: element.dataset.repository,
|
repository: element.dataset.repository,
|
||||||
|
};
|
||||||
|
if (packageUpdateButton) {
|
||||||
|
packageUpdateButton.innerHTML = `<i class="bi bi-play"></i> update<span class="d-none d-sm-inline"> ${safe(repository.repository)} (${safe(repository.architecture)})</span>`;
|
||||||
|
}
|
||||||
|
bootstrap.Tab.getOrCreateInstance(document.getElementById(element.id)).show();
|
||||||
|
reload();
|
||||||
};
|
};
|
||||||
packageUpdateButton.html(`<i class="bi bi-play"></i> update<span class="d-none d-sm-inline"> ${safe(repository.repository)} (${safe(repository.architecture)})</span>`);
|
|
||||||
$(`#${element.id}`).tab("show");
|
|
||||||
reload();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", _ => {
|
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", _ => {
|
||||||
packageRemoveButton.prop("disabled", !table.bootstrapTable("getSelections").length);
|
if (packageRemoveButton) {
|
||||||
|
packageRemoveButton.disabled = !table.bootstrapTable("getSelections").length;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
table.on("click-row.bs.table", (self, data, row, cell) => {
|
table.on("click-row.bs.table", (self, data, row, cell) => {
|
||||||
if (0 === cell || "base" === cell) {
|
if (0 === cell || "base" === cell) {
|
||||||
@ -204,26 +196,38 @@
|
|||||||
} else showPackageInfo(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");
|
new easepick.create({
|
||||||
pickerInput.daterangepicker({
|
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
|
||||||
autoUpdateInput: false,
|
css: [
|
||||||
|
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
|
||||||
|
],
|
||||||
|
grid: 2,
|
||||||
|
calendars: 2,
|
||||||
|
autoApply: false,
|
||||||
locale: {
|
locale: {
|
||||||
cancelLabel: "Clear",
|
cancel: "Clear",
|
||||||
|
},
|
||||||
|
RangePlugin: {
|
||||||
|
tooltip: false,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
"RangePlugin",
|
||||||
|
],
|
||||||
|
setup: picker => {
|
||||||
|
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
|
||||||
|
// replace "Cancel" behaviour to "Clear"
|
||||||
|
picker.onClickCancelButton = element => {
|
||||||
|
if (picker.isCancelButton(element)) {
|
||||||
|
picker.clear();
|
||||||
|
picker.hide();
|
||||||
|
table.bootstrapTable("triggerSearch");
|
||||||
|
}
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
pickerInput.on("apply.daterangepicker", (event, picker) => {
|
|
||||||
pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`);
|
|
||||||
table.bootstrapTable("triggerSearch");
|
|
||||||
});
|
|
||||||
|
|
||||||
pickerInput.on("cancel.daterangepicker", _ => {
|
|
||||||
pickerInput.val("");
|
|
||||||
table.bootstrapTable("triggerSearch");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
statusBadge.popover();
|
bootstrap.Popover.getOrCreateInstance(statusBadge);
|
||||||
selectRepository();
|
selectRepository();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -7,6 +7,10 @@
|
|||||||
|
|
||||||
{% include "utils/style.jinja2" %}
|
{% include "utils/style.jinja2" %}
|
||||||
{% include "user-style.jinja2" ignore missing %}
|
{% include "user-style.jinja2" ignore missing %}
|
||||||
|
|
||||||
|
{% if rss_url is not none %}
|
||||||
|
<link rel="alternate" href="{{ rss_url }}" type="application/rss+xml">
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@ -101,13 +105,13 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const table = $("#packages");
|
const table = $(document.getElementById("packages"));
|
||||||
|
|
||||||
const pacmanConf = $("#pacman-conf");
|
const pacmanConf = document.getElementById("pacman-conf");
|
||||||
const pacmanConfCopyButton = $("#copy-btn");
|
const pacmanConfCopyButton = document.getElementById("copy-btn");
|
||||||
|
|
||||||
async function copyPacmanConf() {
|
async function copyPacmanConf() {
|
||||||
const conf = pacmanConf.text();
|
const conf = pacmanConf.textContent;
|
||||||
await copyToClipboard(conf, pacmanConfCopyButton);
|
await copyToClipboard(conf, pacmanConfCopyButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,24 +127,36 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
|
|||||||
return extractDataList(table.bootstrapTable("getData"), "licenses");
|
return extractDataList(table.bootstrapTable("getData"), "licenses");
|
||||||
}
|
}
|
||||||
|
|
||||||
$(_ => {
|
ready(_ => {
|
||||||
table.on("created-controls.bs.table", _ => {
|
table.on("created-controls.bs.table", _ => {
|
||||||
const pickerInput = $(".bootstrap-table-filter-control-timestamp");
|
new easepick.create({
|
||||||
pickerInput.daterangepicker({
|
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
|
||||||
autoUpdateInput: false,
|
css: [
|
||||||
|
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
|
||||||
|
],
|
||||||
|
grid: 2,
|
||||||
|
calendars: 2,
|
||||||
|
autoApply: false,
|
||||||
locale: {
|
locale: {
|
||||||
cancelLabel: "Clear",
|
cancel: "Clear",
|
||||||
|
},
|
||||||
|
RangePlugin: {
|
||||||
|
tooltip: false,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
"RangePlugin",
|
||||||
|
],
|
||||||
|
setup: picker => {
|
||||||
|
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
|
||||||
|
// replace "Cancel" behaviour to "Clear"
|
||||||
|
picker.onClickCancelButton = element => {
|
||||||
|
if (picker.isCancelButton(element)) {
|
||||||
|
picker.clear();
|
||||||
|
picker.hide();
|
||||||
|
table.bootstrapTable("triggerSearch");
|
||||||
|
}
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
pickerInput.on("apply.daterangepicker", (event, picker) => {
|
|
||||||
pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`);
|
|
||||||
table.bootstrapTable("triggerSearch");
|
|
||||||
});
|
|
||||||
|
|
||||||
pickerInput.on("cancel.daterangepicker", _ => {
|
|
||||||
pickerInput.val("");
|
|
||||||
table.bootstrapTable("triggerSearch");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
27
package/share/ahriman/templates/rss.jinja2
Normal file
27
package/share/ahriman/templates/rss.jinja2
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<channel>
|
||||||
|
<title>{{ repository }}: Recent package updates</title>
|
||||||
|
{% if homepage is not none %}
|
||||||
|
<link>{{ homepage }}</link>
|
||||||
|
{% endif %}
|
||||||
|
<description>Recently updated packages in the {{ repository }}.</description>
|
||||||
|
{% if rss_url is not none %}
|
||||||
|
<atom:link href="{{ rss_url }}" rel="self"/>
|
||||||
|
{% endif %}
|
||||||
|
<language>en-us</language>
|
||||||
|
<lastBuildDate>{{ last_update }}</lastBuildDate>
|
||||||
|
|
||||||
|
{% for package in packages %}
|
||||||
|
<item>
|
||||||
|
<title>{{ package.name }} {{ package.version }} {{ package.architecture }}</title>
|
||||||
|
<link>{{ link_path }}/{{ package.filename }}</link>
|
||||||
|
<description>{{ package.description }}</description>
|
||||||
|
<pubDate>{{ package.build_date }}</pubDate>
|
||||||
|
<guid isPermaLink="false">{{ package.tag }}</guid>
|
||||||
|
<category>{{ repository }}</category>
|
||||||
|
<category>{{ package.architecture }}</category>
|
||||||
|
</item>
|
||||||
|
{% endfor %}
|
||||||
|
</channel>
|
||||||
|
</rss>
|
@ -1,38 +1,30 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/js-md5@0.8.3/src/md5.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.30.0/tableExport.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.28.0/tableExport.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/bootstrap-table.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/bootstrap-table.min.js" 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" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/export/bootstrap-table-export.min.js" 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" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/resizable/bootstrap-table-resizable.js" 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" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/filter-control/bootstrap-table-filter-control.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/highlight.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function copyToClipboard(text, button) {
|
async function copyToClipboard(text, button) {
|
||||||
if (navigator.clipboard === undefined) {
|
await navigator.clipboard.writeText(text);
|
||||||
const input = document.createElement("textarea");
|
button.innerHTML = "<i class=\"bi bi-clipboard-check\"></i> copied";
|
||||||
input.innerHTML = text;
|
setTimeout(_ => {
|
||||||
document.body.appendChild(input);
|
button.innerHTML = "<i class=\"bi bi-clipboard\"></i> copy";
|
||||||
input.select();
|
|
||||||
document.execCommand("copy");
|
|
||||||
document.body.removeChild(input);
|
|
||||||
} else {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.html("<i class=\"bi bi-clipboard-check\"></i> copied");
|
|
||||||
setTimeout(()=> {
|
|
||||||
button.html("<i class=\"bi bi-clipboard\"></i> copy");
|
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,6 +65,47 @@
|
|||||||
.join("<br>");
|
.join("<br>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeRequest(url, params, onSuccess, onFailure) {
|
||||||
|
const requestParams = {
|
||||||
|
method: params.method,
|
||||||
|
body: params.json ? JSON.stringify(params.json) : params.json,
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (params.query) {
|
||||||
|
const query = new URLSearchParams(params.query);
|
||||||
|
url += `?${query.toString()}`;
|
||||||
|
}
|
||||||
|
const convert = params.convert ?? (response => response.text());
|
||||||
|
|
||||||
|
return fetch(url, requestParams)
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
return convert(response);
|
||||||
|
} else {
|
||||||
|
const error = new Error("Network request error");
|
||||||
|
error.status = response.status;
|
||||||
|
error.statusText = response.statusText;
|
||||||
|
return response.text().then(text => {
|
||||||
|
error.text = text;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(data => onSuccess && onSuccess(data))
|
||||||
|
.catch(error => onFailure && onFailure(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
function ready(fn) {
|
||||||
|
if (document.readyState === "complete" || document.readyState === "interactive") {
|
||||||
|
setTimeout(fn, 1);
|
||||||
|
} else {
|
||||||
|
document.addEventListener("DOMContentLoaded", fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function safe(string) {
|
function safe(string) {
|
||||||
return String(string)
|
return String(string)
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
@ -86,7 +119,9 @@
|
|||||||
const element = document.createElement("a");
|
const element = document.createElement("a");
|
||||||
element.href = url;
|
element.href = url;
|
||||||
element.innerText = text;
|
element.innerText = text;
|
||||||
if (title) element.title = title;
|
if (title) {
|
||||||
|
element.title = title;
|
||||||
|
}
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" crossorigin="anonymous" type="text/css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" crossorigin="anonymous" type="text/css">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" crossorigin="anonymous" type="text/css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/bootstrap-table.min.css" crossorigin="anonymous" type="text/css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/bootstrap-table.min.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" crossorigin="anonymous" type="text/css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" 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" crossorigin="anonymous" type="text/css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/filter-control/bootstrap-table-filter-control.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.2/dist/cosmo/bootstrap.min.css" crossorigin="anonymous" type="text/css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/cosmo/bootstrap.min.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.css" crossorigin="anonymous" type="text/css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/styles/github.min.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css" crossorigin="anonymous" type="text/css">
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.pre-scrollable {
|
.pre-scrollable {
|
||||||
|
@ -17,7 +17,6 @@ authors = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cerberus",
|
|
||||||
"inflection",
|
"inflection",
|
||||||
"passlib",
|
"passlib",
|
||||||
"pyelftools",
|
"pyelftools",
|
||||||
@ -62,6 +61,9 @@ pacman = [
|
|||||||
s3 = [
|
s3 = [
|
||||||
"boto3",
|
"boto3",
|
||||||
]
|
]
|
||||||
|
stats = [
|
||||||
|
"matplotlib",
|
||||||
|
]
|
||||||
tests = [
|
tests = [
|
||||||
"pytest",
|
"pytest",
|
||||||
"pytest-aiohttp",
|
"pytest-aiohttp",
|
||||||
@ -71,6 +73,9 @@ tests = [
|
|||||||
"pytest-resource-path",
|
"pytest-resource-path",
|
||||||
"pytest-spec",
|
"pytest-spec",
|
||||||
]
|
]
|
||||||
|
validator = [
|
||||||
|
"cerberus",
|
||||||
|
]
|
||||||
web = [
|
web = [
|
||||||
"Jinja2",
|
"Jinja2",
|
||||||
"aioauth-client",
|
"aioauth-client",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Index
|
# Index
|
||||||
|
|
||||||
1. Setup repository named `ahriman-demo` with architecture `x86_64`.
|
1. Setup repository named `ahriman-demo` with architecture `x86_64`.
|
||||||
2. Generate index page.
|
2. Generate index page and RSS feed.
|
||||||
3. Repository is available at `http://localhost:8080/repo`.
|
3. Repository is available at `http://localhost:8080/repo`.
|
||||||
4. Index page is available at `http://localhost:8080/repo/ahriman-demo/x86_64/index.html`
|
4. Index page is available at `http://localhost:8080/repo/ahriman-demo/x86_64/index.html`
|
||||||
|
5. Index page is available at `http://localhost:8080/repo/ahriman-demo/x86_64/rss.xml`
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
[report]
|
[report]
|
||||||
target = html
|
target = html rss
|
||||||
|
|
||||||
[html]
|
[html]
|
||||||
path = /var/lib/ahriman/ahriman/repository/ahriman-demo/x86_64/index.html
|
path = ${repository:root}/repository/ahriman-demo/x86_64/index.html
|
||||||
link_path = http://localhost:8080/repo/ahriman-demo/x86_64
|
link_path = http://localhost:8080/repo/ahriman-demo/x86_64
|
||||||
|
rss_url = ${html:link_path}/rss.xml
|
||||||
|
|
||||||
|
[rss]
|
||||||
|
link_path = ${html:link_path}
|
||||||
|
path = ${repository:root}/repository/ahriman-demo/x86_64/rss.xml
|
||||||
|
rss_url = ${html:link_path}/rss.xml
|
||||||
|
@ -28,6 +28,7 @@ from ahriman.application import handlers
|
|||||||
from ahriman.core.utils import enum_values, extract_user
|
from ahriman.core.utils import enum_values, extract_user
|
||||||
from ahriman.models.action import Action
|
from ahriman.models.action import Action
|
||||||
from ahriman.models.build_status import BuildStatusEnum
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
|
from ahriman.models.event import EventType
|
||||||
from ahriman.models.log_handler import LogHandler
|
from ahriman.models.log_handler import LogHandler
|
||||||
from ahriman.models.package_source import PackageSource
|
from ahriman.models.package_source import PackageSource
|
||||||
from ahriman.models.sign_settings import SignSettings
|
from ahriman.models.sign_settings import SignSettings
|
||||||
@ -119,6 +120,7 @@ def _parser() -> argparse.ArgumentParser:
|
|||||||
_set_repo_report_parser(subparsers)
|
_set_repo_report_parser(subparsers)
|
||||||
_set_repo_restore_parser(subparsers)
|
_set_repo_restore_parser(subparsers)
|
||||||
_set_repo_sign_parser(subparsers)
|
_set_repo_sign_parser(subparsers)
|
||||||
|
_set_repo_statistics_parser(subparsers)
|
||||||
_set_repo_status_update_parser(subparsers)
|
_set_repo_status_update_parser(subparsers)
|
||||||
_set_repo_sync_parser(subparsers)
|
_set_repo_sync_parser(subparsers)
|
||||||
_set_repo_tree_parser(subparsers)
|
_set_repo_tree_parser(subparsers)
|
||||||
@ -264,6 +266,8 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
|||||||
"5) and finally you can add package from AUR.",
|
"5) and finally you can add package from AUR.",
|
||||||
formatter_class=_formatter)
|
formatter_class=_formatter)
|
||||||
parser.add_argument("package", help="package source (base name, path to local files, remote URL)", nargs="+")
|
parser.add_argument("package", help="package source (base name, path to local files, remote URL)", nargs="+")
|
||||||
|
parser.add_argument("--changes", help="calculate changes from the latest known commit if available",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
parser.add_argument("--dependencies", help="process missing package dependencies",
|
parser.add_argument("--dependencies", help="process missing package dependencies",
|
||||||
action=argparse.BooleanOptionalAction, default=True)
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
|
||||||
@ -532,8 +536,7 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
|||||||
description="check for packages updates. Same as repo-update --dry-run --no-manual",
|
description="check for packages updates. Same as repo-update --dry-run --no-manual",
|
||||||
formatter_class=_formatter)
|
formatter_class=_formatter)
|
||||||
parser.add_argument("package", help="filter check by package base", nargs="*")
|
parser.add_argument("package", help="filter check by package base", nargs="*")
|
||||||
parser.add_argument("--changes", help="calculate changes from the latest known commit if available. "
|
parser.add_argument("--changes", help="calculate changes from the latest known commit if available",
|
||||||
"Only applicable in dry run mode",
|
|
||||||
action=argparse.BooleanOptionalAction, default=True)
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
parser.add_argument("--check-files", help="enable or disable checking of broken dependencies "
|
parser.add_argument("--check-files", help="enable or disable checking of broken dependencies "
|
||||||
"(e.g. dynamically linked libraries or modules directories)",
|
"(e.g. dynamically linked libraries or modules directories)",
|
||||||
@ -735,6 +738,30 @@ def _set_repo_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
|||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def _set_repo_statistics_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for repository statistics subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("repo-statistics", help="repository statistics",
|
||||||
|
description="fetch repository statistics", formatter_class=_formatter)
|
||||||
|
parser.add_argument("package", help="fetch only events for the specified package", nargs="?")
|
||||||
|
parser.add_argument("--chart", help="create updates chart and save it to the specified path", type=Path)
|
||||||
|
parser.add_argument("-e", "--event", help="event type filter",
|
||||||
|
type=EventType, choices=enum_values(EventType), default=EventType.PackageUpdated)
|
||||||
|
parser.add_argument("--from-date", help="only fetch events which are newer than the date")
|
||||||
|
parser.add_argument("--limit", help="limit response by specified amount of events", type=int, default=-1)
|
||||||
|
parser.add_argument("--offset", help="skip specified amount of events", type=int, default=0)
|
||||||
|
parser.add_argument("--to-date", help="only fetch events which are older than the date")
|
||||||
|
parser.set_defaults(handler=handlers.Statistics, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
"""
|
"""
|
||||||
add parser for repository status update subcommand
|
add parser for repository status update subcommand
|
||||||
|
@ -120,8 +120,7 @@ class Application(ApplicationPackages, ApplicationRepository):
|
|||||||
process_dependencies(bool): if no set, dependencies will not be processed
|
process_dependencies(bool): if no set, dependencies will not be processed
|
||||||
|
|
||||||
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 the original package
|
||||||
original package
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
In the most cases, in order to avoid build failure, it is required to add missing packages, which can be
|
In the most cases, in order to avoid build failure, it is required to add missing packages, which can be
|
||||||
|
@ -40,8 +40,6 @@ class ApplicationProperties(LazyLogging):
|
|||||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration, *, report: bool,
|
def __init__(self, repository_id: RepositoryId, configuration: Configuration, *, report: bool,
|
||||||
refresh_pacman_database: PacmanSynchronization = PacmanSynchronization.Disabled) -> None:
|
refresh_pacman_database: PacmanSynchronization = PacmanSynchronization.Disabled) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
repository_id(RepositoryId): repository unique identifier
|
repository_id(RepositoryId): repository unique identifier
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
|
@ -49,8 +49,6 @@ class UpdatesIterator(Iterator[list[str] | None]):
|
|||||||
|
|
||||||
def __init__(self, application: Application, interval: int) -> None:
|
def __init__(self, application: Application, interval: int) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
application(Application): application instance
|
application(Application): application instance
|
||||||
interval(int): predefined interval for updates
|
interval(int): predefined interval for updates
|
||||||
|
@ -37,8 +37,6 @@ class LocalUpdater(Updater):
|
|||||||
|
|
||||||
def __init__(self, repository: Repository) -> None:
|
def __init__(self, repository: Repository) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
repository(Repository): repository instance
|
repository(Repository): repository instance
|
||||||
"""
|
"""
|
||||||
|
@ -43,8 +43,6 @@ class RemoteUpdater(Updater):
|
|||||||
|
|
||||||
def __init__(self, workers: list[Worker], repository_id: RepositoryId, configuration: Configuration) -> None:
|
def __init__(self, workers: list[Worker], repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
workers(list[Worker]): worker identifiers
|
workers(list[Worker]): worker identifiers
|
||||||
repository_id(RepositoryId): repository unique identifier
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
@ -38,6 +38,7 @@ from ahriman.application.handlers.service_updates import ServiceUpdates
|
|||||||
from ahriman.application.handlers.setup import Setup
|
from ahriman.application.handlers.setup import Setup
|
||||||
from ahriman.application.handlers.shell import Shell
|
from ahriman.application.handlers.shell import Shell
|
||||||
from ahriman.application.handlers.sign import Sign
|
from ahriman.application.handlers.sign import Sign
|
||||||
|
from ahriman.application.handlers.statistics import Statistics
|
||||||
from ahriman.application.handlers.status import Status
|
from ahriman.application.handlers.status import Status
|
||||||
from ahriman.application.handlers.status_update import StatusUpdate
|
from ahriman.application.handlers.status_update import StatusUpdate
|
||||||
from ahriman.application.handlers.structure import Structure
|
from ahriman.application.handlers.structure import Structure
|
||||||
|
@ -57,6 +57,9 @@ class Add(Handler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False, check_files=False)
|
packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False, check_files=False)
|
||||||
|
if args.changes: # generate changes if requested
|
||||||
|
application.changes(packages)
|
||||||
|
|
||||||
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
|
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
|
||||||
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
|
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class Handler:
|
|||||||
repository_id(RepositoryId): repository unique identifier
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True on success, False otherwise
|
bool: ``True`` on success, ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
configuration = Configuration.from_path(args.configuration, repository_id)
|
configuration = Configuration.from_path(args.configuration, repository_id)
|
||||||
@ -129,7 +129,7 @@ class Handler:
|
|||||||
check condition and flag and raise ExitCode exception in case if it is enabled and condition match
|
check condition and flag and raise ExitCode exception in case if it is enabled and condition match
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
enabled(bool): if False no check will be performed
|
enabled(bool): if ``False`` no check will be performed
|
||||||
predicate(bool): indicates condition on which exception should be thrown
|
predicate(bool): indicates condition on which exception should be thrown
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
|
170
src/ahriman/application/handlers/statistics.py
Normal file
170
src/ahriman/application/handlers/statistics.py
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 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 datetime
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import EventStatsPrinter, PackageStatsPrinter
|
||||||
|
from ahriman.core.utils import pretty_datetime
|
||||||
|
from ahriman.models.event import Event
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Statistics(Handler):
|
||||||
|
"""
|
||||||
|
repository statistics handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=True)
|
||||||
|
|
||||||
|
from_date = to_date = None
|
||||||
|
if (value := args.from_date) is not None:
|
||||||
|
from_date = datetime.datetime.fromisoformat(value).timestamp()
|
||||||
|
if (value := args.to_date) is not None:
|
||||||
|
to_date = datetime.datetime.fromisoformat(value).timestamp()
|
||||||
|
|
||||||
|
events = application.reporter.event_get(args.event, args.package, from_date, to_date, args.limit, args.offset)
|
||||||
|
|
||||||
|
match args.package:
|
||||||
|
case None:
|
||||||
|
Statistics.stats_per_package(args.event, events, args.chart)
|
||||||
|
case _:
|
||||||
|
Statistics.stats_for_package(args.event, events, args.chart)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def event_stats(event_type: str, events: list[Event]) -> None:
|
||||||
|
"""
|
||||||
|
calculate event stats
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type(str): event type
|
||||||
|
events(list[Event]): list of events
|
||||||
|
"""
|
||||||
|
times = [event.get("took") for event in events if event.get("took") is not None]
|
||||||
|
EventStatsPrinter(f"{event_type} duration, s", times)(verbose=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def plot_packages(event_type: str, events: dict[str, int], path: Path) -> None:
|
||||||
|
"""
|
||||||
|
plot packages frequency
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type(str): event type
|
||||||
|
events(dict[str, int]): list of events
|
||||||
|
path(Path): path to save plot
|
||||||
|
"""
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
|
||||||
|
x, y = list(events.keys()), list(events.values())
|
||||||
|
plt.bar(x, y)
|
||||||
|
|
||||||
|
plt.xlabel("Package base")
|
||||||
|
plt.ylabel("Frequency")
|
||||||
|
plt.title(f"Frequency of the {event_type} event per package")
|
||||||
|
|
||||||
|
plt.savefig(path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def plot_times(event_type: str, events: list[Event], path: Path) -> None:
|
||||||
|
"""
|
||||||
|
plot events timeline
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type(str): event type
|
||||||
|
events(list[Event]): list of events
|
||||||
|
path(Path): path to save plot
|
||||||
|
"""
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
|
||||||
|
figure = plt.figure()
|
||||||
|
|
||||||
|
x, y = zip(*[(pretty_datetime(event.created), event.get("took")) for event in events])
|
||||||
|
plt.plot(x, y)
|
||||||
|
|
||||||
|
plt.xlabel("Event timestamp")
|
||||||
|
plt.ylabel("Duration, s")
|
||||||
|
plt.title(f"Duration of the {event_type} event")
|
||||||
|
figure.autofmt_xdate()
|
||||||
|
|
||||||
|
plt.savefig(path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def stats_for_package(event_type: str, events: list[Event], chart_path: Path | None) -> None:
|
||||||
|
"""
|
||||||
|
calculate statistics for a package
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type(str): event type
|
||||||
|
events(list[Event]): list of events
|
||||||
|
chart_path(Path): path to save plot if any
|
||||||
|
"""
|
||||||
|
# event statistics
|
||||||
|
Statistics.event_stats(event_type, events)
|
||||||
|
|
||||||
|
# chart if enabled
|
||||||
|
if chart_path is not None:
|
||||||
|
Statistics.plot_times(event_type, events, chart_path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def stats_per_package(event_type: str, events: list[Event], chart_path: Path | None) -> None:
|
||||||
|
"""
|
||||||
|
calculate overall statistics
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type(str): event type
|
||||||
|
events(list[Event]): list of events
|
||||||
|
chart_path(Path): path to save plot if any
|
||||||
|
"""
|
||||||
|
key: Callable[[Event], str] = lambda event: event.object_id
|
||||||
|
by_object_id = {
|
||||||
|
object_id: len(list(related))
|
||||||
|
for object_id, related in itertools.groupby(sorted(events, key=key), key=key)
|
||||||
|
}
|
||||||
|
|
||||||
|
# distribution per package
|
||||||
|
PackageStatsPrinter(by_object_id)(verbose=True)
|
||||||
|
EventStatsPrinter(f"{event_type} frequency", list(by_object_id.values()))(verbose=True)
|
||||||
|
|
||||||
|
# event statistics
|
||||||
|
Statistics.event_stats(event_type, events)
|
||||||
|
|
||||||
|
# chart if enabled
|
||||||
|
if chart_path is not None:
|
||||||
|
Statistics.plot_packages(event_type, by_object_id, chart_path)
|
@ -50,10 +50,10 @@ class Update(Handler):
|
|||||||
|
|
||||||
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
|
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
|
||||||
check_files=args.check_files)
|
check_files=args.check_files)
|
||||||
if args.dry_run: # some check specific actions
|
if args.changes: # generate changes if requested
|
||||||
if args.changes: # generate changes if requested
|
application.changes(packages)
|
||||||
application.changes(packages)
|
|
||||||
|
|
||||||
|
if args.dry_run: # exit from application if no build requested
|
||||||
Update.check_if_empty(args.exit_code, not packages) # status code check
|
Update.check_if_empty(args.exit_code, not packages) # status code check
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ from typing import Any
|
|||||||
from ahriman.application.handlers.handler import Handler
|
from ahriman.application.handlers.handler import Handler
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.configuration.schema import CONFIGURATION_SCHEMA, ConfigurationSchema
|
from ahriman.core.configuration.schema import CONFIGURATION_SCHEMA, ConfigurationSchema
|
||||||
from ahriman.core.configuration.validator import Validator
|
|
||||||
from ahriman.core.exceptions import ExtensionError
|
from ahriman.core.exceptions import ExtensionError
|
||||||
from ahriman.core.formatters import ValidationPrinter
|
from ahriman.core.formatters import ValidationPrinter
|
||||||
from ahriman.core.triggers import TriggerLoader
|
from ahriman.core.triggers import TriggerLoader
|
||||||
@ -51,6 +50,8 @@ class Validate(Handler):
|
|||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
report(bool): force enable or disable reporting
|
report(bool): force enable or disable reporting
|
||||||
"""
|
"""
|
||||||
|
from ahriman.core.configuration.validator import Validator
|
||||||
|
|
||||||
schema = Validate.schema(repository_id, configuration)
|
schema = Validate.schema(repository_id, configuration)
|
||||||
validator = Validator(configuration=configuration, schema=schema)
|
validator = Validator(configuration=configuration, schema=schema)
|
||||||
|
|
||||||
|
@ -59,15 +59,13 @@ class Lock(LazyLogging):
|
|||||||
>>> configuration = Configuration()
|
>>> configuration = Configuration()
|
||||||
>>> try:
|
>>> try:
|
||||||
>>> with Lock(args, RepositoryId("x86_64", "aur-clone"), configuration):
|
>>> with Lock(args, RepositoryId("x86_64", "aur-clone"), configuration):
|
||||||
>>> perform_actions()
|
>>> do_something()
|
||||||
>>> except Exception as exception:
|
>>> except Exception as exception:
|
||||||
>>> handle_exceptions(exception)
|
>>> handle_exceptions(exception)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration) -> None:
|
def __init__(self, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
args(argparse.Namespace): command line args
|
args(argparse.Namespace): command line args
|
||||||
repository_id(RepositoryId): repository unique identifier
|
repository_id(RepositoryId): repository unique identifier
|
||||||
@ -99,7 +97,7 @@ class Lock(LazyLogging):
|
|||||||
fd(int): file descriptor:
|
fd(int): file descriptor:
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if file is locked and False otherwise
|
bool: ``True`` in case if file is locked and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
@ -121,7 +119,7 @@ class Lock(LazyLogging):
|
|||||||
watch until lock disappear
|
watch until lock disappear
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if file is locked and False otherwise
|
bool: ``True`` in case if file is locked and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
# there are reasons why we are not using inotify here. First of all, if we would use it, it would bring to
|
# there are reasons why we are not using inotify here. First of all, if we would use it, it would bring to
|
||||||
# race conditions because multiple processes will be notified at the same time. Secondly, it is good library,
|
# race conditions because multiple processes will be notified at the same time. Secondly, it is good library,
|
||||||
@ -225,7 +223,7 @@ class Lock(LazyLogging):
|
|||||||
exc_tb(TracebackType): exception traceback if any
|
exc_tb(TracebackType): exception traceback if any
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Literal[False]: always False (do not suppress any exception)
|
Literal[False]: always ``False`` (do not suppress any exception)
|
||||||
"""
|
"""
|
||||||
self.clear()
|
self.clear()
|
||||||
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
|
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
|
||||||
|
@ -33,9 +33,7 @@ class _Context:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""
|
""""""
|
||||||
default constructor. Must not be used directly
|
|
||||||
"""
|
|
||||||
self._content: dict[str, Any] = {}
|
self._content: dict[str, Any] = {}
|
||||||
|
|
||||||
def get(self, key: ContextKey[T] | type[T]) -> T:
|
def get(self, key: ContextKey[T] | type[T]) -> T:
|
||||||
|
@ -49,8 +49,6 @@ class Pacman(LazyLogging):
|
|||||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration, *,
|
def __init__(self, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
refresh_database: PacmanSynchronization) -> None:
|
refresh_database: PacmanSynchronization) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
repository_id(RepositoryId): repository unique identifier
|
repository_id(RepositoryId): repository unique identifier
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
|
@ -45,8 +45,6 @@ class PacmanDatabase(SyncHttpClient):
|
|||||||
|
|
||||||
def __init__(self, database: DB, configuration: Configuration) -> None:
|
def __init__(self, database: DB, configuration: Configuration) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
database(DB): pyalpm database object
|
database(DB): pyalpm database object
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
@ -102,7 +100,7 @@ class PacmanDatabase(SyncHttpClient):
|
|||||||
local_path(Path): path to locally stored file
|
local_path(Path): path to locally stored file
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if remote file is newer than local file
|
bool: ``True`` in case if remote file is newer than local file
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
PacmanError: in case if no last-modified header was found
|
PacmanError: in case if no last-modified header was found
|
||||||
|
@ -38,8 +38,6 @@ class Repo(LazyLogging):
|
|||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name(str): repository name
|
name(str): repository name
|
||||||
paths(RepositoryPaths): repository paths instance
|
paths(RepositoryPaths): repository paths instance
|
||||||
|
@ -38,8 +38,6 @@ class Auth(LazyLogging):
|
|||||||
|
|
||||||
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Disabled) -> None:
|
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Disabled) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
provider(AuthSettings, optional): authorization type definition (Default value = AuthSettings.Disabled)
|
provider(AuthSettings, optional): authorization type definition (Default value = AuthSettings.Disabled)
|
||||||
@ -96,7 +94,7 @@ class Auth(LazyLogging):
|
|||||||
password(str | None): entered password
|
password(str | None): entered password
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if password matches, False otherwise
|
bool: ``True`` in case if password matches, ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
del username, password
|
del username, password
|
||||||
return True
|
return True
|
||||||
@ -109,7 +107,7 @@ class Auth(LazyLogging):
|
|||||||
username(str): username
|
username(str): username
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if user is known and can be authorized and False otherwise
|
bool: ``True`` in case if user is known and can be authorized and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
del username
|
del username
|
||||||
return True
|
return True
|
||||||
@ -124,7 +122,7 @@ class Auth(LazyLogging):
|
|||||||
context(str | None): URI request path
|
context(str | None): URI request path
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if user is allowed to do this request and False otherwise
|
bool: ``True`` in case if user is allowed to do this request and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
del username, required, context
|
del username, required, context
|
||||||
return True
|
return True
|
||||||
|
@ -38,7 +38,7 @@ async def authorized_userid(*args: Any, **kwargs: Any) -> Any:
|
|||||||
**kwargs(Any): named argument list as provided by authorized_userid function
|
**kwargs(Any): named argument list as provided by authorized_userid function
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Any: None in case if no aiohttp_security module found and function call otherwise
|
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||||
"""
|
"""
|
||||||
if _has_aiohttp_security:
|
if _has_aiohttp_security:
|
||||||
return await aiohttp_security.authorized_userid(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
return await aiohttp_security.authorized_userid(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||||
@ -54,7 +54,7 @@ async def check_authorized(*args: Any, **kwargs: Any) -> Any:
|
|||||||
**kwargs(Any): named argument list as provided by authorized_userid function
|
**kwargs(Any): named argument list as provided by authorized_userid function
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Any: None in case if no aiohttp_security module found and function call otherwise
|
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||||
"""
|
"""
|
||||||
if _has_aiohttp_security:
|
if _has_aiohttp_security:
|
||||||
return await aiohttp_security.check_authorized(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
return await aiohttp_security.check_authorized(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||||
@ -70,7 +70,7 @@ async def forget(*args: Any, **kwargs: Any) -> Any:
|
|||||||
**kwargs(Any): named argument list as provided by authorized_userid function
|
**kwargs(Any): named argument list as provided by authorized_userid function
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Any: None in case if no aiohttp_security module found and function call otherwise
|
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||||
"""
|
"""
|
||||||
if _has_aiohttp_security:
|
if _has_aiohttp_security:
|
||||||
return await aiohttp_security.forget(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
return await aiohttp_security.forget(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||||
@ -86,7 +86,7 @@ async def remember(*args: Any, **kwargs: Any) -> Any:
|
|||||||
**kwargs(Any): named argument list as provided by authorized_userid function
|
**kwargs(Any): named argument list as provided by authorized_userid function
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Any: None in case if no aiohttp_security module found and function call otherwise
|
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||||
"""
|
"""
|
||||||
if _has_aiohttp_security:
|
if _has_aiohttp_security:
|
||||||
return await aiohttp_security.remember(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
return await aiohttp_security.remember(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||||
|
@ -37,8 +37,6 @@ class Mapping(Auth):
|
|||||||
def __init__(self, configuration: Configuration, database: SQLite,
|
def __init__(self, configuration: Configuration, database: SQLite,
|
||||||
provider: AuthSettings = AuthSettings.Configuration) -> None:
|
provider: AuthSettings = AuthSettings.Configuration) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
database(SQLite): database instance
|
database(SQLite): database instance
|
||||||
@ -57,7 +55,7 @@ class Mapping(Auth):
|
|||||||
password(str | None): entered password
|
password(str | None): entered password
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if password matches, False otherwise
|
bool: ``True`` in case if password matches, ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
if password is None:
|
if password is None:
|
||||||
return False # invalid data supplied
|
return False # invalid data supplied
|
||||||
@ -72,7 +70,7 @@ class Mapping(Auth):
|
|||||||
username(str): username
|
username(str): username
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
User | None: user descriptor if username is known and None otherwise
|
User | None: user descriptor if username is known and ``None`` otherwise
|
||||||
"""
|
"""
|
||||||
return self.database.user_get(username)
|
return self.database.user_get(username)
|
||||||
|
|
||||||
@ -84,7 +82,7 @@ class Mapping(Auth):
|
|||||||
username(str): username
|
username(str): username
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if user is known and can be authorized and False otherwise
|
bool: ``True`` in case if user is known and can be authorized and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
return username is not None and self.get_user(username) is not None
|
return username is not None and self.get_user(username) is not None
|
||||||
|
|
||||||
@ -98,7 +96,7 @@ class Mapping(Auth):
|
|||||||
context(str | None): URI request path
|
context(str | None): URI request path
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if user is allowed to do this request and False otherwise
|
bool: ``True`` in case if user is allowed to do this request and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
user = self.get_user(username)
|
user = self.get_user(username)
|
||||||
return user is not None and user.verify_access(required)
|
return user is not None and user.verify_access(required)
|
||||||
|
@ -43,8 +43,6 @@ class OAuth(Mapping):
|
|||||||
def __init__(self, configuration: Configuration, database: SQLite,
|
def __init__(self, configuration: Configuration, database: SQLite,
|
||||||
provider: AuthSettings = AuthSettings.OAuth) -> None:
|
provider: AuthSettings = AuthSettings.OAuth) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
database(SQLite): database instance
|
database(SQLite): database instance
|
||||||
|
@ -41,8 +41,6 @@ class PAM(Mapping):
|
|||||||
def __init__(self, configuration: Configuration, database: SQLite,
|
def __init__(self, configuration: Configuration, database: SQLite,
|
||||||
provider: AuthSettings = AuthSettings.PAM) -> None:
|
provider: AuthSettings = AuthSettings.PAM) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
database(SQLite): database instance
|
database(SQLite): database instance
|
||||||
@ -79,7 +77,7 @@ class PAM(Mapping):
|
|||||||
password(str | None): entered password
|
password(str | None): entered password
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if password matches, False otherwise
|
bool: ``True`` in case if password matches, ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
if password is None:
|
if password is None:
|
||||||
return False # invalid data supplied
|
return False # invalid data supplied
|
||||||
@ -101,7 +99,7 @@ class PAM(Mapping):
|
|||||||
username(str): username
|
username(str): username
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if user is known and can be authorized and False otherwise
|
bool: ``True`` in case if user is known and can be authorized and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
_ = getpwnam(username)
|
_ = getpwnam(username)
|
||||||
@ -119,7 +117,7 @@ class PAM(Mapping):
|
|||||||
context(str | None): URI request path
|
context(str | None): URI request path
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if user is allowed to do this request and False otherwise
|
bool: ``True`` in case if user is allowed to do this request and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
# this method is basically inverted, first we check overrides in database and then fallback to the PAM logic
|
# this method is basically inverted, first we check overrides in database and then fallback to the PAM logic
|
||||||
if (user := self.get_user(username)) is not None:
|
if (user := self.get_user(username)) is not None:
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
# 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 dataclasses import dataclass
|
|
||||||
from elftools.elf.dynamic import DynamicSection
|
from elftools.elf.dynamic import DynamicSection
|
||||||
from elftools.elf.elffile import ELFFile
|
from elftools.elf.elffile import ELFFile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -33,7 +32,6 @@ from ahriman.models.package import Package
|
|||||||
from ahriman.models.scan_paths import ScanPaths
|
from ahriman.models.scan_paths import ScanPaths
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PackageArchive:
|
class PackageArchive:
|
||||||
"""
|
"""
|
||||||
helper for package archives
|
helper for package archives
|
||||||
@ -45,10 +43,18 @@ class PackageArchive:
|
|||||||
scan_paths(ScanPaths): scan paths holder
|
scan_paths(ScanPaths): scan paths holder
|
||||||
"""
|
"""
|
||||||
|
|
||||||
root: Path
|
def __init__(self, root: Path, package: Package, pacman: Pacman, scan_paths: ScanPaths) -> None:
|
||||||
package: Package
|
"""
|
||||||
pacman: Pacman
|
Args:
|
||||||
scan_paths: ScanPaths
|
root(Path): path to root filesystem
|
||||||
|
package(Package): package descriptor
|
||||||
|
pacman(Pacman): alpm wrapper instance
|
||||||
|
scan_paths(ScanPaths): scan paths holder
|
||||||
|
"""
|
||||||
|
self.root = root
|
||||||
|
self.package = package
|
||||||
|
self.pacman = pacman
|
||||||
|
self.scan_paths = scan_paths
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def dynamic_needed(binary_path: Path) -> list[str]:
|
def dynamic_needed(binary_path: Path) -> list[str]:
|
||||||
@ -163,7 +169,7 @@ class PackageArchive:
|
|||||||
|
|
||||||
result: dict[Path, list[FilesystemPackage]] = {}
|
result: dict[Path, list[FilesystemPackage]] = {}
|
||||||
# sort items from children directories to root
|
# sort items from children directories to root
|
||||||
for path, packages in reversed(sorted(source.items())):
|
for path, packages in sorted(source.items(), reverse=True):
|
||||||
# skip if this path belongs to the one of the base packages
|
# skip if this path belongs to the one of the base packages
|
||||||
if any(package.package_name in base_packages for package in packages):
|
if any(package.package_name in base_packages for package in packages):
|
||||||
continue
|
continue
|
||||||
@ -228,7 +234,7 @@ class PackageArchive:
|
|||||||
extract list of the installed packages and their content
|
extract list of the installed packages and their content
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict[str, FilesystemPackage]; map of package name to list of directories and files contained
|
dict[str, FilesystemPackage]: map of package name to list of directories and files contained
|
||||||
by this package
|
by this package
|
||||||
"""
|
"""
|
||||||
result = {}
|
result = {}
|
@ -64,8 +64,9 @@ class Sources(LazyLogging):
|
|||||||
return None # no previous reference found
|
return None # no previous reference found
|
||||||
|
|
||||||
instance = Sources()
|
instance = Sources()
|
||||||
instance.fetch_until(source_dir, commit_sha=last_commit_sha)
|
if instance.fetch_until(source_dir, commit_sha=last_commit_sha) is not None:
|
||||||
return instance.diff(source_dir, last_commit_sha)
|
return instance.diff(source_dir, last_commit_sha)
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def extend_architectures(sources_dir: Path, architecture: str) -> list[PkgbuildPatch]:
|
def extend_architectures(sources_dir: Path, architecture: str) -> list[PkgbuildPatch]:
|
||||||
@ -138,7 +139,7 @@ class Sources(LazyLogging):
|
|||||||
sources_dir(Path): local path to git repository
|
sources_dir(Path): local path to git repository
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
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 = check_output(*instance.git(), "remote", cwd=sources_dir, logger=instance.logger)
|
remotes = check_output(*instance.git(), "remote", cwd=sources_dir, logger=instance.logger)
|
||||||
@ -261,7 +262,7 @@ class Sources(LazyLogging):
|
|||||||
commit_author(tuple[str, str] | None, optional): optional commit author if any (Default value = None)
|
commit_author(tuple[str, str] | None, optional): optional commit author if any (Default value = None)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True in case if changes have been committed and False otherwise
|
bool: ``True`` in case if changes have been committed and ``False`` otherwise
|
||||||
"""
|
"""
|
||||||
if not self.has_changes(sources_dir):
|
if not self.has_changes(sources_dir):
|
||||||
return False # nothing to commit
|
return False # nothing to commit
|
||||||
@ -298,7 +299,8 @@ class Sources(LazyLogging):
|
|||||||
args.append(sha)
|
args.append(sha)
|
||||||
return check_output(*self.git(), "diff", *args, cwd=sources_dir, logger=self.logger)
|
return check_output(*self.git(), "diff", *args, cwd=sources_dir, logger=self.logger)
|
||||||
|
|
||||||
def fetch_until(self, sources_dir: Path, *, branch: str | None = None, commit_sha: str | None = None) -> None:
|
def fetch_until(self, sources_dir: Path, *, branch: str | None = None, commit_sha: str | None = None,
|
||||||
|
max_depth: int = 10) -> str | None:
|
||||||
"""
|
"""
|
||||||
fetch repository until commit sha
|
fetch repository until commit sha
|
||||||
|
|
||||||
@ -307,11 +309,16 @@ class Sources(LazyLogging):
|
|||||||
branch(str | None, optional): use specified branch (Default value = None)
|
branch(str | None, optional): use specified branch (Default value = None)
|
||||||
commit_sha(str | None, optional): commit hash to fetch. If none set, only one will be fetched
|
commit_sha(str | None, optional): commit hash to fetch. If none set, only one will be fetched
|
||||||
(Default value = None)
|
(Default value = None)
|
||||||
|
max_depth(int, optional): maximal amount of commits to fetch if ``commit_sha`` is set (Default value = 10)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str | None: fetched ``commit_sha`` (if set) and ``None`` in case if commit wasn't found or
|
||||||
|
``commit_sha`` is not set
|
||||||
"""
|
"""
|
||||||
commit_sha = commit_sha or "HEAD" # if none set we just fetch the last commit
|
commit_sha = commit_sha or "HEAD" # if none set we just fetch the last commit
|
||||||
|
|
||||||
commits_count = 1
|
commits_count = 1
|
||||||
while commit_sha is not None:
|
while commits_count <= max_depth:
|
||||||
command = self.git() + ["fetch", "--quiet", "--depth", str(commits_count)]
|
command = self.git() + ["fetch", "--quiet", "--depth", str(commits_count)]
|
||||||
if branch is not None:
|
if branch is not None:
|
||||||
command += ["origin", branch]
|
command += ["origin", branch]
|
||||||
@ -320,10 +327,13 @@ class Sources(LazyLogging):
|
|||||||
try:
|
try:
|
||||||
# check if there is an object in current git directory
|
# check if there is an object in current git directory
|
||||||
check_output(*self.git(), "cat-file", "-e", commit_sha, cwd=sources_dir, logger=self.logger)
|
check_output(*self.git(), "cat-file", "-e", commit_sha, cwd=sources_dir, logger=self.logger)
|
||||||
commit_sha = None # reset search
|
return commit_sha # found the required commit
|
||||||
except CalledProcessError:
|
except CalledProcessError:
|
||||||
commits_count += 1 # increase depth
|
commits_count += 1 # increase depth
|
||||||
|
|
||||||
|
# no commits found at the requested depth
|
||||||
|
return None
|
||||||
|
|
||||||
def git(self, gitconfig: dict[str, str] | None = None) -> list[str]:
|
def git(self, gitconfig: dict[str, str] | None = None) -> list[str]:
|
||||||
"""
|
"""
|
||||||
git command prefix
|
git command prefix
|
||||||
@ -351,7 +361,7 @@ class Sources(LazyLogging):
|
|||||||
sources_dir(Path): local path to git repository
|
sources_dir(Path): local path to git repository
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
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 = check_output(*self.git(), "diff", "--cached", "--name-only", cwd=sources_dir, logger=self.logger)
|
changes = check_output(*self.git(), "diff", "--cached", "--name-only", cwd=sources_dir, logger=self.logger)
|
||||||
|
@ -48,8 +48,6 @@ class Task(LazyLogging):
|
|||||||
def __init__(self, package: Package, configuration: Configuration, architecture: str,
|
def __init__(self, package: Package, configuration: Configuration, architecture: str,
|
||||||
paths: RepositoryPaths) -> None:
|
paths: RepositoryPaths) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package(Package): package definitions
|
package(Package): package definitions
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
|
@ -46,7 +46,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 :class:`configparser.RawConfigParser` class, the same flow is applicable here.
|
derived from built-in :class:`configparser.RawConfigParser` class, the same flow is applicable here.
|
||||||
Nevertheless, it is recommended to use :func:`from_path` class method which also calls initialization methods::
|
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,7 +58,7 @@ 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 :func:`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
|
||||||
:exc:`ahriman.core.exceptions.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()
|
||||||
@ -70,8 +71,6 @@ class Configuration(configparser.RawConfigParser):
|
|||||||
|
|
||||||
def __init__(self, allow_no_value: bool = False) -> None:
|
def __init__(self, allow_no_value: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor. In the most cases must not be called directly
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
allow_no_value(bool, optional): copies :class:`configparser.RawConfigParser` behaviour. In case if it is set
|
allow_no_value(bool, optional): copies :class:`configparser.RawConfigParser` 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)
|
||||||
@ -344,7 +343,8 @@ 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 :func:`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
|
||||||
|
@ -169,14 +169,6 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
|||||||
"build": {
|
"build": {
|
||||||
"type": "dict",
|
"type": "dict",
|
||||||
"schema": {
|
"schema": {
|
||||||
"allowed_scan_paths": {
|
|
||||||
"type": "list",
|
|
||||||
"coerce": "list",
|
|
||||||
"schema": {
|
|
||||||
"type": "path",
|
|
||||||
"coerce": "absolute_path",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"archbuild_flags": {
|
"archbuild_flags": {
|
||||||
"type": "list",
|
"type": "list",
|
||||||
"coerce": "list",
|
"coerce": "list",
|
||||||
@ -185,14 +177,6 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
|||||||
"empty": False,
|
"empty": False,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"blacklisted_scan_paths": {
|
|
||||||
"type": "list",
|
|
||||||
"coerce": "list",
|
|
||||||
"schema": {
|
|
||||||
"type": "path",
|
|
||||||
"coerce": "absolute_path",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"build_command": {
|
"build_command": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": True,
|
"required": True,
|
||||||
@ -226,6 +210,14 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
|||||||
"empty": False,
|
"empty": False,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scan_paths": {
|
||||||
|
"type": "list",
|
||||||
|
"coerce": "list",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"empty": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"type": "list",
|
"type": "list",
|
||||||
"coerce": "list",
|
"coerce": "list",
|
||||||
|
@ -19,21 +19,93 @@
|
|||||||
#
|
#
|
||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
from collections.abc import Mapping, MutableMapping
|
from collections.abc import Generator, Mapping, MutableMapping
|
||||||
from string import Template
|
from string import Template
|
||||||
|
|
||||||
|
|
||||||
|
class ExtendedTemplate(Template):
|
||||||
|
"""
|
||||||
|
extension to the default :class:`Template` class, which also enabled braces regex to lookup in sections
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
braceidpattern(str): regular expression to match a colon inside braces
|
||||||
|
"""
|
||||||
|
|
||||||
|
braceidpattern = r"(?a:[_a-z0-9][_a-z0-9:]*)"
|
||||||
|
|
||||||
|
|
||||||
class ShellInterpolator(configparser.Interpolation):
|
class ShellInterpolator(configparser.Interpolation):
|
||||||
"""
|
"""
|
||||||
custom string interpolator, because we cannot use defaults argument due to config validation
|
custom string interpolator, because we cannot use defaults argument due to config validation
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
DATA_LINK_ESCAPE = "\x10"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_variables(parser: MutableMapping[str, Mapping[str, str]], value: str,
|
||||||
|
defaults: Mapping[str, str]) -> Generator[tuple[str, str], None, None]:
|
||||||
|
"""
|
||||||
|
extract keys and values (if available) from the configuration. In case if a key is not available, it will be
|
||||||
|
silently skipped from the result
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parser(MutableMapping[str, Mapping[str, str]]): option parser
|
||||||
|
value(str): source (not-converted) value
|
||||||
|
defaults(Mapping[str, str]): default values
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
tuple[str, str]: variable name used for substitution and its value
|
||||||
|
"""
|
||||||
|
def identifiers() -> Generator[tuple[str | None, str], None, None]:
|
||||||
|
# extract all found identifiers and parse them
|
||||||
|
for identifier in ExtendedTemplate(value).get_identifiers():
|
||||||
|
match identifier.split(":"):
|
||||||
|
case [lookup_option]: # single option from the same section
|
||||||
|
yield None, lookup_option
|
||||||
|
case [lookup_section, lookup_option]: # reference to another section
|
||||||
|
yield lookup_section, lookup_option
|
||||||
|
|
||||||
|
for section, option in identifiers():
|
||||||
|
# key to be substituted
|
||||||
|
key = f"{section}:{option}" if section else option
|
||||||
|
|
||||||
|
if section is not None: # foreign section case
|
||||||
|
if section not in parser:
|
||||||
|
continue # section was not found, silently skip it
|
||||||
|
values = parser[section]
|
||||||
|
else: # same section
|
||||||
|
values = defaults
|
||||||
|
|
||||||
|
if (raw := values.get(option)) is not None:
|
||||||
|
yield key, raw
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def environment() -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
extract environment variables
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, str]: environment variables and some custom variables
|
||||||
|
"""
|
||||||
|
return os.environ | {
|
||||||
|
"prefix": sys.prefix,
|
||||||
|
}
|
||||||
|
|
||||||
def before_get(self, parser: MutableMapping[str, Mapping[str, str]], section: str, option: str, value: str,
|
def before_get(self, parser: MutableMapping[str, Mapping[str, str]], section: str, option: str, value: str,
|
||||||
defaults: Mapping[str, str]) -> str:
|
defaults: Mapping[str, str]) -> str:
|
||||||
"""
|
"""
|
||||||
interpolate option value
|
interpolate option value
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
This method is using :class:`string.Template` class in order to render both cross-references and
|
||||||
|
environment variables, because it seems that it is the most legit way to handle it. In addition,
|
||||||
|
we are using shell-like variables in some cases (see :attr:`alpm.mirror` option), thus we would like
|
||||||
|
to keep them alive.
|
||||||
|
|
||||||
|
First this method resolves substitution from the configuration and then renders environment variables
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
parser(MutableMapping[str, Mapping[str, str]]): option parser
|
parser(MutableMapping[str, Mapping[str, str]]): option parser
|
||||||
section(str): section name
|
section(str): section name
|
||||||
@ -44,8 +116,15 @@ class ShellInterpolator(configparser.Interpolation):
|
|||||||
Returns:
|
Returns:
|
||||||
str: substituted value
|
str: substituted value
|
||||||
"""
|
"""
|
||||||
# At the moment it seems that it is the most legit way to handle environment variables
|
# because any substitution effectively replace escaped $ ($$) in result, we have to escape it manually
|
||||||
# Template behaviour is literally the same as shell
|
escaped = value.replace("$$", self.DATA_LINK_ESCAPE)
|
||||||
# In addition, we are using shell-like variables in some cases (see :attr:`alpm.mirror` option),
|
|
||||||
# thus we would like to keep them alive
|
# resolve internal references
|
||||||
return Template(value).safe_substitute(os.environ)
|
variables = dict(self._extract_variables(parser, value, defaults))
|
||||||
|
internal = ExtendedTemplate(escaped).safe_substitute(variables)
|
||||||
|
|
||||||
|
# resolve enriched environment variables by using default Template class
|
||||||
|
environment = Template(internal).safe_substitute(self.environment())
|
||||||
|
|
||||||
|
# replace escaped values back
|
||||||
|
return environment.replace(self.DATA_LINK_ESCAPE, "$")
|
||||||
|
@ -35,13 +35,12 @@ class Validator(RootValidator):
|
|||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
"""
|
"""
|
||||||
|
|
||||||
types_mapping = RootValidator.types_mapping.copy()
|
types_mapping = RootValidator.types_mapping | {
|
||||||
types_mapping["path"] = TypeDefinition("path", (Path,), ())
|
"path": TypeDefinition("path", (Path,), ()),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
configuration(Configuration): configuration instance used for extraction
|
configuration(Configuration): configuration instance used for extraction
|
||||||
*args(Any): positional arguments to be passed to base validator
|
*args(Any): positional arguments to be passed to base validator
|
||||||
@ -149,7 +148,7 @@ class Validator(RootValidator):
|
|||||||
check if paths exists
|
check if paths exists
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
constraint(bool): True in case if path must exist and False otherwise
|
constraint(bool): ``True`` in case if path must exist and ``False`` otherwise
|
||||||
field(str): field name to be checked
|
field(str): field name to be checked
|
||||||
value(Path): value to be checked
|
value(Path): value to be checked
|
||||||
|
|
||||||
|
@ -41,8 +41,6 @@ class Migrations(LazyLogging):
|
|||||||
|
|
||||||
def __init__(self, connection: Connection, configuration: Configuration) -> None:
|
def __init__(self, connection: Connection, configuration: Configuration) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
connection(Connection): database connection
|
connection(Connection): database connection
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
|
38
src/ahriman/core/database/migrations/m014_auditlog.py
Normal file
38
src/ahriman/core/database/migrations/m014_auditlog.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
__all__ = ["steps"]
|
||||||
|
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
"""
|
||||||
|
create table auditlog (
|
||||||
|
created integer not null,
|
||||||
|
repository text not null,
|
||||||
|
event text not null,
|
||||||
|
object_id text not null,
|
||||||
|
message text,
|
||||||
|
data json
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
create index auditlog_created_repository_event_object_id
|
||||||
|
on auditlog (created, repository, event, object_id)
|
||||||
|
""",
|
||||||
|
]
|
@ -21,6 +21,7 @@ from ahriman.core.database.operations.auth_operations import AuthOperations
|
|||||||
from ahriman.core.database.operations.build_operations import BuildOperations
|
from ahriman.core.database.operations.build_operations import BuildOperations
|
||||||
from ahriman.core.database.operations.changes_operations import ChangesOperations
|
from ahriman.core.database.operations.changes_operations import ChangesOperations
|
||||||
from ahriman.core.database.operations.dependencies_operations import DependenciesOperations
|
from ahriman.core.database.operations.dependencies_operations import DependenciesOperations
|
||||||
|
from ahriman.core.database.operations.event_operations import EventOperations
|
||||||
from ahriman.core.database.operations.logs_operations import LogsOperations
|
from ahriman.core.database.operations.logs_operations import LogsOperations
|
||||||
from ahriman.core.database.operations.package_operations import PackageOperations
|
from ahriman.core.database.operations.package_operations import PackageOperations
|
||||||
from ahriman.core.database.operations.patch_operations import PatchOperations
|
from ahriman.core.database.operations.patch_operations import PatchOperations
|
||||||
|
@ -39,7 +39,7 @@ class DependenciesOperations(Operations):
|
|||||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dependencies: changes for the package base if available
|
Dependencies: dependencies for the package base if available
|
||||||
"""
|
"""
|
||||||
repository_id = repository_id or self._repository_id
|
repository_id = repository_id or self._repository_id
|
||||||
|
|
||||||
|
108
src/ahriman/core/database/operations/event_operations.py
Normal file
108
src/ahriman/core/database/operations/event_operations.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from sqlite3 import Connection
|
||||||
|
|
||||||
|
from ahriman.core.database.operations.operations import Operations
|
||||||
|
from ahriman.models.event import Event, EventType
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class EventOperations(Operations):
|
||||||
|
"""
|
||||||
|
operations for audit log table
|
||||||
|
"""
|
||||||
|
|
||||||
|
def event_get(self, event: str | EventType | None = None, object_id: str | None = None,
|
||||||
|
from_date: int | float | None = None, to_date: int | float | None = None,
|
||||||
|
limit: int = -1, offset: int = 0, repository_id: RepositoryId | None = None) -> list[Event]:
|
||||||
|
"""
|
||||||
|
get list of events with filters applied
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event(str | EventType | None, optional): filter by event type (Default value = None)
|
||||||
|
object_id(str | None, optional): filter by event object (Default value = None)
|
||||||
|
from_date(int | float | None, optional): minimal creation date, inclusive (Default value = None)
|
||||||
|
to_date(int | float | None, optional): maximal creation date, exclusive (Default value = None)
|
||||||
|
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||||
|
offset(int, optional): records offset (Default value = 0)
|
||||||
|
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Event]: list of audit log events
|
||||||
|
"""
|
||||||
|
repository_id = repository_id or self._repository_id
|
||||||
|
|
||||||
|
def run(connection: Connection) -> list[Event]:
|
||||||
|
return [
|
||||||
|
Event.from_json(row)
|
||||||
|
for row in connection.execute(
|
||||||
|
"""
|
||||||
|
select created, event, object_id, message, data from (
|
||||||
|
select * from auditlog
|
||||||
|
where (:event is null or event = :event)
|
||||||
|
and (:object_id is null or object_id = :object_id)
|
||||||
|
and (:from_date is null or created >= :from_date)
|
||||||
|
and (:to_date is null or created < :to_date)
|
||||||
|
and repository = :repository
|
||||||
|
order by created desc limit :limit offset :offset
|
||||||
|
) order by created asc
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"event": event,
|
||||||
|
"object_id": object_id,
|
||||||
|
"repository": repository_id.id,
|
||||||
|
"from_date": from_date,
|
||||||
|
"to_date": to_date,
|
||||||
|
"limit": limit,
|
||||||
|
"offset": offset,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.with_connection(run)
|
||||||
|
|
||||||
|
def event_insert(self, event: Event, repository_id: RepositoryId | None = None) -> None:
|
||||||
|
"""
|
||||||
|
insert audit log event
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event(Event): event to insert
|
||||||
|
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||||
|
"""
|
||||||
|
repository_id = repository_id or self._repository_id
|
||||||
|
|
||||||
|
def run(connection: Connection) -> None:
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
insert into auditlog
|
||||||
|
(created, repository, event, object_id, message, data)
|
||||||
|
values
|
||||||
|
(:created, :repository, :event, :object_id, :message, :data)
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"created": event.created,
|
||||||
|
"repository": repository_id.id,
|
||||||
|
"event": event.event,
|
||||||
|
"object_id": event.object_id,
|
||||||
|
"message": event.message,
|
||||||
|
"data": event.data,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.with_connection(run, commit=True)
|
@ -50,9 +50,11 @@ class LogsOperations(Operations):
|
|||||||
(row["created"], row["record"])
|
(row["created"], row["record"])
|
||||||
for row in connection.execute(
|
for row in connection.execute(
|
||||||
"""
|
"""
|
||||||
select created, record from logs
|
select created, record from (
|
||||||
where package_base = :package_base and repository = :repository
|
select * from logs
|
||||||
order by created limit :limit offset :offset
|
where package_base = :package_base and repository = :repository
|
||||||
|
order by created desc limit :limit offset :offset
|
||||||
|
) order by created asc
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
"package_base": package_base,
|
"package_base": package_base,
|
||||||
|
@ -41,16 +41,25 @@ class Operations(LazyLogging):
|
|||||||
|
|
||||||
def __init__(self, path: Path, repository_id: RepositoryId, repository_paths: RepositoryPaths) -> None:
|
def __init__(self, path: Path, repository_id: RepositoryId, repository_paths: RepositoryPaths) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path(Path): path to the database file
|
path(Path): path to the database file
|
||||||
repository_id(RepositoryId): repository unique identifier
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
repository_paths(RepositoryPaths): repository paths
|
||||||
"""
|
"""
|
||||||
self.path = path
|
self.path = path
|
||||||
self._repository_id = repository_id
|
self._repository_id = repository_id
|
||||||
self._repository_paths = repository_paths
|
self._repository_paths = repository_paths
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logger_name(self) -> str:
|
||||||
|
"""
|
||||||
|
extract logger name for the class
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: logger name override
|
||||||
|
"""
|
||||||
|
return "sql"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def factory(cursor: sqlite3.Cursor, row: tuple[Any, ...]) -> dict[str, Any]:
|
def factory(cursor: sqlite3.Cursor, row: tuple[Any, ...]) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@ -74,12 +83,13 @@ class Operations(LazyLogging):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
query(Callable[[Connection], T]): function to be called with connection
|
query(Callable[[Connection], T]): function to be called with connection
|
||||||
commit(bool, optional): if True commit() will be called on success (Default value = False)
|
commit(bool, optional): if ``True`` commit() will be called on success (Default value = False)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
T: result of the ``query`` call
|
T: result of the ``query`` call
|
||||||
"""
|
"""
|
||||||
with sqlite3.connect(self.path, detect_types=sqlite3.PARSE_DECLTYPES) as connection:
|
with sqlite3.connect(self.path, detect_types=sqlite3.PARSE_DECLTYPES) as connection:
|
||||||
|
connection.set_trace_callback(self.logger.debug)
|
||||||
connection.row_factory = self.factory
|
connection.row_factory = self.factory
|
||||||
result = query(connection)
|
result = query(connection)
|
||||||
if commit:
|
if commit:
|
||||||
|
@ -26,7 +26,7 @@ from typing import Self
|
|||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.database.migrations import Migrations
|
from ahriman.core.database.migrations import Migrations
|
||||||
from ahriman.core.database.operations import AuthOperations, BuildOperations, ChangesOperations, \
|
from ahriman.core.database.operations import AuthOperations, BuildOperations, ChangesOperations, \
|
||||||
DependenciesOperations, LogsOperations, PackageOperations, PatchOperations
|
DependenciesOperations, EventOperations, LogsOperations, PackageOperations, PatchOperations
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
@ -36,6 +36,7 @@ class SQLite(
|
|||||||
BuildOperations,
|
BuildOperations,
|
||||||
ChangesOperations,
|
ChangesOperations,
|
||||||
DependenciesOperations,
|
DependenciesOperations,
|
||||||
|
EventOperations,
|
||||||
LogsOperations,
|
LogsOperations,
|
||||||
PackageOperations,
|
PackageOperations,
|
||||||
PatchOperations):
|
PatchOperations):
|
||||||
|
@ -59,8 +59,6 @@ class DistributedSystem(Trigger, WebClient):
|
|||||||
|
|
||||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
repository_id(RepositoryId): repository unique identifier
|
repository_id(RepositoryId): repository unique identifier
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
|
@ -34,8 +34,6 @@ class WorkerTrigger(DistributedSystem):
|
|||||||
|
|
||||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
repository_id(RepositoryId): repository unique identifier
|
repository_id(RepositoryId): repository unique identifier
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
|
@ -36,8 +36,6 @@ class WorkersCache(LazyLogging):
|
|||||||
|
|
||||||
def __init__(self, configuration: Configuration) -> None:
|
def __init__(self, configuration: Configuration) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
"""
|
"""
|
||||||
|
@ -33,8 +33,6 @@ class BuildError(RuntimeError):
|
|||||||
|
|
||||||
def __init__(self, package_base: str, stderr: str | None = None) -> None:
|
def __init__(self, package_base: str, stderr: str | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package_base(str): package base raised exception
|
package_base(str): package base raised exception
|
||||||
stderr(str | None, optional): stderr of the process if available (Default value = None)
|
stderr(str | None, optional): stderr of the process if available (Default value = None)
|
||||||
@ -67,8 +65,6 @@ class CalledProcessError(subprocess.CalledProcessError):
|
|||||||
|
|
||||||
def __init__(self, status_code: int, process: list[str], stderr: str) -> None:
|
def __init__(self, status_code: int, process: list[str], stderr: str) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
status_code(int): process return code
|
status_code(int): process return code
|
||||||
process(list[str]): process argument list
|
process(list[str]): process argument list
|
||||||
@ -94,9 +90,7 @@ class DuplicateRunError(RuntimeError):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""
|
""""""
|
||||||
default constructor
|
|
||||||
"""
|
|
||||||
RuntimeError.__init__(
|
RuntimeError.__init__(
|
||||||
self, "Another application instance is run. This error can be suppressed by using --force flag.")
|
self, "Another application instance is run. This error can be suppressed by using --force flag.")
|
||||||
|
|
||||||
@ -119,9 +113,7 @@ class GitRemoteError(RuntimeError):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""
|
""""""
|
||||||
default constructor
|
|
||||||
"""
|
|
||||||
RuntimeError.__init__(self, "Git remote failed")
|
RuntimeError.__init__(self, "Git remote failed")
|
||||||
|
|
||||||
|
|
||||||
@ -132,8 +124,6 @@ class InitializeError(RuntimeError):
|
|||||||
|
|
||||||
def __init__(self, details: str) -> None:
|
def __init__(self, details: str) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
details(str): details of the exception
|
details(str): details of the exception
|
||||||
"""
|
"""
|
||||||
@ -147,8 +137,6 @@ class MigrationError(RuntimeError):
|
|||||||
|
|
||||||
def __init__(self, details: str) -> None:
|
def __init__(self, details: str) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
details(str): error details
|
details(str): error details
|
||||||
"""
|
"""
|
||||||
@ -162,8 +150,6 @@ class MissingArchitectureError(ValueError):
|
|||||||
|
|
||||||
def __init__(self, command: str) -> None:
|
def __init__(self, command: str) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
command(str): command name which throws exception
|
command(str): command name which throws exception
|
||||||
"""
|
"""
|
||||||
@ -177,8 +163,6 @@ class MultipleArchitecturesError(ValueError):
|
|||||||
|
|
||||||
def __init__(self, command: str, repositories: list[RepositoryId] | None = None) -> None:
|
def __init__(self, command: str, repositories: list[RepositoryId] | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
command(str): command name which throws exception
|
command(str): command name which throws exception
|
||||||
repositories(list[RepositoryId] | None, optional): found repository list (Default value = None)
|
repositories(list[RepositoryId] | None, optional): found repository list (Default value = None)
|
||||||
@ -196,8 +180,6 @@ class OptionError(ValueError):
|
|||||||
|
|
||||||
def __init__(self, value: Any) -> None:
|
def __init__(self, value: Any) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
value(Any): option value
|
value(Any): option value
|
||||||
"""
|
"""
|
||||||
@ -211,8 +193,6 @@ class PackageInfoError(RuntimeError):
|
|||||||
|
|
||||||
def __init__(self, details: Any) -> None:
|
def __init__(self, details: Any) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
details(Any): error details
|
details(Any): error details
|
||||||
"""
|
"""
|
||||||
@ -226,8 +206,6 @@ class PacmanError(RuntimeError):
|
|||||||
|
|
||||||
def __init__(self, details: Any) -> None:
|
def __init__(self, details: Any) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
details(Any): error details
|
details(Any): error details
|
||||||
"""
|
"""
|
||||||
@ -241,8 +219,6 @@ class PathError(ValueError):
|
|||||||
|
|
||||||
def __init__(self, path: Path, root: Path) -> None:
|
def __init__(self, path: Path, root: Path) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path(Path): path which raised an exception
|
path(Path): path which raised an exception
|
||||||
root(Path): repository root (i.e. ahriman home)
|
root(Path): repository root (i.e. ahriman home)
|
||||||
@ -257,8 +233,6 @@ class PasswordError(ValueError):
|
|||||||
|
|
||||||
def __init__(self, details: Any) -> None:
|
def __init__(self, details: Any) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
details(Any); error details
|
details(Any); error details
|
||||||
"""
|
"""
|
||||||
@ -272,8 +246,6 @@ class PartitionError(RuntimeError):
|
|||||||
|
|
||||||
def __init__(self, count: int) -> None:
|
def __init__(self, count: int) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
count(int): count of partitions
|
count(int): count of partitions
|
||||||
"""
|
"""
|
||||||
@ -286,9 +258,7 @@ class PkgbuildGeneratorError(RuntimeError):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""
|
""""""
|
||||||
default constructor
|
|
||||||
"""
|
|
||||||
RuntimeError.__init__(self, "Could not generate package")
|
RuntimeError.__init__(self, "Could not generate package")
|
||||||
|
|
||||||
|
|
||||||
@ -298,9 +268,7 @@ class ReportError(RuntimeError):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""
|
""""""
|
||||||
default constructor
|
|
||||||
"""
|
|
||||||
RuntimeError.__init__(self, "Report failed")
|
RuntimeError.__init__(self, "Report failed")
|
||||||
|
|
||||||
|
|
||||||
@ -310,9 +278,7 @@ class SynchronizationError(RuntimeError):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""
|
""""""
|
||||||
default constructor
|
|
||||||
"""
|
|
||||||
RuntimeError.__init__(self, "Sync failed")
|
RuntimeError.__init__(self, "Sync failed")
|
||||||
|
|
||||||
|
|
||||||
@ -323,8 +289,6 @@ class UnknownPackageError(ValueError):
|
|||||||
|
|
||||||
def __init__(self, package_base: str) -> None:
|
def __init__(self, package_base: str) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package_base(str): package base name
|
package_base(str): package base name
|
||||||
"""
|
"""
|
||||||
@ -338,8 +302,6 @@ class UnsafeRunError(RuntimeError):
|
|||||||
|
|
||||||
def __init__(self, current_uid: int, root_uid: int) -> None:
|
def __init__(self, current_uid: int, root_uid: int) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
current_uid(int): current user ID
|
current_uid(int): current user ID
|
||||||
root_uid(int): ID of the owner of root directory
|
root_uid(int): ID of the owner of root directory
|
||||||
|
@ -22,7 +22,9 @@ from ahriman.core.formatters.build_printer import BuildPrinter
|
|||||||
from ahriman.core.formatters.changes_printer import ChangesPrinter
|
from ahriman.core.formatters.changes_printer import ChangesPrinter
|
||||||
from ahriman.core.formatters.configuration_paths_printer import ConfigurationPathsPrinter
|
from ahriman.core.formatters.configuration_paths_printer import ConfigurationPathsPrinter
|
||||||
from ahriman.core.formatters.configuration_printer import ConfigurationPrinter
|
from ahriman.core.formatters.configuration_printer import ConfigurationPrinter
|
||||||
|
from ahriman.core.formatters.event_stats_printer import EventStatsPrinter
|
||||||
from ahriman.core.formatters.package_printer import PackagePrinter
|
from ahriman.core.formatters.package_printer import PackagePrinter
|
||||||
|
from ahriman.core.formatters.package_stats_printer import PackageStatsPrinter
|
||||||
from ahriman.core.formatters.patch_printer import PatchPrinter
|
from ahriman.core.formatters.patch_printer import PatchPrinter
|
||||||
from ahriman.core.formatters.printer import Printer
|
from ahriman.core.formatters.printer import Printer
|
||||||
from ahriman.core.formatters.repository_printer import RepositoryPrinter
|
from ahriman.core.formatters.repository_printer import RepositoryPrinter
|
||||||
|
@ -33,8 +33,6 @@ class AurPrinter(StringPrinter):
|
|||||||
|
|
||||||
def __init__(self, package: AURPackage) -> None:
|
def __init__(self, package: AURPackage) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package(AURPackage): AUR package description
|
package(AURPackage): AUR package description
|
||||||
"""
|
"""
|
||||||
|
@ -28,11 +28,9 @@ class BuildPrinter(StringPrinter):
|
|||||||
|
|
||||||
def __init__(self, package: Package, is_success: bool, use_utf: bool) -> None:
|
def __init__(self, package: Package, is_success: bool, use_utf: bool) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package(Package): built package
|
package(Package): built package
|
||||||
is_success(bool): True in case if build has success status and False otherwise
|
is_success(bool): ``True`` in case if build has success status and ``False`` otherwise
|
||||||
use_utf(bool): use utf instead of normal symbols
|
use_utf(bool): use utf instead of normal symbols
|
||||||
"""
|
"""
|
||||||
StringPrinter.__init__(self, f"{self.sign(is_success, use_utf)} {package.base}")
|
StringPrinter.__init__(self, f"{self.sign(is_success, use_utf)} {package.base}")
|
||||||
@ -43,7 +41,7 @@ class BuildPrinter(StringPrinter):
|
|||||||
generate sign according to settings
|
generate sign according to settings
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
is_success(bool): True in case if build has success status and False otherwise
|
is_success(bool): ``True`` in case if build has success status and ``False`` otherwise
|
||||||
use_utf(bool): use utf instead of normal symbols
|
use_utf(bool): use utf instead of normal symbols
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -32,8 +32,6 @@ class ChangesPrinter(Printer):
|
|||||||
|
|
||||||
def __init__(self, changes: Changes) -> None:
|
def __init__(self, changes: Changes) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
changes(Changes): package changes
|
changes(Changes): package changes
|
||||||
"""
|
"""
|
||||||
@ -57,7 +55,7 @@ class ChangesPrinter(Printer):
|
|||||||
generate entry title from content
|
generate entry title from content
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: content title if it can be generated and None otherwise
|
str | None: content title if it can be generated and ``None`` otherwise
|
||||||
"""
|
"""
|
||||||
if self.changes.is_empty:
|
if self.changes.is_empty:
|
||||||
return None
|
return None
|
||||||
|
@ -33,8 +33,6 @@ class ConfigurationPathsPrinter(StringPrinter):
|
|||||||
|
|
||||||
def __init__(self, root: Path, includes: list[Path]) -> None:
|
def __init__(self, root: Path, includes: list[Path]) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
root(Path): path to root configuration file
|
root(Path): path to root configuration file
|
||||||
includes(list[Path]): list of include files
|
includes(list[Path]): list of include files
|
||||||
|
@ -42,8 +42,6 @@ class ConfigurationPrinter(StringPrinter):
|
|||||||
|
|
||||||
def __init__(self, section: str, values: dict[str, str]) -> None:
|
def __init__(self, section: str, values: dict[str, str]) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
section(str): section name
|
section(str): section name
|
||||||
values(dict[str, str]): configuration values dictionary
|
values(dict[str, str]): configuration values dictionary
|
||||||
|
72
src/ahriman/core/formatters/event_stats_printer.py
Normal file
72
src/ahriman/core/formatters/event_stats_printer.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import statistics
|
||||||
|
|
||||||
|
from ahriman.core.formatters.string_printer import StringPrinter
|
||||||
|
from ahriman.core.utils import minmax
|
||||||
|
from ahriman.models.property import Property
|
||||||
|
|
||||||
|
|
||||||
|
class EventStatsPrinter(StringPrinter):
|
||||||
|
"""
|
||||||
|
print event statistics
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
events(list[float | int]): event values to build statistics
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, event_type: str, events: list[float | int]) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
event_type(str): event type used for this statistics
|
||||||
|
events(list[float | int]): event values to build statistics
|
||||||
|
"""
|
||||||
|
StringPrinter.__init__(self, event_type)
|
||||||
|
self.events = events
|
||||||
|
|
||||||
|
def properties(self) -> list[Property]:
|
||||||
|
"""
|
||||||
|
convert content into printable data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Property]: list of content properties
|
||||||
|
"""
|
||||||
|
properties = [
|
||||||
|
Property("total", len(self.events)),
|
||||||
|
]
|
||||||
|
|
||||||
|
# time statistics
|
||||||
|
if self.events:
|
||||||
|
min_time, max_time = minmax(self.events)
|
||||||
|
mean = statistics.mean(self.events)
|
||||||
|
|
||||||
|
if len(self.events) > 1:
|
||||||
|
stdev = statistics.stdev(self.events)
|
||||||
|
average = f"{mean:.3f} ± {stdev:.3f}"
|
||||||
|
else:
|
||||||
|
average = f"{mean:.3f}"
|
||||||
|
|
||||||
|
properties.extend([
|
||||||
|
Property("min", min_time),
|
||||||
|
Property("average", average),
|
||||||
|
Property("max", max_time),
|
||||||
|
])
|
||||||
|
|
||||||
|
return properties
|
@ -34,8 +34,6 @@ class PackagePrinter(StringPrinter):
|
|||||||
|
|
||||||
def __init__(self, package: Package, status: BuildStatus) -> None:
|
def __init__(self, package: Package, status: BuildStatus) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package(Package): package description
|
package(Package): package description
|
||||||
status(BuildStatus): build status
|
status(BuildStatus): build status
|
||||||
|
56
src/ahriman/core/formatters/package_stats_printer.py
Normal file
56
src/ahriman/core/formatters/package_stats_printer.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from ahriman.core.formatters.string_printer import StringPrinter
|
||||||
|
from ahriman.models.property import Property
|
||||||
|
|
||||||
|
|
||||||
|
class PackageStatsPrinter(StringPrinter):
|
||||||
|
"""
|
||||||
|
print packages statistics
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
events(dict[str, int]): map of package to its event frequency
|
||||||
|
"""
|
||||||
|
|
||||||
|
MAX_COUNT = 10
|
||||||
|
|
||||||
|
def __init__(self, events: dict[str, int]) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
events(dict[str, int]): map of package to its event frequency
|
||||||
|
"""
|
||||||
|
StringPrinter.__init__(self, "The most frequent packages")
|
||||||
|
self.events = events
|
||||||
|
|
||||||
|
def properties(self) -> list[Property]:
|
||||||
|
"""
|
||||||
|
convert content into printable data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Property]: list of content properties
|
||||||
|
"""
|
||||||
|
if not self.events:
|
||||||
|
return [] # no events found, discard any stats
|
||||||
|
|
||||||
|
properties = []
|
||||||
|
for object_id, count in sorted(self.events.items(), key=lambda pair: pair[1], reverse=True)[:self.MAX_COUNT]:
|
||||||
|
properties.append(Property(object_id, count))
|
||||||
|
|
||||||
|
return properties
|
@ -32,8 +32,6 @@ class PatchPrinter(StringPrinter):
|
|||||||
|
|
||||||
def __init__(self, package_base: str, patches: list[PkgbuildPatch]) -> None:
|
def __init__(self, package_base: str, patches: list[PkgbuildPatch]) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package_base(str): package base
|
package_base(str): package base
|
||||||
patches(list[PkgbuildPatch]): PKGBUILD patch object
|
patches(list[PkgbuildPatch]): PKGBUILD patch object
|
||||||
|
@ -63,7 +63,7 @@ class Printer:
|
|||||||
generate entry title from content
|
generate entry title from content
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: content title if it can be generated and None otherwise
|
str | None: content title if it can be generated and ``None`` otherwise
|
||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -32,8 +32,6 @@ class RepositoryPrinter(StringPrinter):
|
|||||||
|
|
||||||
def __init__(self, repository_id: RepositoryId) -> None:
|
def __init__(self, repository_id: RepositoryId) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
repository_id(RepositoryId): repository unique identifier
|
repository_id(RepositoryId): repository unique identifier
|
||||||
"""
|
"""
|
||||||
|
@ -28,8 +28,6 @@ class StatusPrinter(StringPrinter):
|
|||||||
|
|
||||||
def __init__(self, status: BuildStatus) -> None:
|
def __init__(self, status: BuildStatus) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
status(BuildStatus): build status
|
status(BuildStatus): build status
|
||||||
"""
|
"""
|
||||||
|
@ -30,8 +30,6 @@ class StringPrinter(Printer):
|
|||||||
|
|
||||||
def __init__(self, content: str) -> None:
|
def __init__(self, content: str) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
content(str): any content string
|
content(str): any content string
|
||||||
"""
|
"""
|
||||||
@ -42,6 +40,6 @@ class StringPrinter(Printer):
|
|||||||
generate entry title from content
|
generate entry title from content
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: content title if it can be generated and None otherwise
|
str | None: content title if it can be generated and ``None`` otherwise
|
||||||
"""
|
"""
|
||||||
return self.content
|
return self.content
|
||||||
|
@ -32,8 +32,6 @@ class TreePrinter(StringPrinter):
|
|||||||
|
|
||||||
def __init__(self, level: int, packages: list[Package]) -> None:
|
def __init__(self, level: int, packages: list[Package]) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
level(int): dependencies tree level
|
level(int): dependencies tree level
|
||||||
packages(list[Package]): packages which belong to this level
|
packages(list[Package]): packages which belong to this level
|
||||||
|
@ -34,8 +34,6 @@ class UpdatePrinter(StringPrinter):
|
|||||||
|
|
||||||
def __init__(self, remote: Package, local_version: str | None) -> None:
|
def __init__(self, remote: Package, local_version: str | None) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
remote(Package): remote (new) package object
|
remote(Package): remote (new) package object
|
||||||
local_version(str | None): local version of the package if any
|
local_version(str | None): local version of the package if any
|
||||||
|
@ -32,8 +32,6 @@ class UserPrinter(StringPrinter):
|
|||||||
|
|
||||||
def __init__(self, user: User) -> None:
|
def __init__(self, user: User) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user(User): user to print
|
user(User): user to print
|
||||||
"""
|
"""
|
||||||
|
@ -35,8 +35,6 @@ class ValidationPrinter(StringPrinter):
|
|||||||
|
|
||||||
def __init__(self, node: str, errors: list[str | dict[str, Any]]) -> None:
|
def __init__(self, node: str, errors: list[str | dict[str, Any]]) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
node(str): root level name
|
node(str): root level name
|
||||||
errors(list[str | dict[str, Any]]): validation errors
|
errors(list[str | dict[str, Any]]): validation errors
|
||||||
|
@ -31,8 +31,6 @@ class VersionPrinter(StringPrinter):
|
|||||||
|
|
||||||
def __init__(self, title: str, packages: dict[str, str]) -> None:
|
def __init__(self, title: str, packages: dict[str, str]) -> None:
|
||||||
"""
|
"""
|
||||||
default constructor
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
title(str): title of the message
|
title(str): title of the message
|
||||||
packages(dict[str, str]): map of package name to its version
|
packages(dict[str, str]): map of package name to its version
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user