mirror of
				https://github.com/arcan1s/ahriman.git
				synced 2025-10-30 21:33:43 +00:00 
			
		
		
		
	Email report (#11)
* Demo email report implementation * improved ssl mode * correct default option spelling and more fields to be hidden for not extended reports
This commit is contained in:
		| @ -47,7 +47,22 @@ Settings for signing packages or repository. Group name must refer to architectu | ||||
|  | ||||
| Report generation settings. | ||||
|  | ||||
| * `target` - list of reports to be generated, space separated list of strings, optional. Allowed values are `html`. | ||||
| * `target` - list of reports to be generated, space separated list of strings, optional. Allowed values are `html`, `email`. | ||||
|  | ||||
| ### `email:*` groups | ||||
|  | ||||
| Group name must refer to architecture, e.g. it should be `email:x86_64` for x86_64 architecture. | ||||
|  | ||||
| * `homepage` - link to homepage, string, optional. | ||||
| * `host` - SMTP host for sending emails, string, required. | ||||
| * `link_path` - prefix for HTML links, string, required. | ||||
| * `password` - SMTP password to authenticate, string, optional. | ||||
| * `port` - SMTP port for sending emails, int, required. | ||||
| * `receivers` - SMTP receiver addresses, space separated list of strings, required. | ||||
| * `sender` - SMTP sender address, string, required. | ||||
| * `ssl` - SSL mode for SMTP connection, one of `ssl`, `starttls`, `disabled`, optional, default `disabled`. | ||||
| * `template_path` - path to Jinja2 template, string, required. | ||||
| * `user` - SMTP user to authenticate, string, optional. | ||||
|  | ||||
| ### `html:*` groups | ||||
|  | ||||
|  | ||||
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							| @ -25,7 +25,7 @@ archlinux: archive | ||||
|  | ||||
| check: clean | ||||
| 	cd src && mypy --implicit-reexport --strict -p "$(PROJECT)" | ||||
| 	find "src/$(PROJECT)" tests -name "*.py" -execdir autopep8 --exit-code --max-line-length 120 -aa -i {} + | ||||
| 	find "src/$(PROJECT)" "tests/$(PROJECT)" -name "*.py" -execdir autopep8 --exit-code --max-line-length 120 -aa -i {} + | ||||
| 	cd src && pylint --rcfile=../.pylintrc "$(PROJECT)" | ||||
|  | ||||
| clean: | ||||
|  | ||||
| @ -25,10 +25,11 @@ target = | ||||
| [report] | ||||
| target = | ||||
|  | ||||
| [email] | ||||
| template_path = /usr/share/ahriman/repo-index.jinja2 | ||||
| ssl = disabled | ||||
|  | ||||
| [html] | ||||
| path = | ||||
| homepage = | ||||
| link_path = | ||||
| template_path = /usr/share/ahriman/repo-index.jinja2 | ||||
|  | ||||
| [upload] | ||||
|  | ||||
| @ -5,26 +5,30 @@ | ||||
|  | ||||
|         {% include "style.jinja2" %} | ||||
|  | ||||
|         {% include "sorttable.jinja2" %} | ||||
|         {% include "search.jinja2" %} | ||||
|         {% if extended_report %} | ||||
|             {% include "sorttable.jinja2" %} | ||||
|             {% include "search.jinja2" %} | ||||
|         {% endif %} | ||||
|     </head> | ||||
|  | ||||
|     <body> | ||||
|         <div class="root"> | ||||
|             <h1>Archlinux user repository</h1> | ||||
|             {% if extended_report %} | ||||
|                 <h1>Archlinux user repository</h1> | ||||
|  | ||||
|             <section class="element"> | ||||
|                 {% if pgp_key is not none %} | ||||
|                     <p>This repository is signed with <a href="http://keys.gnupg.net/pks/lookup?search=0x{{ pgp_key|e }}&fingerprint=on&op=index" title="key search">{{ pgp_key|e }}</a> by default.</p> | ||||
|                 {% endif %} | ||||
|                 <section class="element"> | ||||
|                     {% if pgp_key is not none %} | ||||
|                         <p>This repository is signed with <a href="http://keys.gnupg.net/pks/lookup?search=0x{{ pgp_key|e }}&fingerprint=on&op=index" title="key search">{{ pgp_key|e }}</a> by default.</p> | ||||
|                     {% endif %} | ||||
|  | ||||
|                 <code> | ||||
|                     $ cat /etc/pacman.conf<br> | ||||
|                     [{{ repository|e }}]<br> | ||||
|                     Server = {{ link_path|e }}<br> | ||||
|                     SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly | ||||
|                 </code> | ||||
|             </section> | ||||
|                     <code> | ||||
|                         $ cat /etc/pacman.conf<br> | ||||
|                         [{{ repository|e }}]<br> | ||||
|                         Server = {{ link_path|e }}<br> | ||||
|                         SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly | ||||
|                     </code> | ||||
|                 </section> | ||||
|             {% endif %} | ||||
|  | ||||
|             {% include "search-line.jinja2" %} | ||||
|  | ||||
| @ -50,13 +54,15 @@ | ||||
|                 </table> | ||||
|             </section> | ||||
|  | ||||
|             <footer> | ||||
|                 <ul class="navigation"> | ||||
|                     {% if homepage is not none %} | ||||
|                         <li><a href="{{ homepage|e }}" title="homepage">Homepage</a></li> | ||||
|                     {% endif %} | ||||
|                 </ul> | ||||
|             </footer> | ||||
|             {% if extended_report %} | ||||
|                 <footer> | ||||
|                     <ul class="navigation"> | ||||
|                         {% if homepage is not none %} | ||||
|                             <li><a href="{{ homepage|e }}" title="homepage">Homepage</a></li> | ||||
|                         {% endif %} | ||||
|                     </ul> | ||||
|                 </footer> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|     </body> | ||||
| </html> | ||||
|  | ||||
| @ -51,12 +51,12 @@ class Application: | ||||
|         self.architecture = architecture | ||||
|         self.repository = Repository(architecture, configuration) | ||||
|  | ||||
|     def _finalize(self) -> None: | ||||
|     def _finalize(self, built_packages: Iterable[Package]) -> None: | ||||
|         """ | ||||
|         generate report and sync to remote server | ||||
|         """ | ||||
|         self.report([]) | ||||
|         self.sync([]) | ||||
|         self.report([], built_packages) | ||||
|         self.sync([], built_packages) | ||||
|  | ||||
|     def _known_packages(self) -> Set[str]: | ||||
|         """ | ||||
| @ -160,15 +160,16 @@ class Application: | ||||
|         :param names: list of packages (either base or name) to remove | ||||
|         """ | ||||
|         self.repository.process_remove(names) | ||||
|         self._finalize() | ||||
|         self._finalize([]) | ||||
|  | ||||
|     def report(self, target: Iterable[str]) -> None: | ||||
|     def report(self, target: Iterable[str], built_packages: Iterable[Package]) -> None: | ||||
|         """ | ||||
|         generate report | ||||
|         :param target: list of targets to run (e.g. html) | ||||
|         :param built_packages: list of packages which has just been built | ||||
|         """ | ||||
|         targets = target or None | ||||
|         self.repository.process_report(targets) | ||||
|         self.repository.process_report(targets, built_packages) | ||||
|  | ||||
|     def sign(self, packages: Iterable[str]) -> None: | ||||
|         """ | ||||
| @ -191,15 +192,16 @@ class Application: | ||||
|         self.update([]) | ||||
|         # sign repository database if set | ||||
|         self.repository.sign.sign_repository(self.repository.repo.repo_path) | ||||
|         self._finalize() | ||||
|         self._finalize([]) | ||||
|  | ||||
|     def sync(self, target: Iterable[str]) -> None: | ||||
|     def sync(self, target: Iterable[str], built_packages: Iterable[Package]) -> None: | ||||
|         """ | ||||
|         sync to remote server | ||||
|         :param target: list of targets to run (e.g. s3) | ||||
|         :param built_packages: list of packages which has just been built | ||||
|         """ | ||||
|         targets = target or None | ||||
|         self.repository.process_sync(targets) | ||||
|         self.repository.process_sync(targets, built_packages) | ||||
|  | ||||
|     def update(self, updates: Iterable[Package]) -> None: | ||||
|         """ | ||||
| @ -207,8 +209,9 @@ class Application: | ||||
|         :param updates: list of packages to update | ||||
|         """ | ||||
|         def process_update(paths: Iterable[Path]) -> None: | ||||
|             updated = [Package.load(path, self.repository.pacman, self.repository.aur_url) for path in paths] | ||||
|             self.repository.process_update(paths) | ||||
|             self._finalize() | ||||
|             self._finalize(updated) | ||||
|  | ||||
|         # process built packages | ||||
|         packages = self.repository.packages_built() | ||||
|  | ||||
| @ -39,4 +39,4 @@ class Report(Handler): | ||||
|         :param architecture: repository architecture | ||||
|         :param configuration: configuration instance | ||||
|         """ | ||||
|         Application(architecture, configuration).report(args.target) | ||||
|         Application(architecture, configuration).report(args.target, []) | ||||
|  | ||||
| @ -39,4 +39,4 @@ class Sync(Handler): | ||||
|         :param architecture: repository architecture | ||||
|         :param configuration: configuration instance | ||||
|         """ | ||||
|         Application(architecture, configuration).sync(args.target) | ||||
|         Application(architecture, configuration).sync(args.target, []) | ||||
|  | ||||
							
								
								
									
										101
									
								
								src/ahriman/core/report/email.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/ahriman/core/report/email.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,101 @@ | ||||
| # | ||||
| # Copyright (c) 2021 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 | ||||
| import smtplib | ||||
|  | ||||
| from email.mime.multipart import MIMEMultipart | ||||
| from email.mime.text import MIMEText | ||||
| from typing import Dict, 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 pretty_datetime | ||||
| from ahriman.models.package import Package | ||||
| from ahriman.models.smtp_ssl_settings import SmtpSSLSettings | ||||
|  | ||||
|  | ||||
| class Email(Report, JinjaTemplate): | ||||
|     """ | ||||
|     email report generator | ||||
|     :ivar host: SMTP host to connect | ||||
|     :ivar password: password to authenticate via SMTP | ||||
|     :ivar port: SMTP port to connect | ||||
|     :ivar receivers: list of receivers emails | ||||
|     :ivar sender: sender email address | ||||
|     :ivar ssl: SSL mode for SMTP connection | ||||
|     :ivar user: username to authenticate via SMTP | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, architecture: str, configuration: Configuration) -> None: | ||||
|         """ | ||||
|         default constructor | ||||
|         :param architecture: repository architecture | ||||
|         :param configuration: configuration instance | ||||
|         """ | ||||
|         Report.__init__(self, architecture, configuration) | ||||
|         JinjaTemplate.__init__(self, "email", configuration) | ||||
|  | ||||
|         # base smtp settings | ||||
|         self.host = configuration.get("email", "host") | ||||
|         self.password = configuration.get("email", "password", fallback=None) | ||||
|         self.port = configuration.getint("email", "port") | ||||
|         self.receivers = configuration.getlist("email", "receivers") | ||||
|         self.sender = configuration.get("email", "sender") | ||||
|         self.ssl = SmtpSSLSettings.from_option(configuration.get("email", "ssl", fallback="disabled")) | ||||
|         self.user = configuration.get("email", "user", fallback=None) | ||||
|  | ||||
|     def _send(self, text: str, attachment: Dict[str, str]) -> None: | ||||
|         """ | ||||
|         send email callback | ||||
|         :param text: email body text | ||||
|         :param attachment: map of attachment filename to attachment content | ||||
|         """ | ||||
|         message = MIMEMultipart() | ||||
|         message["From"] = self.sender | ||||
|         message["To"] = ", ".join(self.receivers) | ||||
|         message["Subject"] = f"{self.name} build report at {pretty_datetime(datetime.datetime.utcnow().timestamp())}" | ||||
|  | ||||
|         message.attach(MIMEText(text, "html")) | ||||
|         for filename, content in attachment.items(): | ||||
|             attach = MIMEText(content, "html") | ||||
|             attach.add_header("Content-Disposition", "attachment", filename=filename) | ||||
|             message.attach(attach) | ||||
|  | ||||
|         if self.ssl != SmtpSSLSettings.SSL: | ||||
|             session = smtplib.SMTP(self.host, self.port) | ||||
|             if self.ssl == SmtpSSLSettings.STARTTLS: | ||||
|                 session.starttls() | ||||
|         else: | ||||
|             session = smtplib.SMTP_SSL(self.host, self.port) | ||||
|         if self.user is not None and self.password is not None: | ||||
|             session.login(self.user, self.password) | ||||
|         session.sendmail(self.sender, self.receivers, message.as_string()) | ||||
|         session.quit() | ||||
|  | ||||
|     def generate(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None: | ||||
|         """ | ||||
|         generate report for the specified packages | ||||
|         :param packages: list of packages to generate report | ||||
|         :param built_packages: list of packages which has just been built | ||||
|         """ | ||||
|         text = self.make_html(built_packages, False) | ||||
|         attachments = {"index.html": self.make_html(packages, True)} | ||||
|         self._send(text, attachments) | ||||
| @ -17,51 +17,18 @@ | ||||
| # 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 jinja2 | ||||
|  | ||||
| from typing import Callable, Dict, Iterable | ||||
| 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.sign.gpg import GPG | ||||
| from ahriman.core.util import pretty_datetime, pretty_size | ||||
| from ahriman.models.package import Package | ||||
| from ahriman.models.sign_settings import SignSettings | ||||
|  | ||||
|  | ||||
| class HTML(Report): | ||||
| class HTML(Report, JinjaTemplate): | ||||
|     """ | ||||
|     html report generator | ||||
|  | ||||
|     It uses jinja2 templates for report generation, the following variables are allowed: | ||||
|  | ||||
|         homepage - link to homepage, string, optional | ||||
|         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 | ||||
|         packages - sorted list of packages properties, required | ||||
|                    * architecture, string | ||||
|                    * archive_size, pretty printed size, string | ||||
|                    * build_date, pretty printed datetime, string | ||||
|                    * depends, sorted list of strings | ||||
|                    * description, string | ||||
|                    * filename, string, | ||||
|                    * groups, sorted list of strings | ||||
|                    * installed_size, pretty printed datetime, string | ||||
|                    * licenses, sorted list of strings | ||||
|                    * name, string | ||||
|                    * url, string | ||||
|                    * version, string | ||||
|         pgp_key - default PGP key ID, string, optional | ||||
|         repository - repository name, string, required | ||||
|  | ||||
|     :ivar homepage: homepage link if any (for footer) | ||||
|     :ivar link_path: prefix fo packages to download | ||||
|     :ivar name: repository name | ||||
|     :ivar default_pgp_key: default PGP key | ||||
|     :ivar report_path: output path to html report | ||||
|     :ivar sign_targets: targets to sign enabled in configuration | ||||
|     :ivar template_path: path to directory with jinja templates | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, architecture: str, configuration: Configuration) -> None: | ||||
| @ -71,51 +38,15 @@ class HTML(Report): | ||||
|         :param configuration: configuration instance | ||||
|         """ | ||||
|         Report.__init__(self, architecture, configuration) | ||||
|         JinjaTemplate.__init__(self, "html", configuration) | ||||
|  | ||||
|         self.report_path = configuration.getpath("html", "path") | ||||
|         self.link_path = configuration.get("html", "link_path") | ||||
|         self.template_path = configuration.getpath("html", "template_path") | ||||
|  | ||||
|         # base template vars | ||||
|         self.homepage = configuration.get("html", "homepage", fallback=None) | ||||
|         self.name = configuration.get("repository", "name") | ||||
|  | ||||
|         self.sign_targets, self.default_pgp_key = GPG.sign_options(configuration) | ||||
|  | ||||
|     def generate(self, packages: Iterable[Package]) -> None: | ||||
|     def generate(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None: | ||||
|         """ | ||||
|         generate report for the specified packages | ||||
|         :param packages: list of packages to generate report | ||||
|         :param built_packages: list of packages which has just been built | ||||
|         """ | ||||
|         # idea comes from https://stackoverflow.com/a/38642558 | ||||
|         loader = jinja2.FileSystemLoader(searchpath=self.template_path.parent) | ||||
|         environment = jinja2.Environment(loader=loader) | ||||
|         template = environment.get_template(self.template_path.name) | ||||
|  | ||||
|         content = [ | ||||
|             { | ||||
|                 "architecture": properties.architecture or "", | ||||
|                 "archive_size": pretty_size(properties.archive_size), | ||||
|                 "build_date": pretty_datetime(properties.build_date), | ||||
|                 "depends": properties.depends, | ||||
|                 "description": properties.description or "", | ||||
|                 "filename": properties.filename, | ||||
|                 "groups": properties.groups, | ||||
|                 "installed_size": pretty_size(properties.installed_size), | ||||
|                 "licenses": properties.licenses, | ||||
|                 "name": package, | ||||
|                 "url": properties.url or "", | ||||
|                 "version": base.version | ||||
|             } for base in packages for package, properties in base.packages.items() | ||||
|         ] | ||||
|         comparator: Callable[[Dict[str, str]], str] = lambda item: item["filename"] | ||||
|  | ||||
|         html = template.render( | ||||
|             homepage=self.homepage, | ||||
|             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), | ||||
|             pgp_key=self.default_pgp_key, | ||||
|             repository=self.name) | ||||
|  | ||||
|         html = self.make_html(packages, True) | ||||
|         self.report_path.write_text(html) | ||||
|  | ||||
							
								
								
									
										117
									
								
								src/ahriman/core/report/jinja_template.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src/ahriman/core/report/jinja_template.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,117 @@ | ||||
| # | ||||
| # Copyright (c) 2021 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 jinja2 | ||||
|  | ||||
| from typing import Callable, Dict, Iterable | ||||
|  | ||||
| from ahriman.core.configuration import Configuration | ||||
| from ahriman.core.sign.gpg import GPG | ||||
| from ahriman.core.util import pretty_datetime, pretty_size | ||||
| from ahriman.models.package import Package | ||||
| from ahriman.models.sign_settings import SignSettings | ||||
|  | ||||
|  | ||||
| class JinjaTemplate: | ||||
|     """ | ||||
|     jinja based report generator | ||||
|  | ||||
|     It uses jinja2 templates for report generation, the following variables are allowed: | ||||
|  | ||||
|         homepage - link to homepage, string, optional | ||||
|         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 | ||||
|         packages - sorted list of packages properties, required | ||||
|                    * architecture, string | ||||
|                    * archive_size, pretty printed size, string | ||||
|                    * build_date, pretty printed datetime, string | ||||
|                    * depends, sorted list of strings | ||||
|                    * description, string | ||||
|                    * filename, string, | ||||
|                    * groups, sorted list of strings | ||||
|                    * installed_size, pretty printed datetime, string | ||||
|                    * licenses, sorted list of strings | ||||
|                    * name, string | ||||
|                    * url, string | ||||
|                    * version, string | ||||
|         pgp_key - default PGP key ID, string, optional | ||||
|         repository - repository name, string, required | ||||
|  | ||||
|     :ivar homepage: homepage link if any (for footer) | ||||
|     :ivar link_path: prefix fo packages to download | ||||
|     :ivar name: repository name | ||||
|     :ivar default_pgp_key: default PGP key | ||||
|     :ivar sign_targets: targets to sign enabled in configuration | ||||
|     :ivar template_path: path to directory with jinja templates | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, section: str, configuration: Configuration) -> None: | ||||
|         """ | ||||
|         default constructor | ||||
|         :param section: settings section name | ||||
|         :param configuration: configuration instance | ||||
|         """ | ||||
|         self.link_path = configuration.get(section, "link_path") | ||||
|         self.template_path = configuration.getpath(section, "template_path") | ||||
|  | ||||
|         # base template vars | ||||
|         self.homepage = configuration.get(section, "homepage", fallback=None) | ||||
|         self.name = configuration.get("repository", "name") | ||||
|  | ||||
|         self.sign_targets, self.default_pgp_key = GPG.sign_options(configuration) | ||||
|  | ||||
|     def make_html(self, packages: Iterable[Package], extended_report: bool) -> str: | ||||
|         """ | ||||
|         generate report for the specified packages | ||||
|         :param packages: list of packages to generate report | ||||
|         :param extended_report: include additional blocks to the report | ||||
|         """ | ||||
|         # idea comes from https://stackoverflow.com/a/38642558 | ||||
|         loader = jinja2.FileSystemLoader(searchpath=self.template_path.parent) | ||||
|         environment = jinja2.Environment(loader=loader) | ||||
|         template = environment.get_template(self.template_path.name) | ||||
|  | ||||
|         content = [ | ||||
|             { | ||||
|                 "architecture": properties.architecture or "", | ||||
|                 "archive_size": pretty_size(properties.archive_size), | ||||
|                 "build_date": pretty_datetime(properties.build_date), | ||||
|                 "depends": properties.depends, | ||||
|                 "description": properties.description or "", | ||||
|                 "filename": properties.filename, | ||||
|                 "groups": properties.groups, | ||||
|                 "installed_size": pretty_size(properties.installed_size), | ||||
|                 "licenses": properties.licenses, | ||||
|                 "name": package, | ||||
|                 "url": properties.url or "", | ||||
|                 "version": base.version | ||||
|             } for base in packages for package, properties in base.packages.items() | ||||
|         ] | ||||
|         comparator: Callable[[Dict[str, str]], str] = lambda item: item["filename"] | ||||
|  | ||||
|         return template.render( | ||||
|             extended_report=extended_report, | ||||
|             homepage=self.homepage, | ||||
|             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), | ||||
|             pgp_key=self.default_pgp_key, | ||||
|             repository=self.name) | ||||
| @ -60,21 +60,26 @@ class Report: | ||||
|         if provider == ReportSettings.HTML: | ||||
|             from ahriman.core.report.html import HTML | ||||
|             return HTML(architecture, configuration) | ||||
|         if provider == ReportSettings.Email: | ||||
|             from ahriman.core.report.email import Email | ||||
|             return Email(architecture, configuration) | ||||
|         return cls(architecture, configuration)  # should never happen | ||||
|  | ||||
|     def generate(self, packages: Iterable[Package]) -> None: | ||||
|     def generate(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None: | ||||
|         """ | ||||
|         generate report for the specified packages | ||||
|         :param packages: list of packages to generate report | ||||
|         :param built_packages: list of packages which has just been built | ||||
|         """ | ||||
|  | ||||
|     def run(self, packages: Iterable[Package]) -> None: | ||||
|     def run(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None: | ||||
|         """ | ||||
|         run report generation | ||||
|         :param packages: list of packages to generate report | ||||
|         :param built_packages: list of packages which has just been built | ||||
|         """ | ||||
|         try: | ||||
|             self.generate(packages) | ||||
|             self.generate(packages, built_packages) | ||||
|         except Exception: | ||||
|             self.logger.exception("report generation failed") | ||||
|             raise ReportFailed() | ||||
|  | ||||
| @ -100,27 +100,29 @@ class Executor(Cleaner): | ||||
|  | ||||
|         return self.repo.repo_path | ||||
|  | ||||
|     def process_report(self, targets: Optional[Iterable[str]]) -> None: | ||||
|     def process_report(self, targets: Optional[Iterable[str]], built_packages: Iterable[Package]) -> None: | ||||
|         """ | ||||
|         generate reports | ||||
|         :param targets: list of targets to generate reports. Configuration option will be used if it is not set | ||||
|         :param built_packages: list of packages which has just been built | ||||
|         """ | ||||
|         if targets is None: | ||||
|             targets = self.configuration.getlist("report", "target") | ||||
|         for target in targets: | ||||
|             runner = Report.load(self.architecture, self.configuration, target) | ||||
|             runner.run(self.packages()) | ||||
|             runner.run(self.packages(), built_packages) | ||||
|  | ||||
|     def process_sync(self, targets: Optional[Iterable[str]]) -> None: | ||||
|     def process_sync(self, targets: Optional[Iterable[str]], built_packages: Iterable[Package]) -> None: | ||||
|         """ | ||||
|         process synchronization to remote servers | ||||
|         :param targets: list of targets to sync. Configuration option will be used if it is not set | ||||
|         :param built_packages: list of packages which has just been built | ||||
|         """ | ||||
|         if targets is None: | ||||
|             targets = self.configuration.getlist("upload", "target") | ||||
|         for target in targets: | ||||
|             runner = Upload.load(self.architecture, self.configuration, target) | ||||
|             runner.run(self.paths.repository) | ||||
|             runner.run(self.paths.repository, built_packages) | ||||
|  | ||||
|     def process_update(self, packages: Iterable[Path]) -> Path: | ||||
|         """ | ||||
|  | ||||
| @ -37,9 +37,7 @@ class Repository(Executor, UpdateHandler): | ||||
|         :return: list of packages properties | ||||
|         """ | ||||
|         result: Dict[str, Package] = {} | ||||
|         for full_path in self.paths.repository.iterdir(): | ||||
|             if not package_like(full_path): | ||||
|                 continue | ||||
|         for full_path in filter(package_like, self.paths.repository.iterdir()): | ||||
|             try: | ||||
|                 local = Package.load(full_path, self.pacman, self.aur_url) | ||||
|                 result.setdefault(local.base, local).packages.update(local.packages) | ||||
|  | ||||
| @ -18,10 +18,12 @@ | ||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| from pathlib import Path | ||||
| from typing import Iterable | ||||
|  | ||||
| from ahriman.core.configuration import Configuration | ||||
| from ahriman.core.upload.upload import Upload | ||||
| from ahriman.core.util import check_output | ||||
| from ahriman.models.package import Package | ||||
|  | ||||
|  | ||||
| class Rsync(Upload): | ||||
| @ -43,9 +45,10 @@ class Rsync(Upload): | ||||
|         self.command = configuration.getlist("rsync", "command") | ||||
|         self.remote = configuration.get("rsync", "remote") | ||||
|  | ||||
|     def sync(self, path: Path) -> None: | ||||
|     def sync(self, path: Path, built_packages: Iterable[Package]) -> None: | ||||
|         """ | ||||
|         sync data to remote server | ||||
|         :param path: local path to sync | ||||
|         :param built_packages: list of packages which has just been built | ||||
|         """ | ||||
|         Rsync._check_output(*self.command, str(path), self.remote, exception=None, logger=self.logger) | ||||
|  | ||||
| @ -18,10 +18,12 @@ | ||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| from pathlib import Path | ||||
| from typing import Iterable | ||||
|  | ||||
| from ahriman.core.configuration import Configuration | ||||
| from ahriman.core.upload.upload import Upload | ||||
| from ahriman.core.util import check_output | ||||
| from ahriman.models.package import Package | ||||
|  | ||||
|  | ||||
| class S3(Upload): | ||||
| @ -43,10 +45,11 @@ class S3(Upload): | ||||
|         self.bucket = configuration.get("s3", "bucket") | ||||
|         self.command = configuration.getlist("s3", "command") | ||||
|  | ||||
|     def sync(self, path: Path) -> None: | ||||
|     def sync(self, path: Path, built_packages: Iterable[Package]) -> None: | ||||
|         """ | ||||
|         sync data to remote server | ||||
|         :param path: local path to sync | ||||
|         :param built_packages: list of packages which has just been built | ||||
|         """ | ||||
|         # TODO rewrite to boto, but it is bullshit | ||||
|         S3._check_output(*self.command, str(path), self.bucket, exception=None, logger=self.logger) | ||||
|  | ||||
| @ -22,10 +22,11 @@ from __future__ import annotations | ||||
| import logging | ||||
|  | ||||
| from pathlib import Path | ||||
| from typing import Type | ||||
| from typing import Iterable, Type | ||||
|  | ||||
| from ahriman.core.configuration import Configuration | ||||
| from ahriman.core.exceptions import SyncFailed | ||||
| from ahriman.models.package import Package | ||||
| from ahriman.models.upload_settings import UploadSettings | ||||
|  | ||||
|  | ||||
| @ -65,19 +66,21 @@ class Upload: | ||||
|             return S3(architecture, configuration) | ||||
|         return cls(architecture, configuration)  # should never happen | ||||
|  | ||||
|     def run(self, path: Path) -> None: | ||||
|     def run(self, path: Path, built_packages: Iterable[Package]) -> None: | ||||
|         """ | ||||
|         run remote sync | ||||
|         :param path: local path to sync | ||||
|         :param built_packages: list of packages which has just been built | ||||
|         """ | ||||
|         try: | ||||
|             self.sync(path) | ||||
|             self.sync(path, built_packages) | ||||
|         except Exception: | ||||
|             self.logger.exception("remote sync failed") | ||||
|             raise SyncFailed() | ||||
|  | ||||
|     def sync(self, path: Path) -> None: | ||||
|     def sync(self, path: Path, built_packages: Iterable[Package]) -> None: | ||||
|         """ | ||||
|         sync data to remote server | ||||
|         :param path: local path to sync | ||||
|         :param built_packages: list of packages which has just been built | ||||
|         """ | ||||
|  | ||||
| @ -22,7 +22,7 @@ import subprocess | ||||
|  | ||||
| from logging import Logger | ||||
| from pathlib import Path | ||||
| from typing import Optional | ||||
| from typing import Optional, Union | ||||
|  | ||||
| from ahriman.core.exceptions import InvalidOption | ||||
|  | ||||
| @ -60,7 +60,7 @@ def package_like(filename: Path) -> bool: | ||||
|     return ".pkg." in name and not name.endswith(".sig") | ||||
|  | ||||
|  | ||||
| def pretty_datetime(timestamp: Optional[int]) -> str: | ||||
| def pretty_datetime(timestamp: Optional[Union[float, int]]) -> str: | ||||
|     """ | ||||
|     convert datetime object to string | ||||
|     :param timestamp: datetime to convert | ||||
|  | ||||
| @ -28,11 +28,14 @@ from ahriman.core.exceptions import InvalidOption | ||||
| class ReportSettings(Enum): | ||||
|     """ | ||||
|     report targets enumeration | ||||
|     :cvar Disabled: option which generates no report for testing purpose | ||||
|     :cvar HTML: html report generation | ||||
|     :cvar Email: email report generation | ||||
|     """ | ||||
|  | ||||
|     Disabled = auto()  # for testing purpose | ||||
|     HTML = auto() | ||||
|     Email = auto() | ||||
|  | ||||
|     @classmethod | ||||
|     def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings: | ||||
| @ -43,4 +46,6 @@ class ReportSettings(Enum): | ||||
|         """ | ||||
|         if value.lower() in ("html",): | ||||
|             return cls.HTML | ||||
|         if value.lower() in ("email",): | ||||
|             return cls.Email | ||||
|         raise InvalidOption(value) | ||||
|  | ||||
							
								
								
									
										49
									
								
								src/ahriman/models/smtp_ssl_settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/ahriman/models/smtp_ssl_settings.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | ||||
| # | ||||
| # Copyright (c) 2021 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 | ||||
|  | ||||
| from enum import Enum, auto | ||||
| from typing import Type | ||||
|  | ||||
|  | ||||
| class SmtpSSLSettings(Enum): | ||||
|     """ | ||||
|     SMTP SSL mode enumeration | ||||
|     :cvar Disabled: no SSL enabled | ||||
|     :cvar SSL: use SMTP_SSL instead of normal SMTP client | ||||
|     :cvar STARTTLS: use STARTTLS in normal SMTP client | ||||
|     """ | ||||
|  | ||||
|     Disabled = auto() | ||||
|     SSL = auto() | ||||
|     STARTTLS = auto() | ||||
|  | ||||
|     @classmethod | ||||
|     def from_option(cls: Type[SmtpSSLSettings], value: str) -> SmtpSSLSettings: | ||||
|         """ | ||||
|         construct value from configuration | ||||
|         :param value: configuration value | ||||
|         :return: parsed value | ||||
|         """ | ||||
|         if value.lower() in ("ssl", "ssl/tls"): | ||||
|             return cls.SSL | ||||
|         if value.lower() in ("starttls",): | ||||
|             return cls.STARTTLS | ||||
|         return cls.Disabled | ||||
| @ -28,6 +28,7 @@ from ahriman.core.exceptions import InvalidOption | ||||
| class UploadSettings(Enum): | ||||
|     """ | ||||
|     remote synchronization targets enumeration | ||||
|     :cvar Disabled: no sync will be performed, required for testing purpose | ||||
|     :cvar Rsync: sync via rsync | ||||
|     :cvar S3: sync to Amazon S3 | ||||
|     """ | ||||
|  | ||||
| @ -15,7 +15,7 @@ def test_finalize(application: Application, mocker: MockerFixture) -> None: | ||||
|     report_mock = mocker.patch("ahriman.application.application.Application.report") | ||||
|     sync_mock = mocker.patch("ahriman.application.application.Application.sync") | ||||
|  | ||||
|     application._finalize() | ||||
|     application._finalize([]) | ||||
|     report_mock.assert_called_once() | ||||
|     sync_mock.assert_called_once() | ||||
|  | ||||
| @ -218,7 +218,7 @@ def test_report(application: Application, mocker: MockerFixture) -> None: | ||||
|     must generate report | ||||
|     """ | ||||
|     executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_report") | ||||
|     application.report([]) | ||||
|     application.report([], []) | ||||
|     executor_mock.assert_called_once() | ||||
|  | ||||
|  | ||||
| @ -279,7 +279,7 @@ def test_sync(application: Application, mocker: MockerFixture) -> None: | ||||
|     must sync to remote | ||||
|     """ | ||||
|     executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_sync") | ||||
|     application.sync([]) | ||||
|     application.sync([], []) | ||||
|     executor_mock.assert_called_once() | ||||
|  | ||||
|  | ||||
| @ -292,6 +292,7 @@ def test_update(application: Application, package_ahriman: Package, mocker: Mock | ||||
|  | ||||
|     mocker.patch("ahriman.core.tree.Tree.load", return_value=tree) | ||||
|     mocker.patch("ahriman.core.repository.repository.Repository.packages_built", return_value=[]) | ||||
|     mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) | ||||
|     build_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_build", return_value=paths) | ||||
|     update_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_update") | ||||
|     finalize_mock = mocker.patch("ahriman.application.application.Application._finalize") | ||||
| @ -299,4 +300,4 @@ def test_update(application: Application, package_ahriman: Package, mocker: Mock | ||||
|     application.update([package_ahriman]) | ||||
|     build_mock.assert_called_once() | ||||
|     update_mock.assert_has_calls([mock.call([]), mock.call(paths)]) | ||||
|     finalize_mock.assert_has_calls([mock.call(), mock.call()]) | ||||
|     finalize_mock.assert_has_calls([mock.call([]), mock.call([package_ahriman])]) | ||||
|  | ||||
							
								
								
									
										105
									
								
								tests/ahriman/core/report/test_email.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								tests/ahriman/core/report/test_email.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,105 @@ | ||||
| from pytest_mock import MockerFixture | ||||
|  | ||||
| from ahriman.core.configuration import Configuration | ||||
| from ahriman.core.report.email import Email | ||||
| from ahriman.models.package import Package | ||||
|  | ||||
|  | ||||
| def test_send(configuration: Configuration, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must send an email with attachment | ||||
|     """ | ||||
|     smtp_mock = mocker.patch("smtplib.SMTP") | ||||
|  | ||||
|     report = Email("x86_64", configuration) | ||||
|     report._send("a text", {"attachment.html": "an attachment"}) | ||||
|     smtp_mock.return_value.starttls.assert_not_called() | ||||
|     smtp_mock.return_value.login.assert_not_called() | ||||
|     smtp_mock.return_value.sendmail.assert_called_once() | ||||
|     smtp_mock.return_value.quit.assert_called_once() | ||||
|  | ||||
|  | ||||
| def test_send_auth(configuration: Configuration, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must send an email with attachment with auth | ||||
|     """ | ||||
|     configuration.set("email", "user", "username") | ||||
|     configuration.set("email", "password", "password") | ||||
|     smtp_mock = mocker.patch("smtplib.SMTP") | ||||
|  | ||||
|     report = Email("x86_64", configuration) | ||||
|     report._send("a text", {"attachment.html": "an attachment"}) | ||||
|     smtp_mock.return_value.login.assert_called_once() | ||||
|  | ||||
|  | ||||
| def test_send_auth_no_password(configuration: Configuration, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must send an email with attachment without auth if no password supplied | ||||
|     """ | ||||
|     configuration.set("email", "user", "username") | ||||
|     smtp_mock = mocker.patch("smtplib.SMTP") | ||||
|  | ||||
|     report = Email("x86_64", configuration) | ||||
|     report._send("a text", {"attachment.html": "an attachment"}) | ||||
|     smtp_mock.return_value.login.assert_not_called() | ||||
|  | ||||
|  | ||||
| def test_send_auth_no_user(configuration: Configuration, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must send an email with attachment without auth if no user supplied | ||||
|     """ | ||||
|     configuration.set("email", "password", "password") | ||||
|     smtp_mock = mocker.patch("smtplib.SMTP") | ||||
|  | ||||
|     report = Email("x86_64", configuration) | ||||
|     report._send("a text", {"attachment.html": "an attachment"}) | ||||
|     smtp_mock.return_value.login.assert_not_called() | ||||
|  | ||||
|  | ||||
| def test_send_ssl_tls(configuration: Configuration, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must send an email with attachment with ssl/tls | ||||
|     """ | ||||
|     configuration.set("email", "ssl", "ssl") | ||||
|     smtp_mock = mocker.patch("smtplib.SMTP_SSL") | ||||
|  | ||||
|     report = Email("x86_64", configuration) | ||||
|     report._send("a text", {"attachment.html": "an attachment"}) | ||||
|     smtp_mock.return_value.starttls.assert_not_called() | ||||
|     smtp_mock.return_value.login.assert_not_called() | ||||
|     smtp_mock.return_value.sendmail.assert_called_once() | ||||
|     smtp_mock.return_value.quit.assert_called_once() | ||||
|  | ||||
|  | ||||
| def test_send_starttls(configuration: Configuration, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must send an email with attachment with starttls | ||||
|     """ | ||||
|     configuration.set("email", "ssl", "starttls") | ||||
|     smtp_mock = mocker.patch("smtplib.SMTP") | ||||
|  | ||||
|     report = Email("x86_64", configuration) | ||||
|     report._send("a text", {"attachment.html": "an attachment"}) | ||||
|     smtp_mock.return_value.starttls.assert_called_once() | ||||
|  | ||||
|  | ||||
| def test_generate(configuration: Configuration, package_ahriman: Package, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must generate report | ||||
|     """ | ||||
|     send_mock = mocker.patch("ahriman.core.report.email.Email._send") | ||||
|  | ||||
|     report = Email("x86_64", configuration) | ||||
|     report.generate([package_ahriman], []) | ||||
|     send_mock.assert_called_once() | ||||
|  | ||||
|  | ||||
| def test_generate_with_built(configuration: Configuration, package_ahriman: Package, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must generate report with built packages | ||||
|     """ | ||||
|     send_mock = mocker.patch("ahriman.core.report.email.Email._send") | ||||
|  | ||||
|     report = Email("x86_64", configuration) | ||||
|     report.generate([package_ahriman], [package_ahriman]) | ||||
|     send_mock.assert_called_once() | ||||
| @ -12,5 +12,5 @@ def test_generate(configuration: Configuration, package_ahriman: Package, mocker | ||||
|     write_mock = mocker.patch("pathlib.Path.write_text") | ||||
|  | ||||
|     report = HTML("x86_64", configuration) | ||||
|     report.generate([package_ahriman]) | ||||
|     report.generate([package_ahriman], []) | ||||
|     write_mock.assert_called_once() | ||||
|  | ||||
							
								
								
									
										19
									
								
								tests/ahriman/core/report/test_jinja_tempalte.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								tests/ahriman/core/report/test_jinja_tempalte.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| from ahriman.core.configuration import Configuration | ||||
| from ahriman.core.report.jinja_template import JinjaTemplate | ||||
| from ahriman.models.package import Package | ||||
|  | ||||
|  | ||||
| def test_generate(configuration: Configuration, package_ahriman: Package) -> None: | ||||
|     """ | ||||
|     must generate html report | ||||
|     """ | ||||
|     report = JinjaTemplate("html", configuration) | ||||
|     assert report.make_html([package_ahriman], extended_report=False) | ||||
|  | ||||
|  | ||||
| def test_generate_extended(configuration: Configuration, package_ahriman: Package) -> None: | ||||
|     """ | ||||
|     must generate extended html report | ||||
|     """ | ||||
|     report = JinjaTemplate("html", configuration) | ||||
|     assert report.make_html([package_ahriman], extended_report=True) | ||||
| @ -15,7 +15,7 @@ def test_report_failure(configuration: Configuration, mocker: MockerFixture) -> | ||||
|     """ | ||||
|     mocker.patch("ahriman.core.report.html.HTML.generate", side_effect=Exception()) | ||||
|     with pytest.raises(ReportFailed): | ||||
|         Report.load("x86_64", configuration, ReportSettings.HTML.name).run(Path("path")) | ||||
|         Report.load("x86_64", configuration, ReportSettings.HTML.name).run(Path("path"), []) | ||||
|  | ||||
|  | ||||
| def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> None: | ||||
| @ -24,7 +24,16 @@ def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> No | ||||
|     """ | ||||
|     mocker.patch("ahriman.models.report_settings.ReportSettings.from_option", return_value=ReportSettings.Disabled) | ||||
|     report_mock = mocker.patch("ahriman.core.report.report.Report.generate") | ||||
|     Report.load("x86_64", configuration, ReportSettings.Disabled.name).run(Path("path")) | ||||
|     Report.load("x86_64", configuration, ReportSettings.Disabled.name).run(Path("path"), []) | ||||
|     report_mock.assert_called_once() | ||||
|  | ||||
|  | ||||
| def test_report_email(configuration: Configuration, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must generate email report | ||||
|     """ | ||||
|     report_mock = mocker.patch("ahriman.core.report.email.Email.generate") | ||||
|     Report.load("x86_64", configuration, ReportSettings.Email.name).run(Path("path"), []) | ||||
|     report_mock.assert_called_once() | ||||
|  | ||||
|  | ||||
| @ -33,5 +42,5 @@ def test_report_html(configuration: Configuration, mocker: MockerFixture) -> Non | ||||
|     must generate html report | ||||
|     """ | ||||
|     report_mock = mocker.patch("ahriman.core.report.html.HTML.generate") | ||||
|     Report.load("x86_64", configuration, ReportSettings.HTML.name).run(Path("path")) | ||||
|     Report.load("x86_64", configuration, ReportSettings.HTML.name).run(Path("path"), []) | ||||
|     report_mock.assert_called_once() | ||||
|  | ||||
| @ -133,7 +133,7 @@ def test_process_report(executor: Executor, package_ahriman: Package, mocker: Mo | ||||
|     mocker.patch("ahriman.core.report.report.Report.load", return_value=Report("x86_64", executor.configuration)) | ||||
|     report_mock = mocker.patch("ahriman.core.report.report.Report.run") | ||||
|  | ||||
|     executor.process_report(["dummy"]) | ||||
|     executor.process_report(["dummy"], []) | ||||
|     report_mock.assert_called_once() | ||||
|  | ||||
|  | ||||
| @ -143,7 +143,7 @@ def test_process_report_auto(executor: Executor, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     configuration_getlist_mock = mocker.patch("ahriman.core.configuration.Configuration.getlist") | ||||
|  | ||||
|     executor.process_report(None) | ||||
|     executor.process_report(None, []) | ||||
|     configuration_getlist_mock.assert_called_once() | ||||
|  | ||||
|  | ||||
| @ -154,7 +154,7 @@ def test_process_upload(executor: Executor, mocker: MockerFixture) -> None: | ||||
|     mocker.patch("ahriman.core.upload.upload.Upload.load", return_value=Upload("x86_64", executor.configuration)) | ||||
|     upload_mock = mocker.patch("ahriman.core.upload.upload.Upload.run") | ||||
|  | ||||
|     executor.process_sync(["dummy"]) | ||||
|     executor.process_sync(["dummy"], []) | ||||
|     upload_mock.assert_called_once() | ||||
|  | ||||
|  | ||||
| @ -164,7 +164,7 @@ def test_process_upload_auto(executor: Executor, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     configuration_getlist_mock = mocker.patch("ahriman.core.configuration.Configuration.getlist") | ||||
|  | ||||
|     executor.process_sync(None) | ||||
|     executor.process_sync(None, []) | ||||
|     configuration_getlist_mock.assert_called_once() | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -12,5 +12,5 @@ def test_sync(configuration: Configuration, mocker: MockerFixture) -> None: | ||||
|     check_output_mock = mocker.patch("ahriman.core.upload.rsync.Rsync._check_output") | ||||
|  | ||||
|     upload = Rsync("x86_64", configuration) | ||||
|     upload.sync(Path("path")) | ||||
|     upload.sync(Path("path"), []) | ||||
|     check_output_mock.assert_called_once() | ||||
|  | ||||
| @ -12,5 +12,5 @@ def test_sync(configuration: Configuration, mocker: MockerFixture) -> None: | ||||
|     check_output_mock = mocker.patch("ahriman.core.upload.s3.S3._check_output") | ||||
|  | ||||
|     upload = S3("x86_64", configuration) | ||||
|     upload.sync(Path("path")) | ||||
|     upload.sync(Path("path"), []) | ||||
|     check_output_mock.assert_called_once() | ||||
|  | ||||
| @ -15,7 +15,7 @@ def test_upload_failure(configuration: Configuration, mocker: MockerFixture) -> | ||||
|     """ | ||||
|     mocker.patch("ahriman.core.upload.rsync.Rsync.sync", side_effect=Exception()) | ||||
|     with pytest.raises(SyncFailed): | ||||
|         Upload.load("x86_64", configuration, UploadSettings.Rsync.name).run(Path("path")) | ||||
|         Upload.load("x86_64", configuration, UploadSettings.Rsync.name).run(Path("path"), []) | ||||
|  | ||||
|  | ||||
| def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> None: | ||||
| @ -24,7 +24,7 @@ def test_report_dummy(configuration: Configuration, mocker: MockerFixture) -> No | ||||
|     """ | ||||
|     mocker.patch("ahriman.models.upload_settings.UploadSettings.from_option", return_value=UploadSettings.Disabled) | ||||
|     upload_mock = mocker.patch("ahriman.core.upload.upload.Upload.sync") | ||||
|     Upload.load("x86_64", configuration, UploadSettings.Disabled.name).run(Path("path")) | ||||
|     Upload.load("x86_64", configuration, UploadSettings.Disabled.name).run(Path("path"), []) | ||||
|     upload_mock.assert_called_once() | ||||
|  | ||||
|  | ||||
| @ -33,7 +33,7 @@ def test_upload_rsync(configuration: Configuration, mocker: MockerFixture) -> No | ||||
|     must upload via rsync | ||||
|     """ | ||||
|     upload_mock = mocker.patch("ahriman.core.upload.rsync.Rsync.sync") | ||||
|     Upload.load("x86_64", configuration, UploadSettings.Rsync.name).run(Path("path")) | ||||
|     Upload.load("x86_64", configuration, UploadSettings.Rsync.name).run(Path("path"), []) | ||||
|     upload_mock.assert_called_once() | ||||
|  | ||||
|  | ||||
| @ -42,5 +42,5 @@ def test_upload_s3(configuration: Configuration, mocker: MockerFixture) -> None: | ||||
|     must upload via s3 | ||||
|     """ | ||||
|     upload_mock = mocker.patch("ahriman.core.upload.s3.S3.sync") | ||||
|     Upload.load("x86_64", configuration, UploadSettings.S3.name).run(Path("path")) | ||||
|     Upload.load("x86_64", configuration, UploadSettings.S3.name).run(Path("path"), []) | ||||
|     upload_mock.assert_called_once() | ||||
|  | ||||
| @ -18,3 +18,6 @@ def test_from_option_valid() -> None: | ||||
|     """ | ||||
|     assert ReportSettings.from_option("html") == ReportSettings.HTML | ||||
|     assert ReportSettings.from_option("HTML") == ReportSettings.HTML | ||||
|  | ||||
|     assert ReportSettings.from_option("email") == ReportSettings.Email | ||||
|     assert ReportSettings.from_option("EmAil") == ReportSettings.Email | ||||
|  | ||||
							
								
								
									
										21
									
								
								tests/ahriman/models/test_smtp_settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								tests/ahriman/models/test_smtp_settings.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| from ahriman.models.smtp_ssl_settings import SmtpSSLSettings | ||||
|  | ||||
|  | ||||
| def test_from_option_invalid() -> None: | ||||
|     """ | ||||
|     must return disabled value on invalid option | ||||
|     """ | ||||
|     assert SmtpSSLSettings.from_option("invalid") == SmtpSSLSettings.Disabled | ||||
|  | ||||
|  | ||||
| def test_from_option_valid() -> None: | ||||
|     """ | ||||
|     must return value from valid options | ||||
|     """ | ||||
|     assert SmtpSSLSettings.from_option("ssl") == SmtpSSLSettings.SSL | ||||
|     assert SmtpSSLSettings.from_option("SSL") == SmtpSSLSettings.SSL | ||||
|     assert SmtpSSLSettings.from_option("ssl/tls") == SmtpSSLSettings.SSL | ||||
|     assert SmtpSSLSettings.from_option("SSL/TLS") == SmtpSSLSettings.SSL | ||||
|  | ||||
|     assert SmtpSSLSettings.from_option("starttls") == SmtpSSLSettings.STARTTLS | ||||
|     assert SmtpSSLSettings.from_option("STARTTLS") == SmtpSSLSettings.STARTTLS | ||||
| @ -25,6 +25,14 @@ target = | ||||
| [report] | ||||
| target = | ||||
|  | ||||
| [email] | ||||
| host = 0.0.0.0 | ||||
| link_path = | ||||
| port = 587 | ||||
| receivers = mail@example.com | ||||
| sender = mail@example.com | ||||
| template_path = ../web/templates/repo-index.jinja2 | ||||
|  | ||||
| [html] | ||||
| path = | ||||
| homepage = | ||||
|  | ||||
		Reference in New Issue
	
	Block a user