services.py•5.73 kB
# app/services.py
import httpx
from typing import Dict, Any, Optional
from app.core.config import get_settings # To access API key and other settings
# Get settings instance
settings = get_settings()
OPENWEATHERMAP_API_URL = "http://api.openweathermap.org/data/2.5/weather"
class WeatherServiceError(Exception):
"""Custom exception for weather service errors."""
def __init__(self, message: str, status_code: Optional[int] = None):
super().__init__(message)
self.status_code = status_code
async def fetch_weather_data_from_provider(location: str) -> Dict[str, Any]:
"""
Asynchronously fetches and processes weather data from OpenWeatherMap API.
Args:
location: The city name for which to fetch weather data.
Returns:
A dictionary containing processed weather data.
Raises:
WeatherServiceError: If there's an issue fetching or processing weather data.
"""
if not settings.OPENWEATHERMAP_API_KEY or settings.OPENWEATHERMAP_API_KEY == "YOUR_ACTUAL_API_KEY_HERE":
# This check is crucial. Do not proceed without a valid API key.
# Log this error server-side for a real application
print("CRITICAL ERROR: OpenWeatherMap API key is not configured correctly.")
raise WeatherServiceError("Weather service is not configured on the server.", status_code=500)
params = {
'q': location,
'appid': settings.OPENWEATHERMAP_API_KEY,
'units': 'metric' # Get temperature in Celsius, wind speed in meter/sec
}
async with httpx.AsyncClient() as client:
try:
response = await client.get(OPENWEATHERMAP_API_URL, params=params)
response.raise_for_status() # Raises an HTTPStatusError for bad responses (4XX or 5XX)
raw_data = response.json()
# Basic check for expected data
if not raw_data or 'main' not in raw_data or 'weather' not in raw_data or not raw_data['weather']:
# This WeatherServiceError should propagate with status_code=502
raise WeatherServiceError(f"Incomplete data received from weather API for {location}.", status_code=502)
# Process and extract relevant information
temp_celsius = raw_data.get('main', {}).get('temp')
humidity = raw_data.get('main', {}).get('humidity')
pressure_hpa = raw_data.get('main', {}).get('pressure')
condition = raw_data.get('weather', [{}])[0].get('main', 'N/A')
description = raw_data.get('weather', [{}])[0].get('description', 'N/A').capitalize()
wind_speed_mps = raw_data.get('wind', {}).get('speed') # meters per second
# Convert wind speed from m/s to km/h
wind_speed_kph = (wind_speed_mps * 3.6) if wind_speed_mps is not None else None
# Convert temperature from Celsius to Fahrenheit
temp_fahrenheit = (temp_celsius * 9/5) + 32 if temp_celsius is not None else None
processed_data = {
"location": raw_data.get('name', location), # Use name from API if available
"temperature_celsius": temp_celsius,
"temperature_fahrenheit": temp_fahrenheit,
"condition": condition,
"description": description,
"humidity_percent": humidity,
"wind_kph": wind_speed_kph,
"pressure_hpa": pressure_hpa,
}
return processed_data
except httpx.HTTPStatusError as e:
# Log the error for server-side debugging in a real app: print(f"HTTPStatusError: {e.response.status_code} - {e.response.text}")
if e.response.status_code == 401: # Unauthorized
raise WeatherServiceError("Invalid API key or subscription issue with the weather service.", status_code=e.response.status_code)
elif e.response.status_code == 404: # Not Found
raise WeatherServiceError(f"Weather data not found for location: {location}.", status_code=e.response.status_code)
elif e.response.status_code == 429: # Too Many Requests
raise WeatherServiceError("Rate limit exceeded with the weather service. Please try again later.", status_code=e.response.status_code)
else: # Other HTTP errors
raise WeatherServiceError(f"Error fetching data from weather service: HTTP {e.response.status_code}.", status_code=e.response.status_code)
except httpx.RequestError as e:
# Network errors or other request issues (e.g., DNS failure, connection refused)
# Log the error: print(f"RequestError: {e}")
raise WeatherServiceError(f"Could not connect to the weather service. Please check network connectivity.", status_code=503) # Service Unavailable
# ---- THIS IS THE KEY CHANGE ----
except WeatherServiceError as wse:
# If a WeatherServiceError was already explicitly raised within the try block
# (e.g., for incomplete data with status_code=502),
# re-raise it as is to preserve its original status code and message.
raise wse
# ---- END KEY CHANGE ----
except Exception as e:
# Catch any *other* unexpected errors during the API call or data processing
# that weren't an httpx error or an already specific WeatherServiceError.
# Log the error: print(f"Unexpected error in weather service: {type(e).__name__} - {e}")
raise WeatherServiceError(f"An unexpected error occurred while fetching weather data: {str(e)}", status_code=500) # Internal Server Error