SteGONZALEZ commited on
Commit
1d0150f
·
0 Parent(s):

Clean main (no binary history)

Browse files
.coverage ADDED
Binary file (53.2 kB). View file
 
.dockerignore ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ *.pytest_cache/
7
+
8
+ # Tests & notebooks
9
+ tests/
10
+ notebooks/
11
+
12
+ # Git & CI
13
+ .git/
14
+ .github/
15
+
16
+ # Local env
17
+ .env
18
+ .env.*
19
+
20
+ # Poetry / venv
21
+ .venv/
22
+ .cache/
23
+
24
+ # OS
25
+ .DS_Store
26
+ Thumbs.db
27
+
28
+ # Docker / compose inutiles en prod
29
+ docker-compose.yml
.env.example ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ DB_USER= username
2
+ DB_PASSWORD=password
3
+ DB_HOST=localhost
4
+ DB_PORT=5432
5
+ DB_NAME=TechNovaPartners
6
+ DATABASE_URL=postgresql+psycopg://user:pass@host:5432/dbname
7
+ API_KEY=super-secret-key
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
.github/workflows/cd.yml ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CD
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+
7
+
8
+ jobs:
9
+ test:
10
+ name: tests_CD
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - name: Checkout
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.13"
21
+
22
+ - name: Install Poetry
23
+ uses: snok/install-poetry@v1
24
+ with:
25
+ version: "2.0.0"
26
+ virtualenvs-create: true
27
+ virtualenvs-in-project: true
28
+
29
+ - name: Install dependencies
30
+ run: poetry install --no-interaction
31
+
32
+ - name: Run tests (SQLite)
33
+ env:
34
+ API_KEY: "ci-test-key"
35
+ DATABASE_URL: "sqlite+pysqlite:///:memory:"
36
+ run: poetry run pytest -q
37
+
38
+ build-and-push:
39
+ runs-on: ubuntu-latest
40
+ needs: test
41
+
42
+ permissions:
43
+ contents: read
44
+ packages: write
45
+
46
+ steps:
47
+ - name: Checkout
48
+ uses: actions/checkout@v4
49
+
50
+ - name: Set image name (lowercase)
51
+ run: echo "IMAGE_NAME=ghcr.io/${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
52
+
53
+ - name: Log in to GHCR
54
+ uses: docker/login-action@v3
55
+ with:
56
+ registry: ghcr.io
57
+ username: ${{ github.actor }}
58
+ password: ${{ secrets.GITHUB_TOKEN }}
59
+
60
+ - name: Build and push image
61
+ uses: docker/build-push-action@v6
62
+ with:
63
+ context: .
64
+ push: true
65
+ tags: |
66
+ ${{ env.IMAGE_NAME }}:latest
67
+ ${{ env.IMAGE_NAME }}:${{ github.sha }}
.github/workflows/ci.yml ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+
8
+ jobs:
9
+ tests:
10
+ name: tests_CI
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - name: Checkout
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.13"
21
+
22
+ - name: Install Poetry
23
+ uses: snok/install-poetry@v1
24
+ with:
25
+ version: "2.0.0"
26
+ virtualenvs-create: true
27
+ virtualenvs-in-project: true
28
+
29
+ - name: Install dependencies
30
+ run: poetry install --no-interaction --no-ansi
31
+
32
+ - name: Tests + Coverage
33
+ env:
34
+ DATABASE_URL: "sqlite+pysqlite:///:memory:"
35
+ API_KEY: "ci-test-key"
36
+ run: poetry run pytest -q --cov=app --cov=service --cov-report=term-missing
.gitignore ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- VARIABLES D’ENVIRONNEMENT ---
2
+ *.env
3
+ *.venv
4
+
5
+ # --- DONNÉES LOURDES / NON CODE ---
6
+ *.zip
7
+ *.pptx
8
+
9
+ # Fichiers personnels
10
+ /perso/
11
+ /my-postgres/
12
+
13
+ # livrables formation
14
+ /livrables/
15
+
16
+ # --- DOSSIERS DE DONNÉES ---
17
+ /data/
18
+ *.csv
19
+
20
+ __pycache__/
21
+ .ipynb_checkpoints/
22
+
23
+ artifacts/*.joblib
24
+ artifacts/*.pkl
25
+ artifacts/*.bin
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.13-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ PIP_NO_CACHE_DIR=1
6
+
7
+ WORKDIR /app
8
+
9
+ RUN pip install --no-cache-dir poetry==2.0.0
10
+
11
+ COPY pyproject.toml poetry.lock* README.md /app/
12
+
13
+ RUN poetry config virtualenvs.create false \
14
+ && poetry install --no-interaction --no-ansi --only main --no-root
15
+
16
+ COPY . /app
17
+
18
+ EXPOSE 7860
19
+
20
+ CMD ["poetry", "run", "uvicorn", "app.api:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: technova-api
3
+ sdk: docker
4
+ app_port: 7860
5
+ ---
6
+
7
+ # TechNova Partners – Déploiement d’un modèle de Machine Learning
8
+
9
+ Ce projet met en œuvre le **déploiement d’un modèle de Machine Learning** de prédiction du **turnover employé** via une **API FastAPI**, connectée à une **base de données PostgreSQL**, et pilotée par un **dashboard Streamlit**.
10
+
11
+ L’objectif est de proposer une architecture **production-ready**, respectant les bonnes pratiques **MLOps** :
12
+ - séparation ingestion / préparation / inference,
13
+ - API sans logique de preprocessing,
14
+ - traçabilité des prédictions,
15
+ - tests automatisés,
16
+ - CI/CD,
17
+ - déploiement reproductible.
18
+
19
+ ---
20
+ ## Live API
21
+
22
+ Base URL:
23
+ https://stegonzalez-technova-api.hf.space
24
+
25
+ Swagger UI:
26
+ https://stegonzalez-technova-api.hf.space/docs
27
+
28
+ Health checks:
29
+ - /health
30
+ - /ready
31
+
32
+ ---
33
+
34
+ ## Architecture du projet
35
+
36
+ Projet_TechNova_Partners/
37
+
38
+ ├── app/
39
+ │ ├── api.py # API FastAPI (routes, orchestration)
40
+ │ ├── database.py # Connexion DB (SQLAlchemy)
41
+ │ ├── main.py # Launcher API + Dashboard
42
+ │ ├── models.py # Modèles ORM
43
+ │ └── security.py # Sécurité via API Key
44
+
45
+ ├── artifacts/
46
+ │ ├── modele_classification_technova.joblib # Modèle ML entraîné
47
+ │ └── threshold.json # Seuil de décision
48
+
49
+ ├── dashboard/
50
+ │ ├── dshbd.py # Dashboard Streamlit
51
+ │ └── feature_schema.py # Schéma des features (source de vérité UI)
52
+
53
+ ├── domain/
54
+ │ └── domain.py # Schémas Pydantic (ModelRequest / ModelResponse)
55
+
56
+ ├── my-postgres/
57
+ │ └── docker-compose.yml # PostgreSQL via Docker
58
+
59
+ ├── scripts/
60
+ │ ├── build_ml_features.py # Création des tables clean
61
+ │ ├── create_db.py # Création DB + tables (one-shot)
62
+ │ ├── seed_from_csv.py # Seed optionnel depuis CSV
63
+ │ ├── seed_ml_features.py # Nettoyage + feature engineering
64
+ │ ├── init_project_technova.py # Lance tous les scripts nécessaires
65
+ │ └── generate_docs.py # Génération de documentation
66
+
67
+ ├── service/
68
+ │ └── technova_service.py # Logique ML (chargement modèle + prédiction)
69
+
70
+ ├── tests/ # Tests Pytest
71
+
72
+ ├── Dockerfile
73
+ ├── pyproject.toml
74
+ ├── .env
75
+ ├── .env.example
76
+ └── README.md
77
+
78
+ ---
79
+
80
+ ---
81
+
82
+ ## Principe de fonctionnement
83
+
84
+ ### Séparation des pipelines
85
+ - Les **données brutes** sont stockées dans des tables dédiées.
86
+ - Un **pipeline de nettoyage et de transformation** est exécuté via des scripts indépendants.
87
+ - Les données transformées sont stockées dans des tables **clean**.
88
+ - **L’API consomme uniquement les tables clean**, directement compatibles avec le modèle.
89
+
90
+ **Aucun preprocessing n’est effectué dans l’API**.
91
+
92
+ Cette approche améliore :
93
+ - la performance,
94
+ - la robustesse,
95
+ - la reproductibilité,
96
+ - la conformité aux standards MLOps.
97
+
98
+ ---
99
+
100
+ ## API – Endpoints
101
+
102
+ ### Sécurité
103
+ Tous les endpoints sont protégés par une **API Key**, transmise via le header :
104
+ X-API-Key: <API_KEY>
105
+
106
+ ---
107
+
108
+ ### 🔹 Prédiction – mode production (recommandé)
109
+ POST /predict/by-id/{employee_id}
110
+
111
+ - Récupère les features depuis la table **clean**
112
+ - Effectue la prédiction
113
+ - Enregistre la requête et la réponse en base
114
+
115
+ ---
116
+
117
+ ### 🔹 Prédiction – mode scoring (features prêtes)
118
+ POST /predict/by-features
119
+
120
+ - Attend des **features déjà préparées au format attendu par le modèle**
121
+ - Aucun nettoyage ou feature engineering dans l’API
122
+ - Utile pour intégration externe, tests ou scoring
123
+
124
+ ---
125
+
126
+ ### 🔹 Logs & monitoring
127
+ GET /predictions/latest
128
+ Retourne les dernières prédictions stockées en base.
129
+ GET /health
130
+ GET /ready
131
+
132
+ - `/health` : API disponible
133
+ - `/ready` : API + base de données + modèle opérationnels
134
+
135
+ ---
136
+
137
+ ## Dashboard Streamlit
138
+ Le dashboard permet :
139
+ - la saisie des features métier,
140
+ - l’appel à l’API,
141
+ - la visualisation des prédictions,
142
+ - la consultation de l’historique des prédictions stockées en base.
143
+
144
+ ---
145
+
146
+ ## Installation & exécution (Quickstart)
147
+
148
+ ### 🔹 Cloner le projet
149
+ git clone <repo_url>
150
+ cd Projet_TechNova_Partners
151
+
152
+ ### 🔹 Installer les dépendances
153
+ poetry install
154
+
155
+ ### 🔹 Configurer l’environnement
156
+ cp .env.example .env
157
+
158
+ ### 🔹 Lancer PostgreSQL via Docker
159
+ cd my-postgres
160
+ docker-compose up -d
161
+
162
+ ### 🔹 Initialiser la base (une seule fois)
163
+ poetry run python scripts/init_project_technova.py
164
+
165
+ ### 🔹 Lancer l’API et le dashboard
166
+ poetry run technova
167
+
168
+ ### Accès
169
+ API : http://127.0.0.1:8000
170
+ Swagger : http://127.0.0.1:8000/docs
171
+ Dashboard : http://127.0.0.1:8501
172
+
173
+ ---
174
+ ## Example Request
175
+
176
+ curl -X POST "https://stegonzalez-technova-api.hf.hf.space/predict/by-features" \
177
+ -H "X-API-Key: technova-secret-2026" \
178
+ -H "Content-Type: application/json" \
179
+ -d '{"feature_1": 1.2, "feature_2": 0.8}'
180
+
181
+
182
+ ## Tests & Qualité
183
+
184
+ ## Lancement des tests
185
+ poetry run pytest
186
+
187
+ ## Couverture de tests
188
+ poetry run pytest --cov=app --cov=service
189
+
190
+ . Couverture actuelle : > 90 %
191
+ . Tests exécutés sur SQLite pour rapidité et portabilité
192
+ . PostgreSQL utilisé pour le développement et la production
193
+
194
+ ---
195
+
196
+ ## CI/CD
197
+ CI :
198
+ . Tests et coverage exécutés automatiquement à chaque Pull Request
199
+ . Branche main protégée : merge bloqué si la CI échoue
200
+ CD :
201
+ . Build de l’image Docker après merge sur main
202
+ . Push automatique de l’image vers GitHub Container Registry (GHCR)
203
+
204
+ ---
205
+
206
+ ## Auteur
207
+
208
+ Projet réalisé par Stéphane Gonzalez
209
+ Formation OpenClassrooms — Data Scientist / AI Engineer
app/.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
app/__init__.py ADDED
File without changes
app/api.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from time import perf_counter
4
+
5
+ from fastapi import FastAPI, HTTPException, Depends
6
+ from fastapi.responses import RedirectResponse
7
+ from sqlalchemy.orm import Session
8
+ from sqlalchemy import desc
9
+
10
+ from domain.domain import ModelRequest, ModelResponse
11
+ from service.technova_service import TechNovaService
12
+ from app.security import verify_api_key
13
+
14
+ from contextlib import asynccontextmanager
15
+
16
+ from .database import get_db
17
+ from .models import PredictionRequest, Prediction
18
+
19
+ MODEL_VERSION = "xgb_v1"
20
+
21
+ service_singleton: TechNovaService | None = None
22
+
23
+ @asynccontextmanager
24
+ async def lifespan(app: FastAPI):
25
+ global service_singleton
26
+ service_singleton = TechNovaService() # charge modèle + seuil une seule fois
27
+ yield
28
+ service_singleton = None
29
+
30
+ app = FastAPI(lifespan=lifespan)
31
+
32
+ def get_service() -> TechNovaService:
33
+ assert service_singleton is not None, "Service not initialized"
34
+ return service_singleton
35
+
36
+ #accessible via "/" pour rediriger vers la doc interactive, mais pas dans la doc elle-même
37
+ @app.get("/", include_in_schema=False)
38
+ def root():
39
+ return RedirectResponse(url="/docs")
40
+
41
+ # endpoint de santé pour monitoring
42
+ @app.get("/health", dependencies=[Depends(verify_api_key)])
43
+ def health():
44
+ return {"status": "ok"}
45
+ from sqlalchemy import text
46
+
47
+ @app.get("/ready", dependencies=[Depends(verify_api_key)])
48
+ def ready(
49
+ db: Session = Depends(get_db),
50
+ service: TechNovaService = Depends(get_service),
51
+ ):
52
+ if service.model is None:
53
+ raise HTTPException(status_code=503, detail="Model not loaded")
54
+ try:
55
+ db.execute(text("SELECT 1"))
56
+ except Exception as e:
57
+ raise HTTPException(status_code=503, detail=f"DB not ready: {e}") from e
58
+ try:
59
+ db.execute(text("SELECT 1 FROM clean.ml_features_employees LIMIT 1"))
60
+ except Exception as e:
61
+ raise HTTPException(status_code=503, detail=f"Clean table not ready: {e}") from e
62
+
63
+ return {"status": "ready", "model_version": MODEL_VERSION}
64
+
65
+ #prédiction via ID employé — mode recommandé pour la production
66
+ @app.post("/predict/by-id/{employee_id}", response_model=ModelResponse, dependencies=[Depends(verify_api_key)])
67
+ def predict_by_employee_id(
68
+ employee_id: int,
69
+ db: Session = Depends(get_db),
70
+ service: TechNovaService = Depends(get_service),
71
+ ) -> ModelResponse:
72
+ try:
73
+ t0 = perf_counter()
74
+ response = service.predict_from_clean(db=db, employee_id=employee_id)
75
+ latency_ms = int((perf_counter() - t0) * 1000)
76
+
77
+ req = PredictionRequest(
78
+ employee_id=employee_id,
79
+ payload_json={"mode": "by_employee_id", "employee_id": employee_id},
80
+ )
81
+ db.add(req)
82
+ db.flush()
83
+
84
+ db.add(
85
+ Prediction(
86
+ request_id=req.id,
87
+ model_version=MODEL_VERSION,
88
+ predicted_class=int(response.will_leave),
89
+ predicted_proba=float(response.turnover_probability),
90
+ threshold_used=float(service.threshold),
91
+ latency_ms=latency_ms,
92
+ )
93
+ )
94
+ db.commit()
95
+ return response
96
+
97
+ except ValueError as e:
98
+ db.rollback()
99
+ raise HTTPException(status_code=404, detail=str(e)) from e
100
+ except Exception as e:
101
+ db.rollback()
102
+ raise HTTPException(status_code=500, detail=str(e)) from e
103
+
104
+ #endpoint de prédiction via features brutes — utile pour tests et debug, mais pas recommandé pour la production
105
+ @app.post("/predict/by-features", response_model=ModelResponse, dependencies=[Depends(verify_api_key)])
106
+ def predict_by_features(
107
+ payload: ModelRequest,
108
+ db: Session = Depends(get_db),
109
+ service: TechNovaService = Depends(get_service),
110
+ ) -> ModelResponse:
111
+ try:
112
+ t0 = perf_counter()
113
+ response = service.predict_from_payload(request=payload)
114
+ latency_ms = int((perf_counter() - t0) * 1000)
115
+
116
+ req = PredictionRequest(
117
+ payload_json={"mode": "by_features", "payload": payload.model_dump()},
118
+ )
119
+ db.add(req)
120
+ db.flush()
121
+
122
+ db.add(
123
+ Prediction(
124
+ request_id=req.id,
125
+ model_version=MODEL_VERSION,
126
+ predicted_class=int(response.will_leave),
127
+ predicted_proba=float(response.turnover_probability),
128
+ threshold_used=float(service.threshold),
129
+ latency_ms=latency_ms,
130
+ )
131
+ )
132
+ db.commit()
133
+ return response
134
+
135
+ except ValueError as e:
136
+ db.rollback()
137
+ raise HTTPException(status_code=422, detail=str(e)) from e
138
+
139
+ #endpoint pour récupérer les dernières prédictions stockées en base, avec pagination
140
+ @app.get("/predictions/latest", dependencies=[Depends(verify_api_key)])
141
+ def latest_predictions(
142
+ limit: int = 20,
143
+ db: Session = Depends(get_db),
144
+ ):
145
+ rows = (
146
+ db.query(Prediction, PredictionRequest)
147
+ .join(PredictionRequest, Prediction.request_id == PredictionRequest.id)
148
+ .order_by(desc(Prediction.created_at))
149
+ .limit(limit)
150
+ .all()
151
+ )
152
+
153
+ return [
154
+ {
155
+ "prediction_id": p.id,
156
+ "request_id": p.request_id,
157
+ "created_at": p.created_at.isoformat(),
158
+ "predicted_class": int(p.predicted_class),
159
+ "predicted_proba": round(float(p.predicted_proba), 4),
160
+ "threshold_used": float(p.threshold_used),
161
+ "model_version": p.model_version,
162
+ "latency_ms": p.latency_ms,
163
+ "payload": r.payload_json,
164
+ }
165
+ for p, r in rows
166
+ ]
app/database.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Generator
5
+
6
+ from dotenv import load_dotenv, find_dotenv
7
+ from sqlalchemy import create_engine
8
+ from sqlalchemy.orm import sessionmaker, Session
9
+
10
+ # Charger les variables d'environnement depuis .env, sauf si on est dans un contexte de test
11
+ # (pytest définit la variable d'environnement PYTEST_CURRENT_TEST)
12
+ if "PYTEST_CURRENT_TEST" not in os.environ:
13
+ load_dotenv(find_dotenv())
14
+
15
+ # Si PYTEST_CURRENT_TEST est défini, on suppose que les tests gèrent eux-mêmes les variables
16
+ # d'environnement nécessaires (par exemple via des fixtures pytest)
17
+ DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+pysqlite:///:memory:")
18
+
19
+ # SQLAlchemy 2.0 recommande d'utiliser future=True pour préparer la transition vers la nouvelle API,
20
+ # et pool_pre_ping=True pour éviter les erreurs de connexion "MySQL server has gone away"
21
+ engine = create_engine(
22
+ DATABASE_URL,
23
+ pool_pre_ping=True,
24
+ future=True,
25
+ connect_args={"check_same_thread": False}
26
+ if DATABASE_URL.startswith("sqlite")
27
+ else {},
28
+ )
29
+
30
+ # sessionmaker est configuré pour ne pas faire de commit automatique,
31
+ # ne pas faire de flush automatique, et ne pas expirer les objets après commit (expire_on_commit=False)
32
+ # pour éviter les problèmes d'accès aux données après le commit
33
+ SessionLocal = sessionmaker(
34
+ bind=engine,
35
+ autocommit=False,
36
+ autoflush=False,
37
+ expire_on_commit=False,
38
+ )
39
+
40
+ # Dependency pour obtenir une session de base de données dans les endpoints FastAPI,
41
+ # avec gestion automatique de la fermeture de la session après usage
42
+ def get_db() -> Generator[Session, None, None]:
43
+ db = SessionLocal()
44
+ try:
45
+ yield db
46
+ finally:
47
+ db.close()
app/main.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import sys
3
+ import time
4
+ from pathlib import Path
5
+
6
+ ROOT = Path(__file__).resolve().parents[1]
7
+
8
+ def run_api():
9
+ return subprocess.Popen(
10
+ [
11
+ sys.executable, "-m", "uvicorn",
12
+ "app.api:app",
13
+ "--host", "127.0.0.1",
14
+ "--port", "8000",
15
+ ],
16
+ cwd=ROOT,
17
+ )
18
+
19
+ def run_dashboard():
20
+ return subprocess.Popen(
21
+ [
22
+ sys.executable, "-m", "streamlit",
23
+ "run", str(ROOT / "dashboard" / "dshbd.py"),
24
+ "--server.port", "8501",
25
+ ],
26
+ cwd=ROOT,
27
+ )
28
+
29
+ def main():
30
+ api = run_api()
31
+ dashboard = run_dashboard()
32
+
33
+ print("API: http://127.0.0.1:8000/docs")
34
+ print("Dashboard: http://127.0.0.1:8501")
35
+
36
+ try:
37
+ while True:
38
+ api_code = api.poll()
39
+ dash_code = dashboard.poll()
40
+
41
+ if api_code is not None:
42
+ print(f"\nAPI stopped (code={api_code}). Stopping dashboard...")
43
+ dashboard.terminate()
44
+ break
45
+
46
+ if dash_code is not None:
47
+ print(f"\nDashboard stopped (code={dash_code}). Stopping API...")
48
+ api.terminate()
49
+ break
50
+
51
+ time.sleep(0.3)
52
+
53
+ except KeyboardInterrupt:
54
+ print("\nSTOP: arrêt demandé, fermeture propre...")
55
+ api.terminate()
56
+ dashboard.terminate()
57
+
58
+ if __name__ == "__main__":
59
+ main()
app/models.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Any, Dict, Optional
5
+
6
+ from sqlalchemy import (
7
+ BigInteger,
8
+ DateTime,
9
+ Float,
10
+ ForeignKey,
11
+ Index,
12
+ Integer,
13
+ String,
14
+ )
15
+ from sqlalchemy import JSON
16
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
17
+
18
+
19
+ # Fonction utilitaire pour obtenir l'heure actuelle en UTC, utilisée comme valeur par défaut pour les champs created_at
20
+ def utcnow() -> datetime:
21
+ return datetime.now(timezone.utc)
22
+
23
+
24
+ # Base de données SQLAlchemy pour les modèles ORM
25
+ class Base(DeclarativeBase):
26
+ pass
27
+
28
+
29
+ # ---------------------------------------------------------------------
30
+ # Compat SQLite (tests) / Postgres (prod)
31
+ # SQLite n'auto-incrémente correctement que "INTEGER PRIMARY KEY".
32
+ # On garde BIGINT en Postgres, et on traduit en INTEGER pour SQLite.
33
+ # ---------------------------------------------------------------------
34
+ ID_PK = BigInteger().with_variant(Integer, "sqlite")
35
+ ID_FK = BigInteger().with_variant(Integer, "sqlite")
36
+
37
+
38
+ # Modèles SQLAlchemy représentant les tables de la base de données, organisés par schéma (raw, clean, app)
39
+ class RawEmployee(Base):
40
+ __tablename__ = "employees"
41
+ __table_args__ = (
42
+ Index("ix_raw_employees_employee_external_id", "employee_external_id"),
43
+ {"schema": "raw"},
44
+ )
45
+
46
+ id: Mapped[int] = mapped_column(ID_PK, primary_key=True, autoincrement=True)
47
+ employee_external_id: Mapped[int] = mapped_column(Integer, nullable=False, unique=True)
48
+
49
+
50
+ # Les autres champs de la table raw.employees peuvent être ajoutés ici si nécessaire,
51
+ # mais ne sont pas indispensables pour les prédictions basées sur employee_id
52
+ class RawEmployeeSnapshot(Base):
53
+ __tablename__ = "employee_snapshots"
54
+ __table_args__ = ({"schema": "raw"},)
55
+
56
+ id: Mapped[int] = mapped_column(ID_PK, primary_key=True, autoincrement=True)
57
+
58
+
59
+ # Les champs de la table raw.employee_snapshots peuvent être ajoutés ici si nécessaire,
60
+ # mais ne sont pas indispensables pour les prédictions basées sur employee_id
61
+ class RawSurvey(Base):
62
+ __tablename__ = "surveys"
63
+ __table_args__ = ({"schema": "raw"},)
64
+
65
+ id: Mapped[int] = mapped_column(ID_PK, primary_key=True, autoincrement=True)
66
+
67
+
68
+ # Les champs de la table raw.surveys peuvent être ajoutés ici si nécessaire,
69
+ # mais ne sont pas indispensables pour les prédictions basées sur employee_id
70
+ class MLFeaturesEmployee(Base):
71
+ __tablename__ = "ml_features_employees"
72
+ __table_args__ = (
73
+ Index("idx_clean_employee_id_created_at", "employee_id", "created_at"),
74
+ Index("idx_ml_features_target", "a_quitte_l_entreprise"),
75
+ Index("idx_ml_features_target_created_at", "a_quitte_l_entreprise", "created_at"),
76
+ {"schema": "clean"},
77
+ )
78
+
79
+ id: Mapped[int] = mapped_column(ID_PK, primary_key=True, autoincrement=True)
80
+
81
+ employee_id: Mapped[int] = mapped_column(
82
+ ID_FK,
83
+ ForeignKey("raw.employees.id", ondelete="CASCADE"),
84
+ nullable=False,
85
+ )
86
+
87
+ created_at: Mapped[datetime] = mapped_column(
88
+ DateTime(timezone=True),
89
+ default=utcnow,
90
+ nullable=False,
91
+ )
92
+
93
+ employee: Mapped["RawEmployee"] = relationship("RawEmployee")
94
+
95
+ note_evaluation_precedente: Mapped[int] = mapped_column(Integer, nullable=False)
96
+ niveau_hierarchique_poste: Mapped[int] = mapped_column(Integer, nullable=False)
97
+ note_evaluation_actuelle: Mapped[int] = mapped_column(Integer, nullable=False)
98
+
99
+ heures_supplementaires: Mapped[int] = mapped_column(Integer, nullable=False)
100
+ augmentation_salaire_precedente: Mapped[float] = mapped_column(Float, nullable=False)
101
+
102
+ age: Mapped[int] = mapped_column(Integer, nullable=False)
103
+ genre: Mapped[int] = mapped_column(Integer, nullable=False)
104
+
105
+ revenu_mensuel: Mapped[int] = mapped_column(Integer, nullable=False)
106
+ statut_marital: Mapped[str] = mapped_column(String(50), nullable=False)
107
+ departement: Mapped[str] = mapped_column(String(120), nullable=False)
108
+ poste: Mapped[str] = mapped_column(String(120), nullable=False)
109
+
110
+ nombre_experiences_precedentes: Mapped[int] = mapped_column(Integer, nullable=False)
111
+ annee_experience_totale: Mapped[int] = mapped_column(Integer, nullable=False)
112
+ annees_dans_l_entreprise: Mapped[int] = mapped_column(Integer, nullable=False)
113
+ annees_dans_le_poste_actuel: Mapped[int] = mapped_column(Integer, nullable=False)
114
+
115
+ nombre_participation_pee: Mapped[int] = mapped_column(Integer, nullable=False)
116
+ nb_formations_suivies: Mapped[int] = mapped_column(Integer, nullable=False)
117
+ distance_domicile_travail: Mapped[int] = mapped_column(Integer, nullable=False)
118
+
119
+ niveau_education: Mapped[int] = mapped_column(Integer, nullable=False)
120
+ domaine_etude: Mapped[str] = mapped_column(String(120), nullable=False)
121
+ frequence_deplacement: Mapped[int] = mapped_column(Integer, nullable=False)
122
+
123
+ annees_depuis_la_derniere_promotion: Mapped[int] = mapped_column(Integer, nullable=False)
124
+ annees_sous_responsable_actuel: Mapped[int] = mapped_column(Integer, nullable=False)
125
+
126
+ satisfaction_moyenne: Mapped[float] = mapped_column(Float, nullable=False)
127
+ nonlineaire_participation_pee: Mapped[float] = mapped_column(Float, nullable=False)
128
+ ratio_heures_sup_salaire: Mapped[float] = mapped_column(Float, nullable=False)
129
+ nonlinaire_charge_contrainte: Mapped[float] = mapped_column(Float, nullable=False)
130
+ nonlinaire_surmenage_insatisfaction: Mapped[float] = mapped_column(Float, nullable=False)
131
+
132
+ jeune_surcharge: Mapped[int] = mapped_column(Integer, nullable=False)
133
+ anciennete_sans_promotion: Mapped[float] = mapped_column(Float, nullable=False)
134
+ mobilite_carriere: Mapped[float] = mapped_column(Float, nullable=False)
135
+ risque_global: Mapped[float] = mapped_column(Float, nullable=False)
136
+
137
+ a_quitte_l_entreprise: Mapped[int] = mapped_column(Integer, nullable=False)
138
+
139
+
140
+ # Les champs de la table app.prediction_requests peuvent être ajustés en fonction des besoins,
141
+ # mais les champs essentiels pour le suivi des prédictions sont employee_id, payload_json, et created_at
142
+ class PredictionRequest(Base):
143
+ __tablename__ = "prediction_requests"
144
+ __table_args__ = (
145
+ Index("ix_prediction_requests_created_at", "created_at"),
146
+ {"schema": "app"},
147
+ )
148
+
149
+ id: Mapped[int] = mapped_column(ID_PK, primary_key=True, autoincrement=True)
150
+
151
+ employee_id: Mapped[Optional[int]] = mapped_column(
152
+ ID_FK,
153
+ ForeignKey("raw.employees.id", ondelete="SET NULL"),
154
+ nullable=True,
155
+ )
156
+ snapshot_id: Mapped[Optional[int]] = mapped_column(
157
+ ID_FK,
158
+ ForeignKey("raw.employee_snapshots.id", ondelete="SET NULL"),
159
+ nullable=True,
160
+ )
161
+ survey_id: Mapped[Optional[int]] = mapped_column(
162
+ ID_FK,
163
+ ForeignKey("raw.surveys.id", ondelete="SET NULL"),
164
+ nullable=True,
165
+ )
166
+
167
+ payload_json: Mapped[Dict[str, Any]] = mapped_column(JSON, nullable=False)
168
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
169
+
170
+ predictions: Mapped[list["Prediction"]] = relationship(
171
+ back_populates="request",
172
+ cascade="all, delete-orphan",
173
+ )
174
+
175
+ # Relations optionnelles vers les données brutes associées à la prédiction, si disponibles.
176
+ employee: Mapped[Optional["RawEmployee"]] = relationship("RawEmployee", foreign_keys=[employee_id])
177
+ snapshot: Mapped[Optional["RawEmployeeSnapshot"]] = relationship("RawEmployeeSnapshot", foreign_keys=[snapshot_id])
178
+ survey: Mapped[Optional["RawSurvey"]] = relationship("RawSurvey", foreign_keys=[survey_id])
179
+
180
+
181
+ # Les champs de la table app.predictions peuvent être ajustés en fonction des besoins,
182
+ # mais les champs essentiels pour le suivi des prédictions sont request_id, predicted_proba, et created_at
183
+ class Prediction(Base):
184
+ __tablename__ = "predictions"
185
+ __table_args__ = (
186
+ Index("ix_predictions_request_id_created_at", "request_id", "created_at"),
187
+ Index("ix_predictions_created_at", "created_at"),
188
+ Index("ix_predictions_model_version", "model_version"),
189
+ {"schema": "app"},
190
+ )
191
+
192
+ id: Mapped[int] = mapped_column(ID_PK, primary_key=True, autoincrement=True)
193
+
194
+ request_id: Mapped[int] = mapped_column(
195
+ ID_FK,
196
+ ForeignKey("app.prediction_requests.id", ondelete="CASCADE"),
197
+ nullable=False,
198
+ )
199
+
200
+ model_version: Mapped[str] = mapped_column(String(50), nullable=False)
201
+ predicted_class: Mapped[int] = mapped_column(Integer, nullable=False)
202
+ predicted_proba: Mapped[float] = mapped_column(Float, nullable=False)
203
+ threshold_used: Mapped[float] = mapped_column(Float, nullable=False)
204
+ latency_ms: Mapped[Optional[int]] = mapped_column(Integer)
205
+
206
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
207
+
208
+ request: Mapped["PredictionRequest"] = relationship(back_populates="predictions")
app/security.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Header, HTTPException, status
2
+ import os
3
+
4
+
5
+ def verify_api_key(x_api_key: str | None = Header(default=None, alias="X-API-Key")):
6
+ api_key = os.getenv("API_KEY") # lu à chaque requête (robuste)
7
+
8
+ if not api_key:
9
+ raise HTTPException(
10
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
11
+ detail="API_KEY not configured on server",
12
+ )
13
+
14
+ if x_api_key != api_key:
15
+ raise HTTPException(
16
+ status_code=status.HTTP_401_UNAUTHORIZED,
17
+ detail="Invalid or missing API key",
18
+ )
artifacts/threshold.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "threshold": 0.42523789405822754
3
+ }
dashboard/__init__.py ADDED
File without changes
dashboard/dshbd.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import streamlit as st
3
+ import requests
4
+ from typing import Any, Dict, List, Tuple
5
+
6
+ # charge .env quand le dashboard est lancé via VS Code / bouton
7
+ from dotenv import load_dotenv, find_dotenv
8
+ load_dotenv(find_dotenv())
9
+
10
+ st.set_page_config(page_title="TechNova Dashboard", layout="centered")
11
+
12
+ # Ce script Streamlit affiche un dashboard simple pour interagir avec l'API FastAPI et visualiser les prédictions.
13
+ API_BASE = os.getenv("API_BASE", "http://127.0.0.1:8000")
14
+ API_PREDICT_BY_ID = f"{API_BASE}/predict/by-id"
15
+ API_PREDICT_DEBUG = f"{API_BASE}/predict/debug"
16
+ API_LATEST = f"{API_BASE}/predictions/latest"
17
+ API_ROOT = f"{API_BASE}/"
18
+
19
+ # API key (le dashboard est un client HTTP, il envoie seulement le header)
20
+ API_KEY = os.getenv("API_KEY")
21
+ DEFAULT_HEADERS = {"X-API-Key": API_KEY} if API_KEY else {}
22
+
23
+ # La fonction safe_request encapsule les appels à l'API avec une gestion d'erreur simple,
24
+ # affichant un message d'erreur dans le dashboard en cas de problème de connexion ou de timeout.
25
+ from dashboard.feature_schema import FEATURES
26
+
27
+ def safe_request(method: str, url: str, **kwargs):
28
+ try:
29
+ # injection automatique du header X-API-Key
30
+ headers = kwargs.pop("headers", {}) or {}
31
+ merged_headers = {**DEFAULT_HEADERS, **headers}
32
+
33
+ # appel de l'API avec un timeout de 10 secondes
34
+ return requests.request(
35
+ method,
36
+ url,
37
+ timeout=10,
38
+ headers=merged_headers,
39
+ **kwargs
40
+ )
41
+ except requests.RequestException as e:
42
+ st.error(f"Impossible de joindre l'API : {e}")
43
+ return None
44
+
45
+ # La fonction validate_inputs vérifie que les valeurs saisies par l'utilisateur respectent les contraintes définies dans FEATURES,
46
+ # telles que les types de données, les plages de valeurs, et les choix pour les variables catégorielles.
47
+ def validate_inputs(values: Dict[str, Any]) -> Tuple[bool, List[str]]:
48
+ errors: List[str] = []
49
+
50
+ for f in FEATURES:
51
+ v = values.get(f.key)
52
+ if f.required and (v is None or (isinstance(v, str) and v.strip() == "")):
53
+ errors.append(f"{f.label} est requis.")
54
+ continue
55
+ if f.dtype in ("int", "float"):
56
+ if not isinstance(v, (int, float)) or isinstance(v, bool):
57
+ errors.append(f"{f.label} doit être un nombre.")
58
+ continue
59
+ if f.dtype == "int":
60
+ if isinstance(v, float) and not v.is_integer():
61
+ errors.append(f"{f.label} doit être un entier.")
62
+ continue
63
+ if f.min is not None and v < f.min:
64
+ errors.append(f"{f.label} doit être ≥ {f.min}.")
65
+ if f.max is not None and v > f.max:
66
+ errors.append(f"{f.label} doit être ≤ {f.max}.")
67
+ elif f.dtype == "cat":
68
+ if not isinstance(v, str):
69
+ errors.append(f"{f.label} doit être une chaîne.")
70
+ continue
71
+ if f.choices is not None and v not in f.choices:
72
+ errors.append(f"{f.label} doit être dans {f.choices}.")
73
+
74
+ return (len(errors) == 0), errors
75
+
76
+ # Le reste du code Streamlit affiche le dashboard avec deux onglets : un pour faire des prédictions,
77
+ # et un pour visualiser l'historique des prédictions enregistrées dans la base de données via l'API.
78
+ st.title("TechNova – Dashboard")
79
+ st.caption("Interface Streamlit connectée à une API FastAPI et une base")
80
+
81
+ # Cadre gestion de l'API
82
+ with st.expander("Configuration API", expanded=False):
83
+ st.write(f"API utilisée : {API_BASE}")
84
+ st.write(f"API_KEY chargée : {'oui' if API_KEY else 'non'}")
85
+
86
+ c1, c2 = st.columns(2)
87
+ with c1:
88
+ if st.button("Tester l’API"):
89
+ r = safe_request("GET", API_ROOT)
90
+ if r is None:
91
+ st.stop()
92
+ if r.ok:
93
+ st.success("API accessible")
94
+ else:
95
+ st.error(f"Erreur API ({r.status_code})")
96
+ try:
97
+ st.write(r.json())
98
+ except Exception:
99
+ st.write(r.text)
100
+
101
+ with c2:
102
+ st.write("L’URL peut être modifiée via la variable API_BASE")
103
+
104
+ tab_predict, tab_history = st.tabs(["Prédire", "Historique"])
105
+
106
+ # ONGLET PRÉDICTION
107
+ with tab_predict:
108
+ st.subheader("Prédiction")
109
+ # 2 modes de validation : par ID employé (recommandé) ou par features (debug)
110
+ mode = st.radio(
111
+ "Mode",
112
+ ["Par ID employé (prod, clean)", "Par features (debug)"],
113
+ horizontal=True,
114
+ )
115
+
116
+ # En mode ID, l'utilisateur saisit un employee_external_id,
117
+ # et l'API lit les features correspondantes dans clean.ml_features_employees pour faire la prédiction.
118
+ if mode == "Par ID employé (prod, clean)":
119
+ st.caption("L’API lit les features dans clean.ml_features_employees via employee_external_id.")
120
+
121
+ employee_external_id = st.number_input("employee_external_id", min_value=1, step=1, value=1)
122
+ run_pred = st.button("Lancer la prédiction (ID)")
123
+
124
+ if run_pred:
125
+ url = f"{API_PREDICT_BY_ID}/{int(employee_external_id)}"
126
+ response = safe_request("POST", url)
127
+ if response is None:
128
+ st.stop()
129
+ if response.ok:
130
+ result = response.json()
131
+ st.success("Prédiction réalisée")
132
+ st.write("Employé :", result.get("employee_id"))
133
+ st.write("Départ prédit :", result.get("will_leave"))
134
+ st.write("Probabilité :", round(result.get("turnover_probability", 0), 4))
135
+ else:
136
+ st.error(f"Erreur API ({response.status_code})")
137
+ try:
138
+ st.write(response.json())
139
+ except Exception:
140
+ st.write(response.text)
141
+
142
+ else:
143
+ # En mode features, l'utilisateur remplit un formulaire avec toutes les features nécessaires à la prédiction,
144
+ st.caption("Mode debug: envoi d’un payload complet de features à l’API.")
145
+
146
+ # 2 options d'affichage permettent de choisir entre un affichage compact (6 champs par ligne)
147
+ # et un affichage avec les noms techniques des features (keys).
148
+ col1, col2 = st.columns(2)
149
+ with col1:
150
+ compact = st.checkbox("Affichage compact", value=True)
151
+ with col2:
152
+ show_keys = st.checkbox("Afficher les noms techniques", value=False)
153
+
154
+ values_by_key: Dict[str, Any] = {}
155
+
156
+ for idx, f in enumerate(FEATURES):
157
+ label = f.label if not show_keys else f"{f.label} ({f.key})"
158
+
159
+ if f.dtype == "int":
160
+ default = int(f.min) if f.min is not None else 0
161
+ v = st.number_input(
162
+ label,
163
+ min_value=int(f.min) if f.min is not None else None,
164
+ max_value=int(f.max) if f.max is not None else None,
165
+ value=default,
166
+ step=1,
167
+ key=f"feat_{f.key}"
168
+ )
169
+ values_by_key[f.key] = int(v)
170
+
171
+ elif f.dtype == "float":
172
+ default = float(f.min) if f.min is not None else 0.0
173
+ v = st.number_input(
174
+ label,
175
+ min_value=float(f.min) if f.min is not None else None,
176
+ max_value=float(f.max) if f.max is not None else None,
177
+ value=default,
178
+ step=0.1,
179
+ key=f"feat_{f.key}"
180
+ )
181
+ values_by_key[f.key] = float(v)
182
+
183
+ else:
184
+ if f.choices:
185
+ values_by_key[f.key] = st.selectbox(label, f.choices, key=f"feat_{f.key}")
186
+ else:
187
+ values_by_key[f.key] = st.text_input(label, key=f"feat_{f.key}").strip()
188
+
189
+ if compact and (idx + 1) % 6 == 0:
190
+ st.divider()
191
+
192
+ c1, c2 = st.columns(2)
193
+ with c1:
194
+ run_pred = st.button("Lancer la prédiction (features)")
195
+ with c2:
196
+ if st.button("Réinitialiser"):
197
+ st.rerun()
198
+
199
+ # Lors du lancement de la prédiction, les valeurs saisies sont d'abord validées via la fonction validate_inputs,
200
+ if run_pred:
201
+ ok, errors = validate_inputs(values_by_key)
202
+ if not ok:
203
+ st.error("Erreurs dans le formulaire")
204
+ for e in errors:
205
+ st.write(e)
206
+ st.stop()
207
+
208
+ # puis envoyées à l'API via la fonction safe_request, et les résultats sont affichés dans le dashboard.
209
+ response = safe_request("POST", API_PREDICT_DEBUG, json=values_by_key)
210
+ if response is None:
211
+ st.stop()
212
+ if response.ok:
213
+ result = response.json()
214
+ st.success("Prédiction réalisée")
215
+ st.write("Départ prédit :", result.get("will_leave"))
216
+ st.write("Probabilité :", round(result.get("turnover_probability", 0), 4))
217
+ else:
218
+ st.error(f"Erreur API ({response.status_code})")
219
+ try:
220
+ st.write(response.json())
221
+ except Exception:
222
+ st.write(response.text)
223
+
224
+ # ONGLET HISTORIQUE
225
+ with tab_history:
226
+ st.subheader("Historique des prédictions")
227
+
228
+ limit = st.slider("Nombre de lignes", 5, 200, 20)
229
+
230
+ if st.button("Rafraîchir"):
231
+ # L'onglet historique affiche les dernières prédictions enregistrées dans la base de données via l'API,
232
+ # avec un slider pour choisir le nombre de lignes à afficher.
233
+ response = safe_request("GET", API_LATEST, params={"limit": limit})
234
+ if response and response.ok:
235
+ rows = response.json()
236
+ if not rows:
237
+ st.info("Aucune prédiction enregistrée.")
238
+ for row in rows:
239
+ title = f'{row.get("created_at", "")} | proba={row.get("predicted_proba", "")}'
240
+ with st.expander(title):
241
+ st.json(row)
242
+ else:
243
+ st.error("Impossible de récupérer l’historique.")
244
+ if response is not None:
245
+ st.write(response.text)
dashboard/feature_schema.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import List, Optional, Literal
5
+
6
+ @dataclass(frozen=True)
7
+ class Feature:
8
+ key: str
9
+ label: str
10
+ dtype: Literal["int", "float", "cat"]
11
+ required: bool = True
12
+ min: Optional[float] = None
13
+ max: Optional[float] = None
14
+ choices: Optional[List[str]] = None
15
+
16
+ # DB keys
17
+ DB_ID_KEY = "id" # id technique BIGSERIAL
18
+ EMPLOYEE_ID_KEY = "employee_external_id" # id métier stable (SIRH)
19
+ TARGET_KEY = "a_quitte_l_entreprise"
20
+
21
+ # Liste complète des features utilisées par le modèle, avec leurs types et contraintes,
22
+ # qui seront utilisées à la fois pour la validation des inputs dans le dashboard Streamlit,
23
+ # et pour construire les payloads de prédiction envoyés à l'API FastAPI.
24
+ # L'ordre des features doit correspondre à celui attendu par le modèle de prédiction.
25
+ FEATURES: List[Feature] = [
26
+ Feature("note_evaluation_precedente", "Note d’évaluation précédente", "int", min=1, max=5),
27
+ Feature("note_evaluation_actuelle", "Note d’évaluation actuelle", "int", min=1, max=5),
28
+ Feature("niveau_hierarchique_poste", "Niveau hiérarchique du poste", "int", min=1, max=5),
29
+ Feature("heures_supplementaires", "Heures supplémentaires (0/1)", "int", min=0, max=1),
30
+ Feature("augmentation_salaire_precedente", "Augmentation salariale précédente (%)", "float", min=0),
31
+ Feature("age", "Âge", "int", min=16, max=80),
32
+ Feature("genre", "Genre (0 = F, 1 = M)", "int", min=0, max=1),
33
+ Feature("revenu_mensuel", "Revenu mensuel (€)", "int", min=0),
34
+ Feature("statut_marital", "Statut marital", "cat"),
35
+ Feature("niveau_education", "Niveau d’éducation", "int", min=1, max=5),
36
+ Feature("domaine_etude", "Domaine d’étude", "cat"),
37
+ Feature("departement", "Département", "cat"),
38
+ Feature("poste", "Poste occupé", "cat"),
39
+ Feature("nombre_experiences_precedentes", "Expériences précédentes", "int", min=0),
40
+ Feature("annee_experience_totale", "Années d’expérience totale", "int", min=0),
41
+ Feature("annees_dans_l_entreprise", "Ancienneté dans l’entreprise", "int", min=0),
42
+ Feature("annees_dans_le_poste_actuel", "Ancienneté dans le poste", "int", min=0),
43
+ Feature("nombre_participation_pee", "Participations au PEE", "int", min=0),
44
+ Feature("nb_formations_suivies", "Formations suivies", "int", min=0),
45
+ Feature("frequence_deplacement", "Fréquence de déplacement (0–3)", "int", min=0, max=3),
46
+ Feature("annees_depuis_la_derniere_promotion", "Années depuis la dernière promotion", "int", min=0),
47
+ Feature("annees_sous_responsable_actuel", "Années sous le responsable actuel", "int", min=0),
48
+ Feature("distance_domicile_travail", "Distance domicile–travail (km)", "int", min=0),
49
+ Feature("satisfaction_moyenne", "Satisfaction moyenne", "float", min=0, max=5),
50
+ Feature("nonlineaire_participation_pee", "Participation PEE (non linéaire)", "float"),
51
+ Feature("ratio_heures_sup_salaire", "Ratio heures sup / salaire", "float"),
52
+ Feature("nonlinaire_charge_contrainte", "Charge contrainte (non linéaire)", "float"),
53
+ Feature("nonlinaire_surmenage_insatisfaction", "Surmenage & insatisfaction", "float"),
54
+ Feature("jeune_surcharge", "Jeune avec surcharge (0/1)", "int", min=0, max=1),
55
+ Feature("anciennete_sans_promotion", "Ancienneté sans promotion", "float"),
56
+ Feature("mobilite_carriere", "Mobilité de carrière", "float"),
57
+ Feature("risque_global", "Risque global agrégé", "float")
58
+ ]
59
+
60
+ # Ordre des features attendu par le modèle
61
+ MODEL_FEATURE_ORDER: List[str] = [f.key for f in FEATURES]
62
+
63
+ # Colonnes clean utiles en API (sans target)
64
+ DEPLOYMENT_COLUMNS: List[str] = [EMPLOYEE_ID_KEY] + MODEL_FEATURE_ORDER
65
+
66
+ # Colonnes complètes DB clean
67
+ DB_COLUMNS: List[str] = [DB_ID_KEY, EMPLOYEE_ID_KEY] + MODEL_FEATURE_ORDER + [TARGET_KEY]
docs/app.api.html ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Python: module app.api</title>
6
+ </head><body>
7
+
8
+ <table class="heading">
9
+ <tr class="heading-text decor">
10
+ <td class="title">&nbsp;<br><strong class="title"><a href="app.html" class="white">app</a>.api</strong></td>
11
+ <td class="extra"><a href=".">index</a><br><a href="file:c%3A%5Cusers%5Cgonza%5Copenclassroom%5Cprojet_4_classification%5Cprojet_technova_partners%5Capp%5Capi.py">c:\users\gonza\openclassroom\projet_4_classification\projet_technova_partners\app\api.py</a></td></tr></table>
12
+ <p></p>
13
+ <p>
14
+ <table class="section">
15
+ <tr class="decor functions-decor heading-text">
16
+ <td class="section-title" colspan=3>&nbsp;<br><strong class="bigsection">Functions</strong></td></tr>
17
+
18
+ <tr><td class="decor functions-decor"><span class="code">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></td><td>&nbsp;</td>
19
+ <td class="singlecolumn"><dl><dt><a name="-get_service"><strong>get_service</strong></a>() -&gt; 'TechNovaService'</dt><dd><span class="code">#&nbsp;Dependency&nbsp;pour&nbsp;obtenir&nbsp;une&nbsp;instance&nbsp;du&nbsp;service&nbsp;métier&nbsp;dans&nbsp;les&nbsp;endpoints&nbsp;FastAPI</span></dd></dl>
20
+ <dl><dt><a name="-health"><strong>health</strong></a>()</dt><dd><span class="code">#&nbsp;endpoint&nbsp;de&nbsp;santé&nbsp;pour&nbsp;monitoring</span></dd></dl>
21
+ <dl><dt><a name="-latest_predictions"><strong>latest_predictions</strong></a>(
22
+ limit: 'int' = 20,
23
+ db: 'Session' = Depends(dependency=&lt;function get_db at 0x00000224C1C0E7A0&gt;, use_cache=True, scope=None)
24
+ )</dt><dd><span class="code">#endpoint&nbsp;pour&nbsp;récupérer&nbsp;les&nbsp;dernières&nbsp;prédictions&nbsp;stockées&nbsp;en&nbsp;base,&nbsp;avec&nbsp;pagination</span></dd></dl>
25
+ <dl><dt><a name="-perf_counter"><strong>perf_counter</strong></a>()</dt><dd><span class="code"><a href="#-perf_counter">perf_counter</a>()&nbsp;-&gt;&nbsp;float<br>
26
+ &nbsp;<br>
27
+ Performance&nbsp;counter&nbsp;for&nbsp;benchmarking.</span></dd></dl>
28
+ <dl><dt><a name="-predict_by_employee_id"><strong>predict_by_employee_id</strong></a>(
29
+ employee_id: 'int',
30
+ db: 'Session' = Depends(dependency=&lt;function get_db at 0x00000224C1C0E7A0&gt;, use_cache=True, scope=None),
31
+ service: 'TechNovaService' = Depends(dependency=&lt;function get_service at 0x00000224C1C5EDE0&gt;, use_cache=True, scope=None)
32
+ ) -&gt; 'ModelResponse'</dt><dd><span class="code">#prédiction&nbsp;via&nbsp;ID&nbsp;employé&nbsp;—&nbsp;mode&nbsp;recommandé&nbsp;pour&nbsp;la&nbsp;production</span></dd></dl>
33
+ <dl><dt><a name="-predict_by_features"><strong>predict_by_features</strong></a>(
34
+ payload: 'ModelRequest',
35
+ db: 'Session' = Depends(dependency=&lt;function get_db at 0x00000224C1C0E7A0&gt;, use_cache=True, scope=None),
36
+ service: 'TechNovaService' = Depends(dependency=&lt;function get_service at 0x00000224C1C5EDE0&gt;, use_cache=True, scope=None)
37
+ ) -&gt; 'ModelResponse'</dt><dd><span class="code">#endpoint&nbsp;de&nbsp;prédiction&nbsp;via&nbsp;features&nbsp;brutes&nbsp;—&nbsp;utile&nbsp;pour&nbsp;tests&nbsp;et&nbsp;debug,&nbsp;mais&nbsp;pas&nbsp;recommandé&nbsp;pour&nbsp;la&nbsp;production</span></dd></dl>
38
+ <dl><dt><a name="-root"><strong>root</strong></a>()</dt><dd><span class="code">#accessible&nbsp;via&nbsp;"/"&nbsp;pour&nbsp;rediriger&nbsp;vers&nbsp;la&nbsp;doc&nbsp;interactive,&nbsp;mais&nbsp;pas&nbsp;dans&nbsp;la&nbsp;doc&nbsp;elle-même</span></dd></dl>
39
+ </td></tr></table><p>
40
+ <table class="section">
41
+ <tr class="decor data-decor heading-text">
42
+ <td class="section-title" colspan=3>&nbsp;<br><strong class="bigsection">Data</strong></td></tr>
43
+
44
+ <tr><td class="decor data-decor"><span class="code">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></td><td>&nbsp;</td>
45
+ <td class="singlecolumn"><strong>MODEL_VERSION</strong> = 'xgb_v1'<br>
46
+ <strong>app</strong> = &lt;fastapi.applications.FastAPI object&gt;</td></tr></table>
47
+ </body></html>
docs/app.models.html ADDED
@@ -0,0 +1,712 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Python: module app.models</title>
6
+ </head><body>
7
+
8
+ <table class="heading">
9
+ <tr class="heading-text decor">
10
+ <td class="title">&nbsp;<br><strong class="title"><a href="app.html" class="white">app</a>.models</strong></td>
11
+ <td class="extra"><a href=".">index</a><br><a href="file:c%3A%5Cusers%5Cgonza%5Copenclassroom%5Cprojet_4_classification%5Cprojet_technova_partners%5Capp%5Cmodels.py">c:\users\gonza\openclassroom\projet_4_classification\projet_technova_partners\app\models.py</a></td></tr></table>
12
+ <p></p>
13
+ <p>
14
+ <table class="section">
15
+ <tr class="decor index-decor heading-text">
16
+ <td class="section-title" colspan=3>&nbsp;<br><strong class="bigsection">Classes</strong></td></tr>
17
+
18
+ <tr><td class="decor index-decor"><span class="code">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></td><td>&nbsp;</td>
19
+ <td class="singlecolumn"><dl>
20
+ <dt class="heading-text"><a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>(<a href="sqlalchemy.inspection.html#Inspectable">sqlalchemy.inspection.Inspectable</a>)
21
+ </dt><dd>
22
+ <dl>
23
+ <dt class="heading-text"><a href="app.models.html#Base">Base</a>
24
+ </dt><dd>
25
+ <dl>
26
+ <dt class="heading-text"><a href="app.models.html#MLFeaturesEmployee">MLFeaturesEmployee</a>
27
+ </dt><dt class="heading-text"><a href="app.models.html#Prediction">Prediction</a>
28
+ </dt><dt class="heading-text"><a href="app.models.html#PredictionRequest">PredictionRequest</a>
29
+ </dt><dt class="heading-text"><a href="app.models.html#RawEmployee">RawEmployee</a>
30
+ </dt><dt class="heading-text"><a href="app.models.html#RawEmployeeSnapshot">RawEmployeeSnapshot</a>
31
+ </dt><dt class="heading-text"><a href="app.models.html#RawSurvey">RawSurvey</a>
32
+ </dt></dl>
33
+ </dd>
34
+ </dl>
35
+ </dd>
36
+ </dl>
37
+ <p>
38
+ <table class="section">
39
+ <tr class="decor title-decor heading-text">
40
+ <td class="section-title" colspan=3>&nbsp;<br><a name="Base">class <strong>Base</strong></a>(<a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>)</td></tr>
41
+
42
+ <tr><td class="decor title-decor" rowspan=2><span class="code">&nbsp;&nbsp;&nbsp;</span></td>
43
+ <td class="decor title-decor" colspan=2><span class="code"><a href="#Base">Base</a>(**kwargs:&nbsp;'Any')&nbsp;-&amp;gt;&nbsp;'None'<br>
44
+ &nbsp;<br>
45
+ #&nbsp;<a href="#Base">Base</a>&nbsp;de&nbsp;données&nbsp;SQLAlchemy&nbsp;pour&nbsp;les&nbsp;modèles&nbsp;ORM<br>&nbsp;</span></td></tr>
46
+ <tr><td>&nbsp;</td>
47
+ <td class="singlecolumn"><dl><dt>Method resolution order:</dt>
48
+ <dd><a href="app.models.html#Base">Base</a></dd>
49
+ <dd><a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a></dd>
50
+ <dd><a href="sqlalchemy.inspection.html#Inspectable">sqlalchemy.inspection.Inspectable</a></dd>
51
+ <dd><a href="typing.html#Generic">typing.Generic</a></dd>
52
+ <dd><a href="builtins.html#object">builtins.object</a></dd>
53
+ </dl>
54
+ <hr>
55
+ Methods defined here:<br>
56
+ <dl><dt><a name="Base-__init__"><strong>__init__</strong></a>(self: 'Any', **kwargs: 'Any') -&gt; 'None'<span class="grey"><span class="heading-text"> from sqlalchemy.orm.decl_base</span></span></dt><dd><span class="code">A&nbsp;simple&nbsp;constructor&nbsp;that&nbsp;allows&nbsp;initialization&nbsp;from&nbsp;kwargs.<br>
57
+ &nbsp;<br>
58
+ Sets&nbsp;attributes&nbsp;on&nbsp;the&nbsp;constructed&nbsp;instance&nbsp;using&nbsp;the&nbsp;names&nbsp;and<br>
59
+ values&nbsp;in&nbsp;``kwargs``.<br>
60
+ &nbsp;<br>
61
+ Only&nbsp;keys&nbsp;that&nbsp;are&nbsp;present&nbsp;as<br>
62
+ attributes&nbsp;of&nbsp;the&nbsp;instance's&nbsp;class&nbsp;are&nbsp;allowed.&nbsp;These&nbsp;could&nbsp;be,<br>
63
+ for&nbsp;example,&nbsp;any&nbsp;mapped&nbsp;columns&nbsp;or&nbsp;relationships.</span></dd></dl>
64
+
65
+ <hr>
66
+ Data and other attributes defined here:<br>
67
+ <dl><dt><strong>__annotations__</strong> = {}</dl>
68
+
69
+ <dl><dt><strong>__parameters__</strong> = ()</dl>
70
+
71
+ <dl><dt><strong>metadata</strong> = MetaData()</dl>
72
+
73
+ <dl><dt><strong>registry</strong> = &lt;sqlalchemy.orm.decl_api.registry object&gt;</dl>
74
+
75
+ <hr>
76
+ Class methods inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
77
+ <dl><dt><a name="Base-__init_subclass__"><strong>__init_subclass__</strong></a>(**kw: 'Any') -&gt; 'None'</dt><dd><span class="code">Function&nbsp;to&nbsp;initialize&nbsp;subclasses.</span></dd></dl>
78
+
79
+ <hr>
80
+ Data descriptors inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
81
+ <dl><dt><strong>__dict__</strong></dt>
82
+ <dd><span class="code">dictionary&nbsp;for&nbsp;instance&nbsp;variables</span></dd>
83
+ </dl>
84
+ <dl><dt><strong>__weakref__</strong></dt>
85
+ <dd><span class="code">list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object</span></dd>
86
+ </dl>
87
+ <hr>
88
+ Data and other attributes inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
89
+ <dl><dt><strong>__orig_bases__</strong> = (sqlalchemy.inspection.Inspectable[sqlalchemy.orm.state.InstanceState[typing.Any]],)</dl>
90
+
91
+ <hr>
92
+ Class methods inherited from <a href="typing.html#Generic">typing.Generic</a>:<br>
93
+ <dl><dt><a name="Base-__class_getitem__"><strong>__class_getitem__</strong></a>(...)</dt><dd><span class="code">Parameterizes&nbsp;a&nbsp;generic&nbsp;class.<br>
94
+ &nbsp;<br>
95
+ At&nbsp;least,&nbsp;parameterizing&nbsp;a&nbsp;generic&nbsp;class&nbsp;is&nbsp;the&nbsp;*main*&nbsp;thing&nbsp;this<br>
96
+ method&nbsp;does.&nbsp;For&nbsp;example,&nbsp;for&nbsp;some&nbsp;generic&nbsp;class&nbsp;`Foo`,&nbsp;this&nbsp;is&nbsp;called<br>
97
+ when&nbsp;we&nbsp;do&nbsp;`Foo[int]`&nbsp;-&nbsp;there,&nbsp;with&nbsp;`cls=Foo`&nbsp;and&nbsp;`params=int`.<br>
98
+ &nbsp;<br>
99
+ However,&nbsp;note&nbsp;that&nbsp;this&nbsp;method&nbsp;is&nbsp;also&nbsp;called&nbsp;when&nbsp;defining&nbsp;generic<br>
100
+ classes&nbsp;in&nbsp;the&nbsp;first&nbsp;place&nbsp;with&nbsp;`class&nbsp;Foo[T]:&nbsp;...`.</span></dd></dl>
101
+
102
+ </td></tr></table> <p>
103
+ <table class="section">
104
+ <tr class="decor title-decor heading-text">
105
+ <td class="section-title" colspan=3>&nbsp;<br><a name="MLFeaturesEmployee">class <strong>MLFeaturesEmployee</strong></a>(<a href="app.models.html#Base">Base</a>)</td></tr>
106
+
107
+ <tr><td class="decor title-decor" rowspan=2><span class="code">&nbsp;&nbsp;&nbsp;</span></td>
108
+ <td class="decor title-decor" colspan=2><span class="code"><a href="#MLFeaturesEmployee">MLFeaturesEmployee</a>(**kwargs)<br>
109
+ &nbsp;<br>
110
+ #&nbsp;Les&nbsp;champs&nbsp;de&nbsp;la&nbsp;table&nbsp;raw.surveys&nbsp;peuvent&nbsp;être&nbsp;ajoutés&nbsp;ici&nbsp;si&nbsp;nécessaire,<br>
111
+ #&nbsp;mais&nbsp;ne&nbsp;sont&nbsp;pas&nbsp;indispensables&nbsp;pour&nbsp;les&nbsp;prédictions&nbsp;basées&nbsp;sur&nbsp;employee_id<br>&nbsp;</span></td></tr>
112
+ <tr><td>&nbsp;</td>
113
+ <td class="singlecolumn"><dl><dt>Method resolution order:</dt>
114
+ <dd><a href="app.models.html#MLFeaturesEmployee">MLFeaturesEmployee</a></dd>
115
+ <dd><a href="app.models.html#Base">Base</a></dd>
116
+ <dd><a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a></dd>
117
+ <dd><a href="sqlalchemy.inspection.html#Inspectable">sqlalchemy.inspection.Inspectable</a></dd>
118
+ <dd><a href="typing.html#Generic">typing.Generic</a></dd>
119
+ <dd><a href="builtins.html#object">builtins.object</a></dd>
120
+ </dl>
121
+ <hr>
122
+ Methods defined here:<br>
123
+ <dl><dt><a name="MLFeaturesEmployee-__init__"><strong>__init__</strong></a>(self, **kwargs)<span class="grey"><span class="heading-text"> from sqlalchemy.orm.instrumentation</span></span></dt><dd><span class="code">A&nbsp;simple&nbsp;constructor&nbsp;that&nbsp;allows&nbsp;initialization&nbsp;from&nbsp;kwargs.<br>
124
+ &nbsp;<br>
125
+ Sets&nbsp;attributes&nbsp;on&nbsp;the&nbsp;constructed&nbsp;instance&nbsp;using&nbsp;the&nbsp;names&nbsp;and<br>
126
+ values&nbsp;in&nbsp;``kwargs``.<br>
127
+ &nbsp;<br>
128
+ Only&nbsp;keys&nbsp;that&nbsp;are&nbsp;present&nbsp;as<br>
129
+ attributes&nbsp;of&nbsp;the&nbsp;instance's&nbsp;class&nbsp;are&nbsp;allowed.&nbsp;These&nbsp;could&nbsp;be,<br>
130
+ for&nbsp;example,&nbsp;any&nbsp;mapped&nbsp;columns&nbsp;or&nbsp;relationships.</span></dd></dl>
131
+
132
+ <hr>
133
+ Data descriptors defined here:<br>
134
+ <dl><dt><strong>a_quitte_l_entreprise</strong></dt>
135
+ </dl>
136
+ <dl><dt><strong>age</strong></dt>
137
+ </dl>
138
+ <dl><dt><strong>anciennete_sans_promotion</strong></dt>
139
+ </dl>
140
+ <dl><dt><strong>annee_experience_totale</strong></dt>
141
+ </dl>
142
+ <dl><dt><strong>annees_dans_l_entreprise</strong></dt>
143
+ </dl>
144
+ <dl><dt><strong>annees_dans_le_poste_actuel</strong></dt>
145
+ </dl>
146
+ <dl><dt><strong>annees_depuis_la_derniere_promotion</strong></dt>
147
+ </dl>
148
+ <dl><dt><strong>annees_sous_responsable_actuel</strong></dt>
149
+ </dl>
150
+ <dl><dt><strong>augmentation_salaire_precedente</strong></dt>
151
+ </dl>
152
+ <dl><dt><strong>created_at</strong></dt>
153
+ </dl>
154
+ <dl><dt><strong>departement</strong></dt>
155
+ </dl>
156
+ <dl><dt><strong>distance_domicile_travail</strong></dt>
157
+ </dl>
158
+ <dl><dt><strong>domaine_etude</strong></dt>
159
+ </dl>
160
+ <dl><dt><strong>employee</strong></dt>
161
+ </dl>
162
+ <dl><dt><strong>employee_id</strong></dt>
163
+ </dl>
164
+ <dl><dt><strong>frequence_deplacement</strong></dt>
165
+ </dl>
166
+ <dl><dt><strong>genre</strong></dt>
167
+ </dl>
168
+ <dl><dt><strong>heures_supplementaires</strong></dt>
169
+ </dl>
170
+ <dl><dt><strong>id</strong></dt>
171
+ </dl>
172
+ <dl><dt><strong>jeune_surcharge</strong></dt>
173
+ </dl>
174
+ <dl><dt><strong>mobilite_carriere</strong></dt>
175
+ </dl>
176
+ <dl><dt><strong>nb_formations_suivies</strong></dt>
177
+ </dl>
178
+ <dl><dt><strong>niveau_education</strong></dt>
179
+ </dl>
180
+ <dl><dt><strong>niveau_hierarchique_poste</strong></dt>
181
+ </dl>
182
+ <dl><dt><strong>nombre_experiences_precedentes</strong></dt>
183
+ </dl>
184
+ <dl><dt><strong>nombre_participation_pee</strong></dt>
185
+ </dl>
186
+ <dl><dt><strong>nonlinaire_charge_contrainte</strong></dt>
187
+ </dl>
188
+ <dl><dt><strong>nonlinaire_surmenage_insatisfaction</strong></dt>
189
+ </dl>
190
+ <dl><dt><strong>nonlineaire_participation_pee</strong></dt>
191
+ </dl>
192
+ <dl><dt><strong>note_evaluation_actuelle</strong></dt>
193
+ </dl>
194
+ <dl><dt><strong>note_evaluation_precedente</strong></dt>
195
+ </dl>
196
+ <dl><dt><strong>poste</strong></dt>
197
+ </dl>
198
+ <dl><dt><strong>ratio_heures_sup_salaire</strong></dt>
199
+ </dl>
200
+ <dl><dt><strong>revenu_mensuel</strong></dt>
201
+ </dl>
202
+ <dl><dt><strong>risque_global</strong></dt>
203
+ </dl>
204
+ <dl><dt><strong>satisfaction_moyenne</strong></dt>
205
+ </dl>
206
+ <dl><dt><strong>statut_marital</strong></dt>
207
+ </dl>
208
+ <hr>
209
+ Data and other attributes defined here:<br>
210
+ <dl><dt><strong>__annotations__</strong> = {'a_quitte_l_entreprise': 'Mapped[int]', 'age': 'Mapped[int]', 'anciennete_sans_promotion': 'Mapped[float]', 'annee_experience_totale': 'Mapped[int]', 'annees_dans_l_entreprise': 'Mapped[int]', 'annees_dans_le_poste_actuel': 'Mapped[int]', 'annees_depuis_la_derniere_promotion': 'Mapped[int]', 'annees_sous_responsable_actuel': 'Mapped[int]', 'augmentation_salaire_precedente': 'Mapped[float]', 'created_at': 'Mapped[datetime]', ...}</dl>
211
+
212
+ <dl><dt><strong>__mapper__</strong> = &lt;Mapper at 0x28b5d075310; MLFeaturesEmployee&gt;</dl>
213
+
214
+ <dl><dt><strong>__parameters__</strong> = ()</dl>
215
+
216
+ <dl><dt><strong>__table__</strong> = Table('ml_features_employees', MetaData(), Colum...ures_employees&gt;, nullable=False), schema='clean')</dl>
217
+
218
+ <dl><dt><strong>__table_args__</strong> = (Index('idx_clean_employee_id_created_at', Column...efault(&lt;function utcnow at 0x0000028B5D04ACA0&gt;))), Index('idx_ml_features_target', Column('a_quitte..., table=&lt;ml_features_employees&gt;, nullable=False)), Index('idx_ml_features_target_created_at', Colum...efault(&lt;function utcnow at 0x0000028B5D04ACA0&gt;))), {'schema': 'clean'})</dl>
219
+
220
+ <dl><dt><strong>__tablename__</strong> = 'ml_features_employees'</dl>
221
+
222
+ <hr>
223
+ Data and other attributes inherited from <a href="app.models.html#Base">Base</a>:<br>
224
+ <dl><dt><strong>metadata</strong> = MetaData()</dl>
225
+
226
+ <dl><dt><strong>registry</strong> = &lt;sqlalchemy.orm.decl_api.registry object&gt;</dl>
227
+
228
+ <hr>
229
+ Class methods inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
230
+ <dl><dt><a name="MLFeaturesEmployee-__init_subclass__"><strong>__init_subclass__</strong></a>(**kw: 'Any') -&gt; 'None'</dt><dd><span class="code">Function&nbsp;to&nbsp;initialize&nbsp;subclasses.</span></dd></dl>
231
+
232
+ <hr>
233
+ Data descriptors inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
234
+ <dl><dt><strong>__dict__</strong></dt>
235
+ <dd><span class="code">dictionary&nbsp;for&nbsp;instance&nbsp;variables</span></dd>
236
+ </dl>
237
+ <dl><dt><strong>__weakref__</strong></dt>
238
+ <dd><span class="code">list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object</span></dd>
239
+ </dl>
240
+ <hr>
241
+ Data and other attributes inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
242
+ <dl><dt><strong>__orig_bases__</strong> = (sqlalchemy.inspection.Inspectable[sqlalchemy.orm.state.InstanceState[typing.Any]],)</dl>
243
+
244
+ <hr>
245
+ Class methods inherited from <a href="typing.html#Generic">typing.Generic</a>:<br>
246
+ <dl><dt><a name="MLFeaturesEmployee-__class_getitem__"><strong>__class_getitem__</strong></a>(...)</dt><dd><span class="code">Parameterizes&nbsp;a&nbsp;generic&nbsp;class.<br>
247
+ &nbsp;<br>
248
+ At&nbsp;least,&nbsp;parameterizing&nbsp;a&nbsp;generic&nbsp;class&nbsp;is&nbsp;the&nbsp;*main*&nbsp;thing&nbsp;this<br>
249
+ method&nbsp;does.&nbsp;For&nbsp;example,&nbsp;for&nbsp;some&nbsp;generic&nbsp;class&nbsp;`Foo`,&nbsp;this&nbsp;is&nbsp;called<br>
250
+ when&nbsp;we&nbsp;do&nbsp;`Foo[int]`&nbsp;-&nbsp;there,&nbsp;with&nbsp;`cls=Foo`&nbsp;and&nbsp;`params=int`.<br>
251
+ &nbsp;<br>
252
+ However,&nbsp;note&nbsp;that&nbsp;this&nbsp;method&nbsp;is&nbsp;also&nbsp;called&nbsp;when&nbsp;defining&nbsp;generic<br>
253
+ classes&nbsp;in&nbsp;the&nbsp;first&nbsp;place&nbsp;with&nbsp;`class&nbsp;Foo[T]:&nbsp;...`.</span></dd></dl>
254
+
255
+ </td></tr></table> <p>
256
+ <table class="section">
257
+ <tr class="decor title-decor heading-text">
258
+ <td class="section-title" colspan=3>&nbsp;<br><a name="Prediction">class <strong>Prediction</strong></a>(<a href="app.models.html#Base">Base</a>)</td></tr>
259
+
260
+ <tr><td class="decor title-decor" rowspan=2><span class="code">&nbsp;&nbsp;&nbsp;</span></td>
261
+ <td class="decor title-decor" colspan=2><span class="code"><a href="#Prediction">Prediction</a>(**kwargs)<br>
262
+ &nbsp;<br>
263
+ #&nbsp;Les&nbsp;champs&nbsp;de&nbsp;la&nbsp;table&nbsp;app.predictions&nbsp;peuvent&nbsp;être&nbsp;ajustés&nbsp;en&nbsp;fonction&nbsp;des&nbsp;besoins,<br>
264
+ #&nbsp;mais&nbsp;les&nbsp;champs&nbsp;essentiels&nbsp;pour&nbsp;le&nbsp;suivi&nbsp;des&nbsp;prédictions&nbsp;sont&nbsp;request_id,&nbsp;predicted_proba,&nbsp;et&nbsp;created_at<br>&nbsp;</span></td></tr>
265
+ <tr><td>&nbsp;</td>
266
+ <td class="singlecolumn"><dl><dt>Method resolution order:</dt>
267
+ <dd><a href="app.models.html#Prediction">Prediction</a></dd>
268
+ <dd><a href="app.models.html#Base">Base</a></dd>
269
+ <dd><a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a></dd>
270
+ <dd><a href="sqlalchemy.inspection.html#Inspectable">sqlalchemy.inspection.Inspectable</a></dd>
271
+ <dd><a href="typing.html#Generic">typing.Generic</a></dd>
272
+ <dd><a href="builtins.html#object">builtins.object</a></dd>
273
+ </dl>
274
+ <hr>
275
+ Methods defined here:<br>
276
+ <dl><dt><a name="Prediction-__init__"><strong>__init__</strong></a>(self, **kwargs)<span class="grey"><span class="heading-text"> from sqlalchemy.orm.instrumentation</span></span></dt><dd><span class="code">A&nbsp;simple&nbsp;constructor&nbsp;that&nbsp;allows&nbsp;initialization&nbsp;from&nbsp;kwargs.<br>
277
+ &nbsp;<br>
278
+ Sets&nbsp;attributes&nbsp;on&nbsp;the&nbsp;constructed&nbsp;instance&nbsp;using&nbsp;the&nbsp;names&nbsp;and<br>
279
+ values&nbsp;in&nbsp;``kwargs``.<br>
280
+ &nbsp;<br>
281
+ Only&nbsp;keys&nbsp;that&nbsp;are&nbsp;present&nbsp;as<br>
282
+ attributes&nbsp;of&nbsp;the&nbsp;instance's&nbsp;class&nbsp;are&nbsp;allowed.&nbsp;These&nbsp;could&nbsp;be,<br>
283
+ for&nbsp;example,&nbsp;any&nbsp;mapped&nbsp;columns&nbsp;or&nbsp;relationships.</span></dd></dl>
284
+
285
+ <hr>
286
+ Data descriptors defined here:<br>
287
+ <dl><dt><strong>created_at</strong></dt>
288
+ </dl>
289
+ <dl><dt><strong>id</strong></dt>
290
+ </dl>
291
+ <dl><dt><strong>latency_ms</strong></dt>
292
+ </dl>
293
+ <dl><dt><strong>model_version</strong></dt>
294
+ </dl>
295
+ <dl><dt><strong>predicted_class</strong></dt>
296
+ </dl>
297
+ <dl><dt><strong>predicted_proba</strong></dt>
298
+ </dl>
299
+ <dl><dt><strong>request</strong></dt>
300
+ </dl>
301
+ <dl><dt><strong>request_id</strong></dt>
302
+ </dl>
303
+ <dl><dt><strong>threshold_used</strong></dt>
304
+ </dl>
305
+ <hr>
306
+ Data and other attributes defined here:<br>
307
+ <dl><dt><strong>__annotations__</strong> = {'created_at': 'Mapped[datetime]', 'id': 'Mapped[int]', 'latency_ms': 'Mapped[Optional[int]]', 'model_version': 'Mapped[str]', 'predicted_class': 'Mapped[int]', 'predicted_proba': 'Mapped[float]', 'request': "Mapped['PredictionRequest']", 'request_id': 'Mapped[int]', 'threshold_used': 'Mapped[float]'}</dl>
308
+
309
+ <dl><dt><strong>__mapper__</strong> = &lt;Mapper at 0x28b5d077250; Prediction&gt;</dl>
310
+
311
+ <dl><dt><strong>__parameters__</strong> = ()</dl>
312
+
313
+ <dl><dt><strong>__table__</strong> = Table('predictions', MetaData(), Column('id', Bi...on utcnow at 0x0000028B5D0A0900&gt;)), schema='app')</dl>
314
+
315
+ <dl><dt><strong>__table_args__</strong> = (Index('ix_predictions_request_id_created_at', Co...efault(&lt;function utcnow at 0x0000028B5D0A0900&gt;))), Index('ix_predictions_created_at', Column('creat...efault(&lt;function utcnow at 0x0000028B5D0A0900&gt;))), Index('ix_predictions_model_version', Column('mo...length=50), table=&lt;predictions&gt;, nullable=False)), {'schema': 'app'})</dl>
316
+
317
+ <dl><dt><strong>__tablename__</strong> = 'predictions'</dl>
318
+
319
+ <hr>
320
+ Data and other attributes inherited from <a href="app.models.html#Base">Base</a>:<br>
321
+ <dl><dt><strong>metadata</strong> = MetaData()</dl>
322
+
323
+ <dl><dt><strong>registry</strong> = &lt;sqlalchemy.orm.decl_api.registry object&gt;</dl>
324
+
325
+ <hr>
326
+ Class methods inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
327
+ <dl><dt><a name="Prediction-__init_subclass__"><strong>__init_subclass__</strong></a>(**kw: 'Any') -&gt; 'None'</dt><dd><span class="code">Function&nbsp;to&nbsp;initialize&nbsp;subclasses.</span></dd></dl>
328
+
329
+ <hr>
330
+ Data descriptors inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
331
+ <dl><dt><strong>__dict__</strong></dt>
332
+ <dd><span class="code">dictionary&nbsp;for&nbsp;instance&nbsp;variables</span></dd>
333
+ </dl>
334
+ <dl><dt><strong>__weakref__</strong></dt>
335
+ <dd><span class="code">list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object</span></dd>
336
+ </dl>
337
+ <hr>
338
+ Data and other attributes inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
339
+ <dl><dt><strong>__orig_bases__</strong> = (sqlalchemy.inspection.Inspectable[sqlalchemy.orm.state.InstanceState[typing.Any]],)</dl>
340
+
341
+ <hr>
342
+ Class methods inherited from <a href="typing.html#Generic">typing.Generic</a>:<br>
343
+ <dl><dt><a name="Prediction-__class_getitem__"><strong>__class_getitem__</strong></a>(...)</dt><dd><span class="code">Parameterizes&nbsp;a&nbsp;generic&nbsp;class.<br>
344
+ &nbsp;<br>
345
+ At&nbsp;least,&nbsp;parameterizing&nbsp;a&nbsp;generic&nbsp;class&nbsp;is&nbsp;the&nbsp;*main*&nbsp;thing&nbsp;this<br>
346
+ method&nbsp;does.&nbsp;For&nbsp;example,&nbsp;for&nbsp;some&nbsp;generic&nbsp;class&nbsp;`Foo`,&nbsp;this&nbsp;is&nbsp;called<br>
347
+ when&nbsp;we&nbsp;do&nbsp;`Foo[int]`&nbsp;-&nbsp;there,&nbsp;with&nbsp;`cls=Foo`&nbsp;and&nbsp;`params=int`.<br>
348
+ &nbsp;<br>
349
+ However,&nbsp;note&nbsp;that&nbsp;this&nbsp;method&nbsp;is&nbsp;also&nbsp;called&nbsp;when&nbsp;defining&nbsp;generic<br>
350
+ classes&nbsp;in&nbsp;the&nbsp;first&nbsp;place&nbsp;with&nbsp;`class&nbsp;Foo[T]:&nbsp;...`.</span></dd></dl>
351
+
352
+ </td></tr></table> <p>
353
+ <table class="section">
354
+ <tr class="decor title-decor heading-text">
355
+ <td class="section-title" colspan=3>&nbsp;<br><a name="PredictionRequest">class <strong>PredictionRequest</strong></a>(<a href="app.models.html#Base">Base</a>)</td></tr>
356
+
357
+ <tr><td class="decor title-decor" rowspan=2><span class="code">&nbsp;&nbsp;&nbsp;</span></td>
358
+ <td class="decor title-decor" colspan=2><span class="code"><a href="#PredictionRequest">PredictionRequest</a>(**kwargs)<br>
359
+ &nbsp;<br>
360
+ #&nbsp;Les&nbsp;champs&nbsp;de&nbsp;la&nbsp;table&nbsp;app.prediction_requests&nbsp;peuvent&nbsp;être&nbsp;ajustés&nbsp;en&nbsp;fonction&nbsp;des&nbsp;besoins,<br>
361
+ #&nbsp;mais&nbsp;les&nbsp;champs&nbsp;essentiels&nbsp;pour&nbsp;le&nbsp;suivi&nbsp;des&nbsp;prédictions&nbsp;sont&nbsp;employee_id,&nbsp;payload_json,&nbsp;et&nbsp;created_at<br>&nbsp;</span></td></tr>
362
+ <tr><td>&nbsp;</td>
363
+ <td class="singlecolumn"><dl><dt>Method resolution order:</dt>
364
+ <dd><a href="app.models.html#PredictionRequest">PredictionRequest</a></dd>
365
+ <dd><a href="app.models.html#Base">Base</a></dd>
366
+ <dd><a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a></dd>
367
+ <dd><a href="sqlalchemy.inspection.html#Inspectable">sqlalchemy.inspection.Inspectable</a></dd>
368
+ <dd><a href="typing.html#Generic">typing.Generic</a></dd>
369
+ <dd><a href="builtins.html#object">builtins.object</a></dd>
370
+ </dl>
371
+ <hr>
372
+ Methods defined here:<br>
373
+ <dl><dt><a name="PredictionRequest-__init__"><strong>__init__</strong></a>(self, **kwargs)<span class="grey"><span class="heading-text"> from sqlalchemy.orm.instrumentation</span></span></dt><dd><span class="code">A&nbsp;simple&nbsp;constructor&nbsp;that&nbsp;allows&nbsp;initialization&nbsp;from&nbsp;kwargs.<br>
374
+ &nbsp;<br>
375
+ Sets&nbsp;attributes&nbsp;on&nbsp;the&nbsp;constructed&nbsp;instance&nbsp;using&nbsp;the&nbsp;names&nbsp;and<br>
376
+ values&nbsp;in&nbsp;``kwargs``.<br>
377
+ &nbsp;<br>
378
+ Only&nbsp;keys&nbsp;that&nbsp;are&nbsp;present&nbsp;as<br>
379
+ attributes&nbsp;of&nbsp;the&nbsp;instance's&nbsp;class&nbsp;are&nbsp;allowed.&nbsp;These&nbsp;could&nbsp;be,<br>
380
+ for&nbsp;example,&nbsp;any&nbsp;mapped&nbsp;columns&nbsp;or&nbsp;relationships.</span></dd></dl>
381
+
382
+ <hr>
383
+ Data descriptors defined here:<br>
384
+ <dl><dt><strong>created_at</strong></dt>
385
+ </dl>
386
+ <dl><dt><strong>employee</strong></dt>
387
+ </dl>
388
+ <dl><dt><strong>employee_id</strong></dt>
389
+ </dl>
390
+ <dl><dt><strong>id</strong></dt>
391
+ </dl>
392
+ <dl><dt><strong>payload_json</strong></dt>
393
+ </dl>
394
+ <dl><dt><strong>predictions</strong></dt>
395
+ </dl>
396
+ <dl><dt><strong>snapshot</strong></dt>
397
+ </dl>
398
+ <dl><dt><strong>snapshot_id</strong></dt>
399
+ </dl>
400
+ <dl><dt><strong>survey</strong></dt>
401
+ </dl>
402
+ <dl><dt><strong>survey_id</strong></dt>
403
+ </dl>
404
+ <hr>
405
+ Data and other attributes defined here:<br>
406
+ <dl><dt><strong>__annotations__</strong> = {'created_at': 'Mapped[datetime]', 'employee': "Mapped[Optional['RawEmployee']]", 'employee_id': 'Mapped[Optional[int]]', 'id': 'Mapped[int]', 'payload_json': 'Mapped[Dict[str, Any]]', 'predictions': "Mapped[list['Prediction']]", 'snapshot': "Mapped[Optional['RawEmployeeSnapshot']]", 'snapshot_id': 'Mapped[Optional[int]]', 'survey': "Mapped[Optional['RawSurvey']]", 'survey_id': 'Mapped[Optional[int]]'}</dl>
407
+
408
+ <dl><dt><strong>__mapper__</strong> = &lt;Mapper at 0x28b5d076490; PredictionRequest&gt;</dl>
409
+
410
+ <dl><dt><strong>__parameters__</strong> = ()</dl>
411
+
412
+ <dl><dt><strong>__table__</strong> = Table('prediction_requests', MetaData(), Column(...on utcnow at 0x0000028B5D06B240&gt;)), schema='app')</dl>
413
+
414
+ <dl><dt><strong>__table_args__</strong> = (Index('ix_prediction_requests_created_at', Colum...efault(&lt;function utcnow at 0x0000028B5D06B240&gt;))), {'schema': 'app'})</dl>
415
+
416
+ <dl><dt><strong>__tablename__</strong> = 'prediction_requests'</dl>
417
+
418
+ <hr>
419
+ Data and other attributes inherited from <a href="app.models.html#Base">Base</a>:<br>
420
+ <dl><dt><strong>metadata</strong> = MetaData()</dl>
421
+
422
+ <dl><dt><strong>registry</strong> = &lt;sqlalchemy.orm.decl_api.registry object&gt;</dl>
423
+
424
+ <hr>
425
+ Class methods inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
426
+ <dl><dt><a name="PredictionRequest-__init_subclass__"><strong>__init_subclass__</strong></a>(**kw: 'Any') -&gt; 'None'</dt><dd><span class="code">Function&nbsp;to&nbsp;initialize&nbsp;subclasses.</span></dd></dl>
427
+
428
+ <hr>
429
+ Data descriptors inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
430
+ <dl><dt><strong>__dict__</strong></dt>
431
+ <dd><span class="code">dictionary&nbsp;for&nbsp;instance&nbsp;variables</span></dd>
432
+ </dl>
433
+ <dl><dt><strong>__weakref__</strong></dt>
434
+ <dd><span class="code">list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object</span></dd>
435
+ </dl>
436
+ <hr>
437
+ Data and other attributes inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
438
+ <dl><dt><strong>__orig_bases__</strong> = (sqlalchemy.inspection.Inspectable[sqlalchemy.orm.state.InstanceState[typing.Any]],)</dl>
439
+
440
+ <hr>
441
+ Class methods inherited from <a href="typing.html#Generic">typing.Generic</a>:<br>
442
+ <dl><dt><a name="PredictionRequest-__class_getitem__"><strong>__class_getitem__</strong></a>(...)</dt><dd><span class="code">Parameterizes&nbsp;a&nbsp;generic&nbsp;class.<br>
443
+ &nbsp;<br>
444
+ At&nbsp;least,&nbsp;parameterizing&nbsp;a&nbsp;generic&nbsp;class&nbsp;is&nbsp;the&nbsp;*main*&nbsp;thing&nbsp;this<br>
445
+ method&nbsp;does.&nbsp;For&nbsp;example,&nbsp;for&nbsp;some&nbsp;generic&nbsp;class&nbsp;`Foo`,&nbsp;this&nbsp;is&nbsp;called<br>
446
+ when&nbsp;we&nbsp;do&nbsp;`Foo[int]`&nbsp;-&nbsp;there,&nbsp;with&nbsp;`cls=Foo`&nbsp;and&nbsp;`params=int`.<br>
447
+ &nbsp;<br>
448
+ However,&nbsp;note&nbsp;that&nbsp;this&nbsp;method&nbsp;is&nbsp;also&nbsp;called&nbsp;when&nbsp;defining&nbsp;generic<br>
449
+ classes&nbsp;in&nbsp;the&nbsp;first&nbsp;place&nbsp;with&nbsp;`class&nbsp;Foo[T]:&nbsp;...`.</span></dd></dl>
450
+
451
+ </td></tr></table> <p>
452
+ <table class="section">
453
+ <tr class="decor title-decor heading-text">
454
+ <td class="section-title" colspan=3>&nbsp;<br><a name="RawEmployee">class <strong>RawEmployee</strong></a>(<a href="app.models.html#Base">Base</a>)</td></tr>
455
+
456
+ <tr><td class="decor title-decor" rowspan=2><span class="code">&nbsp;&nbsp;&nbsp;</span></td>
457
+ <td class="decor title-decor" colspan=2><span class="code"><a href="#RawEmployee">RawEmployee</a>(**kwargs)<br>
458
+ &nbsp;<br>
459
+ #&nbsp;Modèles&nbsp;SQLAlchemy&nbsp;représentant&nbsp;les&nbsp;tables&nbsp;de&nbsp;la&nbsp;base&nbsp;de&nbsp;données,&nbsp;organisés&nbsp;par&nbsp;schéma&nbsp;(raw,&nbsp;clean,&nbsp;app)<br>&nbsp;</span></td></tr>
460
+ <tr><td>&nbsp;</td>
461
+ <td class="singlecolumn"><dl><dt>Method resolution order:</dt>
462
+ <dd><a href="app.models.html#RawEmployee">RawEmployee</a></dd>
463
+ <dd><a href="app.models.html#Base">Base</a></dd>
464
+ <dd><a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a></dd>
465
+ <dd><a href="sqlalchemy.inspection.html#Inspectable">sqlalchemy.inspection.Inspectable</a></dd>
466
+ <dd><a href="typing.html#Generic">typing.Generic</a></dd>
467
+ <dd><a href="builtins.html#object">builtins.object</a></dd>
468
+ </dl>
469
+ <hr>
470
+ Methods defined here:<br>
471
+ <dl><dt><a name="RawEmployee-__init__"><strong>__init__</strong></a>(self, **kwargs)<span class="grey"><span class="heading-text"> from sqlalchemy.orm.instrumentation</span></span></dt><dd><span class="code">A&nbsp;simple&nbsp;constructor&nbsp;that&nbsp;allows&nbsp;initialization&nbsp;from&nbsp;kwargs.<br>
472
+ &nbsp;<br>
473
+ Sets&nbsp;attributes&nbsp;on&nbsp;the&nbsp;constructed&nbsp;instance&nbsp;using&nbsp;the&nbsp;names&nbsp;and<br>
474
+ values&nbsp;in&nbsp;``kwargs``.<br>
475
+ &nbsp;<br>
476
+ Only&nbsp;keys&nbsp;that&nbsp;are&nbsp;present&nbsp;as<br>
477
+ attributes&nbsp;of&nbsp;the&nbsp;instance's&nbsp;class&nbsp;are&nbsp;allowed.&nbsp;These&nbsp;could&nbsp;be,<br>
478
+ for&nbsp;example,&nbsp;any&nbsp;mapped&nbsp;columns&nbsp;or&nbsp;relationships.</span></dd></dl>
479
+
480
+ <hr>
481
+ Data descriptors defined here:<br>
482
+ <dl><dt><strong>employee_external_id</strong></dt>
483
+ </dl>
484
+ <dl><dt><strong>id</strong></dt>
485
+ </dl>
486
+ <hr>
487
+ Data and other attributes defined here:<br>
488
+ <dl><dt><strong>__annotations__</strong> = {'employee_external_id': 'Mapped[int]', 'id': 'Mapped[int]'}</dl>
489
+
490
+ <dl><dt><strong>__mapper__</strong> = &lt;Mapper at 0x28b5cf4f230; RawEmployee&gt;</dl>
491
+
492
+ <dl><dt><strong>__parameters__</strong> = ()</dl>
493
+
494
+ <dl><dt><strong>__table__</strong> = Table('employees', MetaData(), Column('id', BigI...table=&lt;employees&gt;, nullable=False), schema='raw')</dl>
495
+
496
+ <dl><dt><strong>__table_args__</strong> = (Index('ix_raw_employees_employee_external_id', C...', Integer(), table=&lt;employees&gt;, nullable=False)), {'schema': 'raw'})</dl>
497
+
498
+ <dl><dt><strong>__tablename__</strong> = 'employees'</dl>
499
+
500
+ <hr>
501
+ Data and other attributes inherited from <a href="app.models.html#Base">Base</a>:<br>
502
+ <dl><dt><strong>metadata</strong> = MetaData()</dl>
503
+
504
+ <dl><dt><strong>registry</strong> = &lt;sqlalchemy.orm.decl_api.registry object&gt;</dl>
505
+
506
+ <hr>
507
+ Class methods inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
508
+ <dl><dt><a name="RawEmployee-__init_subclass__"><strong>__init_subclass__</strong></a>(**kw: 'Any') -&gt; 'None'</dt><dd><span class="code">Function&nbsp;to&nbsp;initialize&nbsp;subclasses.</span></dd></dl>
509
+
510
+ <hr>
511
+ Data descriptors inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
512
+ <dl><dt><strong>__dict__</strong></dt>
513
+ <dd><span class="code">dictionary&nbsp;for&nbsp;instance&nbsp;variables</span></dd>
514
+ </dl>
515
+ <dl><dt><strong>__weakref__</strong></dt>
516
+ <dd><span class="code">list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object</span></dd>
517
+ </dl>
518
+ <hr>
519
+ Data and other attributes inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
520
+ <dl><dt><strong>__orig_bases__</strong> = (sqlalchemy.inspection.Inspectable[sqlalchemy.orm.state.InstanceState[typing.Any]],)</dl>
521
+
522
+ <hr>
523
+ Class methods inherited from <a href="typing.html#Generic">typing.Generic</a>:<br>
524
+ <dl><dt><a name="RawEmployee-__class_getitem__"><strong>__class_getitem__</strong></a>(...)</dt><dd><span class="code">Parameterizes&nbsp;a&nbsp;generic&nbsp;class.<br>
525
+ &nbsp;<br>
526
+ At&nbsp;least,&nbsp;parameterizing&nbsp;a&nbsp;generic&nbsp;class&nbsp;is&nbsp;the&nbsp;*main*&nbsp;thing&nbsp;this<br>
527
+ method&nbsp;does.&nbsp;For&nbsp;example,&nbsp;for&nbsp;some&nbsp;generic&nbsp;class&nbsp;`Foo`,&nbsp;this&nbsp;is&nbsp;called<br>
528
+ when&nbsp;we&nbsp;do&nbsp;`Foo[int]`&nbsp;-&nbsp;there,&nbsp;with&nbsp;`cls=Foo`&nbsp;and&nbsp;`params=int`.<br>
529
+ &nbsp;<br>
530
+ However,&nbsp;note&nbsp;that&nbsp;this&nbsp;method&nbsp;is&nbsp;also&nbsp;called&nbsp;when&nbsp;defining&nbsp;generic<br>
531
+ classes&nbsp;in&nbsp;the&nbsp;first&nbsp;place&nbsp;with&nbsp;`class&nbsp;Foo[T]:&nbsp;...`.</span></dd></dl>
532
+
533
+ </td></tr></table> <p>
534
+ <table class="section">
535
+ <tr class="decor title-decor heading-text">
536
+ <td class="section-title" colspan=3>&nbsp;<br><a name="RawEmployeeSnapshot">class <strong>RawEmployeeSnapshot</strong></a>(<a href="app.models.html#Base">Base</a>)</td></tr>
537
+
538
+ <tr><td class="decor title-decor" rowspan=2><span class="code">&nbsp;&nbsp;&nbsp;</span></td>
539
+ <td class="decor title-decor" colspan=2><span class="code"><a href="#RawEmployeeSnapshot">RawEmployeeSnapshot</a>(**kwargs)<br>
540
+ &nbsp;<br>
541
+ #&nbsp;Les&nbsp;autres&nbsp;champs&nbsp;de&nbsp;la&nbsp;table&nbsp;raw.employees&nbsp;peuvent&nbsp;être&nbsp;ajoutés&nbsp;ici&nbsp;si&nbsp;nécessaire,<br>
542
+ #&nbsp;mais&nbsp;ne&nbsp;sont&nbsp;pas&nbsp;indispensables&nbsp;pour&nbsp;les&nbsp;prédictions&nbsp;basées&nbsp;sur&nbsp;employee_id<br>&nbsp;</span></td></tr>
543
+ <tr><td>&nbsp;</td>
544
+ <td class="singlecolumn"><dl><dt>Method resolution order:</dt>
545
+ <dd><a href="app.models.html#RawEmployeeSnapshot">RawEmployeeSnapshot</a></dd>
546
+ <dd><a href="app.models.html#Base">Base</a></dd>
547
+ <dd><a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a></dd>
548
+ <dd><a href="sqlalchemy.inspection.html#Inspectable">sqlalchemy.inspection.Inspectable</a></dd>
549
+ <dd><a href="typing.html#Generic">typing.Generic</a></dd>
550
+ <dd><a href="builtins.html#object">builtins.object</a></dd>
551
+ </dl>
552
+ <hr>
553
+ Methods defined here:<br>
554
+ <dl><dt><a name="RawEmployeeSnapshot-__init__"><strong>__init__</strong></a>(self, **kwargs)<span class="grey"><span class="heading-text"> from sqlalchemy.orm.instrumentation</span></span></dt><dd><span class="code">A&nbsp;simple&nbsp;constructor&nbsp;that&nbsp;allows&nbsp;initialization&nbsp;from&nbsp;kwargs.<br>
555
+ &nbsp;<br>
556
+ Sets&nbsp;attributes&nbsp;on&nbsp;the&nbsp;constructed&nbsp;instance&nbsp;using&nbsp;the&nbsp;names&nbsp;and<br>
557
+ values&nbsp;in&nbsp;``kwargs``.<br>
558
+ &nbsp;<br>
559
+ Only&nbsp;keys&nbsp;that&nbsp;are&nbsp;present&nbsp;as<br>
560
+ attributes&nbsp;of&nbsp;the&nbsp;instance's&nbsp;class&nbsp;are&nbsp;allowed.&nbsp;These&nbsp;could&nbsp;be,<br>
561
+ for&nbsp;example,&nbsp;any&nbsp;mapped&nbsp;columns&nbsp;or&nbsp;relationships.</span></dd></dl>
562
+
563
+ <hr>
564
+ Data descriptors defined here:<br>
565
+ <dl><dt><strong>id</strong></dt>
566
+ </dl>
567
+ <hr>
568
+ Data and other attributes defined here:<br>
569
+ <dl><dt><strong>__annotations__</strong> = {'id': 'Mapped[int]'}</dl>
570
+
571
+ <dl><dt><strong>__mapper__</strong> = &lt;Mapper at 0x28b5cfdd310; RawEmployeeSnapshot&gt;</dl>
572
+
573
+ <dl><dt><strong>__parameters__</strong> = ()</dl>
574
+
575
+ <dl><dt><strong>__table__</strong> = Table('employee_snapshots', MetaData(), Column('... primary_key=True, nullable=False), schema='raw')</dl>
576
+
577
+ <dl><dt><strong>__table_args__</strong> = ({'schema': 'raw'},)</dl>
578
+
579
+ <dl><dt><strong>__tablename__</strong> = 'employee_snapshots'</dl>
580
+
581
+ <hr>
582
+ Data and other attributes inherited from <a href="app.models.html#Base">Base</a>:<br>
583
+ <dl><dt><strong>metadata</strong> = MetaData()</dl>
584
+
585
+ <dl><dt><strong>registry</strong> = &lt;sqlalchemy.orm.decl_api.registry object&gt;</dl>
586
+
587
+ <hr>
588
+ Class methods inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
589
+ <dl><dt><a name="RawEmployeeSnapshot-__init_subclass__"><strong>__init_subclass__</strong></a>(**kw: 'Any') -&gt; 'None'</dt><dd><span class="code">Function&nbsp;to&nbsp;initialize&nbsp;subclasses.</span></dd></dl>
590
+
591
+ <hr>
592
+ Data descriptors inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
593
+ <dl><dt><strong>__dict__</strong></dt>
594
+ <dd><span class="code">dictionary&nbsp;for&nbsp;instance&nbsp;variables</span></dd>
595
+ </dl>
596
+ <dl><dt><strong>__weakref__</strong></dt>
597
+ <dd><span class="code">list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object</span></dd>
598
+ </dl>
599
+ <hr>
600
+ Data and other attributes inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
601
+ <dl><dt><strong>__orig_bases__</strong> = (sqlalchemy.inspection.Inspectable[sqlalchemy.orm.state.InstanceState[typing.Any]],)</dl>
602
+
603
+ <hr>
604
+ Class methods inherited from <a href="typing.html#Generic">typing.Generic</a>:<br>
605
+ <dl><dt><a name="RawEmployeeSnapshot-__class_getitem__"><strong>__class_getitem__</strong></a>(...)</dt><dd><span class="code">Parameterizes&nbsp;a&nbsp;generic&nbsp;class.<br>
606
+ &nbsp;<br>
607
+ At&nbsp;least,&nbsp;parameterizing&nbsp;a&nbsp;generic&nbsp;class&nbsp;is&nbsp;the&nbsp;*main*&nbsp;thing&nbsp;this<br>
608
+ method&nbsp;does.&nbsp;For&nbsp;example,&nbsp;for&nbsp;some&nbsp;generic&nbsp;class&nbsp;`Foo`,&nbsp;this&nbsp;is&nbsp;called<br>
609
+ when&nbsp;we&nbsp;do&nbsp;`Foo[int]`&nbsp;-&nbsp;there,&nbsp;with&nbsp;`cls=Foo`&nbsp;and&nbsp;`params=int`.<br>
610
+ &nbsp;<br>
611
+ However,&nbsp;note&nbsp;that&nbsp;this&nbsp;method&nbsp;is&nbsp;also&nbsp;called&nbsp;when&nbsp;defining&nbsp;generic<br>
612
+ classes&nbsp;in&nbsp;the&nbsp;first&nbsp;place&nbsp;with&nbsp;`class&nbsp;Foo[T]:&nbsp;...`.</span></dd></dl>
613
+
614
+ </td></tr></table> <p>
615
+ <table class="section">
616
+ <tr class="decor title-decor heading-text">
617
+ <td class="section-title" colspan=3>&nbsp;<br><a name="RawSurvey">class <strong>RawSurvey</strong></a>(<a href="app.models.html#Base">Base</a>)</td></tr>
618
+
619
+ <tr><td class="decor title-decor" rowspan=2><span class="code">&nbsp;&nbsp;&nbsp;</span></td>
620
+ <td class="decor title-decor" colspan=2><span class="code"><a href="#RawSurvey">RawSurvey</a>(**kwargs)<br>
621
+ &nbsp;<br>
622
+ #&nbsp;Les&nbsp;champs&nbsp;de&nbsp;la&nbsp;table&nbsp;raw.employee_snapshots&nbsp;peuvent&nbsp;être&nbsp;ajoutés&nbsp;ici&nbsp;si&nbsp;nécessaire,<br>
623
+ #&nbsp;mais&nbsp;ne&nbsp;sont&nbsp;pas&nbsp;indispensables&nbsp;pour&nbsp;les&nbsp;prédictions&nbsp;basées&nbsp;sur&nbsp;employee_id<br>&nbsp;</span></td></tr>
624
+ <tr><td>&nbsp;</td>
625
+ <td class="singlecolumn"><dl><dt>Method resolution order:</dt>
626
+ <dd><a href="app.models.html#RawSurvey">RawSurvey</a></dd>
627
+ <dd><a href="app.models.html#Base">Base</a></dd>
628
+ <dd><a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a></dd>
629
+ <dd><a href="sqlalchemy.inspection.html#Inspectable">sqlalchemy.inspection.Inspectable</a></dd>
630
+ <dd><a href="typing.html#Generic">typing.Generic</a></dd>
631
+ <dd><a href="builtins.html#object">builtins.object</a></dd>
632
+ </dl>
633
+ <hr>
634
+ Methods defined here:<br>
635
+ <dl><dt><a name="RawSurvey-__init__"><strong>__init__</strong></a>(self, **kwargs)<span class="grey"><span class="heading-text"> from sqlalchemy.orm.instrumentation</span></span></dt><dd><span class="code">A&nbsp;simple&nbsp;constructor&nbsp;that&nbsp;allows&nbsp;initialization&nbsp;from&nbsp;kwargs.<br>
636
+ &nbsp;<br>
637
+ Sets&nbsp;attributes&nbsp;on&nbsp;the&nbsp;constructed&nbsp;instance&nbsp;using&nbsp;the&nbsp;names&nbsp;and<br>
638
+ values&nbsp;in&nbsp;``kwargs``.<br>
639
+ &nbsp;<br>
640
+ Only&nbsp;keys&nbsp;that&nbsp;are&nbsp;present&nbsp;as<br>
641
+ attributes&nbsp;of&nbsp;the&nbsp;instance's&nbsp;class&nbsp;are&nbsp;allowed.&nbsp;These&nbsp;could&nbsp;be,<br>
642
+ for&nbsp;example,&nbsp;any&nbsp;mapped&nbsp;columns&nbsp;or&nbsp;relationships.</span></dd></dl>
643
+
644
+ <hr>
645
+ Data descriptors defined here:<br>
646
+ <dl><dt><strong>id</strong></dt>
647
+ </dl>
648
+ <hr>
649
+ Data and other attributes defined here:<br>
650
+ <dl><dt><strong>__annotations__</strong> = {'id': 'Mapped[int]'}</dl>
651
+
652
+ <dl><dt><strong>__mapper__</strong> = &lt;Mapper at 0x28b5cfdda90; RawSurvey&gt;</dl>
653
+
654
+ <dl><dt><strong>__parameters__</strong> = ()</dl>
655
+
656
+ <dl><dt><strong>__table__</strong> = Table('surveys', MetaData(), Column('id', BigInt... primary_key=True, nullable=False), schema='raw')</dl>
657
+
658
+ <dl><dt><strong>__table_args__</strong> = ({'schema': 'raw'},)</dl>
659
+
660
+ <dl><dt><strong>__tablename__</strong> = 'surveys'</dl>
661
+
662
+ <hr>
663
+ Data and other attributes inherited from <a href="app.models.html#Base">Base</a>:<br>
664
+ <dl><dt><strong>metadata</strong> = MetaData()</dl>
665
+
666
+ <dl><dt><strong>registry</strong> = &lt;sqlalchemy.orm.decl_api.registry object&gt;</dl>
667
+
668
+ <hr>
669
+ Class methods inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
670
+ <dl><dt><a name="RawSurvey-__init_subclass__"><strong>__init_subclass__</strong></a>(**kw: 'Any') -&gt; 'None'</dt><dd><span class="code">Function&nbsp;to&nbsp;initialize&nbsp;subclasses.</span></dd></dl>
671
+
672
+ <hr>
673
+ Data descriptors inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
674
+ <dl><dt><strong>__dict__</strong></dt>
675
+ <dd><span class="code">dictionary&nbsp;for&nbsp;instance&nbsp;variables</span></dd>
676
+ </dl>
677
+ <dl><dt><strong>__weakref__</strong></dt>
678
+ <dd><span class="code">list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object</span></dd>
679
+ </dl>
680
+ <hr>
681
+ Data and other attributes inherited from <a href="sqlalchemy.orm.decl_api.html#DeclarativeBase">sqlalchemy.orm.decl_api.DeclarativeBase</a>:<br>
682
+ <dl><dt><strong>__orig_bases__</strong> = (sqlalchemy.inspection.Inspectable[sqlalchemy.orm.state.InstanceState[typing.Any]],)</dl>
683
+
684
+ <hr>
685
+ Class methods inherited from <a href="typing.html#Generic">typing.Generic</a>:<br>
686
+ <dl><dt><a name="RawSurvey-__class_getitem__"><strong>__class_getitem__</strong></a>(...)</dt><dd><span class="code">Parameterizes&nbsp;a&nbsp;generic&nbsp;class.<br>
687
+ &nbsp;<br>
688
+ At&nbsp;least,&nbsp;parameterizing&nbsp;a&nbsp;generic&nbsp;class&nbsp;is&nbsp;the&nbsp;*main*&nbsp;thing&nbsp;this<br>
689
+ method&nbsp;does.&nbsp;For&nbsp;example,&nbsp;for&nbsp;some&nbsp;generic&nbsp;class&nbsp;`Foo`,&nbsp;this&nbsp;is&nbsp;called<br>
690
+ when&nbsp;we&nbsp;do&nbsp;`Foo[int]`&nbsp;-&nbsp;there,&nbsp;with&nbsp;`cls=Foo`&nbsp;and&nbsp;`params=int`.<br>
691
+ &nbsp;<br>
692
+ However,&nbsp;note&nbsp;that&nbsp;this&nbsp;method&nbsp;is&nbsp;also&nbsp;called&nbsp;when&nbsp;defining&nbsp;generic<br>
693
+ classes&nbsp;in&nbsp;the&nbsp;first&nbsp;place&nbsp;with&nbsp;`class&nbsp;Foo[T]:&nbsp;...`.</span></dd></dl>
694
+
695
+ </td></tr></table></td></tr></table><p>
696
+ <table class="section">
697
+ <tr class="decor functions-decor heading-text">
698
+ <td class="section-title" colspan=3>&nbsp;<br><strong class="bigsection">Functions</strong></td></tr>
699
+
700
+ <tr><td class="decor functions-decor"><span class="code">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></td><td>&nbsp;</td>
701
+ <td class="singlecolumn"><dl><dt><a name="-utcnow"><strong>utcnow</strong></a>() -&gt; 'datetime'</dt><dd><span class="code">#&nbsp;Fonction&nbsp;utilitaire&nbsp;pour&nbsp;obtenir&nbsp;l'heure&nbsp;actuelle&nbsp;en&nbsp;UTC,&nbsp;utilisée&nbsp;comme&nbsp;valeur&nbsp;par&nbsp;défaut&nbsp;pour&nbsp;les&nbsp;champs&nbsp;created_at</span></dd></dl>
702
+ </td></tr></table><p>
703
+ <table class="section">
704
+ <tr class="decor data-decor heading-text">
705
+ <td class="section-title" colspan=3>&nbsp;<br><strong class="bigsection">Data</strong></td></tr>
706
+
707
+ <tr><td class="decor data-decor"><span class="code">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></td><td>&nbsp;</td>
708
+ <td class="singlecolumn"><strong>Dict</strong> = typing.Dict<br>
709
+ <strong>ID_FK</strong> = BigInteger()<br>
710
+ <strong>ID_PK</strong> = BigInteger()<br>
711
+ <strong>Optional</strong> = typing.Optional</td></tr></table>
712
+ </body></html>
docs/domain.domain.html ADDED
@@ -0,0 +1,1225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Python: module domain.domain</title>
6
+ </head><body>
7
+
8
+ <table class="heading">
9
+ <tr class="heading-text decor">
10
+ <td class="title">&nbsp;<br><strong class="title"><a href="domain.html" class="white">domain</a>.domain</strong></td>
11
+ <td class="extra"><a href=".">index</a><br><a href="file:c%3A%5Cusers%5Cgonza%5Copenclassroom%5Cprojet_4_classification%5Cprojet_technova_partners%5Cdomain%5Cdomain.py">c:\users\gonza\openclassroom\projet_4_classification\projet_technova_partners\domain\domain.py</a></td></tr></table>
12
+ <p></p>
13
+ <p>
14
+ <table class="section">
15
+ <tr class="decor index-decor heading-text">
16
+ <td class="section-title" colspan=3>&nbsp;<br><strong class="bigsection">Classes</strong></td></tr>
17
+
18
+ <tr><td class="decor index-decor"><span class="code">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></td><td>&nbsp;</td>
19
+ <td class="singlecolumn"><dl>
20
+ <dt class="heading-text"><a href="pydantic.main.html#BaseModel">pydantic.main.BaseModel</a>(<a href="builtins.html#object">builtins.object</a>)
21
+ </dt><dd>
22
+ <dl>
23
+ <dt class="heading-text"><a href="domain.domain.html#ModelRequest">ModelRequest</a>
24
+ </dt><dt class="heading-text"><a href="domain.domain.html#ModelResponse">ModelResponse</a>
25
+ </dt></dl>
26
+ </dd>
27
+ </dl>
28
+ <p>
29
+ <table class="section">
30
+ <tr class="decor title-decor heading-text">
31
+ <td class="section-title" colspan=3>&nbsp;<br><a name="ModelRequest">class <strong>ModelRequest</strong></a>(<a href="pydantic.main.html#BaseModel">pydantic.main.BaseModel</a>)</td></tr>
32
+
33
+ <tr><td class="decor title-decor" rowspan=2><span class="code">&nbsp;&nbsp;&nbsp;</span></td>
34
+ <td class="decor title-decor" colspan=2><span class="code"><a href="#ModelRequest">ModelRequest</a>(<br>
35
+ &nbsp;&nbsp;&nbsp;&nbsp;*,<br>
36
+ &nbsp;&nbsp;&nbsp;&nbsp;note_evaluation_precedente:&nbsp;int,<br>
37
+ &nbsp;&nbsp;&nbsp;&nbsp;niveau_hierarchique_poste:&nbsp;int,<br>
38
+ &nbsp;&nbsp;&nbsp;&nbsp;note_evaluation_actuelle:&nbsp;int,<br>
39
+ &nbsp;&nbsp;&nbsp;&nbsp;heures_supplementaires:&nbsp;Literal[0,&nbsp;1],<br>
40
+ &nbsp;&nbsp;&nbsp;&nbsp;augmentation_salaire_precedente:&nbsp;float,<br>
41
+ &nbsp;&nbsp;&nbsp;&nbsp;age:&nbsp;int,<br>
42
+ &nbsp;&nbsp;&nbsp;&nbsp;genre:&nbsp;Literal[0,&nbsp;1],<br>
43
+ &nbsp;&nbsp;&nbsp;&nbsp;revenu_mensuel:&nbsp;int,<br>
44
+ &nbsp;&nbsp;&nbsp;&nbsp;statut_marital:&nbsp;str,<br>
45
+ &nbsp;&nbsp;&nbsp;&nbsp;departement:&nbsp;str,<br>
46
+ &nbsp;&nbsp;&nbsp;&nbsp;poste:&nbsp;str,<br>
47
+ &nbsp;&nbsp;&nbsp;&nbsp;nombre_experiences_precedentes:&nbsp;int,<br>
48
+ &nbsp;&nbsp;&nbsp;&nbsp;annee_experience_totale:&nbsp;int,<br>
49
+ &nbsp;&nbsp;&nbsp;&nbsp;annees_dans_l_entreprise:&nbsp;int,<br>
50
+ &nbsp;&nbsp;&nbsp;&nbsp;annees_dans_le_poste_actuel:&nbsp;int,<br>
51
+ &nbsp;&nbsp;&nbsp;&nbsp;nombre_participation_pee:&nbsp;int,<br>
52
+ &nbsp;&nbsp;&nbsp;&nbsp;nb_formations_suivies:&nbsp;int,<br>
53
+ &nbsp;&nbsp;&nbsp;&nbsp;distance_domicile_travail:&nbsp;int,<br>
54
+ &nbsp;&nbsp;&nbsp;&nbsp;niveau_education:&nbsp;int,<br>
55
+ &nbsp;&nbsp;&nbsp;&nbsp;domaine_etude:&nbsp;str,<br>
56
+ &nbsp;&nbsp;&nbsp;&nbsp;frequence_deplacement:&nbsp;Literal[0,&nbsp;1,&nbsp;2,&nbsp;3],<br>
57
+ &nbsp;&nbsp;&nbsp;&nbsp;annees_depuis_la_derniere_promotion:&nbsp;int,<br>
58
+ &nbsp;&nbsp;&nbsp;&nbsp;annees_sous_responsable_actuel:&nbsp;int,<br>
59
+ &nbsp;&nbsp;&nbsp;&nbsp;satisfaction_moyenne:&nbsp;float,<br>
60
+ &nbsp;&nbsp;&nbsp;&nbsp;nonlineaire_participation_pee:&nbsp;float,<br>
61
+ &nbsp;&nbsp;&nbsp;&nbsp;ratio_heures_sup_salaire:&nbsp;float,<br>
62
+ &nbsp;&nbsp;&nbsp;&nbsp;nonlinaire_charge_contrainte:&nbsp;float,<br>
63
+ &nbsp;&nbsp;&nbsp;&nbsp;nonlinaire_surmenage_insatisfaction:&nbsp;float,<br>
64
+ &nbsp;&nbsp;&nbsp;&nbsp;jeune_surcharge:&nbsp;Literal[0,&nbsp;1],<br>
65
+ &nbsp;&nbsp;&nbsp;&nbsp;anciennete_sans_promotion:&nbsp;float,<br>
66
+ &nbsp;&nbsp;&nbsp;&nbsp;mobilite_carriere:&nbsp;float,<br>
67
+ &nbsp;&nbsp;&nbsp;&nbsp;risque_global:&nbsp;float<br>
68
+ )&nbsp;-&amp;gt;&nbsp;None<br>
69
+ &nbsp;<br>
70
+ #&nbsp;Modèles&nbsp;de&nbsp;données&nbsp;pour&nbsp;les&nbsp;requêtes&nbsp;et&nbsp;réponses&nbsp;de&nbsp;l'API,&nbsp;utilisés&nbsp;pour&nbsp;la&nbsp;validation&nbsp;des&nbsp;données&nbsp;d'entrée&nbsp;et&nbsp;de&nbsp;sortie.<br>&nbsp;</span></td></tr>
71
+ <tr><td>&nbsp;</td>
72
+ <td class="singlecolumn"><dl><dt>Method resolution order:</dt>
73
+ <dd><a href="domain.domain.html#ModelRequest">ModelRequest</a></dd>
74
+ <dd><a href="pydantic.main.html#BaseModel">pydantic.main.BaseModel</a></dd>
75
+ <dd><a href="builtins.html#object">builtins.object</a></dd>
76
+ </dl>
77
+ <hr>
78
+ Data descriptors defined here:<br>
79
+ <dl><dt><strong>__weakref__</strong></dt>
80
+ <dd><span class="code">list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object</span></dd>
81
+ </dl>
82
+ <hr>
83
+ Data and other attributes defined here:<br>
84
+ <dl><dt><strong>__abstractmethods__</strong> = frozenset()</dl>
85
+
86
+ <dl><dt><strong>__annotations__</strong> = {'age': 'int', 'anciennete_sans_promotion': 'float', 'annee_experience_totale': 'int', 'annees_dans_l_entreprise': 'int', 'annees_dans_le_poste_actuel': 'int', 'annees_depuis_la_derniere_promotion': 'int', 'annees_sous_responsable_actuel': 'int', 'augmentation_salaire_precedente': 'float', 'departement': 'str', 'distance_domicile_travail': 'int', ...}</dl>
87
+
88
+ <dl><dt><strong>__class_vars__</strong> = set()</dl>
89
+
90
+ <dl><dt><strong>__private_attributes__</strong> = {}</dl>
91
+
92
+ <dl><dt><strong>__pydantic_complete__</strong> = True</dl>
93
+
94
+ <dl><dt><strong>__pydantic_computed_fields__</strong> = {}</dl>
95
+
96
+ <dl><dt><strong>__pydantic_core_schema__</strong> = {'cls': &lt;class 'domain.domain.ModelRequest'&gt;, 'config': {'title': 'ModelRequest'}, 'custom_init': False, 'metadata': {'pydantic_js_functions': [&lt;bound method BaseModel.__get_pydantic_json_schema__ of &lt;class 'domain.domain.ModelRequest'&gt;&gt;]}, 'ref': 'domain.domain.ModelRequest:2430587751824', 'root_model': False, 'schema': {'computed_fields': [], 'fields': {'age': {'metadata': {}, 'schema': {'type': 'int'}, 'type': 'model-field'}, 'anciennete_sans_promotion': {'metadata': {}, 'schema': {'type': 'float'}, 'type': 'model-field'}, 'annee_experience_totale': {'metadata': {}, 'schema': {'type': 'int'}, 'type': 'model-field'}, 'annees_dans_l_entreprise': {'metadata': {}, 'schema': {'type': 'int'}, 'type': 'model-field'}, 'annees_dans_le_poste_actuel': {'metadata': {}, 'schema': {'type': 'int'}, 'type': 'model-field'}, 'annees_depuis_la_derniere_promotion': {'metadata': {}, 'schema': {'type': 'int'}, 'type': 'model-field'}, 'annees_sous_responsable_actuel': {'metadata': {}, 'schema': {'type': 'int'}, 'type': 'model-field'}, 'augmentation_salaire_precedente': {'metadata': {}, 'schema': {'type': 'float'}, 'type': 'model-field'}, 'departement': {'metadata': {}, 'schema': {'type': 'str'}, 'type': 'model-field'}, 'distance_domicile_travail': {'metadata': {}, 'schema': {'type': 'int'}, 'type': 'model-field'}, ...}, 'model_name': 'ModelRequest', 'type': 'model-fields'}, 'type': 'model'}</dl>
97
+
98
+ <dl><dt><strong>__pydantic_custom_init__</strong> = False</dl>
99
+
100
+ <dl><dt><strong>__pydantic_decorators__</strong> = DecoratorInfos(validators={}, field_validators={...zers={}, model_validators={}, computed_fields={})</dl>
101
+
102
+ <dl><dt><strong>__pydantic_fields__</strong> = {'age': FieldInfo(annotation=int, required=True), 'anciennete_sans_promotion': FieldInfo(annotation=float, required=True), 'annee_experience_totale': FieldInfo(annotation=int, required=True), 'annees_dans_l_entreprise': FieldInfo(annotation=int, required=True), 'annees_dans_le_poste_actuel': FieldInfo(annotation=int, required=True), 'annees_depuis_la_derniere_promotion': FieldInfo(annotation=int, required=True), 'annees_sous_responsable_actuel': FieldInfo(annotation=int, required=True), 'augmentation_salaire_precedente': FieldInfo(annotation=float, required=True), 'departement': FieldInfo(annotation=str, required=True), 'distance_domicile_travail': FieldInfo(annotation=int, required=True), ...}</dl>
103
+
104
+ <dl><dt><strong>__pydantic_generic_metadata__</strong> = {'args': (), 'origin': None, 'parameters': ()}</dl>
105
+
106
+ <dl><dt><strong>__pydantic_parent_namespace__</strong> = None</dl>
107
+
108
+ <dl><dt><strong>__pydantic_post_init__</strong> = None</dl>
109
+
110
+ <dl><dt><strong>__pydantic_serializer__</strong> = SchemaSerializer(serializer=Model(
111
+ ModelSeri... name: "ModelRequest",
112
+ },
113
+ ), definitions=[])</dl>
114
+
115
+ <dl><dt><strong>__pydantic_setattr_handlers__</strong> = {}</dl>
116
+
117
+ <dl><dt><strong>__pydantic_validator__</strong> = SchemaValidator(title="ModelRequest", validator=...t",
118
+ },
119
+ ), definitions=[], cache_strings=True)</dl>
120
+
121
+ <dl><dt><strong>__signature__</strong> = &lt;Signature (*, note_evaluation_precedente: int, ...e_carriere: float, risque_global: float) -&gt; None&gt;</dl>
122
+
123
+ <dl><dt><strong>model_config</strong> = {}</dl>
124
+
125
+ <hr>
126
+ Methods inherited from <a href="pydantic.main.html#BaseModel">pydantic.main.BaseModel</a>:<br>
127
+ <dl><dt><a name="ModelRequest-__copy__"><strong>__copy__</strong></a>(self) -&gt; 'Self'</dt><dd><span class="code">Returns&nbsp;a&nbsp;shallow&nbsp;copy&nbsp;of&nbsp;the&nbsp;model.</span></dd></dl>
128
+
129
+ <dl><dt><a name="ModelRequest-__deepcopy__"><strong>__deepcopy__</strong></a>(self, memo: 'dict[int, Any] | None' = None) -&gt; 'Self'</dt><dd><span class="code">Returns&nbsp;a&nbsp;deep&nbsp;copy&nbsp;of&nbsp;the&nbsp;model.</span></dd></dl>
130
+
131
+ <dl><dt><a name="ModelRequest-__delattr__"><strong>__delattr__</strong></a>(self, item: 'str') -&gt; 'Any'</dt><dd><span class="code">Implement&nbsp;delattr(self,&nbsp;name).</span></dd></dl>
132
+
133
+ <dl><dt><a name="ModelRequest-__eq__"><strong>__eq__</strong></a>(self, other: 'Any') -&gt; 'bool'</dt><dd><span class="code">Return&nbsp;self==value.</span></dd></dl>
134
+
135
+ <dl><dt><a name="ModelRequest-__getattr__"><strong>__getattr__</strong></a>(self, item: 'str') -&gt; 'Any'</dt></dl>
136
+
137
+ <dl><dt><a name="ModelRequest-__getstate__"><strong>__getstate__</strong></a>(self) -&gt; 'dict[Any, Any]'</dt><dd><span class="code">Helper&nbsp;for&nbsp;pickle.</span></dd></dl>
138
+
139
+ <dl><dt><a name="ModelRequest-__init__"><strong>__init__</strong></a>(self, /, **data: 'Any') -&gt; 'None'</dt><dd><span class="code">Create&nbsp;a&nbsp;new&nbsp;model&nbsp;by&nbsp;parsing&nbsp;and&nbsp;validating&nbsp;input&nbsp;data&nbsp;from&nbsp;keyword&nbsp;arguments.<br>
140
+ &nbsp;<br>
141
+ Raises&nbsp;[`ValidationError`][pydantic_core.ValidationError]&nbsp;if&nbsp;the&nbsp;input&nbsp;data&nbsp;cannot&nbsp;be<br>
142
+ validated&nbsp;to&nbsp;form&nbsp;a&nbsp;valid&nbsp;model.<br>
143
+ &nbsp;<br>
144
+ `self`&nbsp;is&nbsp;explicitly&nbsp;positional-only&nbsp;to&nbsp;allow&nbsp;`self`&nbsp;as&nbsp;a&nbsp;field&nbsp;name.</span></dd></dl>
145
+
146
+ <dl><dt><a name="ModelRequest-__iter__"><strong>__iter__</strong></a>(self) -&gt; 'TupleGenerator'</dt><dd><span class="code">So&nbsp;`<a href="#ModelRequest-dict">dict</a>(model)`&nbsp;works.</span></dd></dl>
147
+
148
+ <dl><dt><a name="ModelRequest-__pretty__"><strong>__pretty__</strong></a>(self, fmt: 'Callable[[Any], Any]', **kwargs: 'Any') -&gt; 'Generator[Any]'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Used&nbsp;by&nbsp;devtools&nbsp;(<a href="https://python-devtools.helpmanual.io/">https://python-devtools.helpmanual.io/</a>)&nbsp;to&nbsp;pretty&nbsp;print&nbsp;objects.</span></dd></dl>
149
+
150
+ <dl><dt><a name="ModelRequest-__replace__"><strong>__replace__</strong></a>(self, **changes: 'Any') -&gt; 'Self'</dt><dd><span class="code">#&nbsp;Because&nbsp;we&nbsp;make&nbsp;use&nbsp;of&nbsp;`@dataclass_transform()`,&nbsp;`__replace__`&nbsp;is&nbsp;already&nbsp;synthesized&nbsp;by<br>
151
+ #&nbsp;type&nbsp;checkers,&nbsp;so&nbsp;we&nbsp;define&nbsp;the&nbsp;implementation&nbsp;in&nbsp;this&nbsp;`if&nbsp;not&nbsp;TYPE_CHECKING:`&nbsp;block:</span></dd></dl>
152
+
153
+ <dl><dt><a name="ModelRequest-__repr__"><strong>__repr__</strong></a>(self) -&gt; 'str'</dt><dd><span class="code">Return&nbsp;repr(self).</span></dd></dl>
154
+
155
+ <dl><dt><a name="ModelRequest-__repr_args__"><strong>__repr_args__</strong></a>(self) -&gt; '_repr.ReprArgs'</dt></dl>
156
+
157
+ <dl><dt><a name="ModelRequest-__repr_name__"><strong>__repr_name__</strong></a>(self) -&gt; 'str'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Name&nbsp;of&nbsp;the&nbsp;instance's&nbsp;class,&nbsp;used&nbsp;in&nbsp;__repr__.</span></dd></dl>
158
+
159
+ <dl><dt><a name="ModelRequest-__repr_recursion__"><strong>__repr_recursion__</strong></a>(self, object: 'Any') -&gt; 'str'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Returns&nbsp;the&nbsp;string&nbsp;representation&nbsp;of&nbsp;a&nbsp;recursive&nbsp;object.</span></dd></dl>
160
+
161
+ <dl><dt><a name="ModelRequest-__repr_str__"><strong>__repr_str__</strong></a>(self, join_str: 'str') -&gt; 'str'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt></dl>
162
+
163
+ <dl><dt><a name="ModelRequest-__rich_repr__"><strong>__rich_repr__</strong></a>(self) -&gt; 'RichReprResult'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Used&nbsp;by&nbsp;Rich&nbsp;(<a href="https://rich.readthedocs.io/en/stable/pretty.html">https://rich.readthedocs.io/en/stable/pretty.html</a>)&nbsp;to&nbsp;pretty&nbsp;print&nbsp;objects.</span></dd></dl>
164
+
165
+ <dl><dt><a name="ModelRequest-__setattr__"><strong>__setattr__</strong></a>(self, name: 'str', value: 'Any') -&gt; 'None'</dt><dd><span class="code">Implement&nbsp;setattr(self,&nbsp;name,&nbsp;value).</span></dd></dl>
166
+
167
+ <dl><dt><a name="ModelRequest-__setstate__"><strong>__setstate__</strong></a>(self, state: 'dict[Any, Any]') -&gt; 'None'</dt></dl>
168
+
169
+ <dl><dt><a name="ModelRequest-__str__"><strong>__str__</strong></a>(self) -&gt; 'str'</dt><dd><span class="code">Return&nbsp;str(self).</span></dd></dl>
170
+
171
+ <dl><dt><a name="ModelRequest-copy"><strong>copy</strong></a>(
172
+ self,
173
+ *,
174
+ include: 'AbstractSetIntStr | MappingIntStrAny | None' = None,
175
+ exclude: 'AbstractSetIntStr | MappingIntStrAny | None' = None,
176
+ update: 'Dict[str, Any] | None' = None,
177
+ deep: 'bool' = False
178
+ ) -&gt; 'Self'</dt><dd><span class="code">Returns&nbsp;a&nbsp;copy&nbsp;of&nbsp;the&nbsp;model.<br>
179
+ &nbsp;<br>
180
+ !!!&nbsp;warning&nbsp;"Deprecated"<br>
181
+ &nbsp;&nbsp;&nbsp;&nbsp;This&nbsp;method&nbsp;is&nbsp;now&nbsp;deprecated;&nbsp;use&nbsp;`model_copy`&nbsp;instead.<br>
182
+ &nbsp;<br>
183
+ If&nbsp;you&nbsp;need&nbsp;`include`&nbsp;or&nbsp;`exclude`,&nbsp;use:<br>
184
+ &nbsp;<br>
185
+ ```python&nbsp;{test="skip"&nbsp;lint="skip"}<br>
186
+ data&nbsp;=&nbsp;self.<a href="#ModelRequest-model_dump">model_dump</a>(include=include,&nbsp;exclude=exclude,&nbsp;round_trip=True)<br>
187
+ data&nbsp;=&nbsp;{**data,&nbsp;**(update&nbsp;or&nbsp;{})}<br>
188
+ copied&nbsp;=&nbsp;self.<a href="#ModelRequest-model_validate">model_validate</a>(data)<br>
189
+ ```<br>
190
+ &nbsp;<br>
191
+ Args:<br>
192
+ &nbsp;&nbsp;&nbsp;&nbsp;include:&nbsp;Optional&nbsp;set&nbsp;or&nbsp;mapping&nbsp;specifying&nbsp;which&nbsp;fields&nbsp;to&nbsp;include&nbsp;in&nbsp;the&nbsp;copied&nbsp;model.<br>
193
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude:&nbsp;Optional&nbsp;set&nbsp;or&nbsp;mapping&nbsp;specifying&nbsp;which&nbsp;fields&nbsp;to&nbsp;exclude&nbsp;in&nbsp;the&nbsp;copied&nbsp;model.<br>
194
+ &nbsp;&nbsp;&nbsp;&nbsp;update:&nbsp;Optional&nbsp;dictionary&nbsp;of&nbsp;field-value&nbsp;pairs&nbsp;to&nbsp;override&nbsp;field&nbsp;values&nbsp;in&nbsp;the&nbsp;copied&nbsp;model.<br>
195
+ &nbsp;&nbsp;&nbsp;&nbsp;deep:&nbsp;If&nbsp;True,&nbsp;the&nbsp;values&nbsp;of&nbsp;fields&nbsp;that&nbsp;are&nbsp;Pydantic&nbsp;models&nbsp;will&nbsp;be&nbsp;deep-copied.<br>
196
+ &nbsp;<br>
197
+ Returns:<br>
198
+ &nbsp;&nbsp;&nbsp;&nbsp;A&nbsp;copy&nbsp;of&nbsp;the&nbsp;model&nbsp;with&nbsp;included,&nbsp;excluded&nbsp;and&nbsp;updated&nbsp;fields&nbsp;as&nbsp;specified.</span></dd></dl>
199
+
200
+ <dl><dt><a name="ModelRequest-dict"><strong>dict</strong></a>(
201
+ self,
202
+ *,
203
+ include: 'IncEx | None' = None,
204
+ exclude: 'IncEx | None' = None,
205
+ by_alias: 'bool' = False,
206
+ exclude_unset: 'bool' = False,
207
+ exclude_defaults: 'bool' = False,
208
+ exclude_none: 'bool' = False
209
+ ) -&gt; 'Dict[str, Any]'</dt></dl>
210
+
211
+ <dl><dt><a name="ModelRequest-json"><strong>json</strong></a>(
212
+ self,
213
+ *,
214
+ include: 'IncEx | None' = None,
215
+ exclude: 'IncEx | None' = None,
216
+ by_alias: 'bool' = False,
217
+ exclude_unset: 'bool' = False,
218
+ exclude_defaults: 'bool' = False,
219
+ exclude_none: 'bool' = False,
220
+ encoder: 'Callable[[Any], Any] | None' = PydanticUndefined,
221
+ models_as_dict: 'bool' = PydanticUndefined,
222
+ **dumps_kwargs: 'Any'
223
+ ) -&gt; 'str'</dt></dl>
224
+
225
+ <dl><dt><a name="ModelRequest-model_copy"><strong>model_copy</strong></a>(
226
+ self,
227
+ *,
228
+ update: 'Mapping[str, Any] | None' = None,
229
+ deep: 'bool' = False
230
+ ) -&gt; 'Self'</dt><dd><span class="code">!!!&nbsp;abstract&nbsp;"Usage&nbsp;Documentation"<br>
231
+ &nbsp;&nbsp;&nbsp;&nbsp;[`model_copy`](../concepts/models.md#model-copy)<br>
232
+ &nbsp;<br>
233
+ Returns&nbsp;a&nbsp;copy&nbsp;of&nbsp;the&nbsp;model.<br>
234
+ &nbsp;<br>
235
+ !!!&nbsp;note<br>
236
+ &nbsp;&nbsp;&nbsp;&nbsp;The&nbsp;underlying&nbsp;instance's&nbsp;[`__dict__`][object.__dict__]&nbsp;attribute&nbsp;is&nbsp;copied.&nbsp;This<br>
237
+ &nbsp;&nbsp;&nbsp;&nbsp;might&nbsp;have&nbsp;unexpected&nbsp;side&nbsp;effects&nbsp;if&nbsp;you&nbsp;store&nbsp;anything&nbsp;in&nbsp;it,&nbsp;on&nbsp;top&nbsp;of&nbsp;the&nbsp;model<br>
238
+ &nbsp;&nbsp;&nbsp;&nbsp;fields&nbsp;(e.g.&nbsp;the&nbsp;value&nbsp;of&nbsp;[cached&nbsp;properties][functools.cached_property]).<br>
239
+ &nbsp;<br>
240
+ Args:<br>
241
+ &nbsp;&nbsp;&nbsp;&nbsp;update:&nbsp;Values&nbsp;to&nbsp;change/add&nbsp;in&nbsp;the&nbsp;new&nbsp;model.&nbsp;Note:&nbsp;the&nbsp;data&nbsp;is&nbsp;not&nbsp;validated<br>
242
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;before&nbsp;creating&nbsp;the&nbsp;new&nbsp;model.&nbsp;You&nbsp;should&nbsp;trust&nbsp;this&nbsp;data.<br>
243
+ &nbsp;&nbsp;&nbsp;&nbsp;deep:&nbsp;Set&nbsp;to&nbsp;`True`&nbsp;to&nbsp;make&nbsp;a&nbsp;deep&nbsp;copy&nbsp;of&nbsp;the&nbsp;model.<br>
244
+ &nbsp;<br>
245
+ Returns:<br>
246
+ &nbsp;&nbsp;&nbsp;&nbsp;New&nbsp;model&nbsp;instance.</span></dd></dl>
247
+
248
+ <dl><dt><a name="ModelRequest-model_dump"><strong>model_dump</strong></a>(
249
+ self,
250
+ *,
251
+ mode: "Literal['json', 'python'] | str" = 'python',
252
+ include: 'IncEx | None' = None,
253
+ exclude: 'IncEx | None' = None,
254
+ context: 'Any | None' = None,
255
+ by_alias: 'bool | None' = None,
256
+ exclude_unset: 'bool' = False,
257
+ exclude_defaults: 'bool' = False,
258
+ exclude_none: 'bool' = False,
259
+ exclude_computed_fields: 'bool' = False,
260
+ round_trip: 'bool' = False,
261
+ warnings: "bool | Literal['none', 'warn', 'error']" = True,
262
+ fallback: 'Callable[[Any], Any] | None' = None,
263
+ serialize_as_any: 'bool' = False
264
+ ) -&gt; 'dict[str, Any]'</dt><dd><span class="code">!!!&nbsp;abstract&nbsp;"Usage&nbsp;Documentation"<br>
265
+ &nbsp;&nbsp;&nbsp;&nbsp;[`model_dump`](../concepts/serialization.md#python-mode)<br>
266
+ &nbsp;<br>
267
+ Generate&nbsp;a&nbsp;dictionary&nbsp;representation&nbsp;of&nbsp;the&nbsp;model,&nbsp;optionally&nbsp;specifying&nbsp;which&nbsp;fields&nbsp;to&nbsp;include&nbsp;or&nbsp;exclude.<br>
268
+ &nbsp;<br>
269
+ Args:<br>
270
+ &nbsp;&nbsp;&nbsp;&nbsp;mode:&nbsp;The&nbsp;mode&nbsp;in&nbsp;which&nbsp;`to_python`&nbsp;should&nbsp;run.<br>
271
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;If&nbsp;mode&nbsp;is&nbsp;'json',&nbsp;the&nbsp;output&nbsp;will&nbsp;only&nbsp;contain&nbsp;JSON&nbsp;serializable&nbsp;types.<br>
272
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;If&nbsp;mode&nbsp;is&nbsp;'python',&nbsp;the&nbsp;output&nbsp;may&nbsp;contain&nbsp;non-JSON-serializable&nbsp;Python&nbsp;objects.<br>
273
+ &nbsp;&nbsp;&nbsp;&nbsp;include:&nbsp;A&nbsp;set&nbsp;of&nbsp;fields&nbsp;to&nbsp;include&nbsp;in&nbsp;the&nbsp;output.<br>
274
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude:&nbsp;A&nbsp;set&nbsp;of&nbsp;fields&nbsp;to&nbsp;exclude&nbsp;from&nbsp;the&nbsp;output.<br>
275
+ &nbsp;&nbsp;&nbsp;&nbsp;context:&nbsp;Additional&nbsp;context&nbsp;to&nbsp;pass&nbsp;to&nbsp;the&nbsp;serializer.<br>
276
+ &nbsp;&nbsp;&nbsp;&nbsp;by_alias:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;the&nbsp;field's&nbsp;alias&nbsp;in&nbsp;the&nbsp;dictionary&nbsp;key&nbsp;if&nbsp;defined.<br>
277
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_unset:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;fields&nbsp;that&nbsp;have&nbsp;not&nbsp;been&nbsp;explicitly&nbsp;set.<br>
278
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_defaults:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;fields&nbsp;that&nbsp;are&nbsp;set&nbsp;to&nbsp;their&nbsp;default&nbsp;value.<br>
279
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_none:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;fields&nbsp;that&nbsp;have&nbsp;a&nbsp;value&nbsp;of&nbsp;`None`.<br>
280
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_computed_fields:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;computed&nbsp;fields.<br>
281
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;While&nbsp;this&nbsp;can&nbsp;be&nbsp;useful&nbsp;for&nbsp;round-tripping,&nbsp;it&nbsp;is&nbsp;usually&nbsp;recommended&nbsp;to&nbsp;use&nbsp;the&nbsp;dedicated<br>
282
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`round_trip`&nbsp;parameter&nbsp;instead.<br>
283
+ &nbsp;&nbsp;&nbsp;&nbsp;round_trip:&nbsp;If&nbsp;True,&nbsp;dumped&nbsp;values&nbsp;should&nbsp;be&nbsp;valid&nbsp;as&nbsp;input&nbsp;for&nbsp;non-idempotent&nbsp;types&nbsp;such&nbsp;as&nbsp;Json[T].<br>
284
+ &nbsp;&nbsp;&nbsp;&nbsp;warnings:&nbsp;How&nbsp;to&nbsp;handle&nbsp;serialization&nbsp;errors.&nbsp;False/"none"&nbsp;ignores&nbsp;them,&nbsp;True/"warn"&nbsp;logs&nbsp;errors,<br>
285
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"error"&nbsp;raises&nbsp;a&nbsp;[`PydanticSerializationError`][pydantic_core.PydanticSerializationError].<br>
286
+ &nbsp;&nbsp;&nbsp;&nbsp;fallback:&nbsp;A&nbsp;function&nbsp;to&nbsp;call&nbsp;when&nbsp;an&nbsp;unknown&nbsp;value&nbsp;is&nbsp;encountered.&nbsp;If&nbsp;not&nbsp;provided,<br>
287
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a&nbsp;[`PydanticSerializationError`][pydantic_core.PydanticSerializationError]&nbsp;error&nbsp;is&nbsp;raised.<br>
288
+ &nbsp;&nbsp;&nbsp;&nbsp;serialize_as_any:&nbsp;Whether&nbsp;to&nbsp;serialize&nbsp;fields&nbsp;with&nbsp;duck-typing&nbsp;serialization&nbsp;behavior.<br>
289
+ &nbsp;<br>
290
+ Returns:<br>
291
+ &nbsp;&nbsp;&nbsp;&nbsp;A&nbsp;dictionary&nbsp;representation&nbsp;of&nbsp;the&nbsp;model.</span></dd></dl>
292
+
293
+ <dl><dt><a name="ModelRequest-model_dump_json"><strong>model_dump_json</strong></a>(
294
+ self,
295
+ *,
296
+ indent: 'int | None' = None,
297
+ ensure_ascii: 'bool' = False,
298
+ include: 'IncEx | None' = None,
299
+ exclude: 'IncEx | None' = None,
300
+ context: 'Any | None' = None,
301
+ by_alias: 'bool | None' = None,
302
+ exclude_unset: 'bool' = False,
303
+ exclude_defaults: 'bool' = False,
304
+ exclude_none: 'bool' = False,
305
+ exclude_computed_fields: 'bool' = False,
306
+ round_trip: 'bool' = False,
307
+ warnings: "bool | Literal['none', 'warn', 'error']" = True,
308
+ fallback: 'Callable[[Any], Any] | None' = None,
309
+ serialize_as_any: 'bool' = False
310
+ ) -&gt; 'str'</dt><dd><span class="code">!!!&nbsp;abstract&nbsp;"Usage&nbsp;Documentation"<br>
311
+ &nbsp;&nbsp;&nbsp;&nbsp;[`model_dump_json`](../concepts/serialization.md#json-mode)<br>
312
+ &nbsp;<br>
313
+ Generates&nbsp;a&nbsp;JSON&nbsp;representation&nbsp;of&nbsp;the&nbsp;model&nbsp;using&nbsp;Pydantic's&nbsp;`to_json`&nbsp;method.<br>
314
+ &nbsp;<br>
315
+ Args:<br>
316
+ &nbsp;&nbsp;&nbsp;&nbsp;indent:&nbsp;Indentation&nbsp;to&nbsp;use&nbsp;in&nbsp;the&nbsp;JSON&nbsp;output.&nbsp;If&nbsp;None&nbsp;is&nbsp;passed,&nbsp;the&nbsp;output&nbsp;will&nbsp;be&nbsp;compact.<br>
317
+ &nbsp;&nbsp;&nbsp;&nbsp;ensure_ascii:&nbsp;If&nbsp;`True`,&nbsp;the&nbsp;output&nbsp;is&nbsp;guaranteed&nbsp;to&nbsp;have&nbsp;all&nbsp;incoming&nbsp;non-ASCII&nbsp;characters&nbsp;escaped.<br>
318
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;If&nbsp;`False`&nbsp;(the&nbsp;default),&nbsp;these&nbsp;characters&nbsp;will&nbsp;be&nbsp;output&nbsp;as-is.<br>
319
+ &nbsp;&nbsp;&nbsp;&nbsp;include:&nbsp;Field(s)&nbsp;to&nbsp;include&nbsp;in&nbsp;the&nbsp;JSON&nbsp;output.<br>
320
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude:&nbsp;Field(s)&nbsp;to&nbsp;exclude&nbsp;from&nbsp;the&nbsp;JSON&nbsp;output.<br>
321
+ &nbsp;&nbsp;&nbsp;&nbsp;context:&nbsp;Additional&nbsp;context&nbsp;to&nbsp;pass&nbsp;to&nbsp;the&nbsp;serializer.<br>
322
+ &nbsp;&nbsp;&nbsp;&nbsp;by_alias:&nbsp;Whether&nbsp;to&nbsp;serialize&nbsp;using&nbsp;field&nbsp;aliases.<br>
323
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_unset:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;fields&nbsp;that&nbsp;have&nbsp;not&nbsp;been&nbsp;explicitly&nbsp;set.<br>
324
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_defaults:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;fields&nbsp;that&nbsp;are&nbsp;set&nbsp;to&nbsp;their&nbsp;default&nbsp;value.<br>
325
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_none:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;fields&nbsp;that&nbsp;have&nbsp;a&nbsp;value&nbsp;of&nbsp;`None`.<br>
326
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_computed_fields:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;computed&nbsp;fields.<br>
327
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;While&nbsp;this&nbsp;can&nbsp;be&nbsp;useful&nbsp;for&nbsp;round-tripping,&nbsp;it&nbsp;is&nbsp;usually&nbsp;recommended&nbsp;to&nbsp;use&nbsp;the&nbsp;dedicated<br>
328
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`round_trip`&nbsp;parameter&nbsp;instead.<br>
329
+ &nbsp;&nbsp;&nbsp;&nbsp;round_trip:&nbsp;If&nbsp;True,&nbsp;dumped&nbsp;values&nbsp;should&nbsp;be&nbsp;valid&nbsp;as&nbsp;input&nbsp;for&nbsp;non-idempotent&nbsp;types&nbsp;such&nbsp;as&nbsp;Json[T].<br>
330
+ &nbsp;&nbsp;&nbsp;&nbsp;warnings:&nbsp;How&nbsp;to&nbsp;handle&nbsp;serialization&nbsp;errors.&nbsp;False/"none"&nbsp;ignores&nbsp;them,&nbsp;True/"warn"&nbsp;logs&nbsp;errors,<br>
331
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"error"&nbsp;raises&nbsp;a&nbsp;[`PydanticSerializationError`][pydantic_core.PydanticSerializationError].<br>
332
+ &nbsp;&nbsp;&nbsp;&nbsp;fallback:&nbsp;A&nbsp;function&nbsp;to&nbsp;call&nbsp;when&nbsp;an&nbsp;unknown&nbsp;value&nbsp;is&nbsp;encountered.&nbsp;If&nbsp;not&nbsp;provided,<br>
333
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a&nbsp;[`PydanticSerializationError`][pydantic_core.PydanticSerializationError]&nbsp;error&nbsp;is&nbsp;raised.<br>
334
+ &nbsp;&nbsp;&nbsp;&nbsp;serialize_as_any:&nbsp;Whether&nbsp;to&nbsp;serialize&nbsp;fields&nbsp;with&nbsp;duck-typing&nbsp;serialization&nbsp;behavior.<br>
335
+ &nbsp;<br>
336
+ Returns:<br>
337
+ &nbsp;&nbsp;&nbsp;&nbsp;A&nbsp;JSON&nbsp;string&nbsp;representation&nbsp;of&nbsp;the&nbsp;model.</span></dd></dl>
338
+
339
+ <dl><dt><a name="ModelRequest-model_post_init"><strong>model_post_init</strong></a>(self, context: 'Any', /) -&gt; 'None'</dt><dd><span class="code">Override&nbsp;this&nbsp;method&nbsp;to&nbsp;perform&nbsp;additional&nbsp;initialization&nbsp;after&nbsp;`__init__`&nbsp;and&nbsp;`model_construct`.<br>
340
+ This&nbsp;is&nbsp;useful&nbsp;if&nbsp;you&nbsp;want&nbsp;to&nbsp;do&nbsp;some&nbsp;validation&nbsp;that&nbsp;requires&nbsp;the&nbsp;entire&nbsp;model&nbsp;to&nbsp;be&nbsp;initialized.</span></dd></dl>
341
+
342
+ <hr>
343
+ Class methods inherited from <a href="pydantic.main.html#BaseModel">pydantic.main.BaseModel</a>:<br>
344
+ <dl><dt><a name="ModelRequest-__class_getitem__"><strong>__class_getitem__</strong></a>(typevar_values: 'type[Any] | tuple[type[Any], ...]') -&gt; 'type[BaseModel] | _forward_ref.PydanticRecursiveRef'</dt></dl>
345
+
346
+ <dl><dt><a name="ModelRequest-__get_pydantic_core_schema__"><strong>__get_pydantic_core_schema__</strong></a>(
347
+ source: 'type[BaseModel]',
348
+ handler: 'GetCoreSchemaHandler',
349
+ /
350
+ ) -&gt; 'CoreSchema'</dt></dl>
351
+
352
+ <dl><dt><a name="ModelRequest-__get_pydantic_json_schema__"><strong>__get_pydantic_json_schema__</strong></a>(
353
+ core_schema: 'CoreSchema',
354
+ handler: 'GetJsonSchemaHandler',
355
+ /
356
+ ) -&gt; 'JsonSchemaValue'</dt><dd><span class="code">Hook&nbsp;into&nbsp;generating&nbsp;the&nbsp;model's&nbsp;JSON&nbsp;schema.<br>
357
+ &nbsp;<br>
358
+ Args:<br>
359
+ &nbsp;&nbsp;&nbsp;&nbsp;core_schema:&nbsp;A&nbsp;`pydantic-core`&nbsp;CoreSchema.<br>
360
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;You&nbsp;can&nbsp;ignore&nbsp;this&nbsp;argument&nbsp;and&nbsp;call&nbsp;the&nbsp;handler&nbsp;with&nbsp;a&nbsp;new&nbsp;CoreSchema,<br>
361
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;wrap&nbsp;this&nbsp;CoreSchema&nbsp;(`{'type':&nbsp;'nullable',&nbsp;'schema':&nbsp;current_schema}`),<br>
362
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;or&nbsp;just&nbsp;call&nbsp;the&nbsp;handler&nbsp;with&nbsp;the&nbsp;original&nbsp;schema.<br>
363
+ &nbsp;&nbsp;&nbsp;&nbsp;handler:&nbsp;Call&nbsp;into&nbsp;Pydantic's&nbsp;internal&nbsp;JSON&nbsp;schema&nbsp;generation.<br>
364
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;This&nbsp;will&nbsp;raise&nbsp;a&nbsp;`pydantic.errors.PydanticInvalidForJsonSchema`&nbsp;if&nbsp;JSON&nbsp;schema<br>
365
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;generation&nbsp;fails.<br>
366
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Since&nbsp;this&nbsp;gets&nbsp;called&nbsp;by&nbsp;`<a href="pydantic.main.html#BaseModel">BaseModel</a>.model_json_schema`&nbsp;you&nbsp;can&nbsp;override&nbsp;the<br>
367
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`schema_generator`&nbsp;argument&nbsp;to&nbsp;that&nbsp;function&nbsp;to&nbsp;change&nbsp;JSON&nbsp;schema&nbsp;generation&nbsp;globally<br>
368
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for&nbsp;a&nbsp;type.<br>
369
+ &nbsp;<br>
370
+ Returns:<br>
371
+ &nbsp;&nbsp;&nbsp;&nbsp;A&nbsp;JSON&nbsp;schema,&nbsp;as&nbsp;a&nbsp;Python&nbsp;object.</span></dd></dl>
372
+
373
+ <dl><dt><a name="ModelRequest-__pydantic_init_subclass__"><strong>__pydantic_init_subclass__</strong></a>(**kwargs: 'Any') -&gt; 'None'</dt><dd><span class="code">This&nbsp;is&nbsp;intended&nbsp;to&nbsp;behave&nbsp;just&nbsp;like&nbsp;`__init_subclass__`,&nbsp;but&nbsp;is&nbsp;called&nbsp;by&nbsp;`ModelMetaclass`<br>
374
+ only&nbsp;after&nbsp;basic&nbsp;class&nbsp;initialization&nbsp;is&nbsp;complete.&nbsp;In&nbsp;particular,&nbsp;attributes&nbsp;like&nbsp;`model_fields`&nbsp;will<br>
375
+ be&nbsp;present&nbsp;when&nbsp;this&nbsp;is&nbsp;called,&nbsp;but&nbsp;forward&nbsp;annotations&nbsp;are&nbsp;not&nbsp;guaranteed&nbsp;to&nbsp;be&nbsp;resolved&nbsp;yet,<br>
376
+ meaning&nbsp;that&nbsp;creating&nbsp;an&nbsp;instance&nbsp;of&nbsp;the&nbsp;class&nbsp;may&nbsp;fail.<br>
377
+ &nbsp;<br>
378
+ This&nbsp;is&nbsp;necessary&nbsp;because&nbsp;`__init_subclass__`&nbsp;will&nbsp;always&nbsp;be&nbsp;called&nbsp;by&nbsp;`type.__new__`,<br>
379
+ and&nbsp;it&nbsp;would&nbsp;require&nbsp;a&nbsp;prohibitively&nbsp;large&nbsp;refactor&nbsp;to&nbsp;the&nbsp;`ModelMetaclass`&nbsp;to&nbsp;ensure&nbsp;that<br>
380
+ `type.__new__`&nbsp;was&nbsp;called&nbsp;in&nbsp;such&nbsp;a&nbsp;manner&nbsp;that&nbsp;the&nbsp;class&nbsp;would&nbsp;already&nbsp;be&nbsp;sufficiently&nbsp;initialized.<br>
381
+ &nbsp;<br>
382
+ This&nbsp;will&nbsp;receive&nbsp;the&nbsp;same&nbsp;`kwargs`&nbsp;that&nbsp;would&nbsp;be&nbsp;passed&nbsp;to&nbsp;the&nbsp;standard&nbsp;`__init_subclass__`,&nbsp;namely,<br>
383
+ any&nbsp;kwargs&nbsp;passed&nbsp;to&nbsp;the&nbsp;class&nbsp;definition&nbsp;that&nbsp;aren't&nbsp;used&nbsp;internally&nbsp;by&nbsp;Pydantic.<br>
384
+ &nbsp;<br>
385
+ Args:<br>
386
+ &nbsp;&nbsp;&nbsp;&nbsp;**kwargs:&nbsp;Any&nbsp;keyword&nbsp;arguments&nbsp;passed&nbsp;to&nbsp;the&nbsp;class&nbsp;definition&nbsp;that&nbsp;aren't&nbsp;used&nbsp;internally<br>
387
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;by&nbsp;Pydantic.<br>
388
+ &nbsp;<br>
389
+ Note:<br>
390
+ &nbsp;&nbsp;&nbsp;&nbsp;You&nbsp;may&nbsp;want&nbsp;to&nbsp;override&nbsp;[`<a href="#ModelRequest-__pydantic_on_complete__">__pydantic_on_complete__</a>()`][pydantic.main.<a href="pydantic.main.html#BaseModel">BaseModel</a>.__pydantic_on_complete__]<br>
391
+ &nbsp;&nbsp;&nbsp;&nbsp;instead,&nbsp;which&nbsp;is&nbsp;called&nbsp;once&nbsp;the&nbsp;class&nbsp;and&nbsp;its&nbsp;fields&nbsp;are&nbsp;fully&nbsp;initialized&nbsp;and&nbsp;ready&nbsp;for&nbsp;validation.</span></dd></dl>
392
+
393
+ <dl><dt><a name="ModelRequest-__pydantic_on_complete__"><strong>__pydantic_on_complete__</strong></a>() -&gt; 'None'</dt><dd><span class="code">This&nbsp;is&nbsp;called&nbsp;once&nbsp;the&nbsp;class&nbsp;and&nbsp;its&nbsp;fields&nbsp;are&nbsp;fully&nbsp;initialized&nbsp;and&nbsp;ready&nbsp;to&nbsp;be&nbsp;used.<br>
394
+ &nbsp;<br>
395
+ This&nbsp;typically&nbsp;happens&nbsp;when&nbsp;the&nbsp;class&nbsp;is&nbsp;created&nbsp;(just&nbsp;before<br>
396
+ [`<a href="#ModelRequest-__pydantic_init_subclass__">__pydantic_init_subclass__</a>()`][pydantic.main.<a href="pydantic.main.html#BaseModel">BaseModel</a>.__pydantic_init_subclass__]&nbsp;is&nbsp;called&nbsp;on&nbsp;the&nbsp;superclass),<br>
397
+ except&nbsp;when&nbsp;forward&nbsp;annotations&nbsp;are&nbsp;used&nbsp;that&nbsp;could&nbsp;not&nbsp;immediately&nbsp;be&nbsp;resolved.<br>
398
+ In&nbsp;that&nbsp;case,&nbsp;it&nbsp;will&nbsp;be&nbsp;called&nbsp;later,&nbsp;when&nbsp;the&nbsp;model&nbsp;is&nbsp;rebuilt&nbsp;automatically&nbsp;or&nbsp;explicitly&nbsp;using<br>
399
+ [`<a href="#ModelRequest-model_rebuild">model_rebuild</a>()`][pydantic.main.<a href="pydantic.main.html#BaseModel">BaseModel</a>.model_rebuild].</span></dd></dl>
400
+
401
+ <dl><dt><a name="ModelRequest-construct"><strong>construct</strong></a>(_fields_set: 'set[str] | None' = None, **values: 'Any') -&gt; 'Self'</dt></dl>
402
+
403
+ <dl><dt><a name="ModelRequest-from_orm"><strong>from_orm</strong></a>(obj: 'Any') -&gt; 'Self'</dt></dl>
404
+
405
+ <dl><dt><a name="ModelRequest-model_construct"><strong>model_construct</strong></a>(_fields_set: 'set[str] | None' = None, **values: 'Any') -&gt; 'Self'</dt><dd><span class="code">Creates&nbsp;a&nbsp;new&nbsp;instance&nbsp;of&nbsp;the&nbsp;`Model`&nbsp;class&nbsp;with&nbsp;validated&nbsp;data.<br>
406
+ &nbsp;<br>
407
+ Creates&nbsp;a&nbsp;new&nbsp;model&nbsp;setting&nbsp;`__dict__`&nbsp;and&nbsp;`__pydantic_fields_set__`&nbsp;from&nbsp;trusted&nbsp;or&nbsp;pre-validated&nbsp;data.<br>
408
+ Default&nbsp;values&nbsp;are&nbsp;respected,&nbsp;but&nbsp;no&nbsp;other&nbsp;validation&nbsp;is&nbsp;performed.<br>
409
+ &nbsp;<br>
410
+ !!!&nbsp;note<br>
411
+ &nbsp;&nbsp;&nbsp;&nbsp;`<a href="#ModelRequest-model_construct">model_construct</a>()`&nbsp;generally&nbsp;respects&nbsp;the&nbsp;`model_config.extra`&nbsp;setting&nbsp;on&nbsp;the&nbsp;provided&nbsp;model.<br>
412
+ &nbsp;&nbsp;&nbsp;&nbsp;That&nbsp;is,&nbsp;if&nbsp;`model_config.extra&nbsp;==&nbsp;'allow'`,&nbsp;then&nbsp;all&nbsp;extra&nbsp;passed&nbsp;values&nbsp;are&nbsp;added&nbsp;to&nbsp;the&nbsp;model&nbsp;instance's&nbsp;`__dict__`<br>
413
+ &nbsp;&nbsp;&nbsp;&nbsp;and&nbsp;`__pydantic_extra__`&nbsp;fields.&nbsp;If&nbsp;`model_config.extra&nbsp;==&nbsp;'ignore'`&nbsp;(the&nbsp;default),&nbsp;then&nbsp;all&nbsp;extra&nbsp;passed&nbsp;values&nbsp;are&nbsp;ignored.<br>
414
+ &nbsp;&nbsp;&nbsp;&nbsp;Because&nbsp;no&nbsp;validation&nbsp;is&nbsp;performed&nbsp;with&nbsp;a&nbsp;call&nbsp;to&nbsp;`<a href="#ModelRequest-model_construct">model_construct</a>()`,&nbsp;having&nbsp;`model_config.extra&nbsp;==&nbsp;'forbid'`&nbsp;does&nbsp;not&nbsp;result&nbsp;in<br>
415
+ &nbsp;&nbsp;&nbsp;&nbsp;an&nbsp;error&nbsp;if&nbsp;extra&nbsp;values&nbsp;are&nbsp;passed,&nbsp;but&nbsp;they&nbsp;will&nbsp;be&nbsp;ignored.<br>
416
+ &nbsp;<br>
417
+ Args:<br>
418
+ &nbsp;&nbsp;&nbsp;&nbsp;_fields_set:&nbsp;A&nbsp;set&nbsp;of&nbsp;field&nbsp;names&nbsp;that&nbsp;were&nbsp;originally&nbsp;explicitly&nbsp;set&nbsp;during&nbsp;instantiation.&nbsp;If&nbsp;provided,<br>
419
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;this&nbsp;is&nbsp;directly&nbsp;used&nbsp;for&nbsp;the&nbsp;[`model_fields_set`][pydantic.<a href="pydantic.main.html#BaseModel">BaseModel</a>.model_fields_set]&nbsp;attribute.<br>
420
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Otherwise,&nbsp;the&nbsp;field&nbsp;names&nbsp;from&nbsp;the&nbsp;`values`&nbsp;argument&nbsp;will&nbsp;be&nbsp;used.<br>
421
+ &nbsp;&nbsp;&nbsp;&nbsp;values:&nbsp;Trusted&nbsp;or&nbsp;pre-validated&nbsp;data&nbsp;dictionary.<br>
422
+ &nbsp;<br>
423
+ Returns:<br>
424
+ &nbsp;&nbsp;&nbsp;&nbsp;A&nbsp;new&nbsp;instance&nbsp;of&nbsp;the&nbsp;`Model`&nbsp;class&nbsp;with&nbsp;validated&nbsp;data.</span></dd></dl>
425
+
426
+ <dl><dt><a name="ModelRequest-model_json_schema"><strong>model_json_schema</strong></a>(
427
+ by_alias: 'bool' = True,
428
+ ref_template: 'str' = '#/$defs/{model}',
429
+ schema_generator: 'type[GenerateJsonSchema]' = &lt;class 'pydantic.json_schema.GenerateJsonSchema'&gt;,
430
+ mode: 'JsonSchemaMode' = 'validation',
431
+ *,
432
+ union_format: "Literal['any_of', 'primitive_type_array']" = 'any_of'
433
+ ) -&gt; 'dict[str, Any]'</dt><dd><span class="code">Generates&nbsp;a&nbsp;JSON&nbsp;schema&nbsp;for&nbsp;a&nbsp;model&nbsp;class.<br>
434
+ &nbsp;<br>
435
+ Args:<br>
436
+ &nbsp;&nbsp;&nbsp;&nbsp;by_alias:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;attribute&nbsp;aliases&nbsp;or&nbsp;not.<br>
437
+ &nbsp;&nbsp;&nbsp;&nbsp;ref_template:&nbsp;The&nbsp;reference&nbsp;template.<br>
438
+ &nbsp;&nbsp;&nbsp;&nbsp;union_format:&nbsp;The&nbsp;format&nbsp;to&nbsp;use&nbsp;when&nbsp;combining&nbsp;schemas&nbsp;from&nbsp;unions&nbsp;together.&nbsp;Can&nbsp;be&nbsp;one&nbsp;of:<br>
439
+ &nbsp;<br>
440
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;`'any_of'`:&nbsp;Use&nbsp;the&nbsp;[`anyOf`](<a href="https://json-schema.org/understanding-json-schema/reference/combining#anyOf">https://json-schema.org/understanding-json-schema/reference/combining#anyOf</a>)<br>
441
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;keyword&nbsp;to&nbsp;combine&nbsp;schemas&nbsp;(the&nbsp;default).<br>
442
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;`'primitive_type_array'`:&nbsp;Use&nbsp;the&nbsp;[`type`](<a href="https://json-schema.org/understanding-json-schema/reference/type">https://json-schema.org/understanding-json-schema/reference/type</a>)<br>
443
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;keyword&nbsp;as&nbsp;an&nbsp;array&nbsp;of&nbsp;strings,&nbsp;containing&nbsp;each&nbsp;type&nbsp;of&nbsp;the&nbsp;combination.&nbsp;If&nbsp;any&nbsp;of&nbsp;the&nbsp;schemas&nbsp;is&nbsp;not&nbsp;a&nbsp;primitive<br>
444
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;type&nbsp;(`string`,&nbsp;`boolean`,&nbsp;`null`,&nbsp;`integer`&nbsp;or&nbsp;`number`)&nbsp;or&nbsp;contains&nbsp;constraints/metadata,&nbsp;falls&nbsp;back&nbsp;to<br>
445
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`any_of`.<br>
446
+ &nbsp;&nbsp;&nbsp;&nbsp;schema_generator:&nbsp;To&nbsp;override&nbsp;the&nbsp;logic&nbsp;used&nbsp;to&nbsp;generate&nbsp;the&nbsp;JSON&nbsp;schema,&nbsp;as&nbsp;a&nbsp;subclass&nbsp;of<br>
447
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`GenerateJsonSchema`&nbsp;with&nbsp;your&nbsp;desired&nbsp;modifications<br>
448
+ &nbsp;&nbsp;&nbsp;&nbsp;mode:&nbsp;The&nbsp;mode&nbsp;in&nbsp;which&nbsp;to&nbsp;generate&nbsp;the&nbsp;schema.<br>
449
+ &nbsp;<br>
450
+ Returns:<br>
451
+ &nbsp;&nbsp;&nbsp;&nbsp;The&nbsp;JSON&nbsp;schema&nbsp;for&nbsp;the&nbsp;given&nbsp;model&nbsp;class.</span></dd></dl>
452
+
453
+ <dl><dt><a name="ModelRequest-model_parametrized_name"><strong>model_parametrized_name</strong></a>(params: 'tuple[type[Any], ...]') -&gt; 'str'</dt><dd><span class="code">Compute&nbsp;the&nbsp;class&nbsp;name&nbsp;for&nbsp;parametrizations&nbsp;of&nbsp;generic&nbsp;classes.<br>
454
+ &nbsp;<br>
455
+ This&nbsp;method&nbsp;can&nbsp;be&nbsp;overridden&nbsp;to&nbsp;achieve&nbsp;a&nbsp;custom&nbsp;naming&nbsp;scheme&nbsp;for&nbsp;generic&nbsp;BaseModels.<br>
456
+ &nbsp;<br>
457
+ Args:<br>
458
+ &nbsp;&nbsp;&nbsp;&nbsp;params:&nbsp;Tuple&nbsp;of&nbsp;types&nbsp;of&nbsp;the&nbsp;class.&nbsp;Given&nbsp;a&nbsp;generic&nbsp;class<br>
459
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`Model`&nbsp;with&nbsp;2&nbsp;type&nbsp;variables&nbsp;and&nbsp;a&nbsp;concrete&nbsp;model&nbsp;`Model[str,&nbsp;int]`,<br>
460
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;the&nbsp;value&nbsp;`(str,&nbsp;int)`&nbsp;would&nbsp;be&nbsp;passed&nbsp;to&nbsp;`params`.<br>
461
+ &nbsp;<br>
462
+ Returns:<br>
463
+ &nbsp;&nbsp;&nbsp;&nbsp;String&nbsp;representing&nbsp;the&nbsp;new&nbsp;class&nbsp;where&nbsp;`params`&nbsp;are&nbsp;passed&nbsp;to&nbsp;`cls`&nbsp;as&nbsp;type&nbsp;variables.<br>
464
+ &nbsp;<br>
465
+ Raises:<br>
466
+ &nbsp;&nbsp;&nbsp;&nbsp;TypeError:&nbsp;Raised&nbsp;when&nbsp;trying&nbsp;to&nbsp;generate&nbsp;concrete&nbsp;names&nbsp;for&nbsp;non-generic&nbsp;models.</span></dd></dl>
467
+
468
+ <dl><dt><a name="ModelRequest-model_rebuild"><strong>model_rebuild</strong></a>(
469
+ *,
470
+ force: 'bool' = False,
471
+ raise_errors: 'bool' = True,
472
+ _parent_namespace_depth: 'int' = 2,
473
+ _types_namespace: 'MappingNamespace | None' = None
474
+ ) -&gt; 'bool | None'</dt><dd><span class="code">Try&nbsp;to&nbsp;rebuild&nbsp;the&nbsp;pydantic-core&nbsp;schema&nbsp;for&nbsp;the&nbsp;model.<br>
475
+ &nbsp;<br>
476
+ This&nbsp;may&nbsp;be&nbsp;necessary&nbsp;when&nbsp;one&nbsp;of&nbsp;the&nbsp;annotations&nbsp;is&nbsp;a&nbsp;ForwardRef&nbsp;which&nbsp;could&nbsp;not&nbsp;be&nbsp;resolved&nbsp;during<br>
477
+ the&nbsp;initial&nbsp;attempt&nbsp;to&nbsp;build&nbsp;the&nbsp;schema,&nbsp;and&nbsp;automatic&nbsp;rebuilding&nbsp;fails.<br>
478
+ &nbsp;<br>
479
+ Args:<br>
480
+ &nbsp;&nbsp;&nbsp;&nbsp;force:&nbsp;Whether&nbsp;to&nbsp;force&nbsp;the&nbsp;rebuilding&nbsp;of&nbsp;the&nbsp;model&nbsp;schema,&nbsp;defaults&nbsp;to&nbsp;`False`.<br>
481
+ &nbsp;&nbsp;&nbsp;&nbsp;raise_errors:&nbsp;Whether&nbsp;to&nbsp;raise&nbsp;errors,&nbsp;defaults&nbsp;to&nbsp;`True`.<br>
482
+ &nbsp;&nbsp;&nbsp;&nbsp;_parent_namespace_depth:&nbsp;The&nbsp;depth&nbsp;level&nbsp;of&nbsp;the&nbsp;parent&nbsp;namespace,&nbsp;defaults&nbsp;to&nbsp;2.<br>
483
+ &nbsp;&nbsp;&nbsp;&nbsp;_types_namespace:&nbsp;The&nbsp;types&nbsp;namespace,&nbsp;defaults&nbsp;to&nbsp;`None`.<br>
484
+ &nbsp;<br>
485
+ Returns:<br>
486
+ &nbsp;&nbsp;&nbsp;&nbsp;Returns&nbsp;`None`&nbsp;if&nbsp;the&nbsp;schema&nbsp;is&nbsp;already&nbsp;"complete"&nbsp;and&nbsp;rebuilding&nbsp;was&nbsp;not&nbsp;required.<br>
487
+ &nbsp;&nbsp;&nbsp;&nbsp;If&nbsp;rebuilding&nbsp;_was_&nbsp;required,&nbsp;returns&nbsp;`True`&nbsp;if&nbsp;rebuilding&nbsp;was&nbsp;successful,&nbsp;otherwise&nbsp;`False`.</span></dd></dl>
488
+
489
+ <dl><dt><a name="ModelRequest-model_validate"><strong>model_validate</strong></a>(
490
+ obj: 'Any',
491
+ *,
492
+ strict: 'bool | None' = None,
493
+ extra: 'ExtraValues | None' = None,
494
+ from_attributes: 'bool | None' = None,
495
+ context: 'Any | None' = None,
496
+ by_alias: 'bool | None' = None,
497
+ by_name: 'bool | None' = None
498
+ ) -&gt; 'Self'</dt><dd><span class="code">Validate&nbsp;a&nbsp;pydantic&nbsp;model&nbsp;instance.<br>
499
+ &nbsp;<br>
500
+ Args:<br>
501
+ &nbsp;&nbsp;&nbsp;&nbsp;obj:&nbsp;The&nbsp;object&nbsp;to&nbsp;validate.<br>
502
+ &nbsp;&nbsp;&nbsp;&nbsp;strict:&nbsp;Whether&nbsp;to&nbsp;enforce&nbsp;types&nbsp;strictly.<br>
503
+ &nbsp;&nbsp;&nbsp;&nbsp;extra:&nbsp;Whether&nbsp;to&nbsp;ignore,&nbsp;allow,&nbsp;or&nbsp;forbid&nbsp;extra&nbsp;data&nbsp;during&nbsp;model&nbsp;validation.<br>
504
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;See&nbsp;the&nbsp;[`extra`&nbsp;configuration&nbsp;value][pydantic.ConfigDict.extra]&nbsp;for&nbsp;details.<br>
505
+ &nbsp;&nbsp;&nbsp;&nbsp;from_attributes:&nbsp;Whether&nbsp;to&nbsp;extract&nbsp;data&nbsp;from&nbsp;object&nbsp;attributes.<br>
506
+ &nbsp;&nbsp;&nbsp;&nbsp;context:&nbsp;Additional&nbsp;context&nbsp;to&nbsp;pass&nbsp;to&nbsp;the&nbsp;validator.<br>
507
+ &nbsp;&nbsp;&nbsp;&nbsp;by_alias:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;the&nbsp;field's&nbsp;alias&nbsp;when&nbsp;validating&nbsp;against&nbsp;the&nbsp;provided&nbsp;input&nbsp;data.<br>
508
+ &nbsp;&nbsp;&nbsp;&nbsp;by_name:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;the&nbsp;field's&nbsp;name&nbsp;when&nbsp;validating&nbsp;against&nbsp;the&nbsp;provided&nbsp;input&nbsp;data.<br>
509
+ &nbsp;<br>
510
+ Raises:<br>
511
+ &nbsp;&nbsp;&nbsp;&nbsp;ValidationError:&nbsp;If&nbsp;the&nbsp;object&nbsp;could&nbsp;not&nbsp;be&nbsp;validated.<br>
512
+ &nbsp;<br>
513
+ Returns:<br>
514
+ &nbsp;&nbsp;&nbsp;&nbsp;The&nbsp;validated&nbsp;model&nbsp;instance.</span></dd></dl>
515
+
516
+ <dl><dt><a name="ModelRequest-model_validate_json"><strong>model_validate_json</strong></a>(
517
+ json_data: 'str | bytes | bytearray',
518
+ *,
519
+ strict: 'bool | None' = None,
520
+ extra: 'ExtraValues | None' = None,
521
+ context: 'Any | None' = None,
522
+ by_alias: 'bool | None' = None,
523
+ by_name: 'bool | None' = None
524
+ ) -&gt; 'Self'</dt><dd><span class="code">!!!&nbsp;abstract&nbsp;"Usage&nbsp;Documentation"<br>
525
+ &nbsp;&nbsp;&nbsp;&nbsp;[JSON&nbsp;Parsing](../concepts/json.md#json-parsing)<br>
526
+ &nbsp;<br>
527
+ Validate&nbsp;the&nbsp;given&nbsp;JSON&nbsp;data&nbsp;against&nbsp;the&nbsp;Pydantic&nbsp;model.<br>
528
+ &nbsp;<br>
529
+ Args:<br>
530
+ &nbsp;&nbsp;&nbsp;&nbsp;json_data:&nbsp;The&nbsp;JSON&nbsp;data&nbsp;to&nbsp;validate.<br>
531
+ &nbsp;&nbsp;&nbsp;&nbsp;strict:&nbsp;Whether&nbsp;to&nbsp;enforce&nbsp;types&nbsp;strictly.<br>
532
+ &nbsp;&nbsp;&nbsp;&nbsp;extra:&nbsp;Whether&nbsp;to&nbsp;ignore,&nbsp;allow,&nbsp;or&nbsp;forbid&nbsp;extra&nbsp;data&nbsp;during&nbsp;model&nbsp;validation.<br>
533
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;See&nbsp;the&nbsp;[`extra`&nbsp;configuration&nbsp;value][pydantic.ConfigDict.extra]&nbsp;for&nbsp;details.<br>
534
+ &nbsp;&nbsp;&nbsp;&nbsp;context:&nbsp;Extra&nbsp;variables&nbsp;to&nbsp;pass&nbsp;to&nbsp;the&nbsp;validator.<br>
535
+ &nbsp;&nbsp;&nbsp;&nbsp;by_alias:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;the&nbsp;field's&nbsp;alias&nbsp;when&nbsp;validating&nbsp;against&nbsp;the&nbsp;provided&nbsp;input&nbsp;data.<br>
536
+ &nbsp;&nbsp;&nbsp;&nbsp;by_name:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;the&nbsp;field's&nbsp;name&nbsp;when&nbsp;validating&nbsp;against&nbsp;the&nbsp;provided&nbsp;input&nbsp;data.<br>
537
+ &nbsp;<br>
538
+ Returns:<br>
539
+ &nbsp;&nbsp;&nbsp;&nbsp;The&nbsp;validated&nbsp;Pydantic&nbsp;model.<br>
540
+ &nbsp;<br>
541
+ Raises:<br>
542
+ &nbsp;&nbsp;&nbsp;&nbsp;ValidationError:&nbsp;If&nbsp;`json_data`&nbsp;is&nbsp;not&nbsp;a&nbsp;JSON&nbsp;string&nbsp;or&nbsp;the&nbsp;object&nbsp;could&nbsp;not&nbsp;be&nbsp;validated.</span></dd></dl>
543
+
544
+ <dl><dt><a name="ModelRequest-model_validate_strings"><strong>model_validate_strings</strong></a>(
545
+ obj: 'Any',
546
+ *,
547
+ strict: 'bool | None' = None,
548
+ extra: 'ExtraValues | None' = None,
549
+ context: 'Any | None' = None,
550
+ by_alias: 'bool | None' = None,
551
+ by_name: 'bool | None' = None
552
+ ) -&gt; 'Self'</dt><dd><span class="code">Validate&nbsp;the&nbsp;given&nbsp;object&nbsp;with&nbsp;string&nbsp;data&nbsp;against&nbsp;the&nbsp;Pydantic&nbsp;model.<br>
553
+ &nbsp;<br>
554
+ Args:<br>
555
+ &nbsp;&nbsp;&nbsp;&nbsp;obj:&nbsp;The&nbsp;object&nbsp;containing&nbsp;string&nbsp;data&nbsp;to&nbsp;validate.<br>
556
+ &nbsp;&nbsp;&nbsp;&nbsp;strict:&nbsp;Whether&nbsp;to&nbsp;enforce&nbsp;types&nbsp;strictly.<br>
557
+ &nbsp;&nbsp;&nbsp;&nbsp;extra:&nbsp;Whether&nbsp;to&nbsp;ignore,&nbsp;allow,&nbsp;or&nbsp;forbid&nbsp;extra&nbsp;data&nbsp;during&nbsp;model&nbsp;validation.<br>
558
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;See&nbsp;the&nbsp;[`extra`&nbsp;configuration&nbsp;value][pydantic.ConfigDict.extra]&nbsp;for&nbsp;details.<br>
559
+ &nbsp;&nbsp;&nbsp;&nbsp;context:&nbsp;Extra&nbsp;variables&nbsp;to&nbsp;pass&nbsp;to&nbsp;the&nbsp;validator.<br>
560
+ &nbsp;&nbsp;&nbsp;&nbsp;by_alias:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;the&nbsp;field's&nbsp;alias&nbsp;when&nbsp;validating&nbsp;against&nbsp;the&nbsp;provided&nbsp;input&nbsp;data.<br>
561
+ &nbsp;&nbsp;&nbsp;&nbsp;by_name:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;the&nbsp;field's&nbsp;name&nbsp;when&nbsp;validating&nbsp;against&nbsp;the&nbsp;provided&nbsp;input&nbsp;data.<br>
562
+ &nbsp;<br>
563
+ Returns:<br>
564
+ &nbsp;&nbsp;&nbsp;&nbsp;The&nbsp;validated&nbsp;Pydantic&nbsp;model.</span></dd></dl>
565
+
566
+ <dl><dt><a name="ModelRequest-parse_file"><strong>parse_file</strong></a>(
567
+ path: 'str | Path',
568
+ *,
569
+ content_type: 'str | None' = None,
570
+ encoding: 'str' = 'utf8',
571
+ proto: 'DeprecatedParseProtocol | None' = None,
572
+ allow_pickle: 'bool' = False
573
+ ) -&gt; 'Self'</dt></dl>
574
+
575
+ <dl><dt><a name="ModelRequest-parse_obj"><strong>parse_obj</strong></a>(obj: 'Any') -&gt; 'Self'</dt></dl>
576
+
577
+ <dl><dt><a name="ModelRequest-parse_raw"><strong>parse_raw</strong></a>(
578
+ b: 'str | bytes',
579
+ *,
580
+ content_type: 'str | None' = None,
581
+ encoding: 'str' = 'utf8',
582
+ proto: 'DeprecatedParseProtocol | None' = None,
583
+ allow_pickle: 'bool' = False
584
+ ) -&gt; 'Self'</dt></dl>
585
+
586
+ <dl><dt><a name="ModelRequest-schema"><strong>schema</strong></a>(by_alias: 'bool' = True, ref_template: 'str' = '#/$defs/{model}') -&gt; 'Dict[str, Any]'</dt></dl>
587
+
588
+ <dl><dt><a name="ModelRequest-schema_json"><strong>schema_json</strong></a>(
589
+ *,
590
+ by_alias: 'bool' = True,
591
+ ref_template: 'str' = '#/$defs/{model}',
592
+ **dumps_kwargs: 'Any'
593
+ ) -&gt; 'str'</dt></dl>
594
+
595
+ <dl><dt><a name="ModelRequest-update_forward_refs"><strong>update_forward_refs</strong></a>(**localns: 'Any') -&gt; 'None'</dt></dl>
596
+
597
+ <dl><dt><a name="ModelRequest-validate"><strong>validate</strong></a>(value: 'Any') -&gt; 'Self'</dt></dl>
598
+
599
+ <hr>
600
+ Readonly properties inherited from <a href="pydantic.main.html#BaseModel">pydantic.main.BaseModel</a>:<br>
601
+ <dl><dt><strong>__fields_set__</strong></dt>
602
+ </dl>
603
+ <dl><dt><strong>model_extra</strong></dt>
604
+ <dd><span class="code">Get&nbsp;extra&nbsp;fields&nbsp;set&nbsp;during&nbsp;validation.<br>
605
+ &nbsp;<br>
606
+ Returns:<br>
607
+ &nbsp;&nbsp;&nbsp;&nbsp;A&nbsp;dictionary&nbsp;of&nbsp;extra&nbsp;fields,&nbsp;or&nbsp;`None`&nbsp;if&nbsp;`config.extra`&nbsp;is&nbsp;not&nbsp;set&nbsp;to&nbsp;`"allow"`.</span></dd>
608
+ </dl>
609
+ <dl><dt><strong>model_fields_set</strong></dt>
610
+ <dd><span class="code">Returns&nbsp;the&nbsp;set&nbsp;of&nbsp;fields&nbsp;that&nbsp;have&nbsp;been&nbsp;explicitly&nbsp;set&nbsp;on&nbsp;this&nbsp;model&nbsp;instance.<br>
611
+ &nbsp;<br>
612
+ Returns:<br>
613
+ &nbsp;&nbsp;&nbsp;&nbsp;A&nbsp;set&nbsp;of&nbsp;strings&nbsp;representing&nbsp;the&nbsp;fields&nbsp;that&nbsp;have&nbsp;been&nbsp;set,<br>
614
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;i.e.&nbsp;that&nbsp;were&nbsp;not&nbsp;filled&nbsp;from&nbsp;defaults.</span></dd>
615
+ </dl>
616
+ <hr>
617
+ Data descriptors inherited from <a href="pydantic.main.html#BaseModel">pydantic.main.BaseModel</a>:<br>
618
+ <dl><dt><strong>__dict__</strong></dt>
619
+ <dd><span class="code">dictionary&nbsp;for&nbsp;instance&nbsp;variables</span></dd>
620
+ </dl>
621
+ <dl><dt><strong>__pydantic_extra__</strong></dt>
622
+ </dl>
623
+ <dl><dt><strong>__pydantic_fields_set__</strong></dt>
624
+ </dl>
625
+ <dl><dt><strong>__pydantic_private__</strong></dt>
626
+ </dl>
627
+ <hr>
628
+ Data and other attributes inherited from <a href="pydantic.main.html#BaseModel">pydantic.main.BaseModel</a>:<br>
629
+ <dl><dt><strong>__hash__</strong> = None</dl>
630
+
631
+ <dl><dt><strong>__pydantic_root_model__</strong> = False</dl>
632
+
633
+ <dl><dt><strong>model_computed_fields</strong> = {}</dl>
634
+
635
+ <dl><dt><strong>model_fields</strong> = {'age': FieldInfo(annotation=int, required=True), 'anciennete_sans_promotion': FieldInfo(annotation=float, required=True), 'annee_experience_totale': FieldInfo(annotation=int, required=True), 'annees_dans_l_entreprise': FieldInfo(annotation=int, required=True), 'annees_dans_le_poste_actuel': FieldInfo(annotation=int, required=True), 'annees_depuis_la_derniere_promotion': FieldInfo(annotation=int, required=True), 'annees_sous_responsable_actuel': FieldInfo(annotation=int, required=True), 'augmentation_salaire_precedente': FieldInfo(annotation=float, required=True), 'departement': FieldInfo(annotation=str, required=True), 'distance_domicile_travail': FieldInfo(annotation=int, required=True), ...}</dl>
636
+
637
+ </td></tr></table> <p>
638
+ <table class="section">
639
+ <tr class="decor title-decor heading-text">
640
+ <td class="section-title" colspan=3>&nbsp;<br><a name="ModelResponse">class <strong>ModelResponse</strong></a>(<a href="pydantic.main.html#BaseModel">pydantic.main.BaseModel</a>)</td></tr>
641
+
642
+ <tr><td class="decor title-decor" rowspan=2><span class="code">&nbsp;&nbsp;&nbsp;</span></td>
643
+ <td class="decor title-decor" colspan=2><span class="code"><a href="#ModelResponse">ModelResponse</a>(<br>
644
+ &nbsp;&nbsp;&nbsp;&nbsp;*,<br>
645
+ &nbsp;&nbsp;&nbsp;&nbsp;employee_id:&nbsp;Optional[int]&nbsp;=&nbsp;None,<br>
646
+ &nbsp;&nbsp;&nbsp;&nbsp;turnover_probability:&nbsp;float,<br>
647
+ &nbsp;&nbsp;&nbsp;&nbsp;will_leave:&nbsp;bool<br>
648
+ )&nbsp;-&amp;gt;&nbsp;None<br>
649
+ &nbsp;<br>
650
+ #&nbsp;Le&nbsp;modèle&nbsp;de&nbsp;réponse&nbsp;inclut&nbsp;la&nbsp;probabilité&nbsp;de&nbsp;départ&nbsp;et&nbsp;la&nbsp;classe&nbsp;prédite&nbsp;(will_leave),&nbsp;ainsi&nbsp;que&nbsp;l'employee_id&nbsp;si&nbsp;disponible.<br>&nbsp;</span></td></tr>
651
+ <tr><td>&nbsp;</td>
652
+ <td class="singlecolumn"><dl><dt>Method resolution order:</dt>
653
+ <dd><a href="domain.domain.html#ModelResponse">ModelResponse</a></dd>
654
+ <dd><a href="pydantic.main.html#BaseModel">pydantic.main.BaseModel</a></dd>
655
+ <dd><a href="builtins.html#object">builtins.object</a></dd>
656
+ </dl>
657
+ <hr>
658
+ Data descriptors defined here:<br>
659
+ <dl><dt><strong>__weakref__</strong></dt>
660
+ <dd><span class="code">list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object</span></dd>
661
+ </dl>
662
+ <hr>
663
+ Data and other attributes defined here:<br>
664
+ <dl><dt><strong>__abstractmethods__</strong> = frozenset()</dl>
665
+
666
+ <dl><dt><strong>__annotations__</strong> = {'employee_id': 'Optional[int]', 'turnover_probability': 'float', 'will_leave': 'bool'}</dl>
667
+
668
+ <dl><dt><strong>__class_vars__</strong> = set()</dl>
669
+
670
+ <dl><dt><strong>__private_attributes__</strong> = {}</dl>
671
+
672
+ <dl><dt><strong>__pydantic_complete__</strong> = True</dl>
673
+
674
+ <dl><dt><strong>__pydantic_computed_fields__</strong> = {}</dl>
675
+
676
+ <dl><dt><strong>__pydantic_core_schema__</strong> = {'cls': &lt;class 'domain.domain.ModelResponse'&gt;, 'config': {'title': 'ModelResponse'}, 'custom_init': False, 'metadata': {'pydantic_js_functions': [&lt;bound method BaseModel.__get_pydantic_json_schema__ of &lt;class 'domain.domain.ModelResponse'&gt;&gt;]}, 'ref': 'domain.domain.ModelResponse:2430588436352', 'root_model': False, 'schema': {'computed_fields': [], 'fields': {'employee_id': {'metadata': {}, 'schema': {'default': None, 'schema': {'schema': {...}, 'type': 'nullable'}, 'type': 'default'}, 'type': 'model-field'}, 'turnover_probability': {'metadata': {}, 'schema': {'type': 'float'}, 'type': 'model-field'}, 'will_leave': {'metadata': {}, 'schema': {'type': 'bool'}, 'type': 'model-field'}}, 'model_name': 'ModelResponse', 'type': 'model-fields'}, 'type': 'model'}</dl>
677
+
678
+ <dl><dt><strong>__pydantic_custom_init__</strong> = False</dl>
679
+
680
+ <dl><dt><strong>__pydantic_decorators__</strong> = DecoratorInfos(validators={}, field_validators={...zers={}, model_validators={}, computed_fields={})</dl>
681
+
682
+ <dl><dt><strong>__pydantic_fields__</strong> = {'employee_id': FieldInfo(annotation=Union[int, NoneType], required=False, default=None), 'turnover_probability': FieldInfo(annotation=float, required=True), 'will_leave': FieldInfo(annotation=bool, required=True)}</dl>
683
+
684
+ <dl><dt><strong>__pydantic_generic_metadata__</strong> = {'args': (), 'origin': None, 'parameters': ()}</dl>
685
+
686
+ <dl><dt><strong>__pydantic_parent_namespace__</strong> = None</dl>
687
+
688
+ <dl><dt><strong>__pydantic_post_init__</strong> = None</dl>
689
+
690
+ <dl><dt><strong>__pydantic_serializer__</strong> = SchemaSerializer(serializer=Model(
691
+ ModelSeri... name: "ModelResponse",
692
+ },
693
+ ), definitions=[])</dl>
694
+
695
+ <dl><dt><strong>__pydantic_setattr_handlers__</strong> = {}</dl>
696
+
697
+ <dl><dt><strong>__pydantic_validator__</strong> = SchemaValidator(title="ModelResponse", validator...e",
698
+ },
699
+ ), definitions=[], cache_strings=True)</dl>
700
+
701
+ <dl><dt><strong>__signature__</strong> = &lt;Signature (*, employee_id: Optional[int] = None...er_probability: float, will_leave: bool) -&gt; None&gt;</dl>
702
+
703
+ <dl><dt><strong>model_config</strong> = {}</dl>
704
+
705
+ <hr>
706
+ Methods inherited from <a href="pydantic.main.html#BaseModel">pydantic.main.BaseModel</a>:<br>
707
+ <dl><dt><a name="ModelResponse-__copy__"><strong>__copy__</strong></a>(self) -&gt; 'Self'</dt><dd><span class="code">Returns&nbsp;a&nbsp;shallow&nbsp;copy&nbsp;of&nbsp;the&nbsp;model.</span></dd></dl>
708
+
709
+ <dl><dt><a name="ModelResponse-__deepcopy__"><strong>__deepcopy__</strong></a>(self, memo: 'dict[int, Any] | None' = None) -&gt; 'Self'</dt><dd><span class="code">Returns&nbsp;a&nbsp;deep&nbsp;copy&nbsp;of&nbsp;the&nbsp;model.</span></dd></dl>
710
+
711
+ <dl><dt><a name="ModelResponse-__delattr__"><strong>__delattr__</strong></a>(self, item: 'str') -&gt; 'Any'</dt><dd><span class="code">Implement&nbsp;delattr(self,&nbsp;name).</span></dd></dl>
712
+
713
+ <dl><dt><a name="ModelResponse-__eq__"><strong>__eq__</strong></a>(self, other: 'Any') -&gt; 'bool'</dt><dd><span class="code">Return&nbsp;self==value.</span></dd></dl>
714
+
715
+ <dl><dt><a name="ModelResponse-__getattr__"><strong>__getattr__</strong></a>(self, item: 'str') -&gt; 'Any'</dt></dl>
716
+
717
+ <dl><dt><a name="ModelResponse-__getstate__"><strong>__getstate__</strong></a>(self) -&gt; 'dict[Any, Any]'</dt><dd><span class="code">Helper&nbsp;for&nbsp;pickle.</span></dd></dl>
718
+
719
+ <dl><dt><a name="ModelResponse-__init__"><strong>__init__</strong></a>(self, /, **data: 'Any') -&gt; 'None'</dt><dd><span class="code">Create&nbsp;a&nbsp;new&nbsp;model&nbsp;by&nbsp;parsing&nbsp;and&nbsp;validating&nbsp;input&nbsp;data&nbsp;from&nbsp;keyword&nbsp;arguments.<br>
720
+ &nbsp;<br>
721
+ Raises&nbsp;[`ValidationError`][pydantic_core.ValidationError]&nbsp;if&nbsp;the&nbsp;input&nbsp;data&nbsp;cannot&nbsp;be<br>
722
+ validated&nbsp;to&nbsp;form&nbsp;a&nbsp;valid&nbsp;model.<br>
723
+ &nbsp;<br>
724
+ `self`&nbsp;is&nbsp;explicitly&nbsp;positional-only&nbsp;to&nbsp;allow&nbsp;`self`&nbsp;as&nbsp;a&nbsp;field&nbsp;name.</span></dd></dl>
725
+
726
+ <dl><dt><a name="ModelResponse-__iter__"><strong>__iter__</strong></a>(self) -&gt; 'TupleGenerator'</dt><dd><span class="code">So&nbsp;`<a href="#ModelResponse-dict">dict</a>(model)`&nbsp;works.</span></dd></dl>
727
+
728
+ <dl><dt><a name="ModelResponse-__pretty__"><strong>__pretty__</strong></a>(self, fmt: 'Callable[[Any], Any]', **kwargs: 'Any') -&gt; 'Generator[Any]'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Used&nbsp;by&nbsp;devtools&nbsp;(<a href="https://python-devtools.helpmanual.io/">https://python-devtools.helpmanual.io/</a>)&nbsp;to&nbsp;pretty&nbsp;print&nbsp;objects.</span></dd></dl>
729
+
730
+ <dl><dt><a name="ModelResponse-__replace__"><strong>__replace__</strong></a>(self, **changes: 'Any') -&gt; 'Self'</dt><dd><span class="code">#&nbsp;Because&nbsp;we&nbsp;make&nbsp;use&nbsp;of&nbsp;`@dataclass_transform()`,&nbsp;`__replace__`&nbsp;is&nbsp;already&nbsp;synthesized&nbsp;by<br>
731
+ #&nbsp;type&nbsp;checkers,&nbsp;so&nbsp;we&nbsp;define&nbsp;the&nbsp;implementation&nbsp;in&nbsp;this&nbsp;`if&nbsp;not&nbsp;TYPE_CHECKING:`&nbsp;block:</span></dd></dl>
732
+
733
+ <dl><dt><a name="ModelResponse-__repr__"><strong>__repr__</strong></a>(self) -&gt; 'str'</dt><dd><span class="code">Return&nbsp;repr(self).</span></dd></dl>
734
+
735
+ <dl><dt><a name="ModelResponse-__repr_args__"><strong>__repr_args__</strong></a>(self) -&gt; '_repr.ReprArgs'</dt></dl>
736
+
737
+ <dl><dt><a name="ModelResponse-__repr_name__"><strong>__repr_name__</strong></a>(self) -&gt; 'str'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Name&nbsp;of&nbsp;the&nbsp;instance's&nbsp;class,&nbsp;used&nbsp;in&nbsp;__repr__.</span></dd></dl>
738
+
739
+ <dl><dt><a name="ModelResponse-__repr_recursion__"><strong>__repr_recursion__</strong></a>(self, object: 'Any') -&gt; 'str'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Returns&nbsp;the&nbsp;string&nbsp;representation&nbsp;of&nbsp;a&nbsp;recursive&nbsp;object.</span></dd></dl>
740
+
741
+ <dl><dt><a name="ModelResponse-__repr_str__"><strong>__repr_str__</strong></a>(self, join_str: 'str') -&gt; 'str'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt></dl>
742
+
743
+ <dl><dt><a name="ModelResponse-__rich_repr__"><strong>__rich_repr__</strong></a>(self) -&gt; 'RichReprResult'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Used&nbsp;by&nbsp;Rich&nbsp;(<a href="https://rich.readthedocs.io/en/stable/pretty.html">https://rich.readthedocs.io/en/stable/pretty.html</a>)&nbsp;to&nbsp;pretty&nbsp;print&nbsp;objects.</span></dd></dl>
744
+
745
+ <dl><dt><a name="ModelResponse-__setattr__"><strong>__setattr__</strong></a>(self, name: 'str', value: 'Any') -&gt; 'None'</dt><dd><span class="code">Implement&nbsp;setattr(self,&nbsp;name,&nbsp;value).</span></dd></dl>
746
+
747
+ <dl><dt><a name="ModelResponse-__setstate__"><strong>__setstate__</strong></a>(self, state: 'dict[Any, Any]') -&gt; 'None'</dt></dl>
748
+
749
+ <dl><dt><a name="ModelResponse-__str__"><strong>__str__</strong></a>(self) -&gt; 'str'</dt><dd><span class="code">Return&nbsp;str(self).</span></dd></dl>
750
+
751
+ <dl><dt><a name="ModelResponse-copy"><strong>copy</strong></a>(
752
+ self,
753
+ *,
754
+ include: 'AbstractSetIntStr | MappingIntStrAny | None' = None,
755
+ exclude: 'AbstractSetIntStr | MappingIntStrAny | None' = None,
756
+ update: 'Dict[str, Any] | None' = None,
757
+ deep: 'bool' = False
758
+ ) -&gt; 'Self'</dt><dd><span class="code">Returns&nbsp;a&nbsp;copy&nbsp;of&nbsp;the&nbsp;model.<br>
759
+ &nbsp;<br>
760
+ !!!&nbsp;warning&nbsp;"Deprecated"<br>
761
+ &nbsp;&nbsp;&nbsp;&nbsp;This&nbsp;method&nbsp;is&nbsp;now&nbsp;deprecated;&nbsp;use&nbsp;`model_copy`&nbsp;instead.<br>
762
+ &nbsp;<br>
763
+ If&nbsp;you&nbsp;need&nbsp;`include`&nbsp;or&nbsp;`exclude`,&nbsp;use:<br>
764
+ &nbsp;<br>
765
+ ```python&nbsp;{test="skip"&nbsp;lint="skip"}<br>
766
+ data&nbsp;=&nbsp;self.<a href="#ModelResponse-model_dump">model_dump</a>(include=include,&nbsp;exclude=exclude,&nbsp;round_trip=True)<br>
767
+ data&nbsp;=&nbsp;{**data,&nbsp;**(update&nbsp;or&nbsp;{})}<br>
768
+ copied&nbsp;=&nbsp;self.<a href="#ModelResponse-model_validate">model_validate</a>(data)<br>
769
+ ```<br>
770
+ &nbsp;<br>
771
+ Args:<br>
772
+ &nbsp;&nbsp;&nbsp;&nbsp;include:&nbsp;Optional&nbsp;set&nbsp;or&nbsp;mapping&nbsp;specifying&nbsp;which&nbsp;fields&nbsp;to&nbsp;include&nbsp;in&nbsp;the&nbsp;copied&nbsp;model.<br>
773
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude:&nbsp;Optional&nbsp;set&nbsp;or&nbsp;mapping&nbsp;specifying&nbsp;which&nbsp;fields&nbsp;to&nbsp;exclude&nbsp;in&nbsp;the&nbsp;copied&nbsp;model.<br>
774
+ &nbsp;&nbsp;&nbsp;&nbsp;update:&nbsp;Optional&nbsp;dictionary&nbsp;of&nbsp;field-value&nbsp;pairs&nbsp;to&nbsp;override&nbsp;field&nbsp;values&nbsp;in&nbsp;the&nbsp;copied&nbsp;model.<br>
775
+ &nbsp;&nbsp;&nbsp;&nbsp;deep:&nbsp;If&nbsp;True,&nbsp;the&nbsp;values&nbsp;of&nbsp;fields&nbsp;that&nbsp;are&nbsp;Pydantic&nbsp;models&nbsp;will&nbsp;be&nbsp;deep-copied.<br>
776
+ &nbsp;<br>
777
+ Returns:<br>
778
+ &nbsp;&nbsp;&nbsp;&nbsp;A&nbsp;copy&nbsp;of&nbsp;the&nbsp;model&nbsp;with&nbsp;included,&nbsp;excluded&nbsp;and&nbsp;updated&nbsp;fields&nbsp;as&nbsp;specified.</span></dd></dl>
779
+
780
+ <dl><dt><a name="ModelResponse-dict"><strong>dict</strong></a>(
781
+ self,
782
+ *,
783
+ include: 'IncEx | None' = None,
784
+ exclude: 'IncEx | None' = None,
785
+ by_alias: 'bool' = False,
786
+ exclude_unset: 'bool' = False,
787
+ exclude_defaults: 'bool' = False,
788
+ exclude_none: 'bool' = False
789
+ ) -&gt; 'Dict[str, Any]'</dt></dl>
790
+
791
+ <dl><dt><a name="ModelResponse-json"><strong>json</strong></a>(
792
+ self,
793
+ *,
794
+ include: 'IncEx | None' = None,
795
+ exclude: 'IncEx | None' = None,
796
+ by_alias: 'bool' = False,
797
+ exclude_unset: 'bool' = False,
798
+ exclude_defaults: 'bool' = False,
799
+ exclude_none: 'bool' = False,
800
+ encoder: 'Callable[[Any], Any] | None' = PydanticUndefined,
801
+ models_as_dict: 'bool' = PydanticUndefined,
802
+ **dumps_kwargs: 'Any'
803
+ ) -&gt; 'str'</dt></dl>
804
+
805
+ <dl><dt><a name="ModelResponse-model_copy"><strong>model_copy</strong></a>(
806
+ self,
807
+ *,
808
+ update: 'Mapping[str, Any] | None' = None,
809
+ deep: 'bool' = False
810
+ ) -&gt; 'Self'</dt><dd><span class="code">!!!&nbsp;abstract&nbsp;"Usage&nbsp;Documentation"<br>
811
+ &nbsp;&nbsp;&nbsp;&nbsp;[`model_copy`](../concepts/models.md#model-copy)<br>
812
+ &nbsp;<br>
813
+ Returns&nbsp;a&nbsp;copy&nbsp;of&nbsp;the&nbsp;model.<br>
814
+ &nbsp;<br>
815
+ !!!&nbsp;note<br>
816
+ &nbsp;&nbsp;&nbsp;&nbsp;The&nbsp;underlying&nbsp;instance's&nbsp;[`__dict__`][object.__dict__]&nbsp;attribute&nbsp;is&nbsp;copied.&nbsp;This<br>
817
+ &nbsp;&nbsp;&nbsp;&nbsp;might&nbsp;have&nbsp;unexpected&nbsp;side&nbsp;effects&nbsp;if&nbsp;you&nbsp;store&nbsp;anything&nbsp;in&nbsp;it,&nbsp;on&nbsp;top&nbsp;of&nbsp;the&nbsp;model<br>
818
+ &nbsp;&nbsp;&nbsp;&nbsp;fields&nbsp;(e.g.&nbsp;the&nbsp;value&nbsp;of&nbsp;[cached&nbsp;properties][functools.cached_property]).<br>
819
+ &nbsp;<br>
820
+ Args:<br>
821
+ &nbsp;&nbsp;&nbsp;&nbsp;update:&nbsp;Values&nbsp;to&nbsp;change/add&nbsp;in&nbsp;the&nbsp;new&nbsp;model.&nbsp;Note:&nbsp;the&nbsp;data&nbsp;is&nbsp;not&nbsp;validated<br>
822
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;before&nbsp;creating&nbsp;the&nbsp;new&nbsp;model.&nbsp;You&nbsp;should&nbsp;trust&nbsp;this&nbsp;data.<br>
823
+ &nbsp;&nbsp;&nbsp;&nbsp;deep:&nbsp;Set&nbsp;to&nbsp;`True`&nbsp;to&nbsp;make&nbsp;a&nbsp;deep&nbsp;copy&nbsp;of&nbsp;the&nbsp;model.<br>
824
+ &nbsp;<br>
825
+ Returns:<br>
826
+ &nbsp;&nbsp;&nbsp;&nbsp;New&nbsp;model&nbsp;instance.</span></dd></dl>
827
+
828
+ <dl><dt><a name="ModelResponse-model_dump"><strong>model_dump</strong></a>(
829
+ self,
830
+ *,
831
+ mode: "Literal['json', 'python'] | str" = 'python',
832
+ include: 'IncEx | None' = None,
833
+ exclude: 'IncEx | None' = None,
834
+ context: 'Any | None' = None,
835
+ by_alias: 'bool | None' = None,
836
+ exclude_unset: 'bool' = False,
837
+ exclude_defaults: 'bool' = False,
838
+ exclude_none: 'bool' = False,
839
+ exclude_computed_fields: 'bool' = False,
840
+ round_trip: 'bool' = False,
841
+ warnings: "bool | Literal['none', 'warn', 'error']" = True,
842
+ fallback: 'Callable[[Any], Any] | None' = None,
843
+ serialize_as_any: 'bool' = False
844
+ ) -&gt; 'dict[str, Any]'</dt><dd><span class="code">!!!&nbsp;abstract&nbsp;"Usage&nbsp;Documentation"<br>
845
+ &nbsp;&nbsp;&nbsp;&nbsp;[`model_dump`](../concepts/serialization.md#python-mode)<br>
846
+ &nbsp;<br>
847
+ Generate&nbsp;a&nbsp;dictionary&nbsp;representation&nbsp;of&nbsp;the&nbsp;model,&nbsp;optionally&nbsp;specifying&nbsp;which&nbsp;fields&nbsp;to&nbsp;include&nbsp;or&nbsp;exclude.<br>
848
+ &nbsp;<br>
849
+ Args:<br>
850
+ &nbsp;&nbsp;&nbsp;&nbsp;mode:&nbsp;The&nbsp;mode&nbsp;in&nbsp;which&nbsp;`to_python`&nbsp;should&nbsp;run.<br>
851
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;If&nbsp;mode&nbsp;is&nbsp;'json',&nbsp;the&nbsp;output&nbsp;will&nbsp;only&nbsp;contain&nbsp;JSON&nbsp;serializable&nbsp;types.<br>
852
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;If&nbsp;mode&nbsp;is&nbsp;'python',&nbsp;the&nbsp;output&nbsp;may&nbsp;contain&nbsp;non-JSON-serializable&nbsp;Python&nbsp;objects.<br>
853
+ &nbsp;&nbsp;&nbsp;&nbsp;include:&nbsp;A&nbsp;set&nbsp;of&nbsp;fields&nbsp;to&nbsp;include&nbsp;in&nbsp;the&nbsp;output.<br>
854
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude:&nbsp;A&nbsp;set&nbsp;of&nbsp;fields&nbsp;to&nbsp;exclude&nbsp;from&nbsp;the&nbsp;output.<br>
855
+ &nbsp;&nbsp;&nbsp;&nbsp;context:&nbsp;Additional&nbsp;context&nbsp;to&nbsp;pass&nbsp;to&nbsp;the&nbsp;serializer.<br>
856
+ &nbsp;&nbsp;&nbsp;&nbsp;by_alias:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;the&nbsp;field's&nbsp;alias&nbsp;in&nbsp;the&nbsp;dictionary&nbsp;key&nbsp;if&nbsp;defined.<br>
857
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_unset:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;fields&nbsp;that&nbsp;have&nbsp;not&nbsp;been&nbsp;explicitly&nbsp;set.<br>
858
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_defaults:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;fields&nbsp;that&nbsp;are&nbsp;set&nbsp;to&nbsp;their&nbsp;default&nbsp;value.<br>
859
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_none:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;fields&nbsp;that&nbsp;have&nbsp;a&nbsp;value&nbsp;of&nbsp;`None`.<br>
860
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_computed_fields:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;computed&nbsp;fields.<br>
861
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;While&nbsp;this&nbsp;can&nbsp;be&nbsp;useful&nbsp;for&nbsp;round-tripping,&nbsp;it&nbsp;is&nbsp;usually&nbsp;recommended&nbsp;to&nbsp;use&nbsp;the&nbsp;dedicated<br>
862
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`round_trip`&nbsp;parameter&nbsp;instead.<br>
863
+ &nbsp;&nbsp;&nbsp;&nbsp;round_trip:&nbsp;If&nbsp;True,&nbsp;dumped&nbsp;values&nbsp;should&nbsp;be&nbsp;valid&nbsp;as&nbsp;input&nbsp;for&nbsp;non-idempotent&nbsp;types&nbsp;such&nbsp;as&nbsp;Json[T].<br>
864
+ &nbsp;&nbsp;&nbsp;&nbsp;warnings:&nbsp;How&nbsp;to&nbsp;handle&nbsp;serialization&nbsp;errors.&nbsp;False/"none"&nbsp;ignores&nbsp;them,&nbsp;True/"warn"&nbsp;logs&nbsp;errors,<br>
865
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"error"&nbsp;raises&nbsp;a&nbsp;[`PydanticSerializationError`][pydantic_core.PydanticSerializationError].<br>
866
+ &nbsp;&nbsp;&nbsp;&nbsp;fallback:&nbsp;A&nbsp;function&nbsp;to&nbsp;call&nbsp;when&nbsp;an&nbsp;unknown&nbsp;value&nbsp;is&nbsp;encountered.&nbsp;If&nbsp;not&nbsp;provided,<br>
867
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a&nbsp;[`PydanticSerializationError`][pydantic_core.PydanticSerializationError]&nbsp;error&nbsp;is&nbsp;raised.<br>
868
+ &nbsp;&nbsp;&nbsp;&nbsp;serialize_as_any:&nbsp;Whether&nbsp;to&nbsp;serialize&nbsp;fields&nbsp;with&nbsp;duck-typing&nbsp;serialization&nbsp;behavior.<br>
869
+ &nbsp;<br>
870
+ Returns:<br>
871
+ &nbsp;&nbsp;&nbsp;&nbsp;A&nbsp;dictionary&nbsp;representation&nbsp;of&nbsp;the&nbsp;model.</span></dd></dl>
872
+
873
+ <dl><dt><a name="ModelResponse-model_dump_json"><strong>model_dump_json</strong></a>(
874
+ self,
875
+ *,
876
+ indent: 'int | None' = None,
877
+ ensure_ascii: 'bool' = False,
878
+ include: 'IncEx | None' = None,
879
+ exclude: 'IncEx | None' = None,
880
+ context: 'Any | None' = None,
881
+ by_alias: 'bool | None' = None,
882
+ exclude_unset: 'bool' = False,
883
+ exclude_defaults: 'bool' = False,
884
+ exclude_none: 'bool' = False,
885
+ exclude_computed_fields: 'bool' = False,
886
+ round_trip: 'bool' = False,
887
+ warnings: "bool | Literal['none', 'warn', 'error']" = True,
888
+ fallback: 'Callable[[Any], Any] | None' = None,
889
+ serialize_as_any: 'bool' = False
890
+ ) -&gt; 'str'</dt><dd><span class="code">!!!&nbsp;abstract&nbsp;"Usage&nbsp;Documentation"<br>
891
+ &nbsp;&nbsp;&nbsp;&nbsp;[`model_dump_json`](../concepts/serialization.md#json-mode)<br>
892
+ &nbsp;<br>
893
+ Generates&nbsp;a&nbsp;JSON&nbsp;representation&nbsp;of&nbsp;the&nbsp;model&nbsp;using&nbsp;Pydantic's&nbsp;`to_json`&nbsp;method.<br>
894
+ &nbsp;<br>
895
+ Args:<br>
896
+ &nbsp;&nbsp;&nbsp;&nbsp;indent:&nbsp;Indentation&nbsp;to&nbsp;use&nbsp;in&nbsp;the&nbsp;JSON&nbsp;output.&nbsp;If&nbsp;None&nbsp;is&nbsp;passed,&nbsp;the&nbsp;output&nbsp;will&nbsp;be&nbsp;compact.<br>
897
+ &nbsp;&nbsp;&nbsp;&nbsp;ensure_ascii:&nbsp;If&nbsp;`True`,&nbsp;the&nbsp;output&nbsp;is&nbsp;guaranteed&nbsp;to&nbsp;have&nbsp;all&nbsp;incoming&nbsp;non-ASCII&nbsp;characters&nbsp;escaped.<br>
898
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;If&nbsp;`False`&nbsp;(the&nbsp;default),&nbsp;these&nbsp;characters&nbsp;will&nbsp;be&nbsp;output&nbsp;as-is.<br>
899
+ &nbsp;&nbsp;&nbsp;&nbsp;include:&nbsp;Field(s)&nbsp;to&nbsp;include&nbsp;in&nbsp;the&nbsp;JSON&nbsp;output.<br>
900
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude:&nbsp;Field(s)&nbsp;to&nbsp;exclude&nbsp;from&nbsp;the&nbsp;JSON&nbsp;output.<br>
901
+ &nbsp;&nbsp;&nbsp;&nbsp;context:&nbsp;Additional&nbsp;context&nbsp;to&nbsp;pass&nbsp;to&nbsp;the&nbsp;serializer.<br>
902
+ &nbsp;&nbsp;&nbsp;&nbsp;by_alias:&nbsp;Whether&nbsp;to&nbsp;serialize&nbsp;using&nbsp;field&nbsp;aliases.<br>
903
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_unset:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;fields&nbsp;that&nbsp;have&nbsp;not&nbsp;been&nbsp;explicitly&nbsp;set.<br>
904
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_defaults:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;fields&nbsp;that&nbsp;are&nbsp;set&nbsp;to&nbsp;their&nbsp;default&nbsp;value.<br>
905
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_none:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;fields&nbsp;that&nbsp;have&nbsp;a&nbsp;value&nbsp;of&nbsp;`None`.<br>
906
+ &nbsp;&nbsp;&nbsp;&nbsp;exclude_computed_fields:&nbsp;Whether&nbsp;to&nbsp;exclude&nbsp;computed&nbsp;fields.<br>
907
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;While&nbsp;this&nbsp;can&nbsp;be&nbsp;useful&nbsp;for&nbsp;round-tripping,&nbsp;it&nbsp;is&nbsp;usually&nbsp;recommended&nbsp;to&nbsp;use&nbsp;the&nbsp;dedicated<br>
908
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`round_trip`&nbsp;parameter&nbsp;instead.<br>
909
+ &nbsp;&nbsp;&nbsp;&nbsp;round_trip:&nbsp;If&nbsp;True,&nbsp;dumped&nbsp;values&nbsp;should&nbsp;be&nbsp;valid&nbsp;as&nbsp;input&nbsp;for&nbsp;non-idempotent&nbsp;types&nbsp;such&nbsp;as&nbsp;Json[T].<br>
910
+ &nbsp;&nbsp;&nbsp;&nbsp;warnings:&nbsp;How&nbsp;to&nbsp;handle&nbsp;serialization&nbsp;errors.&nbsp;False/"none"&nbsp;ignores&nbsp;them,&nbsp;True/"warn"&nbsp;logs&nbsp;errors,<br>
911
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"error"&nbsp;raises&nbsp;a&nbsp;[`PydanticSerializationError`][pydantic_core.PydanticSerializationError].<br>
912
+ &nbsp;&nbsp;&nbsp;&nbsp;fallback:&nbsp;A&nbsp;function&nbsp;to&nbsp;call&nbsp;when&nbsp;an&nbsp;unknown&nbsp;value&nbsp;is&nbsp;encountered.&nbsp;If&nbsp;not&nbsp;provided,<br>
913
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a&nbsp;[`PydanticSerializationError`][pydantic_core.PydanticSerializationError]&nbsp;error&nbsp;is&nbsp;raised.<br>
914
+ &nbsp;&nbsp;&nbsp;&nbsp;serialize_as_any:&nbsp;Whether&nbsp;to&nbsp;serialize&nbsp;fields&nbsp;with&nbsp;duck-typing&nbsp;serialization&nbsp;behavior.<br>
915
+ &nbsp;<br>
916
+ Returns:<br>
917
+ &nbsp;&nbsp;&nbsp;&nbsp;A&nbsp;JSON&nbsp;string&nbsp;representation&nbsp;of&nbsp;the&nbsp;model.</span></dd></dl>
918
+
919
+ <dl><dt><a name="ModelResponse-model_post_init"><strong>model_post_init</strong></a>(self, context: 'Any', /) -&gt; 'None'</dt><dd><span class="code">Override&nbsp;this&nbsp;method&nbsp;to&nbsp;perform&nbsp;additional&nbsp;initialization&nbsp;after&nbsp;`__init__`&nbsp;and&nbsp;`model_construct`.<br>
920
+ This&nbsp;is&nbsp;useful&nbsp;if&nbsp;you&nbsp;want&nbsp;to&nbsp;do&nbsp;some&nbsp;validation&nbsp;that&nbsp;requires&nbsp;the&nbsp;entire&nbsp;model&nbsp;to&nbsp;be&nbsp;initialized.</span></dd></dl>
921
+
922
+ <hr>
923
+ Class methods inherited from <a href="pydantic.main.html#BaseModel">pydantic.main.BaseModel</a>:<br>
924
+ <dl><dt><a name="ModelResponse-__class_getitem__"><strong>__class_getitem__</strong></a>(typevar_values: 'type[Any] | tuple[type[Any], ...]') -&gt; 'type[BaseModel] | _forward_ref.PydanticRecursiveRef'</dt></dl>
925
+
926
+ <dl><dt><a name="ModelResponse-__get_pydantic_core_schema__"><strong>__get_pydantic_core_schema__</strong></a>(
927
+ source: 'type[BaseModel]',
928
+ handler: 'GetCoreSchemaHandler',
929
+ /
930
+ ) -&gt; 'CoreSchema'</dt></dl>
931
+
932
+ <dl><dt><a name="ModelResponse-__get_pydantic_json_schema__"><strong>__get_pydantic_json_schema__</strong></a>(
933
+ core_schema: 'CoreSchema',
934
+ handler: 'GetJsonSchemaHandler',
935
+ /
936
+ ) -&gt; 'JsonSchemaValue'</dt><dd><span class="code">Hook&nbsp;into&nbsp;generating&nbsp;the&nbsp;model's&nbsp;JSON&nbsp;schema.<br>
937
+ &nbsp;<br>
938
+ Args:<br>
939
+ &nbsp;&nbsp;&nbsp;&nbsp;core_schema:&nbsp;A&nbsp;`pydantic-core`&nbsp;CoreSchema.<br>
940
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;You&nbsp;can&nbsp;ignore&nbsp;this&nbsp;argument&nbsp;and&nbsp;call&nbsp;the&nbsp;handler&nbsp;with&nbsp;a&nbsp;new&nbsp;CoreSchema,<br>
941
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;wrap&nbsp;this&nbsp;CoreSchema&nbsp;(`{'type':&nbsp;'nullable',&nbsp;'schema':&nbsp;current_schema}`),<br>
942
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;or&nbsp;just&nbsp;call&nbsp;the&nbsp;handler&nbsp;with&nbsp;the&nbsp;original&nbsp;schema.<br>
943
+ &nbsp;&nbsp;&nbsp;&nbsp;handler:&nbsp;Call&nbsp;into&nbsp;Pydantic's&nbsp;internal&nbsp;JSON&nbsp;schema&nbsp;generation.<br>
944
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;This&nbsp;will&nbsp;raise&nbsp;a&nbsp;`pydantic.errors.PydanticInvalidForJsonSchema`&nbsp;if&nbsp;JSON&nbsp;schema<br>
945
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;generation&nbsp;fails.<br>
946
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Since&nbsp;this&nbsp;gets&nbsp;called&nbsp;by&nbsp;`<a href="pydantic.main.html#BaseModel">BaseModel</a>.model_json_schema`&nbsp;you&nbsp;can&nbsp;override&nbsp;the<br>
947
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`schema_generator`&nbsp;argument&nbsp;to&nbsp;that&nbsp;function&nbsp;to&nbsp;change&nbsp;JSON&nbsp;schema&nbsp;generation&nbsp;globally<br>
948
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for&nbsp;a&nbsp;type.<br>
949
+ &nbsp;<br>
950
+ Returns:<br>
951
+ &nbsp;&nbsp;&nbsp;&nbsp;A&nbsp;JSON&nbsp;schema,&nbsp;as&nbsp;a&nbsp;Python&nbsp;object.</span></dd></dl>
952
+
953
+ <dl><dt><a name="ModelResponse-__pydantic_init_subclass__"><strong>__pydantic_init_subclass__</strong></a>(**kwargs: 'Any') -&gt; 'None'</dt><dd><span class="code">This&nbsp;is&nbsp;intended&nbsp;to&nbsp;behave&nbsp;just&nbsp;like&nbsp;`__init_subclass__`,&nbsp;but&nbsp;is&nbsp;called&nbsp;by&nbsp;`ModelMetaclass`<br>
954
+ only&nbsp;after&nbsp;basic&nbsp;class&nbsp;initialization&nbsp;is&nbsp;complete.&nbsp;In&nbsp;particular,&nbsp;attributes&nbsp;like&nbsp;`model_fields`&nbsp;will<br>
955
+ be&nbsp;present&nbsp;when&nbsp;this&nbsp;is&nbsp;called,&nbsp;but&nbsp;forward&nbsp;annotations&nbsp;are&nbsp;not&nbsp;guaranteed&nbsp;to&nbsp;be&nbsp;resolved&nbsp;yet,<br>
956
+ meaning&nbsp;that&nbsp;creating&nbsp;an&nbsp;instance&nbsp;of&nbsp;the&nbsp;class&nbsp;may&nbsp;fail.<br>
957
+ &nbsp;<br>
958
+ This&nbsp;is&nbsp;necessary&nbsp;because&nbsp;`__init_subclass__`&nbsp;will&nbsp;always&nbsp;be&nbsp;called&nbsp;by&nbsp;`type.__new__`,<br>
959
+ and&nbsp;it&nbsp;would&nbsp;require&nbsp;a&nbsp;prohibitively&nbsp;large&nbsp;refactor&nbsp;to&nbsp;the&nbsp;`ModelMetaclass`&nbsp;to&nbsp;ensure&nbsp;that<br>
960
+ `type.__new__`&nbsp;was&nbsp;called&nbsp;in&nbsp;such&nbsp;a&nbsp;manner&nbsp;that&nbsp;the&nbsp;class&nbsp;would&nbsp;already&nbsp;be&nbsp;sufficiently&nbsp;initialized.<br>
961
+ &nbsp;<br>
962
+ This&nbsp;will&nbsp;receive&nbsp;the&nbsp;same&nbsp;`kwargs`&nbsp;that&nbsp;would&nbsp;be&nbsp;passed&nbsp;to&nbsp;the&nbsp;standard&nbsp;`__init_subclass__`,&nbsp;namely,<br>
963
+ any&nbsp;kwargs&nbsp;passed&nbsp;to&nbsp;the&nbsp;class&nbsp;definition&nbsp;that&nbsp;aren't&nbsp;used&nbsp;internally&nbsp;by&nbsp;Pydantic.<br>
964
+ &nbsp;<br>
965
+ Args:<br>
966
+ &nbsp;&nbsp;&nbsp;&nbsp;**kwargs:&nbsp;Any&nbsp;keyword&nbsp;arguments&nbsp;passed&nbsp;to&nbsp;the&nbsp;class&nbsp;definition&nbsp;that&nbsp;aren't&nbsp;used&nbsp;internally<br>
967
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;by&nbsp;Pydantic.<br>
968
+ &nbsp;<br>
969
+ Note:<br>
970
+ &nbsp;&nbsp;&nbsp;&nbsp;You&nbsp;may&nbsp;want&nbsp;to&nbsp;override&nbsp;[`<a href="#ModelResponse-__pydantic_on_complete__">__pydantic_on_complete__</a>()`][pydantic.main.<a href="pydantic.main.html#BaseModel">BaseModel</a>.__pydantic_on_complete__]<br>
971
+ &nbsp;&nbsp;&nbsp;&nbsp;instead,&nbsp;which&nbsp;is&nbsp;called&nbsp;once&nbsp;the&nbsp;class&nbsp;and&nbsp;its&nbsp;fields&nbsp;are&nbsp;fully&nbsp;initialized&nbsp;and&nbsp;ready&nbsp;for&nbsp;validation.</span></dd></dl>
972
+
973
+ <dl><dt><a name="ModelResponse-__pydantic_on_complete__"><strong>__pydantic_on_complete__</strong></a>() -&gt; 'None'</dt><dd><span class="code">This&nbsp;is&nbsp;called&nbsp;once&nbsp;the&nbsp;class&nbsp;and&nbsp;its&nbsp;fields&nbsp;are&nbsp;fully&nbsp;initialized&nbsp;and&nbsp;ready&nbsp;to&nbsp;be&nbsp;used.<br>
974
+ &nbsp;<br>
975
+ This&nbsp;typically&nbsp;happens&nbsp;when&nbsp;the&nbsp;class&nbsp;is&nbsp;created&nbsp;(just&nbsp;before<br>
976
+ [`<a href="#ModelResponse-__pydantic_init_subclass__">__pydantic_init_subclass__</a>()`][pydantic.main.<a href="pydantic.main.html#BaseModel">BaseModel</a>.__pydantic_init_subclass__]&nbsp;is&nbsp;called&nbsp;on&nbsp;the&nbsp;superclass),<br>
977
+ except&nbsp;when&nbsp;forward&nbsp;annotations&nbsp;are&nbsp;used&nbsp;that&nbsp;could&nbsp;not&nbsp;immediately&nbsp;be&nbsp;resolved.<br>
978
+ In&nbsp;that&nbsp;case,&nbsp;it&nbsp;will&nbsp;be&nbsp;called&nbsp;later,&nbsp;when&nbsp;the&nbsp;model&nbsp;is&nbsp;rebuilt&nbsp;automatically&nbsp;or&nbsp;explicitly&nbsp;using<br>
979
+ [`<a href="#ModelResponse-model_rebuild">model_rebuild</a>()`][pydantic.main.<a href="pydantic.main.html#BaseModel">BaseModel</a>.model_rebuild].</span></dd></dl>
980
+
981
+ <dl><dt><a name="ModelResponse-construct"><strong>construct</strong></a>(_fields_set: 'set[str] | None' = None, **values: 'Any') -&gt; 'Self'</dt></dl>
982
+
983
+ <dl><dt><a name="ModelResponse-from_orm"><strong>from_orm</strong></a>(obj: 'Any') -&gt; 'Self'</dt></dl>
984
+
985
+ <dl><dt><a name="ModelResponse-model_construct"><strong>model_construct</strong></a>(_fields_set: 'set[str] | None' = None, **values: 'Any') -&gt; 'Self'</dt><dd><span class="code">Creates&nbsp;a&nbsp;new&nbsp;instance&nbsp;of&nbsp;the&nbsp;`Model`&nbsp;class&nbsp;with&nbsp;validated&nbsp;data.<br>
986
+ &nbsp;<br>
987
+ Creates&nbsp;a&nbsp;new&nbsp;model&nbsp;setting&nbsp;`__dict__`&nbsp;and&nbsp;`__pydantic_fields_set__`&nbsp;from&nbsp;trusted&nbsp;or&nbsp;pre-validated&nbsp;data.<br>
988
+ Default&nbsp;values&nbsp;are&nbsp;respected,&nbsp;but&nbsp;no&nbsp;other&nbsp;validation&nbsp;is&nbsp;performed.<br>
989
+ &nbsp;<br>
990
+ !!!&nbsp;note<br>
991
+ &nbsp;&nbsp;&nbsp;&nbsp;`<a href="#ModelResponse-model_construct">model_construct</a>()`&nbsp;generally&nbsp;respects&nbsp;the&nbsp;`model_config.extra`&nbsp;setting&nbsp;on&nbsp;the&nbsp;provided&nbsp;model.<br>
992
+ &nbsp;&nbsp;&nbsp;&nbsp;That&nbsp;is,&nbsp;if&nbsp;`model_config.extra&nbsp;==&nbsp;'allow'`,&nbsp;then&nbsp;all&nbsp;extra&nbsp;passed&nbsp;values&nbsp;are&nbsp;added&nbsp;to&nbsp;the&nbsp;model&nbsp;instance's&nbsp;`__dict__`<br>
993
+ &nbsp;&nbsp;&nbsp;&nbsp;and&nbsp;`__pydantic_extra__`&nbsp;fields.&nbsp;If&nbsp;`model_config.extra&nbsp;==&nbsp;'ignore'`&nbsp;(the&nbsp;default),&nbsp;then&nbsp;all&nbsp;extra&nbsp;passed&nbsp;values&nbsp;are&nbsp;ignored.<br>
994
+ &nbsp;&nbsp;&nbsp;&nbsp;Because&nbsp;no&nbsp;validation&nbsp;is&nbsp;performed&nbsp;with&nbsp;a&nbsp;call&nbsp;to&nbsp;`<a href="#ModelResponse-model_construct">model_construct</a>()`,&nbsp;having&nbsp;`model_config.extra&nbsp;==&nbsp;'forbid'`&nbsp;does&nbsp;not&nbsp;result&nbsp;in<br>
995
+ &nbsp;&nbsp;&nbsp;&nbsp;an&nbsp;error&nbsp;if&nbsp;extra&nbsp;values&nbsp;are&nbsp;passed,&nbsp;but&nbsp;they&nbsp;will&nbsp;be&nbsp;ignored.<br>
996
+ &nbsp;<br>
997
+ Args:<br>
998
+ &nbsp;&nbsp;&nbsp;&nbsp;_fields_set:&nbsp;A&nbsp;set&nbsp;of&nbsp;field&nbsp;names&nbsp;that&nbsp;were&nbsp;originally&nbsp;explicitly&nbsp;set&nbsp;during&nbsp;instantiation.&nbsp;If&nbsp;provided,<br>
999
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;this&nbsp;is&nbsp;directly&nbsp;used&nbsp;for&nbsp;the&nbsp;[`model_fields_set`][pydantic.<a href="pydantic.main.html#BaseModel">BaseModel</a>.model_fields_set]&nbsp;attribute.<br>
1000
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Otherwise,&nbsp;the&nbsp;field&nbsp;names&nbsp;from&nbsp;the&nbsp;`values`&nbsp;argument&nbsp;will&nbsp;be&nbsp;used.<br>
1001
+ &nbsp;&nbsp;&nbsp;&nbsp;values:&nbsp;Trusted&nbsp;or&nbsp;pre-validated&nbsp;data&nbsp;dictionary.<br>
1002
+ &nbsp;<br>
1003
+ Returns:<br>
1004
+ &nbsp;&nbsp;&nbsp;&nbsp;A&nbsp;new&nbsp;instance&nbsp;of&nbsp;the&nbsp;`Model`&nbsp;class&nbsp;with&nbsp;validated&nbsp;data.</span></dd></dl>
1005
+
1006
+ <dl><dt><a name="ModelResponse-model_json_schema"><strong>model_json_schema</strong></a>(
1007
+ by_alias: 'bool' = True,
1008
+ ref_template: 'str' = '#/$defs/{model}',
1009
+ schema_generator: 'type[GenerateJsonSchema]' = &lt;class 'pydantic.json_schema.GenerateJsonSchema'&gt;,
1010
+ mode: 'JsonSchemaMode' = 'validation',
1011
+ *,
1012
+ union_format: "Literal['any_of', 'primitive_type_array']" = 'any_of'
1013
+ ) -&gt; 'dict[str, Any]'</dt><dd><span class="code">Generates&nbsp;a&nbsp;JSON&nbsp;schema&nbsp;for&nbsp;a&nbsp;model&nbsp;class.<br>
1014
+ &nbsp;<br>
1015
+ Args:<br>
1016
+ &nbsp;&nbsp;&nbsp;&nbsp;by_alias:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;attribute&nbsp;aliases&nbsp;or&nbsp;not.<br>
1017
+ &nbsp;&nbsp;&nbsp;&nbsp;ref_template:&nbsp;The&nbsp;reference&nbsp;template.<br>
1018
+ &nbsp;&nbsp;&nbsp;&nbsp;union_format:&nbsp;The&nbsp;format&nbsp;to&nbsp;use&nbsp;when&nbsp;combining&nbsp;schemas&nbsp;from&nbsp;unions&nbsp;together.&nbsp;Can&nbsp;be&nbsp;one&nbsp;of:<br>
1019
+ &nbsp;<br>
1020
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;`'any_of'`:&nbsp;Use&nbsp;the&nbsp;[`anyOf`](<a href="https://json-schema.org/understanding-json-schema/reference/combining#anyOf">https://json-schema.org/understanding-json-schema/reference/combining#anyOf</a>)<br>
1021
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;keyword&nbsp;to&nbsp;combine&nbsp;schemas&nbsp;(the&nbsp;default).<br>
1022
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;`'primitive_type_array'`:&nbsp;Use&nbsp;the&nbsp;[`type`](<a href="https://json-schema.org/understanding-json-schema/reference/type">https://json-schema.org/understanding-json-schema/reference/type</a>)<br>
1023
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;keyword&nbsp;as&nbsp;an&nbsp;array&nbsp;of&nbsp;strings,&nbsp;containing&nbsp;each&nbsp;type&nbsp;of&nbsp;the&nbsp;combination.&nbsp;If&nbsp;any&nbsp;of&nbsp;the&nbsp;schemas&nbsp;is&nbsp;not&nbsp;a&nbsp;primitive<br>
1024
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;type&nbsp;(`string`,&nbsp;`boolean`,&nbsp;`null`,&nbsp;`integer`&nbsp;or&nbsp;`number`)&nbsp;or&nbsp;contains&nbsp;constraints/metadata,&nbsp;falls&nbsp;back&nbsp;to<br>
1025
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`any_of`.<br>
1026
+ &nbsp;&nbsp;&nbsp;&nbsp;schema_generator:&nbsp;To&nbsp;override&nbsp;the&nbsp;logic&nbsp;used&nbsp;to&nbsp;generate&nbsp;the&nbsp;JSON&nbsp;schema,&nbsp;as&nbsp;a&nbsp;subclass&nbsp;of<br>
1027
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`GenerateJsonSchema`&nbsp;with&nbsp;your&nbsp;desired&nbsp;modifications<br>
1028
+ &nbsp;&nbsp;&nbsp;&nbsp;mode:&nbsp;The&nbsp;mode&nbsp;in&nbsp;which&nbsp;to&nbsp;generate&nbsp;the&nbsp;schema.<br>
1029
+ &nbsp;<br>
1030
+ Returns:<br>
1031
+ &nbsp;&nbsp;&nbsp;&nbsp;The&nbsp;JSON&nbsp;schema&nbsp;for&nbsp;the&nbsp;given&nbsp;model&nbsp;class.</span></dd></dl>
1032
+
1033
+ <dl><dt><a name="ModelResponse-model_parametrized_name"><strong>model_parametrized_name</strong></a>(params: 'tuple[type[Any], ...]') -&gt; 'str'</dt><dd><span class="code">Compute&nbsp;the&nbsp;class&nbsp;name&nbsp;for&nbsp;parametrizations&nbsp;of&nbsp;generic&nbsp;classes.<br>
1034
+ &nbsp;<br>
1035
+ This&nbsp;method&nbsp;can&nbsp;be&nbsp;overridden&nbsp;to&nbsp;achieve&nbsp;a&nbsp;custom&nbsp;naming&nbsp;scheme&nbsp;for&nbsp;generic&nbsp;BaseModels.<br>
1036
+ &nbsp;<br>
1037
+ Args:<br>
1038
+ &nbsp;&nbsp;&nbsp;&nbsp;params:&nbsp;Tuple&nbsp;of&nbsp;types&nbsp;of&nbsp;the&nbsp;class.&nbsp;Given&nbsp;a&nbsp;generic&nbsp;class<br>
1039
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`Model`&nbsp;with&nbsp;2&nbsp;type&nbsp;variables&nbsp;and&nbsp;a&nbsp;concrete&nbsp;model&nbsp;`Model[str,&nbsp;int]`,<br>
1040
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;the&nbsp;value&nbsp;`(str,&nbsp;int)`&nbsp;would&nbsp;be&nbsp;passed&nbsp;to&nbsp;`params`.<br>
1041
+ &nbsp;<br>
1042
+ Returns:<br>
1043
+ &nbsp;&nbsp;&nbsp;&nbsp;String&nbsp;representing&nbsp;the&nbsp;new&nbsp;class&nbsp;where&nbsp;`params`&nbsp;are&nbsp;passed&nbsp;to&nbsp;`cls`&nbsp;as&nbsp;type&nbsp;variables.<br>
1044
+ &nbsp;<br>
1045
+ Raises:<br>
1046
+ &nbsp;&nbsp;&nbsp;&nbsp;TypeError:&nbsp;Raised&nbsp;when&nbsp;trying&nbsp;to&nbsp;generate&nbsp;concrete&nbsp;names&nbsp;for&nbsp;non-generic&nbsp;models.</span></dd></dl>
1047
+
1048
+ <dl><dt><a name="ModelResponse-model_rebuild"><strong>model_rebuild</strong></a>(
1049
+ *,
1050
+ force: 'bool' = False,
1051
+ raise_errors: 'bool' = True,
1052
+ _parent_namespace_depth: 'int' = 2,
1053
+ _types_namespace: 'MappingNamespace | None' = None
1054
+ ) -&gt; 'bool | None'</dt><dd><span class="code">Try&nbsp;to&nbsp;rebuild&nbsp;the&nbsp;pydantic-core&nbsp;schema&nbsp;for&nbsp;the&nbsp;model.<br>
1055
+ &nbsp;<br>
1056
+ This&nbsp;may&nbsp;be&nbsp;necessary&nbsp;when&nbsp;one&nbsp;of&nbsp;the&nbsp;annotations&nbsp;is&nbsp;a&nbsp;ForwardRef&nbsp;which&nbsp;could&nbsp;not&nbsp;be&nbsp;resolved&nbsp;during<br>
1057
+ the&nbsp;initial&nbsp;attempt&nbsp;to&nbsp;build&nbsp;the&nbsp;schema,&nbsp;and&nbsp;automatic&nbsp;rebuilding&nbsp;fails.<br>
1058
+ &nbsp;<br>
1059
+ Args:<br>
1060
+ &nbsp;&nbsp;&nbsp;&nbsp;force:&nbsp;Whether&nbsp;to&nbsp;force&nbsp;the&nbsp;rebuilding&nbsp;of&nbsp;the&nbsp;model&nbsp;schema,&nbsp;defaults&nbsp;to&nbsp;`False`.<br>
1061
+ &nbsp;&nbsp;&nbsp;&nbsp;raise_errors:&nbsp;Whether&nbsp;to&nbsp;raise&nbsp;errors,&nbsp;defaults&nbsp;to&nbsp;`True`.<br>
1062
+ &nbsp;&nbsp;&nbsp;&nbsp;_parent_namespace_depth:&nbsp;The&nbsp;depth&nbsp;level&nbsp;of&nbsp;the&nbsp;parent&nbsp;namespace,&nbsp;defaults&nbsp;to&nbsp;2.<br>
1063
+ &nbsp;&nbsp;&nbsp;&nbsp;_types_namespace:&nbsp;The&nbsp;types&nbsp;namespace,&nbsp;defaults&nbsp;to&nbsp;`None`.<br>
1064
+ &nbsp;<br>
1065
+ Returns:<br>
1066
+ &nbsp;&nbsp;&nbsp;&nbsp;Returns&nbsp;`None`&nbsp;if&nbsp;the&nbsp;schema&nbsp;is&nbsp;already&nbsp;"complete"&nbsp;and&nbsp;rebuilding&nbsp;was&nbsp;not&nbsp;required.<br>
1067
+ &nbsp;&nbsp;&nbsp;&nbsp;If&nbsp;rebuilding&nbsp;_was_&nbsp;required,&nbsp;returns&nbsp;`True`&nbsp;if&nbsp;rebuilding&nbsp;was&nbsp;successful,&nbsp;otherwise&nbsp;`False`.</span></dd></dl>
1068
+
1069
+ <dl><dt><a name="ModelResponse-model_validate"><strong>model_validate</strong></a>(
1070
+ obj: 'Any',
1071
+ *,
1072
+ strict: 'bool | None' = None,
1073
+ extra: 'ExtraValues | None' = None,
1074
+ from_attributes: 'bool | None' = None,
1075
+ context: 'Any | None' = None,
1076
+ by_alias: 'bool | None' = None,
1077
+ by_name: 'bool | None' = None
1078
+ ) -&gt; 'Self'</dt><dd><span class="code">Validate&nbsp;a&nbsp;pydantic&nbsp;model&nbsp;instance.<br>
1079
+ &nbsp;<br>
1080
+ Args:<br>
1081
+ &nbsp;&nbsp;&nbsp;&nbsp;obj:&nbsp;The&nbsp;object&nbsp;to&nbsp;validate.<br>
1082
+ &nbsp;&nbsp;&nbsp;&nbsp;strict:&nbsp;Whether&nbsp;to&nbsp;enforce&nbsp;types&nbsp;strictly.<br>
1083
+ &nbsp;&nbsp;&nbsp;&nbsp;extra:&nbsp;Whether&nbsp;to&nbsp;ignore,&nbsp;allow,&nbsp;or&nbsp;forbid&nbsp;extra&nbsp;data&nbsp;during&nbsp;model&nbsp;validation.<br>
1084
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;See&nbsp;the&nbsp;[`extra`&nbsp;configuration&nbsp;value][pydantic.ConfigDict.extra]&nbsp;for&nbsp;details.<br>
1085
+ &nbsp;&nbsp;&nbsp;&nbsp;from_attributes:&nbsp;Whether&nbsp;to&nbsp;extract&nbsp;data&nbsp;from&nbsp;object&nbsp;attributes.<br>
1086
+ &nbsp;&nbsp;&nbsp;&nbsp;context:&nbsp;Additional&nbsp;context&nbsp;to&nbsp;pass&nbsp;to&nbsp;the&nbsp;validator.<br>
1087
+ &nbsp;&nbsp;&nbsp;&nbsp;by_alias:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;the&nbsp;field's&nbsp;alias&nbsp;when&nbsp;validating&nbsp;against&nbsp;the&nbsp;provided&nbsp;input&nbsp;data.<br>
1088
+ &nbsp;&nbsp;&nbsp;&nbsp;by_name:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;the&nbsp;field's&nbsp;name&nbsp;when&nbsp;validating&nbsp;against&nbsp;the&nbsp;provided&nbsp;input&nbsp;data.<br>
1089
+ &nbsp;<br>
1090
+ Raises:<br>
1091
+ &nbsp;&nbsp;&nbsp;&nbsp;ValidationError:&nbsp;If&nbsp;the&nbsp;object&nbsp;could&nbsp;not&nbsp;be&nbsp;validated.<br>
1092
+ &nbsp;<br>
1093
+ Returns:<br>
1094
+ &nbsp;&nbsp;&nbsp;&nbsp;The&nbsp;validated&nbsp;model&nbsp;instance.</span></dd></dl>
1095
+
1096
+ <dl><dt><a name="ModelResponse-model_validate_json"><strong>model_validate_json</strong></a>(
1097
+ json_data: 'str | bytes | bytearray',
1098
+ *,
1099
+ strict: 'bool | None' = None,
1100
+ extra: 'ExtraValues | None' = None,
1101
+ context: 'Any | None' = None,
1102
+ by_alias: 'bool | None' = None,
1103
+ by_name: 'bool | None' = None
1104
+ ) -&gt; 'Self'</dt><dd><span class="code">!!!&nbsp;abstract&nbsp;"Usage&nbsp;Documentation"<br>
1105
+ &nbsp;&nbsp;&nbsp;&nbsp;[JSON&nbsp;Parsing](../concepts/json.md#json-parsing)<br>
1106
+ &nbsp;<br>
1107
+ Validate&nbsp;the&nbsp;given&nbsp;JSON&nbsp;data&nbsp;against&nbsp;the&nbsp;Pydantic&nbsp;model.<br>
1108
+ &nbsp;<br>
1109
+ Args:<br>
1110
+ &nbsp;&nbsp;&nbsp;&nbsp;json_data:&nbsp;The&nbsp;JSON&nbsp;data&nbsp;to&nbsp;validate.<br>
1111
+ &nbsp;&nbsp;&nbsp;&nbsp;strict:&nbsp;Whether&nbsp;to&nbsp;enforce&nbsp;types&nbsp;strictly.<br>
1112
+ &nbsp;&nbsp;&nbsp;&nbsp;extra:&nbsp;Whether&nbsp;to&nbsp;ignore,&nbsp;allow,&nbsp;or&nbsp;forbid&nbsp;extra&nbsp;data&nbsp;during&nbsp;model&nbsp;validation.<br>
1113
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;See&nbsp;the&nbsp;[`extra`&nbsp;configuration&nbsp;value][pydantic.ConfigDict.extra]&nbsp;for&nbsp;details.<br>
1114
+ &nbsp;&nbsp;&nbsp;&nbsp;context:&nbsp;Extra&nbsp;variables&nbsp;to&nbsp;pass&nbsp;to&nbsp;the&nbsp;validator.<br>
1115
+ &nbsp;&nbsp;&nbsp;&nbsp;by_alias:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;the&nbsp;field's&nbsp;alias&nbsp;when&nbsp;validating&nbsp;against&nbsp;the&nbsp;provided&nbsp;input&nbsp;data.<br>
1116
+ &nbsp;&nbsp;&nbsp;&nbsp;by_name:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;the&nbsp;field's&nbsp;name&nbsp;when&nbsp;validating&nbsp;against&nbsp;the&nbsp;provided&nbsp;input&nbsp;data.<br>
1117
+ &nbsp;<br>
1118
+ Returns:<br>
1119
+ &nbsp;&nbsp;&nbsp;&nbsp;The&nbsp;validated&nbsp;Pydantic&nbsp;model.<br>
1120
+ &nbsp;<br>
1121
+ Raises:<br>
1122
+ &nbsp;&nbsp;&nbsp;&nbsp;ValidationError:&nbsp;If&nbsp;`json_data`&nbsp;is&nbsp;not&nbsp;a&nbsp;JSON&nbsp;string&nbsp;or&nbsp;the&nbsp;object&nbsp;could&nbsp;not&nbsp;be&nbsp;validated.</span></dd></dl>
1123
+
1124
+ <dl><dt><a name="ModelResponse-model_validate_strings"><strong>model_validate_strings</strong></a>(
1125
+ obj: 'Any',
1126
+ *,
1127
+ strict: 'bool | None' = None,
1128
+ extra: 'ExtraValues | None' = None,
1129
+ context: 'Any | None' = None,
1130
+ by_alias: 'bool | None' = None,
1131
+ by_name: 'bool | None' = None
1132
+ ) -&gt; 'Self'</dt><dd><span class="code">Validate&nbsp;the&nbsp;given&nbsp;object&nbsp;with&nbsp;string&nbsp;data&nbsp;against&nbsp;the&nbsp;Pydantic&nbsp;model.<br>
1133
+ &nbsp;<br>
1134
+ Args:<br>
1135
+ &nbsp;&nbsp;&nbsp;&nbsp;obj:&nbsp;The&nbsp;object&nbsp;containing&nbsp;string&nbsp;data&nbsp;to&nbsp;validate.<br>
1136
+ &nbsp;&nbsp;&nbsp;&nbsp;strict:&nbsp;Whether&nbsp;to&nbsp;enforce&nbsp;types&nbsp;strictly.<br>
1137
+ &nbsp;&nbsp;&nbsp;&nbsp;extra:&nbsp;Whether&nbsp;to&nbsp;ignore,&nbsp;allow,&nbsp;or&nbsp;forbid&nbsp;extra&nbsp;data&nbsp;during&nbsp;model&nbsp;validation.<br>
1138
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;See&nbsp;the&nbsp;[`extra`&nbsp;configuration&nbsp;value][pydantic.ConfigDict.extra]&nbsp;for&nbsp;details.<br>
1139
+ &nbsp;&nbsp;&nbsp;&nbsp;context:&nbsp;Extra&nbsp;variables&nbsp;to&nbsp;pass&nbsp;to&nbsp;the&nbsp;validator.<br>
1140
+ &nbsp;&nbsp;&nbsp;&nbsp;by_alias:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;the&nbsp;field's&nbsp;alias&nbsp;when&nbsp;validating&nbsp;against&nbsp;the&nbsp;provided&nbsp;input&nbsp;data.<br>
1141
+ &nbsp;&nbsp;&nbsp;&nbsp;by_name:&nbsp;Whether&nbsp;to&nbsp;use&nbsp;the&nbsp;field's&nbsp;name&nbsp;when&nbsp;validating&nbsp;against&nbsp;the&nbsp;provided&nbsp;input&nbsp;data.<br>
1142
+ &nbsp;<br>
1143
+ Returns:<br>
1144
+ &nbsp;&nbsp;&nbsp;&nbsp;The&nbsp;validated&nbsp;Pydantic&nbsp;model.</span></dd></dl>
1145
+
1146
+ <dl><dt><a name="ModelResponse-parse_file"><strong>parse_file</strong></a>(
1147
+ path: 'str | Path',
1148
+ *,
1149
+ content_type: 'str | None' = None,
1150
+ encoding: 'str' = 'utf8',
1151
+ proto: 'DeprecatedParseProtocol | None' = None,
1152
+ allow_pickle: 'bool' = False
1153
+ ) -&gt; 'Self'</dt></dl>
1154
+
1155
+ <dl><dt><a name="ModelResponse-parse_obj"><strong>parse_obj</strong></a>(obj: 'Any') -&gt; 'Self'</dt></dl>
1156
+
1157
+ <dl><dt><a name="ModelResponse-parse_raw"><strong>parse_raw</strong></a>(
1158
+ b: 'str | bytes',
1159
+ *,
1160
+ content_type: 'str | None' = None,
1161
+ encoding: 'str' = 'utf8',
1162
+ proto: 'DeprecatedParseProtocol | None' = None,
1163
+ allow_pickle: 'bool' = False
1164
+ ) -&gt; 'Self'</dt></dl>
1165
+
1166
+ <dl><dt><a name="ModelResponse-schema"><strong>schema</strong></a>(by_alias: 'bool' = True, ref_template: 'str' = '#/$defs/{model}') -&gt; 'Dict[str, Any]'</dt></dl>
1167
+
1168
+ <dl><dt><a name="ModelResponse-schema_json"><strong>schema_json</strong></a>(
1169
+ *,
1170
+ by_alias: 'bool' = True,
1171
+ ref_template: 'str' = '#/$defs/{model}',
1172
+ **dumps_kwargs: 'Any'
1173
+ ) -&gt; 'str'</dt></dl>
1174
+
1175
+ <dl><dt><a name="ModelResponse-update_forward_refs"><strong>update_forward_refs</strong></a>(**localns: 'Any') -&gt; 'None'</dt></dl>
1176
+
1177
+ <dl><dt><a name="ModelResponse-validate"><strong>validate</strong></a>(value: 'Any') -&gt; 'Self'</dt></dl>
1178
+
1179
+ <hr>
1180
+ Readonly properties inherited from <a href="pydantic.main.html#BaseModel">pydantic.main.BaseModel</a>:<br>
1181
+ <dl><dt><strong>__fields_set__</strong></dt>
1182
+ </dl>
1183
+ <dl><dt><strong>model_extra</strong></dt>
1184
+ <dd><span class="code">Get&nbsp;extra&nbsp;fields&nbsp;set&nbsp;during&nbsp;validation.<br>
1185
+ &nbsp;<br>
1186
+ Returns:<br>
1187
+ &nbsp;&nbsp;&nbsp;&nbsp;A&nbsp;dictionary&nbsp;of&nbsp;extra&nbsp;fields,&nbsp;or&nbsp;`None`&nbsp;if&nbsp;`config.extra`&nbsp;is&nbsp;not&nbsp;set&nbsp;to&nbsp;`"allow"`.</span></dd>
1188
+ </dl>
1189
+ <dl><dt><strong>model_fields_set</strong></dt>
1190
+ <dd><span class="code">Returns&nbsp;the&nbsp;set&nbsp;of&nbsp;fields&nbsp;that&nbsp;have&nbsp;been&nbsp;explicitly&nbsp;set&nbsp;on&nbsp;this&nbsp;model&nbsp;instance.<br>
1191
+ &nbsp;<br>
1192
+ Returns:<br>
1193
+ &nbsp;&nbsp;&nbsp;&nbsp;A&nbsp;set&nbsp;of&nbsp;strings&nbsp;representing&nbsp;the&nbsp;fields&nbsp;that&nbsp;have&nbsp;been&nbsp;set,<br>
1194
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;i.e.&nbsp;that&nbsp;were&nbsp;not&nbsp;filled&nbsp;from&nbsp;defaults.</span></dd>
1195
+ </dl>
1196
+ <hr>
1197
+ Data descriptors inherited from <a href="pydantic.main.html#BaseModel">pydantic.main.BaseModel</a>:<br>
1198
+ <dl><dt><strong>__dict__</strong></dt>
1199
+ <dd><span class="code">dictionary&nbsp;for&nbsp;instance&nbsp;variables</span></dd>
1200
+ </dl>
1201
+ <dl><dt><strong>__pydantic_extra__</strong></dt>
1202
+ </dl>
1203
+ <dl><dt><strong>__pydantic_fields_set__</strong></dt>
1204
+ </dl>
1205
+ <dl><dt><strong>__pydantic_private__</strong></dt>
1206
+ </dl>
1207
+ <hr>
1208
+ Data and other attributes inherited from <a href="pydantic.main.html#BaseModel">pydantic.main.BaseModel</a>:<br>
1209
+ <dl><dt><strong>__hash__</strong> = None</dl>
1210
+
1211
+ <dl><dt><strong>__pydantic_root_model__</strong> = False</dl>
1212
+
1213
+ <dl><dt><strong>model_computed_fields</strong> = {}</dl>
1214
+
1215
+ <dl><dt><strong>model_fields</strong> = {'employee_id': FieldInfo(annotation=Union[int, NoneType], required=False, default=None), 'turnover_probability': FieldInfo(annotation=float, required=True), 'will_leave': FieldInfo(annotation=bool, required=True)}</dl>
1216
+
1217
+ </td></tr></table></td></tr></table><p>
1218
+ <table class="section">
1219
+ <tr class="decor data-decor heading-text">
1220
+ <td class="section-title" colspan=3>&nbsp;<br><strong class="bigsection">Data</strong></td></tr>
1221
+
1222
+ <tr><td class="decor data-decor"><span class="code">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></td><td>&nbsp;</td>
1223
+ <td class="singlecolumn"><strong>Literal</strong> = typing.Literal<br>
1224
+ <strong>Optional</strong> = typing.Optional</td></tr></table>
1225
+ </body></html>
docs/schema.puml ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @startuml
2
+ hide circle
3
+ skinparam linetype ortho
4
+ skinparam classAttributeIconSize 0
5
+
6
+ ' =========================
7
+ ' TABLES RAW
8
+ ' =========================
9
+
10
+ class raw_employees {
11
+ + id : SERIAL <<PK>>
12
+ --
13
+ employee_external_id : INTEGER <<UNIQUE, NOT NULL>>
14
+ age : INTEGER
15
+ genre : VARCHAR(20)
16
+ statut_marital : VARCHAR(50)
17
+ ayant_enfants : VARCHAR(10)
18
+ niveau_education : INTEGER
19
+ domaine_etude : VARCHAR(120)
20
+ departement : VARCHAR(120)
21
+ poste : VARCHAR(120)
22
+ distance_domicile_travail : INTEGER
23
+ created_at : TIMESTAMPTZ <<NOT NULL, default now()>>
24
+ }
25
+
26
+ class raw_employee_snapshots {
27
+ + id : SERIAL <<PK>>
28
+ --
29
+ employee_id : INTEGER <<FK, NOT NULL>>
30
+ nombre_experiences_precedentes : INTEGER
31
+ nombre_heures_travaillees : INTEGER
32
+ annee_experience_totale : INTEGER
33
+ annees_dans_l_entreprise : INTEGER
34
+ annees_dans_le_poste_actuel : INTEGER
35
+ annees_sous_responsable_actuel : INTEGER
36
+ niveau_hierarchique_poste : INTEGER
37
+ revenu_mensuel : INTEGER
38
+ augmentation_salaire_precedente : VARCHAR(50)
39
+ heures_supplementaires : VARCHAR(50)
40
+ nombre_participation_pee : INTEGER
41
+ nb_formations_suivies : INTEGER
42
+ nombre_employee_sous_responsabilite : INTEGER
43
+ frequence_deplacement : VARCHAR(50)
44
+ annees_depuis_la_derniere_promotion : INTEGER
45
+ created_at : TIMESTAMPTZ <<NOT NULL, default now()>>
46
+ }
47
+
48
+ class raw_surveys {
49
+ + id : SERIAL <<PK>>
50
+ --
51
+ employee_id : INTEGER <<FK, NOT NULL>>
52
+ code_sondage : INTEGER
53
+ eval_number : VARCHAR(50)
54
+ note_evaluation_precedente : INTEGER
55
+ note_evaluation_actuelle : INTEGER
56
+ satisfaction_employee_environnement : INTEGER
57
+ satisfaction_employee_nature_travail : INTEGER
58
+ satisfaction_employee_equipe : INTEGER
59
+ satisfaction_employee_equilibre_pro_perso : INTEGER
60
+ created_at : TIMESTAMPTZ <<NOT NULL, default now()>>
61
+ }
62
+
63
+ class ground_truth {
64
+ + id : SERIAL <<PK>>
65
+ --
66
+ employee_id : INTEGER <<FK, NOT NULL>>
67
+ date_event : TIMESTAMPTZ <<NOT NULL, default now()>>
68
+ a_quitte_l_entreprise : INTEGER <<NOT NULL>>
69
+ created_at : TIMESTAMPTZ <<NOT NULL, default now()>>
70
+ }
71
+
72
+ raw_employee_snapshots }o--|| raw_employees : employee_id → raw_employees.id\n(on delete cascade)
73
+ raw_surveys }o--|| raw_employees : employee_id → raw_employees.id\n(on delete cascade)
74
+ ground_truth }o--|| raw_employees : employee_id → raw_employees.id\n(on delete cascade)
75
+
76
+ ' =========================
77
+ ' TABLE CONTRACTUELLE ML
78
+ ' =========================
79
+
80
+ class ml_features_employees {
81
+ + id : BIGSERIAL <<PK>>
82
+ created_at : TIMESTAMPTZ <<NOT NULL, default now()>>
83
+ --
84
+ note_evaluation_precedente : SMALLINT <<NOT NULL>>
85
+ niveau_hierarchique_poste : SMALLINT <<NOT NULL>>
86
+ note_evaluation_actuelle : SMALLINT <<NOT NULL>>
87
+
88
+ heures_supplementaires : SMALLINT <<NOT NULL, CHECK 0/1>>
89
+ augmentation_salaire_precedente : NUMERIC(10,2) <<NOT NULL>>
90
+
91
+ age : SMALLINT <<NOT NULL>>
92
+ genre : SMALLINT <<NOT NULL>>
93
+
94
+ revenu_mensuel : INTEGER <<NOT NULL>>
95
+ statut_marital : VARCHAR(50) <<NOT NULL>>
96
+ departement : VARCHAR(120) <<NOT NULL>>
97
+ poste : VARCHAR(120) <<NOT NULL>>
98
+
99
+ nombre_experiences_precedentes : SMALLINT <<NOT NULL>>
100
+ annee_experience_totale : SMALLINT <<NOT NULL>>
101
+ annees_dans_l_entreprise : SMALLINT <<NOT NULL>>
102
+ annees_dans_le_poste_actuel : SMALLINT <<NOT NULL>>
103
+
104
+ a_quitte_l_entreprise : SMALLINT <<NOT NULL>>
105
+
106
+ nombre_participation_pee : SMALLINT <<NOT NULL>>
107
+ nb_formations_suivies : SMALLINT <<NOT NULL>>
108
+ distance_domicile_travail : SMALLINT <<NOT NULL>>
109
+
110
+ niveau_education : SMALLINT <<NOT NULL>>
111
+ domaine_etude : VARCHAR(120) <<NOT NULL>>
112
+ frequence_deplacement : SMALLINT <<NOT NULL>>
113
+
114
+ annees_depuis_la_derniere_promotion : SMALLINT <<NOT NULL>>
115
+ annees_sous_responsable_actuel : SMALLINT <<NOT NULL>>
116
+
117
+ satisfaction_moyenne : NUMERIC(10,4) <<NOT NULL>>
118
+ nonlineaire_participation_pee : NUMERIC(18,16) <<NOT NULL>>
119
+ ratio_heures_sup_salaire : NUMERIC(18,16) <<NOT NULL>>
120
+ nonlinaire_charge_contrainte : NUMERIC(18,16) <<NOT NULL>>
121
+ nonlinaire_surmenage_insatisfaction : NUMERIC(18,16) <<NOT NULL>>
122
+
123
+ jeune_surcharge : SMALLINT <<NOT NULL>>
124
+ anciennete_sans_promotion : NUMERIC(18,16) <<NOT NULL>>
125
+ mobilite_carriere : NUMERIC(18,16) <<NOT NULL>>
126
+ risque_global : NUMERIC(18,16) <<NOT NULL>>
127
+ }
128
+
129
+ ' =========================
130
+ ' HISTORIQUE DES PREDICTIONS
131
+ ' =========================
132
+
133
+ class prediction_requests {
134
+ + id : SERIAL <<PK>>
135
+ --
136
+ employee_id : INTEGER <<FK, NULL>>
137
+ snapshot_id : INTEGER <<FK, NULL>>
138
+ survey_id : INTEGER <<FK, NULL>>
139
+ payload_json : JSONB <<NOT NULL>>
140
+ created_at : TIMESTAMPTZ <<NOT NULL, default now()>>
141
+ }
142
+
143
+ class predictions {
144
+ + id : SERIAL <<PK>>
145
+ --
146
+ request_id : INTEGER <<FK, NOT NULL>>
147
+ model_version : VARCHAR(50) <<NOT NULL>>
148
+ predicted_class : INTEGER <<NOT NULL>>
149
+ predicted_proba : DOUBLE PRECISION <<NOT NULL>>
150
+ threshold_used : DOUBLE PRECISION <<NOT NULL>>
151
+ latency_ms : INTEGER
152
+ created_at : TIMESTAMPTZ <<NOT NULL, default now()>>
153
+ }
154
+
155
+ predictions }o--|| prediction_requests : request_id → prediction_requests.id\n(on delete cascade)
156
+
157
+ prediction_requests }o--o| raw_employees : employee_id → raw_employees.id\n(on delete set null)
158
+ prediction_requests }o--o| raw_employee_snapshots : snapshot_id → raw_employee_snapshots.id\n(on delete set null)
159
+ prediction_requests }o--o| raw_surveys : survey_id → raw_surveys.id\n(on delete set null)
160
+
161
+ @enduml
docs/service.technova_service.html ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Python: module service.technova_service</title>
6
+ </head><body>
7
+
8
+ <table class="heading">
9
+ <tr class="heading-text decor">
10
+ <td class="title">&nbsp;<br><strong class="title"><a href="service.html" class="white">service</a>.technova_service</strong></td>
11
+ <td class="extra"><a href=".">index</a><br><a href="file:c%3A%5Cusers%5Cgonza%5Copenclassroom%5Cprojet_4_classification%5Cprojet_technova_partners%5Cservice%5Ctechnova_service.py">c:\users\gonza\openclassroom\projet_4_classification\projet_technova_partners\service\technova_service.py</a></td></tr></table>
12
+ <p></p>
13
+ <p>
14
+ <table class="section">
15
+ <tr class="decor pkg-content-decor heading-text">
16
+ <td class="section-title" colspan=3>&nbsp;<br><strong class="bigsection">Modules</strong></td></tr>
17
+
18
+ <tr><td class="decor pkg-content-decor"><span class="code">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></td><td>&nbsp;</td>
19
+ <td class="singlecolumn"><table><tr><td class="multicolumn"><a href="joblib.html">joblib</a><br>
20
+ </td><td class="multicolumn"><a href="json.html">json</a><br>
21
+ </td><td class="multicolumn"><a href="pandas.html">pandas</a><br>
22
+ </td><td class="multicolumn"></td></tr></table></td></tr></table><p>
23
+ <table class="section">
24
+ <tr class="decor index-decor heading-text">
25
+ <td class="section-title" colspan=3>&nbsp;<br><strong class="bigsection">Classes</strong></td></tr>
26
+
27
+ <tr><td class="decor index-decor"><span class="code">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></td><td>&nbsp;</td>
28
+ <td class="singlecolumn"><dl>
29
+ <dt class="heading-text"><a href="builtins.html#object">builtins.object</a>
30
+ </dt><dd>
31
+ <dl>
32
+ <dt class="heading-text"><a href="service.technova_service.html#TechNovaService">TechNovaService</a>
33
+ </dt></dl>
34
+ </dd>
35
+ </dl>
36
+ <p>
37
+ <table class="section">
38
+ <tr class="decor title-decor heading-text">
39
+ <td class="section-title" colspan=3>&nbsp;<br><a name="TechNovaService">class <strong>TechNovaService</strong></a>(<a href="builtins.html#object">builtins.object</a>)</td></tr>
40
+
41
+ <tr><td class="decor title-decor" rowspan=2><span class="code">&nbsp;&nbsp;&nbsp;</span></td>
42
+ <td class="decor title-decor" colspan=2><span class="code"><a href="#TechNovaService">TechNovaService</a>()&nbsp;-&amp;gt;&nbsp;'None'<br>
43
+ &nbsp;<br>
44
+ <br>&nbsp;</span></td></tr>
45
+ <tr><td>&nbsp;</td>
46
+ <td class="singlecolumn">Methods defined here:<br>
47
+ <dl><dt><a name="TechNovaService-__init__"><strong>__init__</strong></a>(self) -&gt; 'None'</dt><dd><span class="code">Initialize&nbsp;self.&nbsp;&nbsp;See&nbsp;help(type(self))&nbsp;for&nbsp;accurate&nbsp;signature.</span></dd></dl>
48
+
49
+ <dl><dt><a name="TechNovaService-adapt_input"><strong>adapt_input</strong></a>(self, request: 'ModelRequest') -&gt; 'pd.DataFrame'</dt><dd><span class="code">#&nbsp;----------&nbsp;Payload&nbsp;----------</span></dd></dl>
50
+
51
+ <dl><dt><a name="TechNovaService-fetch_latest_clean_row"><strong>fetch_latest_clean_row</strong></a>(self, db: 'Session', employee_id: 'int') -&gt; 'dict[str, Any] | None'</dt><dd><span class="code">#&nbsp;----------&nbsp;Clean&nbsp;fetch&nbsp;(isolé&nbsp;pour&nbsp;tests&nbsp;/&nbsp;mocks)&nbsp;----------</span></dd></dl>
52
+
53
+ <dl><dt><a name="TechNovaService-predict_from_clean"><strong>predict_from_clean</strong></a>(self, db: 'Session', employee_id: 'int') -&gt; 'ModelResponse'</dt><dd><span class="code">#&nbsp;----------&nbsp;Predict&nbsp;from&nbsp;clean&nbsp;----------</span></dd></dl>
54
+
55
+ <dl><dt><a name="TechNovaService-predict_from_payload"><strong>predict_from_payload</strong></a>(self, request: 'ModelRequest') -&gt; 'ModelResponse'</dt></dl>
56
+
57
+ <hr>
58
+ Data descriptors defined here:<br>
59
+ <dl><dt><strong>__dict__</strong></dt>
60
+ <dd><span class="code">dictionary&nbsp;for&nbsp;instance&nbsp;variables</span></dd>
61
+ </dl>
62
+ <dl><dt><strong>__weakref__</strong></dt>
63
+ <dd><span class="code">list&nbsp;of&nbsp;weak&nbsp;references&nbsp;to&nbsp;the&nbsp;object</span></dd>
64
+ </dl>
65
+ </td></tr></table></td></tr></table>
66
+ </body></html>
domain/__init__.py ADDED
File without changes
domain/domain.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal, Optional
4
+ from pydantic import BaseModel
5
+
6
+ # Ce module contient les définitions de classes et constantes partagées entre le dashboard Streamlit et l'API FastAPI, notamment la liste des features utilisées par le modèle de prédiction, ainsi que les modèles de données pour les requêtes et réponses de l'API.
7
+
8
+ # Modèles de données pour les requêtes et réponses de l'API, utilisés pour la validation des données d'entrée et de sortie.
9
+ class ModelRequest(BaseModel):
10
+ note_evaluation_precedente: int
11
+ niveau_hierarchique_poste: int
12
+ note_evaluation_actuelle: int
13
+ heures_supplementaires: Literal[0, 1]
14
+ augmentation_salaire_precedente: float
15
+ age: int
16
+ genre: Literal[0, 1]
17
+ revenu_mensuel: int
18
+ statut_marital: str
19
+ departement: str
20
+ poste: str
21
+ nombre_experiences_precedentes: int
22
+ annee_experience_totale: int
23
+ annees_dans_l_entreprise: int
24
+ annees_dans_le_poste_actuel: int
25
+ nombre_participation_pee: int
26
+ nb_formations_suivies: int
27
+ distance_domicile_travail: int
28
+ niveau_education: int
29
+ domaine_etude: str
30
+ frequence_deplacement: Literal[0, 1, 2, 3]
31
+ annees_depuis_la_derniere_promotion: int
32
+ annees_sous_responsable_actuel: int
33
+ satisfaction_moyenne: float
34
+ nonlineaire_participation_pee: float
35
+ ratio_heures_sup_salaire: float
36
+ nonlinaire_charge_contrainte: float
37
+ nonlinaire_surmenage_insatisfaction: float
38
+ jeune_surcharge: Literal[0, 1]
39
+ anciennete_sans_promotion: float
40
+ mobilite_carriere: float
41
+ risque_global: float
42
+
43
+ # Le modèle de réponse inclut la probabilité de départ et la classe prédite (will_leave), ainsi que l'employee_id si disponible.
44
+ class ModelResponse(BaseModel):
45
+ employee_id: Optional[int] = None
46
+ turnover_probability: float
47
+ will_leave: bool
my-postgres/docker-compose.yml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ db:
3
+ image: postgres:16
4
+ container_name: TechNovaPartners
5
+ restart: unless-stopped
6
+ environment:
7
+ POSTGRES_USER: steph
8
+ POSTGRES_PASSWORD: 151176
9
+ POSTGRES_DB: TechNovaPartners
10
+ ports:
11
+ - "5432:5432"
12
+ volumes:
13
+ - pgdata:/var/lib/postgresql/data
14
+
15
+ volumes:
16
+ pgdata:
17
+
18
+ #instruction : docker compose -f my-postgres/docker-compose.yml up -d
19
+ #docker ps
20
+ # si pas -v garde les données même après l'arrêt du conteneur
21
+ #instruction : docker-compose -f my-postgres/docker-compose.yml down
notebooks/projet_technova_partners.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
poetry.lock ADDED
The diff for this file is too large to render. See raw diff
 
pyproject.toml ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.poetry]
2
+ name = "technova"
3
+ version = "0.1.0"
4
+ description = "API et dashboard de prédiction du turnover – TechNova Partners"
5
+ authors = ["Stephane GONZALEZ <gonzalezcourrier34@gmail.com>"]
6
+ readme = "README.md"
7
+ packages = [
8
+ { include = "app" },
9
+ { include = "service" },
10
+ { include = "domain" },
11
+ { include = "dashboard" }
12
+ ]
13
+
14
+ [tool.poetry.dependencies]
15
+ python = "^3.13"
16
+
17
+ # --- API ---
18
+ fastapi = "^0.128.0"
19
+ uvicorn = "^0.40.0"
20
+ pydantic = "^2.12"
21
+ python-dotenv = "^1.2.1"
22
+ requests = "^2.32"
23
+
24
+ # --- Database ---
25
+ sqlalchemy = "^2.0.46"
26
+ psycopg = { version = "^3.3.2", extras = ["binary"] }
27
+
28
+ # --- ML ---
29
+ numpy = "^2.3"
30
+ pandas = "^2.3.3"
31
+ scikit-learn = "^1.8.0"
32
+ xgboost = "^3.1.2"
33
+ imbalanced-learn = "^0.14.1"
34
+ joblib = "^1.5"
35
+ shap = "^0.50.0"
36
+
37
+ # --- Dashboard ---
38
+ streamlit = "^1.53.1"
39
+ altair = "^6.0.0"
40
+ huggingface-hub = "^1.4.1"
41
+
42
+ [tool.poetry.group.dev.dependencies]
43
+ pytest = "^9.0.2"
44
+ pytest-cov = "^7.0.0"
45
+ httpx = "^0.28.1"
46
+
47
+ ipykernel = "^7.1.0"
48
+ ipywidgets = "^8.1.8"
49
+ matplotlib = "^3.10.8"
50
+ seaborn = "^0.13.2"
51
+
52
+ [tool.poetry.scripts]
53
+ technova = "app.main:main"
54
+
55
+ [build-system]
56
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
57
+ build-backend = "poetry.core.masonry.api"
58
+
59
+ [tool.pytest.ini_options]
60
+ markers = ["security: tests related to API security (API key, auth)"]
requirements.txt ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ altair==6.0.0 ; python_version >= "3.13"
2
+ annotated-doc==0.0.4 ; python_version >= "3.13"
3
+ annotated-types==0.7.0 ; python_version >= "3.13"
4
+ anyio==4.12.1 ; python_version >= "3.13"
5
+ appnope==0.1.4 ; python_version >= "3.13" and platform_system == "Darwin"
6
+ asn1crypto==1.5.1 ; python_version >= "3.13"
7
+ asttokens==3.0.1 ; python_version >= "3.13"
8
+ attrs==25.4.0 ; python_version >= "3.13"
9
+ blinker==1.9.0 ; python_version >= "3.13"
10
+ cachetools==6.2.6 ; python_version >= "3.13"
11
+ certifi==2026.1.4 ; python_version >= "3.13"
12
+ cffi==2.0.0 ; python_version >= "3.13" and implementation_name == "pypy"
13
+ charset-normalizer==3.4.4 ; python_version >= "3.13"
14
+ click==8.3.1 ; python_version >= "3.13"
15
+ cloudpickle==3.1.2 ; python_version >= "3.13"
16
+ colorama==0.4.6 ; (sys_platform == "win32" or platform_system == "Windows") and python_version >= "3.13"
17
+ comm==0.2.3 ; python_version >= "3.13"
18
+ datetime==6.0 ; python_version >= "3.13"
19
+ debugpy==1.8.19 ; python_version >= "3.13"
20
+ decorator==5.2.1 ; python_version >= "3.13"
21
+ executing==2.2.1 ; python_version >= "3.13"
22
+ fastapi==0.128.0 ; python_version >= "3.13"
23
+ gitdb==4.0.12 ; python_version >= "3.13"
24
+ gitpython==3.1.46 ; python_version >= "3.13"
25
+ greenlet==3.3.0 ; python_version >= "3.13" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32")
26
+ h11==0.16.0 ; python_version >= "3.13"
27
+ idna==3.11 ; python_version >= "3.13"
28
+ imbalanced-learn==0.14.1 ; python_version >= "3.13"
29
+ imblearn==0.0 ; python_version >= "3.13"
30
+ ipykernel==7.1.0 ; python_version >= "3.13"
31
+ ipython-pygments-lexers==1.1.1 ; python_version >= "3.13"
32
+ ipython==9.8.0 ; python_version >= "3.13"
33
+ ipywidgets==8.1.8 ; python_version >= "3.13"
34
+ jedi==0.19.2 ; python_version >= "3.13"
35
+ jinja2==3.1.6 ; python_version >= "3.13"
36
+ joblib==1.5.3 ; python_version >= "3.13"
37
+ jsonschema-specifications==2025.9.1 ; python_version >= "3.13"
38
+ jsonschema==4.26.0 ; python_version >= "3.13"
39
+ jupyter-client==8.7.0 ; python_version >= "3.13"
40
+ jupyter-core==5.9.1 ; python_version >= "3.13"
41
+ jupyterlab-widgets==3.0.16 ; python_version >= "3.13"
42
+ llvmlite==0.46.0 ; python_version == "3.13"
43
+ llvmlite==0.46.0b1 ; python_version >= "3.14"
44
+ markupsafe==3.0.3 ; python_version >= "3.13"
45
+ matplotlib-inline==0.2.1 ; python_version >= "3.13"
46
+ narwhals==2.16.0 ; python_version >= "3.13"
47
+ nest-asyncio==1.6.0 ; python_version >= "3.13"
48
+ numba==0.63.0b1 ; python_version >= "3.14"
49
+ numba==0.63.1 ; python_version == "3.13"
50
+ numpy==2.3.5 ; python_version == "3.13"
51
+ numpy==2.4.0 ; python_version >= "3.14"
52
+ nvidia-nccl-cu12==2.28.9 ; platform_system == "Linux" and platform_machine != "aarch64" and python_version >= "3.13"
53
+ packaging==25.0 ; python_version >= "3.13"
54
+ pandas==2.3.3 ; python_version >= "3.13"
55
+ parso==0.8.5 ; python_version >= "3.13"
56
+ pexpect==4.9.0 ; python_version >= "3.13" and sys_platform != "win32" and sys_platform != "emscripten"
57
+ pg8000==1.31.5 ; python_version >= "3.13"
58
+ pillow==12.0.0 ; python_version >= "3.13"
59
+ platformdirs==4.5.1 ; python_version >= "3.13"
60
+ prompt-toolkit==3.0.52 ; python_version >= "3.13"
61
+ protobuf==6.33.5 ; python_version >= "3.13"
62
+ psutil==7.1.3 ; python_version >= "3.13"
63
+ psycopg2==2.9.11 ; python_version >= "3.13"
64
+ psycopg==3.3.2 ; python_version >= "3.13"
65
+ ptyprocess==0.7.0 ; python_version >= "3.13" and sys_platform != "win32" and sys_platform != "emscripten"
66
+ pure-eval==0.2.3 ; python_version >= "3.13"
67
+ pyarrow==23.0.0 ; python_version >= "3.13"
68
+ pycparser==2.23 ; python_version >= "3.13" and implementation_name == "pypy"
69
+ pydantic-core==2.41.5 ; python_version >= "3.13"
70
+ pydantic==2.12.5 ; python_version >= "3.13"
71
+ pydeck==0.9.1 ; python_version >= "3.13"
72
+ pygments==2.19.2 ; python_version >= "3.13"
73
+ python-dateutil==2.9.0.post0 ; python_version >= "3.13"
74
+ python-dotenv==1.2.1 ; python_version >= "3.13"
75
+ pytz==2025.2 ; python_version >= "3.13"
76
+ pyzmq==27.1.0 ; python_version >= "3.13"
77
+ referencing==0.37.0 ; python_version >= "3.13"
78
+ requests==2.32.5 ; python_version >= "3.13"
79
+ rpds-py==0.30.0 ; python_version >= "3.13"
80
+ scikit-learn==1.8.0 ; python_version >= "3.13"
81
+ scipy==1.16.3 ; python_version >= "3.13"
82
+ scramp==1.4.8 ; python_version >= "3.13"
83
+ shap==0.50.0 ; python_version >= "3.13"
84
+ six==1.17.0 ; python_version >= "3.13"
85
+ sklearn-compat==0.1.5 ; python_version >= "3.13"
86
+ slicer==0.0.8 ; python_version >= "3.13"
87
+ smmap==5.0.2 ; python_version >= "3.13"
88
+ sqlalchemy==2.0.46 ; python_version >= "3.13"
89
+ stack-data==0.6.3 ; python_version >= "3.13"
90
+ starlette==0.50.0 ; python_version >= "3.13"
91
+ streamlit==1.53.1 ; python_version >= "3.13"
92
+ tenacity==9.1.2 ; python_version >= "3.13"
93
+ threadpoolctl==3.6.0 ; python_version >= "3.13"
94
+ toml==0.10.2 ; python_version >= "3.13"
95
+ tornado==6.5.4 ; python_version >= "3.13"
96
+ tqdm==4.67.1 ; python_version >= "3.13"
97
+ traitlets==5.14.3 ; python_version >= "3.13"
98
+ typing-extensions==4.15.0 ; python_version >= "3.13"
99
+ typing-inspection==0.4.2 ; python_version >= "3.13"
100
+ tzdata==2025.3 ; python_version >= "3.13"
101
+ urllib3==2.6.3 ; python_version >= "3.13"
102
+ uvicorn==0.40.0 ; python_version >= "3.13"
103
+ watchdog==6.0.0 ; python_version >= "3.13" and platform_system != "Darwin"
104
+ wcwidth==0.2.14 ; python_version >= "3.13"
105
+ widgetsnbextension==4.0.15 ; python_version >= "3.13"
106
+ xgboost==3.1.2 ; python_version >= "3.13"
107
+ zope-interface==8.2 ; python_version >= "3.13"
scripts/build_ml_features.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from sqlalchemy import create_engine, text
3
+ from dotenv import load_dotenv, find_dotenv
4
+
5
+ load_dotenv(find_dotenv())
6
+ DATABASE_URL = os.getenv("DATABASE_URL")
7
+ if not DATABASE_URL:
8
+ raise RuntimeError("DATABASE_URL manquant dans .env")
9
+
10
+ # Créer une connexion à la base de données
11
+ engine = create_engine(DATABASE_URL, pool_pre_ping=True)
12
+
13
+ # DDL pour créer la table clean.ml_features_employees
14
+ DDL = """
15
+ CREATE SCHEMA IF NOT EXISTS raw;
16
+ CREATE SCHEMA IF NOT EXISTS clean;
17
+
18
+ CREATE TABLE IF NOT EXISTS clean.ml_features_employees (
19
+ id BIGSERIAL PRIMARY KEY,
20
+
21
+ employee_id BIGINT NOT NULL
22
+ REFERENCES raw.employees(id) ON DELETE CASCADE,
23
+
24
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
25
+
26
+ note_evaluation_precedente SMALLINT NOT NULL,
27
+ niveau_hierarchique_poste SMALLINT NOT NULL,
28
+ note_evaluation_actuelle SMALLINT NOT NULL,
29
+
30
+ heures_supplementaires SMALLINT NOT NULL CHECK (heures_supplementaires IN (0,1)),
31
+ augmentation_salaire_precedente NUMERIC(10,2) NOT NULL CHECK (augmentation_salaire_precedente >= 0),
32
+
33
+ age SMALLINT NOT NULL CHECK (age BETWEEN 16 AND 80),
34
+ genre SMALLINT NOT NULL CHECK (genre IN (0,1)),
35
+ revenu_mensuel INTEGER NOT NULL CHECK (revenu_mensuel >= 0),
36
+
37
+ statut_marital VARCHAR(50) NOT NULL,
38
+ departement VARCHAR(120) NOT NULL,
39
+ poste VARCHAR(120) NOT NULL,
40
+
41
+ nombre_experiences_precedentes SMALLINT NOT NULL CHECK (nombre_experiences_precedentes >= 0),
42
+ annee_experience_totale SMALLINT NOT NULL CHECK (annee_experience_totale >= 0),
43
+ annees_dans_l_entreprise SMALLINT NOT NULL CHECK (annees_dans_l_entreprise >= 0),
44
+ annees_dans_le_poste_actuel SMALLINT NOT NULL CHECK (annees_dans_le_poste_actuel >= 0),
45
+
46
+ a_quitte_l_entreprise SMALLINT NOT NULL CHECK (a_quitte_l_entreprise IN (0,1)),
47
+
48
+ nombre_participation_pee SMALLINT NOT NULL CHECK (nombre_participation_pee >= 0),
49
+ nb_formations_suivies SMALLINT NOT NULL CHECK (nb_formations_suivies >= 0),
50
+ distance_domicile_travail SMALLINT NOT NULL CHECK (distance_domicile_travail >= 0),
51
+
52
+ niveau_education SMALLINT NOT NULL CHECK (niveau_education BETWEEN 1 AND 5),
53
+ domaine_etude VARCHAR(120) NOT NULL,
54
+ frequence_deplacement SMALLINT NOT NULL CHECK (frequence_deplacement BETWEEN 0 AND 3),
55
+
56
+ annees_depuis_la_derniere_promotion SMALLINT NOT NULL CHECK (annees_depuis_la_derniere_promotion >= 0),
57
+ annees_sous_responsable_actuel SMALLINT NOT NULL CHECK (annees_sous_responsable_actuel >= 0),
58
+
59
+ satisfaction_moyenne NUMERIC(10,4) NOT NULL,
60
+ nonlineaire_participation_pee NUMERIC(18,16) NOT NULL,
61
+ ratio_heures_sup_salaire NUMERIC(18,16) NOT NULL,
62
+ nonlinaire_charge_contrainte NUMERIC(18,16) NOT NULL,
63
+ nonlinaire_surmenage_insatisfaction NUMERIC(18,16) NOT NULL,
64
+
65
+ jeune_surcharge SMALLINT NOT NULL CHECK (jeune_surcharge IN (0,1)),
66
+ anciennete_sans_promotion NUMERIC(18,16) NOT NULL,
67
+ mobilite_carriere NUMERIC(18,16) NOT NULL,
68
+ risque_global NUMERIC(18,16) NOT NULL
69
+ );
70
+
71
+ CREATE INDEX IF NOT EXISTS idx_clean_employee_id_created_at
72
+ ON clean.ml_features_employees(employee_id, created_at);
73
+
74
+ CREATE INDEX IF NOT EXISTS idx_ml_features_target
75
+ ON clean.ml_features_employees(a_quitte_l_entreprise);
76
+
77
+ CREATE INDEX IF NOT EXISTS idx_ml_features_target_created_at
78
+ ON clean.ml_features_employees(a_quitte_l_entreprise, created_at);
79
+ """
80
+
81
+ # Fonction pour exécuter les commandes DDL
82
+ def _exec_ddl(conn, ddl: str) -> None:
83
+ parts = [p.strip() for p in ddl.split(";") if p.strip()]
84
+ for stmt in parts:
85
+ conn.execute(text(stmt))
86
+
87
+ # Point d'entrée du script
88
+ def main():
89
+ with engine.begin() as conn:
90
+ _exec_ddl(conn, DDL)
91
+ print("Table clean.ml_features_employees créée / vérifiée (avec employee_id)")
92
+
93
+ if __name__ == "__main__":
94
+ main()
scripts/create_db.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from sqlalchemy import create_engine, text
3
+ from dotenv import load_dotenv, find_dotenv
4
+
5
+ load_dotenv(find_dotenv())
6
+ DATABASE_URL = os.getenv("DATABASE_URL")
7
+ if not DATABASE_URL:
8
+ raise RuntimeError("DATABASE_URL manquant dans .env")
9
+
10
+ engine = create_engine(DATABASE_URL, pool_pre_ping=True)
11
+
12
+ # DDL pour créer les schemas et tables de la base de données
13
+ DDL = """
14
+ -- SCHEMAS
15
+ CREATE SCHEMA IF NOT EXISTS raw;
16
+ CREATE SCHEMA IF NOT EXISTS clean;
17
+ CREATE SCHEMA IF NOT EXISTS app;
18
+
19
+ -- RAW TABLES
20
+ CREATE TABLE IF NOT EXISTS raw.employees (
21
+ id BIGSERIAL PRIMARY KEY,
22
+ employee_external_id INTEGER NOT NULL UNIQUE,
23
+
24
+ age INTEGER,
25
+ genre VARCHAR(20),
26
+ statut_marital VARCHAR(50),
27
+ ayant_enfants VARCHAR(10),
28
+ niveau_education INTEGER,
29
+ domaine_etude VARCHAR(120),
30
+ departement VARCHAR(120),
31
+ poste VARCHAR(120),
32
+ distance_domicile_travail INTEGER,
33
+
34
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
35
+ );
36
+
37
+ CREATE INDEX IF NOT EXISTS ix_raw_employees_employee_external_id
38
+ ON raw.employees(employee_external_id);
39
+
40
+ CREATE TABLE IF NOT EXISTS raw.employee_snapshots (
41
+ id BIGSERIAL PRIMARY KEY,
42
+ employee_id BIGINT NOT NULL REFERENCES raw.employees(id) ON DELETE CASCADE,
43
+
44
+ nombre_experiences_precedentes INTEGER,
45
+ nombre_heures_travaillees INTEGER,
46
+ annee_experience_totale INTEGER,
47
+ annees_dans_l_entreprise INTEGER,
48
+ annees_dans_le_poste_actuel INTEGER,
49
+ annees_sous_responsable_actuel INTEGER,
50
+ niveau_hierarchique_poste INTEGER,
51
+ revenu_mensuel INTEGER,
52
+
53
+ augmentation_salaire_precedente VARCHAR(50),
54
+ heures_supplementaires VARCHAR(50),
55
+
56
+ nombre_participation_pee INTEGER,
57
+ nb_formations_suivies INTEGER,
58
+ nombre_employee_sous_responsabilite INTEGER,
59
+
60
+ frequence_deplacement VARCHAR(50),
61
+ annees_depuis_la_derniere_promotion INTEGER,
62
+
63
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
64
+ );
65
+
66
+ CREATE INDEX IF NOT EXISTS ix_raw_snapshots_employee_id_created_at
67
+ ON raw.employee_snapshots(employee_id, created_at);
68
+
69
+ CREATE TABLE IF NOT EXISTS raw.surveys (
70
+ id BIGSERIAL PRIMARY KEY,
71
+ employee_id BIGINT NOT NULL REFERENCES raw.employees(id) ON DELETE CASCADE,
72
+
73
+ code_sondage INTEGER,
74
+ eval_number VARCHAR(50),
75
+
76
+ note_evaluation_precedente INTEGER,
77
+ note_evaluation_actuelle INTEGER,
78
+
79
+ satisfaction_employee_environnement INTEGER,
80
+ satisfaction_employee_nature_travail INTEGER,
81
+ satisfaction_employee_equipe INTEGER,
82
+ satisfaction_employee_equilibre_pro_perso INTEGER,
83
+
84
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
85
+ );
86
+
87
+ CREATE INDEX IF NOT EXISTS ix_raw_surveys_employee_id_created_at
88
+ ON raw.surveys(employee_id, created_at);
89
+
90
+ CREATE TABLE IF NOT EXISTS raw.ground_truth (
91
+ id BIGSERIAL PRIMARY KEY,
92
+ employee_id BIGINT NOT NULL REFERENCES raw.employees(id) ON DELETE CASCADE,
93
+
94
+ date_event TIMESTAMPTZ NOT NULL DEFAULT now(),
95
+ a_quitte_l_entreprise INTEGER NOT NULL CHECK (a_quitte_l_entreprise IN (0,1)),
96
+
97
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
98
+ );
99
+
100
+ CREATE INDEX IF NOT EXISTS ix_ground_truth_employee_id_date_event
101
+ ON raw.ground_truth(employee_id, date_event);
102
+
103
+ -- APP LOGGING TABLES
104
+ CREATE TABLE IF NOT EXISTS app.prediction_requests (
105
+ id BIGSERIAL PRIMARY KEY,
106
+
107
+ employee_id BIGINT REFERENCES raw.employees(id) ON DELETE SET NULL,
108
+ snapshot_id BIGINT REFERENCES raw.employee_snapshots(id) ON DELETE SET NULL,
109
+ survey_id BIGINT REFERENCES raw.surveys(id) ON DELETE SET NULL,
110
+
111
+ payload_json JSONB NOT NULL,
112
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
113
+ );
114
+
115
+ CREATE INDEX IF NOT EXISTS ix_prediction_requests_created_at
116
+ ON app.prediction_requests(created_at);
117
+
118
+ CREATE TABLE IF NOT EXISTS app.predictions (
119
+ id BIGSERIAL PRIMARY KEY,
120
+ request_id BIGINT NOT NULL REFERENCES app.prediction_requests(id) ON DELETE CASCADE,
121
+
122
+ model_version VARCHAR(50) NOT NULL,
123
+ predicted_class INTEGER NOT NULL,
124
+ predicted_proba DOUBLE PRECISION NOT NULL,
125
+ threshold_used DOUBLE PRECISION NOT NULL,
126
+ latency_ms INTEGER,
127
+
128
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
129
+ );
130
+
131
+ CREATE INDEX IF NOT EXISTS ix_predictions_request_id_created_at
132
+ ON app.predictions(request_id, created_at);
133
+
134
+ CREATE INDEX IF NOT EXISTS ix_predictions_created_at
135
+ ON app.predictions(created_at);
136
+
137
+ CREATE INDEX IF NOT EXISTS ix_predictions_model_version
138
+ ON app.predictions(model_version);
139
+ """
140
+
141
+ # Fonction utilitaire pour exécuter du DDL de manière robuste
142
+ def _exec_ddl(conn, ddl: str) -> None:
143
+ # robuste: exécute statement par statement
144
+ parts = [p.strip() for p in ddl.split(";") if p.strip()]
145
+ for stmt in parts:
146
+ conn.execute(text(stmt))
147
+
148
+ # Point d'entrée du script
149
+ def main():
150
+ with engine.begin() as conn:
151
+ _exec_ddl(conn, DDL)
152
+ print("Schemas + tables créés / vérifiés (raw / clean / app)")
153
+
154
+ if __name__ == "__main__":
155
+ main()
scripts/generate_docs.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import webbrowser
3
+
4
+ # les docs à générer sont celles des modules suivants, qui contiennent les fonctions et classes principales de l'app:
5
+ modules = [
6
+ "app.models",
7
+ "app.api",
8
+ "domain.domain",
9
+ "service.technova_service",
10
+ ]
11
+
12
+ DOCS_DIR = "docs"
13
+
14
+ # Crée le dossier docs s'il n'existe pas
15
+ os.makedirs(DOCS_DIR, exist_ok=True)
16
+
17
+ # Se placer dans docs
18
+ os.chdir(DOCS_DIR)
19
+
20
+ # Génération avec pydoc
21
+ for m in modules:
22
+ os.system(f"python -m pydoc -w {m}")
23
+
24
+ # Ouvre la doc principale
25
+ webbrowser.open("app.models.html")
26
+ webbrowser.open("app.api.html")
scripts/init_project_technova.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from scripts.create_db import main as create_db
2
+ from scripts.seed_from_csv import main as seed_csv
3
+ from scripts.build_ml_features import main as build_ml_features
4
+ from scripts.seed_ml_features import main as seed_ml_features
5
+
6
+ # Script d'initialisation du projet : crée la base, ingère les CSV, construit les features, etc.
7
+ def main():
8
+ print("Initialisation du projet")
9
+
10
+ print("Création / vérification du schéma")
11
+ create_db()
12
+
13
+ print("Ingestion des CSV")
14
+ seed_csv()
15
+
16
+ print("Construction table ML features")
17
+ build_ml_features()
18
+
19
+ print("Remplissage table ML features")
20
+ seed_ml_features()
21
+
22
+ print("Initialisation terminée")
23
+
24
+ if __name__ == "__main__":
25
+ main()
scripts/query_examples.sql ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- DERNIÈRES PRÉDICTIONS EFFECTUÉES
2
+ SELECT
3
+ p.id AS prediction_id,
4
+ p.created_at AS prediction_date,
5
+ p.model_version,
6
+ p.predicted_class,
7
+ p.predicted_proba,
8
+ p.threshold_used,
9
+ p.latency_ms,
10
+ r.employee_id
11
+ FROM app.predictions p
12
+ JOIN app.prediction_requests r
13
+ ON p.request_id = r.id
14
+ ORDER BY p.created_at DESC
15
+ LIMIT 20;
16
+
17
+
18
+ -- DERNIÈRES PRÉDICTIONS AVEC INFORMATIONS EMPLOYÉ
19
+ SELECT
20
+ e.employee_external_id,
21
+ e.departement,
22
+ e.poste,
23
+ p.predicted_class,
24
+ p.predicted_proba,
25
+ p.created_at
26
+ FROM app.predictions p
27
+ JOIN app.prediction_requests r
28
+ ON p.request_id = r.id
29
+ JOIN raw.employees e
30
+ ON r.employee_id = e.id
31
+ ORDER BY p.created_at DESC
32
+ LIMIT 20;
33
+
34
+
35
+ -- NOMBRE DE PRÉDICTIONS PAR CLASSE
36
+ SELECT
37
+ predicted_class,
38
+ COUNT(*) AS nb_predictions
39
+ FROM app.predictions
40
+ GROUP BY predicted_class
41
+ ORDER BY predicted_class;
42
+
43
+
44
+ -- TAUX DE RISQUE MOYEN PAR DÉPARTEMENT
45
+ SELECT
46
+ e.departement,
47
+ ROUND(AVG(p.predicted_proba)::numeric, 3) AS risque_moyen,
48
+ COUNT(*) AS nb_predictions
49
+ FROM app.predictions p
50
+ JOIN app.prediction_requests r
51
+ ON p.request_id = r.id
52
+ JOIN raw.employees e
53
+ ON r.employee_id = e.id
54
+ GROUP BY e.departement
55
+ ORDER BY risque_moyen DESC;
56
+
57
+
58
+ -- EMPLOYÉS AVEC RISQUE ÉLEVÉ (> 0.8)
59
+ SELECT
60
+ e.employee_external_id,
61
+ e.departement,
62
+ e.poste,
63
+ p.predicted_proba,
64
+ p.created_at
65
+ FROM app.predictions p
66
+ JOIN app.prediction_requests r
67
+ ON p.request_id = r.id
68
+ JOIN raw.employees e
69
+ ON r.employee_id = e.id
70
+ WHERE p.predicted_proba >= 0.8
71
+ ORDER BY p.predicted_proba DESC;
72
+
73
+
74
+ -- COMPARAISON PRÉDICTION vs RÉALITÉ (DERNIER GROUND TRUTH PAR EMPLOYÉ)
75
+ WITH last_truth AS (
76
+ SELECT DISTINCT ON (employee_id)
77
+ employee_id,
78
+ a_quitte_l_entreprise,
79
+ date_event
80
+ FROM raw.ground_truth
81
+ ORDER BY employee_id, date_event DESC
82
+ )
83
+ SELECT
84
+ e.employee_external_id,
85
+ p.predicted_class,
86
+ gt.a_quitte_l_entreprise,
87
+ p.predicted_proba,
88
+ p.created_at AS prediction_date,
89
+ gt.date_event AS real_event_date
90
+ FROM app.predictions p
91
+ JOIN app.prediction_requests r
92
+ ON p.request_id = r.id
93
+ JOIN raw.employees e
94
+ ON r.employee_id = e.id
95
+ JOIN last_truth gt
96
+ ON gt.employee_id = e.id
97
+ ORDER BY p.created_at DESC
98
+ LIMIT 50;
99
+
100
+
101
+ -- MATRICE DE CONFUSION (SIMPLIFIÉE) - DERNIER GROUND TRUTH PAR EMPLOYÉ
102
+ WITH last_truth AS (
103
+ SELECT DISTINCT ON (employee_id)
104
+ employee_id,
105
+ a_quitte_l_entreprise
106
+ FROM raw.ground_truth
107
+ ORDER BY employee_id, date_event DESC
108
+ )
109
+ SELECT
110
+ p.predicted_class AS prediction,
111
+ gt.a_quitte_l_entreprise AS real_value,
112
+ COUNT(*) AS count
113
+ FROM app.predictions p
114
+ JOIN app.prediction_requests r
115
+ ON p.request_id = r.id
116
+ JOIN last_truth gt
117
+ ON gt.employee_id = r.employee_id
118
+ GROUP BY p.predicted_class, gt.a_quitte_l_entreprise
119
+ ORDER BY p.predicted_class, gt.a_quitte_l_entreprise;
120
+
121
+
122
+ -- LATENCE MOYENNE DES PRÉDICTIONS
123
+ SELECT
124
+ ROUND(AVG(latency_ms)::numeric, 2) AS avg_latency_ms,
125
+ MAX(latency_ms) AS max_latency_ms,
126
+ MIN(latency_ms) AS min_latency_ms
127
+ FROM app.predictions;
128
+
129
+
130
+ -- HISTORIQUE DES PRÉDICTIONS POUR UN EMPLOYÉ DONNÉ
131
+ SELECT
132
+ p.created_at,
133
+ p.predicted_class,
134
+ p.predicted_proba,
135
+ p.model_version
136
+ FROM app.predictions p
137
+ JOIN app.prediction_requests r
138
+ ON p.request_id = r.id
139
+ JOIN raw.employees e
140
+ ON r.employee_id = e.id
141
+ WHERE e.employee_external_id = 123
142
+ ORDER BY p.created_at DESC;
143
+
144
+
145
+ -- EMPLOYÉS À RISQUE MAIS TOUJOURS PRÉSENTS (DERNIER GROUND TRUTH)
146
+ WITH last_truth AS (
147
+ SELECT DISTINCT ON (employee_id)
148
+ employee_id,
149
+ a_quitte_l_entreprise
150
+ FROM raw.ground_truth
151
+ ORDER BY employee_id, date_event DESC
152
+ )
153
+ SELECT
154
+ e.employee_external_id,
155
+ e.departement,
156
+ e.poste,
157
+ p.predicted_proba
158
+ FROM app.predictions p
159
+ JOIN app.prediction_requests r
160
+ ON p.request_id = r.id
161
+ JOIN raw.employees e
162
+ ON r.employee_id = e.id
163
+ JOIN last_truth gt
164
+ ON gt.employee_id = e.id
165
+ WHERE p.predicted_class = 1
166
+ AND gt.a_quitte_l_entreprise = 0
167
+ ORDER BY p.predicted_proba DESC;
scripts/seed_from_csv.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ import pandas as pd
9
+ from dotenv import load_dotenv, find_dotenv
10
+ from sqlalchemy import create_engine, text, bindparam
11
+
12
+ load_dotenv(find_dotenv())
13
+
14
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s | %(message)s")
15
+ logger = logging.getLogger("technova.seed")
16
+
17
+ # Utilitaire pour convertir des valeurs oui/non en 1/0
18
+ def yesno_to_int(x):
19
+ if x is None:
20
+ return None
21
+ if isinstance(x, bool):
22
+ return int(x)
23
+ if isinstance(x, (int, float)) and x in (0, 1):
24
+ return int(x)
25
+
26
+ s = str(x).strip().lower()
27
+ if s in {"oui", "y", "yes", "1", "true", "vrai"}:
28
+ return 1
29
+ if s in {"non", "n", "no", "0", "false", "faux"}:
30
+ return 0
31
+ return None
32
+
33
+ # Normalisation basique des colonnes (trim, etc.)
34
+ def norm_cols(df: pd.DataFrame) -> pd.DataFrame:
35
+ df = df.copy()
36
+ df.columns = [c.strip() for c in df.columns]
37
+ return df
38
+
39
+ # Utilitaire pour pick une valeur parmi plusieurs clés possibles (utile pour gérer les variations de noms de colonnes)
40
+ def pick(r: dict, *keys, default=None):
41
+ for k in keys:
42
+ if k in r and r.get(k) is not None:
43
+ return r.get(k)
44
+ return default
45
+
46
+ # Point d'entrée du script
47
+ def main(
48
+ sirh_csv: str = "data/extrait_sirh.csv",
49
+ eval_csv: str = "data/extrait_eval.csv",
50
+ sondage_csv: str = "data/extrait_sondage.csv",
51
+ ):
52
+ refresh = "--refresh" in sys.argv
53
+
54
+ db_url = os.getenv("DATABASE_URL")
55
+ if not db_url:
56
+ raise RuntimeError("DATABASE_URL manquant dans .env")
57
+
58
+ engine = create_engine(db_url, pool_pre_ping=True)
59
+
60
+ base = Path(__file__).resolve().parents[1]
61
+ sirh_path = (base / sirh_csv).resolve()
62
+ eval_path = (base / eval_csv).resolve()
63
+ sondage_path = (base / sondage_csv).resolve()
64
+
65
+ for p in (sirh_path, eval_path, sondage_path):
66
+ if not p.exists():
67
+ raise RuntimeError(f"Fichier introuvable: {p}")
68
+
69
+ df_sirh = norm_cols(pd.read_csv(sirh_path))
70
+ df_eval = norm_cols(pd.read_csv(eval_path))
71
+ df_sond = norm_cols(pd.read_csv(sondage_path))
72
+
73
+ if "id_employee" not in df_sirh.columns:
74
+ raise RuntimeError("extrait_sirh.csv doit contenir la colonne id_employee")
75
+
76
+ if not (len(df_sirh) == len(df_eval) == len(df_sond)):
77
+ raise RuntimeError("Les 3 CSV doivent avoir le même nombre de lignes (jointure par index).")
78
+
79
+ df = pd.concat([df_sirh, df_eval, df_sond], axis=1)
80
+
81
+ # Remplace NaN -> None
82
+ df = df.where(pd.notnull(df), None)
83
+ # Optionnel mais utile: transforme "" / " " -> None
84
+ df = df.replace(r"^\s*$", None, regex=True)
85
+
86
+ # SQL (SCHEMA raw.*)
87
+ upsert_employees = text(
88
+ """
89
+ INSERT INTO raw.employees (
90
+ employee_external_id, age, genre, statut_marital, ayant_enfants,
91
+ niveau_education, domaine_etude, departement, poste, distance_domicile_travail
92
+ )
93
+ VALUES (
94
+ :employee_external_id, :age, :genre, :statut_marital, :ayant_enfants,
95
+ :niveau_education, :domaine_etude, :departement, :poste, :distance_domicile_travail
96
+ )
97
+ ON CONFLICT (employee_external_id) DO UPDATE SET
98
+ age = EXCLUDED.age,
99
+ genre = EXCLUDED.genre,
100
+ statut_marital = EXCLUDED.statut_marital,
101
+ ayant_enfants = EXCLUDED.ayant_enfants,
102
+ niveau_education = EXCLUDED.niveau_education,
103
+ domaine_etude = EXCLUDED.domaine_etude,
104
+ departement = EXCLUDED.departement,
105
+ poste = EXCLUDED.poste,
106
+ distance_domicile_travail = EXCLUDED.distance_domicile_travail
107
+ """
108
+ )
109
+
110
+ select_emp_map = (
111
+ text(
112
+ """
113
+ SELECT id, employee_external_id
114
+ FROM raw.employees
115
+ WHERE employee_external_id IN :ext_ids
116
+ """
117
+ )
118
+ .bindparams(bindparam("ext_ids", expanding=True))
119
+ )
120
+
121
+ insert_snapshot = text(
122
+ """
123
+ INSERT INTO raw.employee_snapshots (
124
+ employee_id,
125
+ nombre_experiences_precedentes,
126
+ nombre_heures_travaillees,
127
+ annee_experience_totale,
128
+ annees_dans_l_entreprise,
129
+ annees_dans_le_poste_actuel,
130
+ annees_sous_responsable_actuel,
131
+ niveau_hierarchique_poste,
132
+ revenu_mensuel,
133
+ augmentation_salaire_precedente,
134
+ heures_supplementaires,
135
+ nombre_participation_pee,
136
+ nb_formations_suivies,
137
+ nombre_employee_sous_responsabilite,
138
+ frequence_deplacement,
139
+ annees_depuis_la_derniere_promotion
140
+ )
141
+ VALUES (
142
+ :employee_id,
143
+ :nombre_experiences_precedentes,
144
+ :nombre_heures_travaillees,
145
+ :annee_experience_totale,
146
+ :annees_dans_l_entreprise,
147
+ :annees_dans_le_poste_actuel,
148
+ :annees_sous_responsable_actuel,
149
+ :niveau_hierarchique_poste,
150
+ :revenu_mensuel,
151
+ :augmentation_salaire_precedente,
152
+ :heures_supplementaires,
153
+ :nombre_participation_pee,
154
+ :nb_formations_suivies,
155
+ :nombre_employee_sous_responsabilite,
156
+ :frequence_deplacement,
157
+ :annees_depuis_la_derniere_promotion
158
+ )
159
+ """
160
+ )
161
+
162
+ insert_survey = text(
163
+ """
164
+ INSERT INTO raw.surveys (
165
+ employee_id,
166
+ code_sondage,
167
+ eval_number,
168
+ note_evaluation_precedente,
169
+ note_evaluation_actuelle,
170
+ satisfaction_employee_environnement,
171
+ satisfaction_employee_nature_travail,
172
+ satisfaction_employee_equipe,
173
+ satisfaction_employee_equilibre_pro_perso
174
+ )
175
+ VALUES (
176
+ :employee_id,
177
+ :code_sondage,
178
+ :eval_number,
179
+ :note_evaluation_precedente,
180
+ :note_evaluation_actuelle,
181
+ :satisfaction_employee_environnement,
182
+ :satisfaction_employee_nature_travail,
183
+ :satisfaction_employee_equipe,
184
+ :satisfaction_employee_equilibre_pro_perso
185
+ )
186
+ """
187
+ )
188
+
189
+ insert_gt = text(
190
+ """
191
+ INSERT INTO raw.ground_truth (employee_id, date_event, a_quitte_l_entreprise)
192
+ VALUES (:employee_id, now(), :a_quitte_l_entreprise)
193
+ """
194
+ )
195
+
196
+ # Build records
197
+ employees_records = []
198
+ for r in df.to_dict(orient="records"):
199
+ employees_records.append(
200
+ {
201
+ "employee_external_id": int(r["id_employee"]),
202
+ "age": r.get("age"),
203
+ "genre": r.get("genre"),
204
+ "statut_marital": r.get("statut_marital"),
205
+ "ayant_enfants": r.get("ayant_enfants"),
206
+ "niveau_education": r.get("niveau_education"),
207
+ "domaine_etude": r.get("domaine_etude"),
208
+ "departement": r.get("departement"),
209
+ "poste": r.get("poste"),
210
+ "distance_domicile_travail": r.get("distance_domicile_travail")
211
+ }
212
+ )
213
+
214
+ ext_ids = df["id_employee"].astype(int).unique().tolist()
215
+
216
+ # RUN
217
+ with engine.begin() as conn:
218
+ if refresh:
219
+ logger.info("Mode --refresh: purge raw tables (snapshots/surveys/ground_truth)")
220
+ conn.execute(text("TRUNCATE TABLE raw.employee_snapshots RESTART IDENTITY CASCADE"))
221
+ conn.execute(text("TRUNCATE TABLE raw.surveys RESTART IDENTITY CASCADE"))
222
+ conn.execute(text("TRUNCATE TABLE raw.ground_truth RESTART IDENTITY CASCADE"))
223
+ # raw.employees: on garde l'upsert
224
+
225
+ # 1) upsert employees
226
+ conn.execute(upsert_employees, employees_records)
227
+
228
+ # 2) map ext -> id
229
+ emp_rows = conn.execute(select_emp_map, {"ext_ids": ext_ids}).mappings().all()
230
+ ext_to_id = {int(r["employee_external_id"]): int(r["id"]) for r in emp_rows}
231
+
232
+ snapshots_records = []
233
+ surveys_records = []
234
+ gt_records = []
235
+
236
+ for r in df.to_dict(orient="records"):
237
+ ext = int(r["id_employee"])
238
+ employee_id = ext_to_id.get(ext)
239
+ if not employee_id:
240
+ continue
241
+
242
+ snapshots_records.append(
243
+ {
244
+ "employee_id": employee_id,
245
+ "nombre_experiences_precedentes": pick(r, "nombre_experiences_precedentes"),
246
+ "nombre_heures_travaillees": pick(r, "nombre_heures_travaillees", "nombre_heures_travailless"),
247
+ "annee_experience_totale": pick(r, "annee_experience_totale"),
248
+ "annees_dans_l_entreprise": pick(r, "annees_dans_l_entreprise"),
249
+ "annees_dans_le_poste_actuel": pick(r, "annees_dans_le_poste_actuel"),
250
+ "annees_sous_responsable_actuel": pick(r, "annees_sous_responsable_actuel", "annes_sous_responsable_actuel"),
251
+ "niveau_hierarchique_poste": pick(r, "niveau_hierarchique_poste"),
252
+ "revenu_mensuel": pick(r, "revenu_mensuel"),
253
+ "augmentation_salaire_precedente": pick(r, "augmentation_salaire_precedente", "augementation_salaire_precedente"),
254
+ "heures_supplementaires": pick(r, "heures_supplementaires", "heure_supplementaires"),
255
+ "nombre_participation_pee": pick(r, "nombre_participation_pee"),
256
+ "nb_formations_suivies": pick(r, "nb_formations_suivies"),
257
+ "nombre_employee_sous_responsabilite": pick(r, "nombre_employee_sous_responsabilite"),
258
+ "frequence_deplacement": pick(r, "frequence_deplacement"),
259
+ "annees_depuis_la_derniere_promotion": pick(r, "annees_depuis_la_derniere_promotion")
260
+ }
261
+ )
262
+
263
+ surveys_records.append(
264
+ {
265
+ "employee_id": employee_id,
266
+ "code_sondage": pick(r, "code_sondage"),
267
+ "eval_number": pick(r, "eval_number"),
268
+ "note_evaluation_precedente": pick(r, "note_evaluation_precedente"),
269
+ "note_evaluation_actuelle": pick(r, "note_evaluation_actuelle"),
270
+ "satisfaction_employee_environnement": pick(r, "satisfaction_employee_environnement"),
271
+ "satisfaction_employee_nature_travail": pick(r, "satisfaction_employee_nature_travail"),
272
+ "satisfaction_employee_equipe": pick(r, "satisfaction_employee_equipe"),
273
+ "satisfaction_employee_equilibre_pro_perso": pick(r, "satisfaction_employee_equilibre_pro_perso")
274
+ }
275
+ )
276
+
277
+ gt_records.append(
278
+ {
279
+ "employee_id": employee_id,
280
+ "a_quitte_l_entreprise": yesno_to_int(pick(r, "a_quitte_l_entreprise"))
281
+ }
282
+ )
283
+
284
+ if snapshots_records:
285
+ conn.execute(insert_snapshot, snapshots_records)
286
+ if surveys_records:
287
+ conn.execute(insert_survey, surveys_records)
288
+ if gt_records:
289
+ conn.execute(insert_gt, gt_records)
290
+
291
+ print("Seed terminé depuis les 3 CSV")
292
+ print(f" employees: {len(employees_records)} (upsert)")
293
+ print(f" snapshots: {len(snapshots_records)}")
294
+ print(f" surveys: {len(surveys_records)}")
295
+ print(f" ground_truth: {len(gt_records)}")
296
+
297
+ if __name__ == "__main__":
298
+ main()
scripts/seed_ml_features.py ADDED
@@ -0,0 +1,411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ import logging
6
+ from dataclasses import dataclass
7
+
8
+ import pandas as pd
9
+ from sqlalchemy import create_engine, text
10
+ from dotenv import load_dotenv, find_dotenv
11
+
12
+ # CONFIG & LOGGING
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format="%(asctime)s | %(levelname)s | %(message)s",
16
+ )
17
+ logger = logging.getLogger("technova.etl")
18
+
19
+ # SETTINGS
20
+ @dataclass(frozen=True)
21
+ class Settings:
22
+ # RAW schema tables
23
+ raw_employees: str = "raw.employees"
24
+ raw_snapshots: str = "raw.employee_snapshots"
25
+ raw_surveys: str = "raw.surveys"
26
+ ground_truth: str = "raw.ground_truth"
27
+
28
+ # CLEAN destination
29
+ dst_schema: str = "clean"
30
+ dst_name: str = "ml_features_employees"
31
+ dst_qualified: str = "clean.ml_features_employees"
32
+
33
+ # UTILS
34
+ def get_engine():
35
+ load_dotenv(find_dotenv())
36
+ db_url = os.getenv("DATABASE_URL")
37
+ if not db_url:
38
+ raise RuntimeError("DATABASE_URL manquant dans .env")
39
+ return create_engine(db_url, pool_pre_ping=True)
40
+
41
+ # TRANSFORMS
42
+ class Transform:
43
+ @staticmethod
44
+ def clean_raw_inputs(df: pd.DataFrame) -> pd.DataFrame:
45
+ df = df.copy()
46
+
47
+ # % -> float
48
+ if "augmentation_salaire_precedente" in df.columns:
49
+ s = df["augmentation_salaire_precedente"].astype("string")
50
+ s = s.str.replace("%", "", regex=False)
51
+ df["augmentation_salaire_precedente"] = pd.to_numeric(s, errors="coerce")
52
+
53
+ # Oui/Non -> 1/0
54
+ if "heures_supplementaires" in df.columns:
55
+ df["heures_supplementaires"] = df["heures_supplementaires"].map(
56
+ {"Oui": 1, "Non": 0, "oui": 1, "non": 0, 1: 1, 0: 0, True: 1, False: 0}
57
+ )
58
+
59
+ # Genre -> 1/0
60
+ if "genre" in df.columns:
61
+ df["genre"] = df["genre"].map({"M": 1, "F": 0, 1: 1, 0: 0})
62
+
63
+ # Fréquence déplacement -> 0/1/2 (+ éventuellement 3 si tu veux)
64
+ if "frequence_deplacement" in df.columns:
65
+ df["frequence_deplacement"] = df["frequence_deplacement"].map(
66
+ {
67
+ "Aucun": 0,
68
+ "Occasionnel": 1,
69
+ "Frequent": 2,
70
+ "Fréquent": 2,
71
+ 0: 0, 1: 1, 2: 2, 3: 3,
72
+ }
73
+ )
74
+
75
+ numeric_cols = [
76
+ "employee_id",
77
+ "age",
78
+ "revenu_mensuel",
79
+ "niveau_education",
80
+ "distance_domicile_travail",
81
+ "note_evaluation_precedente",
82
+ "note_evaluation_actuelle",
83
+ "niveau_hierarchique_poste",
84
+ "nombre_experiences_precedentes",
85
+ "annee_experience_totale",
86
+ "annees_dans_l_entreprise",
87
+ "annees_dans_le_poste_actuel",
88
+ "annees_depuis_la_derniere_promotion",
89
+ "annees_sous_responsable_actuel",
90
+ "nombre_participation_pee",
91
+ "nb_formations_suivies",
92
+ "a_quitte_l_entreprise"
93
+ ]
94
+ for c in numeric_cols:
95
+ if c in df.columns:
96
+ df[c] = pd.to_numeric(df[c], errors="coerce")
97
+
98
+ return df
99
+
100
+ @staticmethod
101
+ def feature_engineering(df: pd.DataFrame) -> pd.DataFrame:
102
+ df = df.copy()
103
+
104
+ sat_cols = [
105
+ "satisfaction_employee_environnement",
106
+ "satisfaction_employee_nature_travail",
107
+ "satisfaction_employee_equipe",
108
+ "satisfaction_employee_equilibre_pro_perso"
109
+ ]
110
+ missing = [c for c in sat_cols if c not in df.columns]
111
+ if missing:
112
+ raise KeyError(f"Colonnes satisfaction manquantes (raw.surveys?) : {missing}")
113
+
114
+ # satisfaction moyenne
115
+ df["satisfaction_moyenne"] = df[sat_cols].mean(axis=1)
116
+
117
+ # features dérivées
118
+ df["nonlineaire_participation_pee"] = (
119
+ df["nombre_participation_pee"]
120
+ / (df["nombre_participation_pee"] + df["annees_dans_l_entreprise"] + 1)
121
+ )
122
+
123
+ df["ratio_heures_sup_salaire"] = (
124
+ df["heures_supplementaires"] / (df["revenu_mensuel"] + 1)
125
+ )
126
+
127
+ d = df["distance_domicile_travail"]
128
+ df["nonlinaire_charge_contrainte"] = (
129
+ df["heures_supplementaires"] * d / (d + 10) / (d + 10)
130
+ )
131
+
132
+ df["nonlinaire_surmenage_insatisfaction"] = (
133
+ df["heures_supplementaires"] * (1 - df["satisfaction_moyenne"])
134
+ )
135
+
136
+ df["jeune_surcharge"] = (
137
+ (df["age"] < 30) & (df["heures_supplementaires"] == 1)
138
+ ).astype(int)
139
+
140
+ df["anciennete_sans_promotion"] = (
141
+ (df["annees_dans_l_entreprise"] - df["annees_depuis_la_derniere_promotion"])
142
+ / (df["annees_dans_l_entreprise"] + 1)
143
+ )
144
+
145
+ df["mobilite_carriere"] = (
146
+ df["nombre_experiences_precedentes"]
147
+ / (df["annee_experience_totale"] + 1)
148
+ )
149
+
150
+ df["risque_global"] = (
151
+ df["ratio_heures_sup_salaire"]
152
+ * df["anciennete_sans_promotion"]
153
+ * (1 - df["satisfaction_moyenne"])
154
+ )
155
+
156
+ return df
157
+
158
+ @staticmethod
159
+ def suppression_features(df: pd.DataFrame) -> pd.DataFrame:
160
+ return df.drop(
161
+ columns=[
162
+ "satisfaction_employee_environnement",
163
+ "satisfaction_employee_nature_travail",
164
+ "satisfaction_employee_equipe",
165
+ "satisfaction_employee_equilibre_pro_perso"
166
+ ],
167
+ errors="ignore",
168
+ )
169
+
170
+ # DESTINATION COLUMNS
171
+ DEST_COLS = [
172
+ "employee_id",
173
+ "note_evaluation_precedente",
174
+ "niveau_hierarchique_poste",
175
+ "note_evaluation_actuelle",
176
+ "heures_supplementaires",
177
+ "augmentation_salaire_precedente",
178
+ "age",
179
+ "genre",
180
+ "revenu_mensuel",
181
+ "statut_marital",
182
+ "departement",
183
+ "poste",
184
+ "nombre_experiences_precedentes",
185
+ "annee_experience_totale",
186
+ "annees_dans_l_entreprise",
187
+ "annees_dans_le_poste_actuel",
188
+ "a_quitte_l_entreprise",
189
+ "nombre_participation_pee",
190
+ "nb_formations_suivies",
191
+ "distance_domicile_travail",
192
+ "niveau_education",
193
+ "domaine_etude",
194
+ "frequence_deplacement",
195
+ "annees_depuis_la_derniere_promotion",
196
+ "annees_sous_responsable_actuel",
197
+ "satisfaction_moyenne",
198
+ "nonlineaire_participation_pee",
199
+ "ratio_heures_sup_salaire",
200
+ "nonlinaire_charge_contrainte",
201
+ "nonlinaire_surmenage_insatisfaction",
202
+ "jeune_surcharge",
203
+ "anciennete_sans_promotion",
204
+ "mobilite_carriere",
205
+ "risque_global"
206
+ ]
207
+
208
+
209
+ def build_sql_master(s: Settings) -> str:
210
+ return f"""
211
+ WITH last_snapshot AS (
212
+ SELECT DISTINCT ON (employee_id)
213
+ employee_id,
214
+ nombre_experiences_precedentes,
215
+ annee_experience_totale,
216
+ annees_dans_l_entreprise,
217
+ annees_dans_le_poste_actuel,
218
+ annees_sous_responsable_actuel,
219
+ niveau_hierarchique_poste,
220
+ revenu_mensuel,
221
+ augmentation_salaire_precedente,
222
+ heures_supplementaires,
223
+ nombre_participation_pee,
224
+ nb_formations_suivies,
225
+ frequence_deplacement,
226
+ annees_depuis_la_derniere_promotion,
227
+ created_at
228
+ FROM {s.raw_snapshots}
229
+ ORDER BY employee_id, created_at DESC
230
+ ),
231
+ last_survey AS (
232
+ SELECT DISTINCT ON (employee_id)
233
+ employee_id,
234
+ note_evaluation_precedente,
235
+ note_evaluation_actuelle,
236
+ satisfaction_employee_environnement,
237
+ satisfaction_employee_nature_travail,
238
+ satisfaction_employee_equipe,
239
+ satisfaction_employee_equilibre_pro_perso,
240
+ created_at
241
+ FROM {s.raw_surveys}
242
+ ORDER BY employee_id, created_at DESC
243
+ ),
244
+ last_truth AS (
245
+ SELECT DISTINCT ON (employee_id)
246
+ employee_id,
247
+ a_quitte_l_entreprise,
248
+ date_event
249
+ FROM {s.ground_truth}
250
+ ORDER BY employee_id, date_event DESC
251
+ )
252
+ SELECT
253
+ e.id AS employee_id,
254
+ e.age,
255
+ e.genre,
256
+ e.statut_marital,
257
+ e.niveau_education,
258
+ e.domaine_etude,
259
+ e.departement,
260
+ e.poste,
261
+ e.distance_domicile_travail,
262
+ s.nombre_experiences_precedentes,
263
+ s.annee_experience_totale,
264
+ s.annees_dans_l_entreprise,
265
+ s.annees_dans_le_poste_actuel,
266
+ s.annees_sous_responsable_actuel,
267
+ s.niveau_hierarchique_poste,
268
+ s.revenu_mensuel,
269
+ s.augmentation_salaire_precedente,
270
+ s.heures_supplementaires,
271
+ s.nombre_participation_pee,
272
+ s.nb_formations_suivies,
273
+ s.frequence_deplacement,
274
+ s.annees_depuis_la_derniere_promotion,
275
+ sv.note_evaluation_precedente,
276
+ sv.note_evaluation_actuelle,
277
+ sv.satisfaction_employee_environnement,
278
+ sv.satisfaction_employee_nature_travail,
279
+ sv.satisfaction_employee_equipe,
280
+ sv.satisfaction_employee_equilibre_pro_perso,
281
+ COALESCE(gt.a_quitte_l_entreprise, 0) AS a_quitte_l_entreprise
282
+
283
+ FROM {s.raw_employees} e
284
+ LEFT JOIN last_snapshot s ON s.employee_id = e.id
285
+ LEFT JOIN last_survey sv ON sv.employee_id = e.id
286
+ LEFT JOIN last_truth gt ON gt.employee_id = e.id
287
+
288
+ WHERE s.employee_id IS NOT NULL
289
+ AND sv.employee_id IS NOT NULL
290
+ ;
291
+ """
292
+
293
+ # ETL STEPS
294
+ def fetch_master_df(engine, sql: str) -> pd.DataFrame:
295
+ with engine.connect() as conn:
296
+ return pd.read_sql(text(sql), conn)
297
+
298
+ def validate_columns(df: pd.DataFrame, dst_qualified: str) -> None:
299
+ missing = [c for c in DEST_COLS if c not in df.columns]
300
+ if missing:
301
+ raise KeyError(f"Colonnes manquantes pour insertion dans {dst_qualified}: {missing}")
302
+
303
+ def validate_quality(df: pd.DataFrame) -> None:
304
+ critical = ["employee_id", "age", "revenu_mensuel", "heures_supplementaires", "a_quitte_l_entreprise"]
305
+ bad = [c for c in critical if c in df.columns and df[c].isna().mean() > 0.20]
306
+ if bad:
307
+ raise ValueError(f"Trop de NaN sur colonnes critiques (>20%): {bad}")
308
+
309
+ def truncate_destination(engine, dst_qualified: str) -> None:
310
+ with engine.begin() as conn:
311
+ conn.execute(text(f"TRUNCATE TABLE {dst_qualified} RESTART IDENTITY;"))
312
+
313
+ def enforce_not_null_ready(df: pd.DataFrame) -> pd.DataFrame:
314
+ out = df.copy()
315
+ out = out.dropna(subset=DEST_COLS)
316
+
317
+ # cast ints propres (pandas peut garder float si NaN existe, mais là on a drop)
318
+ int_like = [
319
+ "employee_id",
320
+ "note_evaluation_precedente",
321
+ "niveau_hierarchique_poste",
322
+ "note_evaluation_actuelle",
323
+ "heures_supplementaires",
324
+ "age",
325
+ "genre",
326
+ "revenu_mensuel",
327
+ "nombre_experiences_precedentes",
328
+ "annee_experience_totale",
329
+ "annees_dans_l_entreprise",
330
+ "annees_dans_le_poste_actuel",
331
+ "a_quitte_l_entreprise",
332
+ "nombre_participation_pee",
333
+ "nb_formations_suivies",
334
+ "distance_domicile_travail",
335
+ "niveau_education",
336
+ "frequence_deplacement",
337
+ "annees_depuis_la_derniere_promotion",
338
+ "annees_sous_responsable_actuel",
339
+ "jeune_surcharge"
340
+ ]
341
+ for c in int_like:
342
+ if c in out.columns:
343
+ out[c] = out[c].astype(int)
344
+
345
+ return out
346
+
347
+ def insert_destination(engine, df: pd.DataFrame, dst_schema: str, dst_name: str, chunk_size: int = 2000) -> int:
348
+ df_out = df[DEST_COLS].copy()
349
+ with engine.begin() as conn:
350
+ df_out.to_sql(
351
+ name=dst_name,
352
+ schema=dst_schema,
353
+ con=conn,
354
+ if_exists="append",
355
+ index=False,
356
+ method="multi",
357
+ chunksize=chunk_size,
358
+ )
359
+ return len(df_out)
360
+
361
+ # MAIN
362
+ def main():
363
+ refresh = "--refresh" in sys.argv
364
+ dry_run = "--dry-run" in sys.argv
365
+
366
+ s = Settings()
367
+ engine = get_engine()
368
+ sql_master = build_sql_master(s)
369
+
370
+ logger.info("Build master DF depuis raw tables")
371
+ df = fetch_master_df(engine, sql_master)
372
+ logger.info("Master DF: %s lignes | %s colonnes", df.shape[0], df.shape[1])
373
+
374
+ logger.info("Nettoyage types / mapping")
375
+ df = Transform.clean_raw_inputs(df)
376
+
377
+ logger.info("Feature engineering")
378
+ df = Transform.feature_engineering(df)
379
+
380
+ logger.info("Drop colonnes intermédiaires")
381
+ df = Transform.suppression_features(df)
382
+
383
+ logger.info("Validation colonnes destination")
384
+ validate_columns(df, s.dst_qualified)
385
+
386
+ logger.info("Contrôle qualité (NaN critiques)")
387
+ validate_quality(df)
388
+
389
+ logger.info("Préparation NOT NULL (drop rows invalides + cast ints)")
390
+ before = len(df)
391
+ df = enforce_not_null_ready(df)
392
+ after = len(df)
393
+ if after < before:
394
+ logger.warning("Lignes retirées (NULL sur colonnes clean NOT NULL): %s -> %s", before, after)
395
+
396
+ if dry_run:
397
+ logger.info("DRY-RUN: aucune écriture en base. Aperçu:")
398
+ logger.info("Colonnes finales: %s", list(df[DEST_COLS].columns))
399
+ logger.info("Head:\n%s", df[DEST_COLS].head(5).to_string(index=False))
400
+ return
401
+
402
+ if refresh:
403
+ logger.info("Mode REFRESH: TRUNCATE %s", s.dst_qualified)
404
+ truncate_destination(engine, s.dst_qualified)
405
+
406
+ logger.info("Insertion vers %s", s.dst_qualified)
407
+ n = insert_destination(engine, df, s.dst_schema, s.dst_name)
408
+ logger.info("OK: %s lignes insérées dans %s", n, s.dst_qualified)
409
+
410
+ if __name__ == "__main__":
411
+ main()
service/__init__.py ADDED
File without changes
service/technova_service.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import joblib
9
+ import pandas as pd
10
+ from huggingface_hub import hf_hub_download
11
+ from sqlalchemy import text
12
+ from sqlalchemy.orm import Session
13
+
14
+ from domain.domain import ModelRequest, ModelResponse
15
+
16
+ MODEL_REPO = os.getenv("MODEL_REPO", "SteGONZALEZ/technova-model")
17
+ MODEL_FILE = os.getenv("MODEL_FILE", "modele_classification_technova.joblib")
18
+ THRESH_FILE = os.getenv("THRESH_FILE", "threshold.json")
19
+
20
+ def _get_artifact_path(artifacts_dir: Path, filename: str) -> str:
21
+ """
22
+ 1) Try local artifacts/ (dev)
23
+ 2) Fallback to Hugging Face Hub (prod / Space)
24
+ Returns a filesystem path as string.
25
+ """
26
+ local_path = artifacts_dir / filename
27
+ if local_path.exists():
28
+ return str(local_path)
29
+
30
+ return hf_hub_download(
31
+ repo_id=MODEL_REPO,
32
+ filename=filename,
33
+ repo_type="model",
34
+ )
35
+
36
+ class TechNovaService:
37
+ def __init__(self) -> None:
38
+ artifacts_dir = Path(__file__).resolve().parents[1] / "artifacts"
39
+
40
+ # --- Load model (local or HF) ---
41
+ model_path = _get_artifact_path(artifacts_dir, MODEL_FILE)
42
+ self.model = joblib.load(model_path)
43
+
44
+ # --- Load threshold (local or HF) ---
45
+ thresh_path = _get_artifact_path(artifacts_dir, THRESH_FILE)
46
+ with open(thresh_path, "r", encoding="utf-8") as f:
47
+ self.threshold = float(json.load(f)["threshold"])
48
+
49
+ # Ordre EXACT attendu par le pipeline
50
+ self.feature_columns: list[str] = [
51
+ "note_evaluation_precedente",
52
+ "niveau_hierarchique_poste",
53
+ "note_evaluation_actuelle",
54
+ "heures_supplementaires",
55
+ "augmentation_salaire_precedente",
56
+ "age",
57
+ "genre",
58
+ "revenu_mensuel",
59
+ "statut_marital",
60
+ "departement",
61
+ "poste",
62
+ "nombre_experiences_precedentes",
63
+ "annee_experience_totale",
64
+ "annees_dans_l_entreprise",
65
+ "annees_dans_le_poste_actuel",
66
+ "nombre_participation_pee",
67
+ "nb_formations_suivies",
68
+ "distance_domicile_travail",
69
+ "niveau_education",
70
+ "domaine_etude",
71
+ "frequence_deplacement",
72
+ "annees_depuis_la_derniere_promotion",
73
+ "annees_sous_responsable_actuel",
74
+ "satisfaction_moyenne",
75
+ "nonlineaire_participation_pee",
76
+ "ratio_heures_sup_salaire",
77
+ "nonlinaire_charge_contrainte",
78
+ "nonlinaire_surmenage_insatisfaction",
79
+ "jeune_surcharge",
80
+ "anciennete_sans_promotion",
81
+ "mobilite_carriere",
82
+ "risque_global",
83
+ ]
84
+
85
+ self._sql_clean_latest = text(
86
+ f"""
87
+ SELECT {", ".join(self.feature_columns)}
88
+ FROM clean.ml_features_employees
89
+ WHERE employee_id = :employee_id
90
+ ORDER BY created_at DESC
91
+ LIMIT 1
92
+ """
93
+ )
94
+
95
+ def fetch_latest_clean_row(self, db: Session, employee_id: int) -> dict[str, Any] | None:
96
+ row = (
97
+ db.execute(self._sql_clean_latest, {"employee_id": employee_id})
98
+ .mappings()
99
+ .first()
100
+ )
101
+ return dict(row) if row else None
102
+
103
+ def _row_to_X(self, row: dict[str, Any]) -> pd.DataFrame:
104
+ missing = [c for c in self.feature_columns if c not in row]
105
+ if missing:
106
+ raise ValueError(f"Clean row missing required features: {missing}")
107
+
108
+ X = pd.DataFrame([{c: row[c] for c in self.feature_columns}])
109
+ return X[self.feature_columns]
110
+
111
+ def predict_from_clean(self, db: Session, employee_id: int) -> ModelResponse:
112
+ row = self.fetch_latest_clean_row(db, employee_id)
113
+ if row is None:
114
+ raise ValueError(
115
+ f"Aucune ligne dans clean.ml_features_employees pour employee_id={employee_id}. "
116
+ f"As-tu bien exécuté l'ETL vers clean ?"
117
+ )
118
+
119
+ X = self._row_to_X(row)
120
+ proba = float(self.model.predict_proba(X)[0, 1])
121
+
122
+ return ModelResponse(
123
+ employee_id=employee_id,
124
+ turnover_probability=proba,
125
+ will_leave=proba >= self.threshold,
126
+ )
127
+
128
+ # ---------- Payload ----------
129
+ def adapt_input(self, request: ModelRequest) -> pd.DataFrame:
130
+ data = request.model_dump()
131
+ X = pd.DataFrame([data])
132
+
133
+ missing = [c for c in self.feature_columns if c not in X.columns]
134
+ if missing:
135
+ raise ValueError(f"Missing features: {missing}")
136
+
137
+ extra = [c for c in X.columns if c not in self.feature_columns]
138
+ if extra:
139
+ raise ValueError(f"Unexpected features: {extra}")
140
+
141
+ return X[self.feature_columns]
142
+
143
+ def predict_from_payload(self, request: ModelRequest) -> ModelResponse:
144
+ X = self.adapt_input(request)
145
+ proba = float(self.model.predict_proba(X)[0, 1])
146
+
147
+ return ModelResponse(
148
+ employee_id=None, # pas d'id côté payload
149
+ turnover_probability=proba,
150
+ will_leave=proba >= self.threshold,
151
+ )
tests/conftest.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pytest
3
+ from fastapi.testclient import TestClient
4
+ from sqlalchemy import create_engine
5
+ from sqlalchemy.orm import sessionmaker
6
+ from sqlalchemy.pool import StaticPool
7
+
8
+ from app.api import app
9
+ from app.database import get_db
10
+ from app.models import Base
11
+ from app.security import verify_api_key
12
+
13
+ os.environ.setdefault("API_KEY", "ci-test-key")
14
+
15
+ TEST_DB_URL = "sqlite+pysqlite:///:memory:"
16
+
17
+ # On définit une fixture pour créer une base de données en mémoire pour les tests,
18
+ # ce qui permet d'avoir un environnement de test isolé et rapide.
19
+ @pytest.fixture(scope="session")
20
+ def engine():
21
+ eng = (
22
+ create_engine(
23
+ TEST_DB_URL,
24
+ connect_args={"check_same_thread": False},
25
+ poolclass=StaticPool,
26
+ )
27
+ .execution_options(schema_translate_map={"raw": None, "clean": None, "app": None})
28
+ )
29
+ Base.metadata.create_all(eng)
30
+ yield eng
31
+ eng.dispose()
32
+
33
+ # On définit une fonction pour générer un payload d'exemple,
34
+ # qui correspond aux features attendues par le modèle de prédiction.
35
+ @pytest.fixture()
36
+ def db(engine):
37
+ SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
38
+ session = SessionLocal()
39
+ try:
40
+ yield session
41
+ finally:
42
+ session.close()
43
+
44
+ # On définit une fixture pour créer un client de test FastAPI,
45
+ # avec la sécurité désactivée pour les tests de routes,
46
+ def _override_db(db):
47
+ def override_get_db():
48
+ yield db
49
+ return override_get_db
50
+
51
+ # On définit une fixture pour créer un client de test FastAPI,
52
+ # avec la sécurité activée pour les tests de sécurité, ce qui permet de tester les comportements d'authentification.
53
+ @pytest.fixture()
54
+ def client(db):
55
+ # Sécurité OFF pour les tests routes
56
+ app.dependency_overrides[verify_api_key] = lambda: None
57
+ app.dependency_overrides[get_db] = _override_db(db)
58
+
59
+ with TestClient(app) as c:
60
+ yield c
61
+
62
+ app.dependency_overrides.clear()
63
+
64
+ # On définit une fixture pour créer un client de test FastAPI avec la sécurité activée,
65
+ # ce qui permet de tester les comportements d'authentification, notamment que les routes protégées nécessitent une clé API valide.
66
+ @pytest.fixture()
67
+ def secure_client(db):
68
+ # Sécurité ON pour les tests security
69
+ app.dependency_overrides.pop(verify_api_key, None)
70
+ app.dependency_overrides[get_db] = _override_db(db)
71
+
72
+ with TestClient(app) as c:
73
+ yield c
74
+
75
+ app.dependency_overrides.clear()
tests/test_api.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tests/test_api.py
2
+ import pytest
3
+ import numpy as np
4
+
5
+ from domain.domain import ModelResponse
6
+ from service.technova_service import TechNovaService
7
+
8
+
9
+ def sample_payload():
10
+ return {
11
+ "note_evaluation_precedente": 3,
12
+ "niveau_hierarchique_poste": 2,
13
+ "note_evaluation_actuelle": 3,
14
+ "heures_supplementaires": 1,
15
+ "augmentation_salaire_precedente": 11.0,
16
+ "age": 41,
17
+ "genre": 0,
18
+ "revenu_mensuel": 5993,
19
+ "statut_marital": "célibataire",
20
+ "departement": "commercial",
21
+ "poste": "cadre commercial",
22
+ "nombre_experiences_precedentes": 8,
23
+ "annee_experience_totale": 8,
24
+ "annees_dans_l_entreprise": 6,
25
+ "annees_dans_le_poste_actuel": 4,
26
+ "nombre_participation_pee": 0,
27
+ "nb_formations_suivies": 0,
28
+ "distance_domicile_travail": 1,
29
+ "niveau_education": 2,
30
+ "domaine_etude": "infra & cloud",
31
+ "frequence_deplacement": 1,
32
+ "annees_depuis_la_derniere_promotion": 0,
33
+ "annees_sous_responsable_actuel": 5,
34
+ "satisfaction_moyenne": 2.0,
35
+ "nonlineaire_participation_pee": 0.0,
36
+ "ratio_heures_sup_salaire": 0.0001668335001668335,
37
+ "nonlinaire_charge_contrainte": 0.008264462809917356,
38
+ "nonlinaire_surmenage_insatisfaction": -1.0,
39
+ "jeune_surcharge": 0,
40
+ "anciennete_sans_promotion": 0.8571428571428571,
41
+ "mobilite_carriere": 0.8888888888888888,
42
+ "risque_global": -0.000143000143000143,
43
+ }
44
+
45
+
46
+ @pytest.fixture(autouse=True)
47
+ def patch_service(monkeypatch):
48
+ class FakeModel:
49
+ def predict_proba(self, X):
50
+ return np.array([[0.2, 0.8]])
51
+
52
+ def fake_init(self):
53
+ self.threshold = 0.5
54
+ self.feature_columns = list(sample_payload().keys())
55
+ self.model = FakeModel()
56
+
57
+ def fake_predict_from_clean(self, db, employee_id: int):
58
+ if employee_id == 1:
59
+ return ModelResponse(employee_id=1, turnover_probability=0.7, will_leave=True)
60
+ raise ValueError("not found")
61
+
62
+ monkeypatch.setattr(TechNovaService, "__init__", fake_init)
63
+ monkeypatch.setattr(TechNovaService, "predict_from_clean", fake_predict_from_clean)
64
+
65
+ def test_health(client):
66
+ r = client.get("/health")
67
+ assert r.status_code == 200
68
+
69
+ def test_predict_by_features_ok(client):
70
+ r = client.post("/predict/by-features", json=sample_payload())
71
+ assert r.status_code == 200, r.text
72
+ data = r.json()
73
+ assert "turnover_probability" in data
74
+ assert "will_leave" in data
75
+
76
+ def test_predict_by_id_200_and_404(client):
77
+ assert client.post("/predict/by-id/1").status_code == 200
78
+ assert client.post("/predict/by-id/2").status_code == 404
79
+
80
+ def test_latest_predictions_returns_list(client):
81
+ client.post("/predict/by-features", json=sample_payload())
82
+ r = client.get("/predictions/latest", params={"limit": 3})
83
+ assert r.status_code == 200, r.text
84
+ assert isinstance(r.json(), list)
85
+
86
+ ## Obligaoitre pour couvrir le coverage de la branche
87
+ # tester que l'endpoint de prédiction par ID gère une exception inattendue et renvoie un HTTP 500.
88
+ def test_predict_by_id_returns_500_on_unexpected_error(client, monkeypatch):
89
+ # Force la branche "except Exception" -> HTTP 500
90
+ def boom(self, db, employee_id: int):
91
+ raise Exception("boom")
92
+
93
+ monkeypatch.setattr(TechNovaService, "predict_from_clean", boom)
94
+
95
+ r = client.post("/predict/by-id/1")
96
+ assert r.status_code == 500, r.text
97
+
98
+ # tester que l'endpoint de readiness vérifie la connexion à la DB et renvoie 503 si la table clean.ml_features_employees est manquante.
99
+ def test_ready_returns_503_when_clean_table_missing(client, monkeypatch):
100
+ # Ici on override directement les dépendances FastAPI (get_db, get_service)
101
+ import app.api as api
102
+
103
+ class FakeService:
104
+ model = object() # "modèle chargé"
105
+
106
+ class FakeDB:
107
+ def execute(self, stmt, *_a, **_kw):
108
+ s = str(stmt)
109
+ if "FROM clean.ml_features_employees" in s:
110
+ raise Exception("relation does not exist")
111
+ return True
112
+
113
+ def fake_get_service():
114
+ return FakeService()
115
+
116
+ def fake_get_db():
117
+ yield FakeDB()
118
+
119
+ api.app.dependency_overrides[api.get_service] = fake_get_service
120
+ api.app.dependency_overrides[api.get_db] = fake_get_db
121
+
122
+ try:
123
+ r = client.get("/ready")
124
+ assert r.status_code == 503, r.text
125
+ assert "Clean table not ready" in r.text
126
+ finally:
127
+ api.app.dependency_overrides.clear()
tests/test_database.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib
2
+ import sys
3
+
4
+
5
+ # On doit recharger le module database pour que les changements d'environnement soient pris en compte
6
+ def reload_database_module(monkeypatch, database_url: str | None):
7
+ if database_url is None:
8
+ monkeypatch.delenv("DATABASE_URL", raising=False)
9
+ else:
10
+ monkeypatch.setenv("DATABASE_URL", database_url)
11
+
12
+ sys.modules.pop("app.database", None)
13
+ import app.database # noqa: F401
14
+ return importlib.reload(app.database)
15
+
16
+ # tester que si DATABASE_URL n'est pas défini, le module database utilise une URL SQLite en mémoire par défaut.
17
+ def test_database_defaults_to_sqlite_memory_when_env_missing(monkeypatch):
18
+ db = reload_database_module(monkeypatch, None)
19
+ assert hasattr(db, "DATABASE_URL")
20
+ assert db.DATABASE_URL.startswith("sqlite+pysqlite:///")
21
+
22
+ # tester que si DATABASE_URL est défini, le module database l'utilise.
23
+ # Ici on ne teste pas la connexion à la DB, juste que la variable est bien prise en compte.
24
+ def test_get_db_closes_session(monkeypatch):
25
+ db = reload_database_module(monkeypatch, "sqlite+pysqlite:///:memory:")
26
+
27
+ class FakeSession:
28
+ def __init__(self):
29
+ self.closed = False
30
+
31
+ def close(self):
32
+ self.closed = True
33
+
34
+ fake_session = FakeSession()
35
+
36
+ # On remplace SessionLocal par une fonction qui retourne notre FakeSession
37
+ monkeypatch.setattr(db, "SessionLocal", lambda: fake_session)
38
+
39
+ gen = db.get_db()
40
+ session = next(gen)
41
+
42
+ assert session is fake_session
43
+
44
+ # Ferme le generator => déclenche finally => close()
45
+ gen.close()
46
+ assert fake_session.closed is True
tests/test_main.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib
2
+ import sys
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ # On importe les éléments nécessaires pour tester le module main, notamment pour simuler les processus de l'API et du dashboard.
6
+ def reload_main():
7
+ sys.modules.pop("app.main", None)
8
+ import app.main # noqa
9
+ return importlib.reload(app.main)
10
+
11
+ # On définit une fonction pour générer un payload d'exemple, qui correspond aux features attendues par le modèle de prédiction.
12
+ def test_main_starts_api_and_dashboard_and_stops_dashboard_when_api_exits():
13
+ fake_api = MagicMock()
14
+ fake_dash = MagicMock()
15
+
16
+ # API tourne puis s'arrête, dashboard tourne
17
+ fake_api.poll.side_effect = [None, 0]
18
+ fake_dash.poll.return_value = None
19
+
20
+ # On patch subprocess.Popen pour retourner nos mocks, et time.sleep pour éviter de faire attendre le test.
21
+ with patch("app.main.subprocess.Popen") as popen_mock, patch("app.main.time.sleep") as sleep_mock:
22
+ popen_mock.side_effect = [fake_api, fake_dash]
23
+ # On recharge le module main pour exécuter la fonction main() avec nos mocks en place, ce qui permet de
24
+ # tester le comportement de lancement et d'arrêt des processus.
25
+ main = reload_main()
26
+ main.main()
27
+
28
+ # On vérifie que les processus ont été lancés et que
29
+ # le dashboard a été arrêté quand l'API s'est arrêtée, ce qui correspond au comportement attendu.
30
+ assert popen_mock.call_count == 2
31
+ fake_dash.terminate.assert_called_once()
32
+ fake_api.terminate.assert_not_called()
33
+
34
+ # Optionnel: vérifier qu'on a bien "bouclé"
35
+ assert sleep_mock.called
36
+
37
+ # On teste aussi que si on interrompt le programme avec un KeyboardInterrupt,
38
+ # les deux processus sont correctement terminés, ce qui est important pour éviter de laisser des processus zombies.
39
+ def test_main_handles_keyboard_interrupt_and_terminates_both():
40
+ fake_api = MagicMock()
41
+ fake_dash = MagicMock()
42
+
43
+ # Les deux processus tournent, puis on simule une interruption clavier.
44
+ fake_api.poll.return_value = None
45
+ fake_dash.poll.return_value = None
46
+
47
+ # On patch subprocess.Popen pour retourner nos mocks, et time.sleep pour simuler une interruption clavier,
48
+ # ce qui permet de tester que les deux processus sont terminés dans ce cas.
49
+ with patch("app.main.subprocess.Popen") as popen_mock, patch(
50
+ "app.main.time.sleep", side_effect=KeyboardInterrupt
51
+ ):
52
+ popen_mock.side_effect = [fake_api, fake_dash]
53
+
54
+ main = reload_main()
55
+ main.main()
56
+
57
+ fake_api.terminate.assert_called_once()
58
+ fake_dash.terminate.assert_called_once()
tests/test_security.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # on teste que l'accès à l'endpoint de santé nécessite une clé API valide.
4
+ def test_health_requires_api_key(secure_client):
5
+ r = secure_client.get("/health")
6
+ assert r.status_code == 401, r.text
7
+
8
+ # on teste que l'accès à l'endpoint de santé avec une clé API valide est autorisé.
9
+ def test_health_with_valid_api_key_is_ok(secure_client):
10
+ api_key = os.getenv("API_KEY", "ci-test-key")
11
+ r = secure_client.get("/health", headers={"X-API-Key": api_key})
12
+ assert r.status_code == 200, r.text
tests/test_technova_service.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ import numpy as np
3
+
4
+ from service.technova_service import TechNovaService
5
+
6
+ class FakeModel:
7
+ def __init__(self, p1=0.8):
8
+ self.p1 = float(p1)
9
+
10
+ def predict_proba(self, X):
11
+ return np.array([[1.0 - self.p1, self.p1]], dtype=float)
12
+
13
+ class FakeRequest:
14
+ def __init__(self, data):
15
+ self._data = data
16
+
17
+ def model_dump(self):
18
+ return self._data
19
+
20
+ def make_service(feature_columns, p1=0.8, threshold=0.5):
21
+ # instancie sans __init__ (pas de joblib / fichiers)
22
+ s = TechNovaService.__new__(TechNovaService)
23
+ s.model = FakeModel(p1=p1)
24
+ s.threshold = float(threshold)
25
+ s.feature_columns = list(feature_columns)
26
+ return s
27
+
28
+ def test_adapt_input_missing_feature_raises():
29
+ s = make_service(feature_columns=["age", "revenu_mensuel"])
30
+
31
+ req = FakeRequest({"age": 30}) # manque revenu_mensuel
32
+
33
+ with pytest.raises(ValueError, match="Missing features"):
34
+ s.adapt_input(req)
35
+
36
+ def test_predict_from_payload_applies_threshold():
37
+ s = make_service(feature_columns=["age"], p1=0.8, threshold=0.5)
38
+
39
+ req = FakeRequest({"age": 29})
40
+
41
+ resp = s.predict_from_payload(req)
42
+
43
+ assert resp.turnover_probability == 0.8
44
+ assert resp.will_leave is True
45
+ assert resp.employee_id is None
46
+
47
+ class FakeDBNoRow:
48
+ def execute(self, *_a, **_kw):
49
+ class R:
50
+ def mappings(self):
51
+ return self
52
+ def first(self):
53
+ return None
54
+ return R()
55
+
56
+
57
+ def test_adapt_input_extra_feature_raises():
58
+ s = make_service(feature_columns=["age", "revenu_mensuel"])
59
+
60
+ req = FakeRequest({"age": 30, "revenu_mensuel": 2500, "foo": 123})
61
+
62
+ with pytest.raises(ValueError, match="Unexpected features"):
63
+ s.adapt_input(req)
64
+
65
+
66
+ def test_predict_from_payload_below_threshold():
67
+ s = make_service(feature_columns=["age"], p1=0.2, threshold=0.5)
68
+
69
+ req = FakeRequest({"age": 29})
70
+ resp = s.predict_from_payload(req)
71
+
72
+ assert resp.turnover_probability == 0.2
73
+ assert resp.will_leave is False
74
+
75
+
76
+ def test_predict_from_clean_not_found_raises():
77
+ # couvre la branche "aucune ligne clean"
78
+ s = make_service(feature_columns=["age"], p1=0.8, threshold=0.5)
79
+ s._sql_clean_latest = "SQL" # juste pour éviter None
80
+
81
+ with pytest.raises(ValueError):
82
+ s.predict_from_clean(FakeDBNoRow(), employee_id=999)