mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-09-10 18:59:57 +00:00
Compare commits
8 Commits
2.0.0rc4
...
6551c8d983
Author | SHA1 | Date | |
---|---|---|---|
6551c8d983 | |||
a6c8d64053 | |||
fd78f2b5e2 | |||
900907cdaa | |||
5ff2f43506 | |||
dd521b49b5 | |||
5b1f5a8473 | |||
86af13f09e |
@ -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: 508 KiB After Width: | Height: | Size: 509 KiB |
@ -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.
|
||||||
|
40
docs/faq.md
40
docs/faq.md
@ -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?
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Evgeniy Alekseev
|
# Maintainer: Evgeniy Alekseev
|
||||||
|
|
||||||
pkgname='ahriman'
|
pkgname='ahriman'
|
||||||
pkgver=2.0.0rc4
|
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
|
||||||
|
@ -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 =
|
||||||
|
|
||||||
|
4
package/share/ahriman/templates/telegram-index.jinja2
Normal file
4
package/share/ahriman/templates/telegram-index.jinja2
Normal 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 %}
|
1
setup.py
1
setup.py
@ -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",
|
||||||
|
@ -89,7 +89,6 @@ 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)
|
||||||
@ -375,6 +374,12 @@ def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
|||||||
parser.add_argument("--depends-on", help="only rebuild packages that depend on specified package", action="append")
|
parser.add_argument("--depends-on", help="only rebuild packages that depend on specified package", action="append")
|
||||||
parser.add_argument("--dry-run", help="just perform check for packages without rebuild process itself",
|
parser.add_argument("--dry-run", help="just perform check for packages without rebuild process itself",
|
||||||
action="store_true")
|
action="store_true")
|
||||||
|
parser.add_argument("--from-database",
|
||||||
|
help="read packages from database instead of filesystem. This feature in particular is "
|
||||||
|
"required in case if you would like to restore repository from another repository "
|
||||||
|
"instance. Note however that in order to restore packages you need to have original "
|
||||||
|
"ahriman instance run with web service and have run repo-update at least once.",
|
||||||
|
action="store_true")
|
||||||
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
|
||||||
parser.set_defaults(handler=handlers.Rebuild)
|
parser.set_defaults(handler=handlers.Rebuild)
|
||||||
return parser
|
return parser
|
||||||
@ -410,21 +415,6 @@ 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
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
#
|
#
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from typing import List, Type
|
from typing import 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,20 +43,10 @@ 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)
|
||||||
packages = Add.extract_packages(application) if args.package is None else args.package
|
application.add(args.package, args.source, args.without_dependencies)
|
||||||
application.add(packages, args.source, args.without_dependencies)
|
|
||||||
if not args.now:
|
if not args.now:
|
||||||
return
|
return
|
||||||
|
|
||||||
packages = application.updates(packages, True, True, False, True, application.logger.info)
|
packages = application.updates(args.package, 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()]
|
|
||||||
|
@ -19,12 +19,13 @@
|
|||||||
#
|
#
|
||||||
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
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.formatters.update_printer import UpdatePrinter
|
from ahriman.core.formatters.update_printer import UpdatePrinter
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
|
||||||
|
|
||||||
class Rebuild(Handler):
|
class Rebuild(Handler):
|
||||||
@ -46,7 +47,10 @@ class Rebuild(Handler):
|
|||||||
depends_on = set(args.depends_on) if args.depends_on else None
|
depends_on = set(args.depends_on) if args.depends_on else None
|
||||||
|
|
||||||
application = Application(architecture, configuration, no_report, unsafe)
|
application = Application(architecture, configuration, no_report, unsafe)
|
||||||
updates = application.repository.packages_depends_on(depends_on)
|
if args.from_database:
|
||||||
|
updates = Rebuild.extract_packages(application)
|
||||||
|
else:
|
||||||
|
updates = application.repository.packages_depends_on(depends_on)
|
||||||
|
|
||||||
Rebuild.check_if_empty(args.exit_code, not updates)
|
Rebuild.check_if_empty(args.exit_code, not updates)
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
@ -56,3 +60,12 @@ class Rebuild(Handler):
|
|||||||
|
|
||||||
result = application.update(updates)
|
result = application.update(updates)
|
||||||
Rebuild.check_if_empty(args.exit_code, result.is_empty)
|
Rebuild.check_if_empty(args.exit_code, result.is_empty)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extract_packages(application: Application) -> List[Package]:
|
||||||
|
"""
|
||||||
|
extract packages from database file
|
||||||
|
:param application: application instance
|
||||||
|
:return: list of packages which were stored in database
|
||||||
|
"""
|
||||||
|
return [package for (package, _) in application.database.packages_get()]
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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"]
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
95
src/ahriman/core/report/telegram.py
Normal file
95
src/ahriman/core/report/telegram.py
Normal 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)
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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.0rc4"
|
__version__ = "2.0.0rc6"
|
||||||
|
@ -3,7 +3,6 @@ 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
|
||||||
@ -37,20 +36,6 @@ 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:
|
||||||
"""
|
"""
|
||||||
@ -87,12 +72,3 @@ 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()
|
|
||||||
|
@ -4,6 +4,7 @@ import pytest
|
|||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
from ahriman.application.handlers import Rebuild
|
from ahriman.application.handlers import Rebuild
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
@ -18,6 +19,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
|||||||
"""
|
"""
|
||||||
args.depends_on = []
|
args.depends_on = []
|
||||||
args.dry_run = False
|
args.dry_run = False
|
||||||
|
args.from_database = False
|
||||||
args.exit_code = False
|
args.exit_code = False
|
||||||
return args
|
return args
|
||||||
|
|
||||||
@ -42,6 +44,20 @@ def test_run(args: argparse.Namespace, package_ahriman: Package,
|
|||||||
check_mock.assert_has_calls([mock.call(False, False), mock.call(False, False)])
|
check_mock.assert_has_calls([mock.call(False, False), mock.call(False, False)])
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_extract_packages(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must run command
|
||||||
|
"""
|
||||||
|
args = _default_args(args)
|
||||||
|
args.from_database = True
|
||||||
|
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
|
||||||
|
mocker.patch("ahriman.application.application.Application.add")
|
||||||
|
extract_mock = mocker.patch("ahriman.application.handlers.Rebuild.extract_packages", return_value=[])
|
||||||
|
|
||||||
|
Rebuild.run(args, "x86_64", configuration, True, False)
|
||||||
|
extract_mock.assert_called_once_with(pytest.helpers.anyvar(int))
|
||||||
|
|
||||||
|
|
||||||
def test_run_dry_run(args: argparse.Namespace, configuration: Configuration,
|
def test_run_dry_run(args: argparse.Namespace, configuration: Configuration,
|
||||||
package_ahriman: Package, mocker: MockerFixture) -> None:
|
package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
@ -116,3 +132,12 @@ def test_run_build_empty_exception(args: argparse.Namespace, configuration: Conf
|
|||||||
|
|
||||||
Rebuild.run(args, "x86_64", configuration, True, False)
|
Rebuild.run(args, "x86_64", configuration, True, False)
|
||||||
check_mock.assert_has_calls([mock.call(True, False), mock.call(True, True)])
|
check_mock.assert_has_calls([mock.call(True, False), mock.call(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")
|
||||||
|
Rebuild.extract_packages(application)
|
||||||
|
packages_mock.assert_called_once_with()
|
||||||
|
@ -6,7 +6,6 @@ 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
|
||||||
|
|
||||||
@ -340,25 +339,6 @@ 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
|
||||||
|
@ -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}",
|
||||||
|
@ -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)
|
||||||
|
83
tests/ahriman/core/report/test_telegram.py
Normal file
83
tests/ahriman/core/report/test_telegram.py
Normal 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()
|
@ -329,6 +329,7 @@ def test_walk(resource_path_root: Path) -> None:
|
|||||||
resource_path_root / "web" / "templates" / "build-status.jinja2",
|
resource_path_root / "web" / "templates" / "build-status.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "email-index.jinja2",
|
resource_path_root / "web" / "templates" / "email-index.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "repo-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
|
||||||
|
@ -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
|
||||||
|
@ -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 =
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user