smooth-py 0.1.3__tar.gz → 0.1.4__tar.gz

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

Potentially problematic release.


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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: smooth-py
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary:
5
5
  Author: Luca Pinchetti
6
6
  Author-email: luca@circlemind.co
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "smooth-py"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  description = ""
5
5
  authors = [
6
6
  {name = "Luca Pinchetti",email = "luca@circlemind.co"}
@@ -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", live_url: str | None, poll_interval: int, timeout: int | None):
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 self._timeout is None or (time.time() - start_time) < self._timeout:
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(self._poll_interval)
179
- raise TimeoutError(f"Task {self.id} did not complete within {self._timeout} seconds.")
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) -> str:
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
- params = {
184
- "interactive": interactive,
185
- "embed": embed
186
- }
187
- return f"{self._live_url}?{urlencode(params)}"
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
- def recording_url(self) -> str:
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) < 8:
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, initial_response.live_url, poll_interval, timeout)
336
+ return TaskHandle(initial_response.id, self)
313
337
 
314
- def open_session(self, session_id: str | None = None) -> BrowserSessionResponse:
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", live_url: str | None, poll_interval: int, timeout: int | None):
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 self._timeout is None or (time.time() - start_time) < self._timeout:
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(self._poll_interval)
392
- raise TimeoutError(f"Task {self.id} did not complete within {self._timeout} seconds.")
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) -> str:
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
- params = {
397
- "interactive": interactive,
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
- async def recording_url(self) -> str:
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) < 8:
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
- raise TimeoutError(f"Recording not available for task {self.id}.")
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
- start_time = time.time()
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) -> BrowserSessionResponse:
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
File without changes