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 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 if name else None
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 if name else None,
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyzotero
3
- Version: 1.8.0
3
+ Version: 1.9.0
4
4
  Summary: Python wrapper for the Zotero API
5
5
  Keywords: Zotero,DH
6
6
  Author: Stephan Hügel
@@ -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=q1qo6VH_WmCmuSQFSjqa7PRhhUEwecb_hHT0oPsXRAI,28674
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.8.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
14
- pyzotero-1.8.0.dist-info/entry_points.txt,sha256=MzN7IMRj_oPNmDCsseYFPum3bHWE1gFxywhlbFbcn2k,48
15
- pyzotero-1.8.0.dist-info/METADATA,sha256=OUkZcCjw-3nQ6mzYWsN_5gHWr-dNBzSYi007h3xz4Hk,9956
16
- pyzotero-1.8.0.dist-info/RECORD,,
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,,