smooth-py 0.2.6__tar.gz → 0.2.7.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
3
+ Version: 0.2.7.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"
3
+ version = "0.2.7.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, Field, model_validator
15
17
 
16
18
  # Configure logging
17
19
  logger = logging.getLogger("smooth")
@@ -71,9 +73,12 @@ class TaskRequest(BaseModel):
71
73
  ),
72
74
  )
73
75
  enable_recording: bool = Field(default=True, description="Enable video recording of the task execution. Default is True")
74
- session_id: str | None = Field(
76
+ profile_id: str | None = Field(
75
77
  default=None,
76
- description="Browser session ID to use. Each session maintains its own state, such as login credentials.",
78
+ description=("Browser profile ID to use. Each profile maintains its own state, such as cookies and login credentials."),
79
+ )
80
+ profile_read_only: bool = Field(
81
+ default=False, description="If true, the profile specified by `profile_id` will be loaded in read-only mode."
77
82
  )
78
83
  stealth_mode: bool = Field(default=False, description="Run the browser in stealth mode.")
79
84
  proxy_server: str | None = Field(
@@ -84,29 +89,114 @@ class TaskRequest(BaseModel):
84
89
  )
85
90
  proxy_username: str | None = Field(default=None, description="Proxy server username.")
86
91
  proxy_password: str | None = Field(default=None, description="Proxy server password.")
92
+ experimental_features: dict[str, Any] | None = Field(default=None, description="Experimental features to enable for the task.")
93
+
94
+ @model_validator(mode="before")
95
+ @classmethod
96
+ def _handle_deprecated_session_id(cls, data: Any) -> Any:
97
+ if isinstance(data, dict) and "session_id" in data:
98
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
99
+ data["profile_id"] = data.pop("session_id")
100
+ return data
101
+
102
+ @property
103
+ def session_id(self):
104
+ """(Deprecated) Returns the session ID."""
105
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
106
+ return self.profile_id
107
+
108
+ @session_id.setter
109
+ def session_id(self, value):
110
+ """(Deprecated) Sets the session ID."""
111
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
112
+ self.profile_id = value
87
113
 
88
114
 
89
115
  class BrowserSessionRequest(BaseModel):
90
116
  """Request model for creating a browser session."""
91
117
 
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."),
118
+ profile_id: str | None = Field(
119
+ default=None, description=("The profile ID to use for the browser session. If None, a new profile will be created.")
95
120
  )
96
121
  live_view: bool | None = Field(default=True, description="Request a live URL to interact with the browser session.")
97
122
 
123
+ @model_validator(mode="before")
124
+ @classmethod
125
+ def _handle_deprecated_session_id(cls, data: Any) -> Any:
126
+ if isinstance(data, dict) and "session_id" in data:
127
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
128
+ data["profile_id"] = data.pop("session_id")
129
+ return data
130
+
131
+ @property
132
+ def session_id(self):
133
+ """(Deprecated) Returns the session ID."""
134
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
135
+ return self.profile_id
136
+
137
+ @session_id.setter
138
+ def session_id(self, value):
139
+ """(Deprecated) Sets the session ID."""
140
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
141
+ self.profile_id = value
142
+
98
143
 
99
144
  class BrowserSessionResponse(BaseModel):
100
145
  """Browser session response model."""
101
146
 
102
- session_id: str = Field(description="The ID of the browser session associated with the opened browser instance.")
147
+ profile_id: str = Field(description="The ID of the browser profile associated with the opened browser instance.")
103
148
  live_url: str | None = Field(default=None, description="The live URL to interact with the browser session.")
104
149
 
150
+ @model_validator(mode="before")
151
+ @classmethod
152
+ def _handle_deprecated_session_id(cls, data: Any) -> Any:
153
+ if isinstance(data, dict) and "session_id" in data:
154
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
155
+ data["profile_id"] = data.pop("session_id")
156
+ return data
157
+
158
+ @property
159
+ def session_id(self):
160
+ """(Deprecated) Returns the session ID."""
161
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
162
+ return self.profile_id
163
+
164
+ @session_id.setter
165
+ def session_id(self, value):
166
+ """(Deprecated) Sets the session ID."""
167
+ warnings.warn("'session_id' is deprecated, use 'profile_id' instead", DeprecationWarning, stacklevel=2)
168
+ self.profile_id = value
169
+
170
+
171
+ class BrowserProfilesResponse(BaseModel):
172
+ """Response model for listing browser profiles."""
105
173
 
106
- class BrowserSessionsResponse(BaseModel):
107
- """Response model for listing browser sessions."""
174
+ profile_ids: list[str] = Field(description="The IDs of the browser profiles.")
108
175
 
109
- session_ids: list[str] = Field(description="The IDs of the browser sessions.")
176
+ @model_validator(mode="before")
177
+ @classmethod
178
+ def _handle_deprecated_session_ids(cls, data: Any) -> Any:
179
+ if isinstance(data, dict) and "session_ids" in data:
180
+ warnings.warn("'session_ids' is deprecated, use 'profile_ids' instead", DeprecationWarning, stacklevel=2)
181
+ data["profile_ids"] = data.pop("session_ids")
182
+ return data
183
+
184
+ @property
185
+ def session_ids(self):
186
+ """(Deprecated) Returns the session IDs."""
187
+ warnings.warn("'session_ids' is deprecated, use 'profile_ids' instead", DeprecationWarning, stacklevel=2)
188
+ return self.profile_ids
189
+
190
+ @session_ids.setter
191
+ def session_ids(self, value):
192
+ """(Deprecated) Sets the session IDs."""
193
+ warnings.warn("'session_ids' is deprecated, use 'profile_ids' instead", DeprecationWarning, stacklevel=2)
194
+ self.profile_ids = value
195
+
196
+
197
+ class BrowserSessionsResponse(BrowserProfilesResponse):
198
+ """Response model for listing browser profiles."""
199
+ pass
110
200
 
111
201
 
112
202
  class UploadFileResponse(BaseModel):
@@ -189,9 +279,14 @@ class BrowserSessionHandle(BaseModel):
189
279
 
190
280
  browser_session: BrowserSessionResponse = Field(description="The browser session associated with this handle.")
191
281
 
282
+ @deprecated("session_id is deprecated, use profile_id instead")
192
283
  def session_id(self):
193
284
  """Returns the session ID for the browser session."""
194
- return self.browser_session.session_id
285
+ return self.profile_id()
286
+
287
+ def profile_id(self):
288
+ """Returns the profile ID for the browser session."""
289
+ return self.browser_session.profile_id
195
290
 
196
291
  def live_url(self, interactive: bool = True, embed: bool = False):
197
292
  """Returns the live URL for the browser session."""
@@ -321,6 +416,8 @@ class SmoothClient(BaseClient):
321
416
  allowed_urls: list[str] | None = None,
322
417
  enable_recording: bool = False,
323
418
  session_id: str | None = None,
419
+ profile_id: str | None = None,
420
+ profile_read_only: bool | None = None,
324
421
  stealth_mode: bool = False,
325
422
  proxy_server: str | None = None,
326
423
  proxy_username: str | None = None,
@@ -343,7 +440,9 @@ class SmoothClient(BaseClient):
343
440
  allowed_urls: List of allowed URL patterns using wildcard syntax (e.g., https://*example.com/*).
344
441
  If None, all URLs are allowed.
345
442
  enable_recording: Enable video recording of the task execution.
346
- session_id: Browser session ID to use.
443
+ session_id: (Deprecated, now `profile_id`) Browser session ID to use.
444
+ profile_id: Browser profile ID to use. Each profile maintains its own state, such as cookies and login credentials.
445
+ profile_read_only: If true, the profile specified by `profile_id` will be loaded in read-only mode.
347
446
  stealth_mode: Run the browser in stealth mode.
348
447
  proxy_server: Proxy server url to route browser traffic through.
349
448
  proxy_username: Proxy server username.
@@ -366,7 +465,8 @@ class SmoothClient(BaseClient):
366
465
  device=device,
367
466
  allowed_urls=allowed_urls,
368
467
  enable_recording=enable_recording,
369
- session_id=session_id,
468
+ profile_id=profile_id or session_id,
469
+ profile_read_only=profile_read_only,
370
470
  stealth_mode=stealth_mode,
371
471
  proxy_server=proxy_server,
372
472
  proxy_username=proxy_username,
@@ -376,11 +476,14 @@ class SmoothClient(BaseClient):
376
476
 
377
477
  return TaskHandle(initial_response.id, self)
378
478
 
379
- def open_session(self, session_id: str | None = None, live_view: bool = True) -> BrowserSessionHandle:
380
- """Gets an interactive browser instance.
479
+ def open_session(
480
+ self, profile_id: str | None = None, session_id: str | None = None, live_view: bool = True
481
+ ) -> BrowserSessionHandle:
482
+ """Opens an interactive browser instance to interact with a specific browser profile.
381
483
 
382
484
  Args:
383
- session_id: The session ID to associate with the browser. If None, a new session will be created.
485
+ profile_id: The profile ID to use for the session. If None, a new profile will be created.
486
+ session_id: (Deprecated, now `profile_id`) The session ID to associate with the browser.
384
487
  live_view: Whether to enable live view for the session.
385
488
 
386
489
  Returns:
@@ -392,7 +495,7 @@ class SmoothClient(BaseClient):
392
495
  try:
393
496
  response = self._session.post(
394
497
  f"{self.base_url}/browser/session",
395
- json=BrowserSessionRequest(session_id=session_id, live_view=live_view).model_dump(exclude_none=True),
498
+ json=BrowserSessionRequest(profile_id=profile_id or session_id, live_view=live_view).model_dump(exclude_none=True),
396
499
  )
397
500
  data = self._handle_response(response)
398
501
  return BrowserSessionHandle(browser_session=BrowserSessionResponse(**data["r"]))
@@ -400,11 +503,11 @@ class SmoothClient(BaseClient):
400
503
  logger.error(f"Request failed: {e}")
401
504
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
402
505
 
403
- def list_sessions(self) -> BrowserSessionsResponse:
404
- """Lists all browser sessions for the user.
506
+ def list_profiles(self):
507
+ """Lists all browser profiles for the user.
405
508
 
406
509
  Returns:
407
- A list of existing browser sessions.
510
+ A list of existing browser profiles.
408
511
 
409
512
  Raises:
410
513
  ApiException: If the API request fails.
@@ -412,20 +515,30 @@ class SmoothClient(BaseClient):
412
515
  try:
413
516
  response = self._session.get(f"{self.base_url}/browser/session")
414
517
  data = self._handle_response(response)
415
- return BrowserSessionsResponse(**data["r"])
518
+ return BrowserProfilesResponse(**data["r"])
416
519
  except requests.exceptions.RequestException as e:
417
520
  logger.error(f"Request failed: {e}")
418
521
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
419
522
 
420
- def delete_session(self, session_id: str):
421
- """Delete a browser session."""
523
+ @deprecated("list_sessions is deprecated, use list_profiles instead")
524
+ def list_sessions(self):
525
+ """Lists all browser profiles for the user."""
526
+ return self.list_profiles()
527
+
528
+ def delete_profile(self, profile_id: str):
529
+ """Delete a browser profile."""
422
530
  try:
423
- response = self._session.delete(f"{self.base_url}/browser/session/{session_id}")
531
+ response = self._session.delete(f"{self.base_url}/browser/session/{profile_id}")
424
532
  self._handle_response(response)
425
533
  except requests.exceptions.RequestException as e:
426
534
  logger.error(f"Request failed: {e}")
427
535
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
428
536
 
537
+ @deprecated("delete_session is deprecated, use delete_profile instead")
538
+ def delete_session(self, session_id: str):
539
+ """Delete a browser profile."""
540
+ self.delete_profile(session_id)
541
+
429
542
  def upload_file(self, file: io.IOBase, name: str | None = None, purpose: str | None = None) -> UploadFileResponse:
430
543
  """Upload a file and return the file ID.
431
544
 
@@ -588,6 +701,8 @@ class SmoothAsyncClient(BaseClient):
588
701
  allowed_urls: list[str] | None = None,
589
702
  enable_recording: bool = False,
590
703
  session_id: str | None = None,
704
+ profile_id: str | None = None,
705
+ profile_read_only: bool | None = None,
591
706
  stealth_mode: bool = False,
592
707
  proxy_server: str | None = None,
593
708
  proxy_username: str | None = None,
@@ -610,7 +725,9 @@ class SmoothAsyncClient(BaseClient):
610
725
  allowed_urls: List of allowed URL patterns using wildcard syntax (e.g., https://*example.com/*).
611
726
  If None, all URLs are allowed.
612
727
  enable_recording: Enable video recording of the task execution.
613
- session_id: Browser session ID to use.
728
+ session_id: (Deprecated, now `profile_id`) Browser session ID to use.
729
+ profile_id: Browser profile ID to use. Each profile maintains its own state, such as cookies and login credentials.
730
+ profile_read_only: If true, the profile specified by `profile_id` will be loaded in read-only mode.
614
731
  stealth_mode: Run the browser in stealth mode.
615
732
  proxy_server: Proxy server url to route browser traffic through.
616
733
  proxy_username: Proxy server username.
@@ -635,7 +752,8 @@ class SmoothAsyncClient(BaseClient):
635
752
  device=device,
636
753
  allowed_urls=allowed_urls,
637
754
  enable_recording=enable_recording,
638
- session_id=session_id,
755
+ profile_id=profile_id or session_id,
756
+ profile_read_only=profile_read_only,
639
757
  stealth_mode=stealth_mode,
640
758
  proxy_server=proxy_server,
641
759
  proxy_username=proxy_username,
@@ -645,11 +763,14 @@ class SmoothAsyncClient(BaseClient):
645
763
  initial_response = await self._submit_task(payload)
646
764
  return AsyncTaskHandle(initial_response.id, self)
647
765
 
648
- async def open_session(self, session_id: str | None = None, live_view: bool = True) -> BrowserSessionHandle:
766
+ async def open_session(
767
+ self, profile_id: str | None = None, session_id: str | None = None, live_view: bool = True
768
+ ) -> BrowserSessionHandle:
649
769
  """Opens an interactive browser instance asynchronously.
650
770
 
651
771
  Args:
652
- session_id: The session ID to associate with the browser.
772
+ session_id: (Deprecated, now `profile_id`) The session ID to associate with the browser.
773
+ profile_id: The profile ID to associate with the browser.
653
774
  live_view: Whether to enable live view for the session.
654
775
 
655
776
  Returns:
@@ -661,7 +782,7 @@ class SmoothAsyncClient(BaseClient):
661
782
  try:
662
783
  response = await self._client.post(
663
784
  f"{self.base_url}/browser/session",
664
- json=BrowserSessionRequest(session_id=session_id, live_view=live_view).model_dump(exclude_none=True),
785
+ json=BrowserSessionRequest(profile_id=profile_id or session_id, live_view=live_view).model_dump(exclude_none=True),
665
786
  )
666
787
  data = self._handle_response(response)
667
788
  return BrowserSessionHandle(browser_session=BrowserSessionResponse(**data["r"]))
@@ -669,11 +790,11 @@ class SmoothAsyncClient(BaseClient):
669
790
  logger.error(f"Request failed: {e}")
670
791
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
671
792
 
672
- async def list_sessions(self) -> BrowserSessionsResponse:
673
- """Lists all browser sessions for the user.
793
+ async def list_profiles(self):
794
+ """Lists all browser profiles for the user.
674
795
 
675
796
  Returns:
676
- A list of existing browser sessions.
797
+ A list of existing browser profiles.
677
798
 
678
799
  Raises:
679
800
  ApiException: If the API request fails.
@@ -681,20 +802,30 @@ class SmoothAsyncClient(BaseClient):
681
802
  try:
682
803
  response = await self._client.get(f"{self.base_url}/browser/session")
683
804
  data = self._handle_response(response)
684
- return BrowserSessionsResponse(**data["r"])
805
+ return BrowserProfilesResponse(**data["r"])
685
806
  except httpx.RequestError as e:
686
807
  logger.error(f"Request failed: {e}")
687
808
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
688
809
 
689
- async def delete_session(self, session_id: str):
690
- """Delete a browser session."""
810
+ @deprecated("list_sessions is deprecated, use list_profiles instead")
811
+ async def list_sessions(self):
812
+ """Lists all browser profiles for the user."""
813
+ return await self.list_profiles()
814
+
815
+ async def delete_profile(self, profile_id: str):
816
+ """Delete a browser profile."""
691
817
  try:
692
- response = await self._client.delete(f"{self.base_url}/browser/session/{session_id}")
818
+ response = await self._client.delete(f"{self.base_url}/browser/session/{profile_id}")
693
819
  self._handle_response(response)
694
820
  except httpx.RequestError as e:
695
821
  logger.error(f"Request failed: {e}")
696
822
  raise ApiError(status_code=0, detail=f"Request failed: {str(e)}") from None
697
823
 
824
+ @deprecated("delete_session is deprecated, use delete_profile instead")
825
+ async def delete_session(self, session_id: str):
826
+ """Delete a browser profile."""
827
+ await self.delete_profile(session_id)
828
+
698
829
  async def upload_file(self, file: io.IOBase, name: str | None = None, purpose: str | None = None) -> UploadFileResponse:
699
830
  """Upload a file and return the file ID.
700
831