Files
stack/dev/scripts/check_pdf_coverage.py
kert 16f3b43974
Some checks failed
CI / skinny-install (aco) (push) Successful in 1m12s
CI / skinny-install (api) (push) Successful in 30s
CI / skinny-install (bcda) (push) Successful in 36s
CI / skinny-install (bib) (push) Successful in 35s
CI / skinny-install (bls) (push) Successful in 27s
CI / skinny-install (ccw) (push) Successful in 32s
CI / skinny-install (cli) (push) Successful in 41s
CI / skinny-install (cms) (push) Successful in 37s
CI / skinny-install (conf) (push) Successful in 38s
CI / skinny-install (opps) (push) Successful in 33s
CI / skinny-install (perf) (push) Successful in 38s
CI / skinny-install (pfs) (push) Successful in 38s
CI / skinny-install (rex) (push) Successful in 34s
Deploy / build-scan-report (push) Failing after 46s
Infra CI / notebooks (push) Failing after 25s
Infra CI / zotero (push) Successful in 12s
Infra CI / docs (push) Failing after 16s
CI / lint-test (push) Failing after 11m2s
Infra CI / mc (push) Successful in 21s
Infra CI / api (push) Successful in 29s
Package Supply Chain / pkg-supply-chain (push) Failing after 41s
feat: full session — mail servers, comment pipeline, PRISMA fetch, email ingest
Mail: Maddy on DO (corwins.media+Resend, fhirworx.io+Postmark),
touchless/stateless/idempotent. Gitea SMTP via env_file. CMS inbox
at cmsupdates@mail.fhirworx.io with IMAP→bib poller.

Bib: regulations.gov v4 client, Federal Register discovery, 164K
comment backfill (running), IMAP email ingest, Zotero sync routing.

PRISMA: altcha PoW solver, CrossRef DOI resolution, 83/129 PDFs.
Zotero: schema parity, ops module, CLI, fail-fast guard.
CI: docs.Dockerfile COPY glob fix (tracks #341).
Infra: Gitea+marimo fhirworx themes, IOM/OIG modules.
2026-04-16 09:04:38 -04:00

97 lines
3.4 KiB
Python

"""Check PDF retrieval coverage for skin substitute evidence base.
Reports how many Zotero items have PDF attachments vs DOI-bearing items
that could potentially be retrieved. Run after using Zotero's
"Find Available PDF" with the scipdf plugin.
Usage:
uv run python dev/scripts/check_pdf_coverage.py
"""
from __future__ import annotations
import sqlite3
from pathlib import Path
from conf import path as conf_path
ZOTERO_DB = str(conf_path("db.zotero"))
def main() -> None:
print("Checking PDF coverage for skin-subs items in Zotero ...\n")
con = sqlite3.connect(ZOTERO_DB, timeout=10)
con.row_factory = sqlite3.Row
# Total skin-subs items
total = con.execute("""
SELECT count(DISTINCT it.itemID) FROM itemTags it
JOIN tags t ON it.tagID = t.tagID
WHERE t.name = 'module:skin-subs'
""").fetchone()[0]
# Items with DOI (retrievable)
with_doi = con.execute("""
SELECT count(DISTINCT i.itemID) FROM items i
JOIN itemTags it ON i.itemID = it.itemID
JOIN tags t ON it.tagID = t.tagID
JOIN itemData id ON i.itemID = id.itemID
WHERE t.name = 'module:skin-subs' AND id.fieldID = {FIELD_MAP['DOI']}
""").fetchone()[0]
# Items with PDF attachments
with_pdf = con.execute("""
SELECT count(DISTINCT parent.itemID) FROM items parent
JOIN itemTags it ON parent.itemID = it.itemID
JOIN tags t ON it.tagID = t.tagID
JOIN itemAttachments ia ON ia.parentItemID = parent.itemID
JOIN items att ON ia.itemID = att.itemID
WHERE t.name = 'module:skin-subs'
AND ia.contentType = 'application/pdf'
""").fetchone()[0]
print(f"Total skin-subs items: {total:>6}")
print(f"Items with DOI: {with_doi:>6} ({with_doi * 100 // total}%)")
print(
f"Items with PDF attached: {with_pdf:>6} ({with_pdf * 100 // max(total, 1)}%)"
)
print(f"DOIs without PDF: {with_doi - with_pdf:>6}")
print(f"Coverage (of DOI items): {with_pdf * 100 // max(with_doi, 1)}%")
# Breakdown by item type
print("\nBy item type:")
for r in con.execute("""
SELECT it2.typeName,
count(DISTINCT i.itemID) as total,
count(DISTINCT CASE WHEN ia.itemID IS NOT NULL THEN i.itemID END) as with_pdf
FROM items i
JOIN itemTags it ON i.itemID = it.itemID
JOIN tags t ON it.tagID = t.tagID
JOIN itemTypes it2 ON i.itemTypeID = it2.itemTypeID
LEFT JOIN itemAttachments ia ON ia.parentItemID = i.itemID
AND ia.contentType = 'application/pdf'
WHERE t.name = 'module:skin-subs'
GROUP BY it2.typeName
ORDER BY total DESC
""").fetchall():
pct = r["with_pdf"] * 100 // max(r["total"], 1)
print(
f" {r['typeName']:20s} total={r['total']:>5} pdf={r['with_pdf']:>5} ({pct}%)"
)
# Storage size
storage = Path(conf_path("storage.zotero"))
if storage.exists():
total_bytes = sum(f.stat().st_size for f in storage.rglob("*") if f.is_file())
print(f"\nZotero storage size: {total_bytes / 1024 / 1024:.1f} MB")
con.close()
print("\nTo retrieve PDFs: open Zotero, select 'Skin Substitutes' collection,")
print("select all items (Ctrl+A), right-click → Find Available PDF.")
print("scipdf plugin (v8.0.4) will try Unpaywall then 7 SciHub mirrors.")
if __name__ == "__main__":
main()