Compare commits

...

25 Commits

Author SHA1 Message Date
d283dccc1e type: update for new cors release 2025-03-17 13:56:59 +02:00
8a4e900ab9 docs: update docs
This commit includes following changes
* add newly added option to configuration referenec
* remove few legacy options from configuration schemas used for
  validation, which might lead to errors during validation.
  Note, however, that settings will be still read by the service
* add link to aurcache
* hide service-setup command description under spoiler
2025-03-17 13:43:04 +02:00
fa6cf8ce36 website: use date instead of version for listing logs
website: make dropdown from logs versions to add some space
2025-03-13 15:45:31 +02:00
a706fbb751 bug: handle dependencies iteratively (fix #141)
It has been found that if there are missing dependencies than whole
process will break instead of just skipping packages. During package
addition it is fine-ish, but it will break updates run
2025-03-13 15:45:27 +02:00
9a23f5c79d refactor: streamline migrations 2025-03-09 23:22:24 +02:00
aaab9069bf docs: rebuild indices 2025-03-09 15:43:41 +02:00
f00b575641 type: use ClassVar decorator for class attributes 2025-03-09 15:43:27 +02:00
6f57ed550b
feat: refine log system (#142)
* refine package logging

* add interface

* revert version selection

* replace tuple with model

* rename column in logs table, add coverters

* generate process identifier for child proocesses
2025-03-09 14:46:33 +02:00
08640d9108 feat: add dashboard (#139) 2025-02-24 00:10:15 +02:00
65324633b4 feat: add counters to repository stats overview 2025-02-24 00:10:15 +02:00
ed67898012 fix: parse non-utf pkgbuilds as well (#140)
it has been reported that duriing reading pkgbuilds with latin-1 charset
the execption will be raised. Well, it is one more point to rewrite
parser to use own impl instead of shlex and parse raw byte array instead
2025-02-24 00:10:15 +02:00
a1a8dd68e8 type: remove unused ignore directive 2025-02-24 00:10:15 +02:00
a9505386c2 fix: force dry run build on task initialization for VCS packages
Previously if package is VCS and version in PKGBUILD doesn't match to
AUR one, then makepkg will update pkgbuild ignoring all previous pkgrel
patches

With this change during task init dry ryn process is always run for vcs
packages
2025-02-24 00:10:15 +02:00
a07b20bf50 Release 2.17.1 2025-01-06 01:14:28 +02:00
ed70897c39 fix: suppress traceback in shell if no ipython installed
Old implementation was showing import error, new implementation instead
hides it behind separated call and if-else check
2025-01-06 01:07:13 +02:00
0423c3e67c Release 2.17.0 2024-12-29 18:07:23 +02:00
571f62327f build: remove unused line from dockerfile 2024-12-24 16:49:30 +02:00
286ff4bcef fix: update packages properties after rebuild
This case leads to issue when it is impossible to update list of
implicit dependencies correctly in case of multi-packages
2024-12-24 15:13:18 +02:00
0660c33de3 chore: copyright update 2024-12-23 16:03:26 +02:00
c8421e97ee fix: fix pkgbuild parsing in case if comment mark is followed by token
without whitespaces

In this case, the next line was ignored
2024-12-23 15:55:07 +02:00
bc2288afc1 fix: suppress codefactor warning 2024-12-23 01:52:23 +02:00
503c8b0355
feat: make apispec dependency optional (#138) 2024-12-22 20:33:31 +02:00
6738f9206d type: remove unused typeguard 2024-12-21 17:02:09 +02:00
f865e998b0 feat: add link to logo 2024-12-19 12:47:17 +02:00
4880ca4fee feat: use IPython shell if available 2024-12-18 15:41:36 +02:00
406 changed files with 5804 additions and 3533 deletions

View File

@ -18,7 +18,7 @@ if [[ -z $MINIMAL_INSTALL ]]; then
# web server # web server
pacman -S --noconfirm python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-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 -S --noconfirm gnupg python-boto3 python-cerberus python-matplotlib rsync pacman -S --noconfirm gnupg ipython 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"

View File

@ -305,7 +305,7 @@ max-branches=12
max-locals=15 max-locals=15
# Maximum number of parents for a class (see R0901). # Maximum number of parents for a class (see R0901).
max-parents=7 max-parents=15
# Maximum number of public methods for a class (see R0904). # Maximum number of public methods for a class (see R0904).
max-public-methods=20 max-public-methods=20

View File

@ -80,7 +80,7 @@ Again, the most checks can be performed by `tox` command, though some additional
>>> clazz = Clazz() >>> clazz = Clazz()
""" """
CLAZZ_ATTRIBUTE = 42 CLAZZ_ATTRIBUTE: ClassVar[int] = 42
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
""" """
@ -96,6 +96,7 @@ Again, the most checks can be performed by `tox` command, though some additional
* Type annotations are the must, even for local functions. For the function argument `self` (for instance methods) and `cls` (for class methods) should not be annotated. * Type annotations are the must, even for local functions. For the function argument `self` (for instance methods) and `cls` (for class methods) should not be annotated.
* For collection types built-in classes must be used if possible (e.g. `dict` instead of `typing.Dict`, `tuple` instead of `typing.Tuple`). In case if built-in type is not available, but `collections.abc` provides interface, it must be used (e.g. `collections.abc.Awaitable` instead of `typing.Awaitable`, `collections.abc.Iterable` instead of `typing.Iterable`). For union classes, the bar operator (`|`) must be used (e.g. `float | int` instead of `typing.Union[float, int]`), which also includes `typing.Optional` (e.g. `str | None` instead of `Optional[str]`). * 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.
* Class attributes must be decorated as `ClassVar[...]`.
* Recommended order of function definitions in class: * Recommended order of function definitions in class:
```python ```python
@ -175,11 +176,10 @@ Again, the most checks can be performed by `tox` command, though some additional
* Web API methods must be documented by using `aiohttp_apispec` library. The schema testing mostly should be implemented in related view class tests. Recommended example for documentation (excluding comments): * Web API methods must be documented by using `aiohttp_apispec` library. The schema testing mostly should be implemented in related view class tests. Recommended example for documentation (excluding comments):
```python ```python
import aiohttp_apispec
from marshmallow import Schema, fields from marshmallow import Schema, fields
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PaginationSchema from ahriman.web.apispec.decorators import apidocs
from ahriman.web.schemas import PackageNameSchema, PaginationSchema
from ahriman.web.views.base import BaseView from ahriman.web.views.base import BaseView
@ -198,25 +198,17 @@ Again, the most checks can be performed by `tox` command, though some additional
POST_PERMISSION = ... POST_PERMISSION = ...
ROUTES = ... ROUTES = ...
@aiohttp_apispec.docs( @apidocs(
tags=["Tag"], tags=["Tag"],
summary="Do foo", summary="Do foo",
description="Extended description of the method which does foo", description="Extended description of the method which does foo",
responses={ error_400_enabled=True, # exception raised by this method
200: {"description": "Success response", "schema": ResponseSchema}, error_404_description="Repository is unknown",
204: {"description": "Success response"}, # example without json schema response schema=ResponseSchema, # leave empty if no responses here
400: {"description": "Bad data is supplied", "schema": ErrorSchema}, # exception raised by this method match_schema=PackageNameSchema,
401: {"description": "Authorization required", "schema": ErrorSchema}, # should be always presented query_schema=PaginationSchema,
403: {"description": "Access is forbidden", "schema": ErrorSchema}, # should be always presented body_schema=RequestSchema(many=True),
404: {"description": "Repository is unknown", "schema": ErrorSchema}, # include if BaseView.service() method is called
500: {"description": "Internal server error", "schema": ErrorSchema}, # should be always presented
},
security=[{"token": [POST_PERMISSION]}],
) )
@aiohttp_apispec.cookies_schema(AuthSchema) # should be always presented
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.querystring_schema(PaginationSchema)
@aiohttp_apispec.json_schema(RequestSchema(many=True))
async def post(self) -> None: ... async def post(self) -> None: ...
``` ```

View File

@ -108,9 +108,7 @@ RUN cp "/etc/pacman.d/mirrorlist" "/etc/pacman.d/mirrorlist.orig" && \
sed -i "s/SigLevel *=.*/SigLevel = Optional/g" "/etc/pacman.conf" && \ sed -i "s/SigLevel *=.*/SigLevel = Optional/g" "/etc/pacman.conf" && \
pacman -Sy pacman -Sy
## install package and its optional dependencies ## install package and its optional dependencies
RUN pacman -S --noconfirm \ RUN pacman -S --noconfirm ahriman
--assume-installed python-aiohttp-apispec=3.0.0 \
ahriman
RUN pacman -S --noconfirm --asdeps \ RUN pacman -S --noconfirm --asdeps \
python-aioauth-client \ python-aioauth-client \
python-aiohttp-apispec-git \ python-aiohttp-apispec-git \

File diff suppressed because it is too large Load Diff

View File

@ -29,6 +29,14 @@ ahriman.application.help\_formatter module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.application.interactive\_shell module
---------------------------------------------
.. automodule:: ahriman.application.interactive_shell
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.lock module ahriman.application.lock module
------------------------------- -------------------------------

View File

@ -124,6 +124,14 @@ ahriman.core.database.migrations.m014\_auditlog module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.core.database.migrations.m015\_logs\_process\_id module
---------------------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m015_logs_process_id
:members:
:no-undoc-members:
:show-inheritance:
Module contents Module contents
--------------- ---------------

View File

@ -92,6 +92,14 @@ ahriman.core.formatters.repository\_printer module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.core.formatters.repository\_stats\_printer module
---------------------------------------------------------
.. automodule:: ahriman.core.formatters.repository_stats_printer
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.formatters.status\_printer module ahriman.core.formatters.status\_printer module
---------------------------------------------- ----------------------------------------------

View File

@ -100,6 +100,14 @@ ahriman.models.log\_handler module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.models.log\_record module
---------------------------------
.. automodule:: ahriman.models.log_record
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.log\_record\_id module ahriman.models.log\_record\_id module
------------------------------------- -------------------------------------
@ -236,6 +244,14 @@ ahriman.models.repository\_paths module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.models.repository\_stats module
---------------------------------------
.. automodule:: ahriman.models.repository_stats
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.result module ahriman.models.result module
---------------------------- ----------------------------
@ -252,6 +268,14 @@ ahriman.models.scan\_paths module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.models.series\_statistics module
----------------------------------------
.. automodule:: ahriman.models.series_statistics
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.sign\_settings module ahriman.models.sign\_settings module
------------------------------------ ------------------------------------

View File

@ -0,0 +1,29 @@
ahriman.web.apispec package
===========================
Submodules
----------
ahriman.web.apispec.decorators module
-------------------------------------
.. automodule:: ahriman.web.apispec.decorators
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.apispec.info module
-------------------------------
.. automodule:: ahriman.web.apispec.info
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: ahriman.web.apispec
:members:
:no-undoc-members:
:show-inheritance:

View File

@ -7,6 +7,7 @@ Subpackages
.. toctree:: .. toctree::
:maxdepth: 4 :maxdepth: 4
ahriman.web.apispec
ahriman.web.middlewares ahriman.web.middlewares
ahriman.web.schemas ahriman.web.schemas
ahriman.web.views ahriman.web.views
@ -14,14 +15,6 @@ Subpackages
Submodules Submodules
---------- ----------
ahriman.web.apispec module
--------------------------
.. automodule:: ahriman.web.apispec
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.cors module ahriman.web.cors module
----------------------- -----------------------

View File

@ -116,6 +116,14 @@ ahriman.web.schemas.login\_schema module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.web.schemas.logs\_rotate\_schema module
-----------------------------------------------
.. automodule:: ahriman.web.schemas.logs_rotate_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.logs\_schema module ahriman.web.schemas.logs\_schema module
--------------------------------------- ---------------------------------------
@ -260,6 +268,14 @@ ahriman.web.schemas.repository\_id\_schema module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.web.schemas.repository\_stats\_schema module
----------------------------------------------------
.. automodule:: ahriman.web.schemas.repository_stats_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.search\_schema module ahriman.web.schemas.search\_schema module
----------------------------------------- -----------------------------------------
@ -284,14 +300,6 @@ ahriman.web.schemas.update\_flags\_schema module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.web.schemas.versioned\_log\_schema module
-------------------------------------------------
.. automodule:: ahriman.web.schemas.versioned_log_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.worker\_schema module ahriman.web.schemas.worker\_schema module
----------------------------------------- -----------------------------------------

View File

@ -12,6 +12,14 @@ ahriman.web.views.v1.service.add module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.web.views.v1.service.logs module
----------------------------------------
.. automodule:: ahriman.web.views.v1.service.logs
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.service.pgp module ahriman.web.views.v1.service.pgp module
--------------------------------------- ---------------------------------------

View File

@ -81,6 +81,7 @@ Base configuration settings.
* ``apply_migrations`` - perform database migrations on the application start, boolean, optional, default ``yes``. Useful if you are using git version. Note, however, that this option must be changed only if you know what to do and going to handle migrations manually. * ``apply_migrations`` - perform database migrations on the application start, boolean, optional, default ``yes``. Useful if you are using git version. Note, however, that this option must be changed only if you know what to do and going to handle migrations manually.
* ``database`` - path to the application SQLite database, string, required. * ``database`` - path to the application SQLite database, string, required.
* ``include`` - path to directory with configuration files overrides, string, optional. Files will be read in alphabetical order. * ``include`` - path to directory with configuration files overrides, string, optional. Files will be read in alphabetical order.
* ``keep_last_logs`` - amount of build logs to be kept for each package, integer, optional ,default ``0``. Logs will be cleared at the end of each process.
* ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference. * ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference.
``alpm:*`` groups ``alpm:*`` groups
@ -217,7 +218,7 @@ Mirrorlist generator plugin
``remote-pull`` group ``remote-pull`` group
--------------------- ---------------------
Remote git source synchronization settings. Unlike ``Upload`` triggers those triggers are used for PKGBUILD synchronization - fetch from remote repository PKGBUILDs before updating process. Remote git source synchronization settings. Unlike ``upload`` triggers those triggers are used for PKGBUILD synchronization - fetch from remote repository PKGBUILDs before updating process.
It supports authorization; to do so you'd need to prefix the URL with authorization part, e.g. ``https://key:token@github.com/arcan1s/ahriman.git``. It is highly recommended to use application tokens instead of your user authorization details. Alternatively, you can use any other option supported by git, e.g.: It supports authorization; to do so you'd need to prefix the URL with authorization part, e.g. ``https://key:token@github.com/arcan1s/ahriman.git``. It is highly recommended to use application tokens instead of your user authorization details. Alternatively, you can use any other option supported by git, e.g.:

View File

@ -56,6 +56,13 @@ Though originally I've created ahriman by trying to improve the project, it stil
It is automation tools for ``repoctl`` mentioned above. Except for using shell it looks pretty cool and also offers some additional features like patches, remote synchronization (isn't it?) and reporting. It is automation tools for ``repoctl`` mentioned above. Except for using shell it looks pretty cool and also offers some additional features like patches, remote synchronization (isn't it?) and reporting.
`AURCache <https://github.com/Lukas-Heiligenbrunner/AURCache>`__
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
That's really cool project if you are looking for simple service to build AUR packages. It provides very informative dashboard and easy to configure and use. However, it doesn't provide direct way to control build process (e.g. it is neither trivial to build packages for architectures which are not supported by default nor to change build flags).
Also this application relies on docker setup (e.g. builders are only available as special docker containers). In addition, it uses ``paru`` to build packages instead of ``devtools``.
How to check service logs How to check service logs
^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -12,17 +12,20 @@ Initial setup
sudo ahriman -a x86_64 -r aur service-setup ... sudo ahriman -a x86_64 -r aur service-setup ...
``service-setup`` literally does the following steps: .. admonition:: Details
:collapsible: closed
#. ``service-setup`` literally does the following steps:
Create ``/var/lib/ahriman/.makepkg.conf`` with ``makepkg.conf`` overrides if required (at least you might want to set ``PACKAGER``):
.. code-block:: shell #.
Create ``/var/lib/ahriman/.makepkg.conf`` with ``makepkg.conf`` overrides if required (at least you might want to set ``PACKAGER``):
echo 'PACKAGER="ahriman bot <ahriman@example.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf .. code-block:: shell
#. echo 'PACKAGER="ahriman bot <ahriman@example.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf
Configure build tools (it is required for correct dependency management system):
#.
Configure build tools (it is required for correct dependency management system):
#. #.
Create build command (you can choose any name for command, basically it should be ``{name}-{arch}-build``): Create build command (you can choose any name for command, basically it should be ``{name}-{arch}-build``):
@ -67,7 +70,7 @@ Initial setup
echo 'ahriman ALL=(ALL) NOPASSWD:SETENV: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman echo 'ahriman ALL=(ALL) NOPASSWD:SETENV: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman
chmod 400 /etc/sudoers.d/ahriman chmod 400 /etc/sudoers.d/ahriman
This command supports several arguments, kindly refer to its help message. This command supports several arguments, kindly refer to its help message.
#. #.
Start and enable ``ahriman@.timer`` via ``systemctl``: Start and enable ``ahriman@.timer`` via ``systemctl``:

View File

@ -2,7 +2,7 @@
pkgbase='ahriman' pkgbase='ahriman'
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web') pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
pkgver=2.16.0 pkgver=2.17.1
pkgrel=1 pkgrel=1
pkgdesc="ArcH linux ReposItory MANager" pkgdesc="ArcH linux ReposItory MANager"
arch=('any') arch=('any')
@ -30,6 +30,7 @@ package_ahriman-core() {
pkgname='ahriman-core' pkgname='ahriman-core'
optdepends=('ahriman-triggers: additional extensions for the application' optdepends=('ahriman-triggers: additional extensions for the application'
'ahriman-web: web server' 'ahriman-web: web server'
'ipython: an enhanced shell interpreter'
'python-boto3: sync to s3' 'python-boto3: sync to s3'
'python-cerberus: configuration validator' 'python-cerberus: configuration validator'
'python-matplotlib: usage statistics chart' 'python-matplotlib: usage statistics chart'
@ -71,8 +72,9 @@ package_ahriman-triggers() {
package_ahriman-web() { package_ahriman-web() {
pkgname='ahriman-web' pkgname='ahriman-web'
pkgdesc="ArcH linux ReposItory MANager, web server" pkgdesc="ArcH linux ReposItory MANager, web server"
depends=("$pkgbase-core=$pkgver" 'python-aiohttp-apispec>=3.0.0' 'python-aiohttp-cors' 'python-aiohttp-jinja2') depends=("$pkgbase-core=$pkgver" 'python-aiohttp-cors' 'python-aiohttp-jinja2')
optdepends=('python-aioauth-client: OAuth2 authorization support' optdepends=('python-aioauth-client: OAuth2 authorization support'
'python-aiohttp-apispec>=3.0.0: autogenerated API documentation'
'python-aiohttp-security: authorization support' 'python-aiohttp-security: authorization support'
'python-aiohttp-session: authorization support' 'python-aiohttp-session: authorization support'
'python-cryptography: authorization support') 'python-cryptography: authorization support')

View File

@ -7,6 +7,8 @@ logging = ahriman.ini.d/logging.ini
;apply_migrations = yes ;apply_migrations = yes
; Path to the application SQLite database. ; Path to the application SQLite database.
database = ${repository:root}/ahriman.db database = ${repository:root}/ahriman.db
; Keep last build logs for each package
keep_last_logs = 5
[alpm] [alpm]
; Path to pacman system database cache. ; Path to pacman system database cache.

View File

@ -15,12 +15,12 @@
<div class="container"> <div class="container">
<nav class="navbar navbar-expand-lg"> <nav class="navbar navbar-expand-lg">
<div class="navbar-brand"><img src="/static/logo.svg" width="30" height="30" alt=""></div> <div class="navbar-brand"><a href="https://github.com/arcan1s/ahriman" title="logo"><img src="/static/logo.svg" width="30" height="30" alt=""></a></div>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#repositories-navbar-supported-content" aria-controls="repositories-navbar-supported-content" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#repositories-navbar" aria-controls="repositories-navbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div id="repositories-navbar-supported-content" class="collapse navbar-collapse"> <div id="repositories-navbar" class="collapse navbar-collapse">
<ul id="repositories" class="nav nav-tabs"> <ul id="repositories" class="nav nav-tabs">
{% for repository in repositories %} {% for repository in repositories %}
<li class="nav-item"> <li class="nav-item">
@ -36,7 +36,9 @@
<div class="container"> <div class="container">
<div id="toolbar" class="dropdown"> <div id="toolbar" class="dropdown">
<a id="badge-status" tabindex="0" role="button" class="btn btn-outline-secondary" data-bs-toggle="popover" data-bs-trigger="focus" data-bs-content="no run data"><i class="bi bi-info-circle"></i></a> <button id="dashboard-button" type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#dashboard-modal">
<i class="bi bi-info-circle"></i>
</button>
{% if not auth.enabled or auth.username is not none %} {% if not auth.enabled or auth.username is not none %}
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
@ -119,7 +121,9 @@
<li><a id="badge-version" class="nav-link" href="https://github.com/arcan1s/ahriman" title="sources"><i class="bi bi-github"></i> ahriman</a></li> <li><a id="badge-version" class="nav-link" href="https://github.com/arcan1s/ahriman" title="sources"><i class="bi bi-github"></i> ahriman</a></li>
<li><a class="nav-link" href="https://github.com/arcan1s/ahriman/releases" title="releases list">releases</a></li> <li><a class="nav-link" href="https://github.com/arcan1s/ahriman/releases" title="releases list">releases</a></li>
<li><a class="nav-link" href="https://github.com/arcan1s/ahriman/issues" title="issues tracker">report a bug</a></li> <li><a class="nav-link" href="https://github.com/arcan1s/ahriman/issues" title="issues tracker">report a bug</a></li>
<li><a class="nav-link" href="/api-docs" title="API documentation">api</a></li> {% if docs_enabled %}
<li><a class="nav-link" href="/api-docs" title="API documentation">api</a></li>
{% endif %}
</ul> </ul>
{% if index_url is not none %} {% if index_url is not none %}
@ -150,6 +154,7 @@
{% include "build-status/alerts.jinja2" %} {% include "build-status/alerts.jinja2" %}
{% include "build-status/dashboard.jinja2" %}
{% include "build-status/package-add-modal.jinja2" %} {% include "build-status/package-add-modal.jinja2" %}
{% include "build-status/package-rebuild-modal.jinja2" %} {% include "build-status/package-rebuild-modal.jinja2" %}
{% include "build-status/key-import-modal.jinja2" %} {% include "build-status/key-import-modal.jinja2" %}

View File

@ -0,0 +1,76 @@
<div id="dashboard-modal" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div id="dashboard-modal-header" class="modal-header">
<h4 class="modal-title">System health</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="form-group row mt-2">
<div class="col-4 col-lg-2" style="text-align: right">Repository name</div>
<div id="dashboard-name" class="col-8 col-lg-3"></div>
<div class="col-4 col-lg-2" style="text-align: right">Repository architecture</div>
<div id="dashboard-architecture" class="col-8 col-lg-3"></div>
</div>
<div class="form-group row mt-2">
<div class="col-4 col-lg-2" style="text-align: right">Current status</div>
<div id="dashboard-status" class="col-8 col-lg-3"></div>
<div class="col-4 col-lg-2" style="text-align: right">Updated at</div>
<div id="dashboard-status-timestamp" class="col-8 col-lg-3"></div>
</div>
<div id="dashboard-canvas" class="form-group row mt-2">
<div class="col-8 col-lg-6">
<canvas id="dashboard-packages-count-chart"></canvas>
</div>
<div class="col-8 col-lg-6">
<canvas id="dashboard-packages-statuses-chart"></canvas>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i><span class="d-none d-sm-inline"> close</span></button>
</div>
</div>
</div>
</div>
<script>
const dashboardModal = document.getElementById("dashboard-modal");
const dashboardModalHeader = document.getElementById("dashboard-modal-header");
const dashboardName = document.getElementById("dashboard-name");
const dashboardArchitecture = document.getElementById("dashboard-architecture");
const dashboardStatus = document.getElementById("dashboard-status");
const dashboardStatusTimestamp = document.getElementById("dashboard-status-timestamp");
const dashboardCanvas = document.getElementById("dashboard-canvas");
const dashboardPackagesStatusesChartCanvas = document.getElementById("dashboard-packages-statuses-chart");
let dashboardPackagesStatusesChart = null;
const dashboardPackagesCountChartCanvas = document.getElementById("dashboard-packages-count-chart");
let dashboardPackagesCountChart = null;
ready(_ => {
dashboardPackagesStatusesChart = new Chart(dashboardPackagesStatusesChartCanvas, {
type: "pie",
data: {},
options: {
responsive: true,
},
});
dashboardPackagesCountChart = new Chart(dashboardPackagesCountChartCanvas, {
type: "bar",
data: {},
options: {
maintainAspectRatio: false,
responsive: true,
scales: {
x: {
stacked: true,
},
},
},
});
});
</script>

View File

@ -59,7 +59,17 @@
</nav> </nav>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div id="package-info-logs" class="tab-pane fade show active" role="tabpanel" aria-labelledby="package-info-logs-button" tabindex="0"> <div id="package-info-logs" class="tab-pane fade show active" role="tabpanel" aria-labelledby="package-info-logs-button" tabindex="0">
<pre class="language-console"><code id="package-info-logs-input" class="pre-scrollable language-console"></code><button id="package-info-logs-copy-button" type="button" class="btn language-console" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre> <div class="row">
<div class="col-1 dropend">
<button id="package-info-logs-dropdown" class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-list"></i>
</button>
<nav id="package-info-logs-versions" class="dropdown-menu" aria-labelledby="package-info-logs-dropdown"></nav>
</div>
<div class="col-11">
<pre class="language-console"><code id="package-info-logs-input" class="pre-scrollable language-console"></code><button id="package-info-logs-copy-button" type="button" class="btn language-console" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre>
</div>
</div>
</div> </div>
<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>
@ -100,6 +110,7 @@
const packageInfoModalHeader = document.getElementById("package-info-modal-header"); const packageInfoModalHeader = document.getElementById("package-info-modal-header");
const packageInfo = document.getElementById("package-info"); const packageInfo = document.getElementById("package-info");
const packageInfoLogsVersions = document.getElementById("package-info-logs-versions");
const packageInfoLogsInput = document.getElementById("package-info-logs-input"); const packageInfoLogsInput = document.getElementById("package-info-logs-input");
const packageInfoLogsCopyButton = document.getElementById("package-info-logs-copy-button"); const packageInfoLogsCopyButton = document.getElementById("package-info-logs-copy-button");
@ -285,25 +296,51 @@
convert: response => response.json(), convert: response => response.json(),
}, },
data => { data => {
const logs = data.map(log_record => { const selectors = Object
return `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`; .values(
}); data.reduce((acc, log_record) => {
packageInfoLogsInput.textContent = logs.join("\n"); const id = `${log_record.version}-${log_record.process_id}`;
highlight(packageInfoLogsInput); if (acc[id])
acc[id].created = Math.min(log_record.created, acc[id].created);
else
acc[id] = log_record;
return acc;
}, {})
)
.sort(({created: left}, {created: right}) =>
right - left
)
.map(version => {
const link = document.createElement("a");
link.classList.add("dropdown-item");
link.textContent = new Date(1000 * version.created).toISOStringShort();
link.href = "#";
link.onclick = _ => {
const logs = data
.filter(log_record => log_record.version === version.version && log_record.process_id === version.process_id)
.map(log_record => `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`);
packageInfoLogsInput.textContent = logs.join("\n");
highlight(packageInfoLogsInput);
Array.from(packageInfoLogsVersions.children).forEach(el => el.classList.remove("active"));
link.classList.add("active");
return false;
};
return link;
});
packageInfoLogsVersions.replaceChildren(...selectors);
selectors.find(Boolean)?.click();
}, },
onFailure, onFailure,
); );
} }
function loadPackage(packageBase, onFailure) { function loadPackage(packageBase, onFailure) {
const headerClass = status => {
if (status === "pending") return ["bg-warning"];
if (status === "building") return ["bg-warning"];
if (status === "failed") return ["bg-danger", "text-white"];
if (status === "success") return ["bg-success", "text-white"];
return ["bg-secondary", "text-white"];
};
makeRequest( makeRequest(
`/api/v1/packages/${packageBase}`, `/api/v1/packages/${packageBase}`,
{ {

View File

@ -7,7 +7,7 @@
// so far bootstrap-table only operates with jquery elements // so far bootstrap-table only operates with jquery elements
const table = $(document.getElementById("packages")); const table = $(document.getElementById("packages"));
const statusBadge = document.getElementById("badge-status"); const dashboardButton = document.getElementById("dashboard-button");
const versionBadge = document.getElementById("badge-version"); const versionBadge = document.getElementById("badge-version");
function doPackageAction(uri, packages, repository, successText, failureText, data) { function doPackageAction(uri, packages, repository, successText, failureText, data) {
@ -141,14 +141,62 @@
data => { data => {
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`; versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`;
statusBadge.classList.remove(...statusBadge.classList); dashboardButton.classList.remove(...dashboardButton.classList);
statusBadge.classList.add("btn"); dashboardButton.classList.add("btn");
statusBadge.classList.add(badgeClass(data.status.status)); dashboardButton.classList.add(badgeClass(data.status.status));
const popover = bootstrap.Popover.getOrCreateInstance(statusBadge); dashboardModalHeader.classList.remove(...dashboardModalHeader.classList);
popover.dispose(); dashboardModalHeader.classList.add("modal-header");
statusBadge.dataset.bsContent = `${data.status.status} at ${new Date(1000 * data.status.timestamp).toISOStringShort()}`; headerClass(data.status.status).forEach(clz => dashboardModalHeader.classList.add(clz));
bootstrap.Popover.getOrCreateInstance(statusBadge);
dashboardName.textContent = data.repository;
dashboardArchitecture.textContent = data.architecture;
dashboardStatus.textContent = data.status.status;
dashboardStatusTimestamp.textContent = new Date(1000 * data.status.timestamp).toISOStringShort();
if (dashboardPackagesStatusesChart) {
const labels = [
"unknown",
"pending",
"building",
"failed",
"success",
];
dashboardPackagesStatusesChart.config.data = {
labels: labels,
datasets: [{
label: "packages in status",
data: labels.map(label => data.packages[label]),
backgroundColor: [
"rgb(55, 58, 60)",
"rgb(255, 117, 24)",
"rgb(255, 117, 24)",
"rgb(255, 0, 57)",
"rgb(63, 182, 24)", // copy-paste from current style
],
}],
};
dashboardPackagesStatusesChart.update();
}
if (dashboardPackagesCountChart) {
dashboardPackagesCountChart.config.data = {
labels: ["packages"],
datasets: [
{
label: "archives",
data: [data.stats.packages],
},
{
label: "bases",
data: [data.stats.bases],
},
],
};
dashboardPackagesCountChart.update();
}
dashboardCanvas.hidden = data.status.total > 0;
}, },
); );
} }
@ -227,7 +275,6 @@
}); });
}); });
bootstrap.Popover.getOrCreateInstance(statusBadge);
selectRepository(); selectRepository();
}); });
</script> </script>

View File

@ -58,6 +58,14 @@
return value.includes(dataList[index].toLowerCase()); return value.includes(dataList[index].toLowerCase());
} }
function headerClass(status) {
if (status === "pending") return ["bg-warning"];
if (status === "building") return ["bg-warning"];
if (status === "failed") return ["bg-danger", "text-white"];
if (status === "success") return ["bg-success", "text-white"];
return ["bg-secondary", "text-white"];
}
function listToTable(data) { function listToTable(data) {
return Array.from(new Set(data)) return Array.from(new Set(data))
.sort() .sort()

View File

@ -64,10 +64,10 @@ _shtab_ahriman_service_key_import_option_strings=('-h' '--help' '--key-server')
_shtab_ahriman_service_repositories_option_strings=('-h' '--help' '--id-only' '--no-id-only') _shtab_ahriman_service_repositories_option_strings=('-h' '--help' '--id-only' '--no-id-only')
_shtab_ahriman_service_run_option_strings=('-h' '--help') _shtab_ahriman_service_run_option_strings=('-h' '--help')
_shtab_ahriman_service_setup_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket') _shtab_ahriman_service_setup_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_service_shell_option_strings=('-h' '--help') _shtab_ahriman_service_shell_option_strings=('-h' '--help' '-o' '--output')
_shtab_ahriman_service_tree_migrate_option_strings=('-h' '--help') _shtab_ahriman_service_tree_migrate_option_strings=('-h' '--help')
_shtab_ahriman_setup_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket') _shtab_ahriman_setup_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_shell_option_strings=('-h' '--help') _shtab_ahriman_shell_option_strings=('-h' '--help' '-o' '--output')
_shtab_ahriman_sign_option_strings=('-h' '--help') _shtab_ahriman_sign_option_strings=('-h' '--help')
_shtab_ahriman_status_option_strings=('-h' '--help' '--ahriman' '-e' '--exit-code' '--info' '--no-info' '-s' '--status') _shtab_ahriman_status_option_strings=('-h' '--help' '--ahriman' '-e' '--exit-code' '--info' '--no-info' '-s' '--status')
_shtab_ahriman_status_update_option_strings=('-h' '--help' '-s' '--status') _shtab_ahriman_status_update_option_strings=('-h' '--help' '-s' '--status')

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2024\-12\-01" "ahriman" "Generated Python Manual" .TH AHRIMAN "1" "2025\-01\-05" "ahriman" "Generated Python Manual"
.SH NAME .SH NAME
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS
@ -940,7 +940,7 @@ port of the web service
path to unix socket used for interprocess communications path to unix socket used for interprocess communications
.SH COMMAND \fI\,'ahriman service\-shell'\/\fR .SH COMMAND \fI\,'ahriman service\-shell'\/\fR
usage: ahriman service\-shell [\-h] [code] usage: ahriman service\-shell [\-h] [\-o OUTPUT] [code]
drop into python shell drop into python shell
@ -948,6 +948,11 @@ drop into python shell
\fBcode\fR \fBcode\fR
instead of dropping into shell, just execute the specified code instead of dropping into shell, just execute the specified code
.SH OPTIONS \fI\,'ahriman service\-shell'\/\fR
.TP
\fB\-o\fR \fI\,OUTPUT\/\fR, \fB\-\-output\fR \fI\,OUTPUT\/\fR
output commands and result to the file
.SH COMMAND \fI\,'ahriman service\-tree\-migrate'\/\fR .SH COMMAND \fI\,'ahriman service\-tree\-migrate'\/\fR
usage: ahriman service\-tree\-migrate [\-h] usage: ahriman service\-tree\-migrate [\-h]

View File

@ -583,6 +583,7 @@ _shtab_ahriman_service_setup_options=(
_shtab_ahriman_service_shell_options=( _shtab_ahriman_service_shell_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
{-o,--output}"[output commands and result to the file (default\: None)]:output:"
":instead of dropping into shell, just execute the specified code (default\: None):" ":instead of dropping into shell, just execute the specified code (default\: None):"
) )
@ -608,6 +609,7 @@ _shtab_ahriman_setup_options=(
_shtab_ahriman_shell_options=( _shtab_ahriman_shell_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
{-o,--output}"[output commands and result to the file (default\: None)]:output:"
":instead of dropping into shell, just execute the specified code (default\: None):" ":instead of dropping into shell, just execute the specified code (default\: None):"
) )

View File

@ -60,6 +60,9 @@ pacman = [
s3 = [ s3 = [
"boto3", "boto3",
] ]
shell = [
"IPython"
]
stats = [ stats = [
"matplotlib", "matplotlib",
] ]

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
__version__ = "2.16.0" __version__ = "2.17.1"

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -117,7 +117,7 @@ class Application(ApplicationPackages, ApplicationRepository):
Args: Args:
packages(list[Package]): list of source packages of which dependencies have to be processed packages(list[Package]): list of source packages of which dependencies have to be processed
process_dependencies(bool): if no set, dependencies will not be processed process_dependencies(bool): if set to ``False``, dependencies will not be processed
Returns: Returns:
list[Package]: updated packages list. Packager for dependencies will be copied from the original package list[Package]: updated packages list. Packager for dependencies will be copied from the original package
@ -130,6 +130,9 @@ class Application(ApplicationPackages, ApplicationRepository):
>>> packages = application.with_dependencies(packages, process_dependencies=True) >>> packages = application.with_dependencies(packages, process_dependencies=True)
>>> application.print_updates(packages, log_fn=print) >>> application.print_updates(packages, log_fn=print)
""" """
if not process_dependencies or not packages:
return packages
def missing_dependencies(source: Iterable[Package]) -> dict[str, str | None]: def missing_dependencies(source: Iterable[Package]) -> dict[str, str | None]:
# append list of known packages with packages which are in current sources # append list of known packages with packages which are in current sources
satisfied_packages = known_packages | { satisfied_packages = known_packages | {
@ -145,22 +148,29 @@ class Application(ApplicationPackages, ApplicationRepository):
if dependency not in satisfied_packages if dependency not in satisfied_packages
} }
if not process_dependencies or not packages: def new_packages(root: Package) -> dict[str, Package]:
return packages portion = {root.base: root}
while missing := missing_dependencies(portion.values()):
for package_name, packager in missing.items():
if (source_dir := self.repository.paths.cache_for(package_name)).is_dir():
# there is local cache, load package from it
leaf = Package.from_build(source_dir, self.repository.architecture, packager)
else:
leaf = Package.from_aur(package_name, packager)
portion[leaf.base] = leaf
# register package in the database
self.repository.reporter.set_unknown(leaf)
return portion
known_packages = self._known_packages() known_packages = self._known_packages()
with_dependencies = {package.base: package for package in packages} with_dependencies: dict[str, Package] = {}
for package in packages:
while missing := missing_dependencies(with_dependencies.values()): with self.in_package_context(package.base, package.version): # use the same context for the logger
for package_name, username in missing.items(): try:
if (source_dir := self.repository.paths.cache_for(package_name)).is_dir(): with_dependencies |= new_packages(package)
# there is local cache, load package from it except Exception:
package = Package.from_build(source_dir, self.repository.architecture, username) self.logger.exception("could not process dependencies of %s, skip the package", package.base)
else:
package = Package.from_aur(package_name, username)
with_dependencies[package.base] = package
# register package in the database
self.repository.reporter.set_unknown(package)
return list(with_dependencies.values()) return list(with_dependencies.values())

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -22,7 +22,7 @@ import logging
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
from multiprocessing import Pool from multiprocessing import Pool
from typing import TypeVar from typing import ClassVar, TypeVar
from ahriman.application.lock import Lock from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -53,13 +53,13 @@ class Handler:
Wrapper for all command line actions, though each derived class implements :func:`run()` method, it usually Wrapper for all command line actions, though each derived class implements :func:`run()` method, it usually
must not be called directly. The recommended way is to call :func:`execute()` class method, e.g.:: must not be called directly. The recommended way is to call :func:`execute()` class method, e.g.::
>>> from ahriman.application.handlers import Add >>> from ahriman.application.handlers.add import Add
>>> >>>
>>> Add.execute(args) >>> Add.execute(args)
""" """
ALLOW_MULTI_ARCHITECTURE_RUN = True ALLOW_MULTI_ARCHITECTURE_RUN: ClassVar[bool] = True
arguments: list[Callable[[SubParserAction], argparse.ArgumentParser]] arguments: ClassVar[list[Callable[[SubParserAction], argparse.ArgumentParser]]]
@classmethod @classmethod
def call(cls, args: argparse.Namespace, repository_id: RepositoryId) -> bool: def call(cls, args: argparse.Namespace, repository_id: RepositoryId) -> bool:

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -21,6 +21,7 @@ import argparse
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
from dataclasses import fields from dataclasses import fields
from typing import ClassVar
from ahriman.application.handlers.handler import Handler, SubParserAction from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.alpm.remote import AUR, Official from ahriman.core.alpm.remote import AUR, Official
@ -40,7 +41,7 @@ class Search(Handler):
""" """
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
SORT_FIELDS = { SORT_FIELDS: ClassVar[set[str]] = {
field.name field.name
for field in fields(AURPackage) for field in fields(AURPackage)
if field.default_factory is not list if field.default_factory is not list

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -21,6 +21,7 @@ import argparse
from pathlib import Path from pathlib import Path
from pwd import getpwuid from pwd import getpwuid
from typing import ClassVar
from urllib.parse import quote_plus as url_encode from urllib.parse import quote_plus as url_encode
from ahriman.application.application import Application from ahriman.application.application import Application
@ -46,9 +47,9 @@ class Setup(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
ARCHBUILD_COMMAND_PATH = Path("/") / "usr" / "bin" / "archbuild" ARCHBUILD_COMMAND_PATH: ClassVar[Path] = Path("/") / "usr" / "bin" / "archbuild"
MIRRORLIST_PATH = Path("/") / "etc" / "pacman.d" / "mirrorlist" MIRRORLIST_PATH: ClassVar[Path] = Path("/") / "etc" / "pacman.d" / "mirrorlist"
SUDOERS_DIR_PATH = Path("/") / "etc" / "sudoers.d" SUDOERS_DIR_PATH: ClassVar[Path] = Path("/") / "etc" / "sudoers.d"
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -18,12 +18,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import argparse import argparse
import code
import sys import sys
from pathlib import Path from pathlib import Path
from ahriman.application.handlers.handler import Handler, SubParserAction from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.application.interactive_shell import InteractiveShell
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.formatters import StringPrinter from ahriman.core.formatters import StringPrinter
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
@ -58,11 +58,13 @@ class Shell(Handler):
"configuration": configuration, "configuration": configuration,
"repository_id": repository_id, "repository_id": repository_id,
} }
console = InteractiveShell(locals=local_variables)
if args.code is None: match args.code:
code.interact(local=local_variables) case None:
else: console.interact()
code.InteractiveConsole(locals=local_variables).runcode(args.code) case snippet:
console.runcode(snippet)
@staticmethod @staticmethod
def _set_service_shell_parser(root: SubParserAction) -> argparse.ArgumentParser: def _set_service_shell_parser(root: SubParserAction) -> argparse.ArgumentParser:
@ -79,6 +81,7 @@ class Shell(Handler):
description="drop into python shell") description="drop into python shell")
parser.add_argument("code", help="instead of dropping into shell, just execute the specified code", nargs="?") parser.add_argument("code", help="instead of dropping into shell, just execute the specified code", nargs="?")
parser.add_argument("-v", "--verbose", help=argparse.SUPPRESS, action="store_true") parser.add_argument("-v", "--verbose", help=argparse.SUPPRESS, action="store_true")
parser.add_argument("-o", "--output", help="output commands and result to the file", type=Path)
parser.set_defaults(lock=None, report=False) parser.set_defaults(lock=None, report=False)
return parser return parser

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -27,7 +27,7 @@ from pathlib import Path
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.formatters import EventStatsPrinter, PackageStatsPrinter from ahriman.core.formatters import EventStatsPrinter, PackageStatsPrinter, RepositoryStatsPrinter
from ahriman.core.utils import enum_values, pretty_datetime from ahriman.core.utils import enum_values, pretty_datetime
from ahriman.models.event import Event, EventType from ahriman.models.event import Event, EventType
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
@ -64,6 +64,7 @@ class Statistics(Handler):
match args.package: match args.package:
case None: case None:
RepositoryStatsPrinter(repository_id, application.reporter.statistics())(verbose=True)
Statistics.stats_per_package(args.event, events, args.chart) Statistics.stats_per_package(args.event, events, args.chart)
case _: case _:
Statistics.stats_for_package(args.event, events, args.chart) Statistics.stats_for_package(args.event, events, args.chart)

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -23,6 +23,7 @@ import sys
from collections.abc import Generator from collections.abc import Generator
from importlib import metadata from importlib import metadata
from typing import ClassVar
from ahriman import __version__ from ahriman import __version__
from ahriman.application.handlers.handler import Handler, SubParserAction from ahriman.application.handlers.handler import Handler, SubParserAction
@ -36,11 +37,11 @@ class Versions(Handler):
version handler version handler
Attributes: Attributes:
PEP423_PACKAGE_NAME(str): (class attribute) special regex for valid PEP423 package name PEP423_PACKAGE_NAME(re.Pattern[str]): (class attribute) special regex for valid PEP423 package name
""" """
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
PEP423_PACKAGE_NAME = re.compile(r"^[A-Za-z0-9._-]+") PEP423_PACKAGE_NAME: ClassVar[re.Pattern[str]] = re.compile(r"^[A-Za-z0-9._-]+")
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -0,0 +1,60 @@
#
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from code import InteractiveConsole
from importlib.util import find_spec
from typing import Any
class InteractiveShell(InteractiveConsole):
"""
wrapper around :class:`code.InteractiveConsole` to pass :func:`interact()` to IPython shell
"""
@staticmethod
def has_ipython() -> bool:
"""
check if IPython shell is available
Returns:
bool: ``True`` if IPython shell is available, ``False`` otherwise
"""
try:
return find_spec("IPython.terminal.embed") is not None
except ModuleNotFoundError:
return False
def interact(self, *args: Any, **kwargs: Any) -> None:
"""
pass controller to IPython shell
Args:
*args(Any): positional arguments
**kwargs(Any): keyword arguments
"""
if self.has_ipython():
from IPython.terminal.embed import InteractiveShellEmbed
shell = InteractiveShellEmbed(user_ns=self.locals) # type: ignore[no-untyped-call]
shell.show_banner() # type: ignore[no-untyped-call]
shell.interact() # type: ignore[no-untyped-call]
else:
# fallback to default
import readline # pylint: disable=unused-import
InteractiveConsole.interact(self, *args, **kwargs)

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -23,6 +23,7 @@ import shutil
from email.utils import parsedate_to_datetime from email.utils import parsedate_to_datetime
from pathlib import Path from pathlib import Path
from pyalpm import DB # type: ignore[import-not-found] from pyalpm import DB # type: ignore[import-not-found]
from typing import ClassVar
from urllib.parse import urlparse from urllib.parse import urlparse
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -41,7 +42,7 @@ class PacmanDatabase(SyncHttpClient):
sync_files_database(bool): sync files database sync_files_database(bool): sync files database
""" """
LAST_MODIFIED_HEADER = "Last-Modified" LAST_MODIFIED_HEADER: ClassVar[str] = "Last-Modified"
def __init__(self, database: DB, configuration: Configuration) -> None: def __init__(self, database: DB, configuration: Configuration) -> None:
""" """

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -34,13 +34,14 @@ class PkgbuildToken(StrEnum):
well-known tokens dictionary well-known tokens dictionary
Attributes: Attributes:
ArrayEnds(PkgbuildToken): (class attribute) array ends token ArrayEnds(PkgbuildToken): array ends token
ArrayStarts(PkgbuildToken): (class attribute) array starts token ArrayStarts(PkgbuildToken): array starts token
Comma(PkgbuildToken): (class attribute) comma token Comma(PkgbuildToken): comma token
Comment(PkgbuildToken): (class attribute) comment token Comment(PkgbuildToken): comment token
FunctionDeclaration(PkgbuildToken): (class attribute) function declaration token FunctionDeclaration(PkgbuildToken): function declaration token
FunctionEnds(PkgbuildToken): (class attribute) function ends token FunctionEnds(PkgbuildToken): function ends token
FunctionStarts(PkgbuildToken): (class attribute) function starts token FunctionStarts(PkgbuildToken): function starts token
NewLine(PkgbuildToken): new line token
""" """
ArrayStarts = "(" ArrayStarts = "("
@ -54,6 +55,8 @@ class PkgbuildToken(StrEnum):
FunctionStarts = "{" FunctionStarts = "{"
FunctionEnds = "}" FunctionEnds = "}"
NewLine = "\n"
class PkgbuildParser(shlex.shlex): class PkgbuildParser(shlex.shlex):
""" """
@ -174,31 +177,18 @@ class PkgbuildParser(shlex.shlex):
Returns: Returns:
bool: ``True`` if the previous element of the stream is a quote or escaped and ``False`` otherwise bool: ``True`` if the previous element of the stream is a quote or escaped and ``False`` otherwise
""" """
# wrapper around reading utf symbols from random position of the stream
def read_last() -> tuple[int, str]:
while (position := self._io.tell()) > 0:
try:
return position, self._io.read(1)
except UnicodeDecodeError:
self._io.seek(position - 1)
raise PkgbuildParserError("reached starting position, no valid symbols found")
current_position = self._io.tell() current_position = self._io.tell()
last_char = penultimate_char = None last_char = penultimate_char = None
index = current_position - 1 index = current_position - 1
while index > 0: while index > 0:
self._io.seek(index) index, last_char = self._read_last(index)
index, last_char = read_last()
if last_char.isspace(): if last_char.isspace():
index -= 1 index -= 1
continue continue
if index > 1: if index > 1:
self._io.seek(index - 1) _, penultimate_char = self._read_last(index - 1)
_, penultimate_char = read_last()
break break
@ -227,7 +217,7 @@ class PkgbuildParser(shlex.shlex):
case PkgbuildToken.ArrayEnds: case PkgbuildToken.ArrayEnds:
break break
case comment if comment.startswith(PkgbuildToken.Comment): case comment if comment.startswith(PkgbuildToken.Comment):
self.instream.readline() self._read_comment()
continue continue
yield token yield token
@ -268,7 +258,7 @@ class PkgbuildParser(shlex.shlex):
if counter == 0: if counter == 0:
break break
case comment if comment.startswith(PkgbuildToken.Comment): case comment if comment.startswith(PkgbuildToken.Comment):
self.instream.readline() self._read_comment()
if not 0 < start_position < end_position: if not 0 < start_position < end_position:
raise PkgbuildParserError("function body wasn't found") raise PkgbuildParserError("function body wasn't found")
@ -304,7 +294,7 @@ class PkgbuildParser(shlex.shlex):
return return
if token.startswith(PkgbuildToken.Comment): if token.startswith(PkgbuildToken.Comment):
self.instream.readline() self._read_comment()
return return
match self.get_token(): match self.get_token():
@ -332,6 +322,44 @@ class PkgbuildParser(shlex.shlex):
case other if other is not None: case other if other is not None:
yield from self._parse_token(other) yield from self._parse_token(other)
def _read_comment(self) -> None:
"""
read comment from the current position. This method doesn't check comment itself, just read the stream
until the comment line ends
"""
_, last_symbol = self._read_last()
if last_symbol != PkgbuildToken.NewLine:
self.instream.readline()
def _read_last(self, initial_index: int | None = None) -> tuple[int, str]:
"""
wrapper around read to read the last symbol from the input stream. This method is designed to process UTF-8
symbols correctly. This method does not reset current stream position
Args:
initial_index(int | None, optional): initial index to start reading from. If none set, the previous position
will be used (Default value = None)
Returns:
tuple[int, str]: last symbol and its position in the stream
Raises:
PkgbuildParserError: in case if stream reached starting position, but no valid symbols were found
"""
if initial_index is None:
initial_index = self._io.tell() - 1
if initial_index < 0:
raise PkgbuildParserError("stream is on starting position")
self._io.seek(initial_index)
while (position := self._io.tell()) > 0:
try:
return position, self._io.read(1)
except UnicodeDecodeError:
self._io.seek(position - 1)
raise PkgbuildParserError("reached starting position, no valid symbols found")
def parse(self) -> Generator[PkgbuildPatch, None, None]: def parse(self) -> Generator[PkgbuildPatch, None, None]:
""" """
parse source stream and yield parsed entries parse source stream and yield parsed entries

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -17,7 +17,7 @@
# 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 typing import Any from typing import Any, ClassVar
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote.remote import Remote from ahriman.core.alpm.remote.remote import Remote
@ -35,9 +35,9 @@ class AUR(Remote):
DEFAULT_RPC_VERSION(str): (class attribute) default AUR RPC version DEFAULT_RPC_VERSION(str): (class attribute) default AUR RPC version
""" """
DEFAULT_AUR_URL = "https://aur.archlinux.org" DEFAULT_AUR_URL: ClassVar[str] = "https://aur.archlinux.org"
DEFAULT_RPC_URL = f"{DEFAULT_AUR_URL}/rpc" DEFAULT_RPC_URL: ClassVar[str] = f"{DEFAULT_AUR_URL}/rpc"
DEFAULT_RPC_VERSION = "5" DEFAULT_RPC_VERSION: ClassVar[str] = "5"
@classmethod @classmethod
def remote_git_url(cls, package_base: str, repository: str) -> str: def remote_git_url(cls, package_base: str, repository: str) -> str:

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -17,7 +17,7 @@
# 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 typing import Any from typing import Any, ClassVar
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote.remote import Remote from ahriman.core.alpm.remote.remote import Remote
@ -36,10 +36,10 @@ class Official(Remote):
DEFAULT_RPC_URL(str): (class attribute) default archlinux repositories RPC url DEFAULT_RPC_URL(str): (class attribute) default archlinux repositories RPC url
""" """
DEFAULT_ARCHLINUX_GIT_URL = "https://gitlab.archlinux.org" DEFAULT_ARCHLINUX_GIT_URL: ClassVar[str] = "https://gitlab.archlinux.org"
DEFAULT_ARCHLINUX_URL = "https://archlinux.org" DEFAULT_ARCHLINUX_URL: ClassVar[str] = "https://archlinux.org"
DEFAULT_SEARCH_REPOSITORIES = ["Core", "Extra", "Multilib"] DEFAULT_SEARCH_REPOSITORIES: ClassVar[list[str]] = ["Core", "Extra", "Multilib"]
DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json" DEFAULT_RPC_URL: ClassVar[str] = "https://archlinux.org/packages/search/json"
@classmethod @classmethod
def remote_git_url(cls, package_base: str, repository: str) -> str: def remote_git_url(cls, package_base: str, repository: str) -> str:

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -19,9 +19,8 @@
# #
try: try:
import aiohttp_security import aiohttp_security
_has_aiohttp_security = True
except ImportError: except ImportError:
_has_aiohttp_security = False aiohttp_security = None # type: ignore[assignment]
from typing import Any from typing import Any
@ -40,7 +39,7 @@ async def authorized_userid(*args: Any, **kwargs: Any) -> Any:
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 aiohttp_security is not None:
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
return None return None
@ -56,7 +55,7 @@ async def check_authorized(*args: Any, **kwargs: Any) -> Any:
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 aiohttp_security is not None:
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
return None return None
@ -72,7 +71,7 @@ async def forget(*args: Any, **kwargs: Any) -> Any:
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 aiohttp_security is not None:
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
return None return None
@ -88,6 +87,6 @@ async def remember(*args: Any, **kwargs: Any) -> Any:
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 aiohttp_security is not None:
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
return None return None

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -136,7 +136,7 @@ class PackageArchive:
dependencies, roots = self.depends_on_paths() dependencies, roots = self.depends_on_paths()
installed_packages = self.installed_packages() installed_packages = self.installed_packages()
# build list of packages, which contains both the package itself and (possible) debug packages # build list of packages, which contains both the package itself and (possible) debug packages
packages = list(self.package.packages) + [f"{package}-debug" for package in self.package.packages] packages = list(self.package.packages) + [f"{self.package.base}-debug"]
# build initial map of file path -> packages containing this path # build initial map of file path -> packages containing this path
# in fact, keys will contain all libraries the package linked to and all directories it contains # in fact, keys will contain all libraries the package linked to and all directories it contains

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -21,6 +21,7 @@ import shutil
from collections.abc import Generator from collections.abc import Generator
from pathlib import Path from pathlib import Path
from typing import ClassVar
from ahriman.core.exceptions import CalledProcessError from ahriman.core.exceptions import CalledProcessError
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
@ -42,9 +43,9 @@ class Sources(LazyLogging):
GITCONFIG(dict[str, str]): (class attribute) git config options to suppress annoying hints GITCONFIG(dict[str, str]): (class attribute) git config options to suppress annoying hints
""" """
DEFAULT_BRANCH = "master" # default fallback branch DEFAULT_BRANCH: ClassVar[str] = "master" # default fallback branch
DEFAULT_COMMIT_AUTHOR = ("ahriman", "ahriman@localhost") DEFAULT_COMMIT_AUTHOR: ClassVar[tuple[str, str]] = ("ahriman", "ahriman@localhost")
GITCONFIG = { GITCONFIG: ClassVar[dict[str, str]] = {
"init.defaultBranch": DEFAULT_BRANCH, "init.defaultBranch": DEFAULT_BRANCH,
} }

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2021-2024 ahriman team. # Copyright (c) 2021-2025 ahriman team.
# #
# This file is part of ahriman # This file is part of ahriman
# (see https://github.com/arcan1s/ahriman). # (see https://github.com/arcan1s/ahriman).
@ -149,8 +149,11 @@ class Task(LazyLogging):
str | None: current commit sha if available str | None: current commit sha if available
""" """
last_commit_sha = Sources.load(sources_dir, self.package, patches, self.paths) last_commit_sha = Sources.load(sources_dir, self.package, patches, self.paths)
if local_version is None: if self.package.is_vcs: # if package is VCS, then make sure to update PKGBUILD to the latest version
return last_commit_sha # there is no local package or pkgrel increment is disabled self.build(sources_dir, dry_run=True)
if local_version is None: # there is no local package or pkgrel increment is disabled
return last_commit_sha
# load fresh package # load fresh package
loaded_package = Package.from_build(sources_dir, self.architecture, None) loaded_package = Package.from_build(sources_dir, self.architecture, None)

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