"""Types d'entrée/sortie du solveur — suggestions, pas d'affectation automatique. Le solveur ne connaît que ces structures simples (jamais les modèles SQLModel), comme le moteur de validation dont il réutilise les briques (`app.validation.models`, `app.validation.engine.uncovered_minutes`). """ import datetime as dt from dataclasses import dataclass, field from app.validation.models import AvailabilityWindow, ShiftInfo @dataclass(frozen=True) class SolverShift: """Un créneau à compléter (`assigned < ideal_staff`).""" id: str position_id: str starts_at: dt.datetime ends_at: dt.datetime min_staff: int ideal_staff: int already_assigned: int # Créneau(x) — n'importe quel poste — dont la fin coïncide avec le début # de celui-ci, le même jour (vide s'il n'y en a pas) : sert au malus de # chaînage (voir `engine._score`), pour limiter les enchaînements de # shifts plutôt que cibler la rotation d'un poste en particulier. predecessor_shift_ids: frozenset[str] = frozenset() @property def shift_info(self) -> ShiftInfo: return ShiftInfo(id=self.id, starts_at=self.starts_at, ends_at=self.ends_at) @dataclass(frozen=True) class SolverVolunteer: id: str availability: tuple[AvailabilityWindow, ...] = () wished_position_ids: frozenset[str] = frozenset() # Passages des artistes que le bénévole veut voir (heure de passage), # sous forme de fenêtres datées : un créneau qui chevauche l'une d'elles # lui ferait manquer le passage. wished_artist_windows: tuple[ShiftInfo, ...] = () # Créneaux déjà occupés (affectations existantes, décisions figées) : # comptent pour chevauchement, repos, chaînage. existing_shifts: tuple[ShiftInfo, ...] = () # Total déjà affecté sur l'édition (minutes), pour répartir la charge. existing_minutes: int = 0 @dataclass(frozen=True) class SolverInput: shifts: tuple[SolverShift, ...] = field(default_factory=tuple) volunteers: tuple[SolverVolunteer, ...] = field(default_factory=tuple) @dataclass(frozen=True) class ShiftSuggestion: """Bénévoles suggérés pour un créneau, meilleur en tête.""" shift_id: str volunteer_ids: tuple[str, ...] @dataclass(frozen=True) class SolverSolution: suggestions: tuple[ShiftSuggestion, ...] = field(default_factory=tuple)