smooth-py 0.1.3__py3-none-any.whl → 0.1.4__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,11 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import time
|
|
7
|
+
import urllib.parse
|
|
7
8
|
from typing import (
|
|
8
9
|
Any,
|
|
9
10
|
Literal,
|
|
10
11
|
)
|
|
11
|
-
from urllib.parse import urlencode
|
|
12
12
|
|
|
13
13
|
import httpx
|
|
14
14
|
import requests
|
|
@@ -20,6 +20,20 @@ logger = logging.getLogger("smooth")
|
|
|
20
20
|
|
|
21
21
|
BASE_URL = "https://api2.circlemind.co/api/"
|
|
22
22
|
|
|
23
|
+
|
|
24
|
+
# --- Utils ---
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _encode_url(url: str, interactive: bool = True, embed: bool = False) -> str:
|
|
28
|
+
parsed_url = urllib.parse.urlparse(url)
|
|
29
|
+
params = urllib.parse.parse_qs(parsed_url.query)
|
|
30
|
+
params.update({
|
|
31
|
+
"interactive": "true" if interactive else "false",
|
|
32
|
+
"embed": "true" if embed else "false"
|
|
33
|
+
})
|
|
34
|
+
return urllib.parse.urlunparse(parsed_url._replace(query=urllib.parse.urlencode(params)))
|
|
35
|
+
|
|
36
|
+
|
|
23
37
|
# --- Models ---
|
|
24
38
|
# These models define the data structures for API requests and responses.
|
|
25
39
|
|
|
@@ -151,54 +165,77 @@ class BaseClient:
|
|
|
151
165
|
# --- Synchronous Client ---
|
|
152
166
|
|
|
153
167
|
|
|
168
|
+
class BrowserSessionHandle(BaseModel):
|
|
169
|
+
"""Browser session handle model."""
|
|
170
|
+
|
|
171
|
+
browser_session: BrowserSessionResponse = Field(description="The browser session associated with this handle.")
|
|
172
|
+
|
|
173
|
+
def session_id(self):
|
|
174
|
+
"""Returns the session ID for the browser session."""
|
|
175
|
+
return self.browser_session.session_id
|
|
176
|
+
|
|
177
|
+
def live_url(self, interactive: bool = True, embed: bool = False):
|
|
178
|
+
"""Returns the live URL for the browser session."""
|
|
179
|
+
return _encode_url(self.browser_session.live_url, interactive=interactive, embed=embed)
|
|
180
|
+
|
|
181
|
+
|
|
154
182
|
class TaskHandle:
|
|
155
183
|
"""A handle to a running task."""
|
|
156
184
|
|
|
157
|
-
def __init__(self, task_id: str, client: "SmoothClient"
|
|
185
|
+
def __init__(self, task_id: str, client: "SmoothClient"):
|
|
158
186
|
"""Initializes the task handle."""
|
|
159
187
|
self._client = client
|
|
160
|
-
self._poll_interval = poll_interval
|
|
161
|
-
self._timeout = timeout
|
|
162
188
|
self._task_response: TaskResponse | None = None
|
|
163
|
-
self._live_url = live_url
|
|
164
189
|
|
|
165
190
|
self.id = task_id
|
|
166
191
|
|
|
167
|
-
def result(self) -> TaskResponse:
|
|
192
|
+
def result(self, timeout: int | None = None, poll_interval: float = 1) -> TaskResponse:
|
|
168
193
|
"""Waits for the task to complete and returns the result."""
|
|
169
194
|
if self._task_response and self._task_response.status not in ["running", "waiting"]:
|
|
170
195
|
return self._task_response
|
|
171
196
|
|
|
197
|
+
if timeout is not None and timeout < 1:
|
|
198
|
+
raise ValueError("Timeout must be at least 1 second.")
|
|
199
|
+
if poll_interval < 0.1:
|
|
200
|
+
raise ValueError("Poll interval must be at least 100 milliseconds.")
|
|
201
|
+
|
|
172
202
|
start_time = time.time()
|
|
173
|
-
while
|
|
203
|
+
while timeout is None or (time.time() - start_time) < timeout:
|
|
174
204
|
task_response = self._client._get_task(self.id) # pyright: ignore [reportPrivateUsage]
|
|
205
|
+
self._task_response = task_response
|
|
175
206
|
if task_response.status not in ["running", "waiting"]:
|
|
176
|
-
self._task_response = task_response
|
|
177
207
|
return task_response
|
|
178
|
-
time.sleep(
|
|
179
|
-
raise TimeoutError(f"Task {self.id} did not complete within {
|
|
208
|
+
time.sleep(poll_interval)
|
|
209
|
+
raise TimeoutError(f"Task {self.id} did not complete within {timeout} seconds.")
|
|
180
210
|
|
|
181
|
-
def live_url(self, interactive: bool = True, embed: bool = False
|
|
211
|
+
def live_url(self, interactive: bool = True, embed: bool = False, timeout: int | None = None):
|
|
182
212
|
"""Returns the live URL for the task."""
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
213
|
+
if self._task_response and self._task_response.live_url:
|
|
214
|
+
return _encode_url(self._task_response.live_url, interactive=interactive, embed=embed)
|
|
215
|
+
|
|
216
|
+
start_time = time.time()
|
|
217
|
+
while timeout is None or (time.time() - start_time) < timeout:
|
|
218
|
+
task_response = self._client._get_task(self.id) # pyright: ignore [reportPrivateUsage]
|
|
219
|
+
self._task_response = task_response
|
|
220
|
+
if self._task_response.live_url:
|
|
221
|
+
return _encode_url(self._task_response.live_url, interactive=interactive, embed=embed)
|
|
222
|
+
time.sleep(1)
|
|
188
223
|
|
|
189
|
-
|
|
224
|
+
raise TimeoutError(f"Live URL not available for task {self.id}.")
|
|
225
|
+
|
|
226
|
+
def recording_url(self, timeout: int | None = None) -> str:
|
|
190
227
|
"""Returns the recording URL for the task."""
|
|
191
228
|
if self._task_response and self._task_response.recording_url is not None:
|
|
192
229
|
return self._task_response.recording_url
|
|
193
230
|
|
|
194
231
|
start_time = time.time()
|
|
195
|
-
while (time.time() - start_time) <
|
|
232
|
+
while timeout is None or (time.time() - start_time) < timeout:
|
|
196
233
|
task_response = self._client._get_task(self.id) # pyright: ignore [reportPrivateUsage]
|
|
197
234
|
self._task_response = task_response
|
|
198
235
|
if task_response.recording_url is not None:
|
|
199
236
|
return task_response.recording_url
|
|
200
237
|
time.sleep(1)
|
|
201
|
-
raise TimeoutError(f"Recording not available for task {self.id}.")
|
|
238
|
+
raise TimeoutError(f"Recording URL not available for task {self.id}.")
|
|
202
239
|
|
|
203
240
|
|
|
204
241
|
class SmoothClient(BaseClient):
|
|
@@ -249,8 +286,6 @@ class SmoothClient(BaseClient):
|
|
|
249
286
|
def run(
|
|
250
287
|
self,
|
|
251
288
|
task: str,
|
|
252
|
-
poll_interval: int = 1,
|
|
253
|
-
timeout: int = 60 * 15,
|
|
254
289
|
agent: Literal["smooth"] = "smooth",
|
|
255
290
|
max_steps: int = 32,
|
|
256
291
|
device: Literal["desktop", "mobile"] = "mobile",
|
|
@@ -268,8 +303,6 @@ class SmoothClient(BaseClient):
|
|
|
268
303
|
|
|
269
304
|
Args:
|
|
270
305
|
task: The task to run.
|
|
271
|
-
poll_interval: The time in seconds to wait between polling for status.
|
|
272
|
-
timeout: The maximum time in seconds to wait for the task to complete.
|
|
273
306
|
agent: The agent to use for the task.
|
|
274
307
|
max_steps: Maximum number of steps the agent can take (max 64).
|
|
275
308
|
device: Device type for the task. Default is mobile.
|
|
@@ -286,11 +319,6 @@ class SmoothClient(BaseClient):
|
|
|
286
319
|
Raises:
|
|
287
320
|
ApiException: If the API request fails.
|
|
288
321
|
"""
|
|
289
|
-
if poll_interval < 0.1:
|
|
290
|
-
raise ValueError("Poll interval must be at least 100 milliseconds.")
|
|
291
|
-
if timeout < 1:
|
|
292
|
-
raise ValueError("Timeout must be at least 1 second.")
|
|
293
|
-
|
|
294
322
|
payload = TaskRequest(
|
|
295
323
|
task=task,
|
|
296
324
|
agent=agent,
|
|
@@ -304,14 +332,10 @@ class SmoothClient(BaseClient):
|
|
|
304
332
|
proxy_password=proxy_password,
|
|
305
333
|
)
|
|
306
334
|
initial_response = self._submit_task(payload)
|
|
307
|
-
start_time = time.time()
|
|
308
|
-
while time.time() - start_time < 16 and initial_response.live_url is None:
|
|
309
|
-
initial_response = self._get_task(initial_response.id)
|
|
310
|
-
time.sleep(poll_interval)
|
|
311
335
|
|
|
312
|
-
return TaskHandle(initial_response.id, self
|
|
336
|
+
return TaskHandle(initial_response.id, self)
|
|
313
337
|
|
|
314
|
-
def open_session(self, session_id: str | None = None) ->
|
|
338
|
+
def open_session(self, session_id: str | None = None) -> BrowserSessionHandle:
|
|
315
339
|
"""Gets an interactive browser instance.
|
|
316
340
|
|
|
317
341
|
Args:
|
|
@@ -329,7 +353,7 @@ class SmoothClient(BaseClient):
|
|
|
329
353
|
json=BrowserSessionRequest(session_id=session_id).model_dump(exclude_none=True),
|
|
330
354
|
)
|
|
331
355
|
data = self._handle_response(response)
|
|
332
|
-
return BrowserSessionResponse(**data["r"])
|
|
356
|
+
return BrowserSessionHandle(browser_session=BrowserSessionResponse(**data["r"]))
|
|
333
357
|
except requests.exceptions.RequestException as e:
|
|
334
358
|
logger.error(f"Request failed: {e}")
|
|
335
359
|
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
@@ -367,51 +391,61 @@ class SmoothClient(BaseClient):
|
|
|
367
391
|
class AsyncTaskHandle:
|
|
368
392
|
"""An asynchronous handle to a running task."""
|
|
369
393
|
|
|
370
|
-
def __init__(self, task_id: str, client: "SmoothAsyncClient"
|
|
394
|
+
def __init__(self, task_id: str, client: "SmoothAsyncClient"):
|
|
371
395
|
"""Initializes the asynchronous task handle."""
|
|
372
396
|
self._client = client
|
|
373
|
-
self._poll_interval = poll_interval
|
|
374
|
-
self._timeout = timeout
|
|
375
|
-
self._live_url = live_url
|
|
376
397
|
self._task_response: TaskResponse | None = None
|
|
377
398
|
|
|
378
399
|
self.id = task_id
|
|
379
400
|
|
|
380
|
-
async def result(self) -> TaskResponse:
|
|
401
|
+
async def result(self, timeout: int | None = None, poll_interval: float = 1) -> TaskResponse:
|
|
381
402
|
"""Waits for the task to complete and returns the result."""
|
|
382
403
|
if self._task_response and self._task_response.status not in ["running", "waiting"]:
|
|
383
404
|
return self._task_response
|
|
384
405
|
|
|
406
|
+
if timeout is not None and timeout < 1:
|
|
407
|
+
raise ValueError("Timeout must be at least 1 second.")
|
|
408
|
+
if poll_interval < 0.1:
|
|
409
|
+
raise ValueError("Poll interval must be at least 100 milliseconds.")
|
|
410
|
+
|
|
385
411
|
start_time = time.time()
|
|
386
|
-
while
|
|
412
|
+
while timeout is None or (time.time() - start_time) < timeout:
|
|
387
413
|
task_response = await self._client._get_task(self.id) # pyright: ignore [reportPrivateUsage]
|
|
388
414
|
self._task_response = task_response
|
|
389
415
|
if task_response.status not in ["running", "waiting"]:
|
|
390
416
|
return task_response
|
|
391
|
-
await asyncio.sleep(
|
|
392
|
-
raise TimeoutError(f"Task {self.id} did not complete within {
|
|
417
|
+
await asyncio.sleep(poll_interval)
|
|
418
|
+
raise TimeoutError(f"Task {self.id} did not complete within {timeout} seconds.")
|
|
393
419
|
|
|
394
|
-
def live_url(self, interactive: bool = True, embed: bool = False
|
|
420
|
+
async def live_url(self, interactive: bool = True, embed: bool = False, timeout: int | None = None):
|
|
395
421
|
"""Returns the live URL for the task."""
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
"embed": embed
|
|
399
|
-
}
|
|
400
|
-
return f"{self._live_url}?{urlencode(params)}"
|
|
422
|
+
if self._task_response and self._task_response.live_url:
|
|
423
|
+
return _encode_url(self._task_response.live_url, interactive=interactive, embed=embed)
|
|
401
424
|
|
|
402
|
-
|
|
425
|
+
start_time = time.time()
|
|
426
|
+
while timeout is None or (time.time() - start_time) < timeout:
|
|
427
|
+
task_response = await self._client._get_task(self.id) # pyright: ignore [reportPrivateUsage]
|
|
428
|
+
self._task_response = task_response
|
|
429
|
+
if task_response.live_url is not None:
|
|
430
|
+
return _encode_url(self._task_response.live_url, interactive=interactive, embed=embed)
|
|
431
|
+
await asyncio.sleep(1)
|
|
432
|
+
|
|
433
|
+
raise TimeoutError(f"Live URL not available for task {self.id}.")
|
|
434
|
+
|
|
435
|
+
async def recording_url(self, timeout: int | None = None):
|
|
403
436
|
"""Returns the recording URL for the task."""
|
|
404
437
|
if self._task_response and self._task_response.recording_url is not None:
|
|
405
438
|
return self._task_response.recording_url
|
|
406
439
|
|
|
407
440
|
start_time = time.time()
|
|
408
|
-
while (time.time() - start_time) <
|
|
441
|
+
while timeout is None or (time.time() - start_time) < timeout:
|
|
409
442
|
task_response = await self._client._get_task(self.id) # pyright: ignore [reportPrivateUsage]
|
|
410
443
|
self._task_response = task_response
|
|
411
444
|
if task_response.recording_url is not None:
|
|
412
445
|
return task_response.recording_url
|
|
413
446
|
await asyncio.sleep(1)
|
|
414
|
-
|
|
447
|
+
|
|
448
|
+
raise TimeoutError(f"Recording URL not available for task {self.id}.")
|
|
415
449
|
|
|
416
450
|
class SmoothAsyncClient(BaseClient):
|
|
417
451
|
"""An asynchronous client for the API."""
|
|
@@ -464,8 +498,6 @@ class SmoothAsyncClient(BaseClient):
|
|
|
464
498
|
proxy_server: str | None = None,
|
|
465
499
|
proxy_username: str | None = None,
|
|
466
500
|
proxy_password: str | None = None,
|
|
467
|
-
poll_interval: int = 1,
|
|
468
|
-
timeout: int = 60 * 15,
|
|
469
501
|
) -> AsyncTaskHandle:
|
|
470
502
|
"""Runs a task and returns a handle to the task asynchronously.
|
|
471
503
|
|
|
@@ -492,11 +524,6 @@ class SmoothAsyncClient(BaseClient):
|
|
|
492
524
|
Raises:
|
|
493
525
|
ApiException: If the API request fails.
|
|
494
526
|
"""
|
|
495
|
-
if poll_interval < 0.1:
|
|
496
|
-
raise ValueError("Poll interval must be at least 100 milliseconds.")
|
|
497
|
-
if timeout < 1:
|
|
498
|
-
raise ValueError("Timeout must be at least 1 second.")
|
|
499
|
-
|
|
500
527
|
payload = TaskRequest(
|
|
501
528
|
task=task,
|
|
502
529
|
agent=agent,
|
|
@@ -511,13 +538,9 @@ class SmoothAsyncClient(BaseClient):
|
|
|
511
538
|
)
|
|
512
539
|
|
|
513
540
|
initial_response = await self._submit_task(payload)
|
|
514
|
-
|
|
515
|
-
while time.time() - start_time < 16 and initial_response.live_url is None:
|
|
516
|
-
initial_response = await self._get_task(initial_response.id)
|
|
517
|
-
await asyncio.sleep(poll_interval)
|
|
518
|
-
return AsyncTaskHandle(initial_response.id, self, initial_response.live_url, poll_interval, timeout)
|
|
541
|
+
return AsyncTaskHandle(initial_response.id, self)
|
|
519
542
|
|
|
520
|
-
async def open_session(self, session_id: str | None = None) ->
|
|
543
|
+
async def open_session(self, session_id: str | None = None) -> BrowserSessionHandle:
|
|
521
544
|
"""Opens an interactive browser instance asynchronously.
|
|
522
545
|
|
|
523
546
|
Args:
|
|
@@ -535,7 +558,7 @@ class SmoothAsyncClient(BaseClient):
|
|
|
535
558
|
json=BrowserSessionRequest(session_id=session_id).model_dump(exclude_none=True),
|
|
536
559
|
)
|
|
537
560
|
data = self._handle_response(response)
|
|
538
|
-
return BrowserSessionResponse(**data["r"])
|
|
561
|
+
return BrowserSessionHandle(browser_session=BrowserSessionResponse(**data["r"]))
|
|
539
562
|
except httpx.RequestError as e:
|
|
540
563
|
logger.error(f"Request failed: {e}")
|
|
541
564
|
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
smooth/__init__.py,sha256=6DXnXHhf9vINX2U7U9fpruEU4BCReAXUiE0rbBTzoMw,22222
|
|
2
|
+
smooth_py-0.1.4.dist-info/METADATA,sha256=ySqmg9gw3t1G2dr4IEZTJ0pAK_U8jIgtUiJaZ4-zcUQ,5388
|
|
3
|
+
smooth_py-0.1.4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
4
|
+
smooth_py-0.1.4.dist-info/RECORD,,
|
smooth_py-0.1.3.dist-info/RECORD
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
smooth/__init__.py,sha256=dmofDTp_gE9sHBG3ZrCgApWQKtJn7a4MTOWuzxG6f_A,21078
|
|
2
|
-
smooth_py-0.1.3.dist-info/METADATA,sha256=DiTe2Cg4TNmJGCGX_qHOhrKYzwHzvKWIxYIq3Yrqi9w,5388
|
|
3
|
-
smooth_py-0.1.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
4
|
-
smooth_py-0.1.3.dist-info/RECORD,,
|
|
File without changes
|