Pivot from SMS to calendar-only: weekly pull, independent schedule
All checks were successful
Deploy / deploy (push) Successful in 36s
All checks were successful
Deploy / deploy (push) Successful in 36s
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
This commit is contained in:
parent
d3498849d3
commit
1c31ec780b
6 changed files with 231 additions and 646 deletions
94
scraper.py
94
scraper.py
|
|
@ -1,9 +1,5 @@
|
|||
import os
|
||||
import sys
|
||||
import logging
|
||||
import smtplib
|
||||
from datetime import datetime
|
||||
from email.message import EmailMessage
|
||||
|
||||
import requests
|
||||
|
||||
|
|
@ -15,10 +11,7 @@ MENU_TYPE = "lunch"
|
|||
API_BASE = "https://kyrene.api.nutrislice.com/menu/api/weeks/school"
|
||||
|
||||
|
||||
def get_lunch_menu(date_str: str | None = None) -> tuple[list[str], str]:
|
||||
if date_str is None:
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
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}")
|
||||
|
|
@ -29,73 +22,30 @@ def get_lunch_menu(date_str: str | None = None) -> tuple[list[str], str]:
|
|||
timeout=20,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def _extract_items(day: dict) -> list[str]:
|
||||
items = []
|
||||
for day in data.get("days", []):
|
||||
if day.get("date") != date_str:
|
||||
for entry in day.get("menu_items", []):
|
||||
if entry.get("is_section_title") or entry.get("is_station_header"):
|
||||
continue
|
||||
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, date_str
|
||||
food = entry.get("food")
|
||||
if food and food.get("name"):
|
||||
items.append(food["name"].strip())
|
||||
return items
|
||||
|
||||
|
||||
def send_sms(message: str, phone: str) -> None:
|
||||
gmail_user = os.environ["GMAIL_USER"]
|
||||
gmail_pass = os.environ["GMAIL_APP_PASSWORD"]
|
||||
gateway = os.environ.get("CARRIER_GATEWAY", "vtext.com")
|
||||
to_addr = f"{phone}@{gateway}"
|
||||
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")
|
||||
|
||||
msg = EmailMessage()
|
||||
msg["From"] = gmail_user
|
||||
msg["To"] = to_addr
|
||||
msg.set_content(message)
|
||||
|
||||
log.info(f"Sending email-to-SMS → {to_addr}")
|
||||
with smtplib.SMTP("smtp.gmail.com", 587) as smtp:
|
||||
smtp.starttls()
|
||||
smtp.login(gmail_user, gmail_pass)
|
||||
smtp.send_message(msg)
|
||||
log.info("Sent.")
|
||||
|
||||
|
||||
def main():
|
||||
phone = os.environ.get("PHONE_NUMBER", "").strip()
|
||||
if not phone:
|
||||
log.error("PHONE_NUMBER environment variable is not set.")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
items, date_str = get_lunch_menu()
|
||||
if items:
|
||||
body = "\n".join(f"• {i}" for i in items)
|
||||
message = f"Mariposa Lunch {date_str}:\n{body}"
|
||||
else:
|
||||
message = (
|
||||
f"No lunch menu posted for {date_str} "
|
||||
f"(Kyrene de la Mariposa — may be a break or holiday)."
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception("Failed to fetch menu")
|
||||
message = f"Error fetching Mariposa lunch menu: {e}"
|
||||
|
||||
# SMS via email has a ~160 char limit per message segment; keep it short
|
||||
if len(message) > 1400:
|
||||
message = message[:1397] + "..."
|
||||
|
||||
log.info(f"Message:\n{message}")
|
||||
|
||||
try:
|
||||
send_sms(message, phone)
|
||||
except Exception as e:
|
||||
log.error(f"Failed to send SMS: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
data = _fetch_week(date_str)
|
||||
return [
|
||||
{"date": day.get("date"), "items": _extract_items(day)}
|
||||
for day in data.get("days", [])
|
||||
if day.get("date")
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue