Files
reyna-cli/backup/macmini-automation-baseline-2026-08-03/hermes-scripts/chiapas_flight_price_monitor.py
T
2026-08-03 11:47:09 -04:00

149 lines
4.7 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Silent watchdog for Chiapas flight prices.
Checks the user's preferred KAYAK search URL and only prints an alert when a
meaningful price drop is detected. Empty stdout means no WhatsApp message.
"""
import json
import os
import re
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
URL = "https://www.kayak.com/flights/MIA,MCO-TGZ/2026-09-21/2026-10-09/2adults/children-5-8?ucs=uwkx32&sort=bestflight_a"
STATE_PATH = Path.home() / ".hermes" / "state" / "chiapas_flight_monitor.json"
CHROMIUM = "/snap/bin/chromium"
# Alert policy: establish a baseline silently, then alert only if the best price
# is materially better than what we have seen before.
DROP_PERCENT = 0.20 # at least 20% lower than baseline/best seen
DROP_ABSOLUTE = 300 # or at least $300 lower for the searched party
VERY_GOOD_TOTAL = 450 # always alert at or below this displayed fare
MIN_REASONABLE = 400 # ignore UI noise like baggage/filter slider prices
MAX_REASONABLE = 8000 # ignore unrelated large numbers
def load_state():
if STATE_PATH.exists():
try:
return json.loads(STATE_PATH.read_text())
except Exception:
return {}
return {}
def save_state(state):
STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
STATE_PATH.write_text(json.dumps(state, indent=2, sort_keys=True))
def fetch_html():
if not Path(CHROMIUM).exists():
return ""
with tempfile.NamedTemporaryFile(prefix="chiapas_kayak_", suffix=".html", delete=False) as tmp:
out_path = tmp.name
try:
cmd = [
CHROMIUM,
"--headless",
"--disable-gpu",
"--no-sandbox",
"--disable-dev-shm-usage",
"--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120 Safari/537.36",
"--virtual-time-budget=60000",
"--dump-dom",
URL,
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, timeout=120)
return result.stdout or ""
except Exception:
return ""
finally:
try:
os.unlink(out_path)
except Exception:
pass
def extract_prices(html):
# KAYAK may render prices as visible text or escaped JSON. Keep this simple
# and conservative: only currency-looking amounts in a plausible full-party range.
raw = re.findall(r"\$\s*([0-9][0-9,]{2,5})", html)
prices = []
for s in raw:
try:
val = int(s.replace(",", ""))
except ValueError:
continue
if MIN_REASONABLE <= val <= MAX_REASONABLE:
prices.append(val)
return sorted(set(prices))
def main():
now = datetime.now(timezone.utc).isoformat()
state = load_state()
html = fetch_html()
prices = extract_prices(html)
state["last_checked_utc"] = now
state["url"] = URL
if not prices:
state["last_status"] = "no_parseable_prices"
save_state(state)
return 0
current = min(prices)
state["last_prices_seen"] = prices[:10]
state["last_lowest"] = current
baseline = state.get("baseline_lowest")
best_seen = state.get("best_seen")
if baseline is None:
state["baseline_lowest"] = current
state["best_seen"] = current
state["last_status"] = "baseline_set_silent"
save_state(state)
return 0
if best_seen is None or current < best_seen:
state["best_seen"] = current
threshold_from_baseline = int(baseline * (1 - DROP_PERCENT))
threshold_from_best = int(best_seen * (1 - DROP_PERCENT)) if best_seen else threshold_from_baseline
absolute_threshold = max(0, baseline - DROP_ABSOLUTE)
should_alert = (
current <= VERY_GOOD_TOTAL
or current <= threshold_from_baseline
or current <= threshold_from_best
or current <= absolute_threshold
)
already_alerted_at = state.get("last_alert_price")
if should_alert and (already_alerted_at is None or current < already_alerted_at):
state["last_alert_price"] = current
state["last_status"] = "alerted"
save_state(state)
print(
"Chiapas flight price alert: KAYAK is showing a notably better fare.\n"
f"Route/search: MIA or MCO → TGZ, Sept 21–Oct 9, 2026, family search.\n"
f"Lowest price I could parse today: ${current:,}.\n"
f"Baseline: ${baseline:,}. Best previously seen: ${best_seen:,}.\n"
f"Check/book here: {URL}"
)
return 0
state["last_status"] = "checked_no_alert"
save_state(state)
return 0
if __name__ == "__main__":
raise SystemExit(main())