From 4f6897855a42b1a266e5f1f8a58c9d0a5a218452 Mon Sep 17 00:00:00 2001 From: Vladimir Bryzgalov Date: Sun, 16 Aug 2026 01:53:56 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=BD=D1=8F=D1=82=D0=BD=D0=BE?= =?UTF-8?q?=D0=B5=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=BB=D0=BE=D0=BC=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=20config.toml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Дублированный раздел ронял обновление трассировкой tomllib. Теперь ConfigError объясняет по-русски, что раздел объявлен дважды и какие разделы должны встречаться ровно один раз. Co-Authored-By: Claude Opus 5 (1M context) --- app/config.py | 20 ++++++++++++++++++-- app/main.py | 10 ++++++++-- tests/test_api.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/app/config.py b/app/config.py index eb9c41f..e5c1f7d 100644 --- a/app/config.py +++ b/app/config.py @@ -10,7 +10,11 @@ import tomllib from dataclasses import dataclass, field from pathlib import Path -__all__ = ["Settings", "load_settings", "BASE_DIR"] +__all__ = ["Settings", "load_settings", "BASE_DIR", "ConfigError"] + + +class ConfigError(RuntimeError): + """Настройки прочитать не удалось. Текст рассчитан на чтение человеком.""" # Корень пакета: при запуске из ZIP это папка, куда его распаковали. BASE_DIR = Path(os.environ.get("TALKSCORE_ASR_HOME") or Path(__file__).resolve().parent.parent) @@ -144,7 +148,19 @@ def load_settings(config_path: Path | None = None) -> Settings: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(_DEFAULT_CONFIG.format(token=secrets.token_urlsafe(32)), encoding="utf-8") - data = tomllib.loads(path.read_text(encoding="utf-8")) + try: + data = tomllib.loads(path.read_text(encoding="utf-8")) + except tomllib.TOMLDecodeError as exc: + # Самая частая причина - раздел объявлен дважды: настройки дописывали + # блоками, и рядом со старым [security] появился второй. + raise ConfigError( + f"не удалось прочитать {path}\n" + f" {exc}\n\n" + " Скорее всего, один и тот же раздел объявлен дважды.\n" + " Каждый раздел ([server], [security], [processing], [webhook], [update])\n" + " должен встречаться в файле ровно один раз, а все его настройки -\n" + " идти внутри него одним блоком." + ) from exc server = data.get("server", {}) security = data.get("security", {}) proc = data.get("processing", {}) diff --git a/app/main.py b/app/main.py index d9c232e..8c07bc7 100644 --- a/app/main.py +++ b/app/main.py @@ -15,7 +15,7 @@ from fastapi.openapi.utils import get_openapi from fastapi.responses import JSONResponse from fastapi.security import HTTPBearer -from app.config import Settings, load_settings +from app.config import ConfigError, Settings, load_settings from app.logbuffer import get as log_buffer from app.logbuffer import install as install_log_buffer from app.pipeline import ModelsMissing, Pipeline, to_wav16k @@ -26,7 +26,13 @@ from app.version import __version__ log = logging.getLogger("talkscore-asr") -settings: Settings = load_settings() +try: + settings: Settings = load_settings() +except ConfigError as exc: + # Печатаем понятную причину и выходим: со сломанным конфигом + # сервис всё равно ничего осмысленного не сделает. + print(f"\n {exc}\n") + raise SystemExit(1) from None allowlist = parse_allowlist(settings.allow_ips) trusted_proxies = parse_allowlist(settings.trust_proxy) diff --git a/tests/test_api.py b/tests/test_api.py index 37b306d..9f38352 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -255,3 +255,31 @@ class TestLogs: buf.emit(logging.LogRecord("t", logging.INFO, "f", 1, f"строка {i}", None, None)) rows = buf.tail(limit=100) assert len(rows) == 10 and "строка 49" in rows[-1]["message"] + + +class TestBrokenConfig: + """Со сломанным config.toml сервис должен объяснить причину, а не падать стеком.""" + + def test_duplicate_section_gives_readable_error(self, tmp_path): + from app.config import ConfigError, load_settings + + config = tmp_path / "config.toml" + config.write_text('[security]\ntoken="a"\n[security]\ntrust_proxy="127.0.0.1"\n', + encoding="utf-8") + with pytest.raises(ConfigError, match="дважды"): + load_settings(config) + + def test_error_names_the_file(self, tmp_path): + from app.config import ConfigError, load_settings + + config = tmp_path / "config.toml" + config.write_text('[server]\n[server]\n', encoding="utf-8") + with pytest.raises(ConfigError, match="config.toml"): + load_settings(config) + + def test_valid_config_still_loads(self, tmp_path): + from app.config import load_settings + + config = tmp_path / "config.toml" + config.write_text('[security]\ntoken="a"\ntrust_proxy="127.0.0.1"\n', encoding="utf-8") + assert load_settings(config).token == "a"