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.
@@ -1,3 +1,3 @@
1
1
  """patchright-cli — Undetected browser automation CLI using Patchright."""
2
2
 
3
- __version__ = "0.3.1"
3
+ __version__ = "0.4.1"
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> Evaluate JavaScript [--file=F or stdin]",
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
- session_name = "default"
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
- extra_opts = {}
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
- extra_opts[k] = v
417
+ cmd_opts[k] = v
305
418
  elif a.startswith("--"):
306
- extra_opts[a[2:]] = True
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
- options = {"session": session_name, **extra_opts}
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 'open', require for others)
459
+ # Ensure daemon is running (auto-start for all commands)
344
460
  try:
345
- if command == "open":
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)