280 lines
8.3 KiB
Python
280 lines
8.3 KiB
Python
"""
|
|
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),
|
|
)
|