Spaces:
Sleeping
Sleeping
Commit ·
1d0150f
0
Parent(s):
Clean main (no binary history)
Browse files- .coverage +0 -0
- .dockerignore +29 -0
- .env.example +7 -0
- .gitattributes +2 -0
- .github/workflows/cd.yml +67 -0
- .github/workflows/ci.yml +36 -0
- .gitignore +25 -0
- Dockerfile +20 -0
- README.md +209 -0
- app/.gitattributes +2 -0
- app/__init__.py +0 -0
- app/api.py +166 -0
- app/database.py +47 -0
- app/main.py +59 -0
- app/models.py +208 -0
- app/security.py +18 -0
- artifacts/threshold.json +3 -0
- dashboard/__init__.py +0 -0
- dashboard/dshbd.py +245 -0
- dashboard/feature_schema.py +67 -0
- docs/app.api.html +47 -0
- docs/app.models.html +712 -0
- docs/domain.domain.html +1225 -0
- docs/schema.puml +161 -0
- docs/service.technova_service.html +66 -0
- domain/__init__.py +0 -0
- domain/domain.py +47 -0
- my-postgres/docker-compose.yml +21 -0
- notebooks/projet_technova_partners.ipynb +0 -0
- poetry.lock +0 -0
- pyproject.toml +60 -0
- requirements.txt +107 -0
- scripts/build_ml_features.py +94 -0
- scripts/create_db.py +155 -0
- scripts/generate_docs.py +26 -0
- scripts/init_project_technova.py +25 -0
- scripts/query_examples.sql +167 -0
- scripts/seed_from_csv.py +298 -0
- scripts/seed_ml_features.py +411 -0
- service/__init__.py +0 -0
- service/technova_service.py +151 -0
- tests/conftest.py +75 -0
- tests/test_api.py +127 -0
- tests/test_database.py +46 -0
- tests/test_main.py +58 -0
- tests/test_security.py +12 -0
- tests/test_technova_service.py +82 -0
.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"> <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> <br><strong class="bigsection">Functions</strong></td></tr>
|
| 17 |
+
|
| 18 |
+
<tr><td class="decor functions-decor"><span class="code"> </span></td><td> </td>
|
| 19 |
+
<td class="singlecolumn"><dl><dt><a name="-get_service"><strong>get_service</strong></a>() -> 'TechNovaService'</dt><dd><span class="code"># Dependency pour obtenir une instance du service métier dans les endpoints FastAPI</span></dd></dl>
|
| 20 |
+
<dl><dt><a name="-health"><strong>health</strong></a>()</dt><dd><span class="code"># endpoint de santé pour 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=<function get_db at 0x00000224C1C0E7A0>, use_cache=True, scope=None)
|
| 24 |
+
)</dt><dd><span class="code">#endpoint pour récupérer les dernières prédictions stockées en base, avec 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>() -> float<br>
|
| 26 |
+
<br>
|
| 27 |
+
Performance counter for 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=<function get_db at 0x00000224C1C0E7A0>, use_cache=True, scope=None),
|
| 31 |
+
service: 'TechNovaService' = Depends(dependency=<function get_service at 0x00000224C1C5EDE0>, use_cache=True, scope=None)
|
| 32 |
+
) -> 'ModelResponse'</dt><dd><span class="code">#prédiction via ID employé — mode recommandé pour la 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=<function get_db at 0x00000224C1C0E7A0>, use_cache=True, scope=None),
|
| 36 |
+
service: 'TechNovaService' = Depends(dependency=<function get_service at 0x00000224C1C5EDE0>, use_cache=True, scope=None)
|
| 37 |
+
) -> 'ModelResponse'</dt><dd><span class="code">#endpoint de prédiction via features brutes — utile pour tests et debug, mais pas recommandé pour la production</span></dd></dl>
|
| 38 |
+
<dl><dt><a name="-root"><strong>root</strong></a>()</dt><dd><span class="code">#accessible via "/" pour rediriger vers la doc interactive, mais pas dans la doc 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> <br><strong class="bigsection">Data</strong></td></tr>
|
| 43 |
+
|
| 44 |
+
<tr><td class="decor data-decor"><span class="code"> </span></td><td> </td>
|
| 45 |
+
<td class="singlecolumn"><strong>MODEL_VERSION</strong> = 'xgb_v1'<br>
|
| 46 |
+
<strong>app</strong> = <fastapi.applications.FastAPI object></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"> <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> <br><strong class="bigsection">Classes</strong></td></tr>
|
| 17 |
+
|
| 18 |
+
<tr><td class="decor index-decor"><span class="code"> </span></td><td> </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> <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"> </span></td>
|
| 43 |
+
<td class="decor title-decor" colspan=2><span class="code"><a href="#Base">Base</a>(**kwargs: 'Any') -&gt; 'None'<br>
|
| 44 |
+
<br>
|
| 45 |
+
# <a href="#Base">Base</a> de données SQLAlchemy pour les modèles ORM<br> </span></td></tr>
|
| 46 |
+
<tr><td> </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') -> 'None'<span class="grey"><span class="heading-text"> from sqlalchemy.orm.decl_base</span></span></dt><dd><span class="code">A simple constructor that allows initialization from kwargs.<br>
|
| 57 |
+
<br>
|
| 58 |
+
Sets attributes on the constructed instance using the names and<br>
|
| 59 |
+
values in ``kwargs``.<br>
|
| 60 |
+
<br>
|
| 61 |
+
Only keys that are present as<br>
|
| 62 |
+
attributes of the instance's class are allowed. These could be,<br>
|
| 63 |
+
for example, any mapped columns or 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> = <sqlalchemy.orm.decl_api.registry object></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') -> 'None'</dt><dd><span class="code">Function to initialize 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 for instance variables</span></dd>
|
| 83 |
+
</dl>
|
| 84 |
+
<dl><dt><strong>__weakref__</strong></dt>
|
| 85 |
+
<dd><span class="code">list of weak references to the 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 a generic class.<br>
|
| 94 |
+
<br>
|
| 95 |
+
At least, parameterizing a generic class is the *main* thing this<br>
|
| 96 |
+
method does. For example, for some generic class `Foo`, this is called<br>
|
| 97 |
+
when we do `Foo[int]` - there, with `cls=Foo` and `params=int`.<br>
|
| 98 |
+
<br>
|
| 99 |
+
However, note that this method is also called when defining generic<br>
|
| 100 |
+
classes in the first place with `class Foo[T]: ...`.</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> <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"> </span></td>
|
| 108 |
+
<td class="decor title-decor" colspan=2><span class="code"><a href="#MLFeaturesEmployee">MLFeaturesEmployee</a>(**kwargs)<br>
|
| 109 |
+
<br>
|
| 110 |
+
# Les champs de la table raw.surveys peuvent être ajoutés ici si nécessaire,<br>
|
| 111 |
+
# mais ne sont pas indispensables pour les prédictions basées sur employee_id<br> </span></td></tr>
|
| 112 |
+
<tr><td> </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 simple constructor that allows initialization from kwargs.<br>
|
| 124 |
+
<br>
|
| 125 |
+
Sets attributes on the constructed instance using the names and<br>
|
| 126 |
+
values in ``kwargs``.<br>
|
| 127 |
+
<br>
|
| 128 |
+
Only keys that are present as<br>
|
| 129 |
+
attributes of the instance's class are allowed. These could be,<br>
|
| 130 |
+
for example, any mapped columns or 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> = <Mapper at 0x28b5d075310; MLFeaturesEmployee></dl>
|
| 213 |
+
|
| 214 |
+
<dl><dt><strong>__parameters__</strong> = ()</dl>
|
| 215 |
+
|
| 216 |
+
<dl><dt><strong>__table__</strong> = Table('ml_features_employees', MetaData(), Colum...ures_employees>, nullable=False), schema='clean')</dl>
|
| 217 |
+
|
| 218 |
+
<dl><dt><strong>__table_args__</strong> = (Index('idx_clean_employee_id_created_at', Column...efault(<function utcnow at 0x0000028B5D04ACA0>))), Index('idx_ml_features_target', Column('a_quitte..., table=<ml_features_employees>, nullable=False)), Index('idx_ml_features_target_created_at', Colum...efault(<function utcnow at 0x0000028B5D04ACA0>))), {'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> = <sqlalchemy.orm.decl_api.registry object></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') -> 'None'</dt><dd><span class="code">Function to initialize 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 for instance variables</span></dd>
|
| 236 |
+
</dl>
|
| 237 |
+
<dl><dt><strong>__weakref__</strong></dt>
|
| 238 |
+
<dd><span class="code">list of weak references to the 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 a generic class.<br>
|
| 247 |
+
<br>
|
| 248 |
+
At least, parameterizing a generic class is the *main* thing this<br>
|
| 249 |
+
method does. For example, for some generic class `Foo`, this is called<br>
|
| 250 |
+
when we do `Foo[int]` - there, with `cls=Foo` and `params=int`.<br>
|
| 251 |
+
<br>
|
| 252 |
+
However, note that this method is also called when defining generic<br>
|
| 253 |
+
classes in the first place with `class Foo[T]: ...`.</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> <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"> </span></td>
|
| 261 |
+
<td class="decor title-decor" colspan=2><span class="code"><a href="#Prediction">Prediction</a>(**kwargs)<br>
|
| 262 |
+
<br>
|
| 263 |
+
# Les champs de la table app.predictions peuvent être ajustés en fonction des besoins,<br>
|
| 264 |
+
# mais les champs essentiels pour le suivi des prédictions sont request_id, predicted_proba, et created_at<br> </span></td></tr>
|
| 265 |
+
<tr><td> </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 simple constructor that allows initialization from kwargs.<br>
|
| 277 |
+
<br>
|
| 278 |
+
Sets attributes on the constructed instance using the names and<br>
|
| 279 |
+
values in ``kwargs``.<br>
|
| 280 |
+
<br>
|
| 281 |
+
Only keys that are present as<br>
|
| 282 |
+
attributes of the instance's class are allowed. These could be,<br>
|
| 283 |
+
for example, any mapped columns or 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> = <Mapper at 0x28b5d077250; Prediction></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>)), schema='app')</dl>
|
| 314 |
+
|
| 315 |
+
<dl><dt><strong>__table_args__</strong> = (Index('ix_predictions_request_id_created_at', Co...efault(<function utcnow at 0x0000028B5D0A0900>))), Index('ix_predictions_created_at', Column('creat...efault(<function utcnow at 0x0000028B5D0A0900>))), Index('ix_predictions_model_version', Column('mo...length=50), table=<predictions>, 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> = <sqlalchemy.orm.decl_api.registry object></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') -> 'None'</dt><dd><span class="code">Function to initialize 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 for instance variables</span></dd>
|
| 333 |
+
</dl>
|
| 334 |
+
<dl><dt><strong>__weakref__</strong></dt>
|
| 335 |
+
<dd><span class="code">list of weak references to the 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 a generic class.<br>
|
| 344 |
+
<br>
|
| 345 |
+
At least, parameterizing a generic class is the *main* thing this<br>
|
| 346 |
+
method does. For example, for some generic class `Foo`, this is called<br>
|
| 347 |
+
when we do `Foo[int]` - there, with `cls=Foo` and `params=int`.<br>
|
| 348 |
+
<br>
|
| 349 |
+
However, note that this method is also called when defining generic<br>
|
| 350 |
+
classes in the first place with `class Foo[T]: ...`.</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> <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"> </span></td>
|
| 358 |
+
<td class="decor title-decor" colspan=2><span class="code"><a href="#PredictionRequest">PredictionRequest</a>(**kwargs)<br>
|
| 359 |
+
<br>
|
| 360 |
+
# Les champs de la table app.prediction_requests peuvent être ajustés en fonction des besoins,<br>
|
| 361 |
+
# mais les champs essentiels pour le suivi des prédictions sont employee_id, payload_json, et created_at<br> </span></td></tr>
|
| 362 |
+
<tr><td> </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 simple constructor that allows initialization from kwargs.<br>
|
| 374 |
+
<br>
|
| 375 |
+
Sets attributes on the constructed instance using the names and<br>
|
| 376 |
+
values in ``kwargs``.<br>
|
| 377 |
+
<br>
|
| 378 |
+
Only keys that are present as<br>
|
| 379 |
+
attributes of the instance's class are allowed. These could be,<br>
|
| 380 |
+
for example, any mapped columns or 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> = <Mapper at 0x28b5d076490; PredictionRequest></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>)), schema='app')</dl>
|
| 413 |
+
|
| 414 |
+
<dl><dt><strong>__table_args__</strong> = (Index('ix_prediction_requests_created_at', Colum...efault(<function utcnow at 0x0000028B5D06B240>))), {'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> = <sqlalchemy.orm.decl_api.registry object></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') -> 'None'</dt><dd><span class="code">Function to initialize 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 for instance variables</span></dd>
|
| 432 |
+
</dl>
|
| 433 |
+
<dl><dt><strong>__weakref__</strong></dt>
|
| 434 |
+
<dd><span class="code">list of weak references to the 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 a generic class.<br>
|
| 443 |
+
<br>
|
| 444 |
+
At least, parameterizing a generic class is the *main* thing this<br>
|
| 445 |
+
method does. For example, for some generic class `Foo`, this is called<br>
|
| 446 |
+
when we do `Foo[int]` - there, with `cls=Foo` and `params=int`.<br>
|
| 447 |
+
<br>
|
| 448 |
+
However, note that this method is also called when defining generic<br>
|
| 449 |
+
classes in the first place with `class Foo[T]: ...`.</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> <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"> </span></td>
|
| 457 |
+
<td class="decor title-decor" colspan=2><span class="code"><a href="#RawEmployee">RawEmployee</a>(**kwargs)<br>
|
| 458 |
+
<br>
|
| 459 |
+
# Modèles SQLAlchemy représentant les tables de la base de données, organisés par schéma (raw, clean, app)<br> </span></td></tr>
|
| 460 |
+
<tr><td> </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 simple constructor that allows initialization from kwargs.<br>
|
| 472 |
+
<br>
|
| 473 |
+
Sets attributes on the constructed instance using the names and<br>
|
| 474 |
+
values in ``kwargs``.<br>
|
| 475 |
+
<br>
|
| 476 |
+
Only keys that are present as<br>
|
| 477 |
+
attributes of the instance's class are allowed. These could be,<br>
|
| 478 |
+
for example, any mapped columns or 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> = <Mapper at 0x28b5cf4f230; RawEmployee></dl>
|
| 491 |
+
|
| 492 |
+
<dl><dt><strong>__parameters__</strong> = ()</dl>
|
| 493 |
+
|
| 494 |
+
<dl><dt><strong>__table__</strong> = Table('employees', MetaData(), Column('id', BigI...table=<employees>, nullable=False), schema='raw')</dl>
|
| 495 |
+
|
| 496 |
+
<dl><dt><strong>__table_args__</strong> = (Index('ix_raw_employees_employee_external_id', C...', Integer(), table=<employees>, 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> = <sqlalchemy.orm.decl_api.registry object></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') -> 'None'</dt><dd><span class="code">Function to initialize 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 for instance variables</span></dd>
|
| 514 |
+
</dl>
|
| 515 |
+
<dl><dt><strong>__weakref__</strong></dt>
|
| 516 |
+
<dd><span class="code">list of weak references to the 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 a generic class.<br>
|
| 525 |
+
<br>
|
| 526 |
+
At least, parameterizing a generic class is the *main* thing this<br>
|
| 527 |
+
method does. For example, for some generic class `Foo`, this is called<br>
|
| 528 |
+
when we do `Foo[int]` - there, with `cls=Foo` and `params=int`.<br>
|
| 529 |
+
<br>
|
| 530 |
+
However, note that this method is also called when defining generic<br>
|
| 531 |
+
classes in the first place with `class Foo[T]: ...`.</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> <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"> </span></td>
|
| 539 |
+
<td class="decor title-decor" colspan=2><span class="code"><a href="#RawEmployeeSnapshot">RawEmployeeSnapshot</a>(**kwargs)<br>
|
| 540 |
+
<br>
|
| 541 |
+
# Les autres champs de la table raw.employees peuvent être ajoutés ici si nécessaire,<br>
|
| 542 |
+
# mais ne sont pas indispensables pour les prédictions basées sur employee_id<br> </span></td></tr>
|
| 543 |
+
<tr><td> </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 simple constructor that allows initialization from kwargs.<br>
|
| 555 |
+
<br>
|
| 556 |
+
Sets attributes on the constructed instance using the names and<br>
|
| 557 |
+
values in ``kwargs``.<br>
|
| 558 |
+
<br>
|
| 559 |
+
Only keys that are present as<br>
|
| 560 |
+
attributes of the instance's class are allowed. These could be,<br>
|
| 561 |
+
for example, any mapped columns or 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> = <Mapper at 0x28b5cfdd310; RawEmployeeSnapshot></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> = <sqlalchemy.orm.decl_api.registry object></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') -> 'None'</dt><dd><span class="code">Function to initialize 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 for instance variables</span></dd>
|
| 595 |
+
</dl>
|
| 596 |
+
<dl><dt><strong>__weakref__</strong></dt>
|
| 597 |
+
<dd><span class="code">list of weak references to the 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 a generic class.<br>
|
| 606 |
+
<br>
|
| 607 |
+
At least, parameterizing a generic class is the *main* thing this<br>
|
| 608 |
+
method does. For example, for some generic class `Foo`, this is called<br>
|
| 609 |
+
when we do `Foo[int]` - there, with `cls=Foo` and `params=int`.<br>
|
| 610 |
+
<br>
|
| 611 |
+
However, note that this method is also called when defining generic<br>
|
| 612 |
+
classes in the first place with `class Foo[T]: ...`.</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> <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"> </span></td>
|
| 620 |
+
<td class="decor title-decor" colspan=2><span class="code"><a href="#RawSurvey">RawSurvey</a>(**kwargs)<br>
|
| 621 |
+
<br>
|
| 622 |
+
# Les champs de la table raw.employee_snapshots peuvent être ajoutés ici si nécessaire,<br>
|
| 623 |
+
# mais ne sont pas indispensables pour les prédictions basées sur employee_id<br> </span></td></tr>
|
| 624 |
+
<tr><td> </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 simple constructor that allows initialization from kwargs.<br>
|
| 636 |
+
<br>
|
| 637 |
+
Sets attributes on the constructed instance using the names and<br>
|
| 638 |
+
values in ``kwargs``.<br>
|
| 639 |
+
<br>
|
| 640 |
+
Only keys that are present as<br>
|
| 641 |
+
attributes of the instance's class are allowed. These could be,<br>
|
| 642 |
+
for example, any mapped columns or 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> = <Mapper at 0x28b5cfdda90; RawSurvey></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> = <sqlalchemy.orm.decl_api.registry object></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') -> 'None'</dt><dd><span class="code">Function to initialize 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 for instance variables</span></dd>
|
| 676 |
+
</dl>
|
| 677 |
+
<dl><dt><strong>__weakref__</strong></dt>
|
| 678 |
+
<dd><span class="code">list of weak references to the 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 a generic class.<br>
|
| 687 |
+
<br>
|
| 688 |
+
At least, parameterizing a generic class is the *main* thing this<br>
|
| 689 |
+
method does. For example, for some generic class `Foo`, this is called<br>
|
| 690 |
+
when we do `Foo[int]` - there, with `cls=Foo` and `params=int`.<br>
|
| 691 |
+
<br>
|
| 692 |
+
However, note that this method is also called when defining generic<br>
|
| 693 |
+
classes in the first place with `class Foo[T]: ...`.</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> <br><strong class="bigsection">Functions</strong></td></tr>
|
| 699 |
+
|
| 700 |
+
<tr><td class="decor functions-decor"><span class="code"> </span></td><td> </td>
|
| 701 |
+
<td class="singlecolumn"><dl><dt><a name="-utcnow"><strong>utcnow</strong></a>() -> 'datetime'</dt><dd><span class="code"># Fonction utilitaire pour obtenir l'heure actuelle en UTC, utilisée comme valeur par défaut pour les champs 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> <br><strong class="bigsection">Data</strong></td></tr>
|
| 706 |
+
|
| 707 |
+
<tr><td class="decor data-decor"><span class="code"> </span></td><td> </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"> <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> <br><strong class="bigsection">Classes</strong></td></tr>
|
| 17 |
+
|
| 18 |
+
<tr><td class="decor index-decor"><span class="code"> </span></td><td> </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> <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"> </span></td>
|
| 34 |
+
<td class="decor title-decor" colspan=2><span class="code"><a href="#ModelRequest">ModelRequest</a>(<br>
|
| 35 |
+
*,<br>
|
| 36 |
+
note_evaluation_precedente: int,<br>
|
| 37 |
+
niveau_hierarchique_poste: int,<br>
|
| 38 |
+
note_evaluation_actuelle: int,<br>
|
| 39 |
+
heures_supplementaires: Literal[0, 1],<br>
|
| 40 |
+
augmentation_salaire_precedente: float,<br>
|
| 41 |
+
age: int,<br>
|
| 42 |
+
genre: Literal[0, 1],<br>
|
| 43 |
+
revenu_mensuel: int,<br>
|
| 44 |
+
statut_marital: str,<br>
|
| 45 |
+
departement: str,<br>
|
| 46 |
+
poste: str,<br>
|
| 47 |
+
nombre_experiences_precedentes: int,<br>
|
| 48 |
+
annee_experience_totale: int,<br>
|
| 49 |
+
annees_dans_l_entreprise: int,<br>
|
| 50 |
+
annees_dans_le_poste_actuel: int,<br>
|
| 51 |
+
nombre_participation_pee: int,<br>
|
| 52 |
+
nb_formations_suivies: int,<br>
|
| 53 |
+
distance_domicile_travail: int,<br>
|
| 54 |
+
niveau_education: int,<br>
|
| 55 |
+
domaine_etude: str,<br>
|
| 56 |
+
frequence_deplacement: Literal[0, 1, 2, 3],<br>
|
| 57 |
+
annees_depuis_la_derniere_promotion: int,<br>
|
| 58 |
+
annees_sous_responsable_actuel: int,<br>
|
| 59 |
+
satisfaction_moyenne: float,<br>
|
| 60 |
+
nonlineaire_participation_pee: float,<br>
|
| 61 |
+
ratio_heures_sup_salaire: float,<br>
|
| 62 |
+
nonlinaire_charge_contrainte: float,<br>
|
| 63 |
+
nonlinaire_surmenage_insatisfaction: float,<br>
|
| 64 |
+
jeune_surcharge: Literal[0, 1],<br>
|
| 65 |
+
anciennete_sans_promotion: float,<br>
|
| 66 |
+
mobilite_carriere: float,<br>
|
| 67 |
+
risque_global: float<br>
|
| 68 |
+
) -&gt; None<br>
|
| 69 |
+
<br>
|
| 70 |
+
# 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.<br> </span></td></tr>
|
| 71 |
+
<tr><td> </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 of weak references to the 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': <class 'domain.domain.ModelRequest'>, 'config': {'title': 'ModelRequest'}, 'custom_init': False, 'metadata': {'pydantic_js_functions': [<bound method BaseModel.__get_pydantic_json_schema__ of <class 'domain.domain.ModelRequest'>>]}, '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> = <Signature (*, note_evaluation_precedente: int, ...e_carriere: float, risque_global: float) -> None></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) -> 'Self'</dt><dd><span class="code">Returns a shallow copy of the model.</span></dd></dl>
|
| 128 |
+
|
| 129 |
+
<dl><dt><a name="ModelRequest-__deepcopy__"><strong>__deepcopy__</strong></a>(self, memo: 'dict[int, Any] | None' = None) -> 'Self'</dt><dd><span class="code">Returns a deep copy of the model.</span></dd></dl>
|
| 130 |
+
|
| 131 |
+
<dl><dt><a name="ModelRequest-__delattr__"><strong>__delattr__</strong></a>(self, item: 'str') -> 'Any'</dt><dd><span class="code">Implement delattr(self, name).</span></dd></dl>
|
| 132 |
+
|
| 133 |
+
<dl><dt><a name="ModelRequest-__eq__"><strong>__eq__</strong></a>(self, other: 'Any') -> 'bool'</dt><dd><span class="code">Return self==value.</span></dd></dl>
|
| 134 |
+
|
| 135 |
+
<dl><dt><a name="ModelRequest-__getattr__"><strong>__getattr__</strong></a>(self, item: 'str') -> 'Any'</dt></dl>
|
| 136 |
+
|
| 137 |
+
<dl><dt><a name="ModelRequest-__getstate__"><strong>__getstate__</strong></a>(self) -> 'dict[Any, Any]'</dt><dd><span class="code">Helper for pickle.</span></dd></dl>
|
| 138 |
+
|
| 139 |
+
<dl><dt><a name="ModelRequest-__init__"><strong>__init__</strong></a>(self, /, **data: 'Any') -> 'None'</dt><dd><span class="code">Create a new model by parsing and validating input data from keyword arguments.<br>
|
| 140 |
+
<br>
|
| 141 |
+
Raises [`ValidationError`][pydantic_core.ValidationError] if the input data cannot be<br>
|
| 142 |
+
validated to form a valid model.<br>
|
| 143 |
+
<br>
|
| 144 |
+
`self` is explicitly positional-only to allow `self` as a field name.</span></dd></dl>
|
| 145 |
+
|
| 146 |
+
<dl><dt><a name="ModelRequest-__iter__"><strong>__iter__</strong></a>(self) -> 'TupleGenerator'</dt><dd><span class="code">So `<a href="#ModelRequest-dict">dict</a>(model)` works.</span></dd></dl>
|
| 147 |
+
|
| 148 |
+
<dl><dt><a name="ModelRequest-__pretty__"><strong>__pretty__</strong></a>(self, fmt: 'Callable[[Any], Any]', **kwargs: 'Any') -> 'Generator[Any]'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Used by devtools (<a href="https://python-devtools.helpmanual.io/">https://python-devtools.helpmanual.io/</a>) to pretty print objects.</span></dd></dl>
|
| 149 |
+
|
| 150 |
+
<dl><dt><a name="ModelRequest-__replace__"><strong>__replace__</strong></a>(self, **changes: 'Any') -> 'Self'</dt><dd><span class="code"># Because we make use of `@dataclass_transform()`, `__replace__` is already synthesized by<br>
|
| 151 |
+
# type checkers, so we define the implementation in this `if not TYPE_CHECKING:` block:</span></dd></dl>
|
| 152 |
+
|
| 153 |
+
<dl><dt><a name="ModelRequest-__repr__"><strong>__repr__</strong></a>(self) -> 'str'</dt><dd><span class="code">Return repr(self).</span></dd></dl>
|
| 154 |
+
|
| 155 |
+
<dl><dt><a name="ModelRequest-__repr_args__"><strong>__repr_args__</strong></a>(self) -> '_repr.ReprArgs'</dt></dl>
|
| 156 |
+
|
| 157 |
+
<dl><dt><a name="ModelRequest-__repr_name__"><strong>__repr_name__</strong></a>(self) -> 'str'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Name of the instance's class, used in __repr__.</span></dd></dl>
|
| 158 |
+
|
| 159 |
+
<dl><dt><a name="ModelRequest-__repr_recursion__"><strong>__repr_recursion__</strong></a>(self, object: 'Any') -> 'str'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Returns the string representation of a recursive object.</span></dd></dl>
|
| 160 |
+
|
| 161 |
+
<dl><dt><a name="ModelRequest-__repr_str__"><strong>__repr_str__</strong></a>(self, join_str: 'str') -> '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) -> 'RichReprResult'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Used by Rich (<a href="https://rich.readthedocs.io/en/stable/pretty.html">https://rich.readthedocs.io/en/stable/pretty.html</a>) to pretty print objects.</span></dd></dl>
|
| 164 |
+
|
| 165 |
+
<dl><dt><a name="ModelRequest-__setattr__"><strong>__setattr__</strong></a>(self, name: 'str', value: 'Any') -> 'None'</dt><dd><span class="code">Implement setattr(self, name, value).</span></dd></dl>
|
| 166 |
+
|
| 167 |
+
<dl><dt><a name="ModelRequest-__setstate__"><strong>__setstate__</strong></a>(self, state: 'dict[Any, Any]') -> 'None'</dt></dl>
|
| 168 |
+
|
| 169 |
+
<dl><dt><a name="ModelRequest-__str__"><strong>__str__</strong></a>(self) -> 'str'</dt><dd><span class="code">Return 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 |
+
) -> 'Self'</dt><dd><span class="code">Returns a copy of the model.<br>
|
| 179 |
+
<br>
|
| 180 |
+
!!! warning "Deprecated"<br>
|
| 181 |
+
This method is now deprecated; use `model_copy` instead.<br>
|
| 182 |
+
<br>
|
| 183 |
+
If you need `include` or `exclude`, use:<br>
|
| 184 |
+
<br>
|
| 185 |
+
```python {test="skip" lint="skip"}<br>
|
| 186 |
+
data = self.<a href="#ModelRequest-model_dump">model_dump</a>(include=include, exclude=exclude, round_trip=True)<br>
|
| 187 |
+
data = {**data, **(update or {})}<br>
|
| 188 |
+
copied = self.<a href="#ModelRequest-model_validate">model_validate</a>(data)<br>
|
| 189 |
+
```<br>
|
| 190 |
+
<br>
|
| 191 |
+
Args:<br>
|
| 192 |
+
include: Optional set or mapping specifying which fields to include in the copied model.<br>
|
| 193 |
+
exclude: Optional set or mapping specifying which fields to exclude in the copied model.<br>
|
| 194 |
+
update: Optional dictionary of field-value pairs to override field values in the copied model.<br>
|
| 195 |
+
deep: If True, the values of fields that are Pydantic models will be deep-copied.<br>
|
| 196 |
+
<br>
|
| 197 |
+
Returns:<br>
|
| 198 |
+
A copy of the model with included, excluded and updated fields as 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 |
+
) -> '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 |
+
) -> '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 |
+
) -> 'Self'</dt><dd><span class="code">!!! abstract "Usage Documentation"<br>
|
| 231 |
+
[`model_copy`](../concepts/models.md#model-copy)<br>
|
| 232 |
+
<br>
|
| 233 |
+
Returns a copy of the model.<br>
|
| 234 |
+
<br>
|
| 235 |
+
!!! note<br>
|
| 236 |
+
The underlying instance's [`__dict__`][object.__dict__] attribute is copied. This<br>
|
| 237 |
+
might have unexpected side effects if you store anything in it, on top of the model<br>
|
| 238 |
+
fields (e.g. the value of [cached properties][functools.cached_property]).<br>
|
| 239 |
+
<br>
|
| 240 |
+
Args:<br>
|
| 241 |
+
update: Values to change/add in the new model. Note: the data is not validated<br>
|
| 242 |
+
before creating the new model. You should trust this data.<br>
|
| 243 |
+
deep: Set to `True` to make a deep copy of the model.<br>
|
| 244 |
+
<br>
|
| 245 |
+
Returns:<br>
|
| 246 |
+
New model 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 |
+
) -> 'dict[str, Any]'</dt><dd><span class="code">!!! abstract "Usage Documentation"<br>
|
| 265 |
+
[`model_dump`](../concepts/serialization.md#python-mode)<br>
|
| 266 |
+
<br>
|
| 267 |
+
Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.<br>
|
| 268 |
+
<br>
|
| 269 |
+
Args:<br>
|
| 270 |
+
mode: The mode in which `to_python` should run.<br>
|
| 271 |
+
If mode is 'json', the output will only contain JSON serializable types.<br>
|
| 272 |
+
If mode is 'python', the output may contain non-JSON-serializable Python objects.<br>
|
| 273 |
+
include: A set of fields to include in the output.<br>
|
| 274 |
+
exclude: A set of fields to exclude from the output.<br>
|
| 275 |
+
context: Additional context to pass to the serializer.<br>
|
| 276 |
+
by_alias: Whether to use the field's alias in the dictionary key if defined.<br>
|
| 277 |
+
exclude_unset: Whether to exclude fields that have not been explicitly set.<br>
|
| 278 |
+
exclude_defaults: Whether to exclude fields that are set to their default value.<br>
|
| 279 |
+
exclude_none: Whether to exclude fields that have a value of `None`.<br>
|
| 280 |
+
exclude_computed_fields: Whether to exclude computed fields.<br>
|
| 281 |
+
While this can be useful for round-tripping, it is usually recommended to use the dedicated<br>
|
| 282 |
+
`round_trip` parameter instead.<br>
|
| 283 |
+
round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].<br>
|
| 284 |
+
warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors,<br>
|
| 285 |
+
"error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].<br>
|
| 286 |
+
fallback: A function to call when an unknown value is encountered. If not provided,<br>
|
| 287 |
+
a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.<br>
|
| 288 |
+
serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.<br>
|
| 289 |
+
<br>
|
| 290 |
+
Returns:<br>
|
| 291 |
+
A dictionary representation of the 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 |
+
) -> 'str'</dt><dd><span class="code">!!! abstract "Usage Documentation"<br>
|
| 311 |
+
[`model_dump_json`](../concepts/serialization.md#json-mode)<br>
|
| 312 |
+
<br>
|
| 313 |
+
Generates a JSON representation of the model using Pydantic's `to_json` method.<br>
|
| 314 |
+
<br>
|
| 315 |
+
Args:<br>
|
| 316 |
+
indent: Indentation to use in the JSON output. If None is passed, the output will be compact.<br>
|
| 317 |
+
ensure_ascii: If `True`, the output is guaranteed to have all incoming non-ASCII characters escaped.<br>
|
| 318 |
+
If `False` (the default), these characters will be output as-is.<br>
|
| 319 |
+
include: Field(s) to include in the JSON output.<br>
|
| 320 |
+
exclude: Field(s) to exclude from the JSON output.<br>
|
| 321 |
+
context: Additional context to pass to the serializer.<br>
|
| 322 |
+
by_alias: Whether to serialize using field aliases.<br>
|
| 323 |
+
exclude_unset: Whether to exclude fields that have not been explicitly set.<br>
|
| 324 |
+
exclude_defaults: Whether to exclude fields that are set to their default value.<br>
|
| 325 |
+
exclude_none: Whether to exclude fields that have a value of `None`.<br>
|
| 326 |
+
exclude_computed_fields: Whether to exclude computed fields.<br>
|
| 327 |
+
While this can be useful for round-tripping, it is usually recommended to use the dedicated<br>
|
| 328 |
+
`round_trip` parameter instead.<br>
|
| 329 |
+
round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].<br>
|
| 330 |
+
warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors,<br>
|
| 331 |
+
"error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].<br>
|
| 332 |
+
fallback: A function to call when an unknown value is encountered. If not provided,<br>
|
| 333 |
+
a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.<br>
|
| 334 |
+
serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.<br>
|
| 335 |
+
<br>
|
| 336 |
+
Returns:<br>
|
| 337 |
+
A JSON string representation of the model.</span></dd></dl>
|
| 338 |
+
|
| 339 |
+
<dl><dt><a name="ModelRequest-model_post_init"><strong>model_post_init</strong></a>(self, context: 'Any', /) -> 'None'</dt><dd><span class="code">Override this method to perform additional initialization after `__init__` and `model_construct`.<br>
|
| 340 |
+
This is useful if you want to do some validation that requires the entire model to be 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], ...]') -> '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 |
+
) -> '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 |
+
) -> 'JsonSchemaValue'</dt><dd><span class="code">Hook into generating the model's JSON schema.<br>
|
| 357 |
+
<br>
|
| 358 |
+
Args:<br>
|
| 359 |
+
core_schema: A `pydantic-core` CoreSchema.<br>
|
| 360 |
+
You can ignore this argument and call the handler with a new CoreSchema,<br>
|
| 361 |
+
wrap this CoreSchema (`{'type': 'nullable', 'schema': current_schema}`),<br>
|
| 362 |
+
or just call the handler with the original schema.<br>
|
| 363 |
+
handler: Call into Pydantic's internal JSON schema generation.<br>
|
| 364 |
+
This will raise a `pydantic.errors.PydanticInvalidForJsonSchema` if JSON schema<br>
|
| 365 |
+
generation fails.<br>
|
| 366 |
+
Since this gets called by `<a href="pydantic.main.html#BaseModel">BaseModel</a>.model_json_schema` you can override the<br>
|
| 367 |
+
`schema_generator` argument to that function to change JSON schema generation globally<br>
|
| 368 |
+
for a type.<br>
|
| 369 |
+
<br>
|
| 370 |
+
Returns:<br>
|
| 371 |
+
A JSON schema, as a Python object.</span></dd></dl>
|
| 372 |
+
|
| 373 |
+
<dl><dt><a name="ModelRequest-__pydantic_init_subclass__"><strong>__pydantic_init_subclass__</strong></a>(**kwargs: 'Any') -> 'None'</dt><dd><span class="code">This is intended to behave just like `__init_subclass__`, but is called by `ModelMetaclass`<br>
|
| 374 |
+
only after basic class initialization is complete. In particular, attributes like `model_fields` will<br>
|
| 375 |
+
be present when this is called, but forward annotations are not guaranteed to be resolved yet,<br>
|
| 376 |
+
meaning that creating an instance of the class may fail.<br>
|
| 377 |
+
<br>
|
| 378 |
+
This is necessary because `__init_subclass__` will always be called by `type.__new__`,<br>
|
| 379 |
+
and it would require a prohibitively large refactor to the `ModelMetaclass` to ensure that<br>
|
| 380 |
+
`type.__new__` was called in such a manner that the class would already be sufficiently initialized.<br>
|
| 381 |
+
<br>
|
| 382 |
+
This will receive the same `kwargs` that would be passed to the standard `__init_subclass__`, namely,<br>
|
| 383 |
+
any kwargs passed to the class definition that aren't used internally by Pydantic.<br>
|
| 384 |
+
<br>
|
| 385 |
+
Args:<br>
|
| 386 |
+
**kwargs: Any keyword arguments passed to the class definition that aren't used internally<br>
|
| 387 |
+
by Pydantic.<br>
|
| 388 |
+
<br>
|
| 389 |
+
Note:<br>
|
| 390 |
+
You may want to override [`<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 |
+
instead, which is called once the class and its fields are fully initialized and ready for validation.</span></dd></dl>
|
| 392 |
+
|
| 393 |
+
<dl><dt><a name="ModelRequest-__pydantic_on_complete__"><strong>__pydantic_on_complete__</strong></a>() -> 'None'</dt><dd><span class="code">This is called once the class and its fields are fully initialized and ready to be used.<br>
|
| 394 |
+
<br>
|
| 395 |
+
This typically happens when the class is created (just 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__] is called on the superclass),<br>
|
| 397 |
+
except when forward annotations are used that could not immediately be resolved.<br>
|
| 398 |
+
In that case, it will be called later, when the model is rebuilt automatically or explicitly 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') -> 'Self'</dt></dl>
|
| 402 |
+
|
| 403 |
+
<dl><dt><a name="ModelRequest-from_orm"><strong>from_orm</strong></a>(obj: 'Any') -> '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') -> 'Self'</dt><dd><span class="code">Creates a new instance of the `Model` class with validated data.<br>
|
| 406 |
+
<br>
|
| 407 |
+
Creates a new model setting `__dict__` and `__pydantic_fields_set__` from trusted or pre-validated data.<br>
|
| 408 |
+
Default values are respected, but no other validation is performed.<br>
|
| 409 |
+
<br>
|
| 410 |
+
!!! note<br>
|
| 411 |
+
`<a href="#ModelRequest-model_construct">model_construct</a>()` generally respects the `model_config.extra` setting on the provided model.<br>
|
| 412 |
+
That is, if `model_config.extra == 'allow'`, then all extra passed values are added to the model instance's `__dict__`<br>
|
| 413 |
+
and `__pydantic_extra__` fields. If `model_config.extra == 'ignore'` (the default), then all extra passed values are ignored.<br>
|
| 414 |
+
Because no validation is performed with a call to `<a href="#ModelRequest-model_construct">model_construct</a>()`, having `model_config.extra == 'forbid'` does not result in<br>
|
| 415 |
+
an error if extra values are passed, but they will be ignored.<br>
|
| 416 |
+
<br>
|
| 417 |
+
Args:<br>
|
| 418 |
+
_fields_set: A set of field names that were originally explicitly set during instantiation. If provided,<br>
|
| 419 |
+
this is directly used for the [`model_fields_set`][pydantic.<a href="pydantic.main.html#BaseModel">BaseModel</a>.model_fields_set] attribute.<br>
|
| 420 |
+
Otherwise, the field names from the `values` argument will be used.<br>
|
| 421 |
+
values: Trusted or pre-validated data dictionary.<br>
|
| 422 |
+
<br>
|
| 423 |
+
Returns:<br>
|
| 424 |
+
A new instance of the `Model` class with validated 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]' = <class 'pydantic.json_schema.GenerateJsonSchema'>,
|
| 430 |
+
mode: 'JsonSchemaMode' = 'validation',
|
| 431 |
+
*,
|
| 432 |
+
union_format: "Literal['any_of', 'primitive_type_array']" = 'any_of'
|
| 433 |
+
) -> 'dict[str, Any]'</dt><dd><span class="code">Generates a JSON schema for a model class.<br>
|
| 434 |
+
<br>
|
| 435 |
+
Args:<br>
|
| 436 |
+
by_alias: Whether to use attribute aliases or not.<br>
|
| 437 |
+
ref_template: The reference template.<br>
|
| 438 |
+
union_format: The format to use when combining schemas from unions together. Can be one of:<br>
|
| 439 |
+
<br>
|
| 440 |
+
- `'any_of'`: Use the [`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 |
+
keyword to combine schemas (the default).<br>
|
| 442 |
+
- `'primitive_type_array'`: Use the [`type`](<a href="https://json-schema.org/understanding-json-schema/reference/type">https://json-schema.org/understanding-json-schema/reference/type</a>)<br>
|
| 443 |
+
keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive<br>
|
| 444 |
+
type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to<br>
|
| 445 |
+
`any_of`.<br>
|
| 446 |
+
schema_generator: To override the logic used to generate the JSON schema, as a subclass of<br>
|
| 447 |
+
`GenerateJsonSchema` with your desired modifications<br>
|
| 448 |
+
mode: The mode in which to generate the schema.<br>
|
| 449 |
+
<br>
|
| 450 |
+
Returns:<br>
|
| 451 |
+
The JSON schema for the given model class.</span></dd></dl>
|
| 452 |
+
|
| 453 |
+
<dl><dt><a name="ModelRequest-model_parametrized_name"><strong>model_parametrized_name</strong></a>(params: 'tuple[type[Any], ...]') -> 'str'</dt><dd><span class="code">Compute the class name for parametrizations of generic classes.<br>
|
| 454 |
+
<br>
|
| 455 |
+
This method can be overridden to achieve a custom naming scheme for generic BaseModels.<br>
|
| 456 |
+
<br>
|
| 457 |
+
Args:<br>
|
| 458 |
+
params: Tuple of types of the class. Given a generic class<br>
|
| 459 |
+
`Model` with 2 type variables and a concrete model `Model[str, int]`,<br>
|
| 460 |
+
the value `(str, int)` would be passed to `params`.<br>
|
| 461 |
+
<br>
|
| 462 |
+
Returns:<br>
|
| 463 |
+
String representing the new class where `params` are passed to `cls` as type variables.<br>
|
| 464 |
+
<br>
|
| 465 |
+
Raises:<br>
|
| 466 |
+
TypeError: Raised when trying to generate concrete names for non-generic 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 |
+
) -> 'bool | None'</dt><dd><span class="code">Try to rebuild the pydantic-core schema for the model.<br>
|
| 475 |
+
<br>
|
| 476 |
+
This may be necessary when one of the annotations is a ForwardRef which could not be resolved during<br>
|
| 477 |
+
the initial attempt to build the schema, and automatic rebuilding fails.<br>
|
| 478 |
+
<br>
|
| 479 |
+
Args:<br>
|
| 480 |
+
force: Whether to force the rebuilding of the model schema, defaults to `False`.<br>
|
| 481 |
+
raise_errors: Whether to raise errors, defaults to `True`.<br>
|
| 482 |
+
_parent_namespace_depth: The depth level of the parent namespace, defaults to 2.<br>
|
| 483 |
+
_types_namespace: The types namespace, defaults to `None`.<br>
|
| 484 |
+
<br>
|
| 485 |
+
Returns:<br>
|
| 486 |
+
Returns `None` if the schema is already "complete" and rebuilding was not required.<br>
|
| 487 |
+
If rebuilding _was_ required, returns `True` if rebuilding was successful, otherwise `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 |
+
) -> 'Self'</dt><dd><span class="code">Validate a pydantic model instance.<br>
|
| 499 |
+
<br>
|
| 500 |
+
Args:<br>
|
| 501 |
+
obj: The object to validate.<br>
|
| 502 |
+
strict: Whether to enforce types strictly.<br>
|
| 503 |
+
extra: Whether to ignore, allow, or forbid extra data during model validation.<br>
|
| 504 |
+
See the [`extra` configuration value][pydantic.ConfigDict.extra] for details.<br>
|
| 505 |
+
from_attributes: Whether to extract data from object attributes.<br>
|
| 506 |
+
context: Additional context to pass to the validator.<br>
|
| 507 |
+
by_alias: Whether to use the field's alias when validating against the provided input data.<br>
|
| 508 |
+
by_name: Whether to use the field's name when validating against the provided input data.<br>
|
| 509 |
+
<br>
|
| 510 |
+
Raises:<br>
|
| 511 |
+
ValidationError: If the object could not be validated.<br>
|
| 512 |
+
<br>
|
| 513 |
+
Returns:<br>
|
| 514 |
+
The validated model 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 |
+
) -> 'Self'</dt><dd><span class="code">!!! abstract "Usage Documentation"<br>
|
| 525 |
+
[JSON Parsing](../concepts/json.md#json-parsing)<br>
|
| 526 |
+
<br>
|
| 527 |
+
Validate the given JSON data against the Pydantic model.<br>
|
| 528 |
+
<br>
|
| 529 |
+
Args:<br>
|
| 530 |
+
json_data: The JSON data to validate.<br>
|
| 531 |
+
strict: Whether to enforce types strictly.<br>
|
| 532 |
+
extra: Whether to ignore, allow, or forbid extra data during model validation.<br>
|
| 533 |
+
See the [`extra` configuration value][pydantic.ConfigDict.extra] for details.<br>
|
| 534 |
+
context: Extra variables to pass to the validator.<br>
|
| 535 |
+
by_alias: Whether to use the field's alias when validating against the provided input data.<br>
|
| 536 |
+
by_name: Whether to use the field's name when validating against the provided input data.<br>
|
| 537 |
+
<br>
|
| 538 |
+
Returns:<br>
|
| 539 |
+
The validated Pydantic model.<br>
|
| 540 |
+
<br>
|
| 541 |
+
Raises:<br>
|
| 542 |
+
ValidationError: If `json_data` is not a JSON string or the object could not be 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 |
+
) -> 'Self'</dt><dd><span class="code">Validate the given object with string data against the Pydantic model.<br>
|
| 553 |
+
<br>
|
| 554 |
+
Args:<br>
|
| 555 |
+
obj: The object containing string data to validate.<br>
|
| 556 |
+
strict: Whether to enforce types strictly.<br>
|
| 557 |
+
extra: Whether to ignore, allow, or forbid extra data during model validation.<br>
|
| 558 |
+
See the [`extra` configuration value][pydantic.ConfigDict.extra] for details.<br>
|
| 559 |
+
context: Extra variables to pass to the validator.<br>
|
| 560 |
+
by_alias: Whether to use the field's alias when validating against the provided input data.<br>
|
| 561 |
+
by_name: Whether to use the field's name when validating against the provided input data.<br>
|
| 562 |
+
<br>
|
| 563 |
+
Returns:<br>
|
| 564 |
+
The validated Pydantic 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 |
+
) -> 'Self'</dt></dl>
|
| 574 |
+
|
| 575 |
+
<dl><dt><a name="ModelRequest-parse_obj"><strong>parse_obj</strong></a>(obj: 'Any') -> '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 |
+
) -> 'Self'</dt></dl>
|
| 585 |
+
|
| 586 |
+
<dl><dt><a name="ModelRequest-schema"><strong>schema</strong></a>(by_alias: 'bool' = True, ref_template: 'str' = '#/$defs/{model}') -> '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 |
+
) -> 'str'</dt></dl>
|
| 594 |
+
|
| 595 |
+
<dl><dt><a name="ModelRequest-update_forward_refs"><strong>update_forward_refs</strong></a>(**localns: 'Any') -> 'None'</dt></dl>
|
| 596 |
+
|
| 597 |
+
<dl><dt><a name="ModelRequest-validate"><strong>validate</strong></a>(value: 'Any') -> '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 extra fields set during validation.<br>
|
| 605 |
+
<br>
|
| 606 |
+
Returns:<br>
|
| 607 |
+
A dictionary of extra fields, or `None` if `config.extra` is not set to `"allow"`.</span></dd>
|
| 608 |
+
</dl>
|
| 609 |
+
<dl><dt><strong>model_fields_set</strong></dt>
|
| 610 |
+
<dd><span class="code">Returns the set of fields that have been explicitly set on this model instance.<br>
|
| 611 |
+
<br>
|
| 612 |
+
Returns:<br>
|
| 613 |
+
A set of strings representing the fields that have been set,<br>
|
| 614 |
+
i.e. that were not filled from 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 for instance 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> <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"> </span></td>
|
| 643 |
+
<td class="decor title-decor" colspan=2><span class="code"><a href="#ModelResponse">ModelResponse</a>(<br>
|
| 644 |
+
*,<br>
|
| 645 |
+
employee_id: Optional[int] = None,<br>
|
| 646 |
+
turnover_probability: float,<br>
|
| 647 |
+
will_leave: bool<br>
|
| 648 |
+
) -&gt; None<br>
|
| 649 |
+
<br>
|
| 650 |
+
# 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.<br> </span></td></tr>
|
| 651 |
+
<tr><td> </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 of weak references to the 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': <class 'domain.domain.ModelResponse'>, 'config': {'title': 'ModelResponse'}, 'custom_init': False, 'metadata': {'pydantic_js_functions': [<bound method BaseModel.__get_pydantic_json_schema__ of <class 'domain.domain.ModelResponse'>>]}, '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> = <Signature (*, employee_id: Optional[int] = None...er_probability: float, will_leave: bool) -> None></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) -> 'Self'</dt><dd><span class="code">Returns a shallow copy of the model.</span></dd></dl>
|
| 708 |
+
|
| 709 |
+
<dl><dt><a name="ModelResponse-__deepcopy__"><strong>__deepcopy__</strong></a>(self, memo: 'dict[int, Any] | None' = None) -> 'Self'</dt><dd><span class="code">Returns a deep copy of the model.</span></dd></dl>
|
| 710 |
+
|
| 711 |
+
<dl><dt><a name="ModelResponse-__delattr__"><strong>__delattr__</strong></a>(self, item: 'str') -> 'Any'</dt><dd><span class="code">Implement delattr(self, name).</span></dd></dl>
|
| 712 |
+
|
| 713 |
+
<dl><dt><a name="ModelResponse-__eq__"><strong>__eq__</strong></a>(self, other: 'Any') -> 'bool'</dt><dd><span class="code">Return self==value.</span></dd></dl>
|
| 714 |
+
|
| 715 |
+
<dl><dt><a name="ModelResponse-__getattr__"><strong>__getattr__</strong></a>(self, item: 'str') -> 'Any'</dt></dl>
|
| 716 |
+
|
| 717 |
+
<dl><dt><a name="ModelResponse-__getstate__"><strong>__getstate__</strong></a>(self) -> 'dict[Any, Any]'</dt><dd><span class="code">Helper for pickle.</span></dd></dl>
|
| 718 |
+
|
| 719 |
+
<dl><dt><a name="ModelResponse-__init__"><strong>__init__</strong></a>(self, /, **data: 'Any') -> 'None'</dt><dd><span class="code">Create a new model by parsing and validating input data from keyword arguments.<br>
|
| 720 |
+
<br>
|
| 721 |
+
Raises [`ValidationError`][pydantic_core.ValidationError] if the input data cannot be<br>
|
| 722 |
+
validated to form a valid model.<br>
|
| 723 |
+
<br>
|
| 724 |
+
`self` is explicitly positional-only to allow `self` as a field name.</span></dd></dl>
|
| 725 |
+
|
| 726 |
+
<dl><dt><a name="ModelResponse-__iter__"><strong>__iter__</strong></a>(self) -> 'TupleGenerator'</dt><dd><span class="code">So `<a href="#ModelResponse-dict">dict</a>(model)` works.</span></dd></dl>
|
| 727 |
+
|
| 728 |
+
<dl><dt><a name="ModelResponse-__pretty__"><strong>__pretty__</strong></a>(self, fmt: 'Callable[[Any], Any]', **kwargs: 'Any') -> 'Generator[Any]'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Used by devtools (<a href="https://python-devtools.helpmanual.io/">https://python-devtools.helpmanual.io/</a>) to pretty print objects.</span></dd></dl>
|
| 729 |
+
|
| 730 |
+
<dl><dt><a name="ModelResponse-__replace__"><strong>__replace__</strong></a>(self, **changes: 'Any') -> 'Self'</dt><dd><span class="code"># Because we make use of `@dataclass_transform()`, `__replace__` is already synthesized by<br>
|
| 731 |
+
# type checkers, so we define the implementation in this `if not TYPE_CHECKING:` block:</span></dd></dl>
|
| 732 |
+
|
| 733 |
+
<dl><dt><a name="ModelResponse-__repr__"><strong>__repr__</strong></a>(self) -> 'str'</dt><dd><span class="code">Return repr(self).</span></dd></dl>
|
| 734 |
+
|
| 735 |
+
<dl><dt><a name="ModelResponse-__repr_args__"><strong>__repr_args__</strong></a>(self) -> '_repr.ReprArgs'</dt></dl>
|
| 736 |
+
|
| 737 |
+
<dl><dt><a name="ModelResponse-__repr_name__"><strong>__repr_name__</strong></a>(self) -> 'str'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Name of the instance's class, used in __repr__.</span></dd></dl>
|
| 738 |
+
|
| 739 |
+
<dl><dt><a name="ModelResponse-__repr_recursion__"><strong>__repr_recursion__</strong></a>(self, object: 'Any') -> 'str'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Returns the string representation of a recursive object.</span></dd></dl>
|
| 740 |
+
|
| 741 |
+
<dl><dt><a name="ModelResponse-__repr_str__"><strong>__repr_str__</strong></a>(self, join_str: 'str') -> '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) -> 'RichReprResult'<span class="grey"><span class="heading-text"> from pydantic._internal._repr.Representation</span></span></dt><dd><span class="code">Used by Rich (<a href="https://rich.readthedocs.io/en/stable/pretty.html">https://rich.readthedocs.io/en/stable/pretty.html</a>) to pretty print objects.</span></dd></dl>
|
| 744 |
+
|
| 745 |
+
<dl><dt><a name="ModelResponse-__setattr__"><strong>__setattr__</strong></a>(self, name: 'str', value: 'Any') -> 'None'</dt><dd><span class="code">Implement setattr(self, name, value).</span></dd></dl>
|
| 746 |
+
|
| 747 |
+
<dl><dt><a name="ModelResponse-__setstate__"><strong>__setstate__</strong></a>(self, state: 'dict[Any, Any]') -> 'None'</dt></dl>
|
| 748 |
+
|
| 749 |
+
<dl><dt><a name="ModelResponse-__str__"><strong>__str__</strong></a>(self) -> 'str'</dt><dd><span class="code">Return 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 |
+
) -> 'Self'</dt><dd><span class="code">Returns a copy of the model.<br>
|
| 759 |
+
<br>
|
| 760 |
+
!!! warning "Deprecated"<br>
|
| 761 |
+
This method is now deprecated; use `model_copy` instead.<br>
|
| 762 |
+
<br>
|
| 763 |
+
If you need `include` or `exclude`, use:<br>
|
| 764 |
+
<br>
|
| 765 |
+
```python {test="skip" lint="skip"}<br>
|
| 766 |
+
data = self.<a href="#ModelResponse-model_dump">model_dump</a>(include=include, exclude=exclude, round_trip=True)<br>
|
| 767 |
+
data = {**data, **(update or {})}<br>
|
| 768 |
+
copied = self.<a href="#ModelResponse-model_validate">model_validate</a>(data)<br>
|
| 769 |
+
```<br>
|
| 770 |
+
<br>
|
| 771 |
+
Args:<br>
|
| 772 |
+
include: Optional set or mapping specifying which fields to include in the copied model.<br>
|
| 773 |
+
exclude: Optional set or mapping specifying which fields to exclude in the copied model.<br>
|
| 774 |
+
update: Optional dictionary of field-value pairs to override field values in the copied model.<br>
|
| 775 |
+
deep: If True, the values of fields that are Pydantic models will be deep-copied.<br>
|
| 776 |
+
<br>
|
| 777 |
+
Returns:<br>
|
| 778 |
+
A copy of the model with included, excluded and updated fields as 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 |
+
) -> '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 |
+
) -> '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 |
+
) -> 'Self'</dt><dd><span class="code">!!! abstract "Usage Documentation"<br>
|
| 811 |
+
[`model_copy`](../concepts/models.md#model-copy)<br>
|
| 812 |
+
<br>
|
| 813 |
+
Returns a copy of the model.<br>
|
| 814 |
+
<br>
|
| 815 |
+
!!! note<br>
|
| 816 |
+
The underlying instance's [`__dict__`][object.__dict__] attribute is copied. This<br>
|
| 817 |
+
might have unexpected side effects if you store anything in it, on top of the model<br>
|
| 818 |
+
fields (e.g. the value of [cached properties][functools.cached_property]).<br>
|
| 819 |
+
<br>
|
| 820 |
+
Args:<br>
|
| 821 |
+
update: Values to change/add in the new model. Note: the data is not validated<br>
|
| 822 |
+
before creating the new model. You should trust this data.<br>
|
| 823 |
+
deep: Set to `True` to make a deep copy of the model.<br>
|
| 824 |
+
<br>
|
| 825 |
+
Returns:<br>
|
| 826 |
+
New model 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 |
+
) -> 'dict[str, Any]'</dt><dd><span class="code">!!! abstract "Usage Documentation"<br>
|
| 845 |
+
[`model_dump`](../concepts/serialization.md#python-mode)<br>
|
| 846 |
+
<br>
|
| 847 |
+
Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.<br>
|
| 848 |
+
<br>
|
| 849 |
+
Args:<br>
|
| 850 |
+
mode: The mode in which `to_python` should run.<br>
|
| 851 |
+
If mode is 'json', the output will only contain JSON serializable types.<br>
|
| 852 |
+
If mode is 'python', the output may contain non-JSON-serializable Python objects.<br>
|
| 853 |
+
include: A set of fields to include in the output.<br>
|
| 854 |
+
exclude: A set of fields to exclude from the output.<br>
|
| 855 |
+
context: Additional context to pass to the serializer.<br>
|
| 856 |
+
by_alias: Whether to use the field's alias in the dictionary key if defined.<br>
|
| 857 |
+
exclude_unset: Whether to exclude fields that have not been explicitly set.<br>
|
| 858 |
+
exclude_defaults: Whether to exclude fields that are set to their default value.<br>
|
| 859 |
+
exclude_none: Whether to exclude fields that have a value of `None`.<br>
|
| 860 |
+
exclude_computed_fields: Whether to exclude computed fields.<br>
|
| 861 |
+
While this can be useful for round-tripping, it is usually recommended to use the dedicated<br>
|
| 862 |
+
`round_trip` parameter instead.<br>
|
| 863 |
+
round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].<br>
|
| 864 |
+
warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors,<br>
|
| 865 |
+
"error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].<br>
|
| 866 |
+
fallback: A function to call when an unknown value is encountered. If not provided,<br>
|
| 867 |
+
a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.<br>
|
| 868 |
+
serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.<br>
|
| 869 |
+
<br>
|
| 870 |
+
Returns:<br>
|
| 871 |
+
A dictionary representation of the 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 |
+
) -> 'str'</dt><dd><span class="code">!!! abstract "Usage Documentation"<br>
|
| 891 |
+
[`model_dump_json`](../concepts/serialization.md#json-mode)<br>
|
| 892 |
+
<br>
|
| 893 |
+
Generates a JSON representation of the model using Pydantic's `to_json` method.<br>
|
| 894 |
+
<br>
|
| 895 |
+
Args:<br>
|
| 896 |
+
indent: Indentation to use in the JSON output. If None is passed, the output will be compact.<br>
|
| 897 |
+
ensure_ascii: If `True`, the output is guaranteed to have all incoming non-ASCII characters escaped.<br>
|
| 898 |
+
If `False` (the default), these characters will be output as-is.<br>
|
| 899 |
+
include: Field(s) to include in the JSON output.<br>
|
| 900 |
+
exclude: Field(s) to exclude from the JSON output.<br>
|
| 901 |
+
context: Additional context to pass to the serializer.<br>
|
| 902 |
+
by_alias: Whether to serialize using field aliases.<br>
|
| 903 |
+
exclude_unset: Whether to exclude fields that have not been explicitly set.<br>
|
| 904 |
+
exclude_defaults: Whether to exclude fields that are set to their default value.<br>
|
| 905 |
+
exclude_none: Whether to exclude fields that have a value of `None`.<br>
|
| 906 |
+
exclude_computed_fields: Whether to exclude computed fields.<br>
|
| 907 |
+
While this can be useful for round-tripping, it is usually recommended to use the dedicated<br>
|
| 908 |
+
`round_trip` parameter instead.<br>
|
| 909 |
+
round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].<br>
|
| 910 |
+
warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors,<br>
|
| 911 |
+
"error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].<br>
|
| 912 |
+
fallback: A function to call when an unknown value is encountered. If not provided,<br>
|
| 913 |
+
a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.<br>
|
| 914 |
+
serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.<br>
|
| 915 |
+
<br>
|
| 916 |
+
Returns:<br>
|
| 917 |
+
A JSON string representation of the model.</span></dd></dl>
|
| 918 |
+
|
| 919 |
+
<dl><dt><a name="ModelResponse-model_post_init"><strong>model_post_init</strong></a>(self, context: 'Any', /) -> 'None'</dt><dd><span class="code">Override this method to perform additional initialization after `__init__` and `model_construct`.<br>
|
| 920 |
+
This is useful if you want to do some validation that requires the entire model to be 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], ...]') -> '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 |
+
) -> '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 |
+
) -> 'JsonSchemaValue'</dt><dd><span class="code">Hook into generating the model's JSON schema.<br>
|
| 937 |
+
<br>
|
| 938 |
+
Args:<br>
|
| 939 |
+
core_schema: A `pydantic-core` CoreSchema.<br>
|
| 940 |
+
You can ignore this argument and call the handler with a new CoreSchema,<br>
|
| 941 |
+
wrap this CoreSchema (`{'type': 'nullable', 'schema': current_schema}`),<br>
|
| 942 |
+
or just call the handler with the original schema.<br>
|
| 943 |
+
handler: Call into Pydantic's internal JSON schema generation.<br>
|
| 944 |
+
This will raise a `pydantic.errors.PydanticInvalidForJsonSchema` if JSON schema<br>
|
| 945 |
+
generation fails.<br>
|
| 946 |
+
Since this gets called by `<a href="pydantic.main.html#BaseModel">BaseModel</a>.model_json_schema` you can override the<br>
|
| 947 |
+
`schema_generator` argument to that function to change JSON schema generation globally<br>
|
| 948 |
+
for a type.<br>
|
| 949 |
+
<br>
|
| 950 |
+
Returns:<br>
|
| 951 |
+
A JSON schema, as a Python object.</span></dd></dl>
|
| 952 |
+
|
| 953 |
+
<dl><dt><a name="ModelResponse-__pydantic_init_subclass__"><strong>__pydantic_init_subclass__</strong></a>(**kwargs: 'Any') -> 'None'</dt><dd><span class="code">This is intended to behave just like `__init_subclass__`, but is called by `ModelMetaclass`<br>
|
| 954 |
+
only after basic class initialization is complete. In particular, attributes like `model_fields` will<br>
|
| 955 |
+
be present when this is called, but forward annotations are not guaranteed to be resolved yet,<br>
|
| 956 |
+
meaning that creating an instance of the class may fail.<br>
|
| 957 |
+
<br>
|
| 958 |
+
This is necessary because `__init_subclass__` will always be called by `type.__new__`,<br>
|
| 959 |
+
and it would require a prohibitively large refactor to the `ModelMetaclass` to ensure that<br>
|
| 960 |
+
`type.__new__` was called in such a manner that the class would already be sufficiently initialized.<br>
|
| 961 |
+
<br>
|
| 962 |
+
This will receive the same `kwargs` that would be passed to the standard `__init_subclass__`, namely,<br>
|
| 963 |
+
any kwargs passed to the class definition that aren't used internally by Pydantic.<br>
|
| 964 |
+
<br>
|
| 965 |
+
Args:<br>
|
| 966 |
+
**kwargs: Any keyword arguments passed to the class definition that aren't used internally<br>
|
| 967 |
+
by Pydantic.<br>
|
| 968 |
+
<br>
|
| 969 |
+
Note:<br>
|
| 970 |
+
You may want to override [`<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 |
+
instead, which is called once the class and its fields are fully initialized and ready for validation.</span></dd></dl>
|
| 972 |
+
|
| 973 |
+
<dl><dt><a name="ModelResponse-__pydantic_on_complete__"><strong>__pydantic_on_complete__</strong></a>() -> 'None'</dt><dd><span class="code">This is called once the class and its fields are fully initialized and ready to be used.<br>
|
| 974 |
+
<br>
|
| 975 |
+
This typically happens when the class is created (just 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__] is called on the superclass),<br>
|
| 977 |
+
except when forward annotations are used that could not immediately be resolved.<br>
|
| 978 |
+
In that case, it will be called later, when the model is rebuilt automatically or explicitly 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') -> 'Self'</dt></dl>
|
| 982 |
+
|
| 983 |
+
<dl><dt><a name="ModelResponse-from_orm"><strong>from_orm</strong></a>(obj: 'Any') -> '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') -> 'Self'</dt><dd><span class="code">Creates a new instance of the `Model` class with validated data.<br>
|
| 986 |
+
<br>
|
| 987 |
+
Creates a new model setting `__dict__` and `__pydantic_fields_set__` from trusted or pre-validated data.<br>
|
| 988 |
+
Default values are respected, but no other validation is performed.<br>
|
| 989 |
+
<br>
|
| 990 |
+
!!! note<br>
|
| 991 |
+
`<a href="#ModelResponse-model_construct">model_construct</a>()` generally respects the `model_config.extra` setting on the provided model.<br>
|
| 992 |
+
That is, if `model_config.extra == 'allow'`, then all extra passed values are added to the model instance's `__dict__`<br>
|
| 993 |
+
and `__pydantic_extra__` fields. If `model_config.extra == 'ignore'` (the default), then all extra passed values are ignored.<br>
|
| 994 |
+
Because no validation is performed with a call to `<a href="#ModelResponse-model_construct">model_construct</a>()`, having `model_config.extra == 'forbid'` does not result in<br>
|
| 995 |
+
an error if extra values are passed, but they will be ignored.<br>
|
| 996 |
+
<br>
|
| 997 |
+
Args:<br>
|
| 998 |
+
_fields_set: A set of field names that were originally explicitly set during instantiation. If provided,<br>
|
| 999 |
+
this is directly used for the [`model_fields_set`][pydantic.<a href="pydantic.main.html#BaseModel">BaseModel</a>.model_fields_set] attribute.<br>
|
| 1000 |
+
Otherwise, the field names from the `values` argument will be used.<br>
|
| 1001 |
+
values: Trusted or pre-validated data dictionary.<br>
|
| 1002 |
+
<br>
|
| 1003 |
+
Returns:<br>
|
| 1004 |
+
A new instance of the `Model` class with validated 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]' = <class 'pydantic.json_schema.GenerateJsonSchema'>,
|
| 1010 |
+
mode: 'JsonSchemaMode' = 'validation',
|
| 1011 |
+
*,
|
| 1012 |
+
union_format: "Literal['any_of', 'primitive_type_array']" = 'any_of'
|
| 1013 |
+
) -> 'dict[str, Any]'</dt><dd><span class="code">Generates a JSON schema for a model class.<br>
|
| 1014 |
+
<br>
|
| 1015 |
+
Args:<br>
|
| 1016 |
+
by_alias: Whether to use attribute aliases or not.<br>
|
| 1017 |
+
ref_template: The reference template.<br>
|
| 1018 |
+
union_format: The format to use when combining schemas from unions together. Can be one of:<br>
|
| 1019 |
+
<br>
|
| 1020 |
+
- `'any_of'`: Use the [`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 |
+
keyword to combine schemas (the default).<br>
|
| 1022 |
+
- `'primitive_type_array'`: Use the [`type`](<a href="https://json-schema.org/understanding-json-schema/reference/type">https://json-schema.org/understanding-json-schema/reference/type</a>)<br>
|
| 1023 |
+
keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive<br>
|
| 1024 |
+
type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to<br>
|
| 1025 |
+
`any_of`.<br>
|
| 1026 |
+
schema_generator: To override the logic used to generate the JSON schema, as a subclass of<br>
|
| 1027 |
+
`GenerateJsonSchema` with your desired modifications<br>
|
| 1028 |
+
mode: The mode in which to generate the schema.<br>
|
| 1029 |
+
<br>
|
| 1030 |
+
Returns:<br>
|
| 1031 |
+
The JSON schema for the given model class.</span></dd></dl>
|
| 1032 |
+
|
| 1033 |
+
<dl><dt><a name="ModelResponse-model_parametrized_name"><strong>model_parametrized_name</strong></a>(params: 'tuple[type[Any], ...]') -> 'str'</dt><dd><span class="code">Compute the class name for parametrizations of generic classes.<br>
|
| 1034 |
+
<br>
|
| 1035 |
+
This method can be overridden to achieve a custom naming scheme for generic BaseModels.<br>
|
| 1036 |
+
<br>
|
| 1037 |
+
Args:<br>
|
| 1038 |
+
params: Tuple of types of the class. Given a generic class<br>
|
| 1039 |
+
`Model` with 2 type variables and a concrete model `Model[str, int]`,<br>
|
| 1040 |
+
the value `(str, int)` would be passed to `params`.<br>
|
| 1041 |
+
<br>
|
| 1042 |
+
Returns:<br>
|
| 1043 |
+
String representing the new class where `params` are passed to `cls` as type variables.<br>
|
| 1044 |
+
<br>
|
| 1045 |
+
Raises:<br>
|
| 1046 |
+
TypeError: Raised when trying to generate concrete names for non-generic 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 |
+
) -> 'bool | None'</dt><dd><span class="code">Try to rebuild the pydantic-core schema for the model.<br>
|
| 1055 |
+
<br>
|
| 1056 |
+
This may be necessary when one of the annotations is a ForwardRef which could not be resolved during<br>
|
| 1057 |
+
the initial attempt to build the schema, and automatic rebuilding fails.<br>
|
| 1058 |
+
<br>
|
| 1059 |
+
Args:<br>
|
| 1060 |
+
force: Whether to force the rebuilding of the model schema, defaults to `False`.<br>
|
| 1061 |
+
raise_errors: Whether to raise errors, defaults to `True`.<br>
|
| 1062 |
+
_parent_namespace_depth: The depth level of the parent namespace, defaults to 2.<br>
|
| 1063 |
+
_types_namespace: The types namespace, defaults to `None`.<br>
|
| 1064 |
+
<br>
|
| 1065 |
+
Returns:<br>
|
| 1066 |
+
Returns `None` if the schema is already "complete" and rebuilding was not required.<br>
|
| 1067 |
+
If rebuilding _was_ required, returns `True` if rebuilding was successful, otherwise `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 |
+
) -> 'Self'</dt><dd><span class="code">Validate a pydantic model instance.<br>
|
| 1079 |
+
<br>
|
| 1080 |
+
Args:<br>
|
| 1081 |
+
obj: The object to validate.<br>
|
| 1082 |
+
strict: Whether to enforce types strictly.<br>
|
| 1083 |
+
extra: Whether to ignore, allow, or forbid extra data during model validation.<br>
|
| 1084 |
+
See the [`extra` configuration value][pydantic.ConfigDict.extra] for details.<br>
|
| 1085 |
+
from_attributes: Whether to extract data from object attributes.<br>
|
| 1086 |
+
context: Additional context to pass to the validator.<br>
|
| 1087 |
+
by_alias: Whether to use the field's alias when validating against the provided input data.<br>
|
| 1088 |
+
by_name: Whether to use the field's name when validating against the provided input data.<br>
|
| 1089 |
+
<br>
|
| 1090 |
+
Raises:<br>
|
| 1091 |
+
ValidationError: If the object could not be validated.<br>
|
| 1092 |
+
<br>
|
| 1093 |
+
Returns:<br>
|
| 1094 |
+
The validated model 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 |
+
) -> 'Self'</dt><dd><span class="code">!!! abstract "Usage Documentation"<br>
|
| 1105 |
+
[JSON Parsing](../concepts/json.md#json-parsing)<br>
|
| 1106 |
+
<br>
|
| 1107 |
+
Validate the given JSON data against the Pydantic model.<br>
|
| 1108 |
+
<br>
|
| 1109 |
+
Args:<br>
|
| 1110 |
+
json_data: The JSON data to validate.<br>
|
| 1111 |
+
strict: Whether to enforce types strictly.<br>
|
| 1112 |
+
extra: Whether to ignore, allow, or forbid extra data during model validation.<br>
|
| 1113 |
+
See the [`extra` configuration value][pydantic.ConfigDict.extra] for details.<br>
|
| 1114 |
+
context: Extra variables to pass to the validator.<br>
|
| 1115 |
+
by_alias: Whether to use the field's alias when validating against the provided input data.<br>
|
| 1116 |
+
by_name: Whether to use the field's name when validating against the provided input data.<br>
|
| 1117 |
+
<br>
|
| 1118 |
+
Returns:<br>
|
| 1119 |
+
The validated Pydantic model.<br>
|
| 1120 |
+
<br>
|
| 1121 |
+
Raises:<br>
|
| 1122 |
+
ValidationError: If `json_data` is not a JSON string or the object could not be 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 |
+
) -> 'Self'</dt><dd><span class="code">Validate the given object with string data against the Pydantic model.<br>
|
| 1133 |
+
<br>
|
| 1134 |
+
Args:<br>
|
| 1135 |
+
obj: The object containing string data to validate.<br>
|
| 1136 |
+
strict: Whether to enforce types strictly.<br>
|
| 1137 |
+
extra: Whether to ignore, allow, or forbid extra data during model validation.<br>
|
| 1138 |
+
See the [`extra` configuration value][pydantic.ConfigDict.extra] for details.<br>
|
| 1139 |
+
context: Extra variables to pass to the validator.<br>
|
| 1140 |
+
by_alias: Whether to use the field's alias when validating against the provided input data.<br>
|
| 1141 |
+
by_name: Whether to use the field's name when validating against the provided input data.<br>
|
| 1142 |
+
<br>
|
| 1143 |
+
Returns:<br>
|
| 1144 |
+
The validated Pydantic 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 |
+
) -> 'Self'</dt></dl>
|
| 1154 |
+
|
| 1155 |
+
<dl><dt><a name="ModelResponse-parse_obj"><strong>parse_obj</strong></a>(obj: 'Any') -> '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 |
+
) -> 'Self'</dt></dl>
|
| 1165 |
+
|
| 1166 |
+
<dl><dt><a name="ModelResponse-schema"><strong>schema</strong></a>(by_alias: 'bool' = True, ref_template: 'str' = '#/$defs/{model}') -> '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 |
+
) -> 'str'</dt></dl>
|
| 1174 |
+
|
| 1175 |
+
<dl><dt><a name="ModelResponse-update_forward_refs"><strong>update_forward_refs</strong></a>(**localns: 'Any') -> 'None'</dt></dl>
|
| 1176 |
+
|
| 1177 |
+
<dl><dt><a name="ModelResponse-validate"><strong>validate</strong></a>(value: 'Any') -> '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 extra fields set during validation.<br>
|
| 1185 |
+
<br>
|
| 1186 |
+
Returns:<br>
|
| 1187 |
+
A dictionary of extra fields, or `None` if `config.extra` is not set to `"allow"`.</span></dd>
|
| 1188 |
+
</dl>
|
| 1189 |
+
<dl><dt><strong>model_fields_set</strong></dt>
|
| 1190 |
+
<dd><span class="code">Returns the set of fields that have been explicitly set on this model instance.<br>
|
| 1191 |
+
<br>
|
| 1192 |
+
Returns:<br>
|
| 1193 |
+
A set of strings representing the fields that have been set,<br>
|
| 1194 |
+
i.e. that were not filled from 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 for instance 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> <br><strong class="bigsection">Data</strong></td></tr>
|
| 1221 |
+
|
| 1222 |
+
<tr><td class="decor data-decor"><span class="code"> </span></td><td> </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"> <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> <br><strong class="bigsection">Modules</strong></td></tr>
|
| 17 |
+
|
| 18 |
+
<tr><td class="decor pkg-content-decor"><span class="code"> </span></td><td> </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> <br><strong class="bigsection">Classes</strong></td></tr>
|
| 26 |
+
|
| 27 |
+
<tr><td class="decor index-decor"><span class="code"> </span></td><td> </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> <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"> </span></td>
|
| 42 |
+
<td class="decor title-decor" colspan=2><span class="code"><a href="#TechNovaService">TechNovaService</a>() -&gt; 'None'<br>
|
| 43 |
+
<br>
|
| 44 |
+
<br> </span></td></tr>
|
| 45 |
+
<tr><td> </td>
|
| 46 |
+
<td class="singlecolumn">Methods defined here:<br>
|
| 47 |
+
<dl><dt><a name="TechNovaService-__init__"><strong>__init__</strong></a>(self) -> 'None'</dt><dd><span class="code">Initialize self. See help(type(self)) for accurate signature.</span></dd></dl>
|
| 48 |
+
|
| 49 |
+
<dl><dt><a name="TechNovaService-adapt_input"><strong>adapt_input</strong></a>(self, request: 'ModelRequest') -> 'pd.DataFrame'</dt><dd><span class="code"># ---------- Payload ----------</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') -> 'dict[str, Any] | None'</dt><dd><span class="code"># ---------- Clean fetch (isolé pour tests / mocks) ----------</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') -> 'ModelResponse'</dt><dd><span class="code"># ---------- Predict from clean ----------</span></dd></dl>
|
| 54 |
+
|
| 55 |
+
<dl><dt><a name="TechNovaService-predict_from_payload"><strong>predict_from_payload</strong></a>(self, request: 'ModelRequest') -> '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 for instance variables</span></dd>
|
| 61 |
+
</dl>
|
| 62 |
+
<dl><dt><strong>__weakref__</strong></dt>
|
| 63 |
+
<dd><span class="code">list of weak references to the 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)
|