Coverage for src/finbot/config.py: 95%
31 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-04 12:05 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-04 12:05 +0000
1import os
2from dataclasses import dataclass
4from dotenv import load_dotenv
6load_dotenv()
9@dataclass(frozen=True)
10class Settings:
11 alpaca_api_key: str
12 alpaca_api_secret: str
13 alpaca_feed: str = "iex"
14 paper_trading_enabled: bool = False
17def _load_bool_env(name: str, *, default: bool = False) -> bool:
18 value = os.getenv(name)
20 if value is None:
21 return default
23 normalized = value.strip().lower()
25 if normalized in {"1", "true", "yes", "on"}:
26 return True
28 if normalized in {"0", "false", "no", "off"}: 28 ↛ 29line 28 didn't jump to line 29 because the condition on line 28 was never true
29 return False
31 raise RuntimeError(f"{name} must be a boolean value (true/false, 1/0, yes/no, on/off).")
34def load_settings(*, api_key: str | None = None, api_secret: str | None = None) -> Settings:
35 resolved_api_key = api_key or os.getenv("APCA_API_KEY_ID")
36 resolved_api_secret = api_secret or os.getenv("APCA_API_SECRET_KEY")
38 missing = []
39 if not resolved_api_key:
40 missing.append("APCA_API_KEY_ID")
41 if not resolved_api_secret:
42 missing.append("APCA_API_SECRET_KEY")
44 if missing:
45 raise RuntimeError(f"Missing required credentials: {', '.join(missing)}")
47 return Settings(
48 alpaca_api_key=resolved_api_key, # type: ignore[arg-type]
49 alpaca_api_secret=resolved_api_secret, # type: ignore[arg-type]
50 alpaca_feed=os.getenv("ALPACA_FEED", "iex"),
51 paper_trading_enabled=_load_bool_env("FINBOT_PAPER_TRADING_ENABLED"),
52 )