92 lines
2.9 KiB
Python
92 lines
2.9 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"
|
|
|
|
#: Файл-heartbeat для Docker healthcheck (обновляется ботом периодически).
|
|
heartbeat_path: str = "/tmp/dispelevilbot.heartbeat"
|
|
|
|
@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]
|