
- 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)
177 lines
6.7 KiB
Python
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() |