Files
2026-05-29 04:00:36 +00:00

284 lines
10 KiB
Python

#!/usr/bin/env python3
"""Daily digest for Andy — checks birthdays, holidays, notable weather.
Silent exit (no output) if nothing noteworthy. Meant for no_agent=True cron."""
import json
import urllib.request
import urllib.error
from datetime import date, timedelta
import sys
# ─── CONFIG ──────────────────────────────────────────────────────────────────
BIRTHDAYS = {
"Muna": (8, 2), # mom
"Steve": (3, 26), # dad
"Lisa": (9, 30), # sister (1985)
"Donna": (1, 19), # wife (1977)
"Calvin": (10, 9), # son (2013)
}
ANNIVERSARIES = {
"Wedding 🥂": (8, 20), # married 8/20/2011
}
# ─── SCHOOL (TCS 2026-2027) ───────────────────────────────────────────────────
# Format: (month, day, label, lead_days)
# lead_days = how far in advance to start notifying
SCHOOL_EVENTS = [
# (month, day, label, lead_days)
# lead_days = how far in advance to start notifying
(8, 10, "📚 Teacher Work Days begin (school starts Aug 17)", 1),
(8, 17, "📚 **First Day of School** — TCS 2026-27!", 20),
(9, 7, "🏖️ Labor Day — no school", 1),
(10, 2, "📋 Progress Reports issued", 7),
(10, 6, "👩‍🏫 Parent-Teacher Conferences begin", 7),
(10, 12, "🏖️ Columbus Day — no school", 1),
(11, 6, "🏖️ Veterans Day observed — no school", 1),
(11, 25, "🦃 Thanksgiving Break begins — no school", 7),
(12, 18, "📚 End of 1st Semester", 7),
(12, 21, "🎄 Winter Break begins — no school", 7),
(1, 8, "📋 Progress Reports issued", 7),
(1, 18, "🏖️ MLK Day — no school", 1),
(2, 15, "🏖️ Presidents' Day — no school", 1),
(3, 4, "📋 Progress Reports issued", 7),
(3, 9, "👩‍🏫 Parent-Teacher Conferences begin", 7),
(4, 2, "🌸 Spring Break begins — no school", 7),
(4, 6, "🚌 School Trip", 7),
(4, 19, "🌸 Spring Break continues — no school", 7),
(5, 28, "🎓 **Last Day of School!**", 20),
(5, 31, "🏖️ Memorial Day — no school", 1),
(6, 4, "📋 Progress Reports issued", 7),
]
# Beaufort, SC
LAT, LON = "32.43", "-80.67"
# ─── HELPERS ─────────────────────────────────────────────────────────────────
def nth_weekday(year, month, weekday, n):
"""Return date of nth weekday in month (0=Mon .. 6=Sun)."""
first = date(year, month, 1)
offset = (weekday - first.weekday()) % 7
return first + timedelta(days=offset + 7 * (n - 1))
def father_sunday(year):
"""Third Sunday of June."""
return nth_weekday(year, 6, 6, 3)
def mother_sunday(year):
"""Second Sunday of May."""
return nth_weekday(year, 5, 6, 2)
def fetch_json(url, timeout=10):
try:
req = urllib.request.Request(url, headers={"User-Agent": "Hermes-Digest/1.0"})
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read())
except Exception as e:
return None
def days_until_birthday(month, day, today):
"""Return (days_until, date_obj). Handles past birthdays by wrapping to next year."""
try:
this_year = date(today.year, month, day)
except ValueError:
return None, None
diff = (this_year - today).days
if diff >= 0:
return diff, this_year
# Already past — check next year
try:
next_year = date(today.year + 1, month, day)
return (next_year - today).days, next_year
except ValueError:
return None, None
# ─── WEATHER ─────────────────────────────────────────────────────────────────
def check_alerts():
"""Check NWS for active severe weather alerts near Beaufort, SC."""
data = fetch_json(f"https://api.weather.gov/alerts/active?point={LAT},{LON}")
if not data:
return None
features = data.get("features", [])
notable = []
for f in features:
p = f.get("properties", {})
sev = p.get("severity", "")
event = p.get("event", "")
headline = p.get("headline", "")
if sev in ("Severe", "Extreme"):
notable.append(f"⚠️ {event}: {headline}")
elif sev == "Moderate":
notable.append(f"⚠️ {event}: {headline}")
return notable if notable else None
def check_forecast():
"""Check NWS forecast for notably bad conditions today/tomorrow."""
points = fetch_json(f"https://api.weather.gov/points/{LAT},{LON}")
if not points:
return None
forecast_url = points.get("properties", {}).get("forecast")
if not forecast_url:
return None
data = fetch_json(forecast_url)
if not data:
return None
periods = data.get("properties", {}).get("periods", [])
today_str = date.today().isoformat()
tomorrow_str = (date.today() + timedelta(days=1)).isoformat()
notes = []
SEVERE = [
"hurricane", "tropical storm", "severe thunderstorm", "tornado",
"blizzard", "ice storm", "winter storm", "flash flood",
]
for p in periods[:4]:
start = (p.get("startTime") or "")[:10]
if start not in (today_str, tomorrow_str):
continue
temp = p.get("temperature", 70)
wind = (p.get("windSpeed") or "").lower()
short = p.get("shortForecast", "")
if temp >= 100:
notes.append(f"🌡️ Extreme heat: {temp}°F — {short}")
elif temp <= 20:
notes.append(f"🥶 Extreme cold: {temp}°F — {short}")
for kw in SEVERE:
if kw in short.lower():
notes.append(f"🌩️ {short}")
break
# High wind (grab first number from wind string)
nums = [c for c in wind.split()[0] if c.isdigit()] if wind else []
if nums:
try:
if int("".join(nums)) >= 30:
notes.append(f"💨 High wind: {p.get('windSpeed', '')}")
except ValueError:
pass
return notes if notes else None
# ─── MAIN ────────────────────────────────────────────────────────────────────
def main():
today = date.today()
lines = []
# ── Birthdays ──
upcoming = []
for name, (m, d) in BIRTHDAYS.items():
diff, dt = days_until_birthday(m, d, today)
if diff is not None and 0 <= diff <= 20:
upcoming.append((name, diff, dt))
if upcoming:
lines.append("🎂 **Upcoming Birthdays:**")
for name, diff, dt in sorted(upcoming, key=lambda x: x[1]):
if diff == 0:
lines.append(f" • **{name}** — TODAY! 🎉")
elif diff == 1:
lines.append(f" • **{name}** — tomorrow!")
else:
lines.append(f" • **{name}** — in {diff} days ({dt.strftime('%B %d')})")
lines.append("")
# ── Anniversaries ──
anniv_upcoming = []
for label, (m, d) in ANNIVERSARIES.items():
diff, dt = days_until_birthday(m, d, today)
if diff is not None and 0 <= diff <= 20:
anniv_upcoming.append((label, diff, dt))
if anniv_upcoming:
lines.append("💍 **Upcoming Anniversaries:**")
for label, diff, dt in sorted(anniv_upcoming, key=lambda x: x[1]):
if diff == 0:
lines.append(f" • **{label}** — TODAY! 🎉")
elif diff == 1:
lines.append(f" • **{label}** — tomorrow!")
else:
lines.append(f" • **{label}** — in {diff} days ({dt.strftime('%B %d')})")
lines.append("")
# ── Holidays ──
events = []
for label, fn in [("Father's Day 👔", father_sunday), ("Mother's Day 🌸", mother_sunday)]:
d = fn(today.year)
diff = (d - today).days
if 0 <= diff <= 20:
events.append((label, diff, d))
elif diff < 0:
d_next = fn(today.year + 1)
diff = (d_next - today).days
if diff <= 20:
events.append((label, diff, d_next))
if events:
lines.append("📅 **Upcoming Events:**")
for label, diff, dt in sorted(events, key=lambda x: x[1]):
if diff == 0:
lines.append(f" • **{label}** — TODAY!")
elif diff == 1:
lines.append(f" • **{label}** — tomorrow!")
else:
lines.append(f" • **{label}** — in {diff} days ({dt.strftime('%B %d')})")
lines.append("")
# ── Weather alerts ──
alerts = check_alerts()
if alerts:
lines.append("🚨 **Weather Alerts:**")
for a in alerts:
lines.append(f" {a}")
lines.append("")
# ── Notable forecast ──
forecast_notes = check_forecast()
if forecast_notes:
lines.append("🌤️ **Notable Weather:**")
for n in forecast_notes:
lines.append(f" {n}")
lines.append("")
# ── School events ──
school_upcoming = []
school_year = 2026 # School year starts in Aug 2026
for month, day, label, lead_days in SCHOOL_EVENTS:
# School year spans Aug 2026 - Jun 2027
ev_year = 2026 if month >= 8 else 2027
try:
ev_date = date(ev_year, month, day)
except ValueError:
continue
diff = (ev_date - today).days
if 0 <= diff <= lead_days:
school_upcoming.append((label, diff, ev_date))
if school_upcoming:
lines.append("🏫 **School Calendar:**")
for label, diff, dt in sorted(school_upcoming, key=lambda x: x[1]):
if diff == 0:
lines.append(f" • {label} — **TODAY!**")
elif diff == 1:
lines.append(f" • {label} — tomorrow")
else:
lines.append(f" • {label} — in {diff} days ({dt.strftime('%B %d')})")
lines.append("")
# ── Output ──
if lines:
header = f"☀️ **Daily Digest** — {today.strftime('%A, %B %d, %Y')}\n\n"
footer = "\n— Hermes"
print(header + "\n".join(lines).strip() + footer)
# else: silent exit — nothing worth reporting
if __name__ == "__main__":
main()