""" Centralized configuration for Golf game server. Configuration is loaded from (in order of precedence): 1. Environment variables 2. .env file (if exists) 3. Default values Usage: from config import config print(config.PORT) print(config.card_values) """ import os from dataclasses import dataclass, field from pathlib import Path from typing import Optional # Load .env file if it exists try: from dotenv import load_dotenv # Check server/.env first, then project root .env env_path = Path(__file__).parent / ".env" if not env_path.exists(): env_path = Path(__file__).parent.parent / ".env" if env_path.exists(): load_dotenv(env_path) except ImportError: pass # python-dotenv not installed, use env vars only def get_env(key: str, default: str = "") -> str: """Get environment variable with default.""" return os.environ.get(key, default) def get_env_bool(key: str, default: bool = False) -> bool: """Get boolean environment variable.""" val = os.environ.get(key, "").lower() if val in ("true", "1", "yes", "on"): return True if val in ("false", "0", "no", "off"): return False return default def get_env_int(key: str, default: int = 0) -> int: """Get integer environment variable.""" try: return int(os.environ.get(key, str(default))) except ValueError: return default @dataclass class CardValues: """Card point values - the single source of truth.""" ACE: int = 1 TWO: int = -2 THREE: int = 3 FOUR: int = 4 FIVE: int = 5 SIX: int = 6 SEVEN: int = 7 EIGHT: int = 8 NINE: int = 9 TEN: int = 10 JACK: int = 10 QUEEN: int = 10 KING: int = 0 JOKER: int = -2 # House rule modifiers SUPER_KINGS: int = -2 # King value when super_kings enabled TEN_PENNY: int = 1 # 10 value when ten_penny enabled LUCKY_SWING_JOKER: int = -5 # Joker value when lucky_swing enabled def to_dict(self) -> dict[str, int]: """Get card values as dictionary for game use.""" return { 'A': self.ACE, '2': self.TWO, '3': self.THREE, '4': self.FOUR, '5': self.FIVE, '6': self.SIX, '7': self.SEVEN, '8': self.EIGHT, '9': self.NINE, '10': self.TEN, 'J': self.JACK, 'Q': self.QUEEN, 'K': self.KING, '★': self.JOKER, } @dataclass class GameDefaults: """Default game settings.""" rounds: int = 9 initial_flips: int = 2 use_jokers: bool = False flip_mode: str = "never" # "never", "always", or "endgame" @dataclass class ServerConfig: """Server configuration.""" HOST: str = "0.0.0.0" PORT: int = 8000 DEBUG: bool = False LOG_LEVEL: str = "INFO" # Environment (development, staging, production) ENVIRONMENT: str = "development" # Database (SQLite for legacy analytics/auth) DATABASE_URL: str = "sqlite:///games.db" # PostgreSQL for V2 event store # Format: postgresql://user:password@host:port/database POSTGRES_URL: str = "" # Redis for V2 live state cache and pub/sub # Format: redis://host:port or redis://:password@host:port REDIS_URL: str = "" # Email settings (Resend integration) RESEND_API_KEY: str = "" EMAIL_FROM: str = "Golf Game " BASE_URL: str = "http://localhost:8000" # Session settings SESSION_EXPIRY_HOURS: int = 168 # 1 week # Email verification REQUIRE_EMAIL_VERIFICATION: bool = False # Room settings MAX_PLAYERS_PER_ROOM: int = 6 ROOM_TIMEOUT_MINUTES: int = 60 ROOM_CODE_LENGTH: int = 4 # Security (for future auth system) SECRET_KEY: str = "" INVITE_ONLY: bool = False ADMIN_EMAILS: list[str] = field(default_factory=list) # Rate limiting RATE_LIMIT_ENABLED: bool = True # Error tracking (Sentry) SENTRY_DSN: str = "" # Card values card_values: CardValues = field(default_factory=CardValues) # Game defaults game_defaults: GameDefaults = field(default_factory=GameDefaults) @classmethod def from_env(cls) -> "ServerConfig": """Load configuration from environment variables.""" admin_emails_str = get_env("ADMIN_EMAILS", "") admin_emails = [e.strip() for e in admin_emails_str.split(",") if e.strip()] return cls( HOST=get_env("HOST", "0.0.0.0"), PORT=get_env_int("PORT", 8000), DEBUG=get_env_bool("DEBUG", False), LOG_LEVEL=get_env("LOG_LEVEL", "INFO"), ENVIRONMENT=get_env("ENVIRONMENT", "development"), DATABASE_URL=get_env("DATABASE_URL", "sqlite:///games.db"), POSTGRES_URL=get_env("POSTGRES_URL", ""), REDIS_URL=get_env("REDIS_URL", ""), RESEND_API_KEY=get_env("RESEND_API_KEY", ""), EMAIL_FROM=get_env("EMAIL_FROM", "Golf Game "), BASE_URL=get_env("BASE_URL", "http://localhost:8000"), SESSION_EXPIRY_HOURS=get_env_int("SESSION_EXPIRY_HOURS", 168), REQUIRE_EMAIL_VERIFICATION=get_env_bool("REQUIRE_EMAIL_VERIFICATION", False), MAX_PLAYERS_PER_ROOM=get_env_int("MAX_PLAYERS_PER_ROOM", 6), ROOM_TIMEOUT_MINUTES=get_env_int("ROOM_TIMEOUT_MINUTES", 60), ROOM_CODE_LENGTH=get_env_int("ROOM_CODE_LENGTH", 4), SECRET_KEY=get_env("SECRET_KEY", ""), INVITE_ONLY=get_env_bool("INVITE_ONLY", False), ADMIN_EMAILS=admin_emails, RATE_LIMIT_ENABLED=get_env_bool("RATE_LIMIT_ENABLED", True), SENTRY_DSN=get_env("SENTRY_DSN", ""), card_values=CardValues( ACE=get_env_int("CARD_ACE", 1), TWO=get_env_int("CARD_TWO", -2), KING=get_env_int("CARD_KING", 0), JOKER=get_env_int("CARD_JOKER", -2), SUPER_KINGS=get_env_int("CARD_SUPER_KINGS", -2), TEN_PENNY=get_env_int("CARD_TEN_PENNY", 1), LUCKY_SWING_JOKER=get_env_int("CARD_LUCKY_SWING_JOKER", -5), ), game_defaults=GameDefaults( rounds=get_env_int("DEFAULT_ROUNDS", 9), initial_flips=get_env_int("DEFAULT_INITIAL_FLIPS", 2), use_jokers=get_env_bool("DEFAULT_USE_JOKERS", False), flip_mode=get_env("DEFAULT_FLIP_MODE", "never"), ), ) # Global config instance - loaded once at module import config = ServerConfig.from_env() def reload_config() -> ServerConfig: """Reload configuration from environment (useful for testing).""" global config config = ServerConfig.from_env() return config