Big-Link-Man/src/core/config.py

214 lines
6.1 KiB
Python

"""
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