smooth-py 0.3.0.dev20251014__py3-none-any.whl → 0.3.1.dev20251027__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 +97 -9
- {smooth_py-0.3.0.dev20251014.dist-info → smooth_py-0.3.1.dev20251027.dist-info}/METADATA +1 -1
- smooth_py-0.3.1.dev20251027.dist-info/RECORD +6 -0
- smooth_py-0.3.0.dev20251014.dist-info/RECORD +0 -6
- {smooth_py-0.3.0.dev20251014.dist-info → smooth_py-0.3.1.dev20251027.dist-info}/WHEEL +0 -0
smooth/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Smooth python SDK."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import base64
|
|
4
5
|
import io
|
|
5
6
|
import logging
|
|
6
7
|
import os
|
|
@@ -8,7 +9,7 @@ import time
|
|
|
8
9
|
import urllib.parse
|
|
9
10
|
import warnings
|
|
10
11
|
from pathlib import Path
|
|
11
|
-
from typing import Any, Literal, Type
|
|
12
|
+
from typing import Any, Literal, NotRequired, Type, TypedDict
|
|
12
13
|
|
|
13
14
|
import httpx
|
|
14
15
|
import requests
|
|
@@ -33,7 +34,47 @@ def _encode_url(url: str, interactive: bool = True, embed: bool = False) -> str:
|
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
# --- Models ---
|
|
36
|
-
|
|
37
|
+
|
|
38
|
+
class Certificate(TypedDict):
|
|
39
|
+
"""Client certificate for accessing secure websites.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
file: p12 file object to be uploaded (e.g., open("cert.p12", "rb")).
|
|
43
|
+
password: Password to decrypt the certificate file. Optional.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
file: str | io.IOBase # Required - base64 string or binary IO
|
|
47
|
+
password: NotRequired[str] # Optional
|
|
48
|
+
filters: NotRequired[list[str]] # Optional - TODO: Reserved for future use to specify URL patterns where the certificate should be applied.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _process_certificates(certificates: list[Certificate] | None) -> list[dict[str, Any]] | None:
|
|
52
|
+
"""Process certificates, converting binary IO to base64-encoded strings.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
certificates: List of certificates with file field as string or binary IO.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of certificates with file field as base64-encoded string, or None if input is None.
|
|
59
|
+
"""
|
|
60
|
+
if certificates is None:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
processed_certs = []
|
|
64
|
+
for cert in certificates:
|
|
65
|
+
processed_cert = dict(cert) # Create a copy
|
|
66
|
+
|
|
67
|
+
file_content = processed_cert["file"]
|
|
68
|
+
if isinstance(file_content, io.IOBase):
|
|
69
|
+
# Read the binary content and encode to base64
|
|
70
|
+
binary_data = file_content.read()
|
|
71
|
+
processed_cert["file"] = base64.b64encode(binary_data).decode("utf-8")
|
|
72
|
+
elif not isinstance(file_content, str):
|
|
73
|
+
raise TypeError(f"Certificate file must be a string or binary IO, got {type(file_content)}")
|
|
74
|
+
|
|
75
|
+
processed_certs.append(processed_cert)
|
|
76
|
+
|
|
77
|
+
return processed_certs
|
|
37
78
|
|
|
38
79
|
|
|
39
80
|
class TaskResponse(BaseModel):
|
|
@@ -48,6 +89,7 @@ class TaskResponse(BaseModel):
|
|
|
48
89
|
device: Literal["desktop", "mobile"] | None = Field(default=None, description="The device type used for the task.")
|
|
49
90
|
live_url: str | None = Field(default=None, description="The URL to view and interact with the task execution.")
|
|
50
91
|
recording_url: str | None = Field(default=None, description="The URL to view the task recording.")
|
|
92
|
+
created_at: int | None = Field(default=None, description="The timestamp when the task was created.")
|
|
51
93
|
|
|
52
94
|
|
|
53
95
|
class TaskRequest(BaseModel):
|
|
@@ -82,7 +124,11 @@ class TaskRequest(BaseModel):
|
|
|
82
124
|
description=("Browser profile ID to use. Each profile maintains its own state, such as cookies and login credentials."),
|
|
83
125
|
)
|
|
84
126
|
profile_read_only: bool = Field(
|
|
85
|
-
default=False,
|
|
127
|
+
default=False,
|
|
128
|
+
description=(
|
|
129
|
+
"If true, the profile specified by `profile_id` will be loaded in read-only mode. "
|
|
130
|
+
"Changes made during the task will not be saved back to the profile."
|
|
131
|
+
),
|
|
86
132
|
)
|
|
87
133
|
stealth_mode: bool = Field(default=False, description="Run the browser in stealth mode.")
|
|
88
134
|
proxy_server: str | None = Field(
|
|
@@ -93,6 +139,15 @@ class TaskRequest(BaseModel):
|
|
|
93
139
|
)
|
|
94
140
|
proxy_username: str | None = Field(default=None, description="Proxy server username.")
|
|
95
141
|
proxy_password: str | None = Field(default=None, description="Proxy server password.")
|
|
142
|
+
certificates: list[dict[str, Any]] | None = Field(
|
|
143
|
+
default=None,
|
|
144
|
+
description=(
|
|
145
|
+
"List of client certificates to use when accessing secure websites. "
|
|
146
|
+
"Each certificate is a dictionary with the following fields:\n"
|
|
147
|
+
" - `file`: p12 file object to be uploaded (e.g., open('cert.p12', 'rb')).\n"
|
|
148
|
+
" - `password` (optional): Password to decrypt the certificate file."
|
|
149
|
+
),
|
|
150
|
+
)
|
|
96
151
|
experimental_features: dict[str, Any] | None = Field(
|
|
97
152
|
default=None, description="Experimental features to enable for the task."
|
|
98
153
|
)
|
|
@@ -330,6 +385,10 @@ class BrowserSessionHandle(BaseModel):
|
|
|
330
385
|
return _encode_url(self.browser_session.live_url, interactive=interactive, embed=embed)
|
|
331
386
|
return None
|
|
332
387
|
|
|
388
|
+
def live_id(self):
|
|
389
|
+
"""Returns the live ID for the browser session."""
|
|
390
|
+
return self.browser_session.live_id
|
|
391
|
+
|
|
333
392
|
|
|
334
393
|
class TaskHandle:
|
|
335
394
|
"""A handle to a running task."""
|
|
@@ -393,6 +452,14 @@ class TaskHandle:
|
|
|
393
452
|
task_response = self._client._get_task(self.id())
|
|
394
453
|
self._task_response = task_response
|
|
395
454
|
if task_response.recording_url is not None:
|
|
455
|
+
if not task_response.recording_url:
|
|
456
|
+
raise ApiError(
|
|
457
|
+
status_code=404,
|
|
458
|
+
detail=(
|
|
459
|
+
f"Recording URL not available for task {self.id()}."
|
|
460
|
+
" Set `enable_recording=True` when creating the task to enable it."
|
|
461
|
+
)
|
|
462
|
+
)
|
|
396
463
|
return task_response.recording_url
|
|
397
464
|
time.sleep(1)
|
|
398
465
|
raise TimeoutError(f"Recording URL not available for task {self.id()}.")
|
|
@@ -466,7 +533,7 @@ class SmoothClient(BaseClient):
|
|
|
466
533
|
max_steps: int = 32,
|
|
467
534
|
device: Literal["desktop", "mobile"] = "mobile",
|
|
468
535
|
allowed_urls: list[str] | None = None,
|
|
469
|
-
enable_recording: bool =
|
|
536
|
+
enable_recording: bool = True,
|
|
470
537
|
session_id: str | None = None,
|
|
471
538
|
profile_id: str | None = None,
|
|
472
539
|
profile_read_only: bool = False,
|
|
@@ -474,6 +541,7 @@ class SmoothClient(BaseClient):
|
|
|
474
541
|
proxy_server: str | None = None,
|
|
475
542
|
proxy_username: str | None = None,
|
|
476
543
|
proxy_password: str | None = None,
|
|
544
|
+
certificates: list[Certificate] | None = None,
|
|
477
545
|
experimental_features: dict[str, Any] | None = None,
|
|
478
546
|
) -> TaskHandle:
|
|
479
547
|
"""Runs a task and returns a handle to the task.
|
|
@@ -500,6 +568,10 @@ class SmoothClient(BaseClient):
|
|
|
500
568
|
proxy_server: Proxy server url to route browser traffic through.
|
|
501
569
|
proxy_username: Proxy server username.
|
|
502
570
|
proxy_password: Proxy server password.
|
|
571
|
+
certificates: List of client certificates to use when accessing secure websites.
|
|
572
|
+
Each certificate is a dictionary with the following fields:
|
|
573
|
+
- `file` (required): p12 file object to be uploaded (e.g., open("cert.p12", "rb")).
|
|
574
|
+
- `password` (optional): Password to decrypt the certificate file, if password-protected.
|
|
503
575
|
experimental_features: Experimental features to enable for the task.
|
|
504
576
|
|
|
505
577
|
Returns:
|
|
@@ -525,6 +597,7 @@ class SmoothClient(BaseClient):
|
|
|
525
597
|
proxy_server=proxy_server,
|
|
526
598
|
proxy_username=proxy_username,
|
|
527
599
|
proxy_password=proxy_password,
|
|
600
|
+
certificates=_process_certificates(certificates),
|
|
528
601
|
experimental_features=experimental_features,
|
|
529
602
|
)
|
|
530
603
|
initial_response = self._submit_task(payload)
|
|
@@ -577,7 +650,7 @@ class SmoothClient(BaseClient):
|
|
|
577
650
|
ApiException: If the API request fails.
|
|
578
651
|
"""
|
|
579
652
|
try:
|
|
580
|
-
response = self._session.get(f"{self.base_url}/browser/
|
|
653
|
+
response = self._session.get(f"{self.base_url}/browser/profile")
|
|
581
654
|
data = self._handle_response(response)
|
|
582
655
|
return BrowserProfilesResponse(**data["r"])
|
|
583
656
|
except requests.exceptions.RequestException as e:
|
|
@@ -592,7 +665,7 @@ class SmoothClient(BaseClient):
|
|
|
592
665
|
def delete_profile(self, profile_id: str):
|
|
593
666
|
"""Delete a browser profile."""
|
|
594
667
|
try:
|
|
595
|
-
response = self._session.delete(f"{self.base_url}/browser/
|
|
668
|
+
response = self._session.delete(f"{self.base_url}/browser/profile/{profile_id}")
|
|
596
669
|
self._handle_response(response)
|
|
597
670
|
except requests.exceptions.RequestException as e:
|
|
598
671
|
logger.error(f"Request failed: {e}")
|
|
@@ -711,6 +784,14 @@ class AsyncTaskHandle:
|
|
|
711
784
|
task_response = await self._client._get_task(self.id())
|
|
712
785
|
self._task_response = task_response
|
|
713
786
|
if task_response.recording_url is not None:
|
|
787
|
+
if not task_response.recording_url:
|
|
788
|
+
raise ApiError(
|
|
789
|
+
status_code=404,
|
|
790
|
+
detail=(
|
|
791
|
+
f"Recording URL not available for task {self.id()}."
|
|
792
|
+
" Set `enable_recording=True` when creating the task to enable it."
|
|
793
|
+
)
|
|
794
|
+
)
|
|
714
795
|
return task_response.recording_url
|
|
715
796
|
await asyncio.sleep(1)
|
|
716
797
|
|
|
@@ -779,7 +860,7 @@ class SmoothAsyncClient(BaseClient):
|
|
|
779
860
|
max_steps: int = 32,
|
|
780
861
|
device: Literal["desktop", "mobile"] = "mobile",
|
|
781
862
|
allowed_urls: list[str] | None = None,
|
|
782
|
-
enable_recording: bool =
|
|
863
|
+
enable_recording: bool = True,
|
|
783
864
|
session_id: str | None = None,
|
|
784
865
|
profile_id: str | None = None,
|
|
785
866
|
profile_read_only: bool = False,
|
|
@@ -787,6 +868,7 @@ class SmoothAsyncClient(BaseClient):
|
|
|
787
868
|
proxy_server: str | None = None,
|
|
788
869
|
proxy_username: str | None = None,
|
|
789
870
|
proxy_password: str | None = None,
|
|
871
|
+
certificates: list[Certificate] | None = None,
|
|
790
872
|
experimental_features: dict[str, Any] | None = None,
|
|
791
873
|
) -> AsyncTaskHandle:
|
|
792
874
|
"""Runs a task and returns a handle to the task asynchronously.
|
|
@@ -813,6 +895,10 @@ class SmoothAsyncClient(BaseClient):
|
|
|
813
895
|
proxy_server: Proxy server url to route browser traffic through.
|
|
814
896
|
proxy_username: Proxy server username.
|
|
815
897
|
proxy_password: Proxy server password.
|
|
898
|
+
certificates: List of client certificates to use when accessing secure websites.
|
|
899
|
+
Each certificate is a dictionary with the following fields:
|
|
900
|
+
- `file` (required): p12 file object to be uploaded (e.g., open("cert.p12", "rb")).
|
|
901
|
+
- `password` (optional): Password to decrypt the certificate file.
|
|
816
902
|
experimental_features: Experimental features to enable for the task.
|
|
817
903
|
|
|
818
904
|
Returns:
|
|
@@ -838,6 +924,7 @@ class SmoothAsyncClient(BaseClient):
|
|
|
838
924
|
proxy_server=proxy_server,
|
|
839
925
|
proxy_username=proxy_username,
|
|
840
926
|
proxy_password=proxy_password,
|
|
927
|
+
certificates=_process_certificates(certificates),
|
|
841
928
|
experimental_features=experimental_features,
|
|
842
929
|
)
|
|
843
930
|
|
|
@@ -890,7 +977,7 @@ class SmoothAsyncClient(BaseClient):
|
|
|
890
977
|
ApiException: If the API request fails.
|
|
891
978
|
"""
|
|
892
979
|
try:
|
|
893
|
-
response = await self._client.get(f"{self.base_url}/browser/
|
|
980
|
+
response = await self._client.get(f"{self.base_url}/browser/profile")
|
|
894
981
|
data = self._handle_response(response)
|
|
895
982
|
return BrowserProfilesResponse(**data["r"])
|
|
896
983
|
except httpx.RequestError as e:
|
|
@@ -905,7 +992,7 @@ class SmoothAsyncClient(BaseClient):
|
|
|
905
992
|
async def delete_profile(self, profile_id: str):
|
|
906
993
|
"""Delete a browser profile."""
|
|
907
994
|
try:
|
|
908
|
-
response = await self._client.delete(f"{self.base_url}/browser/
|
|
995
|
+
response = await self._client.delete(f"{self.base_url}/browser/profile/{profile_id}")
|
|
909
996
|
self._handle_response(response)
|
|
910
997
|
except httpx.RequestError as e:
|
|
911
998
|
logger.error(f"Request failed: {e}")
|
|
@@ -976,6 +1063,7 @@ __all__ = [
|
|
|
976
1063
|
"BrowserSessionResponse",
|
|
977
1064
|
"BrowserSessionsResponse",
|
|
978
1065
|
"UploadFileResponse",
|
|
1066
|
+
"Certificate",
|
|
979
1067
|
"ApiError",
|
|
980
1068
|
"TimeoutError",
|
|
981
1069
|
]
|
|
@@ -0,0 +1,6 @@
|
|
|
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,,
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
smooth/__init__.py,sha256=yhlG6m00uOw8umRC7ONImCMCiCkWTV5khj050qfD_V8,38411
|
|
2
|
-
smooth/mcp/__init__.py,sha256=0aJVFi2a8Ah3-5xtgyZ5UMbaaJsBWu2T8QLWoFQITk8,219
|
|
3
|
-
smooth/mcp/server.py,sha256=9SymTD4NOGTMN8P-LNGlvYNvv81yCIZfZeeuhEcAc6s,20068
|
|
4
|
-
smooth_py-0.3.0.dev20251014.dist-info/METADATA,sha256=Ef7Ys-PREpnYOCtk6XuQRlzWwh-adFP0FxU8jMOHCGM,7529
|
|
5
|
-
smooth_py-0.3.0.dev20251014.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
6
|
-
smooth_py-0.3.0.dev20251014.dist-info/RECORD,,
|
|
File without changes
|