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.
Files changed (98) hide show
  1. browserwright/__init__.py +33 -0
  2. browserwright/__main__.py +6 -0
  3. browserwright/_executor/__init__.py +47 -0
  4. browserwright/_executor/__main__.py +9 -0
  5. browserwright/_executor/client.py +127 -0
  6. browserwright/_executor/process.py +652 -0
  7. browserwright/_executor/protocol.py +152 -0
  8. browserwright/api.py +66 -0
  9. browserwright/cdp.py +285 -0
  10. browserwright/cli.py +741 -0
  11. browserwright/daemon/__init__.py +8 -0
  12. browserwright/daemon/_ipc.py +444 -0
  13. browserwright/daemon/active_tab.py +183 -0
  14. browserwright/daemon/auth.py +395 -0
  15. browserwright/daemon/backends/__init__.py +59 -0
  16. browserwright/daemon/backends/base.py +120 -0
  17. browserwright/daemon/backends/cloud.py +222 -0
  18. browserwright/daemon/backends/env.py +119 -0
  19. browserwright/daemon/backends/extension.py +185 -0
  20. browserwright/daemon/backends/rdp.py +214 -0
  21. browserwright/daemon/cli.py +1437 -0
  22. browserwright/daemon/config.py +380 -0
  23. browserwright/daemon/doctor.py +179 -0
  24. browserwright/daemon/errors.py +34 -0
  25. browserwright/daemon/launch_chrome.py +353 -0
  26. browserwright/daemon/observability.py +181 -0
  27. browserwright/daemon/platforms.py +234 -0
  28. browserwright/daemon/resolver.py +72 -0
  29. browserwright/daemon/server/__init__.py +6 -0
  30. browserwright/daemon/server/daemon.py +229 -0
  31. browserwright/daemon/server/executor_registry.py +434 -0
  32. browserwright/daemon/server/extension_upstream.py +677 -0
  33. browserwright/daemon/server/facade.py +375 -0
  34. browserwright/daemon/server/facade_extension.py +969 -0
  35. browserwright/daemon/server/listener.py +1058 -0
  36. browserwright/daemon/server/proxy.py +1991 -0
  37. browserwright/daemon/server/relay.py +783 -0
  38. browserwright/daemon/server/state.py +432 -0
  39. browserwright/daemon/server/upstream.py +266 -0
  40. browserwright/daemon/userscripts.py +150 -0
  41. browserwright/discovery.py +213 -0
  42. browserwright/errors.py +177 -0
  43. browserwright/health.py +169 -0
  44. browserwright/install.py +628 -0
  45. browserwright/memory/__init__.py +15 -0
  46. browserwright/memory/_md.py +120 -0
  47. browserwright/memory/_yaml.py +217 -0
  48. browserwright/memory/global_mem.py +201 -0
  49. browserwright/memory/repl_mem.py +28 -0
  50. browserwright/memory/session_decisions.py +53 -0
  51. browserwright/memory/site_mem.py +381 -0
  52. browserwright/mode_b_client.py +590 -0
  53. browserwright/multitask.py +131 -0
  54. browserwright/output_schema.py +99 -0
  55. browserwright/primitives/__init__.py +67 -0
  56. browserwright/primitives/discovery_api.py +79 -0
  57. browserwright/primitives/http.py +42 -0
  58. browserwright/primitives/inspect.py +876 -0
  59. browserwright/primitives/interact.py +518 -0
  60. browserwright/primitives/page.py +556 -0
  61. browserwright/primitives/site.py +143 -0
  62. browserwright/release_install.py +466 -0
  63. browserwright/repl/__init__.py +6 -0
  64. browserwright/repl/_namespace.py +106 -0
  65. browserwright/repl/_smart_goto.py +236 -0
  66. browserwright/repl/inline.py +180 -0
  67. browserwright/repl/playwright_handle.py +449 -0
  68. browserwright/repl/snapshot.py +150 -0
  69. browserwright/session.py +229 -0
  70. browserwright/session_create.py +252 -0
  71. browserwright/session_ctx.py +24 -0
  72. browserwright/session_registry.py +133 -0
  73. browserwright/session_runtime.py +133 -0
  74. browserwright/site_skills_starter/github.com/SKILL.md +14 -0
  75. browserwright/site_skills_starter/github.com/memory.md +29 -0
  76. browserwright/site_skills_starter/github.com/tasks/list_issues.py +55 -0
  77. browserwright/site_skills_starter/google.com/SKILL.md +16 -0
  78. browserwright/site_skills_starter/google.com/memory.md +27 -0
  79. browserwright/site_skills_starter/google.com/tasks/search.py +53 -0
  80. browserwright/site_skills_starter/producthunt.com/SKILL.md +7 -0
  81. browserwright/site_skills_starter/producthunt.com/memory.md +26 -0
  82. browserwright/site_skills_starter/producthunt.com/tasks/today.py +64 -0
  83. browserwright/site_skills_starter/wikipedia.org/SKILL.md +7 -0
  84. browserwright/site_skills_starter/wikipedia.org/memory.md +22 -0
  85. browserwright/site_skills_starter/wikipedia.org/tasks/lookup.py +55 -0
  86. browserwright/site_skills_starter/ycombinator.com/SKILL.md +8 -0
  87. browserwright/site_skills_starter/ycombinator.com/memory.md +25 -0
  88. browserwright/site_skills_starter/ycombinator.com/tasks/front_page.py +63 -0
  89. browserwright/skill_doc.py +140 -0
  90. browserwright/skill_runtime.md +194 -0
  91. browserwright/subscriptions.py +213 -0
  92. browserwright/task_runner.py +125 -0
  93. browserwright/version.py +117 -0
  94. browserwright-0.6.2.dist-info/METADATA +12 -0
  95. browserwright-0.6.2.dist-info/RECORD +98 -0
  96. browserwright-0.6.2.dist-info/WHEEL +5 -0
  97. browserwright-0.6.2.dist-info/entry_points.txt +3 -0
  98. 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,6 @@
1
+ """Allow ``python -m browserwright ...`` as an alternate entry point."""
2
+ from .cli import main
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -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,9 @@
1
+ """``python -m browserwright._executor --session <id>`` entrypoint."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+
6
+ from .process import main
7
+
8
+ if __name__ == "__main__":
9
+ sys.exit(main())
@@ -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")