smooth-py 0.3.1.dev20251027__py3-none-any.whl → 0.3.1.post0.dev20251028__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
@@ -89,6 +89,7 @@ class TaskResponse(BaseModel):
89
89
  device: Literal["desktop", "mobile"] | None = Field(default=None, description="The device type used for the task.")
90
90
  live_url: str | None = Field(default=None, description="The URL to view and interact with the task execution.")
91
91
  recording_url: str | None = Field(default=None, description="The URL to view the task recording.")
92
+ downloads_url: str | None = Field(default=None, description="The URL of the archive containing the downloaded files.")
92
93
  created_at: int | None = Field(default=None, description="The timestamp when the task was created.")
93
94
 
94
95
 
@@ -111,7 +112,7 @@ class TaskRequest(BaseModel):
111
112
  files: list[str] | None = Field(default=None, description="A list of file ids to pass to the agent.")
112
113
  agent: Literal["smooth", "smooth-lite"] = Field(default="smooth", description="The agent to use for the task.")
113
114
  max_steps: int = Field(default=32, ge=2, le=128, description="Maximum number of steps the agent can take (min 2, max 128).")
114
- device: Literal["desktop", "mobile"] = Field(default="mobile", description="Device type for the task. Default is mobile.")
115
+ device: Literal["desktop", "mobile"] = Field(default="desktop", description="Device type for the task. Default is desktop.")
115
116
  allowed_urls: list[str] | None = Field(
116
117
  default=None,
117
118
  description=(
@@ -148,6 +149,7 @@ class TaskRequest(BaseModel):
148
149
  " - `password` (optional): Password to decrypt the certificate file."
149
150
  ),
150
151
  )
152
+ use_adblock: bool | None = Field(default=True, description="Enable adblock for the browser session. Default is True.")
151
153
  experimental_features: dict[str, Any] | None = Field(
152
154
  default=None, description="Experimental features to enable for the task."
153
155
  )
@@ -464,6 +466,28 @@ class TaskHandle:
464
466
  time.sleep(1)
465
467
  raise TimeoutError(f"Recording URL not available for task {self.id()}.")
466
468
 
469
+ def downloads_url(self, timeout: int | None = None) -> str:
470
+ """Returns the downloads URL for the task."""
471
+ if self._task_response and self._task_response.downloads_url is not None:
472
+ return self._task_response.downloads_url
473
+
474
+ start_time = time.time()
475
+ while timeout is None or (time.time() - start_time) < timeout:
476
+ task_response = self._client._get_task(self.id(), query_params={"downloads": "true"})
477
+ self._task_response = task_response
478
+ if task_response.downloads_url is not None:
479
+ if not task_response.downloads_url:
480
+ raise ApiError(
481
+ status_code=404,
482
+ detail=(
483
+ f"Downloads URL not available for task {self.id()}."
484
+ " Make sure the task downloaded files during its execution."
485
+ )
486
+ )
487
+ return task_response.downloads_url
488
+ time.sleep(1)
489
+ raise TimeoutError(f"Downloads URL not available for task {self.id()}.")
490
+
467
491
 
468
492
  class SmoothClient(BaseClient):
469
493
  """A synchronous client for the API."""
@@ -490,20 +514,21 @@ class SmoothClient(BaseClient):
490
514
  def _submit_task(self, payload: TaskRequest) -> TaskResponse:
491
515
  """Submits a task to be run."""
492
516
  try:
493
- response = self._session.post(f"{self.base_url}/task", json=payload.model_dump(exclude_none=True))
517
+ response = self._session.post(f"{self.base_url}/task", json=payload.model_dump())
494
518
  data = self._handle_response(response)
495
519
  return TaskResponse(**data["r"])
496
520
  except requests.exceptions.RequestException as e:
497
521
  logger.error(f"Request failed: {e}")
498
522
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
499
523
 
500
- def _get_task(self, task_id: str) -> TaskResponse:
524
+ def _get_task(self, task_id: str, query_params: dict[str, Any] | None = None) -> TaskResponse:
501
525
  """Retrieves the status and result of a task."""
502
526
  if not task_id:
503
527
  raise ValueError("Task ID cannot be empty.")
504
528
 
505
529
  try:
506
- response = self._session.get(f"{self.base_url}/task/{task_id}")
530
+ url = f"{self.base_url}/task/{task_id}"
531
+ response = self._session.get(url, params=query_params)
507
532
  data = self._handle_response(response)
508
533
  return TaskResponse(**data["r"])
509
534
  except requests.exceptions.RequestException as e:
@@ -542,6 +567,7 @@ class SmoothClient(BaseClient):
542
567
  proxy_username: str | None = None,
543
568
  proxy_password: str | None = None,
544
569
  certificates: list[Certificate] | None = None,
570
+ use_adblock: bool | None = True,
545
571
  experimental_features: dict[str, Any] | None = None,
546
572
  ) -> TaskHandle:
547
573
  """Runs a task and returns a handle to the task.
@@ -572,6 +598,7 @@ class SmoothClient(BaseClient):
572
598
  Each certificate is a dictionary with the following fields:
573
599
  - `file` (required): p12 file object to be uploaded (e.g., open("cert.p12", "rb")).
574
600
  - `password` (optional): Password to decrypt the certificate file, if password-protected.
601
+ use_adblock: Enable adblock for the browser session. Default is True.
575
602
  experimental_features: Experimental features to enable for the task.
576
603
 
577
604
  Returns:
@@ -598,6 +625,7 @@ class SmoothClient(BaseClient):
598
625
  proxy_username=proxy_username,
599
626
  proxy_password=proxy_password,
600
627
  certificates=_process_certificates(certificates),
628
+ use_adblock=use_adblock,
601
629
  experimental_features=experimental_features,
602
630
  )
603
631
  initial_response = self._submit_task(payload)
@@ -623,7 +651,7 @@ class SmoothClient(BaseClient):
623
651
  try:
624
652
  response = self._session.post(
625
653
  f"{self.base_url}/browser/session",
626
- json=BrowserSessionRequest(profile_id=profile_id or session_id, live_view=live_view).model_dump(exclude_none=True),
654
+ json=BrowserSessionRequest(profile_id=profile_id or session_id, live_view=live_view).model_dump(),
627
655
  )
628
656
  data = self._handle_response(response)
629
657
  return BrowserSessionHandle(browser_session=BrowserSessionResponse(**data["r"]))
@@ -797,6 +825,29 @@ class AsyncTaskHandle:
797
825
 
798
826
  raise TimeoutError(f"Recording URL not available for task {self.id()}.")
799
827
 
828
+ async def downloads_url(self, timeout: int | None = None) -> str:
829
+ """Returns the downloads URL for the task."""
830
+ if self._task_response and self._task_response.downloads_url is not None:
831
+ return self._task_response.downloads_url
832
+
833
+ start_time = time.time()
834
+ while timeout is None or (time.time() - start_time) < timeout:
835
+ task_response = await self._client._get_task(self.id(), query_params={"downloads": "true"})
836
+ self._task_response = task_response
837
+ if task_response.downloads_url is not None:
838
+ if not task_response.downloads_url:
839
+ raise ApiError(
840
+ status_code=404,
841
+ detail=(
842
+ f"Downloads URL not available for task {self.id()}."
843
+ " Make sure the task downloaded files during its execution."
844
+ )
845
+ )
846
+ return task_response.downloads_url
847
+ await asyncio.sleep(1)
848
+
849
+ raise TimeoutError(f"Downloads URL not available for task {self.id()}.")
850
+
800
851
 
801
852
  class SmoothAsyncClient(BaseClient):
802
853
  """An asynchronous client for the API."""
@@ -817,20 +868,21 @@ class SmoothAsyncClient(BaseClient):
817
868
  async def _submit_task(self, payload: TaskRequest) -> TaskResponse:
818
869
  """Submits a task to be run asynchronously."""
819
870
  try:
820
- response = await self._client.post(f"{self.base_url}/task", json=payload.model_dump(exclude_none=True))
871
+ response = await self._client.post(f"{self.base_url}/task", json=payload.model_dump())
821
872
  data = self._handle_response(response)
822
873
  return TaskResponse(**data["r"])
823
874
  except httpx.RequestError as e:
824
875
  logger.error(f"Request failed: {e}")
825
876
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
826
877
 
827
- async def _get_task(self, task_id: str) -> TaskResponse:
878
+ async def _get_task(self, task_id: str, query_params: dict[str, Any] | None = None) -> TaskResponse:
828
879
  """Retrieves the status and result of a task asynchronously."""
829
880
  if not task_id:
830
881
  raise ValueError("Task ID cannot be empty.")
831
882
 
832
883
  try:
833
- response = await self._client.get(f"{self.base_url}/task/{task_id}")
884
+ url = f"{self.base_url}/task/{task_id}"
885
+ response = await self._client.get(url, params=query_params)
834
886
  data = self._handle_response(response)
835
887
  return TaskResponse(**data["r"])
836
888
  except httpx.RequestError as e:
@@ -869,6 +921,7 @@ class SmoothAsyncClient(BaseClient):
869
921
  proxy_username: str | None = None,
870
922
  proxy_password: str | None = None,
871
923
  certificates: list[Certificate] | None = None,
924
+ use_adblock: bool | None = True,
872
925
  experimental_features: dict[str, Any] | None = None,
873
926
  ) -> AsyncTaskHandle:
874
927
  """Runs a task and returns a handle to the task asynchronously.
@@ -899,6 +952,7 @@ class SmoothAsyncClient(BaseClient):
899
952
  Each certificate is a dictionary with the following fields:
900
953
  - `file` (required): p12 file object to be uploaded (e.g., open("cert.p12", "rb")).
901
954
  - `password` (optional): Password to decrypt the certificate file.
955
+ use_adblock: Enable adblock for the browser session. Default is True.
902
956
  experimental_features: Experimental features to enable for the task.
903
957
 
904
958
  Returns:
@@ -925,6 +979,7 @@ class SmoothAsyncClient(BaseClient):
925
979
  proxy_username=proxy_username,
926
980
  proxy_password=proxy_password,
927
981
  certificates=_process_certificates(certificates),
982
+ use_adblock=use_adblock,
928
983
  experimental_features=experimental_features,
929
984
  )
930
985
 
@@ -950,7 +1005,7 @@ class SmoothAsyncClient(BaseClient):
950
1005
  try:
951
1006
  response = await self._client.post(
952
1007
  f"{self.base_url}/browser/session",
953
- json=BrowserSessionRequest(profile_id=profile_id or session_id, live_view=live_view).model_dump(exclude_none=True),
1008
+ json=BrowserSessionRequest(profile_id=profile_id or session_id, live_view=live_view).model_dump(),
954
1009
  )
955
1010
  data = self._handle_response(response)
956
1011
  return BrowserSessionHandle(browser_session=BrowserSessionResponse(**data["r"]))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: smooth-py
3
- Version: 0.3.1.dev20251027
3
+ Version: 0.3.1.post0.dev20251028
4
4
  Summary:
5
5
  Author: Luca Pinchetti
6
6
  Author-email: luca@circlemind.co
@@ -0,0 +1,6 @@
1
+ smooth/__init__.py,sha256=HpLvLf7rhN4w4CT06ZpW8KHE-GNP6x1LrS_NhfAetro,44637
2
+ smooth/mcp/__init__.py,sha256=0aJVFi2a8Ah3-5xtgyZ5UMbaaJsBWu2T8QLWoFQITk8,219
3
+ smooth/mcp/server.py,sha256=9SymTD4NOGTMN8P-LNGlvYNvv81yCIZfZeeuhEcAc6s,20068
4
+ smooth_py-0.3.1.post0.dev20251028.dist-info/METADATA,sha256=n119UWy4ReLHLGtjRJ4GXqrDReIXl7b87uQnrKUEvec,7535
5
+ smooth_py-0.3.1.post0.dev20251028.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
6
+ smooth_py-0.3.1.post0.dev20251028.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- smooth/__init__.py,sha256=5ebKGse4vLyqBRFRcCZX2qTB2LAkKf--LgBNeF8rmek,42091
2
- smooth/mcp/__init__.py,sha256=0aJVFi2a8Ah3-5xtgyZ5UMbaaJsBWu2T8QLWoFQITk8,219
3
- smooth/mcp/server.py,sha256=9SymTD4NOGTMN8P-LNGlvYNvv81yCIZfZeeuhEcAc6s,20068
4
- smooth_py-0.3.1.dev20251027.dist-info/METADATA,sha256=BeMJNBO-bSmV94LjZQJ8SjaCtUUuh7Pq8c7HpJv-N7k,7529
5
- smooth_py-0.3.1.dev20251027.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
6
- smooth_py-0.3.1.dev20251027.dist-info/RECORD,,