Files
stack/tests/zot/fetch/test_oa.py
kert bee5f57893 feat(zot): stack zot fetch — open-access full-text connector
Attaches an open-access PDF to Zotero items that lack a full text.
Built on the selection, ledger and attach machinery from PR #826
(feat/zot-fetch-z1, left untouched on its branch); delivery is the
stack's legal open-access cascade only — every Unpaywall location →
Semantic Scholar openAccessPdf → PMC S3 → PMC OA service → Europe PMC
(prisma.fetch.fetch_one with no proxied client, so its mirror tier
never runs). No DOI? a Crossref title match first. Downloads must be
real PDFs before they are attached; the run ledgers every attempt
(attached / no_candidate / error; misses wait 30 days unless --retry).

From the #828 review list:
- Lazy selection: select_ids() is one cheap query (deleted and
  full-text items excluded in SQL for --all) and iter_refs() reads each
  item only when the run reaches it, so --limit bites before ~167k
  file-less items are materialised while the desktop is held down.
- A locked database (--no-hold with the desktop writing) aborts the run
  with a usage error that names the Zotero desktop, instead of a raw
  sqlite3.OperationalError — or one error per item.
- --dry-run reads only (checked against the running desktop), so it no
  longer stops/starts zotero; UNPAYWALL_EMAIL is documented in --help.

Tests: 66 (scratch Zotero DB, no network), 100% of src/zot/fetch.
2026-10-27 14:20:05 -04:00

74 lines
2.6 KiB
Python

"""zot.fetch.oa — the open-access cascade wrapper (no network)."""
from __future__ import annotations
from pathlib import Path
import zot.fetch.oa as oa
from zot.fetch.select import ItemRef
def _ref(doi: str = "10.1/x", title: str = "A paper") -> ItemRef:
return ItemRef(
item_id=7,
key="ABCD1234",
item_type="journalArticle",
title=title,
doi=doi,
isbn="",
year="2024",
)
def test_make_client_identifies_itself_and_follows_redirects():
with oa.make_client("me@example.org") as c:
assert c.headers["User-Agent"] == "stack-zot-fetch/1 (mailto:me@example.org)"
assert c.follow_redirects is True
def test_find_runs_the_cascade_without_a_proxied_client(monkeypatch, tmp_path):
seen = {}
def fake_fetch_one(item, **kw):
seen.update(item=item, kw=kw)
return "pmc", tmp_path / "x.pdf"
monkeypatch.setattr(oa, "fetch_one", fake_fetch_one)
out = oa.find("CLIENT", _ref(), email="e@x", scratch=tmp_path)
assert out == ("pmc", tmp_path / "x.pdf")
assert seen["item"].doi == "10.1/x" and seen["item"].zot_id == 7
assert "client_proxied" not in seen["kw"] # the mirror tier never runs
assert seen["kw"]["client"] == "CLIENT" and seen["kw"]["scratch"] == tmp_path
def test_preview_uses_the_items_doi(monkeypatch):
monkeypatch.setattr(
oa, "unpaywall_urls", lambda c, doi, email: [f"https://oa/{doi}"]
)
monkeypatch.setattr(oa, "resolve_doi_from_title", lambda c, t: pytest_fail())
assert oa.preview(None, _ref(), email="e") == ("10.1/x", ["https://oa/10.1/x"])
def test_preview_resolves_a_missing_doi_from_the_title(monkeypatch):
monkeypatch.setattr(oa, "resolve_doi_from_title", lambda c, t: "10.9/found")
monkeypatch.setattr(oa, "unpaywall_urls", lambda c, doi, email: [])
assert oa.preview(None, _ref(doi=""), email="e") == ("10.9/found", [])
def test_preview_without_doi_or_match_tries_nothing(monkeypatch):
monkeypatch.setattr(oa, "resolve_doi_from_title", lambda c, t: None)
called = []
monkeypatch.setattr(oa, "unpaywall_urls", lambda *a: called.append(a) or [])
assert oa.preview(None, _ref(doi=""), email="e") == ("", [])
assert oa.preview(None, _ref(doi="", title=""), email="e") == ("", [])
assert called == []
def pytest_fail(): # pragma: no cover — only runs if preview misbehaves
raise AssertionError("title lookup must not run when the item has a DOI")
def test_find_returns_none_when_nothing_is_open(monkeypatch, tmp_path: Path):
monkeypatch.setattr(oa, "fetch_one", lambda item, **kw: None)
assert oa.find(None, _ref(), email="e", scratch=tmp_path) is None