pyzotero 1.8.0__py3-none-any.whl → 1.9.0__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.
- pyzotero/cli.py +369 -3
- {pyzotero-1.8.0.dist-info → pyzotero-1.9.0.dist-info}/METADATA +1 -1
- {pyzotero-1.8.0.dist-info → pyzotero-1.9.0.dist-info}/RECORD +5 -5
- {pyzotero-1.8.0.dist-info → pyzotero-1.9.0.dist-info}/WHEEL +0 -0
- {pyzotero-1.8.0.dist-info → pyzotero-1.9.0.dist-info}/entry_points.txt +0 -0
pyzotero/cli.py
CHANGED
|
@@ -83,12 +83,23 @@ def main(ctx, locale):
|
|
|
83
83
|
"--collection",
|
|
84
84
|
help="Filter by collection key (returns only items in this collection)",
|
|
85
85
|
)
|
|
86
|
+
@click.option(
|
|
87
|
+
"--tag",
|
|
88
|
+
multiple=True,
|
|
89
|
+
help="Filter by tag (can be specified multiple times for AND search)",
|
|
90
|
+
)
|
|
86
91
|
@click.option(
|
|
87
92
|
"--limit",
|
|
88
93
|
type=int,
|
|
89
94
|
default=1000000,
|
|
90
95
|
help="Maximum number of results to return (default: 1000000)",
|
|
91
96
|
)
|
|
97
|
+
@click.option(
|
|
98
|
+
"--offset",
|
|
99
|
+
type=int,
|
|
100
|
+
default=0,
|
|
101
|
+
help="Number of results to skip for pagination (default: 0)",
|
|
102
|
+
)
|
|
92
103
|
@click.option(
|
|
93
104
|
"--json",
|
|
94
105
|
"output_json",
|
|
@@ -96,7 +107,7 @@ def main(ctx, locale):
|
|
|
96
107
|
help="Output results as JSON",
|
|
97
108
|
)
|
|
98
109
|
@click.pass_context
|
|
99
|
-
def search(ctx, query, fulltext, itemtype, collection, limit, output_json): # noqa: PLR0912, PLR0915
|
|
110
|
+
def search(ctx, query, fulltext, itemtype, collection, tag, limit, offset, output_json): # noqa: PLR0912, PLR0915
|
|
100
111
|
"""Search local Zotero library.
|
|
101
112
|
|
|
102
113
|
By default, searches top-level items in titles and metadata.
|
|
@@ -116,6 +127,10 @@ def search(ctx, query, fulltext, itemtype, collection, limit, output_json): # n
|
|
|
116
127
|
|
|
117
128
|
pyzotero search -q "climate" --json
|
|
118
129
|
|
|
130
|
+
pyzotero search -q "topic" --limit 20 --offset 20 --json
|
|
131
|
+
|
|
132
|
+
pyzotero search -q "topic" --tag "climate" --tag "adaptation" --json
|
|
133
|
+
|
|
119
134
|
"""
|
|
120
135
|
try:
|
|
121
136
|
locale = ctx.obj.get("locale", "en-US")
|
|
@@ -124,6 +139,9 @@ def search(ctx, query, fulltext, itemtype, collection, limit, output_json): # n
|
|
|
124
139
|
# Build query parameters
|
|
125
140
|
params = {"limit": limit}
|
|
126
141
|
|
|
142
|
+
if offset > 0:
|
|
143
|
+
params["start"] = offset
|
|
144
|
+
|
|
127
145
|
if query:
|
|
128
146
|
params["q"] = query
|
|
129
147
|
|
|
@@ -134,6 +152,10 @@ def search(ctx, query, fulltext, itemtype, collection, limit, output_json): # n
|
|
|
134
152
|
# Join multiple item types with || for OR search
|
|
135
153
|
params["itemType"] = " || ".join(itemtype)
|
|
136
154
|
|
|
155
|
+
if tag:
|
|
156
|
+
# Multiple tags are passed as a list for AND search
|
|
157
|
+
params["tag"] = list(tag)
|
|
158
|
+
|
|
137
159
|
# Execute search
|
|
138
160
|
# When fulltext is enabled, use items() or collection_items() to get both
|
|
139
161
|
# top-level items and attachments. Otherwise use top() or collection_items_top()
|
|
@@ -320,7 +342,7 @@ def listcollections(ctx, limit):
|
|
|
320
342
|
key = data.get("key", "")
|
|
321
343
|
name = data.get("name", "")
|
|
322
344
|
if key:
|
|
323
|
-
collection_map[key] = name
|
|
345
|
+
collection_map[key] = name or None
|
|
324
346
|
|
|
325
347
|
# Build JSON output
|
|
326
348
|
output = []
|
|
@@ -335,7 +357,7 @@ def listcollections(ctx, limit):
|
|
|
335
357
|
|
|
336
358
|
collection_obj = {
|
|
337
359
|
"id": key,
|
|
338
|
-
"name": name
|
|
360
|
+
"name": name or None,
|
|
339
361
|
"items": num_items,
|
|
340
362
|
}
|
|
341
363
|
|
|
@@ -429,6 +451,254 @@ def test(ctx):
|
|
|
429
451
|
sys.exit(1)
|
|
430
452
|
|
|
431
453
|
|
|
454
|
+
@main.command()
|
|
455
|
+
@click.argument("key")
|
|
456
|
+
@click.option(
|
|
457
|
+
"--json",
|
|
458
|
+
"output_json",
|
|
459
|
+
is_flag=True,
|
|
460
|
+
help="Output results as JSON",
|
|
461
|
+
)
|
|
462
|
+
@click.pass_context
|
|
463
|
+
def item(ctx, key, output_json):
|
|
464
|
+
"""Get a single item by its key.
|
|
465
|
+
|
|
466
|
+
Returns full item data for the specified Zotero item key.
|
|
467
|
+
|
|
468
|
+
Examples:
|
|
469
|
+
pyzotero item ABC123
|
|
470
|
+
|
|
471
|
+
pyzotero item ABC123 --json
|
|
472
|
+
|
|
473
|
+
"""
|
|
474
|
+
try:
|
|
475
|
+
locale = ctx.obj.get("locale", "en-US")
|
|
476
|
+
zot = _get_zotero_client(locale)
|
|
477
|
+
|
|
478
|
+
# Fetch the item
|
|
479
|
+
result = zot.item(key)
|
|
480
|
+
|
|
481
|
+
if not result:
|
|
482
|
+
if output_json:
|
|
483
|
+
click.echo(json.dumps(None))
|
|
484
|
+
else:
|
|
485
|
+
click.echo(f"Item not found: {key}")
|
|
486
|
+
return
|
|
487
|
+
|
|
488
|
+
data = result.get("data", {})
|
|
489
|
+
|
|
490
|
+
if output_json:
|
|
491
|
+
click.echo(json.dumps(result, indent=2))
|
|
492
|
+
else:
|
|
493
|
+
title = data.get("title", "No title")
|
|
494
|
+
item_type = data.get("itemType", "Unknown")
|
|
495
|
+
date = data.get("date", "No date")
|
|
496
|
+
item_key = data.get("key", "")
|
|
497
|
+
doi = data.get("DOI", "")
|
|
498
|
+
url = data.get("url", "")
|
|
499
|
+
|
|
500
|
+
# Format creators
|
|
501
|
+
creators = data.get("creators", [])
|
|
502
|
+
creator_names = []
|
|
503
|
+
for creator in creators:
|
|
504
|
+
if "lastName" in creator:
|
|
505
|
+
if "firstName" in creator:
|
|
506
|
+
creator_names.append(
|
|
507
|
+
f"{creator['firstName']} {creator['lastName']}"
|
|
508
|
+
)
|
|
509
|
+
else:
|
|
510
|
+
creator_names.append(creator["lastName"])
|
|
511
|
+
elif "name" in creator:
|
|
512
|
+
creator_names.append(creator["name"])
|
|
513
|
+
|
|
514
|
+
authors_str = ", ".join(creator_names) if creator_names else "No authors"
|
|
515
|
+
|
|
516
|
+
click.echo(f"[{item_type}] {title}")
|
|
517
|
+
click.echo(f"Authors: {authors_str}")
|
|
518
|
+
click.echo(f"Date: {date}")
|
|
519
|
+
click.echo(f"DOI: {doi}")
|
|
520
|
+
click.echo(f"URL: {url}")
|
|
521
|
+
click.echo(f"Key: {item_key}")
|
|
522
|
+
|
|
523
|
+
except Exception as e:
|
|
524
|
+
click.echo(f"Error: {e!s}", err=True)
|
|
525
|
+
sys.exit(1)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
@main.command()
|
|
529
|
+
@click.argument("key")
|
|
530
|
+
@click.option(
|
|
531
|
+
"--json",
|
|
532
|
+
"output_json",
|
|
533
|
+
is_flag=True,
|
|
534
|
+
help="Output results as JSON",
|
|
535
|
+
)
|
|
536
|
+
@click.pass_context
|
|
537
|
+
def children(ctx, key, output_json):
|
|
538
|
+
"""Get child items (attachments, notes) of a specific item.
|
|
539
|
+
|
|
540
|
+
Returns all child items for the specified Zotero item key.
|
|
541
|
+
Useful for finding PDF attachments without the N+1 overhead during search.
|
|
542
|
+
|
|
543
|
+
Examples:
|
|
544
|
+
pyzotero children ABC123
|
|
545
|
+
|
|
546
|
+
pyzotero children ABC123 --json
|
|
547
|
+
|
|
548
|
+
"""
|
|
549
|
+
try:
|
|
550
|
+
locale = ctx.obj.get("locale", "en-US")
|
|
551
|
+
zot = _get_zotero_client(locale)
|
|
552
|
+
|
|
553
|
+
# Fetch children
|
|
554
|
+
results = zot.children(key)
|
|
555
|
+
|
|
556
|
+
if not results:
|
|
557
|
+
if output_json:
|
|
558
|
+
click.echo(json.dumps([]))
|
|
559
|
+
else:
|
|
560
|
+
click.echo(f"No children found for item: {key}")
|
|
561
|
+
return
|
|
562
|
+
|
|
563
|
+
if output_json:
|
|
564
|
+
click.echo(json.dumps(results, indent=2))
|
|
565
|
+
else:
|
|
566
|
+
click.echo(f"\nFound {len(results)} child items:\n")
|
|
567
|
+
for idx, child in enumerate(results, 1):
|
|
568
|
+
data = child.get("data", {})
|
|
569
|
+
item_type = data.get("itemType", "Unknown")
|
|
570
|
+
child_key = data.get("key", "")
|
|
571
|
+
title = data.get("title", data.get("note", "No title")[:50] + "...")
|
|
572
|
+
content_type = data.get("contentType", "")
|
|
573
|
+
|
|
574
|
+
click.echo(f"{idx}. [{item_type}] {title}")
|
|
575
|
+
click.echo(f" Key: {child_key}")
|
|
576
|
+
if content_type:
|
|
577
|
+
click.echo(f" Content-Type: {content_type}")
|
|
578
|
+
|
|
579
|
+
# Show file URL for attachments
|
|
580
|
+
file_url = child.get("links", {}).get("enclosure", {}).get("href", "")
|
|
581
|
+
if file_url:
|
|
582
|
+
click.echo(f" File: {file_url}")
|
|
583
|
+
click.echo()
|
|
584
|
+
|
|
585
|
+
except Exception as e:
|
|
586
|
+
click.echo(f"Error: {e!s}", err=True)
|
|
587
|
+
sys.exit(1)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
@main.command()
|
|
591
|
+
@click.option(
|
|
592
|
+
"--collection",
|
|
593
|
+
help="Filter tags to a specific collection key",
|
|
594
|
+
)
|
|
595
|
+
@click.option(
|
|
596
|
+
"--json",
|
|
597
|
+
"output_json",
|
|
598
|
+
is_flag=True,
|
|
599
|
+
help="Output results as JSON",
|
|
600
|
+
)
|
|
601
|
+
@click.pass_context
|
|
602
|
+
def tags(ctx, collection, output_json):
|
|
603
|
+
"""List all tags in the library.
|
|
604
|
+
|
|
605
|
+
Returns all tags used in the library, or only tags from a specific collection.
|
|
606
|
+
|
|
607
|
+
Examples:
|
|
608
|
+
pyzotero tags
|
|
609
|
+
|
|
610
|
+
pyzotero tags --collection ABC123
|
|
611
|
+
|
|
612
|
+
pyzotero tags --json
|
|
613
|
+
|
|
614
|
+
"""
|
|
615
|
+
try:
|
|
616
|
+
locale = ctx.obj.get("locale", "en-US")
|
|
617
|
+
zot = _get_zotero_client(locale)
|
|
618
|
+
|
|
619
|
+
# Fetch tags
|
|
620
|
+
if collection:
|
|
621
|
+
results = zot.collection_tags(collection)
|
|
622
|
+
else:
|
|
623
|
+
results = zot.tags()
|
|
624
|
+
|
|
625
|
+
if not results:
|
|
626
|
+
if output_json:
|
|
627
|
+
click.echo(json.dumps([]))
|
|
628
|
+
else:
|
|
629
|
+
click.echo("No tags found.")
|
|
630
|
+
return
|
|
631
|
+
|
|
632
|
+
if output_json:
|
|
633
|
+
click.echo(json.dumps(results, indent=2))
|
|
634
|
+
else:
|
|
635
|
+
click.echo(f"\nFound {len(results)} tags:\n")
|
|
636
|
+
for tag in sorted(results):
|
|
637
|
+
click.echo(f" {tag}")
|
|
638
|
+
|
|
639
|
+
except Exception as e:
|
|
640
|
+
click.echo(f"Error: {e!s}", err=True)
|
|
641
|
+
sys.exit(1)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
@main.command()
|
|
645
|
+
@click.argument("keys", nargs=-1, required=True)
|
|
646
|
+
@click.option(
|
|
647
|
+
"--json",
|
|
648
|
+
"output_json",
|
|
649
|
+
is_flag=True,
|
|
650
|
+
help="Output results as JSON",
|
|
651
|
+
)
|
|
652
|
+
@click.pass_context
|
|
653
|
+
def subset(ctx, keys, output_json):
|
|
654
|
+
"""Get multiple items by their keys in a single call.
|
|
655
|
+
|
|
656
|
+
Efficiently retrieve up to 50 items by key in a single API call.
|
|
657
|
+
Far more efficient than multiple individual item lookups.
|
|
658
|
+
|
|
659
|
+
Examples:
|
|
660
|
+
pyzotero subset ABC123 DEF456 GHI789
|
|
661
|
+
|
|
662
|
+
pyzotero subset ABC123 DEF456 --json
|
|
663
|
+
|
|
664
|
+
"""
|
|
665
|
+
try:
|
|
666
|
+
locale = ctx.obj.get("locale", "en-US")
|
|
667
|
+
zot = _get_zotero_client(locale)
|
|
668
|
+
|
|
669
|
+
if len(keys) > 50: # noqa: PLR2004 - Zotero API limit
|
|
670
|
+
click.echo("Error: Maximum 50 items per call.", err=True)
|
|
671
|
+
sys.exit(1)
|
|
672
|
+
|
|
673
|
+
# Fetch items
|
|
674
|
+
results = zot.get_subset(list(keys))
|
|
675
|
+
|
|
676
|
+
if not results:
|
|
677
|
+
if output_json:
|
|
678
|
+
click.echo(json.dumps([]))
|
|
679
|
+
else:
|
|
680
|
+
click.echo("No items found.")
|
|
681
|
+
return
|
|
682
|
+
|
|
683
|
+
if output_json:
|
|
684
|
+
click.echo(json.dumps(results, indent=2))
|
|
685
|
+
else:
|
|
686
|
+
click.echo(f"\nFound {len(results)} items:\n")
|
|
687
|
+
for idx, item in enumerate(results, 1):
|
|
688
|
+
data = item.get("data", {})
|
|
689
|
+
title = data.get("title", "No title")
|
|
690
|
+
item_type = data.get("itemType", "Unknown")
|
|
691
|
+
item_key = data.get("key", "")
|
|
692
|
+
|
|
693
|
+
click.echo(f"{idx}. [{item_type}] {title}")
|
|
694
|
+
click.echo(f" Key: {item_key}")
|
|
695
|
+
click.echo()
|
|
696
|
+
|
|
697
|
+
except Exception as e:
|
|
698
|
+
click.echo(f"Error: {e!s}", err=True)
|
|
699
|
+
sys.exit(1)
|
|
700
|
+
|
|
701
|
+
|
|
432
702
|
@main.command()
|
|
433
703
|
@click.argument("dois", nargs=-1)
|
|
434
704
|
@click.option(
|
|
@@ -523,6 +793,102 @@ def alldoi(ctx, dois, output_json): # noqa: PLR0912
|
|
|
523
793
|
sys.exit(1)
|
|
524
794
|
|
|
525
795
|
|
|
796
|
+
@main.command()
|
|
797
|
+
@click.pass_context
|
|
798
|
+
def doiindex(ctx):
|
|
799
|
+
"""Output the complete DOI-to-key mapping for the library.
|
|
800
|
+
|
|
801
|
+
Returns a JSON mapping of normalised DOIs to item keys and original DOIs.
|
|
802
|
+
This allows the skill to cache the index and avoid repeated full-library scans.
|
|
803
|
+
|
|
804
|
+
Output format:
|
|
805
|
+
{
|
|
806
|
+
"10.1234/abc": {"key": "ABC123", "original": "https://doi.org/10.1234/ABC"},
|
|
807
|
+
...
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
Examples:
|
|
811
|
+
pyzotero doiindex
|
|
812
|
+
|
|
813
|
+
pyzotero doiindex > doi_cache.json
|
|
814
|
+
|
|
815
|
+
"""
|
|
816
|
+
try:
|
|
817
|
+
locale = ctx.obj.get("locale", "en-US")
|
|
818
|
+
zot = _get_zotero_client(locale)
|
|
819
|
+
|
|
820
|
+
click.echo("Building DOI index from library...", err=True)
|
|
821
|
+
doi_map = _build_doi_index_full(zot)
|
|
822
|
+
click.echo(f"Indexed {len(doi_map)} items with DOIs", err=True)
|
|
823
|
+
|
|
824
|
+
click.echo(json.dumps(doi_map, indent=2))
|
|
825
|
+
|
|
826
|
+
except Exception as e:
|
|
827
|
+
click.echo(f"Error: {e!s}", err=True)
|
|
828
|
+
sys.exit(1)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
@main.command()
|
|
832
|
+
@click.argument("key")
|
|
833
|
+
@click.pass_context
|
|
834
|
+
def fulltext(ctx, key):
|
|
835
|
+
"""Get full-text content of an attachment.
|
|
836
|
+
|
|
837
|
+
Returns the full-text content extracted from a PDF or other attachment.
|
|
838
|
+
The key should be the key of an attachment item (not a top-level item).
|
|
839
|
+
|
|
840
|
+
Output format:
|
|
841
|
+
{
|
|
842
|
+
"content": "Full-text extracted from PDF...",
|
|
843
|
+
"indexedPages": 50,
|
|
844
|
+
"totalPages": 50
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
Examples:
|
|
848
|
+
pyzotero fulltext ABC123
|
|
849
|
+
|
|
850
|
+
"""
|
|
851
|
+
try:
|
|
852
|
+
locale = ctx.obj.get("locale", "en-US")
|
|
853
|
+
zot = _get_zotero_client(locale)
|
|
854
|
+
|
|
855
|
+
result = zot.fulltext_item(key)
|
|
856
|
+
|
|
857
|
+
if not result:
|
|
858
|
+
click.echo(json.dumps({"error": "No full-text content available"}))
|
|
859
|
+
return
|
|
860
|
+
|
|
861
|
+
click.echo(json.dumps(result, indent=2))
|
|
862
|
+
|
|
863
|
+
except Exception as e:
|
|
864
|
+
click.echo(f"Error: {e!s}", err=True)
|
|
865
|
+
sys.exit(1)
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def _build_doi_index_full(zot):
|
|
869
|
+
"""Build a mapping of normalised DOIs to Zotero item keys and original DOIs.
|
|
870
|
+
|
|
871
|
+
Returns:
|
|
872
|
+
Dict mapping normalised DOIs to dicts with 'key' and 'original' fields
|
|
873
|
+
|
|
874
|
+
"""
|
|
875
|
+
doi_map = {}
|
|
876
|
+
all_items = zot.everything(zot.items())
|
|
877
|
+
|
|
878
|
+
for item in all_items:
|
|
879
|
+
data = item.get("data", {})
|
|
880
|
+
item_doi = data.get("DOI", "")
|
|
881
|
+
|
|
882
|
+
if item_doi:
|
|
883
|
+
normalised_doi = _normalize_doi(item_doi)
|
|
884
|
+
item_key = data.get("key", "")
|
|
885
|
+
|
|
886
|
+
if normalised_doi and item_key:
|
|
887
|
+
doi_map[normalised_doi] = {"key": item_key, "original": item_doi}
|
|
888
|
+
|
|
889
|
+
return doi_map
|
|
890
|
+
|
|
891
|
+
|
|
526
892
|
def _build_doi_index(zot):
|
|
527
893
|
"""Build a mapping of normalised DOIs to Zotero item keys.
|
|
528
894
|
|
|
@@ -4,13 +4,13 @@ pyzotero/_decorators.py,sha256=nMx3bBM8UFp5cS-c1vcm-toJ2CSDXM78coh-beWqHbA,6343
|
|
|
4
4
|
pyzotero/_search.py,sha256=LNFbouGZ2rBF2LblFnL39998ZGXYQeqSsMArAgkhBtI,7259
|
|
5
5
|
pyzotero/_upload.py,sha256=_Q3Ex7EGf6GFaxaT_Rv2UG3u9rrCJE4H-tmQUYGOr7M,9052
|
|
6
6
|
pyzotero/_utils.py,sha256=c8L0WXzv0_SqDd-Pio4MCCyydq_fdhGX4UXYGNf92hA,2382
|
|
7
|
-
pyzotero/cli.py,sha256=
|
|
7
|
+
pyzotero/cli.py,sha256=nY90mW5LXP-f1FHwC2Wtu5hIj68T_Y1V5bOS4NnOqfA,38744
|
|
8
8
|
pyzotero/errors.py,sha256=To9IkdcA5zGkR3Rl88kAIktGBXhrMimfZdNxkqHNZLY,5211
|
|
9
9
|
pyzotero/filetransport.py,sha256=HFXfRP8haG31JIA-CmEcKE3oTzqNN04Pjn7u8KwE7u0,5498
|
|
10
10
|
pyzotero/semantic_scholar.py,sha256=2_a6UyUYVBFPQKS8JgBylGksM90aVtq4V3HtpbF1OTI,12728
|
|
11
11
|
pyzotero/zotero.py,sha256=8oOSlCZ4fOCVLZBbM07-oCCouI43m_bTG7oAEqAHzuM,1591
|
|
12
12
|
pyzotero/zotero_errors.py,sha256=9EVkGS73SJy5VuKBGtxtQtxFqz1y-jQgYyrx9aBICvw,1475
|
|
13
|
-
pyzotero-1.
|
|
14
|
-
pyzotero-1.
|
|
15
|
-
pyzotero-1.
|
|
16
|
-
pyzotero-1.
|
|
13
|
+
pyzotero-1.9.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
14
|
+
pyzotero-1.9.0.dist-info/entry_points.txt,sha256=MzN7IMRj_oPNmDCsseYFPum3bHWE1gFxywhlbFbcn2k,48
|
|
15
|
+
pyzotero-1.9.0.dist-info/METADATA,sha256=hM2UY5z5jMm6JvHxMCQllkDukeIIesZLcvPJ4331L70,9956
|
|
16
|
+
pyzotero-1.9.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|