- 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
170 lines
4.9 KiB
Python
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)
|