diff --git a/docs/configuration.rst b/docs/configuration.rst index 1864ee7c..cb2d58b8 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -114,6 +114,7 @@ Web server settings. If any of ``host``/``port`` is not set, web integration wil * ``port`` - port to bind, int, optional. * ``static_path`` - path to directory with static files, string, required. * ``templates`` - path to templates directory, string, required. +* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``. * ``unix_socket`` - path to the listening unix socket, string, optional. If set, server will create the socket on the specified address which can (and will) be used by application. Note, that unlike usual host/port configuration, unix socket allows to perform requests without authorization. * ``unix_socket_unsafe`` - set unsafe (o+w) permissions to unix socket, boolean, optional, default ``yes``. This option is enabled by default, because it is supposed that unix socket is created in safe environment (only web service is supposed to be used in unsafe), but it can be disabled by configuration. * ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled. @@ -311,6 +312,7 @@ 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. +* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``. ``rsync`` type ^^^^^^^^^^^^^^ diff --git a/src/ahriman/core/alpm/remote/aur.py b/src/ahriman/core/alpm/remote/aur.py index 771c3997..9930a79f 100644 --- a/src/ahriman/core/alpm/remote/aur.py +++ b/src/ahriman/core/alpm/remote/aur.py @@ -17,14 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import requests - from typing import Any from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.remote import Remote from ahriman.core.exceptions import PackageInfoError, UnknownPackageError -from ahriman.core.util import exception_response_text from ahriman.models.aur_package import AURPackage @@ -36,13 +33,11 @@ class AUR(Remote): DEFAULT_AUR_URL(str): (class attribute) default AUR url DEFAULT_RPC_URL(str): (class attribute) default AUR RPC url DEFAULT_RPC_VERSION(str): (class attribute) default AUR RPC version - DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds """ DEFAULT_AUR_URL = "https://aur.archlinux.org" DEFAULT_RPC_URL = f"{DEFAULT_AUR_URL}/rpc" DEFAULT_RPC_VERSION = "5" - DEFAULT_TIMEOUT = 30 @classmethod def remote_git_url(cls, package_base: str, repository: str) -> str: @@ -91,7 +86,7 @@ class AUR(Remote): raise PackageInfoError(error_details) return [AURPackage.from_json(package) for package in response["results"]] - def make_request(self, request_type: str, *args: str, **kwargs: str) -> list[AURPackage]: + def aur_request(self, request_type: str, *args: str, **kwargs: str) -> list[AURPackage]: """ perform request to AUR RPC @@ -103,34 +98,20 @@ class AUR(Remote): Returns: list[AURPackage]: response parsed to package list """ - query: dict[str, Any] = { - "type": request_type, - "v": self.DEFAULT_RPC_VERSION - } + query: list[tuple[str, str]] = [ + ("type", request_type), + ("v", self.DEFAULT_RPC_VERSION), + ] arg_query = "arg[]" if len(args) > 1 else "arg" - query[arg_query] = list(args) + for arg in args: + query.append((arg_query, arg)) for key, value in kwargs.items(): - query[key] = value + query.append((key, value)) - try: - response = requests.get( - self.DEFAULT_RPC_URL, - params=query, - headers={"User-Agent": self.DEFAULT_USER_AGENT}, - timeout=self.DEFAULT_TIMEOUT) - response.raise_for_status() - return self.parse_response(response.json()) - except requests.HTTPError as e: - self.logger.exception( - "could not perform request by using type %s: %s", - request_type, - exception_response_text(e)) - raise - except Exception: - self.logger.exception("could not perform request by using type %s", request_type) - raise + response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query) + return self.parse_response(response.json()) def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage: """ @@ -146,7 +127,7 @@ class AUR(Remote): Raises: UnknownPackageError: package doesn't exist """ - packages = self.make_request("info", package_name) + packages = self.aur_request("info", package_name) try: return next(package for package in packages if package.name == package_name) except StopIteration: @@ -163,4 +144,4 @@ class AUR(Remote): Returns: list[AURPackage]: list of packages which match the criteria """ - return self.make_request("search", *keywords, by="name-desc") + return self.aur_request("search", *keywords, by="name-desc") diff --git a/src/ahriman/core/alpm/remote/official.py b/src/ahriman/core/alpm/remote/official.py index d64d1c68..e99344af 100644 --- a/src/ahriman/core/alpm/remote/official.py +++ b/src/ahriman/core/alpm/remote/official.py @@ -17,14 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import requests - from typing import Any from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.remote import Remote from ahriman.core.exceptions import PackageInfoError, UnknownPackageError -from ahriman.core.util import exception_response_text from ahriman.models.aur_package import AURPackage @@ -37,14 +34,12 @@ class Official(Remote): DEFAULT_ARCHLINUX_GIT_URL(str): (class attribute) default url for git packages DEFAULT_SEARCH_REPOSITORIES(list[str]): (class attribute) default list of repositories to search DEFAULT_RPC_URL(str): (class attribute) default archlinux repositories RPC url - DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds """ DEFAULT_ARCHLINUX_GIT_URL = "https://gitlab.archlinux.org" DEFAULT_ARCHLINUX_URL = "https://archlinux.org" DEFAULT_SEARCH_REPOSITORIES = ["Core", "Extra", "Multilib"] DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json" - DEFAULT_TIMEOUT = 30 @classmethod def remote_git_url(cls, package_base: str, repository: str) -> str: @@ -91,7 +86,7 @@ class Official(Remote): raise PackageInfoError("API validation error") return [AURPackage.from_repo(package) for package in response["results"]] - def make_request(self, *args: str, by: str) -> list[AURPackage]: + def arch_request(self, *args: str, by: str) -> list[AURPackage]: """ perform request to official repositories RPC @@ -102,20 +97,15 @@ class Official(Remote): Returns: list[AURPackage]: response parsed to package list """ - try: - response = requests.get( - self.DEFAULT_RPC_URL, - params={by: args, "repo": self.DEFAULT_SEARCH_REPOSITORIES}, - headers={"User-Agent": self.DEFAULT_USER_AGENT}, - timeout=self.DEFAULT_TIMEOUT) - response.raise_for_status() - return self.parse_response(response.json()) - except requests.HTTPError as e: - self.logger.exception("could not perform request: %s", exception_response_text(e)) - raise - except Exception: - self.logger.exception("could not perform request") - raise + query: list[tuple[str, str]] = [ + ("repo", repository) + for repository in self.DEFAULT_SEARCH_REPOSITORIES + ] + for arg in args: + query.append((by, arg)) + + response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query) + return self.parse_response(response.json()) def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage: """ @@ -131,7 +121,7 @@ class Official(Remote): Raises: UnknownPackageError: package doesn't exist """ - packages = self.make_request(package_name, by="name") + packages = self.arch_request(package_name, by="name") try: return next(package for package in packages if package.name == package_name) except StopIteration: @@ -148,4 +138,4 @@ class Official(Remote): Returns: list[AURPackage]: list of packages which match the criteria """ - return self.make_request(*keywords, by="q") + return self.arch_request(*keywords, by="q") diff --git a/src/ahriman/core/alpm/remote/remote.py b/src/ahriman/core/alpm/remote/remote.py index bcd150d1..82afdf26 100644 --- a/src/ahriman/core/alpm/remote/remote.py +++ b/src/ahriman/core/alpm/remote/remote.py @@ -17,19 +17,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from ahriman import __version__ from ahriman.core.alpm.pacman import Pacman -from ahriman.core.log import LazyLogging +from ahriman.core.http import SyncHttpClient from ahriman.models.aur_package import AURPackage -class Remote(LazyLogging): +class Remote(SyncHttpClient): """ base class for remote package search - Attributes: - DEFAULT_USER_AGENT(str): (class attribute) default user agent - Examples: These classes are designed to be used without instancing. In order to achieve it several class methods are provided: ``info``, ``multisearch`` and ``search``. Thus, the basic flow is the following:: @@ -43,8 +39,6 @@ class Remote(LazyLogging): directly, whereas ``multisearch`` splits search one by one and finds intersection between search results. """ - DEFAULT_USER_AGENT = f"ahriman/{__version__}" - @classmethod def info(cls, package_name: str, *, pacman: Pacman) -> AURPackage: """ diff --git a/src/ahriman/core/http/__init__.py b/src/ahriman/core/http/__init__.py new file mode 100644 index 00000000..3e476dd1 --- /dev/null +++ b/src/ahriman/core/http/__init__.py @@ -0,0 +1,20 @@ +# +# Copyright (c) 2021-2023 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 . +# +from ahriman.core.http.sync_http_client import MultipartType, SyncHttpClient diff --git a/src/ahriman/core/http/sync_http_client.py b/src/ahriman/core/http/sync_http_client.py new file mode 100644 index 00000000..731e0b6f --- /dev/null +++ b/src/ahriman/core/http/sync_http_client.py @@ -0,0 +1,137 @@ +# +# Copyright (c) 2021-2023 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 . +# +import requests + +from functools import cached_property +from typing import Any, IO, Literal + +from ahriman import __version__ +from ahriman.core.configuration import Configuration +from ahriman.core.log import LazyLogging + + +# filename, file, content-type, headers +MultipartType = tuple[str, IO[bytes], str, dict[str, str]] + + +class SyncHttpClient(LazyLogging): + """ + wrapper around requests library to reduce boilerplate + + Attributes: + auth(tuple[str, str] | None): HTTP basic auth object if set + suppress_errors(bool): suppress logging of request errors + timeout(int): HTTP request timeout in seconds + """ + + def __init__(self, section: str | None = None, configuration: Configuration | None = None, *, + suppress_errors: bool = False) -> None: + """ + default constructor + + Args: + section(str, optional): settings section name (Default value = None) + configuration(Configuration | None): configuration instance (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 + + 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 = configuration.getint(section, "timeout", fallback=30) + self.suppress_errors = suppress_errors + + @cached_property + def session(self) -> requests.Session: + """ + get or create session + + Returns: + request.Session: created session object + """ + session = requests.Session() + session.headers["User-Agent"] = f"ahriman/{__version__}" + + return session + + @staticmethod + def exception_response_text(exception: requests.exceptions.RequestException) -> str: + """ + safe response exception text generation + + Args: + exception(requests.exceptions.RequestException): exception raised + + Returns: + str: text of the response if it is not None and empty string otherwise + """ + result: str = exception.response.text if exception.response is not None else "" + return result + + def make_request(self, method: Literal["DELETE", "GET", "POST", "PUT"], url: str, *, + headers: dict[str, str] | None = None, + params: list[tuple[str, str]] | None = None, + data: Any | None = None, + json: dict[str, Any] | None = None, + files: dict[str, MultipartType] | None = None, + session: requests.Session | None = None, + suppress_errors: bool | None = None) -> requests.Response: + """ + perform request with specified parameters + + Args: + method(Literal["DELETE", "GET", "POST", "PUT"]): HTTP method to call + url(str): remote url to call + headers(dict[str, str] | None, optional): request headers (Default value = None) + params(list[tuple[str, str]] | None, optional): request query parameters (Default value = None) + data(Any | None, optional): request raw data parameters (Default value = None) + json(dict[str, Any] | None, optional): request json parameters (Default value = None) + files(dict[str, MultipartType] | None, optional): multipart upload (Default value = None) + session(requests.Session | None, optional): session object if any (Default value = None) + suppress_errors(bool | None, optional): suppress logging errors (e.g. if no web server available). If none + set, the instance-wide value will be used (Default value = None) + + Returns: + requests.Response: response object + """ + # defaults + if suppress_errors is None: + suppress_errors = self.suppress_errors + if session is None: + session = self.session + + try: + response = session.request(method, url, params=params, data=data, headers=headers, files=files, json=json, + auth=self.auth, timeout=self.timeout) + response.raise_for_status() + return response + except requests.HTTPError as ex: + if not suppress_errors: + self.logger.exception("could not perform http request: %s", self.exception_response_text(ex)) + raise + except Exception: + if not suppress_errors: + self.logger.exception("could not perform http request") + raise diff --git a/src/ahriman/core/report/remote_call.py b/src/ahriman/core/report/remote_call.py index ad467cb5..94d5feb3 100644 --- a/src/ahriman/core/report/remote_call.py +++ b/src/ahriman/core/report/remote_call.py @@ -81,8 +81,9 @@ class RemoteCall(Report): """ try: response = self.client.make_request("GET", f"/api/v1/service/process/{process_id}") - except requests.RequestException as e: - if e.response is not None and e.response.status_code == 404: + except requests.HTTPError as ex: + status_code = ex.response.status_code if ex.response is not None else None + if status_code == 404: return False raise diff --git a/src/ahriman/core/report/telegram.py b/src/ahriman/core/report/telegram.py index 3ae8e838..bab40311 100644 --- a/src/ahriman/core/report/telegram.py +++ b/src/ahriman/core/report/telegram.py @@ -17,17 +17,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import requests # technically we could use python-telegram-bot, but it is just a single request, c'mon - from ahriman.core.configuration import Configuration +from ahriman.core.http import SyncHttpClient from ahriman.core.report.jinja_template import JinjaTemplate from ahriman.core.report.report import Report -from ahriman.core.util import exception_response_text from ahriman.models.package import Package from ahriman.models.result import Result -class Telegram(Report, JinjaTemplate): +class Telegram(Report, JinjaTemplate, SyncHttpClient): """ telegram report generator @@ -38,7 +36,6 @@ class Telegram(Report, JinjaTemplate): chat_id(str): chat id to post message, either string with @ or integer template_path(Path): path to template for built packages template_type(str): template message type to be used in parse mode, one of MarkdownV2, HTML, Markdown - timeout(int): HTTP request timeout in seconds """ TELEGRAM_API_URL = "https://api.telegram.org" @@ -55,12 +52,12 @@ class Telegram(Report, JinjaTemplate): """ Report.__init__(self, architecture, configuration) JinjaTemplate.__init__(self, section, configuration) + SyncHttpClient.__init__(self, section, configuration) self.api_key = configuration.get(section, "api_key") self.chat_id = configuration.get(section, "chat_id") self.template_path = configuration.getpath(section, "template_path") self.template_type = configuration.get(section, "template_type", fallback="HTML") - self.timeout = configuration.getint(section, "timeout", fallback=30) def _send(self, text: str) -> None: """ @@ -69,18 +66,8 @@ class Telegram(Report, JinjaTemplate): Args: text(str): message body text """ - try: - response = requests.post( - f"{self.TELEGRAM_API_URL}/bot{self.api_key}/sendMessage", - data={"chat_id": self.chat_id, "text": text, "parse_mode": self.template_type}, - timeout=self.timeout) - response.raise_for_status() - except requests.HTTPError as e: - self.logger.exception("could not perform request: %s", exception_response_text(e)) - raise - except Exception: - self.logger.exception("could not perform request") - raise + self.make_request("POST", f"{self.TELEGRAM_API_URL}/bot{self.api_key}/sendMessage", + data={"chat_id": self.chat_id, "text": text, "parse_mode": self.template_type}) def generate(self, packages: list[Package], result: Result) -> None: """ diff --git a/src/ahriman/core/sign/gpg.py b/src/ahriman/core/sign/gpg.py index 5e7b7a1e..99fd15ec 100644 --- a/src/ahriman/core/sign/gpg.py +++ b/src/ahriman/core/sign/gpg.py @@ -17,30 +17,26 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import requests - from pathlib import Path from ahriman.core.configuration import Configuration from ahriman.core.exceptions import BuildError -from ahriman.core.log import LazyLogging -from ahriman.core.util import check_output, exception_response_text +from ahriman.core.http import SyncHttpClient +from ahriman.core.util import check_output from ahriman.models.sign_settings import SignSettings -class GPG(LazyLogging): +class GPG(SyncHttpClient): """ gnupg wrapper Attributes: - DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds configuration(Configuration): configuration instance default_key(str | None): default PGP key ID to use targets(set[SignSettings]): list of targets to sign (repository, package etc) """ _check_output = check_output - DEFAULT_TIMEOUT = 30 def __init__(self, configuration: Configuration) -> None: """ @@ -49,6 +45,7 @@ class GPG(LazyLogging): Args: configuration(Configuration): configuration instance """ + SyncHttpClient.__init__(self) self.configuration = configuration self.targets, self.default_key = self.sign_options(configuration) @@ -126,16 +123,11 @@ class GPG(LazyLogging): str: key as plain text """ key = key if key.startswith("0x") else f"0x{key}" - try: - response = requests.get(f"https://{server}/pks/lookup", params={ - "op": "get", - "options": "mr", - "search": key - }, timeout=self.DEFAULT_TIMEOUT) - response.raise_for_status() - except requests.exceptions.HTTPError as e: - self.logger.exception("could not download key %s from %s: %s", key, server, exception_response_text(e)) - raise + response = self.make_request("GET", f"https://{server}/pks/lookup", params=[ + ("op", "get"), + ("options", "mr"), + ("search", key), + ]) return response.text def key_export(self, key: str) -> str: diff --git a/src/ahriman/core/status/web_client.py b/src/ahriman/core/status/web_client.py index b8514edd..76e2a2b7 100644 --- a/src/ahriman/core/status/web_client.py +++ b/src/ahriman/core/status/web_client.py @@ -22,39 +22,27 @@ import logging import requests from functools import cached_property -from typing import Any, IO, Literal from urllib.parse import quote_plus as urlencode from ahriman import __version__ from ahriman.core.configuration import Configuration -from ahriman.core.log import LazyLogging +from ahriman.core.http import SyncHttpClient from ahriman.core.status.client import Client -from ahriman.core.util import exception_response_text from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.internal_status import InternalStatus from ahriman.models.log_record_id import LogRecordId from ahriman.models.package import Package -from ahriman.models.user import User -# filename, file, content-type, headers -MultipartType = tuple[str, IO[bytes], str, dict[str, str]] - - -class WebClient(Client, LazyLogging): +class WebClient(Client, SyncHttpClient): """ build status reporter web client Attributes: address(str): address of the web service - suppress_errors(bool): suppress logging errors (e.g. if no web server available) - user(User | None): web service user descriptor use_unix_socket(bool): use websocket or not """ - _login_url = "/api/v1/login" - _status_url = "/api/v1/status" - def __init__(self, configuration: Configuration) -> None: """ default constructor @@ -62,11 +50,10 @@ class WebClient(Client, LazyLogging): Args: configuration(Configuration): configuration instance """ + suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False) + SyncHttpClient.__init__(self, "web", configuration, suppress_errors=suppress_errors) + self.address, self.use_unix_socket = self.parse_address(configuration) - self.user = User.from_option( - configuration.get("web", "username", fallback=None), - configuration.get("web", "password", fallback=None)) - self.suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False) @cached_property def session(self) -> requests.Session: @@ -78,34 +65,6 @@ class WebClient(Client, LazyLogging): """ return self._create_session(use_unix_socket=self.use_unix_socket) - @staticmethod - def _logs_url(package_base: str) -> str: - """ - get url for the logs api - - Args: - package_base(str): package base - - Returns: - str: full url for web service for logs - """ - return f"/api/v1/packages/{package_base}/logs" - - @staticmethod - def _package_url(package_base: str = "") -> str: - """ - url generator - - Args: - package_base(str, optional): package base to generate url (Default value = "") - - Returns: - str: full url of web service for specific package base - """ - # in case if unix socket is used we need to normalize url - suffix = f"/{package_base}" if package_base else "" - return f"/api/v1/packages{suffix}" - @staticmethod def parse_address(configuration: Configuration) -> tuple[str, bool]: """ @@ -157,56 +116,60 @@ class WebClient(Client, LazyLogging): Args: session(requests.Session): request session to login """ - if self.user is None: + if self.auth is None: return # no auth configured + username, password = self.auth payload = { - "username": self.user.username, - "password": self.user.password + "username": username, + "password": password, } with contextlib.suppress(Exception): - self.make_request("POST", self._login_url, json=payload, session=session) + self.make_request("POST", self._login_url(), json=payload, session=session) - def make_request(self, method: Literal["DELETE", "GET", "POST"], url: str, *, - params: list[tuple[str, str]] | None = None, - json: dict[str, Any] | None = None, - files: dict[str, MultipartType] | None = None, - session: requests.Session | None = None, - suppress_errors: bool | None = None) -> requests.Response: + def _login_url(self) -> str: """ - perform request with specified parameters - - Args: - method(Literal["DELETE", "GET", "POST"]): HTTP method to call - url(str): remote url to call - params(list[tuple[str, str]] | None, optional): request query parameters (Default value = None) - json(dict[str, Any] | None, optional): request json parameters (Default value = None) - files(dict[str, MultipartType] | None, optional): multipart upload (Default value = None) - session(requests.Session | None, optional): session object if any (Default value = None) - suppress_errors(bool | None, optional): suppress logging errors (e.g. if no web server available). If none - set, the instance-wide value will be used (Default value = None) + get url for the login api Returns: - requests.Response: response object + str: full url for web service to log in """ - # defaults - if suppress_errors is None: - suppress_errors = self.suppress_errors - if session is None: - session = self.session + return f"{self.address}/api/v1/login" - try: - response = session.request(method, f"{self.address}{url}", params=params, json=json, files=files) - response.raise_for_status() - return response - except requests.RequestException as e: - if not suppress_errors: - self.logger.exception("could not perform http request: %s", exception_response_text(e)) - raise - except Exception: - if not suppress_errors: - self.logger.exception("could not perform http request") - raise + def _logs_url(self, package_base: str) -> str: + """ + get url for the logs api + + Args: + package_base(str): package base + + Returns: + str: full url for web service for logs + """ + return f"{self.address}/api/v1/packages/{package_base}/logs" + + def _package_url(self, package_base: str = "") -> str: + """ + url generator + + Args: + package_base(str, optional): package base to generate url (Default value = "") + + Returns: + str: full url of web service for specific package base + """ + # in case if unix socket is used we need to normalize url + suffix = f"/{package_base}" if package_base else "" + return f"{self.address}/api/v1/packages{suffix}" + + def _status_url(self) -> str: + """ + get url for the status api + + Returns: + str: full url for web service for status + """ + return f"{self.address}/api/v1/status" def package_add(self, package: Package, status: BuildStatusEnum) -> None: """ @@ -293,7 +256,7 @@ class WebClient(Client, LazyLogging): InternalStatus: current internal (web) service status """ with contextlib.suppress(Exception): - response = self.make_request("GET", self._status_url) + response = self.make_request("GET", self._status_url()) response_json = response.json() return InternalStatus.from_json(response_json) @@ -309,4 +272,4 @@ class WebClient(Client, LazyLogging): """ payload = {"status": status.value} with contextlib.suppress(Exception): - self.make_request("POST", self._status_url, json=payload) + self.make_request("POST", self._status_url(), json=payload) diff --git a/src/ahriman/core/upload/github.py b/src/ahriman/core/upload/github.py index b511895a..19062036 100644 --- a/src/ahriman/core/upload/github.py +++ b/src/ahriman/core/upload/github.py @@ -61,7 +61,7 @@ class Github(HttpUpload): """ try: asset = next(asset for asset in release["assets"] if asset["name"] == name) - self._request("DELETE", asset["url"]) + self.make_request("DELETE", asset["url"]) except StopIteration: self.logger.info("no asset %s found in release %s", name, release["name"]) @@ -81,7 +81,7 @@ class Github(HttpUpload): headers = {"Content-Type": mime} if mime is not None else {"Content-Type": "application/octet-stream"} with path.open("rb") as archive: - self._request("POST", url, params={"name": path.name}, data=archive, headers=headers) + self.make_request("POST", url, params=[("name", path.name)], data=archive, headers=headers) def get_local_files(self, path: Path) -> dict[Path, str]: """ @@ -136,7 +136,7 @@ class Github(HttpUpload): dict[str, Any]: github API release object for the new release """ url = f"https://api.github.com/repos/{self.github_owner}/{self.github_repository}/releases" - response = self._request("POST", url, json={"tag_name": self.architecture, "name": self.architecture}) + response = self.make_request("POST", url, json={"tag_name": self.architecture, "name": self.architecture}) release: dict[str, Any] = response.json() return release @@ -149,11 +149,11 @@ class Github(HttpUpload): """ url = f"https://api.github.com/repos/{self.github_owner}/{self.github_repository}/releases/tags/{self.architecture}" try: - response = self._request("GET", url) + response = self.make_request("GET", url) release: dict[str, Any] = response.json() return release - except requests.HTTPError as e: - status_code = e.response.status_code if e.response is not None else None + except requests.HTTPError as ex: + status_code = ex.response.status_code if ex.response is not None else None if status_code == 404: return None raise @@ -166,7 +166,7 @@ class Github(HttpUpload): release(dict[str, Any]): release object body(str): new release body """ - self._request("POST", release["url"], json={"body": body}) + self.make_request("POST", release["url"], json={"body": body}) def sync(self, path: Path, built_packages: list[Package]) -> None: """ diff --git a/src/ahriman/core/upload/http_upload.py b/src/ahriman/core/upload/http_upload.py index 69eb6334..68cc5c52 100644 --- a/src/ahriman/core/upload/http_upload.py +++ b/src/ahriman/core/upload/http_upload.py @@ -18,24 +18,17 @@ # along with this program. If not, see . # import hashlib -import requests -from functools import cached_property from pathlib import Path -from typing import Any from ahriman.core.configuration import Configuration +from ahriman.core.http import SyncHttpClient from ahriman.core.upload.upload import Upload -from ahriman.core.util import exception_response_text -class HttpUpload(Upload): +class HttpUpload(Upload, SyncHttpClient): """ helper for the http based uploads - - Attributes: - auth(tuple[str, str] | None): HTTP auth object if set - timeout(int): HTTP request timeout in seconds """ def __init__(self, architecture: str, configuration: Configuration, section: str) -> None: @@ -48,20 +41,7 @@ class HttpUpload(Upload): section(str): configuration section name """ Upload.__init__(self, architecture, configuration) - password = configuration.get(section, "password", fallback=None) - username = configuration.get(section, "username", fallback=None) - self.auth = (password, username) if password and username else None - self.timeout = configuration.getint(section, "timeout", fallback=30) - - @cached_property - def session(self) -> requests.Session: - """ - get or create session - - Returns: - request.Session: created session object - """ - return requests.Session() + SyncHttpClient.__init__(self, section, configuration) @staticmethod def calculate_hash(path: Path) -> str: @@ -107,23 +87,3 @@ class HttpUpload(Upload): file, md5 = line.split() files[file] = md5 return files - - def _request(self, method: str, url: str, **kwargs: Any) -> requests.Response: - """ - request wrapper - - Args: - method(str): request method - url(str): request url - **kwargs(Any): request parameters to be passed as is - - Returns: - requests.Response: request response object - """ - try: - response = self.session.request(method, url, auth=self.auth, timeout=self.timeout, **kwargs) - response.raise_for_status() - except requests.HTTPError as e: - self.logger.exception("could not perform %s request to %s: %s", method, url, exception_response_text(e)) - raise - return response diff --git a/src/ahriman/core/upload/remote_service.py b/src/ahriman/core/upload/remote_service.py index 50c415f5..5ea2e03a 100644 --- a/src/ahriman/core/upload/remote_service.py +++ b/src/ahriman/core/upload/remote_service.py @@ -23,8 +23,9 @@ from functools import cached_property from pathlib import Path from ahriman.core.configuration import Configuration +from ahriman.core.http import MultipartType from ahriman.core.sign.gpg import GPG -from ahriman.core.status.web_client import MultipartType, WebClient +from ahriman.core.status.web_client import WebClient from ahriman.core.upload.http_upload import HttpUpload from ahriman.models.package import Package @@ -77,7 +78,7 @@ class RemoteService(HttpUpload): if signature_path is not None: files["signature"] = signature_path.name, signature_path.open("rb"), "application/octet-stream", {} - self._request("POST", f"{self.client.address}/api/v1/service/upload", files=files) + self.make_request("POST", f"{self.client.address}/api/v1/service/upload", files=files) finally: for _, fd, _, _ in files.values(): fd.close() diff --git a/src/ahriman/core/upload/upload.py b/src/ahriman/core/upload/upload.py index 3acf888b..ce443b87 100644 --- a/src/ahriman/core/upload/upload.py +++ b/src/ahriman/core/upload/upload.py @@ -51,8 +51,8 @@ class Upload(LazyLogging): >>> try: >>> upload.sync(configuration.repository_paths.repository, []) - >>> except Exception as exception: - >>> handle_exceptions(exception) + >>> except Exception as ex: + >>> handle_exceptions(ex) """ def __init__(self, architecture: str, configuration: Configuration) -> None: diff --git a/src/ahriman/core/util.py b/src/ahriman/core/util.py index c47da89c..c1791c80 100644 --- a/src/ahriman/core/util.py +++ b/src/ahriman/core/util.py @@ -24,7 +24,6 @@ import itertools import logging import os import re -import requests import selectors import subprocess @@ -44,7 +43,6 @@ __all__ = [ "check_user", "dataclass_view", "enum_values", - "exception_response_text", "extract_user", "filter_json", "full_version", @@ -214,20 +212,6 @@ def enum_values(enum: type[Enum]) -> list[str]: return [str(key.value) for key in enum] # explicit str conversion for typing -def exception_response_text(exception: requests.exceptions.RequestException) -> str: - """ - safe response exception text generation - - Args: - exception(requests.exceptions.RequestException): exception raised - - Returns: - str: text of the response if it is not None and empty string otherwise - """ - result: str = exception.response.text if exception.response is not None else "" - return result - - def extract_user() -> str | None: """ extract user from system environment diff --git a/src/ahriman/models/user.py b/src/ahriman/models/user.py index 35e6c0db..6c3c3349 100644 --- a/src/ahriman/models/user.py +++ b/src/ahriman/models/user.py @@ -75,24 +75,6 @@ class User: object.__setattr__(self, "packager_id", self.packager_id or None) object.__setattr__(self, "key", self.key or None) - @classmethod - def from_option(cls, username: str | None, password: str | None, - access: UserAccess = UserAccess.Read) -> Self | None: - """ - build user descriptor from configuration options - - Args: - username(str | None): username - password(str | None): password as string - access(UserAccess, optional): optional user access (Default value = UserAccess.Read) - - Returns: - Self | None: generated user descriptor if all options are supplied and None otherwise - """ - if username is None or password is None: - return None - return cls(username=username, password=password, access=access, packager_id=None, key=None) - @staticmethod def generate_password(length: int) -> str: """ diff --git a/src/ahriman/web/middlewares/exception_handler.py b/src/ahriman/web/middlewares/exception_handler.py index 9e05669d..460c8641 100644 --- a/src/ahriman/web/middlewares/exception_handler.py +++ b/src/ahriman/web/middlewares/exception_handler.py @@ -60,31 +60,31 @@ def exception_handler(logger: logging.Logger) -> MiddlewareType: async def handle(request: Request, handler: HandlerType) -> StreamResponse: try: return await handler(request) - except HTTPUnauthorized as e: + except HTTPUnauthorized as ex: if _is_templated_unauthorized(request): - context = {"code": e.status_code, "reason": e.reason} - return aiohttp_jinja2.render_template("error.jinja2", request, context, status=e.status_code) - return json_response(data={"error": e.reason}, status=e.status_code) - except HTTPMethodNotAllowed as e: - if e.method == "OPTIONS": + context = {"code": ex.status_code, "reason": ex.reason} + return aiohttp_jinja2.render_template("error.jinja2", request, context, status=ex.status_code) + return json_response(data={"error": ex.reason}, status=ex.status_code) + except HTTPMethodNotAllowed as ex: + if ex.method == "OPTIONS": # automatically handle OPTIONS method, idea comes from # https://github.com/arcan1s/ffxivbis/blob/master/src/main/scala/me/arcanis/ffxivbis/http/api/v1/HttpHandler.scala#L32 - raise HTTPNoContent(headers={"Allow": ",".join(sorted(e.allowed_methods))}) - if e.method == "HEAD": + raise HTTPNoContent(headers={"Allow": ",".join(sorted(ex.allowed_methods))}) + if ex.method == "HEAD": # since we have special autogenerated HEAD method, we need to remove it from list of available - e.allowed_methods = {method for method in e.allowed_methods if method != "HEAD"} - e.headers["Allow"] = ",".join(sorted(e.allowed_methods)) - raise e + ex.allowed_methods = {method for method in ex.allowed_methods if method != "HEAD"} + ex.headers["Allow"] = ",".join(sorted(ex.allowed_methods)) + raise ex raise - except HTTPClientError as e: - return json_response(data={"error": e.reason}, status=e.status_code) - except HTTPServerError as e: + except HTTPClientError as ex: + return json_response(data={"error": ex.reason}, status=ex.status_code) + except HTTPServerError as ex: logger.exception("server exception during performing request to %s", request.path) - return json_response(data={"error": e.reason}, status=e.status_code) + return json_response(data={"error": ex.reason}, status=ex.status_code) except HTTPException: # just raise 2xx and 3xx codes raise - except Exception as e: + except Exception as ex: logger.exception("unknown exception during performing request to %s", request.path) - return json_response(data={"error": str(e)}, status=500) + return json_response(data={"error": str(ex)}, status=500) return handle diff --git a/src/ahriman/web/views/service/add.py b/src/ahriman/web/views/service/add.py index 73eab627..68dbe0c6 100644 --- a/src/ahriman/web/views/service/add.py +++ b/src/ahriman/web/views/service/add.py @@ -64,8 +64,8 @@ class AddView(BaseView): try: data = await self.extract_data(["packages"]) packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages") - except Exception as e: - raise HTTPBadRequest(reason=str(e)) + except Exception as ex: + raise HTTPBadRequest(reason=str(ex)) username = await self.username() process_id = self.spawner.packages_add(packages, username, now=True) diff --git a/src/ahriman/web/views/service/pgp.py b/src/ahriman/web/views/service/pgp.py index 9f596bea..cfb56d88 100644 --- a/src/ahriman/web/views/service/pgp.py +++ b/src/ahriman/web/views/service/pgp.py @@ -68,8 +68,8 @@ class PGPView(BaseView): try: key = self.get_non_empty(self.request.query.getone, "key") server = self.get_non_empty(self.request.query.getone, "server") - except Exception as e: - raise HTTPBadRequest(reason=str(e)) + except Exception as ex: + raise HTTPBadRequest(reason=str(ex)) try: key = self.service.repository.sign.key_download(server, key) @@ -107,8 +107,8 @@ class PGPView(BaseView): try: key = self.get_non_empty(data.get, "key") - except Exception as e: - raise HTTPBadRequest(reason=str(e)) + except Exception as ex: + raise HTTPBadRequest(reason=str(ex)) process_id = self.spawner.key_import(key, data.get("server")) diff --git a/src/ahriman/web/views/service/rebuild.py b/src/ahriman/web/views/service/rebuild.py index 4ae851c5..bf32beeb 100644 --- a/src/ahriman/web/views/service/rebuild.py +++ b/src/ahriman/web/views/service/rebuild.py @@ -65,8 +65,8 @@ class RebuildView(BaseView): data = await self.extract_data(["packages"]) packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages") depends_on = next(iter(packages)) - except Exception as e: - raise HTTPBadRequest(reason=str(e)) + except Exception as ex: + raise HTTPBadRequest(reason=str(ex)) username = await self.username() process_id = self.spawner.packages_rebuild(depends_on, username) diff --git a/src/ahriman/web/views/service/remove.py b/src/ahriman/web/views/service/remove.py index dcb4a699..b241cd0d 100644 --- a/src/ahriman/web/views/service/remove.py +++ b/src/ahriman/web/views/service/remove.py @@ -64,8 +64,8 @@ class RemoveView(BaseView): try: data = await self.extract_data(["packages"]) packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages") - except Exception as e: - raise HTTPBadRequest(reason=str(e)) + except Exception as ex: + raise HTTPBadRequest(reason=str(ex)) process_id = self.spawner.packages_remove(packages) diff --git a/src/ahriman/web/views/service/request.py b/src/ahriman/web/views/service/request.py index 2831c33b..f6dbfede 100644 --- a/src/ahriman/web/views/service/request.py +++ b/src/ahriman/web/views/service/request.py @@ -64,8 +64,8 @@ class RequestView(BaseView): try: data = await self.extract_data(["packages"]) packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages") - except Exception as e: - raise HTTPBadRequest(reason=str(e)) + except Exception as ex: + raise HTTPBadRequest(reason=str(ex)) username = await self.username() process_id = self.spawner.packages_add(packages, username, now=False) diff --git a/src/ahriman/web/views/service/search.py b/src/ahriman/web/views/service/search.py index ffa4273f..4c75c3fd 100644 --- a/src/ahriman/web/views/service/search.py +++ b/src/ahriman/web/views/service/search.py @@ -69,8 +69,8 @@ class SearchView(BaseView): try: search: list[str] = self.get_non_empty(lambda key: self.request.query.getall(key, default=[]), "for") packages = AUR.multisearch(*search, pacman=self.service.repository.pacman) - except Exception as e: - raise HTTPBadRequest(reason=str(e)) + except Exception as ex: + raise HTTPBadRequest(reason=str(ex)) if not packages: raise HTTPNotFound(reason=f"No packages found for terms: {search}") diff --git a/src/ahriman/web/views/service/update.py b/src/ahriman/web/views/service/update.py index 0ff278ac..944c04e2 100644 --- a/src/ahriman/web/views/service/update.py +++ b/src/ahriman/web/views/service/update.py @@ -63,8 +63,8 @@ class UpdateView(BaseView): """ try: data = await self.extract_data() - except Exception as e: - raise HTTPBadRequest(reason=str(e)) + except Exception as ex: + raise HTTPBadRequest(reason=str(ex)) username = await self.username() process_id = self.spawner.packages_update( diff --git a/src/ahriman/web/views/service/upload.py b/src/ahriman/web/views/service/upload.py index cae5953b..686e2c85 100644 --- a/src/ahriman/web/views/service/upload.py +++ b/src/ahriman/web/views/service/upload.py @@ -120,8 +120,8 @@ class UploadView(BaseView): try: reader = await self.request.multipart() - except Exception as e: - raise HTTPBadRequest(reason=str(e)) + except Exception as ex: + raise HTTPBadRequest(reason=str(ex)) max_body_size = self.configuration.getint("web", "max_body_size", fallback=None) target = self.configuration.repository_paths.packages diff --git a/src/ahriman/web/views/status/logs.py b/src/ahriman/web/views/status/logs.py index 188fd418..4659ae88 100644 --- a/src/ahriman/web/views/status/logs.py +++ b/src/ahriman/web/views/status/logs.py @@ -138,8 +138,8 @@ class LogsView(BaseView): created = data["created"] record = data["message"] version = data["version"] - except Exception as e: - raise HTTPBadRequest(reason=str(e)) + except Exception as ex: + raise HTTPBadRequest(reason=str(ex)) self.service.logs_update(LogRecordId(package_base, version), created, record) diff --git a/src/ahriman/web/views/status/package.py b/src/ahriman/web/views/status/package.py index e834e790..68c1da49 100644 --- a/src/ahriman/web/views/status/package.py +++ b/src/ahriman/web/views/status/package.py @@ -138,8 +138,8 @@ class PackageView(BaseView): try: package = Package.from_json(data["package"]) if "package" in data else None status = BuildStatusEnum(data["status"]) - except Exception as e: - raise HTTPBadRequest(reason=str(e)) + except Exception as ex: + raise HTTPBadRequest(reason=str(ex)) try: self.service.package_update(package_base, status, package) diff --git a/src/ahriman/web/views/status/status.py b/src/ahriman/web/views/status/status.py index 2de728a3..c30dcc41 100644 --- a/src/ahriman/web/views/status/status.py +++ b/src/ahriman/web/views/status/status.py @@ -99,8 +99,8 @@ class StatusView(BaseView): try: data = await self.extract_data() status = BuildStatusEnum(data["status"]) - except Exception as e: - raise HTTPBadRequest(reason=str(e)) + except Exception as ex: + raise HTTPBadRequest(reason=str(ex)) self.service.status_update(status) diff --git a/tests/ahriman/core/alpm/remote/test_aur.py b/tests/ahriman/core/alpm/remote/test_aur.py index af3b6d92..fc3b76a7 100644 --- a/tests/ahriman/core/alpm/remote/test_aur.py +++ b/tests/ahriman/core/alpm/remote/test_aur.py @@ -67,80 +67,73 @@ def test_remote_web_url(aur_package_ahriman: AURPackage) -> None: assert web_url.startswith(AUR.DEFAULT_AUR_URL) -def test_make_request(aur: AUR, aur_package_ahriman: AURPackage, - mocker: MockerFixture, resource_path_root: Path) -> None: +def test_aur_request(aur: AUR, aur_package_ahriman: AURPackage, + mocker: MockerFixture, resource_path_root: Path) -> None: """ must perform request to AUR """ response_mock = MagicMock() response_mock.json.return_value = json.loads(_get_response(resource_path_root)) - request_mock = mocker.patch("requests.get", return_value=response_mock) + request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.make_request", return_value=response_mock) - assert aur.make_request("info", "ahriman") == [aur_package_ahriman] + assert aur.aur_request("info", "ahriman") == [aur_package_ahriman] request_mock.assert_called_once_with( - "https://aur.archlinux.org/rpc", - params={"v": "5", "type": "info", "arg": ["ahriman"]}, - headers={"User-Agent": AUR.DEFAULT_USER_AGENT}, - timeout=aur.DEFAULT_TIMEOUT) + "GET", "https://aur.archlinux.org/rpc", + params=[("type", "info"), ("v", "5"), ("arg", "ahriman")]) -def test_make_request_multi_arg(aur: AUR, aur_package_ahriman: AURPackage, - mocker: MockerFixture, resource_path_root: Path) -> None: +def test_aur_request_multi_arg(aur: AUR, aur_package_ahriman: AURPackage, + mocker: MockerFixture, resource_path_root: Path) -> None: """ must perform request to AUR with multiple args """ response_mock = MagicMock() response_mock.json.return_value = json.loads(_get_response(resource_path_root)) - request_mock = mocker.patch("requests.get", return_value=response_mock) + request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.make_request", return_value=response_mock) - assert aur.make_request("search", "ahriman", "is", "cool") == [aur_package_ahriman] + assert aur.aur_request("search", "ahriman", "is", "cool") == [aur_package_ahriman] request_mock.assert_called_once_with( - "https://aur.archlinux.org/rpc", - params={"v": "5", "type": "search", "arg[]": ["ahriman", "is", "cool"]}, - headers={"User-Agent": AUR.DEFAULT_USER_AGENT}, - timeout=aur.DEFAULT_TIMEOUT) + "GET", "https://aur.archlinux.org/rpc", + params=[("type", "search"), ("v", "5"), ("arg[]", "ahriman"), ("arg[]", "is"), ("arg[]", "cool")]) -def test_make_request_with_kwargs(aur: AUR, aur_package_ahriman: AURPackage, - mocker: MockerFixture, resource_path_root: Path) -> None: +def test_aur_request_with_kwargs(aur: AUR, aur_package_ahriman: AURPackage, + mocker: MockerFixture, resource_path_root: Path) -> None: """ must perform request to AUR with named parameters """ response_mock = MagicMock() response_mock.json.return_value = json.loads(_get_response(resource_path_root)) - request_mock = mocker.patch("requests.get", return_value=response_mock) + request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.make_request", return_value=response_mock) - assert aur.make_request("search", "ahriman", by="name") == [aur_package_ahriman] + assert aur.aur_request("search", "ahriman", by="name") == [aur_package_ahriman] request_mock.assert_called_once_with( - "https://aur.archlinux.org/rpc", - params={"v": "5", "type": "search", "arg": ["ahriman"], "by": "name"}, - headers={"User-Agent": AUR.DEFAULT_USER_AGENT}, - timeout=aur.DEFAULT_TIMEOUT) + "GET", "https://aur.archlinux.org/rpc", + params=[("type", "search"), ("v", "5"), ("arg", "ahriman"), ("by", "name")]) -def test_make_request_failed(aur: AUR, mocker: MockerFixture) -> None: +def test_aur_request_failed(aur: AUR, mocker: MockerFixture) -> None: """ must reraise generic exception """ - mocker.patch("requests.get", side_effect=Exception()) + mocker.patch("requests.Session.request", side_effect=Exception()) with pytest.raises(Exception): - aur.make_request("info", "ahriman") + aur.aur_request("info", "ahriman") -def test_make_request_failed_http_error(aur: AUR, mocker: MockerFixture) -> None: +def test_aur_request_failed_http_error(aur: AUR, mocker: MockerFixture) -> None: + """ must reraise http exception """ - must reraise http exception - """ - mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError()) - with pytest.raises(requests.exceptions.HTTPError): - aur.make_request("info", "ahriman") + mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) + with pytest.raises(requests.HTTPError): + aur.aur_request("info", "ahriman") def test_package_info(aur: AUR, aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None: """ must make request for info """ - request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.make_request", return_value=[aur_package_ahriman]) + request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.aur_request", return_value=[aur_package_ahriman]) assert aur.package_info(aur_package_ahriman.name, pacman=pacman) == aur_package_ahriman request_mock.assert_called_once_with("info", aur_package_ahriman.name) @@ -150,7 +143,7 @@ def test_package_info_not_found(aur: AUR, aur_package_ahriman: AURPackage, pacma """ must raise UnknownPackage exception in case if no package was found """ - mocker.patch("ahriman.core.alpm.remote.AUR.make_request", return_value=[]) + mocker.patch("ahriman.core.alpm.remote.AUR.aur_request", return_value=[]) with pytest.raises(UnknownPackageError, match=aur_package_ahriman.name): assert aur.package_info(aur_package_ahriman.name, pacman=pacman) @@ -159,6 +152,6 @@ def test_package_search(aur: AUR, aur_package_ahriman: AURPackage, pacman: Pacma """ must make request for search """ - request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.make_request", return_value=[aur_package_ahriman]) + request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.aur_request", return_value=[aur_package_ahriman]) assert aur.package_search(aur_package_ahriman.name, pacman=pacman) == [aur_package_ahriman] request_mock.assert_called_once_with("search", aur_package_ahriman.name, by="name-desc") diff --git a/tests/ahriman/core/alpm/remote/test_official.py b/tests/ahriman/core/alpm/remote/test_official.py index ee467319..23a23d91 100644 --- a/tests/ahriman/core/alpm/remote/test_official.py +++ b/tests/ahriman/core/alpm/remote/test_official.py @@ -62,39 +62,37 @@ def test_remote_web_url(aur_package_akonadi: AURPackage) -> None: assert web_url.startswith(Official.DEFAULT_ARCHLINUX_URL) -def test_make_request(official: Official, aur_package_akonadi: AURPackage, +def test_arch_request(official: Official, aur_package_akonadi: AURPackage, mocker: MockerFixture, resource_path_root: Path) -> None: """ must perform request to official repositories """ response_mock = MagicMock() response_mock.json.return_value = json.loads(_get_response(resource_path_root)) - request_mock = mocker.patch("requests.get", return_value=response_mock) + request_mock = mocker.patch("ahriman.core.alpm.remote.Official.make_request", return_value=response_mock) - assert official.make_request("akonadi", by="q") == [aur_package_akonadi] + assert official.arch_request("akonadi", by="q") == [aur_package_akonadi] request_mock.assert_called_once_with( - "https://archlinux.org/packages/search/json", - params={"q": ("akonadi",), "repo": Official.DEFAULT_SEARCH_REPOSITORIES}, - headers={"User-Agent": Official.DEFAULT_USER_AGENT}, - timeout=official.DEFAULT_TIMEOUT) + "GET", "https://archlinux.org/packages/search/json", + params=[("repo", repository) for repository in Official.DEFAULT_SEARCH_REPOSITORIES] + [("q", "akonadi")]) -def test_make_request_failed(official: Official, mocker: MockerFixture) -> None: +def test_arch_request_failed(official: Official, mocker: MockerFixture) -> None: """ must reraise generic exception """ - mocker.patch("requests.get", side_effect=Exception()) + mocker.patch("requests.Session.request", side_effect=Exception()) with pytest.raises(Exception): - official.make_request("akonadi", by="q") + official.arch_request("akonadi", by="q") -def test_make_request_failed_http_error(official: Official, mocker: MockerFixture) -> None: +def test_arch_request_failed_http_error(official: Official, mocker: MockerFixture) -> None: """ must reraise http exception """ - mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError()) - with pytest.raises(requests.exceptions.HTTPError): - official.make_request("akonadi", by="q") + mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) + with pytest.raises(requests.HTTPError): + official.arch_request("akonadi", by="q") def test_package_info(official: Official, aur_package_akonadi: AURPackage, pacman: Pacman, @@ -102,7 +100,7 @@ def test_package_info(official: Official, aur_package_akonadi: AURPackage, pacma """ must make request for info """ - request_mock = mocker.patch("ahriman.core.alpm.remote.Official.make_request", + request_mock = mocker.patch("ahriman.core.alpm.remote.Official.arch_request", return_value=[aur_package_akonadi]) assert official.package_info(aur_package_akonadi.name, pacman=pacman) == aur_package_akonadi request_mock.assert_called_once_with(aur_package_akonadi.name, by="name") @@ -113,7 +111,7 @@ def test_package_info_not_found(official: Official, aur_package_ahriman: AURPack """ must raise UnknownPackage exception in case if no package was found """ - mocker.patch("ahriman.core.alpm.remote.Official.make_request", return_value=[]) + mocker.patch("ahriman.core.alpm.remote.Official.arch_request", return_value=[]) with pytest.raises(UnknownPackageError, match=aur_package_ahriman.name): assert official.package_info(aur_package_ahriman.name, pacman=pacman) @@ -123,7 +121,7 @@ def test_package_search(official: Official, aur_package_akonadi: AURPackage, pac """ must make request for search """ - request_mock = mocker.patch("ahriman.core.alpm.remote.Official.make_request", + request_mock = mocker.patch("ahriman.core.alpm.remote.Official.arch_request", return_value=[aur_package_akonadi]) assert official.package_search(aur_package_akonadi.name, pacman=pacman) == [aur_package_akonadi] request_mock.assert_called_once_with(aur_package_akonadi.name, by="q") diff --git a/tests/ahriman/core/http/test_sync_http_client.py b/tests/ahriman/core/http/test_sync_http_client.py new file mode 100644 index 00000000..9154102a --- /dev/null +++ b/tests/ahriman/core/http/test_sync_http_client.py @@ -0,0 +1,154 @@ +import pytest +import requests + +from pytest_mock import MockerFixture +from unittest.mock import MagicMock, call as MockCall + +from ahriman.core.configuration import Configuration +from ahriman.core.http import SyncHttpClient + + +def test_init() -> None: + """ + must init from empty parameters + """ + assert SyncHttpClient() + + +def test_init_auth(configuration: Configuration) -> None: + """ + must init with auth + """ + configuration.set_option("web", "username", "username") + configuration.set_option("web", "password", "password") + + assert SyncHttpClient("web", configuration).auth == ("username", "password") + assert SyncHttpClient(configuration=configuration).auth is None + + +def test_init_auth_empty() -> None: + """ + must init with empty auth + """ + assert SyncHttpClient().auth is None + + +def test_session() -> None: + """ + must generate valid session + """ + session = SyncHttpClient().session + assert "User-Agent" in session.headers + + +def test_exception_response_text() -> None: + """ + must parse HTTP response to string + """ + response_mock = MagicMock() + response_mock.text = "hello" + exception = requests.exceptions.HTTPError(response=response_mock) + + assert SyncHttpClient.exception_response_text(exception) == "hello" + + +def test_exception_response_text_empty() -> None: + """ + must parse HTTP exception with empty response to empty string + """ + exception = requests.exceptions.HTTPError(response=None) + assert SyncHttpClient.exception_response_text(exception) == "" + + +def test_make_request(mocker: MockerFixture) -> None: + """ + must make HTTP request + """ + request_mock = mocker.patch("requests.Session.request") + client = SyncHttpClient() + + assert client.make_request("GET", "url1") is not None + assert client.make_request("GET", "url2", params=[("param", "value")]) is not None + + assert client.make_request("POST", "url3") is not None + assert client.make_request("POST", "url4", json={"param": "value"}) is not None + assert client.make_request("POST", "url5", data={"param": "value"}) is not None + # we don't want to put full descriptor here + assert client.make_request("POST", "url6", files={"file": "tuple"}) is not None + + assert client.make_request("DELETE", "url7") is not None + + assert client.make_request("GET", "url8", headers={"user-agent": "ua"}) is not None + + auth = client.auth = ("username", "password") + assert client.make_request("GET", "url9") is not None + + request_mock.assert_has_calls([ + MockCall("GET", "url1", params=None, data=None, headers=None, files=None, json=None, + auth=None, timeout=client.timeout), + MockCall().raise_for_status(), + MockCall("GET", "url2", params=[("param", "value")], data=None, headers=None, files=None, json=None, + auth=None, timeout=client.timeout), + MockCall().raise_for_status(), + MockCall("POST", "url3", params=None, data=None, headers=None, files=None, json=None, + auth=None, timeout=client.timeout), + MockCall().raise_for_status(), + MockCall("POST", "url4", params=None, data=None, headers=None, files=None, json={"param": "value"}, + auth=None, timeout=client.timeout), + MockCall().raise_for_status(), + MockCall("POST", "url5", params=None, data={"param": "value"}, headers=None, files=None, json=None, + auth=None, timeout=client.timeout), + MockCall().raise_for_status(), + MockCall("POST", "url6", params=None, data=None, headers=None, files={"file": "tuple"}, json=None, + auth=None, timeout=client.timeout), + MockCall().raise_for_status(), + MockCall("DELETE", "url7", params=None, data=None, headers=None, files=None, json=None, + auth=None, timeout=client.timeout), + MockCall().raise_for_status(), + MockCall("GET", "url8", params=None, data=None, headers={"user-agent": "ua"}, files=None, json=None, + auth=None, timeout=client.timeout), + MockCall().raise_for_status(), + MockCall("GET", "url9", params=None, data=None, headers=None, files=None, json=None, + auth=auth, timeout=client.timeout), + MockCall().raise_for_status(), + ]) + + +def test_make_request_failed(mocker: MockerFixture) -> None: + """ + must process request errors + """ + mocker.patch("requests.Session.request", side_effect=Exception()) + logging_mock = mocker.patch("logging.Logger.exception") + + with pytest.raises(Exception): + SyncHttpClient().make_request("GET", "url") + logging_mock.assert_called_once() # we do not check logging arguments + + +def test_make_request_suppress_errors(mocker: MockerFixture) -> None: + """ + must suppress request errors correctly + """ + mocker.patch("requests.Session.request", side_effect=Exception()) + logging_mock = mocker.patch("logging.Logger.exception") + + with pytest.raises(Exception): + SyncHttpClient().make_request("GET", "url", suppress_errors=True) + with pytest.raises(Exception): + SyncHttpClient(suppress_errors=True).make_request("GET", "url") + + logging_mock.assert_not_called() + + +def test_make_request_session() -> None: + """ + must use session from arguments + """ + session_mock = MagicMock() + client = SyncHttpClient() + + client.make_request("GET", "url", session=session_mock) + session_mock.request.assert_called_once_with( + "GET", "url", params=None, data=None, headers=None, files=None, json=None, + auth=None, timeout=client.timeout) diff --git a/tests/ahriman/core/report/test_remote_call.py b/tests/ahriman/core/report/test_remote_call.py index 117129b9..cd669eca 100644 --- a/tests/ahriman/core/report/test_remote_call.py +++ b/tests/ahriman/core/report/test_remote_call.py @@ -40,7 +40,7 @@ def test_is_process_alive_unknown(remote_call: RemoteCall, mocker: MockerFixture response = requests.Response() response.status_code = 404 mocker.patch("ahriman.core.status.web_client.WebClient.make_request", - side_effect=requests.RequestException(response=response)) + side_effect=requests.HTTPError(response=response)) assert not remote_call.is_process_alive("id") @@ -62,9 +62,9 @@ def test_is_process_alive_http_error(remote_call: RemoteCall, mocker: MockerFixt response = requests.Response() response.status_code = 500 mocker.patch("ahriman.core.status.web_client.WebClient.make_request", - side_effect=requests.RequestException(response=response)) + side_effect=requests.HTTPError(response=response)) - with pytest.raises(requests.RequestException): + with pytest.raises(requests.HTTPError): remote_call.is_process_alive("id") diff --git a/tests/ahriman/core/report/test_telegram.py b/tests/ahriman/core/report/test_telegram.py index f6e9ffde..dc86641b 100644 --- a/tests/ahriman/core/report/test_telegram.py +++ b/tests/ahriman/core/report/test_telegram.py @@ -14,35 +14,35 @@ def test_send(configuration: Configuration, mocker: MockerFixture) -> None: """ must send a message """ - request_mock = mocker.patch("requests.post") + request_mock = mocker.patch("ahriman.core.report.telegram.Telegram.make_request") report = Telegram("x86_64", configuration, "telegram") report._send("a text") request_mock.assert_called_once_with( + "POST", pytest.helpers.anyvar(str, strict=True), - data={"chat_id": pytest.helpers.anyvar(str, strict=True), "text": "a text", "parse_mode": "HTML"}, - timeout=report.timeout) + data={"chat_id": pytest.helpers.anyvar(str, strict=True), "text": "a text", "parse_mode": "HTML"}) def test_send_failed(configuration: Configuration, mocker: MockerFixture) -> None: """ must reraise generic exception """ - mocker.patch("requests.post", side_effect=Exception()) + mocker.patch("requests.Session.request", side_effect=Exception()) report = Telegram("x86_64", configuration, "telegram") with pytest.raises(Exception): report._send("a text") -def test_make_request_failed_http_error(configuration: Configuration, mocker: MockerFixture) -> None: +def test_send_failed_http_error(configuration: Configuration, mocker: MockerFixture) -> None: """ must reraise http exception """ - mocker.patch("requests.post", side_effect=requests.exceptions.HTTPError()) + mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) report = Telegram("x86_64", configuration, "telegram") - with pytest.raises(requests.exceptions.HTTPError): + with pytest.raises(requests.HTTPError): report._send("a text") diff --git a/tests/ahriman/core/sign/test_gpg.py b/tests/ahriman/core/sign/test_gpg.py index 0ad8e411..802698c7 100644 --- a/tests/ahriman/core/sign/test_gpg.py +++ b/tests/ahriman/core/sign/test_gpg.py @@ -87,20 +87,19 @@ def test_key_download(gpg: GPG, mocker: MockerFixture) -> None: """ must download the key from public server """ - requests_mock = mocker.patch("requests.get") + requests_mock = mocker.patch("ahriman.core.sign.gpg.GPG.make_request") gpg.key_download("keyserver.ubuntu.com", "0xE989490C") requests_mock.assert_called_once_with( - "https://keyserver.ubuntu.com/pks/lookup", - params={"op": "get", "options": "mr", "search": "0xE989490C"}, - timeout=gpg.DEFAULT_TIMEOUT) + "GET", "https://keyserver.ubuntu.com/pks/lookup", + params=[("op", "get"), ("options", "mr"), ("search", "0xE989490C")]) def test_key_download_failure(gpg: GPG, mocker: MockerFixture) -> None: """ must download the key from public server and log error if any (and raise it again) """ - mocker.patch("requests.get", side_effect=requests.exceptions.HTTPError()) - with pytest.raises(requests.exceptions.HTTPError): + mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) + with pytest.raises(requests.HTTPError): gpg.key_download("keyserver.ubuntu.com", "0xE989490C") diff --git a/tests/ahriman/core/status/test_web_client.py b/tests/ahriman/core/status/test_web_client.py index f16687a3..04223b4b 100644 --- a/tests/ahriman/core/status/test_web_client.py +++ b/tests/ahriman/core/status/test_web_client.py @@ -5,7 +5,6 @@ import requests import requests_unixsocket from pytest_mock import MockerFixture -from unittest.mock import call as MockCall from ahriman.core.configuration import Configuration from ahriman.core.status.web_client import WebClient @@ -16,35 +15,6 @@ from ahriman.models.package import Package from ahriman.models.user import User -def test_login_url(web_client: WebClient) -> None: - """ - must generate login url correctly - """ - assert web_client._login_url.endswith("/api/v1/login") - - -def test_status_url(web_client: WebClient) -> None: - """ - must generate package status url correctly - """ - assert web_client._status_url.endswith("/api/v1/status") - - -def test_logs_url(web_client: WebClient, package_ahriman: Package) -> None: - """ - must generate logs url correctly - """ - assert web_client._logs_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}/logs") - - -def test_package_url(web_client: WebClient, package_ahriman: Package) -> None: - """ - must generate package status url correctly - """ - assert web_client._package_url("").endswith("/api/v1/packages") - assert web_client._package_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}") - - def test_parse_address(configuration: Configuration) -> None: """ must extract address correctly @@ -87,16 +57,16 @@ def test_login(web_client: WebClient, user: User, mocker: MockerFixture) -> None """ must login user """ - web_client.user = user - requests_mock = mocker.patch("requests.Session.request") + web_client.auth = (user.username, user.password) + requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request") payload = { "username": user.username, "password": user.password } + session = requests.Session() - web_client._login(requests.Session()) - requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), - params=None, json=payload, files=None) + web_client._login(session) + requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json=payload, session=session) def test_login_failed(web_client: WebClient, user: User, mocker: MockerFixture) -> None: @@ -113,7 +83,7 @@ def test_login_failed_http_error(web_client: WebClient, user: User, mocker: Mock must suppress HTTP exception happened during login """ web_client.user = user - mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) + mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) web_client._login(requests.Session()) @@ -126,57 +96,50 @@ def test_login_skip(web_client: WebClient, mocker: MockerFixture) -> None: requests_mock.assert_not_called() -def test_make_request(web_client: WebClient, mocker: MockerFixture) -> None: +def test_login_url(web_client: WebClient) -> None: """ - must make HTTP request + must generate login url correctly """ - request_mock = mocker.patch("requests.Session.request") - - assert web_client.make_request("GET", "/url1") is not None - assert web_client.make_request("GET", "/url2", params=[("param", "value")]) is not None - - assert web_client.make_request("POST", "/url3") is not None - assert web_client.make_request("POST", "/url4", json={"param": "value"}) is not None - # we don't want to put full descriptor here - assert web_client.make_request("POST", "/url5", files={"file": "tuple"}) is not None - - assert web_client.make_request("DELETE", "/url6") is not None - - request_mock.assert_has_calls([ - MockCall("GET", f"{web_client.address}/url1", params=None, json=None, files=None), - MockCall().raise_for_status(), - MockCall("GET", f"{web_client.address}/url2", params=[("param", "value")], json=None, files=None), - MockCall().raise_for_status(), - MockCall("POST", f"{web_client.address}/url3", params=None, json=None, files=None), - MockCall().raise_for_status(), - MockCall("POST", f"{web_client.address}/url4", params=None, json={"param": "value"}, files=None), - MockCall().raise_for_status(), - MockCall("POST", f"{web_client.address}/url5", params=None, json=None, files={"file": "tuple"}), - MockCall().raise_for_status(), - MockCall("DELETE", f"{web_client.address}/url6", params=None, json=None, files=None), - MockCall().raise_for_status(), - ]) + assert web_client._login_url().startswith(web_client.address) + assert web_client._login_url().endswith("/api/v1/login") -def test_make_request_failed(web_client: WebClient, mocker: MockerFixture) -> None: +def test_status_url(web_client: WebClient) -> None: """ - must make HTTP request + must generate package status url correctly """ - mocker.patch("requests.Session.request", side_effect=Exception()) - with pytest.raises(Exception): - web_client.make_request("GET", "url") + assert web_client._status_url().startswith(web_client.address) + assert web_client._status_url().endswith("/api/v1/status") + + +def test_logs_url(web_client: WebClient, package_ahriman: Package) -> None: + """ + must generate logs url correctly + """ + assert web_client._logs_url(package_ahriman.base).startswith(web_client.address) + assert web_client._logs_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}/logs") + + +def test_package_url(web_client: WebClient, package_ahriman: Package) -> None: + """ + must generate package status url correctly + """ + assert web_client._package_url("").startswith(web_client.address) + assert web_client._package_url("").endswith("/api/v1/packages") + + assert web_client._package_url(package_ahriman.base).startswith(web_client.address) + assert web_client._package_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}") def test_package_add(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: """ must process package addition """ - requests_mock = mocker.patch("requests.Session.request") + requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request") payload = pytest.helpers.get_package_status(package_ahriman) web_client.package_add(package_ahriman, BuildStatusEnum.Unknown) - requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), - params=None, json=payload, files=None) + requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json=payload) def test_package_add_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: @@ -191,7 +154,7 @@ def test_package_add_failed_http_error(web_client: WebClient, package_ahriman: P """ must suppress HTTP exception happened during addition """ - mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) + mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) web_client.package_add(package_ahriman, BuildStatusEnum.Unknown) @@ -213,7 +176,7 @@ def test_package_add_failed_http_error_suppress(web_client: WebClient, package_a must suppress HTTP exception happened during addition and don't log """ web_client.suppress_errors = True - mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) + mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) logging_mock = mocker.patch("logging.exception") web_client.package_add(package_ahriman, BuildStatusEnum.Unknown) @@ -229,11 +192,11 @@ def test_package_get_all(web_client: WebClient, package_ahriman: Package, mocker response_obj._content = json.dumps(response).encode("utf8") response_obj.status_code = 200 - requests_mock = mocker.patch("requests.Session.request", return_value=response_obj) + requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request", + return_value=response_obj) result = web_client.package_get(None) - requests_mock.assert_called_once_with("GET", f"{web_client.address}{web_client._package_url()}", - params=None, json=None, files=None) + requests_mock.assert_called_once_with("GET", web_client._package_url()) assert len(result) == len(response) assert (package_ahriman, BuildStatusEnum.Unknown) in [(package, status.status) for package, status in result] @@ -250,7 +213,7 @@ def test_package_get_failed_http_error(web_client: WebClient, mocker: MockerFixt """ must suppress HTTP exception happened during status getting """ - mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) + mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) assert web_client.package_get(None) == [] @@ -263,12 +226,11 @@ def test_package_get_single(web_client: WebClient, package_ahriman: Package, moc response_obj._content = json.dumps(response).encode("utf8") response_obj.status_code = 200 - requests_mock = mocker.patch("requests.Session.request", return_value=response_obj) + requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request", + return_value=response_obj) result = web_client.package_get(package_ahriman.base) - requests_mock.assert_called_once_with("GET", - f"{web_client.address}{web_client._package_url(package_ahriman.base)}", - params=None, json=None, files=None) + requests_mock.assert_called_once_with("GET", web_client._package_url(package_ahriman.base)) assert len(result) == len(response) assert (package_ahriman, BuildStatusEnum.Unknown) in [(package, status.status) for package, status in result] @@ -278,7 +240,7 @@ def test_package_logs(web_client: WebClient, log_record: logging.LogRecord, pack """ must process log record """ - requests_mock = mocker.patch("requests.Session.request") + requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request") payload = { "created": log_record.created, "message": log_record.getMessage(), @@ -286,8 +248,8 @@ def test_package_logs(web_client: WebClient, log_record: logging.LogRecord, pack } web_client.package_logs(LogRecordId(package_ahriman.base, package_ahriman.version), log_record) - requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), - params=None, json=payload, files=None) + requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json=payload, + suppress_errors=True) def test_package_logs_failed(web_client: WebClient, log_record: logging.LogRecord, package_ahriman: Package, @@ -306,7 +268,7 @@ def test_package_logs_failed_http_error(web_client: WebClient, log_record: loggi """ must pass exception during log post """ - mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) + mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) log_record.package_base = package_ahriman.base with pytest.raises(Exception): web_client.package_logs(LogRecordId(package_ahriman.base, package_ahriman.version), log_record) @@ -316,11 +278,10 @@ def test_package_remove(web_client: WebClient, package_ahriman: Package, mocker: """ must process package removal """ - requests_mock = mocker.patch("requests.Session.request") + requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request") web_client.package_remove(package_ahriman.base) - requests_mock.assert_called_once_with("DELETE", pytest.helpers.anyvar(str, True), - params=None, json=None, files=None) + requests_mock.assert_called_once_with("DELETE", pytest.helpers.anyvar(str, True)) def test_package_remove_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: @@ -336,7 +297,7 @@ def test_package_remove_failed_http_error(web_client: WebClient, package_ahriman """ must suppress HTTP exception happened during removal """ - mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) + mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) web_client.package_remove(package_ahriman.base) @@ -344,12 +305,12 @@ def test_package_update(web_client: WebClient, package_ahriman: Package, mocker: """ must process package update """ - requests_mock = mocker.patch("requests.Session.request") + requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request") web_client.package_update(package_ahriman.base, BuildStatusEnum.Unknown) - requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), params=None, json={ + requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json={ "status": BuildStatusEnum.Unknown.value - }, files=None) + }) def test_package_update_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: @@ -365,7 +326,7 @@ def test_package_update_failed_http_error(web_client: WebClient, package_ahriman """ must suppress HTTP exception happened during update """ - mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) + mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) web_client.package_update(package_ahriman.base, BuildStatusEnum.Unknown) @@ -378,11 +339,11 @@ def test_status_get(web_client: WebClient, mocker: MockerFixture) -> None: response_obj._content = json.dumps(status.view()).encode("utf8") response_obj.status_code = 200 - requests_mock = mocker.patch("requests.Session.request", return_value=response_obj) + requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request", + return_value=response_obj) result = web_client.status_get() - requests_mock.assert_called_once_with("GET", f"{web_client.address}{web_client._status_url}", - params=None, json=None, files=None) + requests_mock.assert_called_once_with("GET", web_client._status_url()) assert result.architecture == "x86_64" @@ -398,7 +359,7 @@ def test_status_get_failed_http_error(web_client: WebClient, mocker: MockerFixtu """ must suppress HTTP exception happened during web service status getting """ - mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) + mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) assert web_client.status_get().architecture is None @@ -406,12 +367,12 @@ def test_status_update(web_client: WebClient, mocker: MockerFixture) -> None: """ must process service update """ - requests_mock = mocker.patch("requests.Session.request") + requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request") web_client.status_update(BuildStatusEnum.Unknown) - requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), params=None, json={ + requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json={ "status": BuildStatusEnum.Unknown.value - }, files=None) + }) def test_status_update_self_failed(web_client: WebClient, mocker: MockerFixture) -> None: @@ -426,5 +387,5 @@ def test_status_update_failed_http_error(web_client: WebClient, mocker: MockerFi """ must suppress HTTP exception happened during service update """ - mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError()) + mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) web_client.status_update(BuildStatusEnum.Unknown) diff --git a/tests/ahriman/core/test_util.py b/tests/ahriman/core/test_util.py index 7524bf9a..13315d65 100644 --- a/tests/ahriman/core/test_util.py +++ b/tests/ahriman/core/test_util.py @@ -10,9 +10,9 @@ from typing import Any from unittest.mock import MagicMock, call as MockCall from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError -from ahriman.core.util import check_output, check_user, dataclass_view, enum_values, exception_response_text, \ - extract_user, filter_json, full_version, package_like, parse_version, partition, pretty_datetime, pretty_size, \ - safe_filename, srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk +from ahriman.core.util import check_output, check_user, dataclass_view, enum_values, extract_user, filter_json, \ + full_version, package_like, parse_version, partition, pretty_datetime, pretty_size, safe_filename, \ + srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk from ahriman.models.package import Package from ahriman.models.package_source import PackageSource from ahriman.models.repository_paths import RepositoryPaths @@ -204,25 +204,6 @@ def test_dataclass_view_without_none(package_ahriman: Package) -> None: assert Package.from_json(result) == package_ahriman -def test_exception_response_text() -> None: - """ - must parse HTTP response to string - """ - response_mock = MagicMock() - response_mock.text = "hello" - exception = requests.exceptions.HTTPError(response=response_mock) - - assert exception_response_text(exception) == "hello" - - -def test_exception_response_text_empty() -> None: - """ - must parse HTTP exception with empty response to empty string - """ - exception = requests.exceptions.HTTPError(response=None) - assert exception_response_text(exception) == "" - - def test_extract_user() -> None: """ must extract user from system environment diff --git a/tests/ahriman/core/upload/test_github.py b/tests/ahriman/core/upload/test_github.py index dbf5d5fb..477f8751 100644 --- a/tests/ahriman/core/upload/test_github.py +++ b/tests/ahriman/core/upload/test_github.py @@ -13,7 +13,7 @@ def test_asset_remove(github: Github, github_release: dict[str, Any], mocker: Mo """ must remove asset from the release """ - request_mock = mocker.patch("ahriman.core.upload.github.Github._request") + request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request") github.asset_remove(github_release, "asset_name") request_mock.assert_called_once_with("DELETE", "asset_url") @@ -22,7 +22,7 @@ def test_asset_remove_unknown(github: Github, github_release: dict[str, Any], mo """ must not fail if no asset found """ - request_mock = mocker.patch("ahriman.core.upload.github.Github._request") + request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request") github.asset_remove(github_release, "unknown_asset_name") request_mock.assert_not_called() @@ -32,11 +32,11 @@ def test_asset_upload(github: Github, github_release: dict[str, Any], mocker: Mo must upload asset to the repository """ mocker.patch("pathlib.Path.open") - request_mock = mocker.patch("ahriman.core.upload.github.Github._request") + request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request") remove_mock = mocker.patch("ahriman.core.upload.github.Github.asset_remove") github.asset_upload(github_release, Path("/root/new.tar.xz")) - request_mock.assert_called_once_with("POST", "upload_url", params={"name": "new.tar.xz"}, + request_mock.assert_called_once_with("POST", "upload_url", params=[("name", "new.tar.xz")], data=pytest.helpers.anyvar(int), headers={"Content-Type": "application/x-tar"}) remove_mock.assert_not_called() @@ -47,7 +47,7 @@ def test_asset_upload_with_removal(github: Github, github_release: dict[str, Any must remove existing file before upload """ mocker.patch("pathlib.Path.open") - mocker.patch("ahriman.core.upload.github.Github._request") + mocker.patch("ahriman.core.upload.github.Github.make_request") remove_mock = mocker.patch("ahriman.core.upload.github.Github.asset_remove") github.asset_upload(github_release, Path("asset_name")) @@ -65,10 +65,10 @@ def test_asset_upload_empty_mimetype(github: Github, github_release: dict[str, A mocker.patch("pathlib.Path.open") mocker.patch("ahriman.core.upload.github.Github.asset_remove") mocker.patch("mimetypes.guess_type", return_value=(None, None)) - request_mock = mocker.patch("ahriman.core.upload.github.Github._request") + request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request") github.asset_upload(github_release, Path("/root/new.tar.xz")) - request_mock.assert_called_once_with("POST", "upload_url", params={"name": "new.tar.xz"}, + request_mock.assert_called_once_with("POST", "upload_url", params=[("name", "new.tar.xz")], data=pytest.helpers.anyvar(int), headers={"Content-Type": "application/octet-stream"}) @@ -125,7 +125,7 @@ def test_release_create(github: Github, mocker: MockerFixture) -> None: """ must create release """ - request_mock = mocker.patch("ahriman.core.upload.github.Github._request") + request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request") github.release_create() request_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json={"tag_name": github.architecture, "name": github.architecture}) @@ -135,7 +135,7 @@ def test_release_get(github: Github, mocker: MockerFixture) -> None: """ must get release """ - request_mock = mocker.patch("ahriman.core.upload.github.Github._request") + request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request") github.release_get() request_mock.assert_called_once_with("GET", pytest.helpers.anyvar(str, True)) @@ -146,7 +146,8 @@ def test_release_get_empty(github: Github, mocker: MockerFixture) -> None: """ response = requests.Response() response.status_code = 404 - mocker.patch("ahriman.core.upload.github.Github._request", side_effect=requests.HTTPError(response=response)) + mocker.patch("ahriman.core.upload.github.Github.make_request", + side_effect=requests.HTTPError(response=response)) assert github.release_get() is None @@ -154,7 +155,7 @@ def test_release_get_exception(github: Github, mocker: MockerFixture) -> None: """ must re-raise non HTTPError exception """ - mocker.patch("ahriman.core.upload.github.Github._request", side_effect=Exception()) + mocker.patch("ahriman.core.upload.github.Github.make_request", side_effect=Exception()) with pytest.raises(Exception): github.release_get() @@ -164,7 +165,7 @@ def test_release_get_exception_http_error(github: Github, mocker: MockerFixture) must re-raise HTTPError exception with code differs from 404 """ exception = requests.HTTPError(response=requests.Response()) - mocker.patch("ahriman.core.upload.github.Github._request", side_effect=exception) + mocker.patch("ahriman.core.upload.github.Github.make_request", side_effect=exception) with pytest.raises(requests.HTTPError): github.release_get() @@ -173,7 +174,7 @@ def test_release_update(github: Github, github_release: dict[str, Any], mocker: """ must update release """ - request_mock = mocker.patch("ahriman.core.upload.github.Github._request") + request_mock = mocker.patch("ahriman.core.upload.github.Github.make_request") github.release_update(github_release, "body") request_mock.assert_called_once_with("POST", "release_url", json={"body": "body"}) diff --git a/tests/ahriman/core/upload/test_http_upload.py b/tests/ahriman/core/upload/test_http_upload.py index 07dbb23f..9eae379c 100644 --- a/tests/ahriman/core/upload/test_http_upload.py +++ b/tests/ahriman/core/upload/test_http_upload.py @@ -1,11 +1,5 @@ -import pytest -import requests - from pathlib import Path -from pytest_mock import MockerFixture -from unittest.mock import MagicMock -from ahriman.core.upload.github import Github from ahriman.core.upload.http_upload import HttpUpload @@ -40,24 +34,3 @@ def test_get_hashes_empty() -> None: must read empty body """ assert HttpUpload.get_hashes("") == {} - - -def test_request(github: Github, mocker: MockerFixture) -> None: - """ - must call request method - """ - response_mock = MagicMock() - request_mock = mocker.patch("requests.Session.request", return_value=response_mock) - - github._request("GET", "url", arg="arg") - request_mock.assert_called_once_with("GET", "url", auth=github.auth, timeout=github.timeout, arg="arg") - response_mock.raise_for_status.assert_called_once_with() - - -def test_request_exception(github: Github, mocker: MockerFixture) -> None: - """ - must call request method and log HTTPError exception - """ - mocker.patch("requests.Session.request", side_effect=requests.HTTPError()) - with pytest.raises(requests.HTTPError): - github._request("GET", "url", arg="arg") diff --git a/tests/ahriman/core/upload/test_remote_service.py b/tests/ahriman/core/upload/test_remote_service.py index 84fac22c..269187b8 100644 --- a/tests/ahriman/core/upload/test_remote_service.py +++ b/tests/ahriman/core/upload/test_remote_service.py @@ -24,7 +24,7 @@ def test_package_upload(remote_service: RemoteService, package_ahriman: Package, mocker.patch("pathlib.Path.is_file", return_value=False) file_mock = MagicMock() open_mock = mocker.patch("pathlib.Path.open", return_value=file_mock) - upload_mock = mocker.patch("ahriman.core.upload.http_upload.HttpUpload._request") + upload_mock = mocker.patch("ahriman.core.upload.http_upload.HttpUpload.make_request") filename = package_ahriman.packages[package_ahriman.base].filename remote_service.sync(Path("local"), [package_ahriman]) @@ -43,7 +43,7 @@ def test_package_upload_with_signature(remote_service: RemoteService, package_ah mocker.patch("pathlib.Path.is_file", return_value=True) file_mock = MagicMock() open_mock = mocker.patch("pathlib.Path.open", return_value=file_mock) - upload_mock = mocker.patch("ahriman.core.upload.http_upload.HttpUpload._request") + upload_mock = mocker.patch("ahriman.core.upload.http_upload.HttpUpload.make_request") filename = package_ahriman.packages[package_ahriman.base].filename remote_service.sync(Path("local"), [package_ahriman]) diff --git a/tests/ahriman/models/test_user.py b/tests/ahriman/models/test_user.py index 43015829..7f4de565 100644 --- a/tests/ahriman/models/test_user.py +++ b/tests/ahriman/models/test_user.py @@ -4,27 +4,6 @@ from ahriman.models.user import User from ahriman.models.user_access import UserAccess -def test_from_option(user: User) -> None: - """ - must generate user from options - """ - user = replace(user, access=UserAccess.Read, packager_id=None, key=None) - assert User.from_option(user.username, user.password) == user - # default is read access - user = replace(user, access=UserAccess.Full) - assert User.from_option(user.username, user.password) != user - assert User.from_option(user.username, user.password, user.access) == user - - -def test_from_option_empty() -> None: - """ - must return nothing if settings are missed - """ - assert User.from_option(None, "") is None - assert User.from_option("", None) is None - assert User.from_option(None, None) is None - - def test_check_credentials_hash_password(user: User) -> None: """ must generate and validate user password