102 lines
3.0 KiB
Python
102 lines
3.0 KiB
Python
"""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
|