torchcell.literature.ZoteroLibrary

class ZoteroLibrary(config: ZoteroConfig)[source]

Bases: object

Synchronous handle to one Zotero library.

A thin, retry-hardened wrapper over a pyzotero client exposing the operations the literature-capture pipeline needs: list/resolve collections, look up an item by DOI, enumerate a paper’s PDF attachments (main article first, SI after), and download them into the on-disk artifact layout

<data_root>/torchcell-library/<citation_key>/

paper.pdf si/si1.pdf si/si2.pdf …

This mirror is a backup to the existing URL-based dataset downloads, keyed by DOI so a dataset writer can fall back to it when an upstream source is down.

classmethod from_env() → ZoteroLibrary[source]

Build a library from ZoteroConfig.from_env().

list_collections() → list[dict[str, Any]][source]

Every collection in the library (name + key dicts), across all pages.

pyzotero returns at most limit=100 rows per request, so the listing goes through everything(...), which follows the API’s next links. Reading only the first page made collection_key miss any collection past the 100th, which is how the personal torchcell root went “missing” from the nightly bibliography export once the library grew (issue #563).

collection_key(name: str, create_if_missing: bool = False) → str[source]

Resolve a collection name to its key, optionally creating it.

The name is matched case-insensitively against EVERY collection in the library (see list_collections()), so a name that exists is found no matter which page it sits on, and create_if_missing never creates a duplicate of it.

Raises ValueError naming every candidate’s key and parent when two or more collections share the name (a nested name can repeat across branches; pass the key instead), and ValueError listing the available names when the name is not found and create_if_missing is False.

collection_tree(root_collection: str) → list[CollectionNode][source]

A collection and every collection nested beneath it, with display paths.

collection_items returns only direct members, so a nested collection such as torchcell/torchcell-topics/microbe-perturb-seq is invisible without walking the tree, which is exactly where new reading is filed. Returned in depth-first order with the root first. The root is resolved against the same full, paged listing the tree is built from.

find_item_by_doi(doi: str) → dict[str, Any] | None[source]

Return the library item whose data.DOI matches exactly, else None.

Zotero’s q quick search indexes title/creator/year (and, with qmode=everything, attachment full text) but NOT the DOI metadata field, so a q=<doi> search returns nothing here. The reliable lookup is a full everything(items()) scan matched against each item’s actual data["DOI"] – cheap for a curated library (a few paginated GETs).

doi_in_library(doi: str) → bool[source]

True if an item with this exact DOI already exists in the library.

pdf_attachments(item_key: str) → list[dict[str, Any]][source]

PDF attachments of an item, main article first then SI.

Auto-paginates: heavily-annotated items can accrue more than the default 100-item page, which would otherwise hide the source PDF.

download_artifact(item: dict[str, Any], data_root: str | Path | None = None, library_dirname: str = 'torchcell-library') → Path[source]

Download an item’s PDFs into its citation-keyed artifact directory.

Writes the main article to paper.pdf and each SI PDF to si/si1.pdf, si/si2.pdf, … under <data_root>/<library_dirname>/<citation_key>/. The citation key is taken from Zotero (native field or extra) so the directory matches the library; see _resolve_citation_key().

Parameters:
  • item – A Zotero item dict (as returned by find_item_by_doi()).

  • data_root – Root for the mirror; defaults to $DATA_ROOT.

  • library_dirname – Name of the library subdirectory under the root.

Returns:

The artifact directory path.