| 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 |
|
|