Coverage for src/inheritance_calculator_core/utils/config.py: 0%

1"""

2アプリケーション設定管理モジュール

3

4環境変数から設定を読み込み、アプリケーション全体で使用する設定を提供します。

5"""

6

7 import os

8 import warnings

9 from pathlib import Path

10 from typing import Literal

11

12 from dotenv import load_dotenv

13 from pydantic import Field, field_validator, model_validator

14 from pydantic_settings import BaseSettings

15 from typing import Any

16

17 from .exceptions import ConfigurationError

18

19# プロジェクトルートディレクトリ

20 PROJECT_ROOT = Path(__file__).parent.parent.parent

21

22# .envファイルを読み込み

23 try:

24 load_dotenv(PROJECT_ROOT / ".env")

25 except 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 )

32

33

34 class Neo4jSettings(BaseSettings):

35 """Neo4jデータベース接続設定"""

36

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 )

47

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

58

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

66

67 class Config:

68 env_prefix = "NEO4J_"

69

70

71 class OllamaSettings(BaseSettings):

72 """Ollama設定"""

73

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="生成時の温度パラメータ")

78

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

86

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

94

95 class Config:

96 env_prefix = "OLLAMA_"

97

98

99 class LogSettings(BaseSettings):

100 """ログ設定"""

101

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="バックアップファイル数")

106

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

116

117 class Config:

118 env_prefix = "LOG_"

119

120

121 class AppSettings(BaseSettings):

122 """アプリケーション設定"""

123

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="アプリケーション名")

129

130 class Config:

131 env_prefix = "APP_"

132

133

134 class AgentSettings(BaseSettings):

135 """エージェント設定"""

136

137 max_retries: int = Field(default=3, description="最大リトライ回数")

138 timeout: int = Field(default=60, description="タイムアウト秒数")

139

140 class Config:

141 env_prefix = "AGENT_"

142

143

144 class OutputSettings(BaseSettings):

145 """出力設定"""

146

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出力を有効にするか")

152

153 class Config:

154 env_prefix = "OUTPUT_"

155

156

157 class Settings(BaseSettings):

158 """統合設定クラス"""

159

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

166

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

185

186 @property

187 def project_root(self) -> Path:

188 """プロジェクトルートディレクトリを取得"""

189 return PROJECT_ROOT

190

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

202

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

214

215

216# シングルトンインスタンス

217# 注: 実行時にはNEO4J_PASSWORD環境変数が必須

218 try:

219 settings = Settings()

220 except Exception:

221 # テスト環境やインポート時のエラーを回避

222 # 実際の使用時には環境変数が設定されている必要がある

223 settings = None # type: ignore