feat: refactor backend services and add direct PDF links, import/export, and dashboard sync

This commit is contained in:
Justin Visser 2026-02-19 23:45:52 +01:00
parent ba7976d935
commit 7f7a8ce2b0
26 changed files with 4170 additions and 2440 deletions

View file

@ -94,6 +94,7 @@ async def list_publications(
"citation_count": item.citation_count,
"venue_text": item.venue_text,
"pub_url": item.pub_url,
"pdf_url": item.pdf_url,
"is_read": item.is_read,
"first_seen_at": item.first_seen_at,
"is_new_in_latest_run": item.is_new_in_latest_run,

View file

@ -13,6 +13,9 @@ from app.api.errors import ApiException
from app.api.responses import success_payload
from app.api.runtime_deps import get_scholar_source
from app.api.schemas import (
DataExportEnvelope,
DataImportEnvelope,
DataImportRequest,
MessageEnvelope,
ScholarCreateRequest,
ScholarEnvelope,
@ -22,6 +25,7 @@ from app.api.schemas import (
)
from app.db.models import User
from app.db.session import get_db_session
from app.services import import_export as import_export_service
from app.services import scholars as scholar_service
from app.services.scholar_source import ScholarSource
from app.settings import settings
@ -81,6 +85,68 @@ async def list_scholars(
)
@router.get(
"/export",
response_model=DataExportEnvelope,
)
async def export_scholars_and_publications(
request: Request,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_api_current_user),
):
data = await import_export_service.export_user_data(
db_session,
user_id=current_user.id,
)
return success_payload(request, data=data)
@router.post(
"/import",
response_model=DataImportEnvelope,
)
async def import_scholars_and_publications(
payload: DataImportRequest,
request: Request,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_api_current_user),
):
if (
payload.schema_version is not None
and int(payload.schema_version) != import_export_service.EXPORT_SCHEMA_VERSION
):
raise ApiException(
status_code=400,
code="invalid_import_schema_version",
message=(
"Import schema version is not supported. "
f"Expected {import_export_service.EXPORT_SCHEMA_VERSION}."
),
)
try:
result = await import_export_service.import_user_data(
db_session,
user_id=current_user.id,
scholars=[item.model_dump() for item in payload.scholars],
publications=[item.model_dump() for item in payload.publications],
)
except import_export_service.ImportExportError as exc:
raise ApiException(
status_code=400,
code="invalid_import_payload",
message=str(exc),
) from exc
logger.info(
"api.scholars.imported",
extra={
"event": "api.scholars.imported",
"user_id": current_user.id,
**result,
},
)
return success_payload(request, data=result)
@router.post(
"",
response_model=ScholarEnvelope,

View file

@ -186,6 +186,74 @@ class ScholarImageUrlUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
class ScholarExportItemData(BaseModel):
scholar_id: str
display_name: str | None = None
is_enabled: bool = True
profile_image_override_url: str | None = None
model_config = ConfigDict(extra="forbid")
class PublicationExportItemData(BaseModel):
scholar_id: str
cluster_id: str | None = None
fingerprint_sha256: str | None = None
title: str
year: int | None = None
citation_count: int = 0
author_text: str | None = None
venue_text: str | None = None
pub_url: str | None = None
pdf_url: str | None = None
is_read: bool = False
model_config = ConfigDict(extra="forbid")
class DataExportData(BaseModel):
schema_version: int
exported_at: str
scholars: list[ScholarExportItemData]
publications: list[PublicationExportItemData]
model_config = ConfigDict(extra="forbid")
class DataExportEnvelope(BaseModel):
data: DataExportData
meta: ApiMeta
model_config = ConfigDict(extra="forbid")
class DataImportRequest(BaseModel):
schema_version: int | None = None
scholars: list[ScholarExportItemData] = Field(default_factory=list)
publications: list[PublicationExportItemData] = Field(default_factory=list)
model_config = ConfigDict(extra="forbid")
class DataImportResultData(BaseModel):
scholars_created: int
scholars_updated: int
publications_created: int
publications_updated: int
links_created: int
links_updated: int
skipped_records: int
model_config = ConfigDict(extra="forbid")
class DataImportEnvelope(BaseModel):
data: DataImportResultData
meta: ApiMeta
model_config = ConfigDict(extra="forbid")
class RunListItemData(BaseModel):
id: int
trigger_type: str
@ -505,6 +573,7 @@ class PublicationItemData(BaseModel):
citation_count: int
venue_text: str | None
pub_url: str | None
pdf_url: str | None
is_read: bool
first_seen_at: datetime
is_new_in_latest_run: bool