refactor: centralize settings and application services
This commit is contained in:
@@ -12,22 +12,7 @@ from telegram.ext import (
|
|||||||
|
|
||||||
from .ai import AIClient
|
from .ai import AIClient
|
||||||
from .auth import authentication_guard
|
from .auth import authentication_guard
|
||||||
from .config import (
|
from .config import AppSettings, load_app_settings
|
||||||
get_assistant_mode,
|
|
||||||
get_assistant_password,
|
|
||||||
get_bot_token,
|
|
||||||
get_db_path,
|
|
||||||
get_local_timezone,
|
|
||||||
get_ollama_base_url,
|
|
||||||
get_voice_max_duration_seconds,
|
|
||||||
get_whisper_compute_type,
|
|
||||||
get_whisper_device,
|
|
||||||
get_whisper_language,
|
|
||||||
get_whisper_model,
|
|
||||||
get_yandex_cloud_folder,
|
|
||||||
get_yandex_stt_language,
|
|
||||||
get_yandex_stt_model,
|
|
||||||
)
|
|
||||||
from .handlers import (
|
from .handlers import (
|
||||||
ask_command,
|
ask_command,
|
||||||
help_command,
|
help_command,
|
||||||
@@ -42,6 +27,7 @@ from .handlers import (
|
|||||||
)
|
)
|
||||||
from .jobs import post_init, post_shutdown
|
from .jobs import post_init, post_shutdown
|
||||||
from .ollama import OllamaClient
|
from .ollama import OllamaClient
|
||||||
|
from .services import SERVICES_KEY, ApplicationServices
|
||||||
from .storage import AssistantStorage
|
from .storage import AssistantStorage
|
||||||
from .speech import SpeechRecognizer, SpeechTranscriber
|
from .speech import SpeechRecognizer, SpeechTranscriber
|
||||||
from .yandex_ai import YandexAIClient, YandexSpeechRecognizer
|
from .yandex_ai import YandexAIClient, YandexSpeechRecognizer
|
||||||
@@ -55,42 +41,51 @@ def configure_logging() -> None:
|
|||||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
|
||||||
def create_application() -> Application:
|
def create_services(settings: AppSettings) -> ApplicationServices:
|
||||||
assistant_password = get_assistant_password()
|
storage = AssistantStorage(settings.db_path)
|
||||||
storage = AssistantStorage(get_db_path())
|
ai_client: AIClient
|
||||||
mode = get_assistant_mode()
|
speech_recognizer: SpeechTranscriber
|
||||||
|
if settings.mode == "local":
|
||||||
|
ai_client = OllamaClient(settings.ollama_base_url)
|
||||||
|
speech_recognizer = SpeechRecognizer(
|
||||||
|
model_name=settings.whisper_model,
|
||||||
|
device=settings.whisper_device,
|
||||||
|
compute_type=settings.whisper_compute_type,
|
||||||
|
language=settings.whisper_language,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
folder_id = settings.yandex_cloud_folder
|
||||||
|
if folder_id is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"YANDEX_CLOUD_FOLDER is required in yandex mode."
|
||||||
|
)
|
||||||
|
ai_client = YandexAIClient(folder_id=folder_id)
|
||||||
|
speech_recognizer = YandexSpeechRecognizer(
|
||||||
|
folder_id=folder_id,
|
||||||
|
language=settings.yandex_stt_language,
|
||||||
|
model=settings.yandex_stt_model,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ApplicationServices(
|
||||||
|
storage=storage,
|
||||||
|
ai_client=ai_client,
|
||||||
|
timezone=settings.timezone,
|
||||||
|
speech_recognizer=speech_recognizer,
|
||||||
|
voice_max_duration_seconds=settings.voice_max_duration_seconds,
|
||||||
|
assistant_password=settings.assistant_password,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_application(settings: AppSettings | None = None) -> Application:
|
||||||
|
settings = settings or load_app_settings()
|
||||||
application = (
|
application = (
|
||||||
Application.builder()
|
Application.builder()
|
||||||
.token(get_bot_token())
|
.token(settings.bot_token)
|
||||||
.post_init(post_init)
|
.post_init(post_init)
|
||||||
.post_shutdown(post_shutdown)
|
.post_shutdown(post_shutdown)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
application.bot_data["storage"] = storage
|
application.bot_data[SERVICES_KEY] = create_services(settings)
|
||||||
application.bot_data["assistant_password"] = assistant_password
|
|
||||||
ai_client: AIClient
|
|
||||||
speech_recognizer: SpeechTranscriber
|
|
||||||
if mode == "local":
|
|
||||||
ai_client = OllamaClient(get_ollama_base_url())
|
|
||||||
speech_recognizer = SpeechRecognizer(
|
|
||||||
model_name=get_whisper_model(),
|
|
||||||
device=get_whisper_device(),
|
|
||||||
compute_type=get_whisper_compute_type(),
|
|
||||||
language=get_whisper_language(),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
folder_id = get_yandex_cloud_folder()
|
|
||||||
ai_client = YandexAIClient(folder_id=folder_id)
|
|
||||||
speech_recognizer = YandexSpeechRecognizer(
|
|
||||||
folder_id=folder_id,
|
|
||||||
language=get_yandex_stt_language(),
|
|
||||||
model=get_yandex_stt_model(),
|
|
||||||
)
|
|
||||||
|
|
||||||
application.bot_data["ai_client"] = ai_client
|
|
||||||
application.bot_data["timezone"] = get_local_timezone()
|
|
||||||
application.bot_data["speech_recognizer"] = speech_recognizer
|
|
||||||
application.bot_data["voice_max_duration"] = get_voice_max_duration_seconds()
|
|
||||||
|
|
||||||
application.add_handler(TypeHandler(Update, authentication_guard), group=-1)
|
application.add_handler(TypeHandler(Update, authentication_guard), group=-1)
|
||||||
application.add_handler(CommandHandler("start", start))
|
application.add_handler(CommandHandler("start", start))
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ from telegram import Update
|
|||||||
from telegram.constants import ChatType
|
from telegram.constants import ChatType
|
||||||
from telegram.ext import ApplicationHandlerStop, ContextTypes
|
from telegram.ext import ApplicationHandlerStop, ContextTypes
|
||||||
|
|
||||||
from .telegram_utils import get_storage, reply_markdown, require_user_id
|
from .telegram_utils import (
|
||||||
|
get_services,
|
||||||
|
get_storage,
|
||||||
|
reply_markdown,
|
||||||
|
require_user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def passwords_match(candidate: str, expected: str) -> bool:
|
def passwords_match(candidate: str, expected: str) -> bool:
|
||||||
@@ -37,7 +42,7 @@ async def authentication_guard(
|
|||||||
raise ApplicationHandlerStop
|
raise ApplicationHandlerStop
|
||||||
|
|
||||||
candidate = message.text.strip() if message and message.text else ""
|
candidate = message.text.strip() if message and message.text else ""
|
||||||
password = str(context.application.bot_data["assistant_password"])
|
password = get_services(context).assistant_password
|
||||||
if candidate and passwords_match(candidate, password):
|
if candidate and passwords_match(candidate, password):
|
||||||
storage.authorize_user(user_id)
|
storage.authorize_user(user_id)
|
||||||
await reply_markdown(
|
await reply_markdown(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||||
|
|
||||||
@@ -46,6 +47,26 @@ MAX_AGENT_STEPS = 5
|
|||||||
CONVERSATION_HISTORY_LIMIT = 12
|
CONVERSATION_HISTORY_LIMIT = 12
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AppSettings:
|
||||||
|
bot_token: str
|
||||||
|
assistant_password: str
|
||||||
|
db_path: Path
|
||||||
|
mode: str
|
||||||
|
timezone: ZoneInfo
|
||||||
|
voice_max_duration_seconds: int
|
||||||
|
ollama_base_url: str
|
||||||
|
ollama_model: str
|
||||||
|
yandex_cloud_folder: str | None
|
||||||
|
yandex_cloud_model: str
|
||||||
|
yandex_stt_model: str
|
||||||
|
yandex_stt_language: str
|
||||||
|
whisper_model: str
|
||||||
|
whisper_device: str
|
||||||
|
whisper_compute_type: str
|
||||||
|
whisper_language: str | None
|
||||||
|
|
||||||
|
|
||||||
def load_env_file(path: Path = ENV_FILE) -> None:
|
def load_env_file(path: Path = ENV_FILE) -> None:
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
return
|
return
|
||||||
@@ -68,6 +89,128 @@ def load_env_file(path: Path = ENV_FILE) -> None:
|
|||||||
os.environ[key] = value
|
os.environ[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
def load_app_settings() -> AppSettings:
|
||||||
|
"""Load and validate the complete startup configuration once."""
|
||||||
|
load_env_file()
|
||||||
|
|
||||||
|
token = os.getenv(TOKEN_ENV_NAME)
|
||||||
|
if not token:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Set {TOKEN_ENV_NAME} in environment or .env file."
|
||||||
|
)
|
||||||
|
|
||||||
|
password = os.getenv(PASSWORD_ENV_NAME, "").strip()
|
||||||
|
if not password:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Set {PASSWORD_ENV_NAME} in environment or .env file."
|
||||||
|
)
|
||||||
|
|
||||||
|
mode = os.getenv(MODE_ENV_NAME, DEFAULT_MODE).strip().lower()
|
||||||
|
if mode not in {"local", "yandex"}:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{MODE_ENV_NAME} must be either 'local' or 'yandex', got {mode!r}."
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_db_path = os.getenv(DB_ENV_NAME)
|
||||||
|
if raw_db_path:
|
||||||
|
db_path = Path(raw_db_path).expanduser()
|
||||||
|
if not db_path.is_absolute():
|
||||||
|
db_path = PROJECT_ROOT / db_path
|
||||||
|
else:
|
||||||
|
db_path = DEFAULT_DB_FILE
|
||||||
|
|
||||||
|
timezone_name = os.getenv(TIMEZONE_ENV_NAME, DEFAULT_TIMEZONE)
|
||||||
|
try:
|
||||||
|
local_timezone = ZoneInfo(timezone_name)
|
||||||
|
except ZoneInfoNotFoundError:
|
||||||
|
logger.warning(
|
||||||
|
"Unknown timezone %s, falling back to UTC",
|
||||||
|
timezone_name,
|
||||||
|
)
|
||||||
|
local_timezone = ZoneInfo("UTC")
|
||||||
|
|
||||||
|
raw_voice_duration = os.getenv(
|
||||||
|
VOICE_MAX_DURATION_ENV_NAME,
|
||||||
|
str(DEFAULT_VOICE_MAX_DURATION_SECONDS),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
voice_duration = int(raw_voice_duration)
|
||||||
|
except ValueError:
|
||||||
|
voice_duration = 0
|
||||||
|
if voice_duration <= 0:
|
||||||
|
logger.warning(
|
||||||
|
"%s must be a positive integer, using %s",
|
||||||
|
VOICE_MAX_DURATION_ENV_NAME,
|
||||||
|
DEFAULT_VOICE_MAX_DURATION_SECONDS,
|
||||||
|
)
|
||||||
|
voice_duration = DEFAULT_VOICE_MAX_DURATION_SECONDS
|
||||||
|
|
||||||
|
yandex_folder = os.getenv(
|
||||||
|
YANDEX_CLOUD_FOLDER_ENV_NAME,
|
||||||
|
"",
|
||||||
|
).strip().strip("/")
|
||||||
|
if mode == "yandex" and not yandex_folder:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Set {YANDEX_CLOUD_FOLDER_ENV_NAME} in environment or .env file."
|
||||||
|
)
|
||||||
|
|
||||||
|
yandex_model = os.getenv(
|
||||||
|
YANDEX_CLOUD_MODEL_ENV_NAME,
|
||||||
|
DEFAULT_YANDEX_CLOUD_MODEL,
|
||||||
|
).strip().strip("/")
|
||||||
|
if not yandex_model:
|
||||||
|
yandex_model = DEFAULT_YANDEX_CLOUD_MODEL
|
||||||
|
|
||||||
|
whisper_language = os.getenv(
|
||||||
|
WHISPER_LANGUAGE_ENV_NAME,
|
||||||
|
DEFAULT_WHISPER_LANGUAGE,
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
return AppSettings(
|
||||||
|
bot_token=token,
|
||||||
|
assistant_password=password,
|
||||||
|
db_path=db_path,
|
||||||
|
mode=mode,
|
||||||
|
timezone=local_timezone,
|
||||||
|
voice_max_duration_seconds=voice_duration,
|
||||||
|
ollama_base_url=os.getenv(
|
||||||
|
OLLAMA_BASE_URL_ENV_NAME,
|
||||||
|
DEFAULT_OLLAMA_BASE_URL,
|
||||||
|
).rstrip("/"),
|
||||||
|
ollama_model=os.getenv(
|
||||||
|
OLLAMA_MODEL_ENV_NAME,
|
||||||
|
DEFAULT_OLLAMA_MODEL,
|
||||||
|
),
|
||||||
|
yandex_cloud_folder=yandex_folder or None,
|
||||||
|
yandex_cloud_model=yandex_model,
|
||||||
|
yandex_stt_model=os.getenv(
|
||||||
|
YANDEX_STT_MODEL_ENV_NAME,
|
||||||
|
DEFAULT_YANDEX_STT_MODEL,
|
||||||
|
).strip(),
|
||||||
|
yandex_stt_language=os.getenv(
|
||||||
|
YANDEX_STT_LANGUAGE_ENV_NAME,
|
||||||
|
DEFAULT_YANDEX_STT_LANGUAGE,
|
||||||
|
).strip(),
|
||||||
|
whisper_model=os.getenv(
|
||||||
|
WHISPER_MODEL_ENV_NAME,
|
||||||
|
DEFAULT_WHISPER_MODEL,
|
||||||
|
).strip(),
|
||||||
|
whisper_device=os.getenv(
|
||||||
|
WHISPER_DEVICE_ENV_NAME,
|
||||||
|
DEFAULT_WHISPER_DEVICE,
|
||||||
|
).strip(),
|
||||||
|
whisper_compute_type=os.getenv(
|
||||||
|
WHISPER_COMPUTE_TYPE_ENV_NAME,
|
||||||
|
DEFAULT_WHISPER_COMPUTE_TYPE,
|
||||||
|
).strip(),
|
||||||
|
whisper_language=(
|
||||||
|
None
|
||||||
|
if whisper_language.lower() == "auto"
|
||||||
|
else whisper_language or None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_bot_token() -> str:
|
def get_bot_token() -> str:
|
||||||
load_env_file()
|
load_env_file()
|
||||||
token = os.getenv(TOKEN_ENV_NAME)
|
token = os.getenv(TOKEN_ENV_NAME)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from zoneinfo import ZoneInfo
|
|||||||
from telegram.ext import Application
|
from telegram.ext import Application
|
||||||
|
|
||||||
from .config import REMINDER_POLL_SECONDS
|
from .config import REMINDER_POLL_SECONDS
|
||||||
from .storage import AssistantStorage
|
from .services import SERVICES_KEY, ApplicationServices
|
||||||
from .telegram_utils import escape_markdown_text, markdown_code, send_markdown
|
from .telegram_utils import escape_markdown_text, markdown_code, send_markdown
|
||||||
from .time_utils import format_local_dt, utc_now
|
from .time_utils import format_local_dt, utc_now
|
||||||
|
|
||||||
@@ -15,8 +15,9 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def reminder_loop(application: Application) -> None:
|
async def reminder_loop(application: Application) -> None:
|
||||||
storage: AssistantStorage = application.bot_data["storage"]
|
services: ApplicationServices = application.bot_data[SERVICES_KEY]
|
||||||
tz: ZoneInfo = application.bot_data["timezone"]
|
storage = services.storage
|
||||||
|
tz: ZoneInfo = services.timezone
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
|||||||
19
assistant_bot/services.py
Normal file
19
assistant_bot/services.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
from .ai import AIClient
|
||||||
|
from .speech import SpeechTranscriber
|
||||||
|
from .storage import AssistantStorage
|
||||||
|
|
||||||
|
|
||||||
|
SERVICES_KEY = "services"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ApplicationServices:
|
||||||
|
storage: AssistantStorage
|
||||||
|
ai_client: AIClient
|
||||||
|
timezone: ZoneInfo
|
||||||
|
speech_recognizer: SpeechTranscriber
|
||||||
|
voice_max_duration_seconds: int
|
||||||
|
assistant_password: str
|
||||||
@@ -20,6 +20,7 @@ from telegramify_markdown.config import RenderConfig
|
|||||||
|
|
||||||
from .ai import AIClient
|
from .ai import AIClient
|
||||||
from .config import MAX_TELEGRAM_MESSAGE_LENGTH
|
from .config import MAX_TELEGRAM_MESSAGE_LENGTH
|
||||||
|
from .services import SERVICES_KEY, ApplicationServices
|
||||||
from .speech import SpeechTranscriber
|
from .speech import SpeechTranscriber
|
||||||
from .storage import AssistantStorage
|
from .storage import AssistantStorage
|
||||||
|
|
||||||
@@ -262,24 +263,30 @@ def build_inline_results(query: str) -> list[InlineQueryResultArticle]:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_services(
|
||||||
|
context: ContextTypes.DEFAULT_TYPE,
|
||||||
|
) -> ApplicationServices:
|
||||||
|
return context.application.bot_data[SERVICES_KEY]
|
||||||
|
|
||||||
|
|
||||||
def get_storage(context: ContextTypes.DEFAULT_TYPE) -> AssistantStorage:
|
def get_storage(context: ContextTypes.DEFAULT_TYPE) -> AssistantStorage:
|
||||||
return context.application.bot_data["storage"]
|
return get_services(context).storage
|
||||||
|
|
||||||
|
|
||||||
def get_ai_client(context: ContextTypes.DEFAULT_TYPE) -> AIClient:
|
def get_ai_client(context: ContextTypes.DEFAULT_TYPE) -> AIClient:
|
||||||
return context.application.bot_data["ai_client"]
|
return get_services(context).ai_client
|
||||||
|
|
||||||
|
|
||||||
def get_tz(context: ContextTypes.DEFAULT_TYPE) -> ZoneInfo:
|
def get_tz(context: ContextTypes.DEFAULT_TYPE) -> ZoneInfo:
|
||||||
return context.application.bot_data["timezone"]
|
return get_services(context).timezone
|
||||||
|
|
||||||
|
|
||||||
def get_speech_recognizer(context: ContextTypes.DEFAULT_TYPE) -> SpeechTranscriber:
|
def get_speech_recognizer(context: ContextTypes.DEFAULT_TYPE) -> SpeechTranscriber:
|
||||||
return context.application.bot_data["speech_recognizer"]
|
return get_services(context).speech_recognizer
|
||||||
|
|
||||||
|
|
||||||
def get_voice_max_duration(context: ContextTypes.DEFAULT_TYPE) -> int:
|
def get_voice_max_duration(context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
return int(context.application.bot_data["voice_max_duration"])
|
return get_services(context).voice_max_duration_seconds
|
||||||
|
|
||||||
|
|
||||||
def command_text(context: ContextTypes.DEFAULT_TYPE) -> str:
|
def command_text(context: ContextTypes.DEFAULT_TYPE) -> str:
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import Mock, patch
|
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from assistant_bot import application as application_module
|
from assistant_bot import application as application_module
|
||||||
from assistant_bot.auth import authentication_guard
|
from assistant_bot.auth import authentication_guard
|
||||||
|
from assistant_bot.config import AppSettings
|
||||||
from assistant_bot.handlers import (
|
from assistant_bot.handlers import (
|
||||||
ask_command,
|
ask_command,
|
||||||
help_command,
|
help_command,
|
||||||
@@ -22,23 +22,26 @@ from assistant_bot.handlers import (
|
|||||||
|
|
||||||
class ApplicationRegistrationTests(unittest.TestCase):
|
class ApplicationRegistrationTests(unittest.TestCase):
|
||||||
def test_registers_the_current_public_handlers_in_order(self) -> None:
|
def test_registers_the_current_public_handlers_in_order(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as directory, patch.multiple(
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
application_module,
|
settings = AppSettings(
|
||||||
get_assistant_password=Mock(return_value="secret"),
|
bot_token="123456:TEST",
|
||||||
get_bot_token=Mock(return_value="123456:TEST"),
|
assistant_password="secret",
|
||||||
get_db_path=Mock(
|
db_path=Path(directory) / "assistant.sqlite3",
|
||||||
return_value=Path(directory) / "assistant.sqlite3"
|
mode="local",
|
||||||
),
|
timezone=ZoneInfo("UTC"),
|
||||||
get_assistant_mode=Mock(return_value="local"),
|
voice_max_duration_seconds=120,
|
||||||
get_ollama_base_url=Mock(return_value="http://localhost:11434"),
|
ollama_base_url="http://localhost:11434",
|
||||||
get_whisper_model=Mock(return_value="small"),
|
ollama_model="qwen3.5:9b",
|
||||||
get_whisper_device=Mock(return_value="cpu"),
|
yandex_cloud_folder=None,
|
||||||
get_whisper_compute_type=Mock(return_value="int8"),
|
yandex_cloud_model="yandexgpt/latest",
|
||||||
get_whisper_language=Mock(return_value="ru"),
|
yandex_stt_model="general",
|
||||||
get_local_timezone=Mock(return_value=ZoneInfo("UTC")),
|
yandex_stt_language="ru-RU",
|
||||||
get_voice_max_duration_seconds=Mock(return_value=120),
|
whisper_model="small",
|
||||||
):
|
whisper_device="cpu",
|
||||||
application = application_module.create_application()
|
whisper_compute_type="int8",
|
||||||
|
whisper_language="ru",
|
||||||
|
)
|
||||||
|
application = application_module.create_application(settings)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
[handler.callback for handler in application.handlers[-1]],
|
[handler.callback for handler in application.handlers[-1]],
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ from pathlib import Path
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from telegram.constants import ChatType, ParseMode
|
from telegram.constants import ChatType, ParseMode
|
||||||
from telegram.ext import ApplicationHandlerStop
|
from telegram.ext import ApplicationHandlerStop
|
||||||
|
|
||||||
from assistant_bot.auth import authentication_guard
|
from assistant_bot.auth import authentication_guard
|
||||||
|
from assistant_bot.services import SERVICES_KEY, ApplicationServices
|
||||||
from assistant_bot.storage import AssistantStorage
|
from assistant_bot.storage import AssistantStorage
|
||||||
from assistant_bot.telegram_utils import render_markdown_chunks
|
from assistant_bot.telegram_utils import render_markdown_chunks
|
||||||
|
|
||||||
@@ -34,8 +36,14 @@ class AuthenticationGuardTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
context = SimpleNamespace(
|
context = SimpleNamespace(
|
||||||
application=SimpleNamespace(
|
application=SimpleNamespace(
|
||||||
bot_data={
|
bot_data={
|
||||||
"storage": self.storage,
|
SERVICES_KEY: ApplicationServices(
|
||||||
"assistant_password": "секрет",
|
storage=self.storage,
|
||||||
|
ai_client=SimpleNamespace(),
|
||||||
|
timezone=ZoneInfo("UTC"),
|
||||||
|
speech_recognizer=SimpleNamespace(),
|
||||||
|
voice_max_duration_seconds=120,
|
||||||
|
assistant_password="секрет",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,35 @@ from unittest.mock import patch
|
|||||||
from assistant_bot import config
|
from assistant_bot import config
|
||||||
|
|
||||||
|
|
||||||
|
class AppSettingsTests(unittest.TestCase):
|
||||||
|
def test_loads_complete_startup_configuration_once(self) -> None:
|
||||||
|
environment = {
|
||||||
|
config.TOKEN_ENV_NAME: "token",
|
||||||
|
config.PASSWORD_ENV_NAME: "secret",
|
||||||
|
config.MODE_ENV_NAME: "local",
|
||||||
|
config.DB_ENV_NAME: "data/test.sqlite3",
|
||||||
|
config.TIMEZONE_ENV_NAME: "UTC",
|
||||||
|
config.WHISPER_LANGUAGE_ENV_NAME: "auto",
|
||||||
|
}
|
||||||
|
with patch(
|
||||||
|
"assistant_bot.config.load_env_file"
|
||||||
|
) as load_env_file, patch.dict(
|
||||||
|
os.environ,
|
||||||
|
environment,
|
||||||
|
clear=True,
|
||||||
|
):
|
||||||
|
settings = config.load_app_settings()
|
||||||
|
|
||||||
|
load_env_file.assert_called_once_with()
|
||||||
|
self.assertEqual(settings.bot_token, "token")
|
||||||
|
self.assertEqual(settings.assistant_password, "secret")
|
||||||
|
self.assertEqual(settings.mode, "local")
|
||||||
|
self.assertEqual(settings.db_path, config.PROJECT_ROOT / "data/test.sqlite3")
|
||||||
|
self.assertEqual(settings.timezone.key, "UTC")
|
||||||
|
self.assertIsNone(settings.whisper_language)
|
||||||
|
self.assertIsNone(settings.yandex_cloud_folder)
|
||||||
|
|
||||||
|
|
||||||
class WhisperConfigTests(unittest.TestCase):
|
class WhisperConfigTests(unittest.TestCase):
|
||||||
def test_gpu_int8_defaults(self) -> None:
|
def test_gpu_int8_defaults(self) -> None:
|
||||||
variable_names = (
|
variable_names = (
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ from pathlib import Path
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from telegram.constants import ParseMode
|
from telegram.constants import ParseMode
|
||||||
|
|
||||||
from assistant_bot.handlers import private_voice
|
from assistant_bot.handlers import private_voice
|
||||||
|
from assistant_bot.services import SERVICES_KEY, ApplicationServices
|
||||||
from assistant_bot.telegram_utils import render_markdown_chunks
|
from assistant_bot.telegram_utils import render_markdown_chunks
|
||||||
|
|
||||||
|
|
||||||
@@ -31,8 +33,14 @@ class VoiceHandlerTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
bot=bot,
|
bot=bot,
|
||||||
application=SimpleNamespace(
|
application=SimpleNamespace(
|
||||||
bot_data={
|
bot_data={
|
||||||
"speech_recognizer": recognizer,
|
SERVICES_KEY: ApplicationServices(
|
||||||
"voice_max_duration": 120,
|
storage=SimpleNamespace(),
|
||||||
|
ai_client=SimpleNamespace(),
|
||||||
|
timezone=ZoneInfo("UTC"),
|
||||||
|
speech_recognizer=recognizer,
|
||||||
|
voice_max_duration_seconds=120,
|
||||||
|
assistant_password="secret",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user