Files
yellow-bank-soal/app/main.py
2026-04-30 11:35:56 +07:00

254 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
IRT Bank Soal - Adaptive Question Bank System
Main FastAPI application entry point.
Features:
- CTT (Classical Test Theory) scoring with exact Excel formulas
- IRT (Item Response Theory) support for adaptive testing
- Multi-website support for WordPress integration
- AI-powered question generation
"""
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.v1.session import (
admin_router as adaptive_admin_router,
router as adaptive_session_router,
)
from app.admin_web import (
configure_admin_web,
router as admin_web_router,
shutdown_admin_web,
)
from app.core.config import get_settings
from app.database import close_db, init_db
from app.routers import (
admin_router,
ai_router,
import_export_router,
reports_router,
sessions_router,
tryouts_router,
wordpress_router,
)
settings = get_settings()
def validate_security_config() -> None:
"""
Enforce minimum security requirements for production deployments.
"""
if settings.ENVIRONMENT != "production":
return
insecure_secret_values = {
"",
"dev-secret-key-change-in-production",
"your-secret-key-here-change-in-production",
}
if settings.SECRET_KEY in insecure_secret_values:
raise RuntimeError(
"In production, SECRET_KEY must be set to a strong non-default value."
)
if settings.ENABLE_ADMIN and (
not settings.ADMIN_USERNAME
or not settings.ADMIN_PASSWORD
or settings.ADMIN_PASSWORD == "change-me"
):
raise RuntimeError(
"In production with ENABLE_ADMIN=true, ADMIN_USERNAME and ADMIN_PASSWORD must be configured securely."
)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""
Application lifespan manager.
Handles startup and shutdown events.
"""
validate_security_config()
# Startup: Initialize database
await init_db()
if settings.ENABLE_ADMIN:
await configure_admin_web()
yield
# Shutdown: Close database connections
if settings.ENABLE_ADMIN:
await shutdown_admin_web()
await close_db()
# Initialize FastAPI application
app = FastAPI(
title="IRT Bank Soal",
description="""
## Adaptive Question Bank System with IRT/CTT Scoring
This API provides a comprehensive backend for adaptive assessment systems.
### Features
- **CTT Scoring**: Classical Test Theory with exact Excel formula compatibility
- **IRT Support**: Item Response Theory for adaptive testing (1PL Rasch model)
- **Multi-Site**: Single backend serving multiple WordPress sites
- **AI Generation**: Automatic question variant generation
### Scoring Formulas (PRD Section 13.1)
- **CTT p-value**: `p = Σ Benar / Total Peserta`
- **CTT Bobot**: `Bobot = 1 - p`
- **CTT NM**: `NM = (Total_Bobot_Siswa / Total_Bobot_Max) × 1000`
- **CTT NN**: `NN = 500 + 100 × ((NM - Rataan) / SB)`
### Authentication
Most endpoints require `X-Website-ID` header for multi-site isolation.
""",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
lifespan=lifespan,
)
# Configure CORS middleware
# Parse ALLOWED_ORIGINS from settings (comma-separated string)
allowed_origins = settings.ALLOWED_ORIGINS
if isinstance(allowed_origins, str):
allowed_origins = [origin.strip() for origin in allowed_origins.split(",") if origin.strip()]
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Health check endpoint
@app.get(
"/",
summary="Health check",
description="Returns API status and version information.",
tags=["health"],
)
async def root():
"""
Health check endpoint.
Returns basic API information for monitoring and load balancer checks.
"""
return {
"status": "healthy",
"service": "IRT Bank Soal",
"version": "1.0.0",
"docs": "/docs",
}
@app.get(
"/health",
summary="Detailed health check",
description="Returns detailed health status including database connectivity.",
tags=["health"],
)
async def health_check():
"""
Detailed health check endpoint.
Includes database connectivity verification.
"""
from app.database import engine
from sqlalchemy import text
db_status = "unknown"
try:
async with engine.connect() as conn:
await conn.execute(text("SELECT 1"))
db_status = "connected"
except Exception as e:
db_status = f"error: {str(e)}"
return {
"status": "healthy" if db_status == "connected" else "degraded",
"service": "IRT Bank Soal",
"version": "1.0.0",
"database": db_status,
"environment": settings.ENVIRONMENT,
}
# Include API routers with version prefix
app.include_router(
import_export_router,
)
app.include_router(
sessions_router,
prefix=f"{settings.API_V1_STR}",
)
app.include_router(
adaptive_session_router,
prefix=f"{settings.API_V1_STR}/session",
)
app.include_router(
tryouts_router,
prefix=f"{settings.API_V1_STR}",
)
app.include_router(
wordpress_router,
prefix=f"{settings.API_V1_STR}",
)
app.include_router(
reports_router,
prefix=f"{settings.API_V1_STR}",
)
if settings.ENABLE_ADMIN:
app.include_router(
ai_router,
prefix=f"{settings.API_V1_STR}",
)
app.include_router(
adaptive_admin_router,
prefix=f"{settings.API_V1_STR}/admin",
)
app.include_router(admin_web_router)
# Include admin API router for custom actions
app.include_router(
admin_router,
prefix=f"{settings.API_V1_STR}",
)
# Placeholder routers for future implementation
# These will be implemented in subsequent phases
# app.include_router(
# items_router,
# prefix=f"{settings.API_V1_STR}",
# tags=["items"],
# )
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=8000,
reload=settings.ENVIRONMENT == "development",
)