docling-studio/document-parser/api/stores.py
Pier-Jean Malandrino 245ef40a24 feat(#251): /api/stores router + wiring + schemas Pydantic + tests API
- Schemas camelCase : StoreInfoResponse, StoreResponse, StoreCreate/UpdateRequest, StoreDocEntryResponse
- Router /api/stores : GET (list), POST (create 201), GET/{slug}, PATCH/{slug}, DELETE/{slug} (204)
- Endpoints documents-in-store : GET /{slug}/documents, DELETE /{slug}/documents/{docId}
- Mapping erreurs StoreServiceError → HTTPException via http_status hint
- StoreService étendu : list_documents/remove_document avec injection optionnelle de document_repo (filename humain)
- Wiring lifespan : SqliteStoreRepository + SqliteDocumentStoreLinkRepository + StoreService sur app.state
- include_router(stores_router) après documents/analyses
- Tests API : 18 cas couvrant 200/201/204/404/409/422 + camelCase + cycle complet
2026-05-05 09:38:39 +02:00

170 lines
4.9 KiB
Python

"""Stores API router — CRUD on ingestion targets (#251)."""
from __future__ import annotations
import logging
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from api.schemas import (
StoreCreateRequest,
StoreDocEntryResponse,
StoreInfoResponse,
StoreResponse,
StoreUpdateRequest,
)
from domain.value_objects import StoreKind
from services.store_service import (
StoreService,
StoreServiceError,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/stores", tags=["stores"])
def _get_service(request: Request) -> StoreService:
return request.app.state.store_service
ServiceDep = Annotated[StoreService, Depends(_get_service)]
def _parse_kind(value: str) -> StoreKind:
try:
return StoreKind(value)
except ValueError as exc:
valid = ", ".join(k.value for k in StoreKind)
raise HTTPException(
status_code=422,
detail=f"Unknown store kind '{value}'. Valid kinds: {valid}.",
) from exc
def _store_to_response(store) -> StoreResponse:
return StoreResponse(
id=store.id,
name=store.name,
slug=store.slug,
kind=store.kind.value,
embedder=store.embedder,
is_default=store.is_default,
config=store.config,
created_at=str(store.created_at),
)
def _info_to_response(view) -> StoreInfoResponse:
return StoreInfoResponse(
name=view.name,
slug=view.slug,
type=view.kind,
embedder=view.embedder,
is_default=view.is_default,
document_count=view.document_count,
chunk_count=view.chunk_count,
connected=view.connected,
error_message=view.error_message,
)
def _doc_entry_to_response(entry) -> StoreDocEntryResponse:
return StoreDocEntryResponse(
doc_id=entry.doc_id,
filename=entry.filename,
state=entry.state,
chunk_count=entry.chunk_count,
pushed_at=entry.pushed_at,
)
@router.get("", response_model=list[StoreInfoResponse])
async def list_stores(service: ServiceDep) -> list[StoreInfoResponse]:
views = await service.list_stores()
return [_info_to_response(v) for v in views]
@router.post("", response_model=StoreResponse, status_code=201)
async def create_store(
payload: StoreCreateRequest,
service: ServiceDep,
) -> StoreResponse:
kind = _parse_kind(payload.kind)
try:
store = await service.create_store(
name=payload.name,
slug=payload.slug,
kind=kind,
embedder=payload.embedder,
config=payload.config,
is_default=payload.is_default,
)
except StoreServiceError as exc:
raise HTTPException(status_code=exc.http_status, detail=str(exc)) from exc
return _store_to_response(store)
@router.get("/{slug}", response_model=StoreResponse)
async def get_store(slug: str, service: ServiceDep) -> StoreResponse:
try:
store = await service.get_by_slug(slug)
except StoreServiceError as exc:
raise HTTPException(status_code=exc.http_status, detail=str(exc)) from exc
return _store_to_response(store)
@router.patch("/{slug}", response_model=StoreResponse)
async def update_store(
slug: str,
payload: StoreUpdateRequest,
service: ServiceDep,
) -> StoreResponse:
kind = _parse_kind(payload.kind) if payload.kind is not None else None
try:
store = await service.update_store(
slug,
name=payload.name,
new_slug=payload.slug,
kind=kind,
embedder=payload.embedder,
config=payload.config,
is_default=payload.is_default,
)
except StoreServiceError as exc:
raise HTTPException(status_code=exc.http_status, detail=str(exc)) from exc
return _store_to_response(store)
@router.delete("/{slug}", status_code=204)
async def delete_store(slug: str, service: ServiceDep) -> Response:
try:
await service.delete_store(slug)
except StoreServiceError as exc:
raise HTTPException(status_code=exc.http_status, detail=str(exc)) from exc
return Response(status_code=204)
@router.get("/{slug}/documents", response_model=list[StoreDocEntryResponse])
async def list_store_documents(
slug: str,
service: ServiceDep,
) -> list[StoreDocEntryResponse]:
try:
entries = await service.list_documents(slug)
except StoreServiceError as exc:
raise HTTPException(status_code=exc.http_status, detail=str(exc)) from exc
return [_doc_entry_to_response(e) for e in entries]
@router.delete("/{slug}/documents/{doc_id}", status_code=204)
async def remove_store_document(
slug: str,
doc_id: str,
service: ServiceDep,
) -> Response:
try:
await service.remove_document(slug, doc_id)
except StoreServiceError as exc:
raise HTTPException(status_code=exc.http_status, detail=str(exc)) from exc
return Response(status_code=204)