✨ Changements majeurs: - Suppression complète du code Flask legacy - Migration backend FastAPI vers racine /backend - Migration frontend Vue.js vers racine /frontend - Suppression de notytex-v2/ (code monté à la racine) ✅ Validations: - Backend démarre correctement (port 8000) - API /api/v2/health répond healthy - 99/99 tests unitaires passent - Frontend configuré avec proxy Vite 📝 Documentation: - README.md réécrit pour v2 - Instructions de démarrage mises à jour - .gitignore adapté pour backend/frontend/ 🎯 Architecture finale: notytex/ ├── backend/ # FastAPI + SQLAlchemy + Pydantic ├── frontend/ # Vue 3 + Vite + TailwindCSS ├── docs/ # Documentation └── school_management.db # Base de données (inchangée) Jalon 6 complété: Application v2 prête pour utilisation!
111 lines
2.6 KiB
Python
111 lines
2.6 KiB
Python
"""
|
|
Value Objects pour les statistiques.
|
|
"""
|
|
from dataclasses import dataclass
|
|
from typing import List
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class StatisticsResult:
|
|
"""
|
|
Résultat des calculs statistiques.
|
|
|
|
Attributes:
|
|
count: Nombre de valeurs
|
|
mean: Moyenne
|
|
median: Médiane
|
|
min: Minimum
|
|
max: Maximum
|
|
std_dev: Écart-type
|
|
"""
|
|
count: int
|
|
mean: float
|
|
median: float
|
|
min: float
|
|
max: float
|
|
std_dev: float
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convertit en dictionnaire pour la sérialisation."""
|
|
return {
|
|
"count": self.count,
|
|
"mean": self.mean,
|
|
"median": self.median,
|
|
"min": self.min,
|
|
"max": self.max,
|
|
"std_dev": self.std_dev
|
|
}
|
|
|
|
@classmethod
|
|
def empty(cls) -> "StatisticsResult":
|
|
"""Crée un résultat vide."""
|
|
return cls(
|
|
count=0,
|
|
mean=0.0,
|
|
median=0.0,
|
|
min=0.0,
|
|
max=0.0,
|
|
std_dev=0.0
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class HistogramBin:
|
|
"""
|
|
Un bin d'histogramme.
|
|
|
|
Attributes:
|
|
range_start: Début de l'intervalle
|
|
range_end: Fin de l'intervalle
|
|
count: Nombre de valeurs dans l'intervalle
|
|
"""
|
|
range_start: float
|
|
range_end: float
|
|
count: int
|
|
|
|
@property
|
|
def label(self) -> str:
|
|
"""Génère le label pour l'affichage."""
|
|
if self.range_end == float('inf'):
|
|
return f"{int(self.range_start)}+"
|
|
return f"{int(self.range_start)}-{int(self.range_end)}"
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convertit en dictionnaire pour la sérialisation."""
|
|
return {
|
|
"range": self.label,
|
|
"count": self.count
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class HistogramData:
|
|
"""
|
|
Données d'histogramme.
|
|
|
|
Attributes:
|
|
bins: Liste des bins
|
|
max_count: Nombre maximum dans un bin (pour l'affichage)
|
|
"""
|
|
bins: List[HistogramBin]
|
|
|
|
@property
|
|
def max_count(self) -> int:
|
|
"""Retourne le count maximum parmi tous les bins."""
|
|
if not self.bins:
|
|
return 0
|
|
return max(bin.count for bin in self.bins)
|
|
|
|
@property
|
|
def total_count(self) -> int:
|
|
"""Retourne le nombre total d'éléments."""
|
|
return sum(bin.count for bin in self.bins)
|
|
|
|
def to_list(self) -> List[int]:
|
|
"""Convertit en liste simple de counts (pour Chart.js)."""
|
|
return [bin.count for bin in self.bins]
|
|
|
|
def to_dict_list(self) -> List[dict]:
|
|
"""Convertit en liste de dictionnaires."""
|
|
return [bin.to_dict() for bin in self.bins]
|