slybrowser 0.1.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.
slybrowser/__init__.py ADDED
@@ -0,0 +1,124 @@
1
+ """Public Python API for SlyBrowser."""
2
+
3
+ from .errors import (
4
+ ArtifactError,
5
+ ConfigurationError,
6
+ LicenseError,
7
+ LicenseServiceError,
8
+ LICENSE_SERVICE_ERROR_CODES,
9
+ is_license_service_error_code,
10
+ ManifestError,
11
+ SlyBrowserError,
12
+ WebDriverError,
13
+ )
14
+ from .browser import (
15
+ launch_playwright,
16
+ launch_playwright_async,
17
+ launch_playwright_persistent,
18
+ launch_playwright_persistent_async,
19
+ )
20
+ from .automation import AutomationBackend, AutomationCapability, automation_capability, validate_playwright_version
21
+ from .launcher import LaunchPlan, prepare_launch
22
+ from .webdriver import (
23
+ HumanizeConfig,
24
+ SlyWebDriverElement,
25
+ SlyWebDriverService,
26
+ SlyWebDriverSession,
27
+ WebDriverVersions,
28
+ launch,
29
+ launch_webdriver,
30
+ )
31
+ from .license import LicenseClaims, LicenseVerifier
32
+ from .manifest import ReleaseManifest, verify_release_manifest
33
+ from .installer import BrowserInstallation, install_granted_browser
34
+ from .licensed import (
35
+ install_authorized,
36
+ install_latest,
37
+ launch_authorized,
38
+ launch_authorized_playwright,
39
+ launch_authorized_playwright_async,
40
+ launch_authorized_playwright_persistent,
41
+ launch_authorized_playwright_persistent_async,
42
+ launch_latest,
43
+ launch_latest_playwright,
44
+ launch_latest_playwright_async,
45
+ launch_latest_playwright_persistent,
46
+ launch_latest_playwright_persistent_async,
47
+ prepare_authorized_browser,
48
+ prepare_latest_authorized_browser,
49
+ )
50
+ from .service import (
51
+ LicenseAuthorization,
52
+ LicenseInfo,
53
+ LicenseServiceClient,
54
+ LicensedSessionGrant,
55
+ RuntimeActivationGrant,
56
+ RuntimeDownloadTicket,
57
+ RuntimeHeartbeatGrant,
58
+ RuntimeSessionGrant,
59
+ SealedLicenseImportResult,
60
+ import_license_file_to_sealed_authorization,
61
+ read_license_authorization,
62
+ )
63
+
64
+ __all__ = [
65
+ "ArtifactError",
66
+ "AutomationBackend",
67
+ "AutomationCapability",
68
+ "ConfigurationError",
69
+ "LaunchPlan",
70
+ "LicenseClaims",
71
+ "LicenseError",
72
+ "LicenseServiceError",
73
+ "LICENSE_SERVICE_ERROR_CODES",
74
+ "is_license_service_error_code",
75
+ "LicenseVerifier",
76
+ "ManifestError",
77
+ "ReleaseManifest",
78
+ "SlyBrowserError",
79
+ "WebDriverError",
80
+ "HumanizeConfig",
81
+ "SlyWebDriverElement",
82
+ "SlyWebDriverService",
83
+ "SlyWebDriverSession",
84
+ "WebDriverVersions",
85
+ "launch",
86
+ "launch_webdriver",
87
+ "launch_playwright",
88
+ "launch_playwright_async",
89
+ "launch_playwright_persistent",
90
+ "launch_playwright_persistent_async",
91
+ "automation_capability",
92
+ "validate_playwright_version",
93
+ "prepare_launch",
94
+ "verify_release_manifest",
95
+ "BrowserInstallation",
96
+ "LicenseAuthorization",
97
+ "LicenseInfo",
98
+ "LicenseServiceClient",
99
+ "LicensedSessionGrant",
100
+ "RuntimeActivationGrant",
101
+ "RuntimeDownloadTicket",
102
+ "RuntimeHeartbeatGrant",
103
+ "RuntimeSessionGrant",
104
+ "SealedLicenseImportResult",
105
+ "install_granted_browser",
106
+ "install_authorized",
107
+ "install_latest",
108
+ "launch_authorized",
109
+ "launch_authorized_playwright",
110
+ "launch_authorized_playwright_async",
111
+ "launch_authorized_playwright_persistent",
112
+ "launch_authorized_playwright_persistent_async",
113
+ "launch_latest",
114
+ "launch_latest_playwright",
115
+ "launch_latest_playwright_async",
116
+ "launch_latest_playwright_persistent",
117
+ "launch_latest_playwright_persistent_async",
118
+ "prepare_authorized_browser",
119
+ "prepare_latest_authorized_browser",
120
+ "import_license_file_to_sealed_authorization",
121
+ "read_license_authorization",
122
+ ]
123
+
124
+ __version__ = "0.1.0"
slybrowser/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,86 @@
1
+ """Automation backend selection and framework binding compatibility."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import metadata
6
+ from typing import Literal, TypedDict
7
+
8
+ from .errors import ConfigurationError
9
+
10
+ AutomationBackend = Literal["project-webdriver", "playwright"]
11
+
12
+
13
+ class AutomationCapability(TypedDict, total=False):
14
+ backend: AutomationBackend
15
+ language: Literal["python"]
16
+ framework_version: str
17
+ native_humanize: bool
18
+ persistent_context: bool
19
+
20
+
21
+ _SUPPORTED_PLAYWRIGHT_LINES = frozenset({"1.62"})
22
+
23
+
24
+ def validate_playwright_version(version: str) -> str:
25
+ parts = version.split("+", 1)[0].split("-", 1)[0].split(".")
26
+ if len(parts) < 2 or not all(part.isdigit() for part in parts):
27
+ raise ConfigurationError(
28
+ f"Invalid Playwright version: {version}",
29
+ code="framework_version_invalid",
30
+ )
31
+ line = ".".join(parts[:2])
32
+ if line not in _SUPPORTED_PLAYWRIGHT_LINES:
33
+ supported = ", ".join(sorted(_SUPPORTED_PLAYWRIGHT_LINES))
34
+ raise ConfigurationError(
35
+ f"Unsupported Playwright version {version}; supported lines: {supported}",
36
+ code="framework_version_unsupported",
37
+ )
38
+ return version
39
+
40
+
41
+ def resolve_playwright_version(explicit: str | None = None) -> str:
42
+ if explicit is not None:
43
+ return validate_playwright_version(explicit)
44
+ try:
45
+ return validate_playwright_version(metadata.version("playwright"))
46
+ except metadata.PackageNotFoundError as error:
47
+ raise ConfigurationError(
48
+ "Unable to determine the installed Playwright package version; "
49
+ "pass framework_version explicitly",
50
+ code="framework_version_missing",
51
+ ) from error
52
+
53
+
54
+ def require_playwright_humanize_support(requested: bool) -> None:
55
+ _ = requested
56
+
57
+
58
+ def automation_capability(
59
+ backend: AutomationBackend = "project-webdriver",
60
+ *,
61
+ framework_version: str | None = None,
62
+ ) -> AutomationCapability:
63
+ if backend == "project-webdriver":
64
+ if framework_version is not None:
65
+ raise ConfigurationError(
66
+ "Project WebDriver does not accept framework_version",
67
+ code="framework_version_forbidden",
68
+ )
69
+ return {
70
+ "backend": backend,
71
+ "language": "python",
72
+ "native_humanize": True,
73
+ "persistent_context": True,
74
+ }
75
+ if backend != "playwright":
76
+ raise ConfigurationError(
77
+ f"Unsupported Python automation backend: {backend}",
78
+ code="automation_backend_unsupported",
79
+ )
80
+ return {
81
+ "backend": backend,
82
+ "language": "python",
83
+ "framework_version": resolve_playwright_version(framework_version),
84
+ "native_humanize": True,
85
+ "persistent_context": True,
86
+ }
slybrowser/browser.py ADDED
@@ -0,0 +1,306 @@
1
+ """Framework adapters that launch the native SlyBrowser executable."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any, Mapping
7
+
8
+ from .automation import require_playwright_humanize_support, resolve_playwright_version
9
+ from .errors import ConfigurationError
10
+ from .launcher import LaunchPlan, prepare_launch, wait_for_native_ready
11
+ from .webdriver import _release_root_from_lease, resolve_humanize_config
12
+
13
+
14
+ def _launch_arguments(
15
+ launch_options: Mapping[str, Any] | None,
16
+ handoff_arguments: tuple[str, ...],
17
+ ) -> dict[str, Any]:
18
+ result = dict(launch_options or {})
19
+ if "executable_path" in result or "executablePath" in result:
20
+ raise ConfigurationError(
21
+ "The browser executable must be passed to the SlyBrowser adapter",
22
+ code="executable_option_conflict",
23
+ )
24
+ existing_arguments = result.get("args", [])
25
+ if not isinstance(existing_arguments, (list, tuple)) or any(
26
+ not isinstance(argument, str) for argument in existing_arguments
27
+ ):
28
+ raise ConfigurationError("Playwright args must be a string array", code="config_invalid")
29
+ result["args"] = [*handoff_arguments, *existing_arguments]
30
+ return result
31
+
32
+
33
+ def _camel_humanize_config(config: object) -> dict[str, int]:
34
+ return {
35
+ "mouseStepsMin": config.mouse_steps_min,
36
+ "mouseStepsMax": config.mouse_steps_max,
37
+ "mouseStepDelayMin": config.mouse_step_delay_min,
38
+ "mouseStepDelayMax": config.mouse_step_delay_max,
39
+ "clickHoldMin": config.click_hold_min,
40
+ "clickHoldMax": config.click_hold_max,
41
+ "keyDelayMin": config.key_delay_min,
42
+ "keyDelayMax": config.key_delay_max,
43
+ "thinkDelayMin": config.think_delay_min,
44
+ "thinkDelayMax": config.think_delay_max,
45
+ }
46
+
47
+
48
+ def _native_humanize_control(
49
+ *,
50
+ humanize: bool,
51
+ human_preset: str,
52
+ human_config: Mapping[str, int | float] | None,
53
+ human_seed: int | None,
54
+ ) -> dict[str, Any] | None:
55
+ if not humanize:
56
+ return None
57
+ if human_seed is not None and (not isinstance(human_seed, int) or human_seed < 0):
58
+ raise ConfigurationError("Native Humanize seed must be a non-negative integer", code="humanize_seed_invalid")
59
+ return {
60
+ "schemaVersion": 1,
61
+ "kind": "slybrowser.native-humanize-control",
62
+ "backend": "playwright",
63
+ "humanize": {
64
+ "enabled": True,
65
+ "version": 1,
66
+ "preset": human_preset,
67
+ "config": _camel_humanize_config(resolve_humanize_config(human_preset, human_config)),
68
+ **({} if human_seed is None else {"seed": human_seed}),
69
+ },
70
+ }
71
+
72
+
73
+ def _validate_native_ready(native_ready: bool, native_ready_timeout: float) -> None:
74
+ if native_ready and (not isinstance(native_ready_timeout, (int, float)) or native_ready_timeout <= 0):
75
+ raise ConfigurationError("native_ready_timeout must be positive", code="config_invalid")
76
+
77
+
78
+ def _close_runtime(runtime: Any) -> None:
79
+ close = getattr(runtime, "close", None)
80
+ if callable(close):
81
+ try:
82
+ close()
83
+ except Exception:
84
+ pass
85
+
86
+
87
+ async def _close_runtime_async(runtime: Any) -> None:
88
+ import inspect
89
+
90
+ close = getattr(runtime, "close", None)
91
+ if callable(close):
92
+ try:
93
+ result = close()
94
+ if inspect.isawaitable(result):
95
+ await result
96
+ except Exception:
97
+ pass
98
+
99
+
100
+ def _wait_if_requested(plan: LaunchPlan, native_ready: bool, native_ready_timeout: float) -> None:
101
+ if native_ready:
102
+ wait_for_native_ready(plan, timeout=native_ready_timeout)
103
+
104
+
105
+ def launch_playwright(
106
+ playwright: Any,
107
+ executable: str | Path,
108
+ lease: str | bytes | Mapping[str, Any],
109
+ *,
110
+ profile: Mapping[str, Any] | None = None,
111
+ launch_options: Mapping[str, Any] | None = None,
112
+ temp_root: str | Path | None = None,
113
+ framework_version: str | None = None,
114
+ humanize: bool = False,
115
+ human_preset: str = "default",
116
+ human_config: Mapping[str, int | float] | None = None,
117
+ human_seed: int | None = None,
118
+ runtime_handoff: Mapping[str, Any] | None = None,
119
+ allow_runtime_activation_ticket: bool = False,
120
+ release_root: str | Path | None = None,
121
+ native_ready: bool = False,
122
+ native_ready_timeout: float = 15.0,
123
+ ) -> Any:
124
+ """Launch a non-persistent Playwright browser using an existing Playwright object."""
125
+
126
+ resolve_playwright_version(framework_version)
127
+ require_playwright_humanize_support(humanize)
128
+ _validate_native_ready(native_ready, native_ready_timeout)
129
+ with prepare_launch(
130
+ executable,
131
+ profile or {},
132
+ lease,
133
+ temp_root=temp_root,
134
+ runtime_handoff=runtime_handoff,
135
+ allow_runtime_activation_ticket=allow_runtime_activation_ticket,
136
+ release_root=Path(release_root).expanduser().resolve() if release_root is not None else _release_root_from_lease(executable, lease),
137
+ native_ready=native_ready,
138
+ humanize_control=_native_humanize_control(
139
+ humanize=humanize,
140
+ human_preset=human_preset,
141
+ human_config=human_config,
142
+ human_seed=human_seed,
143
+ ),
144
+ ) as plan:
145
+ options = _launch_arguments(launch_options, plan.arguments)
146
+ runtime = playwright.chromium.launch(executable_path=str(plan.executable), **options)
147
+ try:
148
+ _wait_if_requested(plan, native_ready, native_ready_timeout)
149
+ return runtime
150
+ except BaseException:
151
+ _close_runtime(runtime)
152
+ raise
153
+
154
+
155
+ async def launch_playwright_async(
156
+ playwright: Any,
157
+ executable: str | Path,
158
+ lease: str | bytes | Mapping[str, Any],
159
+ *,
160
+ profile: Mapping[str, Any] | None = None,
161
+ launch_options: Mapping[str, Any] | None = None,
162
+ temp_root: str | Path | None = None,
163
+ framework_version: str | None = None,
164
+ humanize: bool = False,
165
+ human_preset: str = "default",
166
+ human_config: Mapping[str, int | float] | None = None,
167
+ human_seed: int | None = None,
168
+ runtime_handoff: Mapping[str, Any] | None = None,
169
+ allow_runtime_activation_ticket: bool = False,
170
+ release_root: str | Path | None = None,
171
+ native_ready: bool = False,
172
+ native_ready_timeout: float = 15.0,
173
+ ) -> Any:
174
+ resolve_playwright_version(framework_version)
175
+ require_playwright_humanize_support(humanize)
176
+ _validate_native_ready(native_ready, native_ready_timeout)
177
+ with prepare_launch(
178
+ executable,
179
+ profile or {},
180
+ lease,
181
+ temp_root=temp_root,
182
+ runtime_handoff=runtime_handoff,
183
+ allow_runtime_activation_ticket=allow_runtime_activation_ticket,
184
+ release_root=Path(release_root).expanduser().resolve() if release_root is not None else _release_root_from_lease(executable, lease),
185
+ native_ready=native_ready,
186
+ humanize_control=_native_humanize_control(
187
+ humanize=humanize,
188
+ human_preset=human_preset,
189
+ human_config=human_config,
190
+ human_seed=human_seed,
191
+ ),
192
+ ) as plan:
193
+ options = _launch_arguments(launch_options, plan.arguments)
194
+ runtime = await playwright.chromium.launch(executable_path=str(plan.executable), **options)
195
+ try:
196
+ _wait_if_requested(plan, native_ready, native_ready_timeout)
197
+ return runtime
198
+ except BaseException:
199
+ await _close_runtime_async(runtime)
200
+ raise
201
+
202
+
203
+ def launch_playwright_persistent(
204
+ playwright: Any,
205
+ user_data_dir: str | Path,
206
+ executable: str | Path,
207
+ lease: str | bytes | Mapping[str, Any],
208
+ *,
209
+ profile: Mapping[str, Any] | None = None,
210
+ launch_options: Mapping[str, Any] | None = None,
211
+ temp_root: str | Path | None = None,
212
+ framework_version: str | None = None,
213
+ humanize: bool = False,
214
+ human_preset: str = "default",
215
+ human_config: Mapping[str, int | float] | None = None,
216
+ human_seed: int | None = None,
217
+ runtime_handoff: Mapping[str, Any] | None = None,
218
+ allow_runtime_activation_ticket: bool = False,
219
+ release_root: str | Path | None = None,
220
+ native_ready: bool = False,
221
+ native_ready_timeout: float = 15.0,
222
+ ) -> Any:
223
+ resolve_playwright_version(framework_version)
224
+ require_playwright_humanize_support(humanize)
225
+ _validate_native_ready(native_ready, native_ready_timeout)
226
+ with prepare_launch(
227
+ executable,
228
+ profile or {},
229
+ lease,
230
+ temp_root=temp_root,
231
+ runtime_handoff=runtime_handoff,
232
+ allow_runtime_activation_ticket=allow_runtime_activation_ticket,
233
+ release_root=Path(release_root).expanduser().resolve() if release_root is not None else _release_root_from_lease(executable, lease),
234
+ native_ready=native_ready,
235
+ humanize_control=_native_humanize_control(
236
+ humanize=humanize,
237
+ human_preset=human_preset,
238
+ human_config=human_config,
239
+ human_seed=human_seed,
240
+ ),
241
+ ) as plan:
242
+ options = _launch_arguments(launch_options, plan.arguments)
243
+ runtime = playwright.chromium.launch_persistent_context(
244
+ str(Path(user_data_dir).expanduser().resolve()),
245
+ executable_path=str(plan.executable),
246
+ **options,
247
+ )
248
+ try:
249
+ _wait_if_requested(plan, native_ready, native_ready_timeout)
250
+ return runtime
251
+ except BaseException:
252
+ _close_runtime(runtime)
253
+ raise
254
+
255
+
256
+ async def launch_playwright_persistent_async(
257
+ playwright: Any,
258
+ user_data_dir: str | Path,
259
+ executable: str | Path,
260
+ lease: str | bytes | Mapping[str, Any],
261
+ *,
262
+ profile: Mapping[str, Any] | None = None,
263
+ launch_options: Mapping[str, Any] | None = None,
264
+ temp_root: str | Path | None = None,
265
+ framework_version: str | None = None,
266
+ humanize: bool = False,
267
+ human_preset: str = "default",
268
+ human_config: Mapping[str, int | float] | None = None,
269
+ human_seed: int | None = None,
270
+ runtime_handoff: Mapping[str, Any] | None = None,
271
+ allow_runtime_activation_ticket: bool = False,
272
+ release_root: str | Path | None = None,
273
+ native_ready: bool = False,
274
+ native_ready_timeout: float = 15.0,
275
+ ) -> Any:
276
+ resolve_playwright_version(framework_version)
277
+ require_playwright_humanize_support(humanize)
278
+ _validate_native_ready(native_ready, native_ready_timeout)
279
+ with prepare_launch(
280
+ executable,
281
+ profile or {},
282
+ lease,
283
+ temp_root=temp_root,
284
+ runtime_handoff=runtime_handoff,
285
+ allow_runtime_activation_ticket=allow_runtime_activation_ticket,
286
+ release_root=Path(release_root).expanduser().resolve() if release_root is not None else _release_root_from_lease(executable, lease),
287
+ native_ready=native_ready,
288
+ humanize_control=_native_humanize_control(
289
+ humanize=humanize,
290
+ human_preset=human_preset,
291
+ human_config=human_config,
292
+ human_seed=human_seed,
293
+ ),
294
+ ) as plan:
295
+ options = _launch_arguments(launch_options, plan.arguments)
296
+ runtime = await playwright.chromium.launch_persistent_context(
297
+ str(Path(user_data_dir).expanduser().resolve()),
298
+ executable_path=str(plan.executable),
299
+ **options,
300
+ )
301
+ try:
302
+ _wait_if_requested(plan, native_ready, native_ready_timeout)
303
+ return runtime
304
+ except BaseException:
305
+ await _close_runtime_async(runtime)
306
+ raise
@@ -0,0 +1,41 @@
1
+ """Canonical JSON and bounded base64url helpers shared by signed documents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import json
7
+ import re
8
+ from typing import Any
9
+
10
+ _BASE64URL_RE = re.compile(r"^[A-Za-z0-9_-]+$")
11
+
12
+
13
+ def canonical_json(value: Any) -> bytes:
14
+ """Serialize JSON deterministically for signature generation and verification."""
15
+
16
+ return json.dumps(
17
+ value,
18
+ ensure_ascii=False,
19
+ allow_nan=False,
20
+ separators=(",", ":"),
21
+ sort_keys=True,
22
+ ).encode("utf-8")
23
+
24
+
25
+ def decode_base64url(value: str, *, max_bytes: int) -> bytes:
26
+ if not isinstance(value, str) or not value or not _BASE64URL_RE.fullmatch(value):
27
+ raise ValueError("value is not unpadded base64url")
28
+ if len(value) > ((max_bytes + 2) // 3) * 4:
29
+ raise ValueError("decoded value exceeds size limit")
30
+ padding = "=" * (-len(value) % 4)
31
+ try:
32
+ decoded = base64.urlsafe_b64decode(value + padding)
33
+ except (ValueError, TypeError) as exc:
34
+ raise ValueError("invalid base64url value") from exc
35
+ if len(decoded) > max_bytes:
36
+ raise ValueError("decoded value exceeds size limit")
37
+ return decoded
38
+
39
+
40
+ def encode_base64url(value: bytes) -> str:
41
+ return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")