Skip to content

Commit 7f6fc8e

Browse files
Expand wait-helper bytes-like rate-limit fetch coverage
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 32d3ae4 commit 7f6fc8e

2 files changed

Lines changed: 169 additions & 9 deletions

File tree

hyperbrowser/client/polling.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,25 +174,27 @@ def _is_executor_shutdown_runtime_error(exc: Exception) -> bool:
174174
)
175175

176176

177+
def _decode_ascii_bytes_like(value: object) -> Optional[str]:
178+
try:
179+
return memoryview(value).tobytes().decode("ascii")
180+
except (TypeError, ValueError, UnicodeDecodeError):
181+
return None
182+
183+
177184
def _normalize_status_code_for_retry(status_code: object) -> Optional[int]:
178185
if isinstance(status_code, bool):
179186
return None
180187
if isinstance(status_code, int):
181188
return status_code
182189
status_text: Optional[str] = None
183190
if isinstance(status_code, memoryview):
184-
status_bytes = status_code.tobytes()
185-
try:
186-
status_text = status_bytes.decode("ascii")
187-
except UnicodeDecodeError:
188-
return None
191+
status_text = _decode_ascii_bytes_like(status_code)
189192
elif isinstance(status_code, (bytes, bytearray)):
190-
try:
191-
status_text = bytes(status_code).decode("ascii")
192-
except UnicodeDecodeError:
193-
return None
193+
status_text = _decode_ascii_bytes_like(status_code)
194194
elif isinstance(status_code, str):
195195
status_text = status_code
196+
else:
197+
status_text = _decode_ascii_bytes_like(status_code)
196198

197199
if status_text is not None:
198200
normalized_status = status_text.strip()

tests/test_polling.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
from array import array
23
from concurrent.futures import BrokenExecutor as ConcurrentBrokenExecutor
34
from concurrent.futures import CancelledError as ConcurrentCancelledError
45
from concurrent.futures import InvalidStateError as ConcurrentInvalidStateError
@@ -168,6 +169,29 @@ def get_status() -> str:
168169
assert attempts["count"] == 1
169170

170171

172+
def test_poll_until_terminal_status_does_not_retry_bytes_like_client_errors():
173+
attempts = {"count": 0}
174+
175+
def get_status() -> str:
176+
attempts["count"] += 1
177+
raise HyperbrowserError(
178+
"client failure",
179+
status_code=array("B", [52, 48, 48]), # type: ignore[arg-type]
180+
)
181+
182+
with pytest.raises(HyperbrowserError, match="client failure"):
183+
poll_until_terminal_status(
184+
operation_name="sync poll bytes-like client error",
185+
get_status=get_status,
186+
is_terminal_status=lambda value: value == "completed",
187+
poll_interval_seconds=0.0001,
188+
max_wait_seconds=1.0,
189+
max_status_failures=5,
190+
)
191+
192+
assert attempts["count"] == 1
193+
194+
171195
def test_poll_until_terminal_status_retries_overlong_numeric_status_codes():
172196
attempts = {"count": 0}
173197

@@ -778,6 +802,29 @@ def operation() -> str:
778802
assert attempts["count"] == 3
779803

780804

805+
def test_retry_operation_retries_bytes_like_rate_limit_errors():
806+
attempts = {"count": 0}
807+
808+
def operation() -> str:
809+
attempts["count"] += 1
810+
if attempts["count"] < 3:
811+
raise HyperbrowserError(
812+
"rate limited",
813+
status_code=array("B", [52, 50, 57]), # type: ignore[arg-type]
814+
)
815+
return "ok"
816+
817+
result = retry_operation(
818+
operation_name="sync retry bytes-like rate limit",
819+
operation=operation,
820+
max_attempts=5,
821+
retry_delay_seconds=0.0001,
822+
)
823+
824+
assert result == "ok"
825+
assert attempts["count"] == 3
826+
827+
781828
def test_retry_operation_does_not_retry_numeric_bytes_client_errors():
782829
attempts = {"count": 0}
783830

@@ -1261,6 +1308,32 @@ async def get_status() -> str:
12611308
asyncio.run(run())
12621309

12631310

1311+
def test_poll_until_terminal_status_async_does_not_retry_bytes_like_client_errors():
1312+
async def run() -> None:
1313+
attempts = {"count": 0}
1314+
1315+
async def get_status() -> str:
1316+
attempts["count"] += 1
1317+
raise HyperbrowserError(
1318+
"client failure",
1319+
status_code=array("B", [52, 48, 52]), # type: ignore[arg-type]
1320+
)
1321+
1322+
with pytest.raises(HyperbrowserError, match="client failure"):
1323+
await poll_until_terminal_status_async(
1324+
operation_name="async poll bytes-like client error",
1325+
get_status=get_status,
1326+
is_terminal_status=lambda value: value == "completed",
1327+
poll_interval_seconds=0.0001,
1328+
max_wait_seconds=1.0,
1329+
max_status_failures=5,
1330+
)
1331+
1332+
assert attempts["count"] == 1
1333+
1334+
asyncio.run(run())
1335+
1336+
12641337
def test_poll_until_terminal_status_async_retries_overlong_numeric_status_codes():
12651338
async def run() -> None:
12661339
attempts = {"count": 0}
@@ -1568,6 +1641,32 @@ async def operation() -> str:
15681641
asyncio.run(run())
15691642

15701643

1644+
def test_retry_operation_async_retries_bytes_like_rate_limit_errors():
1645+
async def run() -> None:
1646+
attempts = {"count": 0}
1647+
1648+
async def operation() -> str:
1649+
attempts["count"] += 1
1650+
if attempts["count"] < 3:
1651+
raise HyperbrowserError(
1652+
"rate limited",
1653+
status_code=array("B", [52, 50, 57]), # type: ignore[arg-type]
1654+
)
1655+
return "ok"
1656+
1657+
result = await retry_operation_async(
1658+
operation_name="async retry bytes-like rate limit",
1659+
operation=operation,
1660+
max_attempts=5,
1661+
retry_delay_seconds=0.0001,
1662+
)
1663+
1664+
assert result == "ok"
1665+
assert attempts["count"] == 3
1666+
1667+
asyncio.run(run())
1668+
1669+
15711670
def test_retry_operation_async_retries_numeric_bytes_rate_limit_errors():
15721671
async def run() -> None:
15731672
attempts = {"count": 0}
@@ -4198,6 +4297,34 @@ def fetch_result() -> dict:
41984297
assert fetch_attempts["count"] == 3
41994298

42004299

4300+
def test_wait_for_job_result_retries_bytes_like_rate_limit_fetch_errors():
4301+
fetch_attempts = {"count": 0}
4302+
4303+
def fetch_result() -> dict:
4304+
fetch_attempts["count"] += 1
4305+
if fetch_attempts["count"] < 3:
4306+
raise HyperbrowserError(
4307+
"rate limited",
4308+
status_code=array("B", [52, 50, 57]), # type: ignore[arg-type]
4309+
)
4310+
return {"ok": True}
4311+
4312+
result = wait_for_job_result(
4313+
operation_name="sync wait helper fetch bytes-like rate limit",
4314+
get_status=lambda: "completed",
4315+
is_terminal_status=lambda value: value == "completed",
4316+
fetch_result=fetch_result,
4317+
poll_interval_seconds=0.0001,
4318+
max_wait_seconds=1.0,
4319+
max_status_failures=5,
4320+
fetch_max_attempts=5,
4321+
fetch_retry_delay_seconds=0.0001,
4322+
)
4323+
4324+
assert result == {"ok": True}
4325+
assert fetch_attempts["count"] == 3
4326+
4327+
42014328
def test_wait_for_job_result_retries_request_timeout_fetch_errors():
42024329
fetch_attempts = {"count": 0}
42034330

@@ -5037,6 +5164,37 @@ async def fetch_result() -> dict:
50375164
asyncio.run(run())
50385165

50395166

5167+
def test_wait_for_job_result_async_retries_bytes_like_rate_limit_fetch_errors():
5168+
async def run() -> None:
5169+
fetch_attempts = {"count": 0}
5170+
5171+
async def fetch_result() -> dict:
5172+
fetch_attempts["count"] += 1
5173+
if fetch_attempts["count"] < 3:
5174+
raise HyperbrowserError(
5175+
"rate limited",
5176+
status_code=array("B", [52, 50, 57]), # type: ignore[arg-type]
5177+
)
5178+
return {"ok": True}
5179+
5180+
result = await wait_for_job_result_async(
5181+
operation_name="async wait helper fetch bytes-like rate limit",
5182+
get_status=lambda: asyncio.sleep(0, result="completed"),
5183+
is_terminal_status=lambda value: value == "completed",
5184+
fetch_result=fetch_result,
5185+
poll_interval_seconds=0.0001,
5186+
max_wait_seconds=1.0,
5187+
max_status_failures=5,
5188+
fetch_max_attempts=5,
5189+
fetch_retry_delay_seconds=0.0001,
5190+
)
5191+
5192+
assert result == {"ok": True}
5193+
assert fetch_attempts["count"] == 3
5194+
5195+
asyncio.run(run())
5196+
5197+
50405198
def test_wait_for_job_result_async_retries_request_timeout_fetch_errors():
50415199
async def run() -> None:
50425200
fetch_attempts = {"count": 0}

0 commit comments

Comments
 (0)