import base64 import json import logging import anthropic import requests logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger(__name__) # Stable Finalsite "resource manager" permalink for the K-5 lunch menu, found # on https://www.kyrene.org/family-student-resources/food-services/school-menus. # This URL itself never changes — it 302-redirects to whichever monthly PDF # is currently published (verified 2026-09-08: it pointed straight at that # month's file). Nutrislice, the previous data source, was discontinued by # the district. MENU_PAGE_URL = "https://www.kyrene.org/fs/resource-manager/view/32bb3f8b-5a50-4b38-ad68-a5d3108de571" MODEL = "claude-opus-5" _MENU_SCHEMA = { "type": "object", "properties": { "days": { "type": "array", "items": { "type": "object", "properties": { "date": {"type": "string", "description": "ISO date, YYYY-MM-DD"}, "items": {"type": "array", "items": {"type": "string"}}, }, "required": ["date", "items"], "additionalProperties": False, }, }, }, "required": ["days"], "additionalProperties": False, } _PROMPT = """This PDF is a school cafeteria's monthly lunch calendar (grades K-5). It's a calendar-grid layout with one cell per school day, each listing a Breakfast section and a Lunch section. Extract ONLY the Lunch items (ignore Breakfast) for every day of the month shown on the calendar. For each day 1 through the last day of the month: - If the calendar shows lunch items for that day, list them. - If the day has no cafeteria menu (weekend, holiday, or a no-school day like a break), return an empty items list for that date. Read the month and year printed on the calendar itself to build the ISO dates. Return every calendar day of the month, in date order.""" def _fetch_current_pdf() -> bytes: resp = requests.get(MENU_PAGE_URL, timeout=20) resp.raise_for_status() return resp.content def get_month_menu() -> list[dict]: """Fetches whichever monthly K-5 lunch menu PDF is currently published and asks Claude to read it into structured per-day data. Returns [{"date": "YYYY-MM-DD", "items": [...]}, ...] for every day of that month — days with no posted menu (weekends, breaks, holidays) come back with an empty items list, same contract the old Nutrislice-based scraper used.""" log.info(f"Fetching {MENU_PAGE_URL}") pdf_b64 = base64.standard_b64encode(_fetch_current_pdf()).decode("utf-8") client = anthropic.Anthropic() response = client.messages.create( model=MODEL, max_tokens=8000, messages=[{ "role": "user", "content": [ { "type": "document", "source": { "type": "base64", "media_type": "application/pdf", "data": pdf_b64, }, }, {"type": "text", "text": _PROMPT}, ], }], output_config={"format": {"type": "json_schema", "schema": _MENU_SCHEMA}}, ) text = next(b.text for b in response.content if b.type == "text") return json.loads(text)["days"]