Coverage for src / sentry_tool / config.py: 100.00%

47 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-15 10:53 -0500

1"""Configuration management with profile support. 

2 

3Loads configuration from multiple sources with layered precedence: 

41. CLI flags (--profile) 

52. Environment variables (SENTRY_PROFILE, SENTRY_URL, etc.) 

63. Config file (~/.config/sentry-tool/config.toml) 

74. Default values (lowest priority) 

8""" 

9 

10import os 

11import tomllib 

12from dataclasses import dataclass, field 

13from pathlib import Path 

14from typing import Any 

15 

16from pydantic import BaseModel, Field 

17 

18from sentry_tool.exceptions import ConfigurationError 

19 

20 

21class SentryProfile(BaseModel): 

22 """All fields optional in profile, required after resolution with env vars.""" 

23 

24 url: str = Field( 

25 default="https://sentry.io", 

26 description="Base URL for Sentry instance", 

27 ) 

28 org: str = Field( 

29 default="sentry", 

30 description="Sentry organization slug", 

31 ) 

32 project: str = Field( 

33 default="otel-collector", 

34 description="Sentry project slug", 

35 ) 

36 auth_token: str | None = Field( 

37 default=None, 

38 description="Sentry API authentication token", 

39 ) 

40 

41 

42class AppConfig(BaseModel): 

43 """Application-wide configuration with profile management.""" 

44 

45 default_profile: str = Field(default="default") 

46 profiles: dict[str, SentryProfile] = Field(default_factory=lambda: {"default": SentryProfile()}) 

47 

48 

49def load_config(config_path: Path | None = None) -> AppConfig: 

50 """Search order: explicit path > ~/.config/sentry-tool/config.toml > defaults. 

51 

52 Returns default AppConfig if no file found. 

53 """ 

54 search_paths = [ 

55 Path.home() / ".config" / "sentry-tool" / "config.toml", 

56 ] 

57 

58 if config_path: 

59 search_paths.insert(0, config_path) 

60 

61 for path in search_paths: 

62 if path.exists(): 

63 with path.open("rb") as f: 

64 config_data = tomllib.load(f) 

65 return AppConfig(**config_data) 

66 

67 # No config file found, return defaults 

68 return AppConfig() 

69 

70 

71def get_profile(config: AppConfig, profile: str | None = None) -> SentryProfile: 

72 """Resolution order: explicit profile > SENTRY_PROFILE env > config default. 

73 

74 Raises ConfigurationError if profile name not found in config. 

75 """ 

76 profile_name = profile or os.environ.get("SENTRY_PROFILE") or config.default_profile 

77 

78 if profile_name not in config.profiles: 

79 available = ", ".join(sorted(config.profiles.keys())) 

80 raise ConfigurationError( 

81 f"Profile '{profile_name}' not found. Available profiles: {available}" 

82 ) 

83 

84 return config.profiles[profile_name] 

85 

86 

87@dataclass 

88class EnvOverrides: 

89 """Environment variable and CLI flag overrides for config resolution.""" 

90 

91 cli_project: str | None = field(default=None) 

92 url: str | None = field(default=None) 

93 org: str | None = field(default=None) 

94 project: str | None = field(default=None) 

95 auth_token: str | None = field(default=None) 

96 

97 

98def resolve_sentry_config( 

99 config: SentryProfile, 

100 overrides: EnvOverrides | None = None, 

101 **kwargs: str | None, 

102) -> dict[str, Any]: 

103 """Precedence: CLI flags > environment variables > profile > defaults. 

104 

105 Raises ConfigurationError if final auth_token is empty or missing. 

106 

107 Accepts either an EnvOverrides object or keyword arguments for backwards compatibility: 

108 cli_project, env_url, env_org, env_project, env_auth_token 

109 """ 

110 if overrides is None: 

111 overrides = EnvOverrides( 

112 cli_project=kwargs.get("cli_project"), 

113 url=kwargs.get("env_url"), 

114 org=kwargs.get("env_org"), 

115 project=kwargs.get("env_project"), 

116 auth_token=kwargs.get("env_auth_token"), 

117 ) 

118 

119 auth_token = overrides.auth_token if overrides.auth_token is not None else config.auth_token 

120 

121 if not auth_token or not auth_token.strip(): 

122 msg = "SENTRY_AUTH_TOKEN not set in profile or environment" 

123 raise ConfigurationError(msg) 

124 

125 # Project precedence: CLI flag > env var > profile 

126 project = ( 

127 overrides.cli_project 

128 if overrides.cli_project is not None 

129 else (overrides.project if overrides.project is not None else config.project) 

130 ) 

131 

132 return { 

133 "url": overrides.url if overrides.url is not None else config.url, 

134 "org": overrides.org if overrides.org is not None else config.org, 

135 "project": project, 

136 "auth_token": auth_token.strip(), 

137 }