provozni zaloha

This commit is contained in:
lachtan
2026-06-24 08:11:12 +02:00
parent 9295dba19f
commit 1db3ec4756
97 changed files with 7698 additions and 817 deletions

View File

@@ -12,20 +12,25 @@ same result, so no state needs to be persisted.
from __future__ import annotations
import random
from datetime import date, datetime, time
from datetime import date, datetime, time, timedelta
MIN_GAP_MIN = 15 # minimum gap between fire times in minutes; tune here
DAYS_PER_WEEK = 7
def compute_fire_times(target_date: date, text: str, cfg: dict) -> list[datetime]:
"""Deterministic fire times for one day.
Returns [] when the day falls outside the days/from/until filters. Raises
ValueError on a malformed config (bad window, days, dates, or when the
requested count cannot fit the window with MIN_GAP_MIN spacing) — these are
structural and validated before any date filter, so the same call validates
a config regardless of the date passed in.
With period 'day' (default) the count is per day; with 'week' it is per week,
spread across distinct days. Returns [] when the day falls outside the
days/from/until filters. Raises ValueError on a malformed config (bad window,
days, dates, or an infeasible count) — these are structural and validated
before any date filter, so the same call validates a config regardless of the
date passed in.
"""
if cfg.get("period", "day") == "week":
return _weekly_fire_times(target_date, text, cfg)
count = _parse_count(cfg.get("times_per_day"))
start, end = parse_window(cfg.get("window"))
day_set = _parse_days(cfg["days"]) if cfg.get("days") is not None else None
@@ -54,6 +59,44 @@ def compute_fire_times(target_date: date, text: str, cfg: dict) -> list[datetime
return [datetime.combine(target_date, _minute_to_time(m)) for m in minutes]
def _weekly_fire_times(target_date: date, text: str, cfg: dict) -> list[datetime]:
"""Deterministic fire times for target_date within a weekly schedule.
Picks `count` distinct days (MonSun week) eligible under the days/from/until
filters, one random time per chosen day inside the window. Seeded by the
week, not the day, so every day of the same week computes the identical plan
and this returns only the slice landing on target_date.
"""
count = _parse_count(cfg.get("times_per_day"))
start, end = parse_window(cfg.get("window"))
day_set = _parse_days(cfg["days"]) if cfg.get("days") is not None else None
from_date = _parse_date(cfg["from"]) if cfg.get("from") is not None else None
until_date = _parse_date(cfg["until"]) if cfg.get("until") is not None else None
week_capacity = len(day_set) if day_set is not None else DAYS_PER_WEEK
if count > week_capacity:
raise ValueError(
f"{count} times per week need {count} eligible days, but only {week_capacity} match the filter"
)
week_start = target_date - timedelta(days=target_date.weekday())
eligible = [
day
for offset in range(DAYS_PER_WEEK)
for day in [week_start + timedelta(days=offset)]
if (from_date is None or day >= from_date)
and (until_date is None or day <= until_date)
and (day_set is None or _cron_weekday(day) in day_set)
]
if not eligible:
return []
rnd = random.Random(f"{week_start.isoformat()}|{text}|week")
chosen = sorted(rnd.sample(eligible, min(count, len(eligible))))
fires = [datetime.combine(day, _minute_to_time(start + rnd.randint(0, end - start))) for day in chosen]
return [fire for fire in fires if fire.date() == target_date]
def _parse_count(raw: object) -> int:
if not isinstance(raw, int) or isinstance(raw, bool) or raw < 1:
raise ValueError(f"times_per_day must be an int >= 1, got {raw!r}")
@@ -91,6 +134,7 @@ def random_cfg_from_row(row) -> dict:
cfg = {
"times_per_day": row["times_per_day"],
"window": f"{minutes_to_hhmm(row['window_start'])}-{minutes_to_hhmm(row['window_end'])}",
"period": row["period"],
}
if row["days_filter"]:
cfg["days"] = row["days_filter"]