Coverage for railway / core / settings.py: 88%
96 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 00:06 +0900
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 00:06 +0900
1"""Settings management for Railway Framework."""
3from pathlib import Path
4from typing import Any
6import yaml
7from pydantic import BaseModel, Field
8from pydantic_settings import BaseSettings, SettingsConfigDict
11class APISettings(BaseModel):
12 """API configuration."""
14 base_url: str = ""
15 timeout: int = 30
16 max_retries: int = 3
19class DatabaseSettings(BaseModel):
20 """Database configuration."""
22 host: str = "localhost"
23 port: int = 5432
24 name: str = "db"
25 user: str | None = None
26 password: str | None = None
29class RetryNodeSettings(BaseModel):
30 """Per-node retry settings."""
32 max_attempts: int = 3
33 min_wait: int = 2
34 max_wait: int = 10
35 multiplier: int = 1
38class RetrySettings(BaseModel):
39 """Retry policy configuration."""
41 default: RetryNodeSettings = Field(default_factory=RetryNodeSettings)
42 nodes: dict[str, RetryNodeSettings] = Field(default_factory=dict)
45class LoggingHandlerSettings(BaseModel):
46 """Logging handler configuration."""
48 type: str # file, console
49 level: str = "INFO"
50 path: str | None = None
51 rotation: str | None = None
52 retention: str | None = None
55class LoggingSettings(BaseModel):
56 """Logging configuration."""
58 level: str = "INFO"
59 format: str = "{time:HH:mm:ss} | {level} | {message}"
60 handlers: list[LoggingHandlerSettings] = Field(default_factory=list)
63def _load_yaml_config(config_dir: Path, env: str) -> dict[str, Any]:
64 """
65 Load YAML configuration file.
67 Args:
68 config_dir: Directory containing config files
69 env: Environment name (development, production, etc.)
71 Returns:
72 Configuration dictionary
73 """
74 config_file = config_dir / f"{env}.yaml"
76 if not config_file.exists():
77 return {}
79 with open(config_file, encoding="utf-8") as f:
80 return yaml.safe_load(f) or {}
83def _parse_api_settings(config: dict[str, Any]) -> APISettings:
84 """Parse API settings from config dict."""
85 api_config = config.get("api", {})
86 return APISettings(**api_config) if api_config else APISettings()
89def _parse_database_settings(config: dict[str, Any]) -> DatabaseSettings:
90 """Parse database settings from config dict."""
91 db_config = config.get("database", {})
92 return DatabaseSettings(**db_config) if db_config else DatabaseSettings()
95def _parse_retry_settings(config: dict[str, Any]) -> RetrySettings:
96 """Parse retry settings from config dict."""
97 retry_config = config.get("retry", {})
98 if not retry_config:
99 return RetrySettings()
101 default_retry = RetryNodeSettings(**retry_config.get("default", {}))
102 nodes_retry = {
103 name: RetryNodeSettings(**settings)
104 for name, settings in retry_config.get("nodes", {}).items()
105 }
106 return RetrySettings(default=default_retry, nodes=nodes_retry)
109def _parse_logging_settings(config: dict[str, Any]) -> LoggingSettings:
110 """Parse logging settings from config dict."""
111 log_config = config.get("logging", {})
112 if not log_config:
113 return LoggingSettings()
115 handlers = [
116 LoggingHandlerSettings(**h) for h in log_config.get("handlers", [])
117 ]
118 return LoggingSettings(
119 level=log_config.get("level", "INFO"),
120 format=log_config.get("format", "{time:HH:mm:ss} | {level} | {message}"),
121 handlers=handlers,
122 )
125def _apply_env_overrides(
126 logging_settings: LoggingSettings,
127 log_level: str | None,
128) -> LoggingSettings:
129 """
130 Apply environment variable overrides to logging settings.
132 Returns new LoggingSettings with overrides applied.
133 """
134 if log_level:
135 return LoggingSettings(
136 level=log_level,
137 format=logging_settings.format,
138 handlers=logging_settings.handlers,
139 )
140 return logging_settings
143class Settings(BaseSettings):
144 """
145 Application settings.
147 Loads configuration from:
148 1. Environment variables (.env file)
149 2. YAML config file (config/{env}.yaml)
151 Environment variables override YAML config values.
152 """
154 model_config = SettingsConfigDict(
155 env_file=".env",
156 env_file_encoding="utf-8",
157 case_sensitive=False,
158 extra="allow",
159 )
161 # Environment variables
162 railway_env: str = "development"
163 app_name: str = "railway_app"
164 log_level: str | None = None # Override from .env
166 # Configuration from YAML (will be populated after init)
167 api: APISettings = Field(default_factory=APISettings)
168 database: DatabaseSettings = Field(default_factory=DatabaseSettings)
169 retry: RetrySettings = Field(default_factory=RetrySettings)
170 logging: LoggingSettings = Field(default_factory=LoggingSettings)
172 def __init__(self, _config_dir: str | None = None, **kwargs: Any) -> None:
173 """Initialize settings and load YAML config."""
174 super().__init__(**kwargs)
176 # Determine config directory
177 config_dir = self._resolve_config_dir(_config_dir)
179 # Load and parse YAML configuration
180 config = _load_yaml_config(config_dir, self.railway_env)
182 # Parse individual sections (functional approach)
183 self.api = _parse_api_settings(config)
184 self.database = _parse_database_settings(config)
185 self.retry = _parse_retry_settings(config)
186 self.logging = _parse_logging_settings(config)
188 # Apply environment variable overrides
189 self.logging = _apply_env_overrides(self.logging, self.log_level)
191 def _resolve_config_dir(self, config_dir: str | None) -> Path:
192 """Resolve the configuration directory path."""
193 if config_dir: 193 ↛ 197line 193 didn't jump to line 197 because the condition on line 193 was always true
194 return Path(config_dir)
196 # Default: look for config/ in current directory or parent
197 cwd_config = Path.cwd() / "config"
198 if cwd_config.exists():
199 return cwd_config
201 return Path(__file__).parent.parent.parent / "config"
203 def get_retry_settings(self, node_name: str) -> RetryNodeSettings:
204 """
205 Get retry settings for a specific node.
207 Args:
208 node_name: Name of the node
210 Returns:
211 RetryNodeSettings for the node, or default if not specified
212 """
213 return self.retry.nodes.get(node_name, self.retry.default)
216# Global settings instance (lazy initialization)
217_settings: Settings | None = None
220def get_settings(_config_dir: str | None = None) -> Settings:
221 """
222 Get the global settings instance.
224 Creates a new instance on first call, returns cached instance thereafter.
226 Args:
227 _config_dir: Optional config directory (only used on first call)
229 Returns:
230 Settings instance
231 """
232 global _settings
233 if _settings is None:
234 _settings = Settings(_config_dir=_config_dir)
235 return _settings
238def reset_settings() -> None:
239 """Reset the global settings instance (for testing)."""
240 global _settings
241 _settings = None