Spaces:
Sleeping
Sleeping
File size: 5,573 Bytes
b92b744 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | import json
import re
from typing import Any
from app.fulltext import normalize_pmcid
from app.ingest.common import NCT_PATTERN, empty_study, integer, pmid
NORMALIZER_VERSION = "openalex-v5-pmcid-locations"
PMC_LOCATION_RE = re.compile(
r"(?:ncbi\.nlm\.nih\.gov/pmc|pmc\.ncbi\.nlm\.nih\.gov|europepmc\.org(?:/pmc)?)"
r"/articles/(?:PMC)?(\d+)",
re.IGNORECASE,
)
REVIEW_TITLE_RE = re.compile(
r"\b(?:(?:systematic|scoping|narrative|umbrella|literature|integrative|rapid)\s+reviews?"
r"|meta[-\s]?analys(?:is|es)"
r"|(?:clinical\s+practice|consensus|evidence[- ]based)\s+(?:guidelines?|recommendations))\b"
r"|^(?:(?:international|clinical|practice)\s+)?(?:guidelines?|recommendations)\s+(?:for|on|from)\b",
re.IGNORECASE,
)
def is_nonprimary_work(work: dict, title: str) -> bool:
"""Recognize explicit synthesis/guideline titles without treating peer review as a review."""
kind = work.get("type")
return (kind is not None and kind not in {"article", "preprint", "dissertation"}) or bool(REVIEW_TITLE_RE.search(title))
def _decoded(value):
if isinstance(value, str):
try:
return json.loads(value)
except json.JSONDecodeError:
return value
return value
def _object(value) -> dict:
parsed = _decoded(value)
return parsed if isinstance(parsed, dict) else {}
def _array(value) -> list:
parsed = _decoded(value)
if isinstance(parsed, list):
return parsed
return [parsed] if isinstance(parsed, str) and parsed else []
def reconstruct_abstract(inverted: Any) -> str:
"""Rebuild sparse indexes without allocating according to an untrusted position.
Parquet exports may encode the index as a JSON string or DuckDB MAP. Invalid
positions are ignored; collisions choose the lexicographically first token.
"""
if isinstance(inverted, str):
try:
inverted = json.loads(inverted)
except json.JSONDecodeError:
return ""
if isinstance(inverted, list):
try:
inverted = dict(inverted)
except (ValueError, TypeError):
return ""
if not isinstance(inverted, dict):
return ""
# Older Arrow MAP conversion produces parallel key/value lists.
if set(inverted) == {"key", "value"}:
inverted = dict(zip(inverted["key"], inverted["value"], strict=False))
positions: dict[int, str] = {}
for word, offsets in sorted(inverted.items(), key=lambda item: str(item[0])):
if not isinstance(word, str) or not isinstance(offsets, (list, tuple)):
continue
for position in offsets:
if isinstance(position, int) and not isinstance(position, bool) and position >= 0:
positions.setdefault(position, word)
return " ".join(positions[position] for position in sorted(positions))
def work_pmcid(work: dict, ids: dict) -> str | None:
"""PMCID from ``ids`` or, as the Parquet snapshot omits it there, a PMC location URL."""
direct = normalize_pmcid(ids.get("pmcid") or work.get("pmcid"))
if direct:
return direct
for location in _array(work.get("locations")):
match = PMC_LOCATION_RE.search(str(_object(location).get("landing_page_url") or ""))
if match:
return normalize_pmcid(match.group(1))
return None
def normalize_work(work: dict, *, require_abstract: bool = False) -> dict | None:
abstract = reconstruct_abstract(work.get("abstract_inverted_index"))
if require_abstract and not abstract:
return None
identifier = str(work.get("id", "")).rstrip("/").rsplit("/", 1)[-1]
if not re.fullmatch(r"W\d+", identifier):
raise ValueError("OpenAlex work requires a valid W identifier")
study = empty_study(identifier, "openalex")
ids = _object(work.get("ids"))
paper_pmid = pmid(ids.get("pmid") or work.get("pmid"))
title = work.get("title") or work.get("display_name") or "Untitled study"
year = integer(work.get("publication_year"))
location = _object(work.get("primary_location"))
venue = _object(location.get("source"))
study.update({
"title": title, "abstract": abstract,
"abstract_available": bool(abstract), "work_type": work.get("type") or "unknown",
"authors": [author["display_name"]
for item in _array(work.get("authorships"))
if (author := _object(_object(item).get("author"))).get("display_name")],
"venue": venue.get("display_name", ""),
"url": ids.get("doi") or work.get("doi") or f"https://openalex.org/{identifier}",
"year": year, "publication_date": f"{year:04d}-01-01" if year else None,
"pmids": [paper_pmid] if paper_pmid else [],
# PubMed Central ID enables query-time full text; absent for most works.
"pmcid": work_pmcid(work, ids),
"nct_ids": sorted({x.upper() for x in NCT_PATTERN.findall(abstract)}),
"referenced_works": [str(x).rstrip("/").rsplit("/", 1)[-1]
for x in _array(work.get("referenced_works"))],
"is_retracted": bool(work.get("is_retracted", False)),
"is_review": is_nonprimary_work(work, title),
"normalizer_version": NORMALIZER_VERSION,
"snapshot_provenance": work.get("snapshot_provenance", {}),
"cited_by_count": integer(work.get("cited_by_count")) or 0,
"topics": [topic.get("display_name", "") if isinstance(topic, dict) else str(topic)
for topic in _array(work.get("topics"))],
})
return study
|