Alexis-Ravet commited on
Commit
b666236
·
verified ·
1 Parent(s): 0d468ed

Upload folder using huggingface_hub

Browse files
.dockerignore CHANGED
@@ -9,6 +9,7 @@ tests/
9
  *.md
10
  *.txt
11
  notebooks/
 
12
  .env
13
  *.csv
14
  .gitignore
 
9
  *.md
10
  *.txt
11
  notebooks/
12
+ data/
13
  .env
14
  *.csv
15
  .gitignore
README.md CHANGED
@@ -186,7 +186,7 @@ Response:
186
  - [x] CI/CD pipeline with GitLab
187
  - [x] Automated deployment to Hugging Face Spaces
188
  - [x] Test coverage reporting with pytest-cov and Cobertura
189
- - [ ] Add a database layer (Alembic + SQLAlchemy) for logging predictions
190
  - [ ] Add a Gradio or Streamlit user interface for non-technical users
191
  - [ ] Implement model versioning and A/B testing
192
 
 
186
  - [x] CI/CD pipeline with GitLab
187
  - [x] Automated deployment to Hugging Face Spaces
188
  - [x] Test coverage reporting with pytest-cov and Cobertura
189
+ - [x] Add a database layer (Alembic + SQLAlchemy) for logging predictions
190
  - [ ] Add a Gradio or Streamlit user interface for non-technical users
191
  - [ ] Implement model versioning and A/B testing
192
 
pyproject.toml CHANGED
@@ -12,6 +12,7 @@ dependencies = [
12
  "joblib==1.5.3",
13
  "numpy==2.3.5",
14
  "pandas==2.3.3",
 
15
  "pydantic==2.12.5",
16
  "scikit-learn==1.8.0",
17
  "sqlalchemy==2.0.46",
 
12
  "joblib==1.5.3",
13
  "numpy==2.3.5",
14
  "pandas==2.3.3",
15
+ "psycopg2-binary==2.9.12",
16
  "pydantic==2.12.5",
17
  "scikit-learn==1.8.0",
18
  "sqlalchemy==2.0.46",
src/api/main.py CHANGED
@@ -8,13 +8,25 @@ Contenu:
8
  - Point d'entrée de l'application
9
  """
10
 
 
 
11
  from fastapi import FastAPI, HTTPException
12
  from fastapi.responses import Response
13
 
14
  from src.api.schemas import DonneesEmploye, ResultatPrediction
 
 
 
 
 
15
  from src.utils.model_loader import get_modele
16
  from src.utils.transformer import transformer_donnees
17
 
 
 
 
 
 
18
  # APPLICATION FASTAPI
19
 
20
  tags_metadata = [
@@ -145,6 +157,22 @@ def predire(employe: DonneesEmploye):
145
  """
146
  donnees = employe.model_dump(exclude_none=True)
147
  resultat = predire_churn(donnees)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  return ResultatPrediction(**resultat)
149
 
150
 
 
8
  - Point d'entrée de l'application
9
  """
10
 
11
+ import logging
12
+
13
  from fastapi import FastAPI, HTTPException
14
  from fastapi.responses import Response
15
 
16
  from src.api.schemas import DonneesEmploye, ResultatPrediction
17
+ from src.db.logger_db import (
18
+ log_api_operation,
19
+ log_prediction_input,
20
+ log_prediction_output,
21
+ )
22
  from src.utils.model_loader import get_modele
23
  from src.utils.transformer import transformer_donnees
24
 
25
+ # CONFIGURATION DU LOGGER
26
+ logging.basicConfig(level=logging.INFO)
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
  # APPLICATION FASTAPI
31
 
32
  tags_metadata = [
 
157
  """
158
  donnees = employe.model_dump(exclude_none=True)
159
  resultat = predire_churn(donnees)
160
+
161
+ # Logging dans la base de données (optionnel, ne bloque pas l'API)
162
+ input_id = log_prediction_input(donnees)
163
+ log_prediction_output(
164
+ input_id=input_id,
165
+ prediction=resultat["prediction"],
166
+ probabilite=resultat["probabilite"],
167
+ classe=resultat["classe"],
168
+ )
169
+ log_api_operation(
170
+ operation="PREDICT",
171
+ table_cible="prediction_inputs",
172
+ details=f"id_employee={donnees.get('id_employee')}, prediction={resultat['prediction']}",
173
+ statut="SUCCESS",
174
+ )
175
+
176
  return ResultatPrediction(**resultat)
177
 
178
 
src/api/schemas.py CHANGED
@@ -21,13 +21,15 @@ class DonneesEmploye(BaseModel):
21
 
22
  # INFORMATIONS PERSONNELLES
23
 
 
 
24
  age: int = Field(
25
  ..., ge=18, le=65, description="Âge de l'employé (entre 18 et 65 ans)"
26
  )
27
 
28
  genre: Literal["M", "F"] = Field(..., description="Genre: M=Homme, F=Femme")
29
 
30
- revenu_mensuel: float = Field(
31
  ..., gt=0, lt=20000, description="Salaire mensuel en euros"
32
  )
33
 
 
21
 
22
  # INFORMATIONS PERSONNELLES
23
 
24
+ id_employee: int = Field(..., ge=1, description="ID unique de l'employé")
25
+
26
  age: int = Field(
27
  ..., ge=18, le=65, description="Âge de l'employé (entre 18 et 65 ans)"
28
  )
29
 
30
  genre: Literal["M", "F"] = Field(..., description="Genre: M=Homme, F=Femme")
31
 
32
+ revenu_mensuel: int = Field(
33
  ..., gt=0, lt=20000, description="Salaire mensuel en euros"
34
  )
35
 
src/db/__init__.py ADDED
File without changes
src/db/create_db.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Script de création de la base de données PostgreSQL et d'insertion du dataset.
3
+ """
4
+
5
+ from pathlib import Path
6
+
7
+ import pandas as pd
8
+ from sqlalchemy.exc import IntegrityError
9
+
10
+ from src.db.database import SessionLocal, engine
11
+ from src.db.models import Base, Employee
12
+
13
+
14
+ def racine_projet() -> Path:
15
+ """Remonte l'arborescence jusqu'à trouver la racine du projet."""
16
+ courant = Path(__file__).resolve()
17
+ for parent in courant.parents:
18
+ if (parent / "pyproject.toml").exists():
19
+ return parent
20
+ raise FileNotFoundError("Racine du projet non trouvée")
21
+
22
+
23
+ PROJECT_ROOT = racine_projet()
24
+ RAW_DIR = PROJECT_ROOT / "data" / "raw"
25
+ PROCESSED_DIR = PROJECT_ROOT / "data" / "processed"
26
+
27
+ CSV_SIRH = RAW_DIR / "extrait_sirh.csv"
28
+ CSV_EVAL = RAW_DIR / "extrait_eval.csv"
29
+ CSV_SONDAGE = RAW_DIR / "extrait_sondage.csv"
30
+ CSV_EMPLOYES = PROCESSED_DIR / "employees.csv"
31
+
32
+ COLONNES_EMPLOYEES = [
33
+ "id_employee",
34
+ "age",
35
+ "genre",
36
+ "revenu_mensuel",
37
+ "statut_marital",
38
+ "departement",
39
+ "poste",
40
+ "annee_experience_totale",
41
+ "annees_dans_l_entreprise",
42
+ "satisfaction_employee_environnement",
43
+ "note_evaluation_precedente",
44
+ "satisfaction_employee_nature_travail",
45
+ "satisfaction_employee_equipe",
46
+ "satisfaction_employee_equilibre_pro_perso",
47
+ "note_evaluation_actuelle",
48
+ "heure_supplementaires",
49
+ "augementation_salaire_precedente",
50
+ "nombre_participation_pee",
51
+ "nb_formations_suivies",
52
+ "distance_domicile_travail",
53
+ "niveau_education",
54
+ "frequence_deplacement",
55
+ "annees_depuis_la_derniere_promotion",
56
+ "a_quitte_l_entreprise",
57
+ ]
58
+
59
+
60
+ def fusionner_csv() -> pd.DataFrame:
61
+ """
62
+ Charge les 3 CSV et les fusionne sur la colonne id_employee.
63
+
64
+ Reproduit les étapes du notebook :
65
+ 1. Renomme eval_number en id_employee dans df_eval
66
+ 2. Retire le préfixe 'E_' et convertit en int
67
+ 3. Renomme code_sondage en id_employee dans df_sondage
68
+ 4. Inner merge des 3 DataFrames
69
+
70
+ Returns:
71
+ DataFrame fusionné (32 colonnes, 1470 lignes)
72
+ """
73
+ df_sirh = pd.read_csv(CSV_SIRH)
74
+ df_eval = pd.read_csv(CSV_EVAL)
75
+ df_sondage = pd.read_csv(CSV_SONDAGE)
76
+
77
+ df_eval = df_eval.rename(columns={"eval_number": "id_employee"})
78
+ df_eval["id_employee"] = df_eval["id_employee"].str[2:].astype("int64")
79
+
80
+ df_sondage = df_sondage.rename(columns={"code_sondage": "id_employee"})
81
+
82
+ df_central = pd.merge(df_sirh, df_eval, on="id_employee", how="inner")
83
+ df_central = pd.merge(df_central, df_sondage, on="id_employee", how="inner")
84
+
85
+ print(
86
+ f"DataFrame fusionné : {df_central.shape[0]} lignes, {df_central.shape[1]} colonnes"
87
+ )
88
+ return df_central
89
+
90
+
91
+ def nettoyer_dataframe(df_central: pd.DataFrame) -> pd.DataFrame:
92
+ """
93
+ Nettoie le DataFrame fusionné pour ne garder que les colonnes utilisées par le modèle.
94
+
95
+ Étapes :
96
+ 1. Retire le '%' de augementation_salaire_precedente et convertit en int
97
+ 2. Sélectionne uniquement les 23 colonnes du modèle
98
+
99
+ Args:
100
+ df_central: DataFrame fusionné (32 colonnes)
101
+
102
+ Returns:
103
+ DataFrame nettoyé (23 colonnes)
104
+ """
105
+ df_central["augementation_salaire_precedente"] = (
106
+ df_central["augementation_salaire_precedente"].str[:-2].astype("int64")
107
+ )
108
+
109
+ df_employees = df_central.loc[:, COLONNES_EMPLOYEES].copy()
110
+
111
+ print(
112
+ f"DataFrame nettoyé : {df_employees.shape[0]} lignes, {df_employees.shape[1]} colonnes"
113
+ )
114
+ return df_employees
115
+
116
+
117
+ def sauvegarder_csv(df_employees: pd.DataFrame) -> None:
118
+ """
119
+ Sauvegarde le DataFrame nettoyé au format CSV.
120
+
121
+ Args:
122
+ df_employees: DataFrame nettoyé (23 colonnes)
123
+ """
124
+ PROCESSED_DIR.mkdir(parents=True, exist_ok=True)
125
+ df_employees.to_csv(CSV_EMPLOYES, index=False)
126
+ print(f"CSV sauvegardé : {CSV_EMPLOYES}")
127
+
128
+
129
+ def creer_tables() -> None:
130
+ """Crée les tables dans PostgreSQL à partir des modèles ORM."""
131
+ Base.metadata.create_all(engine)
132
+ print("Tables créées avec succès")
133
+
134
+
135
+ def inserer_donnees(df_employees: pd.DataFrame) -> None:
136
+ """
137
+ Insère les données du DataFrame dans la table employees.
138
+
139
+ Args:
140
+ df_employees: DataFrame nettoyé (23 colonnes, 1470 lignes)
141
+ """
142
+ session = SessionLocal()
143
+ try:
144
+ employees = [Employee(**row.to_dict()) for _, row in df_employees.iterrows()]
145
+ session.add_all(employees)
146
+ session.commit()
147
+ print(f"{len(employees)} employés insérés avec succès")
148
+ except IntegrityError as e:
149
+ session.rollback()
150
+ print(f"Erreur d'intégrité : {e}")
151
+ print("Les données existent peut-être déjà. Vider la table avant de réinsérer.")
152
+ except Exception as e:
153
+ session.rollback()
154
+ print(f"Erreur lors de l'insertion : {e}")
155
+ finally:
156
+ session.close()
157
+
158
+
159
+ def main() -> None:
160
+ """Point d'entrée principal du script."""
161
+
162
+ # 1. Chargement et fusion des CSV
163
+ df_central = fusionner_csv()
164
+
165
+ # 2. Nettoyage du DataFrame"
166
+ df_employees = nettoyer_dataframe(df_central)
167
+
168
+ # 3. Sauvegarde du CSV nettoyé
169
+ sauvegarder_csv(df_employees)
170
+
171
+ # 4. Création des tables
172
+ creer_tables()
173
+
174
+ # 5. Insertion des données
175
+ inserer_donnees(df_employees)
176
+
177
+ print("Base de données créée avec succès")
178
+
179
+
180
+ if __name__ == "__main__":
181
+ main()
src/db/database.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration de la connexion à la base de données PostgreSQL.
3
+ Utilise SQLAlchemy comme ORM et lit l'URL depuis le fichier .env.
4
+
5
+ Si DATABASE_URL n'est pas définie (ex: conteneur Docker sans PostgreSQL),
6
+ le module ne génère pas d'erreur. Les fonctions de logging détecteront
7
+ l'absence de DB et fonctionneront en mode dégradé.
8
+ """
9
+
10
+ import logging
11
+ import os
12
+
13
+ from dotenv import load_dotenv
14
+ from sqlalchemy import create_engine
15
+ from sqlalchemy.orm import DeclarativeBase, sessionmaker
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Charge les variables d'environnement depuis le fichier .env
20
+ load_dotenv()
21
+
22
+ # URL de connexion à la base de données
23
+ DATABASE_URL = os.getenv("DATABASE_URL")
24
+
25
+ if DATABASE_URL is None:
26
+ logger.warning(
27
+ "DATABASE_URL n'est pas définie. "
28
+ "La base de données ne sera pas disponible. "
29
+ "L'API fonctionnera sans logging en base de données."
30
+ )
31
+ engine = None
32
+ SessionLocal = None
33
+ else:
34
+ # Engine SQLAlchemy = connexion vers PostgreSQL
35
+ engine = create_engine(DATABASE_URL)
36
+ # SessionLocal = fabrique de sessions (une session = une transaction DB)
37
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
38
+
39
+
40
+ # Base = classe de base pour tous les modèles ORM
41
+ class Base(DeclarativeBase):
42
+ pass
43
+
44
+
45
+ def get_db():
46
+ """
47
+ Générateur qui fournit une session de base de données.
48
+
49
+ Yields:
50
+ Session SQLAlchemy ou None si la DB n'est pas configurée
51
+ """
52
+ if SessionLocal is None:
53
+ yield None
54
+ return
55
+
56
+ db = SessionLocal()
57
+ try:
58
+ yield db
59
+ finally:
60
+ db.close()
src/db/logger_db.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module de logging des prédictions dans la base de données PostgreSQL.
3
+
4
+ Si la base de données n'est pas disponible (ex: conteneur Docker sans PostgreSQL),
5
+ les fonctions ne lèvent pas d'erreur et laissent un avertissement dans les logs.
6
+ """
7
+
8
+ import logging
9
+ import os
10
+
11
+ from dotenv import load_dotenv
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Vérifie si la DB est configurée
16
+ load_dotenv()
17
+ DATABASE_URL = os.getenv("DATABASE_URL")
18
+ DB_AVAILABLE = DATABASE_URL is not None
19
+
20
+
21
+ def _get_session():
22
+ """
23
+ Ouvre une session SQLAlchemy si la DB est disponible.
24
+
25
+ Returns:
26
+ Session SQLAlchemy ou None si la DB n'est pas configurée
27
+ """
28
+ if not DB_AVAILABLE:
29
+ return None
30
+
31
+ try:
32
+ from src.db.database import SessionLocal
33
+
34
+ return SessionLocal()
35
+ except Exception as e:
36
+ logger.warning(f"Impossible de se connecter à la base de données : {e}")
37
+ return None
38
+
39
+
40
+ def log_prediction_input(donnees: dict) -> int | None:
41
+ """
42
+ Enregistre les données brutes d'un employé dans prediction_inputs.
43
+
44
+ Args:
45
+ donnees: Dict avec les données de l'employé (validées par Pydantic)
46
+
47
+ Returns:
48
+ ID de l'input inséré, ou None si la DB n'est pas disponible
49
+ """
50
+ if not DB_AVAILABLE:
51
+ logger.info("DB non configurée : log_prediction_input ignoré")
52
+ return None
53
+
54
+ try:
55
+ from src.db.models import PredictionInput
56
+ except Exception:
57
+ logger.warning("Impossible d'importer les modèles DB")
58
+ return None
59
+
60
+ session = _get_session()
61
+ if session is None:
62
+ return None
63
+
64
+ try:
65
+ prediction_input = PredictionInput(**donnees)
66
+ session.add(prediction_input)
67
+ session.commit()
68
+ session.refresh(prediction_input)
69
+ input_id: int = prediction_input.id # type: ignore[assignment]
70
+ logger.info(f"Input inséré en DB (id={input_id})")
71
+ return input_id
72
+ except Exception as e:
73
+ session.rollback()
74
+ logger.warning(f"Erreur lors de l'insertion input : {e}")
75
+ return None
76
+ finally:
77
+ session.close()
78
+
79
+
80
+ def log_prediction_output(
81
+ input_id: int | None, prediction: str, probabilite: float, classe: int
82
+ ) -> bool:
83
+ """
84
+ Enregistre le résultat d'une prédiction dans prediction_outputs.
85
+
86
+ Args:
87
+ input_id: ID de l'input dans prediction_inputs
88
+ prediction: "Oui" ou "Non"
89
+ probabilite: Probabilité de départ (entre 0 et 1)
90
+ classe: 0 ou 1
91
+
92
+ Returns:
93
+ True si l'insertion a réussi, False sinon
94
+ """
95
+ if not DB_AVAILABLE or input_id is None:
96
+ logger.info(
97
+ "DB non configurée ou input_id manquant : log_prediction_output ignoré"
98
+ )
99
+ return False
100
+
101
+ try:
102
+ from src.db.models import PredictionOutput
103
+ except Exception:
104
+ logger.warning("Impossible d'importer les modèles DB")
105
+ return False
106
+
107
+ session = _get_session()
108
+ if session is None:
109
+ return False
110
+
111
+ try:
112
+ prediction_output = PredictionOutput(
113
+ input_id=input_id,
114
+ prediction=prediction,
115
+ probabilite=probabilite,
116
+ classe=classe,
117
+ )
118
+ session.add(prediction_output)
119
+ session.commit()
120
+ logger.info(f"Output inséré en DB (input_id={input_id})")
121
+ return True
122
+ except Exception as e:
123
+ session.rollback()
124
+ logger.warning(f"Erreur lors de l'insertion output : {e}")
125
+ return False
126
+ finally:
127
+ session.close()
128
+
129
+
130
+ def log_api_operation(
131
+ operation: str,
132
+ table_cible: str,
133
+ details: str | None = None,
134
+ statut: str = "SUCCESS",
135
+ ) -> bool:
136
+ """
137
+ Enregistre une opération API dans api_logs.
138
+
139
+ Args:
140
+ operation: Type d'opération (ex: "INSERT", "SELECT")
141
+ table_cible: Table concernée (ex: "prediction_inputs")
142
+ details: Détails optionnels de l'opération
143
+ statut: "SUCCESS" ou "ERROR"
144
+
145
+ Returns:
146
+ True si l'insertion a réussi, False sinon
147
+ """
148
+ if not DB_AVAILABLE:
149
+ logger.info(
150
+ f"DB non configurée : log_api_operation ignoré ({operation} {table_cible})"
151
+ )
152
+ return False
153
+
154
+ try:
155
+ from src.db.models import ApiLog
156
+ except Exception:
157
+ logger.warning("Impossible d'importer les modèles DB")
158
+ return False
159
+
160
+ session = _get_session()
161
+ if session is None:
162
+ return False
163
+
164
+ try:
165
+ api_log = ApiLog(
166
+ operation=operation,
167
+ table_cible=table_cible,
168
+ details=details,
169
+ statut=statut,
170
+ )
171
+ session.add(api_log)
172
+ session.commit()
173
+ logger.info(f"Log API inséré : {operation} {table_cible} ({statut})")
174
+ return True
175
+ except Exception as e:
176
+ session.rollback()
177
+ logger.warning(f"Erreur lors de l'insertion log API : {e}")
178
+ return False
179
+ finally:
180
+ session.close()
src/db/models.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Modèles ORM SQLAlchemy pour la base de données PostgreSQL.
3
+
4
+ Contient les 4 tables du schéma :
5
+ - employees : dataset brut d'entraînement (1470 lignes)
6
+ - prediction_inputs : données brutes envoyées à l'API
7
+ - prediction_outputs : résultat de la prédiction
8
+ - api_logs : traçabilité des échanges API ↔ DB
9
+ """
10
+
11
+ from datetime import datetime
12
+
13
+ from sqlalchemy import Column, DateTime, Float, ForeignKey, Integer, String
14
+ from sqlalchemy.orm import relationship
15
+
16
+ from src.db.database import Base
17
+
18
+
19
+ class Employee(Base):
20
+ """Table du dataset brut (1470 employés)."""
21
+
22
+ __tablename__ = "employees"
23
+
24
+ id_employee = Column(Integer, primary_key=True)
25
+ age = Column(Integer, nullable=False)
26
+ genre = Column(String(1), nullable=False)
27
+ revenu_mensuel = Column(Integer, nullable=False)
28
+ statut_marital = Column(String(20), nullable=False)
29
+ departement = Column(String(20), nullable=False)
30
+ poste = Column(String(30), nullable=False)
31
+ annee_experience_totale = Column(Integer, nullable=False)
32
+ annees_dans_l_entreprise = Column(Integer, nullable=False)
33
+ satisfaction_employee_environnement = Column(Integer, nullable=False)
34
+ note_evaluation_precedente = Column(Integer, nullable=False)
35
+ satisfaction_employee_nature_travail = Column(Integer, nullable=False)
36
+ satisfaction_employee_equipe = Column(Integer, nullable=False)
37
+ satisfaction_employee_equilibre_pro_perso = Column(Integer, nullable=False)
38
+ note_evaluation_actuelle = Column(Integer, nullable=False)
39
+ heure_supplementaires = Column(String(5), nullable=False)
40
+ augementation_salaire_precedente = Column(Integer, nullable=False)
41
+ nombre_participation_pee = Column(Integer, nullable=False)
42
+ nb_formations_suivies = Column(Integer, nullable=False)
43
+ distance_domicile_travail = Column(Integer, nullable=False)
44
+ niveau_education = Column(Integer, nullable=False)
45
+ frequence_deplacement = Column(String(20), nullable=False)
46
+ annees_depuis_la_derniere_promotion = Column(Integer, nullable=False)
47
+ a_quitte_l_entreprise = Column(String(5), nullable=False)
48
+
49
+
50
+ class PredictionInput(Base):
51
+ """Données brutes envoyées à l'API (avant transformation)."""
52
+
53
+ __tablename__ = "prediction_inputs"
54
+
55
+ id = Column(Integer, primary_key=True, autoincrement=True)
56
+ id_employee = Column(Integer, nullable=False)
57
+ created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
58
+ age = Column(Integer, nullable=False)
59
+ genre = Column(String(1), nullable=False)
60
+ revenu_mensuel = Column(Integer, nullable=False)
61
+ statut_marital = Column(String(20), nullable=False)
62
+ departement = Column(String(20), nullable=False)
63
+ poste = Column(String(30), nullable=False)
64
+ annee_experience_totale = Column(Integer, nullable=False)
65
+ annees_dans_l_entreprise = Column(Integer, nullable=False)
66
+ satisfaction_employee_environnement = Column(Integer, nullable=False)
67
+ note_evaluation_precedente = Column(Integer, nullable=False)
68
+ satisfaction_employee_nature_travail = Column(Integer, nullable=False)
69
+ satisfaction_employee_equipe = Column(Integer, nullable=False)
70
+ satisfaction_employee_equilibre_pro_perso = Column(Integer, nullable=False)
71
+ note_evaluation_actuelle = Column(Integer, nullable=False)
72
+ heure_supplementaires = Column(String(5), nullable=False)
73
+ augementation_salaire_precedente = Column(Integer, nullable=False)
74
+ nombre_participation_pee = Column(Integer, nullable=False)
75
+ nb_formations_suivies = Column(Integer, nullable=False)
76
+ distance_domicile_travail = Column(Integer, nullable=False)
77
+ niveau_education = Column(Integer, nullable=False)
78
+ frequence_deplacement = Column(String(20), nullable=False)
79
+ annees_depuis_la_derniere_promotion = Column(Integer, nullable=False)
80
+
81
+ output = relationship("PredictionOutput", back_populates="input", uselist=False)
82
+
83
+
84
+ class PredictionOutput(Base):
85
+ """Résultat de la prédiction du modèle."""
86
+
87
+ __tablename__ = "prediction_outputs"
88
+
89
+ id = Column(Integer, primary_key=True, autoincrement=True)
90
+ input_id = Column(
91
+ Integer, ForeignKey("prediction_inputs.id"), nullable=False, unique=True
92
+ )
93
+ prediction = Column(String(5), nullable=False)
94
+ probabilite = Column(Float, nullable=False)
95
+ classe = Column(Integer, nullable=False)
96
+
97
+ input = relationship("PredictionInput", back_populates="output")
98
+
99
+
100
+ class ApiLog(Base):
101
+ """Traçabilité des échanges entre l'API et la base de données."""
102
+
103
+ __tablename__ = "api_logs"
104
+
105
+ id = Column(Integer, primary_key=True, autoincrement=True)
106
+ created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
107
+ operation = Column(String(30), nullable=False)
108
+ table_cible = Column(String(20), nullable=False)
109
+ details = Column(String(255), nullable=True)
110
+ statut = Column(String(10), nullable=False)
src/db/schema.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```mermaid
2
+ erDiagram
3
+ employees {
4
+ int id_employee PK
5
+ int age
6
+ varchar genre
7
+ int revenu_mensuel
8
+ varchar statut_marital
9
+ varchar departement
10
+ varchar poste
11
+ int annee_experience_totale
12
+ int annees_dans_l_entreprise
13
+ int satisfaction_employee_environnement
14
+ int note_evaluation_precedente
15
+ int satisfaction_employee_nature_travail
16
+ int satisfaction_employee_equipe
17
+ int satisfaction_employee_equilibre_pro_perso
18
+ int note_evaluation_actuelle
19
+ varchar heure_supplementaires
20
+ int augementation_salaire_precedente
21
+ int nombre_participation_pee
22
+ int nb_formations_suivies
23
+ int distance_domicile_travail
24
+ int niveau_education
25
+ varchar frequence_deplacement
26
+ int annees_depuis_la_derniere_promotion
27
+ varchar a_quitte_l_entreprise
28
+ }
29
+
30
+ prediction_inputs {
31
+ int id PK
32
+ timestamp created_at
33
+ int id_employee
34
+ int age
35
+ varchar genre
36
+ int revenu_mensuel
37
+ varchar statut_marital
38
+ varchar departement
39
+ varchar poste
40
+ int annee_experience_totale
41
+ int annees_dans_l_entreprise
42
+ int satisfaction_employee_environnement
43
+ int note_evaluation_precedente
44
+ int satisfaction_employee_nature_travail
45
+ int satisfaction_employee_equipe
46
+ int satisfaction_employee_equilibre_pro_perso
47
+ int note_evaluation_actuelle
48
+ varchar heure_supplementaires
49
+ int augementation_salaire_precedente
50
+ int nombre_participation_pee
51
+ int nb_formations_suivies
52
+ int distance_domicile_travail
53
+ int niveau_education
54
+ varchar frequence_deplacement
55
+ int annees_depuis_la_derniere_promotion
56
+ }
57
+
58
+ prediction_inputs ||--|| prediction_outputs : "1:1"
59
+
60
+ prediction_outputs {
61
+ int id PK
62
+ int input_id FK
63
+ varchar prediction
64
+ float probabilite
65
+ int classe
66
+ }
67
+
68
+ api_logs {
69
+ int id PK
70
+ timestamp created_at
71
+ varchar operation
72
+ varchar table_cible
73
+ varchar details
74
+ varchar statut
75
+ }
76
+ ```
src/utils/transformer.py CHANGED
@@ -92,6 +92,9 @@ def transformer_donnees(donnees_employe: dict) -> pd.DataFrame:
92
  # Créer un DataFrame pandas
93
  df = pd.DataFrame([donnees_employe])
94
 
 
 
 
95
  # ÉTAPE 1: CALCULS PRÉLIMINAIRES
96
  # Ces calculs utilisent les colonnes brutes avant suppression
97
 
 
92
  # Créer un DataFrame pandas
93
  df = pd.DataFrame([donnees_employe])
94
 
95
+ # Supprimer l'ID avant toute transformation (non utilisé par le modèle)
96
+ df = df.drop(columns=["id_employee"], errors="ignore")
97
+
98
  # ÉTAPE 1: CALCULS PRÉLIMINAIRES
99
  # Ces calculs utilisent les colonnes brutes avant suppression
100
 
uv.lock CHANGED
@@ -2012,6 +2012,7 @@ dependencies = [
2012
  { name = "joblib" },
2013
  { name = "numpy" },
2014
  { name = "pandas" },
 
2015
  { name = "pydantic" },
2016
  { name = "scikit-learn" },
2017
  { name = "sqlalchemy" },
@@ -2041,6 +2042,7 @@ requires-dist = [
2041
  { name = "joblib", specifier = "==1.5.3" },
2042
  { name = "numpy", specifier = "==2.3.5" },
2043
  { name = "pandas", specifier = "==2.3.3" },
 
2044
  { name = "pydantic", specifier = "==2.12.5" },
2045
  { name = "scikit-learn", specifier = "==1.8.0" },
2046
  { name = "sqlalchemy", specifier = "==2.0.46" },
@@ -2308,6 +2310,47 @@ wheels = [
2308
  { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
2309
  ]
2310
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2311
  [[package]]
2312
  name = "ptyprocess"
2313
  version = "0.7.0"
 
2012
  { name = "joblib" },
2013
  { name = "numpy" },
2014
  { name = "pandas" },
2015
+ { name = "psycopg2-binary" },
2016
  { name = "pydantic" },
2017
  { name = "scikit-learn" },
2018
  { name = "sqlalchemy" },
 
2042
  { name = "joblib", specifier = "==1.5.3" },
2043
  { name = "numpy", specifier = "==2.3.5" },
2044
  { name = "pandas", specifier = "==2.3.3" },
2045
+ { name = "psycopg2-binary", specifier = "==2.9.12" },
2046
  { name = "pydantic", specifier = "==2.12.5" },
2047
  { name = "scikit-learn", specifier = "==1.8.0" },
2048
  { name = "sqlalchemy", specifier = "==2.0.46" },
 
2310
  { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
2311
  ]
2312
 
2313
+ [[package]]
2314
+ name = "psycopg2-binary"
2315
+ version = "2.9.12"
2316
+ source = { registry = "https://pypi.org/simple" }
2317
+ sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" }
2318
+ wheels = [
2319
+ { url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" },
2320
+ { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" },
2321
+ { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" },
2322
+ { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" },
2323
+ { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" },
2324
+ { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" },
2325
+ { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" },
2326
+ { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" },
2327
+ { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" },
2328
+ { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" },
2329
+ { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" },
2330
+ { url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" },
2331
+ { url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" },
2332
+ { url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" },
2333
+ { url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" },
2334
+ { url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" },
2335
+ { url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" },
2336
+ { url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" },
2337
+ { url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" },
2338
+ { url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" },
2339
+ { url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" },
2340
+ { url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" },
2341
+ { url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" },
2342
+ { url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" },
2343
+ { url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" },
2344
+ { url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" },
2345
+ { url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" },
2346
+ { url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" },
2347
+ { url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" },
2348
+ { url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" },
2349
+ { url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" },
2350
+ { url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" },
2351
+ { url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" },
2352
+ ]
2353
+
2354
  [[package]]
2355
  name = "ptyprocess"
2356
  version = "0.7.0"