Fix issues
This commit is contained in:
0
services/data/app/external/__init__.py
vendored
Normal file
0
services/data/app/external/__init__.py
vendored
Normal file
254
services/data/app/external/aemet.py
vendored
Normal file
254
services/data/app/external/aemet.py
vendored
Normal file
@@ -0,0 +1,254 @@
|
||||
# ================================================================
|
||||
# services/data/app/external/aemet.py
|
||||
# ================================================================
|
||||
"""AEMET (Spanish Weather Service) API client"""
|
||||
|
||||
import math
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
import structlog
|
||||
|
||||
from app.external.base_client import BaseAPIClient
|
||||
from app.core.config import settings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
class AEMETClient(BaseAPIClient):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
base_url="https://opendata.aemet.es/opendata/api",
|
||||
api_key=settings.AEMET_API_KEY
|
||||
)
|
||||
|
||||
async def get_current_weather(self, latitude: float, longitude: float) -> Optional[Dict[str, Any]]:
|
||||
"""Get current weather for coordinates"""
|
||||
try:
|
||||
# Find nearest station
|
||||
station_id = await self._get_nearest_station(latitude, longitude)
|
||||
if not station_id:
|
||||
logger.warning("No weather station found", lat=latitude, lon=longitude)
|
||||
return await self._generate_synthetic_weather()
|
||||
|
||||
# Get current weather from station
|
||||
endpoint = f"/observacion/convencional/datos/estacion/{station_id}"
|
||||
response = await self._get(endpoint)
|
||||
|
||||
if response and response.get("datos"):
|
||||
# Parse AEMET response
|
||||
weather_data = response["datos"][0] if response["datos"] else {}
|
||||
return self._parse_weather_data(weather_data)
|
||||
|
||||
# Fallback to synthetic data
|
||||
return await self._generate_synthetic_weather()
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get current weather", error=str(e))
|
||||
return await self._generate_synthetic_weather()
|
||||
|
||||
async def get_forecast(self, latitude: float, longitude: float, days: int = 7) -> List[Dict[str, Any]]:
|
||||
"""Get weather forecast for coordinates"""
|
||||
try:
|
||||
# Get municipality code for location
|
||||
municipality_code = await self._get_municipality_code(latitude, longitude)
|
||||
if not municipality_code:
|
||||
return await self._generate_synthetic_forecast(days)
|
||||
|
||||
# Get forecast
|
||||
endpoint = f"/prediccion/especifica/municipio/diaria/{municipality_code}"
|
||||
response = await self._get(endpoint)
|
||||
|
||||
if response and response.get("datos"):
|
||||
return self._parse_forecast_data(response["datos"], days)
|
||||
|
||||
# Fallback to synthetic data
|
||||
return await self._generate_synthetic_forecast(days)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get weather forecast", error=str(e))
|
||||
return await self._generate_synthetic_forecast(days)
|
||||
|
||||
async def get_historical_weather(self,
|
||||
latitude: float,
|
||||
longitude: float,
|
||||
start_date: datetime,
|
||||
end_date: datetime) -> List[Dict[str, Any]]:
|
||||
"""Get historical weather data"""
|
||||
try:
|
||||
# For now, generate synthetic historical data
|
||||
# In production, this would use AEMET historical data API
|
||||
return await self._generate_synthetic_historical(start_date, end_date)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get historical weather", error=str(e))
|
||||
return []
|
||||
|
||||
async def _get_nearest_station(self, latitude: float, longitude: float) -> Optional[str]:
|
||||
"""Find nearest weather station"""
|
||||
try:
|
||||
# Madrid area stations (simplified)
|
||||
madrid_stations = {
|
||||
"3195": {"lat": 40.4168, "lon": -3.7038, "name": "Madrid Centro"},
|
||||
"3196": {"lat": 40.4518, "lon": -3.7246, "name": "Madrid Norte"},
|
||||
"3197": {"lat": 40.3833, "lon": -3.7167, "name": "Madrid Sur"}
|
||||
}
|
||||
|
||||
closest_station = None
|
||||
min_distance = float('inf')
|
||||
|
||||
for station_id, station_data in madrid_stations.items():
|
||||
distance = self._calculate_distance(
|
||||
latitude, longitude,
|
||||
station_data["lat"], station_data["lon"]
|
||||
)
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
closest_station = station_id
|
||||
|
||||
return closest_station
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to find nearest station", error=str(e))
|
||||
return None
|
||||
|
||||
async def _get_municipality_code(self, latitude: float, longitude: float) -> Optional[str]:
|
||||
"""Get municipality code for coordinates"""
|
||||
# Madrid municipality code
|
||||
if self._is_in_madrid_area(latitude, longitude):
|
||||
return "28079" # Madrid municipality code
|
||||
return None
|
||||
|
||||
def _is_in_madrid_area(self, latitude: float, longitude: float) -> bool:
|
||||
"""Check if coordinates are in Madrid area"""
|
||||
# Madrid approximate bounds
|
||||
return (40.3 <= latitude <= 40.6) and (-3.9 <= longitude <= -3.5)
|
||||
|
||||
def _calculate_distance(self, lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
||||
"""Calculate distance between two coordinates in km"""
|
||||
R = 6371 # Earth's radius in km
|
||||
|
||||
dlat = math.radians(lat2 - lat1)
|
||||
dlon = math.radians(lon2 - lon1)
|
||||
|
||||
a = (math.sin(dlat/2) * math.sin(dlat/2) +
|
||||
math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) *
|
||||
math.sin(dlon/2) * math.sin(dlon/2))
|
||||
|
||||
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
|
||||
distance = R * c
|
||||
|
||||
return distance
|
||||
|
||||
def _parse_weather_data(self, data: Dict) -> Dict[str, Any]:
|
||||
"""Parse AEMET weather data format"""
|
||||
return {
|
||||
"date": datetime.now(),
|
||||
"temperature": data.get("ta", 15.0), # Temperature
|
||||
"precipitation": data.get("prec", 0.0), # Precipitation
|
||||
"humidity": data.get("hr", 50.0), # Humidity
|
||||
"wind_speed": data.get("vv", 10.0), # Wind speed
|
||||
"pressure": data.get("pres", 1013.0), # Pressure
|
||||
"description": "Partly cloudy",
|
||||
"source": "aemet"
|
||||
}
|
||||
|
||||
def _parse_forecast_data(self, data: List, days: int) -> List[Dict[str, Any]]:
|
||||
"""Parse AEMET forecast data"""
|
||||
forecast = []
|
||||
base_date = datetime.now().date()
|
||||
|
||||
for i in range(min(days, len(data))):
|
||||
forecast_date = base_date + timedelta(days=i)
|
||||
day_data = data[i] if i < len(data) else {}
|
||||
|
||||
forecast.append({
|
||||
"forecast_date": datetime.combine(forecast_date, datetime.min.time()),
|
||||
"generated_at": datetime.now(),
|
||||
"temperature": day_data.get("temperatura", 15.0),
|
||||
"precipitation": day_data.get("precipitacion", 0.0),
|
||||
"humidity": day_data.get("humedad", 50.0),
|
||||
"wind_speed": day_data.get("viento", 10.0),
|
||||
"description": day_data.get("descripcion", "Partly cloudy"),
|
||||
"source": "aemet"
|
||||
})
|
||||
|
||||
return forecast
|
||||
|
||||
async def _generate_synthetic_weather(self) -> Dict[str, Any]:
|
||||
"""Generate realistic synthetic weather for Madrid"""
|
||||
now = datetime.now()
|
||||
month = now.month
|
||||
hour = now.hour
|
||||
|
||||
# Madrid climate simulation
|
||||
base_temp = 5 + (month - 1) * 2.5 # Seasonal variation
|
||||
temp_variation = math.sin((hour - 6) * math.pi / 12) * 8 # Daily variation
|
||||
temperature = base_temp + temp_variation
|
||||
|
||||
# Rain probability (higher in winter)
|
||||
rain_prob = 0.3 if month in [11, 12, 1, 2, 3] else 0.1
|
||||
precipitation = 2.5 if hash(now.date()) % 100 < rain_prob * 100 else 0.0
|
||||
|
||||
return {
|
||||
"date": now,
|
||||
"temperature": round(temperature, 1),
|
||||
"precipitation": precipitation,
|
||||
"humidity": 45 + (month % 6) * 5,
|
||||
"wind_speed": 8 + (hour % 12),
|
||||
"pressure": 1013 + math.sin(now.day * 0.2) * 15,
|
||||
"description": "Lluvioso" if precipitation > 0 else "Soleado",
|
||||
"source": "synthetic"
|
||||
}
|
||||
|
||||
async def _generate_synthetic_forecast(self, days: int) -> List[Dict[str, Any]]:
|
||||
"""Generate synthetic forecast data"""
|
||||
forecast = []
|
||||
base_date = datetime.now().date()
|
||||
|
||||
for i in range(days):
|
||||
forecast_date = base_date + timedelta(days=i)
|
||||
|
||||
# Seasonal temperature
|
||||
month = forecast_date.month
|
||||
base_temp = 5 + (month - 1) * 2.5
|
||||
temp_variation = (i % 7 - 3) * 2 # Weekly variation
|
||||
|
||||
forecast.append({
|
||||
"forecast_date": datetime.combine(forecast_date, datetime.min.time()),
|
||||
"generated_at": datetime.now(),
|
||||
"temperature": round(base_temp + temp_variation, 1),
|
||||
"precipitation": 2.0 if i % 5 == 0 else 0.0,
|
||||
"humidity": 50 + (i % 30),
|
||||
"wind_speed": 10 + (i % 15),
|
||||
"description": "Lluvioso" if i % 5 == 0 else "Soleado",
|
||||
"source": "synthetic"
|
||||
})
|
||||
|
||||
return forecast
|
||||
|
||||
async def _generate_synthetic_historical(self, start_date: datetime, end_date: datetime) -> List[Dict[str, Any]]:
|
||||
"""Generate synthetic historical weather data"""
|
||||
historical_data = []
|
||||
current_date = start_date
|
||||
|
||||
while current_date <= end_date:
|
||||
month = current_date.month
|
||||
base_temp = 5 + (month - 1) * 2.5
|
||||
|
||||
# Add some randomness based on date
|
||||
temp_variation = math.sin(current_date.day * 0.3) * 5
|
||||
|
||||
historical_data.append({
|
||||
"date": current_date,
|
||||
"temperature": round(base_temp + temp_variation, 1),
|
||||
"precipitation": 1.5 if current_date.day % 7 == 0 else 0.0,
|
||||
"humidity": 45 + (current_date.day % 40),
|
||||
"wind_speed": 8 + (current_date.day % 20),
|
||||
"pressure": 1013 + math.sin(current_date.day * 0.2) * 20,
|
||||
"description": "Variable",
|
||||
"source": "synthetic"
|
||||
})
|
||||
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
return historical_data
|
||||
67
services/data/app/external/base_client.py
vendored
Normal file
67
services/data/app/external/base_client.py
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
# ================================================================
|
||||
# services/data/app/external/base_client.py
|
||||
# ================================================================
|
||||
"""Base HTTP client for external APIs"""
|
||||
|
||||
import httpx
|
||||
from typing import Dict, Any, Optional
|
||||
import structlog
|
||||
from datetime import datetime
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
class BaseAPIClient:
|
||||
|
||||
def __init__(self, base_url: str, api_key: Optional[str] = None):
|
||||
self.base_url = base_url
|
||||
self.api_key = api_key
|
||||
self.timeout = httpx.Timeout(30.0)
|
||||
|
||||
async def _get(self, endpoint: str, params: Optional[Dict] = None, headers: Optional[Dict] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Make GET request"""
|
||||
try:
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
|
||||
# Add API key to headers if available
|
||||
request_headers = headers or {}
|
||||
if self.api_key:
|
||||
request_headers["Authorization"] = f"Bearer {self.api_key}"
|
||||
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.get(url, params=params, headers=request_headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error("HTTP error", status_code=e.response.status_code, url=url)
|
||||
return None
|
||||
except httpx.RequestError as e:
|
||||
logger.error("Request error", error=str(e), url=url)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error("Unexpected error", error=str(e), url=url)
|
||||
return None
|
||||
|
||||
async def _post(self, endpoint: str, data: Optional[Dict] = None, headers: Optional[Dict] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Make POST request"""
|
||||
try:
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
|
||||
request_headers = headers or {}
|
||||
if self.api_key:
|
||||
request_headers["Authorization"] = f"Bearer {self.api_key}"
|
||||
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.post(url, json=data, headers=request_headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error("HTTP error", status_code=e.response.status_code, url=url)
|
||||
return None
|
||||
except httpx.RequestError as e:
|
||||
logger.error("Request error", error=str(e), url=url)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error("Unexpected error", error=str(e), url=url)
|
||||
return None
|
||||
235
services/data/app/external/madrid_opendata.py
vendored
Normal file
235
services/data/app/external/madrid_opendata.py
vendored
Normal file
@@ -0,0 +1,235 @@
|
||||
# ================================================================
|
||||
# services/data/app/external/madrid_opendata.py
|
||||
# ================================================================
|
||||
"""Madrid Open Data API client for traffic and events"""
|
||||
|
||||
import math
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
import structlog
|
||||
|
||||
from app.external.base_client import BaseAPIClient
|
||||
from app.core.config import settings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
class MadridOpenDataClient(BaseAPIClient):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
base_url="https://datos.madrid.es/egob/catalogo",
|
||||
api_key=settings.MADRID_OPENDATA_API_KEY
|
||||
)
|
||||
|
||||
async def get_current_traffic(self, latitude: float, longitude: float) -> Optional[Dict[str, Any]]:
|
||||
"""Get current traffic data for location"""
|
||||
try:
|
||||
# In production, this would call real Madrid Open Data API
|
||||
# For now, generate realistic synthetic data
|
||||
return await self._generate_synthetic_traffic(latitude, longitude)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get current traffic", error=str(e))
|
||||
return None
|
||||
|
||||
async def get_historical_traffic(self,
|
||||
latitude: float,
|
||||
longitude: float,
|
||||
start_date: datetime,
|
||||
end_date: datetime) -> List[Dict[str, Any]]:
|
||||
"""Get historical traffic data"""
|
||||
try:
|
||||
# Generate synthetic historical traffic data
|
||||
return await self._generate_historical_traffic(latitude, longitude, start_date, end_date)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get historical traffic", error=str(e))
|
||||
return []
|
||||
|
||||
async def get_events(self, latitude: float, longitude: float, radius_km: float = 5.0) -> List[Dict[str, Any]]:
|
||||
"""Get events near location"""
|
||||
try:
|
||||
# In production, would fetch real events from Madrid Open Data
|
||||
return await self._generate_synthetic_events(latitude, longitude)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get events", error=str(e))
|
||||
return []
|
||||
|
||||
async def _generate_synthetic_traffic(self, latitude: float, longitude: float) -> Dict[str, Any]:
|
||||
"""Generate realistic Madrid traffic data"""
|
||||
now = datetime.now()
|
||||
hour = now.hour
|
||||
is_weekend = now.weekday() >= 5
|
||||
|
||||
# Base traffic volume
|
||||
base_traffic = 100
|
||||
|
||||
# Madrid traffic patterns
|
||||
if not is_weekend: # Weekdays
|
||||
if 7 <= hour <= 9: # Morning rush
|
||||
traffic_multiplier = 2.2
|
||||
congestion = "high"
|
||||
elif 18 <= hour <= 20: # Evening rush
|
||||
traffic_multiplier = 2.5
|
||||
congestion = "high"
|
||||
elif 12 <= hour <= 14: # Lunch time
|
||||
traffic_multiplier = 1.6
|
||||
congestion = "medium"
|
||||
elif 6 <= hour <= 22: # Daytime
|
||||
traffic_multiplier = 1.2
|
||||
congestion = "medium"
|
||||
else: # Night
|
||||
traffic_multiplier = 0.4
|
||||
congestion = "low"
|
||||
else: # Weekends
|
||||
if 11 <= hour <= 14: # Weekend shopping
|
||||
traffic_multiplier = 1.4
|
||||
congestion = "medium"
|
||||
elif 19 <= hour <= 22: # Weekend evening
|
||||
traffic_multiplier = 1.6
|
||||
congestion = "medium"
|
||||
else:
|
||||
traffic_multiplier = 0.8
|
||||
congestion = "low"
|
||||
|
||||
# Calculate pedestrian traffic (higher during meal times and school hours)
|
||||
pedestrian_base = 150
|
||||
if 13 <= hour <= 15: # Lunch time
|
||||
pedestrian_multiplier = 2.8
|
||||
elif hour == 14: # School pickup time
|
||||
pedestrian_multiplier = 3.5
|
||||
elif 20 <= hour <= 22: # Dinner time
|
||||
pedestrian_multiplier = 2.2
|
||||
elif 8 <= hour <= 9: # Morning commute
|
||||
pedestrian_multiplier = 2.0
|
||||
else:
|
||||
pedestrian_multiplier = 1.0
|
||||
|
||||
traffic_volume = int(base_traffic * traffic_multiplier)
|
||||
pedestrian_count = int(pedestrian_base * pedestrian_multiplier)
|
||||
|
||||
# Average speed based on congestion
|
||||
speed_map = {"low": 45, "medium": 25, "high": 15}
|
||||
average_speed = speed_map[congestion] + (hash(f"{latitude}{longitude}") % 10 - 5)
|
||||
|
||||
return {
|
||||
"date": now,
|
||||
"traffic_volume": traffic_volume,
|
||||
"pedestrian_count": pedestrian_count,
|
||||
"congestion_level": congestion,
|
||||
"average_speed": max(10, average_speed), # Minimum 10 km/h
|
||||
"source": "madrid_opendata"
|
||||
}
|
||||
|
||||
async def _generate_historical_traffic(self,
|
||||
latitude: float,
|
||||
longitude: float,
|
||||
start_date: datetime,
|
||||
end_date: datetime) -> List[Dict[str, Any]]:
|
||||
"""Generate synthetic historical traffic data"""
|
||||
historical_data = []
|
||||
current_date = start_date
|
||||
|
||||
while current_date <= end_date:
|
||||
hour = current_date.hour
|
||||
is_weekend = current_date.weekday() >= 5
|
||||
|
||||
# Base patterns similar to current traffic
|
||||
base_traffic = 100
|
||||
|
||||
if not is_weekend:
|
||||
if 7 <= hour <= 9 or 18 <= hour <= 20:
|
||||
traffic_multiplier = 2.0 + (current_date.day % 5) * 0.1
|
||||
elif 12 <= hour <= 14:
|
||||
traffic_multiplier = 1.5
|
||||
else:
|
||||
traffic_multiplier = 1.0
|
||||
else:
|
||||
traffic_multiplier = 0.7 + (current_date.day % 3) * 0.2
|
||||
|
||||
# Add seasonal variations
|
||||
month = current_date.month
|
||||
seasonal_factor = 1.0
|
||||
if month in [12, 1]: # Holiday season
|
||||
seasonal_factor = 0.8
|
||||
elif month in [7, 8]: # Summer vacation
|
||||
seasonal_factor = 0.9
|
||||
|
||||
traffic_volume = int(base_traffic * traffic_multiplier * seasonal_factor)
|
||||
|
||||
# Determine congestion level
|
||||
if traffic_volume > 160:
|
||||
congestion_level = "high"
|
||||
avg_speed = 15
|
||||
elif traffic_volume > 120:
|
||||
congestion_level = "medium"
|
||||
avg_speed = 25
|
||||
else:
|
||||
congestion_level = "low"
|
||||
avg_speed = 40
|
||||
|
||||
# Pedestrian count
|
||||
pedestrian_base = 150
|
||||
if 13 <= hour <= 15:
|
||||
pedestrian_multiplier = 2.5
|
||||
elif hour == 14:
|
||||
pedestrian_multiplier = 3.0
|
||||
else:
|
||||
pedestrian_multiplier = 1.0
|
||||
|
||||
historical_data.append({
|
||||
"date": current_date,
|
||||
"traffic_volume": traffic_volume,
|
||||
"pedestrian_count": int(pedestrian_base * pedestrian_multiplier),
|
||||
"congestion_level": congestion_level,
|
||||
"average_speed": avg_speed + (current_date.day % 10 - 5),
|
||||
"source": "madrid_opendata"
|
||||
})
|
||||
|
||||
current_date += timedelta(hours=1)
|
||||
|
||||
return historical_data
|
||||
|
||||
async def _generate_synthetic_events(self, latitude: float, longitude: float) -> List[Dict[str, Any]]:
|
||||
"""Generate synthetic Madrid events"""
|
||||
events = []
|
||||
base_date = datetime.now().date()
|
||||
|
||||
# Generate some sample events
|
||||
sample_events = [
|
||||
{
|
||||
"name": "Mercado de San Miguel",
|
||||
"type": "market",
|
||||
"impact_level": "medium",
|
||||
"distance_km": 1.2
|
||||
},
|
||||
{
|
||||
"name": "Concierto en el Retiro",
|
||||
"type": "concert",
|
||||
"impact_level": "high",
|
||||
"distance_km": 2.5
|
||||
},
|
||||
{
|
||||
"name": "Partido Real Madrid",
|
||||
"type": "sports",
|
||||
"impact_level": "high",
|
||||
"distance_km": 8.0
|
||||
}
|
||||
]
|
||||
|
||||
for i, event in enumerate(sample_events):
|
||||
event_date = base_date + timedelta(days=i + 1)
|
||||
events.append({
|
||||
"id": f"event_{i+1}",
|
||||
"name": event["name"],
|
||||
"date": datetime.combine(event_date, datetime.min.time()),
|
||||
"type": event["type"],
|
||||
"impact_level": event["impact_level"],
|
||||
"distance_km": event["distance_km"],
|
||||
"latitude": latitude + (hash(event["name"]) % 100 - 50) / 1000,
|
||||
"longitude": longitude + (hash(event["name"]) % 100 - 50) / 1000,
|
||||
"source": "madrid_opendata"
|
||||
})
|
||||
|
||||
return events
|
||||
Reference in New Issue
Block a user