init commit
This commit is contained in:
0
mcp_servers/data/real/__init__.py
Normal file
0
mcp_servers/data/real/__init__.py
Normal file
124
mcp_servers/data/real/faa.py
Normal file
124
mcp_servers/data/real/faa.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""FAA NASSTATUS API client — live airport delay/status data, no API key required.
|
||||
|
||||
Endpoint: https://nasstatus.faa.gov/api/airport-status-information
|
||||
Returns XML with ground stops, ground delay programs, arrival/departure delays, closures.
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import httpx
|
||||
|
||||
BASE_URL = "https://nasstatus.faa.gov/api/airport-status-information"
|
||||
|
||||
# Cache parsed data to avoid hitting the API for every airport query
|
||||
_cache: dict | None = None
|
||||
_cache_raw: str = ""
|
||||
|
||||
|
||||
async def _fetch_all_status() -> dict[str, list[dict]]:
|
||||
"""Fetch and parse all airport status information from FAA."""
|
||||
global _cache, _cache_raw
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client:
|
||||
resp = await client.get(BASE_URL)
|
||||
resp.raise_for_status()
|
||||
xml_text = resp.text
|
||||
|
||||
# Don't re-parse if unchanged
|
||||
if xml_text == _cache_raw and _cache is not None:
|
||||
return _cache
|
||||
|
||||
_cache_raw = xml_text
|
||||
_cache = _parse_status_xml(xml_text)
|
||||
return _cache
|
||||
|
||||
except Exception as e:
|
||||
return {"_error": [{"error": str(e)}]}
|
||||
|
||||
|
||||
def _parse_status_xml(xml_text: str) -> dict[str, list[dict]]:
|
||||
"""Parse FAA airport status XML into a dict of airport → delays."""
|
||||
delays_by_airport: dict[str, list[dict]] = {}
|
||||
root = ET.fromstring(xml_text)
|
||||
|
||||
for delay_type in root.findall("Delay_type"):
|
||||
name = delay_type.findtext("Name", "").strip()
|
||||
|
||||
# Ground Stop Programs
|
||||
for program in delay_type.findall(".//Program"):
|
||||
arpt = program.findtext("ARPT", "").strip()
|
||||
if not arpt:
|
||||
continue
|
||||
delays_by_airport.setdefault(arpt, []).append({
|
||||
"type": "ground_stop",
|
||||
"reason": program.findtext("Reason", "unknown").strip(),
|
||||
"end_time": program.findtext("End_Time", "").strip() or None,
|
||||
})
|
||||
|
||||
# Ground Delay Programs
|
||||
for gdp in delay_type.findall(".//Ground_Delay"):
|
||||
arpt = gdp.findtext("ARPT", "").strip()
|
||||
if not arpt:
|
||||
continue
|
||||
delays_by_airport.setdefault(arpt, []).append({
|
||||
"type": "ground_delay_program",
|
||||
"reason": gdp.findtext("Reason", "unknown").strip(),
|
||||
"average_delay": gdp.findtext("Avg", "").strip() or None,
|
||||
"max_delay": gdp.findtext("Max", "").strip() or None,
|
||||
})
|
||||
|
||||
# Arrival/Departure Delays
|
||||
for delay in delay_type.findall(".//Delay"):
|
||||
arpt = delay.findtext("ARPT", "").strip()
|
||||
if not arpt:
|
||||
continue
|
||||
delays_by_airport.setdefault(arpt, []).append({
|
||||
"type": name.lower().replace(" ", "_") if name else "delay",
|
||||
"reason": delay.findtext("Reason", "unknown").strip(),
|
||||
"average_delay": delay.findtext("Avg", "").strip() or None,
|
||||
"max_delay": delay.findtext("Max", "").strip() or None,
|
||||
"trend": delay.findtext("Trend", "").strip() or None,
|
||||
})
|
||||
|
||||
# Closures
|
||||
for closure in delay_type.findall(".//Closure"):
|
||||
arpt = closure.findtext("ARPT", "").strip()
|
||||
if not arpt:
|
||||
continue
|
||||
delays_by_airport.setdefault(arpt, []).append({
|
||||
"type": "closure",
|
||||
"reason": closure.findtext("Reason", "unknown").strip(),
|
||||
"begin": closure.findtext("Begin", "").strip() or None,
|
||||
"end": closure.findtext("End", "").strip() or None,
|
||||
})
|
||||
|
||||
return delays_by_airport
|
||||
|
||||
|
||||
async def get_airport_status(airport_code: str) -> dict:
|
||||
"""Fetch live FAA airport status for a single airport.
|
||||
|
||||
Returns ground delay programs, ground stops, closures.
|
||||
Falls back to 'status_unavailable' on any error.
|
||||
"""
|
||||
all_status = await _fetch_all_status()
|
||||
|
||||
if "_error" in all_status:
|
||||
return {
|
||||
"airport": airport_code.upper(),
|
||||
"status": "status_unavailable",
|
||||
"error": all_status["_error"][0]["error"],
|
||||
"source": "faa_nasstatus_live",
|
||||
}
|
||||
|
||||
airport = airport_code.upper()
|
||||
delays = all_status.get(airport, [])
|
||||
|
||||
return {
|
||||
"airport": airport,
|
||||
"has_delays": len(delays) > 0,
|
||||
"status": "delays_active" if delays else "normal_operations",
|
||||
"delays": delays,
|
||||
"source": "faa_nasstatus_live",
|
||||
}
|
||||
197
mcp_servers/data/real/openmeteo.py
Normal file
197
mcp_servers/data/real/openmeteo.py
Normal file
@@ -0,0 +1,197 @@
|
||||
"""OpenMeteo API client — live weather data, no API key required."""
|
||||
|
||||
import httpx
|
||||
|
||||
BASE_URL = "https://api.open-meteo.com/v1/forecast"
|
||||
|
||||
# United hub coordinates
|
||||
HUB_COORDS: dict[str, tuple[float, float]] = {
|
||||
"ORD": (41.9742, -87.9073), # Chicago O'Hare
|
||||
"EWR": (40.6895, -74.1745), # Newark
|
||||
"IAH": (29.9902, -95.3368), # Houston Intercontinental
|
||||
"SFO": (37.6213, -122.3790), # San Francisco
|
||||
"DEN": (39.8561, -104.6737), # Denver
|
||||
"LAX": (33.9425, -118.4081), # Los Angeles
|
||||
"IAD": (38.9531, -77.4565), # Washington Dulles
|
||||
}
|
||||
|
||||
# WMO weather interpretation codes
|
||||
WMO_CODES: dict[int, str] = {
|
||||
0: "Clear sky",
|
||||
1: "Mainly clear",
|
||||
2: "Partly cloudy",
|
||||
3: "Overcast",
|
||||
45: "Fog",
|
||||
48: "Depositing rime fog",
|
||||
51: "Light drizzle",
|
||||
53: "Moderate drizzle",
|
||||
55: "Dense drizzle",
|
||||
61: "Slight rain",
|
||||
63: "Moderate rain",
|
||||
65: "Heavy rain",
|
||||
71: "Slight snow",
|
||||
73: "Moderate snow",
|
||||
75: "Heavy snow",
|
||||
77: "Snow grains",
|
||||
80: "Slight rain showers",
|
||||
81: "Moderate rain showers",
|
||||
82: "Violent rain showers",
|
||||
85: "Slight snow showers",
|
||||
86: "Heavy snow showers",
|
||||
95: "Thunderstorm",
|
||||
96: "Thunderstorm with slight hail",
|
||||
99: "Thunderstorm with heavy hail",
|
||||
}
|
||||
|
||||
SIGNIFICANT_CODES = {45, 48, 65, 75, 82, 86, 95, 96, 99}
|
||||
|
||||
|
||||
def _interpolate_waypoints(
|
||||
origin: tuple[float, float], dest: tuple[float, float], n: int = 2
|
||||
) -> list[tuple[float, float]]:
|
||||
"""Generate n intermediate waypoints along a great-circle approximation."""
|
||||
points = []
|
||||
for i in range(1, n + 1):
|
||||
frac = i / (n + 1)
|
||||
lat = origin[0] + frac * (dest[0] - origin[0])
|
||||
lon = origin[1] + frac * (dest[1] - origin[1])
|
||||
points.append((round(lat, 4), round(lon, 4)))
|
||||
return points
|
||||
|
||||
|
||||
def _interpret_weather(code: int) -> dict:
|
||||
return {
|
||||
"code": code,
|
||||
"condition": WMO_CODES.get(code, f"Unknown ({code})"),
|
||||
"is_significant": code in SIGNIFICANT_CODES,
|
||||
}
|
||||
|
||||
|
||||
async def fetch_weather_at_point(
|
||||
lat: float, lon: float, client: httpx.AsyncClient
|
||||
) -> dict:
|
||||
"""Fetch current weather at a single point."""
|
||||
params = {
|
||||
"latitude": lat,
|
||||
"longitude": lon,
|
||||
"current": "temperature_2m,wind_speed_10m,wind_direction_10m,weather_code,visibility,precipitation",
|
||||
"timezone": "UTC",
|
||||
}
|
||||
resp = await client.get(BASE_URL, params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
current = data["current"]
|
||||
weather = _interpret_weather(current["weather_code"])
|
||||
return {
|
||||
"latitude": lat,
|
||||
"longitude": lon,
|
||||
"temperature_c": current["temperature_2m"],
|
||||
"wind_speed_kmh": current["wind_speed_10m"],
|
||||
"wind_direction_deg": current["wind_direction_10m"],
|
||||
"visibility_m": current.get("visibility"),
|
||||
"precipitation_mm": current.get("precipitation", 0),
|
||||
"weather": weather,
|
||||
}
|
||||
|
||||
|
||||
async def get_weather_along_route(origin: str, destination: str) -> dict:
|
||||
"""Fetch weather at origin, destination, and en-route waypoints."""
|
||||
origin_coords = HUB_COORDS.get(origin.upper())
|
||||
dest_coords = HUB_COORDS.get(destination.upper())
|
||||
|
||||
if not origin_coords or not dest_coords:
|
||||
return {
|
||||
"error": f"Unknown airport code. Known: {', '.join(HUB_COORDS.keys())}",
|
||||
"origin": origin,
|
||||
"destination": destination,
|
||||
}
|
||||
|
||||
waypoints = _interpolate_waypoints(origin_coords, dest_coords)
|
||||
all_points = [
|
||||
("origin", origin.upper(), origin_coords),
|
||||
*[(f"waypoint_{i+1}", f"WP{i+1}", wp) for i, wp in enumerate(waypoints)],
|
||||
("destination", destination.upper(), dest_coords),
|
||||
]
|
||||
|
||||
results = {}
|
||||
significant_events = []
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
for label, name, (lat, lon) in all_points:
|
||||
try:
|
||||
weather = await fetch_weather_at_point(lat, lon, client)
|
||||
weather["label"] = label
|
||||
weather["name"] = name
|
||||
results[label] = weather
|
||||
if weather["weather"]["is_significant"]:
|
||||
significant_events.append(
|
||||
{
|
||||
"location": name,
|
||||
"condition": weather["weather"]["condition"],
|
||||
"code": weather["weather"]["code"],
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
results[label] = {"label": label, "name": name, "error": str(e)}
|
||||
|
||||
return {
|
||||
"origin": origin.upper(),
|
||||
"destination": destination.upper(),
|
||||
"waypoints": results,
|
||||
"significant_events": significant_events,
|
||||
"source": "openmeteo_live",
|
||||
}
|
||||
|
||||
|
||||
async def get_weather_forecast_hubs() -> dict:
|
||||
"""Fetch 4-hour forecast for all United hubs."""
|
||||
hubs = ["ORD", "EWR", "IAH", "SFO", "DEN"]
|
||||
results = {}
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
for hub in hubs:
|
||||
lat, lon = HUB_COORDS[hub]
|
||||
params = {
|
||||
"latitude": lat,
|
||||
"longitude": lon,
|
||||
"hourly": "temperature_2m,wind_speed_10m,weather_code,visibility,precipitation_probability",
|
||||
"forecast_hours": 4,
|
||||
"timezone": "UTC",
|
||||
}
|
||||
try:
|
||||
resp = await client.get(BASE_URL, params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
hourly = data["hourly"]
|
||||
|
||||
hours = []
|
||||
risk_flag = False
|
||||
for i in range(len(hourly["time"])):
|
||||
code = hourly["weather_code"][i]
|
||||
weather = _interpret_weather(code)
|
||||
if weather["is_significant"]:
|
||||
risk_flag = True
|
||||
hours.append(
|
||||
{
|
||||
"time": hourly["time"][i],
|
||||
"temperature_c": hourly["temperature_2m"][i],
|
||||
"wind_speed_kmh": hourly["wind_speed_10m"][i],
|
||||
"weather": weather,
|
||||
"visibility_m": hourly.get("visibility", [None])[i]
|
||||
if "visibility" in hourly
|
||||
else None,
|
||||
"precipitation_probability": hourly[
|
||||
"precipitation_probability"
|
||||
][i],
|
||||
}
|
||||
)
|
||||
|
||||
results[hub] = {
|
||||
"hub": hub,
|
||||
"forecast": hours,
|
||||
"risk_flag": risk_flag,
|
||||
}
|
||||
except Exception as e:
|
||||
results[hub] = {"hub": hub, "error": str(e)}
|
||||
|
||||
return {"hubs": results, "source": "openmeteo_live"}
|
||||
Reference in New Issue
Block a user