-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathconfig_reader.py
More file actions
132 lines (98 loc) · 3.27 KB
/
config_reader.py
File metadata and controls
132 lines (98 loc) · 3.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
from enum import StrEnum, auto
from functools import lru_cache
from os import environ
from pathlib import Path
from tomllib import load
from typing import Optional, Type, TypeVar
from pydantic import BaseModel, SecretStr, field_validator
ConfigType = TypeVar("ConfigType", bound=BaseModel)
class LogRenderer(StrEnum):
JSON = auto()
CONSOLE = auto()
class BotConfig(BaseModel):
"""Bot configuration."""
token: SecretStr
owners: list[int] = []
@field_validator("owners", mode="before")
@classmethod
def parse_owners(cls, v):
if isinstance(v, str):
return [int(x.strip()) for x in v.split(",") if x.strip()]
return v
class LogConfig(BaseModel):
"""Logging configuration."""
show_datetime: bool = True
datetime_format: str = "%Y-%m-%d %H:%M:%S"
show_debug_logs: bool = False
time_in_utc: bool = False
use_colors_in_console: bool = True
renderer: LogRenderer = LogRenderer.CONSOLE
@field_validator("renderer", mode="before")
@classmethod
def log_renderer_to_lower(cls, v: str) -> str:
if isinstance(v, str):
return v.lower()
return v
class L10nConfig(BaseModel):
"""Localization configuration."""
default_locale: str = "en"
fallback_locale: str = "en"
locales_path: str = "l10n"
class ThrottlingConfig(BaseModel):
"""Rate limiting configuration."""
enabled: bool = True
rate_limit: float = 0.5 # seconds between messages
max_users: int = 10000 # max users to track
class Config(BaseModel):
"""Root configuration model."""
bot: BotConfig
logs: LogConfig = LogConfig()
localization: L10nConfig = L10nConfig()
def get_config_path() -> Path:
"""Get configuration file path from environment or default."""
env_path = environ.get("CONFIG_FILE_PATH")
if env_path:
return Path(env_path)
return Path("config.toml")
@lru_cache
def parse_config_file() -> dict:
"""Parse TOML configuration file."""
file_path = get_config_path()
if not file_path.exists():
raise FileNotFoundError(f"Config file not found: {file_path}")
with open(file_path, "rb") as file:
return load(file)
@lru_cache
def get_config(model: Type[ConfigType], root_key: str) -> ConfigType:
"""
Get typed configuration section.
Args:
model: Pydantic model class for validation
root_key: Top-level key in config file
Returns:
Validated configuration object
Raises:
KeyError: If root_key not found in config
"""
config_dict = parse_config_file()
if root_key not in config_dict:
raise KeyError(f"Configuration key '{root_key}' not found in config file")
return model.model_validate(config_dict[root_key])
def get_env_or_config(
env_var: str,
config_model: Type[ConfigType],
config_key: str,
config_attr: str,
) -> Optional[str]:
"""
Get value from environment variable or config file.
Environment variables take precedence over config file values.
"""
env_value = environ.get(env_var)
if env_value is not None:
return env_value
try:
config = get_config(model=config_model, root_key=config_key)
return getattr(config, config_attr, None)
except (KeyError, FileNotFoundError):
return None