init commit

This commit is contained in:
2026-01-26 15:08:24 -06:00
commit 67225a725a
33 changed files with 3350 additions and 0 deletions

View 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