Skip to content

Commit 83a874b

Browse files
committed
Merge branch 'master' into alice
2 parents 7809cd2 + fcda9aa commit 83a874b

11 files changed

Lines changed: 985 additions & 179 deletions

File tree

.github/workflows/ci.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ["**"]
6+
pull_request:
7+
branches: [master]
8+
9+
jobs:
10+
compile-check:
11+
name: Vérification compilation / syntaxe
12+
if: github.ref != 'refs/heads/master'
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout du code
16+
uses: actions/checkout@v5
17+
18+
- name: Setup Python
19+
uses: actions/setup-python@v6
20+
with:
21+
python-version: "3.12"
22+
23+
- name: Vérifier que tous les fichiers .py du src compilent
24+
run: python -m py_compile $(find src -name "*.py")
25+
26+
docker-build:
27+
name: Test de build du container
28+
if: github.event_name == 'pull_request'
29+
runs-on: ubuntu-latest
30+
needs: compile-check
31+
steps:
32+
- name: Checkout du code
33+
uses: actions/checkout@v5
34+
35+
- name: Setup Docker Buildx
36+
uses: docker/setup-buildx-action@v3
37+
38+
- name: Build de toutes les images via docker-compose
39+
run: docker compose build

dags/pipeline_accidents.py

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,32 @@
33
from airflow.providers.docker.operators.docker import DockerOperator
44
from airflow.operators.python import PythonOperator
55
from airflow.exceptions import AirflowFailException
6+
from docker.types import Mount
67
import mlflow
8+
from airflow.utils.trigger_rule import TriggerRule
79
from mlflow.tracking import MlflowClient
810
import requests
911
import os
12+
import re
13+
from pathlib import Path
1014

1115

16+
DATA_VOLUME_NAME = os.getenv("DATA_VOLUME_NAME", "mlops_accidents_accidents-data")
1217
MLFLOW_TRACKING_URI = "http://mlflow:5000"
1318
EXPERIMENT_NAME = "Gravité_Accidents"
1419
MODEL_NAME = "Modèle_Gravité_Accidents"
1520
DOCKER_NETWORK = "mlops_accidents_default"
1621

1722
MAX_DROP_ALLOWED = 0.05
1823

24+
1925
default_args = {
20-
'owner': 'mlops_team',
21-
'start_date': datetime(2026, 1, 1),
22-
'retries': 1,
26+
"owner": "mlops_team",
27+
"start_date": datetime(2026, 1, 1),
28+
"retries": 1,
2329
}
2430

31+
2532
def check_metrics_and_alert(**context):
2633
"""
2734
Récupère le f1_score du dernier run de l'expérience 'Gravité_Accidents'
@@ -32,26 +39,34 @@ def check_metrics_and_alert(**context):
3239

3340
experiment = client.get_experiment_by_name(EXPERIMENT_NAME)
3441
if not experiment:
35-
raise AirflowFailException(f"L'expérience '{EXPERIMENT_NAME}' n'a pas été trouvée dans MLflow.")
42+
raise AirflowFailException(
43+
f"L'expérience '{EXPERIMENT_NAME}' n'a pas été trouvée dans MLflow."
44+
)
3645

3746
runs = client.search_runs(
3847
experiment_ids=[experiment.experiment_id],
3948
order_by=["attributes.start_time DESC"],
40-
max_results=1
49+
max_results=1,
4150
)
4251

4352
if not runs:
44-
raise AirflowFailException(f"Aucun run trouvé pour l'expérience {EXPERIMENT_NAME}.")
53+
raise AirflowFailException(
54+
f"Aucun run trouvé pour l'expérience {EXPERIMENT_NAME}."
55+
)
4556

4657
current_run = runs[0]
4758
current_f1 = current_run.data.metrics.get("f1_score")
4859
current_run_id = current_run.info.run_id
4960

50-
context['ti'].xcom_push(key='current_run_id', value=current_run_id)
51-
print(f"Nouveau modèle entraîné détecté - Run ID: {current_run_id} | F1-Score: {current_f1}")
61+
context["ti"].xcom_push(key="current_run_id", value=current_run_id)
62+
print(
63+
f"Nouveau modèle entraîné détecté - Run ID: {current_run_id} | F1-Score: {current_f1}"
64+
)
5265

5366
if current_f1 is None:
54-
raise AirflowFailException("Le dernier run MLflow n'a pas enregistré de métrique 'f1_score'.")
67+
raise AirflowFailException(
68+
"Le dernier run MLflow n'a pas enregistré de métrique 'f1_score'."
69+
)
5570

5671
try:
5772
prod_model_version = client.get_model_version_by_alias(MODEL_NAME, "champion")
@@ -67,6 +82,7 @@ def check_metrics_and_alert(**context):
6782
except mlflow.exceptions.MlflowException:
6883
print("Aucun modèle marqué '@champion' trouvé. Première promotion du projet.")
6984

85+
7086
def promote_model_to_champion(**context):
7187
"""
7288
Associe l'alias 'champion' à la dernière version du modèle validé.
@@ -75,18 +91,22 @@ def promote_model_to_champion(**context):
7591
mlflow.set_tracking_uri(MLFLOW_TRACKING_URI)
7692
client = MlflowClient()
7793

78-
run_id = context['ti'].xcom_pull(key='current_run_id', task_ids='evaluate_metrics')
94+
run_id = context["ti"].xcom_pull(key="current_run_id", task_ids="evaluate_metrics")
7995

8096
filter_string = f"run_id='{run_id}'"
8197
versions = client.search_model_versions(filter_string)
8298

8399
if not versions:
84-
raise AirflowFailException(f"Aucune version de modèle trouvée dans le Registry pour le run {run_id}.")
100+
raise AirflowFailException(
101+
f"Aucune version de modèle trouvée dans le Registry pour le run {run_id}."
102+
)
85103

86104
latest_version = versions[0].version
87105

88106
client.set_registered_model_alias(MODEL_NAME, "champion", latest_version)
89-
print(f"Succès : Le modèle '{MODEL_NAME}' version {latest_version} est maintenant désigné comme '@champion'.")
107+
print(
108+
f"Succès : Le modèle '{MODEL_NAME}' version {latest_version} est maintenant désigné comme '@champion'."
109+
)
90110

91111

92112
def reload_predict_service():
@@ -97,23 +117,20 @@ def reload_predict_service():
97117
bento_url = "http://ml-api:3000/reload_model"
98118
try:
99119
response = requests.post(bento_url, timeout=15)
100-
if response.status_code == 200:
101-
print("Le conteneur 'ml-api' a mis à jour son modèle avec succès.")
102-
else:
103-
print(f"Le service ml-api a répondu avec un code erreur : {response.status_code}")
120+
response.raise_for_status()
121+
print("Le conteneur 'ml-api' a mis à jour son modèle avec succès.")
104122
except Exception as e:
105-
print(f"Notification non envoyée à ml-api (Vérifie si l'API expose ce endpoint) : {e}")
123+
raise AirflowFailException(f"Échec du rechargement de ml-api : {e}")
106124

107125

108126
with DAG(
109-
'mlops_accident_gravity_pipeline',
127+
"mlops_accident_gravity_pipeline",
110128
default_args=default_args,
111-
description='Pipeline d\'entraînement pour la gravité des accidents',
112-
schedule='@monthly',
129+
description="Pipeline d'entraînement pour la gravité des accidents",
130+
schedule="@monthly",
113131
catchup=False,
114132
tags=["accidents"],
115133
) as dag:
116-
117134
# task_make_dataset = DockerOperator(
118135
# task_id='docker_make_dataset',
119136
# image='make_dataset:latest',
@@ -123,28 +140,43 @@ def reload_predict_service():
123140
# mounts=[Mount(source=f"{BASE_DIR}/mlruns", target="/app/mlruns", type="bind")]
124141
# )
125142

143+
task_preprocess = DockerOperator(
144+
task_id="preprocess",
145+
image="mlops_accidents-preprocess:latest",
146+
api_version="auto",
147+
auto_remove=True,
148+
mount_tmp_dir=False,
149+
network_mode=DOCKER_NETWORK,
150+
mounts=[Mount(source=f"{DATA_VOLUME_NAME}", target="/app/data", type="volume")],
151+
)
152+
126153
task_train = DockerOperator(
127-
task_id='docker_train',
128-
image='mlops_accidents-train:latest',
129-
api_version='auto',
154+
task_id="train",
155+
image="mlops_accidents-train:latest",
156+
api_version="auto",
130157
auto_remove=True,
158+
mount_tmp_dir=False,
131159
network_mode=DOCKER_NETWORK,
132-
environment={'MLFLOW_TRACKING_URI': MLFLOW_TRACKING_URI},
160+
environment={"MLFLOW_TRACKING_URI": MLFLOW_TRACKING_URI},
161+
mounts=[Mount(source=f"{DATA_VOLUME_NAME}", target="/app/data", type="volume")],
133162
)
134163

135164
task_evaluate = PythonOperator(
136-
task_id='evaluate_metrics',
165+
task_id="evaluate_metrics",
137166
python_callable=check_metrics_and_alert,
138167
)
139168

140169
task_promote = PythonOperator(
141-
task_id='promote_model',
170+
task_id="promote_model",
142171
python_callable=promote_model_to_champion,
143172
)
144173

145174
task_reload = PythonOperator(
146-
task_id='reload_predict_service',
147-
python_callable=reload_predict_service
175+
task_id="reload_predict_service",
176+
python_callable=reload_predict_service,
177+
trigger_rule=TriggerRule.ALL_DONE,
148178
)
149179

150-
task_train >> task_evaluate >> task_promote >> task_reload
180+
task_preprocess >> task_train >> task_evaluate
181+
task_evaluate >> task_promote
182+
task_evaluate >> task_reload

docker-compose.yml

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# version: '3.8'
21
services:
32
mlflow:
43
image: ghcr.io/mlflow/mlflow:v2.11.3
@@ -7,6 +6,14 @@ services:
76
command: mlflow server --backend-store-uri sqlite:///mlflow.db --artifacts-destination /app/mlruns --serve-artifacts --host 0.0.0.0 --port 5000
87
volumes:
98
- ./mlruns:/app/mlruns
9+
10+
preprocess:
11+
build:
12+
context: .
13+
dockerfile: src/preprocess/Dockerfile
14+
volumes:
15+
- accidents-data:/app/data
16+
restart: "no"
1017

1118
train:
1219
build:
@@ -16,6 +23,12 @@ services:
1623
- MLFLOW_TRACKING_URI=http://mlflow:5000
1724
volumes:
1825
- ./mlruns:/app/mlruns
26+
depends_on:
27+
preprocess:
28+
condition: service_completed_successfully
29+
mlflow:
30+
condition: service_started
31+
restart: "no"
1932

2033
ml-api:
2134
build:
@@ -39,10 +52,11 @@ services:
3952
dockerfile: src/streamlit/Dockerfile
4053
environment:
4154
- MODEL_API_URL=http://ml-api:3000
42-
expose:
55+
ports:
4356
- "8501"
4457
depends_on:
45-
- ml-api
58+
ml-api:
59+
condition: service_started
4660

4761
nginx:
4862
build:
@@ -52,8 +66,10 @@ services:
5266
- "443:443"
5367
- "80:80"
5468
depends_on:
55-
- streamlit
56-
- ml-api
69+
streamlit:
70+
condition: service_started
71+
ml-api:
72+
condition: service_started
5773
restart: unless-stopped
5874

5975
prometheus:
@@ -67,7 +83,8 @@ services:
6783
- '--config.file=/etc/prometheus/prometheus.yml'
6884
- '--storage.tsdb.path=/prometheus'
6985
depends_on:
70-
- ml-api
86+
ml-api:
87+
condition: service_started
7188

7289
grafana:
7390
image: grafana/grafana:latest
@@ -81,7 +98,8 @@ services:
8198
ports:
8299
- "3000:3000"
83100
depends_on:
84-
- prometheus
101+
prometheus:
102+
condition: service_started
85103

86104
postgres-airflow:
87105
image: postgres:15
@@ -93,10 +111,28 @@ services:
93111
- postgres-airflow-data:/var/lib/postgresql/data
94112
healthcheck:
95113
test: ["CMD", "pg_isready", "-U", "airflow"]
96-
interval: 10s
114+
interval: 5s
97115
timeout: 5s
98116
retries: 5
99117

118+
airflow-init:
119+
build:
120+
context: .
121+
dockerfile: Dockerfile.airflow
122+
environment:
123+
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres-airflow/airflow
124+
user: root
125+
command: >
126+
bash -c "
127+
su -s /bin/bash airflow -c '
128+
airflow db migrate &&
129+
airflow users create --username admin --password admin --firstname Admin --lastname Admin --role Admin --email admin@example.com || true
130+
'
131+
"
132+
depends_on:
133+
postgres-airflow:
134+
condition: service_healthy
135+
100136
airflow:
101137
build:
102138
context: .
@@ -115,36 +151,33 @@ services:
115151
volumes:
116152
- ./dags:/opt/airflow/dags
117153
- ./src:/opt/airflow/src
118-
- ./data:/opt/airflow/data
154+
- accidents-data:/opt/airflow/data
119155
- airflow-logs:/opt/airflow/logs
120156
- /var/run/docker.sock:/var/run/docker.sock
121157
ports:
122158
- "8080:8080"
123-
124159
user: root
125-
# user: "0:0"
126-
127160
command: >
128161
bash -c "
129162
chmod 666 /var/run/docker.sock || true &&
130163
rm -f /opt/airflow/airflow-webserver.pid &&
131164
su -s /bin/bash airflow -c '
132-
airflow db init &&
133-
airflow users create --username admin --password admin --firstname Admin --lastname Admin --role Admin --email admin@example.com || true &&
134165
airflow scheduler &
135-
airflow webserver
166+
exec airflow webserver
136167
'
137168
"
138-
139169
depends_on:
140170
postgres-airflow:
141171
condition: service_healthy
172+
airflow-init:
173+
condition: service_completed_successfully
142174
mlflow:
143175
condition: service_started
144176
restart: on-failure
145177

146178
volumes:
147179
postgres-airflow-data:
148180
airflow-logs:
181+
accidents-data:
149182
prometheus_data:
150183
grafana_data:

0 commit comments

Comments
 (0)