mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 15:27:17 +00:00
implement rss generation
This commit is contained in:
parent
fc508e19b8
commit
d2a7e3cd53
@ -251,6 +251,7 @@ Section name must be either ``email`` (plus optional architecture name, e.g. ``e
|
||||
* ``password`` - SMTP password to authenticate, string, optional.
|
||||
* ``port`` - SMTP port for sending emails, integer, required.
|
||||
* ``receivers`` - SMTP receiver addresses, space separated list of strings, required.
|
||||
* ``rss_url`` - link to RSS feed, string, optional.
|
||||
* ``sender`` - SMTP sender address, string, required.
|
||||
* ``ssl`` - SSL mode for SMTP connection, one of ``ssl``, ``starttls``, ``disabled``, optional, default ``disabled``.
|
||||
* ``template`` - Jinja2 template name, string, required.
|
||||
@ -266,7 +267,8 @@ Section name must be either ``html`` (plus optional architecture name, e.g. ``ht
|
||||
* ``type`` - type of the report, string, optional, must be set to ``html`` if exists.
|
||||
* ``homepage`` - link to homepage, string, optional.
|
||||
* ``link_path`` - prefix for HTML links, string, required.
|
||||
* ``path`` - path to html report file, string, required.
|
||||
* ``path`` - path to HTML report file, string, required.
|
||||
* ``rss_url`` - link to RSS feed, string, optional.
|
||||
* ``template`` - Jinja2 template name, string, required.
|
||||
* ``templates`` - path to templates directories, space separated list of paths, required.
|
||||
|
||||
@ -281,6 +283,20 @@ Section name must be either ``remote-call`` (plus optional architecture name, e.
|
||||
* ``manual`` - update manually built packages, boolean, optional, default ``no``.
|
||||
* ``wait_timeout`` - maximum amount of time in seconds to be waited before remote process will be terminated, integer, optional, default ``-1``.
|
||||
|
||||
``rss`` type
|
||||
^^^^^^^^^^^^
|
||||
|
||||
Section name must be either ``rss`` (plus optional architecture name, e.g. ``rss:x86_64``) or random name with ``type`` set.
|
||||
|
||||
* ``type`` - type of the report, string, optional, must be set to ``rss`` if exists.
|
||||
* ``homepage`` - link to homepage, string, optional.
|
||||
* ``link_path`` - prefix for HTML links, string, required.
|
||||
* ``max_entries`` - maximal amount of entries to be included to the report, negative means no limit, integer, optional, default ``-1``.
|
||||
* ``path`` - path to generated RSS file, string, required.
|
||||
* ``rss_url`` - link to RSS feed, string, optional.
|
||||
* ``template`` - Jinja2 template name, string, required.
|
||||
* ``templates`` - path to templates directories, space separated list of paths, required.
|
||||
|
||||
``telegram`` type
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
@ -291,6 +307,7 @@ Section name must be either ``telegram`` (plus optional architecture name, e.g.
|
||||
* ``chat_id`` - telegram chat id, either string with ``@`` or integer value, required.
|
||||
* ``homepage`` - link to homepage, string, optional.
|
||||
* ``link_path`` - prefix for HTML links, string, required.
|
||||
* ``rss_url`` - link to RSS feed, string, optional.
|
||||
* ``template`` - Jinja2 template name, string, required.
|
||||
* ``template_type`` - ``parse_mode`` to be passed to telegram API, one of ``MarkdownV2``, ``HTML``, ``Markdown``, string, optional, default ``HTML``.
|
||||
* ``templates`` - path to templates directories, space separated list of paths, required.
|
||||
|
@ -17,7 +17,7 @@ mirror = https://geo.mirror.pkgbuild.com/$repo/os/$arch
|
||||
repositories = core extra multilib
|
||||
; Pacman's root directory. In the most cases it must point to the system root.
|
||||
root = /
|
||||
; Sync files databases too, which is required by deep dependencies check
|
||||
; Sync files databases too, which is required by deep dependencies check.
|
||||
sync_files_database = yes
|
||||
; Use local packages cache. If this option is enabled, the service will be able to synchronize databases (available
|
||||
; as additional option for some subcommands). If set to no, databases must be synchronized manually.
|
||||
@ -52,17 +52,17 @@ allow_read_only = yes
|
||||
[build]
|
||||
; List of additional flags passed to archbuild command.
|
||||
;archbuild_flags =
|
||||
; Path to build command
|
||||
; Path to build command.
|
||||
;build_command =
|
||||
; List of packages to be ignored during automatic updates.
|
||||
;ignore_packages =
|
||||
; Include debug packages
|
||||
; Include debug packages.
|
||||
;include_debug_packages = yes
|
||||
; List of additional flags passed to makechrootpkg command.
|
||||
;makechrootpkg_flags =
|
||||
; List of additional flags passed to makepkg command.
|
||||
makepkg_flags = --nocolor --ignorearch
|
||||
; List of paths to be used for implicit dependency scan. Regular expressions are supported
|
||||
; List of paths to be used for implicit dependency scan. Regular expressions are supported.
|
||||
scan_paths = ^usr/lib(?!/cmake).*$
|
||||
; List of enabled triggers in the order of calls.
|
||||
triggers = ahriman.core.gitremote.RemotePullTrigger ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger ahriman.core.gitremote.RemotePushTrigger
|
||||
@ -212,14 +212,14 @@ target = console
|
||||
|
||||
; Console reporting trigger configuration sample.
|
||||
[console]
|
||||
; Trigger type name
|
||||
; Trigger type name.
|
||||
;type = console
|
||||
; Use utf8 symbols in output.
|
||||
use_utf = yes
|
||||
|
||||
; Email reporting trigger configuration sample.
|
||||
[email]
|
||||
; Trigger type name
|
||||
; Trigger type name.
|
||||
;type = email
|
||||
; Optional URL to the repository homepage.
|
||||
;homepage=
|
||||
@ -235,6 +235,8 @@ use_utf = yes
|
||||
;port =
|
||||
; List of emails to receive the reports.
|
||||
;receivers =
|
||||
; Optional link to the RSS feed.
|
||||
;rss_url =
|
||||
; Sender email.
|
||||
;sender =
|
||||
; SMTP server SSL mode, one of ssl, starttls, disabled.
|
||||
@ -250,7 +252,7 @@ templates = /usr/share/ahriman/templates
|
||||
|
||||
; HTML reporting trigger configuration sample.
|
||||
[html]
|
||||
; Trigger type name
|
||||
; Trigger type name.
|
||||
;type = html
|
||||
; Optional URL to the repository homepage.
|
||||
;homepage=
|
||||
@ -258,6 +260,8 @@ templates = /usr/share/ahriman/templates
|
||||
;link_path =
|
||||
; Output path for the HTML report.
|
||||
;path =
|
||||
; Optional link to the RSS feed.
|
||||
;rss_url =
|
||||
; Template name to be used.
|
||||
template = repo-index.jinja2
|
||||
; List of directories with templates.
|
||||
@ -265,7 +269,7 @@ templates = /usr/share/ahriman/templates
|
||||
|
||||
; Remote service callback trigger configuration sample.
|
||||
[remote-call]
|
||||
; Trigger type name
|
||||
; Trigger type name.
|
||||
;type = remote-call
|
||||
; Call for AUR packages update.
|
||||
;aur = no
|
||||
@ -276,9 +280,26 @@ templates = /usr/share/ahriman/templates
|
||||
; Wait until remote process will be terminated in seconds.
|
||||
;wait_timeout = -1
|
||||
|
||||
; RSS reporting trigger configuration sample.
|
||||
[rss]
|
||||
; Trigger type name.
|
||||
;type = rss
|
||||
; Optional URL to the repository homepage.
|
||||
;homepage=
|
||||
; Prefix for packages links. Link to a package will be formed as link_path / filename.
|
||||
;link_path =
|
||||
; Output path for the RSS report.
|
||||
;path =
|
||||
; Optional link to the RSS feed.
|
||||
;rss_url =
|
||||
; Template name to be used.
|
||||
template = rss.jinja2
|
||||
; List of directories with templates.
|
||||
templates = /usr/share/ahriman/templates
|
||||
|
||||
; Telegram reporting trigger configuration sample.
|
||||
[telegram]
|
||||
; Trigger type name
|
||||
; Trigger type name.
|
||||
;type = telegram
|
||||
; Telegram bot API key.
|
||||
;api_key =
|
||||
@ -288,6 +309,8 @@ templates = /usr/share/ahriman/templates
|
||||
;homepage=
|
||||
; Prefix for packages links. Link to a package will be formed as link_path / filename.
|
||||
;link_path =
|
||||
; Optional link to the RSS feed.
|
||||
;rss_url =
|
||||
; Template name to be used.
|
||||
template = telegram-index.jinja2
|
||||
; Telegram specific template mode, one of MarkdownV2, HTML or Markdown.
|
||||
@ -304,7 +327,7 @@ target =
|
||||
|
||||
; GitHub upload trigger configuration sample.
|
||||
[github]
|
||||
; Trigger type name
|
||||
; Trigger type name.
|
||||
;type = github
|
||||
; GitHub repository owner username.
|
||||
;owner =
|
||||
@ -321,14 +344,14 @@ target =
|
||||
|
||||
; Remote instance upload trigger configuration sample.
|
||||
[remote-service]
|
||||
; Trigger type name
|
||||
; Trigger type name.
|
||||
;type = remote-service
|
||||
; HTTP request timeout in seconds.
|
||||
;timeout = 30
|
||||
|
||||
; rsync upload trigger configuration sample.
|
||||
[rsync]
|
||||
; Trigger type name
|
||||
; Trigger type name.
|
||||
;type = rsync
|
||||
; rsync command to run.
|
||||
command = rsync --archive --compress --partial --delete
|
||||
@ -338,7 +361,7 @@ command = rsync --archive --compress --partial --delete
|
||||
|
||||
; S3 upload trigger configuration sample.
|
||||
[s3]
|
||||
; Trigger type name
|
||||
; Trigger type name.
|
||||
;type = s3
|
||||
; AWS services access key.
|
||||
;access_key =
|
||||
|
@ -7,6 +7,10 @@
|
||||
|
||||
{% include "utils/style.jinja2" %}
|
||||
{% include "user-style.jinja2" ignore missing %}
|
||||
|
||||
{% if rss_url is not none %}
|
||||
<link rel="alternate" href="{{ rss_url }}" type="application/rss+xml">
|
||||
{% endif %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
27
package/share/ahriman/templates/rss.jinja2
Normal file
27
package/share/ahriman/templates/rss.jinja2
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>{{ repository }}: Recent package updates</title>
|
||||
{% if homepage is not none %}
|
||||
<link>{{ homepage }}</link>
|
||||
{% endif %}
|
||||
<description>Recently updated packages in the {{ repository }}.</description>
|
||||
{% if rss_url is not none %}
|
||||
<atom:link href="{{ rss_url }}" rel="self"/>
|
||||
{% endif %}
|
||||
<language>en-us</language>
|
||||
<lastBuildDate>{{ last_update }}</lastBuildDate>
|
||||
|
||||
{% for package in packages %}
|
||||
<item>
|
||||
<title>{{ package.name }} {{ package.version }} {{ package.architecture }}</title>
|
||||
<link>{{ link_path }}/{{ package.filename }}</link>
|
||||
<description>{{ package.description }}</description>
|
||||
<pubDate>{{ package.build_date }}</pubDate>
|
||||
<guid isPermaLink="false">{{ package.tag }}</guid>
|
||||
<category>{{ repository }}</category>
|
||||
<category>{{ package.architecture }}</category>
|
||||
</item>
|
||||
{% endfor %}
|
||||
</channel>
|
||||
</rss>
|
@ -171,7 +171,7 @@ class PackageArchive:
|
||||
|
||||
result: dict[Path, list[FilesystemPackage]] = {}
|
||||
# sort items from children directories to root
|
||||
for path, packages in reversed(sorted(source.items())):
|
||||
for path, packages in sorted(source.items(), reverse=True):
|
||||
# skip if this path belongs to the one of the base packages
|
||||
if any(package.package_name in base_packages for package in packages):
|
||||
continue
|
||||
|
@ -17,14 +17,16 @@
|
||||
# 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 datetime
|
||||
import jinja2
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.utils import pretty_datetime, pretty_size
|
||||
from ahriman.core.utils import pretty_datetime, pretty_size, utcnow
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.result import Result
|
||||
from ahriman.models.sign_settings import SignSettings
|
||||
@ -37,6 +39,7 @@ class JinjaTemplate:
|
||||
It uses jinja2 templates for report generation, the following variables are allowed:
|
||||
|
||||
* homepage - link to homepage, string, optional
|
||||
* last_update - report generation time, pretty printed datetime, required
|
||||
* link_path - prefix fo packages to download, string, required
|
||||
* has_package_signed - ``True`` in case if package sign enabled, ``False`` otherwise, required
|
||||
* has_repo_signed - ``True`` in case if repository database sign enabled, ``False`` otherwise, required
|
||||
@ -46,21 +49,24 @@ class JinjaTemplate:
|
||||
* build_date, pretty printed datetime, string
|
||||
* depends, sorted list of strings
|
||||
* description, string
|
||||
* filename, string,
|
||||
* filename, string
|
||||
* groups, sorted list of strings
|
||||
* installed_size, pretty printed datetime, string
|
||||
* installed_size, pretty printed size, string
|
||||
* licenses, sorted list of strings
|
||||
* name, string
|
||||
* tag, string
|
||||
* url, string
|
||||
* version, string
|
||||
* pgp_key - default PGP key ID, string, optional
|
||||
* repository - repository name, string, required
|
||||
* rss_url - optional link to the RSS feed, string, optional
|
||||
|
||||
Attributes:
|
||||
default_pgp_key(str | None): default PGP key
|
||||
homepage(str | None): homepage link if any (for footer)
|
||||
link_path(str): prefix fo packages to download
|
||||
name(str): repository name
|
||||
rss_url(str | None): link to the RSS feed
|
||||
sign_targets(set[SignSettings]): targets to sign enabled in configuration
|
||||
templates(list[Path]): list of directories with templates
|
||||
"""
|
||||
@ -80,8 +86,36 @@ class JinjaTemplate:
|
||||
self.homepage = configuration.get(section, "homepage", fallback=None)
|
||||
self.link_path = configuration.get(section, "link_path")
|
||||
self.name = repository_id.name
|
||||
self.rss_url = configuration.get(section, "rss_url", fallback=None)
|
||||
self.sign_targets, self.default_pgp_key = GPG.sign_options(configuration)
|
||||
|
||||
@staticmethod
|
||||
def format_datetime(timestamp: datetime.datetime | float | int | None) -> str:
|
||||
"""
|
||||
convert datetime object to string
|
||||
|
||||
Args:
|
||||
timestamp(datetime.datetime | float | int | None): datetime to convert
|
||||
|
||||
Returns:
|
||||
str: datetime as string representation
|
||||
"""
|
||||
return pretty_datetime(timestamp)
|
||||
|
||||
@staticmethod
|
||||
def sort_content(content: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""
|
||||
sort content before rendering
|
||||
|
||||
Args:
|
||||
content(list[dict[str, str]]): content of the template
|
||||
|
||||
Returns:
|
||||
list[dict[str, str]]: sorted content according to comparator defined
|
||||
"""
|
||||
comparator: Callable[[dict[str, str]], str] = lambda item: item["filename"]
|
||||
return sorted(content, key=comparator)
|
||||
|
||||
def make_html(self, result: Result, template_name: Path | str) -> str:
|
||||
"""
|
||||
generate report for the specified packages
|
||||
@ -104,7 +138,7 @@ class JinjaTemplate:
|
||||
{
|
||||
"architecture": properties.architecture or "",
|
||||
"archive_size": pretty_size(properties.archive_size),
|
||||
"build_date": pretty_datetime(properties.build_date),
|
||||
"build_date": self.format_datetime(properties.build_date),
|
||||
"depends": properties.depends,
|
||||
"description": properties.description or "",
|
||||
"filename": properties.filename,
|
||||
@ -112,17 +146,20 @@ class JinjaTemplate:
|
||||
"installed_size": pretty_size(properties.installed_size),
|
||||
"licenses": properties.licenses,
|
||||
"name": package,
|
||||
"tag": f"tag:{self.name}:{properties.architecture}:{package}:{base.version}:{properties.build_date}",
|
||||
"url": properties.url or "",
|
||||
"version": base.version
|
||||
"version": base.version,
|
||||
} for base in result.success for package, properties in base.packages.items()
|
||||
]
|
||||
comparator: Callable[[dict[str, str]], str] = lambda item: item["filename"]
|
||||
|
||||
return template.render(
|
||||
homepage=self.homepage,
|
||||
last_update=self.format_datetime(utcnow()),
|
||||
link_path=self.link_path,
|
||||
has_package_signed=SignSettings.Packages in self.sign_targets,
|
||||
has_repo_signed=SignSettings.Repository in self.sign_targets,
|
||||
packages=sorted(content, key=comparator),
|
||||
packages=self.sort_content(content),
|
||||
pgp_key=self.default_pgp_key,
|
||||
repository=self.name)
|
||||
repository=self.name,
|
||||
rss_url=self.rss_url,
|
||||
)
|
||||
|
@ -66,7 +66,7 @@ class Report(LazyLogging):
|
||||
self.configuration = configuration
|
||||
|
||||
@staticmethod
|
||||
def load(repository_id: RepositoryId, configuration: Configuration, target: str) -> Report:
|
||||
def load(repository_id: RepositoryId, configuration: Configuration, target: str) -> Report: # pylint: disable=too-many-return-statements
|
||||
"""
|
||||
load client from settings
|
||||
|
||||
@ -92,6 +92,9 @@ class Report(LazyLogging):
|
||||
case ReportSettings.Telegram:
|
||||
from ahriman.core.report.telegram import Telegram
|
||||
return Telegram(repository_id, configuration, section)
|
||||
case ReportSettings.RSS:
|
||||
from ahriman.core.report.rss import RSS
|
||||
return RSS(repository_id, configuration, section)
|
||||
case ReportSettings.RemoteCall:
|
||||
from ahriman.core.report.remote_call import RemoteCall
|
||||
return RemoteCall(repository_id, configuration, section)
|
||||
|
@ -116,6 +116,11 @@ class ReportTrigger(Trigger):
|
||||
"required": True,
|
||||
"empty": False,
|
||||
},
|
||||
"rss_url": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
"is_url": ["http", "https"],
|
||||
},
|
||||
"sender": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
@ -187,6 +192,11 @@ class ReportTrigger(Trigger):
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
},
|
||||
"rss_url": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
"is_url": ["http", "https"],
|
||||
},
|
||||
"template": {
|
||||
"type": "string",
|
||||
"excludes": ["template_path"],
|
||||
@ -243,6 +253,11 @@ class ReportTrigger(Trigger):
|
||||
"empty": False,
|
||||
"is_url": [],
|
||||
},
|
||||
"rss_url": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
"is_url": ["http", "https"],
|
||||
},
|
||||
"template": {
|
||||
"type": "string",
|
||||
"excludes": ["template_path"],
|
||||
@ -304,7 +319,67 @@ class ReportTrigger(Trigger):
|
||||
"coerce": "integer",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"rss": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["rss"],
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
"is_url": ["http", "https"],
|
||||
},
|
||||
"link_path": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"empty": False,
|
||||
"is_url": [],
|
||||
},
|
||||
"max_entries": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
},
|
||||
"path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
},
|
||||
"rss_url": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
"is_url": ["http", "https"],
|
||||
},
|
||||
"template": {
|
||||
"type": "string",
|
||||
"excludes": ["template_path"],
|
||||
"dependencies": ["templates"],
|
||||
"required": True,
|
||||
"empty": False,
|
||||
},
|
||||
"template_path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"excludes": ["template"],
|
||||
"required": True,
|
||||
"path_exists": True,
|
||||
"path_type": "file",
|
||||
},
|
||||
"templates": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"path_exists": True,
|
||||
"path_type": "dir",
|
||||
},
|
||||
"empty": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
|
130
src/ahriman/core/report/rss.py
Normal file
130
src/ahriman/core/report/rss.py
Normal file
@ -0,0 +1,130 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import datetime
|
||||
|
||||
from collections.abc import Callable
|
||||
from email.utils import format_datetime, parsedate_to_datetime
|
||||
from typing import Any
|
||||
|
||||
from ahriman.core import context
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.report.jinja_template import JinjaTemplate
|
||||
from ahriman.core.report.report import Report
|
||||
from ahriman.core.status import Client
|
||||
from ahriman.models.event import EventType
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
class RSS(Report, JinjaTemplate):
|
||||
"""
|
||||
RSS report generator
|
||||
|
||||
Attributes:
|
||||
max_entries(int): the maximal amount of entries in RSS
|
||||
report_path(Path): output path to RSS report
|
||||
template(str): name of the template
|
||||
"""
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None:
|
||||
"""
|
||||
default constructor
|
||||
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
section(str): settings section name
|
||||
"""
|
||||
Report.__init__(self, repository_id, configuration)
|
||||
JinjaTemplate.__init__(self, repository_id, configuration, section)
|
||||
|
||||
self.max_entries = configuration.getint(section, "max_entries", fallback=-1)
|
||||
self.report_path = configuration.getpath(section, "path")
|
||||
self.template = configuration.get(section, "template")
|
||||
|
||||
@staticmethod
|
||||
def format_datetime(timestamp: datetime.datetime | float | int | None) -> str:
|
||||
"""
|
||||
convert datetime object to string
|
||||
|
||||
Args:
|
||||
timestamp(datetime.datetime | float | int | None): datetime to convert
|
||||
|
||||
Returns:
|
||||
str: datetime as string representation
|
||||
"""
|
||||
if timestamp is None:
|
||||
return ""
|
||||
if isinstance(timestamp, (int, float)):
|
||||
timestamp = datetime.datetime.fromtimestamp(timestamp, datetime.UTC)
|
||||
return format_datetime(timestamp)
|
||||
|
||||
@staticmethod
|
||||
def sort_content(content: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""
|
||||
sort content before rendering
|
||||
|
||||
Args:
|
||||
content(list[dict[str, str]]): content of the template
|
||||
|
||||
Returns:
|
||||
list[dict[str, str]]: sorted content according to comparator defined
|
||||
"""
|
||||
comparator: Callable[[dict[str, str]], datetime.datetime] = \
|
||||
lambda item: parsedate_to_datetime(item["build_date"])
|
||||
return sorted(content, key=comparator, reverse=True)
|
||||
|
||||
def content(self, packages: list[Package]) -> Result:
|
||||
"""
|
||||
extract result to be written to template
|
||||
|
||||
Args:
|
||||
packages(list[Package]): list of packages to generate report
|
||||
|
||||
Returns:
|
||||
Result: result descriptor
|
||||
"""
|
||||
ctx = context.get()
|
||||
reporter = ctx.get(Client)
|
||||
events = reporter.event_get(EventType.PackageUpdated, None, limit=self.max_entries)
|
||||
|
||||
known_packages = {package.base: package for package in packages}
|
||||
|
||||
result = Result()
|
||||
for event in events:
|
||||
package = known_packages.get(event.object_id)
|
||||
if package is None:
|
||||
continue # package not found
|
||||
result.add_updated(package)
|
||||
|
||||
return result
|
||||
|
||||
def generate(self, packages: list[Package], result: Result) -> None:
|
||||
"""
|
||||
generate report for the specified packages
|
||||
|
||||
Args:
|
||||
packages(list[Package]): list of packages to generate report
|
||||
result(Result): build result
|
||||
"""
|
||||
result = self.content(packages)
|
||||
rss = self.make_html(result, self.template)
|
||||
self.report_path.write_text(rss, encoding="utf8")
|
@ -32,6 +32,7 @@ class ReportSettings(StrEnum):
|
||||
Email(ReportSettings): (class attribute) email report generation
|
||||
Console(ReportSettings): (class attribute) print result to console
|
||||
Telegram(ReportSettings): (class attribute) markdown report to telegram channel
|
||||
RSS(ReportSettings): (class attribute) RSS report generation
|
||||
RemoteCall(ReportSettings): (class attribute) remote ahriman server call
|
||||
"""
|
||||
|
||||
@ -40,10 +41,11 @@ class ReportSettings(StrEnum):
|
||||
Email = "email"
|
||||
Console = "console"
|
||||
Telegram = "telegram"
|
||||
RSS = "rss"
|
||||
RemoteCall = "remote-call"
|
||||
|
||||
@staticmethod
|
||||
def from_option(value: str) -> ReportSettings:
|
||||
def from_option(value: str) -> ReportSettings: # pylint: disable=too-many-return-statements
|
||||
"""
|
||||
construct value from configuration
|
||||
|
||||
@ -62,6 +64,8 @@ class ReportSettings(StrEnum):
|
||||
return ReportSettings.Console
|
||||
case "telegram":
|
||||
return ReportSettings.Telegram
|
||||
case "rss":
|
||||
return ReportSettings.RSS
|
||||
case "ahriman" | "remote-call":
|
||||
return ReportSettings.RemoteCall
|
||||
case _:
|
||||
|
@ -75,6 +75,7 @@ def test_schema(configuration: Configuration) -> None:
|
||||
assert schema.pop("remote-push")
|
||||
assert schema.pop("remote-service")
|
||||
assert schema.pop("report")
|
||||
assert schema.pop("rss")
|
||||
assert schema.pop("rsync")
|
||||
assert schema.pop("s3")
|
||||
assert schema.pop("telegram")
|
||||
|
@ -3,6 +3,7 @@ import pytest
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.report.email import Email
|
||||
from ahriman.core.report.remote_call import RemoteCall
|
||||
from ahriman.core.report.rss import RSS
|
||||
from ahriman.core.report.telegram import Telegram
|
||||
|
||||
|
||||
@ -15,7 +16,7 @@ def email(configuration: Configuration) -> Email:
|
||||
configuration(Configuration): configuration fixture
|
||||
|
||||
Returns:
|
||||
RemoteCall: email trigger test instance
|
||||
Email: email trigger test instance
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
return Email(repository_id, configuration, "email")
|
||||
@ -38,6 +39,21 @@ def remote_call(configuration: Configuration) -> RemoteCall:
|
||||
return RemoteCall(repository_id, configuration, "remote-call")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rss(configuration: Configuration) -> RSS:
|
||||
"""
|
||||
fixture for rss trigger
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration fixture
|
||||
|
||||
Returns:
|
||||
RSS: rss trigger test instance
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
return RSS(repository_id, configuration, "rss")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def telegram(configuration: Configuration) -> Telegram:
|
||||
"""
|
||||
@ -47,7 +63,7 @@ def telegram(configuration: Configuration) -> Telegram:
|
||||
configuration(Configuration): configuration fixture
|
||||
|
||||
Returns:
|
||||
RemoteCall: telegram trigger test instance
|
||||
Telegram: telegram trigger test instance
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
return Telegram(repository_id, configuration, "telegram")
|
||||
|
@ -1,9 +1,24 @@
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.report.jinja_template import JinjaTemplate
|
||||
from ahriman.core.utils import utcnow
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
def test_format_datetime() -> None:
|
||||
"""
|
||||
must format datetime
|
||||
"""
|
||||
assert JinjaTemplate.format_datetime(utcnow())
|
||||
|
||||
|
||||
def sort_content() -> None:
|
||||
"""
|
||||
must sort content for the template
|
||||
"""
|
||||
assert JinjaTemplate.sort_content([{"filename": "2"}, {"filename": "1"}]) == [{"filename": "1"}, {"filename": "2"}]
|
||||
|
||||
|
||||
def test_generate(configuration: Configuration, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must generate html report
|
||||
|
@ -78,6 +78,17 @@ def test_report_remote_call(configuration: Configuration, result: Result, mocker
|
||||
report_mock.assert_called_once_with([], result)
|
||||
|
||||
|
||||
def test_report_rss(configuration: Configuration, result: Result, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must instantiate rss trigger
|
||||
"""
|
||||
report_mock = mocker.patch("ahriman.core.report.rss.RSS.generate")
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
Report.load(repository_id, configuration, "rss").run(result, [])
|
||||
report_mock.assert_called_once_with([], result)
|
||||
|
||||
|
||||
def test_report_telegram(configuration: Configuration, result: Result, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must generate telegram report
|
||||
|
79
tests/ahriman/core/report/test_rss.py
Normal file
79
tests/ahriman/core/report/test_rss.py
Normal file
@ -0,0 +1,79 @@
|
||||
import pytest
|
||||
|
||||
from email.utils import parsedate_to_datetime
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from ahriman.core.report.rss import RSS
|
||||
from ahriman.core.status import Client
|
||||
from ahriman.core.utils import utcnow
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
def test_format_datetime() -> None:
|
||||
"""
|
||||
must format timestamp to rfc format
|
||||
"""
|
||||
timestamp = utcnow().replace(microsecond=0)
|
||||
assert parsedate_to_datetime(RSS.format_datetime(timestamp.timestamp())) == timestamp
|
||||
|
||||
|
||||
def test_format_datetime_datetime() -> None:
|
||||
"""
|
||||
must format datetime to rfc format
|
||||
"""
|
||||
timestamp = utcnow().replace(microsecond=0)
|
||||
assert parsedate_to_datetime(RSS.format_datetime(timestamp)) == timestamp
|
||||
|
||||
|
||||
def test_format_datetime_empty() -> None:
|
||||
"""
|
||||
must generate empty string from None timestamp
|
||||
"""
|
||||
assert RSS.format_datetime(None) == ""
|
||||
|
||||
|
||||
def test_sort_content() -> None:
|
||||
"""
|
||||
must sort content for the template
|
||||
"""
|
||||
assert RSS.sort_content([
|
||||
{"filename": "2", "build_date": "Thu, 29 Aug 2024 16:36:55 -0000"},
|
||||
{"filename": "1", "build_date": "Thu, 29 Aug 2024 16:36:54 -0000"},
|
||||
{"filename": "3", "build_date": "Thu, 29 Aug 2024 16:36:56 -0000"},
|
||||
]) == [
|
||||
{"filename": "3", "build_date": "Thu, 29 Aug 2024 16:36:56 -0000"},
|
||||
{"filename": "2", "build_date": "Thu, 29 Aug 2024 16:36:55 -0000"},
|
||||
{"filename": "1", "build_date": "Thu, 29 Aug 2024 16:36:54 -0000"},
|
||||
]
|
||||
|
||||
|
||||
def test_content(rss: RSS, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must generate RSS content correctly
|
||||
"""
|
||||
client_mock = MagicMock()
|
||||
client_mock.event_get.return_value = [
|
||||
Event(EventType.PackageUpdated, package_ahriman.base),
|
||||
Event(EventType.PackageUpdated, "random"),
|
||||
Event(EventType.PackageUpdated, package_ahriman.base),
|
||||
]
|
||||
context_mock = mocker.patch("ahriman.core._Context.get", return_value=client_mock)
|
||||
|
||||
assert rss.content([package_ahriman]).success == [package_ahriman]
|
||||
context_mock.assert_called_once_with(Client)
|
||||
client_mock.event_get.assert_called_once_with(EventType.PackageUpdated, None, limit=rss.max_entries)
|
||||
|
||||
|
||||
def test_generate(rss: RSS, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must generate report
|
||||
"""
|
||||
content_mock = mocker.patch("ahriman.core.report.rss.RSS.content", return_value=Result())
|
||||
write_mock = mocker.patch("pathlib.Path.write_text")
|
||||
|
||||
rss.generate([package_ahriman], Result())
|
||||
content_mock.assert_called_once_with([package_ahriman])
|
||||
write_mock.assert_called_once_with(pytest.helpers.anyvar(int), encoding="utf8")
|
@ -489,6 +489,7 @@ def test_walk(resource_path_root: Path) -> None:
|
||||
resource_path_root / "web" / "templates" / "email-index.jinja2",
|
||||
resource_path_root / "web" / "templates" / "error.jinja2",
|
||||
resource_path_root / "web" / "templates" / "repo-index.jinja2",
|
||||
resource_path_root / "web" / "templates" / "rss.jinja2",
|
||||
resource_path_root / "web" / "templates" / "shell",
|
||||
resource_path_root / "web" / "templates" / "telegram-index.jinja2",
|
||||
])
|
||||
|
@ -24,6 +24,9 @@ def test_from_option_valid() -> None:
|
||||
assert ReportSettings.from_option("telegram") == ReportSettings.Telegram
|
||||
assert ReportSettings.from_option("TElegraM") == ReportSettings.Telegram
|
||||
|
||||
assert ReportSettings.from_option("rss") == ReportSettings.RSS
|
||||
assert ReportSettings.from_option("RSS") == ReportSettings.RSS
|
||||
|
||||
assert ReportSettings.from_option("remote-call") == ReportSettings.RemoteCall
|
||||
assert ReportSettings.from_option("reMOte-cALL") == ReportSettings.RemoteCall
|
||||
assert ReportSettings.from_option("ahriman") == ReportSettings.RemoteCall
|
||||
|
@ -81,6 +81,13 @@ templates = ../web/templates
|
||||
[remote-call]
|
||||
manual = yes
|
||||
|
||||
[rss]
|
||||
path =
|
||||
homepage =
|
||||
link_path =
|
||||
template = rss.jinja2
|
||||
templates = ../web/templates
|
||||
|
||||
[telegram]
|
||||
api_key = apikey
|
||||
chat_id = @ahrimantestchat
|
||||
|
Loading…
Reference in New Issue
Block a user