candle-fire / scripts /ingest_papers.py
KevinIsInCoding
feat: automate ALS seed entities from trials and paper corpus
e53bbe5
Raw
History Blame Contribute Delete
5.8 kB
#!/usr/bin/env python3
"""
Ingest ALS papers from PubMed + PMC full text + Semantic Scholar citation counts.
Usage:
uv run python scripts/ingest_papers.py # default query, 500 papers
uv run python scripts/ingest_papers.py --max 10 # small test run
uv run python scripts/ingest_papers.py --pmid-file pmids.txt # from curated PMID list
uv run python scripts/ingest_papers.py --skip-fulltext # abstracts only
uv run python scripts/ingest_papers.py --skip-citations # skip Semantic Scholar
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from dotenv import load_dotenv
load_dotenv()
from rich.console import Console
from rich.progress import track
import ingestion.pmc as pmc
import ingestion.pubmed as pubmed
import ingestion.semantic_scholar as ss
from config import PAPERS_PATH, PUBMED_DEFAULT_MAX, PUBMED_DEFAULT_QUERY, PUBMED_REFRESH_QUERY_TEMPLATE
console = Console()
def main() -> None:
parser = argparse.ArgumentParser(description="Ingest ALS papers")
parser.add_argument("--query", default=None, help="PubMed query string (overrides --since)")
parser.add_argument("--since", metavar="YYYY-MM-DD", help="Fetch papers published on or after this date (incremental append mode)")
parser.add_argument("--max", type=int, default=PUBMED_DEFAULT_MAX, dest="max_results", help="Max papers to fetch")
parser.add_argument("--pmid-file", help="Path to file with one PMID per line")
parser.add_argument("--skip-fulltext", action="store_true", help="Skip PMC full text fetch")
parser.add_argument("--skip-citations", action="store_true", help="Skip Semantic Scholar citation counts")
args = parser.parse_args()
if args.query is None:
if args.since:
args.query = PUBMED_REFRESH_QUERY_TEMPLATE.format(since_date=args.since)
else:
args.query = PUBMED_DEFAULT_QUERY
PAPERS_PATH.parent.mkdir(parents=True, exist_ok=True)
# Step 1: Obtain PMIDs
if args.pmid_file:
lines = Path(args.pmid_file).read_text().splitlines()
pmids = [line.strip() for line in lines if line.strip()]
console.print(f"[cyan]Loaded {len(pmids)} PMIDs from {args.pmid_file}[/cyan]")
else:
console.print(f"[cyan]Searching PubMed (max {args.max_results})...[/cyan]")
pmids = pubmed.search_pmids(args.query, max_results=args.max_results)
console.print(f"[green]Found {len(pmids)} PMIDs[/green]")
if not pmids:
console.print("[red]No PMIDs found — check your query or PMID file.[/red]")
sys.exit(1)
# In append mode, filter out PMIDs already in the existing corpus
existing_pmids: set[str] = set()
if args.since and PAPERS_PATH.exists():
with open(PAPERS_PATH, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
existing_pmids.add(json.loads(line).get("pmid", ""))
original_count = len(pmids)
pmids = [p for p in pmids if p not in existing_pmids]
console.print(f"[cyan]Incremental mode: {original_count} fetched, {len(existing_pmids)} already known, {len(pmids)} new[/cyan]")
if not pmids:
console.print("[green]No new papers found — corpus is up to date.[/green]")
return
# Step 2: Fetch paper records from PubMed
console.print("[cyan]Fetching Medline records...[/cyan]")
papers = pubmed.fetch_by_pmids(pmids)
console.print(f"[green]Parsed {len(papers)} papers with abstracts[/green]")
if not papers:
console.print("[red]No papers with abstracts retrieved.[/red]")
sys.exit(1)
# Step 3: Enrich with PMC full text
if not args.skip_fulltext:
all_pmids = [p.pmid for p in papers]
pmcid_map = pmc.get_pmcids(all_pmids) # shows its own progress bar
console.print(f"[green]{len(pmcid_map)} papers have PMC full text available[/green]")
pmid_to_paper = {p.pmid: p for p in papers}
ft_count = 0
for pmid, pmcid in track(pmcid_map.items(), description="Fetching full text...", console=console):
if pmid in pmid_to_paper:
full_text = pmc.fetch_full_text(pmcid)
if full_text:
pmid_to_paper[pmid].full_text = full_text
ft_count += 1
console.print(f"[green]Retrieved section-parsed full text for {ft_count} papers[/green]")
# Step 4: Enrich with citation counts from Semantic Scholar
if not args.skip_citations:
console.print("[cyan]Fetching citation counts from Semantic Scholar...[/cyan]")
citation_map = ss.fetch_citation_counts([p.pmid for p in papers])
for paper in papers:
paper.citation_count = citation_map.get(paper.pmid, 0)
console.print(f"[green]Got citation counts for {len(citation_map)}/{len(papers)} papers[/green]")
# Step 5: Write to JSONL (append in incremental mode, overwrite otherwise)
write_mode = "a" if args.since else "w"
with open(PAPERS_PATH, write_mode, encoding="utf-8") as f:
for paper in papers:
f.write(json.dumps(paper.to_dict()) + "\n")
has_fulltext = sum(1 for p in papers if p.full_text)
has_citations = sum(1 for p in papers if p.citation_count > 0)
console.print(f"\n[bold green]Done![/bold green] Written to {PAPERS_PATH}")
console.print(f" Papers: {len(papers)}")
console.print(f" With full text: {has_fulltext} ({has_fulltext * 100 // len(papers)}%)")
console.print(f" With citations: {has_citations} ({has_citations * 100 // len(papers)}%)")
if __name__ == "__main__":
main()