"""Weather service for fetching forecasts from VisualCrossing API.""" from typing import Optional from urllib.parse import quote from app.models.weather import WeatherForecast from app.utils.http_client import HttpClient from app.utils.logging_config import get_logger class WeatherServiceError(Exception): """Raised when the weather service encounters an error.""" pass class WeatherService: """Client for the VisualCrossing Weather API.""" BASE_URL = "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline" def __init__( self, api_key: str, http_client: Optional[HttpClient] = None, ) -> None: """Initialize the weather service. Args: api_key: VisualCrossing API key. http_client: Optional HTTP client instance. Creates one if not provided. """ if not api_key: raise WeatherServiceError("VisualCrossing API key is required") self.api_key = api_key self.http_client = http_client or HttpClient() self.logger = get_logger(__name__) def get_forecast( self, location: str, hours_ahead: int = 24, unit_group: str = "us", ) -> WeatherForecast: """Fetch weather forecast for a location. Args: location: Location string (e.g., "viola,tn" or ZIP code). hours_ahead: Number of hours of forecast to retrieve. unit_group: Unit system ("us" for imperial, "metric" for metric). Returns: WeatherForecast with hourly data and any active alerts. Raises: WeatherServiceError: If the API request fails. """ self.logger.info( "fetching_forecast", location=location, hours_ahead=hours_ahead, ) # Build API URL encoded_location = quote(location, safe="") url = f"{self.BASE_URL}/{encoded_location}" params = { "unitGroup": unit_group, "include": "days,hours,alerts,current,events", "key": self.api_key, "contentType": "json", } response = self.http_client.get(url, params=params) if not response.success: self.logger.error( "forecast_fetch_failed", status_code=response.status_code, error=response.text, ) raise WeatherServiceError( f"Failed to fetch forecast: {response.status_code} - {response.text}" ) if response.json_data is None: self.logger.error("forecast_invalid_json", response_text=response.text[:200]) raise WeatherServiceError("Invalid JSON response from weather API") forecast = WeatherForecast.from_api_data(response.json_data, hours_ahead) self.logger.info( "forecast_fetched", location=forecast.resolved_address, hourly_count=len(forecast.hourly_forecasts), alert_count=len(forecast.alerts), ) return forecast