Files
bakery-ia/services/external/app/api/weather.py

257 lines
9.9 KiB
Python
Raw Normal View History

2025-08-12 18:17:30 +02:00
# services/external/app/api/weather.py
"""
Weather API Endpoints
"""
2025-07-18 11:51:43 +02:00
2025-07-26 18:46:52 +02:00
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks, Path
2025-07-21 13:09:30 +02:00
from typing import List, Optional, Dict, Any
from datetime import datetime, date
2025-07-19 12:09:10 +02:00
import structlog
2025-07-26 18:46:52 +02:00
from uuid import UUID
2025-07-18 11:51:43 +02:00
2025-08-12 18:17:30 +02:00
from app.schemas.weather import (
2025-07-21 13:09:30 +02:00
WeatherDataResponse,
2025-07-30 00:23:05 +02:00
WeatherForecastResponse,
2025-08-12 18:17:30 +02:00
WeatherForecastRequest,
2025-08-18 20:50:41 +02:00
HistoricalWeatherRequest,
HourlyForecastRequest,
HourlyForecastResponse
2025-07-21 13:09:30 +02:00
)
2025-07-18 11:51:43 +02:00
from app.services.weather_service import WeatherService
2025-07-19 12:51:28 +02:00
from app.services.messaging import publish_weather_updated
2025-07-21 13:09:30 +02:00
# Import unified authentication from shared library
from shared.auth.decorators import (
get_current_user_dep,
get_current_tenant_id_dep
2025-07-18 11:51:43 +02:00
)
2025-07-27 20:20:09 +02:00
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
2025-07-26 18:46:52 +02:00
router = APIRouter(tags=["weather"])
2025-07-19 12:09:10 +02:00
logger = structlog.get_logger()
2025-07-27 20:20:09 +02:00
weather_service = WeatherService()
2025-07-18 11:51:43 +02:00
2025-07-27 20:20:09 +02:00
@router.get("/tenants/{tenant_id}/weather/current", response_model=WeatherDataResponse)
2025-07-18 11:51:43 +02:00
async def get_current_weather(
latitude: float = Query(..., description="Latitude"),
longitude: float = Query(..., description="Longitude"),
2025-07-26 18:46:52 +02:00
tenant_id: UUID = Path(..., description="Tenant ID"),
2025-07-21 13:09:30 +02:00
current_user: Dict[str, Any] = Depends(get_current_user_dep),
2025-07-18 11:51:43 +02:00
):
2025-07-21 13:09:30 +02:00
"""Get current weather data for location"""
2025-07-18 11:51:43 +02:00
try:
2025-07-21 13:09:30 +02:00
logger.debug("Getting current weather",
lat=latitude,
lon=longitude,
tenant_id=tenant_id,
user_id=current_user["user_id"])
2025-07-19 12:09:10 +02:00
2025-07-18 11:51:43 +02:00
weather = await weather_service.get_current_weather(latitude, longitude)
2025-07-19 12:09:10 +02:00
2025-07-18 11:51:43 +02:00
if not weather:
2025-08-18 21:14:42 +02:00
raise HTTPException(status_code=503, detail="Weather service temporarily unavailable")
2025-07-18 11:51:43 +02:00
2025-07-21 13:09:30 +02:00
# Publish event
try:
await publish_weather_updated({
"type": "current_weather_requested",
"tenant_id": tenant_id,
"latitude": latitude,
"longitude": longitude,
"requested_by": current_user["user_id"],
"timestamp": datetime.utcnow().isoformat()
})
except Exception as e:
logger.warning("Failed to publish weather event", error=str(e))
2025-07-18 11:51:43 +02:00
return weather
2025-07-19 12:09:10 +02:00
except HTTPException:
raise
2025-07-18 11:51:43 +02:00
except Exception as e:
2025-07-21 13:09:30 +02:00
logger.error("Failed to get current weather", error=str(e))
2025-07-19 12:09:10 +02:00
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
2025-07-18 11:51:43 +02:00
2025-07-27 20:20:09 +02:00
@router.post("/tenants/{tenant_id}/weather/historical")
async def get_historical_weather(
request: HistoricalWeatherRequest,
db: AsyncSession = Depends(get_db),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
2025-07-18 11:51:43 +02:00
):
2025-07-27 20:20:09 +02:00
"""Get historical weather data with date range in payload"""
2025-07-18 11:51:43 +02:00
try:
2025-07-27 20:20:09 +02:00
# Validate date range
if request.end_date <= request.start_date:
raise HTTPException(status_code=400, detail="End date must be after start date")
2025-07-21 13:09:30 +02:00
2025-07-27 21:44:18 +02:00
if (request.end_date - request.start_date).days > 1000:
2025-07-27 20:20:09 +02:00
raise HTTPException(status_code=400, detail="Date range cannot exceed 90 days")
historical_data = await weather_service.get_historical_weather(
2025-08-12 18:17:30 +02:00
request.latitude, request.longitude, request.start_date, request.end_date)
2025-07-18 11:51:43 +02:00
2025-07-27 20:20:09 +02:00
# Publish event (with error handling)
try:
await publish_weather_updated({
"type": "historical_requested",
"latitude": request.latitude,
"longitude": request.longitude,
"start_date": request.start_date.isoformat(),
"end_date": request.end_date.isoformat(),
"records_count": len(historical_data),
"timestamp": datetime.utcnow().isoformat()
})
except Exception as pub_error:
logger.warning("Failed to publish historical weather event", error=str(pub_error))
# Continue processing
2025-07-18 11:51:43 +02:00
2025-07-27 20:20:09 +02:00
return historical_data
except HTTPException:
raise
2025-07-21 13:09:30 +02:00
except Exception as e:
2025-07-27 20:20:09 +02:00
logger.error("Unexpected error in historical weather API", error=str(e))
2025-07-21 13:09:30 +02:00
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
2025-08-12 18:17:30 +02:00
@router.post("/tenants/{tenant_id}/weather/forecast", response_model=List[WeatherForecastResponse])
async def get_weather_forecast(
request: WeatherForecastRequest,
2025-07-26 18:46:52 +02:00
tenant_id: UUID = Path(..., description="Tenant ID"),
2025-07-21 13:09:30 +02:00
current_user: Dict[str, Any] = Depends(get_current_user_dep),
2025-07-19 12:09:10 +02:00
):
2025-08-12 18:17:30 +02:00
"""Get weather forecast for location"""
2025-07-19 12:09:10 +02:00
try:
2025-08-12 18:17:30 +02:00
logger.debug("Getting weather forecast",
lat=request.latitude,
lon=request.longitude,
days=request.days,
tenant_id=tenant_id)
2025-07-21 13:09:30 +02:00
2025-08-12 18:17:30 +02:00
forecast = await weather_service.get_weather_forecast(request.latitude, request.longitude, request.days)
2025-07-21 13:09:30 +02:00
2025-08-18 21:14:42 +02:00
# Don't return 404 for empty forecast - return empty list with 200 status
2025-08-12 18:17:30 +02:00
if not forecast:
2025-08-18 21:14:42 +02:00
logger.info("Weather forecast unavailable - returning empty list")
return []
2025-08-12 18:17:30 +02:00
# Publish event
try:
await publish_weather_updated({
"type": "forecast_requested",
"tenant_id": tenant_id,
"latitude": request.latitude,
"longitude": request.longitude,
"days": request.days,
"requested_by": current_user["user_id"],
"timestamp": datetime.utcnow().isoformat()
})
except Exception as e:
logger.warning("Failed to publish forecast event", error=str(e))
2025-07-19 12:09:10 +02:00
2025-08-12 18:17:30 +02:00
return forecast
2025-07-19 12:09:10 +02:00
except HTTPException:
raise
2025-07-18 11:51:43 +02:00
except Exception as e:
2025-08-12 18:17:30 +02:00
logger.error("Failed to get weather forecast", error=str(e))
2025-08-18 20:50:41 +02:00
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.post("/tenants/{tenant_id}/weather/hourly-forecast", response_model=List[HourlyForecastResponse])
async def get_hourly_weather_forecast(
request: HourlyForecastRequest,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
):
"""Get hourly weather forecast for location using AEMET API
This endpoint provides hourly weather predictions for up to 48 hours,
perfect for detailed bakery operations planning and weather-based recommendations.
"""
try:
logger.debug("Getting hourly weather forecast",
lat=request.latitude,
lon=request.longitude,
hours=request.hours,
tenant_id=tenant_id)
hourly_forecast = await weather_service.get_hourly_forecast(
request.latitude, request.longitude, request.hours
)
2025-08-18 21:14:42 +02:00
# Don't return 404 for empty hourly forecast - return empty list with 200 status
2025-08-18 20:50:41 +02:00
if not hourly_forecast:
2025-08-18 21:14:42 +02:00
logger.info("Hourly weather forecast unavailable - returning empty list")
return []
2025-08-18 20:50:41 +02:00
# Publish event
try:
await publish_weather_updated({
"type": "hourly_forecast_requested",
"tenant_id": tenant_id,
"latitude": request.latitude,
"longitude": request.longitude,
"hours": request.hours,
"requested_by": current_user["user_id"],
"forecast_count": len(hourly_forecast),
"timestamp": datetime.utcnow().isoformat()
})
except Exception as e:
logger.warning("Failed to publish hourly forecast event", error=str(e))
return hourly_forecast
except HTTPException:
raise
except Exception as e:
logger.error("Failed to get hourly weather forecast", error=str(e))
2025-08-12 18:17:30 +02:00
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.get("/weather/status")
async def get_weather_status():
"""Get AEMET API status and diagnostics"""
try:
from app.core.config import settings
# Test AEMET API connectivity
aemet_status = "unknown"
aemet_message = "Not tested"
try:
# Quick test of AEMET API
test_weather = await weather_service.get_current_weather(40.4168, -3.7038)
if test_weather and hasattr(test_weather, 'source') and test_weather.source == "aemet":
aemet_status = "healthy"
aemet_message = "AEMET API responding correctly"
elif test_weather and hasattr(test_weather, 'source') and test_weather.source == "synthetic":
aemet_status = "degraded"
aemet_message = "AEMET API unavailable - using synthetic data"
else:
aemet_status = "degraded"
aemet_message = "Weather service returned unexpected data format"
except Exception as e:
aemet_status = "unhealthy"
aemet_message = f"AEMET API error: {str(e)}"
return {
"status": "ok",
"aemet": {
"status": aemet_status,
"message": aemet_message,
"api_key_configured": bool(settings.AEMET_API_KEY),
"enabled": getattr(settings, 'AEMET_ENABLED', True),
"base_url": settings.AEMET_BASE_URL,
"timeout": settings.AEMET_TIMEOUT, # Now correctly shows 60 from config
"retry_attempts": settings.AEMET_RETRY_ATTEMPTS
},
"timestamp": datetime.utcnow().isoformat(),
"service": "external-weather-service"
}
except Exception as e:
logger.error("Failed to get weather status", error=str(e))
raise HTTPException(status_code=500, detail=f"Status check failed: {str(e)}")