Skip to content

Commit 003db16

Browse files
Add shared polling utilities and timeout-aware wait flows
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent bd06490 commit 003db16

23 files changed

Lines changed: 669 additions & 535 deletions
Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import asyncio
21
import jsonref
2+
from typing import Optional
33

44
from hyperbrowser.exceptions import HyperbrowserError
5+
from ....polling import poll_until_terminal_status_async, retry_operation_async
56

67
from .....models import (
78
POLLING_ATTEMPTS,
@@ -20,16 +21,18 @@ def __init__(self, client):
2021
async def start(
2122
self, params: StartBrowserUseTaskParams
2223
) -> StartBrowserUseTaskResponse:
23-
if params.output_model_schema:
24-
if hasattr(params.output_model_schema, "model_json_schema"):
25-
params.output_model_schema = jsonref.replace_refs(
26-
params.output_model_schema.model_json_schema(),
27-
proxies=False,
28-
lazy_load=False,
29-
)
24+
payload = params.model_dump(exclude_none=True, by_alias=True)
25+
if params.output_model_schema and hasattr(
26+
params.output_model_schema, "model_json_schema"
27+
):
28+
payload["outputModelSchema"] = jsonref.replace_refs(
29+
params.output_model_schema.model_json_schema(),
30+
proxies=False,
31+
lazy_load=False,
32+
)
3033
response = await self._client.transport.post(
3134
self._client._build_url("/task/browser-use"),
32-
data=params.model_dump(exclude_none=True, by_alias=True),
35+
data=payload,
3336
)
3437
return StartBrowserUseTaskResponse(**response.data)
3538

@@ -52,28 +55,27 @@ async def stop(self, job_id: str) -> BasicResponse:
5255
return BasicResponse(**response.data)
5356

5457
async def start_and_wait(
55-
self, params: StartBrowserUseTaskParams
58+
self,
59+
params: StartBrowserUseTaskParams,
60+
poll_interval_seconds: float = 2.0,
61+
max_wait_seconds: Optional[float] = 600.0,
5662
) -> BrowserUseTaskResponse:
5763
job_start_resp = await self.start(params)
5864
job_id = job_start_resp.job_id
5965
if not job_id:
6066
raise HyperbrowserError("Failed to start browser-use task job")
6167

62-
failures = 0
63-
while True:
64-
try:
65-
job_response = await self.get_status(job_id)
66-
if (
67-
job_response.status == "completed"
68-
or job_response.status == "failed"
69-
or job_response.status == "stopped"
70-
):
71-
return await self.get(job_id)
72-
failures = 0
73-
except Exception as e:
74-
failures += 1
75-
if failures >= POLLING_ATTEMPTS:
76-
raise HyperbrowserError(
77-
f"Failed to poll browser-use task job {job_id} after {POLLING_ATTEMPTS} attempts: {e}"
78-
)
79-
await asyncio.sleep(2)
68+
await poll_until_terminal_status_async(
69+
operation_name=f"browser-use task job {job_id}",
70+
get_status=lambda: self.get_status(job_id).status,
71+
is_terminal_status=lambda status: status
72+
in {"completed", "failed", "stopped"},
73+
poll_interval_seconds=poll_interval_seconds,
74+
max_wait_seconds=max_wait_seconds,
75+
)
76+
return await retry_operation_async(
77+
operation_name=f"Fetching browser-use task job {job_id}",
78+
operation=lambda: self.get(job_id),
79+
max_attempts=POLLING_ATTEMPTS,
80+
retry_delay_seconds=0.5,
81+
)

hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import asyncio
1+
from typing import Optional
22

33
from hyperbrowser.exceptions import HyperbrowserError
4+
from ....polling import poll_until_terminal_status_async, retry_operation_async
45

56
from .....models import (
67
POLLING_ATTEMPTS,
@@ -44,28 +45,27 @@ async def stop(self, job_id: str) -> BasicResponse:
4445
return BasicResponse(**response.data)
4546

4647
async def start_and_wait(
47-
self, params: StartClaudeComputerUseTaskParams
48+
self,
49+
params: StartClaudeComputerUseTaskParams,
50+
poll_interval_seconds: float = 2.0,
51+
max_wait_seconds: Optional[float] = 600.0,
4852
) -> ClaudeComputerUseTaskResponse:
4953
job_start_resp = await self.start(params)
5054
job_id = job_start_resp.job_id
5155
if not job_id:
5256
raise HyperbrowserError("Failed to start Claude Computer Use task job")
5357

54-
failures = 0
55-
while True:
56-
try:
57-
job_response = await self.get_status(job_id)
58-
if (
59-
job_response.status == "completed"
60-
or job_response.status == "failed"
61-
or job_response.status == "stopped"
62-
):
63-
return await self.get(job_id)
64-
failures = 0
65-
except Exception as e:
66-
failures += 1
67-
if failures >= POLLING_ATTEMPTS:
68-
raise HyperbrowserError(
69-
f"Failed to poll Claude Computer Use task job {job_id} after {POLLING_ATTEMPTS} attempts: {e}"
70-
)
71-
await asyncio.sleep(2)
58+
await poll_until_terminal_status_async(
59+
operation_name=f"Claude Computer Use task job {job_id}",
60+
get_status=lambda: self.get_status(job_id).status,
61+
is_terminal_status=lambda status: status
62+
in {"completed", "failed", "stopped"},
63+
poll_interval_seconds=poll_interval_seconds,
64+
max_wait_seconds=max_wait_seconds,
65+
)
66+
return await retry_operation_async(
67+
operation_name=f"Fetching Claude Computer Use task job {job_id}",
68+
operation=lambda: self.get(job_id),
69+
max_attempts=POLLING_ATTEMPTS,
70+
retry_delay_seconds=0.5,
71+
)

hyperbrowser/client/managers/async_manager/agents/cua.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import asyncio
1+
from typing import Optional
22

33
from hyperbrowser.exceptions import HyperbrowserError
4+
from ....polling import poll_until_terminal_status_async, retry_operation_async
45

56
from .....models import (
67
POLLING_ATTEMPTS,
@@ -41,27 +42,28 @@ async def stop(self, job_id: str) -> BasicResponse:
4142
)
4243
return BasicResponse(**response.data)
4344

44-
async def start_and_wait(self, params: StartCuaTaskParams) -> CuaTaskResponse:
45+
async def start_and_wait(
46+
self,
47+
params: StartCuaTaskParams,
48+
poll_interval_seconds: float = 2.0,
49+
max_wait_seconds: Optional[float] = 600.0,
50+
) -> CuaTaskResponse:
4551
job_start_resp = await self.start(params)
4652
job_id = job_start_resp.job_id
4753
if not job_id:
4854
raise HyperbrowserError("Failed to start CUA task job")
4955

50-
failures = 0
51-
while True:
52-
try:
53-
job_response = await self.get_status(job_id)
54-
if (
55-
job_response.status == "completed"
56-
or job_response.status == "failed"
57-
or job_response.status == "stopped"
58-
):
59-
return await self.get(job_id)
60-
failures = 0
61-
except Exception as e:
62-
failures += 1
63-
if failures >= POLLING_ATTEMPTS:
64-
raise HyperbrowserError(
65-
f"Failed to poll CUA task job {job_id} after {POLLING_ATTEMPTS} attempts: {e}"
66-
)
67-
await asyncio.sleep(2)
56+
await poll_until_terminal_status_async(
57+
operation_name=f"CUA task job {job_id}",
58+
get_status=lambda: self.get_status(job_id).status,
59+
is_terminal_status=lambda status: status
60+
in {"completed", "failed", "stopped"},
61+
poll_interval_seconds=poll_interval_seconds,
62+
max_wait_seconds=max_wait_seconds,
63+
)
64+
return await retry_operation_async(
65+
operation_name=f"Fetching CUA task job {job_id}",
66+
operation=lambda: self.get(job_id),
67+
max_attempts=POLLING_ATTEMPTS,
68+
retry_delay_seconds=0.5,
69+
)

hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import asyncio
1+
from typing import Optional
22

33
from hyperbrowser.exceptions import HyperbrowserError
4+
from ....polling import poll_until_terminal_status_async, retry_operation_async
45

56
from .....models import (
67
POLLING_ATTEMPTS,
@@ -44,28 +45,27 @@ async def stop(self, job_id: str) -> BasicResponse:
4445
return BasicResponse(**response.data)
4546

4647
async def start_and_wait(
47-
self, params: StartGeminiComputerUseTaskParams
48+
self,
49+
params: StartGeminiComputerUseTaskParams,
50+
poll_interval_seconds: float = 2.0,
51+
max_wait_seconds: Optional[float] = 600.0,
4852
) -> GeminiComputerUseTaskResponse:
4953
job_start_resp = await self.start(params)
5054
job_id = job_start_resp.job_id
5155
if not job_id:
5256
raise HyperbrowserError("Failed to start Gemini Computer Use task job")
5357

54-
failures = 0
55-
while True:
56-
try:
57-
job_response = await self.get_status(job_id)
58-
if (
59-
job_response.status == "completed"
60-
or job_response.status == "failed"
61-
or job_response.status == "stopped"
62-
):
63-
return await self.get(job_id)
64-
failures = 0
65-
except Exception as e:
66-
failures += 1
67-
if failures >= POLLING_ATTEMPTS:
68-
raise HyperbrowserError(
69-
f"Failed to poll Gemini Computer Use task job {job_id} after {POLLING_ATTEMPTS} attempts: {e}"
70-
)
71-
await asyncio.sleep(2)
58+
await poll_until_terminal_status_async(
59+
operation_name=f"Gemini Computer Use task job {job_id}",
60+
get_status=lambda: self.get_status(job_id).status,
61+
is_terminal_status=lambda status: status
62+
in {"completed", "failed", "stopped"},
63+
poll_interval_seconds=poll_interval_seconds,
64+
max_wait_seconds=max_wait_seconds,
65+
)
66+
return await retry_operation_async(
67+
operation_name=f"Fetching Gemini Computer Use task job {job_id}",
68+
operation=lambda: self.get(job_id),
69+
max_attempts=POLLING_ATTEMPTS,
70+
retry_delay_seconds=0.5,
71+
)

hyperbrowser/client/managers/async_manager/agents/hyper_agent.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import asyncio
1+
from typing import Optional
22

33
from hyperbrowser.exceptions import HyperbrowserError
4+
from ....polling import poll_until_terminal_status_async, retry_operation_async
45

56
from .....models import (
67
POLLING_ATTEMPTS,
@@ -44,28 +45,27 @@ async def stop(self, job_id: str) -> BasicResponse:
4445
return BasicResponse(**response.data)
4546

4647
async def start_and_wait(
47-
self, params: StartHyperAgentTaskParams
48+
self,
49+
params: StartHyperAgentTaskParams,
50+
poll_interval_seconds: float = 2.0,
51+
max_wait_seconds: Optional[float] = 600.0,
4852
) -> HyperAgentTaskResponse:
4953
job_start_resp = await self.start(params)
5054
job_id = job_start_resp.job_id
5155
if not job_id:
5256
raise HyperbrowserError("Failed to start HyperAgent task")
5357

54-
failures = 0
55-
while True:
56-
try:
57-
job_response = await self.get_status(job_id)
58-
if (
59-
job_response.status == "completed"
60-
or job_response.status == "failed"
61-
or job_response.status == "stopped"
62-
):
63-
return await self.get(job_id)
64-
failures = 0
65-
except Exception as e:
66-
failures += 1
67-
if failures >= POLLING_ATTEMPTS:
68-
raise HyperbrowserError(
69-
f"Failed to poll HyperAgent task {job_id} after {POLLING_ATTEMPTS} attempts: {e}"
70-
)
71-
await asyncio.sleep(2)
58+
await poll_until_terminal_status_async(
59+
operation_name=f"HyperAgent task {job_id}",
60+
get_status=lambda: self.get_status(job_id).status,
61+
is_terminal_status=lambda status: status
62+
in {"completed", "failed", "stopped"},
63+
poll_interval_seconds=poll_interval_seconds,
64+
max_wait_seconds=max_wait_seconds,
65+
)
66+
return await retry_operation_async(
67+
operation_name=f"Fetching HyperAgent task {job_id}",
68+
operation=lambda: self.get(job_id),
69+
max_attempts=POLLING_ATTEMPTS,
70+
retry_delay_seconds=0.5,
71+
)

hyperbrowser/client/managers/async_manager/crawl.py

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import asyncio
2+
import time
23
from typing import Optional
34

45
from hyperbrowser.models.consts import POLLING_ATTEMPTS
6+
from ...polling import (
7+
has_exceeded_max_wait,
8+
poll_until_terminal_status_async,
9+
retry_operation_async,
10+
)
511
from ....models.crawl import (
612
CrawlJobResponse,
7-
CrawlJobStatus,
813
CrawlJobStatusResponse,
914
GetCrawlJobParams,
1015
StartCrawlJobParams,
@@ -41,43 +46,35 @@ async def get(
4146
return CrawlJobResponse(**response.data)
4247

4348
async def start_and_wait(
44-
self, params: StartCrawlJobParams, return_all_pages: bool = True
49+
self,
50+
params: StartCrawlJobParams,
51+
return_all_pages: bool = True,
52+
poll_interval_seconds: float = 2.0,
53+
max_wait_seconds: Optional[float] = 600.0,
4554
) -> CrawlJobResponse:
4655
job_start_resp = await self.start(params)
4756
job_id = job_start_resp.job_id
4857
if not job_id:
4958
raise HyperbrowserError("Failed to start crawl job")
5059

51-
job_status: CrawlJobStatus = "pending"
52-
failures = 0
53-
while True:
54-
try:
55-
job_status_resp = await self.get_status(job_id)
56-
job_status = job_status_resp.status
57-
if job_status == "completed" or job_status == "failed":
58-
break
59-
except Exception as e:
60-
failures += 1
61-
if failures >= POLLING_ATTEMPTS:
62-
raise HyperbrowserError(
63-
f"Failed to poll crawl job {job_id} after {POLLING_ATTEMPTS} attempts: {e}"
64-
)
65-
await asyncio.sleep(2)
60+
job_status = await poll_until_terminal_status_async(
61+
operation_name=f"crawl job {job_id}",
62+
get_status=lambda: self.get_status(job_id).status,
63+
is_terminal_status=lambda status: status in {"completed", "failed"},
64+
poll_interval_seconds=poll_interval_seconds,
65+
max_wait_seconds=max_wait_seconds,
66+
)
6667

67-
failures = 0
6868
if not return_all_pages:
69-
while True:
70-
try:
71-
return await self.get(job_id)
72-
except Exception as e:
73-
failures += 1
74-
if failures >= POLLING_ATTEMPTS:
75-
raise HyperbrowserError(
76-
f"Failed to get crawl job {job_id} after {POLLING_ATTEMPTS} attempts: {e}"
77-
)
78-
await asyncio.sleep(0.5)
69+
return await retry_operation_async(
70+
operation_name=f"Fetching crawl job {job_id}",
71+
operation=lambda: self.get(job_id),
72+
max_attempts=POLLING_ATTEMPTS,
73+
retry_delay_seconds=0.5,
74+
)
7975

8076
failures = 0
77+
page_fetch_start_time = time.monotonic()
8178
job_response = CrawlJobResponse(
8279
jobId=job_id,
8380
status=job_status,
@@ -92,6 +89,10 @@ async def start_and_wait(
9289
first_check
9390
or job_response.current_page_batch < job_response.total_page_batches
9491
):
92+
if has_exceeded_max_wait(page_fetch_start_time, max_wait_seconds):
93+
raise HyperbrowserError(
94+
f"Timed out fetching all pages for crawl job {job_id} after {max_wait_seconds} seconds"
95+
)
9596
try:
9697
tmp_job_response = await self.get(
9798
job_start_resp.job_id,

0 commit comments

Comments
 (0)