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:
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user