# services/data/app/api/weather.py - UPDATED WITH UNIFIED AUTH """Weather data API endpoints with unified authentication""" from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks, Path from typing import List, Optional, Dict, Any from datetime import datetime, date import structlog from uuid import UUID from app.schemas.external import ( WeatherDataResponse, WeatherForecastResponse, WeatherForecastRequest ) from app.services.weather_service import WeatherService from app.services.messaging import publish_weather_updated from app.schemas.external import ( HistoricalWeatherRequest ) # Import unified authentication from shared library from shared.auth.decorators import ( get_current_user_dep, get_current_tenant_id_dep ) from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db router = APIRouter(tags=["weather"]) logger = structlog.get_logger() weather_service = WeatherService() @router.get("/tenants/{tenant_id}/weather/current", response_model=WeatherDataResponse) 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), ): """Get current weather data for location""" 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=404, detail="Weather data not available") # 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)) 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("/tenants/{tenant_id}/weather/forecast", response_model=List[WeatherForecastResponse]) async def get_weather_forecast( request: WeatherForecastRequest, tenant_id: UUID = Path(..., description="Tenant ID"), current_user: Dict[str, Any] = Depends(get_current_user_dep), ): """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: raise HTTPException(status_code=404, detail="Weather forecast not available") # 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)) 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("/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), ): """Get historical weather data with date range in payload""" try: # Validate date range 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, db ) # 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 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("/tenants/{tenant_id}weather/sync") async def sync_weather_data( background_tasks: BackgroundTasks, force: bool = Query(False, description="Force sync even if recently synced"), tenant_id: UUID = Path(..., description="Tenant ID"), current_user: Dict[str, Any] = Depends(get_current_user_dep), ): """Manually trigger weather data synchronization""" try: logger.info("Weather sync requested", tenant_id=tenant_id, user_id=current_user["user_id"], force=force) # Check if user has permission to sync (could be admin only) if current_user.get("role") not in ["admin", "manager"]: raise HTTPException( status_code=403, detail="Insufficient permissions to sync weather data" ) # Schedule background sync background_tasks.add_task( weather_service.sync_weather_data, tenant_id=tenant_id, force=force ) return { "message": "Weather sync initiated", "status": "processing", "initiated_by": current_user["user_id"] } except HTTPException: raise except Exception as e: logger.error("Failed to initiate weather sync", error=str(e)) raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")