""" 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, ForeignKeyConstraint, Index, Integer, String, func, ) 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=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now(), ) # Relationships tryout: Mapped["Tryout"] = relationship( "Tryout", back_populates="stats", lazy="selectin" ) # Constraints and indexes __table_args__ = ( ForeignKeyConstraint( ["website_id", "tryout_id"], ["tryouts.website_id", "tryouts.tryout_id"], name="fk_tryout_stats_tryout", ondelete="CASCADE", onupdate="CASCADE", ), 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""