Versión 3 Backend - Endpoints finales corregidos

This commit is contained in:
Mireya Cueto Garrido
2026-04-27 13:39:32 +02:00
parent a286c2e3ae
commit 96f01c0126
4343 changed files with 1046097 additions and 465 deletions
+101
View File
@@ -0,0 +1,101 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import Response
from sqlalchemy.orm import Session
from uuid import UUID
from app.db.session import get_db
from app.db.models import Publication, Researcher
from app.security.api_key import get_api_key
from app.services.sword_generator import SWORDGenerator
from app.services.zip_generator import ZIPGenerator
router = APIRouter(prefix="/export")
def validate_uuid_list(pub_ids: list[str]) -> list[UUID]:
valid_ids = []
for pid in pub_ids:
try:
valid_ids.append(UUID(pid))
except Exception:
raise HTTPException(
status_code=400,
detail=f"Invalid publication ID (not UUID): {pid}"
)
return valid_ids
@router.post("/sword/publications")
async def export_multiple_sword(
pub_ids: list[str],
db: Session = Depends(get_db),
api_key: str = Depends(get_api_key)
):
validate_uuid_list(pub_ids)
pubs = db.query(Publication).filter(Publication.id.in_(pub_ids)).all()
if not pubs:
raise HTTPException(status_code=404, detail="No publications found")
researcher = db.query(Researcher).filter_by(id=pubs[0].researcher_id).first()
xml_bytes = SWORDGenerator.generate_feed_xml(researcher, pubs)
return Response(content=xml_bytes, media_type="application/xml")
@router.get("/sword/researcher/{orcid_id}")
async def export_researcher_sword(
orcid_id: str,
db: Session = Depends(get_db),
api_key: str = Depends(get_api_key)
):
researcher = db.query(Researcher).filter_by(orcid_id=orcid_id).first()
if not researcher:
raise HTTPException(status_code=404, detail="Researcher not found")
pubs = db.query(Publication).filter_by(researcher_id=researcher.id).all()
if not pubs:
raise HTTPException(status_code=404, detail="No publications found for this researcher")
xml_bytes = SWORDGenerator.generate_feed_xml(researcher, pubs)
return Response(content=xml_bytes, media_type="application/xml")
@router.post("/zip/publications")
async def export_multiple_zip(
pub_ids: list[str],
db: Session = Depends(get_db),
api_key: str = Depends(get_api_key)
):
validate_uuid_list(pub_ids)
pubs = db.query(Publication).filter(Publication.id.in_(pub_ids)).all()
if not pubs:
raise HTTPException(status_code=404, detail="No publications found")
researcher = db.query(Researcher).filter_by(id=pubs[0].researcher_id).first()
zip_bytes = ZIPGenerator.generate_zip(researcher, pubs)
return Response(content=zip_bytes, media_type="application/zip")
@router.get("/zip/researcher/{orcid_id}")
async def export_researcher_zip(
orcid_id: str,
db: Session = Depends(get_db),
api_key: str = Depends(get_api_key)
):
researcher = db.query(Researcher).filter_by(orcid_id=orcid_id).first()
if not researcher:
raise HTTPException(status_code=404, detail="Researcher not found")
pubs = db.query(Publication).filter_by(researcher_id=researcher.id).all()
if not pubs:
raise HTTPException(status_code=404, detail="No publications found for this researcher")
zip_bytes = ZIPGenerator.generate_zip(researcher, pubs)
return Response(content=zip_bytes, media_type="application/zip")
+189 -101
View File
@@ -1,120 +1,208 @@
from datetime import datetime
from typing import List
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import Response
from sqlalchemy.orm import Session
from app.schema.publication import PublicationSchema
from app.db.models import Publication, Researcher
from app.db.session import get_db
from app.repositories.researcher_repository import ResearcherRepository
from app.repositories.publication_repository import PublicationRepository
from app.services.sync_service import SyncService
from app.services.sword_exporter import SWORDExporter
from app.utils.orcid_validator import is_valid_orcid
from app.schema.researcher import ResearcherWithPublicationsSchema
from app.services.normalizer import PublicationNormalizer
from app.services.orcid_client import get_works_summary, get_work_detail
router = APIRouter(prefix="/researchers", tags=["researchers"])
def validate_orcid_or_400(orcid_id: str):
if not is_valid_orcid(orcid_id):
raise HTTPException(
status_code=400,
detail=f"ORCID ID '{orcid_id}' no es válido según el formato y dígito de control."
# ---------------------------------------------------------
# Función auxiliar: detectar si una publicación ha cambiado
# ---------------------------------------------------------
def publication_changed(existing: Publication, data: dict) -> bool:
fields = [
"title", "subtitle", "type", "journal",
"pub_year", "pub_month", "pub_day",
"doi", "url", "short_description",
"citation_type", "citation_value",
"language_code", "country",
"external_ids", "contributors"
]
for f in fields:
if getattr(existing, f) != data[f]:
return True
return False
# ---------------------------------------------------------
# ENDPOINT 1: SEARCH + SYNC (sin contadores)
# ---------------------------------------------------------
@router.get("/search/{orcid_id}", response_model=ResearcherWithPublicationsSchema)
def search_and_sync_researcher(orcid_id: str, db: Session = Depends(get_db)):
# Buscar o crear Researcher
researcher = db.query(Researcher).filter(Researcher.orcid_id == orcid_id).first()
if not researcher:
researcher = Researcher(
orcid_id=orcid_id,
name=None,
authenticated=False,
last_sync_at=None,
)
db.add(researcher)
db.flush()
# Obtener works summary desde ORCID
works = get_works_summary(orcid_id)
groups = works.get("group", [])
publications: List[Publication] = []
for g in groups:
summaries = g.get("work-summary") or []
if not summaries:
continue
summary = summaries[0]
put_code = summary.get("put-code")
if put_code is None:
continue
# Obtener detalle del work
try:
detail = get_work_detail(orcid_id, put_code)
except Exception:
detail = None
# Normalizar datos
data = PublicationNormalizer.normalize(summary, detail)
# Ver si ya existe la publicación
existing = (
db.query(Publication)
.filter(
Publication.researcher_id == researcher.id,
Publication.put_code == data["put_code"],
)
.first()
)
if existing:
for field in [
"title", "subtitle", "type", "journal",
"pub_year", "pub_month", "pub_day",
"doi", "url", "short_description",
"citation_type", "citation_value",
"language_code", "country",
"external_ids", "contributors"
]:
setattr(existing, field, data[field])
existing.last_modified = datetime.utcnow()
existing.status = None
publications.append(existing)
else:
pub = Publication(
researcher_id=researcher.id,
**data,
last_modified=datetime.utcnow(),
)
pub.status = None
db.add(pub)
publications.append(pub)
@router.post("/", response_model=dict)
def create_researcher(orcid_id: str, db: Session = Depends(get_db)):
validate_orcid_or_400(orcid_id)
researcher.last_sync_at = datetime.utcnow()
db.commit()
db.refresh(researcher)
existing = ResearcherRepository.get_by_orcid(db, orcid_id)
if existing:
return {
"status": "ok",
"message": "Researcher ya existe.",
"orcid_id": existing.orcid_id,
"id": existing.id
}
# Aquí podrías opcionalmente validar que el ORCID existe en ORCID API
researcher = ResearcherRepository.create(db, orcid_id, name=None)
return {
"status": "ok",
"message": "Researcher creado correctamente.",
"orcid_id": researcher.orcid_id,
"id": researcher.id
}
return ResearcherWithPublicationsSchema(
researcher=researcher,
publications=publications,
new_records=0,
updated_records=0,
unchanged_records=0,
total_records=len(publications),
)
@router.get("/{orcid_id}", response_model=dict)
def get_researcher(orcid_id: str, db: Session = Depends(get_db)):
validate_orcid_or_400(orcid_id)
researcher = ResearcherRepository.get_by_orcid(db, orcid_id)
if not researcher:
raise HTTPException(status_code=404, detail="Researcher not found")
return {
"orcid_id": researcher.orcid_id,
"name": researcher.name,
"authenticated": researcher.authenticated,
"access_token": researcher.access_token,
"id": researcher.id,
"last_sync_at": researcher.last_sync_at,
}
@router.post("/{orcid_id}/sync", response_model=dict)
# ---------------------------------------------------------
# ENDPOINT 2: SYNC COMPLETO (con contadores + status)
# ---------------------------------------------------------
@router.post("/{orcid_id}/sync", response_model=ResearcherWithPublicationsSchema)
def sync_researcher(orcid_id: str, db: Session = Depends(get_db)):
validate_orcid_or_400(orcid_id)
service = SyncService()
result = service.sync_researcher(db, orcid_id)
return result
@router.get("/{orcid_id}/publications", response_model=list[PublicationSchema], tags=["researchers"])
def get_publications(orcid_id: str, db: Session = Depends(get_db)):
researcher = ResearcherRepository.get_by_orcid(db, orcid_id)
if not researcher:
raise HTTPException(status_code=404, detail="Researcher not found")
return researcher.publications
@router.get("/{orcid_id}/export/sword.xml")
def export_sword_xml(orcid_id: str, db: Session = Depends(get_db)):
validate_orcid_or_400(orcid_id)
researcher = ResearcherRepository.get_by_orcid(db, orcid_id)
researcher = db.query(Researcher).filter_by(orcid_id=orcid_id).first()
if not researcher:
raise HTTPException(status_code=404, detail="Researcher not found")
pubs = PublicationRepository.list_by_researcher(db, researcher.id)
xml_bytes = SWORDExporter.export_feed_xml(researcher, pubs)
works = get_works_summary(orcid_id)
groups = works.get("group", [])
return Response(
content=xml_bytes,
media_type="application/xml",
headers={
"Content-Disposition": f'attachment; filename="sword_{orcid_id}.xml"'
}
)
@router.get("/{orcid_id}/export/sword.zip")
def export_sword_zip(orcid_id: str, db: Session = Depends(get_db)):
validate_orcid_or_400(orcid_id)
researcher = ResearcherRepository.get_by_orcid(db, orcid_id)
if not researcher:
raise HTTPException(status_code=404, detail="Researcher not found")
pubs = PublicationRepository.list_by_researcher(db, researcher.id)
zip_bytes = SWORDExporter.export_zip(researcher, pubs)
return Response(
content=zip_bytes,
media_type="application/zip",
headers={
"Content-Disposition": f'attachment; filename="sword_{orcid_id}.zip"'
}
publications_output = []
new_count = 0
updated_count = 0
unchanged_count = 0
for g in groups:
summaries = g.get("work-summary") or []
if not summaries:
continue
summary = summaries[0]
put_code = summary.get("put-code")
if put_code is None:
continue
try:
detail = get_work_detail(orcid_id, put_code)
except Exception:
detail = None
data = PublicationNormalizer.normalize(summary, detail)
existing = (
db.query(Publication)
.filter(
Publication.researcher_id == researcher.id,
Publication.put_code == data["put_code"],
)
.first()
)
if existing:
if publication_changed(existing, data):
# updated
for field in data:
setattr(existing, field, data[field])
existing.last_modified = datetime.utcnow()
existing.status = "updated"
updated_count += 1
else:
# unchanged
existing.status = "unchanged"
unchanged_count += 1
pub = existing
else:
# new
pub = Publication(
researcher_id=researcher.id,
**data,
last_modified=datetime.utcnow(),
)
pub.status = "new"
db.add(pub)
new_count += 1
db.flush()
publications_output.append(pub)
researcher.last_sync_at = datetime.utcnow()
db.commit()
db.refresh(researcher)
return ResearcherWithPublicationsSchema(
researcher=researcher,
publications=publications_output,
new_records=new_count,
updated_records=updated_count,
unchanged_records=unchanged_count,
total_records=new_count + updated_count + unchanged_count,
)