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

1""" 

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

3 

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

5""" 

6 

7import os 

8import warnings 

9from pathlib import Path 

10from typing import Literal 

11 

12from dotenv import load_dotenv 

13from pydantic import Field, field_validator, model_validator 

14from pydantic_settings import BaseSettings 

15from typing import Any 

16 

17from .exceptions import ConfigurationError 

18 

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

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

21 

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 ) 

32 

33 

34class 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 

71class 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 

99class 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 

121class 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 

134class 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 

144class 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 

157class 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環境変数が必須 

218try: 

219 settings = Settings() 

220except Exception: 

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

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

223 settings = None # type: ignore