patchright-cli 0.4.0__py3-none-any.whl → 0.4.1__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.
- patchright_cli/__init__.py +1 -1
- patchright_cli/cli.py +111 -9
- patchright_cli/daemon.py +287 -36
- patchright_cli/dashboard.py +135 -0
- patchright_cli/ref_registry.py +7 -1
- patchright_cli/snapshot.py +2 -2
- {patchright_cli-0.4.0.dist-info → patchright_cli-0.4.1.dist-info}/METADATA +2 -1
- patchright_cli-0.4.1.dist-info/RECORD +12 -0
- patchright_cli-0.4.0.dist-info/RECORD +0 -11
- {patchright_cli-0.4.0.dist-info → patchright_cli-0.4.1.dist-info}/WHEEL +0 -0
- {patchright_cli-0.4.0.dist-info → patchright_cli-0.4.1.dist-info}/entry_points.txt +0 -0
- {patchright_cli-0.4.0.dist-info → patchright_cli-0.4.1.dist-info}/licenses/LICENSE +0 -0
patchright_cli/__init__.py
CHANGED
patchright_cli/cli.py
CHANGED
|
@@ -11,6 +11,7 @@ import os
|
|
|
11
11
|
import socket
|
|
12
12
|
import struct
|
|
13
13
|
import sys
|
|
14
|
+
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
import click
|
|
16
17
|
|
|
@@ -18,6 +19,23 @@ from patchright_cli import __version__
|
|
|
18
19
|
from patchright_cli.daemon import DEFAULT_PORT, ensure_daemon_running
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
def _load_config(config_path: str | None) -> dict:
|
|
23
|
+
"""Load JSON config file. If config_path is None, try .patchright-cli/config.json in cwd."""
|
|
24
|
+
p = Path(config_path) if config_path else Path.cwd() / ".patchright-cli" / "config.json"
|
|
25
|
+
if p.exists():
|
|
26
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
27
|
+
return {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _merge_config_with_options(config: dict, options: dict) -> dict:
|
|
31
|
+
"""Merge config dict with CLI options. CLI options take precedence."""
|
|
32
|
+
merged = dict(config)
|
|
33
|
+
for k, v in options.items():
|
|
34
|
+
if v is not None:
|
|
35
|
+
merged[k] = v
|
|
36
|
+
return merged
|
|
37
|
+
|
|
38
|
+
|
|
21
39
|
def _send_command(command: str, args: list, options: dict, port: int = DEFAULT_PORT) -> dict:
|
|
22
40
|
"""Connect to daemon, send command, receive response."""
|
|
23
41
|
msg = {
|
|
@@ -61,6 +79,7 @@ def _recv_exact(sock: socket.socket, n: int) -> bytes:
|
|
|
61
79
|
COMMANDS_HELP = {
|
|
62
80
|
# Core
|
|
63
81
|
"open": "open [url] Open browser (starts daemon if needed)",
|
|
82
|
+
"attach": "attach --cdp=<url> Attach to running Chrome via CDP",
|
|
64
83
|
"goto": "goto <url> Navigate to URL",
|
|
65
84
|
"click": "click <ref> [button] Click element [--modifiers=Alt,Shift]",
|
|
66
85
|
"dblclick": "dblclick <ref> [btn] Double-click [--modifiers=Alt,Shift]",
|
|
@@ -70,8 +89,8 @@ COMMANDS_HELP = {
|
|
|
70
89
|
"select": "select <ref> <value> Select dropdown option",
|
|
71
90
|
"check": "check <ref> Check checkbox/radio",
|
|
72
91
|
"uncheck": "uncheck <ref> Uncheck checkbox/radio",
|
|
73
|
-
"snapshot": "snapshot [ref] Take accessibility snapshot",
|
|
74
|
-
"eval": "eval <expr>
|
|
92
|
+
"snapshot": "snapshot [ref] Take accessibility snapshot [--filename=F] [--depth=N]",
|
|
93
|
+
"eval": "eval <expr> [ref] Evaluate JavaScript [--file=F or stdin]",
|
|
75
94
|
"text": "text <ref|selector> Get text content of element",
|
|
76
95
|
"screenshot": "screenshot [ref] Save screenshot [--full-page] [--filename=F]",
|
|
77
96
|
"drag": "drag <from> <to> Drag element to target",
|
|
@@ -117,6 +136,8 @@ COMMANDS_HELP = {
|
|
|
117
136
|
# Dialog
|
|
118
137
|
"dialog-accept": "dialog-accept [text] Accept next dialog",
|
|
119
138
|
"dialog-dismiss": "dialog-dismiss Dismiss next dialog",
|
|
139
|
+
# Permissions
|
|
140
|
+
"grant-permissions": "grant-permissions <perms> Grant permissions [--origin=url]",
|
|
120
141
|
# Upload / Resize
|
|
121
142
|
"upload": "upload <file> [ref] Upload file to input",
|
|
122
143
|
"resize": "resize <w> <h> Resize viewport",
|
|
@@ -142,6 +163,7 @@ COMMANDS_HELP = {
|
|
|
142
163
|
# Video
|
|
143
164
|
"video-start": "video-start Start video recording",
|
|
144
165
|
"video-stop": "video-stop Stop recording and save [--filename=F]",
|
|
166
|
+
"video-chapter": "video-chapter <title> Add chapter marker to video",
|
|
145
167
|
# PDF
|
|
146
168
|
"pdf": "pdf [--filename=F] Save page as PDF",
|
|
147
169
|
# DevTools
|
|
@@ -152,6 +174,11 @@ COMMANDS_HELP = {
|
|
|
152
174
|
"close-all": "close-all Close all sessions",
|
|
153
175
|
"kill-all": "kill-all Kill all sessions",
|
|
154
176
|
"delete-data": "delete-data Delete persistent profile",
|
|
177
|
+
# Dashboard
|
|
178
|
+
"show": "show [--port=N] Open session dashboard in browser",
|
|
179
|
+
# Codegen
|
|
180
|
+
"codegen": "codegen [file] Start recording interactions",
|
|
181
|
+
"codegen-stop": "codegen-stop [file] Stop recording and save script",
|
|
155
182
|
}
|
|
156
183
|
|
|
157
184
|
ALL_COMMANDS = list(COMMANDS_HELP.keys())
|
|
@@ -167,6 +194,18 @@ def _print_help():
|
|
|
167
194
|
click.echo(" --proxy=<url> Proxy server (e.g. http://host:port, socks5://host:port)")
|
|
168
195
|
click.echo(" -s=<name> Named session (default: 'default')")
|
|
169
196
|
click.echo(" --port=<n> Daemon port (default: 9321)")
|
|
197
|
+
click.echo(" --config=<path> Load config from JSON file")
|
|
198
|
+
click.echo(" --timeout-action=ms Default action timeout")
|
|
199
|
+
click.echo(" --timeout-navigation=ms Default navigation timeout")
|
|
200
|
+
click.echo(" --device=<name> Emulate a device (e.g. 'iPhone 15')")
|
|
201
|
+
click.echo(" --viewport-size=WxH Set viewport size")
|
|
202
|
+
click.echo(" --locale=<code> Locale (e.g. en-US)")
|
|
203
|
+
click.echo(" --timezone=<id> Timezone ID")
|
|
204
|
+
click.echo(" --geolocation=lat,lon Geolocation override")
|
|
205
|
+
click.echo(" --user-agent=<ua> Custom user agent")
|
|
206
|
+
click.echo(" --grant-permissions=P Comma-separated permissions to grant")
|
|
207
|
+
click.echo(" --cdp=<url> Attach to Chrome via CDP endpoint")
|
|
208
|
+
click.echo(" --show-port=N Dashboard port (default: 9322)")
|
|
170
209
|
click.echo(" --version Show version")
|
|
171
210
|
click.echo(" --help Show this help\n")
|
|
172
211
|
click.echo("Commands:")
|
|
@@ -176,6 +215,7 @@ def _print_help():
|
|
|
176
215
|
"Core",
|
|
177
216
|
[
|
|
178
217
|
"open",
|
|
218
|
+
"attach",
|
|
179
219
|
"goto",
|
|
180
220
|
"click",
|
|
181
221
|
"dblclick",
|
|
@@ -223,13 +263,16 @@ def _print_help():
|
|
|
223
263
|
"sessionstorage-clear",
|
|
224
264
|
],
|
|
225
265
|
),
|
|
266
|
+
("Permissions", ["grant-permissions"]),
|
|
226
267
|
("Route", ["route", "route-list", "unroute", "network-state-set"]),
|
|
227
268
|
("Code", ["run-code"]),
|
|
228
269
|
("Tracing", ["tracing-start", "tracing-stop"]),
|
|
229
|
-
("Video", ["video-start", "video-stop"]),
|
|
270
|
+
("Video", ["video-start", "video-stop", "video-chapter"]),
|
|
230
271
|
("PDF", ["pdf"]),
|
|
231
272
|
("DevTools", ["console", "network"]),
|
|
232
273
|
("Session", ["list", "close-all", "kill-all", "delete-data"]),
|
|
274
|
+
("Dashboard", ["show"]),
|
|
275
|
+
("Codegen", ["codegen", "codegen-stop"]),
|
|
233
276
|
]
|
|
234
277
|
for cat_name, cmds in categories:
|
|
235
278
|
click.echo(f"\n {cat_name}:")
|
|
@@ -247,8 +290,10 @@ def main():
|
|
|
247
290
|
persistent = False
|
|
248
291
|
profile = None
|
|
249
292
|
proxy = None
|
|
250
|
-
|
|
293
|
+
config_path = None
|
|
294
|
+
session_name = os.environ.get("PATCHRIGHT_CLI_SESSION", "default")
|
|
251
295
|
port = DEFAULT_PORT
|
|
296
|
+
extra_opts: dict = {}
|
|
252
297
|
|
|
253
298
|
# Extract options
|
|
254
299
|
remaining = []
|
|
@@ -287,6 +332,60 @@ def main():
|
|
|
287
332
|
except ValueError:
|
|
288
333
|
click.echo(f"Invalid port value: {argv[i]}", err=True)
|
|
289
334
|
sys.exit(1)
|
|
335
|
+
elif arg.startswith("--config="):
|
|
336
|
+
config_path = arg.split("=", 1)[1]
|
|
337
|
+
elif arg == "--config" and i + 1 < len(argv):
|
|
338
|
+
i += 1
|
|
339
|
+
config_path = argv[i]
|
|
340
|
+
elif arg.startswith("--device="):
|
|
341
|
+
extra_opts["device"] = arg.split("=", 1)[1]
|
|
342
|
+
elif arg == "--device" and i + 1 < len(argv):
|
|
343
|
+
i += 1
|
|
344
|
+
extra_opts["device"] = argv[i]
|
|
345
|
+
elif arg.startswith("--viewport-size="):
|
|
346
|
+
vw, vh = arg.split("=", 1)[1].split("x", 1)
|
|
347
|
+
extra_opts["viewport"] = {"width": vw, "height": vh}
|
|
348
|
+
elif arg == "--viewport-size" and i + 1 < len(argv):
|
|
349
|
+
i += 1
|
|
350
|
+
vw, vh = argv[i].split("x", 1)
|
|
351
|
+
extra_opts["viewport"] = {"width": vw, "height": vh}
|
|
352
|
+
elif arg.startswith("--locale="):
|
|
353
|
+
extra_opts["locale"] = arg.split("=", 1)[1]
|
|
354
|
+
elif arg == "--locale" and i + 1 < len(argv):
|
|
355
|
+
i += 1
|
|
356
|
+
extra_opts["locale"] = argv[i]
|
|
357
|
+
elif arg.startswith("--timezone="):
|
|
358
|
+
extra_opts["timezone"] = arg.split("=", 1)[1]
|
|
359
|
+
elif arg == "--timezone" and i + 1 < len(argv):
|
|
360
|
+
i += 1
|
|
361
|
+
extra_opts["timezone"] = argv[i]
|
|
362
|
+
elif arg.startswith("--geolocation="):
|
|
363
|
+
lat, lon = arg.split("=", 1)[1].split(",", 1)
|
|
364
|
+
extra_opts["geolocation"] = {"lat": lat, "lon": lon}
|
|
365
|
+
elif arg == "--geolocation" and i + 1 < len(argv):
|
|
366
|
+
i += 1
|
|
367
|
+
lat, lon = argv[i].split(",", 1)
|
|
368
|
+
extra_opts["geolocation"] = {"lat": lat, "lon": lon}
|
|
369
|
+
elif arg.startswith("--user-agent="):
|
|
370
|
+
extra_opts["user-agent"] = arg.split("=", 1)[1]
|
|
371
|
+
elif arg == "--user-agent" and i + 1 < len(argv):
|
|
372
|
+
i += 1
|
|
373
|
+
extra_opts["user-agent"] = argv[i]
|
|
374
|
+
elif arg.startswith("--grant-permissions="):
|
|
375
|
+
extra_opts["grant-permissions"] = arg.split("=", 1)[1]
|
|
376
|
+
elif arg == "--grant-permissions" and i + 1 < len(argv):
|
|
377
|
+
i += 1
|
|
378
|
+
extra_opts["grant-permissions"] = argv[i]
|
|
379
|
+
elif arg.startswith("--cdp="):
|
|
380
|
+
extra_opts["cdp"] = arg.split("=", 1)[1]
|
|
381
|
+
elif arg == "--cdp" and i + 1 < len(argv):
|
|
382
|
+
i += 1
|
|
383
|
+
extra_opts["cdp"] = argv[i]
|
|
384
|
+
elif arg.startswith("--show-port="):
|
|
385
|
+
extra_opts["show-port"] = arg.split("=", 1)[1]
|
|
386
|
+
elif arg == "--show-port" and i + 1 < len(argv):
|
|
387
|
+
i += 1
|
|
388
|
+
extra_opts["show-port"] = argv[i]
|
|
290
389
|
elif arg in ("--version", "-v"):
|
|
291
390
|
click.echo(f"patchright-cli {__version__}")
|
|
292
391
|
sys.exit(0)
|
|
@@ -311,16 +410,18 @@ def main():
|
|
|
311
410
|
|
|
312
411
|
# Separate --key=value and --flag from positional args
|
|
313
412
|
positional_args = []
|
|
314
|
-
|
|
413
|
+
cmd_opts: dict = {}
|
|
315
414
|
for a in args:
|
|
316
415
|
if a.startswith("--") and "=" in a:
|
|
317
416
|
k, v = a[2:].split("=", 1)
|
|
318
|
-
|
|
417
|
+
cmd_opts[k] = v
|
|
319
418
|
elif a.startswith("--"):
|
|
320
|
-
|
|
419
|
+
cmd_opts[a[2:]] = True
|
|
321
420
|
else:
|
|
322
421
|
positional_args.append(a)
|
|
323
422
|
args = positional_args
|
|
423
|
+
# Merge: global opts as base, command-level opts take precedence
|
|
424
|
+
extra_opts = {**extra_opts, **cmd_opts}
|
|
324
425
|
|
|
325
426
|
# For eval/run-code: support --file=<path> and stdin via "-"
|
|
326
427
|
if command in ("eval", "run-code"):
|
|
@@ -343,8 +444,9 @@ def main():
|
|
|
343
444
|
click.echo(f"'{command}' requires a JS expression, --file=<path>, or piped stdin.", err=True)
|
|
344
445
|
sys.exit(1)
|
|
345
446
|
|
|
346
|
-
# Build options dict
|
|
347
|
-
|
|
447
|
+
# Build options dict (config file values are overridden by explicit CLI flags)
|
|
448
|
+
config = _load_config(config_path)
|
|
449
|
+
options = {"session": session_name, **_merge_config_with_options(config, extra_opts)}
|
|
348
450
|
if headless:
|
|
349
451
|
options["headless"] = True
|
|
350
452
|
if persistent:
|
patchright_cli/daemon.py
CHANGED
|
@@ -27,6 +27,8 @@ logger = logging.getLogger("patchright-cli.daemon")
|
|
|
27
27
|
DEFAULT_PORT = 9321
|
|
28
28
|
DEFAULT_PROFILE_DIR = str(Path.home() / ".patchright-cli" / "profiles" / "default")
|
|
29
29
|
|
|
30
|
+
_dashboard_runners: dict[int, tuple] = {}
|
|
31
|
+
|
|
30
32
|
# ---------------------------------------------------------------------------
|
|
31
33
|
# Command handler registry
|
|
32
34
|
# ---------------------------------------------------------------------------
|
|
@@ -66,8 +68,10 @@ class Session:
|
|
|
66
68
|
self._video_cdp = None
|
|
67
69
|
self._video_frames: list[bytes] = []
|
|
68
70
|
self._video_recording: bool = False
|
|
71
|
+
self._video_chapters: list[tuple[int, str]] = []
|
|
69
72
|
self._history: list[str] = []
|
|
70
73
|
self._history_index: int = -1
|
|
74
|
+
self._codegen: list[str] | None = None
|
|
71
75
|
|
|
72
76
|
# -- internal helpers ---------------------------------------------------
|
|
73
77
|
|
|
@@ -185,6 +189,16 @@ class DaemonState:
|
|
|
185
189
|
profile: str | None = None,
|
|
186
190
|
proxy: str | None = None,
|
|
187
191
|
url: str | None = None,
|
|
192
|
+
device: str | None = None,
|
|
193
|
+
viewport: dict | None = None,
|
|
194
|
+
locale: str | None = None,
|
|
195
|
+
timezone: str | None = None,
|
|
196
|
+
geolocation: dict | None = None,
|
|
197
|
+
user_agent: str | None = None,
|
|
198
|
+
grant_permissions: str | None = None,
|
|
199
|
+
cdp_endpoint: str | None = None,
|
|
200
|
+
cdp_headers: dict | None = None,
|
|
201
|
+
cdp_timeout: int = 30000,
|
|
188
202
|
) -> Session:
|
|
189
203
|
if name in self.sessions:
|
|
190
204
|
return self.sessions[name]
|
|
@@ -198,37 +212,69 @@ class DaemonState:
|
|
|
198
212
|
profile_dir = profile or str(Path.home() / ".patchright-cli" / "profiles" / name)
|
|
199
213
|
Path(profile_dir).mkdir(parents=True, exist_ok=True)
|
|
200
214
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
215
|
+
context_options: dict = {}
|
|
216
|
+
if device and self.playwright is not None:
|
|
217
|
+
device_descriptor = self.playwright.devices.get(device)
|
|
218
|
+
if device_descriptor:
|
|
219
|
+
context_options.update(device_descriptor)
|
|
220
|
+
if viewport:
|
|
221
|
+
context_options["viewport"] = {"width": int(viewport["width"]), "height": int(viewport["height"])}
|
|
222
|
+
if locale:
|
|
223
|
+
context_options["locale"] = locale
|
|
224
|
+
if timezone:
|
|
225
|
+
context_options["timezone_id"] = timezone
|
|
226
|
+
if geolocation:
|
|
227
|
+
context_options["geolocation"] = {
|
|
228
|
+
"latitude": float(geolocation["lat"]),
|
|
229
|
+
"longitude": float(geolocation["lon"]),
|
|
230
|
+
}
|
|
231
|
+
context_options["permissions"] = context_options.get("permissions", []) + ["geolocation"]
|
|
232
|
+
if user_agent:
|
|
233
|
+
context_options["user_agent"] = user_agent
|
|
234
|
+
if grant_permissions:
|
|
235
|
+
perms = [p.strip() for p in grant_permissions.split(",") if p.strip()]
|
|
236
|
+
context_options["permissions"] = list(set(context_options.get("permissions", []) + perms))
|
|
237
|
+
|
|
238
|
+
if cdp_endpoint:
|
|
239
|
+
browser = await self.playwright.chromium.connect_over_cdp(
|
|
240
|
+
cdp_endpoint, headers=cdp_headers, timeout=cdp_timeout
|
|
241
|
+
)
|
|
242
|
+
context = await browser.new_context(**context_options)
|
|
243
|
+
pages = context.pages or []
|
|
244
|
+
if not pages:
|
|
245
|
+
page = await context.new_page()
|
|
246
|
+
pages = [page]
|
|
247
|
+
else:
|
|
248
|
+
launch_kwargs = {
|
|
249
|
+
"channel": "chrome",
|
|
250
|
+
"headless": use_headless,
|
|
251
|
+
"no_viewport": True,
|
|
252
|
+
"args": ["--disable-blink-features=AutomationControlled"],
|
|
253
|
+
}
|
|
254
|
+
if proxy:
|
|
255
|
+
parsed = urlparse(proxy)
|
|
256
|
+
if parsed.username or parsed.password:
|
|
257
|
+
import base64
|
|
258
|
+
|
|
259
|
+
creds = base64.b64encode(f"{parsed.username or ''}:{parsed.password or ''}".encode()).decode()
|
|
260
|
+
launch_kwargs["extra_http_headers"] = {"Proxy-Authorization": f"Basic {creds}"}
|
|
261
|
+
# Rebuild proxy URL without credentials
|
|
262
|
+
netloc = parsed.hostname or ""
|
|
263
|
+
if parsed.port:
|
|
264
|
+
netloc += f":{parsed.port}"
|
|
265
|
+
cleaned = parsed._replace(netloc=netloc).geturl()
|
|
266
|
+
launch_kwargs["proxy"] = {"server": cleaned}
|
|
267
|
+
else:
|
|
268
|
+
launch_kwargs["proxy"] = {"server": proxy}
|
|
269
|
+
launch_kwargs.update(context_options)
|
|
270
|
+
context = await self.playwright.chromium.launch_persistent_context(
|
|
271
|
+
profile_dir,
|
|
272
|
+
**launch_kwargs,
|
|
273
|
+
)
|
|
274
|
+
pages = context.pages or []
|
|
275
|
+
if not pages:
|
|
276
|
+
page = await context.new_page()
|
|
277
|
+
pages = [page]
|
|
232
278
|
|
|
233
279
|
if url:
|
|
234
280
|
await pages[0].goto(url)
|
|
@@ -294,6 +340,20 @@ async def _resolve_ref(session: Session, page, ref: str):
|
|
|
294
340
|
return session.ref_registry.resolve(page, ref)
|
|
295
341
|
|
|
296
342
|
|
|
343
|
+
async def _apply_timeouts(page, options: dict) -> None:
|
|
344
|
+
"""Apply per-command timeout overrides from CLI options."""
|
|
345
|
+
if options.get("timeout-action"):
|
|
346
|
+
try:
|
|
347
|
+
page.set_default_timeout(float(options["timeout-action"]))
|
|
348
|
+
except Exception:
|
|
349
|
+
pass
|
|
350
|
+
if options.get("timeout-navigation"):
|
|
351
|
+
try:
|
|
352
|
+
page.set_default_navigation_timeout(float(options["timeout-navigation"]))
|
|
353
|
+
except Exception:
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
|
|
297
357
|
# ---------------------------------------------------------------------------
|
|
298
358
|
# Command handlers
|
|
299
359
|
# ---------------------------------------------------------------------------
|
|
@@ -433,11 +493,12 @@ async def cmd_drag(session: Session, page, args: list, options: dict, cwd: str |
|
|
|
433
493
|
@register("snapshot")
|
|
434
494
|
async def cmd_snapshot(session: Session, page, args: list, options: dict, cwd: str | None, state: DaemonState) -> dict:
|
|
435
495
|
element_ref = args[0] if args else None
|
|
496
|
+
max_depth = int(options["depth"]) if options.get("depth") is not None else None
|
|
436
497
|
if element_ref:
|
|
437
498
|
elem = await _resolve_ref(session, page, element_ref)
|
|
438
|
-
snapshot_text, session.ref_registry = await take_snapshot(page, root_element=elem)
|
|
499
|
+
snapshot_text, session.ref_registry = await take_snapshot(page, root_element=elem, max_depth=max_depth)
|
|
439
500
|
else:
|
|
440
|
-
snapshot_text, session.ref_registry = await take_snapshot(page)
|
|
501
|
+
snapshot_text, session.ref_registry = await take_snapshot(page, max_depth=max_depth)
|
|
441
502
|
fn = options.get("filename")
|
|
442
503
|
if fn:
|
|
443
504
|
Path(fn).parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -465,8 +526,16 @@ async def cmd_snapshot(session: Session, page, args: list, options: dict, cwd: s
|
|
|
465
526
|
|
|
466
527
|
@register("eval")
|
|
467
528
|
async def cmd_eval(session: Session, page, args: list, options: dict, cwd: str | None, state: DaemonState) -> dict:
|
|
468
|
-
|
|
469
|
-
|
|
529
|
+
import re
|
|
530
|
+
|
|
531
|
+
if len(args) >= 2 and re.fullmatch(r"e\d+", args[-1]) and session.ref_registry is not None:
|
|
532
|
+
expr = " ".join(args[:-1])
|
|
533
|
+
ref = args[-1]
|
|
534
|
+
elem = await _resolve_ref(session, page, ref)
|
|
535
|
+
result = await elem.evaluate(expr)
|
|
536
|
+
else:
|
|
537
|
+
expr = args[0] if args else ""
|
|
538
|
+
result = await page.evaluate(expr)
|
|
470
539
|
return {"success": True, "output": json.dumps(result, indent=2, default=str)}
|
|
471
540
|
|
|
472
541
|
|
|
@@ -848,6 +917,25 @@ async def cmd_resize(session: Session, page, args: list, options: dict, cwd: str
|
|
|
848
917
|
return {"success": True, "output": f"Viewport resized to {w}x{h}"}
|
|
849
918
|
|
|
850
919
|
|
|
920
|
+
# -- Permissions -------------------------------------------------------------
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
@register("grant-permissions")
|
|
924
|
+
async def cmd_grant_permissions(
|
|
925
|
+
session: Session, page, args: list, options: dict, cwd: str | None, state: DaemonState
|
|
926
|
+
) -> dict:
|
|
927
|
+
perms_str = args[0] if args else ""
|
|
928
|
+
perms = [p.strip() for p in perms_str.split(",") if p.strip()]
|
|
929
|
+
if not perms:
|
|
930
|
+
return {"success": False, "output": "Usage: grant-permissions <perm1,perm2> [--origin=url]"}
|
|
931
|
+
origin = options.get("origin")
|
|
932
|
+
kwargs = {"permissions": perms}
|
|
933
|
+
if origin:
|
|
934
|
+
kwargs["origin"] = origin
|
|
935
|
+
await session.context.grant_permissions(**kwargs)
|
|
936
|
+
return {"success": True, "output": f"Granted permissions: {', '.join(perms)}"}
|
|
937
|
+
|
|
938
|
+
|
|
851
939
|
# -- State save/load ---------------------------------------------------------
|
|
852
940
|
|
|
853
941
|
|
|
@@ -1075,6 +1163,8 @@ async def cmd_video_stop(
|
|
|
1075
1163
|
frames = session._video_frames
|
|
1076
1164
|
session._video_frames = []
|
|
1077
1165
|
session._video_cdp = None
|
|
1166
|
+
chapters = list(session._video_chapters)
|
|
1167
|
+
session._video_chapters = []
|
|
1078
1168
|
|
|
1079
1169
|
if not frames:
|
|
1080
1170
|
return {"success": False, "output": "No video frames were captured."}
|
|
@@ -1084,6 +1174,13 @@ async def cmd_video_stop(
|
|
|
1084
1174
|
snap_dir.mkdir(parents=True, exist_ok=True)
|
|
1085
1175
|
ts = int(time.time() * 1000)
|
|
1086
1176
|
|
|
1177
|
+
if chapters:
|
|
1178
|
+
chapters_path = snap_dir / f"video-{ts}-chapters.json"
|
|
1179
|
+
chapters_path.write_text(
|
|
1180
|
+
json.dumps([{"frame": f, "title": t} for f, t in chapters], indent=2),
|
|
1181
|
+
encoding="utf-8",
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1087
1184
|
fn = options.get("filename")
|
|
1088
1185
|
video_path = snap_dir / (fn or f"video-{ts}.webm")
|
|
1089
1186
|
try:
|
|
@@ -1126,6 +1223,18 @@ async def cmd_video_stop(
|
|
|
1126
1223
|
}
|
|
1127
1224
|
|
|
1128
1225
|
|
|
1226
|
+
@register("video-chapter")
|
|
1227
|
+
async def cmd_video_chapter(
|
|
1228
|
+
session: Session, page, args: list, options: dict, cwd: str | None, state: DaemonState
|
|
1229
|
+
) -> dict:
|
|
1230
|
+
if not session._video_recording:
|
|
1231
|
+
return {"success": False, "output": "No video recording in progress."}
|
|
1232
|
+
title = args[0] if args else "Chapter"
|
|
1233
|
+
frame_index = len(session._video_frames)
|
|
1234
|
+
session._video_chapters.append((frame_index, title))
|
|
1235
|
+
return {"success": True, "output": f"Chapter '{title}' added at frame {frame_index}."}
|
|
1236
|
+
|
|
1237
|
+
|
|
1129
1238
|
# -- PDF ---------------------------------------------------------------------
|
|
1130
1239
|
|
|
1131
1240
|
|
|
@@ -1174,6 +1283,98 @@ async def cmd_wait_for(session: Session, page, args: list, options: dict, cwd: s
|
|
|
1174
1283
|
return {"success": True, "output": f"Element is {state_arg}"}
|
|
1175
1284
|
|
|
1176
1285
|
|
|
1286
|
+
@register("show")
|
|
1287
|
+
async def cmd_show(session: Session, page, args: list, options: dict, cwd: str | None, state: DaemonState) -> dict:
|
|
1288
|
+
from patchright_cli.dashboard import start_dashboard_server
|
|
1289
|
+
|
|
1290
|
+
port = int(options.get("show-port", 9322))
|
|
1291
|
+
if port not in _dashboard_runners:
|
|
1292
|
+
runner, url = await start_dashboard_server(state, port=port)
|
|
1293
|
+
_dashboard_runners[port] = (runner, url)
|
|
1294
|
+
else:
|
|
1295
|
+
_, url = _dashboard_runners[port]
|
|
1296
|
+
return {"success": True, "output": f"Dashboard running at {url}"}
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
_CODEGEN_RECORDABLE = {
|
|
1300
|
+
"goto",
|
|
1301
|
+
"go-back",
|
|
1302
|
+
"go-forward",
|
|
1303
|
+
"reload",
|
|
1304
|
+
"click",
|
|
1305
|
+
"dblclick",
|
|
1306
|
+
"fill",
|
|
1307
|
+
"type",
|
|
1308
|
+
"hover",
|
|
1309
|
+
"select",
|
|
1310
|
+
"check",
|
|
1311
|
+
"uncheck",
|
|
1312
|
+
"drag",
|
|
1313
|
+
"press",
|
|
1314
|
+
"keydown",
|
|
1315
|
+
"keyup",
|
|
1316
|
+
"mousemove",
|
|
1317
|
+
"mousedown",
|
|
1318
|
+
"mouseup",
|
|
1319
|
+
"mousewheel",
|
|
1320
|
+
"scroll",
|
|
1321
|
+
"scroll-to",
|
|
1322
|
+
"wait",
|
|
1323
|
+
"wait-for",
|
|
1324
|
+
"tab-new",
|
|
1325
|
+
"tab-close",
|
|
1326
|
+
"tab-select",
|
|
1327
|
+
"upload",
|
|
1328
|
+
"resize",
|
|
1329
|
+
"cookie-set",
|
|
1330
|
+
"cookie-delete",
|
|
1331
|
+
"cookie-clear",
|
|
1332
|
+
"localstorage-set",
|
|
1333
|
+
"localstorage-delete",
|
|
1334
|
+
"localstorage-clear",
|
|
1335
|
+
"sessionstorage-set",
|
|
1336
|
+
"sessionstorage-delete",
|
|
1337
|
+
"sessionstorage-clear",
|
|
1338
|
+
"state-load",
|
|
1339
|
+
"route",
|
|
1340
|
+
"unroute",
|
|
1341
|
+
"network-state-set",
|
|
1342
|
+
"dialog-accept",
|
|
1343
|
+
"dialog-dismiss",
|
|
1344
|
+
"screenshot",
|
|
1345
|
+
"pdf",
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
@register("codegen")
|
|
1350
|
+
async def cmd_codegen(session: Session, page, args: list, options: dict, cwd: str | None, state: DaemonState) -> dict:
|
|
1351
|
+
session._codegen = []
|
|
1352
|
+
if page:
|
|
1353
|
+
url = page.url
|
|
1354
|
+
session._codegen.append(f'patchright-cli open "{url}"')
|
|
1355
|
+
return {"success": True, "output": "Recording started. Run 'codegen-stop' to save the script."}
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
@register("codegen-stop")
|
|
1359
|
+
async def cmd_codegen_stop(
|
|
1360
|
+
session: Session, page, args: list, options: dict, cwd: str | None, state: DaemonState
|
|
1361
|
+
) -> dict:
|
|
1362
|
+
if session._codegen is None:
|
|
1363
|
+
return {"success": False, "output": "No recording in progress. Run 'codegen' first."}
|
|
1364
|
+
lines = ["#!/usr/bin/env bash", "set -e", ""] + session._codegen
|
|
1365
|
+
script = "\n".join(lines) + "\n"
|
|
1366
|
+
session._codegen = None
|
|
1367
|
+
|
|
1368
|
+
base = Path(cwd) if cwd else Path.cwd()
|
|
1369
|
+
snap_dir = base / ".patchright-cli"
|
|
1370
|
+
snap_dir.mkdir(parents=True, exist_ok=True)
|
|
1371
|
+
ts = int(time.time() * 1000)
|
|
1372
|
+
filepath = args[0] if args else str(snap_dir / f"script-{ts}.sh")
|
|
1373
|
+
Path(filepath).parent.mkdir(parents=True, exist_ok=True)
|
|
1374
|
+
Path(filepath).write_text(script, encoding="utf-8")
|
|
1375
|
+
return {"success": True, "output": f"Saved {len(lines) - 3} command(s) to {filepath}"}
|
|
1376
|
+
|
|
1377
|
+
|
|
1177
1378
|
# ---------------------------------------------------------------------------
|
|
1178
1379
|
# Command dispatch
|
|
1179
1380
|
# ---------------------------------------------------------------------------
|
|
@@ -1202,6 +1403,36 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
1202
1403
|
profile=options.get("profile"),
|
|
1203
1404
|
proxy=options.get("proxy"),
|
|
1204
1405
|
url=url,
|
|
1406
|
+
device=options.get("device"),
|
|
1407
|
+
viewport=options.get("viewport"),
|
|
1408
|
+
locale=options.get("locale"),
|
|
1409
|
+
timezone=options.get("timezone"),
|
|
1410
|
+
geolocation=options.get("geolocation"),
|
|
1411
|
+
user_agent=options.get("user-agent") or options.get("userAgent"),
|
|
1412
|
+
grant_permissions=options.get("grant-permissions") or options.get("grantPermissions"),
|
|
1413
|
+
)
|
|
1414
|
+
if session.page:
|
|
1415
|
+
session.push_history(session.page.url)
|
|
1416
|
+
return await _page_info(session, cwd)
|
|
1417
|
+
|
|
1418
|
+
if cmd == "attach":
|
|
1419
|
+
cdp = options.get("cdp")
|
|
1420
|
+
if not cdp:
|
|
1421
|
+
return {"success": False, "output": "Usage: attach --cdp=<url>"}
|
|
1422
|
+
headless = options.get("headless", False)
|
|
1423
|
+
session = await state.get_or_create_session(
|
|
1424
|
+
session_name,
|
|
1425
|
+
headless=headless,
|
|
1426
|
+
cdp_endpoint=cdp,
|
|
1427
|
+
cdp_headers=options.get("cdp-headers"),
|
|
1428
|
+
cdp_timeout=int(options.get("cdp-timeout", 30000)),
|
|
1429
|
+
device=options.get("device"),
|
|
1430
|
+
viewport=options.get("viewport"),
|
|
1431
|
+
locale=options.get("locale"),
|
|
1432
|
+
timezone=options.get("timezone"),
|
|
1433
|
+
geolocation=options.get("geolocation"),
|
|
1434
|
+
user_agent=options.get("user-agent") or options.get("userAgent"),
|
|
1435
|
+
grant_permissions=options.get("grant-permissions") or options.get("grantPermissions"),
|
|
1205
1436
|
)
|
|
1206
1437
|
if session.page:
|
|
1207
1438
|
session.push_history(session.page.url)
|
|
@@ -1218,6 +1449,11 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
1218
1449
|
return {"success": True, "output": f"Profile data deleted for '{session_name}'."}
|
|
1219
1450
|
return {"success": False, "output": "No persistent profile to delete."}
|
|
1220
1451
|
|
|
1452
|
+
# Dashboard command — no session required
|
|
1453
|
+
if cmd == "show":
|
|
1454
|
+
handler = COMMAND_HANDLERS.get("show")
|
|
1455
|
+
return await handler(None, None, args, options, cwd, state)
|
|
1456
|
+
|
|
1221
1457
|
# All other commands require an existing session
|
|
1222
1458
|
session = state.sessions.get(session_name)
|
|
1223
1459
|
if session is None:
|
|
@@ -1272,11 +1508,26 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
1272
1508
|
"output": "No page open. Run 'tab-new' to create one, or 'close' and 'open' again.",
|
|
1273
1509
|
}
|
|
1274
1510
|
|
|
1511
|
+
await _apply_timeouts(page, options)
|
|
1512
|
+
|
|
1275
1513
|
handler = COMMAND_HANDLERS.get(cmd)
|
|
1276
1514
|
if handler is None:
|
|
1277
1515
|
return {"success": False, "output": f"Unknown command: {cmd}"}
|
|
1278
1516
|
|
|
1279
|
-
|
|
1517
|
+
result = await handler(session, page, args, options, cwd, state)
|
|
1518
|
+
if result.get("success") and session._codegen is not None and cmd in _CODEGEN_RECORDABLE:
|
|
1519
|
+
args_quoted = [f'"{a}"' if " " in a else a for a in args]
|
|
1520
|
+
opt_parts = []
|
|
1521
|
+
for k, v in options.items():
|
|
1522
|
+
if k in ("session", "headless", "persistent", "profile", "proxy"):
|
|
1523
|
+
continue
|
|
1524
|
+
if v is True:
|
|
1525
|
+
opt_parts.append(f"--{k}")
|
|
1526
|
+
elif isinstance(v, str):
|
|
1527
|
+
opt_parts.append(f"--{k}={v}")
|
|
1528
|
+
cmd_line = " ".join(["patchright-cli"] + opt_parts + [cmd] + args_quoted)
|
|
1529
|
+
session._codegen.append(cmd_line)
|
|
1530
|
+
return result
|
|
1280
1531
|
|
|
1281
1532
|
except Exception as e:
|
|
1282
1533
|
logger.error("Command %s failed: %s", cmd, traceback.format_exc())
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Web dashboard for monitoring patchright-cli sessions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import base64
|
|
7
|
+
|
|
8
|
+
from aiohttp import web
|
|
9
|
+
|
|
10
|
+
HTML = """
|
|
11
|
+
<!DOCTYPE html>
|
|
12
|
+
<html>
|
|
13
|
+
<head>
|
|
14
|
+
<meta charset="utf-8">
|
|
15
|
+
<title>patchright-cli dashboard</title>
|
|
16
|
+
<style>
|
|
17
|
+
body { font-family: sans-serif; margin: 0; padding: 20px; background: #111; color: #eee; }
|
|
18
|
+
h1 { margin-top: 0; }
|
|
19
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }
|
|
20
|
+
.card { background: #222; border-radius: 8px; padding: 12px; }
|
|
21
|
+
.card h3 { margin: 0 0 8px; font-size: 14px; }
|
|
22
|
+
.card img { width: 100%; border-radius: 4px; background: #333; }
|
|
23
|
+
.card p { margin: 4px 0; font-size: 12px; color: #aaa; }
|
|
24
|
+
.status { font-size: 11px; color: #888; }
|
|
25
|
+
</style>
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<h1>patchright-cli sessions</h1>
|
|
29
|
+
<div id="grid" class="grid"></div>
|
|
30
|
+
<script>
|
|
31
|
+
const grid = document.getElementById('grid');
|
|
32
|
+
const ws = new WebSocket(`ws://${location.host}/ws`);
|
|
33
|
+
const sessions = {};
|
|
34
|
+
|
|
35
|
+
ws.onmessage = (ev) => {
|
|
36
|
+
const data = JSON.parse(ev.data);
|
|
37
|
+
if (!data.sessions) return;
|
|
38
|
+
data.sessions.forEach(s => {
|
|
39
|
+
sessions[s.name] = s;
|
|
40
|
+
});
|
|
41
|
+
render();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function render() {
|
|
45
|
+
grid.innerHTML = '';
|
|
46
|
+
Object.values(sessions).forEach(s => {
|
|
47
|
+
const div = document.createElement('div');
|
|
48
|
+
div.className = 'card';
|
|
49
|
+
div.innerHTML = `
|
|
50
|
+
<h3>${s.name}</h3>
|
|
51
|
+
<p>${s.url}</p>
|
|
52
|
+
<p class="status">${s.title} — ${s.tabs} tab(s)</p>
|
|
53
|
+
${s.screenshot ? `<img src="data:image/png;base64,${s.screenshot}" />` : '<p class="status">No screenshot yet</p>'}
|
|
54
|
+
`;
|
|
55
|
+
grid.appendChild(div);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
</script>
|
|
59
|
+
</body>
|
|
60
|
+
</html>
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DashboardState:
|
|
65
|
+
def __init__(self, daemon_state):
|
|
66
|
+
self.daemon_state = daemon_state
|
|
67
|
+
self._screenshots: dict[str, str | None] = {}
|
|
68
|
+
|
|
69
|
+
async def capture_loop(self):
|
|
70
|
+
while True:
|
|
71
|
+
await asyncio.sleep(1)
|
|
72
|
+
for name, session in list(self.daemon_state.sessions.items()):
|
|
73
|
+
page = session.page
|
|
74
|
+
if page is None:
|
|
75
|
+
self._screenshots[name] = None
|
|
76
|
+
continue
|
|
77
|
+
try:
|
|
78
|
+
data = await page.screenshot(type="png")
|
|
79
|
+
self._screenshots[name] = base64.b64encode(data).decode()
|
|
80
|
+
except Exception:
|
|
81
|
+
self._screenshots[name] = None
|
|
82
|
+
|
|
83
|
+
def _session_payload(self):
|
|
84
|
+
sessions = []
|
|
85
|
+
for name, session in self.daemon_state.sessions.items():
|
|
86
|
+
page = session.page
|
|
87
|
+
sessions.append(
|
|
88
|
+
{
|
|
89
|
+
"name": name,
|
|
90
|
+
"url": page.url if page else "",
|
|
91
|
+
"title": "",
|
|
92
|
+
"tabs": len(session.pages),
|
|
93
|
+
"screenshot": self._screenshots.get(name),
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
return {"sessions": sessions}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def index(request: web.Request):
|
|
100
|
+
return web.Response(text=HTML, content_type="text/html")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def api_sessions(request: web.Request):
|
|
104
|
+
state: DashboardState = request.app["dashboard_state"]
|
|
105
|
+
return web.json_response(state._session_payload())
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def websocket_handler(request: web.Request):
|
|
109
|
+
ws = web.WebSocketResponse()
|
|
110
|
+
await ws.prepare(request)
|
|
111
|
+
state: DashboardState = request.app["dashboard_state"]
|
|
112
|
+
try:
|
|
113
|
+
while not ws.closed:
|
|
114
|
+
await ws.send_json(state._session_payload())
|
|
115
|
+
await asyncio.sleep(1)
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
return ws
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def start_dashboard_server(daemon_state, port: int = 9322):
|
|
122
|
+
dashboard_state = DashboardState(daemon_state)
|
|
123
|
+
app = web.Application()
|
|
124
|
+
app["dashboard_state"] = dashboard_state
|
|
125
|
+
app.router.add_get("/", index)
|
|
126
|
+
app.router.add_get("/api/sessions", api_sessions)
|
|
127
|
+
app.router.add_get("/ws", websocket_handler)
|
|
128
|
+
|
|
129
|
+
runner = web.AppRunner(app)
|
|
130
|
+
await runner.setup()
|
|
131
|
+
site = web.TCPSite(runner, "127.0.0.1", port)
|
|
132
|
+
await site.start()
|
|
133
|
+
|
|
134
|
+
asyncio.create_task(dashboard_state.capture_loop())
|
|
135
|
+
return runner, f"http://127.0.0.1:{port}"
|
patchright_cli/ref_registry.py
CHANGED
|
@@ -31,7 +31,7 @@ class RefRegistry:
|
|
|
31
31
|
self.entries: dict[str, AriaRefEntry] = {}
|
|
32
32
|
self._counter = 0
|
|
33
33
|
|
|
34
|
-
def parse(self, aria_text: str) -> str:
|
|
34
|
+
def parse(self, aria_text: str, max_depth: int | None = None) -> str:
|
|
35
35
|
"""Return annotated snapshot text with [ref=eN] tags inserted."""
|
|
36
36
|
self.entries.clear()
|
|
37
37
|
self._counter = 0
|
|
@@ -44,6 +44,12 @@ class RefRegistry:
|
|
|
44
44
|
result_lines.append(line)
|
|
45
45
|
continue
|
|
46
46
|
|
|
47
|
+
indent = len(line) - len(line.lstrip())
|
|
48
|
+
depth = indent // 2
|
|
49
|
+
if max_depth is not None and depth > max_depth:
|
|
50
|
+
result_lines.append(line)
|
|
51
|
+
continue
|
|
52
|
+
|
|
47
53
|
role = m.group(1)
|
|
48
54
|
name = m.group(2) or ""
|
|
49
55
|
|
patchright_cli/snapshot.py
CHANGED
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from patchright_cli.ref_registry import RefRegistry
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
async def take_snapshot(page, root_element=None) -> tuple[str, RefRegistry]:
|
|
15
|
+
async def take_snapshot(page, root_element=None, max_depth: int | None = None) -> tuple[str, RefRegistry]:
|
|
16
16
|
"""Take a DOM snapshot. Returns (annotated_text, registry).
|
|
17
17
|
|
|
18
18
|
If root_element is provided, only snapshot the subtree under that element.
|
|
@@ -29,7 +29,7 @@ async def take_snapshot(page, root_element=None) -> tuple[str, RefRegistry]:
|
|
|
29
29
|
return "# Empty page - no accessible elements found\n", RefRegistry()
|
|
30
30
|
|
|
31
31
|
registry = RefRegistry()
|
|
32
|
-
annotated = registry.parse(aria_text)
|
|
32
|
+
annotated = registry.parse(aria_text, max_depth=max_depth)
|
|
33
33
|
return annotated + "\n", registry
|
|
34
34
|
|
|
35
35
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchright-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Anti-detect browser automation CLI using Patchright (undetected Playwright fork)
|
|
5
5
|
Project-URL: Homepage, https://github.com/AhaiMk01/patchright-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/AhaiMk01/patchright-cli
|
|
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
22
22
|
Classifier: Topic :: Software Development :: Testing
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: aiohttp>=3.9
|
|
24
25
|
Requires-Dist: click
|
|
25
26
|
Requires-Dist: patchright
|
|
26
27
|
Requires-Dist: pyyaml
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
patchright_cli/__init__.py,sha256=oB58iGqDXQVGaly01ZXz2e6NME0PUB_q3tsutDeGG-4,100
|
|
2
|
+
patchright_cli/__main__.py,sha256=iVuBuI_rPOENLhffOEHoS7JWCDBQciqT1kPXjRuhbZ4,127
|
|
3
|
+
patchright_cli/cli.py,sha256=NFNouWurV7Ccq6-yR6yaF9GfM3qtTxvtkS_X9Dn5PJE,19201
|
|
4
|
+
patchright_cli/daemon.py,sha256=nYJLTVs3X0n7giCZ1xH7IQXg0K80-goEu7t1Fq0JaMA,63687
|
|
5
|
+
patchright_cli/dashboard.py,sha256=aQHujW9tHTOQhbyqm4M6OknShLaJSnRFHFZRRfs3l20,4022
|
|
6
|
+
patchright_cli/ref_registry.py,sha256=OeXJFdOkEaJdqWAquykve654u0mYByt3-SKatAO5pOk,2413
|
|
7
|
+
patchright_cli/snapshot.py,sha256=mk3WO_caVIgVmUgb_NQok_wLrvYJhQ7dXPE59mHRtrk,1497
|
|
8
|
+
patchright_cli-0.4.1.dist-info/METADATA,sha256=Q0nYW6Q-HF2zGAuhwD-IgrzqEc9cfeDzDya6zcyxMAM,15526
|
|
9
|
+
patchright_cli-0.4.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
10
|
+
patchright_cli-0.4.1.dist-info/entry_points.txt,sha256=Syh904eOQA8ylakyqIvG4LmIHx3LMjgeNcnFap3O0NQ,59
|
|
11
|
+
patchright_cli-0.4.1.dist-info/licenses/LICENSE,sha256=adszqL4YR4pQUrc7LeAFxeg5Mc2cvljXp7QPlzKVWm8,10760
|
|
12
|
+
patchright_cli-0.4.1.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
patchright_cli/__init__.py,sha256=C_CFSqu0FaTOqyo9f_La4N54hGykX-OR-EX9QBt-bjE,100
|
|
2
|
-
patchright_cli/__main__.py,sha256=iVuBuI_rPOENLhffOEHoS7JWCDBQciqT1kPXjRuhbZ4,127
|
|
3
|
-
patchright_cli/cli.py,sha256=FzHkMDl2mI4Moh6MVN0xfdyfoqJX2ubyttN8QQJct9Y,14259
|
|
4
|
-
patchright_cli/daemon.py,sha256=Zwy35BFFzcXB9AX-Bz9Ms3YyqaBVlIyquTeegrnUfu8,54021
|
|
5
|
-
patchright_cli/ref_registry.py,sha256=_RhBC0xcVUhghWJLqWXV3ycWngtVCEnozbX-FVZ_UXM,2171
|
|
6
|
-
patchright_cli/snapshot.py,sha256=ZvJaikXEI9MOwBInI1rW0M3pFfla5bTFtGZYryQYM94,1446
|
|
7
|
-
patchright_cli-0.4.0.dist-info/METADATA,sha256=Z6iUn8P_vfylXnzCHHrUiPtMZGjXjaVC1tWe1VGzdiM,15498
|
|
8
|
-
patchright_cli-0.4.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
-
patchright_cli-0.4.0.dist-info/entry_points.txt,sha256=Syh904eOQA8ylakyqIvG4LmIHx3LMjgeNcnFap3O0NQ,59
|
|
10
|
-
patchright_cli-0.4.0.dist-info/licenses/LICENSE,sha256=adszqL4YR4pQUrc7LeAFxeg5Mc2cvljXp7QPlzKVWm8,10760
|
|
11
|
-
patchright_cli-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|