smooth-py 0.2.8.dev20251008__tar.gz → 0.3.0__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.
- {smooth_py-0.2.8.dev20251008 → smooth_py-0.3.0}/PKG-INFO +1 -1
- {smooth_py-0.2.8.dev20251008 → smooth_py-0.3.0}/pyproject.toml +1 -1
- {smooth_py-0.2.8.dev20251008 → smooth_py-0.3.0}/src/smooth/__init__.py +56 -36
- {smooth_py-0.2.8.dev20251008 → smooth_py-0.3.0}/README.md +0 -0
- {smooth_py-0.2.8.dev20251008 → smooth_py-0.3.0}/src/smooth/mcp/__init__.py +0 -0
- {smooth_py-0.2.8.dev20251008 → smooth_py-0.3.0}/src/smooth/mcp/server.py +0 -0
|
@@ -13,13 +13,13 @@ from typing import Any, Literal, Type
|
|
|
13
13
|
import httpx
|
|
14
14
|
import requests
|
|
15
15
|
from deprecated import deprecated
|
|
16
|
-
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
16
|
+
from pydantic import BaseModel, ConfigDict, Field, computed_field, model_validator
|
|
17
17
|
|
|
18
18
|
# Configure logging
|
|
19
19
|
logger = logging.getLogger("smooth")
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
BASE_URL = "https://
|
|
22
|
+
BASE_URL = "https://api.smooth.sh/api/"
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
# --- Utils ---
|
|
@@ -100,11 +100,12 @@ class TaskRequest(BaseModel):
|
|
|
100
100
|
@model_validator(mode="before")
|
|
101
101
|
@classmethod
|
|
102
102
|
def _handle_deprecated_session_id(cls, data: Any) -> Any:
|
|
103
|
-
if isinstance(data, dict) and "session_id" in data:
|
|
103
|
+
if isinstance(data, dict) and "session_id" in data and "profile_id" not in data:
|
|
104
104
|
warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
|
|
105
105
|
data["profile_id"] = data.pop("session_id")
|
|
106
106
|
return data
|
|
107
107
|
|
|
108
|
+
@computed_field(return_type=str | None)
|
|
108
109
|
@property
|
|
109
110
|
def session_id(self):
|
|
110
111
|
"""(Deprecated) Returns the session ID."""
|
|
@@ -112,12 +113,12 @@ class TaskRequest(BaseModel):
|
|
|
112
113
|
return self.profile_id
|
|
113
114
|
|
|
114
115
|
@session_id.setter
|
|
115
|
-
def session_id(self, value):
|
|
116
|
+
def session_id(self, value: str | None):
|
|
116
117
|
"""(Deprecated) Sets the session ID."""
|
|
117
118
|
warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
|
|
118
119
|
self.profile_id = value
|
|
119
120
|
|
|
120
|
-
def model_dump(self, **kwargs) -> dict[str, Any]:
|
|
121
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
121
122
|
"""Dump model to dict, including deprecated session_id for retrocompatibility."""
|
|
122
123
|
data = super().model_dump(**kwargs)
|
|
123
124
|
# Add deprecated session_id field for retrocompatibility
|
|
@@ -137,11 +138,12 @@ class BrowserSessionRequest(BaseModel):
|
|
|
137
138
|
@model_validator(mode="before")
|
|
138
139
|
@classmethod
|
|
139
140
|
def _handle_deprecated_session_id(cls, data: Any) -> Any:
|
|
140
|
-
if isinstance(data, dict) and "session_id" in data:
|
|
141
|
+
if isinstance(data, dict) and "session_id" in data and "profile_id" not in data:
|
|
141
142
|
warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
|
|
142
143
|
data["profile_id"] = data.pop("session_id")
|
|
143
144
|
return data
|
|
144
145
|
|
|
146
|
+
@computed_field(return_type=str | None)
|
|
145
147
|
@property
|
|
146
148
|
def session_id(self):
|
|
147
149
|
"""(Deprecated) Returns the session ID."""
|
|
@@ -149,12 +151,12 @@ class BrowserSessionRequest(BaseModel):
|
|
|
149
151
|
return self.profile_id
|
|
150
152
|
|
|
151
153
|
@session_id.setter
|
|
152
|
-
def session_id(self, value):
|
|
154
|
+
def session_id(self, value: str | None):
|
|
153
155
|
"""(Deprecated) Sets the session ID."""
|
|
154
156
|
warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
|
|
155
157
|
self.profile_id = value
|
|
156
158
|
|
|
157
|
-
def model_dump(self, **kwargs) -> dict[str, Any]:
|
|
159
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
158
160
|
"""Dump model to dict, including deprecated session_id for retrocompatibility."""
|
|
159
161
|
data = super().model_dump(**kwargs)
|
|
160
162
|
# Add deprecated session_id field for retrocompatibility
|
|
@@ -173,11 +175,12 @@ class BrowserSessionResponse(BaseModel):
|
|
|
173
175
|
@model_validator(mode="before")
|
|
174
176
|
@classmethod
|
|
175
177
|
def _handle_deprecated_session_id(cls, data: Any) -> Any:
|
|
176
|
-
if isinstance(data, dict) and "session_id" in data:
|
|
178
|
+
if isinstance(data, dict) and "session_id" in data and "profile_id" not in data:
|
|
177
179
|
warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
|
|
178
180
|
data["profile_id"] = data.pop("session_id")
|
|
179
181
|
return data
|
|
180
182
|
|
|
183
|
+
@computed_field(return_type=str | None)
|
|
181
184
|
@property
|
|
182
185
|
def session_id(self):
|
|
183
186
|
"""(Deprecated) Returns the session ID."""
|
|
@@ -185,19 +188,11 @@ class BrowserSessionResponse(BaseModel):
|
|
|
185
188
|
return self.profile_id
|
|
186
189
|
|
|
187
190
|
@session_id.setter
|
|
188
|
-
def session_id(self, value):
|
|
191
|
+
def session_id(self, value: str):
|
|
189
192
|
"""(Deprecated) Sets the session ID."""
|
|
190
193
|
warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
|
|
191
194
|
self.profile_id = value
|
|
192
195
|
|
|
193
|
-
def model_dump(self, **kwargs) -> dict[str, Any]:
|
|
194
|
-
"""Dump model to dict, including deprecated session_id for retrocompatibility."""
|
|
195
|
-
data = super().model_dump(**kwargs)
|
|
196
|
-
# Add deprecated session_id field for retrocompatibility
|
|
197
|
-
if "profile_id" in data:
|
|
198
|
-
data["session_id"] = data["profile_id"]
|
|
199
|
-
return data
|
|
200
|
-
|
|
201
196
|
|
|
202
197
|
class BrowserProfilesResponse(BaseModel):
|
|
203
198
|
"""Response model for listing browser profiles."""
|
|
@@ -207,11 +202,12 @@ class BrowserProfilesResponse(BaseModel):
|
|
|
207
202
|
@model_validator(mode="before")
|
|
208
203
|
@classmethod
|
|
209
204
|
def _handle_deprecated_session_ids(cls, data: Any) -> Any:
|
|
210
|
-
if isinstance(data, dict) and "session_ids" in data:
|
|
205
|
+
if isinstance(data, dict) and "session_ids" in data and "profile_ids" not in data:
|
|
211
206
|
warnings.warn("'session_ids' is deprecated, use 'profile_ids' instead", DeprecationWarning, stacklevel=2)
|
|
212
207
|
data["profile_ids"] = data.pop("session_ids")
|
|
213
208
|
return data
|
|
214
209
|
|
|
210
|
+
@computed_field(return_type=list[str])
|
|
215
211
|
@property
|
|
216
212
|
def session_ids(self):
|
|
217
213
|
"""(Deprecated) Returns the session IDs."""
|
|
@@ -219,12 +215,12 @@ class BrowserProfilesResponse(BaseModel):
|
|
|
219
215
|
return self.profile_ids
|
|
220
216
|
|
|
221
217
|
@session_ids.setter
|
|
222
|
-
def session_ids(self, value):
|
|
218
|
+
def session_ids(self, value: list[str]):
|
|
223
219
|
"""(Deprecated) Sets the session IDs."""
|
|
224
220
|
warnings.warn("'session_ids' is deprecated, use 'profile_ids' instead", DeprecationWarning, stacklevel=2)
|
|
225
221
|
self.profile_ids = value
|
|
226
222
|
|
|
227
|
-
def model_dump(self, **kwargs) -> dict[str, Any]:
|
|
223
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
228
224
|
"""Dump model to dict, including deprecated session_ids for retrocompatibility."""
|
|
229
225
|
data = super().model_dump(**kwargs)
|
|
230
226
|
# Add deprecated session_ids field for retrocompatibility
|
|
@@ -351,12 +347,7 @@ class TaskHandle:
|
|
|
351
347
|
|
|
352
348
|
def stop(self):
|
|
353
349
|
"""Stops the task."""
|
|
354
|
-
|
|
355
|
-
response = self._client._client.delete(f"{self._client.base_url}/task/{self._id}")
|
|
356
|
-
self._handle_response(response)
|
|
357
|
-
except requests.exceptions.RequestException as e:
|
|
358
|
-
logger.error(f"Request failed: {e}")
|
|
359
|
-
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
350
|
+
self._client._delete_task(self._id)
|
|
360
351
|
|
|
361
352
|
def result(self, timeout: int | None = None, poll_interval: float = 1) -> TaskResponse:
|
|
362
353
|
"""Waits for the task to complete and returns the result."""
|
|
@@ -452,6 +443,18 @@ class SmoothClient(BaseClient):
|
|
|
452
443
|
logger.error(f"Request failed: {e}")
|
|
453
444
|
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
454
445
|
|
|
446
|
+
def _delete_task(self, task_id: str):
|
|
447
|
+
"""Deletes a task."""
|
|
448
|
+
if not task_id:
|
|
449
|
+
raise ValueError("Task ID cannot be empty.")
|
|
450
|
+
|
|
451
|
+
try:
|
|
452
|
+
response = self._session.delete(f"{self.base_url}/task/{task_id}")
|
|
453
|
+
self._handle_response(response)
|
|
454
|
+
except requests.exceptions.RequestException as e:
|
|
455
|
+
logger.error(f"Request failed: {e}")
|
|
456
|
+
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
457
|
+
|
|
455
458
|
def run(
|
|
456
459
|
self,
|
|
457
460
|
task: str,
|
|
@@ -483,7 +486,7 @@ class SmoothClient(BaseClient):
|
|
|
483
486
|
response_model: If provided, the schema describing the desired output structure.
|
|
484
487
|
url: The starting URL for the task. If not provided, the agent will infer it from the task.
|
|
485
488
|
metadata: A dictionary containing variables or parameters that will be passed to the agent.
|
|
486
|
-
files: A
|
|
489
|
+
files: A list of file ids to pass to the agent.
|
|
487
490
|
agent: The agent to use for the task.
|
|
488
491
|
max_steps: Maximum number of steps the agent can take (max 64).
|
|
489
492
|
device: Device type for the task. Default is mobile.
|
|
@@ -507,7 +510,7 @@ class SmoothClient(BaseClient):
|
|
|
507
510
|
"""
|
|
508
511
|
payload = TaskRequest(
|
|
509
512
|
task=task,
|
|
510
|
-
response_model=response_model
|
|
513
|
+
response_model=response_model if isinstance(response_model, dict | None) else response_model.model_json_schema(),
|
|
511
514
|
url=url,
|
|
512
515
|
metadata=metadata,
|
|
513
516
|
files=files,
|
|
@@ -660,6 +663,10 @@ class AsyncTaskHandle:
|
|
|
660
663
|
"""Returns the task ID."""
|
|
661
664
|
return self._id
|
|
662
665
|
|
|
666
|
+
async def stop(self):
|
|
667
|
+
"""Stops the task."""
|
|
668
|
+
await self._client._delete_task(self._id)
|
|
669
|
+
|
|
663
670
|
async def result(self, timeout: int | None = None, poll_interval: float = 1) -> TaskResponse:
|
|
664
671
|
"""Waits for the task to complete and returns the result."""
|
|
665
672
|
if self._task_response and self._task_response.status not in ["running", "waiting"]:
|
|
@@ -679,7 +686,7 @@ class AsyncTaskHandle:
|
|
|
679
686
|
await asyncio.sleep(poll_interval)
|
|
680
687
|
raise TimeoutError(f"Task {self.id()} did not complete within {timeout} seconds.")
|
|
681
688
|
|
|
682
|
-
async def live_url(self, interactive: bool =
|
|
689
|
+
async def live_url(self, interactive: bool = False, embed: bool = False, timeout: int | None = None):
|
|
683
690
|
"""Returns the live URL for the task."""
|
|
684
691
|
if self._task_response and self._task_response.live_url:
|
|
685
692
|
return _encode_url(self._task_response.live_url, interactive=interactive, embed=embed)
|
|
@@ -688,13 +695,13 @@ class AsyncTaskHandle:
|
|
|
688
695
|
while timeout is None or (time.time() - start_time) < timeout:
|
|
689
696
|
task_response = await self._client._get_task(self.id())
|
|
690
697
|
self._task_response = task_response
|
|
691
|
-
if
|
|
698
|
+
if self._task_response.live_url:
|
|
692
699
|
return _encode_url(self._task_response.live_url, interactive=interactive, embed=embed)
|
|
693
700
|
await asyncio.sleep(1)
|
|
694
701
|
|
|
695
702
|
raise TimeoutError(f"Live URL not available for task {self.id()}.")
|
|
696
703
|
|
|
697
|
-
async def recording_url(self, timeout: int | None = None):
|
|
704
|
+
async def recording_url(self, timeout: int | None = None) -> str:
|
|
698
705
|
"""Returns the recording URL for the task."""
|
|
699
706
|
if self._task_response and self._task_response.recording_url is not None:
|
|
700
707
|
return self._task_response.recording_url
|
|
@@ -749,6 +756,18 @@ class SmoothAsyncClient(BaseClient):
|
|
|
749
756
|
logger.error(f"Request failed: {e}")
|
|
750
757
|
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
751
758
|
|
|
759
|
+
async def _delete_task(self, task_id: str):
|
|
760
|
+
"""Deletes a task asynchronously."""
|
|
761
|
+
if not task_id:
|
|
762
|
+
raise ValueError("Task ID cannot be empty.")
|
|
763
|
+
|
|
764
|
+
try:
|
|
765
|
+
response = await self._client.delete(f"{self.base_url}/task/{task_id}")
|
|
766
|
+
self._handle_response(response)
|
|
767
|
+
except httpx.RequestError as e:
|
|
768
|
+
logger.error(f"Request failed: {e}")
|
|
769
|
+
raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
|
|
770
|
+
|
|
752
771
|
async def run(
|
|
753
772
|
self,
|
|
754
773
|
task: str,
|
|
@@ -780,7 +799,7 @@ class SmoothAsyncClient(BaseClient):
|
|
|
780
799
|
response_model: If provided, the schema describing the desired output structure.
|
|
781
800
|
url: The starting URL for the task. If not provided, the agent will infer it from the task.
|
|
782
801
|
metadata: A dictionary containing variables or parameters that will be passed to the agent.
|
|
783
|
-
files: A
|
|
802
|
+
files: A list of file ids to pass to the agent.
|
|
784
803
|
agent: The agent to use for the task.
|
|
785
804
|
max_steps: Maximum number of steps the agent can take (max 64).
|
|
786
805
|
device: Device type for the task. Default is mobile.
|
|
@@ -804,7 +823,7 @@ class SmoothAsyncClient(BaseClient):
|
|
|
804
823
|
"""
|
|
805
824
|
payload = TaskRequest(
|
|
806
825
|
task=task,
|
|
807
|
-
response_model=response_model
|
|
826
|
+
response_model=response_model if isinstance(response_model, dict | None) else response_model.model_json_schema(),
|
|
808
827
|
url=url,
|
|
809
828
|
metadata=metadata,
|
|
810
829
|
files=files,
|
|
@@ -831,8 +850,8 @@ class SmoothAsyncClient(BaseClient):
|
|
|
831
850
|
"""Opens an interactive browser instance asynchronously.
|
|
832
851
|
|
|
833
852
|
Args:
|
|
853
|
+
profile_id: The profile ID to use for the session. If None, a new profile will be created.
|
|
834
854
|
session_id: (Deprecated, now `profile_id`) The session ID to associate with the browser.
|
|
835
|
-
profile_id: The profile ID to associate with the browser.
|
|
836
855
|
live_view: Whether to enable live view for the session.
|
|
837
856
|
|
|
838
857
|
Returns:
|
|
@@ -917,11 +936,12 @@ class SmoothAsyncClient(BaseClient):
|
|
|
917
936
|
if name is None:
|
|
918
937
|
raise ValueError("File name must be provided or the file object must have a 'name' attribute.")
|
|
919
938
|
|
|
920
|
-
files = {"file": (Path(name).name, file)}
|
|
921
939
|
if purpose:
|
|
922
940
|
data = {"file_purpose": purpose}
|
|
923
941
|
else:
|
|
924
942
|
data = None
|
|
943
|
+
|
|
944
|
+
files = {"file": (Path(name).name, file)}
|
|
925
945
|
response = await self._client.post(f"{self.base_url}/file", files=files, data=data)
|
|
926
946
|
data = self._handle_response(response)
|
|
927
947
|
return UploadFileResponse(**data["r"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|