from __future__ import annotations from dataclasses import dataclass import logging from typing import Any from app.services.domains.publications.types import PublicationListItem from app.services.domains.unpaywall.application import OaResolutionOutcome, resolve_publication_oa_outcomes from app.settings import settings logger = logging.getLogger(__name__) @dataclass(frozen=True) class PipelineOutcome: outcome: OaResolutionOutcome | None scholar_candidates: Any | None # Kept for backward compatibility with calling signatures async def resolve_publication_pdf_outcome_for_row( *, row: PublicationListItem, request_email: str | None, ) -> PipelineOutcome: # 1. OpenAlex OA openalex_outcome = await _openalex_outcome(row, request_email=request_email) if openalex_outcome and openalex_outcome.pdf_url: return PipelineOutcome(openalex_outcome, None) # 2. arXiv arxiv_outcome = await _arxiv_outcome(row, request_email=request_email) if arxiv_outcome and arxiv_outcome.pdf_url: return PipelineOutcome(arxiv_outcome, None) # 3. Unpaywall (which falls back to Crossref) oa_outcome = await _oa_outcome(row=row, request_email=request_email) return PipelineOutcome(oa_outcome, None) async def _openalex_outcome(row: PublicationListItem, request_email: str | None) -> OaResolutionOutcome | None: from app.services.domains.openalex.client import OpenAlexClient from app.services.domains.openalex.matching import find_best_match if not row.title: return None import re safe_title = re.sub(r"[^\w\s]", " ", row.title) safe_title = " ".join(safe_title.split()) if not safe_title: return None client = OpenAlexClient(api_key=settings.openalex_api_key, mailto=request_email or settings.crossref_api_mailto) try: openalex_works = await client.get_works_by_filter({"title.search": safe_title}, limit=5) match = find_best_match( target_title=row.title, target_year=row.year, target_authors=row.scholar_label, candidates=openalex_works, ) if match and match.oa_url: return OaResolutionOutcome( publication_id=row.publication_id, doi=match.doi, pdf_url=match.oa_url, failure_reason=None, source="openalex", used_crossref=False, ) except Exception as exc: logger.warning( "publications.pdf_resolution.openalex_failed", extra={"event": "publications.pdf_resolution.openalex_failed", "error": str(exc)}, ) return None async def _arxiv_outcome(row: PublicationListItem, request_email: str | None) -> OaResolutionOutcome | None: from app.services.domains.arxiv.application import discover_arxiv_id_for_publication try: arxiv_id = await discover_arxiv_id_for_publication(item=row, request_email=request_email) if arxiv_id: pdf_url = f"https://arxiv.org/pdf/{arxiv_id}.pdf" return OaResolutionOutcome( publication_id=row.publication_id, doi=None, pdf_url=pdf_url, failure_reason=None, source="arxiv", used_crossref=False, ) except Exception as exc: logger.warning( "publications.pdf_resolution.arxiv_failed", extra={"event": "publications.pdf_resolution.arxiv_failed", "error": str(exc)}, ) return None async def _oa_outcome( *, row: PublicationListItem, request_email: str | None, ) -> OaResolutionOutcome | None: outcomes = await resolve_publication_oa_outcomes([row], request_email=request_email) return outcomes.get(row.publication_id)