Source code for fhops.scheduling.timeline.models

"""Shift-level scheduling primitives."""

from __future__ import annotations

from collections.abc import Iterable

from pydantic import BaseModel, ValidationInfo, field_validator

__all__ = ["ShiftDefinition", "BlackoutWindow", "TimelineConfig"]


[docs] class ShiftDefinition(BaseModel): """Defines a shift length and count for a machine/job context.""" name: str hours: float shifts_per_day: int @field_validator("hours") @classmethod def _hours_positive(cls, value: float) -> float: if value <= 0: raise ValueError("hours must be positive") return value @field_validator("shifts_per_day") @classmethod def _shifts_positive(cls, value: int) -> int: if value <= 0: raise ValueError("shifts_per_day must be positive") return value
[docs] def daily_hours(self) -> float: return self.hours * self.shifts_per_day
[docs] class BlackoutWindow(BaseModel): """Represents a continuous period where work is disallowed.""" start_day: int end_day: int reason: str | None = None @field_validator("end_day") @classmethod def _end_not_before_start(cls, value: int, info: ValidationInfo) -> int: start_day = info.data.get("start_day", value) if value < start_day: raise ValueError("end_day must be >= start_day") return value
[docs] def contains(self, day: int) -> bool: return self.start_day <= day <= self.end_day
[docs] class TimelineConfig(BaseModel): """Aggregates shift definitions and blackout rules for a scenario.""" shifts: list[ShiftDefinition] blackouts: list[BlackoutWindow] = [] days_per_week: int = 7 @field_validator("days_per_week") @classmethod def _days_positive(cls, value: int) -> int: if value <= 0: raise ValueError("days_per_week must be positive") return value
[docs] def iter_blackouts(self) -> Iterable[BlackoutWindow]: return iter(self.blackouts)