214 lines
6.1 KiB
Python
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 |