Skip to content

Commit 54c2129

Browse files
Normalize polling timing values to float for sleep compatibility
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 2d8b3a4 commit 54c2129

File tree

2 files changed

+111
-29
lines changed

2 files changed

+111
-29
lines changed

hyperbrowser/client/polling.py

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,22 @@ class _NonRetryablePollingError(HyperbrowserError):
2626
pass
2727

2828

29-
def _validate_non_negative_real(value: float, *, field_name: str) -> None:
29+
def _normalize_non_negative_real(value: float, *, field_name: str) -> float:
3030
if isinstance(value, bool) or not isinstance(value, Real):
3131
raise HyperbrowserError(f"{field_name} must be a number")
3232
try:
33-
is_finite = math.isfinite(value)
33+
normalized_value = float(value)
34+
except (TypeError, ValueError, OverflowError):
35+
raise HyperbrowserError(f"{field_name} must be finite")
36+
try:
37+
is_finite = math.isfinite(normalized_value)
3438
except (TypeError, ValueError, OverflowError):
3539
is_finite = False
3640
if not is_finite:
3741
raise HyperbrowserError(f"{field_name} must be finite")
38-
if value < 0:
42+
if normalized_value < 0:
3943
raise HyperbrowserError(f"{field_name} must be non-negative")
44+
return normalized_value
4045

4146

4247
def _validate_operation_name(operation_name: str) -> None:
@@ -259,31 +264,35 @@ def _validate_retry_config(
259264
max_attempts: int,
260265
retry_delay_seconds: float,
261266
max_status_failures: Optional[int] = None,
262-
) -> None:
267+
) -> float:
263268
if isinstance(max_attempts, bool) or not isinstance(max_attempts, int):
264269
raise HyperbrowserError("max_attempts must be an integer")
265270
if max_attempts < 1:
266271
raise HyperbrowserError("max_attempts must be at least 1")
267-
_validate_non_negative_real(retry_delay_seconds, field_name="retry_delay_seconds")
272+
normalized_retry_delay_seconds = _normalize_non_negative_real(
273+
retry_delay_seconds, field_name="retry_delay_seconds"
274+
)
268275
if max_status_failures is not None:
269276
if isinstance(max_status_failures, bool) or not isinstance(
270277
max_status_failures, int
271278
):
272279
raise HyperbrowserError("max_status_failures must be an integer")
273280
if max_status_failures < 1:
274281
raise HyperbrowserError("max_status_failures must be at least 1")
282+
return normalized_retry_delay_seconds
275283

276284

277-
def _validate_poll_interval(poll_interval_seconds: float) -> None:
278-
_validate_non_negative_real(
279-
poll_interval_seconds, field_name="poll_interval_seconds"
285+
def _validate_poll_interval(poll_interval_seconds: float) -> float:
286+
return _normalize_non_negative_real(
287+
poll_interval_seconds,
288+
field_name="poll_interval_seconds",
280289
)
281290

282291

283-
def _validate_max_wait_seconds(max_wait_seconds: Optional[float]) -> None:
292+
def _validate_max_wait_seconds(max_wait_seconds: Optional[float]) -> Optional[float]:
284293
if max_wait_seconds is None:
285-
return
286-
_validate_non_negative_real(max_wait_seconds, field_name="max_wait_seconds")
294+
return None
295+
return _normalize_non_negative_real(max_wait_seconds, field_name="max_wait_seconds")
287296

288297

289298
def _validate_page_batch_values(
@@ -335,9 +344,9 @@ def poll_until_terminal_status(
335344
max_status_failures: int = 5,
336345
) -> str:
337346
_validate_operation_name(operation_name)
338-
_validate_poll_interval(poll_interval_seconds)
339-
_validate_max_wait_seconds(max_wait_seconds)
340-
_validate_retry_config(
347+
poll_interval_seconds = _validate_poll_interval(poll_interval_seconds)
348+
max_wait_seconds = _validate_max_wait_seconds(max_wait_seconds)
349+
_ = _validate_retry_config(
341350
max_attempts=1,
342351
retry_delay_seconds=0,
343352
max_status_failures=max_status_failures,
@@ -390,7 +399,7 @@ def retry_operation(
390399
retry_delay_seconds: float,
391400
) -> T:
392401
_validate_operation_name(operation_name)
393-
_validate_retry_config(
402+
retry_delay_seconds = _validate_retry_config(
394403
max_attempts=max_attempts,
395404
retry_delay_seconds=retry_delay_seconds,
396405
)
@@ -427,9 +436,9 @@ async def poll_until_terminal_status_async(
427436
max_status_failures: int = 5,
428437
) -> str:
429438
_validate_operation_name(operation_name)
430-
_validate_poll_interval(poll_interval_seconds)
431-
_validate_max_wait_seconds(max_wait_seconds)
432-
_validate_retry_config(
439+
poll_interval_seconds = _validate_poll_interval(poll_interval_seconds)
440+
max_wait_seconds = _validate_max_wait_seconds(max_wait_seconds)
441+
_ = _validate_retry_config(
433442
max_attempts=1,
434443
retry_delay_seconds=0,
435444
max_status_failures=max_status_failures,
@@ -502,7 +511,7 @@ async def retry_operation_async(
502511
retry_delay_seconds: float,
503512
) -> T:
504513
_validate_operation_name(operation_name)
505-
_validate_retry_config(
514+
retry_delay_seconds = _validate_retry_config(
506515
max_attempts=max_attempts,
507516
retry_delay_seconds=retry_delay_seconds,
508517
)
@@ -552,8 +561,8 @@ def collect_paginated_results(
552561
retry_delay_seconds: float,
553562
) -> None:
554563
_validate_operation_name(operation_name)
555-
_validate_max_wait_seconds(max_wait_seconds)
556-
_validate_retry_config(
564+
max_wait_seconds = _validate_max_wait_seconds(max_wait_seconds)
565+
retry_delay_seconds = _validate_retry_config(
557566
max_attempts=max_attempts,
558567
retry_delay_seconds=retry_delay_seconds,
559568
)
@@ -658,8 +667,8 @@ async def collect_paginated_results_async(
658667
retry_delay_seconds: float,
659668
) -> None:
660669
_validate_operation_name(operation_name)
661-
_validate_max_wait_seconds(max_wait_seconds)
662-
_validate_retry_config(
670+
max_wait_seconds = _validate_max_wait_seconds(max_wait_seconds)
671+
retry_delay_seconds = _validate_retry_config(
663672
max_attempts=max_attempts,
664673
retry_delay_seconds=retry_delay_seconds,
665674
)
@@ -766,13 +775,13 @@ def wait_for_job_result(
766775
fetch_retry_delay_seconds: float,
767776
) -> T:
768777
_validate_operation_name(operation_name)
769-
_validate_retry_config(
778+
fetch_retry_delay_seconds = _validate_retry_config(
770779
max_attempts=fetch_max_attempts,
771780
retry_delay_seconds=fetch_retry_delay_seconds,
772781
max_status_failures=max_status_failures,
773782
)
774-
_validate_poll_interval(poll_interval_seconds)
775-
_validate_max_wait_seconds(max_wait_seconds)
783+
poll_interval_seconds = _validate_poll_interval(poll_interval_seconds)
784+
max_wait_seconds = _validate_max_wait_seconds(max_wait_seconds)
776785
poll_until_terminal_status(
777786
operation_name=operation_name,
778787
get_status=get_status,
@@ -802,13 +811,13 @@ async def wait_for_job_result_async(
802811
fetch_retry_delay_seconds: float,
803812
) -> T:
804813
_validate_operation_name(operation_name)
805-
_validate_retry_config(
814+
fetch_retry_delay_seconds = _validate_retry_config(
806815
max_attempts=fetch_max_attempts,
807816
retry_delay_seconds=fetch_retry_delay_seconds,
808817
max_status_failures=max_status_failures,
809818
)
810-
_validate_poll_interval(poll_interval_seconds)
811-
_validate_max_wait_seconds(max_wait_seconds)
819+
poll_interval_seconds = _validate_poll_interval(poll_interval_seconds)
820+
max_wait_seconds = _validate_max_wait_seconds(max_wait_seconds)
812821
await poll_until_terminal_status_async(
813822
operation_name=operation_name,
814823
get_status=get_status,

tests/test_polling.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,20 @@ def get_status() -> str:
106106
assert status == "completed"
107107

108108

109+
def test_poll_until_terminal_status_accepts_fraction_timing_values():
110+
status_values = iter(["running", "completed"])
111+
112+
status = poll_until_terminal_status(
113+
operation_name="sync poll fraction timings",
114+
get_status=lambda: next(status_values),
115+
is_terminal_status=lambda value: value == "completed",
116+
poll_interval_seconds=Fraction(1, 10000), # type: ignore[arg-type]
117+
max_wait_seconds=Fraction(1, 1), # type: ignore[arg-type]
118+
)
119+
120+
assert status == "completed"
121+
122+
109123
def test_poll_until_terminal_status_does_not_retry_non_retryable_client_errors():
110124
attempts = {"count": 0}
111125

@@ -804,6 +818,26 @@ def operation() -> str:
804818
assert result == "ok"
805819

806820

821+
def test_retry_operation_accepts_fraction_retry_delay():
822+
attempts = {"count": 0}
823+
824+
def operation() -> str:
825+
attempts["count"] += 1
826+
if attempts["count"] < 3:
827+
raise ValueError("transient")
828+
return "ok"
829+
830+
result = retry_operation(
831+
operation_name="sync retry fraction delay",
832+
operation=operation,
833+
max_attempts=3,
834+
retry_delay_seconds=Fraction(1, 10000), # type: ignore[arg-type]
835+
)
836+
837+
assert result == "ok"
838+
assert attempts["count"] == 3
839+
840+
807841
def test_retry_operation_raises_after_max_attempts():
808842
with pytest.raises(HyperbrowserError, match="sync retry failure"):
809843
retry_operation(
@@ -2587,6 +2621,27 @@ def test_collect_paginated_results_collects_all_pages():
25872621
assert collected == ["a", "b"]
25882622

25892623

2624+
def test_collect_paginated_results_accepts_fraction_retry_delay():
2625+
page_map = {
2626+
1: {"current": 1, "total": 2, "items": ["a"]},
2627+
2: {"current": 2, "total": 2, "items": ["b"]},
2628+
}
2629+
collected = []
2630+
2631+
collect_paginated_results(
2632+
operation_name="sync paginated fraction delay",
2633+
get_next_page=lambda page: page_map[page],
2634+
get_current_page_batch=lambda response: response["current"],
2635+
get_total_page_batches=lambda response: response["total"],
2636+
on_page_success=lambda response: collected.extend(response["items"]),
2637+
max_wait_seconds=1.0,
2638+
max_attempts=2,
2639+
retry_delay_seconds=Fraction(1, 10000), # type: ignore[arg-type]
2640+
)
2641+
2642+
assert collected == ["a", "b"]
2643+
2644+
25902645
def test_collect_paginated_results_rejects_awaitable_page_callback_result():
25912646
async def async_get_page() -> dict:
25922647
return {"current": 1, "total": 1, "items": []}
@@ -3824,6 +3879,24 @@ def test_wait_for_job_result_returns_fetched_value():
38243879
assert result == {"ok": True}
38253880

38263881

3882+
def test_wait_for_job_result_accepts_fraction_timing_values():
3883+
status_values = iter(["running", "completed"])
3884+
3885+
result = wait_for_job_result(
3886+
operation_name="sync wait helper fraction timings",
3887+
get_status=lambda: next(status_values),
3888+
is_terminal_status=lambda value: value == "completed",
3889+
fetch_result=lambda: {"ok": True},
3890+
poll_interval_seconds=Fraction(1, 10000), # type: ignore[arg-type]
3891+
max_wait_seconds=Fraction(1, 1), # type: ignore[arg-type]
3892+
max_status_failures=2,
3893+
fetch_max_attempts=2,
3894+
fetch_retry_delay_seconds=Fraction(1, 10000), # type: ignore[arg-type]
3895+
)
3896+
3897+
assert result == {"ok": True}
3898+
3899+
38273900
def test_wait_for_job_result_status_polling_failures_short_circuit_fetch():
38283901
status_attempts = {"count": 0}
38293902
fetch_attempts = {"count": 0}

0 commit comments

Comments
 (0)