docs: spec schéma en cache seul (#78)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Colin Maudry
2026-06-12 11:10:13 +02:00
parent 8a853b2384
commit 0ebbfcd872
@@ -70,28 +70,43 @@ touche l'API. On ne paie pas cette complexité tant qu'on n'en a pas la preuve.
## Décisions ## Décisions
1. **Schéma** : URL primaire, fallback local. (confirmé) 1. **Schéma** : URL primaire, **cache seul** en fallback (on supprime
`DATA_SCHEMA_LOCAL`). (confirmé)
2. **DuckDB** : réutiliser le dernier DuckDB construit en cas d'échec. (confirmé) 2. **DuckDB** : réutiliser le dernier DuckDB construit en cas d'échec. (confirmé)
3. **Last-known-good réel du schéma** : après un fetch distant réussi, persister 3. **Last-known-good réel du schéma** : après un fetch distant réussi, persister
le schéma localement pour que le fallback soit toujours le _dernier schéma le schéma dans un cache local pour que le fallback soit toujours le _dernier
distant fonctionnel_. (confirmé) schéma distant fonctionnel_. (confirmé)
### Nuance sur le chemin de persistance du schéma ### Chemin de persistance du schéma : `DATA_SCHEMA_CACHE` seul
L'utilisateur a demandé « écrire dans `DATA_SCHEMA_LOCAL` ». Mais en dev, On remplace `DATA_SCHEMA_LOCAL` (qui pointait, en dev, vers
`DATA_SCHEMA_LOCAL = ../decp-processing/dist/schema.json` — un fichier d'un **autre `../decp-processing/dist/schema.json` — un fichier cross-repo qu'on ne veut pas
repo**. L'écraser au boot salirait ce repo. écraser) par un **cache unique possédé par l'app**.
**Choix retenu (à confirmer en relecture) :** introduire un **chemin de cache
dédié que l'app possède**, distinct du fichier statique de secours.
- `DATA_SCHEMA_PATH` (URL) — source primaire. - `DATA_SCHEMA_PATH` (URL) — source primaire.
- `DATA_SCHEMA_CACHE` (nouveau, ex. défaut `./schema.cache.json`) — écrit après - `DATA_SCHEMA_CACHE` (nouveau, ex. défaut `./schema.cache.json`) — écrit après
chaque fetch distant réussi ; lu en fallback n°1 (dernier distant fonctionnel). chaque fetch distant réussi, lu en fallback.
- `DATA_SCHEMA_LOCAL` — graine statique de secours (fichier `decp-processing`),
**lue mais jamais écrite**.
Chaîne de résolution : `URL → cache → local statique → RuntimeError`. Chaîne de résolution : `URL → cache → RuntimeError`.
**Pourquoi c'est suffisant.** Le déploiement est en place sur un VM persistant
(`ssh → cd /var/www/APP_NAME → git pull → restart systemd`), donc le fichier de
cache survit aux déploiements — **même garantie de persistance que le DuckDB
réutilisé**. Tous les incidents constatés (env oubliée, parquet KO, URL schéma en
erreur) surviennent sur un **redéploiement** d'un hôte déjà chaud, où le cache a
déjà été écrit par un boot précédent réussi ⇒ couvert.
**Seul cas non couvert (assumé) :** le _cold start absolu_ — un hôte qui n'a jamais
booté avec succès **et** URL distante down au même instant. Étroit, non-récurrent.
Fermable plus tard par une graine commitée in-repo si jamais il se matérialise
(YAGNI).
**Contraintes :**
- `DATA_SCHEMA_CACHE` (`./schema.cache.json`) doit être **`.gitignore`** — sinon le
`git pull` du déploiement entrerait en conflit. (Comme `decp.duckdb` aujourd'hui.)
- En dev, plus de fallback vers le schéma frais de `decp-processing` : on bascule
sur le cache (dernier schéma data.gouv). Acceptable, l'URL restant primaire.
## Design ## Design
@@ -135,20 +150,19 @@ def _ensure_database() -> Path:
### Invariant 2 — Schéma (`src/utils/data.py`) ### Invariant 2 — Schéma (`src/utils/data.py`)
> Un schéma valide non-vide est toujours retourné si une source (distant, cache, > Un schéma valide non-vide est toujours retourné si une source (distant ou cache)
> ou local statique) en fournit un. Échec dur seulement si aucune. > en fournit un. Échec dur seulement si aucune.
```python ```python
def get_data_schema() -> dict: def get_data_schema() -> dict:
cache_path = os.getenv("DATA_SCHEMA_CACHE", "./schema.cache.json")
raw = _fetch_remote_schema(os.getenv("DATA_SCHEMA_PATH")) # dict valide | None raw = _fetch_remote_schema(os.getenv("DATA_SCHEMA_PATH")) # dict valide | None
if raw is not None: if raw is not None:
_persist_schema_cache(raw, os.getenv("DATA_SCHEMA_CACHE", "./schema.cache.json")) _persist_schema_cache(raw, cache_path)
else: else:
raw = _load_schema_file(os.getenv("DATA_SCHEMA_CACHE", "./schema.cache.json")) raw = _load_schema_file(cache_path)
if raw is None: if raw is None:
raw = _load_schema_file(os.getenv("DATA_SCHEMA_LOCAL", "")) raise RuntimeError("Aucun schéma disponible (ni distant ni cache).")
if raw is None:
raise RuntimeError("Aucun schéma disponible (distant, cache ni local).")
return OrderedDict((c["name"], c) for c in raw["fields"]) return OrderedDict((c["name"], c) for c in raw["fields"])
``` ```
@@ -173,11 +187,15 @@ Couvrir chaque branche de fallback. Sans dépendre du réseau réel.
2. URL renvoie une erreur HTTP (mock 500) ⇒ fallback cache. 2. URL renvoie une erreur HTTP (mock 500) ⇒ fallback cache.
3. URL renvoie un JSON malformé (sans `"fields"`) ⇒ fallback cache. 3. URL renvoie un JSON malformé (sans `"fields"`) ⇒ fallback cache.
4. URL KO + cache présent ⇒ schéma du cache. 4. URL KO + cache présent ⇒ schéma du cache.
5. URL KO + cache absent + local statique présent ⇒ schéma local. 5. URL KO + cache absent `RuntimeError` claire.
6. Toutes sources KO ⇒ `RuntimeError` claire. 6. Échec d'écriture du cache ⇒ schéma quand même retourné (non bloquant).
7. Échec d'écriture du cache ⇒ schéma quand même retourné (non bloquant).
**Bootstrap DuckDB (`_ensure_database`) :** 8. `should_rebuild` lève + DuckDB existant ⇒ réutilisé, pas d'exception. 9. `build_database` lève + DuckDB existant ⇒ réutilisé, pas d'exception. 10. Échec + **aucun** DuckDB (cold start) ⇒ ré-lève. 11. Cas nominal : rebuild nécessaire et possible ⇒ build effectué. **Bootstrap DuckDB (`_ensure_database`) :**
7. `should_rebuild` lève + DuckDB existant ⇒ réutilisé, pas d'exception.
8. `build_database` lève + DuckDB existant ⇒ réutilisé, pas d'exception.
9. Échec + **aucun** DuckDB (cold start) ⇒ ré-lève.
10. Cas nominal : rebuild nécessaire et possible ⇒ build effectué.
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`).
@@ -190,12 +208,16 @@ DuckDB et schéma temporaires (`tmp_path`).
## Variables d'environnement ## Variables d'environnement
| Variable | Rôle | Changement | | Variable | Rôle | Changement |
| ------------------------ | --------------------------------------- | -------------------- | | ------------------------ | --------------------------------------- | ------------ |
| `DATA_FILE_PARQUET_PATH` | Source parquet (URL ou chemin) | inchangé | | `DATA_FILE_PARQUET_PATH` | Source parquet (URL ou chemin) | inchangé |
| `DATA_SCHEMA_PATH` | URL schéma (primaire) | inchangé | | `DATA_SCHEMA_PATH` | URL schéma (primaire) | inchangé |
| `DATA_SCHEMA_LOCAL` | Fichier schéma de secours statique | **lu, jamais écrit** | | `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é |
Mettre à jour `.template.env` avec `DATA_SCHEMA_CACHE`. À faire côté config :
- Ajouter `DATA_SCHEMA_CACHE` à `.template.env`, retirer `DATA_SCHEMA_LOCAL` de
`.template.env` / `.env`.
- Ajouter `schema.cache.json` (ou la valeur de `DATA_SCHEMA_CACHE`) au `.gitignore`.