Automated Action 1468af1391 Add Weather Data API with OpenWeatherMap integration
- Created FastAPI application with SQLite database integration
- Implemented OpenWeatherMap client with caching
- Added endpoints for current weather, forecasts, and history
- Included comprehensive error handling and validation
- Set up Alembic migrations
- Created detailed README with usage examples

generated with BackendIM... (backend.im)
2025-05-12 14:26:44 +00:00

177 lines
6.7 KiB
Python

import httpx
from datetime import datetime
from typing import Dict, Any, Optional, List, Tuple
from fastapi import HTTPException
from tenacity import retry, stop_after_attempt, wait_exponential
from app.core.config import settings
from app.schemas.weather import WeatherRecordCreate
from app.services.cache import cached
class OpenWeatherMapClient:
def __init__(self):
self.api_key = settings.OPENWEATHERMAP_API_KEY
self.base_url = settings.OPENWEATHERMAP_API_URL
self.headers = {
"Accept": "application/json",
"Content-Type": "application/json"
}
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10)
)
async def _make_request(self, endpoint: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""
Make a request to the OpenWeatherMap API with retry functionality.
"""
params["appid"] = self.api_key
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/{endpoint}",
params=params,
headers=self.headers,
timeout=30.0
)
if response.status_code != 200:
error_detail = response.json().get("message", "Unknown error")
raise HTTPException(
status_code=response.status_code,
detail=f"OpenWeatherMap API error: {error_detail}"
)
return response.json()
@cached("current_weather_city", expire_seconds=settings.CACHE_EXPIRE_IN_SECONDS)
async def get_current_weather_by_city(self, city_name: str, country_code: Optional[str] = None) -> Dict[str, Any]:
"""
Get current weather for a city by name and optional country code.
"""
q = city_name
if country_code:
q = f"{city_name},{country_code}"
params = {
"q": q,
"units": "metric"
}
return await self._make_request("weather", params)
@cached("current_weather_coords", expire_seconds=settings.CACHE_EXPIRE_IN_SECONDS)
async def get_current_weather_by_coordinates(self, lat: float, lon: float) -> Dict[str, Any]:
"""
Get current weather for a location by coordinates.
"""
params = {
"lat": lat,
"lon": lon,
"units": "metric"
}
return await self._make_request("weather", params)
@cached("forecast_city", expire_seconds=settings.CACHE_EXPIRE_IN_SECONDS)
async def get_forecast_by_city(self, city_name: str, country_code: Optional[str] = None) -> Dict[str, Any]:
"""
Get 5-day forecast for a city by name and optional country code.
"""
q = city_name
if country_code:
q = f"{city_name},{country_code}"
params = {
"q": q,
"units": "metric"
}
return await self._make_request("forecast", params)
@cached("forecast_coords", expire_seconds=settings.CACHE_EXPIRE_IN_SECONDS)
async def get_forecast_by_coordinates(self, lat: float, lon: float) -> Dict[str, Any]:
"""
Get 5-day forecast for a location by coordinates.
"""
params = {
"lat": lat,
"lon": lon,
"units": "metric"
}
return await self._make_request("forecast", params)
@staticmethod
def parse_weather_data(data: Dict[str, Any]) -> Tuple[Dict[str, Any], WeatherRecordCreate]:
"""
Parse OpenWeatherMap API response into our data models.
Returns a tuple of (city_data, weather_data)
"""
# Parse city data
city_data = {
"name": data.get("name"),
"country": data.get("sys", {}).get("country"),
"latitude": data.get("coord", {}).get("lat"),
"longitude": data.get("coord", {}).get("lon")
}
# Get the weather data
main_data = data.get("main", {})
weather_data = {
"temperature": main_data.get("temp"),
"feels_like": main_data.get("feels_like"),
"humidity": main_data.get("humidity"),
"pressure": main_data.get("pressure"),
"weather_condition": data.get("weather", [{}])[0].get("main", ""),
"weather_description": data.get("weather", [{}])[0].get("description", ""),
"wind_speed": data.get("wind", {}).get("speed"),
"wind_direction": data.get("wind", {}).get("deg"),
"clouds": data.get("clouds", {}).get("all"),
"rain_1h": data.get("rain", {}).get("1h"),
"snow_1h": data.get("snow", {}).get("1h"),
"timestamp": datetime.fromtimestamp(data.get("dt", 0)),
}
return city_data, WeatherRecordCreate(**weather_data, city_id=0) # City ID will be updated later
@staticmethod
def parse_forecast_data(data: Dict[str, Any]) -> Tuple[Dict[str, Any], List[WeatherRecordCreate]]:
"""
Parse OpenWeatherMap forecast API response into our data models.
Returns a tuple of (city_data, list_of_forecast_items)
"""
city_info = data.get("city", {})
# Parse city data
city_data = {
"name": city_info.get("name"),
"country": city_info.get("country"),
"latitude": city_info.get("coord", {}).get("lat"),
"longitude": city_info.get("coord", {}).get("lon")
}
# Parse forecast list
forecast_items = []
for item in data.get("list", []):
main_data = item.get("main", {})
forecast_item = {
"temperature": main_data.get("temp"),
"feels_like": main_data.get("feels_like"),
"humidity": main_data.get("humidity"),
"pressure": main_data.get("pressure"),
"weather_condition": item.get("weather", [{}])[0].get("main", ""),
"weather_description": item.get("weather", [{}])[0].get("description", ""),
"wind_speed": item.get("wind", {}).get("speed"),
"wind_direction": item.get("wind", {}).get("deg"),
"clouds": item.get("clouds", {}).get("all"),
"rain_1h": item.get("rain", {}).get("1h"),
"snow_1h": item.get("snow", {}).get("1h"),
"timestamp": datetime.fromtimestamp(item.get("dt", 0)),
}
forecast_items.append(WeatherRecordCreate(**forecast_item, city_id=0)) # City ID will be updated later
return city_data, forecast_items
# Create a singleton instance of the client
openweather_client = OpenWeatherMapClient()