smooth-py 0.1.0__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 +435 -0
- smooth_py-0.1.0.dist-info/METADATA +161 -0
- smooth_py-0.1.0.dist-info/RECORD +4 -0
- smooth_py-0.1.0.dist-info/WHEEL +4 -0
smooth/__init__.py
ADDED
|
@@ -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()
|
|
@@ -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,4 @@
|
|
|
1
|
+
smooth/__init__.py,sha256=nC4DAi-hkJzENxQBdJMQRoCn_dEm_OruxxMmHewmMqc,15134
|
|
2
|
+
smooth_py-0.1.0.dist-info/METADATA,sha256=jls-c0VkH0qSMgenpK1qpX1ZrGgYTG4e4eddvjMgKeI,4633
|
|
3
|
+
smooth_py-0.1.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
4
|
+
smooth_py-0.1.0.dist-info/RECORD,,
|