Implement configuration loader
This commit is contained in:
@@ -37,6 +37,7 @@ dev = [
|
|||||||
"pytest-asyncio>=0.21",
|
"pytest-asyncio>=0.21",
|
||||||
"ruff>=0.1",
|
"ruff>=0.1",
|
||||||
"mypy>=1.0",
|
"mypy>=1.0",
|
||||||
|
"types-PyYAML>=6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ This module defines Pydantic models for all configuration sections.
|
|||||||
Configuration can be loaded from YAML files and validated at runtime.
|
Configuration can be loaded from YAML files and validated at runtime.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Literal
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
import yaml
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
@@ -117,3 +120,81 @@ class AppConfig(BaseModel):
|
|||||||
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
||||||
dashboard: DashboardConfig = Field(default_factory=DashboardConfig)
|
dashboard: DashboardConfig = Field(default_factory=DashboardConfig)
|
||||||
api: APIConfig = Field(default_factory=APIConfig)
|
api: APIConfig = Field(default_factory=APIConfig)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_env_overrides(config_dict: dict[str, Any]) -> None:
|
||||||
|
"""Apply environment variable overrides to config dictionary.
|
||||||
|
|
||||||
|
Environment variables follow the pattern: PYDVTATE__{SECTION}__{KEY}
|
||||||
|
For nested keys, use double underscores: PYDVTATE__{SECTION}__{SUBSECTION}__{KEY}
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
PYDVTATE__INSTRUMENTS__BACKEND=pyvisa
|
||||||
|
PYDVTATE__PHYSICS__UPDATE_RATE_HZ=50.0
|
||||||
|
PYDVTATE__SIMULATOR__HOST=192.168.1.100
|
||||||
|
"""
|
||||||
|
prefix = "PYDVTATE__"
|
||||||
|
|
||||||
|
for env_key, env_value in os.environ.items():
|
||||||
|
if not env_key.startswith(prefix):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Remove prefix and split into parts
|
||||||
|
key_parts = env_key[len(prefix) :].lower().split("__")
|
||||||
|
|
||||||
|
# Navigate/create nested structure
|
||||||
|
current = config_dict
|
||||||
|
for part in key_parts[:-1]:
|
||||||
|
if part not in current:
|
||||||
|
current[part] = {}
|
||||||
|
current = current[part]
|
||||||
|
|
||||||
|
# Set the final value
|
||||||
|
final_key = key_parts[-1]
|
||||||
|
# Try to parse as YAML to handle types (int, float, bool, etc.)
|
||||||
|
try:
|
||||||
|
current[final_key] = yaml.safe_load(env_value)
|
||||||
|
except yaml.YAMLError:
|
||||||
|
# If parsing fails, use as string
|
||||||
|
current[final_key] = env_value
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(config_path: str | Path | None = None) -> AppConfig:
|
||||||
|
"""Load configuration from YAML file with environment variable overrides.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_path: Path to YAML configuration file. If None, uses defaults only.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Validated AppConfig instance.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If config_path is provided but does not exist.
|
||||||
|
yaml.YAMLError: If YAML file is malformed.
|
||||||
|
pydantic.ValidationError: If configuration is invalid.
|
||||||
|
|
||||||
|
Environment Variables:
|
||||||
|
Configuration can be overridden using environment variables with the
|
||||||
|
pattern PYDVTATE__{SECTION}__{KEY}. For example:
|
||||||
|
PYDVTATE__INSTRUMENTS__BACKEND=pyvisa
|
||||||
|
PYDVTATE__PHYSICS__UPDATE_RATE_HZ=50.0
|
||||||
|
"""
|
||||||
|
# Start with empty dict (will use Pydantic defaults)
|
||||||
|
config_dict: dict[str, Any] = {}
|
||||||
|
|
||||||
|
# Load from YAML file if provided
|
||||||
|
if config_path is not None:
|
||||||
|
path = Path(config_path)
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(f"Configuration file not found: {config_path}")
|
||||||
|
|
||||||
|
with path.open("r") as f:
|
||||||
|
loaded = yaml.safe_load(f)
|
||||||
|
if loaded is not None:
|
||||||
|
config_dict = loaded
|
||||||
|
|
||||||
|
# Apply environment variable overrides
|
||||||
|
_apply_env_overrides(config_dict)
|
||||||
|
|
||||||
|
# Validate and return
|
||||||
|
return AppConfig(**config_dict)
|
||||||
|
|||||||
Reference in New Issue
Block a user