Files
bakery-ia/services/external/app/api/external_operations.py
2025-10-06 15:27:01 +02:00

408 lines
15 KiB
Python

# services/external/app/api/external_operations.py
"""
External Operations API - Business operations for fetching external data
"""
from fastapi import APIRouter, Depends, HTTPException, Query, Path
from typing import List, Dict, Any
from datetime import datetime
from uuid import UUID
import structlog
from app.schemas.weather import (
WeatherDataResponse,
WeatherForecastResponse,
WeatherForecastRequest,
HistoricalWeatherRequest,
HourlyForecastRequest,
HourlyForecastResponse
)
from app.schemas.traffic import (
TrafficDataResponse,
TrafficForecastRequest,
HistoricalTrafficRequest
)
from app.services.weather_service import WeatherService
from app.services.traffic_service import TrafficService
from app.services.messaging import publish_weather_updated, publish_traffic_updated
from shared.auth.decorators import get_current_user_dep
from shared.auth.access_control import require_user_role
from shared.routing.route_builder import RouteBuilder
route_builder = RouteBuilder('external')
router = APIRouter(tags=["external-operations"])
logger = structlog.get_logger()
def get_weather_service():
"""Dependency injection for WeatherService"""
return WeatherService()
def get_traffic_service():
"""Dependency injection for TrafficService"""
return TrafficService()
# Weather Operations
@router.get(
route_builder.build_operations_route("weather/current"),
response_model=WeatherDataResponse
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
async def get_current_weather(
latitude: float = Query(..., description="Latitude"),
longitude: float = Query(..., description="Longitude"),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
weather_service: WeatherService = Depends(get_weather_service)
):
"""Get current weather data for location from external API"""
try:
logger.debug("Getting current weather",
lat=latitude,
lon=longitude,
tenant_id=tenant_id,
user_id=current_user["user_id"])
weather = await weather_service.get_current_weather(latitude, longitude)
if not weather:
raise HTTPException(status_code=503, detail="Weather service temporarily unavailable")
try:
await publish_weather_updated({
"type": "current_weather_requested",
"tenant_id": str(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))
return weather
except HTTPException:
raise
except Exception as e:
logger.error("Failed to get current weather", error=str(e))
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.post(
route_builder.build_operations_route("weather/historical"),
response_model=List[WeatherDataResponse]
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
async def get_historical_weather(
request: HistoricalWeatherRequest,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
weather_service: WeatherService = Depends(get_weather_service)
):
"""Get historical weather data with date range"""
try:
if request.end_date <= request.start_date:
raise HTTPException(status_code=400, detail="End date must be after start date")
if (request.end_date - request.start_date).days > 1000:
raise HTTPException(status_code=400, detail="Date range cannot exceed 90 days")
historical_data = await weather_service.get_historical_weather(
request.latitude, request.longitude, request.start_date, request.end_date)
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))
return historical_data
except HTTPException:
raise
except Exception as e:
logger.error("Unexpected error in historical weather API", error=str(e))
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.post(
route_builder.build_operations_route("weather/forecast"),
response_model=List[WeatherForecastResponse]
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
async def get_weather_forecast(
request: WeatherForecastRequest,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
weather_service: WeatherService = Depends(get_weather_service)
):
"""Get weather forecast for location"""
try:
logger.debug("Getting weather forecast",
lat=request.latitude,
lon=request.longitude,
days=request.days,
tenant_id=tenant_id)
forecast = await weather_service.get_weather_forecast(request.latitude, request.longitude, request.days)
if not forecast:
logger.info("Weather forecast unavailable - returning empty list")
return []
try:
await publish_weather_updated({
"type": "forecast_requested",
"tenant_id": str(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))
return forecast
except HTTPException:
raise
except Exception as e:
logger.error("Failed to get weather forecast", error=str(e))
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.post(
route_builder.build_operations_route("weather/hourly-forecast"),
response_model=List[HourlyForecastResponse]
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
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),
weather_service: WeatherService = Depends(get_weather_service)
):
"""Get hourly weather forecast for location"""
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
)
if not hourly_forecast:
logger.info("Hourly weather forecast unavailable - returning empty list")
return []
try:
await publish_weather_updated({
"type": "hourly_forecast_requested",
"tenant_id": str(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))
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.get(
route_builder.build_operations_route("weather-status"),
response_model=dict
)
async def get_weather_status(
weather_service: WeatherService = Depends(get_weather_service)
):
"""Get weather API status and diagnostics"""
try:
aemet_status = "unknown"
aemet_message = "Not tested"
try:
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 = "Using synthetic weather data (AEMET API unavailable)"
else:
aemet_status = "unknown"
aemet_message = "Weather source unknown"
except Exception as test_error:
aemet_status = "unhealthy"
aemet_message = f"AEMET API test failed: {str(test_error)}"
return {
"status": aemet_status,
"message": aemet_message,
"timestamp": datetime.utcnow().isoformat()
}
except Exception as e:
logger.error("Weather status check failed", error=str(e))
raise HTTPException(status_code=500, detail=f"Status check failed: {str(e)}")
# Traffic Operations
@router.get(
route_builder.build_operations_route("traffic/current"),
response_model=TrafficDataResponse
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
async def get_current_traffic(
latitude: float = Query(..., description="Latitude"),
longitude: float = Query(..., description="Longitude"),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
traffic_service: TrafficService = Depends(get_traffic_service)
):
"""Get current traffic data for location from external API"""
try:
logger.debug("Getting current traffic",
lat=latitude,
lon=longitude,
tenant_id=tenant_id,
user_id=current_user["user_id"])
traffic = await traffic_service.get_current_traffic(latitude, longitude)
if not traffic:
raise HTTPException(status_code=503, detail="Traffic service temporarily unavailable")
try:
await publish_traffic_updated({
"type": "current_traffic_requested",
"tenant_id": str(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 traffic event", error=str(e))
return traffic
except HTTPException:
raise
except Exception as e:
logger.error("Failed to get current traffic", error=str(e))
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.post(
route_builder.build_operations_route("traffic/historical"),
response_model=List[TrafficDataResponse]
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
async def get_historical_traffic(
request: HistoricalTrafficRequest,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
traffic_service: TrafficService = Depends(get_traffic_service)
):
"""Get historical traffic data with date range"""
try:
if request.end_date <= request.start_date:
raise HTTPException(status_code=400, detail="End date must be after start date")
historical_data = await traffic_service.get_historical_traffic(
request.latitude, request.longitude, request.start_date, request.end_date)
try:
await publish_traffic_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 traffic event", error=str(pub_error))
return historical_data
except HTTPException:
raise
except Exception as e:
logger.error("Unexpected error in historical traffic API", error=str(e))
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.post(
route_builder.build_operations_route("traffic/forecast"),
response_model=List[TrafficDataResponse]
)
@require_user_role(['viewer', 'member', 'admin', 'owner'])
async def get_traffic_forecast(
request: TrafficForecastRequest,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
traffic_service: TrafficService = Depends(get_traffic_service)
):
"""Get traffic forecast for location"""
try:
logger.debug("Getting traffic forecast",
lat=request.latitude,
lon=request.longitude,
hours=request.hours,
tenant_id=tenant_id)
forecast = await traffic_service.get_traffic_forecast(request.latitude, request.longitude, request.hours)
if not forecast:
logger.info("Traffic forecast unavailable - returning empty list")
return []
try:
await publish_traffic_updated({
"type": "forecast_requested",
"tenant_id": str(tenant_id),
"latitude": request.latitude,
"longitude": request.longitude,
"hours": request.hours,
"requested_by": current_user["user_id"],
"timestamp": datetime.utcnow().isoformat()
})
except Exception as e:
logger.warning("Failed to publish traffic forecast event", error=str(e))
return forecast
except HTTPException:
raise
except Exception as e:
logger.error("Failed to get traffic forecast", error=str(e))
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")