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 +124 -0
- slybrowser/__main__.py +3 -0
- slybrowser/automation.py +86 -0
- slybrowser/browser.py +306 -0
- slybrowser/canonical.py +41 -0
- slybrowser/cli.py +233 -0
- slybrowser/errors.py +138 -0
- slybrowser/installer.py +647 -0
- slybrowser/launcher.py +350 -0
- slybrowser/license.py +373 -0
- slybrowser/licensed.py +993 -0
- slybrowser/manifest.py +379 -0
- slybrowser/service.py +1567 -0
- slybrowser/webdriver.py +931 -0
- slybrowser-0.1.0.dist-info/METADATA +154 -0
- slybrowser-0.1.0.dist-info/RECORD +18 -0
- slybrowser-0.1.0.dist-info/WHEEL +4 -0
- slybrowser-0.1.0.dist-info/entry_points.txt +2 -0
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
slybrowser/automation.py
ADDED
|
@@ -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
|
slybrowser/canonical.py
ADDED
|
@@ -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")
|