@@ -144,6 +144,31 @@ def get_status() -> str:
144144 assert attempts ["count" ] == 3
145145
146146
147+ def test_poll_until_terminal_status_async_retries_rate_limit_errors ():
148+ async def run () -> None :
149+ attempts = {"count" : 0 }
150+
151+ async def get_status () -> str :
152+ attempts ["count" ] += 1
153+ if attempts ["count" ] < 3 :
154+ raise HyperbrowserError ("rate limited" , status_code = 429 )
155+ return "completed"
156+
157+ status = await poll_until_terminal_status_async (
158+ operation_name = "async poll rate limit retries" ,
159+ get_status = get_status ,
160+ is_terminal_status = lambda value : value == "completed" ,
161+ poll_interval_seconds = 0.0001 ,
162+ max_wait_seconds = 1.0 ,
163+ max_status_failures = 5 ,
164+ )
165+
166+ assert status == "completed"
167+ assert attempts ["count" ] == 3
168+
169+ asyncio .run (run ())
170+
171+
147172def test_poll_until_terminal_status_retries_server_errors ():
148173 attempts = {"count" : 0 }
149174
@@ -292,6 +317,26 @@ def operation() -> str:
292317 assert attempts ["count" ] == 3
293318
294319
320+ def test_retry_operation_retries_rate_limit_errors ():
321+ attempts = {"count" : 0 }
322+
323+ def operation () -> str :
324+ attempts ["count" ] += 1
325+ if attempts ["count" ] < 3 :
326+ raise HyperbrowserError ("rate limited" , status_code = 429 )
327+ return "ok"
328+
329+ result = retry_operation (
330+ operation_name = "sync retry rate limit error" ,
331+ operation = operation ,
332+ max_attempts = 5 ,
333+ retry_delay_seconds = 0.0001 ,
334+ )
335+
336+ assert result == "ok"
337+ assert attempts ["count" ] == 3
338+
339+
295340def test_retry_operation_rejects_awaitable_operation_result ():
296341 async def async_operation () -> str :
297342 return "ok"
@@ -479,6 +524,29 @@ async def operation() -> str:
479524 asyncio .run (run ())
480525
481526
527+ def test_retry_operation_async_retries_rate_limit_errors ():
528+ async def run () -> None :
529+ attempts = {"count" : 0 }
530+
531+ async def operation () -> str :
532+ attempts ["count" ] += 1
533+ if attempts ["count" ] < 3 :
534+ raise HyperbrowserError ("rate limited" , status_code = 429 )
535+ return "ok"
536+
537+ result = await retry_operation_async (
538+ operation_name = "async retry rate limit error" ,
539+ operation = operation ,
540+ max_attempts = 5 ,
541+ retry_delay_seconds = 0.0001 ,
542+ )
543+
544+ assert result == "ok"
545+ assert attempts ["count" ] == 3
546+
547+ asyncio .run (run ())
548+
549+
482550def test_async_poll_until_terminal_status_allows_immediate_terminal_on_zero_max_wait ():
483551 async def run () -> None :
484552 status = await poll_until_terminal_status_async (
@@ -1031,6 +1099,31 @@ def get_next_page(page: int) -> dict:
10311099 assert attempts ["count" ] == 3
10321100
10331101
1102+ def test_collect_paginated_results_retries_rate_limit_errors ():
1103+ attempts = {"count" : 0 }
1104+ collected = []
1105+
1106+ def get_next_page (page : int ) -> dict :
1107+ attempts ["count" ] += 1
1108+ if attempts ["count" ] < 3 :
1109+ raise HyperbrowserError ("rate limited" , status_code = 429 )
1110+ return {"current" : 1 , "total" : 1 , "items" : ["a" ]}
1111+
1112+ collect_paginated_results (
1113+ operation_name = "sync paginated rate limit retries" ,
1114+ get_next_page = get_next_page ,
1115+ get_current_page_batch = lambda response : response ["current" ],
1116+ get_total_page_batches = lambda response : response ["total" ],
1117+ on_page_success = lambda response : collected .extend (response ["items" ]),
1118+ max_wait_seconds = 1.0 ,
1119+ max_attempts = 5 ,
1120+ retry_delay_seconds = 0.0001 ,
1121+ )
1122+
1123+ assert collected == ["a" ]
1124+ assert attempts ["count" ] == 3
1125+
1126+
10341127def test_collect_paginated_results_raises_when_page_batch_stagnates ():
10351128 with pytest .raises (HyperbrowserPollingError , match = "No pagination progress" ):
10361129 collect_paginated_results (
@@ -1250,6 +1343,34 @@ async def get_next_page(page: int) -> dict:
12501343 asyncio .run (run ())
12511344
12521345
1346+ def test_collect_paginated_results_async_retries_rate_limit_errors ():
1347+ async def run () -> None :
1348+ attempts = {"count" : 0 }
1349+ collected = []
1350+
1351+ async def get_next_page (page : int ) -> dict :
1352+ attempts ["count" ] += 1
1353+ if attempts ["count" ] < 3 :
1354+ raise HyperbrowserError ("rate limited" , status_code = 429 )
1355+ return {"current" : 1 , "total" : 1 , "items" : ["a" ]}
1356+
1357+ await collect_paginated_results_async (
1358+ operation_name = "async paginated rate limit retries" ,
1359+ get_next_page = get_next_page ,
1360+ get_current_page_batch = lambda response : response ["current" ],
1361+ get_total_page_batches = lambda response : response ["total" ],
1362+ on_page_success = lambda response : collected .extend (response ["items" ]),
1363+ max_wait_seconds = 1.0 ,
1364+ max_attempts = 5 ,
1365+ retry_delay_seconds = 0.0001 ,
1366+ )
1367+
1368+ assert collected == ["a" ]
1369+ assert attempts ["count" ] == 3
1370+
1371+ asyncio .run (run ())
1372+
1373+
12531374def test_wait_for_job_result_returns_fetched_value ():
12541375 status_values = iter (["running" , "completed" ])
12551376
@@ -1291,6 +1412,31 @@ def fetch_result() -> dict:
12911412 assert fetch_attempts ["count" ] == 1
12921413
12931414
1415+ def test_wait_for_job_result_retries_rate_limit_fetch_errors ():
1416+ fetch_attempts = {"count" : 0 }
1417+
1418+ def fetch_result () -> dict :
1419+ fetch_attempts ["count" ] += 1
1420+ if fetch_attempts ["count" ] < 3 :
1421+ raise HyperbrowserError ("rate limited" , status_code = 429 )
1422+ return {"ok" : True }
1423+
1424+ result = wait_for_job_result (
1425+ operation_name = "sync wait helper rate limit retries" ,
1426+ get_status = lambda : "completed" ,
1427+ is_terminal_status = lambda value : value == "completed" ,
1428+ fetch_result = fetch_result ,
1429+ poll_interval_seconds = 0.0001 ,
1430+ max_wait_seconds = 1.0 ,
1431+ max_status_failures = 2 ,
1432+ fetch_max_attempts = 5 ,
1433+ fetch_retry_delay_seconds = 0.0001 ,
1434+ )
1435+
1436+ assert result == {"ok" : True }
1437+ assert fetch_attempts ["count" ] == 3
1438+
1439+
12941440def test_wait_for_job_result_async_returns_fetched_value ():
12951441 async def run () -> None :
12961442 status_values = iter (["running" , "completed" ])
@@ -1338,6 +1484,34 @@ async def fetch_result() -> dict:
13381484 asyncio .run (run ())
13391485
13401486
1487+ def test_wait_for_job_result_async_retries_rate_limit_fetch_errors ():
1488+ async def run () -> None :
1489+ fetch_attempts = {"count" : 0 }
1490+
1491+ async def fetch_result () -> dict :
1492+ fetch_attempts ["count" ] += 1
1493+ if fetch_attempts ["count" ] < 3 :
1494+ raise HyperbrowserError ("rate limited" , status_code = 429 )
1495+ return {"ok" : True }
1496+
1497+ result = await wait_for_job_result_async (
1498+ operation_name = "async wait helper rate limit retries" ,
1499+ get_status = lambda : asyncio .sleep (0 , result = "completed" ),
1500+ is_terminal_status = lambda value : value == "completed" ,
1501+ fetch_result = fetch_result ,
1502+ poll_interval_seconds = 0.0001 ,
1503+ max_wait_seconds = 1.0 ,
1504+ max_status_failures = 2 ,
1505+ fetch_max_attempts = 5 ,
1506+ fetch_retry_delay_seconds = 0.0001 ,
1507+ )
1508+
1509+ assert result == {"ok" : True }
1510+ assert fetch_attempts ["count" ] == 3
1511+
1512+ asyncio .run (run ())
1513+
1514+
13411515def test_wait_for_job_result_validates_configuration ():
13421516 with pytest .raises (HyperbrowserError , match = "max_attempts must be at least 1" ):
13431517 wait_for_job_result (
0 commit comments