Spaces:
Running
Running
| #!/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() | |