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:
parent
c979f96c3c
commit
41660fe84a
15 changed files with 1727 additions and 11 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -21,6 +21,9 @@ build/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
|
# Visual brainstorming sessions
|
||||||
|
.superpowers/
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.coverage
|
.coverage
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from core.protocol import (
|
||||||
OutgoingEvent,
|
OutgoingEvent,
|
||||||
)
|
)
|
||||||
from core.settings import SettingsManager
|
from core.settings import SettingsManager
|
||||||
from platform.interface import PlatformClient
|
from sdk.interface import PlatformClient
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import structlog
|
||||||
|
|
||||||
from core.protocol import SettingsAction
|
from core.protocol import SettingsAction
|
||||||
from core.store import StateStore
|
from core.store import StateStore
|
||||||
from platform.interface import PlatformClient, UserSettings
|
from sdk.interface import PlatformClient, UserSettings
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
172
docs/research/aiogram-architecture-review.md
Normal file
172
docs/research/aiogram-architecture-review.md
Normal 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-фильтры) небольшие и органично вписываются в текущую структуру.
|
||||||
|
|
@ -9,7 +9,7 @@ from typing import Any, AsyncIterator, Literal
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from platform.interface import (
|
from sdk.interface import (
|
||||||
AgentEvent,
|
AgentEvent,
|
||||||
Attachment,
|
Attachment,
|
||||||
MessageChunk,
|
MessageChunk,
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import pytest
|
import pytest
|
||||||
from core.auth import AuthManager
|
from core.auth import AuthManager
|
||||||
from core.store import InMemoryStore
|
from core.store import InMemoryStore
|
||||||
from platform.mock import MockPlatformClient
|
from sdk.mock import MockPlatformClient
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import pytest
|
import pytest
|
||||||
from core.chat import ChatManager
|
from core.chat import ChatManager
|
||||||
from core.store import InMemoryStore
|
from core.store import InMemoryStore
|
||||||
from platform.mock import MockPlatformClient
|
from sdk.mock import MockPlatformClient
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from core.chat import ChatManager
|
||||||
from core.auth import AuthManager
|
from core.auth import AuthManager
|
||||||
from core.settings import SettingsManager
|
from core.settings import SettingsManager
|
||||||
from core.store import InMemoryStore
|
from core.store import InMemoryStore
|
||||||
from platform.mock import MockPlatformClient
|
from sdk.mock import MockPlatformClient
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ Smoke test: полный цикл через dispatcher + реальные manag
|
||||||
Имитирует что делает адаптер (Telegram или Matrix) при получении события.
|
Имитирует что делает адаптер (Telegram или Matrix) при получении события.
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
from platform.mock import MockPlatformClient
|
from sdk.mock import MockPlatformClient
|
||||||
from core.store import InMemoryStore
|
from core.store import InMemoryStore
|
||||||
from core.chat import ChatManager
|
from core.chat import ChatManager
|
||||||
from core.auth import AuthManager
|
from core.auth import AuthManager
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import pytest
|
||||||
from core.settings import SettingsManager
|
from core.settings import SettingsManager
|
||||||
from core.store import InMemoryStore
|
from core.store import InMemoryStore
|
||||||
from core.protocol import SettingsAction
|
from core.protocol import SettingsAction
|
||||||
from platform.mock import MockPlatformClient
|
from sdk.mock import MockPlatformClient
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from core.store import InMemoryStore
|
||||||
from core.auth import AuthManager
|
from core.auth import AuthManager
|
||||||
from core.chat import ChatManager
|
from core.chat import ChatManager
|
||||||
from core.settings import SettingsManager
|
from core.settings import SettingsManager
|
||||||
from platform.mock import MockPlatformClient
|
from sdk.mock import MockPlatformClient
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# tests/platform/test_mock.py
|
# tests/platform/test_mock.py
|
||||||
from platform.mock import MockPlatformClient
|
from sdk.mock import MockPlatformClient
|
||||||
from platform.interface import User, MessageResponse, UserSettings
|
from sdk.interface import User, MessageResponse, UserSettings
|
||||||
from core.protocol import SettingsAction
|
from core.protocol import SettingsAction
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue