DispelEvilBot/common/config.py

89 lines
2.7 KiB
Python

"""Конфигурация приложения.
Значения читаются из переменных окружения / файла .env (см. .env.example).
Реальных секретов в коде быть не должно.
"""
from __future__ import annotations
from functools import lru_cache
from urllib.parse import quote_plus
from zoneinfo import ZoneInfo
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Настройки, общие для бота и админки."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
case_sensitive=False,
)
app_env: str = "dev"
bot_token: str = Field(..., description="Токен бота от @BotFather")
db_host: str = "localhost"
db_port: int = 5432
db_name: str = "dispel_evil"
db_user: str = "de_admin"
db_password: str = ""
db_pool_min: int = 1
db_pool_max: int = 10
db_statement_timeout_ms: int = 15000
prod_chat_id: int = 0
test_chat_id: int = 0
alert_chat_id: int = 0
admin_ids: list[int] = Field(default_factory=list)
timezone: str = "Europe/Moscow"
@field_validator("admin_ids", mode="before")
@classmethod
def _parse_admin_ids(cls, value: object) -> object:
"""Поддержать формат 'ADMIN_IDS=1,2,3' в дополнение к JSON-списку."""
if value is None or value == "":
return []
if isinstance(value, str):
cleaned = value.strip().strip("[]")
if not cleaned:
return []
return [int(item.strip()) for item in cleaned.split(",") if item.strip()]
return value
@property
def is_prod(self) -> bool:
return self.app_env.strip().lower() in {"prod", "production"}
@property
def chat_id(self) -> int:
"""Основной чат клана для текущего окружения."""
return self.prod_chat_id if self.is_prod else self.test_chat_id
@property
def tzinfo(self) -> ZoneInfo:
return ZoneInfo(self.timezone)
@property
def dsn(self) -> str:
"""Строка подключения psycopg для async-пула (креды URL-экранируются)."""
user = quote_plus(self.db_user)
password = quote_plus(self.db_password)
return (
f"postgresql://{user}:{password}"
f"@{self.db_host}:{self.db_port}/{self.db_name}"
)
@lru_cache
def get_settings() -> Settings:
"""Вернуть singleton настроек."""
return Settings() # type: ignore[call-arg]