smooth-py 0.1.1.post0__py3-none-any.whl → 0.1.2.post0__py3-none-any.whl
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.
smooth/__init__.py
CHANGED
|
@@ -4,11 +4,14 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import time
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import (
|
|
8
|
+
Any,
|
|
9
|
+
Literal,
|
|
10
|
+
)
|
|
8
11
|
|
|
9
12
|
import httpx
|
|
10
13
|
import requests
|
|
11
|
-
from pydantic import BaseModel,
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
12
15
|
|
|
13
16
|
# Configure logging
|
|
14
17
|
logger = logging.getLogger("smooth")
|
|
@@ -23,45 +26,49 @@ BASE_URL = "https://api2.circlemind.co/api/"
|
|
|
23
26
|
class TaskResponse(BaseModel):
|
|
24
27
|
"""Task response model."""
|
|
25
28
|
|
|
26
|
-
model_config = ConfigDict(extra="forbid")
|
|
27
|
-
|
|
28
29
|
id: str = Field(description="The ID of the task.")
|
|
29
30
|
status: Literal["waiting", "running", "done", "failed"] = Field(description="The status of the task.")
|
|
30
|
-
|
|
31
|
-
error: str | None = Field(default=None, description="Error message if the task failed.")
|
|
31
|
+
output: Any | None = Field(default=None, description="The output of the task.")
|
|
32
32
|
credits_used: int | None = Field(default=None, description="The amount of credits used to perform the task.")
|
|
33
|
-
|
|
33
|
+
device: Literal["desktop", "mobile"] | None = Field(default=None, description="The device type used for the task.")
|
|
34
|
+
live_url: str | None = Field(default=None, description="The URL to view and interact with the task execution.")
|
|
35
|
+
recording_url: str | None = Field(default=None, description="The URL to view the task recording.")
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
class TaskRequest(BaseModel):
|
|
37
39
|
"""Run task request model."""
|
|
38
40
|
|
|
39
|
-
model_config = ConfigDict(extra="forbid")
|
|
40
|
-
|
|
41
41
|
task: str = Field(description="The task to run.")
|
|
42
42
|
agent: Literal["smooth"] = Field(default="smooth", description="The agent to use for the task.")
|
|
43
|
-
max_steps: int = Field(default=32, ge=
|
|
43
|
+
max_steps: int = Field(default=32, ge=2, le=128, description="Maximum number of steps the agent can take (min 2, max 128).")
|
|
44
44
|
device: Literal["desktop", "mobile"] = Field(default="mobile", description="Device type for the task. Default is mobile.")
|
|
45
|
-
|
|
45
|
+
enable_recording: bool = Field(default=False, description="Enable video recording of the task execution. Default is False")
|
|
46
|
+
session_id: str | None = Field(
|
|
46
47
|
default=None,
|
|
47
|
-
description="
|
|
48
|
+
description="Browser session ID to use. Each session maintains its own state, such as login credentials.",
|
|
48
49
|
)
|
|
49
|
-
stealth_mode: bool = Field(default=False, description="
|
|
50
|
-
proxy_server:
|
|
50
|
+
stealth_mode: bool = Field(default=False, description="Run the browser in stealth mode.")
|
|
51
|
+
proxy_server: str | None = Field(
|
|
51
52
|
default=None,
|
|
52
53
|
description=(
|
|
53
|
-
"
|
|
54
|
-
" Must include the protocol to use (e.g. http:// or https://)"
|
|
54
|
+
"Proxy server url to route browser traffic through." " Must include the protocol to use (e.g. http:// or https://)"
|
|
55
55
|
),
|
|
56
56
|
)
|
|
57
|
-
proxy_username:
|
|
58
|
-
proxy_password:
|
|
57
|
+
proxy_username: str | None = Field(default=None, description="Proxy server username.")
|
|
58
|
+
proxy_password: str | None = Field(default=None, description="Proxy server password.")
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
class
|
|
62
|
-
"""
|
|
61
|
+
class BrowserSessionRequest(BaseModel):
|
|
62
|
+
"""Request model for creating a browser session."""
|
|
63
|
+
|
|
64
|
+
session_id: str | None = Field(
|
|
65
|
+
default=None,
|
|
66
|
+
description=("The session ID to open in the browser. If None, a new session will be created with a random name."),
|
|
67
|
+
)
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
|
|
70
|
+
class BrowserSessionResponse(BaseModel):
|
|
71
|
+
"""Browser session response model."""
|
|
65
72
|
|
|
66
73
|
live_url: str = Field(description="The live URL to interact with the browser session.")
|
|
67
74
|
session_id: str = Field(description="The ID of the browser session associated with the opened browser instance.")
|
|
@@ -71,12 +78,6 @@ class BrowserSessionsResponse(BaseModel):
|
|
|
71
78
|
"""Response model for listing browser sessions."""
|
|
72
79
|
|
|
73
80
|
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
|
|
|
81
82
|
|
|
82
83
|
# --- Exception Handling ---
|
|
@@ -85,7 +86,7 @@ T = TypeVar("T")
|
|
|
85
86
|
class ApiError(Exception):
|
|
86
87
|
"""Custom exception for API errors."""
|
|
87
88
|
|
|
88
|
-
def __init__(self, status_code: int, detail: str, response_data:
|
|
89
|
+
def __init__(self, status_code: int, detail: str, response_data: dict[str, Any] | None = None):
|
|
89
90
|
"""Initializes the API error."""
|
|
90
91
|
self.status_code = status_code
|
|
91
92
|
self.detail = detail
|
|
@@ -105,7 +106,7 @@ class TimeoutError(Exception):
|
|
|
105
106
|
class BaseClient:
|
|
106
107
|
"""Base client for handling common API interactions."""
|
|
107
108
|
|
|
108
|
-
def __init__(self, api_key:
|
|
109
|
+
def __init__(self, api_key: str | None = None, base_url: str = BASE_URL, api_version: str = "v1"):
|
|
109
110
|
"""Initializes the base client."""
|
|
110
111
|
# Try to get API key from environment if not provided
|
|
111
112
|
if not api_key:
|
|
@@ -122,10 +123,10 @@ class BaseClient:
|
|
|
122
123
|
self.headers = {
|
|
123
124
|
"apikey": self.api_key,
|
|
124
125
|
"Content-Type": "application/json",
|
|
125
|
-
"User-Agent": "smooth-python-sdk/0.1.
|
|
126
|
+
"User-Agent": "smooth-python-sdk/0.1.1",
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
def _handle_response(self, response:
|
|
129
|
+
def _handle_response(self, response: requests.Response | httpx.Response) -> dict[str, Any]:
|
|
129
130
|
"""Handles HTTP responses and raises exceptions for errors."""
|
|
130
131
|
if 200 <= response.status_code < 300:
|
|
131
132
|
try:
|
|
@@ -143,18 +144,44 @@ class BaseClient:
|
|
|
143
144
|
detail = response.text or f"HTTP {response.status_code} error"
|
|
144
145
|
|
|
145
146
|
logger.error(f"API error: {response.status_code} - {detail}")
|
|
146
|
-
raise ApiError(
|
|
147
|
-
status_code=response.status_code, detail=detail, response_data=error_data
|
|
148
|
-
)
|
|
147
|
+
raise ApiError(status_code=response.status_code, detail=detail, response_data=error_data)
|
|
149
148
|
|
|
150
149
|
|
|
151
150
|
# --- Synchronous Client ---
|
|
152
151
|
|
|
153
152
|
|
|
154
|
-
class
|
|
153
|
+
class TaskHandle:
|
|
154
|
+
"""A handle to a running task."""
|
|
155
|
+
|
|
156
|
+
def __init__(self, task_id: str, client: "SmoothClient", live_url: str | None, poll_interval: int, timeout: int | None):
|
|
157
|
+
"""Initializes the task handle."""
|
|
158
|
+
self._client = client
|
|
159
|
+
self._poll_interval = poll_interval
|
|
160
|
+
self._timeout = timeout
|
|
161
|
+
self._task_response: TaskResponse | None = None
|
|
162
|
+
|
|
163
|
+
self.id = task_id
|
|
164
|
+
self.live_url = live_url
|
|
165
|
+
|
|
166
|
+
def result(self) -> TaskResponse:
|
|
167
|
+
"""Waits for the task to complete and returns the result."""
|
|
168
|
+
if self._task_response and self._task_response.status not in ["running", "waiting"]:
|
|
169
|
+
return self._task_response
|
|
170
|
+
|
|
171
|
+
start_time = time.time()
|
|
172
|
+
while self._timeout is None or (time.time() - start_time) < self._timeout:
|
|
173
|
+
task_response = self._client._get_task(self.id) # pyright: ignore [reportPrivateUsage]
|
|
174
|
+
if task_response.status not in ["running", "waiting"]:
|
|
175
|
+
self._task_response = task_response
|
|
176
|
+
return task_response
|
|
177
|
+
time.sleep(self._poll_interval)
|
|
178
|
+
raise TimeoutError(f"Task {self.id} did not complete within {self._timeout} seconds.")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class SmoothClient(BaseClient):
|
|
155
182
|
"""A synchronous client for the API."""
|
|
156
183
|
|
|
157
|
-
def __init__(self, api_key:
|
|
184
|
+
def __init__(self, api_key: str | None = None, base_url: str = BASE_URL, api_version: str = "v1"):
|
|
158
185
|
"""Initializes the synchronous client."""
|
|
159
186
|
super().__init__(api_key, base_url, api_version)
|
|
160
187
|
self._session = requests.Session()
|
|
@@ -173,18 +200,8 @@ class SyncClient(BaseClient):
|
|
|
173
200
|
if hasattr(self, "_session"):
|
|
174
201
|
self._session.close()
|
|
175
202
|
|
|
176
|
-
def
|
|
177
|
-
"""Submits a task to be run.
|
|
178
|
-
|
|
179
|
-
Args:
|
|
180
|
-
payload: The request object containing task details.
|
|
181
|
-
|
|
182
|
-
Returns:
|
|
183
|
-
The initial response for the submitted task.
|
|
184
|
-
|
|
185
|
-
Raises:
|
|
186
|
-
ApiException: If the API request fails.
|
|
187
|
-
"""
|
|
203
|
+
def _submit_task(self, payload: TaskRequest) -> TaskResponse:
|
|
204
|
+
"""Submits a task to be run."""
|
|
188
205
|
try:
|
|
189
206
|
response = self._session.post(f"{self.base_url}/task", json=payload.model_dump(exclude_none=True))
|
|
190
207
|
data = self._handle_response(response)
|
|
@@ -193,19 +210,8 @@ class SyncClient(BaseClient):
|
|
|
193
210
|
logger.error(f"Request failed: {e}")
|
|
194
211
|
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
195
212
|
|
|
196
|
-
def
|
|
197
|
-
"""Retrieves the status and result of a task.
|
|
198
|
-
|
|
199
|
-
Args:
|
|
200
|
-
task_id: The ID of the task to retrieve.
|
|
201
|
-
|
|
202
|
-
Returns:
|
|
203
|
-
The current status and data of the task.
|
|
204
|
-
|
|
205
|
-
Raises:
|
|
206
|
-
ApiException: If the API request fails.
|
|
207
|
-
ValueError: If task_id is empty.
|
|
208
|
-
"""
|
|
213
|
+
def _get_task(self, task_id: str) -> TaskResponse:
|
|
214
|
+
"""Retrieves the status and result of a task."""
|
|
209
215
|
if not task_id:
|
|
210
216
|
raise ValueError("Task ID cannot be empty.")
|
|
211
217
|
|
|
@@ -217,23 +223,44 @@ class SyncClient(BaseClient):
|
|
|
217
223
|
logger.error(f"Request failed: {e}")
|
|
218
224
|
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
219
225
|
|
|
220
|
-
def
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
226
|
+
def run(
|
|
227
|
+
self,
|
|
228
|
+
task: str,
|
|
229
|
+
poll_interval: int = 1,
|
|
230
|
+
timeout: int = 60 * 15,
|
|
231
|
+
agent: Literal["smooth"] = "smooth",
|
|
232
|
+
max_steps: int = 32,
|
|
233
|
+
device: Literal["desktop", "mobile"] = "mobile",
|
|
234
|
+
enable_recording: bool = False,
|
|
235
|
+
session_id: str | None = None,
|
|
236
|
+
stealth_mode: bool = False,
|
|
237
|
+
proxy_server: str | None = None,
|
|
238
|
+
proxy_username: str | None = None,
|
|
239
|
+
proxy_password: str | None = None,
|
|
240
|
+
) -> TaskHandle:
|
|
241
|
+
"""Runs a task and returns a handle to the task.
|
|
242
|
+
|
|
243
|
+
This method submits a task and returns a `TaskHandle` object
|
|
244
|
+
that can be used to get the result of the task.
|
|
225
245
|
|
|
226
246
|
Args:
|
|
227
|
-
|
|
247
|
+
task: The task to run.
|
|
228
248
|
poll_interval: The time in seconds to wait between polling for status.
|
|
229
249
|
timeout: The maximum time in seconds to wait for the task to complete.
|
|
230
|
-
|
|
250
|
+
agent: The agent to use for the task.
|
|
251
|
+
max_steps: Maximum number of steps the agent can take (max 64).
|
|
252
|
+
device: Device type for the task. Default is mobile.
|
|
253
|
+
enable_recording: Enable video recording of the task execution.
|
|
254
|
+
session_id: Browser session ID to use.
|
|
255
|
+
stealth_mode: Run the browser in stealth mode.
|
|
256
|
+
proxy_server: Proxy server url to route browser traffic through.
|
|
257
|
+
proxy_username: Proxy server username.
|
|
258
|
+
proxy_password: Proxy server password.
|
|
231
259
|
|
|
232
260
|
Returns:
|
|
233
|
-
|
|
261
|
+
A handle to the running task.
|
|
234
262
|
|
|
235
263
|
Raises:
|
|
236
|
-
TimeoutError: If the task does not complete within the specified timeout.
|
|
237
264
|
ApiException: If the API request fails.
|
|
238
265
|
"""
|
|
239
266
|
if poll_interval < 0.1:
|
|
@@ -241,26 +268,31 @@ class SyncClient(BaseClient):
|
|
|
241
268
|
if timeout < 1:
|
|
242
269
|
raise ValueError("Timeout must be at least 1 second.")
|
|
243
270
|
|
|
271
|
+
payload = TaskRequest(
|
|
272
|
+
task=task,
|
|
273
|
+
agent=agent,
|
|
274
|
+
max_steps=max_steps,
|
|
275
|
+
device=device,
|
|
276
|
+
enable_recording=enable_recording,
|
|
277
|
+
session_id=session_id,
|
|
278
|
+
stealth_mode=stealth_mode,
|
|
279
|
+
proxy_server=proxy_server,
|
|
280
|
+
proxy_username=proxy_username,
|
|
281
|
+
proxy_password=proxy_password,
|
|
282
|
+
)
|
|
283
|
+
initial_response = self._submit_task(payload)
|
|
244
284
|
start_time = time.time()
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
while (time.time() - start_time) < timeout:
|
|
249
|
-
task_response = self.get_task(task_id)
|
|
250
|
-
|
|
251
|
-
if task_response.status not in ["running", "waiting"]:
|
|
252
|
-
return task_response
|
|
253
|
-
|
|
285
|
+
while time.time() - start_time < 16 and initial_response.live_url is None:
|
|
286
|
+
initial_response = self._get_task(initial_response.id)
|
|
254
287
|
time.sleep(poll_interval)
|
|
255
288
|
|
|
256
|
-
|
|
289
|
+
return TaskHandle(initial_response.id, self, initial_response.live_url, poll_interval, timeout)
|
|
257
290
|
|
|
258
|
-
def
|
|
291
|
+
def open_session(self, session_id: str | None = None) -> BrowserSessionResponse:
|
|
259
292
|
"""Gets an interactive browser instance.
|
|
260
293
|
|
|
261
294
|
Args:
|
|
262
295
|
session_id: The session ID to associate with the browser. If None, a new session will be created.
|
|
263
|
-
session_name: The name to associate to the new browser session. Ignored if a valid session_id is provided.
|
|
264
296
|
|
|
265
297
|
Returns:
|
|
266
298
|
The browser session details, including the live URL.
|
|
@@ -268,16 +300,13 @@ class SyncClient(BaseClient):
|
|
|
268
300
|
Raises:
|
|
269
301
|
ApiException: If the API request fails.
|
|
270
302
|
"""
|
|
271
|
-
params: dict[str, Any] = {}
|
|
272
|
-
if session_id:
|
|
273
|
-
params["session_id"] = session_id
|
|
274
|
-
if session_name:
|
|
275
|
-
params["session_name"] = session_name
|
|
276
|
-
|
|
277
303
|
try:
|
|
278
|
-
response = self._session.
|
|
304
|
+
response = self._session.post(
|
|
305
|
+
f"{self.base_url}/browser/session",
|
|
306
|
+
json=BrowserSessionRequest(session_id=session_id).model_dump(exclude_none=True),
|
|
307
|
+
)
|
|
279
308
|
data = self._handle_response(response)
|
|
280
|
-
return
|
|
309
|
+
return BrowserSessionResponse(**data["r"])
|
|
281
310
|
except requests.exceptions.RequestException as e:
|
|
282
311
|
logger.error(f"Request failed: {e}")
|
|
283
312
|
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
@@ -299,14 +328,51 @@ class SyncClient(BaseClient):
|
|
|
299
328
|
logger.error(f"Request failed: {e}")
|
|
300
329
|
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
301
330
|
|
|
331
|
+
def delete_session(self, session_id: str):
|
|
332
|
+
"""Delete a browser session."""
|
|
333
|
+
try:
|
|
334
|
+
response = self._session.delete(f"{self.base_url}/browser/session/{session_id}")
|
|
335
|
+
self._handle_response(response)
|
|
336
|
+
except httpx.RequestError as e:
|
|
337
|
+
logger.error(f"Request failed: {e}")
|
|
338
|
+
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
339
|
+
|
|
302
340
|
|
|
303
341
|
# --- Asynchronous Client ---
|
|
304
342
|
|
|
305
343
|
|
|
306
|
-
class
|
|
344
|
+
class AsyncTaskHandle:
|
|
345
|
+
"""An asynchronous handle to a running task."""
|
|
346
|
+
|
|
347
|
+
def __init__(self, task_id: str, client: "SmoothAsyncClient", live_url: str | None, poll_interval: int, timeout: int | None):
|
|
348
|
+
"""Initializes the asynchronous task handle."""
|
|
349
|
+
self._client = client
|
|
350
|
+
self._poll_interval = poll_interval
|
|
351
|
+
self._timeout = timeout
|
|
352
|
+
self._task_response: TaskResponse | None = None
|
|
353
|
+
|
|
354
|
+
self.id = task_id
|
|
355
|
+
self.live_url = live_url
|
|
356
|
+
|
|
357
|
+
async def result(self) -> TaskResponse:
|
|
358
|
+
"""Waits for the task to complete and returns the result."""
|
|
359
|
+
if self._task_response and self._task_response.status not in ["running", "waiting"]:
|
|
360
|
+
return self._task_response
|
|
361
|
+
|
|
362
|
+
start_time = time.time()
|
|
363
|
+
while self._timeout is None or (time.time() - start_time) < self._timeout:
|
|
364
|
+
task_response = await self._client._get_task(self.id) # pyright: ignore [reportPrivateUsage]
|
|
365
|
+
if task_response.status not in ["running", "waiting"]:
|
|
366
|
+
self._task_response = task_response
|
|
367
|
+
return task_response
|
|
368
|
+
await asyncio.sleep(self._poll_interval)
|
|
369
|
+
raise TimeoutError(f"Task {self.id} did not complete within {self._timeout} seconds.")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class SmoothAsyncClient(BaseClient):
|
|
307
373
|
"""An asynchronous client for the API."""
|
|
308
374
|
|
|
309
|
-
def __init__(self, api_key:
|
|
375
|
+
def __init__(self, api_key: str | None = None, base_url: str = BASE_URL, api_version: str = "v1", timeout: int = 30):
|
|
310
376
|
"""Initializes the asynchronous client."""
|
|
311
377
|
super().__init__(api_key, base_url, api_version)
|
|
312
378
|
self._client = httpx.AsyncClient(headers=self.headers, timeout=timeout)
|
|
@@ -319,18 +385,8 @@ class AsyncClient(BaseClient):
|
|
|
319
385
|
"""Exits the asynchronous context manager."""
|
|
320
386
|
await self.close()
|
|
321
387
|
|
|
322
|
-
async def
|
|
323
|
-
"""Submits a task to be run asynchronously.
|
|
324
|
-
|
|
325
|
-
Args:
|
|
326
|
-
payload: The request object containing task details.
|
|
327
|
-
|
|
328
|
-
Returns:
|
|
329
|
-
The initial response for the submitted task.
|
|
330
|
-
|
|
331
|
-
Raises:
|
|
332
|
-
ApiException: If the API request fails.
|
|
333
|
-
"""
|
|
388
|
+
async def _submit_task(self, payload: TaskRequest) -> TaskResponse:
|
|
389
|
+
"""Submits a task to be run asynchronously."""
|
|
334
390
|
try:
|
|
335
391
|
response = await self._client.post(f"{self.base_url}/task", json=payload.model_dump(exclude_none=True))
|
|
336
392
|
data = self._handle_response(response)
|
|
@@ -339,19 +395,8 @@ class AsyncClient(BaseClient):
|
|
|
339
395
|
logger.error(f"Request failed: {e}")
|
|
340
396
|
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
341
397
|
|
|
342
|
-
async def
|
|
343
|
-
"""Retrieves the status and result of a task asynchronously.
|
|
344
|
-
|
|
345
|
-
Args:
|
|
346
|
-
task_id: The ID of the task to retrieve.
|
|
347
|
-
|
|
348
|
-
Returns:
|
|
349
|
-
The current status and data of the task.
|
|
350
|
-
|
|
351
|
-
Raises:
|
|
352
|
-
ApiException: If the API request fails.
|
|
353
|
-
ValueError: If task_id is empty.
|
|
354
|
-
"""
|
|
398
|
+
async def _get_task(self, task_id: str) -> TaskResponse:
|
|
399
|
+
"""Retrieves the status and result of a task asynchronously."""
|
|
355
400
|
if not task_id:
|
|
356
401
|
raise ValueError("Task ID cannot be empty.")
|
|
357
402
|
|
|
@@ -363,23 +408,44 @@ class AsyncClient(BaseClient):
|
|
|
363
408
|
logger.error(f"Request failed: {e}")
|
|
364
409
|
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
365
410
|
|
|
366
|
-
async def
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
411
|
+
async def run(
|
|
412
|
+
self,
|
|
413
|
+
task: str,
|
|
414
|
+
poll_interval: int = 1,
|
|
415
|
+
timeout: int = 60 * 15,
|
|
416
|
+
agent: Literal["smooth"] = "smooth",
|
|
417
|
+
max_steps: int = 32,
|
|
418
|
+
device: Literal["desktop", "mobile"] = "mobile",
|
|
419
|
+
enable_recording: bool = False,
|
|
420
|
+
session_id: str | None = None,
|
|
421
|
+
stealth_mode: bool = False,
|
|
422
|
+
proxy_server: str | None = None,
|
|
423
|
+
proxy_username: str | None = None,
|
|
424
|
+
proxy_password: str | None = None,
|
|
425
|
+
) -> AsyncTaskHandle:
|
|
426
|
+
"""Runs a task and returns a handle to the task asynchronously.
|
|
427
|
+
|
|
428
|
+
This method submits a task and returns an `AsyncTaskHandle` object
|
|
429
|
+
that can be used to get the result of the task.
|
|
371
430
|
|
|
372
431
|
Args:
|
|
373
|
-
|
|
432
|
+
task: The task to run.
|
|
374
433
|
poll_interval: The time in seconds to wait between polling for status.
|
|
375
434
|
timeout: The maximum time in seconds to wait for the task to complete.
|
|
376
|
-
|
|
435
|
+
agent: The agent to use for the task.
|
|
436
|
+
max_steps: Maximum number of steps the agent can take (max 64).
|
|
437
|
+
device: Device type for the task. Default is mobile.
|
|
438
|
+
enable_recording: Enable video recording of the task execution.
|
|
439
|
+
session_id: Browser session ID to use.
|
|
440
|
+
stealth_mode: Run the browser in stealth mode.
|
|
441
|
+
proxy_server: Proxy server url to route browser traffic through.
|
|
442
|
+
proxy_username: Proxy server username.
|
|
443
|
+
proxy_password: Proxy server password.
|
|
377
444
|
|
|
378
445
|
Returns:
|
|
379
|
-
|
|
446
|
+
A handle to the running task.
|
|
380
447
|
|
|
381
448
|
Raises:
|
|
382
|
-
TimeoutError: If the task does not complete within the specified timeout.
|
|
383
449
|
ApiException: If the API request fails.
|
|
384
450
|
"""
|
|
385
451
|
if poll_interval < 0.1:
|
|
@@ -387,29 +453,31 @@ class AsyncClient(BaseClient):
|
|
|
387
453
|
if timeout < 1:
|
|
388
454
|
raise ValueError("Timeout must be at least 1 second.")
|
|
389
455
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
456
|
+
payload = TaskRequest(
|
|
457
|
+
task=task,
|
|
458
|
+
agent=agent,
|
|
459
|
+
max_steps=max_steps,
|
|
460
|
+
device=device,
|
|
461
|
+
enable_recording=enable_recording,
|
|
462
|
+
session_id=session_id,
|
|
463
|
+
stealth_mode=stealth_mode,
|
|
464
|
+
proxy_server=proxy_server,
|
|
465
|
+
proxy_username=proxy_username,
|
|
466
|
+
proxy_password=proxy_password,
|
|
467
|
+
)
|
|
402
468
|
|
|
469
|
+
initial_response = await self._submit_task(payload)
|
|
470
|
+
start_time = time.time()
|
|
471
|
+
while time.time() - start_time < 16 and initial_response.live_url is None:
|
|
472
|
+
initial_response = await self._get_task(initial_response.id)
|
|
403
473
|
await asyncio.sleep(poll_interval)
|
|
474
|
+
return AsyncTaskHandle(initial_response.id, self, initial_response.live_url, poll_interval, timeout)
|
|
404
475
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
async def get_browser(self, session_id: Optional[str] = None, session_name: Optional[str] = None) -> BrowserResponse:
|
|
408
|
-
"""Gets an interactive browser instance asynchronously.
|
|
476
|
+
async def open_session(self, session_id: str | None = None) -> BrowserSessionResponse:
|
|
477
|
+
"""Opens an interactive browser instance asynchronously.
|
|
409
478
|
|
|
410
479
|
Args:
|
|
411
480
|
session_id: The session ID to associate with the browser.
|
|
412
|
-
session_name: The name for a new browser session.
|
|
413
481
|
|
|
414
482
|
Returns:
|
|
415
483
|
The browser session details, including the live URL.
|
|
@@ -417,16 +485,13 @@ class AsyncClient(BaseClient):
|
|
|
417
485
|
Raises:
|
|
418
486
|
ApiException: If the API request fails.
|
|
419
487
|
"""
|
|
420
|
-
params: dict[str, Any] = {}
|
|
421
|
-
if session_id:
|
|
422
|
-
params["session_id"] = session_id
|
|
423
|
-
if session_name:
|
|
424
|
-
params["session_name"] = session_name
|
|
425
|
-
|
|
426
488
|
try:
|
|
427
|
-
response = await self._client.
|
|
489
|
+
response = await self._client.post(
|
|
490
|
+
f"{self.base_url}/browser/session",
|
|
491
|
+
json=BrowserSessionRequest(session_id=session_id).model_dump(exclude_none=True),
|
|
492
|
+
)
|
|
428
493
|
data = self._handle_response(response)
|
|
429
|
-
return
|
|
494
|
+
return BrowserSessionResponse(**data["r"])
|
|
430
495
|
except httpx.RequestError as e:
|
|
431
496
|
logger.error(f"Request failed: {e}")
|
|
432
497
|
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
@@ -448,6 +513,15 @@ class AsyncClient(BaseClient):
|
|
|
448
513
|
logger.error(f"Request failed: {e}")
|
|
449
514
|
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
450
515
|
|
|
516
|
+
async def delete_session(self, session_id: str):
|
|
517
|
+
"""Delete a browser session."""
|
|
518
|
+
try:
|
|
519
|
+
response = await self._client.delete(f"{self.base_url}/browser/session/{session_id}")
|
|
520
|
+
self._handle_response(response)
|
|
521
|
+
except httpx.RequestError as e:
|
|
522
|
+
logger.error(f"Request failed: {e}")
|
|
523
|
+
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
524
|
+
|
|
451
525
|
async def close(self):
|
|
452
526
|
"""Closes the async client session."""
|
|
453
527
|
await self._client.aclose()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: smooth-py
|
|
3
|
+
Version: 0.1.2.post0
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Luca Pinchetti
|
|
6
|
+
Author-email: luca@circlemind.co
|
|
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.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
* **Synchronous and Asynchronous Clients**: Choose between `SmoothClient` for traditional sequential programming and `SmoothAsyncClient` for high-performance asynchronous applications.
|
|
24
|
+
* **Task Management**: Easily run tasks and retrieve results upon completion.
|
|
25
|
+
* **Interactive Browser Sessions**: Get access to, interact with, and delete stateful browser sessions to manage your login credentials.
|
|
26
|
+
* **Advanced Task Configuration**: Customize task execution with options for device type, session recording, stealth mode, and proxy settings.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
You can install the Smooth Python SDK using pip:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install smooth-py
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Authentication
|
|
37
|
+
|
|
38
|
+
The SDK requires an API key for authentication. You can provide the API key in two ways:
|
|
39
|
+
|
|
40
|
+
1. **Directly in the client constructor**:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from smooth import SmoothClient
|
|
44
|
+
|
|
45
|
+
client = SmoothClient(api_key="YOUR_API_KEY")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
2. **As an environment variable**:
|
|
49
|
+
|
|
50
|
+
Set the `CIRCLEMIND_API_KEY` environment variable, and the client will automatically use it.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
export CIRCLEMIND_API_KEY="YOUR_API_KEY"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from smooth import SmoothClient
|
|
58
|
+
|
|
59
|
+
# The client will pick up the API key from the environment variable
|
|
60
|
+
client = SmoothClient()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
### Synchronous Client
|
|
66
|
+
|
|
67
|
+
The `SmoothClient` is ideal for scripts and applications that don't require asynchronous operations.
|
|
68
|
+
|
|
69
|
+
#### Running a Task and Waiting for the Result
|
|
70
|
+
|
|
71
|
+
The `run` method returns a `TaskHandle`. You can use the `result()` method on this handle to wait for the task to complete and get its final state.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from smooth import SmoothClient
|
|
75
|
+
from smooth.models import ApiError, TimeoutError
|
|
76
|
+
|
|
77
|
+
with SmoothClient() as client:
|
|
78
|
+
try:
|
|
79
|
+
# The run method returns a handle to the task immediately
|
|
80
|
+
task_handle = client.run(
|
|
81
|
+
task="Go to https://www.google.com and search for 'Smooth SDK'",
|
|
82
|
+
device="desktop",
|
|
83
|
+
enable_recording=True
|
|
84
|
+
)
|
|
85
|
+
print(f"Task submitted with ID: {task_handle.id}")
|
|
86
|
+
print(f"Live view available at: {task_handle.live_url}")
|
|
87
|
+
|
|
88
|
+
# The result() method waits for the task to complete
|
|
89
|
+
completed_task = task_handle.result()
|
|
90
|
+
|
|
91
|
+
if completed_task.status == "done":
|
|
92
|
+
print("Task Result:", completed_task.output)
|
|
93
|
+
print(f"View recording at: {completed_task.recording_url}")
|
|
94
|
+
else:
|
|
95
|
+
print("Task Failed:", completed_task.output)
|
|
96
|
+
|
|
97
|
+
except TimeoutError:
|
|
98
|
+
print("The task timed out.")
|
|
99
|
+
except ApiError as e:
|
|
100
|
+
print(f"An API error occurred: {e}")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### Managing Browser Sessions
|
|
104
|
+
|
|
105
|
+
You can create, list, and delete browser sessions to maintain state (like logins) between tasks.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from smooth import SmoothClient
|
|
109
|
+
|
|
110
|
+
with SmoothClient() as client:
|
|
111
|
+
# Create a new browser session
|
|
112
|
+
browser_session = client.open_session()
|
|
113
|
+
print("Live URL:", browser_session.live_url)
|
|
114
|
+
print("Session ID:", browser_session.session_id)
|
|
115
|
+
|
|
116
|
+
# List all browser sessions
|
|
117
|
+
sessions = client.list_sessions()
|
|
118
|
+
print("All Session IDs:", sessions.session_ids)
|
|
119
|
+
|
|
120
|
+
# Delete the browser session
|
|
121
|
+
client.delete_session(session_id=session_id)
|
|
122
|
+
print(f"Session '{session_id}' deleted.")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Asynchronous Client
|
|
126
|
+
|
|
127
|
+
The `SmoothAsyncClient` is designed for use in asynchronous applications, such as those built with `asyncio`, to handle multiple operations concurrently without blocking.
|
|
128
|
+
|
|
129
|
+
#### Running a Task and Waiting for the Result
|
|
130
|
+
|
|
131
|
+
The `run` method returns an `AsyncTaskHandle`. Await the `result()` method on the handle to get the final task status.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
import asyncio
|
|
135
|
+
from smooth import SmoothAsyncClient
|
|
136
|
+
from smooth.models import ApiError, TimeoutError
|
|
137
|
+
|
|
138
|
+
async def main():
|
|
139
|
+
async with SmoothAsyncClient() as client:
|
|
140
|
+
try:
|
|
141
|
+
# The run method returns a handle to the task immediately
|
|
142
|
+
task_handle = await client.run(
|
|
143
|
+
task="Go to Github and search for \"smooth-sdk\""
|
|
144
|
+
)
|
|
145
|
+
print(f"Task submitted with ID: {task_handle.id}")
|
|
146
|
+
print(f"Live view available at: {task_handle.live_url}")
|
|
147
|
+
|
|
148
|
+
# The result() method waits for the task to complete
|
|
149
|
+
completed_task = await task_handle.result()
|
|
150
|
+
|
|
151
|
+
if completed_task.status == "done":
|
|
152
|
+
print("Task Result:", completed_task.output)
|
|
153
|
+
else:
|
|
154
|
+
print("Task Failed:", completed_task.output)
|
|
155
|
+
|
|
156
|
+
except TimeoutError:
|
|
157
|
+
print("The task timed out.")
|
|
158
|
+
except ApiError as e:
|
|
159
|
+
print(f"An API error occurred: {e}")
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
asyncio.run(main())
|
|
163
|
+
```
|
|
164
|
+
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
smooth/__init__.py,sha256=65IQjCGmh5xRFfOk-48V_NNgItnh7uUCSva_PYGHd5I,19326
|
|
2
|
+
smooth_py-0.1.2.post0.dist-info/METADATA,sha256=WveUBUPZIOGiNvYT1fEB47GOk4SuGRkxIhv_4oqYQpA,5394
|
|
3
|
+
smooth_py-0.1.2.post0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
4
|
+
smooth_py-0.1.2.post0.dist-info/RECORD,,
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: smooth-py
|
|
3
|
-
Version: 0.1.1.post0
|
|
4
|
-
Summary:
|
|
5
|
-
Author: Luca Pinchetti
|
|
6
|
-
Author-email: luca@circlemind.co
|
|
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 `CIRCLEMIND_API_KEY` environment variable, and the client will automatically use it.
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
export CIRCLEMIND_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
|
-
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
smooth/__init__.py,sha256=cAEweO-FSsk9eIAp_068Gm1s8FdylhRKV9W707kNX88,15745
|
|
2
|
-
smooth_py-0.1.1.post0.dist-info/METADATA,sha256=1FsW9jkuCBaE085DcMd0PzYE-fM7mLXx8i0JxQzWC8Y,4645
|
|
3
|
-
smooth_py-0.1.1.post0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
4
|
-
smooth_py-0.1.1.post0.dist-info/RECORD,,
|
|
File without changes
|