From de697b080d0ba7a90511b0f3c154b9b5d9ddd897 Mon Sep 17 00:00:00 2001 From: Johan LEROY Date: Mon, 21 Sep 2026 10:21:18 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20r=C3=A9tablit=20la=20hi=C3=A9rarchie=20?= =?UTF-8?q?des=20titres=20et=20les=20motifs=20du=20document=20Data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/architecture/40-data.md | 83 +++++++++++++++++++++++---------- etl/README.md | 90 +++++++++++++++++++++++++----------- 2 files changed, 122 insertions(+), 51 deletions(-) diff --git a/docs/architecture/40-data.md b/docs/architecture/40-data.md index 65a4d62..40a98e6 100644 --- a/docs/architecture/40-data.md +++ b/docs/architecture/40-data.md @@ -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:`, 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. \ No newline at end of file +de Machine Learning. diff --git a/etl/README.md b/etl/README.md index 15c376b..719b828 100644 --- a/etl/README.md +++ b/etl/README.md @@ -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:" +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. \ No newline at end of file +Le pipeline Data servira ensuite à préparer les données nécessaires au modèle de Machine Learning.