Files
Code_of_Conquest/api/app/config.py
2025-11-24 23:10:55 -06:00

320 lines
8.7 KiB
Python

"""
Configuration loader for Code of Conquest.
Loads configuration from YAML files and environment variables,
providing typed access to all configuration values.
"""
import os
from dataclasses import dataclass, field
from typing import Dict, List, Optional
import yaml
from dotenv import load_dotenv
@dataclass
class AppConfig:
"""Application configuration."""
name: str
version: str
environment: str
debug: bool
@dataclass
class ServerConfig:
"""Server configuration."""
host: str
port: int
workers: int
@dataclass
class RedisConfig:
"""Redis configuration."""
host: str
port: int
db: int
max_connections: int
@property
def url(self) -> str:
"""Generate Redis URL."""
return f"redis://{self.host}:{self.port}/{self.db}"
@dataclass
class RQConfig:
"""RQ (Redis Queue) configuration."""
queues: List[str]
worker_timeout: int
job_timeout: int
@dataclass
class AIModelConfig:
"""AI model configuration."""
provider: str
model: str
max_tokens: int
temperature: float
@dataclass
class AIConfig:
"""AI service configuration."""
timeout: int
max_retries: int
cost_alert_threshold: float
models: Dict[str, AIModelConfig] = field(default_factory=dict)
@dataclass
class RateLimitTier:
"""Rate limit configuration for a subscription tier."""
requests_per_minute: int
ai_calls_per_day: int
custom_actions_per_day: int # -1 for unlimited
custom_action_char_limit: int
@dataclass
class RateLimitingConfig:
"""Rate limiting configuration."""
enabled: bool
storage_url: str
tiers: Dict[str, RateLimitTier] = field(default_factory=dict)
@dataclass
class AuthConfig:
"""Authentication configuration."""
cookie_name: str
duration_normal: int
duration_remember_me: int
http_only: bool
secure: bool
same_site: str
path: str
password_min_length: int
password_require_uppercase: bool
password_require_lowercase: bool
password_require_number: bool
password_require_special: bool
name_min_length: int
name_max_length: int
email_max_length: int
@dataclass
class SessionConfig:
"""Game session configuration."""
timeout_minutes: int
auto_save_interval: int
min_players: int
max_players_by_tier: Dict[str, int] = field(default_factory=dict)
@dataclass
class MarketplaceConfig:
"""Marketplace configuration."""
auction_check_interval: int
max_listings_by_tier: Dict[str, int] = field(default_factory=dict)
@dataclass
class CORSConfig:
"""CORS configuration."""
origins: List[str]
@dataclass
class LoggingConfig:
"""Logging configuration."""
level: str
format: str
handlers: List[str]
file_path: str
@dataclass
class Config:
"""
Main configuration container.
Loads configuration from YAML file based on environment,
with overrides from environment variables.
"""
app: AppConfig
server: ServerConfig
redis: RedisConfig
rq: RQConfig
ai: AIConfig
rate_limiting: RateLimitingConfig
auth: AuthConfig
session: SessionConfig
marketplace: MarketplaceConfig
cors: CORSConfig
logging: LoggingConfig
# Environment variables (loaded from .env)
secret_key: str = ""
appwrite_endpoint: str = ""
appwrite_project_id: str = ""
appwrite_api_key: str = ""
appwrite_database_id: str = ""
anthropic_api_key: str = ""
replicate_api_token: str = ""
@classmethod
def load(cls, environment: Optional[str] = None) -> 'Config':
"""
Load configuration from YAML file and environment variables.
Args:
environment: Environment name (development, production, etc.).
If not provided, uses FLASK_ENV from environment.
Returns:
Config: Loaded configuration object.
Raises:
FileNotFoundError: If config file not found.
ValueError: If required environment variables missing.
"""
# Load environment variables from .env file
load_dotenv()
# Determine environment
if environment is None:
environment = os.getenv('FLASK_ENV', 'development')
# Load YAML configuration
config_path = os.path.join('config', f'{environment}.yaml')
if not os.path.exists(config_path):
raise FileNotFoundError(
f"Configuration file not found: {config_path}"
)
with open(config_path, 'r') as f:
config_data = yaml.safe_load(f)
# Parse configuration sections
app_config = AppConfig(**config_data['app'])
server_config = ServerConfig(**config_data['server'])
redis_config = RedisConfig(**config_data['redis'])
rq_config = RQConfig(**config_data['rq'])
# Parse AI models
ai_models = {}
for tier, model_data in config_data['ai']['models'].items():
ai_models[tier] = AIModelConfig(**model_data)
ai_config = AIConfig(
timeout=config_data['ai']['timeout'],
max_retries=config_data['ai']['max_retries'],
cost_alert_threshold=config_data['ai']['cost_alert_threshold'],
models=ai_models
)
# Parse rate limiting tiers
rate_limit_tiers = {}
for tier, tier_data in config_data['rate_limiting']['tiers'].items():
rate_limit_tiers[tier] = RateLimitTier(**tier_data)
rate_limiting_config = RateLimitingConfig(
enabled=config_data['rate_limiting']['enabled'],
storage_url=config_data['rate_limiting']['storage_url'],
tiers=rate_limit_tiers
)
auth_config = AuthConfig(**config_data['auth'])
session_config = SessionConfig(**config_data['session'])
marketplace_config = MarketplaceConfig(**config_data['marketplace'])
cors_config = CORSConfig(**config_data['cors'])
logging_config = LoggingConfig(**config_data['logging'])
# Load environment variables (secrets)
secret_key = os.getenv('SECRET_KEY')
if not secret_key:
raise ValueError("SECRET_KEY environment variable is required")
appwrite_endpoint = os.getenv('APPWRITE_ENDPOINT', '')
appwrite_project_id = os.getenv('APPWRITE_PROJECT_ID', '')
appwrite_api_key = os.getenv('APPWRITE_API_KEY', '')
appwrite_database_id = os.getenv('APPWRITE_DATABASE_ID', 'main')
anthropic_api_key = os.getenv('ANTHROPIC_API_KEY', '')
replicate_api_token = os.getenv('REPLICATE_API_TOKEN', '')
# Create and return config object
return cls(
app=app_config,
server=server_config,
redis=redis_config,
rq=rq_config,
ai=ai_config,
rate_limiting=rate_limiting_config,
auth=auth_config,
session=session_config,
marketplace=marketplace_config,
cors=cors_config,
logging=logging_config,
secret_key=secret_key,
appwrite_endpoint=appwrite_endpoint,
appwrite_project_id=appwrite_project_id,
appwrite_api_key=appwrite_api_key,
appwrite_database_id=appwrite_database_id,
anthropic_api_key=anthropic_api_key,
replicate_api_token=replicate_api_token
)
def validate(self) -> None:
"""
Validate configuration values.
Raises:
ValueError: If configuration is invalid.
"""
# Validate AI API keys if needed
if self.app.environment == 'production':
if not self.anthropic_api_key:
raise ValueError(
"ANTHROPIC_API_KEY required in production environment"
)
if not self.appwrite_endpoint or not self.appwrite_project_id:
raise ValueError(
"Appwrite configuration required in production environment"
)
# Validate logging level
valid_log_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
if self.logging.level not in valid_log_levels:
raise ValueError(
f"Invalid log level: {self.logging.level}. "
f"Must be one of {valid_log_levels}"
)
# Global config instance (loaded lazily)
_config: Optional[Config] = None
def get_config(environment: Optional[str] = None) -> Config:
"""
Get the global configuration instance.
Args:
environment: Optional environment override.
Returns:
Config: Configuration object.
"""
global _config
if _config is None or environment is not None:
_config = Config.load(environment)
_config.validate()
return _config