Skip to content

Commit 537d0f9

Browse files
Accept decimal timing values via float normalization
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 310c338 commit 537d0f9

4 files changed

Lines changed: 103 additions & 2 deletions

File tree

hyperbrowser/client/polling.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from concurrent.futures import BrokenExecutor as ConcurrentBrokenExecutor
33
from concurrent.futures import CancelledError as ConcurrentCancelledError
44
from concurrent.futures import InvalidStateError as ConcurrentInvalidStateError
5+
from decimal import Decimal
56
import inspect
67
import math
78
from numbers import Real
@@ -27,7 +28,8 @@ class _NonRetryablePollingError(HyperbrowserError):
2728

2829

2930
def _normalize_non_negative_real(value: float, *, field_name: str) -> float:
30-
if isinstance(value, bool) or not isinstance(value, Real):
31+
is_supported_numeric_type = isinstance(value, Real) or isinstance(value, Decimal)
32+
if isinstance(value, bool) or not is_supported_numeric_type:
3133
raise HyperbrowserError(f"{field_name} must be a number")
3234
try:
3335
normalized_value = float(value)

hyperbrowser/client/timeout_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from decimal import Decimal
12
import math
23
from numbers import Real
34
from typing import Optional
@@ -8,7 +9,10 @@
89
def validate_timeout_seconds(timeout: Optional[float]) -> Optional[float]:
910
if timeout is None:
1011
return None
11-
if isinstance(timeout, bool) or not isinstance(timeout, Real):
12+
is_supported_numeric_type = isinstance(timeout, Real) or isinstance(
13+
timeout, Decimal
14+
)
15+
if isinstance(timeout, bool) or not is_supported_numeric_type:
1216
raise HyperbrowserError("timeout must be a number")
1317
try:
1418
normalized_timeout = float(timeout)

tests/test_client_timeout.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
from decimal import Decimal
23
import math
34
from fractions import Fraction
45

@@ -58,6 +59,33 @@ async def run() -> None:
5859
asyncio.run(run())
5960

6061

62+
def test_sync_client_normalizes_decimal_timeout_to_float():
63+
client = Hyperbrowser(
64+
api_key="test-key",
65+
timeout=Decimal("0.5"), # type: ignore[arg-type]
66+
)
67+
try:
68+
assert isinstance(client.transport.client.timeout.connect, float)
69+
assert client.transport.client.timeout.connect == 0.5
70+
finally:
71+
client.close()
72+
73+
74+
def test_async_client_normalizes_decimal_timeout_to_float():
75+
async def run() -> None:
76+
client = AsyncHyperbrowser(
77+
api_key="test-key",
78+
timeout=Decimal("0.5"), # type: ignore[arg-type]
79+
)
80+
try:
81+
assert isinstance(client.transport.client.timeout.connect, float)
82+
assert client.transport.client.timeout.connect == 0.5
83+
finally:
84+
await client.close()
85+
86+
asyncio.run(run())
87+
88+
6189
def test_sync_client_rejects_non_numeric_timeout():
6290
with pytest.raises(HyperbrowserError, match="timeout must be a number"):
6391
Hyperbrowser(api_key="test-key", timeout="30") # type: ignore[arg-type]

tests/test_polling.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from concurrent.futures import BrokenExecutor as ConcurrentBrokenExecutor
44
from concurrent.futures import CancelledError as ConcurrentCancelledError
55
from concurrent.futures import InvalidStateError as ConcurrentInvalidStateError
6+
from decimal import Decimal
67
import math
78
from fractions import Fraction
89

@@ -120,6 +121,20 @@ def test_poll_until_terminal_status_accepts_fraction_timing_values():
120121
assert status == "completed"
121122

122123

124+
def test_poll_until_terminal_status_accepts_decimal_timing_values():
125+
status_values = iter(["running", "completed"])
126+
127+
status = poll_until_terminal_status(
128+
operation_name="sync poll decimal timings",
129+
get_status=lambda: next(status_values),
130+
is_terminal_status=lambda value: value == "completed",
131+
poll_interval_seconds=Decimal("0.0001"), # type: ignore[arg-type]
132+
max_wait_seconds=Decimal("1"), # type: ignore[arg-type]
133+
)
134+
135+
assert status == "completed"
136+
137+
123138
def test_poll_until_terminal_status_does_not_retry_non_retryable_client_errors():
124139
attempts = {"count": 0}
125140

@@ -838,6 +853,26 @@ def operation() -> str:
838853
assert attempts["count"] == 3
839854

840855

856+
def test_retry_operation_accepts_decimal_retry_delay():
857+
attempts = {"count": 0}
858+
859+
def operation() -> str:
860+
attempts["count"] += 1
861+
if attempts["count"] < 3:
862+
raise ValueError("transient")
863+
return "ok"
864+
865+
result = retry_operation(
866+
operation_name="sync retry decimal delay",
867+
operation=operation,
868+
max_attempts=3,
869+
retry_delay_seconds=Decimal("0.0001"), # type: ignore[arg-type]
870+
)
871+
872+
assert result == "ok"
873+
assert attempts["count"] == 3
874+
875+
841876
def test_retry_operation_raises_after_max_attempts():
842877
with pytest.raises(HyperbrowserError, match="sync retry failure"):
843878
retry_operation(
@@ -1464,6 +1499,38 @@ async def get_next_page(page_batch: int) -> dict:
14641499
asyncio.run(run())
14651500

14661501

1502+
def test_async_helpers_accept_decimal_timing_values():
1503+
async def run() -> None:
1504+
status_values = iter(["pending", "completed"])
1505+
status = await poll_until_terminal_status_async(
1506+
operation_name="async poll decimal timings",
1507+
get_status=lambda: asyncio.sleep(0, result=next(status_values)),
1508+
is_terminal_status=lambda value: value == "completed",
1509+
poll_interval_seconds=Decimal("0.0001"), # type: ignore[arg-type]
1510+
max_wait_seconds=Decimal("1"), # type: ignore[arg-type]
1511+
)
1512+
assert status == "completed"
1513+
1514+
attempts = {"count": 0}
1515+
1516+
async def operation() -> str:
1517+
attempts["count"] += 1
1518+
if attempts["count"] < 2:
1519+
raise ValueError("temporary")
1520+
return "ok"
1521+
1522+
result = await retry_operation_async(
1523+
operation_name="async retry decimal delay",
1524+
operation=operation,
1525+
max_attempts=2,
1526+
retry_delay_seconds=Decimal("0.0001"), # type: ignore[arg-type]
1527+
)
1528+
assert result == "ok"
1529+
assert attempts["count"] == 2
1530+
1531+
asyncio.run(run())
1532+
1533+
14671534
def test_retry_operation_async_rejects_non_awaitable_operation_result() -> None:
14681535
async def run() -> None:
14691536
with pytest.raises(

0 commit comments

Comments
 (0)