Files
journal_organizer/config.py
T

334 lines
12 KiB
Python
Raw Normal View History

"""
配置管理模块
负责加载和管理系统的所有配置参数
Enhanced with Pydantic validation and comprehensive error handling
"""
import json
import logging
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, Any, Optional, List
from .dependency_manager import get_dependency_manager
# Import the new validation framework
from .config_validation import (
SystemConfig, ConfigurationValidator,
ObsidianConfig as PydanticObsidianConfig,
ClaudeAPIConfig as PydanticClaudeAPIConfig,
JournalConfig as PydanticJournalConfig,
OutputConfig as PydanticOutputConfig,
AnalysisConfig as PydanticAnalysisConfig,
LoggingConfig as PydanticLoggingConfig
)
# Try to import yaml with graceful degradation
dependency_manager = get_dependency_manager()
yaml = dependency_manager.get_module('yaml')
@dataclass
class ObsidianConfig:
"""Obsidian 配置"""
vault_path: str
rest_api_url: str
rest_api_key: str
verify_ssl: bool = False
def __post_init__(self) -> None:
"""Validate configuration after initialization"""
if not self.vault_path or not self.vault_path.strip():
raise ValueError("vault_path cannot be empty")
if not self.rest_api_key or not self.rest_api_key.strip():
raise ValueError("rest_api_key cannot be empty")
if not self.rest_api_url or not self.rest_api_url.strip():
raise ValueError("rest_api_url cannot be empty")
if not self.rest_api_url.startswith(("http://", "https://")):
raise ValueError("rest_api_url must start with http:// or https://")
@dataclass
class ClaudeConfig:
"""Claude API 配置"""
api_key: str
model: str = "claude-3-5-sonnet-20241022"
api_url: str = "https://api.anthropic.com"
max_tokens: int = 4096
temperature: float = 0.7
def __post_init__(self) -> None:
"""Validate configuration after initialization"""
if not self.api_key or not self.api_key.strip():
raise ValueError("api_key cannot be empty")
if self.max_tokens <= 0:
raise ValueError("max_tokens must be positive")
if not 0.0 <= self.temperature <= 2.0:
raise ValueError("temperature must be between 0.0 and 2.0")
if not self.api_url or not self.api_url.strip():
raise ValueError("api_url cannot be empty")
if not self.api_url.startswith(("http://", "https://")):
raise ValueError("api_url must start with http:// or https://")
@dataclass
class JournalConfig:
"""日记配置"""
daily_notes_folder: str
date_format: str = "YYYY-MM-DD"
file_extension: str = ".md"
def __post_init__(self) -> None:
"""Validate configuration after initialization"""
if not self.daily_notes_folder or not self.daily_notes_folder.strip():
raise ValueError("daily_notes_folder cannot be empty")
if not self.file_extension.startswith("."):
raise ValueError("file_extension must start with a dot")
@dataclass
class OutputConfig:
"""输出配置"""
experiences_folder: str
lessons_folder: str
tasks_folder: str
problems_folder: str
achievements_folder: str
improvements_folder: str
def __post_init__(self) -> None:
"""Validate configuration after initialization"""
folders = [
self.experiences_folder,
self.lessons_folder,
self.tasks_folder,
self.problems_folder,
self.achievements_folder,
self.improvements_folder,
]
for folder in folders:
if not folder or not folder.strip():
raise ValueError("All folder paths must be non-empty")
@dataclass
class AnalysisConfig:
"""分析配置"""
categories: List[str] = field(default_factory=list)
extraction_rules: Dict[str, Dict[str, Any]] = field(default_factory=dict)
def __post_init__(self) -> None:
"""Validate configuration after initialization"""
if not isinstance(self.categories, list):
raise ValueError("categories must be a list")
if not isinstance(self.extraction_rules, dict):
raise ValueError("extraction_rules must be a dictionary")
@dataclass
class LoggingConfig:
"""日志配置"""
level: str = "INFO"
file: str = "logs/journal_organizer.log"
def __post_init__(self) -> None:
"""Validate configuration after initialization"""
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
if self.level not in valid_levels:
raise ValueError(f"level must be one of {valid_levels}")
if not self.file or not self.file.strip():
raise ValueError("file path cannot be empty")
class Config:
"""系统配置管理器 - Enhanced with Pydantic validation"""
def __init__(self, config_file: Optional[str] = None):
"""
初始化配置
Args:
config_file: 配置文件路径,如果为 None 则使用默认位置
"""
self.config_file = config_file or self._get_default_config_path()
self.validator = ConfigurationValidator()
self.logger = logging.getLogger(__name__)
# Load and validate configuration using Pydantic
try:
self.system_config = self.validator.load_and_validate_config(self.config_file)
except (FileNotFoundError, ValueError) as e:
self.logger.error(f"Configuration error: {e}")
raise
# Create legacy-compatible attributes
self._create_legacy_attributes()
# Perform additional validations
self._perform_additional_validations()
def _create_legacy_attributes(self) -> None:
"""Create legacy-compatible attributes from Pydantic models"""
# Convert Pydantic models to legacy dataclass format for backward compatibility
self.obsidian = ObsidianConfig(
vault_path=self.system_config.obsidian.vault_path,
rest_api_url=self.system_config.obsidian.rest_api.url,
rest_api_key=self.system_config.obsidian.rest_api.api_key,
verify_ssl=self.system_config.obsidian.rest_api.verify_ssl
)
self.claude = ClaudeConfig(
api_key=self.system_config.claude.api_key,
model=self.system_config.claude.model,
api_url=self.system_config.claude.api_url,
max_tokens=self.system_config.claude.max_tokens,
temperature=self.system_config.claude.temperature
)
self.journal = JournalConfig(
daily_notes_folder=self.system_config.journal.daily_notes_folder,
date_format=self.system_config.journal.date_format,
file_extension=self.system_config.journal.file_extension
)
self.output = OutputConfig(
experiences_folder=self.system_config.output.experiences_folder,
lessons_folder=self.system_config.output.lessons_folder,
tasks_folder=self.system_config.output.tasks_folder,
problems_folder=self.system_config.output.problems_folder,
achievements_folder=self.system_config.output.achievements_folder,
improvements_folder=self.system_config.output.improvements_folder
)
self.analysis = AnalysisConfig(
categories=self.system_config.analysis.categories,
extraction_rules=self.system_config.analysis.extraction_rules
)
self.logging = LoggingConfig(
level=self.system_config.logging.level,
file=self.system_config.logging.file
)
def _perform_additional_validations(self) -> None:
"""Perform additional validations and provide helpful guidance"""
# Validate API keys and provide setup instructions
api_key_issues = self.validator.validate_api_keys(self.system_config)
if api_key_issues:
error_msg = "API Key Configuration Issues:\n" + "\n\n".join(api_key_issues)
self.logger.error(error_msg)
raise ValueError(error_msg)
# Validate file paths
path_issues = self.validator.validate_file_paths(self.system_config)
if path_issues:
error_msg = "File Path Issues:\n" + "\n".join(path_issues)
self.logger.warning(error_msg)
# Don't raise error for path issues, just warn
def get_environment_variable_help(self) -> str:
"""Get help for setting up environment variables"""
return self.validator.get_environment_variable_help()
def get_path_setup_help(self) -> str:
"""Get help for setting up file paths"""
return self.validator.get_path_setup_help()
def _get_default_config_path(self) -> str:
"""获取默认配置文件路径"""
# First check current directory
current_dir_config = Path("config.yaml")
if current_dir_config.exists():
return str(current_dir_config)
# Then check user home directory
config_dir = Path.home() / ".journal_organizer"
config_dir.mkdir(exist_ok=True)
return str(config_dir / "config.yaml")
def get_validation_errors(self) -> List[str]:
"""Get any validation errors or warnings"""
errors = []
try:
# Re-validate to get current status
api_key_issues = self.validator.validate_api_keys(self.system_config)
errors.extend(api_key_issues)
path_issues = self.validator.validate_file_paths(self.system_config)
errors.extend(path_issues)
except Exception as e:
errors.append(f"Validation error: {e}")
return errors
def reload_config(self) -> None:
"""Reload configuration from file"""
try:
self.system_config = self.validator.load_and_validate_config(self.config_file)
self._create_legacy_attributes()
self._perform_additional_validations()
self.logger.info("Configuration reloaded successfully")
except Exception as e:
self.logger.error(f"Failed to reload configuration: {e}")
raise
def get_daily_note_path(self, date_str: str) -> str:
"""
获取指定日期的日记文件路径
Args:
date_str: 日期字符串,格式应与配置中的 date_format 一致
Returns:
日记文件的相对路径
"""
daily_path = (
Path(self.journal.daily_notes_folder)
/ f"{date_str}{self.journal.file_extension}"
)
return str(daily_path)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典(不包含敏感信息)"""
return {
"obsidian": {
"vault_path": self.obsidian.vault_path,
"rest_api_url": self.obsidian.rest_api_url,
},
"claude": {
"model": self.claude.model,
"api_url": self.claude.api_url,
"max_tokens": self.claude.max_tokens,
"temperature": self.claude.temperature,
},
"journal": {
"daily_notes_folder": self.journal.daily_notes_folder,
"date_format": self.journal.date_format,
"file_extension": self.journal.file_extension,
},
"output": {
"experiences_folder": self.output.experiences_folder,
"lessons_folder": self.output.lessons_folder,
"tasks_folder": self.output.tasks_folder,
"problems_folder": self.output.problems_folder,
"achievements_folder": self.output.achievements_folder,
"improvements_folder": self.output.improvements_folder,
},
"analysis": {
"categories": self.analysis.categories,
},
"logging": {
"level": self.logging.level,
"file": self.logging.file,
},
}