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

1import os 

2from dataclasses import dataclass 

3 

4from dotenv import load_dotenv 

5 

6load_dotenv() 

7 

8 

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 

15 

16 

17def _load_bool_env(name: str, *, default: bool = False) -> bool: 

18 value = os.getenv(name) 

19 

20 if value is None: 

21 return default 

22 

23 normalized = value.strip().lower() 

24 

25 if normalized in {"1", "true", "yes", "on"}: 

26 return True 

27 

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 

30 

31 raise RuntimeError(f"{name} must be a boolean value (true/false, 1/0, yes/no, on/off).") 

32 

33 

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") 

37 

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") 

43 

44 if missing: 

45 raise RuntimeError(f"Missing required credentials: {', '.join(missing)}") 

46 

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 )