pyzotero 1.7.3__tar.gz → 1.7.5__tar.gz

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.
Files changed (34) hide show
  1. {pyzotero-1.7.3 → pyzotero-1.7.5}/PKG-INFO +1 -1
  2. {pyzotero-1.7.3 → pyzotero-1.7.5}/pyproject.toml +1 -1
  3. {pyzotero-1.7.3 → pyzotero-1.7.5}/src/pyzotero/cli.py +117 -0
  4. {pyzotero-1.7.3 → pyzotero-1.7.5}/src/pyzotero/zotero.py +4 -4
  5. {pyzotero-1.7.3 → pyzotero-1.7.5}/LICENSE.md +0 -0
  6. {pyzotero-1.7.3 → pyzotero-1.7.5}/README.md +0 -0
  7. {pyzotero-1.7.3 → pyzotero-1.7.5}/doc/Makefile +0 -0
  8. {pyzotero-1.7.3 → pyzotero-1.7.5}/doc/_templates/layout.html +0 -0
  9. {pyzotero-1.7.3 → pyzotero-1.7.5}/doc/cat.png +0 -0
  10. {pyzotero-1.7.3 → pyzotero-1.7.5}/doc/conf.py +0 -0
  11. {pyzotero-1.7.3 → pyzotero-1.7.5}/doc/index.rst +0 -0
  12. {pyzotero-1.7.3 → pyzotero-1.7.5}/src/pyzotero/__init__.py +0 -0
  13. {pyzotero-1.7.3 → pyzotero-1.7.5}/src/pyzotero/filetransport.py +0 -0
  14. {pyzotero-1.7.3 → pyzotero-1.7.5}/src/pyzotero/zotero_errors.py +0 -0
  15. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/__init__.py +0 -0
  16. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/attachments_doc.json +0 -0
  17. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/citation_doc.xml +0 -0
  18. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/collection_doc.json +0 -0
  19. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/collection_tags.json +0 -0
  20. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/collection_versions.json +0 -0
  21. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/collections_doc.json +0 -0
  22. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/creation_doc.json +0 -0
  23. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/groups_doc.json +0 -0
  24. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/item_doc.json +0 -0
  25. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/item_fields.json +0 -0
  26. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/item_file.pdf +0 -0
  27. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/item_template.json +0 -0
  28. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/item_types.json +0 -0
  29. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/item_versions.json +0 -0
  30. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/items_doc.json +0 -0
  31. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/keys_doc.txt +0 -0
  32. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/api_responses/tags_doc.json +0 -0
  33. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/test_async.py +0 -0
  34. {pyzotero-1.7.3 → pyzotero-1.7.5}/tests/test_zotero.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyzotero
3
- Version: 1.7.3
3
+ Version: 1.7.5
4
4
  Summary: Python wrapper for the Zotero API
5
5
  Keywords: Zotero,DH
6
6
  Author: Stephan Hügel
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyzotero"
3
- version = "1.7.3"
3
+ version = "1.7.5"
4
4
  description = "Python wrapper for the Zotero API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -15,6 +15,29 @@ def _get_zotero_client(locale="en-US"):
15
15
  return zotero.Zotero(library_id="0", library_type="user", local=True, locale=locale)
16
16
 
17
17
 
18
+ def _normalize_doi(doi):
19
+ """Normalise a DOI for case-insensitive matching.
20
+
21
+ Strips common prefixes (https://doi.org/, http://doi.org/, doi:) and converts to lowercase.
22
+ DOIs are case-insensitive per the DOI specification.
23
+ """
24
+ if not doi:
25
+ return ""
26
+
27
+ # Strip whitespace
28
+ doi = doi.strip()
29
+
30
+ # Strip common prefixes
31
+ prefixes = ["https://doi.org/", "http://doi.org/", "doi:"]
32
+ for prefix in prefixes:
33
+ if doi.lower().startswith(prefix.lower()):
34
+ doi = doi[len(prefix) :]
35
+ break
36
+
37
+ # Convert to lowercase for case-insensitive matching
38
+ return doi.lower().strip()
39
+
40
+
18
41
  @click.group()
19
42
  @click.version_option(version=__version__, prog_name="pyzotero")
20
43
  @click.option(
@@ -396,5 +419,99 @@ def test(ctx):
396
419
  sys.exit(1)
397
420
 
398
421
 
422
+ @main.command()
423
+ @click.argument("dois", nargs=-1)
424
+ @click.option(
425
+ "--json",
426
+ "output_json",
427
+ is_flag=True,
428
+ help="Output results as JSON",
429
+ )
430
+ @click.pass_context
431
+ def alldoi(ctx, dois, output_json): # noqa: PLR0912
432
+ """Look up DOIs in the local Zotero library and return their Zotero IDs.
433
+
434
+ Accepts one or more DOIs as arguments and checks if they exist in the library.
435
+ DOI matching is case-insensitive and handles common prefixes (https://doi.org/, doi:).
436
+
437
+ If no DOIs are provided, shows "No items found" (text) or {} (JSON).
438
+
439
+ Examples:
440
+ pyzotero alldoi 10.1234/example
441
+
442
+ pyzotero alldoi 10.1234/abc https://doi.org/10.5678/def doi:10.9012/ghi
443
+
444
+ pyzotero alldoi 10.1234/example --json
445
+
446
+ """
447
+ try:
448
+ locale = ctx.obj.get("locale", "en-US")
449
+ zot = _get_zotero_client(locale)
450
+
451
+ # Build a mapping of normalized DOIs to (original_doi, zotero_key)
452
+ click.echo("Building DOI index from library...", err=True)
453
+ doi_map = {}
454
+
455
+ # Get all items using everything() which handles pagination automatically
456
+ all_items = zot.everything(zot.items())
457
+
458
+ # Process all items
459
+ for item in all_items:
460
+ data = item.get("data", {})
461
+ item_doi = data.get("DOI", "")
462
+
463
+ if item_doi:
464
+ normalized_doi = _normalize_doi(item_doi)
465
+ item_key = data.get("key", "")
466
+
467
+ if normalized_doi and item_key:
468
+ # Store the original DOI from Zotero and the item key
469
+ doi_map[normalized_doi] = (item_doi, item_key)
470
+
471
+ click.echo(f"Indexed {len(doi_map)} items with DOIs", err=True)
472
+
473
+ # If no DOIs provided, return empty result
474
+ if not dois:
475
+ if output_json:
476
+ click.echo(json.dumps({}))
477
+ else:
478
+ click.echo("No items found")
479
+ return
480
+
481
+ # Look up each input DOI
482
+ found = []
483
+ not_found = []
484
+
485
+ for input_doi in dois:
486
+ normalized_input = _normalize_doi(input_doi)
487
+
488
+ if normalized_input in doi_map:
489
+ original_doi, zotero_key = doi_map[normalized_input]
490
+ found.append({"doi": original_doi, "key": zotero_key})
491
+ else:
492
+ not_found.append(input_doi)
493
+
494
+ # Output results
495
+ if output_json:
496
+ result = {"found": found, "not_found": not_found}
497
+ click.echo(json.dumps(result, indent=2))
498
+ else:
499
+ if found:
500
+ click.echo(f"\nFound {len(found)} items:\n")
501
+ for item in found:
502
+ click.echo(f" {item['doi']} → {item['key']}")
503
+ else:
504
+ click.echo("No items found")
505
+
506
+ if not_found:
507
+ click.echo(f"\nNot found ({len(not_found)}):")
508
+ for doi in not_found:
509
+ click.echo(f" {doi}")
510
+
511
+ except Exception as e:
512
+ click.echo(f"Error: {e!s}", err=True)
513
+ sys.exit(1)
514
+
515
+
399
516
  if __name__ == "__main__":
400
517
  main()
@@ -796,12 +796,12 @@ class Zotero:
796
796
  """Dump a file attachment to disk, with optional filename and path"""
797
797
  if not filename:
798
798
  filename = self.item(itemkey)["data"]["filename"]
799
- pth = Path(path) / filename if path else filename
799
+ pth = Path(path) / filename if path else Path(filename)
800
800
  file = self.file(itemkey)
801
801
  if self.snapshot:
802
802
  self.snapshot = False
803
- pth += ".zip"
804
- with Path(pth).open("wb") as f:
803
+ pth = pth.parent / (pth.name + ".zip")
804
+ with pth.open("wb") as f:
805
805
  f.write(file)
806
806
 
807
807
  @retrieve
@@ -1492,7 +1492,7 @@ class Zotero:
1492
1492
 
1493
1493
  @backoff_check
1494
1494
  def addto_collection(self, collection, payload):
1495
- """Add one or more items to a collection
1495
+ """Add item to a collection
1496
1496
  Accepts two arguments:
1497
1497
  The collection ID, and an item dict
1498
1498
  """
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes