browserwright 0.6.2__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.
- browserwright/__init__.py +33 -0
- browserwright/__main__.py +6 -0
- browserwright/_executor/__init__.py +47 -0
- browserwright/_executor/__main__.py +9 -0
- browserwright/_executor/client.py +127 -0
- browserwright/_executor/process.py +652 -0
- browserwright/_executor/protocol.py +152 -0
- browserwright/api.py +66 -0
- browserwright/cdp.py +285 -0
- browserwright/cli.py +741 -0
- browserwright/daemon/__init__.py +8 -0
- browserwright/daemon/_ipc.py +444 -0
- browserwright/daemon/active_tab.py +183 -0
- browserwright/daemon/auth.py +395 -0
- browserwright/daemon/backends/__init__.py +59 -0
- browserwright/daemon/backends/base.py +120 -0
- browserwright/daemon/backends/cloud.py +222 -0
- browserwright/daemon/backends/env.py +119 -0
- browserwright/daemon/backends/extension.py +185 -0
- browserwright/daemon/backends/rdp.py +214 -0
- browserwright/daemon/cli.py +1437 -0
- browserwright/daemon/config.py +380 -0
- browserwright/daemon/doctor.py +179 -0
- browserwright/daemon/errors.py +34 -0
- browserwright/daemon/launch_chrome.py +353 -0
- browserwright/daemon/observability.py +181 -0
- browserwright/daemon/platforms.py +234 -0
- browserwright/daemon/resolver.py +72 -0
- browserwright/daemon/server/__init__.py +6 -0
- browserwright/daemon/server/daemon.py +229 -0
- browserwright/daemon/server/executor_registry.py +434 -0
- browserwright/daemon/server/extension_upstream.py +677 -0
- browserwright/daemon/server/facade.py +375 -0
- browserwright/daemon/server/facade_extension.py +969 -0
- browserwright/daemon/server/listener.py +1058 -0
- browserwright/daemon/server/proxy.py +1991 -0
- browserwright/daemon/server/relay.py +783 -0
- browserwright/daemon/server/state.py +432 -0
- browserwright/daemon/server/upstream.py +266 -0
- browserwright/daemon/userscripts.py +150 -0
- browserwright/discovery.py +213 -0
- browserwright/errors.py +177 -0
- browserwright/health.py +169 -0
- browserwright/install.py +628 -0
- browserwright/memory/__init__.py +15 -0
- browserwright/memory/_md.py +120 -0
- browserwright/memory/_yaml.py +217 -0
- browserwright/memory/global_mem.py +201 -0
- browserwright/memory/repl_mem.py +28 -0
- browserwright/memory/session_decisions.py +53 -0
- browserwright/memory/site_mem.py +381 -0
- browserwright/mode_b_client.py +590 -0
- browserwright/multitask.py +131 -0
- browserwright/output_schema.py +99 -0
- browserwright/primitives/__init__.py +67 -0
- browserwright/primitives/discovery_api.py +79 -0
- browserwright/primitives/http.py +42 -0
- browserwright/primitives/inspect.py +876 -0
- browserwright/primitives/interact.py +518 -0
- browserwright/primitives/page.py +556 -0
- browserwright/primitives/site.py +143 -0
- browserwright/release_install.py +466 -0
- browserwright/repl/__init__.py +6 -0
- browserwright/repl/_namespace.py +106 -0
- browserwright/repl/_smart_goto.py +236 -0
- browserwright/repl/inline.py +180 -0
- browserwright/repl/playwright_handle.py +449 -0
- browserwright/repl/snapshot.py +150 -0
- browserwright/session.py +229 -0
- browserwright/session_create.py +252 -0
- browserwright/session_ctx.py +24 -0
- browserwright/session_registry.py +133 -0
- browserwright/session_runtime.py +133 -0
- browserwright/site_skills_starter/github.com/SKILL.md +14 -0
- browserwright/site_skills_starter/github.com/memory.md +29 -0
- browserwright/site_skills_starter/github.com/tasks/list_issues.py +55 -0
- browserwright/site_skills_starter/google.com/SKILL.md +16 -0
- browserwright/site_skills_starter/google.com/memory.md +27 -0
- browserwright/site_skills_starter/google.com/tasks/search.py +53 -0
- browserwright/site_skills_starter/producthunt.com/SKILL.md +7 -0
- browserwright/site_skills_starter/producthunt.com/memory.md +26 -0
- browserwright/site_skills_starter/producthunt.com/tasks/today.py +64 -0
- browserwright/site_skills_starter/wikipedia.org/SKILL.md +7 -0
- browserwright/site_skills_starter/wikipedia.org/memory.md +22 -0
- browserwright/site_skills_starter/wikipedia.org/tasks/lookup.py +55 -0
- browserwright/site_skills_starter/ycombinator.com/SKILL.md +8 -0
- browserwright/site_skills_starter/ycombinator.com/memory.md +25 -0
- browserwright/site_skills_starter/ycombinator.com/tasks/front_page.py +63 -0
- browserwright/skill_doc.py +140 -0
- browserwright/skill_runtime.md +194 -0
- browserwright/subscriptions.py +213 -0
- browserwright/task_runner.py +125 -0
- browserwright/version.py +117 -0
- browserwright-0.6.2.dist-info/METADATA +12 -0
- browserwright-0.6.2.dist-info/RECORD +98 -0
- browserwright-0.6.2.dist-info/WHEEL +5 -0
- browserwright-0.6.2.dist-info/entry_points.txt +3 -0
- browserwright-0.6.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""browserwright — Layer 2 of the browser stack.
|
|
2
|
+
|
|
3
|
+
Public surface: ``browserwright.cli:main`` is the CLI entry point.
|
|
4
|
+
|
|
5
|
+
For programmatic use inside REPL/task scripts, import names from the top-level
|
|
6
|
+
``browserwright`` namespace (everything from ``browserwright.api`` is re-exported
|
|
7
|
+
here)::
|
|
8
|
+
|
|
9
|
+
from browserwright import http_get, remember, run_task
|
|
10
|
+
|
|
11
|
+
Browser driving itself is done with real Playwright in inline ``-s/-e`` calls
|
|
12
|
+
via the injected ``page`` / ``context`` (and ``snapshot()``) — see
|
|
13
|
+
``repl/_namespace.build_globals``. Those are NOT importable from this module;
|
|
14
|
+
they are bound per call to the session's current tab.
|
|
15
|
+
"""
|
|
16
|
+
from .version import __version__ # noqa: F401
|
|
17
|
+
|
|
18
|
+
# Re-export the primitive namespace assembled in api.py so user scripts can
|
|
19
|
+
# `from browserwright import *`. The REPL/inline/task entry points use the same
|
|
20
|
+
# helper to populate their exec globals.
|
|
21
|
+
from .api import EXPORTS # noqa: F401
|
|
22
|
+
from .api import * # noqa: F401,F403
|
|
23
|
+
from .errors import ( # noqa: F401
|
|
24
|
+
AuthWall,
|
|
25
|
+
BrowserwrightError,
|
|
26
|
+
Captcha,
|
|
27
|
+
CDPError,
|
|
28
|
+
DaemonUnavailable,
|
|
29
|
+
ElementNotFound,
|
|
30
|
+
NeedsUserConfirm,
|
|
31
|
+
NetworkError,
|
|
32
|
+
PageLoadFailed,
|
|
33
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Phase B: the persistent per-session executor.
|
|
2
|
+
|
|
3
|
+
A resident, per-session **sync** subprocess (``python -m browserwright._executor
|
|
4
|
+
--session <id>``) that holds live Playwright ``page`` / ``context`` / ``browser``
|
|
5
|
+
+ a persistent ``state`` dict + one long-lived facade ``connect_over_cdp``
|
|
6
|
+
connection for its whole lifetime. The ``browserwright -s <id> -e <code>`` CLI
|
|
7
|
+
is a **thin client**: it ships the code body to the session's executor,
|
|
8
|
+
which runs it in a namespace where ``page`` / ``context`` / ``state`` are the
|
|
9
|
+
LIVE persistent objects, and returns the result.
|
|
10
|
+
|
|
11
|
+
Why a separate subprocess (not a thread in the asyncio daemon):
|
|
12
|
+
- sync Playwright is thread-affine and can't run on the daemon's event loop;
|
|
13
|
+
- agent code (infinite loop / segfault) crashing the privileged daemon — which
|
|
14
|
+
manages the user's real browser — is an unacceptable blast radius.
|
|
15
|
+
A per-session subprocess crashes only itself (D1 of the task).
|
|
16
|
+
|
|
17
|
+
Transport (Fork 2): the daemon owns the LIFECYCLE (spawn/discover via the
|
|
18
|
+
``ensureExecutor`` verb + an ``_ipc`` discovery file); the executor owns the
|
|
19
|
+
DATA PLANE — its own per-session unix socket speaking a simple length-framed
|
|
20
|
+
request/response of our design (``protocol.py``). The thin client connects
|
|
21
|
+
directly to that socket, keeping arbitrary code + large output OFF the daemon's
|
|
22
|
+
critical path.
|
|
23
|
+
|
|
24
|
+
Concurrency (Fork 3): a single dedicated worker thread owns the sync-Playwright
|
|
25
|
+
objects (thread-affine); the accept loop enqueues ``{code, timeout}`` requests
|
|
26
|
+
and the worker drains them FIFO (serial queue).
|
|
27
|
+
|
|
28
|
+
Status: PR1 (process skeleton + data plane), PR2 (daemon-side supervision —
|
|
29
|
+
idle reap / endSession kill / crash reap / orphan sweep), and PR3 (``reset()`` +
|
|
30
|
+
full output protocol: warnings / screenshots / truncation / traceback-bearing
|
|
31
|
+
errors + per-call timeout enforcement) are all in place.
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from .protocol import (
|
|
36
|
+
ExecuteRequest,
|
|
37
|
+
ExecuteResponse,
|
|
38
|
+
recv_message,
|
|
39
|
+
send_message,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"ExecuteRequest",
|
|
44
|
+
"ExecuteResponse",
|
|
45
|
+
"recv_message",
|
|
46
|
+
"send_message",
|
|
47
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Thin-client side of the executor data plane.
|
|
2
|
+
|
|
3
|
+
Used by ``repl/inline.py`` when inline code touches ``page`` / ``context`` /
|
|
4
|
+
``snapshot`` / ``state`` / ``reset``: the whole code body is shipped to the
|
|
5
|
+
session's resident executor and the response is replayed locally.
|
|
6
|
+
|
|
7
|
+
Control plane (spawn + discover) goes through the daemon's
|
|
8
|
+
``BrowserwrightDaemon.ensureExecutor`` verb over the EXISTING mode_b socket
|
|
9
|
+
(tiny payload). The daemon spawns the executor if absent, waits for it to bind +
|
|
10
|
+
write its ``_ipc`` discovery file, and returns the socket path. The data plane
|
|
11
|
+
(this module) then connects DIRECTLY to that socket — keeping arbitrary code +
|
|
12
|
+
large output off the daemon's event loop (Fork 2).
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import socket
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
from ..errors import BrowserwrightError
|
|
20
|
+
from .protocol import (
|
|
21
|
+
DEFAULT_TIMEOUT_MS,
|
|
22
|
+
ExecuteRequest,
|
|
23
|
+
ExecuteResponse,
|
|
24
|
+
recv_message,
|
|
25
|
+
send_message,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Generous slack added on TOP of the per-call timeout for the data-plane recv.
|
|
29
|
+
# The FIRST execute on a freshly-spawned executor triggers the lazy cold-start
|
|
30
|
+
# (connect_over_cdp + bind), which can take ~10-35s on a daemon-restart race
|
|
31
|
+
# (the executor's `_COLD_START_CONNECT_ATTEMPTS` backoff is ~10s; the registry's
|
|
32
|
+
# spawn-ready budget is 35s). The control-plane RPC no longer waits on that, so
|
|
33
|
+
# the wait moved HERE — the client's own blocking socket has no keepalive, so a
|
|
34
|
+
# long first call is fine as long as we don't time the recv out prematurely.
|
|
35
|
+
_COLD_START_RECV_SLACK_S = 45.0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ExecutorUnavailable(BrowserwrightError):
|
|
39
|
+
"""The session's executor could not be ensured/reached.
|
|
40
|
+
|
|
41
|
+
Surfaced when ``ensureExecutor`` fails or the executor socket can't be
|
|
42
|
+
connected — actionable: the daemon must be running (it spawns the
|
|
43
|
+
executor)."""
|
|
44
|
+
|
|
45
|
+
default_fix = ("ensure the daemon is running (`browserwright-daemon status "
|
|
46
|
+
"--json` should show `alive`); it lazily spawns the "
|
|
47
|
+
"per-session executor on first browser use.")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def ensure_executor(sess) -> str:
|
|
51
|
+
"""Ask the daemon to ensure the session's executor and return its socket
|
|
52
|
+
path. Uses the session's mode_b CDP client (``sess.cdp``) to send the
|
|
53
|
+
control-plane verb."""
|
|
54
|
+
sid = _session_id(sess)
|
|
55
|
+
try:
|
|
56
|
+
# The browserwright session is already bound on the websocket query
|
|
57
|
+
# (`?session=<id>`). Do not pass it as CDP's top-level `sessionId`;
|
|
58
|
+
# that field means "attached target session" inside the proxy mux.
|
|
59
|
+
res = sess.cdp.send(
|
|
60
|
+
"BrowserwrightDaemon.ensureExecutor", bsSession=sid)
|
|
61
|
+
except Exception as e: # noqa: BLE001
|
|
62
|
+
raise ExecutorUnavailable(
|
|
63
|
+
f"ensureExecutor failed for session {sid!r}: {e}") from e
|
|
64
|
+
sock_path = res.get("exec_sock") if isinstance(res, dict) else None
|
|
65
|
+
if not isinstance(sock_path, str) or not sock_path:
|
|
66
|
+
raise ExecutorUnavailable(
|
|
67
|
+
f"ensureExecutor returned no socket for session {sid!r}: {res!r}")
|
|
68
|
+
return sock_path
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_on_executor(sess, code: str, *,
|
|
72
|
+
timeout_ms: int = DEFAULT_TIMEOUT_MS) -> ExecuteResponse:
|
|
73
|
+
"""Ship ``code`` to the session's executor and return its response.
|
|
74
|
+
|
|
75
|
+
Ensures the executor (control plane), connects its socket (data plane),
|
|
76
|
+
sends one :class:`ExecuteRequest`, reads one :class:`ExecuteResponse`.
|
|
77
|
+
|
|
78
|
+
The recv socket timeout is the per-call ``timeout_ms`` PLUS a cold-start
|
|
79
|
+
slack: the first execute on a fresh executor performs the lazy
|
|
80
|
+
connect_over_cdp + bind (moved off the control plane), which can add up to
|
|
81
|
+
~35s. The executor itself bounds the worker per-call timeout; this slack
|
|
82
|
+
only prevents the CLIENT recv from giving up before the executor replies."""
|
|
83
|
+
sock_path = ensure_executor(sess)
|
|
84
|
+
recv_timeout = max(timeout_ms, 1) / 1000.0 + _COLD_START_RECV_SLACK_S
|
|
85
|
+
conn = _connect(sock_path, timeout=recv_timeout)
|
|
86
|
+
try:
|
|
87
|
+
send_message(conn, ExecuteRequest(code=code, timeout_ms=timeout_ms).to_dict())
|
|
88
|
+
msg = recv_message(conn)
|
|
89
|
+
except (ConnectionError, OSError, ValueError) as e:
|
|
90
|
+
raise ExecutorUnavailable(
|
|
91
|
+
f"executor data-plane error on {sock_path!r}: {e}") from e
|
|
92
|
+
finally:
|
|
93
|
+
try:
|
|
94
|
+
conn.close()
|
|
95
|
+
except OSError:
|
|
96
|
+
pass
|
|
97
|
+
return ExecuteResponse.from_dict(msg)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _connect(sock_path: str, *, timeout: float = 30.0,
|
|
101
|
+
retry_until: float = 5.0) -> socket.socket:
|
|
102
|
+
"""Connect the executor's unix socket, briefly retrying a not-yet-bound
|
|
103
|
+
socket (the daemon returns the path the moment it spawns; the bind may race
|
|
104
|
+
by a few ms)."""
|
|
105
|
+
deadline = time.monotonic() + retry_until
|
|
106
|
+
last: OSError | None = None
|
|
107
|
+
while True:
|
|
108
|
+
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
109
|
+
s.settimeout(timeout)
|
|
110
|
+
try:
|
|
111
|
+
s.connect(sock_path)
|
|
112
|
+
return s
|
|
113
|
+
except OSError as e:
|
|
114
|
+
last = e
|
|
115
|
+
s.close()
|
|
116
|
+
if time.monotonic() >= deadline:
|
|
117
|
+
raise ExecutorUnavailable(
|
|
118
|
+
f"could not connect executor socket {sock_path!r}: {e}"
|
|
119
|
+
) from last
|
|
120
|
+
time.sleep(0.05)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _session_id(sess) -> str:
|
|
124
|
+
rec = getattr(sess, "session_record", None)
|
|
125
|
+
if isinstance(rec, dict) and rec.get("id"):
|
|
126
|
+
return str(rec["id"])
|
|
127
|
+
raise ExecutorUnavailable("no session id bound; cannot reach an executor")
|