docs: spec surfaces C/D (chargements pages au boot) (#78)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Colin Maudry
2026-06-12 11:23:35 +02:00
parent 0ebbfcd872
commit 20f81b5732
@@ -177,6 +177,62 @@ Helpers :
un échec d'écriture est loggé mais **non bloquant** (le schéma en mémoire reste un échec d'écriture est loggé mais **non bloquant** (le schéma en mémoire reste
valide). valide).
### Invariant 3 — Chargements au niveau module des pages
> L'import d'une page (exécuté au boot via `use_pages`) ne doit jamais tuer le
> démarrage à cause d'une ressource externe KO. Une ressource indisponible
> dégrade gracieusement l'affichage.
Audit des chargements à l'import (tous les `layout` de pages sont au niveau
module ⇒ leur contenu s'exécute au boot). Deux points de rupture **externes** :
**C — `src/pages/tableau.py:36-38`.** `get_last_modified(URL parquet)` fait un
HTTP HEAD **sans gestion d'erreur** (URL injoignable, en-tête `last-modified`
absent) ⇒ import KO ⇒ boot KO. C'est le même piège que `db.py`, mais dans une page.
Correctif : un helper best-effort dans `src/utils/__init__.py` qui ne lève jamais
et retombe sur le mtime du DuckDB (garanti présent par l'Invariant 1) :
```python
def get_data_update_timestamp(parquet_path: str, fallback_path: str | None = None) -> float | None:
"""Date de MAJ des données, best-effort, sans jamais lever (usage au boot)."""
try:
return get_last_modified(parquet_path)
except Exception as e:
logger.warning(f"Date de mise à jour des données indisponible ({e})")
if fallback_path:
try:
return os.path.getmtime(fallback_path)
except OSError:
pass
return None
```
`tableau.py` l'utilise et gère le cas `None` (affiche « date inconnue »,
`update_date_iso = ""`).
**D — `src/pages/a-propos.py:103`.** `get_sources_tables(SOURCE_STATS_CSV_PATH)`
(`src/figures.py:121`) fait `pl.read_csv(source_path)` mais ne rattrape que
`URLError, HTTPError` — pas les erreurs Polars, ni `source_path` vide/`None`, ni
fichier absent ⇒ import KO ⇒ boot KO.
Correctif : élargir le `except` et gérer le chemin vide :
```python
def get_sources_tables(source_path) -> html.Div:
try:
if not source_path:
raise ValueError("SOURCE_STATS_CSV_PATH non défini")
dff = pl.read_csv(source_path)
except Exception as e:
logger.warning(f"Sources de données indisponibles ({e})")
return html.Div("Sources de données momentanément indisponibles.")
... # suite inchangée
```
Hors périmètre des pages : `data/departements.json` + `.geojson` (fichiers
in-repo apportés par `git pull`, pas pilotés par env/URL — voir Hors périmètre).
## Tests (TDD) ## Tests (TDD)
Couvrir chaque branche de fallback. Sans dépendre du réseau réel. Couvrir chaque branche de fallback. Sans dépendre du réseau réel.
@@ -200,11 +256,24 @@ Couvrir chaque branche de fallback. Sans dépendre du réseau réel.
Mocker `get_last_modified` / `build_database` / `httpx.get` ; utiliser des fichiers Mocker `get_last_modified` / `build_database` / `httpx.get` ; utiliser des fichiers
DuckDB et schéma temporaires (`tmp_path`). DuckDB et schéma temporaires (`tmp_path`).
**Chargements de pages (Invariant 3) :**
11. `get_data_update_timestamp` : `get_last_modified` lève + `fallback_path`
existant ⇒ retourne le mtime du fallback (pas d'exception).
12. `get_data_update_timestamp` : tout KO (lève + pas de fallback) ⇒ `None`.
13. `get_data_update_timestamp` : cas nominal ⇒ retourne la valeur de
`get_last_modified` (mocké).
14. `get_sources_tables(None)``html.Div` de repli (pas d'exception).
15. `get_sources_tables("/inexistant.csv")``html.Div` de repli.
16. `get_sources_tables(<csv valide>)``html.Div` contenant la `DataTable`.
## Hors périmètre ## Hors périmètre
- Séparation des process API / Web (reportée — voir plus haut). - Séparation des process API / Web (reportée — voir plus haut).
- Surveillance / alerting externe (les logs `error`/`critical` suffisent pour ce lot). - Surveillance / alerting externe (les logs `error`/`critical` suffisent pour ce lot).
- Validation fine du contenu du parquet au-delà de « lisible par Polars/DuckDB ». - Validation fine du contenu du parquet au-delà de « lisible par Polars/DuckDB ».
- Durcissement des `open()` in-repo (`data/departements.json` + `.geojson`) :
fichiers versionnés, apportés par `git pull`, jamais pilotés par env/URL (YAGNI).
## Variables d'environnement ## Variables d'environnement
@@ -215,6 +284,7 @@ DuckDB et schéma temporaires (`tmp_path`).
| `DATA_SCHEMA_LOCAL` | Ancien fichier de secours statique | **supprimé** | | `DATA_SCHEMA_LOCAL` | Ancien fichier de secours statique | **supprimé** |
| `DATA_SCHEMA_CACHE` | Cache last-known-good du schéma distant | **nouveau** | | `DATA_SCHEMA_CACHE` | Cache last-known-good du schéma distant | **nouveau** |
| `DUCKDB_PATH` | Fichier DuckDB | inchangé | | `DUCKDB_PATH` | Fichier DuckDB | inchangé |
| `SOURCE_STATS_CSV_PATH` | CSV stats sources (page À propos, D) | inchangé |
À faire côté config : À faire côté config :