REFACTOR ALL APIs
This commit is contained in:
@@ -1,444 +1,145 @@
|
||||
# services/forecasting/app/api/forecasts.py
|
||||
"""
|
||||
Enhanced Forecast API Endpoints with Repository Pattern
|
||||
Updated to use repository pattern with dependency injection and improved error handling
|
||||
Forecasts API - Atomic CRUD operations on Forecast model
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, Path, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, Path
|
||||
from typing import List, Optional
|
||||
from datetime import date, datetime
|
||||
import uuid
|
||||
|
||||
from app.services.forecasting_service import EnhancedForecastingService
|
||||
from app.schemas.forecasts import (
|
||||
ForecastRequest, ForecastResponse, BatchForecastRequest,
|
||||
BatchForecastResponse, MultiDayForecastResponse
|
||||
)
|
||||
from shared.auth.decorators import (
|
||||
get_current_user_dep,
|
||||
require_admin_role
|
||||
)
|
||||
from app.schemas.forecasts import ForecastResponse
|
||||
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
|
||||
|
||||
route_builder = RouteBuilder('forecasting')
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter(tags=["enhanced-forecasts"])
|
||||
router = APIRouter(tags=["forecasts"])
|
||||
|
||||
|
||||
def get_enhanced_forecasting_service():
|
||||
"""Dependency injection for EnhancedForecastingService"""
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "forecasting-service")
|
||||
return EnhancedForecastingService(database_manager)
|
||||
|
||||
@router.post("/tenants/{tenant_id}/forecasts/single", response_model=ForecastResponse)
|
||||
@track_execution_time("enhanced_single_forecast_duration_seconds", "forecasting-service")
|
||||
async def create_enhanced_single_forecast(
|
||||
request: ForecastRequest,
|
||||
|
||||
@router.get(
|
||||
route_builder.build_base_route("forecasts"),
|
||||
response_model=List[ForecastResponse]
|
||||
)
|
||||
async def list_forecasts(
|
||||
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 using enhanced repository pattern"""
|
||||
metrics = get_metrics_collector(request_obj)
|
||||
|
||||
try:
|
||||
logger.info("Generating enhanced single forecast",
|
||||
tenant_id=tenant_id,
|
||||
inventory_product_id=request.inventory_product_id,
|
||||
forecast_date=request.forecast_date.isoformat())
|
||||
|
||||
# Record metrics
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_single_forecasts_total")
|
||||
|
||||
# Generate forecast using enhanced service
|
||||
forecast = await enhanced_forecasting_service.generate_forecast(
|
||||
tenant_id=tenant_id,
|
||||
request=request
|
||||
)
|
||||
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_single_forecasts_success_total")
|
||||
|
||||
logger.info("Enhanced single forecast generated successfully",
|
||||
tenant_id=tenant_id,
|
||||
forecast_id=forecast.id)
|
||||
|
||||
return forecast
|
||||
|
||||
except ValueError as e:
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_forecast_validation_errors_total")
|
||||
logger.error("Enhanced 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("enhanced_single_forecasts_errors_total")
|
||||
logger.error("Enhanced single forecast generation failed",
|
||||
error=str(e),
|
||||
tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Enhanced forecast generation failed"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/forecasts/multi-day", response_model=MultiDayForecastResponse)
|
||||
@track_execution_time("enhanced_multi_day_forecast_duration_seconds", "forecasting-service")
|
||||
async def create_enhanced_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 using enhanced repository pattern"""
|
||||
metrics = get_metrics_collector(request_obj)
|
||||
|
||||
try:
|
||||
logger.info("Generating enhanced 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())
|
||||
|
||||
# Record metrics
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_multi_day_forecasts_total")
|
||||
|
||||
# Validate forecast_days parameter
|
||||
if request.forecast_days <= 0 or request.forecast_days > 30:
|
||||
raise ValueError("forecast_days must be between 1 and 30")
|
||||
|
||||
# Generate multi-day forecast using enhanced service
|
||||
forecast_result = await enhanced_forecasting_service.generate_multi_day_forecast(
|
||||
tenant_id=tenant_id,
|
||||
request=request
|
||||
)
|
||||
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_multi_day_forecasts_success_total")
|
||||
|
||||
logger.info("Enhanced multi-day forecast generated successfully",
|
||||
tenant_id=tenant_id,
|
||||
inventory_product_id=request.inventory_product_id,
|
||||
forecast_days=len(forecast_result.get("forecasts", [])))
|
||||
|
||||
return MultiDayForecastResponse(**forecast_result)
|
||||
|
||||
except ValueError as e:
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_multi_day_forecast_validation_errors_total")
|
||||
logger.error("Enhanced 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("enhanced_multi_day_forecasts_errors_total")
|
||||
logger.error("Enhanced multi-day forecast generation failed",
|
||||
error=str(e),
|
||||
tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Enhanced multi-day forecast generation failed"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/forecasts/batch", response_model=BatchForecastResponse)
|
||||
@track_execution_time("enhanced_batch_forecast_duration_seconds", "forecasting-service")
|
||||
async def create_enhanced_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 batch forecasts using enhanced repository pattern"""
|
||||
metrics = get_metrics_collector(request_obj)
|
||||
|
||||
try:
|
||||
logger.info("Generating enhanced batch forecasts",
|
||||
tenant_id=tenant_id,
|
||||
products_count=len(request.inventory_product_ids),
|
||||
forecast_dates_count=request.forecast_days)
|
||||
|
||||
# Record metrics
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_batch_forecasts_total")
|
||||
metrics.histogram("enhanced_batch_forecast_products_count", len(request.inventory_product_ids))
|
||||
|
||||
# Generate batch forecasts using enhanced service
|
||||
batch_result = await enhanced_forecasting_service.generate_batch_forecasts(
|
||||
tenant_id=tenant_id,
|
||||
request=request
|
||||
)
|
||||
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_batch_forecasts_success_total")
|
||||
|
||||
logger.info("Enhanced batch forecasts generated successfully",
|
||||
tenant_id=tenant_id,
|
||||
batch_id=batch_result.get("batch_id"),
|
||||
forecasts_generated=len(batch_result.get("forecasts", [])))
|
||||
|
||||
return BatchForecastResponse(**batch_result)
|
||||
|
||||
except ValueError as e:
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_batch_forecast_validation_errors_total")
|
||||
logger.error("Enhanced 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("enhanced_batch_forecasts_errors_total")
|
||||
logger.error("Enhanced batch forecast generation failed",
|
||||
error=str(e),
|
||||
tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Enhanced batch forecast generation failed"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/forecasts")
|
||||
@track_execution_time("enhanced_get_forecasts_duration_seconds", "forecasting-service")
|
||||
async def get_enhanced_tenant_forecasts(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
inventory_product_id: Optional[str] = Query(None, description="Filter by inventory product ID"),
|
||||
inventory_product_id: Optional[str] = Query(None, description="Filter by product ID"),
|
||||
start_date: Optional[date] = Query(None, description="Start date filter"),
|
||||
end_date: Optional[date] = Query(None, description="End date filter"),
|
||||
skip: int = Query(0, description="Number of records to skip"),
|
||||
limit: int = Query(100, description="Number of records to return"),
|
||||
request_obj: Request = None,
|
||||
limit: int = Query(50, ge=1, le=1000),
|
||||
offset: int = Query(0, ge=0),
|
||||
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
|
||||
):
|
||||
"""Get tenant forecasts with enhanced filtering using repository pattern"""
|
||||
metrics = get_metrics_collector(request_obj)
|
||||
|
||||
"""List forecasts with optional filters"""
|
||||
try:
|
||||
# Record metrics
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_get_forecasts_total")
|
||||
|
||||
# Get forecasts using enhanced service
|
||||
forecasts = await enhanced_forecasting_service.get_tenant_forecasts(
|
||||
logger.info("Listing forecasts", tenant_id=tenant_id)
|
||||
|
||||
forecasts = await enhanced_forecasting_service.list_forecasts(
|
||||
tenant_id=tenant_id,
|
||||
inventory_product_id=inventory_product_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_get_forecasts_success_total")
|
||||
|
||||
return {
|
||||
"tenant_id": tenant_id,
|
||||
"forecasts": forecasts,
|
||||
"total_returned": len(forecasts),
|
||||
"filters": {
|
||||
"inventory_product_id": inventory_product_id,
|
||||
"start_date": start_date.isoformat() if start_date else None,
|
||||
"end_date": end_date.isoformat() if end_date else None
|
||||
},
|
||||
"pagination": {
|
||||
"skip": skip,
|
||||
"limit": limit
|
||||
},
|
||||
"enhanced_features": True,
|
||||
"repository_integration": True
|
||||
}
|
||||
|
||||
|
||||
return forecasts
|
||||
|
||||
except Exception as e:
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_get_forecasts_errors_total")
|
||||
logger.error("Failed to get enhanced tenant forecasts",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
logger.error("Failed to list forecasts", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get tenant forecasts"
|
||||
detail="Failed to retrieve forecasts"
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/forecasts/{forecast_id}")
|
||||
@track_execution_time("enhanced_get_forecast_duration_seconds", "forecasting-service")
|
||||
async def get_enhanced_forecast_by_id(
|
||||
@router.get(
|
||||
route_builder.build_resource_detail_route("forecasts", "forecast_id"),
|
||||
response_model=ForecastResponse
|
||||
)
|
||||
async def get_forecast(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
forecast_id: str = Path(..., description="Forecast ID"),
|
||||
request_obj: Request = None,
|
||||
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
|
||||
):
|
||||
"""Get specific forecast by ID using enhanced repository pattern"""
|
||||
metrics = get_metrics_collector(request_obj)
|
||||
|
||||
"""Get a specific forecast by ID"""
|
||||
try:
|
||||
# Record metrics
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_get_forecast_by_id_total")
|
||||
|
||||
# Get forecast using enhanced service
|
||||
forecast = await enhanced_forecasting_service.get_forecast_by_id(forecast_id)
|
||||
|
||||
logger.info("Getting forecast", tenant_id=tenant_id, forecast_id=forecast_id)
|
||||
|
||||
forecast = await enhanced_forecasting_service.get_forecast(
|
||||
tenant_id=tenant_id,
|
||||
forecast_id=uuid.UUID(forecast_id)
|
||||
)
|
||||
|
||||
if not forecast:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Forecast not found"
|
||||
)
|
||||
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_get_forecast_by_id_success_total")
|
||||
|
||||
return {
|
||||
**forecast,
|
||||
"enhanced_features": True,
|
||||
"repository_integration": True
|
||||
}
|
||||
|
||||
|
||||
return forecast
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
logger.error("Invalid forecast ID", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid forecast ID format"
|
||||
)
|
||||
except Exception as e:
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_get_forecast_by_id_errors_total")
|
||||
logger.error("Failed to get enhanced forecast by ID",
|
||||
forecast_id=forecast_id,
|
||||
error=str(e))
|
||||
logger.error("Failed to get forecast", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get forecast"
|
||||
detail="Failed to retrieve forecast"
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/tenants/{tenant_id}/forecasts/{forecast_id}")
|
||||
@track_execution_time("enhanced_delete_forecast_duration_seconds", "forecasting-service")
|
||||
async def delete_enhanced_forecast(
|
||||
@router.delete(
|
||||
route_builder.build_resource_detail_route("forecasts", "forecast_id")
|
||||
)
|
||||
async def delete_forecast(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
forecast_id: str = Path(..., description="Forecast ID"),
|
||||
request_obj: Request = None,
|
||||
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
|
||||
):
|
||||
"""Delete forecast using enhanced repository pattern"""
|
||||
metrics = get_metrics_collector(request_obj)
|
||||
|
||||
"""Delete a specific forecast"""
|
||||
try:
|
||||
# Record metrics
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_delete_forecast_total")
|
||||
|
||||
# Delete forecast using enhanced service
|
||||
deleted = await enhanced_forecasting_service.delete_forecast(forecast_id)
|
||||
|
||||
if not deleted:
|
||||
logger.info("Deleting forecast", tenant_id=tenant_id, forecast_id=forecast_id)
|
||||
|
||||
success = await enhanced_forecasting_service.delete_forecast(
|
||||
tenant_id=tenant_id,
|
||||
forecast_id=uuid.UUID(forecast_id)
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Forecast not found"
|
||||
)
|
||||
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_delete_forecast_success_total")
|
||||
|
||||
logger.info("Enhanced forecast deleted successfully",
|
||||
forecast_id=forecast_id,
|
||||
tenant_id=tenant_id)
|
||||
|
||||
return {
|
||||
"message": "Forecast deleted successfully",
|
||||
"forecast_id": forecast_id,
|
||||
"enhanced_features": True,
|
||||
"repository_integration": True
|
||||
}
|
||||
|
||||
|
||||
return {"message": "Forecast deleted successfully"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
logger.error("Invalid forecast ID", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid forecast ID format"
|
||||
)
|
||||
except Exception as e:
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_delete_forecast_errors_total")
|
||||
logger.error("Failed to delete enhanced forecast",
|
||||
forecast_id=forecast_id,
|
||||
error=str(e))
|
||||
logger.error("Failed to delete forecast", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to delete forecast"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/forecasts/statistics")
|
||||
@track_execution_time("enhanced_forecast_statistics_duration_seconds", "forecasting-service")
|
||||
async def get_enhanced_forecast_statistics(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
request_obj: Request = None,
|
||||
enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service)
|
||||
):
|
||||
"""Get comprehensive forecast statistics using enhanced repository pattern"""
|
||||
metrics = get_metrics_collector(request_obj)
|
||||
|
||||
try:
|
||||
# Record metrics
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_forecast_statistics_total")
|
||||
|
||||
# Get statistics using enhanced service
|
||||
statistics = await enhanced_forecasting_service.get_tenant_forecast_statistics(tenant_id)
|
||||
|
||||
if statistics.get("error"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=statistics["error"]
|
||||
)
|
||||
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_forecast_statistics_success_total")
|
||||
|
||||
return {
|
||||
**statistics,
|
||||
"enhanced_features": True,
|
||||
"repository_integration": True
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
if metrics:
|
||||
metrics.increment_counter("enhanced_forecast_statistics_errors_total")
|
||||
logger.error("Failed to get enhanced forecast statistics",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get forecast statistics"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def enhanced_health_check():
|
||||
"""Enhanced health check endpoint for the forecasting service"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "enhanced-forecasting-service",
|
||||
"version": "2.0.0",
|
||||
"features": [
|
||||
"repository-pattern",
|
||||
"dependency-injection",
|
||||
"enhanced-error-handling",
|
||||
"metrics-tracking",
|
||||
"transactional-operations",
|
||||
"batch-processing"
|
||||
],
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
Reference in New Issue
Block a user