""" Configuration loading and validation module """ import json import os from pathlib import Path from typing import Dict, Any, Optional from pydantic import BaseModel, Field from dotenv import load_dotenv class DatabaseConfig(BaseModel): url: str echo: bool = False pool_size: int = 5 max_overflow: int = 10 class AIServiceConfig(BaseModel): provider: str = "openrouter" base_url: str = "https://openrouter.ai/api/v1" model: str = "anthropic/claude-3.5-sonnet" max_tokens: int = 4000 temperature: float = 0.7 timeout: int = 30 available_models: Dict[str, str] = Field(default_factory=dict) class UniversalRulesConfig(BaseModel): min_content_length: int = 1000 max_content_length: int = 5000 word_count_tolerance: int = 10 default_term_frequency: int = 2 title_exact_match_required: bool = True h1_exact_match_required: bool = True h2_exact_match_min: int = 1 h3_exact_match_min: int = 1 faq_section_required: bool = True faq_question_restatement_required: bool = True image_alt_text_keyword_required: bool = True image_alt_text_entity_required: bool = True class CORAValidationConfig(BaseModel): enabled: bool = True tier_1_strict: bool = True tier_2_plus_warn_only: bool = True round_averages_down: bool = True class ContentRulesConfig(BaseModel): universal: UniversalRulesConfig cora_validation: CORAValidationConfig class TemplateConfig(BaseModel): default: str = "basic" mappings: Dict[str, str] = Field(default_factory=dict) class DeploymentConfig(BaseModel): providers: Dict[str, Dict[str, Any]] = Field(default_factory=dict) class InterlinkingConfig(BaseModel): wheel_links: bool = True home_page_link: bool = True random_article_link: bool = True max_links_per_article: int = 5 class LoggingConfig(BaseModel): level: str = "INFO" format: str = "json" file: str = "logs/app.log" max_size: str = "10MB" backup_count: int = 5 class APIConfig(BaseModel): host: str = "0.0.0.0" port: int = 8000 reload: bool = True workers: int = 1 class ApplicationConfig(BaseModel): name: str version: str environment: str class Config(BaseModel): application: ApplicationConfig database: DatabaseConfig ai_service: AIServiceConfig content_rules: ContentRulesConfig templates: TemplateConfig deployment: DeploymentConfig interlinking: InterlinkingConfig logging: LoggingConfig api: APIConfig def get(self, key: str, default: Any = None) -> Any: """Get config value using dot notation (e.g., 'content_rules.universal')""" try: parts = key.split('.') value = self for part in parts: if hasattr(value, part): value = getattr(value, part) else: return default if isinstance(value, BaseModel): return value.model_dump() return value except Exception: return default class ConfigManager: """Manages application configuration loading and validation""" def __init__(self, config_path: Optional[str] = None): self.config_path = config_path or "master.config.json" self._config: Optional[Config] = None def load_config(self) -> Config: """Load and validate configuration from JSON file""" if self._config is None: # Load environment variables first load_dotenv() # Load JSON configuration config_path = Path(self.config_path) if not config_path.exists(): raise FileNotFoundError(f"Configuration file not found: {config_path}") with open(config_path, 'r') as f: config_data = json.load(f) # Override with environment variables where applicable self._override_with_env(config_data) # Validate configuration self._config = Config(**config_data) return self._config def _override_with_env(self, config_data: Dict[str, Any]) -> None: """Override configuration with environment variables""" # Database URL if os.getenv("DATABASE_URL"): config_data["database"]["url"] = os.getenv("DATABASE_URL") # AI Service configuration if os.getenv("AI_API_KEY"): # Note: API key is handled separately for security pass if os.getenv("AI_API_BASE_URL"): config_data["ai_service"]["base_url"] = os.getenv("AI_API_BASE_URL") if os.getenv("AI_MODEL"): config_data["ai_service"]["model"] = os.getenv("AI_MODEL") # Logging level if os.getenv("LOG_LEVEL"): config_data["logging"]["level"] = os.getenv("LOG_LEVEL") # Environment if os.getenv("ENVIRONMENT"): config_data["application"]["environment"] = os.getenv("ENVIRONMENT") def get_config(self) -> Config: """Get the current configuration""" if self._config is None: return self.load_config() return self._config def reload_config(self) -> Config: """Reload configuration from file""" self._config = None return self.load_config() # Global configuration instance config_manager = ConfigManager() def get_config() -> Config: """Get the application configuration""" return config_manager.get_config() def reload_config() -> Config: """Reload the application configuration""" return config_manager.reload_config() def get_ai_api_key() -> str: """Get the AI API key from environment variables""" api_key = os.getenv("AI_API_KEY") if not api_key: raise ValueError("AI_API_KEY environment variable is required") return api_key def get_bunny_account_api_key() -> str: """Get the bunny.net Account API key from environment variables""" api_key = os.getenv("BUNNY_ACCOUNT_API_KEY") if not api_key: raise ValueError("BUNNY_ACCOUNT_API_KEY environment variable is required") return api_key