smooth-py 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of smooth-py might be problematic. Click here for more details.

@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.3
2
+ Name: smooth-py
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: Luca Pinchetti
6
+ Author-email: pincoluca1@gmail.com
7
+ Requires-Python: >=3.10
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: httpx (>=0.28.1,<0.29.0)
14
+ Requires-Dist: pydantic (>=2.11.7,<3.0.0)
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Smooth Python SDK
18
+
19
+ The Smooth Python SDK provides a convenient way to interact with the Smooth API for programmatic browser automation and task execution. This SDK includes both synchronous and asynchronous clients to suit different programming needs.
20
+
21
+ ## Features
22
+
23
+ * **Synchronous and Asynchronous Clients**: Choose between `SyncClient` for traditional sequential programming and `AsyncClient` for high-performance asynchronous applications.
24
+ * **Task Management**: Easily run tasks, check their status, and retrieve results.
25
+ * **Interactive Browser Sessions**: Get access to and manage interactive browser sessions.
26
+
27
+ ## Installation
28
+
29
+ You can install the Smooth Python SDK using pip:
30
+
31
+ ```bash
32
+ pip install smooth-py
33
+ ```
34
+
35
+ ## Authentication
36
+
37
+ The SDK requires an API key for authentication. You can provide the API key in two ways:
38
+
39
+ 1. **Directly in the client constructor**:
40
+
41
+ ```python
42
+ from smooth import SyncClient
43
+
44
+ client = SyncClient(api_key="YOUR_API_KEY")
45
+ ```
46
+
47
+ 2. **As an environment variable**:
48
+
49
+ Set the `SMOOTH_API_KEY` environment variable, and the client will automatically use it.
50
+
51
+ ```bash
52
+ export SMOOTH_API_KEY="YOUR_API_KEY"
53
+ ```
54
+
55
+ ```python
56
+ from smooth import SyncClient
57
+
58
+ # The client will pick up the API key from the environment variable
59
+ client = SyncClient()
60
+ ```
61
+
62
+ ## Usage
63
+
64
+ ### Synchronous Client
65
+
66
+ The `SyncClient` is ideal for scripts and applications that don't require asynchronous operations.
67
+
68
+ #### Running a Task and Waiting for the Result
69
+
70
+ ```python
71
+ from smooth import SyncClient, TaskRequest
72
+
73
+ with SyncClient() as client:
74
+ task_payload = TaskRequest(
75
+ task="Go to https://www.google.com and search for 'Smooth SDK'"
76
+ )
77
+
78
+ try:
79
+ completed_task = client.run_and_wait_for_task(task_payload)
80
+
81
+ if completed_task.result:
82
+ print("Task Result:", completed_task.result)
83
+ else:
84
+ print("Task Error:", completed_task.error)
85
+
86
+ except TimeoutError:
87
+ print("The task timed out.")
88
+ except ApiError as e:
89
+ print(f"An API error occurred: {e}")
90
+ ```
91
+
92
+ #### Managing Browser Sessions
93
+
94
+ ```python
95
+ from smooth import SyncClient
96
+
97
+ with SyncClient() as client:
98
+ # Get a new browser session
99
+ browser_session = client.get_browser(session_name="my-test-session")
100
+ print("Live URL:", browser_session.live_url)
101
+ print("Session ID:", browser_session.session_id)
102
+
103
+ # List all browser sessions
104
+ sessions = client.list_sessions()
105
+ print("All Session IDs:", sessions.session_ids)
106
+ ```
107
+
108
+ ### Asynchronous Client
109
+
110
+ The `AsyncClient` is designed for use in asynchronous applications, such as those built with `asyncio`, to handle multiple operations concurrently without blocking.
111
+
112
+ #### Running a Task and Waiting for the Result
113
+
114
+ ```python
115
+ import asyncio
116
+ from smooth import AsyncClient, TaskRequest
117
+
118
+ async def main():
119
+ async with AsyncClient() as client:
120
+ task_payload = TaskRequest(
121
+ task="Go to Github and search for \"smooth-sdk\""
122
+ )
123
+
124
+ try:
125
+ completed_task = await client.run_and_wait_for_task(task_payload)
126
+
127
+ if completed_task.result:
128
+ print("Task Result:", completed_task.result)
129
+ else:
130
+ print("Task Error:", completed_task.error)
131
+
132
+ except TimeoutError:
133
+ print("The task timed out.")
134
+ except ApiError as e:
135
+ print(f"An API error occurred: {e}")
136
+
137
+ if __name__ == "__main__":
138
+ asyncio.run(main())
139
+ ```
140
+
141
+ #### Managing Browser Sessions
142
+
143
+ ```python
144
+ import asyncio
145
+ from smooth import AsyncClient
146
+
147
+ async def main():
148
+ async with AsyncClient() as client:
149
+ # Get a new browser session
150
+ browser_session = await client.get_browser(session_name="my-async-session")
151
+ print("Live URL:", browser_session.live_url)
152
+ print("Session ID:", browser_session.session_id)
153
+
154
+ # List all browser sessions
155
+ sessions = await client.list_sessions()
156
+ print("All Session IDs:", sessions.session_ids)
157
+
158
+ if __name__ == "__main__":
159
+ asyncio.run(main())
160
+ ```
161
+
@@ -0,0 +1,144 @@
1
+ # Smooth Python SDK
2
+
3
+ The Smooth Python SDK provides a convenient way to interact with the Smooth API for programmatic browser automation and task execution. This SDK includes both synchronous and asynchronous clients to suit different programming needs.
4
+
5
+ ## Features
6
+
7
+ * **Synchronous and Asynchronous Clients**: Choose between `SyncClient` for traditional sequential programming and `AsyncClient` for high-performance asynchronous applications.
8
+ * **Task Management**: Easily run tasks, check their status, and retrieve results.
9
+ * **Interactive Browser Sessions**: Get access to and manage interactive browser sessions.
10
+
11
+ ## Installation
12
+
13
+ You can install the Smooth Python SDK using pip:
14
+
15
+ ```bash
16
+ pip install smooth-py
17
+ ```
18
+
19
+ ## Authentication
20
+
21
+ The SDK requires an API key for authentication. You can provide the API key in two ways:
22
+
23
+ 1. **Directly in the client constructor**:
24
+
25
+ ```python
26
+ from smooth import SyncClient
27
+
28
+ client = SyncClient(api_key="YOUR_API_KEY")
29
+ ```
30
+
31
+ 2. **As an environment variable**:
32
+
33
+ Set the `SMOOTH_API_KEY` environment variable, and the client will automatically use it.
34
+
35
+ ```bash
36
+ export SMOOTH_API_KEY="YOUR_API_KEY"
37
+ ```
38
+
39
+ ```python
40
+ from smooth import SyncClient
41
+
42
+ # The client will pick up the API key from the environment variable
43
+ client = SyncClient()
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ### Synchronous Client
49
+
50
+ The `SyncClient` is ideal for scripts and applications that don't require asynchronous operations.
51
+
52
+ #### Running a Task and Waiting for the Result
53
+
54
+ ```python
55
+ from smooth import SyncClient, TaskRequest
56
+
57
+ with SyncClient() as client:
58
+ task_payload = TaskRequest(
59
+ task="Go to https://www.google.com and search for 'Smooth SDK'"
60
+ )
61
+
62
+ try:
63
+ completed_task = client.run_and_wait_for_task(task_payload)
64
+
65
+ if completed_task.result:
66
+ print("Task Result:", completed_task.result)
67
+ else:
68
+ print("Task Error:", completed_task.error)
69
+
70
+ except TimeoutError:
71
+ print("The task timed out.")
72
+ except ApiError as e:
73
+ print(f"An API error occurred: {e}")
74
+ ```
75
+
76
+ #### Managing Browser Sessions
77
+
78
+ ```python
79
+ from smooth import SyncClient
80
+
81
+ with SyncClient() as client:
82
+ # Get a new browser session
83
+ browser_session = client.get_browser(session_name="my-test-session")
84
+ print("Live URL:", browser_session.live_url)
85
+ print("Session ID:", browser_session.session_id)
86
+
87
+ # List all browser sessions
88
+ sessions = client.list_sessions()
89
+ print("All Session IDs:", sessions.session_ids)
90
+ ```
91
+
92
+ ### Asynchronous Client
93
+
94
+ The `AsyncClient` is designed for use in asynchronous applications, such as those built with `asyncio`, to handle multiple operations concurrently without blocking.
95
+
96
+ #### Running a Task and Waiting for the Result
97
+
98
+ ```python
99
+ import asyncio
100
+ from smooth import AsyncClient, TaskRequest
101
+
102
+ async def main():
103
+ async with AsyncClient() as client:
104
+ task_payload = TaskRequest(
105
+ task="Go to Github and search for \"smooth-sdk\""
106
+ )
107
+
108
+ try:
109
+ completed_task = await client.run_and_wait_for_task(task_payload)
110
+
111
+ if completed_task.result:
112
+ print("Task Result:", completed_task.result)
113
+ else:
114
+ print("Task Error:", completed_task.error)
115
+
116
+ except TimeoutError:
117
+ print("The task timed out.")
118
+ except ApiError as e:
119
+ print(f"An API error occurred: {e}")
120
+
121
+ if __name__ == "__main__":
122
+ asyncio.run(main())
123
+ ```
124
+
125
+ #### Managing Browser Sessions
126
+
127
+ ```python
128
+ import asyncio
129
+ from smooth import AsyncClient
130
+
131
+ async def main():
132
+ async with AsyncClient() as client:
133
+ # Get a new browser session
134
+ browser_session = await client.get_browser(session_name="my-async-session")
135
+ print("Live URL:", browser_session.live_url)
136
+ print("Session ID:", browser_session.session_id)
137
+
138
+ # List all browser sessions
139
+ sessions = await client.list_sessions()
140
+ print("All Session IDs:", sessions.session_ids)
141
+
142
+ if __name__ == "__main__":
143
+ asyncio.run(main())
144
+ ```
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "smooth-py"
3
+ version = "0.1.0"
4
+ description = ""
5
+ authors = [
6
+ {name = "Luca Pinchetti",email = "pincoluca1@gmail.com"}
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "pydantic (>=2.11.7,<3.0.0)",
12
+ "httpx (>=0.28.1,<0.29.0)"
13
+ ]
14
+
15
+ [tool.poetry]
16
+ packages = [{include = "smooth", from = "src"}]
17
+
18
+
19
+ [build-system]
20
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
21
+ build-backend = "poetry.core.masonry.api"
22
+
23
+
24
+ [tool.ruff]
25
+ line-length = 128
26
+ indent-width = 2
27
+
28
+ [tool.ruff.lint]
29
+ select = [
30
+ "E", # pycodestyle errors
31
+ "W", # pycodestyle warnings
32
+ "F", # pyflakes
33
+ "I", # isort
34
+ "B", # flake8-bugbear
35
+ "C4", # flake8-comprehensions
36
+ "N", # PEP8 naming convetions
37
+ "D" # pydocstyle
38
+ ]
39
+ ignore = [
40
+ "C901", # too complex
41
+ "W191", # indentation contains tabs
42
+ "D401", # imperative mood
43
+ "B008" # fastapi
44
+ ]
45
+
46
+ [tool.ruff.lint.pydocstyle]
47
+ convention = "google"
48
+
49
+ [tool.pyright]
50
+ typeCheckingMode = "strict"
51
+ reportMissingTypeStubs = false
52
+ reportIncompatibleMethodOverride = false
@@ -0,0 +1,435 @@
1
+ """Smooth python SDK."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import os
6
+ import time
7
+ from typing import Any, Dict, Literal, Optional, TypeVar, Union
8
+
9
+ import httpx
10
+ import requests
11
+ from pydantic import BaseModel, ConfigDict, Field
12
+
13
+ # Configure logging
14
+ logger = logging.getLogger("smooth")
15
+
16
+
17
+ BASE_URL = "https://api2.circlemind.co/api/"
18
+
19
+ # --- Models ---
20
+ # These models define the data structures for API requests and responses.
21
+
22
+
23
+ class TaskData(BaseModel):
24
+ """Task data model."""
25
+
26
+ result: Any | None = Field(default=None, description="The result of the task if successful.")
27
+ error: str | None = Field(default=None, description="Error message if the task failed.")
28
+ credits_used: int | None = Field(default=None, description="The amount of credits used to perform the task.")
29
+ src: str | None = Field(default=None, description="")
30
+
31
+
32
+ class TaskResponse(BaseModel):
33
+ """Task response model."""
34
+
35
+ model_config = ConfigDict(extra="forbid")
36
+
37
+ id: str = Field(description="The ID of the task.")
38
+ status: str = Field(default="RUNNING", description="The status of the task.")
39
+ data: TaskData = Field(default_factory=lambda: TaskData(), description="The data associated with the task.")
40
+
41
+
42
+ class TaskRequest(BaseModel):
43
+ """Run task request model."""
44
+
45
+ model_config = ConfigDict(extra="forbid")
46
+
47
+ task: str = Field(description="The task to run.")
48
+ agent: Literal["smooth"] = Field(default="smooth", description="The agent to use for the task.")
49
+ max_steps: int = Field(default=32, ge=1, le=64, description="Maximum number of steps the agent can take (max 64).")
50
+ device: Literal["desktop", "mobile"] = Field(default="mobile", description="Device type for the task. Default is mobile.")
51
+ session_id: Optional[str] = Field(
52
+ default=None,
53
+ description="(optional) Browser session ID to use. Each session maintains its own state, such as login credentials.",
54
+ )
55
+ stealth_mode: bool = Field(default=False, description="(optional) Run the browser in stealth mode.")
56
+ proxy_server: Optional[str] = Field(default=None, description="(optional) Proxy server URL.")
57
+ proxy_username: Optional[str] = Field(default=None, description="(optional) Proxy server username.")
58
+ proxy_password: Optional[str] = Field(default=None, description="(optional) Proxy server password.")
59
+
60
+
61
+ class BrowserResponse(BaseModel):
62
+ """Browser session response model."""
63
+
64
+ model_config = ConfigDict(extra="forbid")
65
+
66
+ live_url: str = Field(description="The live URL to interact with the browser session.")
67
+ session_id: str = Field(description="The ID of the browser session associated with the opened browser instance.")
68
+
69
+
70
+ class BrowserSessionsResponse(BaseModel):
71
+ """Response model for listing browser sessions."""
72
+
73
+ session_ids: list[str] = Field(description="The IDs of the browser sessions.")
74
+ session_names: list[str | None] = Field(
75
+ description="The names of the browser sessions (only useful to uniquely identify them)."
76
+ )
77
+
78
+
79
+ T = TypeVar("T")
80
+
81
+
82
+ # --- Exception Handling ---
83
+
84
+
85
+ class ApiError(Exception):
86
+ """Custom exception for API errors."""
87
+
88
+ def __init__(self, status_code: int, detail: str, response_data: Optional[Dict[str, Any]] = None):
89
+ """Initializes the API error."""
90
+ self.status_code = status_code
91
+ self.detail = detail
92
+ self.response_data = response_data
93
+ super().__init__(f"API Error {status_code}: {detail}")
94
+
95
+
96
+ class TimeoutError(Exception):
97
+ """Custom exception for task timeouts."""
98
+
99
+ pass
100
+
101
+
102
+ # --- Base Client ---
103
+
104
+
105
+ class BaseClient:
106
+ """Base client for handling common API interactions."""
107
+
108
+ def __init__(self, api_key: Optional[str] = None, base_url: str = BASE_URL, api_version: str = "v1"):
109
+ """Initializes the base client."""
110
+ # Try to get API key from environment if not provided
111
+ if not api_key:
112
+ api_key = os.getenv("SMOOTH_API_KEY")
113
+
114
+ if not api_key:
115
+ raise ValueError("API key is required. Provide it directly or set SMOOTH_API_KEY environment variable.")
116
+
117
+ if not base_url:
118
+ raise ValueError("Base URL cannot be empty.")
119
+
120
+ self.api_key = api_key
121
+ self.base_url = f"{base_url.rstrip('/')}/{api_version}"
122
+ self.headers = {
123
+ "Authorization": f"Bearer {self.api_key}",
124
+ "Content-Type": "application/json",
125
+ "User-Agent": "smooth-python-sdk/0.1.0",
126
+ }
127
+
128
+ def _handle_response(self, response: Union[requests.Response, httpx.Response]) -> dict:
129
+ """Handles HTTP responses and raises exceptions for errors."""
130
+ if 200 <= response.status_code < 300:
131
+ try:
132
+ return response.json()
133
+ except ValueError as e:
134
+ logger.error(f"Failed to parse JSON response: {e}")
135
+ raise ApiError(status_code=response.status_code, detail="Invalid JSON response from server") from None
136
+
137
+ # Handle error responses
138
+ try:
139
+ error_data = response.json()
140
+ detail = error_data.get("detail", response.text)
141
+ except ValueError:
142
+ detail = response.text or f"HTTP {response.status_code} error"
143
+
144
+ logger.error(f"API error: {response.status_code} - {detail}")
145
+ raise ApiError(
146
+ status_code=response.status_code, detail=detail, response_data=error_data if "error_data" in locals() else None
147
+ )
148
+
149
+
150
+ # --- Synchronous Client ---
151
+
152
+
153
+ class SyncClient(BaseClient):
154
+ """A synchronous client for the API."""
155
+
156
+ def __init__(self, api_key: Optional[str] = None, base_url: str = BASE_URL, api_version: str = "v1"):
157
+ """Initializes the synchronous client."""
158
+ super().__init__(api_key, base_url, api_version)
159
+ self._session = requests.Session()
160
+ self._session.headers.update(self.headers)
161
+
162
+ def __enter__(self):
163
+ """Enters the synchronous context manager."""
164
+ return self
165
+
166
+ def __exit__(self, exc_type, exc_val, exc_tb):
167
+ """Exits the synchronous context manager."""
168
+ self.close()
169
+
170
+ def close(self):
171
+ """Close the session."""
172
+ if hasattr(self, "_session"):
173
+ self._session.close()
174
+
175
+ def run_task(self, payload: TaskRequest) -> TaskResponse:
176
+ """Submits a task to be run.
177
+
178
+ Args:
179
+ payload: The request object containing task details.
180
+
181
+ Returns:
182
+ The initial response for the submitted task.
183
+
184
+ Raises:
185
+ ApiException: If the API request fails.
186
+ """
187
+ try:
188
+ response = self._session.post(f"{self.base_url}/task", json=payload.model_dump(exclude_none=True))
189
+ data = self._handle_response(response)
190
+ return TaskResponse(**data["r"])
191
+ except requests.exceptions.RequestException as e:
192
+ logger.error(f"Request failed: {e}")
193
+ raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
194
+
195
+ def get_task(self, task_id: str) -> TaskResponse:
196
+ """Retrieves the status and result of a task.
197
+
198
+ Args:
199
+ task_id: The ID of the task to retrieve.
200
+
201
+ Returns:
202
+ The current status and data of the task.
203
+
204
+ Raises:
205
+ ApiException: If the API request fails.
206
+ ValueError: If task_id is empty.
207
+ """
208
+ if not task_id:
209
+ raise ValueError("Task ID cannot be empty.")
210
+
211
+ try:
212
+ response = self._session.get(f"{self.base_url}/task/{task_id}")
213
+ data = self._handle_response(response)
214
+ return TaskResponse(**data["r"])
215
+ except requests.exceptions.RequestException as e:
216
+ logger.error(f"Request failed: {e}")
217
+ raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
218
+
219
+ def run_and_wait_for_task(self, payload: TaskRequest, poll_interval: int = 1, timeout: int = 60 * 15) -> TaskResponse:
220
+ """Runs a task and waits for it to complete.
221
+
222
+ This method submits a task and then polls the get_task endpoint
223
+ until the task's status is no longer 'running' or 'waiting'.
224
+
225
+ Args:
226
+ payload: The request object containing task details.
227
+ poll_interval: The time in seconds to wait between polling for status.
228
+ timeout: The maximum time in seconds to wait for the task to complete.
229
+ progress_callback: Optional callback function called with TaskResponse on each poll.
230
+
231
+ Returns:
232
+ The final response of the completed or failed task.
233
+
234
+ Raises:
235
+ TimeoutError: If the task does not complete within the specified timeout.
236
+ ApiException: If the API request fails.
237
+ """
238
+ if poll_interval < 0.1:
239
+ raise ValueError("Poll interval must be at least 100 milliseconds.")
240
+ if timeout < 1:
241
+ raise ValueError("Timeout must be at least 1 second.")
242
+
243
+ start_time = time.time()
244
+ initial_response = self.run_task(payload)
245
+ task_id = initial_response.id
246
+
247
+ while (time.time() - start_time) < timeout:
248
+ task_response = self.get_task(task_id)
249
+
250
+ if task_response.status not in ["running", "waiting"]:
251
+ return task_response
252
+
253
+ time.sleep(poll_interval)
254
+
255
+ raise TimeoutError(f"Task {task_id} did not complete within {timeout} seconds.")
256
+
257
+ def get_browser(self, session_id: Optional[str] = None, session_name: Optional[str] = None) -> BrowserResponse:
258
+ """Gets an interactive browser instance.
259
+
260
+ Args:
261
+ session_id: The session ID to associate with the browser. If None, a new session will be created.
262
+ session_name: The name to associate to the new browser session. Ignored if a valid session_id is provided.
263
+
264
+ Returns:
265
+ The browser session details, including the live URL.
266
+
267
+ Raises:
268
+ ApiException: If the API request fails.
269
+ """
270
+ params = {}
271
+ if session_id:
272
+ params["session_id"] = session_id
273
+ if session_name:
274
+ params["session_name"] = session_name
275
+
276
+ try:
277
+ response = self._session.get(f"{self.base_url}/browser", params=params)
278
+ data = self._handle_response(response)
279
+ return BrowserResponse(**data["r"])
280
+ except requests.exceptions.RequestException as e:
281
+ logger.error(f"Request failed: {e}")
282
+ raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
283
+
284
+ def list_sessions(self) -> BrowserSessionsResponse:
285
+ """Lists all browser sessions for the user.
286
+
287
+ Returns:
288
+ A list of existing browser sessions.
289
+
290
+ Raises:
291
+ ApiException: If the API request fails.
292
+ """
293
+ try:
294
+ response = self._session.get(f"{self.base_url}/browser/session")
295
+ data = self._handle_response(response)
296
+ return BrowserSessionsResponse(**data["r"])
297
+ except requests.exceptions.RequestException as e:
298
+ logger.error(f"Request failed: {e}")
299
+ raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
300
+
301
+
302
+ # --- Asynchronous Client ---
303
+
304
+
305
+ class AsyncClient(BaseClient):
306
+ """An asynchronous client for the API."""
307
+
308
+ def __init__(self, api_key: Optional[str] = None, base_url: str = BASE_URL, api_version: str = "v1", timeout: int = 30):
309
+ """Initializes the asynchronous client."""
310
+ super().__init__(api_key, base_url, api_version)
311
+ self._client = httpx.AsyncClient(headers=self.headers, timeout=timeout)
312
+
313
+ async def __aenter__(self):
314
+ """Enters the asynchronous context manager."""
315
+ return self
316
+
317
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
318
+ """Exits the asynchronous context manager."""
319
+ await self.close()
320
+
321
+ async def run_task(self, payload: TaskRequest) -> TaskResponse:
322
+ """Submits a task to be run asynchronously.
323
+
324
+ Args:
325
+ payload: The request object containing task details.
326
+
327
+ Returns:
328
+ The initial response for the submitted task.
329
+
330
+ Raises:
331
+ ApiException: If the API request fails.
332
+ """
333
+ try:
334
+ response = await self._client.post(f"{self.base_url}/task", json=payload.model_dump(exclude_none=True))
335
+ data = self._handle_response(response)
336
+ return TaskResponse(**data["r"])
337
+ except httpx.RequestError as e:
338
+ logger.error(f"Request failed: {e}")
339
+ raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
340
+
341
+ async def get_task(self, task_id: str) -> TaskResponse:
342
+ """Retrieves the status and result of a task asynchronously.
343
+
344
+ Args:
345
+ task_id: The ID of the task to retrieve.
346
+
347
+ Returns:
348
+ The current status and data of the task.
349
+
350
+ Raises:
351
+ ApiException: If the API request fails.
352
+ ValueError: If task_id is empty.
353
+ """
354
+ if not task_id:
355
+ raise ValueError("Task ID cannot be empty.")
356
+
357
+ try:
358
+ response = await self._client.get(f"{self.base_url}/task/{task_id}")
359
+ data = self._handle_response(response)
360
+ return TaskResponse(**data["r"])
361
+ except httpx.RequestError as e:
362
+ logger.error(f"Request failed: {e}")
363
+ raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
364
+
365
+ async def run_and_wait_for_task(self, payload: TaskRequest, poll_interval: int = 1, timeout: int = 60 * 15) -> TaskResponse:
366
+ """Runs a task and waits for it to complete asynchronously.
367
+
368
+ This method submits a task and then polls the get_task endpoint
369
+ until the task's status is no longer 'RUNNING' or 'waiting'.
370
+
371
+ Args:
372
+ payload: The request object containing task details.
373
+ poll_interval: The time in seconds to wait between polling for status.
374
+ timeout: The maximum time in seconds to wait for the task to complete.
375
+ progress_callback: Optional async callback function called with TaskResponse on each poll.
376
+
377
+ Returns:
378
+ The final response of the completed or failed task.
379
+
380
+ Raises:
381
+ TimeoutError: If the task does not complete within the specified timeout.
382
+ ApiException: If the API request fails.
383
+ """
384
+ if poll_interval < 0.1:
385
+ raise ValueError("Poll interval must be at least 100 milliseconds.")
386
+ if timeout < 1:
387
+ raise ValueError("Timeout must be at least 1 second.")
388
+
389
+ start_time = time.time()
390
+ initial_response = await self.run_task(payload)
391
+ task_id = initial_response.id
392
+
393
+ logger.info(f"Task {task_id} started, polling every {poll_interval}s for up to {timeout}s")
394
+
395
+ while (time.time() - start_time) < timeout:
396
+ task_status = await self.get_task(task_id)
397
+
398
+ if task_status.status.lower() not in ["running", "waiting"]:
399
+ logger.info(f"Task {task_id} completed with status: {task_status.status}")
400
+ return task_status
401
+
402
+ await asyncio.sleep(poll_interval)
403
+
404
+ raise TimeoutError(f"Task {task_id} did not complete within {timeout} seconds.")
405
+
406
+ async def get_browser(self, session_id: Optional[str] = None, session_name: Optional[str] = None) -> BrowserResponse:
407
+ """Gets an interactive browser instance asynchronously.
408
+
409
+ Args:
410
+ session_id: The session ID to associate with the browser.
411
+ session_name: The name for a new browser session.
412
+
413
+ Returns:
414
+ The browser session details, including the live URL.
415
+
416
+ Raises:
417
+ ApiException: If the API request fails.
418
+ """
419
+ params = {}
420
+ if session_id:
421
+ params["session_id"] = session_id
422
+ if session_name:
423
+ params["session_name"] = session_name
424
+
425
+ try:
426
+ response = await self._client.get(f"{self.base_url}/browser", params=params)
427
+ data = self._handle_response(response)
428
+ return BrowserResponse(**data["r"])
429
+ except httpx.RequestError as e:
430
+ logger.error(f"Request failed: {e}")
431
+ raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
432
+
433
+ async def close(self):
434
+ """Closes the async client session."""
435
+ await self._client.aclose()