Picarones / picarones /web /app.py
Claude
feat(web): Sprint A4 β€” sΓ©curitΓ© web (B-11 CSRF, M-3 /health)
c9d381c unverified
Raw
History Blame
6.44 kB
"""Interface web Picarones β€” orchestrateur FastAPI.
Lance avec :
.. code-block:: bash
picarones serve [--port 8000] [--host 127.0.0.1]
# ou directement :
uvicorn picarones.web.app:app --reload --port 8000
L'application est intentionnellement minimaliste : elle se contente
d'instancier ``FastAPI``, de monter le middleware de sΓ©curitΓ© (CSP,
en-tΓͺtes durcis), de servir les fichiers statiques, puis d'inclure
les 11 routers thΓ©matiques de :mod:`picarones.web.routers`. Toute la
logique mΓ©tier vit dans les sous-modules :
- :mod:`picarones.web.state` β€” singletons et helpers transverses
- :mod:`picarones.web.models` β€” Pydantic schemas
- :mod:`picarones.web.corpus_utils` β€” parsing XML, analyse corpus
- :mod:`picarones.web.engine_utils` β€” dΓ©tection moteurs, capacitΓ©s
- :mod:`picarones.web.benchmark_utils` β€” workers threadΓ©s
- :mod:`picarones.web.config_utils` β€” validation config utilisateur
- :mod:`picarones.web.routers.*` β€” 11 ``APIRouter`` thΓ©matiques
"""
from __future__ import annotations
import logging
import sqlite3
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from picarones import __version__
from picarones.web import state
from picarones.web.routers import (
benchmark as _benchmark_router,
config as _config_router,
corpus as _corpus_router,
engines as _engines_router,
history as _history_router,
home as _home_router,
importers as _importers_router,
normalization as _normalization_router,
reports as _reports_router,
synthesis as _synthesis_router,
system as _system_router,
)
from picarones.web.security import csp_middleware, csrf_middleware
_logger = logging.getLogger(__name__)
# ──────────────────────────────────────────────────────────────────────────
# Lifespan
# ──────────────────────────────────────────────────────────────────────────
@asynccontextmanager
async def _lifespan(app: FastAPI):
"""Hook de dΓ©marrage : marque les jobs orphelins comme ``interrupted``.
Au dΓ©marrage d'un nouveau processus, tous les jobs
encore en statut ``pending`` ou ``running`` en base sont
forcΓ©ment orphelins (le processus prΓ©cΓ©dent est mort sans les
finir). On les bascule en ``interrupted`` pour ne pas laisser
d'Γ©tat mensonger sur le tableau de bord.
"""
# NB : on accède via ``state.JOB_STORE`` (pas un import direct) pour
# que les fixtures de tests qui rΓ©-affectent ``state.JOB_STORE`` Γ 
# un store isolΓ© soient effectivement vues par le lifespan.
try:
state.JOB_STORE.mark_orphaned_jobs_interrupted()
except sqlite3.Error as exc: # pragma: no cover β€” dΓ©fense en profondeur
# Si la base de jobs est cassΓ©e au dΓ©marrage, on log en ``error``
# (pas ``warning``) β€” c'est un signal opΓ©rationnel : l'app
# tourne dans un Γ©tat dΓ©gradΓ©, le tableau de bord va Γͺtre incorrect.
_logger.error(
"[jobs] mark_orphaned_jobs_interrupted ÉCHOUÉ — "
"base SQLite inaccessible (%s) : le tableau de bord "
"affichera des jobs zombies.", exc,
)
yield
# ──────────────────────────────────────────────────────────────────────────
# Instance FastAPI
# ──────────────────────────────────────────────────────────────────────────
app = FastAPI(
title="Picarones",
description=(
"Plateforme de comparaison de moteurs OCR/HTR pour documents patrimoniaux"
),
version=__version__,
docs_url="/api/docs",
redoc_url="/api/redoc",
lifespan=_lifespan,
)
# Middleware CSP + en-tΓͺtes durcis (X-Frame-Options, etc.)
app.middleware("http")(csp_middleware)
# Sprint A4 (B-11) β€” protection CSRF, gated par PICARONES_CSRF_REQUIRED.
# En mode public (HuggingFace Space) : bypass complet, pas de cookie.
# En mode institutionnel : double-submit cookie + signature HMAC-SHA256.
# Le middleware s'enregistre toujours mais devient no-op si la variable
# d'env n'est pas activΓ©e β€” coΓ»t ~0 en mode public.
app.middleware("http")(csrf_middleware)
# ──────────────────────────────────────────────────────────────────────────
# Fichiers statiques
# ──────────────────────────────────────────────────────────────────────────
_STATIC_DIR = Path(__file__).parent / "static"
if _STATIC_DIR.is_dir():
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
# ──────────────────────────────────────────────────────────────────────────
# Routers thΓ©matiques
# ──────────────────────────────────────────────────────────────────────────
# Ordre indiffΓ©rent fonctionnellement, mais regroupΓ© par domaine
# pour la lisibilitΓ© (info β†’ donnΓ©es β†’ processus β†’ prΓ©sentation).
app.include_router(_system_router.router)
app.include_router(_engines_router.router)
app.include_router(_corpus_router.router)
app.include_router(_normalization_router.router)
app.include_router(_config_router.router)
app.include_router(_synthesis_router.router)
app.include_router(_history_router.router)
app.include_router(_reports_router.router)
app.include_router(_importers_router.router)
app.include_router(_benchmark_router.router)
app.include_router(_home_router.router)