Coverage for src / sentry_tool / config.py: 100.00%
48 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-28 19:20 -0500
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-28 19:20 -0500
1"""Configuration management with profile support.
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"""
10import os
11import tomllib
12from dataclasses import dataclass, field
13from pathlib import Path
14from typing import Any
16from pydantic import BaseModel, Field
18from sentry_tool.exceptions import ConfigurationError
21class SentryProfile(BaseModel):
22 """All fields optional in profile, required after resolution with env vars."""
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 )
42class AppConfig(BaseModel):
43 """Application-wide configuration with profile management."""
45 default_profile: str = Field(default="default")
46 profiles: dict[str, SentryProfile] = Field(default_factory=lambda: {"default": SentryProfile()})
47 sentry_dsn: str | None = Field(
48 default=None,
49 description="Sentry DSN override (env var > config > hardcoded default)",
50 )
53def load_config(config_path: Path | None = None) -> AppConfig:
54 """Search order: explicit path > ~/.config/sentry-tool/config.toml > defaults.
56 Returns default AppConfig if no file found.
57 """
58 search_paths = [
59 Path.home() / ".config" / "sentry-tool" / "config.toml",
60 ]
62 if config_path:
63 search_paths.insert(0, config_path)
65 for path in search_paths:
66 if path.exists():
67 with path.open("rb") as f:
68 config_data = tomllib.load(f)
69 return AppConfig(**config_data)
71 return AppConfig()
74def get_profile(config: AppConfig, profile: str | None = None) -> SentryProfile:
75 """Resolution order: explicit profile > SENTRY_PROFILE env > config default.
77 Raises ConfigurationError if profile name not found in config.
78 """
79 profile_name = profile or os.environ.get("SENTRY_PROFILE") or config.default_profile
81 if profile_name not in config.profiles:
82 available = ", ".join(sorted(config.profiles.keys()))
83 raise ConfigurationError(
84 f"Profile '{profile_name}' not found. Available profiles: {available}"
85 )
87 return config.profiles[profile_name]
90@dataclass
91class EnvOverrides:
92 """Environment variable and CLI flag overrides for config resolution."""
94 cli_project: str | None = field(default=None)
95 url: str | None = field(default=None)
96 org: str | None = field(default=None)
97 project: str | None = field(default=None)
98 auth_token: str | None = field(default=None)
101def resolve_sentry_config(
102 config: SentryProfile,
103 overrides: EnvOverrides | None = None,
104 **kwargs: str | None,
105) -> dict[str, Any]:
106 """Precedence: CLI flags > environment variables > profile > defaults.
108 Raises ConfigurationError if final auth_token is empty or missing.
110 Accepts either an EnvOverrides object or keyword arguments for backwards compatibility:
111 cli_project, env_url, env_org, env_project, env_auth_token
112 """
113 if overrides is None:
114 overrides = EnvOverrides(
115 cli_project=kwargs.get("cli_project"),
116 url=kwargs.get("env_url"),
117 org=kwargs.get("env_org"),
118 project=kwargs.get("env_project"),
119 auth_token=kwargs.get("env_auth_token"),
120 )
122 auth_token = overrides.auth_token if overrides.auth_token is not None else config.auth_token
124 if not auth_token or not auth_token.strip():
125 msg = "SENTRY_AUTH_TOKEN not set in profile or environment"
126 raise ConfigurationError(msg)
128 # Project precedence: CLI flag > env var > profile
129 project = (
130 overrides.cli_project
131 if overrides.cli_project is not None
132 else (overrides.project if overrides.project is not None else config.project)
133 )
135 return {
136 "url": overrides.url if overrides.url is not None else config.url,
137 "org": overrides.org if overrides.org is not None else config.org,
138 "project": project,
139 "auth_token": auth_token.strip(),
140 }