""" TryoutStats model for tracking tryout-level statistics. Maintains running statistics for dynamic normalization and reporting. """ from datetime import datetime from typing import Union from sqlalchemy import CheckConstraint, DateTime, Float, ForeignKey, Index, Integer, String from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database import Base class TryoutStats(Base): """ TryoutStats model for maintaining tryout-level statistics. Tracks participant counts, score distributions, and calculated normalization parameters (rataan, sb) for dynamic normalization. Attributes: id: Primary key website_id: Website identifier tryout_id: Tryout identifier participant_count: Number of completed sessions total_nm_sum: Running sum of NM scores total_nm_sq_sum: Running sum of squared NM scores (for variance calc) rataan: Calculated mean of NM scores sb: Calculated standard deviation of NM scores min_nm: Minimum NM score observed max_nm: Maximum NM score observed last_calculated: Timestamp of last statistics update created_at: Record creation timestamp updated_at: Record update timestamp tryout: Tryout relationship """ __tablename__ = "tryout_stats" # Primary key id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) # Foreign keys website_id: Mapped[int] = mapped_column( ForeignKey("websites.id", ondelete="CASCADE", onupdate="CASCADE"), nullable=False, index=True, comment="Website identifier", ) tryout_id: Mapped[str] = mapped_column( String(255), nullable=False, index=True, comment="Tryout identifier", ) # Running statistics participant_count: Mapped[int] = mapped_column( Integer, nullable=False, default=0, comment="Number of completed sessions", ) total_nm_sum: Mapped[float] = mapped_column( Float, nullable=False, default=0.0, comment="Running sum of NM scores", ) total_nm_sq_sum: Mapped[float] = mapped_column( Float, nullable=False, default=0.0, comment="Running sum of squared NM scores", ) # Calculated statistics rataan: Mapped[Union[float, None]] = mapped_column( Float, nullable=True, comment="Calculated mean of NM scores", ) sb: Mapped[Union[float, None]] = mapped_column( Float, nullable=True, comment="Calculated standard deviation of NM scores", ) # Score range min_nm: Mapped[Union[int, None]] = mapped_column( Integer, nullable=True, comment="Minimum NM score observed", ) max_nm: Mapped[Union[int, None]] = mapped_column( Integer, nullable=True, comment="Maximum NM score observed", ) # Timestamps last_calculated: Mapped[Union[datetime, None]] = mapped_column( DateTime(timezone=True), nullable=True, comment="Timestamp of last statistics update", ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default="NOW()" ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default="NOW()", onupdate="NOW()", ) # Relationships tryout: Mapped["Tryout"] = relationship( "Tryout", back_populates="stats", lazy="selectin" ) # Constraints and indexes __table_args__ = ( Index( "ix_tryout_stats_website_id_tryout_id", "website_id", "tryout_id", unique=True, ), # Participant count must be non-negative CheckConstraint("participant_count >= 0", "ck_participant_count_non_negative"), # Min and max NM must be within valid range [0, 1000] CheckConstraint( "min_nm IS NULL OR (min_nm >= 0 AND min_nm <= 1000)", "ck_min_nm_range", ), CheckConstraint( "max_nm IS NULL OR (max_nm >= 0 AND max_nm <= 1000)", "ck_max_nm_range", ), # Min must be less than or equal to max CheckConstraint( "min_nm IS NULL OR max_nm IS NULL OR min_nm <= max_nm", "ck_min_max_nm_order", ), ) def __repr__(self) -> str: return f""