From a6a6ac1572458d8abeee9e899330b51434fea2b6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 Jan 2023 13:56:17 +0100 Subject: [PATCH] Raise SFRBoxError on HTTPError (#59) * Raise SFRBoxError on HTTPError * Use decorator * Adjust decorator type hints --- src/sfrbox_api/bridge.py | 29 +++++++++++++++++++++++++++++ tests/test_bridge.py | 24 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/sfrbox_api/bridge.py b/src/sfrbox_api/bridge.py index 8887c6b..55ac56f 100644 --- a/src/sfrbox_api/bridge.py +++ b/src/sfrbox_api/bridge.py @@ -2,12 +2,18 @@ from __future__ import annotations import logging +from functools import wraps from typing import Any +from typing import Awaitable +from typing import Callable +from typing import Coroutine from typing import Mapping +from typing import TypeVar from xml.etree.ElementTree import Element as XmlElement # noqa: S405 import defusedxml.ElementTree as DefusedElementTree import httpx +from typing_extensions import ParamSpec from sfrbox_api.helpers import compute_hash @@ -21,6 +27,27 @@ _LOGGER = logging.getLogger(__name__) +_R = TypeVar("_R") +_P = ParamSpec("_P") + + +def _with_error_wrapping( + func: Callable[_P, Awaitable[_R]] +) -> Callable[_P, Coroutine[Any, Any, _R]]: + """Catch httpx errors.""" + + @wraps(func) + async def wrapper( + *args: _P.args, + **kwargs: _P.kwargs, + ) -> _R: + """Catch RequestError errors and raise SFRBoxError.""" + try: + return await func(*args, **kwargs) + except httpx.HTTPError as err: + raise SFRBoxError(str(err)) from err + + return wrapper class SFRBox: @@ -91,6 +118,7 @@ def _check_response(self, response: httpx.Response) -> XmlElement: raise SFRBoxError(f"Response was not ok: {response.text}") return element + @_with_error_wrapping async def _send_get(self, namespace: str, method: str, **kwargs: str) -> XmlElement: params = httpx.QueryParams(method=f"{namespace}.{method}", **kwargs) response = await self._client.get(f"http://{self._ip}/api/1.0/", params=params) @@ -102,6 +130,7 @@ async def _send_get(self, namespace: str, method: str, **kwargs: str) -> XmlElem ) return result + @_with_error_wrapping async def _send_post( self, namespace: str, diff --git a/tests/test_bridge.py b/tests/test_bridge.py index af0c216..dd19edf 100644 --- a/tests/test_bridge.py +++ b/tests/test_bridge.py @@ -274,3 +274,27 @@ async def test_wan_getinfo_incorrect_namespace() -> None: box = SFRBox(ip="192.168.0.1", client=client) with pytest.raises(SFRBoxError, match="Namespace wan not found in response"): await box.wan_get_info() + + +@respx.mock +@pytest.mark.asyncio +async def test_connect_timeout() -> None: + """It exits with a status code of zero.""" + respx.get("http://192.168.0.1/api/1.0/?method=wan.getInfo").mock( + side_effect=httpx.ConnectTimeout + ) + async with httpx.AsyncClient() as client: + box = SFRBox(ip="192.168.0.1", client=client) + with pytest.raises(SFRBoxError): + await box.wan_get_info() + + +@respx.mock +@pytest.mark.asyncio +async def test_500_error() -> None: + """It exits with a status code of zero.""" + respx.get("http://192.168.0.1/api/1.0/?method=wan.getInfo").respond(500) + async with httpx.AsyncClient() as client: + box = SFRBox(ip="192.168.0.1", client=client) + with pytest.raises(SFRBoxError): + await box.wan_get_info()