File size: 6,436 Bytes
bb74b82
b4cc1c5
 
bb74b82
 
 
b4cc1c5
bb74b82
b4cc1c5
 
bb74b82
 
 
 
 
 
 
 
 
 
 
 
 
b4cc1c5
 
 
 
bb31829
de46be0
890e849
bb74b82
890e849
bb74b82
b4cc1c5
 
0c91c9b
f14102c
bb74b82
ecbec06
 
 
 
f14102c
 
 
 
ecbec06
f14102c
 
c9d381c
243a84a
 
b4cc1c5
bb74b82
 
 
 
b4cc1c5
890e849
 
bb74b82
890e849
781c660
bb74b82
 
 
890e849
 
0c91c9b
 
 
890e849
0c91c9b
de46be0
 
 
 
 
 
 
 
 
890e849
 
 
bb74b82
 
 
 
b4cc1c5
 
bb74b82
 
 
b4cc1c5
 
 
890e849
b4cc1c5
 
781c660
d86e268
 
c9d381c
 
 
 
 
 
 
bb74b82
 
 
 
 
2ff13b5
 
 
 
 
b4cc1c5
bb74b82
 
 
 
 
 
f14102c
ecbec06
 
f14102c
ecbec06
 
 
f14102c
 
bb74b82
f14102c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
"""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)