Compare commits

...

12 Commits

Author SHA1 Message Date
a6c8d64053 Release 2.0.0rc6 2022-04-09 17:34:23 +03:00
fd78f2b5e2 do not render failed packages in jinja (#57)
basic templates require package info which is unavailable if package
wasn't built
2022-04-09 17:31:13 +03:00
900907cdaa Release 2.0.0rc5 2022-04-08 04:42:05 +03:00
5ff2f43506 change telegram default index to telegram-index 2022-04-08 04:32:34 +03:00
dd521b49b5 force git run from the same dir to clone 2022-04-08 04:04:06 +03:00
5b1f5a8473 fix users migration 2022-04-08 03:45:17 +03:00
86af13f09e add telegram integraion 2022-04-08 03:41:07 +03:00
733c014229 Release 2.0.0rc4 2022-04-08 01:14:35 +03:00
783c16b2ed trim versions before dependency list calculation
When dependencies list contains same package with version it tries to
find packages which don't exists
2022-04-07 20:32:55 +03:00
2536b8dc1f add support of repository restoration 2022-04-07 04:49:07 +03:00
e200ac9776 add support of officiall repositories api 2022-04-07 04:19:37 +03:00
6946745153 fix descriptions 2022-04-06 01:48:03 +03:00
67 changed files with 4104 additions and 2973 deletions

View File

@ -13,7 +13,7 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
* Multi-architecture support. * Multi-architecture support.
* VCS packages support. * VCS packages support.
* Sign support with gpg (repository, package, per package settings). * Sign support with gpg (repository, package, per package settings).
* Synchronization to remote services (rsync, s3 and github) and report generation (html). * Synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram).
* Dependency manager. * Dependency manager.
* Ability to patch AUR packages and even create package from local PKGBUILDs. * Ability to patch AUR packages and even create package from local PKGBUILDs.
* Repository status interface with optional authorization and control options: * Repository status interface with optional authorization and control options:

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 503 KiB

After

Width:  |  Height:  |  Size: 509 KiB

View File

@ -3,7 +3,7 @@
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS
.B ahriman .B ahriman
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-v] {aur-search,search,help,help-commands-unsafe,key-import,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,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-update,update,user-add,user-list,user-remove,web} ... [-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-v] {aur-search,search,help,help-commands-unsafe,key-import,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,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-update,update,user-add,user-list,user-remove,web} ...
.SH DESCRIPTION .SH DESCRIPTION
ArcH Linux ReposItory MANager ArcH Linux ReposItory MANager
.SH OPTIONS .SH OPTIONS
@ -97,6 +97,9 @@ remove unknown packages
\fBahriman\fR \fI\,repo-report\/\fR \fBahriman\fR \fI\,repo-report\/\fR
generate report generate report
.TP .TP
\fBahriman\fR \fI\,repo-restore\/\fR
restore repository
.TP
\fBahriman\fR \fI\,repo-setup\/\fR \fBahriman\fR \fI\,repo-setup\/\fR
initial service configuration initial service configuration
.TP .TP
@ -207,7 +210,7 @@ key server for key import
.SH OPTIONS 'ahriman package-add' .SH OPTIONS 'ahriman package-add'
usage: ahriman package-add [-h] [-e] [-n] usage: ahriman package-add [-h] [-e] [-n]
[-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}] [-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}]
[--without-dependencies] [--without-dependencies]
package [package ...] package [package ...]
@ -226,7 +229,7 @@ return non\-zero exit status if result is empty
run update function after run update function after
.TP .TP
\fB\-s\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}, \fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote} \fB\-s\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}, \fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}
explicitly specify the package source for this command explicitly specify the package source for this command
.TP .TP
@ -235,7 +238,7 @@ do not add dependencies
.SH OPTIONS 'ahriman add' .SH OPTIONS 'ahriman add'
usage: ahriman package-add [-h] [-e] [-n] usage: ahriman package-add [-h] [-e] [-n]
[-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}] [-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}]
[--without-dependencies] [--without-dependencies]
package [package ...] package [package ...]
@ -254,7 +257,7 @@ return non\-zero exit status if result is empty
run update function after run update function after
.TP .TP
\fB\-s\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}, \fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote} \fB\-s\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}, \fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}
explicitly specify the package source for this command explicitly specify the package source for this command
.TP .TP
@ -263,7 +266,7 @@ do not add dependencies
.SH OPTIONS 'ahriman package-update' .SH OPTIONS 'ahriman package-update'
usage: ahriman package-add [-h] [-e] [-n] usage: ahriman package-add [-h] [-e] [-n]
[-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}] [-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}]
[--without-dependencies] [--without-dependencies]
package [package ...] package [package ...]
@ -282,7 +285,7 @@ return non\-zero exit status if result is empty
run update function after run update function after
.TP .TP
\fB\-s\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}, \fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote} \fB\-s\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}, \fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote,PackageSource.Repository}
explicitly specify the package source for this command explicitly specify the package source for this command
.TP .TP
@ -615,6 +618,42 @@ generate repository report according to current settings
target to generate report target to generate report
.SH OPTIONS 'ahriman repo-restore'
usage: ahriman repo-restore [-h] [-e] [-n] [--without-dependencies]
restore repository from database file
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.TP
\fB\-n\fR, \fB\-\-now\fR
run update function after
.TP
\fB\-\-without\-dependencies\fR
do not add dependencies
.SH OPTIONS 'ahriman restore'
usage: ahriman repo-restore [-h] [-e] [-n] [--without-dependencies]
restore repository from database file
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.TP
\fB\-n\fR, \fB\-\-now\fR
run update function after
.TP
\fB\-\-without\-dependencies\fR
do not add dependencies
.SH OPTIONS 'ahriman repo-setup' .SH OPTIONS 'ahriman repo-setup'
usage: ahriman repo-setup [-h] [--build-as-user BUILD_AS_USER] [--build-command BUILD_COMMAND] usage: ahriman repo-setup [-h] [--build-as-user BUILD_AS_USER] [--build-command BUILD_COMMAND]
[--from-configuration FROM_CONFIGURATION] [--no-multilib] --packager PACKAGER --repository [--from-configuration FROM_CONFIGURATION] [--no-multilib] --packager PACKAGER --repository

View File

@ -114,6 +114,18 @@ Section name must be either `html` (plus optional architecture name, e.g. `html:
* `link_path` - prefix for HTML links, string, required. * `link_path` - prefix for HTML links, string, required.
* `template_path` - path to Jinja2 template, string, required. * `template_path` - path to Jinja2 template, string, required.
### `telegram` type
Section name must be either `telegram` (plus optional architecture name, e.g. `telegram:x86_64`) or random name with `type` set.
* `type` - type of the report, string, optional, must be set to `telegram` if exists.
* `api_key` - telegram bot API key, string, required. Please refer FAQ about how to create chat and bot
* `chat_id` - telegram chat id, either string with `@` or integer value, required.
* `homepage` - link to homepage, string, optional.
* `link_path` - prefix for HTML links, string, required.
* `template_path` - path to Jinja2 template, string, required.
* `template_type` - `parse_mode` to be passed to telegram API, one of `MarkdownV2`, `HTML`, `Markdown`, string, optional, default `HTML`.
## `upload` group ## `upload` group
Remote synchronization settings. Remote synchronization settings.

View File

@ -402,6 +402,46 @@ There are several choices:
After these steps `index.html` file will be automatically synced to S3 After these steps `index.html` file will be automatically synced to S3
### I would like to get messages to my telegram account/channel
1. It still requires additional dependencies:
```shell
yay -S python-jinja
```
2. Register bot in telegram. You can do it by using by talking with [@BotFather](https://t.me/botfather). For more details please refer to [official documentation](https://core.telegram.org/bots).
3. Optionally (if you want to post message in chat):
1. Create telegram channel.
2. Invite your bot into the channel.
3. Make your channel public
4. Get chat id if you want to use by numerical id or just use id prefixed with `@` (e.g. `@ahriman`). If you are not using chat the chat id is your user id. If you don't want to make channel public you can use [this guide](https://stackoverflow.com/a/33862907).
5. Configure the service:
```ini
[report]
target = telegram
[telegram]
api_key = aaAAbbBBccCC
chat_id = @ahriman
link_path = http://example.com/x86_64
```
`api_key` is the one sent by [@BotFather](https://t.me/botfather), `chat_id` is the value retrieved from previous step.
If you did everything fine you should receive the message with the next update. Quick credentials check can be done by using the following command:
```shell
curl 'https://api.telegram.org/bot${CHAT_ID}/sendMessage?chat_id=${API_KEY}&text=hello'
```
(replace `${CHAT_ID}` and `${API_KEY}` with the values from configuration).
## Web service ## Web service
### Readme mentions web interface, how do I use it? ### Readme mentions web interface, how do I use it?

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev # Maintainer: Evgeniy Alekseev
pkgname='ahriman' pkgname='ahriman'
pkgver=2.0.0rc3 pkgver=2.0.0rc6
pkgrel=1 pkgrel=1
pkgdesc="ArcH Linux ReposItory MANager" pkgdesc="ArcH Linux ReposItory MANager"
arch=('any') arch=('any')
@ -38,7 +38,7 @@ build() {
package() { package() {
cd "$pkgname" cd "$pkgname"
python -m installer --destdir="$pkgdir" dist/*.whl python -m installer --destdir="$pkgdir" "dist/$pkgname-$pkgver-py3-none-any.whl"
# python-installer actually thinks that you cannot just copy files to root # python-installer actually thinks that you cannot just copy files to root
# thus we need to copy them manually # thus we need to copy them manually

View File

@ -45,6 +45,9 @@ ssl = disabled
[html] [html]
template_path = /usr/share/ahriman/templates/repo-index.jinja2 template_path = /usr/share/ahriman/templates/repo-index.jinja2
[telegram]
template_path = /usr/share/ahriman/templates/telegram-index.jinja2
[upload] [upload]
target = target =

View File

@ -0,0 +1,4 @@
{#simplified version of full report#}
<b>{{ repository }} update</b>
{% for package in packages %}
<a href="{{ link_path }}/{{ package.filename }}">{{ package.name }}</a> {{ package.version }}{% endfor %}

View File

@ -67,6 +67,7 @@ setup(
"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/repo-index.jinja2", "package/share/ahriman/templates/repo-index.jinja2",
"package/share/ahriman/templates/telegram-index.jinja2",
]), ]),
("share/ahriman/templates/build-status", [ ("share/ahriman/templates/build-status", [
"package/share/ahriman/templates/build-status/login-modal.jinja2", "package/share/ahriman/templates/build-status/login-modal.jinja2",

View File

@ -89,6 +89,7 @@ def _parser() -> argparse.ArgumentParser:
_set_repo_rebuild_parser(subparsers) _set_repo_rebuild_parser(subparsers)
_set_repo_remove_unknown_parser(subparsers) _set_repo_remove_unknown_parser(subparsers)
_set_repo_report_parser(subparsers) _set_repo_report_parser(subparsers)
_set_repo_restore_parser(subparsers)
_set_repo_setup_parser(subparsers) _set_repo_setup_parser(subparsers)
_set_repo_sign_parser(subparsers) _set_repo_sign_parser(subparsers)
_set_repo_status_update_parser(subparsers) _set_repo_status_update_parser(subparsers)
@ -409,6 +410,21 @@ def _set_repo_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser return parser
def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package addition subcommand
:param root: subparsers for the commands
:return: created argument parser
"""
parser = root.add_parser("repo-restore", aliases=["restore"], help="restore repository",
description="restore repository from database file", formatter_class=_formatter)
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("--without-dependencies", help="do not add dependencies", action="store_true")
parser.set_defaults(handler=handlers.Add, package=None, source=PackageSource.AUR)
return parser
def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser: def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for setup subcommand add parser for setup subcommand

View File

@ -67,7 +67,8 @@ class Packages(Properties):
:param without_dependencies: if set, dependency check will be disabled :param without_dependencies: if set, dependency check will be disabled
""" """
package = Package.load(source, PackageSource.AUR, self.repository.pacman, self.repository.aur_url) package = Package.load(source, PackageSource.AUR, self.repository.pacman, self.repository.aur_url)
self.repository.database.build_queue_insert(package)
self.database.build_queue_insert(package)
with tmpdir() as local_path: with tmpdir() as local_path:
Sources.load(local_path, package.git_url, self.database.patches_get(package.base)) Sources.load(local_path, package.git_url, self.database.patches_get(package.base))
@ -93,7 +94,8 @@ class Packages(Properties):
cache_dir = self.repository.paths.cache_for(package.base) cache_dir = self.repository.paths.cache_for(package.base)
shutil.copytree(Path(source), cache_dir) # copy package to store in caches shutil.copytree(Path(source), 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.repository.database.build_queue_insert(package)
self.database.build_queue_insert(package)
self._process_dependencies(cache_dir, known_packages, without_dependencies) self._process_dependencies(cache_dir, known_packages, without_dependencies)

View File

@ -19,7 +19,7 @@
# #
import argparse import argparse
from typing import Type from typing import List, Type
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
@ -43,10 +43,20 @@ class Add(Handler):
:param unsafe: if set no user check will be performed before path creation :param unsafe: if set no user check will be performed before path creation
""" """
application = Application(architecture, configuration, no_report, unsafe) application = Application(architecture, configuration, no_report, unsafe)
application.add(args.package, args.source, args.without_dependencies) packages = Add.extract_packages(application) if args.package is None else args.package
application.add(packages, args.source, args.without_dependencies)
if not args.now: if not args.now:
return return
packages = application.updates(args.package, True, True, False, True, application.logger.info) packages = application.updates(packages, True, True, False, True, application.logger.info)
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)
@staticmethod
def extract_packages(application: Application) -> List[str]:
"""
extract packages from database file
:param application: application instance
:return: list of packages which were stored in database
"""
return [package.base for (package, _) in application.database.packages_get()]

View File

@ -23,7 +23,8 @@ from dataclasses import fields
from typing import Callable, Iterable, List, Tuple, Type from typing import Callable, Iterable, List, Tuple, Type
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.alpm.aur import AUR from ahriman.core.alpm.remote.aur import AUR
from ahriman.core.alpm.remote.official import Official
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InvalidOption from ahriman.core.exceptions import InvalidOption
from ahriman.core.formatters.aur_printer import AurPrinter from ahriman.core.formatters.aur_printer import AurPrinter
@ -50,10 +51,14 @@ class Search(Handler):
:param no_report: force disable reporting :param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation :param unsafe: if set no user check will be performed before path creation
""" """
packages_list = AUR.multisearch(*args.search) official_packages_list = Official.multisearch(*args.search)
Search.check_if_empty(args.exit_code, not packages_list) aur_packages_list = AUR.multisearch(*args.search)
for package in Search.sort(packages_list, args.sort_by): Search.check_if_empty(args.exit_code, not official_packages_list and not aur_packages_list)
AurPrinter(package).print(args.info)
for packages_list in (official_packages_list, aur_packages_list):
# keep sorting by packages source
for package in Search.sort(packages_list, args.sort_by):
AurPrinter(package).print(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

@ -0,0 +1,19 @@
#
# Copyright (c) 2021-2022 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

@ -17,24 +17,21 @@
# 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
import logging
import requests import requests
from typing import Any, Dict, List, Optional, Type from typing import Any, Dict, List, Optional
from ahriman.core.alpm.remote.remote import Remote
from ahriman.core.exceptions import InvalidPackageInfo from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.core.util import exception_response_text from ahriman.core.util import exception_response_text
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
class AUR: class AUR(Remote):
""" """
AUR RPC wrapper AUR RPC wrapper
:cvar DEFAULT_RPC_URL: default AUR RPC url :cvar DEFAULT_RPC_URL: default AUR RPC url
:cvar DEFAULT_RPC_VERSION: default AUR RPC version :cvar DEFAULT_RPC_VERSION: default AUR RPC version
:ivar logger: class logger
:ivar rpc_url: AUR RPC url :ivar rpc_url: AUR RPC url
:ivar rpc_version: AUR RPC version :ivar rpc_version: AUR RPC version
""" """
@ -48,46 +45,9 @@ class AUR:
:param rpc_url: AUR RPC url :param rpc_url: AUR RPC url
:param rpc_version: AUR RPC version :param rpc_version: AUR RPC version
""" """
Remote.__init__(self)
self.rpc_url = rpc_url or self.DEFAULT_RPC_URL self.rpc_url = rpc_url or self.DEFAULT_RPC_URL
self.rpc_version = rpc_version or self.DEFAULT_RPC_VERSION self.rpc_version = rpc_version or self.DEFAULT_RPC_VERSION
self.logger = logging.getLogger("build_details")
@classmethod
def info(cls: Type[AUR], package_name: str) -> AURPackage:
"""
get package info by its name
:param package_name: package name to search
:return: package which match the package name
"""
return cls().package_info(package_name)
@classmethod
def multisearch(cls: Type[AUR], *keywords: str) -> List[AURPackage]:
"""
search in AUR by using API with multiple words. This method is required in order to handle
https://bugs.archlinux.org/task/49133. In addition short words will be dropped
:param keywords: search terms, e.g. "ahriman", "is", "cool"
:return: list of packages each of them matches all search terms
"""
instance = cls()
packages: Dict[str, AURPackage] = {}
for term in filter(lambda word: len(word) > 3, keywords):
portion = instance.search(term)
packages = {
package.package_base: package
for package in portion
if package.package_base in packages or not packages
}
return list(packages.values())
@classmethod
def search(cls: Type[AUR], *keywords: str) -> List[AURPackage]:
"""
search package in AUR web
:param keywords: keywords to search
:return: list of packages which match the criteria
"""
return cls().package_search(*keywords)
@staticmethod @staticmethod
def parse_response(response: Dict[str, Any]) -> List[AURPackage]: def parse_response(response: Dict[str, Any]) -> List[AURPackage]:
@ -144,11 +104,10 @@ class AUR:
packages = self.make_request("info", package_name) packages = self.make_request("info", package_name)
return next(package for package in packages if package.name == package_name) return next(package for package in packages if package.name == package_name)
def package_search(self, *keywords: str, by: str = "name-desc") -> List[AURPackage]: def package_search(self, *keywords: str) -> List[AURPackage]:
""" """
search package in AUR web search package in AUR web
:param keywords: keywords to search :param keywords: keywords to search
:param by: search by the field
:return: list of packages which match the criteria :return: list of packages which match the criteria
""" """
return self.make_request("search", *keywords, by=by) return self.make_request("search", *keywords, by="name-desc")

View File

@ -0,0 +1,91 @@
#
# Copyright (c) 2021-2022 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 requests
from typing import Any, Dict, List, Optional
from ahriman.core.alpm.remote.remote import Remote
from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.core.util import exception_response_text
from ahriman.models.aur_package import AURPackage
class Official(Remote):
"""
official repository RPC wrapper
:cvar DEFAULT_RPC_URL: default AUR RPC url
:ivar rpc_url: AUR RPC url
"""
DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json"
def __init__(self, rpc_url: Optional[str] = None) -> None:
"""
default constructor
:param rpc_url: AUR RPC url
"""
Remote.__init__(self)
self.rpc_url = rpc_url or self.DEFAULT_RPC_URL
@staticmethod
def parse_response(response: Dict[str, Any]) -> List[AURPackage]:
"""
parse RPC response to package list
:param response: RPC response json
:return: list of parsed packages
"""
if not response["valid"]:
raise InvalidPackageInfo("API validation error")
return [AURPackage.from_repo(package) for package in response["results"]]
def make_request(self, *args: str, by: str) -> List[AURPackage]:
"""
perform request to official repositories RPC
:param args: list of arguments to be passed as args query parameter
:param by: search by the field
:return: response parsed to package list
"""
try:
response = requests.get(self.rpc_url, params={by: args})
response.raise_for_status()
return self.parse_response(response.json())
except requests.HTTPError as e:
self.logger.exception("could not perform request: %s", exception_response_text(e))
raise
except Exception:
self.logger.exception("could not perform request")
raise
def package_info(self, package_name: str) -> AURPackage:
"""
get package info by its name
:param package_name: package name to search
:return: package which match the package name
"""
packages = self.make_request(package_name, by="name")
return next(package for package in packages if package.name == package_name)
def package_search(self, *keywords: str) -> List[AURPackage]:
"""
search package in AUR web
:param keywords: keywords to search
:return: list of packages which match the criteria
"""
return self.make_request(*keywords, by="q")

View File

@ -0,0 +1,92 @@
#
# Copyright (c) 2021-2022 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 logging
from typing import Dict, List, Type
from ahriman.models.aur_package import AURPackage
class Remote:
"""
base class for remote package search
:ivar logger: class logger
"""
def __init__(self) -> None:
"""
default constructor
"""
self.logger = logging.getLogger("build_details")
@classmethod
def info(cls: Type[Remote], package_name: str) -> AURPackage:
"""
get package info by its name
:param package_name: package name to search
:return: package which match the package name
"""
return cls().package_info(package_name)
@classmethod
def multisearch(cls: Type[Remote], *keywords: str) -> List[AURPackage]:
"""
search in remote repository by using API with multiple words. This method is required in order to handle
https://bugs.archlinux.org/task/49133. In addition, short words will be dropped
:param keywords: search terms, e.g. "ahriman", "is", "cool"
:return: list of packages each of them matches all search terms
"""
instance = cls()
packages: Dict[str, AURPackage] = {}
for term in filter(lambda word: len(word) > 3, keywords):
portion = instance.search(term)
packages = {
package.name: package # not mistake to group them by name
for package in portion
if package.name in packages or not packages
}
return list(packages.values())
@classmethod
def search(cls: Type[Remote], *keywords: str) -> List[AURPackage]:
"""
search package in AUR web
:param keywords: keywords to search
:return: list of packages which match the criteria
"""
return cls().package_search(*keywords)
def package_info(self, package_name: str) -> AURPackage:
"""
get package info by its name
:param package_name: package name to search
:return: package which match the package name
"""
raise NotImplementedError
def package_search(self, *keywords: str) -> List[AURPackage]:
"""
search package in AUR web
:param keywords: keywords to search
:return: list of packages which match the criteria
"""
raise NotImplementedError

View File

@ -86,7 +86,8 @@ class Sources:
Sources.logger.warning("%s is not initialized, but no remote provided", sources_dir) Sources.logger.warning("%s is not initialized, but no remote provided", sources_dir)
else: else:
Sources.logger.info("clone remote %s to %s", remote, sources_dir) Sources.logger.info("clone remote %s to %s", remote, sources_dir)
Sources._check_output("git", "clone", remote, str(sources_dir), exception=None, logger=Sources.logger) Sources._check_output("git", "clone", remote, str(sources_dir),
exception=None, cwd=sources_dir, logger=Sources.logger)
# and now force reset to our branch # and now force reset to our branch
Sources._check_output("git", "checkout", "--force", Sources._branch, Sources._check_output("git", "checkout", "--force", Sources._branch,
exception=None, cwd=sources_dir, logger=Sources.logger) exception=None, cwd=sources_dir, logger=Sources.logger)

View File

@ -20,9 +20,9 @@
from sqlite3 import Connection from sqlite3 import Connection
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
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.patches import migrate_patches
from ahriman.core.database.data.users import migrate_users_data from ahriman.core.database.data.users import migrate_users_data
from ahriman.core.database.data.package_statuses import migrate_package_statuses
from ahriman.models.migration_result import MigrationResult from ahriman.models.migration_result import MigrationResult
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -37,7 +37,7 @@ def migrate_data(result: MigrationResult, connection: Connection,
:param paths: repository paths instance :param paths: repository paths instance
""" """
# initial data migration # initial data migration
if result.old_version == 0: if result.old_version <= 0:
migrate_package_statuses(connection, paths) migrate_package_statuses(connection, paths)
migrate_users_data(connection, configuration)
migrate_patches(connection, paths) migrate_patches(connection, paths)
migrate_users_data(connection, configuration)

View File

@ -32,9 +32,9 @@ def migrate_users_data(connection: Connection, configuration: Configuration) ->
for option, value in configuration[section].items(): for option, value in configuration[section].items():
if not section.startswith("auth:"): if not section.startswith("auth:"):
continue continue
permission = section[5:] access = section[5:]
connection.execute( connection.execute(
"""insert into users (username, permission, password) values (:username, :permission, :password)""", """insert into users (username, access, password) values (:username, :access, :password)""",
{"username": option.lower(), "permission": permission, "password": value}) {"username": option.lower(), "access": access, "password": value})
connection.commit() connection.commit()

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from __future__ import annotations
from sqlite3 import Connection from sqlite3 import Connection
from typing import List, Optional from typing import List, Optional
@ -75,7 +73,7 @@ class AuthOperations(Operations):
def user_update(self, user: User) -> None: def user_update(self, user: User) -> None:
""" """
get user by username update user by username
:param user: user descriptor :param user: user descriptor
""" """
def run(connection: Connection) -> None: def run(connection: Connection) -> None:

View File

@ -22,6 +22,7 @@ from __future__ import annotations
import json import json
import sqlite3 import sqlite3
from pathlib import Path
from sqlite3 import Connection from sqlite3 import Connection
from typing import Type from typing import Type
@ -46,10 +47,20 @@ class SQLite(AuthOperations, BuildOperations, PackageOperations, PatchOperations
:param configuration: configuration instance :param configuration: configuration instance
:return: fully initialized instance of the database :return: fully initialized instance of the database
""" """
database = cls(configuration.getpath("settings", "database")) path = cls.database_path(configuration)
database = cls(path)
database.init(configuration) database.init(configuration)
return database return database
@staticmethod
def database_path(configuration: Configuration) -> Path:
"""
read database from configuration
:param configuration: configuration instance
:return: database path according to the configuration
"""
return configuration.getpath("settings", "database")
def init(self, configuration: Configuration) -> None: def init(self, configuration: Configuration) -> None:
""" """
perform database migrations perform database migrations

View File

@ -101,7 +101,7 @@ class JinjaTemplate:
"name": package, "name": package,
"url": properties.url or "", "url": properties.url or "",
"version": base.version "version": base.version
} for base in result.updated for package, properties in base.packages.items() } for base in result.success for package, properties in base.packages.items()
] ]
comparator: Callable[[Dict[str, str]], str] = lambda item: item["filename"] comparator: Callable[[Dict[str, str]], str] = lambda item: item["filename"]

View File

@ -68,6 +68,9 @@ class Report:
if provider == ReportSettings.Console: if provider == ReportSettings.Console:
from ahriman.core.report.console import Console from ahriman.core.report.console import Console
return Console(architecture, configuration, section) return Console(architecture, configuration, section)
if provider == ReportSettings.Telegram:
from ahriman.core.report.telegram import Telegram
return Telegram(architecture, configuration, section)
return cls(architecture, configuration) # should never happen return cls(architecture, configuration) # should never happen
def generate(self, packages: Iterable[Package], result: Result) -> None: def generate(self, packages: Iterable[Package], result: Result) -> None:

View File

@ -0,0 +1,95 @@
#
# Copyright (c) 2021-2022 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/>.
#
# technically we could use python-telegram-bot, but it is just a single request, cmon
import requests
from typing import Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.report.report import Report
from ahriman.core.util import exception_response_text
from ahriman.models.package import Package
from ahriman.models.result import Result
class Telegram(Report, JinjaTemplate):
"""
telegram report generator
:cvar TELEGRAM_API_URL: telegram api base url
:cvar TELEGRAM_MAX_CONTENT_LENGTH: max content length of the message
:ivar api_key: bot api key
:ivar chat_id: chat id to post message, either string with @ or integer
:ivar template_path: path to template for built packages
:ivar template_type: template message type to be used in parse mode, one of MarkdownV2, HTML, Markdown
"""
TELEGRAM_API_URL = "https://api.telegram.org"
TELEGRAM_MAX_CONTENT_LENGTH = 4096
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param section: settings section name
"""
Report.__init__(self, architecture, configuration)
JinjaTemplate.__init__(self, section, configuration)
self.api_key = configuration.get(section, "api_key")
self.chat_id = configuration.get(section, "chat_id")
self.template_path = configuration.getpath(section, "template_path")
self.template_type = configuration.get(section, "template_type", fallback="HTML")
def _send(self, text: str) -> None:
"""
send message to telegram channel
:param text: message body text
"""
try:
response = requests.post(
f"{self.TELEGRAM_API_URL}/bot{self.api_key}/sendMessage",
data={"chat_id": self.chat_id, "text": text, "parse_mode": self.template_type})
response.raise_for_status()
except requests.HTTPError as e:
self.logger.exception("could not perform request: %s", exception_response_text(e))
raise
except Exception:
self.logger.exception("could not perform request")
raise
def generate(self, packages: Iterable[Package], result: Result) -> None:
"""
generate report for the specified packages
:param packages: list of packages to generate report
:param result: build result
"""
if not result.success:
return
text = self.make_html(result, self.template_path)
# telegram content is limited by 4096 symbols, so we are going to split the message by new lines
# to fit into this restriction
if len(text) > self.TELEGRAM_MAX_CONTENT_LENGTH:
position = text.rfind("\n", 0, self.TELEGRAM_MAX_CONTENT_LENGTH)
portion, text = text[:position], text[position + 1:] # +1 to exclude newline we split
self._send(portion)
# send remaining (or full in case if size is less than max length) text
self._send(text)

View File

@ -19,14 +19,11 @@
# #
from __future__ import annotations from __future__ import annotations
import shutil
import tempfile
from pathlib import Path
from typing import Iterable, List, Set, Type from typing import Iterable, List, Set, Type
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
from ahriman.core.database.sqlite import SQLite from ahriman.core.database.sqlite import SQLite
from ahriman.core.util import tmpdir
from ahriman.models.package import Package from ahriman.models.package import Package
@ -61,12 +58,9 @@ class Leaf:
:param database: database instance :param database: database instance
:return: loaded class :return: loaded class
""" """
clone_dir = Path(tempfile.mkdtemp()) with tmpdir() as clone_dir:
try:
Sources.load(clone_dir, package.git_url, database.patches_get(package.base)) Sources.load(clone_dir, package.git_url, database.patches_get(package.base))
dependencies = Package.dependencies(clone_dir) dependencies = Package.dependencies(clone_dir)
finally:
shutil.rmtree(clone_dir, ignore_errors=True)
return cls(package, dependencies) return cls(package, dependencies)
def is_root(self, packages: Iterable[Leaf]) -> bool: def is_root(self, packages: Iterable[Leaf]) -> bool:

View File

@ -116,6 +116,18 @@ def filter_json(source: Dict[str, Any], known_fields: Iterable[str]) -> Dict[str
return {key: value for key, value in source.items() if key in known_fields and value is not None} return {key: value for key, value in source.items() if key in known_fields and value is not None}
def full_version(epoch: Union[str, int, None], pkgver: str, pkgrel: str) -> str:
"""
generate full version from components
:param epoch: package epoch if any
:param pkgver: package version
:param pkgrel: package release version (arch linux specific)
:return: generated version
"""
prefix = f"{epoch}:" if epoch else ""
return f"{prefix}{pkgver}-{pkgrel}"
def package_like(filename: Path) -> bool: def package_like(filename: Path) -> bool:
""" """
check if file looks like package check if file looks like package

View File

@ -25,7 +25,7 @@ import inflection
from dataclasses import dataclass, field, fields from dataclasses import dataclass, field, fields
from typing import Any, Callable, Dict, List, Optional, Type from typing import Any, Callable, Dict, List, Optional, Type
from ahriman.core.util import filter_json from ahriman.core.util import filter_json, full_version
@dataclass @dataclass
@ -59,12 +59,12 @@ class AURPackage:
package_base_id: int package_base_id: int
package_base: str package_base: str
version: str version: str
description: str
num_votes: int num_votes: int
popularity: float popularity: float
first_submitted: datetime.datetime first_submitted: datetime.datetime
last_modified: datetime.datetime last_modified: datetime.datetime
url_path: str url_path: str
description: str = "" # despite the fact that the field is required some packages don't have it
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
@ -88,6 +88,39 @@ class AURPackage:
properties = cls.convert(dump) properties = cls.convert(dump)
return cls(**filter_json(properties, known_fields)) return cls(**filter_json(properties, known_fields))
@classmethod
def from_repo(cls: Type[AURPackage], dump: Dict[str, Any]) -> AURPackage:
"""
construct package descriptor from official repository RPC properties
:param dump: json dump body
:return: AUR package descriptor
"""
return cls(
id=0,
name=dump["pkgname"],
package_base_id=0,
package_base=dump["pkgbase"],
version=full_version(dump["epoch"], dump["pkgver"], dump["pkgrel"]),
description=dump["pkgdesc"],
num_votes=0,
popularity=0.0,
first_submitted=datetime.datetime.utcfromtimestamp(0),
last_modified=datetime.datetime.strptime(dump["last_update"], "%Y-%m-%dT%H:%M:%S.%fZ"),
url_path="",
url=dump["url"],
out_of_date=datetime.datetime.strptime(
dump["flag_date"],
"%Y-%m-%dT%H:%M:%S.%fZ") if dump["flag_date"] is not None else None,
maintainer=next(iter(dump["maintainers"]), None),
depends=dump["depends"],
make_depends=dump["makedepends"],
opt_depends=dump["optdepends"],
conflicts=dump["conflicts"],
provides=dump["provides"],
license=dump["licenses"],
keywords=[],
)
@staticmethod @staticmethod
def convert(descriptor: Dict[str, Any]) -> Dict[str, Any]: def convert(descriptor: Dict[str, Any]) -> Dict[str, Any]:
""" """

View File

@ -26,12 +26,13 @@ from dataclasses import asdict, dataclass
from pathlib import Path from pathlib import Path
from pyalpm import vercmp # type: ignore from pyalpm import vercmp # type: ignore
from srcinfo.parse import parse_srcinfo # type: ignore from srcinfo.parse import parse_srcinfo # type: ignore
from typing import Any, Dict, Iterable, List, Optional, Set, Type from typing import Any, Dict, Iterable, List, Set, Type
from ahriman.core.alpm.aur import AUR
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote.aur import AUR
from ahriman.core.alpm.remote.official import Official
from ahriman.core.exceptions import InvalidPackageInfo from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.core.util import check_output from ahriman.core.util import check_output, full_version
from ahriman.models.package_description import PackageDescription from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -144,7 +145,7 @@ class Package:
if errors: if errors:
raise InvalidPackageInfo(errors) raise InvalidPackageInfo(errors)
packages = {key: PackageDescription() for key in srcinfo["packages"]} packages = {key: PackageDescription() for key in srcinfo["packages"]}
version = cls.full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"]) version = full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"])
return cls(srcinfo["pkgbase"], version, aur_url, packages) return cls(srcinfo["pkgbase"], version, aur_url, packages)
@ -165,6 +166,17 @@ class Package:
aur_url=dump["aur_url"], aur_url=dump["aur_url"],
packages=packages) packages=packages)
@classmethod
def from_official(cls: Type[Package], name: str, aur_url: str) -> Package:
"""
construct package properties from official repository page
:param name: package name (either base or normal name)
:param aur_url: AUR root url
:return: package properties
"""
package = Official.info(name)
return cls(package.package_base, package.version, aur_url, {package.name: PackageDescription()})
@classmethod @classmethod
def load(cls: Type[Package], package: str, source: PackageSource, pacman: Pacman, aur_url: str) -> Package: def load(cls: Type[Package], package: str, source: PackageSource, pacman: Pacman, aur_url: str) -> Package:
""" """
@ -183,6 +195,8 @@ class Package:
return cls.from_aur(package, aur_url) return cls.from_aur(package, aur_url)
if resolved_source == PackageSource.Local: if resolved_source == PackageSource.Local:
return cls.from_build(Path(package), aur_url) return cls.from_build(Path(package), aur_url)
if resolved_source == PackageSource.Repository:
return cls.from_official(package, aur_url)
raise InvalidPackageInfo(f"Unsupported local package source {resolved_source}") raise InvalidPackageInfo(f"Unsupported local package source {resolved_source}")
except InvalidPackageInfo: except InvalidPackageInfo:
raise raise
@ -197,35 +211,25 @@ class Package:
:return: list of package dependencies including makedepends array, but excluding packages from this base :return: list of package dependencies including makedepends array, but excluding packages from this base
""" """
# additional function to remove versions from dependencies # additional function to remove versions from dependencies
def trim_version(name: str) -> str: 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 ("<", "=", ">"): for symbol in ("<", "=", ">"):
name = name.split(symbol)[0] package_name = package_name.split(symbol)[0]
return name return package_name
srcinfo, errors = parse_srcinfo((path / ".SRCINFO").read_text()) srcinfo, errors = parse_srcinfo((path / ".SRCINFO").read_text())
if errors: if errors:
raise InvalidPackageInfo(errors) raise InvalidPackageInfo(errors)
makedepends = srcinfo.get("makedepends", []) makedepends = extract_packages(srcinfo.get("makedepends", []))
# sum over each package # sum over each package
depends: List[str] = srcinfo.get("depends", []) depends = extract_packages(srcinfo.get("depends", []))
for package in srcinfo["packages"].values(): for package in srcinfo["packages"].values():
depends.extend(package.get("depends", [])) depends |= extract_packages(package.get("depends", []))
# we are not interested in dependencies inside pkgbase # we are not interested in dependencies inside pkgbase
packages = set(srcinfo["packages"].keys()) packages = set(srcinfo["packages"].keys())
full_list = set(depends + makedepends) - packages return (depends | makedepends) - packages
return {trim_version(package_name) for package_name in full_list}
@staticmethod
def full_version(epoch: Optional[str], pkgver: str, pkgrel: str) -> str:
"""
generate full version from components
:param epoch: package epoch if any
:param pkgver: package version
:param pkgrel: package release version (arch linux specific)
:return: generated version
"""
prefix = f"{epoch}:" if epoch else ""
return f"{prefix}{pkgver}-{pkgrel}"
def actual_version(self, paths: RepositoryPaths) -> str: def actual_version(self, paths: RepositoryPaths) -> str:
""" """
@ -252,7 +256,7 @@ class Package:
if errors: if errors:
raise InvalidPackageInfo(errors) raise InvalidPackageInfo(errors)
return self.full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"]) return full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"])
except Exception: except Exception:
logger.exception("cannot determine version of VCS package, make sure that you have VCS tools installed") logger.exception("cannot determine version of VCS package, make sure that you have VCS tools installed")

View File

@ -35,6 +35,7 @@ class PackageSource(Enum):
:cvar Directory: source is a directory which contains packages :cvar Directory: source is a directory which contains packages
:cvar Local: source is locally stored PKGBUILD :cvar Local: source is locally stored PKGBUILD
:cvar Remote: source is remote (http, ftp etc) link :cvar Remote: source is remote (http, ftp etc) link
:cvar Repository: source is official repository
""" """
Auto = "auto" Auto = "auto"
@ -43,6 +44,7 @@ class PackageSource(Enum):
Directory = "directory" Directory = "directory"
Local = "local" Local = "local"
Remote = "remote" Remote = "remote"
Repository = "repository"
def resolve(self, source: str) -> PackageSource: def resolve(self, source: str) -> PackageSource:
""" """

View File

@ -32,12 +32,14 @@ class ReportSettings(Enum):
:cvar HTML: html report generation :cvar HTML: html report generation
:cvar Email: email report generation :cvar Email: email report generation
:cvar Console: print result to console :cvar Console: print result to console
:cvar Telegram: markdown report to telegram channel
""" """
Disabled = "disabled" # for testing purpose Disabled = "disabled" # for testing purpose
HTML = "html" HTML = "html"
Email = "email" Email = "email"
Console = "console" Console = "console"
Telegram = "telegram"
@classmethod @classmethod
def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings: def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings:
@ -52,4 +54,6 @@ class ReportSettings(Enum):
return cls.Email return cls.Email
if value.lower() in ("console",): if value.lower() in ("console",):
return cls.Console return cls.Console
if value.lower() in ("telegram",):
return cls.Telegram
raise InvalidOption(value) raise InvalidOption(value)

View File

@ -62,13 +62,6 @@ class Result:
""" """
return list(self._success.values()) return list(self._success.values())
@property
def updated(self) -> List[Package]:
"""
:return: list of updated packages inclding both success and failed
"""
return self.success + self.failed
def add_failed(self, package: Package) -> None: def add_failed(self, package: Package) -> None:
""" """
add new package to failed built add new package to failed built

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.0.0rc3" __version__ = "2.0.0rc6"

View File

@ -20,7 +20,7 @@
from aiohttp.web import HTTPNotFound, Response, json_response from aiohttp.web import HTTPNotFound, Response, json_response
from typing import Callable, List from typing import Callable, List
from ahriman.core.alpm.aur import AUR from ahriman.core.alpm.remote.aur import AUR
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView from ahriman.web.views.base import BaseView

View File

@ -21,7 +21,7 @@ def test_finalize(application_packages: Packages) -> None:
def test_known_packages(application_packages: Packages) -> None: def test_known_packages(application_packages: Packages) -> None:
""" """
must raise NotImplemented for missing finalize method must raise NotImplemented for missing known_packages method
""" """
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
application_packages._known_packages() application_packages._known_packages()
@ -42,17 +42,17 @@ def test_add_aur(application_packages: Packages, package_ahriman: Package, mocke
must add package from AUR must add package from AUR
""" """
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
insert_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_insert")
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load") load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load")
dependencies_mock = mocker.patch("ahriman.application.application.packages.Packages._process_dependencies") dependencies_mock = mocker.patch("ahriman.application.application.packages.Packages._process_dependencies")
build_queue_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_insert")
application_packages._add_aur(package_ahriman.base, set(), False) application_packages._add_aur(package_ahriman.base, set(), False)
insert_mock.assert_called_once_with(package_ahriman)
load_mock.assert_called_once_with( load_mock.assert_called_once_with(
pytest.helpers.anyvar(int), pytest.helpers.anyvar(int),
package_ahriman.git_url, package_ahriman.git_url,
pytest.helpers.anyvar(int)) pytest.helpers.anyvar(int))
dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int), set(), False) dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int), set(), False)
build_queue_mock.assert_called_once_with(package_ahriman)
def test_add_directory(application_packages: Packages, package_ahriman: Package, mocker: MockerFixture) -> None: def test_add_directory(application_packages: Packages, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -75,16 +75,16 @@ def test_add_local(application_packages: Packages, package_ahriman: Package, moc
""" """
mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init") init_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.init")
insert_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_insert")
copytree_mock = mocker.patch("shutil.copytree") copytree_mock = mocker.patch("shutil.copytree")
dependencies_mock = mocker.patch("ahriman.application.application.packages.Packages._process_dependencies") dependencies_mock = mocker.patch("ahriman.application.application.packages.Packages._process_dependencies")
build_queue_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_insert")
application_packages._add_local(package_ahriman.base, set(), False) application_packages._add_local(package_ahriman.base, set(), False)
copytree_mock.assert_called_once_with( copytree_mock.assert_called_once_with(
Path(package_ahriman.base), application_packages.repository.paths.cache_for(package_ahriman.base)) Path(package_ahriman.base), application_packages.repository.paths.cache_for(package_ahriman.base))
init_mock.assert_called_once_with(application_packages.repository.paths.cache_for(package_ahriman.base)) init_mock.assert_called_once_with(application_packages.repository.paths.cache_for(package_ahriman.base))
insert_mock.assert_called_once_with(package_ahriman)
dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int), set(), False) dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int), set(), False)
build_queue_mock.assert_called_once_with(package_ahriman)
def test_add_remote(application_packages: Packages, package_description_ahriman: PackageDescription, def test_add_remote(application_packages: Packages, package_description_ahriman: PackageDescription,

View File

@ -3,6 +3,7 @@ import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.application.application import Application
from ahriman.application.handlers import Add from ahriman.application.handlers import Add
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.package import Package from ahriman.models.package import Package
@ -36,6 +37,20 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
application_mock.assert_called_once_with(args.package, args.source, args.without_dependencies) application_mock.assert_called_once_with(args.package, args.source, args.without_dependencies)
def test_run_extract_packages(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
args.package = None
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
mocker.patch("ahriman.application.application.Application.add")
extract_mock = mocker.patch("ahriman.application.handlers.Add.extract_packages", return_value=[])
Add.run(args, "x86_64", configuration, True, False)
extract_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_run_with_updates(args: argparse.Namespace, configuration: Configuration, def test_run_with_updates(args: argparse.Namespace, configuration: Configuration,
package_ahriman: Package, mocker: MockerFixture) -> None: package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
@ -72,3 +87,12 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat
Add.run(args, "x86_64", configuration, True, False) Add.run(args, "x86_64", configuration, True, False)
check_mock.assert_called_once_with(True, True) check_mock.assert_called_once_with(True, True)
def test_extract_packages(application: Application, mocker: MockerFixture) -> None:
"""
must extract packages from database
"""
packages_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.packages_get")
Add.extract_packages(application)
packages_mock.assert_called_once_with()

View File

@ -3,6 +3,7 @@ import dataclasses
import pytest import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest import mock
from ahriman.application.handlers import Search from ahriman.application.handlers import Search
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -29,23 +30,27 @@ def test_run(args: argparse.Namespace, configuration: Configuration, aur_package
must run command must run command
""" """
args = _default_args(args) args = _default_args(args)
search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[aur_package_ahriman]) aur_search_mock = mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[aur_package_ahriman])
official_search_mock = mocker.patch("ahriman.core.alpm.remote.official.Official.multisearch",
return_value=[aur_package_ahriman])
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_if_empty") check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_if_empty")
print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print") print_mock = mocker.patch("ahriman.core.formatters.printer.Printer.print")
Search.run(args, "x86_64", configuration, True, False) Search.run(args, "x86_64", configuration, True, False)
search_mock.assert_called_once_with("ahriman") aur_search_mock.assert_called_once_with("ahriman")
official_search_mock.assert_called_once_with("ahriman")
check_mock.assert_called_once_with(False, False) check_mock.assert_called_once_with(False, False)
print_mock.assert_called_once_with(False) print_mock.assert_has_calls([mock.call(False), mock.call(False)])
def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must run command must raise ExitCode exception on empty result list
""" """
args = _default_args(args) args = _default_args(args)
args.exit_code = True args.exit_code = True
mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[]) mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[])
mocker.patch("ahriman.core.alpm.remote.official.Official.multisearch", return_value=[])
mocker.patch("ahriman.core.formatters.printer.Printer.print") mocker.patch("ahriman.core.formatters.printer.Printer.print")
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_if_empty") check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_if_empty")
@ -59,11 +64,15 @@ def test_run_sort(args: argparse.Namespace, configuration: Configuration, aur_pa
must run command with sorting must run command with sorting
""" """
args = _default_args(args) args = _default_args(args)
mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[aur_package_ahriman]) mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[aur_package_ahriman])
mocker.patch("ahriman.core.alpm.remote.official.Official.multisearch", return_value=[])
sort_mock = mocker.patch("ahriman.application.handlers.search.Search.sort") sort_mock = mocker.patch("ahriman.application.handlers.search.Search.sort")
Search.run(args, "x86_64", configuration, True, False) Search.run(args, "x86_64", configuration, True, False)
sort_mock.assert_called_once_with([aur_package_ahriman], "name") sort_mock.assert_has_calls([
mock.call([], "name"), mock.call().__iter__(),
mock.call([aur_package_ahriman], "name"), mock.call().__iter__()
])
def test_run_sort_by(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: AURPackage, def test_run_sort_by(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: AURPackage,
@ -73,11 +82,15 @@ def test_run_sort_by(args: argparse.Namespace, configuration: Configuration, aur
""" """
args = _default_args(args) args = _default_args(args)
args.sort_by = "field" args.sort_by = "field"
mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[aur_package_ahriman]) mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[aur_package_ahriman])
mocker.patch("ahriman.core.alpm.remote.official.Official.multisearch", return_value=[])
sort_mock = mocker.patch("ahriman.application.handlers.search.Search.sort") sort_mock = mocker.patch("ahriman.application.handlers.search.Search.sort")
Search.run(args, "x86_64", configuration, True, False) Search.run(args, "x86_64", configuration, True, False)
sort_mock.assert_called_once_with([aur_package_ahriman], "field") sort_mock.assert_has_calls([
mock.call([], "field"), mock.call().__iter__(),
mock.call([aur_package_ahriman], "field"), mock.call().__iter__()
])
def test_sort(aur_package_ahriman: AURPackage) -> None: def test_sort(aur_package_ahriman: AURPackage) -> None:

View File

@ -62,7 +62,7 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat
def test_run_verbose(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package, def test_run_verbose(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must run command must run command with detailed info
""" """
args = _default_args(args) args = _default_args(args)
args.info = True args.info = True

View File

@ -59,7 +59,7 @@ def test_check_unsafe() -> None:
def test_check_unsafe_safe() -> None: def test_check_unsafe_safe() -> None:
""" """
must check if command is unsafe must check if command is safe
""" """
UnsafeCommands.check_unsafe("package-status", ["repo-clean"], _parser()) UnsafeCommands.check_unsafe("package-status", ["repo-clean"], _parser())

View File

@ -34,7 +34,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
def test_disallow_auto_architecture_run() -> None: def test_disallow_auto_architecture_run() -> None:
""" """
must not allow multi architecture run must not allow auto architecture run
""" """
assert not Web.ALLOW_AUTO_ARCHITECTURE_RUN assert not Web.ALLOW_AUTO_ARCHITECTURE_RUN

View File

@ -6,6 +6,7 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Handler from ahriman.application.handlers import Handler
from ahriman.models.action import Action from ahriman.models.action import Action
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package_source import PackageSource
from ahriman.models.sign_settings import SignSettings from ahriman.models.sign_settings import SignSettings
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -339,6 +340,25 @@ def test_subparsers_repo_report_architecture(parser: argparse.ArgumentParser) ->
assert args.architecture == ["x86_64"] assert args.architecture == ["x86_64"]
def test_subparsers_repo_restore(parser: argparse.ArgumentParser) -> None:
"""
repo-restore command must imply package and source
"""
args = parser.parse_args(["repo-restore"])
assert args.package is None
assert args.source == PackageSource.AUR
def test_subparsers_repo_restore_architecture(parser: argparse.ArgumentParser) -> None:
"""
repo-restore command must correctly parse architecture list
"""
args = parser.parse_args(["repo-restore"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-restore"])
assert args.architecture == ["x86_64"]
def test_subparsers_repo_setup(parser: argparse.ArgumentParser) -> None: def test_subparsers_repo_setup(parser: argparse.ArgumentParser) -> None:
""" """
repo-setup command must imply lock, no-report, quiet and unsafe repo-setup command must imply lock, no-report, quiet and unsafe

View File

@ -124,6 +124,50 @@ def aur_package_ahriman() -> AURPackage:
) )
@pytest.fixture
def aur_package_akonadi() -> AURPackage:
"""
fixture for AUR package
:return: AUR package test instance
"""
return AURPackage(
id=0,
name="akonadi",
package_base_id=0,
package_base="akonadi",
version="21.12.3-2",
description="PIM layer, which provides an asynchronous API to access all kind of PIM data",
num_votes=0,
popularity=0,
first_submitted=datetime.datetime(1970, 1, 1, 0, 0, 0),
last_modified=datetime.datetime(2022, 3, 6, 8, 39, 50, 610000),
url_path="",
url="https://kontact.kde.org",
out_of_date=None,
maintainer="felixonmars",
depends=[
"libakonadi",
"mariadb",
],
make_depends=[
"boost",
"doxygen",
"extra-cmake-modules",
"kaccounts-integration",
"kitemmodels",
"postgresql",
"qt5-tools",
],
opt_depends=[
"postgresql: PostgreSQL backend",
],
conflicts=[],
provides=[],
license=["LGPL"],
keywords=[],
)
@pytest.fixture @pytest.fixture
def auth(configuration: Configuration) -> Auth: def auth(configuration: Configuration) -> Auth:
""" """
@ -264,9 +308,7 @@ def repository_paths(configuration: Configuration) -> RepositoryPaths:
:param configuration: configuration fixture :param configuration: configuration fixture
:return: repository paths test instance :return: repository paths test instance
""" """
return RepositoryPaths( return configuration.repository_paths
architecture="x86_64",
root=configuration.getpath("repository", "root"))
@pytest.fixture @pytest.fixture

View File

@ -1,12 +0,0 @@
import pytest
from ahriman.core.alpm.aur import AUR
@pytest.fixture
def aur() -> AUR:
"""
aur helper fixture
:return: aur helper instance
"""
return AUR()

View File

@ -0,0 +1,32 @@
import pytest
from ahriman.core.alpm.remote.aur import AUR
from ahriman.core.alpm.remote.official import Official
from ahriman.core.alpm.remote.remote import Remote
@pytest.fixture
def aur() -> AUR:
"""
aur helper fixture
:return: aur helper instance
"""
return AUR()
@pytest.fixture
def official() -> Official:
"""
official repository fixture
:return: official repository helper instance
"""
return Official()
@pytest.fixture
def remote() -> Remote:
"""
official repository fixture
:return: official repository helper instance
"""
return Remote()

View File

@ -4,10 +4,9 @@ import requests
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest import mock
from unittest.mock import MagicMock from unittest.mock import MagicMock
from ahriman.core.alpm.aur import AUR from ahriman.core.alpm.remote.aur import AUR
from ahriman.core.exceptions import InvalidPackageInfo from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
@ -21,55 +20,6 @@ def _get_response(resource_path_root: Path) -> str:
return (resource_path_root / "models" / "package_ahriman_aur").read_text() return (resource_path_root / "models" / "package_ahriman_aur").read_text()
def test_info(mocker: MockerFixture) -> None:
"""
must call info method
"""
info_mock = mocker.patch("ahriman.core.alpm.aur.AUR.package_info")
AUR.info("ahriman")
info_mock.assert_called_once_with("ahriman")
def test_multisearch(aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
must search in AUR with multiple words
"""
terms = ["ahriman", "is", "cool"]
search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.search", return_value=[aur_package_ahriman])
assert AUR.multisearch(*terms) == [aur_package_ahriman]
search_mock.assert_has_calls([mock.call("ahriman"), mock.call("cool")])
def test_multisearch_empty(mocker: MockerFixture) -> None:
"""
must return empty list if no long terms supplied
"""
terms = ["it", "is"]
search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.search")
assert AUR.multisearch(*terms) == []
search_mock.assert_not_called()
def test_multisearch_single(aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
must search in AUR with one word
"""
search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.search", return_value=[aur_package_ahriman])
assert AUR.multisearch("ahriman") == [aur_package_ahriman]
search_mock.assert_called_once_with("ahriman")
def test_search(mocker: MockerFixture) -> None:
"""
must call search method
"""
search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.package_search")
AUR.search("ahriman")
search_mock.assert_called_once_with("ahriman")
def test_parse_response(aur_package_ahriman: AURPackage, resource_path_root: Path) -> None: def test_parse_response(aur_package_ahriman: AURPackage, resource_path_root: Path) -> None:
""" """
must parse success response must parse success response
@ -87,7 +37,7 @@ def test_parse_response_error(resource_path_root: Path) -> None:
AUR.parse_response(json.loads(response)) AUR.parse_response(json.loads(response))
def test_parse_response_unknown_error(resource_path_root: Path) -> None: def test_parse_response_unknown_error() -> None:
""" """
must raise exception on invalid response with empty error message must raise exception on invalid response with empty error message
""" """
@ -159,7 +109,7 @@ def test_package_info(aur: AUR, aur_package_ahriman: AURPackage, mocker: MockerF
""" """
must make request for info must make request for info
""" """
request_mock = mocker.patch("ahriman.core.alpm.aur.AUR.make_request", return_value=[aur_package_ahriman]) request_mock = mocker.patch("ahriman.core.alpm.remote.aur.AUR.make_request", return_value=[aur_package_ahriman])
assert aur.package_info(aur_package_ahriman.name) == aur_package_ahriman assert aur.package_info(aur_package_ahriman.name) == aur_package_ahriman
request_mock.assert_called_once_with("info", aur_package_ahriman.name) request_mock.assert_called_once_with("info", aur_package_ahriman.name)
@ -168,6 +118,6 @@ def test_package_search(aur: AUR, aur_package_ahriman: AURPackage, mocker: Mocke
""" """
must make request for search must make request for search
""" """
request_mock = mocker.patch("ahriman.core.alpm.aur.AUR.make_request", return_value=[aur_package_ahriman]) request_mock = mocker.patch("ahriman.core.alpm.remote.aur.AUR.make_request", return_value=[aur_package_ahriman])
assert aur.package_search(aur_package_ahriman.name, by="name") == [aur_package_ahriman] assert aur.package_search(aur_package_ahriman.name) == [aur_package_ahriman]
request_mock.assert_called_once_with("search", aur_package_ahriman.name, by="name") request_mock.assert_called_once_with("search", aur_package_ahriman.name, by="name-desc")

View File

@ -0,0 +1,88 @@
import json
import pytest
import requests
from pathlib import Path
from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.core.alpm.remote.official import Official
from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.models.aur_package import AURPackage
def _get_response(resource_path_root: Path) -> str:
"""
load response from resource file
:param resource_path_root: path to resource root
:return: response text
"""
return (resource_path_root / "models" / "package_akonadi_aur").read_text()
def test_parse_response(aur_package_akonadi: AURPackage, resource_path_root: Path) -> None:
"""
must parse success response
"""
response = _get_response(resource_path_root)
assert Official.parse_response(json.loads(response)) == [aur_package_akonadi]
def test_parse_response_unknown_error(resource_path_root: Path) -> None:
"""
must raise exception on invalid response with empty error message
"""
response = (resource_path_root / "models" / "official_error").read_text()
with pytest.raises(InvalidPackageInfo, match="API validation error"):
Official.parse_response(json.loads(response))
def test_make_request(official: Official, aur_package_akonadi: AURPackage,
mocker: MockerFixture, resource_path_root: Path) -> None:
"""
must perform request to official repositories
"""
response_mock = MagicMock()
response_mock.json.return_value = json.loads(_get_response(resource_path_root))
request_mock = mocker.patch("requests.get", return_value=response_mock)
assert official.make_request("akonadi", by="q") == [aur_package_akonadi]
request_mock.assert_called_once_with("https://archlinux.org/packages/search/json", params={"q": ("akonadi",)})
def test_make_request_failed(official: Official, mocker: MockerFixture) -> None:
"""
must reraise generic exception
"""
mocker.patch("requests.get", side_effect=Exception())
with pytest.raises(Exception):
official.make_request("akonadi", by="q")
def test_make_request_failed_http_error(official: Official, mocker: MockerFixture) -> None:
"""
must reraise http exception
"""
mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError())
with pytest.raises(requests.exceptions.HTTPError):
official.make_request("akonadi", by="q")
def test_package_info(official: Official, aur_package_akonadi: AURPackage, mocker: MockerFixture) -> None:
"""
must make request for info
"""
request_mock = mocker.patch("ahriman.core.alpm.remote.official.Official.make_request",
return_value=[aur_package_akonadi])
assert official.package_info(aur_package_akonadi.name) == aur_package_akonadi
request_mock.assert_called_once_with(aur_package_akonadi.name, by="name")
def test_package_search(official: Official, aur_package_akonadi: AURPackage, mocker: MockerFixture) -> None:
"""
must make request for search
"""
request_mock = mocker.patch("ahriman.core.alpm.remote.official.Official.make_request",
return_value=[aur_package_akonadi])
assert official.package_search(aur_package_akonadi.name) == [aur_package_akonadi]
request_mock.assert_called_once_with(aur_package_akonadi.name, by="q")

View File

@ -0,0 +1,72 @@
import pytest
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.core.alpm.remote.remote import Remote
from ahriman.models.aur_package import AURPackage
def test_info(mocker: MockerFixture) -> None:
"""
must call info method
"""
info_mock = mocker.patch("ahriman.core.alpm.remote.remote.Remote.package_info")
Remote.info("ahriman")
info_mock.assert_called_once_with("ahriman")
def test_multisearch(aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
must search in AUR with multiple words
"""
terms = ["ahriman", "is", "cool"]
search_mock = mocker.patch("ahriman.core.alpm.remote.remote.Remote.search", return_value=[aur_package_ahriman])
assert Remote.multisearch(*terms) == [aur_package_ahriman]
search_mock.assert_has_calls([mock.call("ahriman"), mock.call("cool")])
def test_multisearch_empty(mocker: MockerFixture) -> None:
"""
must return empty list if no long terms supplied
"""
terms = ["it", "is"]
search_mock = mocker.patch("ahriman.core.alpm.remote.remote.Remote.search")
assert Remote.multisearch(*terms) == []
search_mock.assert_not_called()
def test_multisearch_single(aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
must search in AUR with one word
"""
search_mock = mocker.patch("ahriman.core.alpm.remote.remote.Remote.search", return_value=[aur_package_ahriman])
assert Remote.multisearch("ahriman") == [aur_package_ahriman]
search_mock.assert_called_once_with("ahriman")
def test_search(mocker: MockerFixture) -> None:
"""
must call search method
"""
search_mock = mocker.patch("ahriman.core.alpm.remote.remote.Remote.package_search")
Remote.search("ahriman")
search_mock.assert_called_once_with("ahriman")
def test_package_info(remote: Remote) -> None:
"""
must raise NotImplemented for missing package info method
"""
with pytest.raises(NotImplementedError):
remote.package_info("package")
def test_package_search(remote: Remote) -> None:
"""
must raise NotImplemented for missing package search method
"""
with pytest.raises(NotImplementedError):
remote.package_search("package")

View File

@ -87,7 +87,7 @@ def test_fetch_new(mocker: MockerFixture) -> None:
local = Path("local") local = Path("local")
Sources.fetch(local, "remote") Sources.fetch(local, "remote")
check_output_mock.assert_has_calls([ check_output_mock.assert_has_calls([
mock.call("git", "clone", "remote", str(local), exception=None, logger=pytest.helpers.anyvar(int)), mock.call("git", "clone", "remote", str(local), exception=None, cwd=local, logger=pytest.helpers.anyvar(int)),
mock.call("git", "checkout", "--force", Sources._branch, mock.call("git", "checkout", "--force", Sources._branch,
exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), exception=None, cwd=local, logger=pytest.helpers.anyvar(int)),
mock.call("git", "reset", "--hard", f"origin/{Sources._branch}", mock.call("git", "reset", "--hard", f"origin/{Sources._branch}",

View File

@ -13,10 +13,12 @@ def test_migrate_data_initial(connection: Connection, configuration: Configurati
must perform initial migration must perform initial migration
""" """
packages = mocker.patch("ahriman.core.database.data.migrate_package_statuses") packages = mocker.patch("ahriman.core.database.data.migrate_package_statuses")
patches = mocker.patch("ahriman.core.database.data.migrate_patches")
users = mocker.patch("ahriman.core.database.data.migrate_users_data") users = mocker.patch("ahriman.core.database.data.migrate_users_data")
migrate_data(MigrationResult(old_version=0, new_version=900), connection, configuration, repository_paths) migrate_data(MigrationResult(old_version=0, new_version=900), connection, configuration, repository_paths)
packages.assert_called_once_with(connection, repository_paths) packages.assert_called_once_with(connection, repository_paths)
patches.assert_called_once_with(connection, repository_paths)
users.assert_called_once_with(connection, configuration) users.assert_called_once_with(connection, configuration)

View File

@ -21,7 +21,7 @@ def test_sign_ascii(package_ahriman: Package) -> None:
def test_sign_utf8(package_ahriman: Package) -> None: def test_sign_utf8(package_ahriman: Package) -> None:
""" """
must correctly generate sign in ascii must correctly generate sign in utf8
""" """
with pytest.raises(UnicodeEncodeError): with pytest.raises(UnicodeEncodeError):
BuildPrinter(package_ahriman, is_success=True, use_utf=True).title().encode("ascii") BuildPrinter(package_ahriman, is_success=True, use_utf=True).title().encode("ascii")
@ -31,6 +31,6 @@ def test_sign_utf8(package_ahriman: Package) -> None:
def test_title(package_ahriman: Package) -> None: def test_title(package_ahriman: Package) -> None:
""" """
must return non empty title must return non-empty title
""" """
assert BuildPrinter(package_ahriman, is_success=True, use_utf=False).title() is not None assert BuildPrinter(package_ahriman, is_success=True, use_utf=False).title() is not None

View File

@ -115,7 +115,7 @@ def test_generate_with_built_and_full_path(
result: Result, result: Result,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must generate report with built packages must generate report with built packages and full packages lists
""" """
send_mock = mocker.patch("ahriman.core.report.email.Email._send") send_mock = mocker.patch("ahriman.core.report.email.Email._send")

View File

@ -53,3 +53,12 @@ def test_report_html(configuration: Configuration, result: Result, mocker: Mocke
report_mock = mocker.patch("ahriman.core.report.html.HTML.generate") report_mock = mocker.patch("ahriman.core.report.html.HTML.generate")
Report.load("x86_64", configuration, "html").run([], result) Report.load("x86_64", configuration, "html").run([], result)
report_mock.assert_called_once_with([], result) report_mock.assert_called_once_with([], result)
def test_report_telegram(configuration: Configuration, result: Result, mocker: MockerFixture) -> None:
"""
must generate telegram report
"""
report_mock = mocker.patch("ahriman.core.report.telegram.Telegram.generate")
Report.load("x86_64", configuration, "telegram").run([], result)
report_mock.assert_called_once_with([], result)

View File

@ -0,0 +1,83 @@
import pytest
import requests
from pytest_mock import MockerFixture
from unittest import mock
from ahriman.core.configuration import Configuration
from ahriman.core.report.telegram import Telegram
from ahriman.models.package import Package
from ahriman.models.result import Result
def test_send(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must send a message
"""
request_mock = mocker.patch("requests.post")
report = Telegram("x86_64", configuration, "telegram")
report._send("a text")
request_mock.assert_called_once_with(
pytest.helpers.anyvar(str, strict=True),
data={"chat_id": pytest.helpers.anyvar(str, strict=True), "text": "a text", "parse_mode": "HTML"})
def test_send_failed(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must reraise generic exception
"""
mocker.patch("requests.post", side_effect=Exception())
report = Telegram("x86_64", configuration, "telegram")
with pytest.raises(Exception):
report._send("a text")
def test_make_request_failed_http_error(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must reraise http exception
"""
mocker.patch("requests.post", side_effect=requests.exceptions.HTTPError())
report = Telegram("x86_64", configuration, "telegram")
with pytest.raises(requests.exceptions.HTTPError):
report._send("a text")
def test_generate(configuration: Configuration, package_ahriman: Package, result: Result,
mocker: MockerFixture) -> None:
"""
must generate report
"""
send_mock = mocker.patch("ahriman.core.report.telegram.Telegram._send")
report = Telegram("x86_64", configuration, "telegram")
report.generate([package_ahriman], result)
send_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_generate_big_text(configuration: Configuration, package_ahriman: Package, result: Result,
mocker: MockerFixture) -> None:
"""
must generate report with big text
"""
mocker.patch("ahriman.core.report.jinja_template.JinjaTemplate.make_html", return_value="a\n" * 4096)
send_mock = mocker.patch("ahriman.core.report.telegram.Telegram._send")
report = Telegram("x86_64", configuration, "telegram")
report.generate([package_ahriman], result)
send_mock.assert_has_calls([
mock.call(pytest.helpers.anyvar(str, strict=True)), mock.call(pytest.helpers.anyvar(str, strict=True))
])
def test_generate_no_empty(configuration: Configuration, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must generate report
"""
send_mock = mocker.patch("ahriman.core.report.telegram.Telegram._send")
report = Telegram("x86_64", configuration, "telegram")
report.generate([package_ahriman], Result())
send_mock.assert_not_called()

View File

@ -64,6 +64,8 @@ def test_process_remove_base(executor: Executor, package_ahriman: Package, mocke
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman]) mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
tree_clear_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_clear") tree_clear_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_clear")
repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove") repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
build_queue_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.build_queue_clear")
patches_mock = mocker.patch("ahriman.core.database.sqlite.SQLite.patches_remove")
status_client_mock = mocker.patch("ahriman.core.status.client.Client.remove") status_client_mock = mocker.patch("ahriman.core.status.client.Client.remove")
executor.process_remove([package_ahriman.base]) executor.process_remove([package_ahriman.base])
@ -72,6 +74,8 @@ def test_process_remove_base(executor: Executor, package_ahriman: Package, mocke
package_ahriman.base, package_ahriman.packages[package_ahriman.base].filepath) package_ahriman.base, package_ahriman.packages[package_ahriman.base].filepath)
# must update status and remove package files # must update status and remove package files
tree_clear_mock.assert_called_once_with(package_ahriman.base) tree_clear_mock.assert_called_once_with(package_ahriman.base)
build_queue_mock.assert_called_once_with(package_ahriman.base)
patches_mock.assert_called_once_with(package_ahriman.base)
status_client_mock.assert_called_once_with(package_ahriman.base) status_client_mock.assert_called_once_with(package_ahriman.base)

View File

@ -26,7 +26,7 @@ def test_load_full_client(configuration: Configuration) -> None:
def test_load_full_client_from_address(configuration: Configuration) -> None: def test_load_full_client_from_address(configuration: Configuration) -> None:
""" """
must load full client if settings set must load full client by using address
""" """
configuration.set_option("web", "address", "http://localhost:8080") configuration.set_option("web", "address", "http://localhost:8080")
assert isinstance(Client.load(configuration), WebClient) assert isinstance(Client.load(configuration), WebClient)

View File

@ -23,7 +23,7 @@ def test_ahriman_url(web_client: WebClient) -> None:
def test_status_url(web_client: WebClient) -> None: def test_status_url(web_client: WebClient) -> None:
""" """
must generate service status url correctly must generate package status url correctly
""" """
assert web_client._status_url.startswith(web_client.address) assert web_client._status_url.startswith(web_client.address)
assert web_client._status_url.endswith("/status-api/v1/status") assert web_client._status_url.endswith("/status-api/v1/status")
@ -67,7 +67,7 @@ def test_login_failed(web_client: WebClient, user: User, mocker: MockerFixture)
def test_login_failed_http_error(web_client: WebClient, user: User, mocker: MockerFixture) -> None: def test_login_failed_http_error(web_client: WebClient, user: User, mocker: MockerFixture) -> None:
""" """
must suppress any exception happened during login must suppress HTTP exception happened during login
""" """
web_client.user = user web_client.user = user
mocker.patch("requests.Session.post", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.post", side_effect=requests.exceptions.HTTPError())
@ -112,7 +112,7 @@ def test_add_failed(web_client: WebClient, package_ahriman: Package, mocker: Moc
def test_add_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: def test_add_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must suppress any exception happened during addition must suppress HTTP exception happened during addition
""" """
mocker.patch("requests.Session.post", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.post", side_effect=requests.exceptions.HTTPError())
web_client.add(package_ahriman, BuildStatusEnum.Unknown) web_client.add(package_ahriman, BuildStatusEnum.Unknown)
@ -145,7 +145,7 @@ def test_get_failed(web_client: WebClient, mocker: MockerFixture) -> None:
def test_get_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None: def test_get_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
""" """
must suppress any exception happened during status getting must suppress HTTP exception happened during status getting
""" """
mocker.patch("requests.Session.get", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.get", side_effect=requests.exceptions.HTTPError())
assert web_client.get(None) == [] assert web_client.get(None) == []
@ -193,7 +193,7 @@ def test_get_internal_failed(web_client: WebClient, mocker: MockerFixture) -> No
def test_get_internal_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None: def test_get_internal_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
""" """
must suppress any exception happened during web service status getting must suppress HTTP exception happened during web service status getting
""" """
mocker.patch("requests.Session.get", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.get", side_effect=requests.exceptions.HTTPError())
assert web_client.get_internal() == InternalStatus() assert web_client.get_internal() == InternalStatus()
@ -224,7 +224,7 @@ def test_get_self_failed(web_client: WebClient, mocker: MockerFixture) -> None:
def test_get_self_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None: def test_get_self_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
""" """
must suppress any exception happened during service status getting must suppress HTTP exception happened during service status getting
""" """
mocker.patch("requests.Session.get", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.get", side_effect=requests.exceptions.HTTPError())
assert web_client.get_self().status == BuildStatusEnum.Unknown assert web_client.get_self().status == BuildStatusEnum.Unknown
@ -250,7 +250,7 @@ def test_remove_failed(web_client: WebClient, package_ahriman: Package, mocker:
def test_remove_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: def test_remove_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must suppress any exception happened during removal must suppress HTTP exception happened during removal
""" """
mocker.patch("requests.Session.delete", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.delete", side_effect=requests.exceptions.HTTPError())
web_client.remove(package_ahriman.base) web_client.remove(package_ahriman.base)
@ -277,7 +277,7 @@ def test_update_failed(web_client: WebClient, package_ahriman: Package, mocker:
def test_update_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: def test_update_failed_http_error(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must suppress any exception happened during update must suppress HTTP exception happened during update
""" """
mocker.patch("requests.Session.post", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.post", side_effect=requests.exceptions.HTTPError())
web_client.update(package_ahriman.base, BuildStatusEnum.Unknown) web_client.update(package_ahriman.base, BuildStatusEnum.Unknown)
@ -304,7 +304,7 @@ def test_update_self_failed(web_client: WebClient, mocker: MockerFixture) -> Non
def test_update_self_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None: def test_update_self_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
""" """
must suppress any exception happened during service update must suppress HTTP exception happened during service update
""" """
mocker.patch("requests.Session.post", side_effect=requests.exceptions.HTTPError()) mocker.patch("requests.Session.post", side_effect=requests.exceptions.HTTPError())
web_client.update_self(BuildStatusEnum.Unknown) web_client.update_self(BuildStatusEnum.Unknown)

View File

@ -231,7 +231,7 @@ def test_gettype_from_section_with_architecture(configuration: Configuration) ->
def test_gettype_from_section_no_section(configuration: Configuration) -> None: def test_gettype_from_section_no_section(configuration: Configuration) -> None:
""" """
must extract type from section name with architecture must raise NoSectionError during type extraction from section name with architecture
""" """
# technically rsync:x86_64 is valid section # technically rsync:x86_64 is valid section
# but in current configuration it must be considered as missing section # but in current configuration it must be considered as missing section

View File

@ -9,8 +9,8 @@ from pytest_mock import MockerFixture
from unittest.mock import MagicMock from unittest.mock import MagicMock
from ahriman.core.exceptions import BuildFailed, InvalidOption, UnsafeRun from ahriman.core.exceptions import BuildFailed, InvalidOption, UnsafeRun
from ahriman.core.util import check_output, check_user, exception_response_text, filter_json, package_like, \ from ahriman.core.util import check_output, check_user, exception_response_text, filter_json, full_version, \
pretty_datetime, pretty_size, tmpdir, walk package_like, pretty_datetime, pretty_size, tmpdir, walk
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -177,6 +177,16 @@ def test_filter_json_empty_value(package_ahriman: Package) -> None:
assert "base" not in filter_json(probe, probe.keys()) assert "base" not in filter_json(probe, probe.keys())
def test_full_version() -> None:
"""
must construct full version
"""
assert full_version("1", "r2388.d30e3201", "1") == "1:r2388.d30e3201-1"
assert full_version(None, "0.12.1", "1") == "0.12.1-1"
assert full_version(0, "0.12.1", "1") == "0.12.1-1"
assert full_version(1, "0.12.1", "1") == "1:0.12.1-1"
def test_package_like(package_ahriman: Package) -> None: def test_package_like(package_ahriman: Package) -> None:
""" """
package_like must return true for archives package_like must return true for archives
@ -298,24 +308,28 @@ def test_walk(resource_path_root: Path) -> None:
must traverse directory recursively must traverse directory recursively
""" """
expected = sorted([ expected = sorted([
resource_path_root / "core/ahriman.ini", resource_path_root / "core" / "ahriman.ini",
resource_path_root / "core/logging.ini", resource_path_root / "core" / "logging.ini",
resource_path_root / "models/aur_error", resource_path_root / "models" / "aur_error",
resource_path_root / "models/big_file_checksum", resource_path_root / "models" / "big_file_checksum",
resource_path_root / "models/empty_file_checksum", resource_path_root / "models" / "empty_file_checksum",
resource_path_root / "models/package_ahriman_aur", resource_path_root / "models" / "official_error",
resource_path_root / "models/package_ahriman_srcinfo", resource_path_root / "models" / "package_ahriman_aur",
resource_path_root / "models/package_tpacpi-bat-git_srcinfo", resource_path_root / "models" / "package_akonadi_aur",
resource_path_root / "models/package_yay_srcinfo", resource_path_root / "models" / "package_ahriman_srcinfo",
resource_path_root / "web/templates/build-status/login-modal.jinja2", resource_path_root / "models" / "package_gcc10_srcinfo",
resource_path_root / "web/templates/build-status/package-actions-modals.jinja2", resource_path_root / "models" / "package_tpacpi-bat-git_srcinfo",
resource_path_root / "web/templates/build-status/package-actions-script.jinja2", resource_path_root / "models" / "package_yay_srcinfo",
resource_path_root / "web/templates/static/favicon.ico", resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2",
resource_path_root / "web/templates/utils/bootstrap-scripts.jinja2", resource_path_root / "web" / "templates" / "build-status" / "package-actions-modals.jinja2",
resource_path_root / "web/templates/utils/style.jinja2", resource_path_root / "web" / "templates" / "build-status" / "package-actions-script.jinja2",
resource_path_root / "web/templates/build-status.jinja2", resource_path_root / "web" / "templates" / "static" / "favicon.ico",
resource_path_root / "web/templates/email-index.jinja2", resource_path_root / "web" / "templates" / "utils" / "bootstrap-scripts.jinja2",
resource_path_root / "web/templates/repo-index.jinja2", resource_path_root / "web" / "templates" / "utils" / "style.jinja2",
resource_path_root / "web" / "templates" / "build-status.jinja2",
resource_path_root / "web" / "templates" / "email-index.jinja2",
resource_path_root / "web" / "templates" / "repo-index.jinja2",
resource_path_root / "web" / "templates" / "telegram-index.jinja2",
]) ])
local_files = list(sorted(walk(resource_path_root))) local_files = list(sorted(walk(resource_path_root)))
assert local_files == expected assert local_files == expected

View File

@ -9,7 +9,7 @@ from typing import Any, Dict
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
def _get_data(resource_path_root: Path) -> Dict[str, Any]: def _get_aur_data(resource_path_root: Path) -> Dict[str, Any]:
""" """
load package description from resource file load package description from resource file
:param resource_path_root: path to resource root :param resource_path_root: path to resource root
@ -19,11 +19,21 @@ def _get_data(resource_path_root: Path) -> Dict[str, Any]:
return json.loads(response)["results"][0] return json.loads(response)["results"][0]
def _get_official_data(resource_path_root: Path) -> Dict[str, Any]:
"""
load package description from resource file
:param resource_path_root: path to resource root
:return: json descriptor
"""
response = (resource_path_root / "models" / "package_akonadi_aur").read_text()
return json.loads(response)["results"][0]
def test_from_json(aur_package_ahriman: AURPackage, resource_path_root: Path) -> None: def test_from_json(aur_package_ahriman: AURPackage, resource_path_root: Path) -> None:
""" """
must load package from json must load package from json
""" """
model = _get_data(resource_path_root) model = _get_aur_data(resource_path_root)
assert AURPackage.from_json(model) == aur_package_ahriman assert AURPackage.from_json(model) == aur_package_ahriman
@ -35,11 +45,19 @@ def test_from_json_2(aur_package_ahriman: AURPackage, mocker: MockerFixture) ->
assert AURPackage.from_json(asdict(aur_package_ahriman)) == aur_package_ahriman assert AURPackage.from_json(asdict(aur_package_ahriman)) == aur_package_ahriman
def test_from_repo(aur_package_akonadi: AURPackage, resource_path_root: Path) -> None:
"""
must load package from repository api json
"""
model = _get_official_data(resource_path_root)
assert AURPackage.from_repo(model) == aur_package_akonadi
def test_convert(aur_package_ahriman: AURPackage, resource_path_root: Path) -> None: def test_convert(aur_package_ahriman: AURPackage, resource_path_root: Path) -> None:
""" """
must convert fields to snakecase and also apply converters must convert fields to snakecase and also apply converters
""" """
model = _get_data(resource_path_root) model = _get_aur_data(resource_path_root)
converted = AURPackage.convert(model) converted = AURPackage.convert(model)
known_fields = [pair.name for pair in fields(AURPackage)] known_fields = [pair.name for pair in fields(AURPackage)]
assert all(field in known_fields for field in converted) assert all(field in known_fields for field in converted)

View File

@ -19,7 +19,7 @@ def test_build_status_enum_badges_color() -> None:
def test_build_status_enum_bootstrap_color() -> None: def test_build_status_enum_bootstrap_color() -> None:
""" """
status color must be one of shields.io supported status color must be one of bootstrap supported
""" """
SUPPORTED_COLORS = [ SUPPORTED_COLORS = [
"primary", "secondary", "success", "danger", "warning", "info", "light", "dark" "primary", "secondary", "success", "danger", "warning", "info", "light", "dark"

View File

@ -101,7 +101,7 @@ def test_from_aur(package_ahriman: Package, aur_package_ahriman: AURPackage, moc
""" """
must construct package from aur must construct package from aur
""" """
mocker.patch("ahriman.core.alpm.aur.AUR.info", return_value=aur_package_ahriman) mocker.patch("ahriman.core.alpm.remote.aur.AUR.info", return_value=aur_package_ahriman)
package = Package.from_aur(package_ahriman.base, package_ahriman.aur_url) package = Package.from_aur(package_ahriman.base, package_ahriman.aur_url)
assert package_ahriman.base == package.base assert package_ahriman.base == package.base
@ -154,6 +154,18 @@ def test_from_json_view_3(package_tpacpi_bat_git: Package) -> None:
assert Package.from_json(package_tpacpi_bat_git.view()) == package_tpacpi_bat_git assert Package.from_json(package_tpacpi_bat_git.view()) == package_tpacpi_bat_git
def test_from_official(package_ahriman: Package, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
must construct package from official repository
"""
mocker.patch("ahriman.core.alpm.remote.official.Official.info", return_value=aur_package_ahriman)
package = Package.from_official(package_ahriman.base, package_ahriman.aur_url)
assert package_ahriman.base == package.base
assert package_ahriman.version == package.version
assert package_ahriman.packages.keys() == package.packages.keys()
def test_load_resolve(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None: def test_load_resolve(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
""" """
must resolve source before package loading must resolve source before package loading
@ -193,6 +205,15 @@ def test_load_from_build(package_ahriman: Package, pyalpm_handle: MagicMock, moc
load_mock.assert_called_once_with(Path("path"), package_ahriman.aur_url) load_mock.assert_called_once_with(Path("path"), package_ahriman.aur_url)
def test_load_from_official(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must load package from AUR
"""
load_mock = mocker.patch("ahriman.models.package.Package.from_official")
Package.load("path", PackageSource.Repository, pyalpm_handle, package_ahriman.aur_url)
load_mock.assert_called_once_with("path", package_ahriman.aur_url)
def test_load_failure(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None: def test_load_failure(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
""" """
must raise InvalidPackageInfo on exception must raise InvalidPackageInfo on exception
@ -240,12 +261,14 @@ def test_dependencies_with_version(mocker: MockerFixture, resource_path_root: Pa
assert Package.dependencies(Path("path")) == {"git", "go", "pacman"} assert Package.dependencies(Path("path")) == {"git", "go", "pacman"}
def test_full_version() -> None: def test_dependencies_with_version_and_overlap(mocker: MockerFixture, resource_path_root: Path) -> None:
""" """
must construct full version must load correct list of dependencies with version
""" """
assert Package.full_version("1", "r2388.d30e3201", "1") == "1:r2388.d30e3201-1" srcinfo = (resource_path_root / "models" / "package_gcc10_srcinfo").read_text()
assert Package.full_version(None, "0.12.1", "1") == "0.12.1-1" mocker.patch("pathlib.Path.read_text", return_value=srcinfo)
assert Package.dependencies(Path("path")) == {"glibc", "doxygen", "binutils", "git", "libmpc", "python", "zstd"}
def test_actual_version(package_ahriman: Package, repository_paths: RepositoryPaths) -> None: def test_actual_version(package_ahriman: Package, repository_paths: RepositoryPaths) -> None:
@ -281,7 +304,7 @@ def test_actual_version_srcinfo_failed(package_tpacpi_bat_git: Package, reposito
def test_actual_version_vcs_failed(package_tpacpi_bat_git: Package, repository_paths: RepositoryPaths, def test_actual_version_vcs_failed(package_tpacpi_bat_git: Package, repository_paths: RepositoryPaths,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must return same version in case if exception occurred must return same version in case if there are errors during parse
""" """
mocker.patch("pathlib.Path.read_text", return_value="") mocker.patch("pathlib.Path.read_text", return_value="")
mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"])) mocker.patch("ahriman.models.package.parse_srcinfo", return_value=({"packages": {}}, ["an error"]))
@ -292,7 +315,7 @@ def test_actual_version_vcs_failed(package_tpacpi_bat_git: Package, repository_p
def test_full_depends(package_ahriman: Package, package_python_schedule: Package, pyalpm_package_ahriman: MagicMock, def test_full_depends(package_ahriman: Package, package_python_schedule: Package, pyalpm_package_ahriman: MagicMock,
pyalpm_handle: MagicMock, mocker: MockerFixture) -> None: pyalpm_handle: MagicMock) -> None:
""" """
must extract all dependencies from the package must extract all dependencies from the package
""" """

View File

@ -24,3 +24,6 @@ def test_from_option_valid() -> None:
assert ReportSettings.from_option("console") == ReportSettings.Console assert ReportSettings.from_option("console") == ReportSettings.Console
assert ReportSettings.from_option("conSOle") == ReportSettings.Console assert ReportSettings.from_option("conSOle") == ReportSettings.Console
assert ReportSettings.from_option("telegram") == ReportSettings.Telegram
assert ReportSettings.from_option("TElegraM") == ReportSettings.Telegram

View File

@ -21,7 +21,7 @@ async def test_get(client: TestClient, aur_package_ahriman: AURPackage, mocker:
""" """
must call get request correctly must call get request correctly
""" """
mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[aur_package_ahriman]) mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[aur_package_ahriman])
response = await client.get("/service-api/v1/search", params={"for": "ahriman"}) response = await client.get("/service-api/v1/search", params={"for": "ahriman"})
assert response.ok assert response.ok
@ -33,7 +33,7 @@ async def test_get_exception(client: TestClient, mocker: MockerFixture) -> None:
""" """
must raise 400 on empty search string must raise 400 on empty search string
""" """
search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.multisearch", return_value=[]) search_mock = mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch", return_value=[])
response = await client.get("/service-api/v1/search") response = await client.get("/service-api/v1/search")
assert response.status == 404 assert response.status == 404
@ -44,7 +44,7 @@ async def test_get_join(client: TestClient, mocker: MockerFixture) -> None:
""" """
must join search args with space must join search args with space
""" """
search_mock = mocker.patch("ahriman.core.alpm.aur.AUR.multisearch") search_mock = mocker.patch("ahriman.core.alpm.remote.aur.AUR.multisearch")
response = await client.get("/service-api/v1/search", params=[("for", "ahriman"), ("for", "maybe")]) response = await client.get("/service-api/v1/search", params=[("for", "ahriman"), ("for", "maybe")])
assert response.ok assert response.ok

View File

@ -52,6 +52,13 @@ homepage =
link_path = link_path =
template_path = ../web/templates/repo-index.jinja2 template_path = ../web/templates/repo-index.jinja2
[telegram]
api_key = apikey
chat_id = @ahrimantestchat
homepage =
link_path =
template_path = ../web/templates/telegram-index.jinja2
[upload] [upload]
target = target =

View File

@ -0,0 +1,8 @@
{
"version": 2,
"limit": 250,
"valid": false,
"results": [],
"num_pages": 1,
"page": 1
}

View File

@ -0,0 +1,55 @@
{
"version": 2,
"limit": 250,
"valid": true,
"results": [
{
"pkgname": "akonadi",
"pkgbase": "akonadi",
"repo": "extra",
"arch": "x86_64",
"pkgver": "21.12.3",
"pkgrel": "2",
"epoch": 0,
"pkgdesc": "PIM layer, which provides an asynchronous API to access all kind of PIM data",
"url": "https://kontact.kde.org",
"filename": "akonadi-21.12.3-2-x86_64.pkg.tar.zst",
"compressed_size": 789510,
"installed_size": 2592656,
"build_date": "2022-03-04T11:50:03Z",
"last_update": "2022-03-06T08:39:50.610Z",
"flag_date": null,
"maintainers": [
"felixonmars",
"arojas"
],
"packager": "arojas",
"groups": [],
"licenses": [
"LGPL"
],
"conflicts": [],
"provides": [],
"replaces": [],
"depends": [
"libakonadi",
"mariadb"
],
"optdepends": [
"postgresql: PostgreSQL backend"
],
"makedepends": [
"boost",
"doxygen",
"extra-cmake-modules",
"kaccounts-integration",
"kitemmodels",
"postgresql",
"qt5-tools"
],
"checkdepends": []
}
],
"num_pages": 1,
"page": 1
}

View File

@ -0,0 +1,57 @@
pkgbase = gcc10
pkgdesc = The GNU Compiler Collection (10.x.x)
pkgver = 10.3.0
pkgrel = 2
url = https://gcc.gnu.org
arch = x86_64
license = GPL
license = LGPL
license = FDL
license = custom
checkdepends = dejagnu
checkdepends = inetutils
makedepends = binutils
makedepends = doxygen
makedepends = git
makedepends = libmpc
makedepends = python
options = !emptydirs
options = !lto
source = https://sourceware.org/pub/gcc/releases/gcc-10.3.0/gcc-10.3.0.tar.xz
source = https://sourceware.org/pub/gcc/releases/gcc-10.3.0/gcc-10.3.0.tar.xz.sig
source = https://mirror.sobukus.de/files/src/isl/isl-0.24.tar.xz
source = c89
source = c99
validpgpkeys = F3691687D867B81B51CE07D9BBE43771487328A9
validpgpkeys = 86CFFCA918CF3AF47147588051E8B148A9999C34
validpgpkeys = 13975A70E63C361C73AE69EF6EEB81F8981C74C7
validpgpkeys = D3A93CAD751C2AF4F8C7AD516C35B99309B5FA62
b2sums = ac7898f5eb8a7c5f151a526d1bb38913a68b50a65e4d010ac09fa20b6c801c671c790d780f23ccb8e4ecdfc686f4aa588082ccc9eb5c80c7b0e30788f824c1eb
b2sums = SKIP
b2sums = 39cbfd18ad05778e3a5a44429261b45e4abc3efe7730ee890674d968890fe5e52c73bc1f8d271c7c3bc72d5754e3f7fcb209bd139e823d19cb9ea4ce1440164d
b2sums = a76d19c7830b0a141302890522086fc1548c177611501caac7e66d576e541b64ca3f6e977de715268a9872dfdd6368a011b92e01f7944ec0088f899ac0d2a2a5
b2sums = 02b655b5668f7dea51c3b3e4ff46d5a4aee5a04ed5e26b98a6470f39c2e98ddc0519bffeeedd982c31ef3c171457e4d1beaff32767d1aedd9346837aac4ec3ee
pkgname = gcc10
pkgdesc = The GNU Compiler Collection - C and C++ frontends (10.x.x)
depends = gcc10-libs=10.3.0-2
depends = binutils>=2.28
depends = libmpc
depends = zstd
options = !emptydirs
options = staticlibs
pkgname = gcc10-libs
pkgdesc = Runtime libraries shipped by GCC (10.x.x)
depends = glibc>=2.27
provides = libgfortran.so
provides = libubsan.so
provides = libasan.so
provides = libtsan.so
provides = liblsan.so
options = !emptydirs
options = !strip
pkgname = gcc10-fortran
pkgdesc = Fortran front-end for GCC (10.x.x)
depends = gcc10=10.3.0-2