Look up a DOI on Crossref and return canonical metadata. Args: doi: A DOI string. Accepts canonical form (``10.NNNN/...``), ``doi:`` prefix, or ``https://doi.org/`` URL form. timeout: HTTP timeout in seconds. Returns: ``CrossrefMetadata`` populated from
(
doi: str,
*,
timeout: float = 15.0,
)
| 79 | |
| 80 | |
| 81 | async def resolve_doi( |
| 82 | doi: str, |
| 83 | *, |
| 84 | timeout: float = 15.0, |
| 85 | ) -> CrossrefMetadata: |
| 86 | """Look up a DOI on Crossref and return canonical metadata. |
| 87 | |
| 88 | Args: |
| 89 | doi: A DOI string. Accepts canonical form (``10.NNNN/...``), |
| 90 | ``doi:`` prefix, or ``https://doi.org/`` URL form. |
| 91 | timeout: HTTP timeout in seconds. |
| 92 | |
| 93 | Returns: |
| 94 | ``CrossrefMetadata`` populated from the ``message`` block of |
| 95 | Crossref's response. |
| 96 | |
| 97 | Raises: |
| 98 | ValueError: If the DOI is malformed. |
| 99 | httpx.HTTPStatusError: On 404 (DOI not registered) or 5xx. |
| 100 | """ |
| 101 | normalized = _normalize_doi(doi) |
| 102 | if not _DOI_PATTERN.match(normalized): |
| 103 | raise ValueError(f"malformed DOI: {doi!r}") |
| 104 | |
| 105 | url = f"{CROSSREF_BASE_URL}/{normalized}" |
| 106 | async with httpx.AsyncClient( |
| 107 | timeout=timeout, follow_redirects=True |
| 108 | ) as client: |
| 109 | response = await client.get(url) |
| 110 | response.raise_for_status() |
| 111 | body = response.json() |
| 112 | |
| 113 | msg = body.get("message", {}) |
| 114 | titles = msg.get("title") or [] |
| 115 | container = msg.get("container-title") or [] |
| 116 | issued = msg.get("issued", {}).get("date-parts") |
| 117 | |
| 118 | return CrossrefMetadata( |
| 119 | doi=normalized, |
| 120 | title=titles[0] if titles else None, |
| 121 | authors=_format_authors(msg.get("author")), |
| 122 | journal=container[0] if container else None, |
| 123 | publisher=msg.get("publisher"), |
| 124 | published_at=_parse_crossref_date(issued), |
| 125 | primary_url=msg.get("URL"), |
| 126 | ) |