- Add AppConfig/SmtpConfig/EmailConfig dataclasses with from_yaml()/from_env()
- Add yaml_config.py: ${VAR} resolver, 0700/0600 permission helpers
- Config.get_*() methods gain YAML fallback in priority chain
- Priority: CLI > env > .env > YAML > code defaults
- CLI --init-config generates YAML from current environment
- --force flag allows overwriting existing config
- Secrets default to ${VAR} references, plaintext allowed
- Full backward compatibility with existing .env setups
Closes #46
59 lines
1.7 KiB
Python
59 lines
1.7 KiB
Python
import logging
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_ENV_VAR_RE = re.compile(r"\$\{([^}]+)\}")
|
|
|
|
|
|
def resolve_env_vars(value: str) -> str:
|
|
"""Replace ${VAR} patterns with environment variable values.
|
|
|
|
Non-recursive: if ${A} expands to a literal ${B}, ${B} is NOT resolved.
|
|
Unknown variables are replaced with empty string and a warning is logged.
|
|
"""
|
|
if not isinstance(value, str):
|
|
return value
|
|
|
|
def _replacer(match: re.Match) -> str:
|
|
var_name = match.group(1)
|
|
env_value = os.environ.get(var_name)
|
|
if env_value is None:
|
|
logger.warning("Environment variable %s is not set, using empty string", var_name)
|
|
return ""
|
|
return env_value
|
|
|
|
return _ENV_VAR_RE.sub(_replacer, value)
|
|
|
|
|
|
def ensure_config_dir(path: Path | None = None) -> Path:
|
|
"""Create ~/.config/redmine-reporter/ with 0o700 permissions.
|
|
|
|
Returns the path to the config directory.
|
|
No-op if the directory already exists.
|
|
"""
|
|
if path is None:
|
|
path = Path.home() / ".config" / "redmine-reporter"
|
|
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
return path
|
|
|
|
|
|
def check_file_permissions(path: Path) -> list[str]:
|
|
"""Check that a config file has safe permissions (0o600 or stricter).
|
|
|
|
Returns a list of warning messages. Empty list means no issues.
|
|
"""
|
|
warnings: list[str] = []
|
|
if not path.exists():
|
|
return warnings
|
|
|
|
mode = path.stat().st_mode & 0o777
|
|
if mode > 0o600:
|
|
warnings.append(
|
|
f"Config file permissions are too open ({mode:04o}). "
|
|
f"Expected 0600. Fix with: chmod 600 {path}"
|
|
)
|
|
return warnings
|