Compare commits

..

27 Commits
2.6.1 ... 2.8.0

Author SHA1 Message Date
2cd61b0a20 Release 2.8.0 2023-04-09 13:06:54 +03:00
9503a9f2ae try to remove unknown packages from api 2023-04-06 18:14:36 +03:00
39fde7cd5f hide cookie key and salt from config output 2023-04-06 00:31:50 +03:00
58379e7bf3 optimize imports 2023-04-06 00:24:39 +03:00
3c068edf4f argument annootation update 2023-04-06 00:24:39 +03:00
1106ff6482 fix license url 2023-04-06 00:24:39 +03:00
e08ab2db10 extract schemas automatically from views 2023-04-06 00:24:39 +03:00
8f4a2547e8 use api generated docs instead of comments (#92) 2023-04-06 00:24:39 +03:00
7f5e541120 execute request in context methods instead of handling them each time
manually
2023-03-23 12:43:04 +02:00
ec0550a275 Release 2.7.1 2023-03-06 01:15:47 +02:00
df23be9269 gracefully terminate web server
In previous revisions server was terminated by itself, thus no lock or
socket was removed. In new version, graceful termination of the queue
has been added as well as server now handles singals
2023-03-06 01:13:41 +02:00
a8c40a6b87 replace InitializeException with InitializeError in docs 2023-03-02 11:07:59 +02:00
a274f91677 simplify login ttl processing 2023-02-24 16:52:55 +02:00
13faf66bdb add more validation rules 2023-02-23 15:18:56 +02:00
4fb9335df9 add ability to read cookie secret from config 2023-02-22 18:47:56 +02:00
d517d8bfbb Release 2.7.0 2023-02-20 03:05:08 +02:00
37e57c13c8 update dependencies before build (#91)
Old implementation has used add step in order to fetch dependencies,
which could lead to build errors in case if dependency list was updated.

New solution uses dependencies which are declared at current version and
fetch them (if required and if enabled) before update process.

Closes #90
2023-02-12 06:02:30 +03:00
19bb19e9f5 handle .gitignore file correctly in remote push trigger 2023-02-11 04:41:24 +02:00
3a4e8f4d97 mask mypy warning
The newest mypy produces the following warning:

src/ahriman/application/handlers/search.py:43: error: Non-overlapping identity check (left operand type: "Union[_DefaultFactory[Any], Literal[_MISSING_TYPE.MISSING]]", right operand type: "Type[List[Any]]")  [comparison-overlap]

which is more likely caused by updated dataclass models to protoocol (however decorators are still calllable). This commit masks problematic line from checking
2023-02-09 22:46:08 +02:00
4db8ad8e8d hide passwords and secrets from repo-config subcommand by default 2023-02-05 16:44:48 +02:00
117f096d41 note about local database update (see #85) 2023-01-31 14:47:58 +02:00
917ec48be5 handle architecture specific fields for dependencies
This change requires srcinfo at least 0.1.2 version. Unfortunatelly aur
api don't support architecture specific arrays for now, so we just leave
it as is

Closes #82
2023-01-31 14:34:09 +02:00
0a2ba4ae07 filter empty packages from database
In some cases (e.g. during addition of the package to build queue) we don't have
full information about package inself; in these cases we produce lines
with empty architecture, which duplicates normal ones.

This commit changes architecture column type to required and also
filters packages which don't have architecture set yet.

Closes #83
2023-01-30 17:57:13 +02:00
9d7f63e549 trim version from provides list
Closes #87
2023-01-30 17:27:34 +02:00
25eee9ca5e add ability to suppress http logging errors (#86) 2023-01-30 17:19:01 +02:00
5af84955ac calculate dependencies based on package information (#89) 2023-01-30 17:28:05 +03:00
d3ad4c3c08 remove debug line 2023-01-27 16:41:41 +02:00
218 changed files with 8622 additions and 5779 deletions

View File

@ -1 +1 @@
skips: ['B101', 'B105', 'B106', 'B404'] skips: ['B101', 'B104', 'B105', 'B106', 'B404']

View File

@ -18,7 +18,7 @@ if [[ -z $MINIMAL_INSTALL ]]; then
# VCS support # VCS support
pacman --noconfirm -Sy breezy darcs mercurial subversion pacman --noconfirm -Sy breezy darcs mercurial subversion
# web server # web server
pacman --noconfirm -Sy python-aioauth-client python-aiohttp python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja pacman --noconfirm -Sy python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja
# additional features # additional features
pacman --noconfirm -Sy gnupg python-boto3 rsync pacman --noconfirm -Sy gnupg python-boto3 rsync
fi fi

View File

@ -533,5 +533,5 @@ valid-metaclass-classmethod-first-arg=cls
# Exceptions that will emit a warning when being caught. Defaults to # Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception". # "BaseException, Exception".
overgeneral-exceptions=BaseException, overgeneral-exceptions=builtins.BaseException,
Exception builtins.Exception

View File

@ -156,6 +156,52 @@ Again, the most checks can be performed by `make check` command, though some add
* No global variable is allowed outside of `ahriman.version` module. `ahriman.core.context` is also special case. * No global variable is allowed outside of `ahriman.version` module. `ahriman.core.context` is also special case.
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent. * Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
* If your class writes anything to log, the `ahriman.core.log.LazyLogging` trait must be used. * If your class writes anything to log, the `ahriman.core.log.LazyLogging` trait must be used.
* Web API methods must be documented by using `aiohttp_apispec` library. Schema testing mostly should be implemented in related view class tests. Recommended example for documentation (excluding comments):
```python
import aiohttp_apispec
from marshmallow import Schema, fields
from ahriman.web.schemas.auth_schema import AuthSchema
from ahriman.web.schemas.error_schema import ErrorSchema
from ahriman.web.schemas.package_name_schema import PackageNameSchema
from ahriman.web.views.base import BaseView
class RequestSchema(Schema):
field = fields.String(metadata={"description": "Field description", "example": "foo"})
class ResponseSchema(Schema):
field = fields.String(required=True, metadata={"description": "Field description"})
class Foo(BaseView):
POST_PERMISSION = ...
@aiohttp_apispec.docs(
tags=["Tag"],
summary="Do foo",
description="Extended description of the method which does foo",
responses={
200: {"description": "Success response", "schema": ResponseSchema},
204: {"description": "Success response"}, # example without json schema response
400: {"description": "Bad data is supplied", "schema": ErrorSchema}, # exception raised by this method
401: {"description": "Authorization required", "schema": ErrorSchema}, # should be always presented
403: {"description": "Access is forbidden", "schema": ErrorSchema}, # should be always presented
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.json_schema(RequestSchema(many=True))
async def post(self) -> None: ...
```
### Other checks ### Other checks

View File

@ -30,9 +30,9 @@ COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
## 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 --noconfirm -Sy devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-setuptools python-srcinfo && \ RUN pacman --noconfirm -Sy devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-setuptools python-srcinfo && \
pacman --noconfirm -Sy python-build python-installer python-wheel && \ pacman --noconfirm -Sy python-build python-installer python-wheel && \
pacman --noconfirm -Sy breezy mercurial python-aiohttp python-boto3 python-cryptography python-jinja python-requests-unixsocket rsync subversion && \ pacman --noconfirm -Sy breezy mercurial python-aiohttp python-aiohttp-cors python-boto3 python-cryptography python-jinja python-requests-unixsocket rsync subversion && \
runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-jinja2 python-aiohttp-debugtoolbar \ runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-apispec-git python-aiohttp-jinja2 \
python-aiohttp-session python-aiohttp-security python-aiohttp-debugtoolbar python-aiohttp-session python-aiohttp-security
# cleanup unused # cleanup unused
RUN find "/var/cache/pacman/pkg" -type f -delete RUN find "/var/cache/pacman/pkg" -type f -delete

View File

@ -36,6 +36,6 @@ The application provides reasonable defaults which allow to use it out-of-box; h
## Live demos ## Live demos
* [Build status page](https://ahriman-demo.arcanis.me). You can log in as `demo` user by using `demo` password. However, you will not be able to run tasks. * [Build status page](https://ahriman-demo.arcanis.me). You can log in as `demo` user by using `demo` password. However, you will not be able to run tasks. [HTTP API documentation](https://ahriman-demo.arcanis.me/api-docs) is also available.
* [Repository index](http://repo.arcanis.me/x86_64/index.html). * [Repository index](http://repo.arcanis.me/x86_64/index.html).
* [Telegram feed](https://t.me/arcanisrepo). * [Telegram feed](https://t.me/arcanisrepo).

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 673 KiB

After

Width:  |  Height:  |  Size: 750 KiB

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2023\-01\-25" "ahriman" "Generated Python Manual" .TH AHRIMAN "1" "2023\-04\-09" "ahriman" "Generated Python Manual"
.SH NAME .SH NAME
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS
@ -156,7 +156,7 @@ web server
.SH COMMAND \fI\,'ahriman aur\-search'\/\fR .SH COMMAND \fI\,'ahriman aur\-search'\/\fR
usage: ahriman aur\-search [\-h] [\-e] [\-\-info | \-\-no\-info] usage: ahriman aur\-search [\-h] [\-e] [\-\-info | \-\-no\-info]
[\-\-sort\-by {description,first_submitted,id,last_modified,maintainer,name,num_votes,out_of_date,package_base,package_base_id,popularity,repository,url,url_path,version}] [\-\-sort\-by {description,first_submitted,id,last_modified,maintainer,name,num_votes,out_of_date,package_base,package_base_id,popularity,repository,submitter,url,url_path,version}]
search [search ...] search [search ...]
search for package in AUR using API search for package in AUR using API
@ -175,7 +175,7 @@ return non\-zero exit status if result is empty
show additional package information (default: False) show additional package information (default: False)
.TP .TP
\fB\-\-sort\-by\fR \fI\,{description,first_submitted,id,last_modified,maintainer,name,num_votes,out_of_date,package_base,package_base_id,popularity,repository,url,url_path,version}\/\fR \fB\-\-sort\-by\fR \fI\,{description,first_submitted,id,last_modified,maintainer,name,num_votes,out_of_date,package_base,package_base_id,popularity,repository,submitter,url,url_path,version}\/\fR
sort field by this field. In case if two packages have the same value of the specified field, they will be always sorted sort field by this field. In case if two packages have the same value of the specified field, they will be always sorted
by name by name
@ -215,8 +215,8 @@ usage: ahriman help\-version [\-h]
print application and its dependencies versions print application and its dependencies versions
.SH COMMAND \fI\,'ahriman package\-add'\/\fR .SH COMMAND \fI\,'ahriman package\-add'\/\fR
usage: ahriman package\-add [\-h] [\-e] [\-n] [\-y] [\-s {auto,archive,aur,directory,local,remote,repository}] usage: ahriman package\-add [\-h] [\-\-dependencies | \-\-no\-dependencies] [\-e] [\-n] [\-y]
[\-\-without\-dependencies] [\-s {auto,archive,aur,directory,local,remote,repository}]
package [package ...] package [package ...]
add existing or new package to the build queue add existing or new package to the build queue
@ -226,6 +226,10 @@ add existing or new package to the build queue
package source (base name, path to local files, remote URL) package source (base name, path to local files, remote URL)
.SH OPTIONS \fI\,'ahriman package\-add'\/\fR .SH OPTIONS \fI\,'ahriman package\-add'\/\fR
.TP
\fB\-\-dependencies\fR, \fB\-\-no\-dependencies\fR
process missing package dependencies (default: True)
.TP .TP
\fB\-e\fR, \fB\-\-exit\-code\fR \fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty return non\-zero exit status if result is empty
@ -242,10 +246,6 @@ download fresh package databases from the mirror before actions, \-yy to force r
\fB\-s\fR \fI\,{auto,archive,aur,directory,local,remote,repository}\/\fR, \fB\-\-source\fR \fI\,{auto,archive,aur,directory,local,remote,repository}\/\fR \fB\-s\fR \fI\,{auto,archive,aur,directory,local,remote,repository}\/\fR, \fB\-\-source\fR \fI\,{auto,archive,aur,directory,local,remote,repository}\/\fR
explicitly specify the package source for this command explicitly specify the package source for this command
.TP
\fB\-\-without\-dependencies\fR
do not add dependencies
.SH COMMAND \fI\,'ahriman package\-remove'\/\fR .SH COMMAND \fI\,'ahriman package\-remove'\/\fR
usage: ahriman package\-remove [\-h] package [package ...] usage: ahriman package\-remove [\-h] package [package ...]
@ -401,8 +401,8 @@ fetch actual version of VCS packages (default: True)
download fresh package databases from the mirror before actions, \-yy to force refresh even if up to date download fresh package databases from the mirror before actions, \-yy to force refresh even if up to date
.SH COMMAND \fI\,'ahriman repo\-daemon'\/\fR .SH COMMAND \fI\,'ahriman repo\-daemon'\/\fR
usage: ahriman repo\-daemon [\-h] [\-i INTERVAL] [\-\-aur | \-\-no\-aur] [\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] usage: ahriman repo\-daemon [\-h] [\-i INTERVAL] [\-\-aur | \-\-no\-aur] [\-\-dependencies | \-\-no\-dependencies]
[\-\-vcs | \-\-no\-vcs] [\-y] [\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-\-vcs | \-\-no\-vcs] [\-y]
start process which periodically will run update process start process which periodically will run update process
@ -415,6 +415,10 @@ interval between runs in seconds
\fB\-\-aur\fR, \fB\-\-no\-aur\fR \fB\-\-aur\fR, \fB\-\-no\-aur\fR
enable or disable checking for AUR updates (default: True) enable or disable checking for AUR updates (default: True)
.TP
\fB\-\-dependencies\fR, \fB\-\-no\-dependencies\fR
process missing package dependencies (default: True)
.TP .TP
\fB\-\-local\fR, \fB\-\-no\-local\fR \fB\-\-local\fR, \fB\-\-no\-local\fR
enable or disable checking of local packages for updates (default: True) enable or disable checking of local packages for updates (default: True)
@ -523,8 +527,8 @@ run triggers on empty build result as configured by settings
instead of running all triggers as set by configuration, just process specified ones in order of mention instead of running all triggers as set by configuration, just process specified ones in order of mention
.SH COMMAND \fI\,'ahriman repo\-update'\/\fR .SH COMMAND \fI\,'ahriman repo\-update'\/\fR
usage: ahriman repo\-update [\-h] [\-\-dry\-run] [\-e] [\-\-aur | \-\-no\-aur] [\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] usage: ahriman repo\-update [\-h] [\-\-aur | \-\-no\-aur] [\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run] [\-e]
[\-\-vcs | \-\-no\-vcs] [\-y] [\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-\-vcs | \-\-no\-vcs] [\-y]
[package ...] [package ...]
check for packages updates and run build process if requested check for packages updates and run build process if requested
@ -534,6 +538,14 @@ check for packages updates and run build process if requested
filter check by package base filter check by package base
.SH OPTIONS \fI\,'ahriman repo\-update'\/\fR .SH OPTIONS \fI\,'ahriman repo\-update'\/\fR
.TP
\fB\-\-aur\fR, \fB\-\-no\-aur\fR
enable or disable checking for AUR updates (default: True)
.TP
\fB\-\-dependencies\fR, \fB\-\-no\-dependencies\fR
process missing package dependencies (default: True)
.TP .TP
\fB\-\-dry\-run\fR \fB\-\-dry\-run\fR
just perform check for updates, same as check command just perform check for updates, same as check command
@ -542,10 +554,6 @@ just perform check for updates, same as check command
\fB\-e\fR, \fB\-\-exit\-code\fR \fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty return non\-zero exit status if result is empty
.TP
\fB\-\-aur\fR, \fB\-\-no\-aur\fR
enable or disable checking for AUR updates (default: True)
.TP .TP
\fB\-\-local\fR, \fB\-\-no\-local\fR \fB\-\-local\fR, \fB\-\-no\-local\fR
enable or disable checking of local packages for updates (default: True) enable or disable checking of local packages for updates (default: True)
@ -590,10 +598,15 @@ clear directory with built packages (default: False)
clear directory with pacman local database cache (default: False) clear directory with pacman local database cache (default: False)
.SH COMMAND \fI\,'ahriman service\-config'\/\fR .SH COMMAND \fI\,'ahriman service\-config'\/\fR
usage: ahriman service\-config [\-h] usage: ahriman service\-config [\-h] [\-\-secure | \-\-no\-secure]
dump configuration for the specified architecture dump configuration for the specified architecture
.SH OPTIONS \fI\,'ahriman service\-config'\/\fR
.TP
\fB\-\-secure\fR, \fB\-\-no\-secure\fR
hide passwords and secrets from output (default: True)
.SH COMMAND \fI\,'ahriman service\-config\-validate'\/\fR .SH COMMAND \fI\,'ahriman service\-config\-validate'\/\fR
usage: ahriman service\-config\-validate [\-h] [\-e] usage: ahriman service\-config\-validate [\-h] [\-e]

View File

@ -1,45 +0,0 @@
ahriman.core.database.data package
==================================
Submodules
----------
ahriman.core.database.data.package\_remotes module
--------------------------------------------------
.. automodule:: ahriman.core.database.data.package_remotes
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.database.data.package\_statuses module
---------------------------------------------------
.. automodule:: ahriman.core.database.data.package_statuses
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.database.data.patches module
-----------------------------------------
.. automodule:: ahriman.core.database.data.patches
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.database.data.users module
---------------------------------------
.. automodule:: ahriman.core.database.data.users
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: ahriman.core.database.data
:members:
:no-undoc-members:
:show-inheritance:

View File

@ -44,6 +44,22 @@ ahriman.core.database.migrations.m004\_logs module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.core.database.migrations.m005\_make\_opt\_depends module
----------------------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m005_make_opt_depends
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.database.migrations.m006\_packages\_architecture\_required module
------------------------------------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m006_packages_architecture_required
:members:
:no-undoc-members:
:show-inheritance:
Module contents Module contents
--------------- ---------------

View File

@ -7,7 +7,6 @@ Subpackages
.. toctree:: .. toctree::
:maxdepth: 4 :maxdepth: 4
ahriman.core.database.data
ahriman.core.database.migrations ahriman.core.database.migrations
ahriman.core.database.operations ahriman.core.database.operations

View File

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

View File

@ -114,7 +114,7 @@ Schema and data migrations
The schema migration are applied according to current ``pragma user_info`` values, located at ``ahriman.core.database.migrations`` package and named as ``m000_migration_name.py`` (the preceding ``m`` is required in order to import migration content for tests). Additional class ``ahriman.core.database.migrations.Migrations`` reads all migrations automatically and applies them in alphabetical order. The schema migration are applied according to current ``pragma user_info`` values, located at ``ahriman.core.database.migrations`` package and named as ``m000_migration_name.py`` (the preceding ``m`` is required in order to import migration content for tests). Additional class ``ahriman.core.database.migrations.Migrations`` reads all migrations automatically and applies them in alphabetical order.
There are also data migrations which are located at ``ahriman.core.database.data`` package and move data from old-style (e.g. json files in filesystem, directory trees, etc) to the database. They are also part of migration and (unlike schema migrations) are applied only at specific version breakpoints (e.g. if ``user_version`` is more than 0 no initial migration will be applied). These migrations also contain data migrations. Though the recommended way is to migrate data directly from SQL requests, sometimes it is required to have external data (like packages list) in order to set correct data. To do so, special method `migrate_data` is used.
Type conversions Type conversions
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
@ -269,6 +269,7 @@ Web application
Web application requires the following python packages to be installed: Web application requires the following python packages to be installed:
* Core part requires ``aiohttp`` (application itself), ``aiohttp_jinja2`` and ``Jinja2`` (HTML generation from templates). * Core part requires ``aiohttp`` (application itself), ``aiohttp_jinja2`` and ``Jinja2`` (HTML generation from templates).
* Additional web features also require ``aiohttp-apispec`` (autogenerated documentation), ``aiohttp_cors`` (CORS support, required by documentation)
* In addition, ``aiohttp_debugtoolbar`` is required for debug panel. Please note that this option does not work together with authorization and basically must not be used in production. * In addition, ``aiohttp_debugtoolbar`` is required for debug panel. Please note that this option does not work together with authorization and basically must not be used in production.
* In addition, authorization feature requires ``aiohttp_security``, ``aiohttp_session`` and ``cryptography``. * In addition, authorization feature requires ``aiohttp_security``, ``aiohttp_session`` and ``cryptography``.
* In addition to base authorization dependencies, OAuth2 also requires ``aioauth-client`` library. * In addition to base authorization dependencies, OAuth2 also requires ``aioauth-client`` library.
@ -279,6 +280,13 @@ Middlewares
Service provides some custom middlewares, e.g. logging every exception (except for user ones) and user authorization. Service provides some custom middlewares, e.g. logging every exception (except for user ones) and user authorization.
HEAD and OPTIONS requests
^^^^^^^^^^^^^^^^^^^^^^^^^
``HEAD`` request is automatically generated by ``ahriman.web.views.base.BaseView`` class. It just calls ``GET`` method, removes any data from body and returns the result. In case if no ``GET`` method available for this view, the ``aiohttp.web.HTTPMethodNotAllowed`` exception will be raised.
On the other side, ``OPTIONS`` method is implemented in the ``ahriman.web.middlewares.exception_handler.exception_handler`` middleware. In case if ``aiohttp.web.HTTPMethodNotAllowed`` exception is raised and original method was ``OPTIONS``, the middleware handles it, converts to valid request and returns response to user.
Web views Web views
^^^^^^^^^ ^^^^^^^^^
@ -288,6 +296,7 @@ REST API supports both form and JSON data, but the last one is recommended.
Different APIs are separated into different packages: Different APIs are separated into different packages:
* ``ahriman.web.views.api`` not a real API, but some views which provide OpenAPI support.
* ``ahriman.web.views.service`` provides views for application controls. * ``ahriman.web.views.service`` provides views for application controls.
* ``ahriman.web.views.status`` package provides REST API for application reporting. * ``ahriman.web.views.status`` package provides REST API for application reporting.
* ``ahriman.web.views.user`` package provides login and logout methods which can be called without authorization. * ``ahriman.web.views.user`` package provides login and logout methods which can be called without authorization.

View File

@ -10,9 +10,9 @@ _shtab_ahriman_help_commands_unsafe_option_strings=('-h' '--help' '--command')
_shtab_ahriman_help_updates_option_strings=('-h' '--help' '-e' '--exit-code') _shtab_ahriman_help_updates_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_help_version_option_strings=('-h' '--help') _shtab_ahriman_help_version_option_strings=('-h' '--help')
_shtab_ahriman_version_option_strings=('-h' '--help') _shtab_ahriman_version_option_strings=('-h' '--help')
_shtab_ahriman_package_add_option_strings=('-h' '--help' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source' '--without-dependencies') _shtab_ahriman_package_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source')
_shtab_ahriman_add_option_strings=('-h' '--help' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source' '--without-dependencies') _shtab_ahriman_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source')
_shtab_ahriman_package_update_option_strings=('-h' '--help' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source' '--without-dependencies') _shtab_ahriman_package_update_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source')
_shtab_ahriman_package_remove_option_strings=('-h' '--help') _shtab_ahriman_package_remove_option_strings=('-h' '--help')
_shtab_ahriman_remove_option_strings=('-h' '--help') _shtab_ahriman_remove_option_strings=('-h' '--help')
_shtab_ahriman_package_status_option_strings=('-h' '--help' '--ahriman' '-e' '--exit-code' '--info' '--no-info' '-s' '--status') _shtab_ahriman_package_status_option_strings=('-h' '--help' '--ahriman' '-e' '--exit-code' '--info' '--no-info' '-s' '--status')
@ -27,8 +27,8 @@ _shtab_ahriman_patch_set_add_option_strings=('-h' '--help' '-t' '--track')
_shtab_ahriman_repo_backup_option_strings=('-h' '--help') _shtab_ahriman_repo_backup_option_strings=('-h' '--help')
_shtab_ahriman_repo_check_option_strings=('-h' '--help' '-e' '--exit-code' '--vcs' '--no-vcs' '-y' '--refresh') _shtab_ahriman_repo_check_option_strings=('-h' '--help' '-e' '--exit-code' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_check_option_strings=('-h' '--help' '-e' '--exit-code' '--vcs' '--no-vcs' '-y' '--refresh') _shtab_ahriman_check_option_strings=('-h' '--help' '-e' '--exit-code' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_repo_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh') _shtab_ahriman_repo_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh') _shtab_ahriman_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_repo_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code') _shtab_ahriman_repo_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code')
_shtab_ahriman_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code') _shtab_ahriman_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code')
_shtab_ahriman_repo_remove_unknown_option_strings=('-h' '--help' '--dry-run') _shtab_ahriman_repo_remove_unknown_option_strings=('-h' '--help' '--dry-run')
@ -43,14 +43,14 @@ _shtab_ahriman_repo_sync_option_strings=('-h' '--help')
_shtab_ahriman_sync_option_strings=('-h' '--help') _shtab_ahriman_sync_option_strings=('-h' '--help')
_shtab_ahriman_repo_tree_option_strings=('-h' '--help') _shtab_ahriman_repo_tree_option_strings=('-h' '--help')
_shtab_ahriman_repo_triggers_option_strings=('-h' '--help') _shtab_ahriman_repo_triggers_option_strings=('-h' '--help')
_shtab_ahriman_repo_update_option_strings=('-h' '--help' '--dry-run' '-e' '--exit-code' '--aur' '--no-aur' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh') _shtab_ahriman_repo_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_update_option_strings=('-h' '--help' '--dry-run' '-e' '--exit-code' '--aur' '--no-aur' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh') _shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_service_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman') _shtab_ahriman_service_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman') _shtab_ahriman_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_repo_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman') _shtab_ahriman_repo_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_service_config_option_strings=('-h' '--help') _shtab_ahriman_service_config_option_strings=('-h' '--help' '--secure' '--no-secure')
_shtab_ahriman_config_option_strings=('-h' '--help') _shtab_ahriman_config_option_strings=('-h' '--help' '--secure' '--no-secure')
_shtab_ahriman_repo_config_option_strings=('-h' '--help') _shtab_ahriman_repo_config_option_strings=('-h' '--help' '--secure' '--no-secure')
_shtab_ahriman_service_config_validate_option_strings=('-h' '--help' '-e' '--exit-code') _shtab_ahriman_service_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_config_validate_option_strings=('-h' '--help' '-e' '--exit-code') _shtab_ahriman_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_repo_config_validate_option_strings=('-h' '--help' '-e' '--exit-code') _shtab_ahriman_repo_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
@ -71,8 +71,8 @@ _shtab_ahriman_web_option_strings=('-h' '--help')
_shtab_ahriman_pos_0_choices=('aur-search' 'search' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'user-add' 'user-list' 'user-remove' 'web') _shtab_ahriman_pos_0_choices=('aur-search' 'search' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'user-add' 'user-list' 'user-remove' 'web')
_shtab_ahriman_aur_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'url' 'url_path' 'version') _shtab_ahriman_aur_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'submitter' 'url' 'url_path' 'version')
_shtab_ahriman_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'url' 'url_path' 'version') _shtab_ahriman_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'submitter' 'url' 'url_path' 'version')
_shtab_ahriman_package_add__s_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository') _shtab_ahriman_package_add__s_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository')
_shtab_ahriman_package_add___source_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository') _shtab_ahriman_package_add___source_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository')
_shtab_ahriman_add__s_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository') _shtab_ahriman_add__s_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository')
@ -139,33 +139,36 @@ _shtab_ahriman_version___help_nargs=0
_shtab_ahriman_package_add_pos_0_nargs=+ _shtab_ahriman_package_add_pos_0_nargs=+
_shtab_ahriman_package_add__h_nargs=0 _shtab_ahriman_package_add__h_nargs=0
_shtab_ahriman_package_add___help_nargs=0 _shtab_ahriman_package_add___help_nargs=0
_shtab_ahriman_package_add___dependencies_nargs=0
_shtab_ahriman_package_add___no_dependencies_nargs=0
_shtab_ahriman_package_add__e_nargs=0 _shtab_ahriman_package_add__e_nargs=0
_shtab_ahriman_package_add___exit_code_nargs=0 _shtab_ahriman_package_add___exit_code_nargs=0
_shtab_ahriman_package_add__n_nargs=0 _shtab_ahriman_package_add__n_nargs=0
_shtab_ahriman_package_add___now_nargs=0 _shtab_ahriman_package_add___now_nargs=0
_shtab_ahriman_package_add__y_nargs=0 _shtab_ahriman_package_add__y_nargs=0
_shtab_ahriman_package_add___refresh_nargs=0 _shtab_ahriman_package_add___refresh_nargs=0
_shtab_ahriman_package_add___without_dependencies_nargs=0
_shtab_ahriman_add_pos_0_nargs=+ _shtab_ahriman_add_pos_0_nargs=+
_shtab_ahriman_add__h_nargs=0 _shtab_ahriman_add__h_nargs=0
_shtab_ahriman_add___help_nargs=0 _shtab_ahriman_add___help_nargs=0
_shtab_ahriman_add___dependencies_nargs=0
_shtab_ahriman_add___no_dependencies_nargs=0
_shtab_ahriman_add__e_nargs=0 _shtab_ahriman_add__e_nargs=0
_shtab_ahriman_add___exit_code_nargs=0 _shtab_ahriman_add___exit_code_nargs=0
_shtab_ahriman_add__n_nargs=0 _shtab_ahriman_add__n_nargs=0
_shtab_ahriman_add___now_nargs=0 _shtab_ahriman_add___now_nargs=0
_shtab_ahriman_add__y_nargs=0 _shtab_ahriman_add__y_nargs=0
_shtab_ahriman_add___refresh_nargs=0 _shtab_ahriman_add___refresh_nargs=0
_shtab_ahriman_add___without_dependencies_nargs=0
_shtab_ahriman_package_update_pos_0_nargs=+ _shtab_ahriman_package_update_pos_0_nargs=+
_shtab_ahriman_package_update__h_nargs=0 _shtab_ahriman_package_update__h_nargs=0
_shtab_ahriman_package_update___help_nargs=0 _shtab_ahriman_package_update___help_nargs=0
_shtab_ahriman_package_update___dependencies_nargs=0
_shtab_ahriman_package_update___no_dependencies_nargs=0
_shtab_ahriman_package_update__e_nargs=0 _shtab_ahriman_package_update__e_nargs=0
_shtab_ahriman_package_update___exit_code_nargs=0 _shtab_ahriman_package_update___exit_code_nargs=0
_shtab_ahriman_package_update__n_nargs=0 _shtab_ahriman_package_update__n_nargs=0
_shtab_ahriman_package_update___now_nargs=0 _shtab_ahriman_package_update___now_nargs=0
_shtab_ahriman_package_update__y_nargs=0 _shtab_ahriman_package_update__y_nargs=0
_shtab_ahriman_package_update___refresh_nargs=0 _shtab_ahriman_package_update___refresh_nargs=0
_shtab_ahriman_package_update___without_dependencies_nargs=0
_shtab_ahriman_package_remove_pos_0_nargs=+ _shtab_ahriman_package_remove_pos_0_nargs=+
_shtab_ahriman_package_remove__h_nargs=0 _shtab_ahriman_package_remove__h_nargs=0
_shtab_ahriman_package_remove___help_nargs=0 _shtab_ahriman_package_remove___help_nargs=0
@ -231,6 +234,8 @@ _shtab_ahriman_repo_daemon__h_nargs=0
_shtab_ahriman_repo_daemon___help_nargs=0 _shtab_ahriman_repo_daemon___help_nargs=0
_shtab_ahriman_repo_daemon___aur_nargs=0 _shtab_ahriman_repo_daemon___aur_nargs=0
_shtab_ahriman_repo_daemon___no_aur_nargs=0 _shtab_ahriman_repo_daemon___no_aur_nargs=0
_shtab_ahriman_repo_daemon___dependencies_nargs=0
_shtab_ahriman_repo_daemon___no_dependencies_nargs=0
_shtab_ahriman_repo_daemon___local_nargs=0 _shtab_ahriman_repo_daemon___local_nargs=0
_shtab_ahriman_repo_daemon___no_local_nargs=0 _shtab_ahriman_repo_daemon___no_local_nargs=0
_shtab_ahriman_repo_daemon___manual_nargs=0 _shtab_ahriman_repo_daemon___manual_nargs=0
@ -243,6 +248,8 @@ _shtab_ahriman_daemon__h_nargs=0
_shtab_ahriman_daemon___help_nargs=0 _shtab_ahriman_daemon___help_nargs=0
_shtab_ahriman_daemon___aur_nargs=0 _shtab_ahriman_daemon___aur_nargs=0
_shtab_ahriman_daemon___no_aur_nargs=0 _shtab_ahriman_daemon___no_aur_nargs=0
_shtab_ahriman_daemon___dependencies_nargs=0
_shtab_ahriman_daemon___no_dependencies_nargs=0
_shtab_ahriman_daemon___local_nargs=0 _shtab_ahriman_daemon___local_nargs=0
_shtab_ahriman_daemon___no_local_nargs=0 _shtab_ahriman_daemon___no_local_nargs=0
_shtab_ahriman_daemon___manual_nargs=0 _shtab_ahriman_daemon___manual_nargs=0
@ -295,11 +302,13 @@ _shtab_ahriman_repo_triggers___help_nargs=0
_shtab_ahriman_repo_update_pos_0_nargs=* _shtab_ahriman_repo_update_pos_0_nargs=*
_shtab_ahriman_repo_update__h_nargs=0 _shtab_ahriman_repo_update__h_nargs=0
_shtab_ahriman_repo_update___help_nargs=0 _shtab_ahriman_repo_update___help_nargs=0
_shtab_ahriman_repo_update___aur_nargs=0
_shtab_ahriman_repo_update___no_aur_nargs=0
_shtab_ahriman_repo_update___dependencies_nargs=0
_shtab_ahriman_repo_update___no_dependencies_nargs=0
_shtab_ahriman_repo_update___dry_run_nargs=0 _shtab_ahriman_repo_update___dry_run_nargs=0
_shtab_ahriman_repo_update__e_nargs=0 _shtab_ahriman_repo_update__e_nargs=0
_shtab_ahriman_repo_update___exit_code_nargs=0 _shtab_ahriman_repo_update___exit_code_nargs=0
_shtab_ahriman_repo_update___aur_nargs=0
_shtab_ahriman_repo_update___no_aur_nargs=0
_shtab_ahriman_repo_update___local_nargs=0 _shtab_ahriman_repo_update___local_nargs=0
_shtab_ahriman_repo_update___no_local_nargs=0 _shtab_ahriman_repo_update___no_local_nargs=0
_shtab_ahriman_repo_update___manual_nargs=0 _shtab_ahriman_repo_update___manual_nargs=0
@ -311,11 +320,13 @@ _shtab_ahriman_repo_update___refresh_nargs=0
_shtab_ahriman_update_pos_0_nargs=* _shtab_ahriman_update_pos_0_nargs=*
_shtab_ahriman_update__h_nargs=0 _shtab_ahriman_update__h_nargs=0
_shtab_ahriman_update___help_nargs=0 _shtab_ahriman_update___help_nargs=0
_shtab_ahriman_update___aur_nargs=0
_shtab_ahriman_update___no_aur_nargs=0
_shtab_ahriman_update___dependencies_nargs=0
_shtab_ahriman_update___no_dependencies_nargs=0
_shtab_ahriman_update___dry_run_nargs=0 _shtab_ahriman_update___dry_run_nargs=0
_shtab_ahriman_update__e_nargs=0 _shtab_ahriman_update__e_nargs=0
_shtab_ahriman_update___exit_code_nargs=0 _shtab_ahriman_update___exit_code_nargs=0
_shtab_ahriman_update___aur_nargs=0
_shtab_ahriman_update___no_aur_nargs=0
_shtab_ahriman_update___local_nargs=0 _shtab_ahriman_update___local_nargs=0
_shtab_ahriman_update___no_local_nargs=0 _shtab_ahriman_update___no_local_nargs=0
_shtab_ahriman_update___manual_nargs=0 _shtab_ahriman_update___manual_nargs=0
@ -362,10 +373,16 @@ _shtab_ahriman_repo_clean___pacman_nargs=0
_shtab_ahriman_repo_clean___no_pacman_nargs=0 _shtab_ahriman_repo_clean___no_pacman_nargs=0
_shtab_ahriman_service_config__h_nargs=0 _shtab_ahriman_service_config__h_nargs=0
_shtab_ahriman_service_config___help_nargs=0 _shtab_ahriman_service_config___help_nargs=0
_shtab_ahriman_service_config___secure_nargs=0
_shtab_ahriman_service_config___no_secure_nargs=0
_shtab_ahriman_config__h_nargs=0 _shtab_ahriman_config__h_nargs=0
_shtab_ahriman_config___help_nargs=0 _shtab_ahriman_config___help_nargs=0
_shtab_ahriman_config___secure_nargs=0
_shtab_ahriman_config___no_secure_nargs=0
_shtab_ahriman_repo_config__h_nargs=0 _shtab_ahriman_repo_config__h_nargs=0
_shtab_ahriman_repo_config___help_nargs=0 _shtab_ahriman_repo_config___help_nargs=0
_shtab_ahriman_repo_config___secure_nargs=0
_shtab_ahriman_repo_config___no_secure_nargs=0
_shtab_ahriman_service_config_validate__h_nargs=0 _shtab_ahriman_service_config_validate__h_nargs=0
_shtab_ahriman_service_config_validate___help_nargs=0 _shtab_ahriman_service_config_validate___help_nargs=0
_shtab_ahriman_service_config_validate__e_nargs=0 _shtab_ahriman_service_config_validate__e_nargs=0

View File

@ -87,11 +87,11 @@ _shtab_ahriman_options=(
_shtab_ahriman_add_options=( _shtab_ahriman_add_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: \%(default)s)]:dependencies:"
{-e,--exit-code}"[return non-zero exit status if result is empty]" {-e,--exit-code}"[return non-zero exit status if result is empty]"
{-n,--now}"[run update function after]" {-n,--now}"[run update function after]"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]" "*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]"
{-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)" {-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)"
"--without-dependencies[do not add dependencies]"
"(*):package source (base name, path to local files, remote URL):" "(*):package source (base name, path to local files, remote URL):"
) )
@ -99,7 +99,7 @@ _shtab_ahriman_aur_search_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
{-e,--exit-code}"[return non-zero exit status if result is empty]" {-e,--exit-code}"[return non-zero exit status if result is empty]"
{--info,--no-info}"[show additional package information (default\: \%(default)s)]:info:" {--info,--no-info}"[show additional package information (default\: \%(default)s)]:info:"
"--sort-by[sort field by this field. In case if two packages have the same value of the specified field, they will be always sorted by name]:sort_by:(description first_submitted id last_modified maintainer name num_votes out_of_date package_base package_base_id popularity repository url url_path version)" "--sort-by[sort field by this field. In case if two packages have the same value of the specified field, they will be always sorted by name]:sort_by:(description first_submitted id last_modified maintainer name num_votes out_of_date package_base package_base_id popularity repository submitter url url_path version)"
"(*):search terms, can be specified multiple times, the result will match all terms:" "(*):search terms, can be specified multiple times, the result will match all terms:"
) )
@ -122,6 +122,7 @@ _shtab_ahriman_clean_options=(
_shtab_ahriman_config_options=( _shtab_ahriman_config_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
{--secure,--no-secure}"[hide passwords and secrets from output (default\: \%(default)s)]:secure:"
) )
_shtab_ahriman_config_validate_options=( _shtab_ahriman_config_validate_options=(
@ -133,6 +134,7 @@ _shtab_ahriman_daemon_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
{-i,--interval}"[interval between runs in seconds]:interval:" {-i,--interval}"[interval between runs in seconds]:interval:"
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: \%(default)s)]:aur:" {--aur,--no-aur}"[enable or disable checking for AUR updates (default\: \%(default)s)]:aur:"
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: \%(default)s)]:dependencies:"
{--local,--no-local}"[enable or disable checking of local packages for updates (default\: \%(default)s)]:local:" {--local,--no-local}"[enable or disable checking of local packages for updates (default\: \%(default)s)]:local:"
{--manual,--no-manual}"[include or exclude manual updates (default\: \%(default)s)]:manual:" {--manual,--no-manual}"[include or exclude manual updates (default\: \%(default)s)]:manual:"
{--vcs,--no-vcs}"[fetch actual version of VCS packages (default\: \%(default)s)]:vcs:" {--vcs,--no-vcs}"[fetch actual version of VCS packages (default\: \%(default)s)]:vcs:"
@ -182,11 +184,11 @@ _shtab_ahriman_key_import_options=(
_shtab_ahriman_package_add_options=( _shtab_ahriman_package_add_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: \%(default)s)]:dependencies:"
{-e,--exit-code}"[return non-zero exit status if result is empty]" {-e,--exit-code}"[return non-zero exit status if result is empty]"
{-n,--now}"[run update function after]" {-n,--now}"[run update function after]"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]" "*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]"
{-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)" {-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)"
"--without-dependencies[do not add dependencies]"
"(*):package source (base name, path to local files, remote URL):" "(*):package source (base name, path to local files, remote URL):"
) )
@ -217,11 +219,11 @@ _shtab_ahriman_package_status_update_options=(
_shtab_ahriman_package_update_options=( _shtab_ahriman_package_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: \%(default)s)]:dependencies:"
{-e,--exit-code}"[return non-zero exit status if result is empty]" {-e,--exit-code}"[return non-zero exit status if result is empty]"
{-n,--now}"[run update function after]" {-n,--now}"[run update function after]"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]" "*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date]"
{-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)" {-s,--source}"[explicitly specify the package source for this command]:source:(auto archive aur directory local remote repository)"
"--without-dependencies[do not add dependencies]"
"(*):package source (base name, path to local files, remote URL):" "(*):package source (base name, path to local files, remote URL):"
) )
@ -293,6 +295,7 @@ _shtab_ahriman_repo_clean_options=(
_shtab_ahriman_repo_config_options=( _shtab_ahriman_repo_config_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
{--secure,--no-secure}"[hide passwords and secrets from output (default\: \%(default)s)]:secure:"
) )
_shtab_ahriman_repo_config_validate_options=( _shtab_ahriman_repo_config_validate_options=(
@ -304,6 +307,7 @@ _shtab_ahriman_repo_daemon_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
{-i,--interval}"[interval between runs in seconds]:interval:" {-i,--interval}"[interval between runs in seconds]:interval:"
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: \%(default)s)]:aur:" {--aur,--no-aur}"[enable or disable checking for AUR updates (default\: \%(default)s)]:aur:"
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: \%(default)s)]:dependencies:"
{--local,--no-local}"[enable or disable checking of local packages for updates (default\: \%(default)s)]:local:" {--local,--no-local}"[enable or disable checking of local packages for updates (default\: \%(default)s)]:local:"
{--manual,--no-manual}"[include or exclude manual updates (default\: \%(default)s)]:manual:" {--manual,--no-manual}"[include or exclude manual updates (default\: \%(default)s)]:manual:"
{--vcs,--no-vcs}"[fetch actual version of VCS packages (default\: \%(default)s)]:vcs:" {--vcs,--no-vcs}"[fetch actual version of VCS packages (default\: \%(default)s)]:vcs:"
@ -390,9 +394,10 @@ _shtab_ahriman_repo_triggers_options=(
_shtab_ahriman_repo_update_options=( _shtab_ahriman_repo_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: \%(default)s)]:aur:"
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: \%(default)s)]:dependencies:"
"--dry-run[just perform check for updates, same as check command]" "--dry-run[just perform check for updates, same as check command]"
{-e,--exit-code}"[return non-zero exit status if result is empty]" {-e,--exit-code}"[return non-zero exit status if result is empty]"
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: \%(default)s)]:aur:"
{--local,--no-local}"[enable or disable checking of local packages for updates (default\: \%(default)s)]:local:" {--local,--no-local}"[enable or disable checking of local packages for updates (default\: \%(default)s)]:local:"
{--manual,--no-manual}"[include or exclude manual updates (default\: \%(default)s)]:manual:" {--manual,--no-manual}"[include or exclude manual updates (default\: \%(default)s)]:manual:"
{--vcs,--no-vcs}"[fetch actual version of VCS packages (default\: \%(default)s)]:vcs:" {--vcs,--no-vcs}"[fetch actual version of VCS packages (default\: \%(default)s)]:vcs:"
@ -408,7 +413,7 @@ _shtab_ahriman_search_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
{-e,--exit-code}"[return non-zero exit status if result is empty]" {-e,--exit-code}"[return non-zero exit status if result is empty]"
{--info,--no-info}"[show additional package information (default\: \%(default)s)]:info:" {--info,--no-info}"[show additional package information (default\: \%(default)s)]:info:"
"--sort-by[sort field by this field. In case if two packages have the same value of the specified field, they will be always sorted by name]:sort_by:(description first_submitted id last_modified maintainer name num_votes out_of_date package_base package_base_id popularity repository url url_path version)" "--sort-by[sort field by this field. In case if two packages have the same value of the specified field, they will be always sorted by name]:sort_by:(description first_submitted id last_modified maintainer name num_votes out_of_date package_base package_base_id popularity repository submitter url url_path version)"
"(*):search terms, can be specified multiple times, the result will match all terms:" "(*):search terms, can be specified multiple times, the result will match all terms:"
) )
@ -423,6 +428,7 @@ _shtab_ahriman_service_clean_options=(
_shtab_ahriman_service_config_options=( _shtab_ahriman_service_config_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
{--secure,--no-secure}"[hide passwords and secrets from output (default\: \%(default)s)]:secure:"
) )
_shtab_ahriman_service_config_validate_options=( _shtab_ahriman_service_config_validate_options=(
@ -504,9 +510,10 @@ _shtab_ahriman_sync_options=(
_shtab_ahriman_update_options=( _shtab_ahriman_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-h,--help}"[show this help message and exit]"
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: \%(default)s)]:aur:"
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: \%(default)s)]:dependencies:"
"--dry-run[just perform check for updates, same as check command]" "--dry-run[just perform check for updates, same as check command]"
{-e,--exit-code}"[return non-zero exit status if result is empty]" {-e,--exit-code}"[return non-zero exit status if result is empty]"
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: \%(default)s)]:aur:"
{--local,--no-local}"[enable or disable checking of local packages for updates (default\: \%(default)s)]:local:" {--local,--no-local}"[enable or disable checking of local packages for updates (default\: \%(default)s)]:local:"
{--manual,--no-manual}"[include or exclude manual updates (default\: \%(default)s)]:manual:" {--manual,--no-manual}"[include or exclude manual updates (default\: \%(default)s)]:manual:"
{--vcs,--no-vcs}"[fetch actual version of VCS packages (default\: \%(default)s)]:vcs:" {--vcs,--no-vcs}"[fetch actual version of VCS packages (default\: \%(default)s)]:vcs:"

View File

@ -28,6 +28,7 @@ Base configuration settings.
* ``include`` - path to directory with configuration files overrides, string, required. * ``include`` - path to directory with configuration files overrides, string, required.
* ``database`` - path to SQLite database, string, required. * ``database`` - path to SQLite database, string, required.
* ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference. * ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference.
* ``suppress_http_log_errors`` - suppress http log errors, boolean, optional, default ``no``. If set to ``yes``, any http log errors (e.g. if web server is not available, but http logging is enabled) will be suppressed.
``alpm`` group ``alpm`` group
-------------- --------------
@ -49,6 +50,7 @@ Base authorization settings. ``OAuth`` provider requires ``aioauth-client`` libr
* ``allow_read_only`` - allow requesting status APIs without authorization, boolean, required. * ``allow_read_only`` - allow requesting status APIs without authorization, boolean, required.
* ``client_id`` - OAuth2 application client ID, string, required in case if ``oauth`` is used. * ``client_id`` - OAuth2 application client ID, string, required in case if ``oauth`` is used.
* ``client_secret`` - OAuth2 application client secret key, string, required in case if ``oauth`` is used. * ``client_secret`` - OAuth2 application client secret key, string, required in case if ``oauth`` is used.
* ``cookie_secret_key`` - secret key which will be used for cookies encryption, string, optional. It must be 32 url-safe base64-encoded bytes and can be generated as following ``base64.urlsafe_b64encode(os.urandom(32)).decode("utf8")``. If not set, it will be generated automatically; note, however, that in this case, all sessions will be automatically expired during restart.
* ``max_age`` - parameter which controls both cookie expiration and token expiration inside the service, integer, optional, default is 7 days. * ``max_age`` - parameter which controls both cookie expiration and token expiration inside the service, integer, optional, default is 7 days.
* ``oauth_provider`` - OAuth2 provider class name as is in ``aioauth-client`` (e.g. ``GoogleClient``, ``GithubClient`` etc), string, required in case if ``oauth`` is used. * ``oauth_provider`` - OAuth2 provider class name as is in ``aioauth-client`` (e.g. ``GoogleClient``, ``GithubClient`` etc), string, required in case if ``oauth`` is used.
* ``oauth_scopes`` - scopes list for OAuth2 provider, which will allow retrieving user email (which is used for checking user permissions), e.g. ``https://www.googleapis.com/auth/userinfo.email`` for ``GoogleClient`` or ``user:email`` for ``GithubClient``, space separated list of strings, required in case if ``oauth`` is used. * ``oauth_scopes`` - scopes list for OAuth2 provider, which will allow retrieving user email (which is used for checking user permissions), e.g. ``https://www.googleapis.com/auth/userinfo.email`` for ``GoogleClient`` or ``user:email`` for ``GithubClient``, space separated list of strings, required in case if ``oauth`` is used.
@ -67,7 +69,7 @@ Build related configuration. Group name can refer to architecture, e.g. ``build:
* ``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.
* ``triggers`` - list of ``ahriman.core.triggers.Trigger`` class implementation (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of mention. * ``triggers`` - list of ``ahriman.core.triggers.Trigger`` class implementation (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of mention.
* ``vcs_allowed_age`` - maximal age in seconds of the VCS packages before their version will be updated with its remote source, int, optional, default ``0``. * ``vcs_allowed_age`` - maximal age in seconds of the VCS packages before their version will be updated with its remote source, int, optional, default ``604800``.
``repository`` group ``repository`` group
-------------------- --------------------

View File

@ -878,6 +878,11 @@ How to enable OAuth authorization
#. #.
Restart web service ``systemctl restart ahriman-web@x86_64``. Restart web service ``systemctl restart ahriman-web@x86_64``.
How to implement own interface
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
You can write your own interface by using API which is provided by the web service. Full autogenerated API documentation is available at ``http://localhost:8080/api-docs``.
Backup and restore Backup and restore
------------------ ------------------

View File

@ -88,4 +88,6 @@ Initial setup
.. code-block:: shell .. code-block:: shell
sudo -u ahriman ahriman -a x86_64 package-add ahriman --now sudo -u ahriman ahriman -a x86_64 package-add ahriman --now --refresh
The ``--refresh`` flag is required in order to handle local database update.

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev # Maintainer: Evgeniy Alekseev
pkgname='ahriman' pkgname='ahriman'
pkgver=2.6.1 pkgver=2.8.0
pkgrel=1 pkgrel=1
pkgdesc="ArcH linux ReposItory MANager" pkgdesc="ArcH linux ReposItory MANager"
arch=('any') arch=('any')
@ -14,6 +14,8 @@ optdepends=('breezy: -bzr packages support'
'mercurial: -hg packages support' 'mercurial: -hg packages support'
'python-aioauth-client: web server with OAuth2 authorization' 'python-aioauth-client: web server with OAuth2 authorization'
'python-aiohttp: web server' 'python-aiohttp: web server'
'python-aiohttp-apispec>=3.0.0: web server'
'python-aiohttp-cors: web server'
'python-aiohttp-debugtoolbar: web server with enabled debug panel' 'python-aiohttp-debugtoolbar: web server with enabled debug panel'
'python-aiohttp-jinja2: web server' 'python-aiohttp-jinja2: web server'
'python-aiohttp-security: web server with authorization' 'python-aiohttp-security: web server with authorization'

View File

@ -2,6 +2,7 @@
include = ahriman.ini.d include = ahriman.ini.d
logging = ahriman.ini.d/logging.ini logging = ahriman.ini.d/logging.ini
database = /var/lib/ahriman/ahriman.db database = /var/lib/ahriman/ahriman.db
suppress_http_log_errors = yes
[alpm] [alpm]
database = /var/lib/pacman database = /var/lib/pacman

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ahriman API</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Embed elements Elements via Web Component -->
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css" type="text/css">
<link rel="shortcut icon" href="/static/favicon.ico">
</head>
<body>
<elements-api
apiDescriptionUrl="/api-docs/swagger.json"
router="hash"
layout="sidebar"
/>
</body>
</html>

View File

@ -68,6 +68,7 @@ setup(
]), ]),
# templates # templates
("share/ahriman/templates", [ ("share/ahriman/templates", [
"package/share/ahriman/templates/api.jinja2",
"package/share/ahriman/templates/build-status.jinja2", "package/share/ahriman/templates/build-status.jinja2",
"package/share/ahriman/templates/email-index.jinja2", "package/share/ahriman/templates/email-index.jinja2",
"package/share/ahriman/templates/error.jinja2", "package/share/ahriman/templates/error.jinja2",
@ -140,9 +141,11 @@ setup(
], ],
"web": [ "web": [
"Jinja2", "Jinja2",
"aiohttp",
"aiohttp_jinja2",
"aioauth-client", "aioauth-client",
"aiohttp",
"aiohttp-apispec",
"aiohttp_cors",
"aiohttp_jinja2",
"aiohttp_debugtoolbar", "aiohttp_debugtoolbar",
"aiohttp_session", "aiohttp_session",
"aiohttp_security", "aiohttp_security",

View File

@ -245,6 +245,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("--dependencies", help="process missing package dependencies",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true") parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-n", "--now", help="run update function after", action="store_true") parser.add_argument("-n", "--now", help="run update function after", action="store_true")
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, " parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
@ -252,7 +254,6 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
action="count", default=False) action="count", default=False)
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command", parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto) type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
parser.set_defaults(handler=handlers.Add) parser.set_defaults(handler=handlers.Add)
return parser return parser
@ -472,7 +473,7 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, " parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
"-yy to force refresh even if up to date", "-yy to force refresh even if up to date",
action="count", default=False) action="count", default=False)
parser.set_defaults(handler=handlers.Update, dry_run=True, aur=True, local=True, manual=False) parser.set_defaults(handler=handlers.Update, dependencies=False, dry_run=True, aur=True, local=True, manual=False)
return parser return parser
@ -492,6 +493,8 @@ def _set_repo_daemon_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-i", "--interval", help="interval between runs in seconds", type=int, default=60 * 60 * 12) parser.add_argument("-i", "--interval", help="interval between runs in seconds", type=int, default=60 * 60 * 12)
parser.add_argument("--aur", help="enable or disable checking for AUR updates", parser.add_argument("--aur", help="enable or disable checking for AUR updates",
action=argparse.BooleanOptionalAction, default=True) action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--dependencies", help="process missing package dependencies",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--local", help="enable or disable checking of local packages for updates", parser.add_argument("--local", help="enable or disable checking of local packages for updates",
action=argparse.BooleanOptionalAction, default=True) action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--manual", help="include or exclude manual updates", parser.add_argument("--manual", help="include or exclude manual updates",
@ -691,10 +694,12 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="check for packages updates and run build process if requested", description="check for packages updates and run build process if requested",
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("--dry-run", help="just perform check for updates, same as check command", action="store_true")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("--aur", help="enable or disable checking for AUR updates", parser.add_argument("--aur", help="enable or disable checking for AUR updates",
action=argparse.BooleanOptionalAction, default=True) action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--dependencies", help="process missing package dependencies",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_true")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("--local", help="enable or disable checking of local packages for updates", parser.add_argument("--local", help="enable or disable checking of local packages for updates",
action=argparse.BooleanOptionalAction, default=True) action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--manual", help="include or exclude manual updates", parser.add_argument("--manual", help="include or exclude manual updates",
@ -750,6 +755,8 @@ def _set_service_config_parser(root: SubParserAction) -> argparse.ArgumentParser
parser = root.add_parser("service-config", aliases=["config", "repo-config"], help="dump configuration", parser = root.add_parser("service-config", aliases=["config", "repo-config"], help="dump configuration",
description="dump configuration for the specified architecture", description="dump configuration for the specified architecture",
formatter_class=_formatter) formatter_class=_formatter)
parser.add_argument("--secure", help="hide passwords and secrets from output",
action=argparse.BooleanOptionalAction, default=True)
parser.set_defaults(handler=handlers.Dump, lock=None, report=False, quiet=True, unsafe=True) parser.set_defaults(handler=handlers.Dump, lock=None, report=False, quiet=True, unsafe=True)
return parser return parser

View File

@ -17,10 +17,11 @@
# 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 Set from typing import Iterable, List, Set
from ahriman.application.application.application_packages import ApplicationPackages from ahriman.application.application.application_packages import ApplicationPackages
from ahriman.application.application.application_repository import ApplicationRepository from ahriman.application.application.application_repository import ApplicationRepository
from ahriman.models.package import Package
from ahriman.models.result import Result from ahriman.models.result import Result
@ -87,3 +88,39 @@ class Application(ApplicationPackages, ApplicationRepository):
directly as it will be called after on_start action directly as it will be called after on_start action
""" """
self.repository.triggers.on_stop() self.repository.triggers.on_stop()
def with_dependencies(self, packages: List[Package], *, process_dependencies: bool) -> List[Package]:
"""
add missing dependencies to list of packages
Args:
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
"""
def missing_dependencies(source: Iterable[Package]) -> Set[str]:
# build initial list of dependencies
result = set()
for package in source:
result.update(package.depends_build)
# remove ones which are already well-known
result = result.difference(known_packages)
# remove ones which are in this list already
for package in source:
result = result.difference(package.packages_full)
return result
if not process_dependencies or not packages:
return packages
known_packages = self._known_packages()
with_dependencies = {package.base: package for package in packages}
while missing := missing_dependencies(with_dependencies.values()):
for package_name in missing:
package = Package.from_aur(package_name, self.repository.pacman)
with_dependencies[package.base] = package
return list(with_dependencies.values())

View File

@ -21,8 +21,7 @@ import requests
import shutil import shutil
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from typing import Any, Iterable
from typing import Any, Iterable, Set
from ahriman.application.application.application_properties import ApplicationProperties from ahriman.application.application.application_properties import ApplicationProperties
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
@ -48,24 +47,18 @@ class ApplicationPackages(ApplicationProperties):
dst = self.repository.paths.packages / local_path.name dst = self.repository.paths.packages / local_path.name
shutil.copy(local_path, dst) shutil.copy(local_path, dst)
def _add_aur(self, source: str, known_packages: Set[str], without_dependencies: bool) -> None: def _add_aur(self, source: str) -> None:
""" """
add package from AUR add package from AUR
Args: Args:
source(str): package base name source(str): package base name
known_packages(Set[str]): list of packages which are known by the service
without_dependencies(bool): if set, dependency check will be disabled
""" """
package = Package.from_aur(source, self.repository.pacman) package = Package.from_aur(source, self.repository.pacman)
self.database.build_queue_insert(package) self.database.build_queue_insert(package)
self.database.remote_update(package) self.database.remote_update(package)
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (local_dir := Path(dir_name)):
Sources.load(local_dir, package, self.database.patches_get(package.base), self.repository.paths)
self._process_dependencies(local_dir, known_packages, without_dependencies)
def _add_directory(self, source: str, *_: Any) -> None: def _add_directory(self, source: str, *_: Any) -> None:
""" """
add packages from directory add packages from directory
@ -77,25 +70,21 @@ class ApplicationPackages(ApplicationProperties):
for full_path in filter(package_like, local_dir.iterdir()): for full_path in filter(package_like, local_dir.iterdir()):
self._add_archive(str(full_path)) self._add_archive(str(full_path))
def _add_local(self, source: str, known_packages: Set[str], without_dependencies: bool) -> None: def _add_local(self, source: str) -> None:
""" """
add package from local PKGBUILDs add package from local PKGBUILDs
Args: Args:
source(str): path to directory with local source files source(str): path to directory with local source files
known_packages(Set[str]): list of packages which are known by the service
without_dependencies(bool): if set, dependency check will be disabled
""" """
source_dir = Path(source) source_dir = Path(source)
package = Package.from_build(source_dir) package = Package.from_build(source_dir, self.architecture)
cache_dir = self.repository.paths.cache_for(package.base) cache_dir = self.repository.paths.cache_for(package.base)
shutil.copytree(source_dir, cache_dir) # copy package to store in caches shutil.copytree(source_dir, cache_dir) # copy package to store in caches
Sources.init(cache_dir) # we need to run init command in directory where we do have permissions Sources.init(cache_dir) # we need to run init command in directory where we do have permissions
self.database.build_queue_insert(package) self.database.build_queue_insert(package)
self._process_dependencies(cache_dir, known_packages, without_dependencies)
def _add_remote(self, source: str, *_: Any) -> None: def _add_remote(self, source: str, *_: Any) -> None:
""" """
add package from remote sources (e.g. HTTP) add package from remote sources (e.g. HTTP)
@ -104,7 +93,8 @@ class ApplicationPackages(ApplicationProperties):
source(str): remote URL of the package archive source(str): remote URL of the package archive
""" """
dst = self.repository.paths.packages / Path(source).name # URL is path, is not it? dst = self.repository.paths.packages / Path(source).name # URL is path, is not it?
response = requests.get(source, stream=True, timeout=None) # timeout=None to suppress pylint warns # timeout=None to suppress pylint warns. Also suppress bandit warnings
response = requests.get(source, stream=True, timeout=None) # nosec
response.raise_for_status() response.raise_for_status()
with dst.open("wb") as local_file: with dst.open("wb") as local_file:
@ -121,50 +111,19 @@ class ApplicationPackages(ApplicationProperties):
package = Package.from_official(source, self.repository.pacman) package = Package.from_official(source, self.repository.pacman)
self.database.build_queue_insert(package) self.database.build_queue_insert(package)
self.database.remote_update(package) self.database.remote_update(package)
# repository packages must not depend on unknown packages, thus we are not going to process dependencies
def _known_packages(self) -> Set[str]: def add(self, names: Iterable[str], source: PackageSource) -> None:
"""
load packages from repository and pacman repositories
Returns:
Set[str]: list of known packages
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def _process_dependencies(self, local_dir: Path, known_packages: Set[str], without_dependencies: bool) -> None:
"""
process package dependencies
Args:
local_dir(Path): path to local package sources (i.e. cloned AUR repository)
known_packages(Set[str]): list of packages which are known by the service
without_dependencies(bool): if set, dependency check will be disabled
"""
if without_dependencies:
return
dependencies = Package.dependencies(local_dir)
self.add(dependencies.difference(known_packages), PackageSource.AUR, without_dependencies)
def add(self, names: Iterable[str], source: PackageSource, without_dependencies: bool) -> None:
""" """
add packages for the next build add packages for the next build
Args: Args:
names(Iterable[str]): list of package bases to add names(Iterable[str]): list of package bases to add
source(PackageSource): package source to add source(PackageSource): package source to add
without_dependencies(bool): if set, dependency check will be disabled
""" """
known_packages = self._known_packages() # speedup dependencies processing
for name in names: for name in names:
resolved_source = source.resolve(name) resolved_source = source.resolve(name)
fn = getattr(self, f"_add_{resolved_source.value}") fn = getattr(self, f"_add_{resolved_source.value}")
fn(name, known_packages, without_dependencies) fn(name)
def on_result(self, result: Result) -> None: def on_result(self, result: Result) -> None:
""" """

View File

@ -111,7 +111,7 @@ class ApplicationRepository(ApplicationProperties):
def unknown_local(probe: Package) -> List[str]: def unknown_local(probe: Package) -> List[str]:
cache_dir = self.repository.paths.cache_for(probe.base) cache_dir = self.repository.paths.cache_for(probe.base)
local = Package.from_build(cache_dir) local = Package.from_build(cache_dir, self.architecture)
packages = set(probe.packages.keys()).difference(local.packages.keys()) packages = set(probe.packages.keys()).difference(local.packages.keys())
return list(packages) return list(packages)
@ -145,7 +145,7 @@ class ApplicationRepository(ApplicationProperties):
process_update(packages, build_result) process_update(packages, build_result)
# process manual packages # process manual packages
tree = Tree.resolve(updates, self.repository.paths, self.database) tree = Tree.resolve(updates)
for num, level in enumerate(tree): for num, level in enumerate(tree):
self.logger.info("processing level #%i %s", num, [package.base for package in level]) self.logger.info("processing level #%i %s", num, [package.base for package in level])
build_result = self.repository.process_build(level) build_result = self.repository.process_build(level)
@ -183,7 +183,7 @@ class ApplicationRepository(ApplicationProperties):
updated_packages = [package for _, package in sorted(updates.items())] updated_packages = [package for _, package in sorted(updates.items())]
# reorder updates according to the dependency tree # reorder updates according to the dependency tree
tree = Tree.resolve(updated_packages, self.repository.paths, self.database) tree = Tree.resolve(updated_packages)
for level in tree: for level in tree:
for package in level: for package in level:
UpdatePrinter(package, local_versions.get(package.base)).print( UpdatePrinter(package, local_versions.get(package.base)).print(

View File

@ -47,11 +47,12 @@ class Add(Handler):
application = Application(architecture, configuration, application = Application(architecture, configuration,
report=report, unsafe=unsafe, refresh_pacman_database=args.refresh) report=report, unsafe=unsafe, refresh_pacman_database=args.refresh)
application.on_start() application.on_start()
application.add(args.package, args.source, args.without_dependencies) application.add(args.package, args.source)
if not args.now: if not args.now:
return return
packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False, packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False,
log_fn=application.logger.info) log_fn=application.logger.info)
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
result = application.update(packages) result = application.update(packages)
Add.check_if_empty(args.exit_code, result.is_empty) Add.check_if_empty(args.exit_code, result.is_empty)

View File

@ -48,4 +48,4 @@ class Dump(Handler):
""" """
dump = configuration.dump() dump = configuration.dump()
for section, values in sorted(dump.items()): for section, values in sorted(dump.items()):
ConfigurationPrinter(section, values).print(verbose=False, separator=" = ") ConfigurationPrinter(section, values).print(verbose=not args.secure, separator=" = ")

View File

@ -58,7 +58,7 @@ class Patch(Handler):
patch = Patch.patch_create_from_function(args.variable, args.patch) patch = Patch.patch_create_from_function(args.variable, args.patch)
Patch.patch_set_create(application, args.package, patch) Patch.patch_set_create(application, args.package, patch)
elif args.action == Action.Update and args.variable is None: elif args.action == Action.Update and args.variable is None:
package_base, patch = Patch.patch_create_from_diff(args.package, args.track) package_base, patch = Patch.patch_create_from_diff(args.package, architecture, args.track)
Patch.patch_set_create(application, package_base, patch) Patch.patch_set_create(application, package_base, patch)
elif args.action == Action.List: elif args.action == Action.List:
Patch.patch_set_list(application, args.package, args.variable, args.exit_code) Patch.patch_set_list(application, args.package, args.variable, args.exit_code)
@ -66,19 +66,20 @@ class Patch(Handler):
Patch.patch_set_remove(application, args.package, args.variable) Patch.patch_set_remove(application, args.package, args.variable)
@staticmethod @staticmethod
def patch_create_from_diff(sources_dir: Path, track: List[str]) -> Tuple[str, PkgbuildPatch]: def patch_create_from_diff(sources_dir: Path, architecture: str, track: List[str]) -> Tuple[str, PkgbuildPatch]:
""" """
create PKGBUILD plain diff patches from sources directory create PKGBUILD plain diff patches from sources directory
Args: Args:
sources_dir(Path): path to directory with the package sources sources_dir(Path): path to directory with the package sources
architecture(str): repository architecture
track(List[str]): track files which match the glob before creating the patch track(List[str]): track files which match the glob before creating the patch
Returns: Returns:
Tuple[str, PkgbuildPatch]: package base and created PKGBUILD patch based on the diff from master HEAD Tuple[str, PkgbuildPatch]: package base and created PKGBUILD patch based on the diff from master HEAD
to current files to current files
""" """
package = Package.from_build(sources_dir) package = Package.from_build(sources_dir, architecture)
patch = Sources.patch_create(sources_dir, *track) patch = Sources.patch_create(sources_dir, *track)
return package.base, PkgbuildPatch(None, patch) return package.base, PkgbuildPatch(None, patch)

View File

@ -51,7 +51,7 @@ class RemoveUnknown(Handler):
if args.dry_run: if args.dry_run:
for package in sorted(unknown_packages): for package in sorted(unknown_packages):
StringPrinter(package).print(False) StringPrinter(package).print(verbose=False)
return return
application.remove(unknown_packages) application.remove(unknown_packages)

View File

@ -40,7 +40,7 @@ class Search(Handler):
""" """
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
SORT_FIELDS = {field.name for field in fields(AURPackage) if field.default_factory is not list} SORT_FIELDS = {field.name for field in fields(AURPackage) if field.default_factory is not list} # type: ignore
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *,
@ -64,7 +64,7 @@ class Search(Handler):
for packages_list in (official_packages_list, aur_packages_list): for packages_list in (official_packages_list, aur_packages_list):
# keep sorting by packages source # keep sorting by packages source
for package in Search.sort(packages_list, args.sort_by): for package in Search.sort(packages_list, args.sort_by):
AurPrinter(package).print(args.info) AurPrinter(package).print(verbose=args.info)
@staticmethod @staticmethod
def sort(packages: Iterable[AURPackage], sort_by: str) -> List[AURPackage]: def sort(packages: Iterable[AURPackage], sort_by: str) -> List[AURPackage]:

View File

@ -53,7 +53,7 @@ class Status(Handler):
client = Application(architecture, configuration, report=True, unsafe=unsafe).repository.reporter client = Application(architecture, configuration, report=True, unsafe=unsafe).repository.reporter
if args.ahriman: if args.ahriman:
service_status = client.get_internal() service_status = client.get_internal()
StatusPrinter(service_status.status).print(args.info) StatusPrinter(service_status.status).print(verbose=args.info)
if args.package: if args.package:
packages: Iterable[Tuple[Package, BuildStatus]] = sum( packages: Iterable[Tuple[Package, BuildStatus]] = sum(
(client.get(base) for base in args.package), (client.get(base) for base in args.package),
@ -67,4 +67,4 @@ class Status(Handler):
filter_fn: Callable[[Tuple[Package, BuildStatus]], bool] =\ filter_fn: Callable[[Tuple[Package, BuildStatus]], bool] =\
lambda item: args.status is None or item[1].status == args.status lambda item: args.status is None or item[1].status == args.status
for package, package_status in sorted(filter(filter_fn, packages), key=comparator): for package, package_status in sorted(filter(filter_fn, packages), key=comparator):
PackagePrinter(package, package_status).print(args.info) PackagePrinter(package, package_status).print(verbose=args.info)

View File

@ -51,6 +51,6 @@ class Structure(Handler):
application = Application(architecture, configuration, report=report, unsafe=unsafe) application = Application(architecture, configuration, report=report, unsafe=unsafe)
packages = application.repository.packages() packages = application.repository.packages()
tree = Tree.resolve(packages, application.repository.paths, application.database) tree = Tree.resolve(packages)
for num, level in enumerate(tree): for num, level in enumerate(tree):
TreePrinter(num, level).print(verbose=True, separator=" ") TreePrinter(num, level).print(verbose=True, separator=" ")

View File

@ -53,6 +53,7 @@ class Update(Handler):
if args.dry_run: if args.dry_run:
return return
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
result = application.update(packages) result = application.update(packages)
Update.check_if_empty(args.exit_code, result.is_empty) Update.check_if_empty(args.exit_code, result.is_empty)

View File

@ -52,7 +52,7 @@ class Validate(Handler):
unsafe(bool): if set no user check will be performed before path creation unsafe(bool): if set no user check will be performed before path creation
""" """
schema = Validate.schema(architecture, configuration) schema = Validate.schema(architecture, configuration)
validator = Validator(instance=configuration, schema=schema) validator = Validator(configuration=configuration, schema=schema)
if validator.validate(configuration.dump()): if validator.validate(configuration.dump()):
return # no errors found return # no errors found

View File

@ -55,3 +55,7 @@ class Web(Handler):
application = setup_service(architecture, configuration, spawner) application = setup_service(architecture, configuration, spawner)
run_server(application) run_server(application)
# terminate spawn process at the last
spawner.stop()
spawner.join()

View File

@ -68,7 +68,6 @@ class Lock(LazyLogging):
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
""" """
self.path = args.lock.with_stem(f"{args.lock.stem}_{architecture}") if args.lock is not None else None self.path = args.lock.with_stem(f"{args.lock.stem}_{architecture}") if args.lock is not None else None
print(self.path)
self.force = args.force self.force = args.force
self.unsafe = args.unsafe self.unsafe = args.unsafe

View File

@ -25,6 +25,7 @@ from typing import Any, Callable, Generator, Set
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
from ahriman.core.util import trim_package
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -175,8 +176,10 @@ class Pacman(LazyLogging):
result: Set[str] = set() result: Set[str] = set()
for database in self.handle.get_syncdbs(): for database in self.handle.get_syncdbs():
for package in database.pkgcache: for package in database.pkgcache:
result.add(package.name) # package itself # package itself
result.update(package.provides) # provides list for meta-packages result.add(package.name)
# provides list for meta-packages
result.update(trim_package(provides) for provides in package.provides)
return result return result

View File

@ -56,7 +56,7 @@ class Configuration(configparser.RawConfigParser):
architecture according to the merge rules. Moreover, the architecture names will be removed from section names. architecture according to the merge rules. Moreover, the architecture names will be removed from section names.
In order to get current settings, the ``check_loaded`` method can be used. This method will raise an In order to get current settings, the ``check_loaded`` method can be used. This method will raise an
``InitializeException`` in case if configuration was not yet loaded:: ``InitializeError`` in case if configuration was not yet loaded::
>>> path, architecture = configuration.check_loaded() >>> path, architecture = configuration.check_loaded()
""" """
@ -165,7 +165,7 @@ class Configuration(configparser.RawConfigParser):
Tuple[Path, str]: configuration root path and architecture if loaded Tuple[Path, str]: configuration root path and architecture if loaded
Raises: Raises:
InitializeException: in case if architecture and/or path are not set InitializeError: in case if architecture and/or path are not set
""" """
if self.path is None or self.architecture is None: if self.path is None or self.architecture is None:
raise InitializeError("Configuration path and/or architecture are not set") raise InitializeError("Configuration path and/or architecture are not set")
@ -213,9 +213,9 @@ class Configuration(configparser.RawConfigParser):
if self.has_section(full_section): if self.has_section(full_section):
return full_section, section return full_section, section
# okay lets just use section as type # okay lets just use section as type
if not self.has_section(section): if self.has_section(section):
raise configparser.NoSectionError(section)
return section, section return section, section
raise configparser.NoSectionError(section)
def load(self, path: Path) -> None: def load(self, path: Path) -> None:
""" """

View File

@ -47,6 +47,10 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"required": True, "required": True,
"path_exists": True, "path_exists": True,
}, },
"suppress_http_log_errors": {
"type": "boolean",
"coerce": "boolean",
}
}, },
}, },
"alpm": { "alpm": {
@ -60,6 +64,7 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"mirror": { "mirror": {
"type": "string", "type": "string",
"required": True, "required": True,
"is_url": [],
}, },
"repositories": { "repositories": {
"type": "list", "type": "list",
@ -105,9 +110,15 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"client_secret": { "client_secret": {
"type": "string", "type": "string",
}, },
"cookie_secret_key": {
"type": "string",
"minlength": 32,
"maxlength": 64, # we cannot verify maxlength, because base64 representation might be longer than bytes
},
"max_age": { "max_age": {
"type": "integer", "type": "integer",
"coerce": "integer", "coerce": "integer",
"min": 0,
}, },
"oauth_provider": { "oauth_provider": {
"type": "string", "type": "string",
@ -155,6 +166,7 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"vcs_allowed_age": { "vcs_allowed_age": {
"type": "integer", "type": "integer",
"coerce": "integer", "coerce": "integer",
"min": 0,
}, },
}, },
}, },
@ -197,6 +209,7 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"schema": { "schema": {
"address": { "address": {
"type": "string", "type": "string",
"is_url": ["http", "https"],
}, },
"debug": { "debug": {
"type": "boolean", "type": "boolean",
@ -213,9 +226,11 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
}, },
"host": { "host": {
"type": "string", "type": "string",
"is_ip_address": ["localhost"],
}, },
"index_url": { "index_url": {
"type": "string", "type": "string",
"is_url": ["http", "https"],
}, },
"password": { "password": {
"type": "string", "type": "string",
@ -251,44 +266,4 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
}, },
}, },
}, },
"remote-pull": {
"type": "dict",
"schema": {
"target": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
},
},
"remote-push": {
"type": "dict",
"schema": {
"target": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
},
},
"report": {
"type": "dict",
"schema": {
"target": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
},
},
"upload": {
"type": "dict",
"schema": {
"target": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
},
},
} }

View File

@ -17,19 +17,22 @@
# 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/>.
# #
import ipaddress
from cerberus import TypeDefinition, Validator as RootValidator # type: ignore from cerberus import TypeDefinition, Validator as RootValidator # type: ignore
from pathlib import Path from pathlib import Path
from typing import Any, List from typing import Any, List
from urllib.parse import urlparse
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
class Validator(RootValidator): # type: ignore class Validator(RootValidator):
""" """
class which defines custom validation methods for the service configuration class which defines custom validation methods for the service configuration
Attributes: Attributes:
instance(Configuration): configuration instance configuration(Configuration): configuration instance
""" """
types_mapping = RootValidator.types_mapping.copy() types_mapping = RootValidator.types_mapping.copy()
@ -40,12 +43,12 @@ class Validator(RootValidator): # type: ignore
default constructor default constructor
Args: Args:
instance(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
**kwargs(): keyword arguments to be passed to base validator **kwargs(): keyword arguments to be passed to base validator
""" """
RootValidator.__init__(self, *args, **kwargs) RootValidator.__init__(self, *args, **kwargs)
self.instance: Configuration = kwargs["instance"] self.configuration: Configuration = kwargs["configuration"]
def _normalize_coerce_absolute_path(self, value: str) -> Path: def _normalize_coerce_absolute_path(self, value: str) -> Path:
""" """
@ -57,7 +60,7 @@ class Validator(RootValidator): # type: ignore
Returns: Returns:
Path: value converted to path instance according to configuration rules Path: value converted to path instance according to configuration rules
""" """
converted: Path = self.instance.converters["path"](value) converted: Path = self.configuration.converters["path"](value)
return converted return converted
def _normalize_coerce_boolean(self, value: str) -> bool: def _normalize_coerce_boolean(self, value: str) -> bool:
@ -71,7 +74,7 @@ class Validator(RootValidator): # type: ignore
bool: value converted to boolean according to configuration rules bool: value converted to boolean according to configuration rules
""" """
# pylint: disable=protected-access # pylint: disable=protected-access
converted: bool = self.instance._convert_to_boolean(value) # type: ignore converted: bool = self.configuration._convert_to_boolean(value) # type: ignore
return converted return converted
def _normalize_coerce_integer(self, value: str) -> int: def _normalize_coerce_integer(self, value: str) -> int:
@ -97,9 +100,50 @@ class Validator(RootValidator): # type: ignore
Returns: Returns:
List[str]: value converted to string list instance according to configuration rules List[str]: value converted to string list instance according to configuration rules
""" """
converted: List[str] = self.instance.converters["list"](value) converted: List[str] = self.configuration.converters["list"](value)
return converted return converted
def _validate_is_ip_address(self, constraint: List[str], field: str, value: str) -> None:
"""
check if the specified value is valid ip address
Args:
constraint(List[str]): optional list of allowed special words (e.g. ``localhost``)
field(str): field name to be checked
value(Path): value to be checked
Examples:
The rule's arguments are validated against this schema:
{"type": "list", "schema": {"type": "string"}}
"""
if value in constraint:
return
try:
ipaddress.ip_address(value)
except ValueError:
self._error(field, f"Value {value} must be valid IP address")
def _validate_is_url(self, constraint: List[str], field: str, value: str) -> None:
"""
check if the specified value is a valid url
Args:
constraint(List[str]): optional list of supported schemas. If empty, no schema validation will be performed
field(str): field name to be checked
value(str): value to be checked
Examples:
The rule's arguments are validated against this schema:
{"type": "list", "schema": {"type": "string"}}
"""
url = urlparse(value) # it probably will never rise exceptions on parse
if not url.scheme:
self._error(field, f"Url scheme is not set for {value}")
if not url.netloc and url.scheme not in ("file",):
self._error(field, f"Location must be set for url {value} of scheme {url.scheme}")
if constraint and url.scheme not in constraint:
self._error(field, f"Url {value} scheme must be one of {constraint}")
def _validate_path_exists(self, constraint: bool, field: str, value: Path) -> None: def _validate_path_exists(self, constraint: bool, field: str, value: Path) -> None:
""" """
check if paths exists check if paths exists

View File

@ -1,47 +0,0 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from ahriman.core.configuration import Configuration
from ahriman.core.database.data.package_remotes import migrate_package_remotes
from ahriman.core.database.data.package_statuses import migrate_package_statuses
from ahriman.core.database.data.patches import migrate_patches
from ahriman.core.database.data.users import migrate_users_data
from ahriman.models.migration_result import MigrationResult
def migrate_data(result: MigrationResult, connection: Connection, configuration: Configuration) -> None:
"""
perform data migration
Args:
result(MigrationResult): result of the schema migration
connection(Connection): database connection
configuration(Configuration): configuration instance
"""
# initial data migration
repository_paths = configuration.repository_paths
if result.old_version <= 0:
migrate_package_statuses(connection, repository_paths)
migrate_patches(connection, repository_paths)
migrate_users_data(connection, configuration)
if result.old_version <= 1:
migrate_package_remotes(connection, repository_paths)

View File

@ -1,64 +0,0 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_paths import RepositoryPaths
__all__ = ["migrate_package_remotes"]
# pylint: disable=protected-access
def migrate_package_remotes(connection: Connection, paths: RepositoryPaths) -> None:
"""
perform migration for package remote sources
Args:
connection(Connection): database connection
paths(RepositoryPaths): repository paths instance
"""
from ahriman.core.database.operations import PackageOperations
def insert_remote(base: str, remote: RemoteSource) -> None:
connection.execute(
"""
update package_bases set
branch = :branch, git_url = :git_url, path = :path,
web_url = :web_url, source = :source
where package_base = :package_base
""",
dict(
package_base=base,
branch=remote.branch, git_url=remote.git_url, path=remote.path,
web_url=remote.web_url, source=remote.source
)
)
packages = PackageOperations._packages_get_select_package_bases(connection)
for package_base, package in packages.items():
local_cache = paths.cache_for(package_base)
if local_cache.exists() and not package.is_vcs:
continue # skip packages which are not VCS and with local cache
remote_source = RemoteSource.from_source(PackageSource.AUR, package_base, "aur")
if remote_source is None:
continue # should never happen
insert_remote(package_base, remote_source)

View File

@ -1,82 +0,0 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import json
from sqlite3 import Connection
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
__all__ = ["migrate_package_statuses"]
def migrate_package_statuses(connection: Connection, paths: RepositoryPaths) -> None:
"""
perform migration for package statuses
Args:
connection(Connection): database connection
paths(RepositoryPaths): repository paths instance
"""
def insert_base(metadata: Package, last_status: BuildStatus) -> None:
connection.execute(
"""
insert into package_bases
(package_base, version, aur_url)
values
(:package_base, :version, :aur_url)
""",
dict(package_base=metadata.base, version=metadata.version, aur_url=""))
connection.execute(
"""
insert into package_statuses
(package_base, status, last_updated)
values
(:package_base, :status, :last_updated)""",
dict(package_base=metadata.base, status=last_status.status.value, last_updated=last_status.timestamp))
def insert_packages(metadata: Package) -> None:
package_list = []
for name, description in metadata.packages.items():
package_list.append(dict(package=name, package_base=metadata.base, **description.view()))
connection.executemany(
"""
insert into packages
(package, package_base, architecture, archive_size, build_date, depends, description,
filename, "groups", installed_size, licenses, provides, url)
values
(:package, :package_base, :architecture, :archive_size, :build_date, :depends, :description,
:filename, :groups, :installed_size, :licenses, :provides, :url)
""",
package_list)
cache_path = paths.root / "status_cache.json"
if not cache_path.is_file():
return # no file found
with cache_path.open() as cache:
dump = json.load(cache)
for item in dump.get("packages", []):
package = Package.from_json(item["package"])
status = BuildStatus.from_json(item["status"])
insert_base(package, status)
insert_packages(package)

View File

@ -1,47 +0,0 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from ahriman.models.repository_paths import RepositoryPaths
__all__ = ["migrate_patches"]
def migrate_patches(connection: Connection, paths: RepositoryPaths) -> None:
"""
perform migration for patches
Args:
connection(Connection): database connection
paths(RepositoryPaths): repository paths instance
"""
root = paths.root / "patches"
if not root.is_dir():
return # no directory found
for package in root.iterdir():
patch_path = package / "00-main.patch"
if not patch_path.is_file():
continue # not exist
content = patch_path.read_text(encoding="utf8")
connection.execute(
"""insert into patches (package_base, patch) values (:package_base, :patch)""",
{"package_base": package.name, "patch": content})

View File

@ -17,16 +17,13 @@
# 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 __future__ import annotations
from importlib import import_module from importlib import import_module
from pathlib import Path from pathlib import Path
from pkgutil import iter_modules from pkgutil import iter_modules
from sqlite3 import Connection from sqlite3 import Connection, Cursor
from typing import List, Type from typing import Callable, List
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.data import migrate_data
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
from ahriman.models.migration import Migration from ahriman.models.migration import Migration
from ahriman.models.migration_result import MigrationResult from ahriman.models.migration_result import MigrationResult
@ -53,8 +50,8 @@ class Migrations(LazyLogging):
self.connection = connection self.connection = connection
self.configuration = configuration self.configuration = configuration
@classmethod @staticmethod
def migrate(cls: Type[Migrations], connection: Connection, configuration: Configuration) -> MigrationResult: def migrate(connection: Connection, configuration: Configuration) -> MigrationResult:
""" """
perform migrations implicitly perform migrations implicitly
@ -65,7 +62,26 @@ class Migrations(LazyLogging):
Returns: Returns:
MigrationResult: current schema version MigrationResult: current schema version
""" """
return cls(connection, configuration).run() return Migrations(connection, configuration).run()
def migration(self, cursor: Cursor, migration: Migration) -> None:
"""
perform single migration
Args:
cursor(Cursor): connection cursor
migration(Migration): single migration to perform
"""
self.logger.info("applying table migration %s at index %s", migration.name, migration.index)
for statement in migration.steps:
cursor.execute(statement)
self.logger.info("table migration %s at index %s has been applied", migration.name, migration.index)
self.logger.info("perform data migration %s at index %s", migration.name, migration.index)
migration.migrate_data(self.connection, self.configuration)
self.logger.info(
"data migration %s at index %s has been performed",
migration.name, migration.index)
def migrations(self) -> List[Migration]: def migrations(self) -> List[Migration]:
""" """
@ -81,9 +97,21 @@ class Migrations(LazyLogging):
for index, module_name in enumerate(sorted(modules)): for index, module_name in enumerate(sorted(modules)):
module = import_module(f"{__name__}.{module_name}") module = import_module(f"{__name__}.{module_name}")
steps: List[str] = getattr(module, "steps", []) steps: List[str] = getattr(module, "steps", [])
self.logger.debug("found migration %s at index %s with steps count %s", module_name, index, len(steps)) self.logger.debug("found migration %s at index %s with steps count %s", module_name, index, len(steps))
migrations.append(Migration(index=index, name=module_name, steps=steps))
migrate_data: Callable[[Connection, Configuration], None] = \
getattr(module, "migrate_data", lambda *args: None)
migrations.append(
Migration(
index=index,
name=module_name,
steps=steps,
migrate_data=migrate_data
)
)
return migrations return migrations
@ -110,13 +138,7 @@ class Migrations(LazyLogging):
try: try:
cursor.execute("begin exclusive") cursor.execute("begin exclusive")
for migration in migrations[current_version:]: for migration in migrations[current_version:]:
self.logger.info("applying migration %s at index %s", migration.name, migration.index) self.migration(cursor, migration)
for statement in migration.steps:
cursor.execute(statement)
self.logger.info("migration %s at index %s has been applied", migration.name, migration.index)
migrate_data(result, self.connection, self.configuration)
cursor.execute(f"pragma user_version = {expected_version}") # no support for ? placeholders cursor.execute(f"pragma user_version = {expected_version}") # no support for ? placeholders
except Exception: except Exception:
self.logger.exception("migration failed with exception") self.logger.exception("migration failed with exception")

View File

@ -17,7 +17,17 @@
# 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/>.
# #
__all__ = ["steps"] import json
from sqlite3 import Connection
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
__all__ = ["migrate_data", "steps"]
steps = [ steps = [
@ -73,3 +83,109 @@ steps = [
) )
""", """,
] ]
def migrate_data(connection: Connection, configuration: Configuration) -> None:
"""
perform data migration
Args:
connection(Connection): database connection
configuration(Configuration): configuration instance
"""
migrate_package_statuses(connection, configuration.repository_paths)
migrate_patches(connection, configuration.repository_paths)
migrate_users_data(connection, configuration)
def migrate_package_statuses(connection: Connection, paths: RepositoryPaths) -> None:
"""
perform migration for package statuses
Args:
connection(Connection): database connection
paths(RepositoryPaths): repository paths instance
"""
def insert_base(metadata: Package, last_status: BuildStatus) -> None:
connection.execute(
"""
insert into package_bases
(package_base, version, aur_url)
values
(:package_base, :version, :aur_url)
""",
{"package_base": metadata.base, "version": metadata.version, "aur_url": ""})
connection.execute(
"""
insert into package_statuses
(package_base, status, last_updated)
values
(:package_base, :status, :last_updated)""",
{"package_base": metadata.base, "status": last_status.status.value, "last_updated": last_status.timestamp})
def insert_packages(metadata: Package) -> None:
package_list = []
for name, description in metadata.packages.items():
package_list.append({"package": name, "package_base": metadata.base, **description.view()})
connection.executemany(
"""
insert into packages
(package, package_base, architecture, archive_size, build_date, depends, description,
filename, "groups", installed_size, licenses, provides, url)
values
(:package, :package_base, :architecture, :archive_size, :build_date, :depends, :description,
:filename, :groups, :installed_size, :licenses, :provides, :url)
""",
package_list)
cache_path = paths.root / "status_cache.json"
if not cache_path.is_file():
return # no file found
with cache_path.open() as cache:
dump = json.load(cache)
for item in dump.get("packages", []):
package = Package.from_json(item["package"])
status = BuildStatus.from_json(item["status"])
insert_base(package, status)
insert_packages(package)
def migrate_patches(connection: Connection, paths: RepositoryPaths) -> None:
"""
perform migration for patches
Args:
connection(Connection): database connection
paths(RepositoryPaths): repository paths instance
"""
root = paths.root / "patches"
if not root.is_dir():
return # no directory found
for package in root.iterdir():
patch_path = package / "00-main.patch"
if not patch_path.is_file():
continue # not exist
content = patch_path.read_text(encoding="utf8")
connection.execute(
"""insert into patches (package_base, patch) values (:package_base, :patch)""",
{"package_base": package.name, "patch": content})
def migrate_users_data(connection: Connection, configuration: Configuration) -> None:
"""
perform migration for users
Args:
connection(Connection): database connection
configuration(Configuration): configuration instance
"""
for section in configuration.sections():
for option, value in configuration[section].items():
if not section.startswith("auth:"):
continue
access = section[5:]
connection.execute(
"""insert into users (username, access, password) values (:username, :access, :password)""",
{"username": option.lower(), "access": access, "password": value})

View File

@ -17,7 +17,15 @@
# 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/>.
# #
__all__ = ["steps"] from sqlite3 import Connection
from ahriman.core.configuration import Configuration
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_paths import RepositoryPaths
__all__ = ["migrate_data", "steps"]
steps = [ steps = [
@ -40,3 +48,51 @@ steps = [
alter table package_bases drop column aur_url alter table package_bases drop column aur_url
""", """,
] ]
def migrate_data(connection: Connection, configuration: Configuration) -> None:
"""
perform data migration
Args:
connection(Connection): database connection
configuration(Configuration): configuration instance
"""
migrate_package_remotes(connection, configuration.repository_paths)
# pylint: disable=protected-access
def migrate_package_remotes(connection: Connection, paths: RepositoryPaths) -> None:
"""
perform migration for package remote sources
Args:
connection(Connection): database connection
paths(RepositoryPaths): repository paths instance
"""
from ahriman.core.database.operations import PackageOperations
def insert_remote(base: str, remote: RemoteSource) -> None:
connection.execute(
"""
update package_bases set
branch = :branch, git_url = :git_url, path = :path,
web_url = :web_url, source = :source
where package_base = :package_base
""",
{
"package_base": base,
"branch": remote.branch, "git_url": remote.git_url, "path": remote.path,
"web_url": remote.web_url, "source": remote.source
}
)
packages = PackageOperations._packages_get_select_package_bases(connection)
for package_base, package in packages.items():
local_cache = paths.cache_for(package_base)
if local_cache.exists() and not package.is_vcs:
continue # skip packages which are not VCS and with local cache
remote_source = RemoteSource.from_source(PackageSource.AUR, package_base, "aur")
if remote_source is None:
continue # should never happen
insert_remote(package_base, remote_source)

View File

@ -0,0 +1,83 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from sqlite3 import Connection
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration
from ahriman.core.util import package_like
from ahriman.models.package import Package
__all__ = ["migrate_data", "steps"]
steps = [
"""
alter table packages add column make_depends json
""",
"""
alter table packages add column opt_depends json
""",
]
def migrate_data(connection: Connection, configuration: Configuration) -> None:
"""
perform data migration
Args:
connection(Connection): database connection
configuration(Configuration): configuration instance
"""
migrate_package_depends(connection, configuration)
def migrate_package_depends(connection: Connection, configuration: Configuration) -> None:
"""
migrate package opt and make depends fields
Args:
connection(Connection): database connection
configuration(Configuration): configuration instance
"""
if not configuration.repository_paths.repository.is_dir():
return
_, architecture = configuration.check_loaded()
pacman = Pacman(architecture, configuration, refresh_database=False)
package_list = []
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
base = Package.from_archive(full_path, pacman, remote=None)
for package, description in base.packages.items():
package_list.append({
"make_depends": description.make_depends,
"opt_depends": description.opt_depends,
"package": package,
})
connection.executemany(
"""
update packages set
make_depends = :make_depends, opt_depends = :opt_depends
where package = :package
""",
package_list
)

View File

@ -0,0 +1,53 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__all__ = ["steps"]
steps = [
"""
alter table packages rename to packages_
""",
"""
create table packages (
package text not null,
package_base text not null,
architecture text not null,
archive_size integer,
build_date integer,
depends json,
description text,
filename text,
"groups" json,
installed_size integer,
licenses json,
provides json,
url text,
make_depends json,
opt_depends json,
unique (package, architecture)
)
""",
"""
insert into packages select * from packages_ where architecture is not null;
""",
"""
drop table packages_;
""",
]

View File

@ -71,12 +71,12 @@ class LogsOperations(Operations):
values values
(:package_base, :process_id, :created, :record) (:package_base, :process_id, :created, :record)
""", """,
dict( {
package_base=log_record_id.package_base, "package_base": log_record_id.package_base,
process_id=log_record_id.process_id, "process_id": log_record_id.process_id,
created=created, "created": created,
record=record "record": record,
) }
) )
return self.with_connection(run, commit=True) return self.with_connection(run, commit=True)

View File

@ -20,7 +20,7 @@
import sqlite3 import sqlite3
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Tuple, TypeVar, Callable from typing import Any, Callable, Dict, Tuple, TypeVar
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging

View File

@ -82,15 +82,15 @@ class PackageOperations(Operations):
on conflict (package_base) do update set on conflict (package_base) do update set
version = :version, branch = :branch, git_url = :git_url, path = :path, web_url = :web_url, source = :source version = :version, branch = :branch, git_url = :git_url, path = :path, web_url = :web_url, source = :source
""", """,
dict( {
package_base=package.base, "package_base": package.base,
version=package.version, "version": package.version,
branch=package.remote.branch if package.remote is not None else None, "branch": package.remote.branch if package.remote is not None else None,
git_url=package.remote.git_url if package.remote is not None else None, "git_url": package.remote.git_url if package.remote is not None else None,
path=package.remote.path if package.remote is not None else None, "path": package.remote.path if package.remote is not None else None,
web_url=package.remote.web_url if package.remote is not None else None, "web_url": package.remote.web_url if package.remote is not None else None,
source=package.remote.source.value if package.remote is not None else None, "source": package.remote.source.value if package.remote is not None else None,
) }
) )
@staticmethod @staticmethod
@ -104,21 +104,26 @@ class PackageOperations(Operations):
""" """
package_list = [] package_list = []
for name, description in package.packages.items(): for name, description in package.packages.items():
package_list.append(dict(package=name, package_base=package.base, **description.view())) if description.architecture is None:
continue # architecture is required
package_list.append({"package": name, "package_base": package.base, **description.view()})
connection.executemany( connection.executemany(
""" """
insert into packages insert into packages
(package, package_base, architecture, archive_size, (package, package_base, architecture, archive_size,
build_date, depends, description, filename, build_date, depends, description, filename,
"groups", installed_size, licenses, provides, url) "groups", installed_size, licenses, provides,
url, make_depends, opt_depends)
values values
(:package, :package_base, :architecture, :archive_size, (:package, :package_base, :architecture, :archive_size,
:build_date, :depends, :description, :filename, :build_date, :depends, :description, :filename,
:groups, :installed_size, :licenses, :provides, :url) :groups, :installed_size, :licenses, :provides,
:url, :make_depends, :opt_depends)
on conflict (package, architecture) do update set on conflict (package, architecture) do update set
package_base = :package_base, archive_size = :archive_size, package_base = :package_base, archive_size = :archive_size,
build_date = :build_date, depends = :depends, description = :description, filename = :filename, build_date = :build_date, depends = :depends, description = :description, filename = :filename,
"groups" = :groups, installed_size = :installed_size, licenses = :licenses, provides = :provides, url = :url "groups" = :groups, installed_size = :installed_size, licenses = :licenses, provides = :provides,
url = :url, make_depends = :make_depends, opt_depends = :opt_depends
""", """,
package_list) package_list)
@ -140,7 +145,7 @@ class PackageOperations(Operations):
on conflict (package_base) do update set on conflict (package_base) do update set
status = :status, last_updated = :last_updated status = :status, last_updated = :last_updated
""", """,
dict(package_base=package_base, status=status.status.value, last_updated=status.timestamp)) {"package_base": package_base, "status": status.status.value, "last_updated": status.timestamp})
@staticmethod @staticmethod
def _packages_get_select_package_bases(connection: Connection) -> Dict[str, Package]: def _packages_get_select_package_bases(connection: Connection) -> Dict[str, Package]:

View File

@ -27,7 +27,7 @@ from ahriman.core.formatters.package_printer import PackagePrinter
from ahriman.core.formatters.patch_printer import PatchPrinter from ahriman.core.formatters.patch_printer import PatchPrinter
from ahriman.core.formatters.status_printer import StatusPrinter from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.core.formatters.tree_printer import TreePrinter from ahriman.core.formatters.tree_printer import TreePrinter
from ahriman.core.formatters.validation_printer import ValidationPrinter
from ahriman.core.formatters.update_printer import UpdatePrinter from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.core.formatters.user_printer import UserPrinter from ahriman.core.formatters.user_printer import UserPrinter
from ahriman.core.formatters.validation_printer import ValidationPrinter
from ahriman.core.formatters.version_printer import VersionPrinter from ahriman.core.formatters.version_printer import VersionPrinter

View File

@ -28,9 +28,20 @@ class ConfigurationPrinter(StringPrinter):
print content of the configuration section print content of the configuration section
Attributes: Attributes:
HIDE_KEYS(List[str]): (class attribute) hide values for mentioned keys. This list must be used in order to hide
passwords from outputs
values(Dict[str, str]): configuration values dictionary values(Dict[str, str]): configuration values dictionary
""" """
HIDE_KEYS = [
"api_key", # telegram key
"client_secret", # oauth secret
"cookie_secret_key", # cookie secret key
"password", # generic password (github, email, web server, etc)
"salt", # password default salt
"secret_key", # aws secret key
]
def __init__(self, section: str, values: Dict[str, str]) -> None: def __init__(self, section: str, values: Dict[str, str]) -> None:
""" """
default constructor default constructor
@ -50,6 +61,6 @@ class ConfigurationPrinter(StringPrinter):
List[Property]: list of content properties List[Property]: list of content properties
""" """
return [ return [
Property(key, value, is_required=True) Property(key, value, is_required=key not in self.HIDE_KEYS)
for key, value in sorted(self.values.items()) for key, value in sorted(self.values.items())
] ]

View File

@ -27,13 +27,13 @@ class Printer:
base class for formatters base class for formatters
""" """
def print(self, verbose: bool, log_fn: Callable[[str], None] = print, separator: str = ": ") -> None: def print(self, *, verbose: bool, log_fn: Callable[[str], None] = print, separator: str = ": ") -> None:
""" """
print content print content
Args: Args:
verbose(bool): print all fields verbose(bool): print all fields
log_fn(Callable[[str], None]): logger function to log data (Default value = print) log_fn(Callable[[str], None], optional): logger function to log data (Default value = print)
separator(str, optional): separator for property name and property value (Default value = ": ") separator(str, optional): separator for property name and property value (Default value = ": ")
""" """
if (title := self.title()) is not None: if (title := self.title()) is not None:

View File

@ -33,6 +33,16 @@ class RemotePullTrigger(Trigger):
""" """
CONFIGURATION_SCHEMA = { CONFIGURATION_SCHEMA = {
"remote-pull": {
"type": "dict",
"schema": {
"target": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
},
},
"gitremote": { "gitremote": {
"type": "dict", "type": "dict",
"schema": { "schema": {

View File

@ -82,7 +82,10 @@ class RemotePush(LazyLogging):
Sources.fetch(package_target_dir, package.remote) Sources.fetch(package_target_dir, package.remote)
# ...and last, but not least, we remove the dot-git directory... # ...and last, but not least, we remove the dot-git directory...
for git_file in package_target_dir.glob(".git*"): for git_file in package_target_dir.glob(".git*"):
shutil.rmtree(package_target_dir / git_file) if git_file.is_file():
git_file.unlink()
else:
shutil.rmtree(git_file)
# ...copy all patches... # ...copy all patches...
for patch in self.database.patches_get(package.base): for patch in self.database.patches_get(package.base):
filename = f"ahriman-{package.base}.patch" if patch.key is None else f"ahriman-{patch.key}.patch" filename = f"ahriman-{package.base}.patch" if patch.key is None else f"ahriman-{patch.key}.patch"

View File

@ -38,6 +38,16 @@ class RemotePushTrigger(Trigger):
""" """
CONFIGURATION_SCHEMA = { CONFIGURATION_SCHEMA = {
"remote-push": {
"type": "dict",
"schema": {
"target": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
},
},
"gitremote": { "gitremote": {
"type": "dict", "type": "dict",
"schema": { "schema": {

View File

@ -31,22 +31,25 @@ class HttpLogHandler(logging.Handler):
Attributes: Attributes:
reporter(Client): build status reporter instance reporter(Client): build status reporter instance
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
""" """
def __init__(self, configuration: Configuration, *, report: bool) -> None: def __init__(self, configuration: Configuration, *, report: bool, suppress_errors: bool) -> None:
""" """
default constructor default constructor
Args: Args:
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
report(bool): force enable or disable reporting report(bool): force enable or disable reporting
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
""" """
# we don't really care about those parameters because they will be handled by the reporter # we don't really care about those parameters because they will be handled by the reporter
logging.Handler.__init__(self) logging.Handler.__init__(self)
# client has to be importer here because of circular imports # client has to be imported here because of circular imports
from ahriman.core.status.client import Client from ahriman.core.status.client import Client
self.reporter = Client.load(configuration, report=report) self.reporter = Client.load(configuration, report=report)
self.suppress_errors = suppress_errors
@classmethod @classmethod
def load(cls, configuration: Configuration, *, report: bool) -> HttpLogHandler: def load(cls, configuration: Configuration, *, report: bool) -> HttpLogHandler:
@ -62,7 +65,8 @@ class HttpLogHandler(logging.Handler):
if (handler := next((handler for handler in root.handlers if isinstance(handler, cls)), None)) is not None: if (handler := next((handler for handler in root.handlers if isinstance(handler, cls)), None)) is not None:
return handler # there is already registered instance return handler # there is already registered instance
handler = cls(configuration, report=report) suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False)
handler = cls(configuration, report=report, suppress_errors=suppress_errors)
root.addHandler(handler) root.addHandler(handler)
return handler return handler
@ -81,4 +85,6 @@ class HttpLogHandler(logging.Handler):
try: try:
self.reporter.logs(package_base, record) self.reporter.logs(package_base, record)
except Exception: except Exception:
if self.suppress_errors:
return
self.handleError(record) self.handleError(record)

View File

@ -35,6 +35,16 @@ class ReportTrigger(Trigger):
""" """
CONFIGURATION_SCHEMA = { CONFIGURATION_SCHEMA = {
"report": {
"type": "dict",
"schema": {
"target": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
},
},
"console": { "console": {
"type": "dict", "type": "dict",
"schema": { "schema": {
@ -62,6 +72,7 @@ class ReportTrigger(Trigger):
}, },
"homepage": { "homepage": {
"type": "string", "type": "string",
"is_url": ["http", "https"],
}, },
"host": { "host": {
"type": "string", "type": "string",
@ -70,6 +81,7 @@ class ReportTrigger(Trigger):
"link_path": { "link_path": {
"type": "string", "type": "string",
"required": True, "required": True,
"is_url": [],
}, },
"no_empty_report": { "no_empty_report": {
"type": "boolean", "type": "boolean",
@ -82,6 +94,8 @@ class ReportTrigger(Trigger):
"type": "integer", "type": "integer",
"coerce": "integer", "coerce": "integer",
"required": True, "required": True,
"min": 0,
"max": 65535,
}, },
"receivers": { "receivers": {
"type": "list", "type": "list",
@ -118,10 +132,12 @@ class ReportTrigger(Trigger):
}, },
"homepage": { "homepage": {
"type": "string", "type": "string",
"is_url": ["http", "https"],
}, },
"link_path": { "link_path": {
"type": "string", "type": "string",
"required": True, "required": True,
"is_url": [],
}, },
"path": { "path": {
"type": "path", "type": "path",
@ -153,10 +169,12 @@ class ReportTrigger(Trigger):
}, },
"homepage": { "homepage": {
"type": "string", "type": "string",
"is_url": ["http", "https"],
}, },
"link_path": { "link_path": {
"type": "string", "type": "string",
"required": True, "required": True,
"is_url": [],
}, },
"template_path": { "template_path": {
"type": "path", "type": "path",
@ -171,6 +189,7 @@ class ReportTrigger(Trigger):
"timeout": { "timeout": {
"type": "integer", "type": "integer",
"coerce": "integer", "coerce": "integer",
"min": 0,
}, },
}, },
}, },

View File

@ -21,7 +21,7 @@ import shutil
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Iterable, List, Optional from typing import Dict, Iterable, List, Optional
from ahriman.core.build_tools.task import Task from ahriman.core.build_tools.task import Task
from ahriman.core.repository.cleaner import Cleaner from ahriman.core.repository.cleaner import Cleaner
@ -122,27 +122,41 @@ class Executor(Cleaner):
except Exception: except Exception:
self.logger.exception("could not remove %s", package) self.logger.exception("could not remove %s", package)
packages_to_remove: Dict[str, Path] = {}
bases_to_remove: List[str] = []
# build package list based on user input
requested = set(packages) requested = set(packages)
for local in self.packages(): for local in self.packages():
if local.base in packages or all(package in requested for package in local.packages): if local.base in packages or all(package in requested for package in local.packages):
to_remove = { packages_to_remove.update({
package: properties.filepath package: properties.filepath
for package, properties in local.packages.items() for package, properties in local.packages.items()
if properties.filepath is not None if properties.filepath is not None
} })
remove_base(local.base) bases_to_remove.append(local.base)
elif requested.intersection(local.packages.keys()): elif requested.intersection(local.packages.keys()):
to_remove = { packages_to_remove.update({
package: properties.filepath package: properties.filepath
for package, properties in local.packages.items() for package, properties in local.packages.items()
if package in requested and properties.filepath is not None if package in requested and properties.filepath is not None
} })
else:
to_remove = {}
for package, filename in to_remove.items(): # check for packages which were requested to remove, but weren't found locally
# it might happen for example, if there were no success build before
for unknown in requested:
if unknown in packages_to_remove or unknown in bases_to_remove:
continue
bases_to_remove.append(unknown)
# remove packages from repository files
for package, filename in packages_to_remove.items():
remove_package(package, filename) remove_package(package, filename)
# remove bases from registered
for package in bases_to_remove:
remove_base(package)
return self.repo.repo_path return self.repo.repo_path
def process_update(self, packages: Iterable[Path]) -> Result: def process_update(self, packages: Iterable[Path]) -> Result:

View File

@ -98,7 +98,7 @@ class UpdateHandler(Cleaner):
with self.in_package_context(cache_dir.name): with self.in_package_context(cache_dir.name):
try: try:
Sources.fetch(cache_dir, remote=None) Sources.fetch(cache_dir, remote=None)
remote = Package.from_build(cache_dir) remote = Package.from_build(cache_dir, self.architecture)
local = packages.get(remote.base) local = packages.get(remote.base)
if local is None: if local is None:

View File

@ -60,7 +60,7 @@ class Spawn(Thread, LazyLogging):
self.lock = Lock() self.lock = Lock()
self.active: Dict[str, Process] = {} self.active: Dict[str, Process] = {}
# stupid pylint does not know that it is possible # stupid pylint does not know that it is possible
self.queue: Queue[Tuple[str, bool]] = Queue() # pylint: disable=unsubscriptable-object self.queue: Queue[Tuple[str, bool] | None] = Queue() # pylint: disable=unsubscriptable-object
@staticmethod @staticmethod
def process(callback: Callable[[argparse.Namespace, str], bool], args: argparse.Namespace, architecture: str, def process(callback: Callable[[argparse.Namespace, str], bool], args: argparse.Namespace, architecture: str,
@ -78,55 +78,7 @@ class Spawn(Thread, LazyLogging):
result = callback(args, architecture) result = callback(args, architecture)
queue.put((process_id, result)) queue.put((process_id, result))
def key_import(self, key: str, server: Optional[str]) -> None: def _spawn_process(self, command: str, *args: str, **kwargs: str) -> None:
"""
import key to service cache
Args:
key(str): key to import
server(str): PGP key server
"""
kwargs = {} if server is None else {"key-server": server}
self.spawn_process("service-key-import", key, **kwargs)
def packages_add(self, packages: Iterable[str], *, now: bool) -> None:
"""
add packages
Args:
packages(Iterable[str]): packages list to add
now(bool): build packages now
"""
kwargs = {"source": PackageSource.AUR.value} # avoid abusing by building non-aur packages
if now:
kwargs["now"] = ""
self.spawn_process("package-add", *packages, **kwargs)
def packages_rebuild(self, depends_on: str) -> None:
"""
rebuild packages which depend on the specified package
Args:
depends_on(str): packages dependency
"""
self.spawn_process("repo-rebuild", **{"depends-on": depends_on})
def packages_remove(self, packages: Iterable[str]) -> None:
"""
remove packages
Args:
packages(Iterable[str]): packages list to remove
"""
self.spawn_process("package-remove", *packages)
def packages_update(self, ) -> None:
"""
run full repository update
"""
self.spawn_process("repo-update")
def spawn_process(self, command: str, *args: str, **kwargs: str) -> None:
""" """
spawn external ahriman process with supplied arguments spawn external ahriman process with supplied arguments
@ -161,6 +113,54 @@ class Spawn(Thread, LazyLogging):
with self.lock: with self.lock:
self.active[process_id] = process self.active[process_id] = process
def key_import(self, key: str, server: Optional[str]) -> None:
"""
import key to service cache
Args:
key(str): key to import
server(str): PGP key server
"""
kwargs = {} if server is None else {"key-server": server}
self._spawn_process("service-key-import", key, **kwargs)
def packages_add(self, packages: Iterable[str], *, now: bool) -> None:
"""
add packages
Args:
packages(Iterable[str]): packages list to add
now(bool): build packages now
"""
kwargs = {"source": PackageSource.AUR.value} # avoid abusing by building non-aur packages
if now:
kwargs["now"] = ""
self._spawn_process("package-add", *packages, **kwargs)
def packages_rebuild(self, depends_on: str) -> None:
"""
rebuild packages which depend on the specified package
Args:
depends_on(str): packages dependency
"""
self._spawn_process("repo-rebuild", **{"depends-on": depends_on})
def packages_remove(self, packages: Iterable[str]) -> None:
"""
remove packages
Args:
packages(Iterable[str]): packages list to remove
"""
self._spawn_process("package-remove", *packages)
def packages_update(self, ) -> None:
"""
run full repository update
"""
self._spawn_process("repo-update")
def run(self) -> None: def run(self) -> None:
""" """
thread run method thread run method
@ -174,3 +174,9 @@ class Spawn(Thread, LazyLogging):
if process is not None: if process is not None:
process.terminate() # make sure lol process.terminate() # make sure lol
process.join() process.join()
def stop(self) -> None:
"""
gracefully terminate thread
"""
self.queue.put(None)

View File

@ -17,17 +17,18 @@
# 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/>.
# #
import contextlib
import logging import logging
import requests import requests
from typing import List, Optional, Tuple from typing import Generator, List, Optional, Tuple
from urllib.parse import quote_plus as urlencode from urllib.parse import quote_plus as urlencode
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
from ahriman.core.status.client import Client from ahriman.core.status.client import Client
from ahriman.core.util import exception_response_text from ahriman.core.util import exception_response_text
from ahriman.models.build_status import BuildStatusEnum, BuildStatus from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.internal_status import InternalStatus from ahriman.models.internal_status import InternalStatus
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.user import User from ahriman.models.user import User
@ -98,6 +99,18 @@ class WebClient(Client, LazyLogging):
address = f"http://{host}:{port}" address = f"http://{host}:{port}"
return address, False return address, False
@contextlib.contextmanager
def __execute_request(self) -> Generator[None, None, None]:
"""
execute request and handle exceptions
"""
try:
yield
except requests.HTTPError as e:
self.logger.exception("could not perform http request: %s", exception_response_text(e))
except Exception:
self.logger.exception("could not perform http request")
def _create_session(self, *, use_unix_socket: bool) -> requests.Session: def _create_session(self, *, use_unix_socket: bool) -> requests.Session:
""" """
generate new request session generate new request session
@ -130,13 +143,9 @@ class WebClient(Client, LazyLogging):
"password": self.user.password "password": self.user.password
} }
try: with self.__execute_request():
response = self.__session.post(self._login_url, json=payload) response = self.__session.post(self._login_url, json=payload)
response.raise_for_status() response.raise_for_status()
except requests.HTTPError as e:
self.logger.exception("could not login as %s: %s", self.user, exception_response_text(e))
except Exception:
self.logger.exception("could not login as %s", self.user)
def _logs_url(self, package_base: str) -> str: def _logs_url(self, package_base: str) -> str:
""" """
@ -177,13 +186,9 @@ class WebClient(Client, LazyLogging):
"package": package.view() "package": package.view()
} }
try: with self.__execute_request():
response = self.__session.post(self._package_url(package.base), json=payload) response = self.__session.post(self._package_url(package.base), json=payload)
response.raise_for_status() response.raise_for_status()
except requests.HTTPError as e:
self.logger.exception("could not add %s: %s", package.base, exception_response_text(e))
except Exception:
self.logger.exception("could not add %s", package.base)
def get(self, package_base: Optional[str]) -> List[Tuple[Package, BuildStatus]]: def get(self, package_base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
""" """
@ -195,7 +200,7 @@ class WebClient(Client, LazyLogging):
Returns: Returns:
List[Tuple[Package, BuildStatus]]: list of current package description and status if it has been found List[Tuple[Package, BuildStatus]]: list of current package description and status if it has been found
""" """
try: with self.__execute_request():
response = self.__session.get(self._package_url(package_base or "")) response = self.__session.get(self._package_url(package_base or ""))
response.raise_for_status() response.raise_for_status()
@ -204,10 +209,8 @@ class WebClient(Client, LazyLogging):
(Package.from_json(package["package"]), BuildStatus.from_json(package["status"])) (Package.from_json(package["package"]), BuildStatus.from_json(package["status"]))
for package in status_json for package in status_json
] ]
except requests.HTTPError as e:
self.logger.exception("could not get %s: %s", package_base, exception_response_text(e)) # noinspection PyUnreachableCode
except Exception:
self.logger.exception("could not get %s", package_base)
return [] return []
def get_internal(self) -> InternalStatus: def get_internal(self) -> InternalStatus:
@ -217,16 +220,14 @@ class WebClient(Client, LazyLogging):
Returns: Returns:
InternalStatus: current internal (web) service status InternalStatus: current internal (web) service status
""" """
try: with self.__execute_request():
response = self.__session.get(self._status_url) response = self.__session.get(self._status_url)
response.raise_for_status() response.raise_for_status()
status_json = response.json() status_json = response.json()
return InternalStatus.from_json(status_json) return InternalStatus.from_json(status_json)
except requests.HTTPError as e:
self.logger.exception("could not get web service status: %s", exception_response_text(e)) # noinspection PyUnreachableCode
except Exception:
self.logger.exception("could not get web service status")
return InternalStatus(status=BuildStatus()) return InternalStatus(status=BuildStatus())
def logs(self, package_base: str, record: logging.LogRecord) -> None: def logs(self, package_base: str, record: logging.LogRecord) -> None:
@ -254,13 +255,9 @@ class WebClient(Client, LazyLogging):
Args: Args:
package_base(str): basename to remove package_base(str): basename to remove
""" """
try: with self.__execute_request():
response = self.__session.delete(self._package_url(package_base)) response = self.__session.delete(self._package_url(package_base))
response.raise_for_status() response.raise_for_status()
except requests.HTTPError as e:
self.logger.exception("could not delete %s: %s", package_base, exception_response_text(e))
except Exception:
self.logger.exception("could not delete %s", package_base)
def update(self, package_base: str, status: BuildStatusEnum) -> None: def update(self, package_base: str, status: BuildStatusEnum) -> None:
""" """
@ -272,13 +269,9 @@ class WebClient(Client, LazyLogging):
""" """
payload = {"status": status.value} payload = {"status": status.value}
try: with self.__execute_request():
response = self.__session.post(self._package_url(package_base), json=payload) response = self.__session.post(self._package_url(package_base), json=payload)
response.raise_for_status() response.raise_for_status()
except requests.HTTPError as e:
self.logger.exception("could not update %s: %s", package_base, exception_response_text(e))
except Exception:
self.logger.exception("could not update %s", package_base)
def update_self(self, status: BuildStatusEnum) -> None: def update_self(self, status: BuildStatusEnum) -> None:
""" """
@ -289,10 +282,6 @@ class WebClient(Client, LazyLogging):
""" """
payload = {"status": status.value} payload = {"status": status.value}
try: with self.__execute_request():
response = self.__session.post(self._status_url, json=payload) response = self.__session.post(self._status_url, json=payload)
response.raise_for_status() response.raise_for_status()
except requests.HTTPError as e:
self.logger.exception("could not update service status: %s", exception_response_text(e))
except Exception:
self.logger.exception("could not update service status")

View File

@ -19,16 +19,12 @@
# #
from __future__ import annotations from __future__ import annotations
import itertools import functools
from pathlib import Path from typing import Callable, Iterable, List
from tempfile import TemporaryDirectory
from typing import Callable, Iterable, List, Set, Tuple, Type
from ahriman.core.build_tools.sources import Sources from ahriman.core.util import partition
from ahriman.core.database import SQLite
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths
class Leaf: class Leaf:
@ -40,16 +36,15 @@ class Leaf:
package(Package): leaf package properties package(Package): leaf package properties
""" """
def __init__(self, package: Package, dependencies: Set[str]) -> None: def __init__(self, package: Package) -> None:
""" """
default constructor default constructor
Args: Args:
package(Package): package properties package(Package): package properties
dependencies(Set[str]): package dependencies
""" """
self.package = package self.package = package
self.dependencies = dependencies self.dependencies = package.depends_build
@property @property
def items(self) -> Iterable[str]: def items(self) -> Iterable[str]:
@ -61,24 +56,6 @@ class Leaf:
""" """
return self.package.packages.keys() return self.package.packages.keys()
@classmethod
def load(cls: Type[Leaf], package: Package, paths: RepositoryPaths, database: SQLite) -> Leaf:
"""
load leaf from package with dependencies
Args:
package(Package): package properties
paths(RepositoryPaths): repository paths instance
database(SQLite): database instance
Returns:
Leaf: loaded class
"""
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.load(clone_dir, package, database.patches_get(package.base), paths)
dependencies = Package.dependencies(clone_dir)
return cls(package, dependencies)
def is_dependency(self, packages: Iterable[Leaf]) -> bool: def is_dependency(self, packages: Iterable[Leaf]) -> bool:
""" """
check if the package is dependency of any other package from list or not check if the package is dependency of any other package from list or not
@ -130,7 +107,7 @@ class Tree:
>>> repository = Repository.load("x86_64", configuration, database, report=True, unsafe=False) >>> repository = Repository.load("x86_64", configuration, database, report=True, unsafe=False)
>>> packages = repository.packages() >>> packages = repository.packages()
>>> >>>
>>> tree = Tree.resolve(packages, configuration.repository_paths, database) >>> tree = Tree.resolve(packages)
>>> for tree_level in tree: >>> for tree_level in tree:
>>> for package in tree_level: >>> for package in tree_level:
>>> print(package.base) >>> print(package.base)
@ -138,14 +115,8 @@ class Tree:
The direct constructor call is also possible but requires tree leaves to be instantioned in advance, e.g.:: The direct constructor call is also possible but requires tree leaves to be instantioned in advance, e.g.::
>>> leaves = [Leaf.load(package, database) for package in packages] >>> leaves = [Leaf(package) for package in packages]
>>> tree = Tree(leaves) >>> tree = Tree(leaves)
Using the default ``Leaf()`` method is possible, but not really recommended because it requires from the user to
build the dependency list by himself::
>>> leaf = Leaf(package, dependecies)
>>> tree = Tree([leaf])
""" """
def __init__(self, leaves: List[Leaf]) -> None: def __init__(self, leaves: List[Leaf]) -> None:
@ -157,23 +128,20 @@ class Tree:
""" """
self.leaves = leaves self.leaves = leaves
@classmethod @staticmethod
def resolve(cls: Type[Tree], packages: Iterable[Package], paths: RepositoryPaths, def resolve(packages: Iterable[Package]) -> List[List[Package]]:
database: SQLite) -> List[List[Package]]:
""" """
resolve dependency tree resolve dependency tree
Args: Args:
packages(Iterable[Package]): packages list packages(Iterable[Package]): packages list
paths(RepositoryPaths): repository paths instance
database(SQLite): database instance
Returns: Returns:
List[List[Package]]: list of packages lists based on their dependencies List[List[Package]]: list of packages lists based on their dependencies
""" """
leaves = [Leaf.load(package, paths, database) for package in packages] leaves = [Leaf(package) for package in packages]
tree = cls(leaves) instance = Tree(leaves)
return tree.levels() return instance.levels()
def levels(self) -> List[List[Package]]: def levels(self) -> List[List[Package]]:
""" """
@ -182,13 +150,6 @@ class Tree:
Returns: Returns:
List[List[Package]]: sorted list of packages lists based on their dependencies List[List[Package]]: sorted list of packages lists based on their dependencies
""" """
# https://docs.python.org/dev/library/itertools.html#itertools-recipes
def partition(source: List[Leaf]) -> Tuple[List[Leaf], Iterable[Leaf]]:
first_iter, second_iter = itertools.tee(source)
filter_fn: Callable[[Leaf], bool] = lambda leaf: leaf.is_dependency(next_level)
# materialize first list and leave second as iterator
return list(filter(filter_fn, first_iter)), itertools.filterfalse(filter_fn, second_iter)
unsorted: List[List[Leaf]] = [] unsorted: List[List[Leaf]] = []
# build initial tree # build initial tree
@ -203,7 +164,9 @@ class Tree:
next_level = unsorted[next_num] next_level = unsorted[next_num]
# change lists inside the collection # change lists inside the collection
unsorted[current_num], to_be_moved = partition(current_level) # additional workaround with partial in order to hide cell-var-from-loop pylint warning
predicate = functools.partial(Leaf.is_dependency, packages=next_level)
unsorted[current_num], to_be_moved = partition(current_level, predicate)
unsorted[next_num].extend(to_be_moved) unsorted[next_num].extend(to_be_moved)
comparator: Callable[[Package], str] = lambda package: package.base comparator: Callable[[Package], str] = lambda package: package.base

View File

@ -35,6 +35,16 @@ class UploadTrigger(Trigger):
""" """
CONFIGURATION_SCHEMA = { CONFIGURATION_SCHEMA = {
"upload": {
"type": "dict",
"schema": {
"target": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
},
},
"github": { "github": {
"type": "dict", "type": "dict",
"schema": { "schema": {
@ -57,6 +67,7 @@ class UploadTrigger(Trigger):
"timeout": { "timeout": {
"type": "integer", "type": "integer",
"coerce": "integer", "coerce": "integer",
"min": 0,
}, },
"username": { "username": {
"type": "string", "type": "string",
@ -101,6 +112,7 @@ class UploadTrigger(Trigger):
"chunk_size": { "chunk_size": {
"type": "integer", "type": "integer",
"coerce": "integer", "coerce": "integer",
"min": 0,
}, },
"region": { "region": {
"type": "string", "type": "string",

View File

@ -19,6 +19,7 @@
# #
import datetime import datetime
import io import io
import itertools
import logging import logging
import os import os
import re import re
@ -28,14 +29,31 @@ import subprocess
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from pwd import getpwuid from pwd import getpwuid
from typing import Any, Dict, Generator, IO, Iterable, List, Optional, Type, Union from typing import Any, Callable, Dict, Generator, IO, Iterable, List, Optional, Tuple, Type, TypeVar, Union
from ahriman.core.exceptions import OptionError, UnsafeRunError from ahriman.core.exceptions import OptionError, UnsafeRunError
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
__all__ = ["check_output", "check_user", "enum_values", "exception_response_text", "filter_json", "full_version", __all__ = [
"package_like", "pretty_datetime", "pretty_size", "safe_filename", "utcnow", "walk"] "check_output",
"check_user",
"enum_values",
"exception_response_text",
"filter_json",
"full_version",
"package_like",
"partition",
"pretty_datetime",
"pretty_size",
"safe_filename",
"trim_package",
"utcnow",
"walk",
]
T = TypeVar("T")
def check_output(*args: str, exception: Optional[Exception] = None, cwd: Optional[Path] = None, def check_output(*args: str, exception: Optional[Exception] = None, cwd: Optional[Path] = None,
@ -225,6 +243,21 @@ def package_like(filename: Path) -> bool:
return ".pkg." in name and not name.endswith(".sig") return ".pkg." in name and not name.endswith(".sig")
def partition(source: List[T], predicate: Callable[[T], bool]) -> Tuple[List[T], List[T]]:
"""
partition list into two based on predicate, based on # https://docs.python.org/dev/library/itertools.html#itertools-recipes
Args:
source(List[T]): source list to be partitioned
predicate(Callable[[T], bool]): filter function
Returns:
Tuple[List[T], List[T]]: two lists, first is which ``predicate`` is ``True``, second is ``False``
"""
first_iter, second_iter = itertools.tee(source)
return list(filter(predicate, first_iter)), list(itertools.filterfalse(predicate, second_iter))
def pretty_datetime(timestamp: Optional[Union[datetime.datetime, float, int]]) -> str: def pretty_datetime(timestamp: Optional[Union[datetime.datetime, float, int]]) -> str:
""" """
convert datetime object to string convert datetime object to string
@ -295,6 +328,23 @@ def safe_filename(source: str) -> str:
return re.sub(r"[^A-Za-z\d\-._~:\[\]@]", "-", source) return re.sub(r"[^A-Za-z\d\-._~:\[\]@]", "-", source)
def trim_package(package_name: str) -> str:
"""
remove version bound and description from package name. Pacman allows to specify version bound (=, <=, >= etc) for
packages in dependencies and also allows to specify description (via :); this function removes trailing parts and
return exact package name
Args:
package_name(str): source package name
Returns:
str: package name without description or version bound
"""
for symbol in ("<", "=", ">", ":"):
package_name = package_name.partition(symbol)[0]
return package_name
def utcnow() -> datetime.datetime: def utcnow() -> datetime.datetime:
""" """
get current time get current time

View File

@ -45,6 +45,7 @@ class AURPackage:
popularity(float): package popularity popularity(float): package popularity
out_of_date(Optional[datetime.datetime]): package out of date timestamp if any out_of_date(Optional[datetime.datetime]): package out of date timestamp if any
maintainer(Optional[str]): package maintainer maintainer(Optional[str]): package maintainer
submitter(Optional[str]): package first submitter
first_submitted(datetime.datetime): timestamp of the first package submission first_submitted(datetime.datetime): timestamp of the first package submission
last_modified(datetime.datetime): timestamp of the last package submission last_modified(datetime.datetime): timestamp of the last package submission
url_path(str): AUR package path url_path(str): AUR package path
@ -89,6 +90,7 @@ class AURPackage:
url: Optional[str] = None url: Optional[str] = None
out_of_date: Optional[datetime.datetime] = None out_of_date: Optional[datetime.datetime] = None
maintainer: Optional[str] = None maintainer: Optional[str] = None
submitter: Optional[str] = None
repository: str = "aur" repository: str = "aur"
depends: List[str] = field(default_factory=list) depends: List[str] = field(default_factory=list)
make_depends: List[str] = field(default_factory=list) make_depends: List[str] = field(default_factory=list)
@ -140,6 +142,7 @@ class AURPackage:
url=package.url, url=package.url,
out_of_date=None, out_of_date=None,
maintainer=None, maintainer=None,
submitter=None,
repository=package.db.name, repository=package.db.name,
depends=package.depends, depends=package.depends,
make_depends=package.makedepends, make_depends=package.makedepends,
@ -178,6 +181,7 @@ class AURPackage:
dump["flag_date"], dump["flag_date"],
"%Y-%m-%dT%H:%M:%S.%fZ") if dump["flag_date"] is not None else None, "%Y-%m-%dT%H:%M:%S.%fZ") if dump["flag_date"] is not None else None,
maintainer=next(iter(dump["maintainers"]), None), maintainer=next(iter(dump["maintainers"]), None),
submitter=None,
repository=dump["repo"], repository=dump["repo"],
depends=dump["depends"], depends=dump["depends"],
make_depends=dump["makedepends"], make_depends=dump["makedepends"],

View File

@ -57,7 +57,8 @@ class InternalStatus:
InternalStatus: internal status InternalStatus: internal status
""" """
counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0) counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0)
return cls(status=BuildStatus.from_json(dump.get("status", {})), build_status = dump.get("status") or {}
return cls(status=BuildStatus.from_json(build_status),
architecture=dump.get("architecture"), architecture=dump.get("architecture"),
packages=counters, packages=counters,
repository=dump.get("repository"), repository=dump.get("repository"),

View File

@ -18,7 +18,10 @@
# 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 dataclasses import dataclass
from typing import List from sqlite3 import Connection
from typing import Callable, List
from ahriman.core.configuration import Configuration
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
@ -30,8 +33,10 @@ class Migration:
index(int): migration position index(int): migration position
name(str): migration name name(str): migration name
steps(List[str]): migration steps steps(List[str]): migration steps
migrate_data(Callable[[Connection, Configuration], None]): data migration callback
""" """
index: int index: int
name: str name: str
steps: List[str] steps: List[str]
migrate_data: Callable[[Connection, Configuration], None]

View File

@ -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/>.
# #
# pylint: disable=too-many-lines # pylint: disable=too-many-lines,too-many-public-methods
from __future__ import annotations from __future__ import annotations
import copy import copy
@ -88,6 +88,36 @@ class Package(LazyLogging):
""" """
return sorted(set(sum((package.depends for package in self.packages.values()), start=[]))) return sorted(set(sum((package.depends for package in self.packages.values()), start=[])))
@property
def depends_build(self) -> Set[str]:
"""
get full list of external dependencies which has to be installed for build process
Returns:
Set[str]: full dependencies list used by devtools
"""
return (set(self.depends) | set(self.depends_make)).difference(self.packages_full)
@property
def depends_make(self) -> List[str]:
"""
get package make dependencies
Returns:
List[str]: sum of make dependencies per each package
"""
return sorted(set(sum((package.make_depends for package in self.packages.values()), start=[])))
@property
def depends_opt(self) -> List[str]:
"""
get package optional dependencies
Returns:
List[str]: sum of optional dependencies per each package
"""
return sorted(set(sum((package.opt_depends for package in self.packages.values()), start=[])))
@property @property
def groups(self) -> List[str]: def groups(self) -> List[str]:
""" """
@ -133,6 +163,20 @@ class Package(LazyLogging):
""" """
return sorted(set(sum((package.licenses for package in self.packages.values()), start=[]))) return sorted(set(sum((package.licenses for package in self.packages.values()), start=[])))
@property
def packages_full(self) -> List[str]:
"""
get full packages list including provides
Returns:
List[str]: full list of packages which this base contains
"""
packages = set()
for package, properties in self.packages.items():
packages.add(package)
packages.update(properties.provides)
return sorted(packages)
@classmethod @classmethod
def from_archive(cls: Type[Package], path: Path, pacman: Pacman, remote: Optional[RemoteSource]) -> Package: def from_archive(cls: Type[Package], path: Path, pacman: Pacman, remote: Optional[RemoteSource]) -> Package:
""" """
@ -168,15 +212,16 @@ class Package(LazyLogging):
base=package.package_base, base=package.package_base,
version=package.version, version=package.version,
remote=remote, remote=remote,
packages={package.name: PackageDescription()}) packages={package.name: PackageDescription.from_aur(package)})
@classmethod @classmethod
def from_build(cls: Type[Package], path: Path) -> Package: def from_build(cls: Type[Package], path: Path, architecture: str) -> Package:
""" """
construct package properties from sources directory construct package properties from sources directory
Args: Args:
path(Path): path to package sources directory path(Path): path to package sources directory
architecture(str): load package for specific architecture
Returns: Returns:
Package: package properties Package: package properties
@ -188,7 +233,21 @@ class Package(LazyLogging):
srcinfo, errors = parse_srcinfo(srcinfo_source) srcinfo, errors = parse_srcinfo(srcinfo_source)
if errors: if errors:
raise PackageInfoError(errors) raise PackageInfoError(errors)
packages = {key: PackageDescription() for key in srcinfo["packages"]}
def get_property(key: str, properties: Dict[str, Any], default: Any) -> Any:
return properties.get(key) or srcinfo.get(key) or default
def get_list(key: str, properties: Dict[str, Any]) -> Any:
return get_property(key, properties, []) + get_property(f"{key}_{architecture}", properties, [])
packages = {
package: PackageDescription(
depends=get_list("depends", properties),
make_depends=get_list("makedepends", properties),
opt_depends=get_list("optdepends", properties),
)
for package, properties in srcinfo["packages"].items()
}
version = full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"]) version = full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"])
return cls(base=srcinfo["pkgbase"], version=version, remote=None, packages=packages) return cls(base=srcinfo["pkgbase"], version=version, remote=None, packages=packages)
@ -204,11 +263,12 @@ class Package(LazyLogging):
Returns: Returns:
Package: package properties Package: package properties
""" """
packages_json = dump.get("packages") or {}
packages = { packages = {
key: PackageDescription.from_json(value) key: PackageDescription.from_json(value)
for key, value in dump.get("packages", {}).items() for key, value in packages_json.items()
} }
remote = dump.get("remote", {}) remote = dump.get("remote") or {}
return cls(base=dump["base"], version=dump["version"], remote=RemoteSource.from_json(remote), packages=packages) return cls(base=dump["base"], version=dump["version"], remote=RemoteSource.from_json(remote), packages=packages)
@classmethod @classmethod
@ -230,43 +290,7 @@ class Package(LazyLogging):
base=package.package_base, base=package.package_base,
version=package.version, version=package.version,
remote=remote, remote=remote,
packages={package.name: PackageDescription()}) packages={package.name: PackageDescription.from_aur(package)})
@staticmethod
def dependencies(path: Path) -> Set[str]:
"""
load dependencies from package sources
Args:
path(Path): path to package sources directory
Returns:
Set[str]: list of package dependencies including makedepends array, but excluding packages from this base
Raises:
InvalidPackageInfo: if there are parsing errors
"""
# additional function to remove versions from dependencies
def extract_packages(raw_packages_list: List[str]) -> Set[str]:
return {trim_version(package_name) for package_name in raw_packages_list}
def trim_version(package_name: str) -> str:
for symbol in ("<", "=", ">"):
package_name = package_name.split(symbol)[0]
return package_name
srcinfo_source = Package._check_output("makepkg", "--printsrcinfo", cwd=path)
srcinfo, errors = parse_srcinfo(srcinfo_source)
if errors:
raise PackageInfoError(errors)
makedepends = extract_packages(srcinfo.get("makedepends", []))
# sum over each package
depends = extract_packages(srcinfo.get("depends", []))
for package in srcinfo["packages"].values():
depends |= extract_packages(package.get("depends", []))
# we are not interested in dependencies inside pkgbase
packages = set(srcinfo["packages"].keys())
return (depends | makedepends) - packages
@staticmethod @staticmethod
def supported_architectures(path: Path) -> Set[str]: def supported_architectures(path: Path) -> Set[str]:

View File

@ -24,7 +24,8 @@ from pathlib import Path
from pyalpm import Package # type: ignore from pyalpm import Package # type: ignore
from typing import Any, Dict, List, Optional, Type from typing import Any, Dict, List, Optional, Type
from ahriman.core.util import filter_json from ahriman.core.util import filter_json, trim_package
from ahriman.models.aur_package import AURPackage
@dataclass(kw_only=True) @dataclass(kw_only=True)
@ -37,6 +38,8 @@ class PackageDescription:
archive_size(Optional[int]): package archive size archive_size(Optional[int]): package archive size
build_date(Optional[int]): package build date build_date(Optional[int]): package build date
depends(List[str]): package dependencies list depends(List[str]): package dependencies list
opt_depends(List[str]): optional package dependencies list
make_depends(List[str]): package dependencies list used for building
description(Optional[str]): package description description(Optional[str]): package description
filename(Optional[str]): package archive name filename(Optional[str]): package archive name
groups(List[str]): package groups groups(List[str]): package groups
@ -67,6 +70,8 @@ class PackageDescription:
archive_size: Optional[int] = None archive_size: Optional[int] = None
build_date: Optional[int] = None build_date: Optional[int] = None
depends: List[str] = field(default_factory=list) depends: List[str] = field(default_factory=list)
make_depends: List[str] = field(default_factory=list)
opt_depends: List[str] = field(default_factory=list)
description: Optional[str] = None description: Optional[str] = None
filename: Optional[str] = None filename: Optional[str] = None
groups: List[str] = field(default_factory=list) groups: List[str] = field(default_factory=list)
@ -75,6 +80,14 @@ class PackageDescription:
provides: List[str] = field(default_factory=list) provides: List[str] = field(default_factory=list)
url: Optional[str] = None url: Optional[str] = None
def __post_init__(self) -> None:
"""
update dependencies list accordingly
"""
self.depends = [trim_package(package) for package in self.depends]
self.opt_depends = [trim_package(package) for package in self.opt_depends]
self.make_depends = [trim_package(package) for package in self.make_depends]
@property @property
def filepath(self) -> Optional[Path]: def filepath(self) -> Optional[Path]:
""" """
@ -85,6 +98,27 @@ class PackageDescription:
""" """
return Path(self.filename) if self.filename is not None else None return Path(self.filename) if self.filename is not None else None
@classmethod
def from_aur(cls: Type[PackageDescription], package: AURPackage) -> PackageDescription:
"""
construct properties from AUR package model
Args:
package(AURPackage): AUR package model
Returns:
PackageDescription: package properties based on source AUR package
"""
return cls(
depends=package.depends,
make_depends=package.make_depends,
opt_depends=package.opt_depends,
description=package.description,
licenses=package.license,
provides=package.provides,
url=package.url,
)
@classmethod @classmethod
def from_json(cls: Type[PackageDescription], dump: Dict[str, Any]) -> PackageDescription: def from_json(cls: Type[PackageDescription], dump: Dict[str, Any]) -> PackageDescription:
""" """
@ -117,13 +151,16 @@ class PackageDescription:
archive_size=package.size, archive_size=package.size,
build_date=package.builddate, build_date=package.builddate,
depends=package.depends, depends=package.depends,
make_depends=package.makedepends,
opt_depends=package.optdepends,
description=package.desc, description=package.desc,
filename=path.name, filename=path.name,
groups=package.groups, groups=package.groups,
installed_size=package.isize, installed_size=package.isize,
licenses=package.licenses, licenses=package.licenses,
provides=package.provides, provides=package.provides,
url=package.url) url=package.url,
)
def view(self) -> Dict[str, Any]: def view(self) -> Dict[str, Any]:
""" """

View File

@ -19,7 +19,7 @@
# #
from __future__ import annotations from __future__ import annotations
from typing import Any, List, Optional, Iterable from typing import Any, Iterable, List, Optional
from ahriman.core.exceptions import UnprocessedPackageStatusError from ahriman.core.exceptions import UnprocessedPackageStatusError
from ahriman.models.package import Package from ahriman.models.package import Package

View File

@ -1,102 +0,0 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
import time
from dataclasses import dataclass
from typing import Optional, Type
@dataclass(frozen=True)
class UserIdentity:
"""
user identity used inside web service
Attributes:
username(str): username
expire_at(int): identity expiration timestamp
"""
username: str
expire_at: int
@classmethod
def from_identity(cls: Type[UserIdentity], identity: str) -> Optional[UserIdentity]:
"""
parse identity into object
Args:
identity(str): identity from session data
Returns:
Optional[UserIdentity]: user identity object if it can be parsed and not expired and None otherwise
"""
try:
username, expire_at = identity.split()
user = cls(username, int(expire_at))
return None if user.is_expired() else user
except ValueError:
return None
@classmethod
def from_username(cls: Type[UserIdentity], username: Optional[str], max_age: int) -> Optional[UserIdentity]:
"""
generate identity from username
Args:
username(Optional[str]): username
max_age(int): time to expire, seconds
Returns:
Optional[UserIdentity]: constructed identity object
"""
return cls(username, cls.expire_when(max_age)) if username is not None else None
@staticmethod
def expire_when(max_age: int) -> int:
"""
generate expiration time using delta
Args:
max_age(int): time delta to generate. Must be usually TTE
Returns:
int: expiration timestamp
"""
return int(time.time()) + max_age
def is_expired(self) -> bool:
"""
compare timestamp with current timestamp and return True in case if identity is expired
Returns:
bool: True in case if identity is expired and False otherwise
"""
return self.expire_when(0) > self.expire_at
def to_identity(self) -> str:
"""
convert object to identity representation
Returns:
str: web service identity
"""
return f"{self.username} {self.expire_at}"

View File

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

120
src/ahriman/web/apispec.py Normal file
View File

@ -0,0 +1,120 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import aiohttp_apispec # type: ignore
from aiohttp.web import Application
from typing import Any, Dict, List
from ahriman import version
from ahriman.core.configuration import Configuration
__all__ = ["setup_apispec"]
def _info() -> Dict[str, Any]:
"""
create info object for swagger docs
Returns:
Dict[str, Any]: info object as per openapi specification
"""
return {
"title": "ahriman",
"description": """Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
## Features
* Install-configure-forget manager for the very own repository.
* Multi-architecture support.
* Dependency manager.
* VCS packages support.
* Official repository support.
* Ability to patch AUR packages and even create package from local PKGBUILDs.
* Sign support with gpg (repository, package, per package settings).
* Triggers for repository updates, e.g. synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram).
* Repository status interface with optional authorization and control options
<security-definitions />
""",
"license": {
"name": "GPL3",
"url": "https://raw.githubusercontent.com/arcan1s/ahriman/master/COPYING",
},
"version": version.__version__,
}
def _security() -> List[Dict[str, Any]]:
"""
get security definitions
Returns:
List[Dict[str, Any]]: generated security definition
"""
return [{
"token": {
"type": "apiKey", # as per specification we are using api key
"name": "API_SESSION",
"in": "cookie",
}
}]
def _servers(application: Application) -> List[Dict[str, Any]]:
"""
get list of defined addresses for server
Args:
application(Application): web application instance
Returns:
List[Dict[str, Any]]: list (actually only one) of defined web urls
"""
configuration: Configuration = application["configuration"]
address = configuration.get("web", "address", fallback=None)
if not address:
host = configuration.get("web", "host")
port = configuration.getint("web", "port")
address = f"http://{host}:{port}"
return [{
"url": address,
}]
def setup_apispec(application: Application) -> aiohttp_apispec.AiohttpApiSpec:
"""
setup swagger api specification
Args:
application(Application): web application instance
Returns:
aiohttp_apispec.AiohttpApiSpec: created specification instance
"""
return aiohttp_apispec.setup_aiohttp_apispec(
application,
url="/api-docs/swagger.json",
openapi_version="3.0.2",
info=_info(),
servers=_servers(application),
security=_security(),
)

View File

@ -17,27 +17,32 @@
# 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 sqlite3 import Connection import aiohttp_cors # type: ignore
from ahriman.core.configuration import Configuration from aiohttp.web import Application
__all__ = ["migrate_users_data"] __all__ = ["setup_cors"]
def migrate_users_data(connection: Connection, configuration: Configuration) -> None: def setup_cors(application: Application) -> aiohttp_cors.CorsConfig:
""" """
perform migration for users setup CORS for the web application
Args: Args:
connection(Connection): database connection application(Application): web application instance
configuration(Configuration): configuration instance
Returns:
aiohttp_cors.CorsConfig: generated CORS configuration
""" """
for section in configuration.sections(): cors = aiohttp_cors.setup(application, defaults={
for option, value in configuration[section].items(): "*": aiohttp_cors.ResourceOptions(
if not section.startswith("auth:"): expose_headers="*",
continue allow_headers="*",
access = section[5:] allow_methods="*",
connection.execute( )
"""insert into users (username, access, password) values (:username, :access, :password)""", })
{"username": option.lower(), "access": access, "password": value}) for route in application.router.routes():
cors.add(route)
return cors

View File

@ -17,8 +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 aiohttp.web import Request from aiohttp.web import Request, StreamResponse
from aiohttp.web_response import StreamResponse
from typing import Awaitable, Callable from typing import Awaitable, Callable

View File

@ -18,29 +18,25 @@
# 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 aiohttp_security # type: ignore import aiohttp_security # type: ignore
import base64
import socket import socket
import types import types
from aiohttp import web from aiohttp.web import Application, Request, StaticResource, StreamResponse, middleware
from aiohttp.web import middleware, Request
from aiohttp.web_response import StreamResponse
from aiohttp.web_urldispatcher import StaticResource
from aiohttp_session import setup as setup_session from aiohttp_session import setup as setup_session
from aiohttp_session.cookie_storage import EncryptedCookieStorage from aiohttp_session.cookie_storage import EncryptedCookieStorage
from cryptography import fernet from cryptography import fernet
from typing import Optional from typing import Optional
from ahriman.core.auth import Auth from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.models.user_identity import UserIdentity
from ahriman.web.middlewares import HandlerType, MiddlewareType from ahriman.web.middlewares import HandlerType, MiddlewareType
__all__ = ["AuthorizationPolicy", "auth_handler", "setup_auth"] __all__ = ["setup_auth"]
class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type: ignore class _AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy):
""" """
authorization policy implementation authorization policy implementation
@ -67,10 +63,7 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type
Returns: Returns:
Optional[str]: user identity (username) in case if user exists and None otherwise Optional[str]: user identity (username) in case if user exists and None otherwise
""" """
user = UserIdentity.from_identity(identity) return identity if await self.validator.known_username(identity) else None
if user is None:
return None
return user.username if await self.validator.known_username(user.username) else None
async def permits(self, identity: str, permission: UserAccess, context: Optional[str] = None) -> bool: async def permits(self, identity: str, permission: UserAccess, context: Optional[str] = None) -> bool:
""" """
@ -84,13 +77,10 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type
Returns: Returns:
bool: True in case if user is allowed to perform this request and False otherwise bool: True in case if user is allowed to perform this request and False otherwise
""" """
user = UserIdentity.from_identity(identity) return await self.validator.verify_access(identity, permission, context)
if user is None:
return False
return await self.validator.verify_access(user.username, permission, context)
def auth_handler(allow_read_only: bool) -> MiddlewareType: def _auth_handler(allow_read_only: bool) -> MiddlewareType:
""" """
authorization and authentication middleware authorization and authentication middleware
@ -125,26 +115,43 @@ def auth_handler(allow_read_only: bool) -> MiddlewareType:
return handle return handle
def setup_auth(application: web.Application, validator: Auth) -> web.Application: def _cookie_secret_key(configuration: Configuration) -> fernet.Fernet:
"""
extract cookie secret key from configuration if set or generate new one
Args:
configuration(Configuration): configuration instance
Returns:
fernet.Fernet: fernet key instance
"""
if (secret_key := configuration.get("auth", "cookie_secret_key", fallback=None)) is not None:
return fernet.Fernet(secret_key)
secret_key = fernet.Fernet.generate_key()
return fernet.Fernet(secret_key)
def setup_auth(application: Application, configuration: Configuration, validator: Auth) -> Application:
""" """
setup authorization policies for the application setup authorization policies for the application
Args: Args:
application(web.Application): web application instance application(Application): web application instance
configuration(Configuration): configuration instance
validator(Auth): authorization module instance validator(Auth): authorization module instance
Returns: Returns:
web.Application: configured web application Application: configured web application
""" """
fernet_key = fernet.Fernet.generate_key() secret_key = _cookie_secret_key(configuration)
secret_key = base64.urlsafe_b64decode(fernet_key)
storage = EncryptedCookieStorage(secret_key, cookie_name="API_SESSION", max_age=validator.max_age) storage = EncryptedCookieStorage(secret_key, cookie_name="API_SESSION", max_age=validator.max_age)
setup_session(application, storage) setup_session(application, storage)
authorization_policy = AuthorizationPolicy(validator) authorization_policy = _AuthorizationPolicy(validator)
identity_policy = aiohttp_security.SessionIdentityPolicy() identity_policy = aiohttp_security.SessionIdentityPolicy()
aiohttp_security.setup(application, identity_policy, authorization_policy) aiohttp_security.setup(application, identity_policy, authorization_policy)
application.middlewares.append(auth_handler(validator.allow_read_only)) application.middlewares.append(_auth_handler(validator.allow_read_only))
return application return application

View File

@ -20,8 +20,8 @@
import aiohttp_jinja2 import aiohttp_jinja2
import logging import logging
from aiohttp.web import HTTPClientError, HTTPException, HTTPServerError, HTTPUnauthorized, Request, StreamResponse, \ from aiohttp.web import HTTPClientError, HTTPException, HTTPMethodNotAllowed, HTTPNoContent, HTTPServerError, \
json_response, middleware HTTPUnauthorized, Request, StreamResponse, json_response, middleware
from ahriman.web.middlewares import HandlerType, MiddlewareType from ahriman.web.middlewares import HandlerType, MiddlewareType
@ -29,6 +29,20 @@ from ahriman.web.middlewares import HandlerType, MiddlewareType
__all__ = ["exception_handler"] __all__ = ["exception_handler"]
def _is_templated_unauthorized(request: Request) -> bool:
"""
check if the request is eligible for rendering html template
Args:
request(Request): source request to check
Returns:
bool: True in case if response should be rendered as html and False otherwise
"""
return request.path in ("/api/v1/login", "/api/v1/logout") \
and "application/json" not in request.headers.getall("accept", [])
def exception_handler(logger: logging.Logger) -> MiddlewareType: def exception_handler(logger: logging.Logger) -> MiddlewareType:
""" """
exception handler middleware. Just log any exception (except for client ones) exception handler middleware. Just log any exception (except for client ones)
@ -44,10 +58,21 @@ def exception_handler(logger: logging.Logger) -> MiddlewareType:
try: try:
return await handler(request) return await handler(request)
except HTTPUnauthorized as e: except HTTPUnauthorized as e:
if is_templated_unauthorized(request): if _is_templated_unauthorized(request):
context = {"code": e.status_code, "reason": e.reason} context = {"code": e.status_code, "reason": e.reason}
return aiohttp_jinja2.render_template("error.jinja2", request, context, status=e.status_code) return aiohttp_jinja2.render_template("error.jinja2", request, context, status=e.status_code)
return json_response(data={"error": e.reason}, status=e.status_code) return json_response(data={"error": e.reason}, status=e.status_code)
except HTTPMethodNotAllowed as e:
if e.method == "OPTIONS":
# automatically handle OPTIONS method, idea comes from
# https://github.com/arcan1s/ffxivbis/blob/master/src/main/scala/me/arcanis/ffxivbis/http/api/v1/HttpHandler.scala#L32
raise HTTPNoContent(headers={"Allow": ",".join(sorted(e.allowed_methods))})
if e.method == "HEAD":
# since we have special autogenerated HEAD method, we need to remove it from list of available
e.allowed_methods = {method for method in e.allowed_methods if method != "HEAD"}
e.headers["Allow"] = ",".join(sorted(e.allowed_methods))
raise e
raise
except HTTPClientError as e: except HTTPClientError as e:
return json_response(data={"error": e.reason}, status=e.status_code) return json_response(data={"error": e.reason}, status=e.status_code)
except HTTPServerError as e: except HTTPServerError as e:
@ -60,17 +85,3 @@ def exception_handler(logger: logging.Logger) -> MiddlewareType:
return json_response(data={"error": str(e)}, status=500) return json_response(data={"error": str(e)}, status=500)
return handle return handle
def is_templated_unauthorized(request: Request) -> bool:
"""
check if the request is eligible for rendering html template
Args:
request(Request): source request to check
Returns:
bool: True in case if response should be rendered as html and False otherwise
"""
return request.path in ("/api/v1/login", "/api/v1/logout") \
and "application/json" not in request.headers.getall("accept", [])

View File

@ -20,6 +20,8 @@
from aiohttp.web import Application from aiohttp.web import Application
from pathlib import Path from pathlib import Path
from ahriman.web.views.api.docs import DocsView
from ahriman.web.views.api.swagger import SwaggerView
from ahriman.web.views.index import IndexView from ahriman.web.views.index import IndexView
from ahriman.web.views.service.add import AddView from ahriman.web.views.service.add import AddView
from ahriman.web.views.service.pgp import PGPView from ahriman.web.views.service.pgp import PGPView
@ -43,82 +45,31 @@ def setup_routes(application: Application, static_path: Path) -> None:
""" """
setup all defined routes setup all defined routes
Available routes are:
* ``GET /`` get build status page
* ``GET /index.html`` same as above
* ``POST /api/v1/service/add`` add new packages to repository
* ``GET /api/v1/service/pgp`` fetch PGP key from the keyserver
* ``POST /api/v1/service/pgp`` import PGP key from the keyserver
* ``POST /api/v1/service/rebuild`` rebuild packages based on their dependency list
* ``POST /api/v1/service/remove`` remove existing package from repository
* ``POST /api/v1/service/request`` request to add new packages to repository
* ``GET /api/v1/service/search`` search for substring in AUR
* ``POST /api/v1/service/update`` update all packages in repository
* ``GET /api/v1/packages`` get all known packages
* ``POST /api/v1/packages`` force update every package from repository
* ``DELETE /api/v1/package/:base`` delete package base from status page
* ``GET /api/v1/package/:base`` get package base status
* ``POST /api/v1/package/:base`` update package base status
* ``DELETE /api/v1/packages/{package}/logs`` delete package related logs
* ``GET /api/v1/packages/{package}/logs`` create log record for the package
* ``POST /api/v1/packages/{package}/logs`` get last package logs
* ``GET /api/v1/status`` get service status itself
* ``POST /api/v1/status`` update service status itself
* ``GET /api/v1/login`` OAuth2 handler for login
* ``POST /api/v1/login`` login to service
* ``POST /api/v1/logout`` logout from service
Args: Args:
application(Application): web application instance application(Application): web application instance
static_path(Path): path to static files directory static_path(Path): path to static files directory
""" """
application.router.add_get("/", IndexView, allow_head=True) application.router.add_view("/", IndexView)
application.router.add_get("/index.html", IndexView, allow_head=True) application.router.add_view("/index.html", IndexView)
application.router.add_view("/api-docs", DocsView)
application.router.add_view("/api-docs/swagger.json", SwaggerView)
application.router.add_static("/static", static_path, follow_symlinks=True) application.router.add_static("/static", static_path, follow_symlinks=True)
application.router.add_post("/api/v1/service/add", AddView) application.router.add_view("/api/v1/service/add", AddView)
application.router.add_view("/api/v1/service/pgp", PGPView)
application.router.add_view("/api/v1/service/rebuild", RebuildView)
application.router.add_view("/api/v1/service/remove", RemoveView)
application.router.add_view("/api/v1/service/request", RequestView)
application.router.add_view("/api/v1/service/search", SearchView)
application.router.add_view("/api/v1/service/update", UpdateView)
application.router.add_get("/api/v1/service/pgp", PGPView, allow_head=True) application.router.add_view("/api/v1/packages", PackagesView)
application.router.add_post("/api/v1/service/pgp", PGPView) application.router.add_view("/api/v1/packages/{package}", PackageView)
application.router.add_view("/api/v1/packages/{package}/logs", LogsView)
application.router.add_post("/api/v1/service/rebuild", RebuildView) application.router.add_view("/api/v1/status", StatusView)
application.router.add_post("/api/v1/service/remove", RemoveView) application.router.add_view("/api/v1/login", LoginView)
application.router.add_view("/api/v1/logout", LogoutView)
application.router.add_post("/api/v1/service/request", RequestView)
application.router.add_get("/api/v1/service/search", SearchView, allow_head=False)
application.router.add_post("/api/v1/service/update", UpdateView)
application.router.add_get("/api/v1/packages", PackagesView, allow_head=True)
application.router.add_post("/api/v1/packages", PackagesView)
application.router.add_delete("/api/v1/packages/{package}", PackageView)
application.router.add_get("/api/v1/packages/{package}", PackageView, allow_head=True)
application.router.add_post("/api/v1/packages/{package}", PackageView)
application.router.add_delete("/api/v1/packages/{package}/logs", LogsView)
application.router.add_get("/api/v1/packages/{package}/logs", LogsView, allow_head=True)
application.router.add_post("/api/v1/packages/{package}/logs", LogsView)
application.router.add_get("/api/v1/status", StatusView, allow_head=True)
application.router.add_post("/api/v1/status", StatusView)
application.router.add_get("/api/v1/login", LoginView)
application.router.add_post("/api/v1/login", LoginView)
application.router.add_post("/api/v1/logout", LogoutView)

View File

@ -0,0 +1,19 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

View File

@ -0,0 +1,35 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
class AURPackageSchema(Schema):
"""
response AUR package schema
"""
package = fields.String(required=True, metadata={
"description": "Package base",
"example": "ahriman",
})
description = fields.String(required=True, metadata={
"description": "Package description",
"example": "ArcH linux ReposItory MANager",
})

View File

@ -0,0 +1,30 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
class AuthSchema(Schema):
"""
request cookie authorization schema
"""
API_SESSION = fields.String(required=True, metadata={
"description": "API session key as returned from authorization",
})

View File

@ -0,0 +1,51 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
class CountersSchema(Schema):
"""
response package counters schema
"""
total = fields.Integer(required=True, metadata={
"description": "Total amount of packages",
"example": 6,
})
_unknown = fields.Integer(data_key="unknown", required=True, metadata={
"description": "Amount of packages in unknown state",
"example": 0,
})
pending = fields.Integer(required=True, metadata={
"description": "Amount of packages in pending state",
"example": 2,
})
building = fields.Integer(required=True, metadata={
"description": "Amount of packages in building state",
"example": 1,
})
failed = fields.Integer(required=True, metadata={
"description": "Amount of packages in failed state",
"example": 1,
})
success = fields.Integer(required=True, metadata={
"description": "Amount of packages in success state",
"example": 3,
})

View File

@ -0,0 +1,30 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
class ErrorSchema(Schema):
"""
response error schema
"""
error = fields.String(required=True, metadata={
"description": "Error description",
})

View File

@ -0,0 +1,49 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
from ahriman import version
from ahriman.web.schemas.counters_schema import CountersSchema
from ahriman.web.schemas.status_schema import StatusSchema
class InternalStatusSchema(Schema):
"""
response service status schema
"""
architecture = fields.String(required=True, metadata={
"description": "Repository architecture",
"example": "x86_64",
})
packages = fields.Nested(CountersSchema, required=True, metadata={
"description": "Repository package counters",
})
repository = fields.String(required=True, metadata={
"description": "Repository name",
"example": "repo-clone",
})
status = fields.Nested(StatusSchema, required=True, metadata={
"description": "Repository status as stored by web service",
})
version = fields.String(required=True, metadata={
"description": "Repository version",
"example": version.__version__,
})

View File

@ -0,0 +1,38 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
class LogSchema(Schema):
"""
request package log schema
"""
created = fields.Float(required=True, metadata={
"description": "Log record timestamp",
"example": 1680537091.233495,
})
process_id = fields.Integer(required=True, metadata={
"description": "Current process id",
"example": 42,
})
message = fields.String(required=True, metadata={
"description": "Log message",
})

View File

@ -0,0 +1,35 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
class LoginSchema(Schema):
"""
request login schema
"""
username = fields.String(required=True, metadata={
"description": "Login username",
"example": "user",
})
password = fields.String(required=True, metadata={
"description": "Login password",
"example": "pa55w0rd",
})

View File

@ -0,0 +1,39 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
from ahriman.web.schemas.status_schema import StatusSchema
class LogsSchema(Schema):
"""
response package logs schema
"""
package_base = fields.String(required=True, metadata={
"description": "Package base name",
"example": "ahriman",
})
status = fields.Nested(StatusSchema, required=True, metadata={
"description": "Last package status",
})
logs = fields.String(required=True, metadata={
"description": "Full package log from the last build",
})

View File

@ -0,0 +1,30 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
class OAuth2Schema(Schema):
"""
request OAuth2 authorization schema
"""
code = fields.String(metadata={
"description": "OAuth2 authorization code. In case if not set, the redirect to provider will be initiated",
})

View File

@ -0,0 +1,31 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
class PackageNameSchema(Schema):
"""
request package name schema
"""
package = fields.String(required=True, metadata={
"description": "Package name",
"example": "ahriman",
})

View File

@ -0,0 +1,31 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
class PackageNamesSchema(Schema):
"""
request package names schema
"""
packages = fields.List(fields.String(), required=True, metadata={
"description": "Package names",
"example": ["ahriman"],
})

View File

@ -0,0 +1,79 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
class PackagePropertiesSchema(Schema):
"""
request and response package properties schema
"""
architecture = fields.String(metadata={
"description": "Package architecture",
"example": "x86_64",
})
archive_size = fields.Integer(metadata={
"description": "Archive size in bytes",
"example": 287989,
})
build_date = fields.Integer(metadata={
"description": "Package build timestamp",
"example": 1680537091,
})
depends = fields.List(fields.String(), metadata={
"description": "Package dependencies list",
"example": ["devtools"],
})
make_depends = fields.List(fields.String(), metadata={
"description": "Package make dependencies list",
"example": ["python-build"],
})
opt_depends = fields.List(fields.String(), metadata={
"description": "Package optional dependencies list",
"example": ["python-aiohttp"],
})
description = fields.String(metadata={
"description": "Package description",
"example": "ArcH linux ReposItory MANager",
})
filename = fields.String(metadata={
"description": "Package file name",
"example": "ahriman-2.7.1-1-any.pkg.tar.zst",
})
groups = fields.List(fields.String(), metadata={
"description": "Package groups",
"example": ["base-devel"],
})
installed_size = fields.Integer(metadata={
"description": "Installed package size in bytes",
"example": 2047658,
})
licenses = fields.List(fields.String(), metadata={
"description": "Package licenses",
"example": ["GPL3"],
})
provides = fields.List(fields.String(), metadata={
"description": "Package provides list",
"example": ["ahriman-git"],
})
url = fields.String(metadata={
"description": "Upstream url",
"example": "https://github.com/arcan1s/ahriman",
})

View File

@ -0,0 +1,46 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
from ahriman import version
from ahriman.web.schemas.package_properties_schema import PackagePropertiesSchema
from ahriman.web.schemas.remote_schema import RemoteSchema
class PackageSchema(Schema):
"""
request and response package schema
"""
base = fields.String(required=True, metadata={
"description": "Package base",
"example": "ahriman",
})
version = fields.String(required=True, metadata={
"description": "Package version",
"example": version.__version__,
})
remote = fields.Nested(RemoteSchema, required=True, metadata={
"description": "Package remote properties",
})
packages = fields.Dict(
keys=fields.String(), values=fields.Nested(PackagePropertiesSchema), required=True, metadata={
"description": "Packages which belong to this base",
})

View File

@ -0,0 +1,50 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
from ahriman.models.build_status import BuildStatusEnum
from ahriman.web.schemas.package_schema import PackageSchema
from ahriman.web.schemas.status_schema import StatusSchema
class PackageStatusSimplifiedSchema(Schema):
"""
special request package status schema
"""
package = fields.Nested(PackageSchema, metadata={
"description": "Package description",
})
status = fields.Enum(BuildStatusEnum, by_value=True, required=True, metadata={
"description": "Current status",
})
class PackageStatusSchema(Schema):
"""
response package status schema
"""
package = fields.Nested(PackageSchema, required=True, metadata={
"description": "Package description",
})
status = fields.Nested(StatusSchema, required=True, metadata={
"description": "Last package status",
})

View File

@ -0,0 +1,35 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
class PGPKeyIdSchema(Schema):
"""
request PGP key ID schema
"""
key = fields.String(required=True, metadata={
"description": "PGP key ID",
"example": "0xE989490C",
})
server = fields.String(required=True, metadata={
"description": "PGP key server",
"example": "keyserver.ubuntu.com",
})

View File

@ -0,0 +1,30 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
class PGPKeySchema(Schema):
"""
response PGP key schema
"""
key = fields.String(required=True, metadata={
"description": "PGP key body",
})

View File

@ -0,0 +1,48 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
from ahriman.models.package_source import PackageSource
class RemoteSchema(Schema):
"""
request and response package remote schema
"""
branch = fields.String(required=True, metadata={
"description": "Repository branch",
"example": "master",
})
git_url = fields.String(required=True, metadata={
"description": "Package git url",
"example": "https://aur.archlinux.org/ahriman.git",
})
path = fields.String(required=True, metadata={
"description": "Path to package sources in git repository",
"example": ".",
})
source = fields.Enum(PackageSource, by_value=True, required=True, metadata={
"description": "Pacakge source",
})
web_url = fields.String(required=True, metadata={
"description": "Package repository page",
"example": "https://aur.archlinux.org/packages/ahriman",
})

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