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)
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."""
if not body.documentId or not body.documentId.strip():
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])
async def list_analyses(service: ServiceDep):
async def list_analyses(service: ServiceDep) -> list[AnalysisResponse]:
"""List all analysis jobs."""
jobs = await service.find_all()
return [_to_response(j) for j in jobs]
@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."""
job = await service.find_by_id(job_id)
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])
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."""
try:
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)
async def delete_analysis(job_id: str, service: ServiceDep):
async def delete_analysis(job_id: str, service: ServiceDep) -> None:
"""Delete an analysis job."""
deleted = await service.delete(job_id)
if not deleted:

View file

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

View file

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

View file

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

View file

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