Versión 3 Backend - Endpoints finales corregidos
This commit is contained in:
@@ -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
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user