108 lines
3.7 KiB
Python
108 lines
3.7 KiB
Python
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)
|