patchright-cli 0.3.1__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 +128 -29
- patchright_cli/daemon.py +1243 -624
- patchright_cli/dashboard.py +135 -0
- patchright_cli/ref_registry.py +80 -0
- patchright_cli/snapshot.py +21 -321
- {patchright_cli-0.3.1.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.3.1.dist-info/RECORD +0 -10
- {patchright_cli-0.3.1.dist-info → patchright_cli-0.4.1.dist-info}/WHEEL +0 -0
- {patchright_cli-0.3.1.dist-info → patchright_cli-0.4.1.dist-info}/entry_points.txt +0 -0
- {patchright_cli-0.3.1.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,9 @@ 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]",
|
|
94
|
+
"text": "text <ref|selector> Get text content of element",
|
|
75
95
|
"screenshot": "screenshot [ref] Save screenshot [--full-page] [--filename=F]",
|
|
76
96
|
"drag": "drag <from> <to> Drag element to target",
|
|
77
97
|
"close": "close Close browser session",
|
|
@@ -79,6 +99,8 @@ COMMANDS_HELP = {
|
|
|
79
99
|
"go-back": "go-back Go back",
|
|
80
100
|
"go-forward": "go-forward Go forward",
|
|
81
101
|
"reload": "reload Reload page",
|
|
102
|
+
"url": "url Print current URL",
|
|
103
|
+
"title": "title Print page title",
|
|
82
104
|
# Keyboard
|
|
83
105
|
"press": "press <key> Press key",
|
|
84
106
|
"keydown": "keydown <key> Key down",
|
|
@@ -88,6 +110,11 @@ COMMANDS_HELP = {
|
|
|
88
110
|
"mousedown": "mousedown [button] Mouse button down",
|
|
89
111
|
"mouseup": "mouseup [button] Mouse button up",
|
|
90
112
|
"mousewheel": "mousewheel <dx> <dy> Scroll mouse wheel",
|
|
113
|
+
# Scroll & Wait
|
|
114
|
+
"scroll": "scroll <dx> <dy> Scroll by pixel offset",
|
|
115
|
+
"scroll-to": "scroll-to <ref> Scroll element into view",
|
|
116
|
+
"wait": "wait <ms> Wait for milliseconds",
|
|
117
|
+
"wait-for": "wait-for <ref> Wait for element to appear [--state=hidden]",
|
|
91
118
|
# Tabs
|
|
92
119
|
"tab-list": "tab-list List tabs",
|
|
93
120
|
"tab-new": "tab-new [url] Open new tab",
|
|
@@ -99,6 +126,8 @@ COMMANDS_HELP = {
|
|
|
99
126
|
"cookie-set": "cookie-set <n> <v> Set cookie [--domain --path --expires --httpOnly --secure --sameSite]",
|
|
100
127
|
"cookie-delete": "cookie-delete <name> Delete cookie",
|
|
101
128
|
"cookie-clear": "cookie-clear Clear all cookies",
|
|
129
|
+
"cookie-import": "cookie-import <file> Import cookies from JSON",
|
|
130
|
+
"cookie-export": "cookie-export [file] Export cookies to JSON",
|
|
102
131
|
"localstorage-list": "localstorage-list List localStorage",
|
|
103
132
|
"localstorage-get": "localstorage-get <k> Get localStorage item",
|
|
104
133
|
"localstorage-set": "localstorage-set <k> <v> Set localStorage item",
|
|
@@ -107,6 +136,8 @@ COMMANDS_HELP = {
|
|
|
107
136
|
# Dialog
|
|
108
137
|
"dialog-accept": "dialog-accept [text] Accept next dialog",
|
|
109
138
|
"dialog-dismiss": "dialog-dismiss Dismiss next dialog",
|
|
139
|
+
# Permissions
|
|
140
|
+
"grant-permissions": "grant-permissions <perms> Grant permissions [--origin=url]",
|
|
110
141
|
# Upload / Resize
|
|
111
142
|
"upload": "upload <file> [ref] Upload file to input",
|
|
112
143
|
"resize": "resize <w> <h> Resize viewport",
|
|
@@ -132,6 +163,7 @@ COMMANDS_HELP = {
|
|
|
132
163
|
# Video
|
|
133
164
|
"video-start": "video-start Start video recording",
|
|
134
165
|
"video-stop": "video-stop Stop recording and save [--filename=F]",
|
|
166
|
+
"video-chapter": "video-chapter <title> Add chapter marker to video",
|
|
135
167
|
# PDF
|
|
136
168
|
"pdf": "pdf [--filename=F] Save page as PDF",
|
|
137
169
|
# DevTools
|
|
@@ -142,6 +174,11 @@ COMMANDS_HELP = {
|
|
|
142
174
|
"close-all": "close-all Close all sessions",
|
|
143
175
|
"kill-all": "kill-all Kill all sessions",
|
|
144
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",
|
|
145
182
|
}
|
|
146
183
|
|
|
147
184
|
ALL_COMMANDS = list(COMMANDS_HELP.keys())
|
|
@@ -157,6 +194,18 @@ def _print_help():
|
|
|
157
194
|
click.echo(" --proxy=<url> Proxy server (e.g. http://host:port, socks5://host:port)")
|
|
158
195
|
click.echo(" -s=<name> Named session (default: 'default')")
|
|
159
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)")
|
|
160
209
|
click.echo(" --version Show version")
|
|
161
210
|
click.echo(" --help Show this help\n")
|
|
162
211
|
click.echo("Commands:")
|
|
@@ -166,6 +215,7 @@ def _print_help():
|
|
|
166
215
|
"Core",
|
|
167
216
|
[
|
|
168
217
|
"open",
|
|
218
|
+
"attach",
|
|
169
219
|
"goto",
|
|
170
220
|
"click",
|
|
171
221
|
"dblclick",
|
|
@@ -177,14 +227,16 @@ def _print_help():
|
|
|
177
227
|
"uncheck",
|
|
178
228
|
"snapshot",
|
|
179
229
|
"eval",
|
|
230
|
+
"text",
|
|
180
231
|
"screenshot",
|
|
181
232
|
"drag",
|
|
182
233
|
"close",
|
|
183
234
|
],
|
|
184
235
|
),
|
|
185
|
-
("Navigation", ["go-back", "go-forward", "reload"]),
|
|
236
|
+
("Navigation", ["go-back", "go-forward", "reload", "url", "title"]),
|
|
186
237
|
("Keyboard", ["press", "keydown", "keyup"]),
|
|
187
238
|
("Mouse", ["mousemove", "mousedown", "mouseup", "mousewheel"]),
|
|
239
|
+
("Scroll & Wait", ["scroll", "scroll-to", "wait", "wait-for"]),
|
|
188
240
|
("Tabs", ["tab-list", "tab-new", "tab-close", "tab-select"]),
|
|
189
241
|
("Dialog", ["dialog-accept", "dialog-dismiss"]),
|
|
190
242
|
("Upload/Resize", ["upload", "resize"]),
|
|
@@ -197,6 +249,8 @@ def _print_help():
|
|
|
197
249
|
"cookie-set",
|
|
198
250
|
"cookie-delete",
|
|
199
251
|
"cookie-clear",
|
|
252
|
+
"cookie-import",
|
|
253
|
+
"cookie-export",
|
|
200
254
|
"localstorage-list",
|
|
201
255
|
"localstorage-get",
|
|
202
256
|
"localstorage-set",
|
|
@@ -209,13 +263,16 @@ def _print_help():
|
|
|
209
263
|
"sessionstorage-clear",
|
|
210
264
|
],
|
|
211
265
|
),
|
|
266
|
+
("Permissions", ["grant-permissions"]),
|
|
212
267
|
("Route", ["route", "route-list", "unroute", "network-state-set"]),
|
|
213
268
|
("Code", ["run-code"]),
|
|
214
269
|
("Tracing", ["tracing-start", "tracing-stop"]),
|
|
215
|
-
("Video", ["video-start", "video-stop"]),
|
|
270
|
+
("Video", ["video-start", "video-stop", "video-chapter"]),
|
|
216
271
|
("PDF", ["pdf"]),
|
|
217
272
|
("DevTools", ["console", "network"]),
|
|
218
273
|
("Session", ["list", "close-all", "kill-all", "delete-data"]),
|
|
274
|
+
("Dashboard", ["show"]),
|
|
275
|
+
("Codegen", ["codegen", "codegen-stop"]),
|
|
219
276
|
]
|
|
220
277
|
for cat_name, cmds in categories:
|
|
221
278
|
click.echo(f"\n {cat_name}:")
|
|
@@ -233,8 +290,10 @@ def main():
|
|
|
233
290
|
persistent = False
|
|
234
291
|
profile = None
|
|
235
292
|
proxy = None
|
|
236
|
-
|
|
293
|
+
config_path = None
|
|
294
|
+
session_name = os.environ.get("PATCHRIGHT_CLI_SESSION", "default")
|
|
237
295
|
port = DEFAULT_PORT
|
|
296
|
+
extra_opts: dict = {}
|
|
238
297
|
|
|
239
298
|
# Extract options
|
|
240
299
|
remaining = []
|
|
@@ -273,6 +332,60 @@ def main():
|
|
|
273
332
|
except ValueError:
|
|
274
333
|
click.echo(f"Invalid port value: {argv[i]}", err=True)
|
|
275
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]
|
|
276
389
|
elif arg in ("--version", "-v"):
|
|
277
390
|
click.echo(f"patchright-cli {__version__}")
|
|
278
391
|
sys.exit(0)
|
|
@@ -297,16 +410,18 @@ def main():
|
|
|
297
410
|
|
|
298
411
|
# Separate --key=value and --flag from positional args
|
|
299
412
|
positional_args = []
|
|
300
|
-
|
|
413
|
+
cmd_opts: dict = {}
|
|
301
414
|
for a in args:
|
|
302
415
|
if a.startswith("--") and "=" in a:
|
|
303
416
|
k, v = a[2:].split("=", 1)
|
|
304
|
-
|
|
417
|
+
cmd_opts[k] = v
|
|
305
418
|
elif a.startswith("--"):
|
|
306
|
-
|
|
419
|
+
cmd_opts[a[2:]] = True
|
|
307
420
|
else:
|
|
308
421
|
positional_args.append(a)
|
|
309
422
|
args = positional_args
|
|
423
|
+
# Merge: global opts as base, command-level opts take precedence
|
|
424
|
+
extra_opts = {**extra_opts, **cmd_opts}
|
|
310
425
|
|
|
311
426
|
# For eval/run-code: support --file=<path> and stdin via "-"
|
|
312
427
|
if command in ("eval", "run-code"):
|
|
@@ -329,8 +444,9 @@ def main():
|
|
|
329
444
|
click.echo(f"'{command}' requires a JS expression, --file=<path>, or piped stdin.", err=True)
|
|
330
445
|
sys.exit(1)
|
|
331
446
|
|
|
332
|
-
# Build options dict
|
|
333
|
-
|
|
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)}
|
|
334
450
|
if headless:
|
|
335
451
|
options["headless"] = True
|
|
336
452
|
if persistent:
|
|
@@ -340,26 +456,9 @@ def main():
|
|
|
340
456
|
if proxy:
|
|
341
457
|
options["proxy"] = proxy
|
|
342
458
|
|
|
343
|
-
# Ensure daemon is running (auto-start for
|
|
459
|
+
# Ensure daemon is running (auto-start for all commands)
|
|
344
460
|
try:
|
|
345
|
-
|
|
346
|
-
ensure_daemon_running(port, headless)
|
|
347
|
-
else:
|
|
348
|
-
# Try connecting first; if fails, tell user to open
|
|
349
|
-
import socket as _socket
|
|
350
|
-
|
|
351
|
-
sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
|
|
352
|
-
try:
|
|
353
|
-
sock.settimeout(1)
|
|
354
|
-
sock.connect(("127.0.0.1", port))
|
|
355
|
-
except (ConnectionRefusedError, OSError, TimeoutError):
|
|
356
|
-
click.echo(
|
|
357
|
-
f"Daemon is not running on port {port}. Run 'patchright-cli open' first.",
|
|
358
|
-
err=True,
|
|
359
|
-
)
|
|
360
|
-
sys.exit(1)
|
|
361
|
-
finally:
|
|
362
|
-
sock.close()
|
|
461
|
+
ensure_daemon_running(port, headless)
|
|
363
462
|
except RuntimeError as e:
|
|
364
463
|
click.echo(str(e), err=True)
|
|
365
464
|
sys.exit(1)
|