Skip to content

Commit 01caa6a

Browse files
Validate polling helper retry and interval configuration
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 96346bd commit 01caa6a

2 files changed

Lines changed: 102 additions & 0 deletions

File tree

hyperbrowser/client/polling.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,25 @@
1111
T = TypeVar("T")
1212

1313

14+
def _validate_retry_config(
15+
*,
16+
max_attempts: int,
17+
retry_delay_seconds: float,
18+
max_status_failures: Optional[int] = None,
19+
) -> None:
20+
if max_attempts < 1:
21+
raise HyperbrowserError("max_attempts must be at least 1")
22+
if retry_delay_seconds < 0:
23+
raise HyperbrowserError("retry_delay_seconds must be non-negative")
24+
if max_status_failures is not None and max_status_failures < 1:
25+
raise HyperbrowserError("max_status_failures must be at least 1")
26+
27+
28+
def _validate_poll_interval(poll_interval_seconds: float) -> None:
29+
if poll_interval_seconds < 0:
30+
raise HyperbrowserError("poll_interval_seconds must be non-negative")
31+
32+
1433
def has_exceeded_max_wait(start_time: float, max_wait_seconds: Optional[float]) -> bool:
1534
return (
1635
max_wait_seconds is not None
@@ -27,6 +46,12 @@ def poll_until_terminal_status(
2746
max_wait_seconds: Optional[float],
2847
max_status_failures: int = 5,
2948
) -> str:
49+
_validate_poll_interval(poll_interval_seconds)
50+
_validate_retry_config(
51+
max_attempts=1,
52+
retry_delay_seconds=0,
53+
max_status_failures=max_status_failures,
54+
)
3055
start_time = time.monotonic()
3156
failures = 0
3257

@@ -60,6 +85,10 @@ def retry_operation(
6085
max_attempts: int,
6186
retry_delay_seconds: float,
6287
) -> T:
88+
_validate_retry_config(
89+
max_attempts=max_attempts,
90+
retry_delay_seconds=retry_delay_seconds,
91+
)
6392
failures = 0
6493
while True:
6594
try:
@@ -82,6 +111,12 @@ async def poll_until_terminal_status_async(
82111
max_wait_seconds: Optional[float],
83112
max_status_failures: int = 5,
84113
) -> str:
114+
_validate_poll_interval(poll_interval_seconds)
115+
_validate_retry_config(
116+
max_attempts=1,
117+
retry_delay_seconds=0,
118+
max_status_failures=max_status_failures,
119+
)
85120
start_time = time.monotonic()
86121
failures = 0
87122

@@ -115,6 +150,10 @@ async def retry_operation_async(
115150
max_attempts: int,
116151
retry_delay_seconds: float,
117152
) -> T:
153+
_validate_retry_config(
154+
max_attempts=max_attempts,
155+
retry_delay_seconds=retry_delay_seconds,
156+
)
118157
failures = 0
119158
while True:
120159
try:
@@ -139,6 +178,10 @@ def collect_paginated_results(
139178
max_attempts: int,
140179
retry_delay_seconds: float,
141180
) -> None:
181+
_validate_retry_config(
182+
max_attempts=max_attempts,
183+
retry_delay_seconds=retry_delay_seconds,
184+
)
142185
start_time = time.monotonic()
143186
current_page_batch = 0
144187
total_page_batches = 0
@@ -177,6 +220,10 @@ async def collect_paginated_results_async(
177220
max_attempts: int,
178221
retry_delay_seconds: float,
179222
) -> None:
223+
_validate_retry_config(
224+
max_attempts=max_attempts,
225+
retry_delay_seconds=retry_delay_seconds,
226+
)
180227
start_time = time.monotonic()
181228
current_page_batch = 0
182229
total_page_batches = 0
@@ -216,6 +263,12 @@ def wait_for_job_result(
216263
fetch_max_attempts: int,
217264
fetch_retry_delay_seconds: float,
218265
) -> T:
266+
_validate_retry_config(
267+
max_attempts=fetch_max_attempts,
268+
retry_delay_seconds=fetch_retry_delay_seconds,
269+
max_status_failures=max_status_failures,
270+
)
271+
_validate_poll_interval(poll_interval_seconds)
219272
poll_until_terminal_status(
220273
operation_name=operation_name,
221274
get_status=get_status,
@@ -244,6 +297,12 @@ async def wait_for_job_result_async(
244297
fetch_max_attempts: int,
245298
fetch_retry_delay_seconds: float,
246299
) -> T:
300+
_validate_retry_config(
301+
max_attempts=fetch_max_attempts,
302+
retry_delay_seconds=fetch_retry_delay_seconds,
303+
max_status_failures=max_status_failures,
304+
)
305+
_validate_poll_interval(poll_interval_seconds)
247306
await poll_until_terminal_status_async(
248307
operation_name=operation_name,
249308
get_status=get_status,

tests/test_polling.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,46 @@ async def run() -> None:
298298
assert result == {"ok": True}
299299

300300
asyncio.run(run())
301+
302+
303+
def test_polling_helpers_validate_retry_and_interval_configuration():
304+
with pytest.raises(HyperbrowserError, match="max_attempts must be at least 1"):
305+
retry_operation(
306+
operation_name="invalid-retry",
307+
operation=lambda: "ok",
308+
max_attempts=0,
309+
retry_delay_seconds=0,
310+
)
311+
312+
with pytest.raises(
313+
HyperbrowserError, match="retry_delay_seconds must be non-negative"
314+
):
315+
retry_operation(
316+
operation_name="invalid-delay",
317+
operation=lambda: "ok",
318+
max_attempts=1,
319+
retry_delay_seconds=-0.1,
320+
)
321+
322+
with pytest.raises(
323+
HyperbrowserError, match="max_status_failures must be at least 1"
324+
):
325+
poll_until_terminal_status(
326+
operation_name="invalid-status-failures",
327+
get_status=lambda: "completed",
328+
is_terminal_status=lambda value: value == "completed",
329+
poll_interval_seconds=0.1,
330+
max_wait_seconds=1.0,
331+
max_status_failures=0,
332+
)
333+
334+
with pytest.raises(
335+
HyperbrowserError, match="poll_interval_seconds must be non-negative"
336+
):
337+
poll_until_terminal_status(
338+
operation_name="invalid-poll-interval",
339+
get_status=lambda: "completed",
340+
is_terminal_status=lambda value: value == "completed",
341+
poll_interval_seconds=-0.1,
342+
max_wait_seconds=1.0,
343+
)

0 commit comments

Comments
 (0)