[EF-05] Alertes de la source en zone argent #165

Closed
opened 2026-09-07 09:37:18 +00:00 by marvin · 1 comment
Member

Exigence couverte

EF-05, ENF-07

Épreuve servie

EC05 · Data, ETL et BI

Charge estimée

1 j.h

Ce qu'on veut obtenir

Les alertes servies par la source s'arrêtent aujourd'hui en zone bronze : 11 546 objets collectés au fil de l'eau depuis le 3 septembre, que rien ne transforme. Ce ticket les fait entrer en zone argent comme quatrième table, pour que la zone or puisse ensuite les charger dans public.alerte et que le tableau de bord cesse de les afficher depuis des fixtures.

Le collecteur délègue déjà un traitement à cette zone : collector/alertes.py:78 range une alerte à l'horodatage illisible sous dt=inconnu, en écrivant que « la zone argent la replacera d'après son contenu ». Personne ne l'a écrit.

Critères d'acceptation

  • silver.alerte est écrite pour une journée donnée, une ligne par alerte présente en bronze.
  • Les cinq types et les quatre sévérités sont écrits tels que la source les sert ; une valeur étrangère fait échouer le job plutôt que d'être écrite, comme le fait déjà la zone or.
  • Une alerte rangée sous dt=inconnu est replacée sur la journée de son horodatage.
  • Chaque ligne porte sa clé bronze : une alerte affichée remonte à l'objet qui l'a produite.
  • Rejouer la même journée réécrit la partition sans créer de doublon.

Comment on le vérifie

Tests      tests/unit/silver/test_alertes_silver.py
Commande   pytest tests/unit/silver/test_alertes_silver.py -v
Preuve     la sortie du test, et le décompte des lignes écrites pour une journée réelle

Hors périmètre

Le chargement de public.alerte en base — c'est la zone or, #35. Les alertes produites par nos propres règles (source = 'regle'), qui relèvent du #39. Le découpage messagetitre / description qu'attend AlerteOut, qui est une décision d'affichage.

### Exigence couverte EF-05, ENF-07 ### Épreuve servie EC05 · Data, ETL et BI ### Charge estimée 1 j.h ### Ce qu'on veut obtenir Les alertes servies par la source s'arrêtent aujourd'hui en zone bronze : 11 546 objets collectés au fil de l'eau depuis le 3 septembre, que rien ne transforme. Ce ticket les fait entrer en zone argent comme quatrième table, pour que la zone or puisse ensuite les charger dans `public.alerte` et que le tableau de bord cesse de les afficher depuis des fixtures. Le collecteur délègue déjà un traitement à cette zone : `collector/alertes.py:78` range une alerte à l'horodatage illisible sous `dt=inconnu`, en écrivant que « la zone argent la replacera d'après son contenu ». Personne ne l'a écrit. ### Critères d'acceptation - [ ] `silver.alerte` est écrite pour une journée donnée, une ligne par alerte présente en bronze. - [ ] Les cinq types et les quatre sévérités sont écrits tels que la source les sert ; une valeur étrangère fait échouer le job plutôt que d'être écrite, comme le fait déjà la zone or. - [ ] Une alerte rangée sous `dt=inconnu` est replacée sur la journée de son horodatage. - [ ] Chaque ligne porte sa clé bronze : une alerte affichée remonte à l'objet qui l'a produite. - [ ] Rejouer la même journée réécrit la partition sans créer de doublon. ### Comment on le vérifie ``` Tests tests/unit/silver/test_alertes_silver.py Commande pytest tests/unit/silver/test_alertes_silver.py -v Preuve la sortie du test, et le décompte des lignes écrites pour une journée réelle ``` ### Hors périmètre Le chargement de `public.alerte` en base — c'est la zone or, #35. Les alertes produites par nos propres règles (`source = 'regle'`), qui relèvent du #39. Le découpage `message` → `titre` / `description` qu'attend `AlerteOut`, qui est une décision d'affichage.
marvin self-assigned this 2026-09-07 09:37:18 +00:00
Author
Member

État : la table est écrite, testée et documentée ; la zone or ne la lit pas encore

Branche marvin/165-alertes-zone-argent, quatre commits. silver.alerte est la quatrième table de la zone argent.

Les critères, un par un

  • silver.alerte est écrite pour une journée donnée, une ligne par alerte présente en bronze. Vérifié sur les alertes réelles du 2026-09-06, sept sites, en lecture seule : 2 984 alertes retenues, soit exactement le nombre d'objets que bronze porte pour ce jour, et 2 984 alert_id distincts. Aucune perte, aucun doublon.

  • Les cinq types et les quatre sévérités sont écrits tels que la source les sert ; une valeur étrangère fait échouer le job. verifier_enumerations_alertes lève ValeurEtrangere avant l'écriture et nomme l'alerte fautive — même parti pris que gold.agregation._controler_enumerations. Attrapée au chargement, la faute serait sortie deux jobs plus loin sur un numéro de ligne. Les vingt combinaisons type × sévérité de la source passent, un cas le garde.

  • Une alerte rangée sous dt=inconnu est replacée sur la journée de son horodatage. charger_alertes lit deux partitions par site — celle du jour et dt=inconnu — puis retient chaque alerte sur la date de SON contenu, jamais sur le segment dt= de sa clé. C'est la promesse que collector/alertes.py délègue à cette zone depuis le #107 et que personne n'avait écrite. Corollaire testé : une alerte lue sous dt=inconnu mais datée d'un autre jour est écartée, elle appartient à la passe de ce jour-là, qui la retrouvera au même endroit. Rien ne se perd, rien ne se duplique.

  • Chaque ligne porte sa clé bronze. bronze_key renseignée sur 100 % des lignes lues en réel — le lignage ENF-07 vaut pour les alertes comme pour les mesures.

  • Rejouer la même journée réécrit la partition sans créer de doublon. Déduplication par alert_id en ceinture de celle que bronze garantit déjà par le nom d'objet. Et la partition est réécrite même vide : une journée dont les alertes ont disparu de bronze doit voir la sienne se vider, sinon la zone or chargerait indéfiniment celles d'un rejeu antérieur. Un cas le garde.

Les preuves

$ pytest tests/unit/silver/test_alertes_silver.py -q
30 passed in 0.03s

$ pytest tests/unit -q
628 passed

$ pytest tests/unit/silver --cov=services/etl/etl/silver
services/etl/etl/silver/alertes.py    70    0   100%
services/etl/etl/silver/job.py       124    9    93%

$ ruff check services packages && ruff format --check services packages
All checks passed! - 61 files already formatted

$ mypy --config-file etl/pyproject.toml etl        # strict
Success: no issues found in 22 source files

Et la chaîne complète du job sur bronze réel, sans rien écrire :

alertes retenues pour 2026-09-06 : 2984
controle des enumerations : passe
schema == ligne produite  : True
dt tous sur la journee    : {'2026-09-06'}
alert_id uniques          : 2984 / 2984
triees par horodatage     : True

Trois choses que l'écriture a révélées

1. Le nom de fichier de test annoncé plus haut ne pouvait pas marcher. tests/unit/collector/test_alertes.py porte déjà ce nom, les répertoires de test n'ont pas d'__init__.py, et pytest refuse deux modules homonymes : la collecte de la suite entière échoue, alors que le fichier passe isolément — donc ça serait tombé en CI, pas en local. Les deux correctifs globaux, poser des __init__.py ou passer en --import-mode=importlib, casseraient l'import des aides _echantillons et _doubles, qui sont des modules de premier niveau partout dans la suite. Le fichier s'appelle donc test_alertes_silver.py, et la raison est écrite en tête. La ligne « Comment on le vérifie » du ticket est corrigée en conséquence.

2. L'horodatage d'une alerte ne se tronque pas à la minute. Une mesure l'est parce qu'elle vit sur une grille. Une alerte n'a pas de grille, et public.alerte est unique sur (site_id, horodatage, type, source) : tronquer ferait de deux alertes du même type à douze secondes d'écart une seule alerte, et la seconde disparaîtrait sans que rien ne le dise.

3. Un tiers à la moitié des alertes de la source ont une valeur EN DESSOUS de leur seuil. Sur 500 alertes échantillonnées, quel que soit le type — threshold 34/102, spike 48/114, outage 38/96, anomaly 44/86, sensor 53/102. Une alerte « Seuil de consommation dépassé » avec value = 70.34 et threshold = 135.0, ça se verra sur l'écran Qualité. C'est la donnée que la source sert, et l'historiser telle quelle est exactement ce que demande l'EF-05 — mais @justine, ça vaut un coup d'oeil avant la démo, et peut-être une ligne au §3 du pipeline, qui recense déjà deux anomalies connues de la source.

Ce que ce ticket ne fait pas, et qui reste ouvert

public.alerte reste vide : gold.agregation ne lit pas silver.alerte, et rien ne charge la table en base. C'était le hors-périmètre annoncé, mais il faut un ticket pour le porter, sans quoi l'écran Qualité et le pavé « alertes ouvertes » du #24 resteront sur fixtures. Le §14 du pipeline le note.

### État : la table est écrite, testée et documentée ; la zone or ne la lit pas encore Branche `marvin/165-alertes-zone-argent`, quatre commits. `silver.alerte` est la quatrième table de la zone argent. ### Les critères, un par un - [x] **`silver.alerte` est écrite pour une journée donnée, une ligne par alerte présente en bronze.** Vérifié sur les alertes réelles du 2026-09-06, sept sites, en lecture seule : **2 984 alertes retenues, soit exactement le nombre d'objets que bronze porte pour ce jour**, et 2 984 `alert_id` distincts. Aucune perte, aucun doublon. - [x] **Les cinq types et les quatre sévérités sont écrits tels que la source les sert ; une valeur étrangère fait échouer le job.** `verifier_enumerations_alertes` lève `ValeurEtrangere` avant l'écriture et **nomme l'alerte fautive** — même parti pris que `gold.agregation._controler_enumerations`. Attrapée au chargement, la faute serait sortie deux jobs plus loin sur un numéro de ligne. Les vingt combinaisons type × sévérité de la source passent, un cas le garde. - [x] **Une alerte rangée sous `dt=inconnu` est replacée sur la journée de son horodatage.** `charger_alertes` lit **deux partitions par site** — celle du jour et `dt=inconnu` — puis retient chaque alerte sur la date de SON contenu, jamais sur le segment `dt=` de sa clé. C'est la promesse que `collector/alertes.py` délègue à cette zone depuis le #107 et que personne n'avait écrite. Corollaire testé : une alerte lue sous `dt=inconnu` mais datée d'un autre jour est **écartée**, elle appartient à la passe de ce jour-là, qui la retrouvera au même endroit. Rien ne se perd, rien ne se duplique. - [x] **Chaque ligne porte sa clé bronze.** `bronze_key` renseignée sur 100 % des lignes lues en réel — le lignage ENF-07 vaut pour les alertes comme pour les mesures. - [x] **Rejouer la même journée réécrit la partition sans créer de doublon.** Déduplication par `alert_id` en ceinture de celle que bronze garantit déjà par le nom d'objet. Et la partition est **réécrite même vide** : une journée dont les alertes ont disparu de bronze doit voir la sienne se vider, sinon la zone or chargerait indéfiniment celles d'un rejeu antérieur. Un cas le garde. ### Les preuves ``` $ pytest tests/unit/silver/test_alertes_silver.py -q 30 passed in 0.03s $ pytest tests/unit -q 628 passed $ pytest tests/unit/silver --cov=services/etl/etl/silver services/etl/etl/silver/alertes.py 70 0 100% services/etl/etl/silver/job.py 124 9 93% $ ruff check services packages && ruff format --check services packages All checks passed! - 61 files already formatted $ mypy --config-file etl/pyproject.toml etl # strict Success: no issues found in 22 source files ``` Et la chaîne complète du job sur bronze réel, sans rien écrire : ``` alertes retenues pour 2026-09-06 : 2984 controle des enumerations : passe schema == ligne produite : True dt tous sur la journee : {'2026-09-06'} alert_id uniques : 2984 / 2984 triees par horodatage : True ``` ### Trois choses que l'écriture a révélées **1. Le nom de fichier de test annoncé plus haut ne pouvait pas marcher.** `tests/unit/collector/test_alertes.py` porte déjà ce nom, les répertoires de test n'ont pas d'`__init__.py`, et pytest refuse deux modules homonymes : **la collecte de la suite entière échoue**, alors que le fichier passe isolément — donc ça serait tombé en CI, pas en local. Les deux correctifs globaux, poser des `__init__.py` ou passer en `--import-mode=importlib`, casseraient l'import des aides `_echantillons` et `_doubles`, qui sont des modules de premier niveau partout dans la suite. Le fichier s'appelle donc `test_alertes_silver.py`, et la raison est écrite en tête. La ligne « Comment on le vérifie » du ticket est corrigée en conséquence. **2. L'horodatage d'une alerte ne se tronque pas à la minute.** Une mesure l'est parce qu'elle vit sur une grille. Une alerte n'a pas de grille, et `public.alerte` est unique sur `(site_id, horodatage, type, source)` : tronquer ferait de deux alertes du même type à douze secondes d'écart une seule alerte, et la seconde disparaîtrait sans que rien ne le dise. **3. Un tiers à la moitié des alertes de la source ont une valeur EN DESSOUS de leur seuil.** Sur 500 alertes échantillonnées, quel que soit le type — `threshold` 34/102, `spike` 48/114, `outage` 38/96, `anomaly` 44/86, `sensor` 53/102. Une alerte « Seuil de consommation dépassé » avec `value = 70.34` et `threshold = 135.0`, ça se verra sur l'écran Qualité. C'est la donnée que la source sert, et l'historiser telle quelle est exactement ce que demande l'EF-05 — mais @justine, ça vaut un coup d'oeil avant la démo, et peut-être une ligne au §3 du pipeline, qui recense déjà deux anomalies connues de la source. ### Ce que ce ticket ne fait pas, et qui reste ouvert `public.alerte` **reste vide** : `gold.agregation` ne lit pas `silver.alerte`, et rien ne charge la table en base. C'était le hors-périmètre annoncé, mais il faut un ticket pour le porter, sans quoi l'écran Qualité et le pavé « alertes ouvertes » du #24 resteront sur fixtures. Le §14 du pipeline le note.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
g2/enervision#165
No description provided.