REFACTOR ALL APIs
This commit is contained in:
414
services/forecasting/app/api/forecasting_operations.py
Normal file
414
services/forecasting/app/api/forecasting_operations.py
Normal file
@@ -0,0 +1,414 @@
|
||||
# services/forecasting/app/api/forecasting_operations.py
|
||||
"""
|
||||
Forecasting Operations API - Business operations for forecast generation and predictions
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, Path, Request
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import date, datetime
|
||||
import uuid
|
||||
|
||||
from app.services.forecasting_service import EnhancedForecastingService
|
||||
from app.services.prediction_service import PredictionService
|
||||
from app.schemas.forecasts import (
|
||||
ForecastRequest, ForecastResponse, BatchForecastRequest,
|
||||
BatchForecastResponse, MultiDayForecastResponse
|
||||
)
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.database.base import create_database_manager
|
||||
from shared.monitoring.decorators import track_execution_time
|
||||
from shared.monitoring.metrics import get_metrics_collector
|
||||
from app.core.config import settings
|
||||
from shared.routing import RouteBuilder
|
||||
from shared.auth.access_control import require_user_role
|
||||
|
||||
route_builder = RouteBuilder('forecasting')
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter(tags=["forecasting-operations"])
|
||||
|
||||
|
||||
def get_enhanced_forecasting_service():
|
||||
"""Dependency injection for EnhancedForecastingService"""
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "forecasting-service")
|
||||
return EnhancedForecastingService(database_manager)
|
||||
|
||||
|
||||
def get_enhanced_prediction_service():
|
||||
"""Dependency injection for enhanced PredictionService"""
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "forecasting-service")
|
||||
return PredictionService(database_manager)
|
||||
|
||||
|
||||
@router.post(
|
||||
route_builder.build_operations_route("single"),
|
||||
response_model=ForecastResponse
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
@track_execution_time("enhanced_single_forecast_duration_seconds", "forecasting-service")
|
||||
async def generate_single_forecast(
|
||||
request: ForecastRequest,
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
request_obj: Request = None,
|
||||
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
|
||||
):
|
||||
"""Generate a single product forecast"""
|
||||
metrics = get_metrics_collector(request_obj)
|
||||
|
||||
try:
|
||||
logger.info("Generating single forecast",
|
||||
tenant_id=tenant_id,
|
||||
inventory_product_id=request.inventory_product_id,
|
||||
forecast_date=request.forecast_date.isoformat())
|
||||
|
||||
if metrics:
|
||||
metrics.increment_counter("single_forecasts_total")
|
||||
|
||||
forecast = await enhanced_forecasting_service.generate_forecast(
|
||||
tenant_id=tenant_id,
|
||||
request=request
|
||||
)
|
||||
|
||||
if metrics:
|
||||
metrics.increment_counter("single_forecasts_success_total")
|
||||
|
||||
logger.info("Single forecast generated successfully",
|
||||
tenant_id=tenant_id,
|
||||
forecast_id=forecast.id)
|
||||
|
||||
return forecast
|
||||
|
||||
except ValueError as e:
|
||||
if metrics:
|
||||
metrics.increment_counter("forecast_validation_errors_total")
|
||||
logger.error("Forecast validation error", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
if metrics:
|
||||
metrics.increment_counter("single_forecasts_errors_total")
|
||||
logger.error("Single forecast generation failed", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Forecast generation failed"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
route_builder.build_operations_route("multi-day"),
|
||||
response_model=MultiDayForecastResponse
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
@track_execution_time("enhanced_multi_day_forecast_duration_seconds", "forecasting-service")
|
||||
async def generate_multi_day_forecast(
|
||||
request: ForecastRequest,
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
request_obj: Request = None,
|
||||
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
|
||||
):
|
||||
"""Generate multiple daily forecasts for the specified period"""
|
||||
metrics = get_metrics_collector(request_obj)
|
||||
|
||||
try:
|
||||
logger.info("Generating multi-day forecast",
|
||||
tenant_id=tenant_id,
|
||||
inventory_product_id=request.inventory_product_id,
|
||||
forecast_days=request.forecast_days,
|
||||
forecast_date=request.forecast_date.isoformat())
|
||||
|
||||
if metrics:
|
||||
metrics.increment_counter("multi_day_forecasts_total")
|
||||
|
||||
if request.forecast_days <= 0 or request.forecast_days > 30:
|
||||
raise ValueError("forecast_days must be between 1 and 30")
|
||||
|
||||
forecast_result = await enhanced_forecasting_service.generate_multi_day_forecast(
|
||||
tenant_id=tenant_id,
|
||||
request=request
|
||||
)
|
||||
|
||||
if metrics:
|
||||
metrics.increment_counter("multi_day_forecasts_success_total")
|
||||
|
||||
logger.info("Multi-day forecast generated successfully",
|
||||
tenant_id=tenant_id,
|
||||
inventory_product_id=request.inventory_product_id,
|
||||
forecast_days=len(forecast_result.get("forecasts", [])))
|
||||
|
||||
return forecast_result
|
||||
|
||||
except ValueError as e:
|
||||
if metrics:
|
||||
metrics.increment_counter("forecast_validation_errors_total")
|
||||
logger.error("Multi-day forecast validation error", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
if metrics:
|
||||
metrics.increment_counter("multi_day_forecasts_errors_total")
|
||||
logger.error("Multi-day forecast generation failed", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Multi-day forecast generation failed"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
route_builder.build_operations_route("batch"),
|
||||
response_model=BatchForecastResponse
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
@track_execution_time("enhanced_batch_forecast_duration_seconds", "forecasting-service")
|
||||
async def generate_batch_forecast(
|
||||
request: BatchForecastRequest,
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
request_obj: Request = None,
|
||||
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
|
||||
):
|
||||
"""Generate forecasts for multiple products in batch"""
|
||||
metrics = get_metrics_collector(request_obj)
|
||||
|
||||
try:
|
||||
logger.info("Generating batch forecast",
|
||||
tenant_id=tenant_id,
|
||||
product_count=len(request.inventory_product_ids))
|
||||
|
||||
if metrics:
|
||||
metrics.increment_counter("batch_forecasts_total")
|
||||
|
||||
if not request.inventory_product_ids:
|
||||
raise ValueError("inventory_product_ids cannot be empty")
|
||||
|
||||
batch_result = await enhanced_forecasting_service.generate_batch_forecast(
|
||||
tenant_id=tenant_id,
|
||||
request=request
|
||||
)
|
||||
|
||||
if metrics:
|
||||
metrics.increment_counter("batch_forecasts_success_total")
|
||||
|
||||
logger.info("Batch forecast generated successfully",
|
||||
tenant_id=tenant_id,
|
||||
total_forecasts=batch_result.total_forecasts)
|
||||
|
||||
return batch_result
|
||||
|
||||
except ValueError as e:
|
||||
if metrics:
|
||||
metrics.increment_counter("forecast_validation_errors_total")
|
||||
logger.error("Batch forecast validation error", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
if metrics:
|
||||
metrics.increment_counter("batch_forecasts_errors_total")
|
||||
logger.error("Batch forecast generation failed", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Batch forecast generation failed"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
route_builder.build_operations_route("realtime")
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
@track_execution_time("enhanced_realtime_prediction_duration_seconds", "forecasting-service")
|
||||
async def generate_realtime_prediction(
|
||||
prediction_request: Dict[str, Any],
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
request_obj: Request = None,
|
||||
prediction_service: PredictionService = Depends(get_enhanced_prediction_service)
|
||||
):
|
||||
"""Generate real-time prediction"""
|
||||
metrics = get_metrics_collector(request_obj)
|
||||
|
||||
try:
|
||||
logger.info("Generating real-time prediction",
|
||||
tenant_id=tenant_id,
|
||||
inventory_product_id=prediction_request.get("inventory_product_id"))
|
||||
|
||||
if metrics:
|
||||
metrics.increment_counter("realtime_predictions_total")
|
||||
|
||||
required_fields = ["inventory_product_id", "model_id", "features"]
|
||||
missing_fields = [field for field in required_fields if field not in prediction_request]
|
||||
if missing_fields:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Missing required fields: {missing_fields}"
|
||||
)
|
||||
|
||||
prediction_result = await prediction_service.predict(
|
||||
model_id=prediction_request["model_id"],
|
||||
model_path=prediction_request.get("model_path", ""),
|
||||
features=prediction_request["features"],
|
||||
confidence_level=prediction_request.get("confidence_level", 0.8)
|
||||
)
|
||||
|
||||
if metrics:
|
||||
metrics.increment_counter("realtime_predictions_success_total")
|
||||
|
||||
logger.info("Real-time prediction generated successfully",
|
||||
tenant_id=tenant_id,
|
||||
prediction_value=prediction_result.get("prediction"))
|
||||
|
||||
return {
|
||||
"tenant_id": tenant_id,
|
||||
"inventory_product_id": prediction_request["inventory_product_id"],
|
||||
"model_id": prediction_request["model_id"],
|
||||
"prediction": prediction_result.get("prediction"),
|
||||
"confidence": prediction_result.get("confidence"),
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
if metrics:
|
||||
metrics.increment_counter("prediction_validation_errors_total")
|
||||
logger.error("Prediction validation error", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
if metrics:
|
||||
metrics.increment_counter("realtime_predictions_errors_total")
|
||||
logger.error("Real-time prediction failed", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Real-time prediction failed"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
route_builder.build_operations_route("batch-predictions")
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
async def generate_batch_predictions(
|
||||
predictions_request: List[Dict[str, Any]],
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
prediction_service: PredictionService = Depends(get_enhanced_prediction_service)
|
||||
):
|
||||
"""Generate batch predictions"""
|
||||
try:
|
||||
logger.info("Generating batch predictions", tenant_id=tenant_id, count=len(predictions_request))
|
||||
|
||||
results = []
|
||||
for pred_request in predictions_request:
|
||||
try:
|
||||
prediction_result = await prediction_service.predict(
|
||||
model_id=pred_request["model_id"],
|
||||
model_path=pred_request.get("model_path", ""),
|
||||
features=pred_request["features"],
|
||||
confidence_level=pred_request.get("confidence_level", 0.8)
|
||||
)
|
||||
results.append({
|
||||
"inventory_product_id": pred_request.get("inventory_product_id"),
|
||||
"prediction": prediction_result.get("prediction"),
|
||||
"confidence": prediction_result.get("confidence"),
|
||||
"success": True
|
||||
})
|
||||
except Exception as e:
|
||||
results.append({
|
||||
"inventory_product_id": pred_request.get("inventory_product_id"),
|
||||
"error": str(e),
|
||||
"success": False
|
||||
})
|
||||
|
||||
return {"predictions": results, "total": len(results)}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Batch predictions failed", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Batch predictions failed"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
route_builder.build_operations_route("validate-predictions")
|
||||
)
|
||||
async def validate_predictions(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
start_date: date = Query(...),
|
||||
end_date: date = Query(...),
|
||||
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
|
||||
):
|
||||
"""Validate predictions against actual sales data"""
|
||||
try:
|
||||
logger.info("Validating predictions", tenant_id=tenant_id)
|
||||
|
||||
validation_results = await enhanced_forecasting_service.validate_predictions(
|
||||
tenant_id=tenant_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
return validation_results
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Prediction validation failed", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Prediction validation failed"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_operations_route("statistics")
|
||||
)
|
||||
async def get_forecast_statistics(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
start_date: Optional[date] = Query(None),
|
||||
end_date: Optional[date] = Query(None),
|
||||
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
|
||||
):
|
||||
"""Get forecast statistics"""
|
||||
try:
|
||||
logger.info("Getting forecast statistics", tenant_id=tenant_id)
|
||||
|
||||
stats = await enhanced_forecasting_service.get_forecast_statistics(
|
||||
tenant_id=tenant_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get forecast statistics", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve forecast statistics"
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
route_builder.build_operations_route("cache")
|
||||
)
|
||||
async def clear_prediction_cache(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
prediction_service: PredictionService = Depends(get_enhanced_prediction_service)
|
||||
):
|
||||
"""Clear prediction cache"""
|
||||
try:
|
||||
logger.info("Clearing prediction cache", tenant_id=tenant_id)
|
||||
|
||||
await prediction_service.clear_cache(tenant_id=tenant_id)
|
||||
|
||||
return {"message": "Prediction cache cleared successfully"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to clear prediction cache", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to clear prediction cache"
|
||||
)
|
||||
Reference in New Issue
Block a user