Source code for varlord.sources.env
"""
Environment variable source.
Loads configuration from environment variables, filtered by model fields.
"""
from __future__ import annotations
import os
from typing import Any, Mapping, Optional, Type
from varlord.metadata import get_all_field_keys
from varlord.sources.base import Source, normalize_key
[docs]
class Env(Source):
"""Source that loads configuration from environment variables.
Only loads environment variables that map to fields defined in the model.
Model is required and will be auto-injected by Config if not provided.
Example:
>>> @dataclass
... class Config:
... api_key: str = field() # Required by default
>>> # Environment: API_KEY=value1 OTHER_VAR=ignored
>>> source = Env(model=Config)
>>> source.load()
{'api_key': 'value1'} # OTHER_VAR is ignored
"""
[docs]
def __init__(
self,
model: Optional[Type[Any]] = None,
prefix: Optional[str] = None,
source_id: Optional[str] = None,
):
"""Initialize Env source.
Args:
model: Optional model to filter environment variables.
Only variables that map to model fields will be loaded.
If None, model will be auto-injected by Config when used in Config.
If provided, this model will be used (allows override).
prefix: Optional prefix for environment variables (e.g., ``TITAN__``).
If None, matches all environment variables that map to model fields.
If provided, only variables starting with this prefix (uppercase) are considered.
source_id: Optional unique identifier (default: "env" or "env:{prefix}")
Note:
- Prefix filtering is optional. If prefix is None, all env vars are checked against model fields.
- Recommended: Omit model parameter when used in Config (auto-injected).
- Advanced: Provide model explicitly if using source independently.
"""
super().__init__(model=model, source_id=source_id or (f"env:{prefix}" if prefix else "env"))
self._prefix = prefix.upper() if prefix else None
@property
def name(self) -> str:
"""Return source name."""
return "env"
def _generate_id(self) -> str:
"""Generate unique ID for Env source."""
if self._prefix:
return f"env:{self._prefix}"
return "env"
[docs]
def load(self) -> Mapping[str, Any]:
"""Load configuration from environment variables, filtered by model fields.
Returns:
A mapping of normalized keys to environment variable values.
Only includes variables that map to model fields.
Raises:
ValueError: If model is not provided
"""
# Reset status
self._load_status = "unknown"
self._load_error = None
try:
if not self._model:
raise ValueError(
"Env source requires model. "
"When used in Config, model is auto-injected. "
"When used independently, provide model explicitly: Env(model=AppConfig)"
)
# Get all valid field keys from model
valid_keys = get_all_field_keys(self._model)
result: dict[str, Any] = {}
for env_key, env_value in os.environ.items():
# Check prefix if specified (case-insensitive)
if self._prefix:
# Compare in uppercase for case-insensitive matching
if not env_key.upper().startswith(self._prefix):
continue
# Remove prefix (preserve original case for normalization)
key_without_prefix = env_key[len(self._prefix) :]
normalized_key = normalize_key(key_without_prefix)
else:
# Normalize env var name (no prefix filtering)
normalized_key = normalize_key(env_key)
# Only load if it matches a model field
if normalized_key in valid_keys:
result[normalized_key] = env_value
self._load_status = "success"
return result
except Exception as e:
self._load_status = "failed"
self._load_error = str(e)
raise
[docs]
def __repr__(self) -> str:
"""Return string representation."""
if self._prefix:
return f"<Env(prefix={self._prefix!r})>"
return "<Env(model-based)>"