habit.core.common.config_base 源代码

"""
Base configuration classes for unified configuration management.

This module provides:
1. BaseConfig: Abstract base class for all configuration schemas
2. ConfigValidationError: Custom exception for configuration validation errors
3. ConfigAccessor: Unified interface for accessing configuration values
"""

from abc import ABC, abstractmethod
from typing import Dict, Any, Optional, Type, TypeVar, Union
from pathlib import Path
from pydantic import BaseModel, ValidationError
import logging

# Type variable for configuration classes
ConfigType = TypeVar('ConfigType', bound='BaseConfig')


[文档] class ConfigValidationError(Exception): """ Custom exception for configuration validation errors. Provides detailed information about validation failures. """
[文档] def __init__(self, message: str, errors: Optional[Dict[str, Any]] = None, config_path: Optional[str] = None): """ Initialize configuration validation error. Args: message: Error message errors: Detailed validation errors from Pydantic config_path: Path to the configuration file that failed validation """ super().__init__(message) self.message = message self.errors = errors or {} self.config_path = config_path
[文档] def __str__(self) -> str: """Format error message with details.""" msg = self.message if self.config_path: msg += f" (config file: {self.config_path})" if self.errors: msg += f"\nValidation errors: {self.errors}" return msg
[文档] class BaseConfig(BaseModel, ABC): """ Abstract base class for all configuration schemas in HABIT. Provides common functionality: - Version tracking - Configuration file path tracking - Validation hooks - Accessor methods All configuration classes should inherit from this base class. """ # Configuration metadata config_file: Optional[str] = None config_version: Optional[str] = None
[文档] class Config: """Pydantic configuration.""" extra = 'forbid' # Forbid extra fields by default validate_assignment = True # Validate on assignment use_enum_values = True # Use enum values
[文档] def __init__(self, **data: Any): """ Initialize configuration with validation. Args: **data: Configuration data Raises: ConfigValidationError: If validation fails """ try: super().__init__(**data) except ValidationError as e: raise ConfigValidationError( message=f"Configuration validation failed for {self.__class__.__name__}", errors=e.errors(), config_path=data.get('config_file') ) from e
[文档] @classmethod def from_dict(cls: Type[ConfigType], config_dict: Dict[str, Any], config_path: Optional[str] = None) -> ConfigType: """ Create configuration instance from dictionary. Args: config_dict: Configuration dictionary config_path: Optional path to configuration file (for error reporting) Returns: Configuration instance Raises: ConfigValidationError: If validation fails """ if config_path: config_dict['config_file'] = config_path try: return cls(**config_dict) except ValidationError as e: raise ConfigValidationError( message=f"Failed to create {cls.__name__} from dictionary", errors=e.errors(), config_path=config_path ) from e
[文档] @classmethod def from_file(cls: Type[ConfigType], config_path: Union[str, Path]) -> ConfigType: """ Load configuration from file. Args: config_path: Path to configuration file (YAML or JSON) Returns: Configuration instance Raises: FileNotFoundError: If configuration file not found ConfigValidationError: If validation fails """ from habit.core.common.config_loader import load_config config_path = Path(config_path) if not config_path.exists(): raise FileNotFoundError(f"Configuration file not found: {config_path}") config_dict = load_config(str(config_path), resolve_paths=True) return cls.from_dict(config_dict, config_path=str(config_path))
[文档] def to_dict(self, exclude_none: bool = False, exclude_unset: bool = False) -> Dict[str, Any]: """ Convert configuration to dictionary. Args: exclude_none: Whether to exclude None values exclude_unset: Whether to exclude unset values Returns: Configuration dictionary """ if hasattr(self, 'model_dump'): # Pydantic v2 return self.model_dump(exclude_none=exclude_none, exclude_unset=exclude_unset) else: # Pydantic v1 return self.dict(exclude_none=exclude_none, exclude_unset=exclude_unset)
[文档] def get(self, key: str, default: Any = None) -> Any: """ Get configuration value by key (dictionary-like access). This method provides backward compatibility with dictionary access patterns. However, direct attribute access (config.field_name) is preferred. Args: key: Configuration key (supports dot notation for nested keys) default: Default value if key not found Returns: Configuration value or default """ try: # Support dot notation for nested access if '.' in key: parts = key.split('.') value = self for part in parts: if hasattr(value, part): value = getattr(value, part) elif isinstance(value, dict): value = value.get(part, default) else: return default return value else: # Direct attribute access if hasattr(self, key): return getattr(self, key) elif hasattr(self, 'model_dump'): # Pydantic v2 return self.model_dump().get(key, default) else: # Pydantic v1 return self.dict().get(key, default) except (AttributeError, KeyError, TypeError): return default
[文档] def validate(self) -> bool: """ Validate configuration (re-validate after modifications). Returns: True if valid Raises: ConfigValidationError: If validation fails """ try: # Trigger validation by creating a new instance self.__class__(**self.to_dict()) return True except ValidationError as e: raise ConfigValidationError( message=f"Configuration validation failed for {self.__class__.__name__}", errors=e.errors(), config_path=self.config_file ) from e
[文档] def __getitem__(self, key: str) -> Any: """ Dictionary-like access for backward compatibility. Prefer direct attribute access: config.field_name """ return self.get(key)
[文档] def __contains__(self, key: str) -> bool: """Check if configuration contains a key.""" return hasattr(self, key) or key in self.to_dict()
[文档] class ConfigAccessor: """ Unified interface for accessing configuration values. Provides a consistent API for accessing configuration regardless of whether it's a Pydantic model or a dictionary. This class helps transition from dictionary-based config access to strongly-typed Pydantic model access. """
[文档] def __init__(self, config: Union[BaseConfig, Dict[str, Any]]): """ Initialize config accessor. Args: config: Configuration object (BaseConfig instance or dict) """ self._config = config self._is_pydantic = isinstance(config, BaseConfig)
[文档] def get(self, key: str, default: Any = None) -> Any: """ Get configuration value by key. Supports: - Direct attribute access for Pydantic models: config.field_name - Dot notation for nested access: config.section.subsection.field - Dictionary access for backward compatibility Args: key: Configuration key (supports dot notation) default: Default value if key not found Returns: Configuration value or default """ if self._is_pydantic: return self._config.get(key, default) else: # Dictionary access with dot notation support if '.' in key: parts = key.split('.') value = self._config for part in parts: if isinstance(value, dict) and part in value: value = value[part] else: return default return value else: return self._config.get(key, default)
[文档] def has(self, key: str) -> bool: """ Check if configuration contains a key. Args: key: Configuration key (supports dot notation) Returns: True if key exists """ if self._is_pydantic: return key in self._config else: if '.' in key: parts = key.split('.') value = self._config for part in parts: if isinstance(value, dict) and part in value: value = value[part] else: return False return True else: return key in self._config
[文档] def get_section(self, section_name: str) -> Optional[Union[BaseConfig, Dict[str, Any]]]: """ Get a configuration section. Args: section_name: Section name (supports dot notation) Returns: Configuration section or None """ value = self.get(section_name) if value is None: return None # If it's a Pydantic model or dict, return as ConfigAccessor if isinstance(value, (BaseConfig, dict)): return ConfigAccessor(value) return value
@property def raw_config(self) -> Union[BaseConfig, Dict[str, Any]]: """Get raw configuration object.""" return self._config
[文档] def to_dict(self) -> Dict[str, Any]: """Convert configuration to dictionary.""" if self._is_pydantic: return self._config.to_dict() else: return self._config.copy() if isinstance(self._config, dict) else dict(self._config)