Files
AdamThomasandClaude Opus 5.5 0f904f5e3d
ci-cd / build-backend (push) Successful in 45s
ci-cd / build-frontend (push) Successful in 24s
ci-cd / deploy (push) Successful in 27s
Deux postes en même temps possibles, malus dans le solveur
Le chevauchement de créneaux (SHIFT_OVERLAP) n'est plus bloquant : il
remonte en avertissement et l'affectation est enregistrée. Le solveur ne
l'exclut plus mais applique un fort malus au bénévole déjà affecté
ailleurs au même moment (proposé seulement sous l'effectif minimal, faute
de mieux) ; il n'est jamais reproposé sur un créneau où il est déjà.
Même malus dans le classement par défaut du panneau et dans le mock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 10:38:52 +01:00

72 lines
1.9 KiB
Python

"""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>`)."""
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, ...] = ()