first commit
This commit is contained in:
279
app/routers/normalization.py
Normal file
279
app/routers/normalization.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
Normalization API router for dynamic normalization management.
|
||||
|
||||
Endpoints:
|
||||
- GET /tryout/{tryout_id}/normalization: Get normalization configuration
|
||||
- PUT /tryout/{tryout_id}/normalization: Update normalization settings
|
||||
- POST /tryout/{tryout_id}/normalization/reset: Reset normalization stats
|
||||
- GET /tryout/{tryout_id}/normalization/validate: Validate dynamic normalization
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Header, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.services.config_management import (
|
||||
get_normalization_config,
|
||||
reset_normalization_stats,
|
||||
toggle_normalization_mode,
|
||||
update_config,
|
||||
)
|
||||
from app.services.normalization import (
|
||||
validate_dynamic_normalization,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/tryout", tags=["normalization"])
|
||||
|
||||
|
||||
def get_website_id_from_header(
|
||||
x_website_id: Optional[str] = Header(None, alias="X-Website-ID"),
|
||||
) -> int:
|
||||
"""
|
||||
Extract and validate website_id from request header.
|
||||
|
||||
Args:
|
||||
x_website_id: Website ID from header
|
||||
|
||||
Returns:
|
||||
Validated website ID as integer
|
||||
|
||||
Raises:
|
||||
HTTPException: If header is missing or invalid
|
||||
"""
|
||||
if x_website_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="X-Website-ID header is required",
|
||||
)
|
||||
try:
|
||||
return int(x_website_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="X-Website-ID must be a valid integer",
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{tryout_id}/normalization",
|
||||
summary="Get normalization configuration",
|
||||
description="Retrieve current normalization configuration including mode, static values, dynamic values, and threshold status.",
|
||||
)
|
||||
async def get_normalization_endpoint(
|
||||
tryout_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
):
|
||||
"""
|
||||
Get normalization configuration for a tryout.
|
||||
|
||||
Returns:
|
||||
Normalization configuration with:
|
||||
- mode (static/dynamic/hybrid)
|
||||
- current rataan, sb (from TryoutStats)
|
||||
- static_rataan, static_sb (from Tryout config)
|
||||
- participant_count
|
||||
- threshold_status (ready for dynamic or not)
|
||||
|
||||
Raises:
|
||||
HTTPException: If tryout not found
|
||||
"""
|
||||
try:
|
||||
config = await get_normalization_config(db, website_id, tryout_id)
|
||||
return config
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{tryout_id}/normalization",
|
||||
summary="Update normalization settings",
|
||||
description="Update normalization mode and static values for a tryout.",
|
||||
)
|
||||
async def update_normalization_endpoint(
|
||||
tryout_id: str,
|
||||
normalization_mode: Optional[str] = None,
|
||||
static_rataan: Optional[float] = None,
|
||||
static_sb: Optional[float] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
):
|
||||
"""
|
||||
Update normalization settings for a tryout.
|
||||
|
||||
Args:
|
||||
tryout_id: Tryout identifier
|
||||
normalization_mode: New normalization mode (static/dynamic/hybrid)
|
||||
static_rataan: New static mean value
|
||||
static_sb: New static standard deviation
|
||||
db: Database session
|
||||
website_id: Website ID from header
|
||||
|
||||
Returns:
|
||||
Updated normalization configuration
|
||||
|
||||
Raises:
|
||||
HTTPException: If tryout not found or validation fails
|
||||
"""
|
||||
# Build updates dictionary
|
||||
updates = {}
|
||||
|
||||
if normalization_mode is not None:
|
||||
if normalization_mode not in ["static", "dynamic", "hybrid"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid normalization_mode: {normalization_mode}. Must be 'static', 'dynamic', or 'hybrid'",
|
||||
)
|
||||
updates["normalization_mode"] = normalization_mode
|
||||
|
||||
if static_rataan is not None:
|
||||
if static_rataan <= 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="static_rataan must be greater than 0",
|
||||
)
|
||||
updates["static_rataan"] = static_rataan
|
||||
|
||||
if static_sb is not None:
|
||||
if static_sb <= 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="static_sb must be greater than 0",
|
||||
)
|
||||
updates["static_sb"] = static_sb
|
||||
|
||||
if not updates:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="No updates provided",
|
||||
)
|
||||
|
||||
try:
|
||||
# Update configuration
|
||||
await update_config(db, website_id, tryout_id, updates)
|
||||
|
||||
# Get updated configuration
|
||||
config = await get_normalization_config(db, website_id, tryout_id)
|
||||
|
||||
return config
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{tryout_id}/normalization/reset",
|
||||
summary="Reset normalization stats",
|
||||
description="Reset TryoutStats to initial values and switch to static normalization mode.",
|
||||
)
|
||||
async def reset_normalization_endpoint(
|
||||
tryout_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
):
|
||||
"""
|
||||
Reset normalization stats for a tryout.
|
||||
|
||||
Resets TryoutStats to initial values (participant_count=0, sums cleared)
|
||||
and temporarily switches normalization_mode to "static".
|
||||
|
||||
Args:
|
||||
tryout_id: Tryout identifier
|
||||
db: Database session
|
||||
website_id: Website ID from header
|
||||
|
||||
Returns:
|
||||
Success message with updated configuration
|
||||
|
||||
Raises:
|
||||
HTTPException: If tryout not found
|
||||
"""
|
||||
try:
|
||||
stats = await reset_normalization_stats(db, website_id, tryout_id)
|
||||
config = await get_normalization_config(db, website_id, tryout_id)
|
||||
|
||||
return {
|
||||
"message": "Normalization stats reset successfully",
|
||||
"tryout_id": tryout_id,
|
||||
"participant_count": stats.participant_count,
|
||||
"normalization_mode": config["normalization_mode"],
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{tryout_id}/normalization/validate",
|
||||
summary="Validate dynamic normalization",
|
||||
description="Validate that dynamic normalization produces expected distribution (mean≈500±5, SD≈100±5).",
|
||||
)
|
||||
async def validate_normalization_endpoint(
|
||||
tryout_id: str,
|
||||
target_mean: float = 500.0,
|
||||
target_sd: float = 100.0,
|
||||
mean_tolerance: float = 5.0,
|
||||
sd_tolerance: float = 5.0,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
website_id: int = Depends(get_website_id_from_header),
|
||||
):
|
||||
"""
|
||||
Validate dynamic normalization for a tryout.
|
||||
|
||||
Checks if calculated rataan and sb are close to target values.
|
||||
Returns validation status, deviations, warnings, and suggestions.
|
||||
|
||||
Args:
|
||||
tryout_id: Tryout identifier
|
||||
target_mean: Target mean (default: 500)
|
||||
target_sd: Target standard deviation (default: 100)
|
||||
mean_tolerance: Allowed deviation from target mean (default: 5)
|
||||
sd_tolerance: Allowed deviation from target SD (default: 5)
|
||||
db: Database session
|
||||
website_id: Website ID from header
|
||||
|
||||
Returns:
|
||||
Validation result with:
|
||||
- is_valid: True if within tolerance
|
||||
- details: Full validation details
|
||||
|
||||
Raises:
|
||||
HTTPException: If tryout not found
|
||||
"""
|
||||
try:
|
||||
is_valid, details = await validate_dynamic_normalization(
|
||||
db=db,
|
||||
website_id=website_id,
|
||||
tryout_id=tryout_id,
|
||||
target_mean=target_mean,
|
||||
target_sd=target_sd,
|
||||
mean_tolerance=mean_tolerance,
|
||||
sd_tolerance=sd_tolerance,
|
||||
)
|
||||
|
||||
return {
|
||||
"tryout_id": tryout_id,
|
||||
"is_valid": is_valid,
|
||||
"target_mean": target_mean,
|
||||
"target_sd": target_sd,
|
||||
"mean_tolerance": mean_tolerance,
|
||||
"sd_tolerance": sd_tolerance,
|
||||
"details": details,
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e),
|
||||
)
|
||||
Reference in New Issue
Block a user