feat: add settings service and config API routes

Add a key-value settings table (Setting model) with DB > env > default
resolution chain. Replace hardcoded Ollama constants in ollama_service
with dynamic _get_config() lookups. Add /api/config/ endpoints for
settings CRUD and tag management (create, rename).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 16:09:02 +01:00
parent 4fe6a4b2c9
commit a1d708d083
7 changed files with 283 additions and 14 deletions

View File

@@ -16,6 +16,7 @@ from .routes import (
dashboard_router,
revenus_router,
ia_router,
config_router,
)
app = FastAPI(
@@ -43,6 +44,7 @@ app.include_router(analytics_router)
app.include_router(dashboard_router)
app.include_router(revenus_router)
app.include_router(ia_router)
app.include_router(config_router)
# Health check endpoints (keep in main app)

View File

@@ -7,6 +7,7 @@ from .analytics import router as analytics_router
from .dashboard import router as dashboard_router
from .revenus import router as revenus_router
from .ia import router as ia_router
from .config import router as config_router
__all__ = [
"extraction_router",
@@ -16,4 +17,5 @@ __all__ = [
"dashboard_router",
"revenus_router",
"ia_router",
"config_router",
]

View File

@@ -0,0 +1,134 @@
"""Routes de configuration — Settings LLM et gestion des tags."""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session
from ...database import get_session, DatabaseService
from ...database.models import Tag
from ...services.settings_service import (
get_all_settings,
set_setting,
delete_setting,
SETTINGS_REGISTRY,
)
router = APIRouter(prefix="/api/config", tags=["config"])
# ============================================================
# Schemas
# ============================================================
class SettingUpdate(BaseModel):
value: str
class TagCreate(BaseModel):
nom: str
class TagUpdate(BaseModel):
nom: str
# ============================================================
# Settings endpoints
# ============================================================
@router.get("/settings")
async def list_settings(
session: Session = Depends(get_session),
) -> dict:
"""Liste tous les settings avec valeur et source."""
return get_all_settings(session)
@router.put("/settings/{key}")
async def update_setting(
key: str,
body: SettingUpdate,
session: Session = Depends(get_session),
) -> dict:
"""Met à jour un setting (persiste en DB)."""
if key not in SETTINGS_REGISTRY:
raise HTTPException(status_code=404, detail=f"Setting inconnu : {key}")
set_setting(session, key, body.value)
return {"key": key, "value": body.value, "source": "database"}
@router.delete("/settings/{key}")
async def reset_setting(
key: str,
session: Session = Depends(get_session),
) -> dict:
"""Reset un setting au défaut (supprime l'override DB)."""
if key not in SETTINGS_REGISTRY:
raise HTTPException(status_code=404, detail=f"Setting inconnu : {key}")
deleted = delete_setting(session, key)
# Return the resolved value after deletion
all_settings = get_all_settings(session)
return all_settings[key]
# ============================================================
# Tags endpoints
# ============================================================
@router.get("/tags")
async def list_tags(
session: Session = Depends(get_session),
) -> list[dict]:
"""Liste tous les tags."""
db_service = DatabaseService(session)
tags = db_service.list_tags()
return [{"id": tag.id, "nom": tag.nom} for tag in tags]
@router.post("/tags", status_code=201)
async def create_tag(
body: TagCreate,
session: Session = Depends(get_session),
) -> dict:
"""Crée un nouveau tag (validation unicité)."""
nom = body.nom.strip()
if not nom:
raise HTTPException(status_code=400, detail="Le nom du tag ne peut pas être vide.")
existing = session.query(Tag).filter(Tag.nom == nom).first()
if existing:
raise HTTPException(status_code=409, detail=f"Le tag '{nom}' existe déjà.")
tag = Tag(nom=nom)
session.add(tag)
session.commit()
session.refresh(tag)
return {"id": tag.id, "nom": tag.nom}
@router.put("/tags/{tag_id}")
async def rename_tag(
tag_id: int,
body: TagUpdate,
session: Session = Depends(get_session),
) -> dict:
"""Renomme un tag (validation unicité)."""
nom = body.nom.strip()
if not nom:
raise HTTPException(status_code=400, detail="Le nom du tag ne peut pas être vide.")
tag = session.query(Tag).filter(Tag.id == tag_id).first()
if not tag:
raise HTTPException(status_code=404, detail="Tag introuvable.")
existing = session.query(Tag).filter(Tag.nom == nom, Tag.id != tag_id).first()
if existing:
raise HTTPException(status_code=409, detail=f"Le tag '{nom}' existe déjà.")
tag.nom = nom
session.commit()
session.refresh(tag)
return {"id": tag.id, "nom": tag.nom}