mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-02-24 21:59:48 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c4fefa6071 | |||
| b0f1828ae7 | |||
| dec025b45a | |||
| 89008e5350 | |||
| 422196d413 | |||
| 6fe2eade26 | |||
| 5266f54257 | |||
| bbf9e38fda | |||
| ba80a91d95 | |||
| 536d040a6a | |||
| bed8752f3a | |||
| 4093ca8986 | |||
| f027155885 | |||
| 443d4ae667 | |||
| c8f7fa8c51 |
2987
docs/_static/architecture.dot
vendored
2987
docs/_static/architecture.dot
vendored
File diff suppressed because it is too large
Load Diff
@@ -97,6 +97,15 @@ libalpm and AUR related configuration. Group name can refer to architecture, e.g
|
||||
* ``sync_files_database`` - download files database from mirror, boolean, required.
|
||||
* ``use_ahriman_cache`` - use local pacman package cache instead of system one, boolean, required. With this option enabled you might want to refresh database periodically (available as additional flag for some subcommands). If set to ``no``, databases must be synchronized manually.
|
||||
|
||||
``aur`` group
|
||||
-------------
|
||||
|
||||
Archlinux User Repository related configuration.
|
||||
|
||||
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
|
||||
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
|
||||
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
|
||||
|
||||
``auth`` group
|
||||
--------------
|
||||
|
||||
@@ -158,7 +167,9 @@ Reporting to web service related settings. In most cases there is fallback to we
|
||||
|
||||
* ``enabled`` - enable reporting to web service, boolean, optional, default ``yes`` for backward compatibility.
|
||||
* ``address`` - remote web service address with protocol, string, optional. In case of websocket, the ``http+unix`` scheme and URL encoded address (e.g. ``%2Fvar%2Flib%2Fahriman`` for ``/var/lib/ahriman``) must be used, e.g. ``http+unix://%2Fvar%2Flib%2Fahriman%2Fsocket``. In case if none set, it will be guessed from ``web`` section.
|
||||
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
|
||||
* ``password`` - password to authorize in web service in order to update service status, string, required in case if authorization enabled.
|
||||
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
|
||||
* ``suppress_http_log_errors`` - suppress HTTP log errors, boolean, optional, default ``no``. If set to ``yes``, any HTTP log errors (e.g. if web server is not available, but HTTP logging is enabled) will be suppressed.
|
||||
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
|
||||
* ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled.
|
||||
@@ -367,6 +378,8 @@ 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.
|
||||
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
|
||||
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
|
||||
* ``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``.
|
||||
@@ -392,6 +405,7 @@ Type will be read from several sources:
|
||||
This feature requires GitHub key creation (see below). Section name must be either ``github`` (plus optional architecture name, e.g. ``github:x86_64``) or random name with ``type`` set.
|
||||
|
||||
* ``type`` - type of the upload, string, optional, must be set to ``github`` if exists.
|
||||
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
|
||||
* ``owner`` - GitHub repository owner, string, required.
|
||||
* ``password`` - created GitHub API key. In order to create it do the following:
|
||||
|
||||
@@ -401,6 +415,7 @@ This feature requires GitHub key creation (see below). Section name must be eith
|
||||
#. Generate new token. Required scope is ``public_repo`` (or ``repo`` for private repository support).
|
||||
|
||||
* ``repository`` - GitHub repository name, string, required. Repository must be created before any action and must have active branch (e.g. with readme).
|
||||
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
|
||||
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
|
||||
* ``use_full_release_name`` - if set to ``yes``, the release will contain both repository name and architecture, and only architecture otherwise, boolean, optional, default ``no`` (legacy behavior).
|
||||
* ``username`` - GitHub authorization user, string, required. Basically the same as ``owner``.
|
||||
@@ -411,6 +426,8 @@ This feature requires GitHub key creation (see below). Section name must be eith
|
||||
Section name must be either ``remote-service`` (plus optional architecture name, e.g. ``remote-service:x86_64``) or random name with ``type`` set.
|
||||
|
||||
* ``type`` - type of the report, string, optional, must be set to ``remote-service`` if exists.
|
||||
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
|
||||
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
|
||||
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
|
||||
|
||||
``rsync`` type
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
pkgbase='ahriman'
|
||||
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
||||
pkgver=2.20.0rc1
|
||||
pkgver=2.20.0rc2
|
||||
pkgrel=1
|
||||
pkgdesc="ArcH linux ReposItory MANager"
|
||||
arch=('any')
|
||||
|
||||
@@ -23,6 +23,14 @@ sync_files_database = yes
|
||||
; as additional option for some subcommands). If set to no, databases must be synchronized manually.
|
||||
use_ahriman_cache = yes
|
||||
|
||||
[aur]
|
||||
; Maximum amount of retries of HTTP requests.
|
||||
max_retries = 3
|
||||
; Retry exponential backoff.
|
||||
retry_backoff = 1.0
|
||||
; HTTP request timeout in seconds.
|
||||
;timeout = 30
|
||||
|
||||
[build]
|
||||
; List of additional flags passed to archbuild command.
|
||||
;archbuild_flags =
|
||||
@@ -73,8 +81,12 @@ enabled = yes
|
||||
; In case if unix sockets are used, it might point to the valid socket with encoded path, e.g.:
|
||||
; address = http+unix://%2Fvar%2Flib%2Fahriman%2Fsocket
|
||||
;address = http://${web:host}:${web:port}
|
||||
; Maximum amount of retries of HTTP requests.
|
||||
;max_retries = 0
|
||||
; Optional password for authentication (if enabled).
|
||||
;password =
|
||||
; Retry exponential backoff.
|
||||
;retry_backoff = 0.0
|
||||
; Do not log HTTP errors if occurs.
|
||||
suppress_http_log_errors = yes
|
||||
; HTTP request timeout in seconds.
|
||||
@@ -216,6 +228,10 @@ templates[] = ${prefix}/share/ahriman/templates
|
||||
;homepage=
|
||||
; Prefix for packages links. Link to a package will be formed as link_path / filename.
|
||||
;link_path =
|
||||
; Maximum amount of retries of HTTP requests.
|
||||
;max_retries = 0
|
||||
; Retry exponential backoff.
|
||||
;retry_backoff = 0.0
|
||||
; Optional link to the RSS feed.
|
||||
;rss_url =
|
||||
; Template name to be used.
|
||||
@@ -236,12 +252,16 @@ target =
|
||||
[github]
|
||||
; Trigger type name.
|
||||
;type = github
|
||||
; Maximum amount of retries of HTTP requests.
|
||||
;max_retries = 0
|
||||
; GitHub repository owner username.
|
||||
;owner =
|
||||
; GitHub API key. public_repo (repo) scope is required.
|
||||
;password =
|
||||
; GitHub repository name.
|
||||
;repository =
|
||||
; Retry exponential backoff.
|
||||
;retry_backoff = 0.0
|
||||
; HTTP request timeout in seconds.
|
||||
;timeout = 30
|
||||
; Include repository name to release name (recommended).
|
||||
@@ -253,6 +273,10 @@ target =
|
||||
[remote-service]
|
||||
; Trigger type name.
|
||||
;type = remote-service
|
||||
; Maximum amount of retries of HTTP requests.
|
||||
;max_retries = 0
|
||||
; Retry exponential backoff.
|
||||
;retry_backoff = 0.0
|
||||
; HTTP request timeout in seconds.
|
||||
;timeout = 30
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
};
|
||||
});
|
||||
|
||||
updateTable(table, payload);
|
||||
updateTable(table, payload, row => row.timestamp);
|
||||
table.bootstrapTable("hideLoading");
|
||||
},
|
||||
onFailure,
|
||||
|
||||
@@ -195,16 +195,19 @@
|
||||
return intervalId;
|
||||
}
|
||||
|
||||
function updateTable(table, rows) {
|
||||
function updateTable(table, rows, rowChangedKey) {
|
||||
// instead of using load method here, we just update rows manually to avoid table reinitialization
|
||||
const currentData = table.bootstrapTable("getData").reduce((accumulator, row) => {
|
||||
accumulator[row.id] = row["0"];
|
||||
accumulator[row.id] = {state: row["0"], key: rowChangedKey(row)};
|
||||
return accumulator;
|
||||
}, {});
|
||||
// insert or update rows
|
||||
// insert or update rows, skipping ones whose status hasn't changed
|
||||
rows.forEach(row => {
|
||||
if (Object.hasOwn(currentData, row.id)) {
|
||||
row["0"] = currentData[row.id]; // copy checkbox state
|
||||
if (rowChangedKey(row) === currentData[row.id].key) {
|
||||
return;
|
||||
}
|
||||
row["0"] = currentData[row.id].state; // copy checkbox state
|
||||
table.bootstrapTable("updateByUniqueId", {
|
||||
id: row.id,
|
||||
row: row,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.TH AHRIMAN "1" "2026\-02\-18" "ahriman 2.20.0rc1" "ArcH linux ReposItory MANager"
|
||||
.TH AHRIMAN "1" "2026\-02\-20" "ahriman 2.20.0rc2" "ArcH linux ReposItory MANager"
|
||||
.SH NAME
|
||||
ahriman \- ArcH linux ReposItory MANager
|
||||
.SH SYNOPSIS
|
||||
|
||||
@@ -245,7 +245,7 @@ _shtab_ahriman_init_options=(
|
||||
{--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:"
|
||||
"--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:"
|
||||
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
|
||||
"--packager[packager name and email (default\: None)]:packager:"
|
||||
"--packager[packager name and email]:packager:"
|
||||
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
|
||||
"--sign-key[sign key id (default\: None)]:sign_key:"
|
||||
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
|
||||
@@ -526,7 +526,7 @@ _shtab_ahriman_repo_init_options=(
|
||||
{--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:"
|
||||
"--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:"
|
||||
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
|
||||
"--packager[packager name and email (default\: None)]:packager:"
|
||||
"--packager[packager name and email]:packager:"
|
||||
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
|
||||
"--sign-key[sign key id (default\: None)]:sign_key:"
|
||||
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
|
||||
@@ -583,7 +583,7 @@ _shtab_ahriman_repo_setup_options=(
|
||||
{--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:"
|
||||
"--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:"
|
||||
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
|
||||
"--packager[packager name and email (default\: None)]:packager:"
|
||||
"--packager[packager name and email]:packager:"
|
||||
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
|
||||
"--sign-key[sign key id (default\: None)]:sign_key:"
|
||||
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
|
||||
@@ -757,7 +757,7 @@ _shtab_ahriman_service_setup_options=(
|
||||
{--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:"
|
||||
"--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:"
|
||||
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
|
||||
"--packager[packager name and email (default\: None)]:packager:"
|
||||
"--packager[packager name and email]:packager:"
|
||||
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
|
||||
"--sign-key[sign key id (default\: None)]:sign_key:"
|
||||
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
|
||||
@@ -792,7 +792,7 @@ _shtab_ahriman_setup_options=(
|
||||
{--makeflags-jobs,--no-makeflags-jobs}"[append MAKEFLAGS variable with parallelism set to number of cores (default\: True)]:makeflags_jobs:"
|
||||
"--mirror[use the specified explicitly mirror instead of including mirrorlist (default\: None)]:mirror:"
|
||||
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
|
||||
"--packager[packager name and email (default\: None)]:packager:"
|
||||
"--packager[packager name and email]:packager:"
|
||||
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
|
||||
"--sign-key[sign key id (default\: None)]:sign_key:"
|
||||
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "2.20.0rc1"
|
||||
__version__ = "2.20.0rc2"
|
||||
|
||||
@@ -81,11 +81,13 @@ class Backup(Handler):
|
||||
Returns:
|
||||
set[Path]: map of the filesystem paths
|
||||
"""
|
||||
paths = set(configuration.include.glob("*.ini"))
|
||||
|
||||
# configuration files
|
||||
root, _ = configuration.check_loaded()
|
||||
paths.add(root) # the configuration itself
|
||||
paths.add(SQLite.database_path(configuration)) # database
|
||||
paths = set(configuration.includes)
|
||||
paths.add(root)
|
||||
|
||||
# database
|
||||
paths.add(SQLite.database_path(configuration))
|
||||
|
||||
# local caches
|
||||
repository_paths = configuration.repository_paths
|
||||
|
||||
@@ -47,7 +47,7 @@ class Restore(Handler):
|
||||
report(bool): force enable or disable reporting
|
||||
"""
|
||||
with tarfile.open(args.path) as archive:
|
||||
archive.extractall(path=args.output) # nosec
|
||||
archive.extractall(path=args.output, filter="data")
|
||||
|
||||
@staticmethod
|
||||
def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
|
||||
@@ -86,7 +86,7 @@ class OAuth(Mapping):
|
||||
Raises:
|
||||
OptionError: in case if invalid OAuth provider name supplied
|
||||
"""
|
||||
provider: type[aioauth_client.OAuth2Client] = getattr(aioauth_client, name)
|
||||
provider: type = getattr(aioauth_client, name, type(None))
|
||||
try:
|
||||
is_oauth2_client = issubclass(provider, aioauth_client.OAuth2Client)
|
||||
except TypeError: # what if it is random string?
|
||||
|
||||
@@ -97,6 +97,26 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"aur": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"max_retries": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
"retry_backoff": {
|
||||
"type": "float",
|
||||
"coerce": "float",
|
||||
"min": 0,
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
"auth": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
@@ -296,10 +316,20 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
"empty": False,
|
||||
"is_url": [],
|
||||
},
|
||||
"max_retries": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
"retry_backoff": {
|
||||
"type": "float",
|
||||
"coerce": "float",
|
||||
"min": 0,
|
||||
},
|
||||
"suppress_http_log_errors": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
|
||||
@@ -76,6 +76,19 @@ class Validator(RootValidator):
|
||||
converted: bool = self.configuration._convert_to_boolean(value) # type: ignore[attr-defined]
|
||||
return converted
|
||||
|
||||
def _normalize_coerce_float(self, value: str) -> float:
|
||||
"""
|
||||
extract float from string value
|
||||
|
||||
Args:
|
||||
value(str): converting value
|
||||
|
||||
Returns:
|
||||
float: value converted to float according to configuration rules
|
||||
"""
|
||||
del self
|
||||
return float(value)
|
||||
|
||||
def _normalize_coerce_integer(self, value: str) -> int:
|
||||
"""
|
||||
extract integer from string value
|
||||
|
||||
@@ -20,10 +20,9 @@
|
||||
import contextlib
|
||||
import requests
|
||||
|
||||
from functools import cached_property
|
||||
from requests.adapters import BaseAdapter
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ahriman import __version__
|
||||
from ahriman.core.http.sync_http_client import SyncHttpClient
|
||||
|
||||
|
||||
@@ -37,32 +36,36 @@ class SyncAhrimanClient(SyncHttpClient):
|
||||
|
||||
address: str
|
||||
|
||||
@cached_property
|
||||
def session(self) -> requests.Session:
|
||||
def _login_url(self) -> str:
|
||||
"""
|
||||
get or create session
|
||||
get url for the login api
|
||||
|
||||
Returns:
|
||||
request.Session: created session object
|
||||
str: full url for web service to log in
|
||||
"""
|
||||
if urlparse(self.address).scheme == "http+unix":
|
||||
import requests_unixsocket
|
||||
session: requests.Session = requests_unixsocket.Session() # type: ignore[no-untyped-call]
|
||||
session.headers["User-Agent"] = f"ahriman/{__version__}"
|
||||
return session
|
||||
return f"{self.address}/api/v1/login"
|
||||
|
||||
session = requests.Session()
|
||||
session.headers["User-Agent"] = f"ahriman/{__version__}"
|
||||
self._login(session)
|
||||
|
||||
return session
|
||||
|
||||
def _login(self, session: requests.Session) -> None:
|
||||
def adapters(self) -> dict[str, BaseAdapter]:
|
||||
"""
|
||||
process login to the service
|
||||
get registered adapters
|
||||
|
||||
Returns:
|
||||
dict[str, BaseAdapter]: map of protocol and adapter used for this protocol
|
||||
"""
|
||||
adapters = SyncHttpClient.adapters(self)
|
||||
|
||||
if (scheme := urlparse(self.address).scheme) == "http+unix":
|
||||
from requests_unixsocket.adapters import UnixAdapter
|
||||
adapters[f"{scheme}://"] = UnixAdapter() # type: ignore[no-untyped-call]
|
||||
|
||||
return adapters
|
||||
|
||||
def on_session_creation(self, session: requests.Session) -> None:
|
||||
"""
|
||||
method which will be called on session creation
|
||||
|
||||
Args:
|
||||
session(requests.Session): request session to login
|
||||
session(requests.Session): created requests session
|
||||
"""
|
||||
if self.auth is None:
|
||||
return # no auth configured
|
||||
@@ -74,12 +77,3 @@ class SyncAhrimanClient(SyncHttpClient):
|
||||
}
|
||||
with contextlib.suppress(Exception):
|
||||
self.make_request("POST", self._login_url(), json=payload, session=session)
|
||||
|
||||
def _login_url(self) -> str:
|
||||
"""
|
||||
get url for the login api
|
||||
|
||||
Returns:
|
||||
str: full url for web service to log in
|
||||
"""
|
||||
return f"{self.address}/api/v1/login"
|
||||
|
||||
@@ -21,7 +21,9 @@ import requests
|
||||
import sys
|
||||
|
||||
from functools import cached_property
|
||||
from requests.adapters import BaseAdapter, HTTPAdapter
|
||||
from typing import Any, IO, Literal
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
from ahriman import __version__
|
||||
from ahriman.core.configuration import Configuration
|
||||
@@ -38,10 +40,14 @@ class SyncHttpClient(LazyLogging):
|
||||
|
||||
Attributes:
|
||||
auth(tuple[str, str] | None): HTTP basic auth object if set
|
||||
retry(Retry): retry policy of the HTTP client. Disabled by default
|
||||
suppress_errors(bool): suppress logging of request errors
|
||||
timeout(int | None): HTTP request timeout in seconds
|
||||
"""
|
||||
|
||||
retry: Retry = Retry()
|
||||
timeout: int | None = None
|
||||
|
||||
def __init__(self, configuration: Configuration | None = None, section: str | None = None, *,
|
||||
suppress_errors: bool = False) -> None:
|
||||
"""
|
||||
@@ -50,18 +56,21 @@ class SyncHttpClient(LazyLogging):
|
||||
section(str | None, optional): settings section name (Default value = None)
|
||||
suppress_errors(bool, optional): suppress logging of request errors (Default value = False)
|
||||
"""
|
||||
if configuration is None:
|
||||
configuration = Configuration() # dummy configuration
|
||||
if section is None:
|
||||
section = configuration.default_section
|
||||
configuration = configuration or Configuration() # dummy configuration
|
||||
section = section or configuration.default_section
|
||||
|
||||
username = configuration.get(section, "username", fallback=None)
|
||||
password = configuration.get(section, "password", fallback=None)
|
||||
self.auth = (username, password) if username and password else None
|
||||
|
||||
self.timeout: int | None = configuration.getint(section, "timeout", fallback=30)
|
||||
self.suppress_errors = suppress_errors
|
||||
|
||||
self.timeout = configuration.getint(section, "timeout", fallback=30)
|
||||
self.retry = SyncHttpClient.retry_policy(
|
||||
max_retries=configuration.getint(section, "max_retries", fallback=0),
|
||||
retry_backoff=configuration.getfloat(section, "retry_backoff", fallback=0.0),
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def session(self) -> requests.Session:
|
||||
"""
|
||||
@@ -71,11 +80,17 @@ class SyncHttpClient(LazyLogging):
|
||||
request.Session: created session object
|
||||
"""
|
||||
session = requests.Session()
|
||||
|
||||
for protocol, adapter in self.adapters().items():
|
||||
session.mount(protocol, adapter)
|
||||
|
||||
python_version = ".".join(map(str, sys.version_info[:3])) # just major.minor.patch
|
||||
session.headers["User-Agent"] = f"ahriman/{__version__} " \
|
||||
f"{requests.utils.default_user_agent()} " \
|
||||
f"python/{python_version}"
|
||||
|
||||
self.on_session_creation(session)
|
||||
|
||||
return session
|
||||
|
||||
@staticmethod
|
||||
@@ -92,6 +107,39 @@ class SyncHttpClient(LazyLogging):
|
||||
result: str = exception.response.text if exception.response is not None else ""
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def retry_policy(max_retries: int = 0, retry_backoff: float = 0.0) -> Retry:
|
||||
"""
|
||||
build retry policy for class
|
||||
|
||||
Args:
|
||||
max_retries(int, optional): maximum amount of retries allowed (Default value = 0)
|
||||
retry_backoff(float, optional): retry exponential backoff (Default value = 0.0)
|
||||
|
||||
Returns:
|
||||
Retry: built retry policy
|
||||
"""
|
||||
return Retry(
|
||||
total=max_retries,
|
||||
connect=max_retries,
|
||||
read=max_retries,
|
||||
status=max_retries,
|
||||
status_forcelist=[429, 500, 502, 503, 504],
|
||||
backoff_factor=retry_backoff,
|
||||
)
|
||||
|
||||
def adapters(self) -> dict[str, BaseAdapter]:
|
||||
"""
|
||||
get registered adapters
|
||||
|
||||
Returns:
|
||||
dict[str, BaseAdapter]: map of protocol and adapter used for this protocol
|
||||
"""
|
||||
return {
|
||||
"http://": HTTPAdapter(max_retries=self.retry),
|
||||
"https://": HTTPAdapter(max_retries=self.retry),
|
||||
}
|
||||
|
||||
def make_request(self, method: Literal["DELETE", "GET", "HEAD", "POST", "PUT"], url: str, *,
|
||||
headers: dict[str, str] | None = None,
|
||||
params: list[tuple[str, str]] | None = None,
|
||||
@@ -139,3 +187,11 @@ class SyncHttpClient(LazyLogging):
|
||||
if not suppress_errors:
|
||||
self.logger.exception("could not perform http request")
|
||||
raise
|
||||
|
||||
def on_session_creation(self, session: requests.Session) -> None:
|
||||
"""
|
||||
method which will be called on session creation
|
||||
|
||||
Args:
|
||||
session(requests.Session): created requests session
|
||||
"""
|
||||
|
||||
@@ -74,6 +74,18 @@ class Email(Report, JinjaTemplate):
|
||||
self.ssl = SmtpSSLSettings.from_option(configuration.get(section, "ssl", fallback="disabled"))
|
||||
self.user = configuration.get(section, "user", fallback=None)
|
||||
|
||||
@property
|
||||
def _smtp_session(self) -> type[smtplib.SMTP]:
|
||||
"""
|
||||
build SMTP session based on configuration settings
|
||||
|
||||
Returns:
|
||||
type[smtplib.SMTP]: SMTP or SMTP_SSL session depending on whether SSL is enabled or not
|
||||
"""
|
||||
if self.ssl == SmtpSSLSettings.SSL:
|
||||
return smtplib.SMTP_SSL
|
||||
return smtplib.SMTP
|
||||
|
||||
def _send(self, text: str, attachment: dict[str, str]) -> None:
|
||||
"""
|
||||
send email callback
|
||||
@@ -93,16 +105,13 @@ class Email(Report, JinjaTemplate):
|
||||
attach.add_header("Content-Disposition", "attachment", filename=filename)
|
||||
message.attach(attach)
|
||||
|
||||
if self.ssl != SmtpSSLSettings.SSL:
|
||||
session = smtplib.SMTP(self.host, self.port)
|
||||
with self._smtp_session(self.host, self.port) as session:
|
||||
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()
|
||||
|
||||
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())
|
||||
|
||||
def generate(self, packages: list[Package], result: Result) -> None:
|
||||
"""
|
||||
|
||||
@@ -302,6 +302,16 @@ class ReportTrigger(Trigger):
|
||||
"empty": False,
|
||||
"is_url": [],
|
||||
},
|
||||
"max_retries": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
"retry_backoff": {
|
||||
"type": "float",
|
||||
"coerce": "float",
|
||||
"min": 0,
|
||||
},
|
||||
"rss_url": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
|
||||
@@ -21,6 +21,7 @@ from typing import Self
|
||||
|
||||
from ahriman.core import _Context, context
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.alpm.remote import AUR
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.repository.executor import Executor
|
||||
@@ -73,9 +74,26 @@ class Repository(Executor, UpdateHandler):
|
||||
"""
|
||||
instance = cls(repository_id, configuration, database,
|
||||
report=report, refresh_pacman_database=refresh_pacman_database)
|
||||
|
||||
instance._set_globals(configuration)
|
||||
instance._set_context()
|
||||
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def _set_globals(configuration: Configuration) -> None:
|
||||
"""
|
||||
set global settings based on configuration via class attributes
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
AUR.timeout = configuration.getint("aur", "timeout", fallback=30)
|
||||
AUR.retry = AUR.retry_policy(
|
||||
max_retries=configuration.getint("aur", "max_retries", fallback=0),
|
||||
retry_backoff=configuration.getfloat("aur", "retry_backoff", fallback=0.0),
|
||||
)
|
||||
|
||||
def _set_context(self) -> None:
|
||||
"""
|
||||
create context variables and set their values
|
||||
|
||||
@@ -54,6 +54,11 @@ class UploadTrigger(Trigger):
|
||||
"type": "string",
|
||||
"allowed": ["github"],
|
||||
},
|
||||
"max_retries": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
@@ -68,6 +73,11 @@ class UploadTrigger(Trigger):
|
||||
"required": True,
|
||||
"empty": False,
|
||||
},
|
||||
"retry_backoff": {
|
||||
"type": "float",
|
||||
"coerce": "float",
|
||||
"min": 0,
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
@@ -90,6 +100,16 @@ class UploadTrigger(Trigger):
|
||||
"type": "string",
|
||||
"allowed": ["ahriman", "remote-service"],
|
||||
},
|
||||
"max_retries": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
"retry_backoff": {
|
||||
"type": "float",
|
||||
"coerce": "float",
|
||||
"min": 0,
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
|
||||
@@ -164,6 +164,11 @@ def check_output(*args: str, exception: Exception | Callable[[int, list[str], st
|
||||
if key in ("PATH",) # whitelisted variables only
|
||||
} | environment
|
||||
|
||||
result: dict[str, list[str]] = {
|
||||
"stdout": [],
|
||||
"stderr": [],
|
||||
}
|
||||
|
||||
with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
user=user, env=full_environment, text=True, encoding="utf8", errors="backslashreplace",
|
||||
bufsize=1) as process:
|
||||
@@ -172,30 +177,27 @@ def check_output(*args: str, exception: Exception | Callable[[int, list[str], st
|
||||
input_channel.write(input_data)
|
||||
input_channel.close()
|
||||
|
||||
selector = selectors.DefaultSelector()
|
||||
selector.register(get_io(process, "stdout"), selectors.EVENT_READ, data="stdout")
|
||||
selector.register(get_io(process, "stderr"), selectors.EVENT_READ, data="stderr")
|
||||
with selectors.DefaultSelector() as selector:
|
||||
selector.register(get_io(process, "stdout"), selectors.EVENT_READ, data="stdout")
|
||||
selector.register(get_io(process, "stderr"), selectors.EVENT_READ, data="stderr")
|
||||
|
||||
result: dict[str, list[str]] = {
|
||||
"stdout": [],
|
||||
"stderr": [],
|
||||
}
|
||||
while selector.get_map(): # while there are unread selectors, keep reading
|
||||
for key_data, output in poll(selector):
|
||||
result[key_data].append(output)
|
||||
|
||||
stdout = "\n".join(result["stdout"]).rstrip("\n") # remove newline at the end of any
|
||||
stderr = "\n".join(result["stderr"]).rstrip("\n")
|
||||
while selector.get_map(): # while there are unread selectors, keep reading
|
||||
for key_data, output in poll(selector):
|
||||
result[key_data].append(output)
|
||||
|
||||
status_code = process.wait()
|
||||
if status_code != 0:
|
||||
if isinstance(exception, Exception):
|
||||
raise exception
|
||||
if callable(exception):
|
||||
raise exception(status_code, list(args), stdout, stderr)
|
||||
raise CalledProcessError(status_code, list(args), stderr)
|
||||
|
||||
return stdout
|
||||
stdout = "\n".join(result["stdout"]).rstrip("\n") # remove newline at the end of any
|
||||
stderr = "\n".join(result["stderr"]).rstrip("\n")
|
||||
|
||||
if status_code != 0:
|
||||
if isinstance(exception, Exception):
|
||||
raise exception
|
||||
if callable(exception):
|
||||
raise exception(status_code, list(args), stdout, stderr)
|
||||
raise CalledProcessError(status_code, list(args), stderr)
|
||||
|
||||
return stdout
|
||||
|
||||
|
||||
def check_user(root: Path, *, unsafe: bool) -> None:
|
||||
|
||||
@@ -72,7 +72,7 @@ def _security() -> list[dict[str, Any]]:
|
||||
return [{
|
||||
"token": {
|
||||
"type": "apiKey", # as per specification we are using api key
|
||||
"name": "API_SESSION",
|
||||
"name": "AHRIMAN",
|
||||
"in": "cookie",
|
||||
}
|
||||
}]
|
||||
|
||||
@@ -149,11 +149,17 @@ def setup_auth(application: Application, configuration: Configuration, validator
|
||||
Application: configured web application
|
||||
"""
|
||||
secret_key = _cookie_secret_key(configuration)
|
||||
storage = EncryptedCookieStorage(secret_key, cookie_name="API_SESSION", max_age=validator.max_age)
|
||||
storage = EncryptedCookieStorage(
|
||||
secret_key,
|
||||
cookie_name="AHRIMAN",
|
||||
max_age=validator.max_age,
|
||||
httponly=True,
|
||||
samesite="Strict",
|
||||
)
|
||||
setup_session(application, storage)
|
||||
|
||||
authorization_policy = _AuthorizationPolicy(validator)
|
||||
identity_policy = aiohttp_security.SessionIdentityPolicy()
|
||||
identity_policy = aiohttp_security.SessionIdentityPolicy("SESSION")
|
||||
|
||||
aiohttp_security.setup(application, identity_policy, authorization_policy)
|
||||
application.middlewares.append(_auth_handler(validator.allow_read_only))
|
||||
|
||||
@@ -25,6 +25,6 @@ class AuthSchema(Schema):
|
||||
request cookie authorization schema
|
||||
"""
|
||||
|
||||
API_SESSION = fields.String(required=True, metadata={
|
||||
AHRIMAN = fields.String(required=True, metadata={
|
||||
"description": "API session key as returned from authorization",
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Restore.run(args, repository_id, configuration, report=False)
|
||||
extract_mock.extractall.assert_called_once_with(path=args.output)
|
||||
extract_mock.extractall.assert_called_once_with(path=args.output, filter="data")
|
||||
|
||||
|
||||
def test_disallow_multi_architecture_run() -> None:
|
||||
|
||||
@@ -33,6 +33,14 @@ def test_normalize_coerce_boolean(validator: Validator, mocker: MockerFixture) -
|
||||
convert_mock.assert_called_once_with("1")
|
||||
|
||||
|
||||
def test_normalize_coerce_float(validator: Validator) -> None:
|
||||
"""
|
||||
must convert string value to float by using configuration converters
|
||||
"""
|
||||
assert validator._normalize_coerce_float("1.5") == 1.5
|
||||
assert validator._normalize_coerce_float("0.0") == 0.0
|
||||
|
||||
|
||||
def test_normalize_coerce_integer(validator: Validator) -> None:
|
||||
"""
|
||||
must convert string value to integer by using configuration converters
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import pytest
|
||||
import requests
|
||||
import requests_unixsocket
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
@@ -8,31 +7,32 @@ from ahriman.core.http import SyncAhrimanClient
|
||||
from ahriman.models.user import User
|
||||
|
||||
|
||||
def test_session(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
|
||||
def test_adapters(ahriman_client: SyncAhrimanClient) -> None:
|
||||
"""
|
||||
must create normal requests session
|
||||
must return native adapters
|
||||
"""
|
||||
login_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient._login")
|
||||
|
||||
assert isinstance(ahriman_client.session, requests.Session)
|
||||
assert not isinstance(ahriman_client.session, requests_unixsocket.Session)
|
||||
login_mock.assert_called_once_with(pytest.helpers.anyvar(int))
|
||||
assert "http+unix://" not in ahriman_client.adapters()
|
||||
|
||||
|
||||
def test_session_unix_socket(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
|
||||
def test_adapters_unix_socket(ahriman_client: SyncAhrimanClient) -> None:
|
||||
"""
|
||||
must create unix socket session
|
||||
must register unix socket adapter
|
||||
"""
|
||||
login_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient._login")
|
||||
ahriman_client.address = "http+unix://path"
|
||||
|
||||
assert isinstance(ahriman_client.session, requests_unixsocket.Session)
|
||||
login_mock.assert_not_called()
|
||||
assert "http+unix://" in ahriman_client.adapters()
|
||||
|
||||
|
||||
def test_login(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
|
||||
def test_login_url(ahriman_client: SyncAhrimanClient) -> None:
|
||||
"""
|
||||
must login user
|
||||
must generate login url correctly
|
||||
"""
|
||||
assert ahriman_client._login_url().startswith(ahriman_client.address)
|
||||
assert ahriman_client._login_url().endswith("/api/v1/login")
|
||||
|
||||
|
||||
def test_on_session_creation(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must log in user on start
|
||||
"""
|
||||
ahriman_client.auth = (user.username, user.password)
|
||||
requests_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient.make_request")
|
||||
@@ -42,40 +42,32 @@ def test_login(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixt
|
||||
}
|
||||
session = requests.Session()
|
||||
|
||||
ahriman_client._login(session)
|
||||
ahriman_client.on_session_creation(session)
|
||||
requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json=payload, session=session)
|
||||
|
||||
|
||||
def test_login_failed(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
|
||||
def test_on_session_creation_failed(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress any exception happened during login
|
||||
must suppress any exception happened during session start
|
||||
"""
|
||||
ahriman_client.user = user
|
||||
mocker.patch("requests.Session.request", side_effect=Exception)
|
||||
ahriman_client._login(requests.Session())
|
||||
ahriman_client.on_session_creation(requests.Session())
|
||||
|
||||
|
||||
def test_login_failed_http_error(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
|
||||
def test_start_failed_http_error(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress HTTP exception happened during login
|
||||
must suppress HTTP exception happened during session start
|
||||
"""
|
||||
ahriman_client.user = user
|
||||
mocker.patch("requests.Session.request", side_effect=requests.HTTPError)
|
||||
ahriman_client._login(requests.Session())
|
||||
ahriman_client.on_session_creation(requests.Session())
|
||||
|
||||
|
||||
def test_login_skip(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
|
||||
def test_start_skip(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip login if no user set
|
||||
"""
|
||||
requests_mock = mocker.patch("requests.Session.request")
|
||||
ahriman_client._login(requests.Session())
|
||||
ahriman_client.on_session_creation(requests.Session())
|
||||
requests_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_login_url(ahriman_client: SyncAhrimanClient) -> None:
|
||||
"""
|
||||
must generate login url correctly
|
||||
"""
|
||||
assert ahriman_client._login_url().startswith(ahriman_client.address)
|
||||
assert ahriman_client._login_url().endswith("/api/v1/login")
|
||||
|
||||
@@ -4,6 +4,7 @@ import requests
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import MagicMock, call as MockCall
|
||||
|
||||
from ahriman.core.alpm.remote import AUR
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.http import SyncHttpClient
|
||||
|
||||
@@ -33,12 +34,29 @@ def test_init_auth_empty() -> None:
|
||||
assert SyncHttpClient().auth is None
|
||||
|
||||
|
||||
def test_session() -> None:
|
||||
def test_session(mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must generate valid session
|
||||
"""
|
||||
on_session_creation_mock = mocker.patch("ahriman.core.http.sync_http_client.SyncHttpClient.on_session_creation")
|
||||
|
||||
session = SyncHttpClient().session
|
||||
assert "User-Agent" in session.headers
|
||||
on_session_creation_mock.assert_called_once_with(pytest.helpers.anyvar(int))
|
||||
|
||||
|
||||
def test_retry_policy() -> None:
|
||||
"""
|
||||
must set retry policy
|
||||
"""
|
||||
SyncHttpClient.retry = SyncHttpClient.retry_policy(1, 2.0)
|
||||
AUR.retry = AUR.retry_policy(3, 4.0)
|
||||
|
||||
assert SyncHttpClient.retry.connect == 1
|
||||
assert SyncHttpClient.retry.backoff_factor == 2.0
|
||||
|
||||
assert AUR.retry.connect == 3
|
||||
assert AUR.retry.backoff_factor == 4.0
|
||||
|
||||
|
||||
def test_exception_response_text() -> None:
|
||||
@@ -60,6 +78,18 @@ def test_exception_response_text_empty() -> None:
|
||||
assert SyncHttpClient.exception_response_text(exception) == ""
|
||||
|
||||
|
||||
def test_adapters() -> None:
|
||||
"""
|
||||
must create adapters with retry policy
|
||||
"""
|
||||
client = SyncHttpClient()
|
||||
adapters = client.adapters()
|
||||
|
||||
assert "http://" in adapters
|
||||
assert "https://" in adapters
|
||||
assert all(adapter.max_retries == client.retry for adapter in adapters.values())
|
||||
|
||||
|
||||
def test_make_request(mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must make HTTP request
|
||||
@@ -158,3 +188,11 @@ def test_make_request_session() -> None:
|
||||
session_mock.request.assert_called_once_with(
|
||||
"GET", "url", params=None, data=None, headers=None, files=None, json=None,
|
||||
stream=None, auth=None, timeout=client.timeout)
|
||||
|
||||
|
||||
def test_on_session_creation() -> None:
|
||||
"""
|
||||
must do nothing on start
|
||||
"""
|
||||
client = SyncHttpClient()
|
||||
client.on_session_creation(client.session)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import smtplib
|
||||
|
||||
import pytest
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
@@ -6,6 +8,7 @@ from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.report.email import Email
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.result import Result
|
||||
from ahriman.models.smtp_ssl_settings import SmtpSSLSettings
|
||||
|
||||
|
||||
def test_template(configuration: Configuration) -> None:
|
||||
@@ -37,17 +40,36 @@ def test_template_full(configuration: Configuration) -> None:
|
||||
assert Email(repository_id, configuration, "email").template_full == root.parent / template
|
||||
|
||||
|
||||
def test_smtp_session(email: Email) -> None:
|
||||
"""
|
||||
must build normal SMTP session if SSL is disabled
|
||||
"""
|
||||
email.ssl = SmtpSSLSettings.Disabled
|
||||
assert email._smtp_session == smtplib.SMTP
|
||||
|
||||
email.ssl = SmtpSSLSettings.STARTTLS
|
||||
assert email._smtp_session == smtplib.SMTP
|
||||
|
||||
|
||||
def test_smtp_session_ssl(email: Email) -> None:
|
||||
"""
|
||||
must build SMTP_SSL session if SSL is enabled
|
||||
"""
|
||||
email.ssl = SmtpSSLSettings.SSL
|
||||
assert email._smtp_session == smtplib.SMTP_SSL
|
||||
|
||||
|
||||
def test_send(email: Email, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must send an email with attachment
|
||||
"""
|
||||
smtp_mock = mocker.patch("smtplib.SMTP")
|
||||
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
|
||||
|
||||
email._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_with(email.sender, email.receivers, pytest.helpers.anyvar(int))
|
||||
smtp_mock.return_value.quit.assert_called_once_with()
|
||||
|
||||
|
||||
def test_send_auth(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
@@ -57,6 +79,7 @@ def test_send_auth(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
configuration.set_option("email", "user", "username")
|
||||
configuration.set_option("email", "password", "password")
|
||||
smtp_mock = mocker.patch("smtplib.SMTP")
|
||||
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
email = Email(repository_id, configuration, "email")
|
||||
@@ -70,6 +93,7 @@ def test_send_auth_no_password(configuration: Configuration, mocker: MockerFixtu
|
||||
"""
|
||||
configuration.set_option("email", "user", "username")
|
||||
smtp_mock = mocker.patch("smtplib.SMTP")
|
||||
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
email = Email(repository_id, configuration, "email")
|
||||
@@ -83,6 +107,7 @@ def test_send_auth_no_user(configuration: Configuration, mocker: MockerFixture)
|
||||
"""
|
||||
configuration.set_option("email", "password", "password")
|
||||
smtp_mock = mocker.patch("smtplib.SMTP")
|
||||
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
email = Email(repository_id, configuration, "email")
|
||||
@@ -96,6 +121,7 @@ def test_send_ssl_tls(configuration: Configuration, mocker: MockerFixture) -> No
|
||||
"""
|
||||
configuration.set_option("email", "ssl", "ssl")
|
||||
smtp_mock = mocker.patch("smtplib.SMTP_SSL")
|
||||
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
email = Email(repository_id, configuration, "email")
|
||||
@@ -103,7 +129,6 @@ def test_send_ssl_tls(configuration: Configuration, mocker: MockerFixture) -> No
|
||||
smtp_mock.return_value.starttls.assert_not_called()
|
||||
smtp_mock.return_value.login.assert_not_called()
|
||||
smtp_mock.return_value.sendmail.assert_called_once_with(email.sender, email.receivers, pytest.helpers.anyvar(int))
|
||||
smtp_mock.return_value.quit.assert_called_once_with()
|
||||
|
||||
|
||||
def test_send_starttls(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
@@ -112,6 +137,7 @@ def test_send_starttls(configuration: Configuration, mocker: MockerFixture) -> N
|
||||
"""
|
||||
configuration.set_option("email", "ssl", "starttls")
|
||||
smtp_mock = mocker.patch("smtplib.SMTP")
|
||||
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
email = Email(repository_id, configuration, "email")
|
||||
|
||||
@@ -2,6 +2,7 @@ from pytest_mock import MockerFixture
|
||||
from unittest.mock import call as MockCall
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.alpm.remote import AUR
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.repository import Repository
|
||||
@@ -13,13 +14,27 @@ def test_load(configuration: Configuration, database: SQLite, mocker: MockerFixt
|
||||
"""
|
||||
must correctly load instance
|
||||
"""
|
||||
globals_mock = mocker.patch("ahriman.core.repository.Repository._set_globals")
|
||||
context_mock = mocker.patch("ahriman.core.repository.Repository._set_context")
|
||||
_, repository_id = configuration.check_loaded()
|
||||
|
||||
Repository.load(repository_id, configuration, database, report=False)
|
||||
globals_mock.assert_called_once_with(configuration)
|
||||
context_mock.assert_called_once_with()
|
||||
|
||||
|
||||
def test_set_globals(configuration: Configuration) -> None:
|
||||
"""
|
||||
must correctly set globals
|
||||
"""
|
||||
configuration.set_option("aur", "timeout", "42")
|
||||
configuration.set_option("aur", "max_retries", "10")
|
||||
|
||||
Repository._set_globals(configuration)
|
||||
assert AUR.timeout == 42
|
||||
assert AUR.retry.connect == 10
|
||||
|
||||
|
||||
def test_set_context(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must set context variables
|
||||
|
||||
@@ -23,7 +23,7 @@ def test_security() -> None:
|
||||
must generate security definitions for swagger
|
||||
"""
|
||||
token = next(iter(_security()))["token"]
|
||||
assert token == {"type": "apiKey", "name": "API_SESSION", "in": "cookie"}
|
||||
assert token == {"type": "apiKey", "name": "AHRIMAN", "in": "cookie"}
|
||||
|
||||
|
||||
def test_servers(application: Application) -> None:
|
||||
|
||||
@@ -6,4 +6,4 @@ def test_schema() -> None:
|
||||
must return valid schema
|
||||
"""
|
||||
schema = AuthSchema()
|
||||
assert not schema.validate({"API_SESSION": "key"})
|
||||
assert not schema.validate({"AHRIMAN": "key"})
|
||||
|
||||
@@ -27,7 +27,7 @@ def _client(client: TestClient, mocker: MockerFixture) -> TestClient:
|
||||
"parameters": [
|
||||
{
|
||||
"in": "cookie",
|
||||
"name": "API_SESSION",
|
||||
"name": "AHRIMAN",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
},
|
||||
@@ -39,7 +39,7 @@ def _client(client: TestClient, mocker: MockerFixture) -> TestClient:
|
||||
"parameters": [
|
||||
{
|
||||
"in": "cookie",
|
||||
"name": "API_SESSION",
|
||||
"name": "AHRIMAN",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
},
|
||||
@@ -60,7 +60,7 @@ def _client(client: TestClient, mocker: MockerFixture) -> TestClient:
|
||||
{
|
||||
"token": {
|
||||
"type": "apiKey",
|
||||
"name": "API_SESSION",
|
||||
"name": "AHRIMAN",
|
||||
"in": "cookie",
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user