#!/usr/bin/env python3 """test_last_full_moon.py — produce a full-moon close-up for the most recent full moon. Runs the same NASA Dial-a-Moon fetch + render_phase_closeup pipeline that moon-phase-monthly.sh uses, but bypasses the east-frame witness step so it works any time without needing captured frames. Also exercises sun_events_in_range() (the sunrise/sunset almanac call that requires a bare topos, not an earth+topos observer) as a regression check for that fix. Output: test_last_full_moon_out.jpg in the sky-cam directory. Run from the sky-cam directory: python3 test_last_full_moon.py """ import sys import pathlib from datetime import datetime, timedelta, timezone HERE = pathlib.Path(__file__).resolve().parent sys.path.insert(0, str(HERE)) OUT_PATH = HERE / 'test_last_full_moon_out.jpg' def main() -> int: import moon_phase as mp import moon_dialamoon from moon_composite import render_phase_closeup now = datetime.now(timezone.utc) # ── Find the most recent full moon ──────────────────────────────────────── candidates = mp.full_moons_in_range(now - timedelta(days=35), now) if not candidates: print("ERROR: no full moon found in the last 35 days", file=sys.stderr) return 1 fm = candidates[-1] illum = mp.illumination(fm) illum_pct = round(illum * 100) pa = mp.phase_angle(fm) alt, az = mp.altaz(fm) para = mp.parallactic_angle(fm) local_str = mp._format_local(fm) print(f"last full moon (UTC) : {fm.strftime('%Y-%m-%dT%H:%M:%SZ')}") print(f"local : {local_str}") print(f"illumination : {illum:.4f} ({illum_pct}%)") print(f"phase angle : {pa:.2f}° (0° = full)") print(f"altitude / azimuth : {alt:.2f}° / {az:.2f}°") print(f"parallactic angle : {para:.2f}°") print() # ── Sunrise/sunset for that day (regression check for topos fix) ────────── day_start = fm.replace(hour=0, minute=0, second=0, microsecond=0) day_end = day_start + timedelta(days=1) events = mp.sun_events_in_range(day_start, day_end) if events: print(f"sun events on {day_start.strftime('%Y-%m-%d')} UTC:") for t, is_rise in events: label = "sunrise" if is_rise else "sunset " print(f" {label} {t.strftime('%H:%M:%S')} UTC ({mp._format_local(t)})") else: print(f"no sun events on {day_start.strftime('%Y-%m-%d')} (polar location?)") print() # ── Fetch NASA Dial-a-Moon render for that hour ─────────────────────────── print(f"fetching NASA Dial-a-Moon for {fm.strftime('%Y-%m-%dT%HZ')} ...") try: nasa_path = moon_dialamoon.fetch_for_time(fm) except Exception as e: print(f"ERROR: dial-a-moon fetch failed: {e}", file=sys.stderr) return 2 print(f"dial-a-moon cache : {nasa_path}") print() # ── Render full-screen composite ────────────────────────────────────────── caption = ( f"Full Moon — {illum_pct}% lit — {fm.strftime('%B %Y')} — " f"{local_str} — NASA SVS Dial-a-Moon" ) render_phase_closeup( str(nasa_path), str(OUT_PATH), output_size=(1920, 1080), moon_height_pct=0.92, caption=caption, when_utc=fm, ) print(f"wrote {OUT_PATH}") print() print("OK") return 0 if __name__ == "__main__": sys.exit(main())