intent-base-sdk 0.4.0__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.
- intent_base/__init__.py +74 -0
- intent_base/_cdp.py +43 -0
- intent_base/_http.py +294 -0
- intent_base/async_client.py +98 -0
- intent_base/client.py +79 -0
- intent_base/errors.py +132 -0
- intent_base/models.py +67 -0
- intent_base/playwright.py +101 -0
- intent_base_sdk-0.4.0.dist-info/METADATA +92 -0
- intent_base_sdk-0.4.0.dist-info/RECORD +11 -0
- intent_base_sdk-0.4.0.dist-info/WHEEL +4 -0
intent_base/__init__.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""intent-base Python SDK.
|
|
2
|
+
|
|
3
|
+
Launch an isolated Profile through the local Profile Service and drive it with
|
|
4
|
+
Playwright over CDP::
|
|
5
|
+
|
|
6
|
+
from intent_base import launch_profile
|
|
7
|
+
|
|
8
|
+
result = launch_profile("profile_123")
|
|
9
|
+
result.page.goto("https://example.com")
|
|
10
|
+
print(result.page.title())
|
|
11
|
+
result.close()
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from ._http import (
|
|
15
|
+
DEFAULT_SERVICE_URL,
|
|
16
|
+
DEFAULT_TIMEOUT_MS,
|
|
17
|
+
IntentBaseAsyncHttpClient,
|
|
18
|
+
IntentBaseHttpClient,
|
|
19
|
+
)
|
|
20
|
+
from .async_client import (
|
|
21
|
+
AsyncLaunchProfileResult,
|
|
22
|
+
launch_profile_async,
|
|
23
|
+
profile_status_async,
|
|
24
|
+
stop_profile_async,
|
|
25
|
+
)
|
|
26
|
+
from .client import launch_profile, profile_status, stop_profile
|
|
27
|
+
from .errors import (
|
|
28
|
+
CdpConnectError,
|
|
29
|
+
CdpEndpointTimeoutError,
|
|
30
|
+
ChromiumLaunchFailedError,
|
|
31
|
+
ChromiumNotFoundError,
|
|
32
|
+
IntentBaseError,
|
|
33
|
+
ProfileAlreadyRunningError,
|
|
34
|
+
ProfileNotFoundError,
|
|
35
|
+
ProfileValidationError,
|
|
36
|
+
ProxyCheckFailedError,
|
|
37
|
+
RequestTimeoutError,
|
|
38
|
+
ServiceBadResponseError,
|
|
39
|
+
ServiceUnavailableError,
|
|
40
|
+
map_api_error,
|
|
41
|
+
)
|
|
42
|
+
from .models import LaunchProfileResponse, LaunchProfileResult
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"launch_profile",
|
|
46
|
+
"stop_profile",
|
|
47
|
+
"profile_status",
|
|
48
|
+
"launch_profile_async",
|
|
49
|
+
"stop_profile_async",
|
|
50
|
+
"profile_status_async",
|
|
51
|
+
"AsyncLaunchProfileResult",
|
|
52
|
+
"LaunchProfileResponse",
|
|
53
|
+
"LaunchProfileResult",
|
|
54
|
+
"IntentBaseHttpClient",
|
|
55
|
+
"IntentBaseAsyncHttpClient",
|
|
56
|
+
"DEFAULT_SERVICE_URL",
|
|
57
|
+
"DEFAULT_TIMEOUT_MS",
|
|
58
|
+
"map_api_error",
|
|
59
|
+
"IntentBaseError",
|
|
60
|
+
"ProfileNotFoundError",
|
|
61
|
+
"ProfileAlreadyRunningError",
|
|
62
|
+
"ProfileValidationError",
|
|
63
|
+
"ProxyCheckFailedError",
|
|
64
|
+
"ChromiumNotFoundError",
|
|
65
|
+
"ChromiumLaunchFailedError",
|
|
66
|
+
"CdpEndpointTimeoutError",
|
|
67
|
+
"CdpConnectError",
|
|
68
|
+
"ServiceUnavailableError",
|
|
69
|
+
"ServiceBadResponseError",
|
|
70
|
+
"RequestTimeoutError",
|
|
71
|
+
"__version__",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
__version__ = "0.4.0"
|
intent_base/_cdp.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""CDP endpoint normalization helpers.
|
|
2
|
+
|
|
3
|
+
The Profile Service returns a websocket browser endpoint
|
|
4
|
+
(``ws://127.0.0.1:<port>/devtools/browser/<uuid>``). Playwright's
|
|
5
|
+
``connect_over_cdp`` accepts either that websocket URL or an ``http://`` base
|
|
6
|
+
URL, so normalization is mostly validation plus stripping a trailing slash.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .errors import CdpConnectError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_websocket_endpoint(endpoint: str) -> bool:
|
|
15
|
+
return endpoint.startswith("ws://") or endpoint.startswith("wss://")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def normalize_cdp_endpoint(endpoint: str, *, profile_id: str | None = None) -> str:
|
|
19
|
+
"""Validate and normalize a CDP endpoint for Playwright/Puppeteer.
|
|
20
|
+
|
|
21
|
+
Raises :class:`CdpConnectError` (code ``CDP_CONNECT_FAILED``) when the
|
|
22
|
+
endpoint is empty or uses an unsupported scheme.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
value = (endpoint or "").strip()
|
|
26
|
+
if not value:
|
|
27
|
+
raise CdpConnectError(
|
|
28
|
+
"CDP_CONNECT_FAILED",
|
|
29
|
+
"service returned an empty CDP endpoint",
|
|
30
|
+
profile_id=profile_id,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if is_websocket_endpoint(value):
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
if value.startswith("http://") or value.startswith("https://"):
|
|
37
|
+
return value.rstrip("/")
|
|
38
|
+
|
|
39
|
+
raise CdpConnectError(
|
|
40
|
+
"CDP_CONNECT_FAILED",
|
|
41
|
+
f"unsupported CDP endpoint scheme: {value}",
|
|
42
|
+
profile_id=profile_id,
|
|
43
|
+
)
|
intent_base/_http.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""HTTP client for the local Profile Service.
|
|
2
|
+
|
|
3
|
+
This layer has no Playwright dependency: it only speaks the Profile Service
|
|
4
|
+
REST API and maps every failure to a typed :class:`IntentBaseError`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from .errors import (
|
|
16
|
+
IntentBaseError,
|
|
17
|
+
RequestTimeoutError,
|
|
18
|
+
ServiceBadResponseError,
|
|
19
|
+
ServiceUnavailableError,
|
|
20
|
+
map_api_error,
|
|
21
|
+
)
|
|
22
|
+
from .models import LaunchProfileResponse
|
|
23
|
+
|
|
24
|
+
DEFAULT_SERVICE_URL = "http://127.0.0.1:17390"
|
|
25
|
+
DEFAULT_TIMEOUT_MS = 30000
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_service_url(service_url: Optional[str]) -> str:
|
|
29
|
+
if service_url:
|
|
30
|
+
return service_url.rstrip("/")
|
|
31
|
+
env = os.environ.get("INTENT_BASE_SERVICE_URL")
|
|
32
|
+
if env:
|
|
33
|
+
return env.rstrip("/")
|
|
34
|
+
return DEFAULT_SERVICE_URL
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def resolve_timeout_ms(timeout_ms: Optional[int]) -> int:
|
|
38
|
+
if timeout_ms is not None:
|
|
39
|
+
return timeout_ms
|
|
40
|
+
env = os.environ.get("INTENT_BASE_TIMEOUT_MS")
|
|
41
|
+
if env:
|
|
42
|
+
try:
|
|
43
|
+
return int(env)
|
|
44
|
+
except ValueError:
|
|
45
|
+
pass
|
|
46
|
+
return DEFAULT_TIMEOUT_MS
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class IntentBaseHttpClient:
|
|
50
|
+
"""Synchronous Profile Service client built on httpx."""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
service_url: Optional[str] = None,
|
|
55
|
+
timeout_ms: Optional[int] = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
self.service_url = resolve_service_url(service_url)
|
|
58
|
+
self.timeout_ms = resolve_timeout_ms(timeout_ms)
|
|
59
|
+
|
|
60
|
+
def _url(self, profile_id: str, suffix: str) -> str:
|
|
61
|
+
return f"{self.service_url}/api/v1/profiles/{profile_id}/{suffix}"
|
|
62
|
+
|
|
63
|
+
def _request(
|
|
64
|
+
self,
|
|
65
|
+
method: str,
|
|
66
|
+
url: str,
|
|
67
|
+
*,
|
|
68
|
+
profile_id: str,
|
|
69
|
+
json_body: Optional[Dict[str, Any]] = None,
|
|
70
|
+
) -> Any:
|
|
71
|
+
timeout = self.timeout_ms / 1000.0
|
|
72
|
+
try:
|
|
73
|
+
response = httpx.request(method, url, json=json_body, timeout=timeout)
|
|
74
|
+
except httpx.TimeoutException as exc:
|
|
75
|
+
raise RequestTimeoutError(
|
|
76
|
+
"REQUEST_TIMEOUT",
|
|
77
|
+
f"request to {url} timed out after {self.timeout_ms}ms",
|
|
78
|
+
profile_id=profile_id,
|
|
79
|
+
cause=exc,
|
|
80
|
+
) from exc
|
|
81
|
+
except httpx.HTTPError as exc:
|
|
82
|
+
raise ServiceUnavailableError(
|
|
83
|
+
"SERVICE_UNAVAILABLE",
|
|
84
|
+
f"cannot reach Profile Service at {self.service_url}: {exc}",
|
|
85
|
+
profile_id=profile_id,
|
|
86
|
+
cause=exc,
|
|
87
|
+
) from exc
|
|
88
|
+
|
|
89
|
+
return self._parse(response, profile_id=profile_id)
|
|
90
|
+
|
|
91
|
+
def _parse(self, response: httpx.Response, *, profile_id: str) -> Any:
|
|
92
|
+
status = response.status_code
|
|
93
|
+
text = response.text
|
|
94
|
+
body: Any = None
|
|
95
|
+
if text:
|
|
96
|
+
try:
|
|
97
|
+
body = json.loads(text)
|
|
98
|
+
except json.JSONDecodeError as exc:
|
|
99
|
+
if 200 <= status < 300:
|
|
100
|
+
raise ServiceBadResponseError(
|
|
101
|
+
"SERVICE_BAD_RESPONSE",
|
|
102
|
+
f"Profile Service returned invalid JSON ({status})",
|
|
103
|
+
status=status,
|
|
104
|
+
profile_id=profile_id,
|
|
105
|
+
cause=exc,
|
|
106
|
+
) from exc
|
|
107
|
+
body = None
|
|
108
|
+
|
|
109
|
+
if 200 <= status < 300:
|
|
110
|
+
return body
|
|
111
|
+
|
|
112
|
+
if isinstance(body, dict) and "code" in body:
|
|
113
|
+
raise map_api_error(
|
|
114
|
+
str(body.get("code")),
|
|
115
|
+
str(body.get("message", "")),
|
|
116
|
+
status,
|
|
117
|
+
profile_id=profile_id,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
raise ServiceBadResponseError(
|
|
121
|
+
"SERVICE_BAD_RESPONSE",
|
|
122
|
+
f"Profile Service returned HTTP {status}",
|
|
123
|
+
status=status,
|
|
124
|
+
profile_id=profile_id,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def launch_profile(
|
|
128
|
+
self,
|
|
129
|
+
profile_id: str,
|
|
130
|
+
extra_args: Optional[List[str]] = None,
|
|
131
|
+
) -> LaunchProfileResponse:
|
|
132
|
+
body: Dict[str, Any] = {}
|
|
133
|
+
if extra_args:
|
|
134
|
+
body["extraArgs"] = extra_args
|
|
135
|
+
result = self._request(
|
|
136
|
+
"POST",
|
|
137
|
+
self._url(profile_id, "launch"),
|
|
138
|
+
profile_id=profile_id,
|
|
139
|
+
json_body=body,
|
|
140
|
+
)
|
|
141
|
+
if not isinstance(result, dict):
|
|
142
|
+
raise ServiceBadResponseError(
|
|
143
|
+
"SERVICE_BAD_RESPONSE",
|
|
144
|
+
"launch response was not a JSON object",
|
|
145
|
+
profile_id=profile_id,
|
|
146
|
+
)
|
|
147
|
+
return LaunchProfileResponse.from_api(result)
|
|
148
|
+
|
|
149
|
+
def stop_profile(self, profile_id: str) -> Dict[str, Any]:
|
|
150
|
+
result = self._request(
|
|
151
|
+
"POST",
|
|
152
|
+
self._url(profile_id, "stop"),
|
|
153
|
+
profile_id=profile_id,
|
|
154
|
+
)
|
|
155
|
+
return result if isinstance(result, dict) else {}
|
|
156
|
+
|
|
157
|
+
def status(self, profile_id: str) -> Dict[str, Any]:
|
|
158
|
+
result = self._request(
|
|
159
|
+
"GET",
|
|
160
|
+
self._url(profile_id, "status"),
|
|
161
|
+
profile_id=profile_id,
|
|
162
|
+
)
|
|
163
|
+
return result if isinstance(result, dict) else {}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class IntentBaseAsyncHttpClient:
|
|
167
|
+
"""Asynchronous Profile Service client built on httpx.AsyncClient."""
|
|
168
|
+
|
|
169
|
+
def __init__(
|
|
170
|
+
self,
|
|
171
|
+
service_url: Optional[str] = None,
|
|
172
|
+
timeout_ms: Optional[int] = None,
|
|
173
|
+
) -> None:
|
|
174
|
+
self.service_url = resolve_service_url(service_url)
|
|
175
|
+
self.timeout_ms = resolve_timeout_ms(timeout_ms)
|
|
176
|
+
|
|
177
|
+
def _url(self, profile_id: str, suffix: str) -> str:
|
|
178
|
+
return f"{self.service_url}/api/v1/profiles/{profile_id}/{suffix}"
|
|
179
|
+
|
|
180
|
+
async def _request(
|
|
181
|
+
self,
|
|
182
|
+
method: str,
|
|
183
|
+
url: str,
|
|
184
|
+
*,
|
|
185
|
+
profile_id: str,
|
|
186
|
+
json_body: Optional[Dict[str, Any]] = None,
|
|
187
|
+
) -> Any:
|
|
188
|
+
timeout = self.timeout_ms / 1000.0
|
|
189
|
+
try:
|
|
190
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
191
|
+
response = await client.request(method, url, json=json_body)
|
|
192
|
+
except httpx.TimeoutException as exc:
|
|
193
|
+
raise RequestTimeoutError(
|
|
194
|
+
"REQUEST_TIMEOUT",
|
|
195
|
+
f"request to {url} timed out after {self.timeout_ms}ms",
|
|
196
|
+
profile_id=profile_id,
|
|
197
|
+
cause=exc,
|
|
198
|
+
) from exc
|
|
199
|
+
except httpx.HTTPError as exc:
|
|
200
|
+
raise ServiceUnavailableError(
|
|
201
|
+
"SERVICE_UNAVAILABLE",
|
|
202
|
+
f"cannot reach Profile Service at {self.service_url}: {exc}",
|
|
203
|
+
profile_id=profile_id,
|
|
204
|
+
cause=exc,
|
|
205
|
+
) from exc
|
|
206
|
+
|
|
207
|
+
return _parse_response(response, profile_id=profile_id)
|
|
208
|
+
|
|
209
|
+
async def launch_profile(
|
|
210
|
+
self,
|
|
211
|
+
profile_id: str,
|
|
212
|
+
extra_args: Optional[List[str]] = None,
|
|
213
|
+
) -> LaunchProfileResponse:
|
|
214
|
+
body: Dict[str, Any] = {}
|
|
215
|
+
if extra_args:
|
|
216
|
+
body["extraArgs"] = extra_args
|
|
217
|
+
result = await self._request(
|
|
218
|
+
"POST",
|
|
219
|
+
self._url(profile_id, "launch"),
|
|
220
|
+
profile_id=profile_id,
|
|
221
|
+
json_body=body,
|
|
222
|
+
)
|
|
223
|
+
if not isinstance(result, dict):
|
|
224
|
+
raise ServiceBadResponseError(
|
|
225
|
+
"SERVICE_BAD_RESPONSE",
|
|
226
|
+
"launch response was not a JSON object",
|
|
227
|
+
profile_id=profile_id,
|
|
228
|
+
)
|
|
229
|
+
return LaunchProfileResponse.from_api(result)
|
|
230
|
+
|
|
231
|
+
async def stop_profile(self, profile_id: str) -> Dict[str, Any]:
|
|
232
|
+
result = await self._request(
|
|
233
|
+
"POST",
|
|
234
|
+
self._url(profile_id, "stop"),
|
|
235
|
+
profile_id=profile_id,
|
|
236
|
+
)
|
|
237
|
+
return result if isinstance(result, dict) else {}
|
|
238
|
+
|
|
239
|
+
async def status(self, profile_id: str) -> Dict[str, Any]:
|
|
240
|
+
result = await self._request(
|
|
241
|
+
"GET",
|
|
242
|
+
self._url(profile_id, "status"),
|
|
243
|
+
profile_id=profile_id,
|
|
244
|
+
)
|
|
245
|
+
return result if isinstance(result, dict) else {}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _parse_response(response: httpx.Response, *, profile_id: str) -> Any:
|
|
249
|
+
"""Shared response parser for the async client (mirrors the sync logic)."""
|
|
250
|
+
status = response.status_code
|
|
251
|
+
text = response.text
|
|
252
|
+
body: Any = None
|
|
253
|
+
if text:
|
|
254
|
+
try:
|
|
255
|
+
body = json.loads(text)
|
|
256
|
+
except json.JSONDecodeError as exc:
|
|
257
|
+
if 200 <= status < 300:
|
|
258
|
+
raise ServiceBadResponseError(
|
|
259
|
+
"SERVICE_BAD_RESPONSE",
|
|
260
|
+
f"Profile Service returned invalid JSON ({status})",
|
|
261
|
+
status=status,
|
|
262
|
+
profile_id=profile_id,
|
|
263
|
+
cause=exc,
|
|
264
|
+
) from exc
|
|
265
|
+
body = None
|
|
266
|
+
|
|
267
|
+
if 200 <= status < 300:
|
|
268
|
+
return body
|
|
269
|
+
|
|
270
|
+
if isinstance(body, dict) and "code" in body:
|
|
271
|
+
raise map_api_error(
|
|
272
|
+
str(body.get("code")),
|
|
273
|
+
str(body.get("message", "")),
|
|
274
|
+
status,
|
|
275
|
+
profile_id=profile_id,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
raise ServiceBadResponseError(
|
|
279
|
+
"SERVICE_BAD_RESPONSE",
|
|
280
|
+
f"Profile Service returned HTTP {status}",
|
|
281
|
+
status=status,
|
|
282
|
+
profile_id=profile_id,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
__all__ = [
|
|
287
|
+
"IntentBaseHttpClient",
|
|
288
|
+
"IntentBaseAsyncHttpClient",
|
|
289
|
+
"IntentBaseError",
|
|
290
|
+
"resolve_service_url",
|
|
291
|
+
"resolve_timeout_ms",
|
|
292
|
+
"DEFAULT_SERVICE_URL",
|
|
293
|
+
"DEFAULT_TIMEOUT_MS",
|
|
294
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Public asynchronous API for the intent-base Python SDK.
|
|
2
|
+
|
|
3
|
+
Shares DTOs and error mapping with the sync client; only the Playwright and
|
|
4
|
+
HTTP transport are async. The returned :class:`LaunchProfileResult` exposes a
|
|
5
|
+
coroutine ``aclose()`` plus a synchronous ``close()`` for symmetry.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from ._http import IntentBaseAsyncHttpClient
|
|
14
|
+
from .models import LaunchProfileResponse
|
|
15
|
+
from .playwright import connect_playwright_async
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class AsyncLaunchProfileResult:
|
|
20
|
+
browser: Any
|
|
21
|
+
context: Any
|
|
22
|
+
page: Any
|
|
23
|
+
metadata: LaunchProfileResponse
|
|
24
|
+
_driver: Any = None
|
|
25
|
+
_http: Any = None
|
|
26
|
+
_profile_id: str = ""
|
|
27
|
+
_auto_stop: bool = False
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def profile_id(self) -> str:
|
|
31
|
+
return self.metadata.profile_id
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def cdp_endpoint(self) -> str:
|
|
35
|
+
return self.metadata.cdp_endpoint
|
|
36
|
+
|
|
37
|
+
async def aclose(self) -> None:
|
|
38
|
+
try:
|
|
39
|
+
await self.browser.close()
|
|
40
|
+
finally:
|
|
41
|
+
if self._driver is not None:
|
|
42
|
+
try:
|
|
43
|
+
await self._driver.stop()
|
|
44
|
+
finally:
|
|
45
|
+
if self._auto_stop and self._http is not None:
|
|
46
|
+
try:
|
|
47
|
+
await self._http.stop_profile(self._profile_id)
|
|
48
|
+
except Exception: # noqa: BLE001
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def launch_profile_async(
|
|
53
|
+
profile_id: str,
|
|
54
|
+
*,
|
|
55
|
+
service_url: Optional[str] = None,
|
|
56
|
+
extra_args: Optional[List[str]] = None,
|
|
57
|
+
timeout_ms: Optional[int] = None,
|
|
58
|
+
auto_stop: bool = False,
|
|
59
|
+
) -> AsyncLaunchProfileResult:
|
|
60
|
+
"""Async variant of :func:`intent_base.launch_profile`."""
|
|
61
|
+
|
|
62
|
+
http = IntentBaseAsyncHttpClient(service_url, timeout_ms)
|
|
63
|
+
metadata = await http.launch_profile(profile_id, extra_args)
|
|
64
|
+
|
|
65
|
+
browser, context, page, driver = await connect_playwright_async(
|
|
66
|
+
metadata.cdp_endpoint, profile_id=profile_id
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return AsyncLaunchProfileResult(
|
|
70
|
+
browser=browser,
|
|
71
|
+
context=context,
|
|
72
|
+
page=page,
|
|
73
|
+
metadata=metadata,
|
|
74
|
+
_driver=driver,
|
|
75
|
+
_http=http,
|
|
76
|
+
_profile_id=profile_id,
|
|
77
|
+
_auto_stop=auto_stop,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def stop_profile_async(
|
|
82
|
+
profile_id: str,
|
|
83
|
+
*,
|
|
84
|
+
service_url: Optional[str] = None,
|
|
85
|
+
timeout_ms: Optional[int] = None,
|
|
86
|
+
) -> Dict[str, Any]:
|
|
87
|
+
return await IntentBaseAsyncHttpClient(service_url, timeout_ms).stop_profile(
|
|
88
|
+
profile_id
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def profile_status_async(
|
|
93
|
+
profile_id: str,
|
|
94
|
+
*,
|
|
95
|
+
service_url: Optional[str] = None,
|
|
96
|
+
timeout_ms: Optional[int] = None,
|
|
97
|
+
) -> Dict[str, Any]:
|
|
98
|
+
return await IntentBaseAsyncHttpClient(service_url, timeout_ms).status(profile_id)
|
intent_base/client.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Public synchronous API for the intent-base Python SDK.
|
|
2
|
+
|
|
3
|
+
``launch_profile`` asks the local Profile Service to start the Profile's
|
|
4
|
+
Chromium, then connects Playwright to the returned CDP endpoint. The SDK never
|
|
5
|
+
launches Chromium itself and never passes isolation-critical browser args.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from ._http import IntentBaseHttpClient
|
|
13
|
+
from .errors import CdpConnectError
|
|
14
|
+
from .models import LaunchProfileResponse, LaunchProfileResult
|
|
15
|
+
from .playwright import connect_playwright
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def launch_profile(
|
|
19
|
+
profile_id: str,
|
|
20
|
+
*,
|
|
21
|
+
service_url: Optional[str] = None,
|
|
22
|
+
extra_args: Optional[List[str]] = None,
|
|
23
|
+
timeout_ms: Optional[int] = None,
|
|
24
|
+
auto_stop: bool = False,
|
|
25
|
+
) -> LaunchProfileResult:
|
|
26
|
+
"""Launch ``profile_id`` and return a connected Playwright handle.
|
|
27
|
+
|
|
28
|
+
Set ``auto_stop=True`` to also stop the Profile on ``result.close()``;
|
|
29
|
+
otherwise only the CDP connection and Playwright driver are torn down and
|
|
30
|
+
the Profile keeps running.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
http = IntentBaseHttpClient(service_url, timeout_ms)
|
|
34
|
+
metadata: LaunchProfileResponse = http.launch_profile(profile_id, extra_args)
|
|
35
|
+
|
|
36
|
+
browser, context, page, driver = connect_playwright(
|
|
37
|
+
metadata.cdp_endpoint, profile_id=profile_id
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def _close() -> None:
|
|
41
|
+
try:
|
|
42
|
+
browser.close()
|
|
43
|
+
finally:
|
|
44
|
+
try:
|
|
45
|
+
driver.stop()
|
|
46
|
+
finally:
|
|
47
|
+
if auto_stop:
|
|
48
|
+
try:
|
|
49
|
+
http.stop_profile(profile_id)
|
|
50
|
+
except Exception: # noqa: BLE001 - best effort on close
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
return LaunchProfileResult(
|
|
54
|
+
browser=browser,
|
|
55
|
+
context=context,
|
|
56
|
+
page=page,
|
|
57
|
+
metadata=metadata,
|
|
58
|
+
_closer=_close,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def stop_profile(
|
|
63
|
+
profile_id: str,
|
|
64
|
+
*,
|
|
65
|
+
service_url: Optional[str] = None,
|
|
66
|
+
timeout_ms: Optional[int] = None,
|
|
67
|
+
) -> Dict[str, Any]:
|
|
68
|
+
"""Stop a running Profile via the Profile Service."""
|
|
69
|
+
return IntentBaseHttpClient(service_url, timeout_ms).stop_profile(profile_id)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def profile_status(
|
|
73
|
+
profile_id: str,
|
|
74
|
+
*,
|
|
75
|
+
service_url: Optional[str] = None,
|
|
76
|
+
timeout_ms: Optional[int] = None,
|
|
77
|
+
) -> Dict[str, Any]:
|
|
78
|
+
"""Fetch the current lifecycle status of a Profile."""
|
|
79
|
+
return IntentBaseHttpClient(service_url, timeout_ms).status(profile_id)
|
intent_base/errors.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Typed errors for the intent-base Python SDK.
|
|
2
|
+
|
|
3
|
+
Every failure raised by the SDK is an :class:`IntentBaseError` (or subclass)
|
|
4
|
+
that preserves the machine-readable ``code`` from the Profile Service so callers
|
|
5
|
+
can branch on it without parsing messages.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IntentBaseError(Exception):
|
|
14
|
+
"""Base class for every error raised by the SDK."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
code: str,
|
|
19
|
+
message: str,
|
|
20
|
+
*,
|
|
21
|
+
status: Optional[int] = None,
|
|
22
|
+
profile_id: Optional[str] = None,
|
|
23
|
+
cause: Optional[BaseException] = None,
|
|
24
|
+
details: Optional[Dict[str, Any]] = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.code = code
|
|
28
|
+
self.message = message
|
|
29
|
+
self.status = status
|
|
30
|
+
self.profile_id = profile_id
|
|
31
|
+
self.cause = cause
|
|
32
|
+
self.details = details or {}
|
|
33
|
+
if cause is not None:
|
|
34
|
+
self.__cause__ = cause
|
|
35
|
+
|
|
36
|
+
def __repr__(self) -> str: # pragma: no cover - debug aid
|
|
37
|
+
return (
|
|
38
|
+
f"{type(self).__name__}(code={self.code!r}, message={self.message!r}, "
|
|
39
|
+
f"status={self.status!r}, profile_id={self.profile_id!r})"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ProfileNotFoundError(IntentBaseError):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ProfileAlreadyRunningError(IntentBaseError):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ProfileValidationError(IntentBaseError):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ProxyCheckFailedError(IntentBaseError):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ChromiumNotFoundError(IntentBaseError):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ChromiumLaunchFailedError(IntentBaseError):
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CdpEndpointTimeoutError(IntentBaseError):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class CdpConnectError(IntentBaseError):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ServiceUnavailableError(IntentBaseError):
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ServiceBadResponseError(IntentBaseError):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class RequestTimeoutError(IntentBaseError):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Service-side codes (see services/profile-service/src/error.rs) plus the
|
|
88
|
+
# SDK-only transport codes the document mandates.
|
|
89
|
+
_CODE_TO_CLASS: Dict[str, type] = {
|
|
90
|
+
"PROFILE_NOT_FOUND": ProfileNotFoundError,
|
|
91
|
+
"PROFILE_ALREADY_RUNNING": ProfileAlreadyRunningError,
|
|
92
|
+
"PROFILE_RUNNING": ProfileAlreadyRunningError,
|
|
93
|
+
"PROFILE_VALIDATION": ProfileValidationError,
|
|
94
|
+
"BROWSER_ARG_FORBIDDEN": ProfileValidationError,
|
|
95
|
+
"PROXY_CHECK_FAILED": ProxyCheckFailedError,
|
|
96
|
+
"PROXY_CONFIG_MISSING": ProxyCheckFailedError,
|
|
97
|
+
"CHROMIUM_NOT_FOUND": ChromiumNotFoundError,
|
|
98
|
+
"CHROMIUM_LAUNCH_FAILED": ChromiumLaunchFailedError,
|
|
99
|
+
"CHROMIUM_EXITED_EARLY": ChromiumLaunchFailedError,
|
|
100
|
+
"CDP_ENDPOINT_TIMEOUT": CdpEndpointTimeoutError,
|
|
101
|
+
"CDP_ENDPOINT_INVALID": CdpConnectError,
|
|
102
|
+
"CDP_CONNECT_FAILED": CdpConnectError,
|
|
103
|
+
"SERVICE_UNAVAILABLE": ServiceUnavailableError,
|
|
104
|
+
"SERVICE_BAD_RESPONSE": ServiceBadResponseError,
|
|
105
|
+
"REQUEST_TIMEOUT": RequestTimeoutError,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def map_api_error(
|
|
110
|
+
code: str,
|
|
111
|
+
message: str,
|
|
112
|
+
status: Optional[int] = None,
|
|
113
|
+
*,
|
|
114
|
+
profile_id: Optional[str] = None,
|
|
115
|
+
cause: Optional[BaseException] = None,
|
|
116
|
+
details: Optional[Dict[str, Any]] = None,
|
|
117
|
+
) -> IntentBaseError:
|
|
118
|
+
"""Build the typed error for a service ``code``.
|
|
119
|
+
|
|
120
|
+
Unknown codes fall back to the base :class:`IntentBaseError` while still
|
|
121
|
+
preserving the original code so callers never lose information.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
cls = _CODE_TO_CLASS.get(code, IntentBaseError)
|
|
125
|
+
return cls(
|
|
126
|
+
code,
|
|
127
|
+
message,
|
|
128
|
+
status=status,
|
|
129
|
+
profile_id=profile_id,
|
|
130
|
+
cause=cause,
|
|
131
|
+
details=details,
|
|
132
|
+
)
|
intent_base/models.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""DTOs for the intent-base Python SDK.
|
|
2
|
+
|
|
3
|
+
These mirror the Profile Service API contract defined in
|
|
4
|
+
``crates/intent-base-types/src/browser.rs``. The HTTP layer speaks camelCase
|
|
5
|
+
on the wire; these dataclasses use snake_case and are built via
|
|
6
|
+
``LaunchProfileResponse.from_api``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Mapping, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class LaunchProfileResponse:
|
|
17
|
+
"""Successful ``POST /api/v1/profiles/:id/launch`` body."""
|
|
18
|
+
|
|
19
|
+
profile_id: str
|
|
20
|
+
status: str
|
|
21
|
+
pid: int
|
|
22
|
+
cdp_endpoint: str
|
|
23
|
+
devtools_port: int
|
|
24
|
+
user_data_path: str
|
|
25
|
+
browser_log_path: str
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_api(cls, body: Mapping[str, Any]) -> "LaunchProfileResponse":
|
|
29
|
+
return cls(
|
|
30
|
+
profile_id=str(body.get("profileId", "")),
|
|
31
|
+
status=str(body.get("status", "")),
|
|
32
|
+
pid=int(body.get("pid", 0)),
|
|
33
|
+
cdp_endpoint=str(body.get("cdpEndpoint", "")),
|
|
34
|
+
devtools_port=int(body.get("devtoolsPort", 0)),
|
|
35
|
+
user_data_path=str(body.get("userDataPath", "")),
|
|
36
|
+
browser_log_path=str(body.get("browserLogPath", "")),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class LaunchProfileResult:
|
|
42
|
+
"""Handles returned by ``launch_profile``.
|
|
43
|
+
|
|
44
|
+
``browser`` / ``context`` / ``page`` are Playwright objects (typed as
|
|
45
|
+
``Any`` so the base SDK does not depend on Playwright). Call ``close()`` to
|
|
46
|
+
disconnect from the browser and stop the Playwright driver; the underlying
|
|
47
|
+
Profile keeps running unless ``auto_stop`` was requested at launch time.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
browser: Any
|
|
51
|
+
context: Any
|
|
52
|
+
page: Any
|
|
53
|
+
metadata: LaunchProfileResponse
|
|
54
|
+
_closer: Optional[Any] = None
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def profile_id(self) -> str:
|
|
58
|
+
return self.metadata.profile_id
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def cdp_endpoint(self) -> str:
|
|
62
|
+
return self.metadata.cdp_endpoint
|
|
63
|
+
|
|
64
|
+
def close(self) -> None:
|
|
65
|
+
"""Disconnect from the browser and tear down the Playwright driver."""
|
|
66
|
+
if self._closer is not None:
|
|
67
|
+
self._closer()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Playwright connection helpers.
|
|
2
|
+
|
|
3
|
+
Playwright is an optional dependency. Importing this module without Playwright
|
|
4
|
+
installed raises a clear :class:`CdpConnectError` telling the user to install
|
|
5
|
+
it, rather than a bare ``ModuleNotFoundError``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Tuple
|
|
11
|
+
|
|
12
|
+
from ._cdp import normalize_cdp_endpoint
|
|
13
|
+
from .errors import CdpConnectError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _require_sync_playwright(profile_id: str | None = None):
|
|
17
|
+
try:
|
|
18
|
+
from playwright.sync_api import sync_playwright # type: ignore
|
|
19
|
+
except ImportError as exc: # pragma: no cover - import guard
|
|
20
|
+
raise CdpConnectError(
|
|
21
|
+
"CDP_CONNECT_FAILED",
|
|
22
|
+
"Playwright is required for launch_profile. Install it with "
|
|
23
|
+
"`pip install \"intent-base[playwright]\"` then run `playwright install chromium`.",
|
|
24
|
+
profile_id=profile_id,
|
|
25
|
+
cause=exc,
|
|
26
|
+
) from exc
|
|
27
|
+
return sync_playwright
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _require_async_playwright(profile_id: str | None = None):
|
|
31
|
+
try:
|
|
32
|
+
from playwright.async_api import async_playwright # type: ignore
|
|
33
|
+
except ImportError as exc: # pragma: no cover - import guard
|
|
34
|
+
raise CdpConnectError(
|
|
35
|
+
"CDP_CONNECT_FAILED",
|
|
36
|
+
"Playwright is required for launch_profile_async. Install it with "
|
|
37
|
+
"`pip install \"intent-base[playwright]\"` then run `playwright install chromium`.",
|
|
38
|
+
profile_id=profile_id,
|
|
39
|
+
cause=exc,
|
|
40
|
+
) from exc
|
|
41
|
+
return async_playwright
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def connect_playwright(
|
|
45
|
+
cdp_endpoint: str,
|
|
46
|
+
*,
|
|
47
|
+
profile_id: str | None = None,
|
|
48
|
+
) -> Tuple[Any, Any, Any, Any]:
|
|
49
|
+
"""Connect to an already-launched browser over CDP (sync API).
|
|
50
|
+
|
|
51
|
+
Returns ``(browser, context, page, driver)`` where ``driver`` is the
|
|
52
|
+
started Playwright instance the caller must stop on close.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
endpoint = normalize_cdp_endpoint(cdp_endpoint, profile_id=profile_id)
|
|
56
|
+
sync_playwright = _require_sync_playwright(profile_id)
|
|
57
|
+
|
|
58
|
+
driver = sync_playwright().start()
|
|
59
|
+
try:
|
|
60
|
+
browser = driver.chromium.connect_over_cdp(endpoint)
|
|
61
|
+
except Exception as exc: # noqa: BLE001 - normalize any connect failure
|
|
62
|
+
driver.stop()
|
|
63
|
+
raise CdpConnectError(
|
|
64
|
+
"CDP_CONNECT_FAILED",
|
|
65
|
+
f"failed to connect to CDP endpoint: {exc}",
|
|
66
|
+
profile_id=profile_id,
|
|
67
|
+
cause=exc,
|
|
68
|
+
) from exc
|
|
69
|
+
|
|
70
|
+
context = browser.contexts[0] if browser.contexts else browser.new_context()
|
|
71
|
+
page = context.pages[0] if context.pages else context.new_page()
|
|
72
|
+
return browser, context, page, driver
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def connect_playwright_async(
|
|
76
|
+
cdp_endpoint: str,
|
|
77
|
+
*,
|
|
78
|
+
profile_id: str | None = None,
|
|
79
|
+
) -> Tuple[Any, Any, Any, Any]:
|
|
80
|
+
"""Connect to an already-launched browser over CDP (async API)."""
|
|
81
|
+
|
|
82
|
+
endpoint = normalize_cdp_endpoint(cdp_endpoint, profile_id=profile_id)
|
|
83
|
+
async_playwright = _require_async_playwright(profile_id)
|
|
84
|
+
|
|
85
|
+
driver = await async_playwright().start()
|
|
86
|
+
try:
|
|
87
|
+
browser = await driver.chromium.connect_over_cdp(endpoint)
|
|
88
|
+
except Exception as exc: # noqa: BLE001
|
|
89
|
+
await driver.stop()
|
|
90
|
+
raise CdpConnectError(
|
|
91
|
+
"CDP_CONNECT_FAILED",
|
|
92
|
+
f"failed to connect to CDP endpoint: {exc}",
|
|
93
|
+
profile_id=profile_id,
|
|
94
|
+
cause=exc,
|
|
95
|
+
) from exc
|
|
96
|
+
|
|
97
|
+
contexts = browser.contexts
|
|
98
|
+
context = contexts[0] if contexts else await browser.new_context()
|
|
99
|
+
pages = context.pages
|
|
100
|
+
page = pages[0] if pages else await context.new_page()
|
|
101
|
+
return browser, context, page, driver
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: intent-base-sdk
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: intent-base Python SDK: launch isolated Profiles via the local Profile Service and drive them with Playwright over CDP.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: httpx>=0.27
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
10
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
12
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
13
|
+
Requires-Dist: twine>=5; extra == 'dev'
|
|
14
|
+
Provides-Extra: playwright
|
|
15
|
+
Requires-Dist: playwright>=1.45; extra == 'playwright'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# intent-base-sdk
|
|
19
|
+
|
|
20
|
+
Python SDK for [intent-base](https://github.com/YOUR_ORG/intent-base): launch an
|
|
21
|
+
isolated Profile through the local Profile Service and drive it with Playwright
|
|
22
|
+
over CDP. The SDK never launches Chromium itself — only the Profile Service
|
|
23
|
+
does.
|
|
24
|
+
|
|
25
|
+
## Install (PyPI)
|
|
26
|
+
|
|
27
|
+
Requires Profile Service running (default `http://127.0.0.1:17390`).
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install intent-base-sdk==0.4.0
|
|
31
|
+
pip install "intent-base-sdk[playwright]==0.4.0"
|
|
32
|
+
playwright install chromium
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The base package only needs `httpx`. Playwright is an optional extra.
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
| Option | Default | Environment variable |
|
|
40
|
+
| ------------- | --------------------------- | --------------------------- |
|
|
41
|
+
| `service_url` | `http://127.0.0.1:17390` | `INTENT_BASE_SERVICE_URL` |
|
|
42
|
+
| `timeout_ms` | `30000` | `INTENT_BASE_TIMEOUT_MS` |
|
|
43
|
+
|
|
44
|
+
## Sync usage
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from intent_base import launch_profile
|
|
48
|
+
|
|
49
|
+
result = launch_profile("profile_123")
|
|
50
|
+
result.page.goto("https://example.com")
|
|
51
|
+
print(result.page.title())
|
|
52
|
+
result.close()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Pass `auto_stop=True` to also stop the Profile on `close()`.
|
|
56
|
+
|
|
57
|
+
## Async usage
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import asyncio
|
|
61
|
+
from intent_base import launch_profile_async
|
|
62
|
+
|
|
63
|
+
async def main():
|
|
64
|
+
result = await launch_profile_async("profile_123")
|
|
65
|
+
await result.page.goto("https://example.com")
|
|
66
|
+
print(await result.page.title())
|
|
67
|
+
await result.aclose()
|
|
68
|
+
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Errors
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from intent_base import launch_profile, ProfileAlreadyRunningError, IntentBaseError
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
result = launch_profile("profile_123")
|
|
79
|
+
except ProfileAlreadyRunningError as err:
|
|
80
|
+
print("already running:", err.code)
|
|
81
|
+
except IntentBaseError as err:
|
|
82
|
+
print(err.code, err.message, err.status)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Monorepo development
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install -e "sdks/python[dev,playwright]"
|
|
89
|
+
python -m pytest sdks/python/tests
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
See [docs/PUBLISH.md](../../docs/PUBLISH.md) for PyPI release steps.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
intent_base/__init__.py,sha256=rbtoO3kxOQjaxndyjpZGSRIlS4D-lyaO4NEMU_3B4rg,1852
|
|
2
|
+
intent_base/_cdp.py,sha256=wZ99I3MMwLOVwWcnBejF5LA61Op-WCVvOCeOXTN4vg8,1342
|
|
3
|
+
intent_base/_http.py,sha256=xgiVYYnZdVGJe-HxunEewwYuLMmXTsCnisO_tNhlzCA,9080
|
|
4
|
+
intent_base/async_client.py,sha256=3FPI1oMoNCUNQB5DT02NYfQsTbuDAlz904uidGoxLjQ,2785
|
|
5
|
+
intent_base/client.py,sha256=IquvNjzRSAKvYytt8sdLfVMWfTegmdc7Jf3-iOw0-vg,2398
|
|
6
|
+
intent_base/errors.py,sha256=z9NyJWxwp6cY6OHoWsw2OrRL2Un0GSmquaGMWWbBIfY,3497
|
|
7
|
+
intent_base/models.py,sha256=0aj9q0amT8PEuFl4VgLq4CeUadj_pAKyq9tfrAq-AyQ,2061
|
|
8
|
+
intent_base/playwright.py,sha256=Qjq4CY5LqWQrMXiYVat0IH9bdbkAQLN29q0kBRYlFRg,3539
|
|
9
|
+
intent_base_sdk-0.4.0.dist-info/METADATA,sha256=Jt6BAiJrBcrL0ToPvEIlGN-i8VJ5e27CoXBzQzm5Lk8,2549
|
|
10
|
+
intent_base_sdk-0.4.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
11
|
+
intent_base_sdk-0.4.0.dist-info/RECORD,,
|