scalebrowser 0.2.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.
- scalebrowser/__init__.py +234 -0
- scalebrowser/_http.py +180 -0
- scalebrowser/_sync.py +604 -0
- scalebrowser/_version.py +3 -0
- scalebrowser/cdp.py +429 -0
- scalebrowser/client.py +825 -0
- scalebrowser/errors.py +97 -0
- scalebrowser/events.py +52 -0
- scalebrowser/models.py +861 -0
- scalebrowser/models_control.py +128 -0
- scalebrowser/models_identity.py +130 -0
- scalebrowser/models_runs.py +56 -0
- scalebrowser-0.2.0.dist-info/METADATA +170 -0
- scalebrowser-0.2.0.dist-info/RECORD +16 -0
- scalebrowser-0.2.0.dist-info/WHEEL +4 -0
- scalebrowser-0.2.0.dist-info/licenses/LICENSE +21 -0
scalebrowser/_sync.py
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
"""Synchronous ergonomics over the async core.
|
|
2
|
+
|
|
3
|
+
A single background daemon thread runs an asyncio loop; the sync client and CDP
|
|
4
|
+
session drive the async implementations through it via
|
|
5
|
+
``run_coroutine_threadsafe``. There is no duplicated endpoint logic — every call
|
|
6
|
+
forwards to :class:`AsyncScalebrowserClient` / :class:`CdpSession`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import threading
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from typing import Any, Coroutine, Iterator, Mapping, Optional, Sequence, TypeVar, Union
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from .cdp import CdpSession, EventCallback
|
|
19
|
+
from .client import DEFAULT_BASE_URL, AsyncScalebrowserClient
|
|
20
|
+
from .models import (
|
|
21
|
+
RunStep,
|
|
22
|
+
CheckProxyConfigBody,
|
|
23
|
+
CredentialBundle,
|
|
24
|
+
CredentialImportResult,
|
|
25
|
+
CredentialMeta,
|
|
26
|
+
CreateGroupBody,
|
|
27
|
+
CreatePresetBody,
|
|
28
|
+
CreateProfileBody,
|
|
29
|
+
CreateProxyBody,
|
|
30
|
+
AuditStatus,
|
|
31
|
+
Extension,
|
|
32
|
+
ExtensionsResult,
|
|
33
|
+
Group,
|
|
34
|
+
Metrics,
|
|
35
|
+
PersonaConstraintOptions,
|
|
36
|
+
Preset,
|
|
37
|
+
Profile,
|
|
38
|
+
Proxy,
|
|
39
|
+
ProxyCheckResult,
|
|
40
|
+
RevealedCredential,
|
|
41
|
+
SessionExportResult,
|
|
42
|
+
StartProfileResult,
|
|
43
|
+
StopProfileResult,
|
|
44
|
+
UpdatePresetBody,
|
|
45
|
+
UpdateProfileBody,
|
|
46
|
+
UpdateProxyBody,
|
|
47
|
+
VaultStatus,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
from .models_control import (
|
|
51
|
+
Account,
|
|
52
|
+
ArtifactBytes,
|
|
53
|
+
ArtifactPutResult,
|
|
54
|
+
HealthStatus,
|
|
55
|
+
InterruptionLockView,
|
|
56
|
+
InterruptionRuleRow,
|
|
57
|
+
ReadyStatus,
|
|
58
|
+
SetInterruptionLockBody,
|
|
59
|
+
SetInterruptionRuleBody,
|
|
60
|
+
)
|
|
61
|
+
from .models_identity import (
|
|
62
|
+
BindInboxBody,
|
|
63
|
+
Inbox,
|
|
64
|
+
InboxBindings,
|
|
65
|
+
PasskeyRow,
|
|
66
|
+
PutInboxBody,
|
|
67
|
+
RevealCookiesBody,
|
|
68
|
+
RevealCookiesResult,
|
|
69
|
+
)
|
|
70
|
+
from .models_runs import ActivitySnapshot, AgentRun
|
|
71
|
+
|
|
72
|
+
T = TypeVar("T")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class _Portal:
|
|
76
|
+
"""A dedicated event loop running on a background daemon thread."""
|
|
77
|
+
|
|
78
|
+
def __init__(self) -> None:
|
|
79
|
+
self._loop = asyncio.new_event_loop()
|
|
80
|
+
self._thread = threading.Thread(
|
|
81
|
+
target=self._loop.run_forever, name="scalebrowser-portal", daemon=True
|
|
82
|
+
)
|
|
83
|
+
self._thread.start()
|
|
84
|
+
|
|
85
|
+
def run(self, coro: Coroutine[Any, Any, T]) -> T:
|
|
86
|
+
return asyncio.run_coroutine_threadsafe(coro, self._loop).result()
|
|
87
|
+
|
|
88
|
+
def close(self) -> None:
|
|
89
|
+
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
90
|
+
self._thread.join(timeout=5)
|
|
91
|
+
if not self._loop.is_running():
|
|
92
|
+
self._loop.close()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class SyncCdpSession:
|
|
96
|
+
"""Synchronous wrapper around :class:`CdpSession`."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, inner: CdpSession, portal: _Portal) -> None:
|
|
99
|
+
self._inner = inner
|
|
100
|
+
self._portal = portal
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def session_id(self) -> Optional[str]:
|
|
104
|
+
return self._inner.session_id
|
|
105
|
+
|
|
106
|
+
def send(self, method: str, params: Optional[dict[str, Any]] = None) -> dict[str, Any]:
|
|
107
|
+
return self._portal.run(self._inner.send(method, params))
|
|
108
|
+
|
|
109
|
+
def on(self, method: str, callback: EventCallback) -> Any:
|
|
110
|
+
return self._inner.on(method, callback)
|
|
111
|
+
|
|
112
|
+
def attach_to_page(
|
|
113
|
+
self, target_id: Optional[str] = None, *, create: bool = False, url: Optional[str] = None
|
|
114
|
+
) -> "SyncCdpSession":
|
|
115
|
+
child = self._portal.run(self._inner.attach_to_page(target_id, create=create, url=url))
|
|
116
|
+
return SyncCdpSession(child, self._portal)
|
|
117
|
+
|
|
118
|
+
def navigate(self, url: str, *, wait: bool = True, timeout: float = 30.0) -> dict[str, Any]:
|
|
119
|
+
return self._portal.run(self._inner.navigate(url, wait=wait, timeout=timeout))
|
|
120
|
+
|
|
121
|
+
def evaluate(
|
|
122
|
+
self,
|
|
123
|
+
expression: str,
|
|
124
|
+
*,
|
|
125
|
+
await_promise: bool = True,
|
|
126
|
+
return_by_value: bool = True,
|
|
127
|
+
isolated: bool = False,
|
|
128
|
+
) -> Any:
|
|
129
|
+
return self._portal.run(
|
|
130
|
+
self._inner.evaluate(
|
|
131
|
+
expression,
|
|
132
|
+
await_promise=await_promise,
|
|
133
|
+
return_by_value=return_by_value,
|
|
134
|
+
isolated=isolated,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def create_isolated_world(self, frame_id: Optional[str] = None, world_name: str = "__sb") -> int:
|
|
139
|
+
return self._portal.run(self._inner.create_isolated_world(frame_id, world_name))
|
|
140
|
+
|
|
141
|
+
def humanize_move(self, x: float, y: float) -> None:
|
|
142
|
+
self._portal.run(self._inner.humanize_move(x, y))
|
|
143
|
+
|
|
144
|
+
def humanize_click(self, x: float, y: float, *, button: str = "left", click_count: int = 1) -> None:
|
|
145
|
+
self._portal.run(self._inner.humanize_click(x, y, button=button, click_count=click_count))
|
|
146
|
+
|
|
147
|
+
def humanize_type(self, text: str) -> None:
|
|
148
|
+
self._portal.run(self._inner.humanize_type(text))
|
|
149
|
+
|
|
150
|
+
def humanize_scroll(self, x: float, y: float, *, delta_x: float = 0, delta_y: float = 0) -> None:
|
|
151
|
+
self._portal.run(self._inner.humanize_scroll(x, y, delta_x=delta_x, delta_y=delta_y))
|
|
152
|
+
|
|
153
|
+
def close(self) -> None:
|
|
154
|
+
self._portal.run(self._inner.close())
|
|
155
|
+
|
|
156
|
+
def __enter__(self) -> "SyncCdpSession":
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
def __exit__(self, *exc: object) -> None:
|
|
160
|
+
self.close()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ScalebrowserClient:
|
|
164
|
+
"""Synchronous client — mirrors :class:`AsyncScalebrowserClient` one-to-one."""
|
|
165
|
+
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
169
|
+
token: Optional[str] = None,
|
|
170
|
+
*,
|
|
171
|
+
timeout: float = 30.0,
|
|
172
|
+
transport: Optional[httpx.AsyncBaseTransport] = None,
|
|
173
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
174
|
+
) -> None:
|
|
175
|
+
self._portal = _Portal()
|
|
176
|
+
self._async = AsyncScalebrowserClient(
|
|
177
|
+
base_url, token, timeout=timeout, transport=transport, http_client=http_client
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def async_client(self) -> AsyncScalebrowserClient:
|
|
182
|
+
return self._async
|
|
183
|
+
|
|
184
|
+
def __repr__(self) -> str:
|
|
185
|
+
masked = "***" if self._async.transport.token else None
|
|
186
|
+
return f"ScalebrowserClient(base_url={self._async.transport.base_url!r}, token={masked!r})"
|
|
187
|
+
|
|
188
|
+
# ── profiles ──────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
def list_profiles(
|
|
191
|
+
self,
|
|
192
|
+
*,
|
|
193
|
+
group: Optional[str] = None,
|
|
194
|
+
state: Optional[str] = None,
|
|
195
|
+
q: Optional[str] = None,
|
|
196
|
+
limit: Optional[int] = None,
|
|
197
|
+
offset: Optional[int] = None,
|
|
198
|
+
) -> list[Profile]:
|
|
199
|
+
return self._portal.run(
|
|
200
|
+
self._async.list_profiles(group=group, state=state, q=q, limit=limit, offset=offset)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def get_profile(self, profile_id: str) -> Profile:
|
|
204
|
+
return self._portal.run(self._async.get_profile(profile_id))
|
|
205
|
+
|
|
206
|
+
def create_profile(self, body: Union[CreateProfileBody, Mapping[str, Any]]) -> Profile:
|
|
207
|
+
return self._portal.run(self._async.create_profile(body))
|
|
208
|
+
|
|
209
|
+
def update_profile(
|
|
210
|
+
self, profile_id: str, body: Union[UpdateProfileBody, Mapping[str, Any]]
|
|
211
|
+
) -> Profile:
|
|
212
|
+
return self._portal.run(self._async.update_profile(profile_id, body))
|
|
213
|
+
|
|
214
|
+
def delete_profile(self, profile_id: str) -> None:
|
|
215
|
+
self._portal.run(self._async.delete_profile(profile_id))
|
|
216
|
+
|
|
217
|
+
def start_profile(self, profile_id: str, *, headless: Optional[bool] = None) -> StartProfileResult:
|
|
218
|
+
return self._portal.run(self._async.start_profile(profile_id, headless=headless))
|
|
219
|
+
|
|
220
|
+
def stop_profile(self, profile_id: str) -> StopProfileResult:
|
|
221
|
+
return self._portal.run(self._async.stop_profile(profile_id))
|
|
222
|
+
|
|
223
|
+
def list_profile_ids(
|
|
224
|
+
self,
|
|
225
|
+
*,
|
|
226
|
+
group: Optional[str] = None,
|
|
227
|
+
state: Optional[str] = None,
|
|
228
|
+
q: Optional[str] = None,
|
|
229
|
+
sort: Optional[str] = None,
|
|
230
|
+
order: Optional[str] = None,
|
|
231
|
+
) -> dict[str, Any]:
|
|
232
|
+
"""Every id matching the filters, unpaged — what "act on all matches" needs."""
|
|
233
|
+
return self._portal.run(
|
|
234
|
+
self._async.list_profile_ids(group=group, state=state, q=q, sort=sort, order=order)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# ── bulk ──────────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
def bulk_create_profiles(
|
|
240
|
+
self,
|
|
241
|
+
preset_id: str,
|
|
242
|
+
count: int,
|
|
243
|
+
*,
|
|
244
|
+
name_prefix: Optional[str] = None,
|
|
245
|
+
group_id: Optional[str] = None,
|
|
246
|
+
) -> list[Profile]:
|
|
247
|
+
return self._portal.run(
|
|
248
|
+
self._async.bulk_create_profiles(
|
|
249
|
+
preset_id, count, name_prefix=name_prefix, group_id=group_id
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def bulk_start(self, ids: Sequence[str]) -> Any:
|
|
254
|
+
return self._portal.run(self._async.bulk_start(ids))
|
|
255
|
+
|
|
256
|
+
def bulk_stop(self, ids: Sequence[str]) -> Any:
|
|
257
|
+
return self._portal.run(self._async.bulk_stop(ids))
|
|
258
|
+
|
|
259
|
+
def bulk_delete(self, ids: Sequence[str]) -> Any:
|
|
260
|
+
return self._portal.run(self._async.bulk_delete(ids))
|
|
261
|
+
|
|
262
|
+
def bulk_assign_proxy(self, ids: Sequence[str], proxy_id: str) -> Any:
|
|
263
|
+
return self._portal.run(self._async.bulk_assign_proxy(ids, proxy_id))
|
|
264
|
+
|
|
265
|
+
def bulk_assign_extensions(self, ids: Sequence[str], ext_refs: Sequence[str]) -> Any:
|
|
266
|
+
return self._portal.run(self._async.bulk_assign_extensions(ids, ext_refs))
|
|
267
|
+
|
|
268
|
+
# ── groups ────────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
def list_groups(self) -> list[Group]:
|
|
271
|
+
return self._portal.run(self._async.list_groups())
|
|
272
|
+
|
|
273
|
+
def create_group(self, body: Union[CreateGroupBody, Mapping[str, Any]]) -> Group:
|
|
274
|
+
return self._portal.run(self._async.create_group(body))
|
|
275
|
+
|
|
276
|
+
def get_group(self, group_id: str) -> Group:
|
|
277
|
+
return self._portal.run(self._async.get_group(group_id))
|
|
278
|
+
|
|
279
|
+
def update_group(self, group_id: str, body: Mapping[str, Any]) -> Group:
|
|
280
|
+
return self._portal.run(self._async.update_group(group_id, body))
|
|
281
|
+
|
|
282
|
+
def delete_group(self, group_id: str) -> None:
|
|
283
|
+
self._portal.run(self._async.delete_group(group_id))
|
|
284
|
+
|
|
285
|
+
# ── presets ───────────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
def list_presets(self) -> list[Preset]:
|
|
288
|
+
return self._portal.run(self._async.list_presets())
|
|
289
|
+
|
|
290
|
+
def create_preset(self, body: Union[CreatePresetBody, Mapping[str, Any]]) -> Preset:
|
|
291
|
+
return self._portal.run(self._async.create_preset(body))
|
|
292
|
+
|
|
293
|
+
def get_preset(self, preset_id: str) -> Preset:
|
|
294
|
+
return self._portal.run(self._async.get_preset(preset_id))
|
|
295
|
+
|
|
296
|
+
def update_preset(
|
|
297
|
+
self, preset_id: str, body: Union[UpdatePresetBody, Mapping[str, Any]]
|
|
298
|
+
) -> Preset:
|
|
299
|
+
return self._portal.run(self._async.update_preset(preset_id, body))
|
|
300
|
+
|
|
301
|
+
def delete_preset(self, preset_id: str) -> None:
|
|
302
|
+
self._portal.run(self._async.delete_preset(preset_id))
|
|
303
|
+
|
|
304
|
+
def get_persona_constraints(self) -> PersonaConstraintOptions:
|
|
305
|
+
return self._portal.run(self._async.get_persona_constraints())
|
|
306
|
+
|
|
307
|
+
# ── proxies ───────────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
def list_proxies(self) -> list[Proxy]:
|
|
310
|
+
return self._portal.run(self._async.list_proxies())
|
|
311
|
+
|
|
312
|
+
def create_proxy(self, body: Union[CreateProxyBody, Mapping[str, Any]]) -> Proxy:
|
|
313
|
+
return self._portal.run(self._async.create_proxy(body))
|
|
314
|
+
|
|
315
|
+
def get_proxy(self, proxy_id: str) -> Proxy:
|
|
316
|
+
return self._portal.run(self._async.get_proxy(proxy_id))
|
|
317
|
+
|
|
318
|
+
def update_proxy(self, proxy_id: str, body: Union[UpdateProxyBody, Mapping[str, Any]]) -> Proxy:
|
|
319
|
+
return self._portal.run(self._async.update_proxy(proxy_id, body))
|
|
320
|
+
|
|
321
|
+
def delete_proxy(self, proxy_id: str) -> None:
|
|
322
|
+
self._portal.run(self._async.delete_proxy(proxy_id))
|
|
323
|
+
|
|
324
|
+
def check_proxy(self, proxy_id: str) -> ProxyCheckResult:
|
|
325
|
+
return self._portal.run(self._async.check_proxy(proxy_id))
|
|
326
|
+
|
|
327
|
+
def check_proxy_config(
|
|
328
|
+
self, body: Union[CheckProxyConfigBody, Mapping[str, Any]]
|
|
329
|
+
) -> ProxyCheckResult:
|
|
330
|
+
return self._portal.run(self._async.check_proxy_config(body))
|
|
331
|
+
|
|
332
|
+
# ── live detector audit ───────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
def start_audit(self, profile_id: str) -> AuditStatus:
|
|
335
|
+
return self._portal.run(self._async.start_audit(profile_id))
|
|
336
|
+
|
|
337
|
+
def get_audit(self, profile_id: str) -> AuditStatus:
|
|
338
|
+
return self._portal.run(self._async.get_audit(profile_id))
|
|
339
|
+
|
|
340
|
+
# ── extension library (daemon-wide) ───────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
def list_library_extensions(self) -> list[Extension]:
|
|
343
|
+
return self._portal.run(self._async.list_library_extensions())
|
|
344
|
+
|
|
345
|
+
def upload_extension(self, crx: bytes) -> Extension:
|
|
346
|
+
return self._portal.run(self._async.upload_extension(crx))
|
|
347
|
+
|
|
348
|
+
def get_library_extension(self, ext_id: str) -> Extension:
|
|
349
|
+
return self._portal.run(self._async.get_library_extension(ext_id))
|
|
350
|
+
|
|
351
|
+
def delete_library_extension(self, ext_id: str) -> None:
|
|
352
|
+
self._portal.run(self._async.delete_library_extension(ext_id))
|
|
353
|
+
|
|
354
|
+
def delete_library_extension_version(self, ext_id: str, version: str) -> None:
|
|
355
|
+
self._portal.run(self._async.delete_library_extension_version(ext_id, version))
|
|
356
|
+
|
|
357
|
+
# ── extensions per profile (ext_ref is a LIBRARY ID, not a path) ──────────
|
|
358
|
+
|
|
359
|
+
def list_extensions(self, profile_id: str) -> ExtensionsResult:
|
|
360
|
+
return self._portal.run(self._async.list_extensions(profile_id))
|
|
361
|
+
|
|
362
|
+
def attach_extension(self, profile_id: str, ext_ref: str) -> ExtensionsResult:
|
|
363
|
+
return self._portal.run(self._async.attach_extension(profile_id, ext_ref))
|
|
364
|
+
|
|
365
|
+
def detach_extension(self, profile_id: str, ext_ref: str) -> ExtensionsResult:
|
|
366
|
+
return self._portal.run(self._async.detach_extension(profile_id, ext_ref))
|
|
367
|
+
|
|
368
|
+
# ── platform credentials ──────────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
def list_credentials(self, profile_id: str) -> list[CredentialMeta]:
|
|
371
|
+
return self._portal.run(self._async.list_credentials(profile_id))
|
|
372
|
+
|
|
373
|
+
def put_credential(
|
|
374
|
+
self,
|
|
375
|
+
profile_id: str,
|
|
376
|
+
platform: str,
|
|
377
|
+
*,
|
|
378
|
+
username: Optional[str] = None,
|
|
379
|
+
password: Optional[str] = None,
|
|
380
|
+
totp_secret: Optional[str] = None,
|
|
381
|
+
login_url: Optional[str] = None,
|
|
382
|
+
) -> CredentialMeta:
|
|
383
|
+
return self._portal.run(
|
|
384
|
+
self._async.put_credential(
|
|
385
|
+
profile_id,
|
|
386
|
+
platform,
|
|
387
|
+
username=username,
|
|
388
|
+
password=password,
|
|
389
|
+
totp_secret=totp_secret,
|
|
390
|
+
login_url=login_url,
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
def delete_credential(self, profile_id: str, platform: str) -> None:
|
|
395
|
+
return self._portal.run(self._async.delete_credential(profile_id, platform))
|
|
396
|
+
|
|
397
|
+
def reveal_credential(
|
|
398
|
+
self, profile_id: str, platform: str, vault_password: str
|
|
399
|
+
) -> RevealedCredential:
|
|
400
|
+
return self._portal.run(
|
|
401
|
+
self._async.reveal_credential(profile_id, platform, vault_password)
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
def export_credentials(
|
|
405
|
+
self, profile_id: str, vault_password: str, password: str
|
|
406
|
+
) -> CredentialBundle:
|
|
407
|
+
return self._portal.run(
|
|
408
|
+
self._async.export_credentials(profile_id, vault_password, password)
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
def import_credentials(
|
|
412
|
+
self, profile_id: str, vault_password: str, password: str, bundle: str
|
|
413
|
+
) -> CredentialImportResult:
|
|
414
|
+
return self._portal.run(
|
|
415
|
+
self._async.import_credentials(profile_id, vault_password, password, bundle)
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
def vault_status(self) -> VaultStatus:
|
|
419
|
+
return self._portal.run(self._async.vault_status())
|
|
420
|
+
|
|
421
|
+
def set_vault_password(
|
|
422
|
+
self, new_password: str, current_password: Optional[str] = None
|
|
423
|
+
) -> None:
|
|
424
|
+
return self._portal.run(
|
|
425
|
+
self._async.set_vault_password(new_password, current_password)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# ── sessions ──────────────────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
def export_session(
|
|
431
|
+
self, profile_id: str, password: str, *, kinds: Optional[Sequence[str]] = None
|
|
432
|
+
) -> SessionExportResult:
|
|
433
|
+
return self._portal.run(self._async.export_session(profile_id, password, kinds=kinds))
|
|
434
|
+
|
|
435
|
+
def import_session(self, profile_id: str, password: str, bundle: str) -> Any:
|
|
436
|
+
return self._portal.run(self._async.import_session(profile_id, password, bundle))
|
|
437
|
+
|
|
438
|
+
# ── input / metrics ──────────────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
def send_input(self, profile_id: str, body: Mapping[str, Any]) -> Any:
|
|
441
|
+
return self._portal.run(self._async.send_input(profile_id, body))
|
|
442
|
+
|
|
443
|
+
def get_metrics(self) -> Metrics:
|
|
444
|
+
return self._portal.run(self._async.get_metrics())
|
|
445
|
+
|
|
446
|
+
# ── agent runs ────────────────────────────────────────────────────────────
|
|
447
|
+
|
|
448
|
+
def list_runs(
|
|
449
|
+
self,
|
|
450
|
+
*,
|
|
451
|
+
profile_id: Optional[str] = None,
|
|
452
|
+
limit: Optional[int] = None,
|
|
453
|
+
offset: Optional[int] = None,
|
|
454
|
+
) -> list[AgentRun]:
|
|
455
|
+
return self._portal.run(
|
|
456
|
+
self._async.list_runs(profile_id=profile_id, limit=limit, offset=offset)
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
def get_run(self, run_id: str) -> AgentRun:
|
|
460
|
+
return self._portal.run(self._async.get_run(run_id))
|
|
461
|
+
|
|
462
|
+
def list_run_steps(
|
|
463
|
+
self, run_id: str, *, limit: Optional[int] = None, offset: Optional[int] = None
|
|
464
|
+
) -> list[RunStep]:
|
|
465
|
+
return self._portal.run(self._async.list_run_steps(run_id, limit=limit, offset=offset))
|
|
466
|
+
|
|
467
|
+
def get_run_shot(self, run_id: str, seq: int) -> ArtifactBytes:
|
|
468
|
+
return self._portal.run(self._async.get_run_shot(run_id, seq))
|
|
469
|
+
|
|
470
|
+
def get_activity(self) -> ActivitySnapshot:
|
|
471
|
+
return self._portal.run(self._async.get_activity())
|
|
472
|
+
|
|
473
|
+
# ── mailboxes ─────────────────────────────────────────────────────────────
|
|
474
|
+
|
|
475
|
+
def list_inboxes(self) -> list[Inbox]:
|
|
476
|
+
return self._portal.run(self._async.list_inboxes())
|
|
477
|
+
|
|
478
|
+
def create_inbox(self, body: Union[PutInboxBody, Mapping[str, Any]]) -> Inbox:
|
|
479
|
+
return self._portal.run(self._async.create_inbox(body))
|
|
480
|
+
|
|
481
|
+
def update_inbox(self, inbox_id: str, body: Union[PutInboxBody, Mapping[str, Any]]) -> Inbox:
|
|
482
|
+
return self._portal.run(self._async.update_inbox(inbox_id, body))
|
|
483
|
+
|
|
484
|
+
def delete_inbox(self, inbox_id: str) -> None:
|
|
485
|
+
self._portal.run(self._async.delete_inbox(inbox_id))
|
|
486
|
+
|
|
487
|
+
def get_inbox_bindings(self, profile_id: str) -> InboxBindings:
|
|
488
|
+
return self._portal.run(self._async.get_inbox_bindings(profile_id))
|
|
489
|
+
|
|
490
|
+
def bind_inbox(
|
|
491
|
+
self, profile_id: str, body: Union[BindInboxBody, Mapping[str, Any]]
|
|
492
|
+
) -> InboxBindings:
|
|
493
|
+
return self._portal.run(self._async.bind_inbox(profile_id, body))
|
|
494
|
+
|
|
495
|
+
def unbind_inbox(self, profile_id: str, channel: str) -> None:
|
|
496
|
+
self._portal.run(self._async.unbind_inbox(profile_id, channel))
|
|
497
|
+
|
|
498
|
+
# ── passkeys ──────────────────────────────────────────────────────────────
|
|
499
|
+
|
|
500
|
+
def list_passkeys(self, profile_id: str) -> list[PasskeyRow]:
|
|
501
|
+
return self._portal.run(self._async.list_passkeys(profile_id))
|
|
502
|
+
|
|
503
|
+
def delete_passkey(self, profile_id: str, credential_id: str) -> None:
|
|
504
|
+
self._portal.run(self._async.delete_passkey(profile_id, credential_id))
|
|
505
|
+
|
|
506
|
+
# ── interruptions ─────────────────────────────────────────────────────────
|
|
507
|
+
|
|
508
|
+
def list_interruption_locks(self) -> InterruptionLockView:
|
|
509
|
+
return self._portal.run(self._async.list_interruption_locks())
|
|
510
|
+
|
|
511
|
+
def set_interruption_lock(
|
|
512
|
+
self, body: Union[SetInterruptionLockBody, Mapping[str, Any]]
|
|
513
|
+
) -> None:
|
|
514
|
+
self._portal.run(self._async.set_interruption_lock(body))
|
|
515
|
+
|
|
516
|
+
def list_interruption_rules(self) -> list[InterruptionRuleRow]:
|
|
517
|
+
return self._portal.run(self._async.list_interruption_rules())
|
|
518
|
+
|
|
519
|
+
def set_interruption_rule(
|
|
520
|
+
self, body: Union[SetInterruptionRuleBody, Mapping[str, Any]]
|
|
521
|
+
) -> None:
|
|
522
|
+
self._portal.run(self._async.set_interruption_rule(body))
|
|
523
|
+
|
|
524
|
+
def delete_interruption_rule(
|
|
525
|
+
self, origin: str, kind: str, profile_id: Optional[str] = None
|
|
526
|
+
) -> None:
|
|
527
|
+
self._portal.run(self._async.delete_interruption_rule(origin, kind, profile_id))
|
|
528
|
+
|
|
529
|
+
# ── artifacts ─────────────────────────────────────────────────────────────
|
|
530
|
+
|
|
531
|
+
def put_artifact(
|
|
532
|
+
self, profile_id: str, data: bytes, name: Optional[str] = None
|
|
533
|
+
) -> ArtifactPutResult:
|
|
534
|
+
return self._portal.run(self._async.put_artifact(profile_id, data, name))
|
|
535
|
+
|
|
536
|
+
def get_artifact(self, artifact_id: str) -> ArtifactBytes:
|
|
537
|
+
return self._portal.run(self._async.get_artifact(artifact_id))
|
|
538
|
+
|
|
539
|
+
# ── cookies ───────────────────────────────────────────────────────────────
|
|
540
|
+
|
|
541
|
+
def reveal_cookies(
|
|
542
|
+
self, profile_id: str, body: Union[RevealCookiesBody, Mapping[str, Any]]
|
|
543
|
+
) -> RevealCookiesResult:
|
|
544
|
+
return self._portal.run(self._async.reveal_cookies(profile_id, body))
|
|
545
|
+
|
|
546
|
+
# ── account + health ──────────────────────────────────────────────────────
|
|
547
|
+
|
|
548
|
+
def get_account(self) -> Account:
|
|
549
|
+
return self._portal.run(self._async.get_account())
|
|
550
|
+
|
|
551
|
+
def health(self) -> HealthStatus:
|
|
552
|
+
return self._portal.run(self._async.health())
|
|
553
|
+
|
|
554
|
+
def ready(self) -> ReadyStatus:
|
|
555
|
+
return self._portal.run(self._async.ready())
|
|
556
|
+
|
|
557
|
+
# ── events ────────────────────────────────────────────────────────────────
|
|
558
|
+
|
|
559
|
+
def iter_events(self) -> Iterator[Any]:
|
|
560
|
+
"""Yield lifecycle events synchronously (blocks the caller thread)."""
|
|
561
|
+
agen = self._async.events()
|
|
562
|
+
try:
|
|
563
|
+
while True:
|
|
564
|
+
yield self._portal.run(agen.__anext__())
|
|
565
|
+
except StopAsyncIteration:
|
|
566
|
+
return
|
|
567
|
+
|
|
568
|
+
# ── direct-CDP ────────────────────────────────────────────────────────────
|
|
569
|
+
|
|
570
|
+
def connect_cdp(
|
|
571
|
+
self,
|
|
572
|
+
target: Union[StartProfileResult, str],
|
|
573
|
+
profile_id: Optional[str] = None,
|
|
574
|
+
*,
|
|
575
|
+
attach: bool = True,
|
|
576
|
+
) -> SyncCdpSession:
|
|
577
|
+
inner = self._portal.run(self._async.connect_cdp(target, profile_id, attach=attach))
|
|
578
|
+
return SyncCdpSession(inner, self._portal)
|
|
579
|
+
|
|
580
|
+
@contextmanager
|
|
581
|
+
def launch(self, profile_id: str, *, headless: bool = True) -> Iterator[SyncCdpSession]:
|
|
582
|
+
result = self.start_profile(profile_id, headless=headless)
|
|
583
|
+
cdp = self.connect_cdp(result, profile_id=profile_id)
|
|
584
|
+
try:
|
|
585
|
+
yield cdp
|
|
586
|
+
finally:
|
|
587
|
+
try:
|
|
588
|
+
cdp.close()
|
|
589
|
+
finally:
|
|
590
|
+
self.stop_profile(profile_id)
|
|
591
|
+
|
|
592
|
+
# ── lifecycle ─────────────────────────────────────────────────────────────
|
|
593
|
+
|
|
594
|
+
def close(self) -> None:
|
|
595
|
+
try:
|
|
596
|
+
self._portal.run(self._async.aclose())
|
|
597
|
+
finally:
|
|
598
|
+
self._portal.close()
|
|
599
|
+
|
|
600
|
+
def __enter__(self) -> "ScalebrowserClient":
|
|
601
|
+
return self
|
|
602
|
+
|
|
603
|
+
def __exit__(self, *exc: object) -> None:
|
|
604
|
+
self.close()
|
scalebrowser/_version.py
ADDED