REFACTOR ALL APIs

This commit is contained in:
Urtzi Alfaro
2025-10-06 15:27:01 +02:00
parent dc8221bd2f
commit 38fb98bc27
166 changed files with 18454 additions and 13605 deletions

View File

@@ -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()
}