init commit
This commit is contained in:
101
app/services/weather_service.py
Normal file
101
app/services/weather_service.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user