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:
120
adapters/kiwi.py
Normal file
120
adapters/kiwi.py
Normal 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 []
|
||||
Reference in New Issue
Block a user