fix syntax

This commit is contained in:
kandrusyak
2026-07-25 15:47:36 +03:00
parent 3f63d0305c
commit c04fb9480b
8 changed files with 56 additions and 32 deletions

View File

@@ -148,16 +148,18 @@ async def private_voice(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
) )
status_message = await update.message.reply_text("Распознаю голосовое сообщение...") status_message = await update.message.reply_text("Распознаю голосовое сообщение...")
temp_path: Path | None = None temp_path: Path | None = None
transcript = ""
try: try:
telegram_file = await context.bot.get_file(voice.file_id) telegram_file = await context.bot.get_file(voice.file_id)
file_descriptor, raw_path = tempfile.mkstemp(suffix=".ogg") file_descriptor, raw_path = tempfile.mkstemp(suffix=".ogg")
os.close(file_descriptor) os.close(file_descriptor)
temp_path = Path(raw_path) voice_path = Path(raw_path)
await telegram_file.download_to_drive(custom_path=temp_path) temp_path = voice_path
await telegram_file.download_to_drive(custom_path=voice_path)
transcript = await asyncio.to_thread( transcript = await asyncio.to_thread(
get_speech_recognizer(context).transcribe, get_speech_recognizer(context).transcribe,
temp_path, voice_path,
) )
except (SpeechRecognitionError, TelegramError): except (SpeechRecognitionError, TelegramError):
logger.exception("Failed to transcribe Telegram voice message") logger.exception("Failed to transcribe Telegram voice message")
@@ -364,7 +366,7 @@ async def remind_command(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
reminder_id = get_storage(context).add_reminder( reminder_id = get_storage(context).add_reminder(
user_id=user_id, user_id=user_id,
chat_id=int(update.effective_chat.id), chat_id=int(update.message.chat_id),
text=parsed.text, text=parsed.text,
remind_at_utc=parsed.remind_at_utc, remind_at_utc=parsed.remind_at_utc,
) )

View File

@@ -2,7 +2,7 @@ import sys
import threading import threading
import warnings import warnings
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Protocol from typing import Any, Callable, Protocol, cast
class SpeechRecognitionError(RuntimeError): class SpeechRecognitionError(RuntimeError):
@@ -21,9 +21,9 @@ def load_whisper_model_class() -> Any:
missing = object() missing = object()
loaded_torch = sys.modules.get("torch", missing) loaded_torch = sys.modules.get("torch", missing)
if loaded_torch is missing: if loaded_torch is missing:
sys.modules["torch"] = None sys.modules["torch"] = cast(Any, None)
try: try:
with warnings.catch_warnings(): with warnings.catch_warnings(record=False):
warnings.filterwarnings( warnings.filterwarnings(
"ignore", "ignore",
message="pkg_resources is deprecated as an API.*", message="pkg_resources is deprecated as an API.*",

View File

@@ -9,6 +9,17 @@ from .config import get_default_model
from .time_utils import to_utc_iso, utc_now from .time_utils import to_utc_iso, utc_now
def row_to_dict(row: sqlite3.Row) -> dict[str, Any]:
return {str(key): row[key] for key in row.keys()}
def required_lastrowid(cursor: sqlite3.Cursor) -> int:
lastrowid = cursor.lastrowid
if lastrowid is None:
raise RuntimeError("SQLite insert did not return a row ID")
return lastrowid
class AssistantStorage: class AssistantStorage:
def __init__(self, path: Path) -> None: def __init__(self, path: Path) -> None:
self.path = path self.path = path
@@ -162,7 +173,7 @@ class AssistantStorage:
""", """,
(user_id, chat_id, started_after_id, limit), (user_id, chat_id, started_after_id, limit),
).fetchall() ).fetchall()
return [dict(row) for row in reversed(rows)] return [row_to_dict(row) for row in reversed(rows)]
def start_new_conversation(self, user_id: int, chat_id: int) -> None: def start_new_conversation(self, user_id: int, chat_id: int) -> None:
now = to_utc_iso(utc_now()) now = to_utc_iso(utc_now())
@@ -230,7 +241,7 @@ class AssistantStorage:
age = max(0, newest_id - int(row["id"])) age = max(0, newest_id - int(row["id"]))
recency_weight = 1.0 / (1.0 + age / 50.0) recency_weight = 1.0 / (1.0 + age / 50.0)
score = (coverage + phrase_bonus) * (0.5 + 0.5 * recency_weight) score = (coverage + phrase_bonus) * (0.5 + 0.5 * recency_weight)
item = dict(row) item = row_to_dict(row)
item["score"] = round(score, 4) item["score"] = round(score, 4)
matches.append((score, item)) matches.append((score, item))
@@ -266,7 +277,7 @@ class AssistantStorage:
(user_id, chat_id, message_id, radius), (user_id, chat_id, message_id, radius),
).fetchall() ).fetchall()
rows = [*reversed(before), *after] rows = [*reversed(before), *after]
return [dict(row) for row in rows] return [row_to_dict(row) for row in rows]
def get_user_model(self, user_id: int, provider: str = "local") -> str: def get_user_model(self, user_id: int, provider: str = "local") -> str:
column = self._model_column(provider) column = self._model_column(provider)
@@ -315,7 +326,7 @@ class AssistantStorage:
"INSERT INTO memories(user_id, text, created_at) VALUES (?, ?, ?)", "INSERT INTO memories(user_id, text, created_at) VALUES (?, ?, ?)",
(user_id, text, to_utc_iso(utc_now())), (user_id, text, to_utc_iso(utc_now())),
) )
return int(cursor.lastrowid) return required_lastrowid(cursor)
def list_memories(self, user_id: int, limit: int = 20) -> list[dict[str, Any]]: def list_memories(self, user_id: int, limit: int = 20) -> list[dict[str, Any]]:
with self._connection() as connection: with self._connection() as connection:
@@ -329,7 +340,7 @@ class AssistantStorage:
""", """,
(user_id, limit), (user_id, limit),
).fetchall() ).fetchall()
return [dict(row) for row in rows] return [row_to_dict(row) for row in rows]
def delete_memory(self, user_id: int, memory_id: int) -> bool: def delete_memory(self, user_id: int, memory_id: int) -> bool:
with self._connection() as connection: with self._connection() as connection:
@@ -345,7 +356,7 @@ class AssistantStorage:
"INSERT INTO notes(user_id, text, created_at) VALUES (?, ?, ?)", "INSERT INTO notes(user_id, text, created_at) VALUES (?, ?, ?)",
(user_id, text, to_utc_iso(utc_now())), (user_id, text, to_utc_iso(utc_now())),
) )
return int(cursor.lastrowid) return required_lastrowid(cursor)
def list_notes(self, user_id: int, limit: int = 20) -> list[dict[str, Any]]: def list_notes(self, user_id: int, limit: int = 20) -> list[dict[str, Any]]:
with self._connection() as connection: with self._connection() as connection:
@@ -359,7 +370,7 @@ class AssistantStorage:
""", """,
(user_id, limit), (user_id, limit),
).fetchall() ).fetchall()
return [dict(row) for row in rows] return [row_to_dict(row) for row in rows]
def delete_note(self, user_id: int, note_id: int) -> bool: def delete_note(self, user_id: int, note_id: int) -> bool:
with self._connection() as connection: with self._connection() as connection:
@@ -390,7 +401,7 @@ class AssistantStorage:
to_utc_iso(utc_now()), to_utc_iso(utc_now()),
), ),
) )
return int(cursor.lastrowid) return required_lastrowid(cursor)
def list_reminders(self, user_id: int, limit: int = 20) -> list[dict[str, Any]]: def list_reminders(self, user_id: int, limit: int = 20) -> list[dict[str, Any]]:
with self._connection() as connection: with self._connection() as connection:
@@ -404,7 +415,7 @@ class AssistantStorage:
""", """,
(user_id, limit), (user_id, limit),
).fetchall() ).fetchall()
return [dict(row) for row in rows] return [row_to_dict(row) for row in rows]
def cancel_reminder(self, user_id: int, reminder_id: int) -> bool: def cancel_reminder(self, user_id: int, reminder_id: int) -> bool:
with self._connection() as connection: with self._connection() as connection:
@@ -430,7 +441,7 @@ class AssistantStorage:
""", """,
(to_utc_iso(now_utc), limit), (to_utc_iso(now_utc), limit),
).fetchall() ).fetchall()
return [dict(row) for row in rows] return [row_to_dict(row) for row in rows]
def mark_reminder_sent(self, reminder_id: int) -> None: def mark_reminder_sent(self, reminder_id: int) -> None:
with self._connection() as connection: with self._connection() as connection:
@@ -453,7 +464,7 @@ class AssistantStorage:
""", """,
(user_id, title, status, now, now), (user_id, title, status, now, now),
) )
return int(cursor.lastrowid) return required_lastrowid(cursor)
def list_tracked_items(self, user_id: int, limit: int = 30) -> list[dict[str, Any]]: def list_tracked_items(self, user_id: int, limit: int = 30) -> list[dict[str, Any]]:
with self._connection() as connection: with self._connection() as connection:
@@ -467,7 +478,7 @@ class AssistantStorage:
""", """,
(user_id, limit), (user_id, limit),
).fetchall() ).fetchall()
return [dict(row) for row in rows] return [row_to_dict(row) for row in rows]
def set_tracked_status(self, user_id: int, item_id: int, status: str) -> bool: def set_tracked_status(self, user_id: int, item_id: int, status: str) -> bool:
with self._connection() as connection: with self._connection() as connection:

View File

@@ -57,12 +57,13 @@ def get_voice_max_duration(context: ContextTypes.DEFAULT_TYPE) -> int:
def command_text(context: ContextTypes.DEFAULT_TYPE) -> str: def command_text(context: ContextTypes.DEFAULT_TYPE) -> str:
return " ".join(context.args).strip() return " ".join(context.args or []).strip()
def require_user_id(update: Update) -> int | None: def require_user_id(update: Update) -> int | None:
if update.effective_user: user = update.effective_user
return int(update.effective_user.id) if user:
return int(user.id)
return None return None

View File

@@ -13,7 +13,10 @@ from assistant_bot.storage import AssistantStorage
class ReminderParserTests(unittest.TestCase): class ReminderParserTests(unittest.TestCase):
def test_supported_relative_unit(self) -> None: def test_supported_relative_unit(self) -> None:
self.assertEqual(unit_to_timedelta(2, "часа").total_seconds(), 7200) delta = unit_to_timedelta(2, "часа")
self.assertIsNotNone(delta)
assert delta is not None
self.assertEqual(delta.total_seconds(), 7200)
def test_absolute_datetime(self) -> None: def test_absolute_datetime(self) -> None:
result = parse_reminder( result = parse_reminder(

View File

@@ -1,6 +1,7 @@
import asyncio import asyncio
import unittest import unittest
from types import SimpleNamespace from types import SimpleNamespace
from typing import Any
from unittest.mock import patch from unittest.mock import patch
from assistant_bot.jobs import post_init, post_shutdown from assistant_bot.jobs import post_init, post_shutdown
@@ -10,11 +11,11 @@ class ReminderJobLifecycleTests(unittest.IsolatedAsyncioTestCase):
async def test_reminder_task_is_cancelled_on_shutdown(self) -> None: async def test_reminder_task_is_cancelled_on_shutdown(self) -> None:
started = asyncio.Event() started = asyncio.Event()
async def fake_reminder_loop(application) -> None: async def fake_reminder_loop(_application) -> None:
started.set() started.set()
await asyncio.Event().wait() await asyncio.Event().wait()
application = SimpleNamespace(bot_data={}) application: Any = SimpleNamespace(bot_data={})
with patch("assistant_bot.jobs.reminder_loop", fake_reminder_loop): with patch("assistant_bot.jobs.reminder_loop", fake_reminder_loop):
await post_init(application) await post_init(application)
await started.wait() await started.wait()

View File

@@ -1,6 +1,7 @@
import unittest import unittest
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from assistant_bot.handlers import private_voice from assistant_bot.handlers import private_voice
@@ -15,7 +16,7 @@ class VoiceHandlerTests(unittest.IsolatedAsyncioTestCase):
voice=SimpleNamespace(duration=duration, file_id="voice-file-id"), voice=SimpleNamespace(duration=duration, file_id="voice-file-id"),
reply_text=AsyncMock(return_value=status_message), reply_text=AsyncMock(return_value=status_message),
) )
update = SimpleNamespace(message=message) update: Any = SimpleNamespace(message=message)
telegram_file = SimpleNamespace(download_to_drive=AsyncMock()) telegram_file = SimpleNamespace(download_to_drive=AsyncMock())
bot = SimpleNamespace( bot = SimpleNamespace(
get_file=AsyncMock(return_value=telegram_file), get_file=AsyncMock(return_value=telegram_file),
@@ -23,7 +24,7 @@ class VoiceHandlerTests(unittest.IsolatedAsyncioTestCase):
) )
recognizer = MagicMock() recognizer = MagicMock()
recognizer.transcribe.return_value = transcript recognizer.transcribe.return_value = transcript
context = SimpleNamespace( context: Any = SimpleNamespace(
bot=bot, bot=bot,
application=SimpleNamespace( application=SimpleNamespace(
bot_data={ bot_data={

View File

@@ -38,6 +38,15 @@ class FakeChatCompletions:
return self.completion return self.completion
class FakeHTTPError(RuntimeError):
def __init__(self) -> None:
super().__init__("forbidden")
self.response = SimpleNamespace(
status_code=403,
text='{"error":{"message":"folder mismatch"}}',
)
class YandexAIClientTests(unittest.IsolatedAsyncioTestCase): class YandexAIClientTests(unittest.IsolatedAsyncioTestCase):
def test_multiple_system_messages_are_merged_at_the_beginning(self) -> None: def test_multiple_system_messages_are_merged_at_the_beginning(self) -> None:
self.assertEqual( self.assertEqual(
@@ -62,11 +71,7 @@ class YandexAIClientTests(unittest.IsolatedAsyncioTestCase):
) )
def test_http_error_includes_safe_response_body(self) -> None: def test_http_error_includes_safe_response_body(self) -> None:
error = RuntimeError("forbidden") error = FakeHTTPError()
error.response = SimpleNamespace(
status_code=403,
text='{"error":{"message":"folder mismatch"}}',
)
self.assertEqual( self.assertEqual(
describe_yandex_error(error), describe_yandex_error(error),