"""
Application Settings
Centralized application configuration with environment variable support
and validation.
"""
import os
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List
from pathlib import Path
from ..errors import handle_errors, ConfigurationError, create_success_response
@dataclass
class Settings:
"""Application-wide settings with environment variable support."""
# Git Operation Settings
default_sign_off: bool = field(default=True)
git_timeout: int = field(default=30)
max_commit_message_length: int = field(default=1000)
# Repository Settings
default_repo_path: Optional[str] = field(default=None)
allow_repository_traversal: bool = field(default=False)
# Plugin Settings
supported_plugins: List[str] = field(
default_factory=lambda: ["ConventionalCommitsCz", "Cz", "ConventionalCommits"]
)
plugin_timeout: int = field(default=10)
# MCP Server Settings
server_name: str = field(default="Commit Helper MCP")
log_level: str = field(default="INFO")
# Performance Settings
enable_caching: bool = field(default=True)
cache_ttl: int = field(default=300) # 5 minutes
# Security Settings
validate_file_paths: bool = field(default=True)
sanitize_inputs: bool = field(default=True)
def __post_init__(self):
"""Load settings from environment variables."""
self._load_from_environment()
self._validate_settings()
@handle_errors(log_errors=True, reraise=True)
def _load_from_environment(self):
"""Load configuration from environment variables."""
# Git settings
if os.getenv("COMMITIZEN_SIGN_OFF"):
self.default_sign_off = (
os.getenv("COMMITIZEN_SIGN_OFF", "").lower() == "true"
)
if os.getenv("COMMITIZEN_GIT_TIMEOUT"):
try:
self.git_timeout = int(os.getenv("COMMITIZEN_GIT_TIMEOUT", "30"))
except ValueError as e:
# For backward compatibility with tests, re-raise as ValueError
raise ValueError(f"Invalid value for COMMITIZEN_GIT_TIMEOUT: {e}")
# Repository settings
if os.getenv("COMMITIZEN_REPO_PATH"):
self.default_repo_path = os.getenv("COMMITIZEN_REPO_PATH")
# Logging
if os.getenv("COMMITIZEN_LOG_LEVEL"):
self.log_level = os.getenv("COMMITIZEN_LOG_LEVEL", "INFO").upper()
# Performance
if os.getenv("COMMITIZEN_ENABLE_CACHING"):
self.enable_caching = (
os.getenv("COMMITIZEN_ENABLE_CACHING", "").lower() == "true"
)
@handle_errors(log_errors=True)
def _validate_settings(self):
"""Validate configuration values."""
if self.git_timeout <= 0:
raise ConfigurationError(
"git_timeout must be positive", config_key="git_timeout"
)
if self.max_commit_message_length <= 0:
raise ConfigurationError(
"max_commit_message_length must be positive",
config_key="max_commit_message_length",
)
if self.log_level not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
raise ConfigurationError(
f"Invalid log_level: {self.log_level}", config_key="log_level"
)
if self.cache_ttl <= 0:
raise ConfigurationError(
"cache_ttl must be positive", config_key="cache_ttl"
)
@handle_errors(log_errors=True)
def to_dict(self) -> Dict[str, Any]:
"""Convert settings to dictionary."""
# For backward compatibility with tests, don't use create_success_response
return {
field.name: getattr(self, field.name)
for field in self.__dataclass_fields__.values()
}
@classmethod
@handle_errors(log_errors=True)
def from_dict(cls, data: Dict[str, Any]) -> "Settings":
"""Create settings from dictionary."""
if not isinstance(data, dict):
raise ConfigurationError(
"Settings data must be a dictionary", config_key="data"
)
return cls(**data)
# Global settings instance
_settings: Optional[Settings] = None
@handle_errors(log_errors=True)
def get_settings() -> Settings:
"""Get global settings instance (singleton pattern)."""
global _settings
if _settings is None:
_settings = Settings()
return _settings
@handle_errors(log_errors=True)
def reload_settings() -> Settings:
"""Reload settings from environment."""
global _settings
_settings = Settings()
return _settings