Coverage for src/inheritance_calculator_core/utils/config.py: 0%
133 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-17 05:31 +0900
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-17 05:31 +0900
1"""
2アプリケーション設定管理モジュール
4環境変数から設定を読み込み、アプリケーション全体で使用する設定を提供します。
5"""
7import os
8import warnings
9from pathlib import Path
10from typing import Literal
12from dotenv import load_dotenv
13from pydantic import Field, field_validator, model_validator
14from pydantic_settings import BaseSettings
15from typing import Any
17from .exceptions import ConfigurationError
19# プロジェクトルートディレクトリ
20PROJECT_ROOT = Path(__file__).parent.parent.parent
22# .envファイルを読み込み
23try:
24 load_dotenv(PROJECT_ROOT / ".env")
25except Exception as e:
26 # .envファイルが無くても動作するが警告を出す
27 warnings.warn(
28 f".env file could not be loaded: {e}. "
29 "Using environment variables or defaults.",
30 UserWarning
31 )
34class Neo4jSettings(BaseSettings):
35 """Neo4jデータベース接続設定"""
37 uri: str = Field(default="bolt://localhost:7687", description="Neo4j接続URI")
38 user: str = Field(default="neo4j", description="Neo4jユーザー名")
39 password: str = Field(..., description="Neo4jパスワード(必須)")
40 database: str = Field(default="neo4j", description="Neo4jデータベース名")
41 auto_create_constraints: bool = Field(
42 default=True, description="制約を自動作成するか"
43 )
44 auto_create_indexes: bool = Field(
45 default=True, description="インデックスを自動作成するか"
46 )
48 @field_validator('uri')
49 @classmethod
50 def validate_uri(cls, v: str) -> str:
51 """URIスキームの検証"""
52 if not v.startswith(('bolt://', 'neo4j://', 'neo4j+s://', 'neo4j+ssc://')):
53 raise ValueError(
54 f"Invalid Neo4j URI scheme: {v}. "
55 "Must start with bolt://, neo4j://, neo4j+s://, or neo4j+ssc://"
56 )
57 return v
59 @field_validator('password')
60 @classmethod
61 def validate_password(cls, v: str) -> str:
62 """パスワード強度の検証"""
63 if len(v) < 8:
64 raise ValueError("Password must be at least 8 characters long")
65 return v
67 class Config:
68 env_prefix = "NEO4J_"
71class OllamaSettings(BaseSettings):
72 """Ollama設定"""
74 host: str = Field(default="http://localhost:11434", description="OllamaホストURL")
75 model: str = Field(default="gpt-oss:20b", description="使用するモデル名")
76 timeout: int = Field(default=120, description="タイムアウト秒数")
77 temperature: float = Field(default=0.7, description="生成時の温度パラメータ")
79 @field_validator('host')
80 @classmethod
81 def validate_host(cls, v: str) -> str:
82 """ホストURLの検証"""
83 if not v.startswith(('http://', 'https://')):
84 raise ValueError(f"Invalid Ollama host URL: {v}. Must start with http:// or https://")
85 return v
87 @field_validator('timeout')
88 @classmethod
89 def validate_timeout(cls, v: int) -> int:
90 """タイムアウト値の検証"""
91 if v < 10 or v > 600:
92 raise ValueError("Timeout must be between 10 and 600 seconds")
93 return v
95 class Config:
96 env_prefix = "OLLAMA_"
99class LogSettings(BaseSettings):
100 """ログ設定"""
102 level: str = Field(default="INFO", description="ログレベル")
103 file: str = Field(default="logs/inheritance.log", description="ログファイルパス")
104 max_bytes: int = Field(default=10485760, description="ログファイル最大サイズ")
105 backup_count: int = Field(default=5, description="バックアップファイル数")
107 @field_validator('level')
108 @classmethod
109 def validate_level(cls, v: str) -> str:
110 """ログレベルの検証"""
111 valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
112 v_upper = v.upper()
113 if v_upper not in valid_levels:
114 raise ValueError(f"Invalid log level: {v}. Must be one of {valid_levels}")
115 return v_upper
117 class Config:
118 env_prefix = "LOG_"
121class AppSettings(BaseSettings):
122 """アプリケーション設定"""
124 env: Literal["development", "production", "test"] = Field(
125 default="development", description="実行環境"
126 )
127 debug: bool = Field(default=False, description="デバッグモード")
128 name: str = Field(default="inheritance-calculator", description="アプリケーション名")
130 class Config:
131 env_prefix = "APP_"
134class AgentSettings(BaseSettings):
135 """エージェント設定"""
137 max_retries: int = Field(default=3, description="最大リトライ回数")
138 timeout: int = Field(default=60, description="タイムアウト秒数")
140 class Config:
141 env_prefix = "AGENT_"
144class OutputSettings(BaseSettings):
145 """出力設定"""
147 dir: str = Field(default="output", description="出力ディレクトリ")
148 format: Literal["json", "yaml", "csv"] = Field(
149 default="json", description="デフォルト出力形式"
150 )
151 enable_rich: bool = Field(default=True, description="Rich出力を有効にするか")
153 class Config:
154 env_prefix = "OUTPUT_"
157class Settings(BaseSettings):
158 """統合設定クラス"""
160 neo4j: Neo4jSettings = Field(default=None) # type: ignore
161 ollama: OllamaSettings = Field(default=None) # type: ignore
162 log: LogSettings = Field(default=None) # type: ignore
163 app: AppSettings = Field(default=None) # type: ignore
164 agent: AgentSettings = Field(default=None) # type: ignore
165 output: OutputSettings = Field(default=None) # type: ignore
167 @model_validator(mode='before')
168 @classmethod
169 def create_nested_settings(cls, data: Any) -> Any:
170 """ネストされた設定を自動生成"""
171 if isinstance(data, dict):
172 if 'neo4j' not in data or data['neo4j'] is None:
173 data['neo4j'] = Neo4jSettings() # type: ignore
174 if 'ollama' not in data or data['ollama'] is None:
175 data['ollama'] = OllamaSettings() # type: ignore
176 if 'log' not in data or data['log'] is None:
177 data['log'] = LogSettings() # type: ignore
178 if 'app' not in data or data['app'] is None:
179 data['app'] = AppSettings() # type: ignore
180 if 'agent' not in data or data['agent'] is None:
181 data['agent'] = AgentSettings() # type: ignore
182 if 'output' not in data or data['output'] is None:
183 data['output'] = OutputSettings() # type: ignore
184 return data
186 @property
187 def project_root(self) -> Path:
188 """プロジェクトルートディレクトリを取得"""
189 return PROJECT_ROOT
191 @property
192 def logs_dir(self) -> Path:
193 """ログディレクトリを取得"""
194 logs_dir = PROJECT_ROOT / "logs"
195 try:
196 logs_dir.mkdir(exist_ok=True)
197 except OSError as e:
198 raise ConfigurationError(
199 f"Failed to create logs directory: {logs_dir}"
200 ) from e
201 return logs_dir
203 @property
204 def output_dir(self) -> Path:
205 """出力ディレクトリを取得"""
206 output_dir = PROJECT_ROOT / self.output.dir
207 try:
208 output_dir.mkdir(exist_ok=True)
209 except OSError as e:
210 raise ConfigurationError(
211 f"Failed to create output directory: {output_dir}"
212 ) from e
213 return output_dir
216# シングルトンインスタンス
217# 注: 実行時にはNEO4J_PASSWORD環境変数が必須
218try:
219 settings = Settings()
220except Exception:
221 # テスト環境やインポート時のエラーを回避
222 # 実際の使用時には環境変数が設定されている必要がある
223 settings = None # type: ignore