Phase 1 Database Implementation (DB-001 through DB-012): Models: - User: OAuth support (Google/Discord), premium subscriptions - Collection: Card ownership with CardSource enum - Deck: JSONB cards/energy_cards, validation state - CampaignProgress: One-to-one with User, medals/NPCs as JSONB - ActiveGame: In-progress games with GameType enum - GameHistory: Completed games with EndReason enum, replay data Infrastructure: - Alembic migrations with sync psycopg2 (avoids async issues) - Docker Compose for Postgres (5433) and Redis (6380) - App config with Pydantic settings - Redis client helper Test Infrastructure: - 68 database tests (47 model + 21 relationship) - Async factory pattern for test data creation - Sync TRUNCATE cleanup (solves pytest-asyncio event loop mismatch) - Uses dev containers instead of testcontainers for reliability Key technical decisions: - passive_deletes=True for ON DELETE SET NULL relationships - NullPool for test sessions (no connection reuse) - expire_on_commit=False with manual expire() for relationship tests
218 lines
6.0 KiB
Python
218 lines
6.0 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)",
|
|
)
|
|
|
|
# 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()
|