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,214 @@
1
+ """rdp backend — Chrome launched with --remote-debugging-port=NNNN.
2
+
3
+ Spec §8.2. The interesting part is the Chrome 136/147+ default-profile lockdown:
4
+ those builds disable `/json/version` HTTP discovery when the user-data-dir is
5
+ the *real* user profile (privacy hardening), returning a 404. The websocket
6
+ path still works, and Chrome still writes it into `DevToolsActivePort`. So the
7
+ fallback is: HTTP 404 → walk PROFILES, match the port number on line 1, read
8
+ the ws path from line 2.
9
+
10
+ This mirrors browser-harness `daemon.py:83-101` `_ws_from_devtools_active_port`
11
+ — which has already eaten the IPv6-host-bracket and stale-port edges.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+ from urllib.parse import urlparse
17
+
18
+ import httpx
19
+
20
+ from ..config import Config
21
+ from ..errors import Unavailable
22
+ from ..platforms import profile_paths
23
+ from .base import Backend, DoctorResult, ResolveResult
24
+
25
+
26
+ class RdpBackend(Backend):
27
+ name = "rdp"
28
+ kind = "UPSTREAM_WS"
29
+ recommended_mode: str = "A"
30
+ ux_cost = "none"
31
+
32
+ def __init__(self, cfg: Config):
33
+ self._cfg = cfg
34
+ self.port = cfg.backends.rdp.port
35
+
36
+ def caps(self) -> dict:
37
+ # --create launches/owns an isolated Chrome and can use real browser
38
+ # contexts for per-session isolation (P4).
39
+ return {"owns_browser": True, "supports_browser_context": True}
40
+
41
+ # ------------------------------------------------------------------ probe
42
+
43
+ async def probe(self) -> DoctorResult:
44
+ """Cheap reachability check — does HTTP discovery succeed, OR (404-case)
45
+ does some profile's DevToolsActivePort point at this port?
46
+
47
+ Per spec §5.2, probe never opens a ws and never produces ws_url.
48
+ """
49
+ port = self.port
50
+ url = f"http://127.0.0.1:{port}/json/version"
51
+ try:
52
+ # trust_env=False keeps the user's HTTP(S)_PROXY / ALL_PROXY from
53
+ # being applied to localhost probes — proxying to your own loopback
54
+ # is never what anyone wants here and triggers httpx[socks] import
55
+ # errors when ALL_PROXY=socks5://...
56
+ async with httpx.AsyncClient(timeout=1.0, trust_env=False) as client:
57
+ resp = await client.get(url)
58
+ except (httpx.HTTPError, OSError) as e:
59
+ return DoctorResult(
60
+ name=self.name,
61
+ available=False,
62
+ ws_url=None,
63
+ detail=f"no service on 127.0.0.1:{port} ({type(e).__name__})",
64
+ ux_cost=self.ux_cost,
65
+ )
66
+ if resp.status_code == 200:
67
+ # 200 == happy path: discovery works. We don't actually parse
68
+ # webSocketDebuggerUrl here — `resolve` does that for real.
69
+ return DoctorResult(
70
+ name=self.name,
71
+ available=True,
72
+ ws_url=None,
73
+ detail=f"HTTP 200 from {url}",
74
+ ux_cost=self.ux_cost,
75
+ )
76
+ if resp.status_code == 404:
77
+ # Chrome 136/147+ default-profile lockdown. Probe by matching the
78
+ # port number in the existing DevToolsActivePort files. No ws open.
79
+ matched = _find_matching_profile(port)
80
+ if matched is not None:
81
+ return DoctorResult(
82
+ name=self.name,
83
+ available=True,
84
+ ws_url=None,
85
+ detail=(
86
+ f"HTTP 404 from {url} (Chrome 136/147+ default-profile "
87
+ f"lockdown); fallback matched {matched}/DevToolsActivePort"
88
+ ),
89
+ ux_cost=self.ux_cost,
90
+ )
91
+ return DoctorResult(
92
+ name=self.name,
93
+ available=False,
94
+ ws_url=None,
95
+ detail=(
96
+ f"HTTP 404 from {url}; no DevToolsActivePort file on port {port} "
97
+ "in known profiles. Chrome likely needs --user-data-dir=<isolated>."
98
+ ),
99
+ ux_cost=self.ux_cost,
100
+ )
101
+ return DoctorResult(
102
+ name=self.name,
103
+ available=False,
104
+ ws_url=None,
105
+ detail=f"HTTP {resp.status_code} from {url}",
106
+ ux_cost=self.ux_cost,
107
+ )
108
+
109
+ # ---------------------------------------------------------------- resolve
110
+
111
+ async def resolve(self, timeout: float) -> ResolveResult:
112
+ port = self.port
113
+ url = f"http://127.0.0.1:{port}/json/version"
114
+ try:
115
+ async with httpx.AsyncClient(timeout=timeout, trust_env=False) as client:
116
+ resp = await client.get(url)
117
+ except (httpx.HTTPError, OSError) as e:
118
+ raise Unavailable(
119
+ f"rdp: cannot reach {url}: {e}",
120
+ attempts={self.name: f"GET {url} -> {type(e).__name__}: {e}"},
121
+ ) from e
122
+ if resp.status_code == 200:
123
+ try:
124
+ body = resp.json()
125
+ except ValueError as e:
126
+ raise Unavailable(
127
+ f"rdp: {url} returned non-JSON: {e}",
128
+ attempts={self.name: f"GET {url} -> non-JSON"},
129
+ ) from e
130
+ ws = body.get("webSocketDebuggerUrl") if isinstance(body, dict) else None
131
+ if not isinstance(ws, str) or not ws:
132
+ raise Unavailable(
133
+ f"rdp: {url} JSON has no webSocketDebuggerUrl",
134
+ attempts={self.name: "no webSocketDebuggerUrl field"},
135
+ )
136
+ return ResolveResult(
137
+ ws_url=ws,
138
+ backend=self.name,
139
+ extras={"isolated_profile": None, "profile_path": None},
140
+ )
141
+ if resp.status_code == 404:
142
+ ws = _ws_from_devtools_active_port(url)
143
+ if ws is None:
144
+ raise Unavailable(
145
+ f"rdp: HTTP 404 on {url} (Chrome 136/147+ default-profile lockdown) "
146
+ f"and no matching DevToolsActivePort file in known profiles",
147
+ attempts={self.name: f"GET {url} -> 404, fallback no match"},
148
+ )
149
+ matched_profile = _find_matching_profile(self.port)
150
+ return ResolveResult(
151
+ ws_url=ws,
152
+ backend=self.name,
153
+ extras={
154
+ "isolated_profile": False, # default-profile lockdown ⇒ user is on default
155
+ "profile_path": str(matched_profile) if matched_profile else None,
156
+ },
157
+ )
158
+ raise Unavailable(
159
+ f"rdp: {url} returned HTTP {resp.status_code}",
160
+ attempts={self.name: f"GET {url} -> HTTP {resp.status_code}"},
161
+ )
162
+
163
+
164
+ # ---- helpers (module-private, also imported by tests) ----------------------
165
+
166
+ def _find_matching_profile(want_port: int) -> Path | None:
167
+ """Walk PROFILES, return the first whose DevToolsActivePort line 1 == want_port
168
+ AND whose line 2 is a non-empty ws path.
169
+
170
+ Multiple matches → mtime-newest.
171
+ """
172
+ matches: list[tuple[float, Path]] = []
173
+ for base in profile_paths():
174
+ f = base / "DevToolsActivePort"
175
+ try:
176
+ lines = f.read_text().splitlines()
177
+ mtime = f.stat().st_mtime
178
+ except (FileNotFoundError, NotADirectoryError, PermissionError, OSError):
179
+ continue
180
+ if not lines:
181
+ continue
182
+ port = lines[0].strip()
183
+ ws_path = lines[1].strip() if len(lines) > 1 else ""
184
+ if port == str(want_port) and ws_path:
185
+ matches.append((mtime, base))
186
+ if not matches:
187
+ return None
188
+ return max(matches, key=lambda t: t[0])[1]
189
+
190
+
191
+ def _ws_from_devtools_active_port(http_url: str) -> str | None:
192
+ """Build a ws:// URL from a DevToolsActivePort file when /json/version 404s.
193
+
194
+ Ported from browser-harness daemon.py:83-101 — preserves the IPv6 bracket
195
+ handling (urlparse strips brackets; we restore them) and the line-1/line-2
196
+ contract.
197
+ """
198
+ p = urlparse(http_url)
199
+ want_port = str(p.port) if p.port else ""
200
+ if not want_port:
201
+ return None
202
+ host = p.hostname or "127.0.0.1"
203
+ if ":" in host: # IPv6 literal
204
+ host = f"[{host}]"
205
+ for base in profile_paths():
206
+ try:
207
+ active = (base / "DevToolsActivePort").read_text().splitlines()
208
+ except (FileNotFoundError, NotADirectoryError, PermissionError, OSError):
209
+ continue
210
+ port = active[0].strip() if active else ""
211
+ ws_path = active[1].strip() if len(active) > 1 else ""
212
+ if port == want_port and ws_path:
213
+ return f"ws://{host}:{port}{ws_path}"
214
+ return None