restore behavior of the httploghandler

This commit is contained in:
Evgenii Alekseev 2023-08-20 02:38:39 +03:00
parent c83936d4d9
commit 7d7fd691b4
12 changed files with 174 additions and 101 deletions

View File

@ -20,6 +20,14 @@ ahriman.core.configuration.schema module
:no-undoc-members:
:show-inheritance:
ahriman.core.configuration.shell\_interpolator module
-----------------------------------------------------
.. automodule:: ahriman.core.configuration.shell_interpolator
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.configuration.validator module
-------------------------------------------

View File

@ -84,6 +84,14 @@ ahriman.core.database.migrations.m009\_local\_source module
:no-undoc-members:
:show-inheritance:
ahriman.core.database.migrations.m010\_version\_based\_logs\_removal module
---------------------------------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m010_version_based_logs_removal
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -58,11 +58,11 @@ _shtab_ahriman_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_repo_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_service_key_import_option_strings=('-h' '--help' '--key-server')
_shtab_ahriman_key_import_option_strings=('-h' '--help' '--key-server')
_shtab_ahriman_service_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_init_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_repo_init_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_repo_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_service_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_init_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_repo_init_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_repo_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_service_shell_option_strings=('-h' '--help')
_shtab_ahriman_shell_option_strings=('-h' '--help')
_shtab_ahriman_user_add_option_strings=('-h' '--help' '--key' '--packager' '-p' '--password' '-r' '--role')

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2023\-08\-13" "ahriman" "Generated Python Manual"
.TH AHRIMAN "1" "2023\-08\-19" "ahriman" "Generated Python Manual"
.SH NAME
ahriman
.SH SYNOPSIS
@ -689,7 +689,7 @@ key server for key import
usage: ahriman service\-setup [\-h] [\-\-build\-as\-user BUILD_AS_USER] [\-\-build\-command BUILD_COMMAND]
[\-\-from\-configuration FROM_CONFIGURATION] [\-\-generate\-salt | \-\-no\-generate\-salt]
[\-\-makeflags\-jobs | \-\-no\-makeflags\-jobs] [\-\-mirror MIRROR] [\-\-multilib | \-\-no\-multilib]
\-\-packager PACKAGER \-\-repository REPOSITORY [\-\-sign\-key SIGN_KEY]
\-\-packager PACKAGER \-\-repository REPOSITORY [\-\-server SERVER] [\-\-sign\-key SIGN_KEY]
[\-\-sign\-target {disabled,packages,repository}] [\-\-web\-port WEB_PORT]
[\-\-web\-unix\-socket WEB_UNIX_SOCKET]
@ -732,6 +732,10 @@ packager name and email
\fB\-\-repository\fR \fI\,REPOSITORY\/\fR
repository name
.TP
\fB\-\-server\fR \fI\,SERVER\/\fR
server to be used for devtools. If none set, local files will be used
.TP
\fB\-\-sign\-key\fR \fI\,SIGN_KEY\/\fR
sign key id

View File

@ -177,6 +177,7 @@ _shtab_ahriman_init_options=(
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
"--packager[packager name and email (default\: None)]:packager:"
"--repository[repository name (default\: None)]:repository:"
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
"--sign-key[sign key id (default\: None)]:sign_key:"
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
"--web-port[port of the web service (default\: None)]:web_port:"
@ -347,6 +348,7 @@ _shtab_ahriman_repo_init_options=(
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
"--packager[packager name and email (default\: None)]:packager:"
"--repository[repository name (default\: None)]:repository:"
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
"--sign-key[sign key id (default\: None)]:sign_key:"
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
"--web-port[port of the web service (default\: None)]:web_port:"
@ -390,6 +392,7 @@ _shtab_ahriman_repo_setup_options=(
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
"--packager[packager name and email (default\: None)]:packager:"
"--repository[repository name (default\: None)]:repository:"
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
"--sign-key[sign key id (default\: None)]:sign_key:"
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
"--web-port[port of the web service (default\: None)]:web_port:"
@ -482,6 +485,7 @@ _shtab_ahriman_service_setup_options=(
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
"--packager[packager name and email (default\: None)]:packager:"
"--repository[repository name (default\: None)]:repository:"
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
"--sign-key[sign key id (default\: None)]:sign_key:"
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
"--web-port[port of the web service (default\: None)]:web_port:"
@ -504,6 +508,7 @@ _shtab_ahriman_setup_options=(
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
"--packager[packager name and email (default\: None)]:packager:"
"--repository[repository name (default\: None)]:repository:"
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
"--sign-key[sign key id (default\: None)]:sign_key:"
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
"--web-port[port of the web service (default\: None)]:web_port:"

View File

@ -66,9 +66,9 @@ class LogsOperations(Operations):
connection.execute(
"""
insert into logs
(package_base, created, version, record)
(package_base, version, created, record)
values
(:package_base, :created, :version, :record)
(:package_base, :version, :created, :record)
""",
{
"package_base": log_record_id.package_base,

View File

@ -31,15 +31,17 @@ class HttpLogHandler(logging.Handler):
Attributes:
reporter(Client): build status reporter instance
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
"""
def __init__(self, configuration: Configuration, *, report: bool) -> None:
def __init__(self, configuration: Configuration, *, report: bool, suppress_errors: bool) -> None:
"""
default constructor
Args:
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
"""
# we don't really care about those parameters because they will be handled by the reporter
logging.Handler.__init__(self)
@ -47,6 +49,7 @@ class HttpLogHandler(logging.Handler):
# client has to be imported here because of circular imports
from ahriman.core.status.client import Client
self.reporter = Client.load(configuration, report=report)
self.suppress_errors = suppress_errors
@classmethod
def load(cls, configuration: Configuration, *, report: bool) -> Self:
@ -65,7 +68,8 @@ class HttpLogHandler(logging.Handler):
if (handler := next((handler for handler in root.handlers if isinstance(handler, cls)), None)) is not None:
return handler # there is already registered instance
handler = cls(configuration, report=report)
suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False)
handler = cls(configuration, report=report, suppress_errors=suppress_errors)
root.addHandler(handler)
return handler
@ -81,4 +85,9 @@ class HttpLogHandler(logging.Handler):
if log_record_id is None:
return # in case if no package base supplied we need just skip log message
self.reporter.package_logs(log_record_id, record)
try:
self.reporter.package_logs(log_record_id, record)
except Exception:
if self.suppress_errors:
return
self.handleError(record)

View File

@ -17,6 +17,8 @@
# 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 ahriman.core.configuration import Configuration
from ahriman.core.report.report import Report
from ahriman.core.status.web_client import WebClient
@ -77,43 +79,41 @@ class RemoteCall(Report):
Returns:
bool: True in case if remote process is alive and False otherwise
"""
response = self.client.make_request("GET", f"/api/v1/service/process/{process_id}")
if response is None:
return False
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:
return False
raise
response_json = response.json()
is_alive: bool = response_json["is_alive"]
return is_alive
def remote_update(self) -> str | None:
def remote_update(self) -> str:
"""
call remote server for update
Returns:
str | None: remote process id on success and ``None`` otherwise
str: remote process id
"""
response = self.client.make_request("POST", "/api/v1/service/update", json={
"aur": self.update_aur,
"local": self.update_local,
"manual": self.update_manual,
})
if response is None:
return None # request terminated with error
response_json = response.json()
process_id: str = response_json["process_id"]
return process_id
def remote_wait(self, process_id: str | None) -> None:
def remote_wait(self, process_id: str) -> None:
"""
wait for remote process termination
Args:
process_id(str | None): remote process id
process_id(str): remote process id
"""
if process_id is None:
return # nothing to track
waiter = Waiter(self.wait_timeout)
waiter.wait(self.is_process_alive, process_id)

View File

@ -21,7 +21,6 @@ import contextlib
import logging
import requests
from collections.abc import Generator
from functools import cached_property
from typing import Any, IO, Literal
from urllib.parse import quote_plus as urlencode
@ -129,32 +128,6 @@ class WebClient(Client, LazyLogging):
address = f"http://{host}:{port}"
return address, False
@contextlib.contextmanager
def __get_session(self, session: requests.Session | None = None) -> Generator[requests.Session, None, None]:
"""
execute request and handle exceptions
Args:
session(requests.Session | None, optional): session to be used or stored instance property otherwise
(Default value = None)
Yields:
requests.Session: session for requests
"""
try:
if session is not None:
yield session # use session from arguments
else:
yield self.session # use instance generated session
except requests.RequestException as e:
if self.suppress_errors:
return
self.logger.exception("could not perform http request: %s", exception_response_text(e))
except Exception:
if self.suppress_errors:
return
self.logger.exception("could not perform http request")
def _create_session(self, *, use_unix_socket: bool) -> requests.Session:
"""
generate new request session
@ -191,13 +164,15 @@ class WebClient(Client, LazyLogging):
"username": self.user.username,
"password": self.user.password
}
self.make_request("POST", self._login_url, json=payload, session=session)
with contextlib.suppress(Exception):
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) -> requests.Response | None:
session: requests.Session | None = None,
suppress_errors: bool | None = None) -> requests.Response:
"""
perform request with specified parameters
@ -208,17 +183,30 @@ class WebClient(Client, LazyLogging):
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 | None: response object or None in case of errors
requests.Response: response object
"""
with self.__get_session(session) as _session:
response = _session.request(method, f"{self.address}{url}", params=params, json=json, files=files)
# defaults
if suppress_errors is None:
suppress_errors = self.suppress_errors
if session is None:
session = self.session
try:
response = session.request(method, f"{self.address}{url}", params=params, json=json, files=files)
response.raise_for_status()
return response
# noinspection PyUnreachableCode
return None
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 package_add(self, package: Package, status: BuildStatusEnum) -> None:
"""
@ -232,7 +220,8 @@ class WebClient(Client, LazyLogging):
"status": status.value,
"package": package.view()
}
self.make_request("POST", self._package_url(package.base), json=payload)
with contextlib.suppress(Exception):
self.make_request("POST", self._package_url(package.base), json=payload)
def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
"""
@ -244,15 +233,16 @@ class WebClient(Client, LazyLogging):
Returns:
list[tuple[Package, BuildStatus]]: list of current package description and status if it has been found
"""
response = self.make_request("GET", self._package_url(package_base or ""))
if response is None:
return []
with contextlib.suppress(Exception):
response = self.make_request("GET", self._package_url(package_base or ""))
response_json = response.json()
response_json = response.json()
return [
(Package.from_json(package["package"]), BuildStatus.from_json(package["status"]))
for package in response_json
]
return [
(Package.from_json(package["package"]), BuildStatus.from_json(package["status"]))
for package in response_json
]
return []
def package_logs(self, log_record_id: LogRecordId, record: logging.LogRecord) -> None:
"""
@ -267,7 +257,11 @@ class WebClient(Client, LazyLogging):
"message": record.getMessage(),
"version": log_record_id.version,
}
self.make_request("POST", self._logs_url(log_record_id.package_base), json=payload)
# this is special case, because we would like to do not suppress exception here
# in case of exception raised it will be handled by upstream HttpLogHandler
# In the other hand, we force to suppress all http logs here to avoid cyclic reporting
self.make_request("POST", self._logs_url(log_record_id.package_base), json=payload, suppress_errors=True)
def package_remove(self, package_base: str) -> None:
"""
@ -276,7 +270,8 @@ class WebClient(Client, LazyLogging):
Args:
package_base(str): basename to remove
"""
self.make_request("DELETE", self._package_url(package_base))
with contextlib.suppress(Exception):
self.make_request("DELETE", self._package_url(package_base))
def package_update(self, package_base: str, status: BuildStatusEnum) -> None:
"""
@ -287,7 +282,8 @@ class WebClient(Client, LazyLogging):
status(BuildStatusEnum): current package build status
"""
payload = {"status": status.value}
self.make_request("POST", self._package_url(package_base), json=payload)
with contextlib.suppress(Exception):
self.make_request("POST", self._package_url(package_base), json=payload)
def status_get(self) -> InternalStatus:
"""
@ -296,12 +292,13 @@ class WebClient(Client, LazyLogging):
Returns:
InternalStatus: current internal (web) service status
"""
response = self.make_request("GET", self._status_url)
if response is None:
return InternalStatus(status=BuildStatus())
with contextlib.suppress(Exception):
response = self.make_request("GET", self._status_url)
response_json = response.json()
response_json = response.json()
return InternalStatus.from_json(response_json)
return InternalStatus.from_json(response_json)
return InternalStatus(status=BuildStatus())
def status_update(self, status: BuildStatusEnum) -> None:
"""
@ -311,4 +308,5 @@ class WebClient(Client, LazyLogging):
status(BuildStatusEnum): current ahriman status
"""
payload = {"status": status.value}
self.make_request("POST", self._status_url, json=payload)
with contextlib.suppress(Exception):
self.make_request("POST", self._status_url, json=payload)

View File

@ -43,18 +43,46 @@ def test_emit(configuration: Configuration, log_record: logging.LogRecord, packa
log_record_id = log_record.package_id = LogRecordId(package_ahriman.base, package_ahriman.version)
log_mock = mocker.patch("ahriman.core.status.client.Client.package_logs")
handler = HttpLogHandler(configuration, report=False)
handler = HttpLogHandler(configuration, report=False, suppress_errors=False)
handler.emit(log_record)
log_mock.assert_called_once_with(log_record_id, log_record)
def test_emit_failed(configuration: Configuration, log_record: logging.LogRecord, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must call handle error on exception
"""
log_record.package_id = LogRecordId(package_ahriman.base, package_ahriman.version)
mocker.patch("ahriman.core.status.client.Client.package_logs", side_effect=Exception())
handle_error_mock = mocker.patch("logging.Handler.handleError")
handler = HttpLogHandler(configuration, report=False, suppress_errors=False)
handler.emit(log_record)
handle_error_mock.assert_called_once_with(log_record)
def test_emit_suppress_failed(configuration: Configuration, log_record: logging.LogRecord, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must not call handle error on exception if suppress flag is set
"""
log_record.package_id = LogRecordId(package_ahriman.base, package_ahriman.version)
mocker.patch("ahriman.core.status.client.Client.package_logs", side_effect=Exception())
handle_error_mock = mocker.patch("logging.Handler.handleError")
handler = HttpLogHandler(configuration, report=False, suppress_errors=True)
handler.emit(log_record)
handle_error_mock.assert_not_called()
def test_emit_skip(configuration: Configuration, log_record: logging.LogRecord, mocker: MockerFixture) -> None:
"""
must skip log record posting if no package base set
"""
log_mock = mocker.patch("ahriman.core.status.client.Client.package_logs")
handler = HttpLogHandler(configuration, report=False)
handler = HttpLogHandler(configuration, report=False, suppress_errors=False)
handler.emit(log_record)
log_mock.assert_not_called()

View File

@ -37,10 +37,37 @@ def test_is_process_alive_unknown(remote_call: RemoteCall, mocker: MockerFixture
"""
must correctly define if process is unknown
"""
mocker.patch("ahriman.core.status.web_client.WebClient.make_request", return_value=None)
response = requests.Response()
response.status_code = 404
mocker.patch("ahriman.core.status.web_client.WebClient.make_request",
side_effect=requests.RequestException(response=response))
assert not remote_call.is_process_alive("id")
def test_is_process_alive_error(remote_call: RemoteCall, mocker: MockerFixture) -> None:
"""
must reraise exception on process request
"""
mocker.patch("ahriman.core.status.web_client.WebClient.make_request", side_effect=Exception)
with pytest.raises(Exception):
remote_call.is_process_alive("id")
def test_is_process_alive_http_error(remote_call: RemoteCall, mocker: MockerFixture) -> None:
"""
must reraise http exception on process request
"""
response = requests.Response()
response.status_code = 500
mocker.patch("ahriman.core.status.web_client.WebClient.make_request",
side_effect=requests.RequestException(response=response))
with pytest.raises(requests.RequestException):
remote_call.is_process_alive("id")
def test_remote_update(remote_call: RemoteCall, mocker: MockerFixture) -> None:
"""
must call remote server for update process
@ -59,14 +86,6 @@ def test_remote_update(remote_call: RemoteCall, mocker: MockerFixture) -> None:
})
def test_remote_update_failed(remote_call: RemoteCall, mocker: MockerFixture) -> None:
"""
must return empty process id in case of errors
"""
mocker.patch("ahriman.core.status.web_client.WebClient.make_request", return_value=None)
assert remote_call.generate([], Result()) is None
def test_remote_wait(remote_call: RemoteCall, mocker: MockerFixture) -> None:
"""
must wait for remote process to success
@ -74,12 +93,3 @@ def test_remote_wait(remote_call: RemoteCall, mocker: MockerFixture) -> None:
wait_mock = mocker.patch("ahriman.models.waiter.Waiter.wait")
remote_call.remote_wait("id")
wait_mock.assert_called_once_with(pytest.helpers.anyvar(int), "id")
def test_remote_wait_skip(remote_call: RemoteCall, mocker: MockerFixture) -> None:
"""
must skip wait if process id is unknown
"""
wait_mock = mocker.patch("ahriman.models.waiter.Waiter.wait")
remote_call.remote_wait(None)
wait_mock.assert_not_called()

View File

@ -163,7 +163,8 @@ def test_make_request_failed(web_client: WebClient, mocker: MockerFixture) -> No
must make HTTP request
"""
mocker.patch("requests.Session.request", side_effect=Exception())
assert web_client.make_request("GET", "url") is None
with pytest.raises(Exception):
web_client.make_request("GET", "url")
def test_package_add(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
@ -296,7 +297,8 @@ def test_package_logs_failed(web_client: WebClient, log_record: logging.LogRecor
"""
mocker.patch("requests.Session.request", side_effect=Exception())
log_record.package_base = package_ahriman.base
web_client.package_logs(LogRecordId(package_ahriman.base, package_ahriman.version), log_record)
with pytest.raises(Exception):
web_client.package_logs(LogRecordId(package_ahriman.base, package_ahriman.version), log_record)
def test_package_logs_failed_http_error(web_client: WebClient, log_record: logging.LogRecord, package_ahriman: Package,
@ -306,7 +308,8 @@ def test_package_logs_failed_http_error(web_client: WebClient, log_record: loggi
"""
mocker.patch("requests.Session.request", side_effect=requests.exceptions.HTTPError())
log_record.package_base = package_ahriman.base
web_client.package_logs(LogRecordId(package_ahriman.base, package_ahriman.version), log_record)
with pytest.raises(Exception):
web_client.package_logs(LogRecordId(package_ahriman.base, package_ahriman.version), log_record)
def test_package_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None: