Musubi / src /entrez.py
Cizencoder's picture
feat: PubMed search via Entrez + dark theme SPA frontend
e0b7bf7
Raw
History Blame Contribute Delete
2.49 kB
from __future__ import annotations
import os
import socket
from io import BytesIO
from Bio import Entrez
MAX_RESULTS_CAP = 50
TIMEOUT = 30
def search_pubmed(
query: str,
max_results: int = 20,
email: str | None = None,
) -> list[dict]:
"""Search PubMed and return [{'pmid','title','abstract'}, ...].
Skips records without an abstract. Caps max_results at 50.
Raises ValueError if no email is configured (NCBI requires it).
"""
email = email or os.environ.get("ENTREZ_EMAIL")
if not email:
raise ValueError(
"ENTREZ_EMAIL is required by NCBI Entrez. Set the environment "
"variable or pass email explicitly."
)
Entrez.email = email
socket.setdefaulttimeout(TIMEOUT)
max_results = min(max(int(max_results), 1), MAX_RESULTS_CAP)
handle = Entrez.esearch(
db="pubmed", term=query, retmax=max_results, sort="relevance"
)
try:
search = Entrez.read(handle)
finally:
handle.close()
pmids = list(search.get("IdList", []))
if not pmids:
return []
handle = Entrez.efetch(
db="pubmed", id=",".join(pmids), rettype="abstract", retmode="xml"
)
try:
raw = handle.read()
finally:
handle.close()
if isinstance(raw, str):
raw = raw.encode("utf-8")
records = Entrez.read(BytesIO(raw))
out: list[dict] = []
for article in records.get("PubmedArticle", []):
try:
medline = article["MedlineCitation"]
pmid = str(medline.get("PMID", ""))
art = medline.get("Article", {})
title = str(art.get("ArticleTitle", "")).strip()
abstract_node = art.get("Abstract")
if not abstract_node:
continue
parts = abstract_node.get("AbstractText", [])
if not parts:
continue
chunks: list[str] = []
for p in parts:
label = getattr(p, "attributes", {}).get("Label") if hasattr(p, "attributes") else None
text = str(p).strip()
if not text:
continue
chunks.append(f"{label}: {text}" if label else text)
abstract = " ".join(chunks).strip()
if not abstract:
continue
out.append({"pmid": pmid, "title": title, "abstract": abstract})
except (KeyError, TypeError, AttributeError):
continue
return out