smooth-py 0.2.6.dev20250922__tar.gz → 0.2.8.dev20251003__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.2.6.dev20250922
3
+ Version: 0.2.8.dev20251003
4
4
  Summary:
5
5
  Author: Luca Pinchetti
6
6
  Author-email: luca@circlemind.co
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
13
  Provides-Extra: mcp
14
+ Requires-Dist: deprecated (>=1.2.18,<2.0.0)
14
15
  Requires-Dist: fastmcp (>=2.12.0,<3.0.0) ; extra == "mcp"
15
16
  Requires-Dist: httpx (>=0.28.1,<0.29.0)
16
17
  Requires-Dist: pydantic (>=2.11.7,<3.0.0)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "smooth-py"
3
- version = "0.2.6.dev20250922"
3
+ version = "0.2.8.dev20251003"
4
4
  description = ""
5
5
  authors = [
6
6
  {name = "Luca Pinchetti",email = "luca@circlemind.co"}
@@ -10,7 +10,8 @@ requires-python = ">=3.10,<4.0"
10
10
  dependencies = [
11
11
  "pydantic (>=2.11.7,<3.0.0)",
12
12
  "httpx (>=0.28.1,<0.29.0)",
13
- "requests (>=2.32.5,<3.0.0)"
13
+ "requests (>=2.32.5,<3.0.0)",
14
+ "deprecated (>=1.2.18,<2.0.0)"
14
15
  ]
15
16
 
16
17
  [project.optional-dependencies]
@@ -6,12 +6,14 @@ import logging
6
6
  import os
7
7
  import time
8
8
  import urllib.parse
9
+ import warnings
9
10
  from pathlib import Path
10
11
  from typing import Any, Literal, Type
11
12
 
12
13
  import httpx
13
14
  import requests
14
- from pydantic import BaseModel, Field
15
+ from deprecated import deprecated
16
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
15
17
 
16
18
  # Configure logging
17
19
  logger = logging.getLogger("smooth")
@@ -36,6 +38,7 @@ def _encode_url(url: str, interactive: bool = True, embed: bool = False) -> str:
36
38
 
37
39
  class TaskResponse(BaseModel):
38
40
  """Task response model."""
41
+ model_config = ConfigDict(extra='allow')
39
42
 
40
43
  id: str = Field(description="The ID of the task.")
41
44
  status: Literal["waiting", "running", "done", "failed"] = Field(description="The status of the task.")
@@ -67,13 +70,16 @@ class TaskRequest(BaseModel):
67
70
  allowed_urls: list[str] | None = Field(
68
71
  default=None,
69
72
  description=(
70
- "List of allowed URL patterns using wildcard syntax (e.g., https://example.com/*). If None, all URLs are allowed."
73
+ "List of allowed URL patterns using wildcard syntax (e.g., https://*example.com/*). If None, all URLs are allowed."
71
74
  ),
72
75
  )
73
76
  enable_recording: bool = Field(default=True, description="Enable video recording of the task execution. Default is True")
74
- session_id: str | None = Field(
77
+ profile_id: str | None = Field(
75
78
  default=None,
76
- description="Browser session ID to use. Each session maintains its own state, such as login credentials.",
79
+ description=("Browser profile ID to use. Each profile maintains its own state, such as cookies and login credentials."),
80
+ )
81
+ profile_read_only: bool = Field(
82
+ default=False, description="If true, the profile specified by `profile_id` will be loaded in read-only mode."
77
83
  )
78
84
  stealth_mode: bool = Field(default=False, description="Run the browser in stealth mode.")
79
85
  proxy_server: str | None = Field(
@@ -84,29 +90,116 @@ class TaskRequest(BaseModel):
84
90
  )
85
91
  proxy_username: str | None = Field(default=None, description="Proxy server username.")
86
92
  proxy_password: str | None = Field(default=None, description="Proxy server password.")
93
+ experimental_features: dict[str, Any] | None = Field(
94
+ default=None, description="Experimental features to enable for the task."
95
+ )
96
+
97
+ @model_validator(mode="before")
98
+ @classmethod
99
+ def _handle_deprecated_session_id(cls, data: Any) -> Any:
100
+ if isinstance(data, dict) and "session_id" in data:
101
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
102
+ data["profile_id"] = data.pop("session_id")
103
+ return data
104
+
105
+ @property
106
+ def session_id(self):
107
+ """(Deprecated) Returns the session ID."""
108
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
109
+ return self.profile_id
110
+
111
+ @session_id.setter
112
+ def session_id(self, value):
113
+ """(Deprecated) Sets the session ID."""
114
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
115
+ self.profile_id = value
87
116
 
88
117
 
89
118
  class BrowserSessionRequest(BaseModel):
90
119
  """Request model for creating a browser session."""
91
120
 
92
- session_id: str | None = Field(
93
- default=None,
94
- description=("The session ID to open in the browser. If None, a new session will be created with a random name."),
121
+ profile_id: str | None = Field(
122
+ default=None, description=("The profile ID to use for the browser session. If None, a new profile will be created.")
95
123
  )
96
124
  live_view: bool | None = Field(default=True, description="Request a live URL to interact with the browser session.")
97
125
 
126
+ @model_validator(mode="before")
127
+ @classmethod
128
+ def _handle_deprecated_session_id(cls, data: Any) -> Any:
129
+ if isinstance(data, dict) and "session_id" in data:
130
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
131
+ data["profile_id"] = data.pop("session_id")
132
+ return data
133
+
134
+ @property
135
+ def session_id(self):
136
+ """(Deprecated) Returns the session ID."""
137
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
138
+ return self.profile_id
139
+
140
+ @session_id.setter
141
+ def session_id(self, value):
142
+ """(Deprecated) Sets the session ID."""
143
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
144
+ self.profile_id = value
145
+
98
146
 
99
147
  class BrowserSessionResponse(BaseModel):
100
148
  """Browser session response model."""
101
149
 
102
- session_id: str = Field(description="The ID of the browser session associated with the opened browser instance.")
150
+ profile_id: str = Field(description="The ID of the browser profile associated with the opened browser instance.")
103
151
  live_url: str | None = Field(default=None, description="The live URL to interact with the browser session.")
104
152
 
153
+ @model_validator(mode="before")
154
+ @classmethod
155
+ def _handle_deprecated_session_id(cls, data: Any) -> Any:
156
+ if isinstance(data, dict) and "session_id" in data:
157
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
158
+ data["profile_id"] = data.pop("session_id")
159
+ return data
160
+
161
+ @property
162
+ def session_id(self):
163
+ """(Deprecated) Returns the session ID."""
164
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
165
+ return self.profile_id
166
+
167
+ @session_id.setter
168
+ def session_id(self, value):
169
+ """(Deprecated) Sets the session ID."""
170
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
171
+ self.profile_id = value
172
+
105
173
 
106
- class BrowserSessionsResponse(BaseModel):
107
- """Response model for listing browser sessions."""
174
+ class BrowserProfilesResponse(BaseModel):
175
+ """Response model for listing browser profiles."""
108
176
 
109
- session_ids: list[str] = Field(description="The IDs of the browser sessions.")
177
+ profile_ids: list[str] = Field(description="The IDs of the browser profiles.")
178
+
179
+ @model_validator(mode="before")
180
+ @classmethod
181
+ def _handle_deprecated_session_ids(cls, data: Any) -> Any:
182
+ if isinstance(data, dict) and "session_ids" in data:
183
+ warnings.warn("'session_ids' is deprecated, use 'profile_ids' instead", DeprecationWarning, stacklevel=2)
184
+ data["profile_ids"] = data.pop("session_ids")
185
+ return data
186
+
187
+ @property
188
+ def session_ids(self):
189
+ """(Deprecated) Returns the session IDs."""
190
+ warnings.warn("'session_ids' is deprecated, use 'profile_ids' instead", DeprecationWarning, stacklevel=2)
191
+ return self.profile_ids
192
+
193
+ @session_ids.setter
194
+ def session_ids(self, value):
195
+ """(Deprecated) Sets the session IDs."""
196
+ warnings.warn("'session_ids' is deprecated, use 'profile_ids' instead", DeprecationWarning, stacklevel=2)
197
+ self.profile_ids = value
198
+
199
+
200
+ class BrowserSessionsResponse(BrowserProfilesResponse):
201
+ """Response model for listing browser profiles."""
202
+ pass
110
203
 
111
204
 
112
205
  class UploadFileResponse(BaseModel):
@@ -189,9 +282,14 @@ class BrowserSessionHandle(BaseModel):
189
282
 
190
283
  browser_session: BrowserSessionResponse = Field(description="The browser session associated with this handle.")
191
284
 
285
+ @deprecated("session_id is deprecated, use profile_id instead")
192
286
  def session_id(self):
193
287
  """Returns the session ID for the browser session."""
194
- return self.browser_session.session_id
288
+ return self.profile_id()
289
+
290
+ def profile_id(self):
291
+ """Returns the profile ID for the browser session."""
292
+ return self.browser_session.profile_id
195
293
 
196
294
  def live_url(self, interactive: bool = True, embed: bool = False):
197
295
  """Returns the live URL for the browser session."""
@@ -321,10 +419,13 @@ class SmoothClient(BaseClient):
321
419
  allowed_urls: list[str] | None = None,
322
420
  enable_recording: bool = False,
323
421
  session_id: str | None = None,
422
+ profile_id: str | None = None,
423
+ profile_read_only: bool = False,
324
424
  stealth_mode: bool = False,
325
425
  proxy_server: str | None = None,
326
426
  proxy_username: str | None = None,
327
427
  proxy_password: str | None = None,
428
+ experimental_features: dict[str, Any] | None = None,
328
429
  ) -> TaskHandle:
329
430
  """Runs a task and returns a handle to the task.
330
431
 
@@ -340,14 +441,17 @@ class SmoothClient(BaseClient):
340
441
  agent: The agent to use for the task.
341
442
  max_steps: Maximum number of steps the agent can take (max 64).
342
443
  device: Device type for the task. Default is mobile.
343
- allowed_urls: List of allowed URL patterns using wildcard syntax (e.g., https://example.com/*).
444
+ allowed_urls: List of allowed URL patterns using wildcard syntax (e.g., https://*example.com/*).
344
445
  If None, all URLs are allowed.
345
446
  enable_recording: Enable video recording of the task execution.
346
- session_id: Browser session ID to use.
447
+ session_id: (Deprecated, now `profile_id`) Browser session ID to use.
448
+ profile_id: Browser profile ID to use. Each profile maintains its own state, such as cookies and login credentials.
449
+ profile_read_only: If true, the profile specified by `profile_id` will be loaded in read-only mode.
347
450
  stealth_mode: Run the browser in stealth mode.
348
451
  proxy_server: Proxy server url to route browser traffic through.
349
452
  proxy_username: Proxy server username.
350
453
  proxy_password: Proxy server password.
454
+ experimental_features: Experimental features to enable for the task.
351
455
 
352
456
  Returns:
353
457
  A handle to the running task.
@@ -366,21 +470,26 @@ class SmoothClient(BaseClient):
366
470
  device=device,
367
471
  allowed_urls=allowed_urls,
368
472
  enable_recording=enable_recording,
369
- session_id=session_id,
473
+ profile_id=profile_id or session_id,
474
+ profile_read_only=profile_read_only,
370
475
  stealth_mode=stealth_mode,
371
476
  proxy_server=proxy_server,
372
477
  proxy_username=proxy_username,
373
478
  proxy_password=proxy_password,
479
+ experimental_features=experimental_features,
374
480
  )
375
481
  initial_response = self._submit_task(payload)
376
482
 
377
483
  return TaskHandle(initial_response.id, self)
378
484
 
379
- def open_session(self, session_id: str | None = None, live_view: bool = True) -> BrowserSessionHandle:
380
- """Gets an interactive browser instance.
485
+ def open_session(
486
+ self, profile_id: str | None = None, session_id: str | None = None, live_view: bool = True
487
+ ) -> BrowserSessionHandle:
488
+ """Opens an interactive browser instance to interact with a specific browser profile.
381
489
 
382
490
  Args:
383
- session_id: The session ID to associate with the browser. If None, a new session will be created.
491
+ profile_id: The profile ID to use for the session. If None, a new profile will be created.
492
+ session_id: (Deprecated, now `profile_id`) The session ID to associate with the browser.
384
493
  live_view: Whether to enable live view for the session.
385
494
 
386
495
  Returns:
@@ -392,7 +501,7 @@ class SmoothClient(BaseClient):
392
501
  try:
393
502
  response = self._session.post(
394
503
  f"{self.base_url}/browser/session",
395
- json=BrowserSessionRequest(session_id=session_id, live_view=live_view).model_dump(exclude_none=True),
504
+ json=BrowserSessionRequest(profile_id=profile_id or session_id, live_view=live_view).model_dump(exclude_none=True),
396
505
  )
397
506
  data = self._handle_response(response)
398
507
  return BrowserSessionHandle(browser_session=BrowserSessionResponse(**data["r"]))
@@ -400,11 +509,11 @@ class SmoothClient(BaseClient):
400
509
  logger.error(f"Request failed: {e}")
401
510
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
402
511
 
403
- def list_sessions(self) -> BrowserSessionsResponse:
404
- """Lists all browser sessions for the user.
512
+ def list_profiles(self):
513
+ """Lists all browser profiles for the user.
405
514
 
406
515
  Returns:
407
- A list of existing browser sessions.
516
+ A list of existing browser profiles.
408
517
 
409
518
  Raises:
410
519
  ApiException: If the API request fails.
@@ -412,20 +521,30 @@ class SmoothClient(BaseClient):
412
521
  try:
413
522
  response = self._session.get(f"{self.base_url}/browser/session")
414
523
  data = self._handle_response(response)
415
- return BrowserSessionsResponse(**data["r"])
524
+ return BrowserProfilesResponse(**data["r"])
416
525
  except requests.exceptions.RequestException as e:
417
526
  logger.error(f"Request failed: {e}")
418
527
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
419
528
 
420
- def delete_session(self, session_id: str):
421
- """Delete a browser session."""
529
+ @deprecated("list_sessions is deprecated, use list_profiles instead")
530
+ def list_sessions(self):
531
+ """Lists all browser profiles for the user."""
532
+ return self.list_profiles()
533
+
534
+ def delete_profile(self, profile_id: str):
535
+ """Delete a browser profile."""
422
536
  try:
423
- response = self._session.delete(f"{self.base_url}/browser/session/{session_id}")
537
+ response = self._session.delete(f"{self.base_url}/browser/session/{profile_id}")
424
538
  self._handle_response(response)
425
539
  except requests.exceptions.RequestException as e:
426
540
  logger.error(f"Request failed: {e}")
427
541
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
428
542
 
543
+ @deprecated("delete_session is deprecated, use delete_profile instead")
544
+ def delete_session(self, session_id: str):
545
+ """Delete a browser profile."""
546
+ self.delete_profile(session_id)
547
+
429
548
  def upload_file(self, file: io.IOBase, name: str | None = None, purpose: str | None = None) -> UploadFileResponse:
430
549
  """Upload a file and return the file ID.
431
550
 
@@ -588,10 +707,13 @@ class SmoothAsyncClient(BaseClient):
588
707
  allowed_urls: list[str] | None = None,
589
708
  enable_recording: bool = False,
590
709
  session_id: str | None = None,
710
+ profile_id: str | None = None,
711
+ profile_read_only: bool = False,
591
712
  stealth_mode: bool = False,
592
713
  proxy_server: str | None = None,
593
714
  proxy_username: str | None = None,
594
715
  proxy_password: str | None = None,
716
+ experimental_features: dict[str, Any] | None = None,
595
717
  ) -> AsyncTaskHandle:
596
718
  """Runs a task and returns a handle to the task asynchronously.
597
719
 
@@ -607,16 +729,17 @@ class SmoothAsyncClient(BaseClient):
607
729
  agent: The agent to use for the task.
608
730
  max_steps: Maximum number of steps the agent can take (max 64).
609
731
  device: Device type for the task. Default is mobile.
610
- allowed_urls: List of allowed URL patterns using wildcard syntax (e.g., https://example.com/*).
732
+ allowed_urls: List of allowed URL patterns using wildcard syntax (e.g., https://*example.com/*).
611
733
  If None, all URLs are allowed.
612
734
  enable_recording: Enable video recording of the task execution.
613
- session_id: Browser session ID to use.
735
+ session_id: (Deprecated, now `profile_id`) Browser session ID to use.
736
+ profile_id: Browser profile ID to use. Each profile maintains its own state, such as cookies and login credentials.
737
+ profile_read_only: If true, the profile specified by `profile_id` will be loaded in read-only mode.
614
738
  stealth_mode: Run the browser in stealth mode.
615
739
  proxy_server: Proxy server url to route browser traffic through.
616
740
  proxy_username: Proxy server username.
617
741
  proxy_password: Proxy server password.
618
- poll_interval: The time in seconds to wait between polling for status.
619
- timeout: The maximum time in seconds to wait for the task to complete.
742
+ experimental_features: Experimental features to enable for the task.
620
743
 
621
744
  Returns:
622
745
  A handle to the running task.
@@ -635,21 +758,26 @@ class SmoothAsyncClient(BaseClient):
635
758
  device=device,
636
759
  allowed_urls=allowed_urls,
637
760
  enable_recording=enable_recording,
638
- session_id=session_id,
761
+ profile_id=profile_id or session_id,
762
+ profile_read_only=profile_read_only,
639
763
  stealth_mode=stealth_mode,
640
764
  proxy_server=proxy_server,
641
765
  proxy_username=proxy_username,
642
766
  proxy_password=proxy_password,
767
+ experimental_features=experimental_features,
643
768
  )
644
769
 
645
770
  initial_response = await self._submit_task(payload)
646
771
  return AsyncTaskHandle(initial_response.id, self)
647
772
 
648
- async def open_session(self, session_id: str | None = None, live_view: bool = True) -> BrowserSessionHandle:
773
+ async def open_session(
774
+ self, profile_id: str | None = None, session_id: str | None = None, live_view: bool = True
775
+ ) -> BrowserSessionHandle:
649
776
  """Opens an interactive browser instance asynchronously.
650
777
 
651
778
  Args:
652
- session_id: The session ID to associate with the browser.
779
+ session_id: (Deprecated, now `profile_id`) The session ID to associate with the browser.
780
+ profile_id: The profile ID to associate with the browser.
653
781
  live_view: Whether to enable live view for the session.
654
782
 
655
783
  Returns:
@@ -661,7 +789,7 @@ class SmoothAsyncClient(BaseClient):
661
789
  try:
662
790
  response = await self._client.post(
663
791
  f"{self.base_url}/browser/session",
664
- json=BrowserSessionRequest(session_id=session_id, live_view=live_view).model_dump(exclude_none=True),
792
+ json=BrowserSessionRequest(profile_id=profile_id or session_id, live_view=live_view).model_dump(exclude_none=True),
665
793
  )
666
794
  data = self._handle_response(response)
667
795
  return BrowserSessionHandle(browser_session=BrowserSessionResponse(**data["r"]))
@@ -669,11 +797,11 @@ class SmoothAsyncClient(BaseClient):
669
797
  logger.error(f"Request failed: {e}")
670
798
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
671
799
 
672
- async def list_sessions(self) -> BrowserSessionsResponse:
673
- """Lists all browser sessions for the user.
800
+ async def list_profiles(self):
801
+ """Lists all browser profiles for the user.
674
802
 
675
803
  Returns:
676
- A list of existing browser sessions.
804
+ A list of existing browser profiles.
677
805
 
678
806
  Raises:
679
807
  ApiException: If the API request fails.
@@ -681,20 +809,30 @@ class SmoothAsyncClient(BaseClient):
681
809
  try:
682
810
  response = await self._client.get(f"{self.base_url}/browser/session")
683
811
  data = self._handle_response(response)
684
- return BrowserSessionsResponse(**data["r"])
812
+ return BrowserProfilesResponse(**data["r"])
685
813
  except httpx.RequestError as e:
686
814
  logger.error(f"Request failed: {e}")
687
815
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
688
816
 
689
- async def delete_session(self, session_id: str):
690
- """Delete a browser session."""
817
+ @deprecated("list_sessions is deprecated, use list_profiles instead")
818
+ async def list_sessions(self):
819
+ """Lists all browser profiles for the user."""
820
+ return await self.list_profiles()
821
+
822
+ async def delete_profile(self, profile_id: str):
823
+ """Delete a browser profile."""
691
824
  try:
692
- response = await self._client.delete(f"{self.base_url}/browser/session/{session_id}")
825
+ response = await self._client.delete(f"{self.base_url}/browser/session/{profile_id}")
693
826
  self._handle_response(response)
694
827
  except httpx.RequestError as e:
695
828
  logger.error(f"Request failed: {e}")
696
829
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
697
830
 
831
+ @deprecated("delete_session is deprecated, use delete_profile instead")
832
+ async def delete_session(self, session_id: str):
833
+ """Delete a browser profile."""
834
+ await self.delete_profile(session_id)
835
+
698
836
  async def upload_file(self, file: io.IOBase, name: str | None = None, purpose: str | None = None) -> UploadFileResponse:
699
837
  """Upload a file and return the file ID.
700
838