Compare commits

..
Author SHA1 Message Date
PhyriosandGitHub 4fb8b5a784 Merge pull request #143 from ineszang/fix/deploiement-airflow-3
fix(infra,ci): aligne le provisionnement et le déploiement sur Airflow 3
2026-09-22 11:44:16 +02:00
Johan LEROYandClaude Opus 5 c1f9dec5fe chore(git): retire le motif airflow-webserver.pid du .gitignore
Airflow 3 n'a plus de webserver : le fichier n'est plus produit. Dernier point
de la revue de la PR #143.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 11:42:25 +02:00
Johan LEROY be6be42947 fix(infra,ci): aligne le provisionnement et le déploiement sur Airflow 3
La migration Airflow 3 est arrivée sur `dev` après l'écriture du chemin de
déploiement, qui en a gardé quatre traces fausses, invisibles en CI puisque
aucun job ne joue ce chemin.

`provision-host.sh` substituait `AIRFLOW_WEBSERVER_SECRET_KEY`, clé disparue.
`AIRFLOW_API_SECRET_KEY` et `AIRFLOW_JWT_SECRET` restaient donc à `change_me`
dans le `.env` posé sur la machine, et le `:?` d'`airflow-init` ne voit pas
une valeur d'exemple : la stack aurait démarré avec un secret de session et un
secret JWT prévisibles. Le `.env` est maintenant écrit après contrôle, et le
script refuse de le poser s'il reste un `change_me` hors `APP_MOCK_API_*`.

`make services-up` démarrait `airflow-webserver`, service supprimé par la
migration ; seul `airflow-up` avait été aligné.

L'overlay posait `AIRFLOW__WEBSERVER__WORKERS`, sans effet en Airflow 3 où la
section est `[api]`. Le réglage disparaît plutôt que d'être renommé : le défaut
y vaut un worker, moins que les deux qu'on visait.

Le diagnostic d'échec de `deploy.yml` lisait les journaux sans l'overlay, donc
sans service `proxy` : il échouait avant d'imprimer quoi que ce soit.
2026-09-22 11:17:02 +02:00
17 changed files with 29 additions and 181 deletions
+2 -2
View File
@@ -70,7 +70,7 @@ PUBLIC_ORIGIN=
PROXY_HTTP_PORT= PROXY_HTTP_PORT=
PROXY_HTTPS_PORT= PROXY_HTTPS_PORT=
# Réglages mémoire de la stack déployée. Sans eux, timescaledb-tune réserve 25 % de la RAM de la # Réglages mémoire de la stack déployée. Sans eux, timescaledb-tune réserve 25 % de la RAM de la
# machine à chaque base au premier démarrage, et le webserver Airflow lance 4 workers gunicorn. # machine à chaque base au premier démarrage. L'api-server Airflow 3 n'a rien à régler ici : son
# nombre de workers vaut 1 par défaut, contre 4 pour le webserver d'Airflow 2.
TS_TUNE_MEMORY=2GB TS_TUNE_MEMORY=2GB
TS_TUNE_NUM_CPUS=2 TS_TUNE_NUM_CPUS=2
AIRFLOW_WEBSERVER_WORKERS=2
+3 -2
View File
@@ -52,6 +52,7 @@ jobs:
done done
echo "L'API ne répond pas après 3 minutes" >&2 echo "L'API ne répond pas après 3 minutes" >&2
cd "/srv/enervision/${ENVIRONNEMENT}" cd "/srv/enervision/${ENVIRONNEMENT}"
docker compose ps compose="docker compose -f docker-compose.yml -f docker-compose.prod.yml"
docker compose logs --tail=50 backend proxy $compose ps
$compose logs --tail=50 backend proxy
exit 1 exit 1
+1 -3
View File
@@ -40,7 +40,6 @@ kubeconfig
# Airflow # Airflow
etl/airflow/logs/ etl/airflow/logs/
airflow.db airflow.db
airflow-webserver.pid
standalone_admin_password.txt standalone_admin_password.txt
# Environnement et secrets # Environnement et secrets
@@ -64,8 +63,7 @@ ml/models/*
!ml/models/.gitkeep !ml/models/.gitkeep
ml/mlruns/ ml/mlruns/
ml/mlartifacts/ ml/mlartifacts/
ml/mlflow.db* ml/mlflow.db
ml/.env
# Airflow : base sqlite locale generee par les tests d'integrite des DAGs (etl/airflow/tests) # Airflow : base sqlite locale generee par les tests d'integrite des DAGs (etl/airflow/tests)
etl/airflow/tests/.airflow_home/ etl/airflow/tests/.airflow_home/
+2 -12
View File
@@ -20,8 +20,6 @@ PG_USER := $(or $(strip $(call env-val,POSTGRES_USER)),enervision)
PG_PASSWORD := $(or $(strip $(call env-val,POSTGRES_PASSWORD)),change_me) PG_PASSWORD := $(or $(strip $(call env-val,POSTGRES_PASSWORD)),change_me)
PG_DB := $(or $(strip $(call env-val,POSTGRES_DB)),enervision) PG_DB := $(or $(strip $(call env-val,POSTGRES_DB)),enervision)
PG_PORT := $(or $(strip $(call env-val,POSTGRES_PORT)),5433) PG_PORT := $(or $(strip $(call env-val,POSTGRES_PORT)),5433)
ml-env-val = $(shell sed -n 's/^$(1)=//p' ml/.env 2>/dev/null | tail -1)
ML_ENV_DB_PASSWORD := $(call ml-env-val,MLFLOW_DB_PASSWORD)
AIRFLOW_PORT := $(or $(strip $(call env-val,AIRFLOW_PORT)),8080) AIRFLOW_PORT := $(or $(strip $(call env-val,AIRFLOW_PORT)),8080)
MAILPIT_UI_PORT := $(or $(strip $(call env-val,MAILPIT_UI_PORT)),8025) MAILPIT_UI_PORT := $(or $(strip $(call env-val,MAILPIT_UI_PORT)),8025)
ML_DATABASE_URL ?= postgresql+psycopg://$(PG_USER):$(PG_PASSWORD)@localhost:$(PG_PORT)/$(PG_DB) ML_DATABASE_URL ?= postgresql+psycopg://$(PG_USER):$(PG_PASSWORD)@localhost:$(PG_PORT)/$(PG_DB)
@@ -37,7 +35,7 @@ DEMO_NOW ?= 2024-12-31T00:00:00Z
lint format typecheck test test-cov test-integration check \ lint format typecheck test test-cov test-integration check \
openapi docker-build db-up db-down db-reset db-logs db-psql db-wait db-ensure-airflow \ openapi docker-build db-up db-down db-reset db-logs db-psql db-wait db-ensure-airflow \
migrate bootstrap-admin services-up demo-data demo-data-force \ migrate bootstrap-admin services-up demo-data demo-data-force \
ml-lint ml-typecheck ml-test ml-check ml-train ml-score mlflow-up detect-alerts recommendations \ ml-lint ml-typecheck ml-test ml-check ml-train ml-score detect-alerts recommendations \
airflow-lint airflow-test airflow-check airflow-up airflow-down airflow-logs \ airflow-lint airflow-test airflow-check airflow-up airflow-down airflow-logs \
tls-selfsigned tls-acme tls-renew stack-up stack-down stack-logs tls-selfsigned tls-acme tls-renew stack-up stack-down stack-logs
@@ -69,7 +67,7 @@ services-up: ## Démarre les services conteneurisés dont `make dev` dépend (ba
docker compose up -d db mailpit docker compose up -d db mailpit
@$(MAKE) --no-print-directory db-wait @$(MAKE) --no-print-directory db-wait
@$(MAKE) --no-print-directory db-ensure-airflow @$(MAKE) --no-print-directory db-ensure-airflow
docker compose up -d airflow-init airflow-webserver airflow-scheduler docker compose up -d airflow-init airflow-apiserver airflow-scheduler airflow-dag-processor
dev-backend: ## Lance l'API seule en rechargement à chaud dev-backend: ## Lance l'API seule en rechargement à chaud
@echo "backend -> http://localhost:8000 (docs sur /docs)" @echo "backend -> http://localhost:8000 (docs sur /docs)"
@@ -120,14 +118,6 @@ ml-train: ## Entraine le modele LightGBM. CSV=chemin optionnel, sinon lit ML_DAT
ml-score: ## Score le prochain pas horaire et l'ecrit dans `prediction`. CSV= et NOW= optionnels ml-score: ## Score le prochain pas horaire et l'ecrit dans `prediction`. CSV= et NOW= optionnels
cd $(ML) && uv run python -m enervision_ml.score $(if $(CSV),--csv $(CSV),) $(if $(NOW),--now $(NOW),) cd $(ML) && uv run python -m enervision_ml.score $(if $(CSV),--csv $(CSV),) $(if $(NOW),--now $(NOW),)
mlflow-up: ## Démarre le serveur MLflow (tracking + registry) en conteneur. ml/.env requis
@test -n "$(strip $(ML_ENV_DB_PASSWORD))" \
|| { echo "MLFLOW_DB_PASSWORD absente de ml/.env (copier ml/.env.example)"; exit 1; }
@echo "$(ML_ENV_DB_PASSWORD)" | grep -qE '^[A-Za-z0-9]+$$' \
|| { echo "MLFLOW_DB_PASSWORD doit contenir uniquement lettres et chiffres (interpolee dans l'URI postgresql://)"; exit 1; }
cd $(ML) && docker compose -f docker-compose.mlflow.yml up -d --build
@echo "mlflow -> http://localhost:5000"
detect-alerts: ## Détecte les alertes internes depuis les lectures en base. SITE= et NOW= optionnels detect-alerts: ## Détecte les alertes internes depuis les lectures en base. SITE= et NOW= optionnels
cd $(BACKEND) && uv run python -m app.detection.internal_alerts $(if $(SITE),--site-id $(SITE),) $(if $(NOW),--now $(NOW),) cd $(BACKEND) && uv run python -m app.detection.internal_alerts $(if $(SITE),--site-id $(SITE),) $(if $(NOW),--now $(NOW),)
+3 -2
View File
@@ -94,8 +94,9 @@ et frontend en rechargement a chaud sur le poste.
| Mailpit | <http://localhost:8025> | | Mailpit | <http://localhost:8025> |
Le `.env` doit porter les cles Airflow avant le premier `make dev` : `AIRFLOW_FERNET_KEY`, Le `.env` doit porter les cles Airflow avant le premier `make dev` : `AIRFLOW_FERNET_KEY`,
`AIRFLOW_WEBSERVER_SECRET_KEY`, `AIRFLOW_APP_SECRET_KEY` et `AIRFLOW_ADMIN_PASSWORD`. Sans elles `AIRFLOW_API_SECRET_KEY`, `AIRFLOW_JWT_SECRET`, `AIRFLOW_APP_SECRET_KEY` et
`airflow-init` refuse de demarrer, et `airflow-webserver` comme `airflow-scheduler` avec lui. `AIRFLOW_ADMIN_PASSWORD`. Sans elles `airflow-init` refuse de demarrer, et `airflow-apiserver`,
`airflow-scheduler` et `airflow-dag-processor` avec lui.
Les cibles d'origine restent disponibles pour ne demarrer qu'une partie : `make db-up`, Les cibles d'origine restent disponibles pour ne demarrer qu'une partie : `make db-up`,
`make airflow-up`, `make dev-backend`, `make dev-frontend`. `make airflow-up`, `make dev-backend`, `make dev-frontend`.
-2
View File
@@ -25,8 +25,6 @@ services:
airflow-apiserver: airflow-apiserver:
ports: !override ports: !override
- "127.0.0.1:${AIRFLOW_PORT:-8080}:8080" - "127.0.0.1:${AIRFLOW_PORT:-8080}:8080"
environment:
AIRFLOW__WEBSERVER__WORKERS: ${AIRFLOW_WEBSERVER_WORKERS:-2}
backend: backend:
ports: !reset null ports: !reset null
@@ -63,8 +63,9 @@ secret.
- Deux TimescaleDB sur une machine de 8 Go : sans réglage, chacune se réserverait 25 % de la - Deux TimescaleDB sur une machine de 8 Go : sans réglage, chacune se réserverait 25 % de la
RAM au premier démarrage. L'overlay fixe `TS_TUNE_MEMORY` à 2 Go et `TS_TUNE_NUM_CPUS` à 2 par RAM au premier démarrage. L'overlay fixe `TS_TUNE_MEMORY` à 2 Go et `TS_TUNE_NUM_CPUS` à 2 par
base, et 2 workers gunicorn par webserver Airflow. La montée à 32 Go prévue par les base. Airflow 3 n'a rien à régler de ce côté : son api-server lance un seul worker par défaut,
consignes est à demander. là où le webserver d'Airflow 2 en lançait quatre. La montée à 32 Go prévue par les consignes
est à demander.
- Un runner auto-hébergé sur un dépôt public exécute le code qu'on lui envoie. `deploy.yml` ne - Un runner auto-hébergé sur un dépôt public exécute le code qu'on lui envoie. `deploy.yml` ne
se déclenche jamais sur `pull_request`, le runner tourne sous un utilisateur dédié, et le se déclenche jamais sur `pull_request`, le runner tourne sous un utilisateur dédié, et le
dépôt doit exiger une approbation pour les workflows des PR externes. dépôt doit exiger une approbation pour les workflows des PR externes.
-29
View File
@@ -9,7 +9,6 @@ quel contexte, quelles décisions sont arrêtées, et ce qui manque encore entre
| Docker Compose plus reverse proxy | Déployer sur la machine on-premise | `Fait` | | Docker Compose plus reverse proxy | Déployer sur la machine on-premise | `Fait` |
| Deux projets Compose sur la VM ENI, recette et production | Déploiement continu depuis GitHub | `En cours` | | Deux projets Compose sur la VM ENI, recette et production | Déploiement continu depuis GitHub | `En cours` |
| k3s single-node | Cible à terme | `En cours` | | k3s single-node | Cible à terme | `En cours` |
| MLflow (`ml/`) | Tracker les expériences et le registre de modèles en local | `Fait`, non relié aux autres topologies |
## Poste de développement ## Poste de développement
@@ -148,34 +147,6 @@ est minimale et n'embarque pas la runtime OpenMP dont LightGBM a besoin, sans qu
(`OSError: libgomp.so.1`) n'apparaît qu'à la première tâche réellement exécutée, pas à la (`OSError: libgomp.so.1`) n'apparaît qu'à la première tâche réellement exécutée, pas à la
construction de l'image. construction de l'image.
### MLflow (`ml/`)
Statut : `Fait`, en local uniquement. Défini par `ml/docker-compose.mlflow.yml`, indépendant
du `docker-compose.yml` principal (réseau, volumes et démarrage séparés).
| Service | Image | Points notables |
|---|---|---|
| `mlflow-db` | `postgres:17` | Stocke le tracking store MLflow. Mot de passe obligatoire via `MLFLOW_DB_PASSWORD` |
| `mlflow` | Construite depuis `ml/` | Expose l'UI et l'API MLflow sur `127.0.0.1:5000`. Artefacts sur volume `mlflow-artifacts`, tracking store sur `mlflow-db` |
Portée actuelle : environnement de tracking et de registre de modèles pour le développement
local uniquement. Ce compose n'est relié ni à `docker-compose.prod.yml`, ni aux deux
environnements Compose de la VM ENI, ni à la cible k3s. Le magasin utilisé par Airflow pour
`ml_train`/`ml_score` (SQLite, volume `airflow_ml_state`) en est distinct — les deux MLflow ne
se voient pas tant que `MLFLOW_TRACKING_URI` n'est pas posé côté Airflow.
Limite connue : le DAG Airflow `ml_train` enregistre lui aussi une version a chaque execution
via `registered_model_name` (magasin SQLite du volume `airflow_ml_state`, distinct de ce
serveur). Versions et artefacts s'y accumulent sans politique de nettoyage -- fonctionne en
l'etat, mais a surveiller si les entrainements deviennent frequents.
Pour relier les runs Airflow (`ml_train`, magasin SQLite local) a ce serveur MLflow, positionner
`MLFLOW_TRACKING_URI=http://mlflow:5000` dans l'environnement du service `airflow-scheduler` (ou
`http://host.docker.internal:5000` si le serveur MLflow tourne hors du reseau Compose principal),
et s'assurer que le conteneur Airflow peut joindre le service `mlflow` -- ce qui suppose de les
rapprocher sur le meme reseau Docker ou d'exposer MLflow autrement qu'en `127.0.0.1` uniquement
(cf. point 1 sur l'exposition du port). Non fait a ce jour : aucun besoin de centraliser les runs
d'entrainement Airflow et locaux n'a encore ete identifie.
## Machine cible, exécution Docker ## Machine cible, exécution Docker
Statut : `Fait`. Défini par l'overlay `docker-compose.prod.yml`, appliqué par-dessus le Statut : `Fait`. Défini par l'overlay `docker-compose.prod.yml`, appliqué par-dessus le
-4
View File
@@ -287,10 +287,6 @@ Chaque table remplit un rôle précis dans le traitement et l'exploitation des d
| `alert` | Enregistrer les alertes, leur type, leur gravité et leur message | API Mock `/alerts` et détections EnerVision | | `alert` | Enregistrer les alertes, leur type, leur gravité et leur message | API Mock `/alerts` et détections EnerVision |
| `recommendation` | Proposer des actions et expliquer la règle qui les motive | Règles métier d'EnerVision | | `recommendation` | Proposer des actions et expliquer la règle qui les motive | Règles métier d'EnerVision |
Le scoring (`ml_score`) charge le modèle depuis un fichier local (`models/lightgbm-consumption.txt`)
et trace son empreinte SHA-256 dans `prediction.model_reference`. Il ne lit aucune version depuis
le Model Registry MLflow (`ml/`) : ce registre sert aujourd'hui à la traçabilité des
entraînements, pas au déploiement du modèle de scoring.
Les anomalies historiques décrites dans les JSON sont conservées dans `dataset.metadata`. Les anomalies historiques décrites dans les JSON sont conservées dans `dataset.metadata`.
Elles servent à l'analyse des données et ne sont pas considérées comme des alertes actuelles. Elles servent à l'analyse des données et ne sont pas considérées comme des alertes actuelles.
-6
View File
@@ -1,6 +0,0 @@
.venv
data
mlruns
mlflow.db*
models
.env
-1
View File
@@ -1 +0,0 @@
MLFLOW_DB_PASSWORD=change-me
-7
View File
@@ -1,7 +0,0 @@
FROM python:3.14-slim
RUN pip install --no-cache-dir --only-binary :all: mlflow==3.16.1 psycopg2-binary==2.9.13
RUN useradd --create-home --uid 1000 mlflow \
&& mkdir /mlartifacts \
&& chown mlflow /mlartifacts
USER mlflow
EXPOSE 5000
-51
View File
@@ -57,50 +57,6 @@ validation. La coupure est **chronologique**, jamais un tirage aleatoire de lign
aleatoire laisserait des lignes de validation "voir" des lignes d'entrainement via leurs aleatoire laisserait des lignes de validation "voir" des lignes d'entrainement via leurs
lags/moyennes glissantes, une fuite qui masquerait un surapprentissage. lags/moyennes glissantes, une fuite qui masquerait un surapprentissage.
## Serveur MLflow (conteneur)
Premiere utilisation : copier `.env.example` en `.env` et y choisir un mot de passe PostgreSQL
(lettres et chiffres uniquement). Le fichier `.env` est ignore par git.
```bash
cp .env.example .env
```
Un serveur MLflow (PostgreSQL pour les metadonnees, volume pour les artefacts) se lance avec
Docker. Prerequis : Docker Desktop demarre.
```bash
make mlflow-up
```
La cible vérifie que `MLFLOW_DB_PASSWORD` (définie dans `ml/.env`) ne contient que des lettres et
des chiffres avant de démarrer le serveur : ce mot de passe est interpolé directement dans l'URI
PostgreSQL (`postgresql://mlflow:${MLFLOW_DB_PASSWORD}@...`), un caractère spécial la rendrait
invalide sans message d'erreur clair.
Interface : http://localhost:5000. Entrainer vers ce serveur :
```
uv run python -m enervision_ml.train --csv data/all_sites_combined.csv --mlflow-tracking-uri http://localhost:5000
```
Arreter : `docker compose -f docker-compose.mlflow.yml down` (ajouter `-v` pour effacer aussi les
runs et les modeles).
Pour voir les runs dans l'interface (MLflow 3.x) :
- Passer le selecteur en haut a gauche sur **Model training**. Le mode **GenAI** affiche des
traces LLM et reste vide pour un entrainement LightGBM.
- **Runs** liste les entrainements, **Models** les artefacts de modele de chaque run (tous nommes
`model`), et **Model registry** les versions numerotees de `consumption-forecast-lightgbm`.
Limites : l'identifiant PostgreSQL du compose est fixe a `mlflow`, le mot de passe vient de la
variable obligatoire `MLFLOW_DB_PASSWORD` (aucune valeur par defaut, le compose refuse de
demarrer sans elle) -- ce mot de passe est choisi lors de la copie de `.env.example`, il ne
convient donc qu'au developpement local tel quel. Un deploiement partage demandera des secrets,
de l'authentification et un stockage d'artefacts dedie (S3/MinIO). Le port 5000 doit etre libre : arreter `mlflow ui` avant,
ou changer le mapping (`"5001:5000"`) dans le compose.
## Scoring ## Scoring
```bash ```bash
@@ -127,13 +83,6 @@ section 2 :
fichier : `train.py` reecrit toujours le meme chemin a chaque entrainement, donc le nom seul ne fichier : `train.py` reecrit toujours le meme chemin a chaque entrainement, donc le nom seul ne
distinguerait pas deux versions du modele. distinguerait pas deux versions du modele.
**Le scoring ne lit pas le Model Registry.** Le fichier charge par `--model` est local
(`models/lightgbm-consumption.txt`), independant des versions enregistrees dans le
**Model registry** MLflow (`consumption-forecast-lightgbm`). `train.py` enregistre bien une
version a chaque entrainement (tracabilite), mais aucun alias (`champion` par exemple) n'est
pose, et `enervision_ml.score` ne les lit pas. Le registre sert aujourd'hui a la tracabilite des
entrainements, pas au deploiement du modele utilise en scoring.
En mode `--csv`, rien n'est ecrit en base : c'est un instantane historique fige (l'heure "future" En mode `--csv`, rien n'est ecrit en base : c'est un instantane historique fige (l'heure "future"
calculee a partir de la fin du CSV n'existe dans aucune base reelle), utile pour valider le calculee a partir de la fin du CSV n'existe dans aucune base reelle), utile pour valider le
pipeline sans base joignable. pipeline sans base joignable.
-32
View File
@@ -1,32 +0,0 @@
services:
mlflow-db:
image: postgres:17
environment:
POSTGRES_USER: mlflow
POSTGRES_PASSWORD: ${MLFLOW_DB_PASSWORD:?definir MLFLOW_DB_PASSWORD dans ml/.env}
POSTGRES_DB: mlflow
volumes:
- mlflow-db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mlflow"]
interval: 5s
retries: 10
mlflow:
build: .
depends_on:
mlflow-db:
condition: service_healthy
ports:
- "127.0.0.1:5000:5000"
volumes:
- mlflow-artifacts:/mlartifacts
environment:
MLFLOW_DB_PASSWORD: ${MLFLOW_DB_PASSWORD}
entrypoint: [ "/bin/sh", "-c" ]
command:
- exec mlflow server --host 0.0.0.0 --port 5000 --backend-store-uri "postgresql://mlflow:$$MLFLOW_DB_PASSWORD@mlflow-db:5432/mlflow" --artifacts-destination /mlartifacts --serve-artifacts
volumes:
mlflow-db-data:
mlflow-artifacts:
+1 -5
View File
@@ -181,11 +181,7 @@ def _log_to_mlflow(
) )
mlflow.log_metrics({f"model_{cle}": valeur for cle, valeur in model_metrics.items()}) mlflow.log_metrics({f"model_{cle}": valeur for cle, valeur in model_metrics.items()})
mlflow.log_metrics({f"baseline_{cle}": valeur for cle, valeur in baseline_metrics.items()}) mlflow.log_metrics({f"baseline_{cle}": valeur for cle, valeur in baseline_metrics.items()})
mlflow.lightgbm.log_model( mlflow.lightgbm.log_model(booster, name="model")
booster,
name="model",
registered_model_name="consumption-forecast-lightgbm",
)
mlflow.log_artifact(str(model_output)) mlflow.log_artifact(str(model_output))
-17
View File
@@ -3,7 +3,6 @@ from pathlib import Path
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import pytest
from enervision_ml.features import TARGET_COLUMN, build_features, feature_columns from enervision_ml.features import TARGET_COLUMN, build_features, feature_columns
from enervision_ml.train import chronological_split, prepare_dataset, train from enervision_ml.train import chronological_split, prepare_dataset, train
@@ -75,19 +74,3 @@ def test_train_runs_end_to_end_on_synthetic_data_and_beats_a_dummy_baseline(
assert model_metrics["n_observations"] > 0 assert model_metrics["n_observations"] > 0
assert model_metrics["mae"] >= 0 assert model_metrics["mae"] >= 0
assert baseline_metrics["n_observations"] == model_metrics["n_observations"] assert baseline_metrics["n_observations"] == model_metrics["n_observations"]
assert model_metrics["mae"] < baseline_metrics["mae"]
def test_train_raises_when_the_validation_window_is_empty(tmp_path: Path) -> None:
depart = datetime(2026, 1, 1, tzinfo=UTC)
frame = make_frame("site-a", heures=50, depart=depart) # trop court pour un lag de 168h
csv_path = tmp_path / "trop_court.csv"
frame.to_csv(csv_path, index=False)
with pytest.raises(ValueError, match="Fenetre d'entrainement ou de validation vide"):
train(
csv_path=csv_path,
model_output=tmp_path / "model.txt",
test_fraction=0.2,
tracking_uri=f"sqlite:///{tmp_path / 'mlflow.db'}",
)
+14 -4
View File
@@ -47,13 +47,15 @@ preparer() {
fi fi
if [[ ! -f "$dossier/.env" ]]; then if [[ ! -f "$dossier/.env" ]]; then
local brouillon="$dossier/.env.brouillon" oubliees
sed -e "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(secret)|" \ sed -e "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(secret)|" \
-e "s|^POSTGRES_PORT=.*|POSTGRES_PORT=$port_pg|" \ -e "s|^POSTGRES_PORT=.*|POSTGRES_PORT=$port_pg|" \
-e "s|^APP_SECRET_KEY=.*|APP_SECRET_KEY=$(secret)|" \ -e "s|^APP_SECRET_KEY=.*|APP_SECRET_KEY=$(secret)|" \
-e "s|^MAILPIT_UI_PORT=.*|MAILPIT_UI_PORT=$port_mailpit|" \ -e "s|^MAILPIT_UI_PORT=.*|MAILPIT_UI_PORT=$port_mailpit|" \
-e "s|^AIRFLOW_PORT=.*|AIRFLOW_PORT=$port_airflow|" \ -e "s|^AIRFLOW_PORT=.*|AIRFLOW_PORT=$port_airflow|" \
-e "s|^AIRFLOW_FERNET_KEY=.*|AIRFLOW_FERNET_KEY=$(fernet)|" \ -e "s|^AIRFLOW_FERNET_KEY=.*|AIRFLOW_FERNET_KEY=$(fernet)|" \
-e "s|^AIRFLOW_WEBSERVER_SECRET_KEY=.*|AIRFLOW_WEBSERVER_SECRET_KEY=$(secret)|" \ -e "s|^AIRFLOW_API_SECRET_KEY=.*|AIRFLOW_API_SECRET_KEY=$(secret)|" \
-e "s|^AIRFLOW_JWT_SECRET=.*|AIRFLOW_JWT_SECRET=$(secret)|" \
-e "s|^AIRFLOW_ADMIN_PASSWORD=.*|AIRFLOW_ADMIN_PASSWORD=$(secret | cut -c1-20)|" \ -e "s|^AIRFLOW_ADMIN_PASSWORD=.*|AIRFLOW_ADMIN_PASSWORD=$(secret | cut -c1-20)|" \
-e "s|^AIRFLOW_APP_SECRET_KEY=.*|AIRFLOW_APP_SECRET_KEY=$(secret)|" \ -e "s|^AIRFLOW_APP_SECRET_KEY=.*|AIRFLOW_APP_SECRET_KEY=$(secret)|" \
-e "s|^PUBLIC_HOST=.*|PUBLIC_HOST=$hote|" \ -e "s|^PUBLIC_HOST=.*|PUBLIC_HOST=$hote|" \
@@ -61,13 +63,21 @@ preparer() {
-e "s|^COMPOSE_PROJECT_NAME=.*|COMPOSE_PROJECT_NAME=enervision-$env|" \ -e "s|^COMPOSE_PROJECT_NAME=.*|COMPOSE_PROJECT_NAME=enervision-$env|" \
-e "s|^PROXY_HTTP_PORT=.*|PROXY_HTTP_PORT=$port_http|" \ -e "s|^PROXY_HTTP_PORT=.*|PROXY_HTTP_PORT=$port_http|" \
-e "s|^PROXY_HTTPS_PORT=.*|PROXY_HTTPS_PORT=$port_https|" \ -e "s|^PROXY_HTTPS_PORT=.*|PROXY_HTTPS_PORT=$port_https|" \
"$dossier/.env.example" > "$dossier/.env" "$dossier/.env.example" > "$brouillon"
# Branche antérieure à l'ADR 0009 : ces clés manquent alors dans .env.example. # Branche antérieure à l'ADR 0009 : ces clés manquent alors dans .env.example.
for cle in "COMPOSE_PROJECT_NAME=enervision-$env" "PUBLIC_ORIGIN=$origine" \ for cle in "COMPOSE_PROJECT_NAME=enervision-$env" "PUBLIC_ORIGIN=$origine" \
"PROXY_HTTP_PORT=$port_http" "PROXY_HTTPS_PORT=$port_https"; do "PROXY_HTTP_PORT=$port_http" "PROXY_HTTPS_PORT=$port_https"; do
grep -q "^${cle%%=*}=" "$dossier/.env" || echo "$cle" >> "$dossier/.env" grep -q "^${cle%%=*}=" "$brouillon" || echo "$cle" >> "$brouillon"
done done
chmod 600 "$dossier/.env" # Piège : une clé renommée en amont garde sa valeur d'exemple, que le `:?` du compose ne
# voit pas puisqu'elle n'est pas vide. Cas vécu : AIRFLOW_WEBSERVER_SECRET_KEY, Airflow 3.
oubliees="$(grep '=change_me$' "$brouillon" | grep -v '^APP_MOCK_API_' | cut -d= -f1 | tr '\n' ' ' || true)"
if [[ -n "$oubliees" ]]; then
rm -f "$brouillon"
erreur "$env : secrets non générés, .env non écrit : $oubliees"
fi
chmod 600 "$brouillon"
mv "$brouillon" "$dossier/.env"
echo "$env : .env généré. Reste à renseigner APP_MOCK_API_USERNAME et APP_MOCK_API_PASSWORD." echo "$env : .env généré. Reste à renseigner APP_MOCK_API_USERNAME et APP_MOCK_API_PASSWORD."
fi fi