raindrop-cli 0.5.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.
rd_cli/cli.py ADDED
@@ -0,0 +1,757 @@
1
+ """Argument parsing and dispatch.
2
+
3
+ A shared ``common`` parent parser carries ``--json`` and ``--no-color`` onto
4
+ every subcommand, so they work in any position (``rd list --json`` as well as
5
+ ``rd --json list``). Each subparser stores its handler in ``func`` and whether
6
+ it needs an API client in ``needs_client``; ``main`` resolves the token and
7
+ builds the :class:`RaindropClient` only when required (config subcommands do
8
+ not touch the network).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import sys
15
+
16
+ from . import __version__, commands, completion, config, output
17
+ from .client import RaindropClient
18
+ from .errors import RaindropError
19
+ from .pinboard import PinboardClient
20
+
21
+ COLORS = "blue brown cyan gray green indigo orange pink purple red teal yellow".split()
22
+ VIEWS = ("list", "simple", "grid", "masonry")
23
+ SORTS = (
24
+ "-created",
25
+ "created",
26
+ "title",
27
+ "-title",
28
+ "domain",
29
+ "-domain",
30
+ "score",
31
+ "-sort",
32
+ )
33
+
34
+
35
+ def build_parser() -> argparse.ArgumentParser:
36
+ # SUPPRESS defaults so a flag given before the subcommand is not clobbered
37
+ # by the subparser re-parsing with its own default (see main() for the
38
+ # normalisation back to False).
39
+ common = argparse.ArgumentParser(add_help=False)
40
+ common.add_argument(
41
+ "--json", action="store_true", default=argparse.SUPPRESS, help="output raw JSON"
42
+ )
43
+ common.add_argument(
44
+ "--no-color",
45
+ action="store_true",
46
+ default=argparse.SUPPRESS,
47
+ help="disable ANSI colour",
48
+ )
49
+ common.add_argument(
50
+ "--dry-run",
51
+ action="store_true",
52
+ default=argparse.SUPPRESS,
53
+ help="preview writes (log method + payload) without calling the API",
54
+ )
55
+ # Lives on the shared parent for the same reason --dry-run does: it must
56
+ # work in any position, and threading it onto each destructive subparser
57
+ # would mean repeating the SUPPRESS dance six times.
58
+ common.add_argument(
59
+ "-y",
60
+ "--yes",
61
+ action="store_true",
62
+ default=argparse.SUPPRESS,
63
+ help="skip confirmation prompts on destructive operations",
64
+ )
65
+
66
+ parser = argparse.ArgumentParser(
67
+ prog="rd",
68
+ description="A command-line client for Raindrop.io.",
69
+ parents=[common],
70
+ )
71
+ parser.add_argument(
72
+ "--version", action="version", version=f"%(prog)s {__version__}"
73
+ )
74
+ sub = parser.add_subparsers(dest="command", metavar="<command>")
75
+
76
+ _add_raindrop_commands(sub, common)
77
+ _add_collection_commands(sub, common)
78
+ _add_tag_commands(sub, common)
79
+ _add_highlight_commands(sub, common)
80
+ _add_pinboard_commands(sub, common)
81
+ _add_misc_commands(sub, common)
82
+ _add_config_commands(sub, common)
83
+ _add_aliases(sub, common)
84
+ return parser
85
+
86
+
87
+ def _p(
88
+ sub, name, common, handler, *, needs_client=True, needs_pinboard=False, **kwargs
89
+ ):
90
+ """Register a subparser wired to ``handler`` with the common flags."""
91
+ parser = sub.add_parser(name, parents=[common], **kwargs)
92
+ parser.set_defaults(
93
+ func=handler, needs_client=needs_client, needs_pinboard=needs_pinboard
94
+ )
95
+ return parser
96
+
97
+
98
+ def _pb(sub, name, common, handler, **kwargs):
99
+ """Register a Pinboard subparser (wants a ``PinboardClient``, not Raindrop)."""
100
+ return _p(
101
+ sub, name, common, handler, needs_client=False, needs_pinboard=True, **kwargs
102
+ )
103
+
104
+
105
+ # -- raindrops ----------------------------------------------------------------
106
+
107
+
108
+ def _add_raindrop_commands(sub, common):
109
+ p = _p(sub, "list", common, commands.cmd_list, help="list raindrops")
110
+ p.add_argument(
111
+ "-c",
112
+ "--collection",
113
+ type=int,
114
+ default=0,
115
+ help="collection id (0 all, -1 unsorted, -99 trash)",
116
+ )
117
+ p.add_argument("-s", "--search", default="", help="search query")
118
+ p.add_argument("--sort", default="-created", choices=SORTS, help="sort order")
119
+ p.add_argument("--page", type=int, default=0, help="page number")
120
+ p.add_argument("--perpage", type=int, default=50, help="items per page (max 50)")
121
+ p.add_argument("-a", "--all", action="store_true", help="fetch all pages")
122
+ p.add_argument(
123
+ "-n", "--nested", action="store_true", help="include nested collections"
124
+ )
125
+ p.add_argument(
126
+ "-d", "--detailed", action="store_true", help="show excerpt, note, tags"
127
+ )
128
+
129
+ p = _p(
130
+ sub,
131
+ "search",
132
+ common,
133
+ commands.cmd_list,
134
+ help="search raindrops (alias of list -s)",
135
+ )
136
+ p.add_argument("search", help="search query")
137
+ p.add_argument("-c", "--collection", type=int, default=0, help="collection id")
138
+ p.add_argument("--sort", default="-created", choices=SORTS, help="sort order")
139
+ p.add_argument("--page", type=int, default=0, help="page number")
140
+ p.add_argument("--perpage", type=int, default=50, help="items per page (max 50)")
141
+ p.add_argument("-a", "--all", action="store_true", help="fetch all pages")
142
+ p.add_argument(
143
+ "-n", "--nested", action="store_true", help="include nested collections"
144
+ )
145
+ p.add_argument(
146
+ "-d", "--detailed", action="store_true", help="show excerpt, note, tags"
147
+ )
148
+
149
+ p = _p(sub, "view", common, commands.cmd_view, help="view a single raindrop")
150
+ p.add_argument("id", type=int, help="raindrop id")
151
+
152
+ p = _p(sub, "open", common, commands.cmd_open, help="open raindrop(s) in a browser")
153
+ p.add_argument("ids", type=int, nargs="+", help="raindrop id(s)")
154
+ p.add_argument(
155
+ "--cache",
156
+ "--permanent",
157
+ dest="cache",
158
+ action="store_true",
159
+ help="open the permanent copy instead of the original link (PRO)",
160
+ )
161
+ p.add_argument(
162
+ "-p",
163
+ "--print",
164
+ dest="print_url",
165
+ action="store_true",
166
+ help="print the URL instead of launching a browser",
167
+ )
168
+
169
+ p = _p(sub, "add", common, commands.cmd_add, help="add a raindrop (or many)")
170
+ p.add_argument("url", nargs="?", help="URL to bookmark")
171
+ p.add_argument("-t", "--title", help="custom title")
172
+ p.add_argument(
173
+ "-c",
174
+ "--collection",
175
+ type=int,
176
+ default=-1,
177
+ help="collection id (default -1 unsorted)",
178
+ )
179
+ p.add_argument("--tags", nargs="*", help="tags")
180
+ p.add_argument("--excerpt", help="excerpt / description")
181
+ p.add_argument("--note", help="note")
182
+ p.add_argument("--important", action="store_true", help="mark as favourite")
183
+ p.add_argument(
184
+ "--no-parse", action="store_true", help="skip background metadata parsing"
185
+ )
186
+ p.add_argument("--file", help="add many: read one URL per line from a file")
187
+ p.add_argument(
188
+ "--stdin",
189
+ action="store_true",
190
+ help="add many: read one URL per line from stdin",
191
+ )
192
+
193
+ p = _p(sub, "edit", common, commands.cmd_edit, help="edit a raindrop")
194
+ p.add_argument("id", type=int, help="raindrop id")
195
+ p.add_argument("-t", "--title", help="new title")
196
+ p.add_argument("--tags", nargs="*", help="replace tags")
197
+ p.add_argument("-c", "--collection", type=int, help="move to collection id")
198
+ p.add_argument("--excerpt", help="new excerpt")
199
+ p.add_argument("--note", help="new note")
200
+ p.add_argument("--important", action="store_true", help="mark as favourite")
201
+ p.add_argument("--not-important", action="store_true", help="unmark as favourite")
202
+
203
+ p = _p(sub, "rm", common, commands.cmd_rm, help="remove raindrop(s) (to trash)")
204
+ p.add_argument("ids", type=int, nargs="*", help="raindrop id(s)")
205
+ p.add_argument(
206
+ "--from",
207
+ dest="from_collection",
208
+ type=int,
209
+ help="scope: remove all raindrops in this collection",
210
+ )
211
+ p.add_argument("-s", "--search", default="", help="scope: filter by search query")
212
+ p.add_argument(
213
+ "-n", "--nested", action="store_true", help="scope: include nested collections"
214
+ )
215
+ p.add_argument(
216
+ "--permanent", action="store_true", help="delete permanently (skip trash)"
217
+ )
218
+
219
+ p = _p(sub, "mv", common, commands.cmd_mv, help="move raindrop(s) to a collection")
220
+ p.add_argument("collection", type=int, help="destination collection id")
221
+ p.add_argument("ids", type=int, nargs="*", help="raindrop id(s) to move")
222
+ p.add_argument(
223
+ "--from",
224
+ dest="from_collection",
225
+ type=int,
226
+ help="scope: move all raindrops from this source collection",
227
+ )
228
+ p.add_argument("-s", "--search", default="", help="scope: filter by search query")
229
+ p.add_argument(
230
+ "-n", "--nested", action="store_true", help="scope: include nested collections"
231
+ )
232
+
233
+ p = _p(sub, "tag", common, commands.cmd_tag, help="add/remove tags on raindrop(s)")
234
+ p.add_argument("ids", type=int, nargs="*", help="raindrop id(s)")
235
+ p.add_argument("--add", nargs="+", help="tags to add")
236
+ p.add_argument("--remove", nargs="+", help="tags to remove (id mode only)")
237
+ p.add_argument("--clear", action="store_true", help="remove all tags first")
238
+ p.add_argument(
239
+ "--from",
240
+ dest="from_collection",
241
+ type=int,
242
+ help="scope: apply to all raindrops in this collection",
243
+ )
244
+ p.add_argument("-s", "--search", default="", help="scope: filter by search query")
245
+ p.add_argument(
246
+ "-n", "--nested", action="store_true", help="scope: include nested collections"
247
+ )
248
+
249
+ p = _p(sub, "cover", common, commands.cmd_cover, help="upload a raindrop cover")
250
+ p.add_argument("id", type=int, help="raindrop id")
251
+ p.add_argument("file", help="image file (PNG, GIF, or JPEG)")
252
+
253
+ p = _p(
254
+ sub,
255
+ "import",
256
+ common,
257
+ commands.cmd_import,
258
+ help="parse/import a Netscape/Pocket/Instapaper HTML export",
259
+ )
260
+ p.add_argument("file", help="HTML bookmark export file")
261
+ p.add_argument(
262
+ "--create",
263
+ action="store_true",
264
+ help="actually create the parsed bookmarks (default: just parse)",
265
+ )
266
+ p.add_argument(
267
+ "-c", "--collection", type=int, default=-1, help="destination collection id"
268
+ )
269
+
270
+ p = _p(sub, "export", common, commands.cmd_export, help="export raindrops")
271
+ p.add_argument(
272
+ "-c", "--collection", type=int, default=0, help="collection id (0 all)"
273
+ )
274
+ p.add_argument(
275
+ "-f",
276
+ "--format",
277
+ default="csv",
278
+ choices=("csv", "html", "zip"),
279
+ help="export format",
280
+ )
281
+ p.add_argument("--sort", default="-created", choices=SORTS, help="sort order")
282
+ p.add_argument("-s", "--search", default="", help="search query")
283
+ p.add_argument("-o", "--output", help="write to file instead of stdout")
284
+
285
+
286
+ # -- collections --------------------------------------------------------------
287
+
288
+
289
+ def _add_collection_commands(sub, common):
290
+ c = sub.add_parser("collections", aliases=["c"], help="manage collections")
291
+ csub = c.add_subparsers(dest="subcommand", metavar="<action>", required=True)
292
+
293
+ _p(
294
+ csub,
295
+ "list",
296
+ common,
297
+ commands.cmd_collections_list,
298
+ help="list root collections",
299
+ )
300
+ _p(
301
+ csub,
302
+ "tree",
303
+ common,
304
+ commands.cmd_collections_tree,
305
+ help="nested collection tree",
306
+ )
307
+
308
+ p = _p(
309
+ csub, "view", common, commands.cmd_collections_view, help="view a collection"
310
+ )
311
+ p.add_argument("id", type=int, help="collection id")
312
+
313
+ p = _p(
314
+ csub, "add", common, commands.cmd_collections_add, help="create a collection"
315
+ )
316
+ p.add_argument("title", help="collection title")
317
+ p.add_argument("--view", choices=VIEWS, help="view style")
318
+ p.add_argument("--parent", type=int, help="parent collection id")
319
+ p.add_argument("--public", action="store_true", help="make public")
320
+
321
+ p = _p(
322
+ csub, "edit", common, commands.cmd_collections_edit, help="edit a collection"
323
+ )
324
+ p.add_argument("id", type=int, help="collection id")
325
+ p.add_argument("-t", "--title", help="new title")
326
+ p.add_argument("--view", choices=VIEWS, help="view style")
327
+ p.add_argument("--parent", type=int, help="move under parent id")
328
+ p.add_argument("--public", action="store_true", help="make public")
329
+ p.add_argument("--private", action="store_true", help="make private")
330
+
331
+ p = _p(csub, "rm", common, commands.cmd_collections_rm, help="delete a collection")
332
+ p.add_argument("id", type=int, help="collection id")
333
+
334
+ p = _p(
335
+ csub, "merge", common, commands.cmd_collections_merge, help="merge collections"
336
+ )
337
+ p.add_argument("to", type=int, help="destination collection id")
338
+ p.add_argument("ids", type=int, nargs="+", help="source collection ids")
339
+
340
+ _p(
341
+ csub,
342
+ "clean",
343
+ common,
344
+ commands.cmd_collections_clean,
345
+ help="remove empty collections",
346
+ )
347
+ _p(
348
+ csub,
349
+ "empty-trash",
350
+ common,
351
+ commands.cmd_collections_empty_trash,
352
+ help="permanently empty trash",
353
+ )
354
+
355
+ p = _p(
356
+ csub,
357
+ "reorder",
358
+ common,
359
+ commands.cmd_collections_reorder,
360
+ help="reorder all collections",
361
+ )
362
+ p.add_argument(
363
+ "--by",
364
+ default="title",
365
+ choices=("title", "-title", "-count"),
366
+ help="sort key",
367
+ )
368
+
369
+ p = _p(
370
+ csub,
371
+ "cover",
372
+ common,
373
+ commands.cmd_collections_cover,
374
+ help="upload a collection cover",
375
+ )
376
+ p.add_argument("id", type=int, help="collection id")
377
+ p.add_argument("file", help="image file (PNG, GIF, or JPEG)")
378
+
379
+ p = _p(
380
+ csub,
381
+ "covers",
382
+ common,
383
+ commands.cmd_collections_covers,
384
+ help="search the icon/cover library",
385
+ )
386
+ p.add_argument("text", help="search text (e.g. 'pokemon')")
387
+
388
+
389
+ # -- tags ---------------------------------------------------------------------
390
+
391
+
392
+ def _add_tag_commands(sub, common):
393
+ t = sub.add_parser("tags", aliases=["t"], help="manage tags")
394
+ tsub = t.add_subparsers(dest="subcommand", metavar="<action>", required=True)
395
+
396
+ p = _p(tsub, "list", common, commands.cmd_tags_list, help="list tags")
397
+ p.add_argument("-c", "--collection", type=int, help="restrict to collection id")
398
+
399
+ p = _p(tsub, "rename", common, commands.cmd_tags_rename, help="rename a tag")
400
+ p.add_argument("old", help="existing tag")
401
+ p.add_argument("new", help="new name")
402
+ p.add_argument("-c", "--collection", type=int, help="restrict to collection id")
403
+
404
+ p = _p(tsub, "merge", common, commands.cmd_tags_merge, help="merge tags into one")
405
+ p.add_argument("into", help="destination tag name")
406
+ p.add_argument("tags", nargs="+", help="tags to merge")
407
+ p.add_argument("-c", "--collection", type=int, help="restrict to collection id")
408
+
409
+ p = _p(tsub, "rm", common, commands.cmd_tags_rm, help="delete tags")
410
+ p.add_argument("tags", nargs="+", help="tags to delete")
411
+ p.add_argument("-c", "--collection", type=int, help="restrict to collection id")
412
+
413
+
414
+ # -- highlights ---------------------------------------------------------------
415
+
416
+
417
+ def _add_highlight_commands(sub, common):
418
+ h = sub.add_parser("highlights", aliases=["h"], help="manage highlights")
419
+ hsub = h.add_subparsers(dest="subcommand", metavar="<action>", required=True)
420
+
421
+ p = _p(hsub, "list", common, commands.cmd_highlights_list, help="list highlights")
422
+ p.add_argument("-r", "--raindrop", type=int, help="highlights of one raindrop")
423
+ p.add_argument("-a", "--all", action="store_true", help="fetch all pages")
424
+ p.add_argument("--page", type=int, default=0, help="page number")
425
+ p.add_argument("--perpage", type=int, default=25, help="items per page (max 50)")
426
+
427
+ p = _p(hsub, "add", common, commands.cmd_highlights_add, help="add a highlight")
428
+ p.add_argument("raindrop", type=int, help="raindrop id")
429
+ p.add_argument("text", help="text to highlight")
430
+ p.add_argument("--color", default="yellow", choices=COLORS, help="highlight colour")
431
+ p.add_argument("--note", default="", help="note for the highlight")
432
+
433
+ p = _p(hsub, "edit", common, commands.cmd_highlights_edit, help="edit a highlight")
434
+ p.add_argument("raindrop", type=int, help="raindrop id")
435
+ p.add_argument("highlight", help="highlight id")
436
+ p.add_argument("--text", help="new text")
437
+ p.add_argument("--color", choices=COLORS, help="new colour")
438
+ p.add_argument("--note", help="new note")
439
+
440
+ p = _p(hsub, "rm", common, commands.cmd_highlights_rm, help="remove a highlight")
441
+ p.add_argument("raindrop", type=int, help="raindrop id")
442
+ p.add_argument("highlight", help="highlight id")
443
+
444
+
445
+ # -- pinboard -----------------------------------------------------------------
446
+
447
+
448
+ def _add_pinboard_commands(sub, common):
449
+ pb = sub.add_parser(
450
+ "pinboard", aliases=["pb"], help="manage Pinboard bookmarks (second service)"
451
+ )
452
+ psub = pb.add_subparsers(dest="subcommand", metavar="<action>", required=True)
453
+
454
+ p = _pb(psub, "list", common, commands.cmd_pb_list, help="list bookmarks")
455
+ p.add_argument("--tag", action="append", help="filter by tag (repeatable, max 3)")
456
+ p.add_argument("--count", type=int, default=15, help="recent count (max 100)")
457
+ p.add_argument("-a", "--all", action="store_true", help="fetch all bookmarks")
458
+ p.add_argument("--toread", action="store_true", help="only unread (to-read) items")
459
+ p.add_argument("-d", "--detailed", action="store_true", help="show description")
460
+
461
+ p = _pb(psub, "get", common, commands.cmd_pb_get, help="show one bookmark by URL")
462
+ p.add_argument("url", help="bookmark URL (Pinboard's key)")
463
+
464
+ p = _pb(psub, "add", common, commands.cmd_pb_add, help="add a bookmark")
465
+ p.add_argument("url", help="URL to bookmark")
466
+ p.add_argument("-t", "--title", help="title (Pinboard 'description')")
467
+ p.add_argument("--extended", help="extended note (Pinboard 'extended')")
468
+ p.add_argument("--tags", nargs="*", help="tags")
469
+ p.add_argument("--toread", action="store_true", help="mark unread")
470
+ p.add_argument("--shared", action="store_true", help="make public")
471
+ p.add_argument("--private", action="store_true", help="make private")
472
+ p.add_argument(
473
+ "--no-replace", action="store_true", help="fail if the URL is already saved"
474
+ )
475
+ p.add_argument("--dt", help="UTC datetime (YYYY-MM-DDTHH:MM:SSZ)")
476
+
477
+ p = _pb(psub, "rm", common, commands.cmd_pb_rm, help="delete a bookmark by URL")
478
+ p.add_argument("url", help="bookmark URL")
479
+
480
+ p = _pb(psub, "edit", common, commands.cmd_pb_edit, help="edit a bookmark")
481
+ p.add_argument("url", help="bookmark URL")
482
+ p.add_argument("-t", "--title", help="new title")
483
+ p.add_argument("--extended", help="new extended note")
484
+ p.add_argument("--tags", nargs="*", help="replace tags")
485
+ p.add_argument("--toread", action="store_true", help="mark unread")
486
+ p.add_argument("--not-toread", action="store_true", help="mark read")
487
+ p.add_argument("--shared", action="store_true", help="make public")
488
+ p.add_argument("--private", action="store_true", help="make private")
489
+
490
+ p = _pb(psub, "tag", common, commands.cmd_pb_tag, help="add/remove tags on a URL")
491
+ p.add_argument("url", help="bookmark URL")
492
+ p.add_argument("--add", nargs="+", help="tags to add")
493
+ p.add_argument("--remove", nargs="+", help="tags to remove")
494
+ p.add_argument("--clear", action="store_true", help="remove all tags first")
495
+
496
+ p = _pb(
497
+ psub, "suggest", common, commands.cmd_pb_suggest, help="suggest tags for a URL"
498
+ )
499
+ p.add_argument("url", help="URL to get tag suggestions for")
500
+
501
+ t = psub.add_parser("tags", help="manage Pinboard tags")
502
+ tsub = t.add_subparsers(dest="tagaction", metavar="<action>", required=True)
503
+ _pb(tsub, "list", common, commands.cmd_pb_tags_list, help="list tags")
504
+ pr = _pb(tsub, "rename", common, commands.cmd_pb_tags_rename, help="rename a tag")
505
+ pr.add_argument("old", help="existing tag")
506
+ pr.add_argument("new", help="new name")
507
+ prm = _pb(tsub, "rm", common, commands.cmd_pb_tags_rm, help="delete tag(s)")
508
+ prm.add_argument("tags", nargs="+", help="tags to delete")
509
+
510
+ n = psub.add_parser("notes", help="read Pinboard notes")
511
+ nsub = n.add_subparsers(dest="noteaction", metavar="<action>", required=True)
512
+ _pb(nsub, "list", common, commands.cmd_pb_notes_list, help="list notes")
513
+ nv = _pb(nsub, "view", common, commands.cmd_pb_notes_view, help="view a note")
514
+ nv.add_argument("id", help="note id")
515
+
516
+
517
+ # -- misc ---------------------------------------------------------------------
518
+
519
+
520
+ def _add_misc_commands(sub, common):
521
+ # `rd user` shows the account; `rd user set ...` updates it.
522
+ u = sub.add_parser(
523
+ "user", parents=[common], help="show or update the authenticated user"
524
+ )
525
+ u.set_defaults(func=commands.cmd_user, needs_client=True)
526
+ usub = u.add_subparsers(dest="subcommand", metavar="<action>")
527
+ _p(usub, "show", common, commands.cmd_user, help="show authenticated user")
528
+ us = _p(usub, "set", common, commands.cmd_user_set, help="update user settings")
529
+ us.add_argument("--name", help="full name")
530
+ us.add_argument("--email", help="email address")
531
+ us.add_argument("--new-password", help="new password (needs --old-password)")
532
+ us.add_argument("--old-password", help="current password")
533
+ us.add_argument(
534
+ "--config", nargs="+", metavar="KEY=VALUE", help="config key=value pairs"
535
+ )
536
+
537
+ _p(sub, "stats", common, commands.cmd_stats, help="system collection counts")
538
+
539
+ comp = _p(
540
+ sub,
541
+ "completion",
542
+ common,
543
+ commands.cmd_completion,
544
+ needs_client=False,
545
+ help="print a shell completion script",
546
+ )
547
+ comp.add_argument(
548
+ "shell",
549
+ choices=list(completion.SHELLS),
550
+ help="shell to generate completion for",
551
+ )
552
+
553
+ p = _p(
554
+ sub,
555
+ "sync",
556
+ common,
557
+ commands.cmd_sync,
558
+ needs_client=False,
559
+ help="two-way additive sync between Raindrop and Pinboard (try --dry-run)",
560
+ )
561
+ p.add_argument(
562
+ "--direction",
563
+ choices=("both", "to-pinboard", "to-raindrop"),
564
+ default="both",
565
+ help="limit which side is written (default both)",
566
+ )
567
+ p.add_argument(
568
+ "--collection",
569
+ type=int,
570
+ action="append",
571
+ help="scope: only push Raindrop items in this collection id (repeatable)",
572
+ )
573
+ p.add_argument(
574
+ "--rd-tag",
575
+ action="append",
576
+ help="scope: only push Raindrop items with this tag (repeatable)",
577
+ )
578
+ p.add_argument(
579
+ "--pb-tag",
580
+ action="append",
581
+ help="scope: only push Pinboard items with this tag (repeatable)",
582
+ )
583
+
584
+ p = _p(
585
+ sub,
586
+ "filters",
587
+ common,
588
+ commands.cmd_filters,
589
+ help="context filters for a collection",
590
+ )
591
+ p.add_argument(
592
+ "-c", "--collection", type=int, default=0, help="collection id (0 all)"
593
+ )
594
+ p.add_argument(
595
+ "--tags-sort", default="-count", choices=("-count", "_id"), help="tag sort"
596
+ )
597
+ p.add_argument("-s", "--search", default="", help="search query")
598
+
599
+ p = _p(
600
+ sub,
601
+ "suggest",
602
+ common,
603
+ commands.cmd_suggest,
604
+ help="suggest collections/tags for a URL",
605
+ )
606
+ group = p.add_mutually_exclusive_group(required=True)
607
+ group.add_argument("--url", help="suggest for a new URL")
608
+ group.add_argument("--id", type=int, help="suggest for an existing raindrop id")
609
+
610
+ p = _p(
611
+ sub,
612
+ "exists",
613
+ common,
614
+ commands.cmd_exists,
615
+ help="check if URL(s) are already saved",
616
+ )
617
+ p.add_argument("urls", nargs="+", help="URLs to check")
618
+
619
+ b = sub.add_parser("backups", help="manage backups")
620
+ bsub = b.add_subparsers(dest="subcommand", metavar="<action>", required=True)
621
+ _p(bsub, "list", common, commands.cmd_backups_list, help="list backups")
622
+ _p(bsub, "create", common, commands.cmd_backups_create, help="request a new backup")
623
+ p = _p(
624
+ bsub,
625
+ "download",
626
+ common,
627
+ commands.cmd_backups_download,
628
+ help="download a backup",
629
+ )
630
+ p.add_argument("id", help="backup id")
631
+ p.add_argument(
632
+ "-f", "--format", default="csv", choices=("csv", "html"), help="format"
633
+ )
634
+ p.add_argument("-o", "--output", help="output path")
635
+
636
+
637
+ # -- config -------------------------------------------------------------------
638
+
639
+
640
+ def _add_config_commands(sub, common):
641
+ c = sub.add_parser("config", help="manage rd-cli configuration")
642
+ csub = c.add_subparsers(dest="subcommand", metavar="<action>", required=True)
643
+ _p(
644
+ csub,
645
+ "path",
646
+ common,
647
+ commands.cfg_path,
648
+ needs_client=False,
649
+ help="print config file path",
650
+ )
651
+ _p(
652
+ csub,
653
+ "show",
654
+ common,
655
+ commands.cfg_show,
656
+ needs_client=False,
657
+ help="show config (token masked)",
658
+ )
659
+ p = _p(
660
+ csub,
661
+ "set-token",
662
+ common,
663
+ commands.cfg_set_token,
664
+ needs_client=False,
665
+ help="store the Raindrop API token",
666
+ )
667
+ p.add_argument("token", help="Raindrop.io test/access token")
668
+
669
+ p = _p(
670
+ csub,
671
+ "set-pinboard-token",
672
+ common,
673
+ commands.cfg_set_pinboard_token,
674
+ needs_client=False,
675
+ help="store the Pinboard API token",
676
+ )
677
+ p.add_argument("token", help="Pinboard token (format user:HEX)")
678
+
679
+
680
+ # -- back-compat aliases ------------------------------------------------------
681
+
682
+
683
+ def _add_aliases(sub, common):
684
+ """Hidden flat aliases for the original command names (no regression)."""
685
+ p = _p(sub, "c-list", common, commands.cmd_collections_list)
686
+ p = _p(sub, "c-add", common, commands.cmd_collections_add)
687
+ p.add_argument("title")
688
+ p.add_argument("--view", choices=VIEWS)
689
+ p.add_argument("--parent", type=int)
690
+ p.add_argument("--public", action="store_true")
691
+ p = _p(sub, "c-rm", common, commands.cmd_collections_rm)
692
+ p.add_argument("id", type=int)
693
+
694
+ p = _p(sub, "t-list", common, commands.cmd_tags_list)
695
+ p.add_argument("-c", "--collection", type=int)
696
+ p = _p(sub, "t-rm", common, commands.cmd_tags_rm)
697
+ p.add_argument("tags", nargs="+")
698
+ p.add_argument("-c", "--collection", type=int)
699
+
700
+ p = _p(sub, "h-list", common, commands.cmd_highlights_list)
701
+ p.add_argument("-r", "--raindrop", type=int)
702
+ p.add_argument("-a", "--all", action="store_true")
703
+ p.add_argument("--page", type=int, default=0)
704
+ p.add_argument("--perpage", type=int, default=25)
705
+ p = _p(sub, "h-add", common, commands.cmd_highlights_add)
706
+ p.add_argument("raindrop", type=int)
707
+ p.add_argument("text")
708
+ p.add_argument("--color", default="yellow", choices=COLORS)
709
+ p.add_argument("--note", default="")
710
+ p = _p(sub, "h-rm", common, commands.cmd_highlights_rm)
711
+ p.add_argument("raindrop", type=int)
712
+ p.add_argument("highlight")
713
+
714
+
715
+ def main(argv: list[str] | None = None) -> int:
716
+ if argv is None:
717
+ argv = sys.argv[1:]
718
+
719
+ parser = build_parser()
720
+ if not argv:
721
+ parser.print_help()
722
+ return 0
723
+
724
+ args = parser.parse_args(argv)
725
+ args.json = getattr(args, "json", False)
726
+ args.no_color = getattr(args, "no_color", False)
727
+ args.dry_run = getattr(args, "dry_run", False)
728
+ args.yes = getattr(args, "yes", False)
729
+ output.configure(no_color=args.no_color)
730
+
731
+ if not getattr(args, "func", None):
732
+ parser.print_help()
733
+ return 0
734
+
735
+ try:
736
+ client = None
737
+ if getattr(args, "needs_pinboard", False):
738
+ client = PinboardClient(
739
+ config.resolve_pinboard_token(), dry_run=args.dry_run
740
+ )
741
+ elif getattr(args, "needs_client", False):
742
+ client = RaindropClient(config.resolve_token(), dry_run=args.dry_run)
743
+ return args.func(client, args)
744
+ except RaindropError as exc:
745
+ if getattr(args, "json", False):
746
+ output.emit_json({"error": str(exc)})
747
+ else:
748
+ output.error(str(exc))
749
+ return 1
750
+ except KeyboardInterrupt:
751
+ return 130
752
+ except BrokenPipeError:
753
+ return 0
754
+
755
+
756
+ if __name__ == "__main__":
757
+ sys.exit(main())