""" Orchestrator step functions. Each step receives a WeeklyRun row and an open Session. Steps are idempotent: a non-null timestamp column causes an immediate return. Steps commit their own changes before returning. All service imports are at module level so tests can monkeypatch via `app.services.orchestrator.steps.`. """ from __future__ import annotations import logging from datetime import datetime, timezone from typing import TYPE_CHECKING from app.config import settings from app.models import FamilyMember, FamilyProfile, MealPlan, MealPlanItemStatus from app.services.approval import issue_token from app.services.email import get_email_backend from app.services.orchestrator.alerts import send_admin_alert from app.services.planner.generate import generate_meal_plan from app.services.scraper_service import ScraperService if TYPE_CHECKING: from sqlalchemy.orm import Session from app.models import WeeklyRun logger = logging.getLogger(__name__) def step_scrape(run: "WeeklyRun", db: "Session") -> None: if run.scraped_at is not None: logger.info("step_scrape: already done for %s, skipping", run.week_start_date) return service = ScraperService(db) last_result: dict = {} for attempt in (1, 2): last_result = service.run_scrape() if last_result["status"] == "success": run.scraped_at = datetime.now(timezone.utc) run.used_stale_data = False db.commit() logger.info("step_scrape: success on attempt %d", attempt) return logger.warning( "step_scrape: attempt %d failed: %s", attempt, last_result.get("error") ) # Both attempts failed — mark stale and continue the cycle run.scraped_at = datetime.now(timezone.utc) run.used_stale_data = True db.commit() send_admin_alert( subject=f"Scrape failed week {run.week_start_date}", body=( f"Both scrape attempts failed. Proceeding with stale grocery data.\n" f"Error: {last_result.get('error')}" ), ) logger.warning("step_scrape: both attempts failed; marked stale")