refactor: rename platform/ → sdk/ to avoid stdlib conflict

platform/ shadowed Python's stdlib platform module, breaking
aiogram/aiohttp/multidict at import time. Renamed to sdk/ and
updated all imports across core/, tests/, and adapter/telegram/.
This commit is contained in:
Mikhail Putilovskij 2026-03-31 21:57:23 +03:00
parent c979f96c3c
commit 41660fe84a
15 changed files with 1727 additions and 11 deletions

3
.gitignore vendored
View file

@ -21,6 +21,9 @@ build/
.vscode/
*.swp
# Visual brainstorming sessions
.superpowers/
# Tests
.pytest_cache/
.coverage

View file

@ -15,7 +15,7 @@ from core.protocol import (
OutgoingEvent,
)
from core.settings import SettingsManager
from platform.interface import PlatformClient
from sdk.interface import PlatformClient
logger = structlog.get_logger(__name__)

View file

@ -5,7 +5,7 @@ import structlog
from core.protocol import SettingsAction
from core.store import StateStore
from platform.interface import PlatformClient, UserSettings
from sdk.interface import PlatformClient, UserSettings
logger = structlog.get_logger(__name__)

View file

@ -0,0 +1,172 @@
# Ресёрч: aiogram 3.x Architecture Review
> **Дата:** 2026-03-30
> **Вердикт:** APPROVED с двумя уточнениями
---
## 1. Структура проекта
**Официальный пример multi_file_bot:**
```
multi_file_bot/
bot.py
handlers/
common.py
...
```
**Best practice для средних проектов (наш случай):**
```
adapter/telegram/
bot.py ← Dispatcher + include_routers + polling/webhook
converter.py ← граница aiogram ↔ core/
states.py ← все StatesGroup
handlers/ ← по одному Router на модуль
keyboards/ ← InlineKeyboardBuilder фабрики
middleware.py ← DI + logging + rate limit
```
**Оценка:** наша структура соответствует стандарту. ✓
---
## 2. Middleware vs Converter
В aiogram 3.x эти два паттерна решают **разные задачи** и должны использоваться вместе.
| | Middleware | Converter |
|---|---|---|
| Назначение | Infrastructure | Бизнес-логика |
| Что делает | Логирование, DI, rate limit, сессия БД | aiogram Event → IncomingEvent |
| Когда вызывается | До и после хендлера | Внутри хендлера |
**Правильная комбинация:**
```python
# middleware.py — только infrastructure
class DependencyMiddleware(BaseMiddleware):
def __init__(self, platform, store):
self.platform = platform
self.store = store
async def __call__(self, handler, event, data):
data["platform"] = self.platform
data["store"] = self.store
return await handler(event, data)
# handler — converter вызывается внутри
async def handle_message(message: Message, platform, store):
event = to_incoming_message(message) # converter
results = await dispatcher.dispatch(event, platform, store)
await send_results(message, results) # converter обратно
```
**Оценка:** наш converter.py — правильный паттерн. Добавить `middleware.py` для DI. ✓+
---
## 3. Dependency Injection
Стандарт aiogram 3.x — **через middleware + data dict**:
```python
# Регистрация в bot.py
dp.message.middleware(DependencyMiddleware(platform=platform_client, store=store))
# Получение в handler (через type hint на имя ключа)
async def handle_message(message: Message, platform: PlatformClient, store: StateStore):
...
```
Альтернатива — через `dp["key"] = value` (Dispatcher workflow data):
```python
dp["platform"] = platform_client # в bot.py
async def handler(message: Message, platform: PlatformClient): # aiogram сам находит по типу
...
```
**Оценка:** нужно явно добавить один из этих механизмов, иначе хендлеры не получат platform/store. ⚠️
---
## 4. InlineKeyboardBuilder
`InlineKeyboardBuilder` — рекомендуемый подход в aiogram 3.x. `InlineKeyboardMarkup` с вложенными списками считается устаревшим стилем.
```python
# keyboards/chat.py
from aiogram.utils.keyboard import InlineKeyboardBuilder
def chats_keyboard(chats: list[ChatContext]) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for chat in chats:
builder.button(text=f"💬 {chat.name}", callback_data=f"chat:{chat.chat_id}")
builder.button(text=" Новый чат", callback_data="new_chat")
builder.adjust(1) # одна кнопка в строку
return builder.as_markup()
```
**Оценка:** использовать `InlineKeyboardBuilder` везде. ✓
---
## 5. F-фильтры (MagicFilter)
aiogram 3.x MagicFilter (`F`) — стандарт вместо ручных проверок в хендлерах:
```python
from aiogram import F
# Вместо if message.text == "/start" внутри хендлера
router.message.register(start_handler, Command("start"))
# Фильтр по типу вложения
router.message.register(voice_handler, F.voice)
router.message.register(photo_handler, F.photo)
# Фильтр по состоянию
router.message.register(handle_name_input, OnboardingState.waiting_for_name)
# Callback фильтр
router.callback_query.register(confirm_handler, F.data.startswith("confirm:"))
```
**Оценка:** использовать F-фильтры при регистрации роутеров — чище, чем if/else в хендлерах. ✓
---
## 6. Сцены (Scenes) — новинка aiogram 3.x
aiogram 3.4+ ввёл `Scene` как улучшенный FSM для сложных диалогов:
```python
from aiogram.fsm.scene import Scene, on
class OnboardingScene(Scene, state="onboarding"):
@on.message.enter()
async def on_enter(self, message: Message):
await message.answer("Как зовут твоего агента?")
@on.message()
async def on_name(self, message: Message, state: FSMContext):
await state.update_data(agent_name=message.text)
await self.wizard.goto(OnboardingScene2)
```
**Оценка:** Scenes — опциональное улучшение для онбординга. Классический FSM через StatesGroup тоже корректен и проще для понимания. Использовать StatesGroup для прототипа, Scenes — в будущем. ✓
---
## Итог
| Решение | Статус |
|---|---|
| Router-based архитектура, один Router на модуль | ✅ Стандарт |
| converter.py как граница aiogram ↔ core/ | ✅ Правильный паттерн |
| InlineKeyboardBuilder в keyboards/ | ✅ Рекомендуется |
| SQLiteStorage для FSM | ✅ Стандарт для MVP |
| **Нужно добавить: DependencyMiddleware** | ⚠️ DI без него не работает |
| **Нужно добавить: F-фильтры при регистрации** | ⚠️ Иначе проверки в хендлерах |
**Архитектура одобрена.** Два уточнения (middleware.py и F-фильтры) небольшие и органично вписываются в текущую структуру.

View file

@ -9,7 +9,7 @@ from typing import Any, AsyncIterator, Literal
import structlog
from platform.interface import (
from sdk.interface import (
AgentEvent,
Attachment,
MessageChunk,

View file

@ -2,7 +2,7 @@
import pytest
from core.auth import AuthManager
from core.store import InMemoryStore
from platform.mock import MockPlatformClient
from sdk.mock import MockPlatformClient
@pytest.fixture

View file

@ -2,7 +2,7 @@
import pytest
from core.chat import ChatManager
from core.store import InMemoryStore
from platform.mock import MockPlatformClient
from sdk.mock import MockPlatformClient
@pytest.fixture

View file

@ -9,7 +9,7 @@ from core.chat import ChatManager
from core.auth import AuthManager
from core.settings import SettingsManager
from core.store import InMemoryStore
from platform.mock import MockPlatformClient
from sdk.mock import MockPlatformClient
@pytest.fixture

View file

@ -4,7 +4,7 @@ Smoke test: полный цикл через dispatcher + реальные manag
Имитирует что делает адаптер (Telegram или Matrix) при получении события.
"""
import pytest
from platform.mock import MockPlatformClient
from sdk.mock import MockPlatformClient
from core.store import InMemoryStore
from core.chat import ChatManager
from core.auth import AuthManager

View file

@ -3,7 +3,7 @@ import pytest
from core.settings import SettingsManager
from core.store import InMemoryStore
from core.protocol import SettingsAction
from platform.mock import MockPlatformClient
from sdk.mock import MockPlatformClient
@pytest.fixture

View file

@ -6,7 +6,7 @@ from core.store import InMemoryStore
from core.auth import AuthManager
from core.chat import ChatManager
from core.settings import SettingsManager
from platform.mock import MockPlatformClient
from sdk.mock import MockPlatformClient
@pytest.fixture

View file

@ -1,6 +1,6 @@
# tests/platform/test_mock.py
from platform.mock import MockPlatformClient
from platform.interface import User, MessageResponse, UserSettings
from sdk.mock import MockPlatformClient
from sdk.interface import User, MessageResponse, UserSettings
from core.protocol import SettingsAction

1541
uv.lock generated Normal file

File diff suppressed because it is too large Load diff