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 Any, Dict, Literal, Optional, TypeVar, Union
7
+ from typing import (
8
+ Any,
9
+ Literal,
10
+ )
8
11
 
9
12
  import httpx
10
13
  import requests
11
- from pydantic import BaseModel, ConfigDict, Field
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
- result: Any | None = Field(default=None, description="The result of the task if successful.")
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
- src: str | None = Field(default=None, description="")
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=1, le=64, description="Maximum number of steps the agent can take (max 64).")
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
- session_id: Optional[str] = Field(
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="(optional) Browser session ID to use. Each session maintains its own state, such as login credentials.",
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="(optional) Run the browser in stealth mode.")
50
- proxy_server: Optional[str] = Field(
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
- "(optional) Proxy server url to route browser traffic through."
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: Optional[str] = Field(default=None, description="(optional) Proxy server username.")
58
- proxy_password: Optional[str] = Field(default=None, description="(optional) Proxy server 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 BrowserResponse(BaseModel):
62
- """Browser session response model."""
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
- model_config = ConfigDict(extra="forbid")
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: Optional[Dict[str, Any]] = None):
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: Optional[str] = None, base_url: str = BASE_URL, api_version: str = "v1"):
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.0",
126
+ "User-Agent": "smooth-python-sdk/0.1.1",
126
127
  }
127
128
 
128
- def _handle_response(self, response: Union[requests.Response, httpx.Response]) -> dict[str, Any]:
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 SyncClient(BaseClient):
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: Optional[str] = None, base_url: str = BASE_URL, api_version: str = "v1"):
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 run_task(self, payload: TaskRequest) -> TaskResponse:
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 get_task(self, task_id: str) -> TaskResponse:
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 run_and_wait_for_task(self, payload: TaskRequest, poll_interval: int = 1, timeout: int = 60 * 15) -> TaskResponse:
221
- """Runs a task and waits for it to complete.
222
-
223
- This method submits a task and then polls the get_task endpoint
224
- until the task's status is no longer 'running' or 'waiting'.
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
- payload: The request object containing task details.
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
- progress_callback: Optional callback function called with TaskResponse on each poll.
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
- The final response of the completed or failed task.
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
- initial_response = self.run_task(payload)
246
- task_id = initial_response.id
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
- raise TimeoutError(f"Task {task_id} did not complete within {timeout} seconds.")
289
+ return TaskHandle(initial_response.id, self, initial_response.live_url, poll_interval, timeout)
257
290
 
258
- def get_browser(self, session_id: Optional[str] = None, session_name: Optional[str] = None) -> BrowserResponse:
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.get(f"{self.base_url}/browser", params=params)
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 BrowserResponse(**data["r"])
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 AsyncClient(BaseClient):
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: Optional[str] = None, base_url: str = BASE_URL, api_version: str = "v1", timeout: int = 30):
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 run_task(self, payload: TaskRequest) -> TaskResponse:
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 get_task(self, task_id: str) -> TaskResponse:
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 run_and_wait_for_task(self, payload: TaskRequest, poll_interval: int = 1, timeout: int = 60 * 15) -> TaskResponse:
367
- """Runs a task and waits for it to complete asynchronously.
368
-
369
- This method submits a task and then polls the get_task endpoint
370
- until the task's status is no longer 'RUNNING' or 'waiting'.
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
- payload: The request object containing task details.
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
- progress_callback: Optional async callback function called with TaskResponse on each poll.
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
- The final response of the completed or failed task.
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
- start_time = time.time()
391
- initial_response = await self.run_task(payload)
392
- task_id = initial_response.id
393
-
394
- logger.info(f"Task {task_id} started, polling every {poll_interval}s for up to {timeout}s")
395
-
396
- while (time.time() - start_time) < timeout:
397
- task_status = await self.get_task(task_id)
398
-
399
- if task_status.status.lower() not in ["running", "waiting"]:
400
- logger.info(f"Task {task_id} completed with status: {task_status.status}")
401
- return task_status
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
- raise TimeoutError(f"Task {task_id} did not complete within {timeout} seconds.")
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.get(f"{self.base_url}/browser", params=params)
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 BrowserResponse(**data["r"])
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,,