lunch-notifier/scraper.py
Barely Removable 1c31ec780b
All checks were successful
Deploy / deploy (push) Successful in 36s
Pivot from SMS to calendar-only: weekly pull, independent schedule
Deletes the entire SMS path at the user's request — Gmail SMTP, phone
number management, carrier gateway settings, and the daily SMS+calendar
job are all gone. Home Assistant calendar events are now the only output.

- scraper.py: get_week_menu() returns the whole school week in one call
  (the Nutrislice API is week-granular regardless of the date requested);
  drops the now-unused single-day get_lunch_menu() and the send_sms()/
  CLI entrypoint that only the SMS path used.
- app.py: push_week_to_calendar() fetches the week and pushes every day
  with a posted menu; runs on its own schedule (calendar_run_time/
  calendar_run_days, tagged "calendar" jobs) fully decoupled from
  anything else, since neither calendar target supports delete-based
  dedup and double-firing would create duplicate events. Manual actions
  become "Push This Week to Calendar" and "Preview This Week's Menu"
  (now a real dry-run over the whole week, not just today).
- Deleted the vacation/no-push date range feature: a day with no posted
  menu already produces empty items, and pushing already skips empty
  items — the extra vacation-range layer for suppressing days the school
  *did* post a menu for was judged unnecessary and removed along with
  its UI, settings field, and API endpoints.
- Deleted scheduler.py, a dead alternate entrypoint (unused by Dockerfile/
  docker-compose) that only worked via the now-deleted scraper.main().
- load_settings() now merges the file over _DEFAULTS instead of returning
  it verbatim — the server's existing settings.json predates this schema
  and has neither calendar_run_time nor calendar_run_days, which would
  otherwise silently schedule zero jobs after this deploys. Verified with
  a migration test using the server's actual old-schema settings shape.

All of this was exercised for real: Docker builds, live containers against
a mock webhook (single-day and full-week pushes, per-entity dedup
behavior), a migration test with the pre-existing settings.json shape,
and a Chrome pass over the rebuilt UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KsJqAZ82Veyf5oihBr71S5
2026-09-03 09:10:36 -07:00

51 lines
1.6 KiB
Python

import logging
from datetime import datetime
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)
SCHOOL_SLUG = "kyrene-de-la-mariposa"
MENU_TYPE = "lunch"
API_BASE = "https://kyrene.api.nutrislice.com/menu/api/weeks/school"
def _fetch_week(date_str: str) -> dict:
dt = datetime.strptime(date_str, "%Y-%m-%d")
url = f"{API_BASE}/{SCHOOL_SLUG}/menu-type/{MENU_TYPE}/{dt.year}/{dt.month:02d}/{dt.day:02d}/"
log.info(f"Fetching {url}")
resp = requests.get(
url,
headers={"Accept": "application/json", "User-Agent": "Mozilla/5.0"},
timeout=20,
)
resp.raise_for_status()
return resp.json()
def _extract_items(day: dict) -> list[str]:
items = []
for entry in day.get("menu_items", []):
if entry.get("is_section_title") or entry.get("is_station_header"):
continue
food = entry.get("food")
if food and food.get("name"):
items.append(food["name"].strip())
return items
def get_week_menu(date_str: str | None = None) -> list[dict]:
"""Returns every day in the school-published week containing date_str,
as [{"date": "YYYY-MM-DD", "items": [...]}, ...] in the API's own order.
Days with no posted menu (weekends, breaks, holidays) have an empty items list."""
if date_str is None:
date_str = datetime.now().strftime("%Y-%m-%d")
data = _fetch_week(date_str)
return [
{"date": day.get("date"), "items": _extract_items(day)}
for day in data.get("days", [])
if day.get("date")
]