Add return type annotations and use 201 for upload

All endpoint functions, lifespan context manager, and get_connection now
have explicit return types. Upload endpoint returns 201 Created instead
of 200 to follow REST conventions.
This commit is contained in:
Pier-Jean Malandrino 2026-04-03 13:50:07 +02:00
parent 69ce1df5a1
commit 661568f2cb
5 changed files with 19 additions and 15 deletions

View file

@ -46,7 +46,7 @@ def _to_response(job) -> AnalysisResponse:
@router.post("", response_model=AnalysisResponse) @router.post("", response_model=AnalysisResponse)
async def create_analysis(body: CreateAnalysisRequest, service: ServiceDep): async def create_analysis(body: CreateAnalysisRequest, service: ServiceDep) -> AnalysisResponse:
"""Create a new analysis job for a document.""" """Create a new analysis job for a document."""
if not body.documentId or not body.documentId.strip(): if not body.documentId or not body.documentId.strip():
raise HTTPException(status_code=400, detail="documentId is required") raise HTTPException(status_code=400, detail="documentId is required")
@ -72,14 +72,14 @@ async def create_analysis(body: CreateAnalysisRequest, service: ServiceDep):
@router.get("", response_model=list[AnalysisResponse]) @router.get("", response_model=list[AnalysisResponse])
async def list_analyses(service: ServiceDep): async def list_analyses(service: ServiceDep) -> list[AnalysisResponse]:
"""List all analysis jobs.""" """List all analysis jobs."""
jobs = await service.find_all() jobs = await service.find_all()
return [_to_response(j) for j in jobs] return [_to_response(j) for j in jobs]
@router.get("/{job_id}", response_model=AnalysisResponse) @router.get("/{job_id}", response_model=AnalysisResponse)
async def get_analysis(job_id: str, service: ServiceDep): async def get_analysis(job_id: str, service: ServiceDep) -> AnalysisResponse:
"""Get a single analysis job.""" """Get a single analysis job."""
job = await service.find_by_id(job_id) job = await service.find_by_id(job_id)
if not job: if not job:
@ -88,7 +88,9 @@ async def get_analysis(job_id: str, service: ServiceDep):
@router.post("/{job_id}/rechunk", response_model=list[ChunkResponse]) @router.post("/{job_id}/rechunk", response_model=list[ChunkResponse])
async def rechunk_analysis(job_id: str, body: RechunkRequest, service: ServiceDep): async def rechunk_analysis(
job_id: str, body: RechunkRequest, service: ServiceDep
) -> list[ChunkResponse]:
"""Re-chunk a completed analysis with new chunking options.""" """Re-chunk a completed analysis with new chunking options."""
try: try:
chunks = await service.rechunk(job_id, body.chunkingOptions.model_dump()) chunks = await service.rechunk(job_id, body.chunkingOptions.model_dump())
@ -107,7 +109,7 @@ async def rechunk_analysis(job_id: str, body: RechunkRequest, service: ServiceDe
@router.delete("/{job_id}", status_code=204) @router.delete("/{job_id}", status_code=204)
async def delete_analysis(job_id: str, service: ServiceDep): async def delete_analysis(job_id: str, service: ServiceDep) -> None:
"""Delete an analysis job.""" """Delete an analysis job."""
deleted = await service.delete(job_id) deleted = await service.delete(job_id)
if not deleted: if not deleted:

View file

@ -25,8 +25,8 @@ def _to_response(doc) -> DocumentResponse:
) )
@router.post("/upload", response_model=DocumentResponse) @router.post("/upload", response_model=DocumentResponse, status_code=201)
async def upload(file: UploadFile): async def upload(file: UploadFile) -> DocumentResponse:
"""Upload a PDF document.""" """Upload a PDF document."""
if not file.filename: if not file.filename:
raise HTTPException(status_code=400, detail="No filename provided") raise HTTPException(status_code=400, detail="No filename provided")
@ -50,14 +50,14 @@ async def upload(file: UploadFile):
@router.get("", response_model=list[DocumentResponse]) @router.get("", response_model=list[DocumentResponse])
async def list_documents(): async def list_documents() -> list[DocumentResponse]:
"""List all documents.""" """List all documents."""
docs = await document_service.find_all() docs = await document_service.find_all()
return [_to_response(d) for d in docs] return [_to_response(d) for d in docs]
@router.get("/{doc_id}", response_model=DocumentResponse) @router.get("/{doc_id}", response_model=DocumentResponse)
async def get_document(doc_id: str): async def get_document(doc_id: str) -> DocumentResponse:
"""Get a single document.""" """Get a single document."""
doc = await document_service.find_by_id(doc_id) doc = await document_service.find_by_id(doc_id)
if not doc: if not doc:
@ -66,7 +66,7 @@ async def get_document(doc_id: str):
@router.delete("/{doc_id}", status_code=204) @router.delete("/{doc_id}", status_code=204)
async def delete_document(doc_id: str): async def delete_document(doc_id: str) -> None:
"""Delete a document and its file.""" """Delete a document and its file."""
deleted = await document_service.delete(doc_id) deleted = await document_service.delete(doc_id)
if not deleted: if not deleted:
@ -78,7 +78,7 @@ async def preview(
doc_id: str, doc_id: str,
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
dpi: int = Query(150, ge=72, le=300), dpi: int = Query(150, ge=72, le=300),
): ) -> Response:
"""Generate a PNG preview of a specific PDF page.""" """Generate a PNG preview of a specific PDF page."""
doc = await document_service.find_by_id(doc_id) doc = await document_service.find_by_id(doc_id)
if not doc: if not doc:

View file

@ -12,6 +12,7 @@ Conversion engine is selected via CONVERSION_ENGINE env var:
from __future__ import annotations from __future__ import annotations
import logging import logging
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
@ -72,7 +73,7 @@ def _build_analysis_service() -> AnalysisService:
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI) -> AsyncIterator[None]:
await init_db() await init_db()
app.state.analysis_service = _build_analysis_service() app.state.analysis_service = _build_analysis_service()
logger.info("Docling Studio backend ready (engine=%s)", settings.conversion_engine) logger.info("Docling Studio backend ready (engine=%s)", settings.conversion_engine)
@ -98,7 +99,7 @@ app.include_router(analyses_router)
@app.get("/api/health") @app.get("/api/health")
def health(): def health() -> dict[str, str]:
"""Health check endpoint.""" """Health check endpoint."""
return { return {
"status": "ok", "status": "ok",

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import logging import logging
import os import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import aiosqlite import aiosqlite
@ -82,7 +83,7 @@ async def get_db() -> aiosqlite.Connection:
@asynccontextmanager @asynccontextmanager
async def get_connection(): async def get_connection() -> AsyncIterator[aiosqlite.Connection]:
"""Context manager that opens and auto-closes a database connection.""" """Context manager that opens and auto-closes a database connection."""
db = await get_db() db = await get_db()
try: try:

View file

@ -89,7 +89,7 @@ class TestDocumentEndpoints:
"/api/documents/upload", "/api/documents/upload",
files={"file": ("uploaded.pdf", b"fake-pdf-content", "application/pdf")}, files={"file": ("uploaded.pdf", b"fake-pdf-content", "application/pdf")},
) )
assert resp.status_code == 200 assert resp.status_code == 201
data = resp.json() data = resp.json()
assert data["id"] == "new-1" assert data["id"] == "new-1"
assert data["filename"] == "uploaded.pdf" assert data["filename"] == "uploaded.pdf"