""" Orchestrator runner. run_step(step_name, week_start_date) — execute one step for all families run_week(week_start_date) — execute all steps in sequence """ from __future__ import annotations import logging from datetime import date, timedelta from typing import Optional from app.database import SessionLocal logger = logging.getLogger(__name__) STEPS = ("scrape", "generate", "email", "reminder", "deadline", "finalize") def _current_week_start() -> date: """Return the upcoming Monday (today if today is Monday). The Friday email advertises the upcoming Mon-Sun week; the plan is keyed by that Monday so the email subject ("Meal plan for week of ") matches the calendar week the meals are for. The frontend uses the same convention via `upcomingMonday()` in `lib/utils.ts`. Paired with the Sprint 7 fix: see `Review/sprint7-verification.md` and the SQL migration script `backend/scripts/fix_2026_06_05_to_2026_06_08.sql` for the one-time data fix that retargets any pre-S7 Friday-keyed plan to the equivalent upcoming Monday. """ today = date.today() if today.weekday() == 0: # Monday return today return today + timedelta(days=(7 - today.weekday())) def _get_or_create_run(db, family_id, week_start_date: date): from app.models import WeeklyRun run = ( db.query(WeeklyRun) .filter( WeeklyRun.family_id == family_id, WeeklyRun.week_start_date == week_start_date, ) .first() ) if run is None: run = WeeklyRun( family_id=family_id, week_start_date=week_start_date, status="pending", ) db.add(run) db.flush() db.refresh(run) return run def run_step(step_name: str, week_start_date: Optional[date] = None) -> None: from app.models import FamilyProfile from app.services.orchestrator import steps as s step_fns = { "scrape": s.step_scrape, "generate": s.step_generate, "email": s.step_email, "reminder": s.step_reminder, "deadline": s.step_deadline, "finalize": s.step_finalize, } if step_name not in step_fns: raise ValueError(f"Unknown step: {step_name!r}. Valid: {list(step_fns)}") week = week_start_date or _current_week_start() db = SessionLocal() try: families = db.query(FamilyProfile).all() if not families: logger.warning("run_step(%s): no family profiles, skipping", step_name) return for family in families: run = _get_or_create_run(db, family.id, week) try: step_fns[step_name](run, db) except Exception: logger.exception( "run_step(%s) failed for family %s", step_name, family.id ) finally: db.close() def run_week(week_start_date: Optional[date] = None) -> None: week = week_start_date or _current_week_start() for step in STEPS: run_step(step, week)