use http client class for all http requests

This commit is contained in:
Evgenii Alekseev 2023-08-23 03:27:42 +03:00
parent 6dfe1b92f2
commit 33e68a59e2
40 changed files with 603 additions and 570 deletions

View File

@ -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
^^^^^^^^^^^^^^

View File

@ -17,14 +17,11 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import 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()
response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query)
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
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")

View File

@ -17,14 +17,11 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import 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()
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())
except requests.HTTPError as e:
self.logger.exception("could not perform request: %s", exception_response_text(e))
raise
except Exception:
self.logger.exception("could not perform request")
raise
def 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")

View File

@ -17,19 +17,15 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from 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:
"""

View File

@ -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 <http://www.gnu.org/licenses/>.
#
from ahriman.core.http.sync_http_client import MultipartType, SyncHttpClient

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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

View File

@ -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

View File

@ -17,17 +17,15 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import 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:
"""

View File

@ -17,30 +17,26 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import 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:

View File

@ -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)

View File

@ -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:
"""

View File

@ -18,24 +18,17 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
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

View File

@ -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()

View File

@ -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:

View File

@ -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

View File

@ -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:
"""

View File

@ -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

View File

@ -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)

View File

@ -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"))

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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}")

View File

@ -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(

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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,
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,
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,
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")

View File

@ -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")

View File

@ -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)

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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)

View File

@ -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

View File

@ -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"})

View File

@ -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")

View File

@ -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])

View File

@ -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