"""Runtime configuration loader — reads secrets from a gitignored ``.env``. Broker login lives in ``.env`` (gitignored). The bridge reads these at runtime and injects them into ``tester.ini``'s ``[Common]`` section **without printing them** (doc 07 §5). Never hard-code credentials and never print them. """ from __future__ import annotations import os from pathlib import Path from typing import Optional try: from dotenv import load_dotenv # type: ignore _HAS_DOTENV = True except ImportError: _HAS_DOTENV = False def load_env(project_root: Optional[str | Path] = None) -> dict[str, str]: """Load ``.env`` from ``project_root`` (defaults to this file's parent). Returns the MT5 credential keys without their values exposed in logs. Falls back to plain line parsing if ``python-dotenv`` isn't installed. """ root = Path(project_root) if project_root else Path(__file__).resolve().parent.parent env_path = root / ".env" if _HAS_DOTENV and env_path.exists(): load_dotenv(env_path) elif env_path.exists(): _parse_plain(env_path) return { "MT5_DEMO_LOGIN": os.environ.get("MT5_DEMO_LOGIN", ""), "MT5_DEMO_SERVER": os.environ.get("MT5_DEMO_SERVER", ""), # Password is intentionally not returned here; read via get_secret(). "MT5_TERMINAL_PATH": os.environ.get( "MT5_TERMINAL_PATH", r"C:\Program Files\MetaTrader 5 IC Markets Global\terminal64.exe", ), } def get_secret(name: str) -> str: """Read a secret from the environment (load_env must be called first).""" return os.environ.get(name, "") def _parse_plain(env_path: Path) -> None: """Minimal ``.env`` parser (KEY=VALUE) without python-dotenv.""" for line in env_path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: continue k, _, v = line.partition("=") k, v = k.strip(), v.strip() # Strip optional surrounding quotes. if len(v) >= 2 and v[0] == v[-1] and v[0] in ("'", '"'): v = v[1:-1] os.environ.setdefault(k, v)