""" Weather Controller Handles weather data operations and coordinates between service and API """ import logging from typing import Dict, Any, List, Optional from datetime import datetime, timedelta from services.weather_service import NASAPowerService from models.weather_model import WeatherRequest, WeatherDataModel from utils import create_error_response, create_success_response class WeatherController: """Controller for weather data operations""" def __init__(self, weather_service: NASAPowerService): self.weather_service = weather_service self.logger = logging.getLogger(__name__) def get_weather_data(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Get weather data for specific coordinates and date Args: data: Request data containing coordinates, date, and optional parameters Returns: Weather data response """ try: # Validate required parameters required_fields = ['latitude', 'longitude', 'disaster_date'] missing_fields = [field for field in required_fields if field not in data or data[field] is None] if missing_fields: return create_error_response( f"Missing required fields: {', '.join(missing_fields)}", {"missing_fields": missing_fields} ) # Extract parameters try: latitude = float(data['latitude']) longitude = float(data['longitude']) disaster_date = str(data['disaster_date']) days_before = int(data.get('days_before', 60)) except (ValueError, TypeError) as e: return create_error_response( f"Invalid parameter format: {str(e)}", {"validation_error": str(e)} ) # Create weather request weather_request = WeatherRequest( latitude=latitude, longitude=longitude, disaster_date=disaster_date, days_before=days_before ) # Validate request validation = weather_request.validate() if not validation['valid']: return create_error_response( "Request validation failed", {"validation_errors": validation['errors']} ) self.logger.info(f"Fetching weather data for lat={latitude}, lon={longitude}, " f"disaster_date={disaster_date}, days_before={days_before}") # Fetch weather data success, result = self.weather_service.fetch_weather_data(weather_request) if success: return create_success_response(result) else: return create_error_response( "Failed to fetch weather data", result ) except Exception as e: self.logger.error(f"Weather data error: {str(e)}") return create_error_response( f"Failed to get weather data: {str(e)}" ) def get_weather_time_series(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Get weather data as time series DataFrame Args: data: Request data containing coordinates, date, and optional parameters Returns: Time series weather data response """ try: # Get weather data first weather_result = self.get_weather_data(data) if weather_result.get('status') != 'success': return weather_result # Extract weather data weather_data = weather_result['data']['weather_data'] disaster_date = data['disaster_date'] days_before = int(data.get('days_before', 60)) # Create time series DataFrame df = WeatherDataModel.create_time_series_dataframe( weather_data, disaster_date, days_before ) # Convert DataFrame to dict for JSON response time_series_data = { 'dates': df['date'].tolist(), 'weather_data': { col: df[col].tolist() for col in df.columns if col != 'date' } } return create_success_response({ 'time_series': time_series_data, 'metadata': weather_result['data']['metadata'], 'validation': weather_result['data']['validation'] }) except Exception as e: self.logger.error(f"Time series error: {str(e)}") return create_error_response( f"Failed to create time series: {str(e)}" ) def batch_get_weather_data(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Get weather data for multiple locations Args: data: Request data containing list of location/date combinations Returns: Batch weather data response """ try: # Validate batch request if 'locations' not in data or not isinstance(data['locations'], list): return create_error_response( "Invalid batch request: 'locations' array required" ) locations = data['locations'] if len(locations) > 100: # Limit batch size return create_error_response( "Batch size too large: maximum 100 locations allowed", {"max_allowed": 100, "requested": len(locations)} ) # Create weather requests weather_requests = [] for i, location in enumerate(locations): try: request = WeatherRequest( latitude=float(location['latitude']), longitude=float(location['longitude']), disaster_date=str(location['disaster_date']), days_before=int(location.get('days_before', 60)) ) weather_requests.append(request) except Exception as e: return create_error_response( f"Invalid location at index {i}: {str(e)}", {"location_index": i, "error": str(e)} ) self.logger.info(f"Starting batch weather fetch for {len(weather_requests)} locations") # Batch fetch weather data batch_result = self.weather_service.batch_fetch_weather_data(weather_requests) return create_success_response(batch_result) except Exception as e: self.logger.error(f"Batch weather error: {str(e)}") return create_error_response( f"Failed to process batch weather request: {str(e)}" ) def get_weather_summary(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Get weather data summary statistics Args: data: Request data containing coordinates, date, and optional parameters Returns: Weather summary response with statistics """ try: # Get weather data first weather_result = self.get_weather_data(data) if weather_result.get('status') != 'success': return weather_result weather_data = weather_result['data']['weather_data'] # Calculate summary statistics summary_stats = {} for field_name, values in weather_data.items(): valid_values = [v for v in values if v is not None] if valid_values: summary_stats[field_name] = { 'mean': sum(valid_values) / len(valid_values), 'min': min(valid_values), 'max': max(valid_values), 'count': len(valid_values), 'missing': len([v for v in values if v is None]), 'completeness': len(valid_values) / len(values) * 100 } else: summary_stats[field_name] = { 'mean': None, 'min': None, 'max': None, 'count': 0, 'missing': len(values), 'completeness': 0.0 } return create_success_response({ 'summary_statistics': summary_stats, 'metadata': weather_result['data']['metadata'], 'data_quality': weather_result['data']['validation']['data_quality'] }) except Exception as e: self.logger.error(f"Weather summary error: {str(e)}") return create_error_response( f"Failed to create weather summary: {str(e)}" ) def get_available_fields(self) -> Dict[str, Any]: """Get available weather fields and their descriptions""" try: field_descriptions = { 'temperature_C': 'Temperature at 2 meters (°C)', 'humidity_perc': 'Relative humidity at 2 meters (%)', 'wind_speed_mps': 'Wind speed at 2 meters (m/s)', 'precipitation_mm': 'Precipitation corrected (mm)', 'surface_pressure_hPa': 'Surface pressure (hPa)', 'solar_radiation_wm2': 'Solar radiation (W/m²)', 'temperature_max_C': 'Maximum temperature (°C)', 'temperature_min_C': 'Minimum temperature (°C)', 'specific_humidity_g_kg': 'Specific humidity at 2m (g/kg)', 'dew_point_C': 'Dew point temperature at 2m (°C)', 'wind_speed_10m_mps': 'Wind speed at 10 meters (m/s)', 'cloud_amount_perc': 'Cloud amount (%)', 'sea_level_pressure_hPa': 'Sea level pressure (hPa)', 'surface_soil_wetness_perc': 'Surface soil wetness (%)', 'wind_direction_10m_degrees': 'Wind direction at 10m (degrees)', 'evapotranspiration_wm2': 'Evapotranspiration energy flux (W/m²)', 'root_zone_soil_moisture_perc': 'Root zone soil moisture (%)' } return create_success_response({ 'available_fields': field_descriptions, 'field_count': len(field_descriptions), 'nasa_power_fields': WeatherDataModel.WEATHER_FIELDS, 'service_info': { 'data_source': 'NASA POWER API', 'temporal_resolution': 'daily', 'spatial_resolution': '0.5° x 0.625°', 'coverage': 'global', 'data_lag': '~7 days' } }) except Exception as e: self.logger.error(f"Available fields error: {str(e)}") return create_error_response( f"Failed to get available fields: {str(e)}" ) def get_service_status(self) -> Dict[str, Any]: """Get weather service status and health""" try: service_status = self.weather_service.get_service_status() return create_success_response({ 'controller': 'Weather Controller', 'service': service_status, 'health': 'healthy' if service_status.get('initialized') else 'unhealthy' }) except Exception as e: self.logger.error(f"Service status error: {str(e)}") return create_error_response( f"Failed to get service status: {str(e)}" )