ASI-Engineer commited on
Commit
8e71f22
·
verified ·
1 Parent(s): 5507763

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. README.md +478 -77
  2. api.py +52 -5
  3. src/gradio_ui.py +23 -10
  4. src/logger.py +2 -2
  5. src/schemas.py +17 -3
README.md CHANGED
@@ -1,106 +1,507 @@
1
- ---
2
- title: Employee Turnover Prediction API
3
- emoji: 👔
4
- colorFrom: blue
5
- colorTo: purple
6
- sdk: gradio
7
- pinned: true
8
- license: mit
9
- app_port: 7860
10
- ---
11
 
 
12
 
13
- # Employee Turnover Prediction API 🚀 (v3.2.1)
14
 
15
- API de prédiction du turnover des employés (XGBoost + SMOTE) avec endpoints batch, validation stricte et documentation à jour.
16
 
17
- ## 🎯 Fonctionnalités
18
-
19
- - ✅ Prédiction de turnover (0 = reste, 1 = part)
20
  - 📦 Endpoint batch CSV (3 fichiers bruts)
21
- - 🎛️ Sliders Gradio et schémas Pydantic alignés sur les min/max réels
22
- - 📊 Probabilités et niveau de risque (Low/Medium/High)
23
- - 🔐 Authentification API Key (obligatoire)
24
- - 📝 Logs structurés JSON
25
- - 🛡️ Rate limiting (20 req/min)
26
- - 📚 Documentation OpenAPI/Swagger
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
 
 
 
 
 
 
 
 
 
28
 
29
- ## 🔗 Endpoints
 
30
 
31
- | Endpoint | Description |
32
- |----------|-------------|
33
- | `/docs` | Documentation interactive Swagger |
34
- | `/health` | Status de l'API |
35
- | `/ui` | Interface Gradio interactive |
36
- | `/predict` | Prédiction unitaire (JSON, contraintes réelles) |
37
- | `/predict/batch` | Prédiction batch (3 fichiers CSV bruts) |
38
 
 
 
 
 
 
 
39
 
40
- ## 🚀 Utilisation
 
 
 
 
41
 
42
- ### Prédiction unitaire (toutes contraintes appliquées)
43
  ```bash
44
- curl -X POST https://asi-engineer-oc-p5-dev.hf.space/predict \
 
 
45
  -H "Content-Type: application/json" \
46
- -H "X-API-Key: your-key" \
47
- -d '{
48
- "nombre_participation_pee": 0,
49
- "nb_formations_suivies": 2,
50
- "nombre_employee_sous_responsabilite": 1,
51
- "distance_domicile_travail": 15,
52
- "niveau_education": 3,
53
- "domaine_etude": "Infra & Cloud",
54
- "ayant_enfants": "Y",
55
- "frequence_deplacement": "Occasionnel",
56
- "annees_depuis_la_derniere_promotion": 2,
57
- "annes_sous_responsable_actuel": 5,
58
- "satisfaction_employee_environnement": 3,
59
- "note_evaluation_precedente": 4,
60
- "niveau_hierarchique_poste": 2,
61
- "satisfaction_employee_nature_travail": 3,
62
- "satisfaction_employee_equipe": 3,
63
- "satisfaction_employee_equilibre_pro_perso": 2,
64
- "note_evaluation_actuelle": 4,
65
- "heure_supplementaires": "Non",
66
- "augementation_salaire_precedente": 5.5,
67
- "age": 35,
68
- "genre": "M",
69
- "revenu_mensuel": 4500.0,
70
- "statut_marital": "Marié(e)",
71
- "departement": "Commercial",
72
- "poste": "Manager",
73
- "nombre_experiences_precedentes": 3,
74
- "nombre_heures_travailless": 80,
75
- "annee_experience_totale": 10,
76
- "annees_dans_l_entreprise": 5,
77
- "annees_dans_le_poste_actuel": 2
78
- }'
79
  ```
80
 
81
- ### Prédiction batch (3 fichiers CSV bruts)
 
 
 
82
  ```bash
83
- curl -X POST https://asi-engineer-oc-p5-dev.hf.space/predict/batch \
84
- -H "X-API-Key: your-key" \
85
- -F "sondage_file=@extrait_sondage.csv" \
86
- -F "eval_file=@extrait_eval.csv" \
87
- -F "sirh_file=@extrait_sirh.csv"
 
 
 
 
88
  ```
89
 
90
- **Réponse :**
91
- ```json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  {
93
  "total_employees": 1470,
94
- "predictions": [...],
 
 
 
95
  "summary": {
96
  "total_stay": 1169,
97
  "total_leave": 301,
98
- "high_risk_count": 222
 
 
99
  }
100
  }
101
  ```
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- ## 📚 Documentation complète
105
 
106
- Voir [docs/API.md](docs/API.md) ou le [GitHub Repository](https://github.com/chaton59/OC_P5) pour la documentation complète et les contraintes détaillées (min/max, enums, etc).
 
 
1
+ # 🚀 Employee Turnover Prediction API - v3.2.1
 
 
 
 
 
 
 
 
 
2
 
3
+ ## 📊 Vue d'ensemble
4
 
5
+ API REST de prédiction du turnover des employés basée sur un modèle XGBoost avec SMOTE.
6
 
 
7
 
8
+ **✨ Nouveautés v3.2.1** :
9
+ - 🎛️ Sliders Gradio et schémas Pydantic alignés sur les min/max réels des données d'entraînement
 
10
  - 📦 Endpoint batch CSV (3 fichiers bruts)
11
+ - 🔑 Authentification API Key (prod)
12
+ - 🔧 Correction preprocessing (scaling, ordre des colonnes)
13
+ - 📝 Documentation et exemples mis à jour
14
+
15
+ ## 🏗️ Architecture
16
+
17
+ ```
18
+ OC_P5/
19
+ ├── app.py # Point d'entrée FastAPI
20
+ ├── src/
21
+ │ ├── auth.py # Authentification API Key
22
+ │ ├── config.py # Configuration centralisée
23
+ │ ├── logger.py # Logging structuré (NOUVEAU)
24
+ │ ├── models.py # Chargement modèle HF Hub
25
+ │ ├── preprocessing.py # Pipeline preprocessing
26
+ │ ├── rate_limit.py # Rate limiting (NOUVEAU)
27
+ │ └── schemas.py # Validation Pydantic
28
+ ├── tests/ # Suite pytest (84 tests, 75.12% couverture)
29
+ ├── logs/ # Logs JSON (NOUVEAU)
30
+ │ ├── api.log # Tous les logs
31
+ │ └── error.log # Erreurs uniquement
32
+ ├── docs/ # Documentation
33
+ ├── ml_model/ # Scripts training
34
+ └── data/ # Données sources
35
+ ## 🗄️ Schéma de la Base de Données (PostgreSQL)
36
+
37
+ Schéma UML pour traçabilité ML (basé sur P5 prédiction turnover employé) :
38
+ ![Schéma BDD](docs/schema.png)
39
+
40
+ - **dataset** : Dataset original (référence pour tests/retraining). Colonnes adaptées au modèle de prédiction turnover.
41
+ - **ml_logs** : Logs inputs/outputs (JSON pour flexibilité, timestamp pour audits).
42
+
43
+ Choix : Structure relationnelle pour efficacité volume data ; sécurité via user dédié (ml_user).
44
+ Instructions : Voir create_db.py pour création.
45
+
46
+ 📖 **Guide complet pour débutants** : [docs/database_guide.md](docs/database_guide.md)
47
+
48
+ ### 🖥️ Outils DB Visuels
49
+
50
+ Pour une gestion visuelle de la base de données PostgreSQL, utilisez DBeaver (recommandé pour la mission POC).
51
+
52
+ #### Installation de DBeaver
53
+ 1. Téléchargez DBeaver Community depuis [dbeaver.io](https://dbeaver.io/download/).
54
+ 2. Installez l'application sur votre système (Windows/Mac/Linux).
55
+
56
+ #### Configuration de la connexion PostgreSQL
57
+ 1. Ouvrez DBeaver et cliquez sur "New Database Connection".
58
+ 2. Sélectionnez "PostgreSQL" comme type de base de données.
59
+ 3. Renseignez les paramètres de connexion :
60
+ - **Host** : `localhost` (ou l'IP de votre serveur PostgreSQL)
61
+ - **Port** : `5432` (port par défaut PostgreSQL)
62
+ - **Database** : `oc_p5_db`
63
+ - **Username** : `ml_user`
64
+ - **Password** : Le mot de passe défini dans votre fichier `.env` (variable `DB_PASSWORD`)
65
+ 4. Cliquez sur "Test Connection" pour vérifier.
66
+ 5. Enregistrez la connexion.
67
+
68
+ #### Utilisation
69
+ - Explorez les tables `dataset` et `ml_logs`.
70
+ - Exécutez des requêtes SQL directement dans l'interface.
71
+ - Visualisez les données et les schémas.
72
+
73
+ ### 💾 Insertion du Dataset
74
+ ```bash
75
+ # Insérer le dataset complet (1470 employés)
76
+ poetry run python scripts/insert_dataset.py
77
+
78
+ # Vérifier l'insertion
79
+ psql -h localhost -U ml_user -d oc_p5_db -c "SELECT COUNT(*) FROM dataset;"
80
+ ```
81
+
82
+ ### Prérequis
83
+ - Python 3.12+
84
+ - Poetry 1.7+
85
+ - Git
86
+
87
+ ### Setup rapide
88
+
89
+ ```bash
90
+ # 1. Cloner le repo
91
+ git clone https://github.com/chaton59/OC_P5.git
92
+ cd OC_P5
93
+
94
+ # 2. Installer les dépendances
95
+ poetry install
96
+
97
+ # 3. Configurer l'environnement
98
+ cp .env.example .env
99
+ # Éditer .env avec vos valeurs
100
+
101
+ # 4. Lancer l'API
102
+ poetry run uvicorn app:app --reload
103
 
104
+ # 5. Accéder à la documentation
105
+ # http://localhost:8000/docs
106
+ ```
107
+
108
+ ## 📝 Configuration (.env)
109
+
110
+ ```bash
111
+ # Mode développement (désactive auth + active logs détaillés)
112
+ DEBUG=true
113
 
114
+ # API Key (requis en production)
115
+ API_KEY=your-secret-key-here
116
 
117
+ # Logging (DEBUG, INFO, WARNING, ERROR, CRITICAL)
118
+ LOG_LEVEL=INFO
 
 
 
 
 
119
 
120
+ # HuggingFace Model
121
+ HF_MODEL_REPO=ASI-Engineer/employee-turnover-model
122
+ MODEL_FILENAME=model/model.pkl
123
+ ```
124
+
125
+ ## 🔒 Authentification
126
 
127
+ ### Mode DEBUG (développement)
128
+ ```bash
129
+ # L'API Key n'est PAS requise
130
+ curl http://localhost:8000/predict -H "Content-Type: application/json" -d '{...}'
131
+ ```
132
 
133
+ ### Mode PRODUCTION
134
  ```bash
135
+ # L'API Key est REQUISE
136
+ curl http://localhost:8000/predict \
137
+ -H "X-API-Key: your-secret-key" \
138
  -H "Content-Type: application/json" \
139
+ -d '{...}'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  ```
141
 
142
+
143
+ ## 📡 Endpoints
144
+
145
+ ### 🏥 Health Check
146
  ```bash
147
+ GET /health
148
+
149
+ # Réponse
150
+ {
151
+ "status": "healthy",
152
+ "model_loaded": true,
153
+ "model_type": "Pipeline",
154
+ "version": "3.2.1"
155
+ }
156
  ```
157
 
158
+ ### 🔮 Prédiction unitaire
159
+ ```bash
160
+ POST /predict
161
+ Content-Type: application/json
162
+ X-API-Key: your-key (en production)
163
+
164
+ # Payload (exemple, contraintes réelles appliquées)
165
+ {
166
+ "nombre_participation_pee": 0,
167
+ "nb_formations_suivies": 2,
168
+ "nombre_employee_sous_responsabilite": 1,
169
+ "distance_domicile_travail": 15,
170
+ "niveau_education": 3,
171
+ "domaine_etude": "Infra & Cloud",
172
+ "ayant_enfants": "Y",
173
+ "frequence_deplacement": "Occasionnel",
174
+ "annees_depuis_la_derniere_promotion": 2,
175
+ "annes_sous_responsable_actuel": 5,
176
+ "satisfaction_employee_environnement": 3,
177
+ "note_evaluation_precedente": 4,
178
+ "niveau_hierarchique_poste": 2,
179
+ "satisfaction_employee_nature_travail": 3,
180
+ "satisfaction_employee_equipe": 3,
181
+ "satisfaction_employee_equilibre_pro_perso": 2,
182
+ "note_evaluation_actuelle": 4,
183
+ "heure_supplementaires": "Non",
184
+ "augementation_salaire_precedente": 5.5,
185
+ "age": 35,
186
+ "genre": "M",
187
+ "revenu_mensuel": 4500.0,
188
+ "statut_marital": "Marié(e)",
189
+ "departement": "Commercial",
190
+ "poste": "Manager",
191
+ "nombre_experiences_precedentes": 3,
192
+ "nombre_heures_travailless": 80,
193
+ "annee_experience_totale": 10,
194
+ "annees_dans_l_entreprise": 5,
195
+ "annees_dans_le_poste_actuel": 2
196
+ }
197
+
198
+ # Réponse
199
+ {
200
+ "prediction": 0, # 0 = reste, 1 = part
201
+ "probability_0": 0.85, # Probabilité de rester
202
+ "probability_1": 0.15, # Probabilité de partir
203
+ "risk_level": "Low" # Low, Medium, High
204
+ }
205
+ ```
206
+
207
+ ### 📦 Prédiction batch (CSV)
208
+ ```bash
209
+ POST /predict/batch
210
+ X-API-Key: your-key (en production)
211
+
212
+ # Envoi des 3 fichiers CSV bruts
213
+ curl -X POST "http://localhost:8000/predict/batch" \
214
+ -H "X-API-Key: your-key" \
215
+ -F "sondage_file=@data/extrait_sondage.csv" \
216
+ -F "eval_file=@data/extrait_eval.csv" \
217
+ -F "sirh_file=@data/extrait_sirh.csv"
218
+
219
+ # Réponse
220
  {
221
  "total_employees": 1470,
222
+ "predictions": [
223
+ {"employee_id": 1, "prediction": 1, "probability_leave": 0.84, "risk_level": "High"},
224
+ {"employee_id": 2, "prediction": 0, "probability_leave": 0.11, "risk_level": "Low"}
225
+ ],
226
  "summary": {
227
  "total_stay": 1169,
228
  "total_leave": 301,
229
+ "high_risk_count": 222,
230
+ "medium_risk_count": 233,
231
+ "low_risk_count": 1015
232
  }
233
  }
234
  ```
235
 
236
+ ## 📊 Logging
237
+
238
+ ### Logs structurés JSON
239
+
240
+ **Fichiers** :
241
+ - `logs/api.log` : Tous les logs
242
+ - `logs/error.log` : Erreurs uniquement
243
+
244
+ **Format** :
245
+ ```json
246
+ {
247
+ "timestamp": "2025-12-26T10:30:45",
248
+ "level": "INFO",
249
+ "logger": "employee_turnover_api",
250
+ "message": "Request POST /predict",
251
+ "method": "POST",
252
+ "path": "/predict",
253
+ "status_code": 200,
254
+ "duration_ms": 23.45,
255
+ "client_host": "127.0.0.1"
256
+ }
257
+ ```
258
+
259
+ ## 🛡️ Rate Limiting
260
+
261
+ **Configuration** :
262
+ - **Développement** : Désactivé (DEBUG=true)
263
+ - **Production** : 20 requêtes/minute par IP ou API Key
264
+
265
+ **En cas de dépassement** :
266
+ ```json
267
+ {
268
+ "error": "Rate limit exceeded",
269
+ "message": "20 per 1 minute"
270
+ }
271
+ ```
272
+
273
+ ## ✅ Tests
274
+
275
+ ### Suite de tests complète
276
+
277
+ **Métriques** :
278
+ - ✅ **97 tests** (86 passés, 11 skippés pour déploiement)
279
+ - 📊 **70.26% de couverture** globale du code
280
+ - ⚡ Temps d'exécution : **~4 secondes**
281
+ - 🎯 **9 fichiers de tests** couvrant tous les aspects
282
+
283
+ ### Catégories de tests
284
+
285
+ #### 🔐 Tests d'authentification (`test_api_auth.py`)
286
+ - Validation système d'authentification API Key
287
+ - Mode DEBUG vs Production
288
+ - Headers de sécurité
289
+ - Rate limiting par clé API
290
+ - **11 tests** - 100% passés
291
+
292
+ #### 🏥 Tests de santé (`test_api_health.py`)
293
+ - Endpoint `/health`
294
+ - Structure des réponses
295
+ - Statut du modèle
296
+ - Versionning
297
+ - **6 tests** - 100% passés
298
+
299
+ #### 🔮 Tests de prédiction (`test_api_predict.py`)
300
+ - Endpoint `/predict` avec données valides
301
+ - Structure des réponses (prediction, probabilities, risk_level)
302
+ - Validation des probabilités (somme = 1, range [0,1])
303
+ - Cohérence des prédictions
304
+ - **9 tests** - 100% passés
305
+
306
+ #### ✔️ Tests de validation (`test_api_validation.py`)
307
+ - Validation des champs requis
308
+ - Types de données
309
+ - Valeurs négatives
310
+ - Limites d'âge (18-70 ans)
311
+ - Énumérations (genre, département, statut_marital, etc.)
312
+ - Formats (augmentation_salaire en %)
313
+ - **15 tests** - 100% passés
314
+
315
+ #### 🗄️ Tests de base de données (`test_database.py`)
316
+ - Connexion PostgreSQL
317
+ - Existence des tables (`dataset`, `ml_logs`)
318
+ - Opérations CRUD
319
+ - Intégrité des contraintes
320
+ - **7 tests** - 100% passés
321
+
322
+ #### 🔄 Tests fonctionnels (`test_functional.py`)
323
+ - Tests end-to-end complets
324
+ - Intégration API + DB + Modèle ML
325
+ - Performance (temps de réponse < 2s)
326
+ - Gestion d'erreurs et rollback
327
+ - Scénarios de charge
328
+ - **19 tests** (17 passés, 2 skippés)
329
+
330
+ #### 🤖 Tests du modèle ML (`test_model.py`)
331
+ - Chargement depuis HuggingFace Hub
332
+ - Pipeline de preprocessing
333
+ - Feature engineering
334
+ - Validation Pydantic
335
+ - Prédictions réelles
336
+ - **23 tests** - 100% passés
337
+
338
+ #### 🌐 Tests d'intégration API déployée (`test_api_demo.py`)
339
+ - Tests sur API déployée HuggingFace Spaces
340
+ - Endpoints réels en production
341
+ - **7 tests** skippés en local (pour déploiement uniquement)
342
+
343
+ ### Exécution des tests
344
+
345
+ ```bash
346
+ # Tous les tests avec détails
347
+ poetry run pytest tests/ -v
348
+
349
+ # Avec couverture détaillée
350
+ poetry run pytest tests/ -v --cov=. --cov-report=term-missing
351
+
352
+ # Avec rapport HTML
353
+ poetry run pytest tests/ --cov=. --cov-report=html
354
+ open htmlcov/index.html
355
+
356
+ # Tests spécifiques
357
+ poetry run pytest tests/test_api_predict.py -v
358
+ poetry run pytest tests/test_database.py -v
359
+
360
+ # Par catégorie (marqueurs)
361
+ poetry run pytest -m "not integration" -v # Exclure tests d'intégration
362
+ ```
363
+
364
+ ### Détail de couverture par module
365
+
366
+ | Module | Couverture | Lignes | Manquantes |
367
+ |--------|------------|--------|------------|
368
+ | `src/config.py` | **100%** | 20 | 0 |
369
+ | `src/schemas.py` | **100%** | 100 | 0 |
370
+ | `src/rate_limit.py` | **100%** | 10 | 0 |
371
+ | `db_models.py` | **100%** | 14 | 0 |
372
+ | `src/logger.py` | **90.32%** | 62 | 6 |
373
+ | `src/preprocessing.py` | **76.36%** | 55 | 13 |
374
+ | `src/models.py` | **61.36%** | 44 | 17 |
375
+ | `api.py` | **55.41%** | 157 | 70 |
376
+ | `src/gradio_ui.py` | **52%** | 125 | 60 |
377
+ | `src/auth.py` | **47.37%** | 19 | 10 |
378
+
379
+ **Note** : Les modules avec couverture < 100% incluent des sections spécifiques au déploiement ou à Gradio UI (interface web), testées en environnement de production.
380
+
381
+ ## 🚀 Déploiement
382
+
383
+ ### Pipeline CI/CD automatisé
384
+
385
+ Le projet utilise **GitHub Actions** pour automatiser le workflow complet :
386
+
387
+ **Fichier** : `.github/workflows/ci-cd.yml`
388
+
389
+ **Workflow** (4 jobs séquentiels) :
390
+
391
+ 1. **🔍 Lint** (~30s)
392
+ - Black (formatage code)
393
+ - Flake8 (qualité code)
394
+
395
+ 2. **🧪 Tests** (~2-3 min)
396
+ - pytest avec 97 tests
397
+ - Couverture de code
398
+ - Upload vers Codecov
399
+ - Génération rapport HTML
400
+
401
+ 3. **🚀 Test API Server** (~1-2 min)
402
+ - Démarrage serveur uvicorn
403
+ - Test endpoint `/health`
404
+ - Test endpoint `/predict` avec payload réel
405
+ - Validation des réponses
406
+
407
+ 4. **📦 Deploy** (selon branche)
408
+ - `dev` → HuggingFace Space `ASI-Engineer/oc_p5-dev`
409
+ - `main` → HuggingFace Space `ASI-Engineer/oc_p5`
410
+
411
+ **⚡ Temps total** : ~5-7 minutes (< 10 min requis)
412
+
413
+ ### Environnements
414
+
415
+ | Environnement | Branche | HF Space | URL |
416
+ |---------------|---------|----------|-----|
417
+ | **Développement** | `dev` | `oc_p5-dev` | https://asi-engineer-oc-p5-dev.hf.space |
418
+ | **Production** | `main` | `oc_p5` | https://asi-engineer-oc-p5.hf.space |
419
+
420
+ ### Déploiement manuel
421
+
422
+ ```bash
423
+ # 1. Vérifier que tous les changements sont commitées
424
+ git status
425
+
426
+ # 2. Push sur dev (déclenche CI/CD automatiquement)
427
+ git push origin dev
428
+
429
+ # 3. Vérifier le pipeline
430
+ # https://github.com/chaton59/OC_P5/actions
431
+
432
+ # 4. Tester sur l'espace dev
433
+ curl https://asi-engineer-oc-p5-dev.hf.space/health
434
+
435
+ # 5. Si OK, merger vers main (après validation)
436
+ git checkout main
437
+ git merge dev
438
+ git push origin main
439
+ ```
440
+
441
+ ### Configuration requise
442
+
443
+ **Secrets GitHub** (`Settings > Secrets and variables > Actions`) :
444
+ - `HF_TOKEN` : Token HuggingFace avec accès write
445
+ - `API_KEY` : Clé API pour les tests CI/CD
446
+
447
+ **Variables HF Spaces** (dans settings du Space) :
448
+ - `API_KEY` : Clé API production (sécurisée)
449
+ - `DEBUG` : `false` (production) / `true` (dev)
450
+ - `LOG_LEVEL` : `INFO`
451
+
452
+ ### Documentation complète
453
+
454
+ 📖 **Guide détaillé** : [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)
455
+ - Docker et containerisation
456
+ - Troubleshooting
457
+ - Monitoring et logs
458
+ - Rollback procedures
459
+
460
+ ## 📚 Documentation
461
+
462
+ - **API Interactive** : http://localhost:8000/docs
463
+ - **ReDoc** : http://localhost:8000/redoc
464
+ - **Guide complet** : [docs/API_GUIDE.md](docs/API_GUIDE.md)
465
+ - **Standards** : [docs/standards.md](docs/standards.md)
466
+ - **Couverture tests** : [docs/TEST_COVERAGE.md](docs/TEST_COVERAGE.md)
467
+
468
+ ## 📦 Dépendances principales
469
+
470
+ - **FastAPI** 0.115.14 : Framework web
471
+ - **Pydantic** 2.12.5 : Validation données
472
+ - **XGBoost** 2.1.3 : Modèle ML
473
+ - **SlowAPI** 0.1.9 : Rate limiting
474
+ - **python-json-logger** 4.0.0 : Logs structurés
475
+ - **pytest** 9.0.2 : Tests
476
+
477
+
478
+ ## 🔄 Changelog
479
+
480
+ ### v3.2.1 (janvier 2026)
481
+ - 🎛️ Sliders Gradio et schémas Pydantic alignés sur les min/max réels des données d'entraînement
482
+ - 📦 Endpoint batch CSV (3 fichiers bruts)
483
+ - 🔑 Authentification API Key (prod)
484
+ - 🔧 Correction preprocessing (scaling, ordre des colonnes)
485
+ - 📝 Documentation et exemples mis à jour
486
+
487
+ ### v2.2.0 (27 décembre 2025)
488
+ - 📦 Nouvel endpoint `/predict/batch` pour traitement CSV direct
489
+ - 🔧 Fix preprocessing : ajout du scaling des features
490
+ - 🔧 Fix preprocessing : correction de l'ordre des colonnes
491
+ - 📊 Amélioration précision des prédictions (~90%)
492
+
493
+ ### v2.1.0 (26 décembre 2025)
494
+ - ✨ Système de logging structuré JSON
495
+ - 🛡️ Rate limiting avec SlowAPI
496
+ - ⚡ Amélioration gestion d'erreurs
497
+ - 📊 Monitoring des performances
498
+
499
+ ### v2.0.0 (26 décembre 2025)
500
+ - ✅ Suite de tests complète (84 tests)
501
+ - 🔐 Authentification API Key
502
+ - 📊 88% de couverture de code
503
 
504
+ ## 👥 Auteurs
505
 
506
+ - **Projet** : OpenClassrooms P5
507
+ - **Repo** : [github.com/chaton59/OC_P5](https://github.com/chaton59/OC_P5)
api.py CHANGED
@@ -13,10 +13,11 @@ Cette API expose le modèle de prédiction de départ des employés avec :
13
  import io
14
  import time
15
  from contextlib import asynccontextmanager
 
16
 
17
  import gradio as gr
18
  import pandas as pd
19
- from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile
20
  from fastapi.middleware.cors import CORSMiddleware
21
  from slowapi import _rate_limit_exceeded_handler
22
  from slowapi.errors import RateLimitExceeded
@@ -24,7 +25,7 @@ from slowapi.errors import RateLimitExceeded
24
  from src.auth import verify_api_key
25
  from src.config import get_settings
26
  from src.gradio_ui import create_gradio_interface
27
- from src.logger import logger, log_model_load, log_request
28
  from src.models import get_model_info, load_model
29
  from src.preprocessing import (
30
  merge_csv_dataframes,
@@ -45,6 +46,31 @@ settings = get_settings()
45
  API_VERSION = settings.API_VERSION
46
 
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  @asynccontextmanager
49
  async def lifespan(app: FastAPI):
50
  """
@@ -87,7 +113,27 @@ app = FastAPI(
87
 
88
  # Ajouter rate limiting
89
  app.state.limiter = limiter
90
- app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
  # Configurer CORS (autoriser tous les domaines en dev)
93
  app.add_middleware(
@@ -164,7 +210,7 @@ async def health_check():
164
  tags=["Prediction"],
165
  dependencies=[Depends(verify_api_key)] if settings.is_api_key_required else [],
166
  )
167
- @limiter.limit("20/minute")
168
  async def predict(request: Request, employee: EmployeeInput):
169
  """
170
  Endpoint de prédiction du turnover d'un employé.
@@ -225,6 +271,7 @@ async def predict(request: Request, employee: EmployeeInput):
225
  try:
226
  from sqlalchemy import create_engine
227
  from sqlalchemy.orm import sessionmaker
 
228
  from db_models import MLLog
229
 
230
  engine = create_engine(settings.DATABASE_URL)
@@ -267,7 +314,7 @@ async def predict(request: Request, employee: EmployeeInput):
267
  tags=["Prediction"],
268
  dependencies=[Depends(verify_api_key)] if settings.is_api_key_required else [],
269
  )
270
- @limiter.limit("5/minute")
271
  async def predict_batch(
272
  request: Request,
273
  sondage_file: UploadFile = File(..., description="Fichier CSV du sondage"),
 
13
  import io
14
  import time
15
  from contextlib import asynccontextmanager
16
+ from typing import Any, Callable
17
 
18
  import gradio as gr
19
  import pandas as pd
20
+ from fastapi import Depends, FastAPI, File, HTTPException, Request, Response, UploadFile
21
  from fastapi.middleware.cors import CORSMiddleware
22
  from slowapi import _rate_limit_exceeded_handler
23
  from slowapi.errors import RateLimitExceeded
 
25
  from src.auth import verify_api_key
26
  from src.config import get_settings
27
  from src.gradio_ui import create_gradio_interface
28
+ from src.logger import log_model_load, log_request, logger
29
  from src.models import get_model_info, load_model
30
  from src.preprocessing import (
31
  merge_csv_dataframes,
 
46
  API_VERSION = settings.API_VERSION
47
 
48
 
49
+ def conditional_rate_limit(
50
+ limit: str,
51
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
52
+ """
53
+ Applique un rate limit seulement si DEBUG=False.
54
+
55
+ En mode DEBUG (tests), pas de rate limiting pour éviter les échecs de tests.
56
+
57
+ Args:
58
+ limit: Limite à appliquer (ex: "20/minute")
59
+
60
+ Returns:
61
+ Décorateur de rate limiting ou fonction identité
62
+ """
63
+ if settings.DEBUG:
64
+ # En mode DEBUG, retourner une fonction qui ne fait rien
65
+ def no_limit(func):
66
+ return func
67
+
68
+ return no_limit
69
+ else:
70
+ # En production, appliquer le rate limit normal
71
+ return limiter.limit(limit)
72
+
73
+
74
  @asynccontextmanager
75
  async def lifespan(app: FastAPI):
76
  """
 
113
 
114
  # Ajouter rate limiting
115
  app.state.limiter = limiter
116
+
117
+
118
+ # Wrapper pour le handler de rate limit qui respecte l'interface FastAPI
119
+ def rate_limit_exception_handler(request: Request, exc: Exception) -> Response:
120
+ """
121
+ Handler pour les exceptions de rate limiting.
122
+
123
+ Utilise le handler de slowapi mais avec l'interface FastAPI.
124
+ """
125
+ if isinstance(exc, RateLimitExceeded):
126
+ return _rate_limit_exceeded_handler(request, exc)
127
+ else:
128
+ # Fallback pour autres exceptions
129
+ from fastapi.responses import JSONResponse
130
+
131
+ return JSONResponse(
132
+ status_code=500, content={"detail": "Internal server error"}
133
+ )
134
+
135
+
136
+ app.add_exception_handler(RateLimitExceeded, rate_limit_exception_handler)
137
 
138
  # Configurer CORS (autoriser tous les domaines en dev)
139
  app.add_middleware(
 
210
  tags=["Prediction"],
211
  dependencies=[Depends(verify_api_key)] if settings.is_api_key_required else [],
212
  )
213
+ @conditional_rate_limit("20/minute")
214
  async def predict(request: Request, employee: EmployeeInput):
215
  """
216
  Endpoint de prédiction du turnover d'un employé.
 
271
  try:
272
  from sqlalchemy import create_engine
273
  from sqlalchemy.orm import sessionmaker
274
+
275
  from db_models import MLLog
276
 
277
  engine = create_engine(settings.DATABASE_URL)
 
314
  tags=["Prediction"],
315
  dependencies=[Depends(verify_api_key)] if settings.is_api_key_required else [],
316
  )
317
+ @conditional_rate_limit("5/minute")
318
  async def predict_batch(
319
  request: Request,
320
  sondage_file: UploadFile = File(..., description="Fichier CSV du sondage"),
src/gradio_ui.py CHANGED
@@ -7,12 +7,24 @@ Cette interface permet de:
7
  - Visualiser la documentation de l'API
8
  - Comprendre les champs requis
9
  """
10
- import gradio as gr
11
  import os
 
 
 
12
 
13
  from src.models import get_model_info, load_model
14
  from src.preprocessing import preprocess_for_prediction
15
- from src.schemas import EmployeeInput
 
 
 
 
 
 
 
 
 
 
16
 
17
 
18
  def predict_turnover(
@@ -61,9 +73,9 @@ def predict_turnover(
61
  ),
62
  distance_domicile_travail=int(distance_domicile_travail),
63
  niveau_education=int(niveau_education),
64
- domaine_etude=domaine_etude,
65
- ayant_enfants=ayant_enfants,
66
- frequence_deplacement=frequence_deplacement,
67
  annees_depuis_la_derniere_promotion=int(
68
  annees_depuis_la_derniere_promotion
69
  ),
@@ -81,14 +93,14 @@ def predict_turnover(
81
  satisfaction_employee_equilibre_pro_perso
82
  ),
83
  note_evaluation_actuelle=int(note_evaluation_actuelle),
84
- heure_supplementaires=heure_supplementaires,
85
  augementation_salaire_precedente=float(augementation_salaire_precedente),
86
  age=int(age),
87
- genre=genre,
88
  revenu_mensuel=float(revenu_mensuel),
89
- statut_marital=statut_marital,
90
- departement=departement,
91
- poste=poste,
92
  nombre_experiences_precedentes=int(nombre_experiences_precedentes),
93
  nombre_heures_travailless=int(nombre_heures_travailless),
94
  annee_experience_totale=int(annee_experience_totale),
@@ -131,6 +143,7 @@ def predict_turnover(
131
  if os.getenv("SPACE_ID") is None: # Pas sur HF Spaces
132
  from sqlalchemy import create_engine
133
  from sqlalchemy.orm import sessionmaker
 
134
  from src.config import get_settings
135
 
136
  settings = get_settings()
 
7
  - Visualiser la documentation de l'API
8
  - Comprendre les champs requis
9
  """
 
10
  import os
11
+ from typing import cast
12
+
13
+ import gradio as gr
14
 
15
  from src.models import get_model_info, load_model
16
  from src.preprocessing import preprocess_for_prediction
17
+ from src.schemas import (
18
+ AyantEnfantsEnum,
19
+ DepartementEnum,
20
+ DomaineEtudeEnum,
21
+ EmployeeInput,
22
+ FrequenceDeplacementEnum,
23
+ GenreEnum,
24
+ HeureSupplementairesEnum,
25
+ PosteEnum,
26
+ StatutMaritalEnum,
27
+ )
28
 
29
 
30
  def predict_turnover(
 
73
  ),
74
  distance_domicile_travail=int(distance_domicile_travail),
75
  niveau_education=int(niveau_education),
76
+ domaine_etude=cast(DomaineEtudeEnum, domaine_etude),
77
+ ayant_enfants=cast(AyantEnfantsEnum, ayant_enfants),
78
+ frequence_deplacement=cast(FrequenceDeplacementEnum, frequence_deplacement),
79
  annees_depuis_la_derniere_promotion=int(
80
  annees_depuis_la_derniere_promotion
81
  ),
 
93
  satisfaction_employee_equilibre_pro_perso
94
  ),
95
  note_evaluation_actuelle=int(note_evaluation_actuelle),
96
+ heure_supplementaires=cast(HeureSupplementairesEnum, heure_supplementaires),
97
  augementation_salaire_precedente=float(augementation_salaire_precedente),
98
  age=int(age),
99
+ genre=cast(GenreEnum, genre),
100
  revenu_mensuel=float(revenu_mensuel),
101
+ statut_marital=cast(StatutMaritalEnum, statut_marital),
102
+ departement=cast(DepartementEnum, departement),
103
+ poste=cast(PosteEnum, poste),
104
  nombre_experiences_precedentes=int(nombre_experiences_precedentes),
105
  nombre_heures_travailless=int(nombre_heures_travailless),
106
  annee_experience_totale=int(annee_experience_totale),
 
143
  if os.getenv("SPACE_ID") is None: # Pas sur HF Spaces
144
  from sqlalchemy import create_engine
145
  from sqlalchemy.orm import sessionmaker
146
+
147
  from src.config import get_settings
148
 
149
  settings = get_settings()
src/logger.py CHANGED
@@ -13,7 +13,7 @@ import sys
13
  from pathlib import Path
14
  from typing import Any, Dict
15
 
16
- from pythonjsonlogger import jsonlogger
17
 
18
  from src.config import get_settings
19
 
@@ -28,7 +28,7 @@ LOG_FILE = LOG_DIR / "api.log"
28
  ERROR_LOG_FILE = LOG_DIR / "error.log"
29
 
30
 
31
- class CustomJsonFormatter(jsonlogger.JsonFormatter):
32
  """
33
  Formatter JSON personnalisé avec champs supplémentaires.
34
  """
 
13
  from pathlib import Path
14
  from typing import Any, Dict
15
 
16
+ from pythonjsonlogger.jsonlogger import JsonFormatter
17
 
18
  from src.config import get_settings
19
 
 
28
  ERROR_LOG_FILE = LOG_DIR / "error.log"
29
 
30
 
31
+ class CustomJsonFormatter(JsonFormatter):
32
  """
33
  Formatter JSON personnalisé avec champs supplémentaires.
34
  """
src/schemas.py CHANGED
@@ -6,7 +6,7 @@ Ces schémas correspondent aux colonnes brutes du dataset avant preprocessing,
6
  permettant une validation stricte des inputs avec messages d'erreur clairs.
7
  """
8
  from enum import Enum
9
- from typing import Annotated, Literal
10
 
11
  from pydantic import BaseModel, BeforeValidator, ConfigDict, Field
12
 
@@ -75,6 +75,20 @@ class FrequenceDeplacementEnum(str, Enum):
75
  FREQUENT = "Frequent"
76
 
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  class EmployeeInput(BaseModel):
79
  """
80
  Schéma de validation pour les données d'entrée d'un employé.
@@ -103,7 +117,7 @@ class EmployeeInput(BaseModel):
103
  ..., ge=1, le=5, description="Niveau d'éducation (1-5)"
104
  )
105
  domaine_etude: DomaineEtudeEnum = Field(..., description="Domaine d'études")
106
- ayant_enfants: Literal["Y", "N"] = Field(..., description="A des enfants (Y/N)")
107
  frequence_deplacement: FrequenceDeplacementEnum = Field(
108
  ..., description="Fréquence des déplacements"
109
  )
@@ -136,7 +150,7 @@ class EmployeeInput(BaseModel):
136
  note_evaluation_actuelle: int = Field(
137
  ..., ge=3, le=4, description="Note évaluation actuelle (3-4)"
138
  )
139
- heure_supplementaires: Literal["Oui", "Non"] = Field(
140
  ..., description="Fait des heures supplémentaires"
141
  )
142
  augementation_salaire_precedente: Annotated[
 
6
  permettant une validation stricte des inputs avec messages d'erreur clairs.
7
  """
8
  from enum import Enum
9
+ from typing import Annotated
10
 
11
  from pydantic import BaseModel, BeforeValidator, ConfigDict, Field
12
 
 
75
  FREQUENT = "Frequent"
76
 
77
 
78
+ class AyantEnfantsEnum(str, Enum):
79
+ """L'employé a des enfants."""
80
+
81
+ OUI = "Y"
82
+ NON = "N"
83
+
84
+
85
+ class HeureSupplementairesEnum(str, Enum):
86
+ """L'employé fait des heures supplémentaires."""
87
+
88
+ OUI = "Oui"
89
+ NON = "Non"
90
+
91
+
92
  class EmployeeInput(BaseModel):
93
  """
94
  Schéma de validation pour les données d'entrée d'un employé.
 
117
  ..., ge=1, le=5, description="Niveau d'éducation (1-5)"
118
  )
119
  domaine_etude: DomaineEtudeEnum = Field(..., description="Domaine d'études")
120
+ ayant_enfants: AyantEnfantsEnum = Field(..., description="A des enfants (Y/N)")
121
  frequence_deplacement: FrequenceDeplacementEnum = Field(
122
  ..., description="Fréquence des déplacements"
123
  )
 
150
  note_evaluation_actuelle: int = Field(
151
  ..., ge=3, le=4, description="Note évaluation actuelle (3-4)"
152
  )
153
+ heure_supplementaires: HeureSupplementairesEnum = Field(
154
  ..., description="Fait des heures supplémentaires"
155
  )
156
  augementation_salaire_precedente: Annotated[