"""Types d'entrée et de sortie du moteur de validation. Découplé de la base : le moteur ne connaît que ces structures simples, jamais les modèles SQLModel. La couche appelante construit l'entrée depuis la base. """ import datetime as dt from dataclasses import dataclass from enum import StrEnum from pydantic import BaseModel, Field class Severity(StrEnum): BLOCKING = "BLOCKING" WARNING = "WARNING" class ViolationCode(StrEnum): SHIFT_OVERLAP = "SHIFT_OVERLAP" SEVERITY_BY_CODE: dict[ViolationCode, Severity] = { # Avertissement seulement : un bénévole peut tenir deux postes à la fois # (le solveur l'évite par un malus, cf. `app.solver.engine`). ViolationCode.SHIFT_OVERLAP: Severity.WARNING, } class Violation(BaseModel): """Une violation : un **code** + des paramètres structurés, jamais de phrase (traduction via `locales/fr.json`, clés `validation.`).""" code: ViolationCode severity: Severity params: dict[str, str | int] = Field(default_factory=dict) @classmethod def of(cls, code: ViolationCode, **params: str | int) -> "Violation": return cls(code=code, severity=SEVERITY_BY_CODE[code], params=params) @dataclass(frozen=True) class ShiftInfo: id: str starts_at: dt.datetime ends_at: dt.datetime @dataclass(frozen=True) class DayPartWindow: """Créneau type. Ne sert plus qu'à fixer la borne de « journée de festival » (`engine.day_boundary`).""" code: str start_time: dt.time # end_time <= start_time ⇒ la demi-journée passe minuit. end_time: dt.time order: int @dataclass(frozen=True) class ValidationInput: """Tout ce qu'il faut pour valider une affectation candidate. `existing_shifts` = les créneaux des autres affectations du même bénévole (une affectation = une décision, il n'y a pas d'étape de confirmation). """ candidate_shift: ShiftInfo existing_shifts: tuple[ShiftInfo, ...] = ()