# shared/clients/external_client.py """ External Service Client Handles all API calls to the external service (weather and traffic data) """ import httpx import structlog from typing import Dict, Any, Optional, List from .base_service_client import BaseServiceClient from shared.config.base import BaseServiceSettings logger = structlog.get_logger() class ExternalServiceClient(BaseServiceClient): """Client for communicating with the external service""" def __init__(self, config: BaseServiceSettings, calling_service_name: str = "unknown"): super().__init__(calling_service_name, config) self.service_url = config.EXTERNAL_SERVICE_URL def get_service_base_path(self) -> str: return "/api/v1" # ================================================================ # WEATHER DATA # ================================================================ async def get_weather_historical( self, tenant_id: str, start_date: str, end_date: str, latitude: Optional[float] = None, longitude: Optional[float] = None ) -> Optional[List[Dict[str, Any]]]: """ Get weather data for a date range and location Uses POST request as per original implementation """ # Prepare request payload with proper date handling payload = { "start_date": start_date, # Already in ISO format from calling code "end_date": end_date, # Already in ISO format from calling code "latitude": latitude or 40.4168, # Default Madrid coordinates "longitude": longitude or -3.7038 } logger.info(f"Weather request payload: {payload}", tenant_id=tenant_id) # Use POST request with extended timeout result = await self._make_request( "POST", "weather/historical", tenant_id=tenant_id, data=payload, timeout=2000.0 # Match original timeout ) if result: logger.info(f"Successfully fetched {len(result)} weather records") return result else: logger.error("Failed to fetch weather data") return [] async def get_weather_forecast( self, tenant_id: str, days: int = 1, latitude: Optional[float] = None, longitude: Optional[float] = None ) -> Optional[List[Dict[str, Any]]]: """ Get weather forecast for location FIXED: Uses GET request with query parameters as expected by the weather API """ payload = { "latitude": latitude or 40.4168, # Default Madrid coordinates "longitude": longitude or -3.7038, "days": days } logger.info(f"Weather forecast request params: {payload}", tenant_id=tenant_id) result = await self._make_request( "POST", "weather/forecast", tenant_id=tenant_id, data=payload, timeout=200.0 ) if result: logger.info(f"Successfully fetched weather forecast for {days} days") return result else: logger.error("Failed to fetch weather forecast") return [] # ================================================================ # TRAFFIC DATA # ================================================================ async def get_traffic_data( self, tenant_id: str, start_date: str, end_date: str, latitude: Optional[float] = None, longitude: Optional[float] = None ) -> Optional[List[Dict[str, Any]]]: """ Get traffic data for a date range and location Uses POST request with extended timeout for Madrid traffic data processing """ # Prepare request payload payload = { "start_date": start_date, # Already in ISO format from calling code "end_date": end_date, # Already in ISO format from calling code "latitude": latitude or 40.4168, # Default Madrid coordinates "longitude": longitude or -3.7038 } logger.info(f"Traffic request payload: {payload}", tenant_id=tenant_id) # Madrid traffic data can take 5-10 minutes to download and process traffic_timeout = httpx.Timeout( connect=30.0, # Connection timeout read=600.0, # Read timeout: 10 minutes (was 30s) write=30.0, # Write timeout pool=30.0 # Pool timeout ) # Use POST request with extended timeout logger.info("Making traffic data request", url="traffic/historical", tenant_id=tenant_id, timeout=traffic_timeout.read) result = await self._make_request( "POST", "traffic/historical", tenant_id=tenant_id, data=payload, timeout=traffic_timeout ) if result: logger.info(f"Successfully fetched {len(result)} traffic records") return result else: logger.error("Failed to fetch traffic data - _make_request returned None") logger.error("This could be due to: network timeout, HTTP error, authentication failure, or service unavailable") return None async def get_stored_traffic_data_for_training( self, tenant_id: str, start_date: str, end_date: str, latitude: Optional[float] = None, longitude: Optional[float] = None ) -> Optional[List[Dict[str, Any]]]: """ Get stored traffic data specifically for model training/re-training This method prioritizes database-stored data over API calls """ # Prepare request payload payload = { "start_date": start_date, "end_date": end_date, "latitude": latitude or 40.4168, # Default Madrid coordinates "longitude": longitude or -3.7038, "stored_only": True # Flag to indicate we want stored data only } logger.info(f"Training traffic data request: {payload}", tenant_id=tenant_id) # Standard timeout since we're only querying the database training_timeout = httpx.Timeout( connect=30.0, read=120.0, # 2 minutes should be enough for database query write=30.0, pool=30.0 ) result = await self._make_request( "POST", "traffic/stored", # New endpoint for stored traffic data tenant_id=tenant_id, data=payload, timeout=training_timeout ) if result: logger.info(f"Successfully retrieved {len(result)} stored traffic records for training") return result else: logger.warning("No stored traffic data available for training") return None