docs: rétablit la hiérarchie des titres et les motifs du document Data

40-data.md était passé à quatre titres de niveau 1 et etl/README.md à cinq,
alors que les huit autres documents d'architecture n'en ont qu'un. Les
sections ajoutées redescendent d'un niveau.

La réécriture de la section « Tables d'authentification » avait aussi vidé
quatre choix de modélisation de leur raison, dont le renvoi à l'ADR 0004 sur
audit_log.actor_id. Ces motifs sont rétablis, et les deux tables de
réinitialisation reçoivent le leur.

Documente enfin la frontière de confiance avec l'API Mock : les quatre
garde-fous, les plages de PHYSICAL_BOUNDS, et ce qu'il reste à faire.
This commit is contained in:
Johan LEROY
2026-09-21 10:21:18 +02:00
parent f238940867
commit de697b080d
2 changed files with 122 additions and 51 deletions
+58 -25
View File
@@ -96,8 +96,8 @@ Les flèches pleines représentent les traitements actuellement implémentés.
Les flèches pointillées représentent les éléments encore prévus comme cibles.
Les lectures futures de l'API et de Grafana visent l'agrégat continu plutôt que la table brute
lorsque cette partie TimescaleDB sera mise en place.
Les lectures de l'API et de Grafana viseront l'agrégat continu, pas la table brute : c'est tout
l'intérêt de TimescaleDB, et cela doit rester vrai quand les volumes augmenteront.
## Tables d'authentification
@@ -170,21 +170,28 @@ erDiagram
}
```
Plusieurs choix de modélisation portent une intention précise :
Six choix de modélisation portent une intention et se défendent seuls :
- **`app_user` et non `user`** : `user` est un mot réservé PostgreSQL, raccourci de
`CURRENT_USER`. Le nom rappelle aussi qu'il s'agit d'un compte applicatif.
- **`credentials_changed_at`, une seule colonne**, couvre notamment le changement de mot de passe,
le changement de rôle et la désactivation.
- **`refresh_token.expires_at` est absolu et hérité** du prédécesseur à chaque rotation.
- **`audit_log.actor_id` n'a aucune clé étrangère** afin de conserver les informations d'audit
même si l'entité d'origine évolue.
- `password_reset_token` ne stocke que l'empreinte du jeton et jamais sa valeur directement.
- `password_reset_attempt` est séparée de `audit_log`, car son volume peut être piloté
par des demandes externes répétées.
`CURRENT_USER`. Le nom rappelle en prime qu'il s'agit d'un compte applicatif, par opposition
au rôle PostgreSQL qui portera le cantonnement de l'ETL.
- **`credentials_changed_at`, une seule colonne**, couvre le changement de mot de passe, le
changement de rôle et la désactivation. Un compteur de version ne dirait rien à un humain qui
lit un audit.
- **`refresh_token.expires_at` est absolu et hérité** du prédécesseur à chaque rotation. S'il
glissait, la promesse de sept jours serait fictive et une session active ne finirait jamais.
- **`audit_log.actor_id` n'a aucune clé étrangère**, et `actor_email` comme `actor_role` sont
dénormalisés. Une contrainte `ON DELETE SET NULL` déclencherait un `UPDATE` que le déclencheur
d'ajout seul refuserait. Voir l'[ADR 0004](../adr/0004-journal-d-audit-en-ajout-seul.md).
- **`password_reset_token` ne stocke que l'empreinte du jeton**, jamais sa valeur. Une fuite de
la table ne donne donc rien à rejouer.
- **`password_reset_attempt` est séparée de `audit_log`** : son volume est piloté par le
demandeur, comme celui de `login_attempt`, donc elle doit pouvoir se purger.
`audit_log` porte des déclencheurs qui refusent `UPDATE`, `DELETE` et `TRUNCATE`.
Elle n'est donc **pas** une hypertable.
`audit_log` porte deux déclencheurs qui refusent `UPDATE`, `DELETE` et `TRUNCATE`. Elle n'est
donc **pas** une hypertable : une politique de rétention émettrait des `DELETE` qu'ils
refuseraient. `login_attempt`, à l'inverse, est faite pour se purger, puisque son volume est
piloté par l'attaquant.
## Gabarit de révision créant une hypertable
@@ -234,7 +241,8 @@ colonne de temps : les index déclarés dans la révision le couvrent déjà.
## Questions ouvertes
Elles portent maintenant principalement sur l'exploitation du schéma :
Elles relèvent du jalon J2, « valider le périmètre retenu ». Le schéma et l'ingestion sont
livrés : ce qui suit porte sur leur exploitation, plus sur leur forme.
- **Quelle granularité** conserver à long terme à l'ingestion : seconde, minute ou quart d'heure.
- **Quels agrégats continus** créer et sur quelles fenêtres.
@@ -287,7 +295,7 @@ Elles servent à l'analyse des données et ne sont pas considérées comme des a
- Une alerte peut être associée à une prévision du même site.
- Une alerte peut donner lieu à plusieurs recommandations.
# Ingestion des données historiques
## Ingestion des données historiques
Statut : `Fait`.
@@ -301,7 +309,7 @@ Les fichiers sources CSV et JSON sont nécessaires uniquement pour l'initialisat
Ils ne sont pas versionnés dans Git et sont placés localement dans `data/raw/`.
## Architecture du flux historique
### Architecture du flux historique
```text
Dataset CSV + métadonnées JSON
@@ -351,7 +359,7 @@ source = "csv"
dataset_id = identifiant du dataset
```
## Résultats validés pour l'historique
### Résultats validés pour l'historique
Le chargement de référence a permis d'obtenir :
@@ -366,7 +374,7 @@ aucune nouvelle mesure n'a été créée et le nombre de `reading` est resté à
La procédure détaillée d'installation, d'exécution, de validation et de contrôle du pipeline
est disponible dans `etl/README.md`.
# Ingestion depuis l'API Mock
## Ingestion depuis l'API Mock
Statut : `Fait`.
@@ -378,7 +386,7 @@ Le traitement est implémenté dans :
apps/backend/app/etl/mock_api_import.py
```
## Endpoints utilisés
### Endpoints utilisés
Le pipeline récupère les informations des sites depuis :
@@ -410,7 +418,7 @@ Les paramètres de ligne de commande disponibles pour l'import sont :
--dry-run
```
## Flux d'ingestion API Mock
### Flux d'ingestion API Mock
```text
API Mock
@@ -454,7 +462,32 @@ La réponse source reçue depuis l'API est conservée dans :
raw_data
```
## Qualité des données de l'API Mock
### Frontière de confiance avec l'API Mock
L'API Mock de l'école n'a aucune authentification et expose un endpoint mutatif à quiconque. Sa
réponse est donc traitée comme une entrée hostile, conformément à API10 dans
[la traçabilité OWASP](owasp-traceabilite.md). Le risque premier n'est pas la fausse alerte,
c'est l'empoisonnement du jeu d'entraînement du modèle de prédiction.
Quatre garde-fous, tous dans `mock_api_import.py` :
| Garde-fou | Mise en œuvre |
|---|---|
| Timeout | `APP_MOCK_API_TIMEOUT_SECONDS`, dix secondes par défaut |
| Taille de tableau plafonnée | `MAX_SITES` sites, et au plus `--limit` mesures par site |
| Bornes physiques | `PHYSICAL_BOUNDS`, une plage par grandeur |
| Frontière d'anti-corruption | `build_site_row()` et `build_reading_row()`, qui ne recopient que les champs attendus |
Une valeur hors bornes, d'un type inattendu, `NaN` ou infinie devient `NULL`. Elle laisse sa
trace dans `null_reasons` sous la forme `out_of_physical_bounds:<colonne>`, et `data_quality`
descend à `degraded`. Une `data_quality` que `ck_reading_quality` refuserait devient `NULL`
plutôt que de faire échouer le lot entier. Dans tous les cas `raw_data` conserve la réponse
d'origine intacte : rien n'est perdu, seule son exploitation est bornée.
Le plafond de taille s'applique après désérialisation de la réponse. Borner le corps HTTP
lui-même demanderait une lecture en flux, et reste à faire.
### Qualité des données de l'API Mock
Les valeurs `NULL` ne sont pas remplacées pendant l'ingestion.
@@ -475,7 +508,7 @@ imputed_values = NULL
imputation_method = NULL
```
## Validation de l'import API Mock
### Validation de l'import API Mock
Un scénario de validation a été exécuté pour les 7 sites sur la période :
@@ -526,7 +559,7 @@ Les tests automatisés couvrent également :
- la conservation des données sources ;
- l'idempotence en base.
# Évolution prévue
## Évolution prévue
La prochaine étape consiste à orchestrer les deux mécanismes d'ingestion avec Apache Airflow.
@@ -563,4 +596,4 @@ Les scripts Python resteront responsables de l'extraction, de la validation, de
et du chargement des données.
Le pipeline servira ensuite de base à la préparation des données nécessaires au modèle
de Machine Learning.
de Machine Learning.
+64 -26
View File
@@ -81,9 +81,9 @@ L'API Mock est utilisée pour compléter les données historiques avec des mesur
| mypy | Vérification du typage |
| Pytest | Tests automatisés |
# Import du dataset historique
## Import du dataset historique
## Fonctionnement du pipeline historique
### Fonctionnement du pipeline historique
Le script d'import se trouve dans :
@@ -115,14 +115,14 @@ CSV + métadonnées JSON
PostgreSQL / TimescaleDB
```
### 1. Extraction
#### 1. Extraction
Le pipeline charge :
- `all_sites_combined.csv` avec Pandas ;
- `dataset_metadata.json` avec le module JSON de Python.
### 2. Validation
#### 2. Validation
Avant toute écriture en base, le pipeline contrôle notamment :
@@ -136,7 +136,7 @@ Avant toute écriture en base, le pipeline contrôle notamment :
Une incohérence détectée pendant cette étape interrompt l'import avant le chargement.
### 3. Dry-run
#### 3. Dry-run
Un mode `--dry-run` permet d'exécuter les contrôles sans écrire de données dans PostgreSQL.
@@ -149,7 +149,7 @@ Il permet notamment de vérifier :
- les valeurs NULL ;
- l'empreinte SHA-256.
### 4. Traçabilité
#### 4. Traçabilité
Une empreinte SHA-256 est calculée à partir du fichier CSV afin d'identifier le dataset utilisé.
@@ -161,7 +161,7 @@ Empreinte SHA-256 du dataset validé :
Cette empreinte participe à la traçabilité du dataset chargé.
### 5. Transformation
#### 5. Transformation
Les timestamps sont normalisés avec la timezone :
@@ -180,7 +180,7 @@ imputed_values = NULL
imputation_method = NULL
```
### 6. Chargement
#### 6. Chargement
Le chargement est réalisé avec SQLAlchemy Async dans PostgreSQL/TimescaleDB.
@@ -207,7 +207,7 @@ dataset_id = identifiant du dataset
Cette représentation respecte les contraintes définies dans le schéma de la base.
## Dataset validé
### Dataset validé
Le dataset traité contient :
@@ -226,7 +226,7 @@ Valeurs manquantes identifiées :
| `humidity_percent` | 3 423 |
| `solar_irradiance_wm2` | 3 964 |
## Exécution historique en dry-run
### Exécution historique en dry-run
Depuis le dossier :
@@ -246,7 +246,7 @@ uv run python -m app.etl.historical_import `
Aucune donnée n'est écrite dans la base pendant cette exécution.
## Chargement historique réel
### Chargement historique réel
Depuis `apps/backend/` :
@@ -268,7 +268,7 @@ Chargement : 2000/122647
Chargement : 122647/122647
```
## Résultats obtenus pour le dataset historique
### Résultats obtenus pour le dataset historique
Après le chargement initial, les contrôles en base ont confirmé :
@@ -285,7 +285,7 @@ Le premier import a créé :
nouvelles lectures : 122647
```
## Idempotence du dataset historique
### Idempotence du dataset historique
Le pipeline a été exécuté une deuxième fois avec exactement le même dataset afin de vérifier son idempotence.
@@ -299,7 +299,7 @@ nouvelles lectures : 0
Une nouvelle exécution du même import ne crée donc pas de mesures supplémentaires pour le dataset testé.
## Vérifications SQL du dataset historique
### Vérifications SQL du dataset historique
Depuis la racine du projet, vérifier le nombre d'enregistrements avec :
@@ -327,9 +327,9 @@ Résultat attendu pour le dataset historique :
csv | 122647
```
# Import depuis l'API Mock
## Import depuis l'API Mock
## Fonctionnement
### Fonctionnement
Le script d'import de l'API Mock se trouve dans :
@@ -386,7 +386,7 @@ limit
Le paramètre `limit` doit être compris entre 1 et 1000.
## Configuration de l'API Mock
### Configuration de l'API Mock
La connexion à l'API Mock est configurée avec les variables d'environnement suivantes :
@@ -401,7 +401,7 @@ Les identifiants réels ne sont pas versionnés dans Git.
Les fichiers `.env.example` indiquent uniquement les variables nécessaires à l'exécution.
## Transformation des mesures API
### Transformation des mesures API
Les mesures provenant de l'API Mock sont enregistrées dans `reading` avec :
@@ -422,7 +422,7 @@ raw_data
afin de préserver la donnée reçue et faciliter la traçabilité.
## Qualité des données API
### Qualité des données API
Les valeurs `NULL` fournies par l'API sont conservées telles quelles.
@@ -444,6 +444,9 @@ degraded
critical
```
Ce sont les quatre seules valeurs que la contrainte `ck_reading_quality` accepte. Toute autre
valeur renvoyée par l'API est remplacée par `NULL` plutôt que de faire échouer le lot entier.
Aucune imputation n'est réalisée pendant l'ingestion :
```text
@@ -453,7 +456,42 @@ imputation_method = NULL
Cette stratégie permet de distinguer une véritable valeur nulle ou manquante d'une consommation égale à zéro et de conserver les informations liées aux défaillances de capteurs.
## Dry-run de l'API Mock
### Bornes physiques et frontière de confiance
La réponse de l'API Mock est traitée comme une entrée hostile : l'API n'a pas
d'authentification et expose un endpoint mutatif à quiconque. Voir API10 dans
`docs/architecture/owasp-traceabilite.md`.
Les plages acceptées sont déclarées dans `PHYSICAL_BOUNDS` :
| Grandeur | Plage acceptée |
|---|---|
| `consumption_kw` | 0 à 100 000 |
| `consumption_kwh` | 0 à 100 000 |
| `voltage_v` | 0 à 1 000 |
| `current_a` | 0 à 10 000 |
| `power_factor` | 0 à 1 |
| `temperature_celsius` | -90 à 60 |
| `humidity_percent` | 0 à 100 |
| `capacity_kw` | 0 à 100 000 |
Une valeur hors plage, d'un type inattendu, `NaN` ou infinie devient `NULL` :
```text
null_reasons += "out_of_physical_bounds:<colonne>"
data_quality = "degraded"
```
L'import ne s'interrompt pas pour autant : le mock émet des anomalies par construction, et
`raw_data` conserve la réponse d'origine.
La taille des réponses est plafonnée : au plus `MAX_SITES` sites, et au plus `--limit` mesures
par site. Au-delà, l'import échoue au lieu de charger.
Enfin, seuls les champs attendus sont recopiés vers la base. Une clé supplémentaire renvoyée par
l'API n'atteint jamais une colonne.
### Dry-run de l'API Mock
Le mode `--dry-run` permet de tester la connexion, la récupération des sites et la récupération des mesures sans écrire dans PostgreSQL.
@@ -467,7 +505,7 @@ uv run python -m app.etl.mock_api_import `
--dry-run
```
## Chargement réel depuis l'API Mock
### Chargement réel depuis l'API Mock
Depuis `apps/backend/` :
@@ -478,7 +516,7 @@ uv run python -m app.etl.mock_api_import `
--limit 60
```
## Résultat validé pour l'API Mock
### Résultat validé pour l'API Mock
Le scénario de validation utilisé couvre la période :
@@ -511,7 +549,7 @@ Les contrôles effectués directement dans PostgreSQL/TimescaleDB ont confirmé
- la conservation de `null_reasons` ;
- la conservation de la donnée source dans `raw_data`.
## Idempotence de l'import API Mock
### Idempotence de l'import API Mock
Le même import a été exécuté plusieurs fois afin de vérifier qu'une mesure déjà présente n'est pas créée une seconde fois.
@@ -519,7 +557,7 @@ L'idempotence repose sur la contrainte d'unicité de la table `reading` et sur l
Un test d'intégration automatisé vérifie également ce comportement.
# Tests et qualité
## Tests et qualité
Les tests automatisés des pipelines ETL sont situés dans :
@@ -599,7 +637,7 @@ Lors de la validation de l'import API Mock :
La suite backend complète a également été validée avec une couverture supérieure au seuil de 85 %.
# Suite du pipeline Data
## Suite du pipeline Data
Deux sources de données sont maintenant prises en charge :
@@ -631,4 +669,4 @@ Airflow permettra de planifier les traitements, gérer leur ordre d'exécution,
Airflow ne remplacera pas la logique ETL Python existante. Les scripts actuels resteront responsables de l'extraction, de la validation, de la transformation et du chargement.
Le pipeline Data servira ensuite à préparer les données nécessaires au modèle de Machine Learning.
Le pipeline Data servira ensuite à préparer les données nécessaires au modèle de Machine Learning.