Added Weather API
This commit is contained in:
117
roadcast/openweather_client.py
Normal file
117
roadcast/openweather_client.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""OpenWeather / Road Risk client.
|
||||
|
||||
Provides:
|
||||
- fetch_weather(lat, lon, api_key=None)
|
||||
- fetch_road_risk(lat, lon, api_key=None, roadrisk_url=None, extra_params=None)
|
||||
|
||||
Never hardcode API keys in source. Provide via api_key argument or set OPENWEATHER_API_KEY / OPENWEATHER_KEY env var.
|
||||
"""
|
||||
import os
|
||||
from typing import Tuple, Dict, Any, Optional
|
||||
import requests
|
||||
|
||||
def _get_api_key(explicit_key: Optional[str] = None) -> Optional[str]:
|
||||
if explicit_key:
|
||||
return explicit_key
|
||||
return os.environ.get("OPENWEATHER_API_KEY") or os.environ.get("OPENWEATHER_KEY")
|
||||
|
||||
BASE_URL = "https://api.openweathermap.org/data/2.5"
|
||||
|
||||
|
||||
def fetch_weather(lat: float, lon: float, params: Optional[dict] = None, api_key: Optional[str] = None) -> dict:
|
||||
"""Call standard OpenWeather /weather endpoint and return parsed JSON."""
|
||||
key = _get_api_key(api_key)
|
||||
if key is None:
|
||||
raise RuntimeError("Set OPENWEATHER_API_KEY or OPENWEATHER_KEY or pass api_key")
|
||||
q = {"lat": lat, "lon": lon, "appid": key, "units": "metric"}
|
||||
if params:
|
||||
q.update(params)
|
||||
resp = requests.get(f"{BASE_URL}/weather", params=q, timeout=10)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def fetch_road_risk(lat: float, lon: float, extra_params: Optional[dict] = None, api_key: Optional[str] = None, roadrisk_url: Optional[str] = None) -> Tuple[dict, Dict[str, Any]]:
|
||||
"""
|
||||
Call OpenWeather /roadrisk endpoint (or provided roadrisk_url) and return (raw_json, features).
|
||||
|
||||
features will always include 'road_risk_score' (float). Other numeric fields are included when present.
|
||||
The implementation:
|
||||
- prefers explicit numeric keys (road_risk_score, risk_score, score, risk)
|
||||
- if absent, collects top-level numeric fields and averages common contributors
|
||||
- if still absent, falls back to a simple weather-derived heuristic using /weather
|
||||
|
||||
Note: Do not commit API keys. Pass api_key or set env var.
|
||||
"""
|
||||
key = _get_api_key(api_key)
|
||||
if key is None:
|
||||
raise RuntimeError("Set OPENWEATHER_API_KEY or OPENWEATHER_KEY or pass api_key")
|
||||
|
||||
params = {"lat": lat, "lon": lon, "appid": key}
|
||||
if extra_params:
|
||||
params.update(extra_params)
|
||||
|
||||
url = roadrisk_url or f"{BASE_URL}/roadrisk"
|
||||
resp = requests.get(url, params=params, timeout=10)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
features: Dict[str, Any] = {}
|
||||
risk: Optional[float] = None
|
||||
|
||||
# direct candidates
|
||||
for candidate in ("road_risk_score", "risk_score", "risk", "score"):
|
||||
if isinstance(data, dict) and candidate in data:
|
||||
try:
|
||||
risk = float(data[candidate])
|
||||
features[candidate] = risk
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# if no direct candidate, collect numeric top-level fields
|
||||
if risk is None and isinstance(data, dict):
|
||||
numeric_fields = {}
|
||||
for k, v in data.items():
|
||||
if isinstance(v, (int, float)):
|
||||
numeric_fields[k] = float(v)
|
||||
features.update(numeric_fields)
|
||||
# try averaging common contributors if present
|
||||
contributors = []
|
||||
for name in ("precipitation", "rain", "snow", "visibility", "wind_speed"):
|
||||
if name in data and isinstance(data[name], (int, float)):
|
||||
contributors.append(float(data[name]))
|
||||
if contributors:
|
||||
# average contributors -> risk proxy
|
||||
risk = float(sum(contributors) / len(contributors))
|
||||
|
||||
# fallback: derive crude risk from /weather
|
||||
if risk is None:
|
||||
try:
|
||||
w = fetch_weather(lat, lon, api_key=key)
|
||||
main = w.get("main", {})
|
||||
wind = w.get("wind", {})
|
||||
weather = w.get("weather", [{}])[0]
|
||||
# heuristic: rain + high wind + low visibility
|
||||
derived = 0.0
|
||||
if isinstance(weather.get("main", ""), str) and "rain" in weather.get("main", "").lower():
|
||||
derived += 1.0
|
||||
if (wind.get("speed") or 0) > 6.0:
|
||||
derived += 0.5
|
||||
if (w.get("visibility") or 10000) < 5000:
|
||||
derived += 1.0
|
||||
risk = float(derived)
|
||||
features.update({
|
||||
"temp": main.get("temp"),
|
||||
"humidity": main.get("humidity"),
|
||||
"wind_speed": wind.get("speed"),
|
||||
"visibility": w.get("visibility"),
|
||||
"weather_main": weather.get("main"),
|
||||
"weather_id": weather.get("id"),
|
||||
})
|
||||
except Exception:
|
||||
# cannot derive anything; set neutral 0.0
|
||||
risk = 0.0
|
||||
|
||||
features["road_risk_score"] = float(risk)
|
||||
return data, features
|
||||
Reference in New Issue
Block a user