Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add retry on protocol errors #39

Merged
merged 1 commit into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 41 additions & 30 deletions haphilipsjs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from urllib.parse import quote
from secrets import token_bytes, token_hex
from base64 import b64decode, b64encode
from functools import wraps


from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.padding import PKCS7
Expand Down Expand Up @@ -200,6 +202,27 @@ def __init__(self, data):
T = TypeVar("T")


def handle_httpx_exceptions(f):
"""Wrap up httpx exceptions in our wanted variants."""
@wraps(f)
async def wrapper(*args, **kwds):
try:
try:
return await f(*args, **kwds)
except httpx.RemoteProtocolError as err:
LOG.warning("%r. We retry once, could be a reused session that was closed", err)
return await f(*args, **kwds)

except (httpx.ConnectTimeout, httpx.ConnectError) as err:
raise ConnectionFailure(err) from err
except (httpx.ProtocolError, httpx.ReadError) as err:
raise ProtocolFailure(err) from err
except httpx.HTTPError as err:
raise GeneralFailure(err) from err

return wrapper


class PhilipsTV(object):

channels: ChannelsType
Expand Down Expand Up @@ -569,38 +592,32 @@ def _url(self, path, protocol = None):

return f"{protocol}://{self._host}:{port}/{self.api_version}/{path}"

@handle_httpx_exceptions
async def getReq(self, path, protocol = None) -> Optional[Dict]:
try:
resp = await self.session.get(self._url(path, protocol = protocol))
if resp.status_code == 401:
raise AutenticationFailure("Authenticaion failed to device")
resp = await self.session.get(self._url(path, protocol = protocol))

if resp.status_code != 200:
LOG.debug("Get failed: %s -> %d %s", path, resp.status_code, resp.text)
return None
if resp.status_code == 401:
raise AutenticationFailure("Authenticaion failed to device")

LOG.debug("Get succeded: %s -> %s", path, resp.text)
return decode_xtv_response(resp)
except (httpx.ConnectTimeout, httpx.ConnectError) as err:
raise ConnectionFailure(err) from err
except httpx.HTTPError as err:
raise GeneralFailure(err) from err
if resp.status_code != 200:
LOG.debug("Get failed: %s -> %d %s", path, resp.status_code, resp.text)
return None

LOG.debug("Get succeded: %s -> %s", path, resp.text)
return decode_xtv_response(resp)

@handle_httpx_exceptions
async def _getBinary(self, path: str) -> Tuple[Optional[bytes], Optional[str]]:

try:
resp = await self.session.get(self._url(path))
if resp.status_code == 401:
raise AutenticationFailure("Authenticaion failed to device")
resp = await self.session.get(self._url(path))
if resp.status_code == 401:
raise AutenticationFailure("Authenticaion failed to device")

if resp.status_code != 200:
return None, None
return resp.content, resp.headers.get("content-type")
except (httpx.ConnectTimeout, httpx.ConnectError) as err:
raise ConnectionFailure(err) from err
except httpx.HTTPError as err:
raise GeneralFailure(err) from err
if resp.status_code != 200:
return None, None
return resp.content, resp.headers.get("content-type")

@handle_httpx_exceptions
async def postReq(self, path: str, data: Any, timeout=None, protocol=None) -> Optional[Dict]:
try:
resp = await self.session.post(self._url(path, protocol), json=data, timeout=timeout)
Expand All @@ -616,12 +633,6 @@ async def postReq(self, path: str, data: Any, timeout=None, protocol=None) -> Op
except httpx.ReadTimeout:
LOG.debug("Read time out on postReq", exc_info=True)
return None
except (httpx.ConnectTimeout, httpx.ConnectError) as err:
raise ConnectionFailure(err) from err
except (httpx.ProtocolError, httpx.ReadError) as err:
raise ProtocolFailure(err) from err
except httpx.HTTPError as err:
raise GeneralFailure(err) from err

async def pairRequest(
self,
Expand Down
9 changes: 9 additions & 0 deletions tests/test_v6.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,15 @@ async def test_send_key_off(client_mock, param: Param):
await client_mock.sendKey("Standby")


async def test_send_key_retry(client_mock, param: Param):
route = respx.post(f"{param.base}/input/key").mock(side_effect=httpx.RemoteProtocolError)

with pytest.raises(haphilipsjs.ProtocolFailure):
await client_mock.sendKey("Standby")

assert route.call_count == 2


async def test_ambilight_mode(client_mock, param):
await client_mock.getSystem()

Expand Down
Loading