Pyzotero: a Python client for the Zotero API pyzotero.readthedocs.io
zotero

Merge pull request #261 from urschrei/shugel/push-yvrwumyuqzyw

Add alldoi CLI command for DOI lookup

authored by urschrei.eurosky.social and committed by

GitHub b90075c6 999b4292

+119 -2
+1 -1
pyproject.toml
··· 1 1 [project] 2 2 name = "pyzotero" 3 - version = "1.7.3" 3 + version = "1.7.4" 4 4 description = "Python wrapper for the Zotero API" 5 5 readme = "README.md" 6 6 requires-python = ">=3.9"
+117
src/pyzotero/cli.py
··· 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( ··· 391 414 err=True, 392 415 ) 393 416 sys.exit(1) 417 + except Exception as e: 418 + click.echo(f"Error: {e!s}", err=True) 419 + sys.exit(1) 420 + 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 + 394 511 except Exception as e: 395 512 click.echo(f"Error: {e!s}", err=True) 396 513 sys.exit(1)
+1 -1
uv.lock
··· 784 784 785 785 [[package]] 786 786 name = "pyzotero" 787 - version = "1.7.3" 787 + version = "1.7.4" 788 788 source = { editable = "." } 789 789 dependencies = [ 790 790 { name = "bibtexparser" },