feat: initial flight monitor

- Python flight price monitor for BER↔EZE and other routes
- Adapter pattern for extensibility (Kiwi/Tequila implemented)
- OpenClaw alerting integration
- Cron-friendly one-shot execution
- Full logging of all checks
- Graceful handling when API key not set

Implements monkey-island/flight-monitor#2
This commit is contained in:
2026-03-21 23:10:25 +00:00
parent 96d2c6f67c
commit 30ee5fbd85
12 changed files with 588 additions and 2 deletions

120
adapters/kiwi.py Normal file
View File

@@ -0,0 +1,120 @@
"""
Kiwi.com / Tequila API adapter.
"""
import os
import logging
from datetime import date, datetime
from typing import List, Dict, Any
import requests
from adapters.base import FlightAdapter
logger = logging.getLogger(__name__)
class KiwiAdapter(FlightAdapter):
"""
Adapter for Kiwi.com Tequila API.
API docs: https://tequila.kiwi.com/docs/search/
"""
API_BASE = "https://api.tequila.kiwi.com"
def __init__(self):
"""Initialize the adapter. Requires KIWI_API_KEY environment variable."""
self.api_key = os.getenv("KIWI_API_KEY")
if not self.api_key:
raise ValueError(
"KIWI_API_KEY environment variable not set. "
"Get your API key from https://tequila.kiwi.com/portal/login"
)
logger.info("Kiwi adapter initialized")
def get_name(self) -> str:
return "Kiwi/Tequila"
def search_round_trip(
self,
origin: str,
destination: str,
departure_date: date,
return_date: date,
**kwargs
) -> List[Dict[str, Any]]:
"""
Search for round-trip flights via Kiwi API.
Args:
origin: IATA code (e.g., "BER")
destination: IATA code (e.g., "EZE")
departure_date: Departure date
return_date: Return date
Returns:
List of normalized flight results.
"""
url = f"{self.API_BASE}/v2/search"
params = {
"fly_from": origin,
"fly_to": destination,
"date_from": departure_date.strftime("%d/%m/%Y"),
"date_to": departure_date.strftime("%d/%m/%Y"),
"return_from": return_date.strftime("%d/%m/%Y"),
"return_to": return_date.strftime("%d/%m/%Y"),
"flight_type": "round",
"adults": 1,
"curr": "EUR",
"limit": 5,
"sort": "price"
}
headers = {
"apikey": self.api_key,
"accept": "application/json"
}
try:
logger.debug(f"Kiwi API request: {origin}{destination} {departure_date}{return_date}")
response = requests.get(url, params=params, headers=headers, timeout=30)
response.raise_for_status()
data = response.json()
results = []
for item in data.get("data", []):
# Extract airline(s) - may be multiple for multi-leg flights
airlines = set()
for route in item.get("route", []):
airline = route.get("airline")
if airline:
airlines.add(airline)
airline_str = ", ".join(sorted(airlines)) if airlines else "Unknown"
# Parse timestamps
departure_time = datetime.fromtimestamp(item.get("dTime", 0))
return_time = datetime.fromtimestamp(item.get("aTime", 0))
# Build deep link
deep_link = item.get("deep_link", "")
results.append({
"price_eur": float(item.get("price", 0)),
"airline": airline_str,
"departure": departure_time,
"return": return_time,
"link": deep_link,
"source": self.get_name()
})
logger.debug(f"Kiwi API returned {len(results)} results")
return results
except requests.exceptions.RequestException as e:
logger.error(f"Kiwi API request failed: {e}")
return []
except Exception as e:
logger.error(f"Error parsing Kiwi API response: {e}")
return []