mantimon-tcg/backend/app/config.py
Cal Corum 2a95316f04 Add FastAPI lifespan hooks and fix Phase 1 gaps
- Add lifespan context manager to app/main.py with startup/shutdown hooks
- Wire startup: init_db(), init_redis(), CardService.load_all()
- Wire shutdown: close_db(), close_redis()
- Add /health/ready endpoint for readiness checks
- Add CORS middleware with configurable origins
- Disable docs in production (only available in dev)

- Export get_session_dependency from app/db/__init__.py for FastAPI DI
- Add game_cache_ttl_seconds to Settings (configurable, was hardcoded)
- Fix datetime.utcnow() deprecation (4 occurrences) -> datetime.now(UTC)
- Update test to match S3 image URL (was placeholder CDN)

All 974 tests passing.
2026-01-27 15:37:19 -06:00

224 lines
6.2 KiB
Python

"""Application configuration using Pydantic Settings.
This module provides environment-based configuration for the Mantimon TCG backend.
Configuration values can be set via environment variables or .env files.
Environment Detection:
- dev: Local development (default)
- staging: Pre-production testing
- prod: Production environment
Example .env file:
ENVIRONMENT=dev
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/mantimon
REDIS_URL=redis://localhost:6379/0
SECRET_KEY=your-secret-key-here
Usage:
from app.config import settings
print(settings.database_url)
print(settings.is_production)
"""
from functools import lru_cache
from typing import Literal
from pydantic import Field, PostgresDsn, RedisDsn, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Application settings loaded from environment variables.
All settings can be overridden via environment variables.
Prefix: None (direct variable names)
Attributes:
environment: Current environment (dev/staging/prod).
debug: Enable debug mode (auto-set based on environment).
database_url: PostgreSQL connection URL.
database_pool_size: Connection pool size.
database_max_overflow: Max overflow connections.
redis_url: Redis connection URL.
redis_max_connections: Max Redis connections.
secret_key: Secret key for JWT signing.
jwt_algorithm: JWT signing algorithm.
jwt_expire_minutes: JWT token expiration in minutes.
google_client_id: Google OAuth client ID.
google_client_secret: Google OAuth client secret.
discord_client_id: Discord OAuth client ID.
discord_client_secret: Discord OAuth client secret.
cors_origins: Allowed CORS origins.
"""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# Environment
environment: Literal["dev", "staging", "prod"] = Field(
default="dev",
description="Current environment",
)
debug: bool = Field(
default=False,
description="Enable debug mode",
)
# Database (PostgreSQL)
database_url: PostgresDsn = Field(
default="postgresql+asyncpg://mantimon:mantimon@localhost:5433/mantimon",
description="PostgreSQL connection URL",
)
database_pool_size: int = Field(
default=5,
ge=1,
le=50,
description="Database connection pool size",
)
database_max_overflow: int = Field(
default=10,
ge=0,
le=100,
description="Max overflow connections beyond pool size",
)
database_echo: bool = Field(
default=False,
description="Echo SQL statements (for debugging)",
)
# Redis
redis_url: RedisDsn = Field(
default="redis://localhost:6380/0",
description="Redis connection URL",
)
redis_max_connections: int = Field(
default=10,
ge=1,
le=100,
description="Max Redis connections in pool",
)
# Security
secret_key: SecretStr = Field(
default="dev-secret-key-change-in-production",
description="Secret key for JWT and other cryptographic operations",
)
jwt_algorithm: str = Field(
default="HS256",
description="JWT signing algorithm",
)
jwt_expire_minutes: int = Field(
default=30,
ge=1,
description="JWT access token expiration in minutes",
)
jwt_refresh_expire_days: int = Field(
default=7,
ge=1,
description="JWT refresh token expiration in days",
)
# OAuth - Google
google_client_id: str | None = Field(
default=None,
description="Google OAuth 2.0 client ID",
)
google_client_secret: SecretStr | None = Field(
default=None,
description="Google OAuth 2.0 client secret",
)
# OAuth - Discord
discord_client_id: str | None = Field(
default=None,
description="Discord OAuth 2.0 client ID",
)
discord_client_secret: SecretStr | None = Field(
default=None,
description="Discord OAuth 2.0 client secret",
)
# CORS
cors_origins: list[str] = Field(
default=["http://localhost:3000", "http://localhost:5173"],
description="Allowed CORS origins",
)
# Game Settings
turn_timeout_seconds: int = Field(
default=120,
ge=30,
le=600,
description="Turn timeout in seconds (2 minutes default)",
)
game_cache_ttl_seconds: int = Field(
default=86400, # 24 hours
ge=3600,
le=604800, # 1 week max
description="Game state cache TTL in Redis (seconds)",
)
# Card Data
card_data_path: str = Field(
default="data/cards",
description="Path to bundled card JSON files",
)
@field_validator("debug", mode="before")
@classmethod
def set_debug_from_environment(cls, v: bool, info) -> bool:
"""Auto-enable debug in dev environment if not explicitly set."""
if v is not None:
return v
env = info.data.get("environment", "dev")
return env == "dev"
@property
def is_production(self) -> bool:
"""Check if running in production environment."""
return self.environment == "prod"
@property
def is_development(self) -> bool:
"""Check if running in development environment."""
return self.environment == "dev"
@property
def database_url_sync(self) -> str:
"""Get synchronous database URL (for Alembic migrations).
Converts asyncpg:// to psycopg2:// for sync operations.
"""
url = str(self.database_url)
return url.replace("postgresql+asyncpg://", "postgresql://")
@lru_cache
def get_settings() -> Settings:
"""Get cached settings instance.
Uses lru_cache to ensure settings are only loaded once.
Returns:
Settings instance loaded from environment.
Example:
settings = get_settings()
print(settings.database_url)
"""
return Settings()
# Convenience alias for direct import
settings = get_settings()