Обновлен Python до версии 3.11.9 и изменены зависимости в Dockerfile и pyproject.toml. Удалены устаревшие файлы RATE_LIMITING_SOLUTION.md и тесты для rate limiting.
Обновлены пути к библиотекам в Dockerfile для соответствия новой версии Python. Исправлены все тесты, теперь все проходят
This commit is contained in:
88
.cursor/rules/architecture.md
Normal file
88
.cursor/rules/architecture.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
description: "Архитектурные паттерны и структура проекта Telegram бота на aiogram"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Архитектура проекта
|
||||
|
||||
Этот проект - Telegram бот на **aiogram 3.10.0** с четкой архитектурой и разделением ответственности.
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
helper_bot/
|
||||
├── handlers/ # Обработчики событий (admin, callback, group, private, voice)
|
||||
│ ├── services.py # Бизнес-логика для каждого модуля
|
||||
│ ├── exceptions.py # Кастомные исключения модуля
|
||||
│ └── dependencies.py # Dependency injection для модуля
|
||||
├── middlewares/ # Middleware для cross-cutting concerns
|
||||
├── utils/ # Утилиты и вспомогательные функции
|
||||
├── keyboards/ # Клавиатуры для бота
|
||||
└── filters/ # Кастомные фильтры
|
||||
|
||||
database/
|
||||
├── repositories/ # Репозитории для работы с БД (Repository pattern)
|
||||
├── models.py # Модели данных
|
||||
├── base.py # Базовый класс DatabaseConnection
|
||||
└── async_db.py # AsyncBotDB - основной интерфейс к БД
|
||||
```
|
||||
|
||||
## Архитектурные паттерны
|
||||
|
||||
### 1. Repository Pattern
|
||||
- Все операции с БД выполняются через репозитории в `database/repositories/`
|
||||
- Каждая сущность имеет свой репозиторий (UserRepository, PostRepository, etc.)
|
||||
- Репозитории наследуются от `DatabaseConnection` из `database/base.py`
|
||||
- Используется `RepositoryFactory` для создания репозиториев
|
||||
|
||||
### 2. Service Layer Pattern
|
||||
- Бизнес-логика вынесена в сервисы (`handlers/*/services.py`)
|
||||
- Handlers только обрабатывают события и вызывают сервисы
|
||||
- Сервисы работают с репозиториями через `AsyncBotDB`
|
||||
|
||||
### 3. Dependency Injection
|
||||
- Используется `BaseDependencyFactory` для управления зависимостями
|
||||
- Глобальный экземпляр доступен через `get_global_instance()`
|
||||
- Зависимости внедряются через `DependenciesMiddleware`
|
||||
- Для каждого модуля handlers может быть свой `dependencies.py` с фабриками
|
||||
|
||||
### 4. Middleware Pattern
|
||||
- Middleware регистрируются в `main.py` на уровне dispatcher
|
||||
- Порядок регистрации важен: DependenciesMiddleware → MetricsMiddleware → BlacklistMiddleware → RateLimitMiddleware
|
||||
- Middleware обрабатывают cross-cutting concerns (логирование, метрики, rate limiting)
|
||||
|
||||
## Принципы
|
||||
|
||||
1. **Разделение ответственности**: Handlers → Services → Repositories
|
||||
2. **Асинхронность**: Все операции с БД и API асинхронные
|
||||
3. **Типизация**: Используются type hints везде, где возможно
|
||||
4. **Логирование**: Всегда через `logs.custom_logger.logger`
|
||||
5. **Метрики**: Декораторы `@track_time`, `@track_errors`, `@db_query_time` для мониторинга
|
||||
|
||||
## Версия Python
|
||||
|
||||
Проект использует **Python 3.11.9** во всех окружениях:
|
||||
- Локальная разработка: Python 3.11.9 (указана в `.python-version`)
|
||||
- Docker (production): Python 3.11.9-alpine (указана в `Dockerfile`)
|
||||
- Минимальная версия: Python 3.11 (указана в `pyproject.toml`)
|
||||
|
||||
**Важно:**
|
||||
- При написании кода можно использовать фичи Python 3.11
|
||||
- Доступны улучшенные type hints, match/case (Python 3.10+)
|
||||
- Используйте type hints везде, где возможно
|
||||
- `@dataclass` доступен (Python 3.7+)
|
||||
|
||||
**Структура проекта:**
|
||||
- Docker файлы находятся в двух местах:
|
||||
- `/prod/Dockerfile` - для инфраструктуры (Python 3.11.9-alpine)
|
||||
- `/prod/bots/telegram-helper-bot/Dockerfile` - для бота (Python 3.11.9-alpine)
|
||||
- При обновлении версии Python нужно обновить оба Dockerfile
|
||||
|
||||
**Для локальной разработки:**
|
||||
Рекомендуется использовать `pyenv` для установки Python 3.11.9:
|
||||
```bash
|
||||
pyenv install 3.11.9
|
||||
pyenv local 3.11.9
|
||||
```
|
||||
|
||||
Подробнее см. `docs/PYTHON_VERSION_MANAGEMENT.md`
|
||||
106
.cursor/rules/code-style.md
Normal file
106
.cursor/rules/code-style.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
description: "Стиль кода, соглашения по именованию и форматированию"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Стиль кода и соглашения
|
||||
|
||||
## Именование
|
||||
|
||||
### Классы
|
||||
- **PascalCase**: `UserRepository`, `AdminService`, `BaseDependencyFactory`
|
||||
- Имена классов должны быть существительными
|
||||
|
||||
### Функции и методы
|
||||
- **snake_case**: `get_user_info()`, `handle_message()`, `create_tables()`
|
||||
- Имена функций должны быть глаголами или начинаться с глагола
|
||||
|
||||
### Переменные
|
||||
- **snake_case**: `user_id`, `bot_db`, `settings`, `message_text`
|
||||
- Константы в **UPPER_SNAKE_CASE**: `FSM_STATES`, `ERROR_MESSAGES`
|
||||
|
||||
### Модули и пакеты
|
||||
- **snake_case**: `admin_handlers.py`, `user_repository.py`
|
||||
- Имена модулей должны быть короткими и понятными
|
||||
|
||||
## Импорты
|
||||
|
||||
Структура импортов (в порядке приоритета):
|
||||
|
||||
```python
|
||||
# 1. Standard library imports
|
||||
import os
|
||||
import asyncio
|
||||
from typing import Optional, List
|
||||
|
||||
# 2. Third-party imports
|
||||
from aiogram import Router, types
|
||||
from aiogram.filters import Command
|
||||
|
||||
# 3. Local imports - модули проекта
|
||||
from database.async_db import AsyncBotDB
|
||||
from helper_bot.handlers.admin.services import AdminService
|
||||
|
||||
# 4. Local imports - utilities
|
||||
from logs.custom_logger import logger
|
||||
|
||||
# 5. Local imports - metrics (если используются)
|
||||
from helper_bot.utils.metrics import track_time, track_errors
|
||||
```
|
||||
|
||||
## Type Hints
|
||||
|
||||
- Всегда используйте type hints для параметров функций и возвращаемых значений
|
||||
- Используйте `Optional[T]` для значений, которые могут быть `None`
|
||||
- Используйте `List[T]`, `Dict[K, V]` для коллекций
|
||||
- Используйте `Annotated` для dependency injection в aiogram
|
||||
|
||||
Пример:
|
||||
```python
|
||||
async def get_user_info(self, user_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Получение информации о пользователе."""
|
||||
...
|
||||
```
|
||||
|
||||
## Документация
|
||||
|
||||
### Docstrings
|
||||
- Используйте docstrings для всех классов и публичных методов
|
||||
- Формат: краткое описание в одну строку или многострочный с подробностями
|
||||
|
||||
```python
|
||||
async def add_user(self, user: User) -> None:
|
||||
"""Добавление нового пользователя с защитой от дублирования."""
|
||||
...
|
||||
```
|
||||
|
||||
### Комментарии
|
||||
- Комментарии на русском языке (как и весь код)
|
||||
- Используйте комментарии для объяснения "почему", а не "что"
|
||||
- Разделители секций: `# ============================================================================`
|
||||
|
||||
## Форматирование
|
||||
|
||||
- Используйте 4 пробела для отступов (не табы)
|
||||
- Максимальная длина строки: 100-120 символов (гибко)
|
||||
- Пустые строки между логическими блоками
|
||||
- Пустая строка перед `return` в конце функции (если функция не короткая)
|
||||
|
||||
## Структура файлов handlers
|
||||
|
||||
```python
|
||||
# 1. Импорты (по категориям)
|
||||
# 2. Создание роутера
|
||||
router = Router()
|
||||
|
||||
# 3. Регистрация middleware (если нужно)
|
||||
router.message.middleware(SomeMiddleware())
|
||||
|
||||
# 4. Handlers с декораторами
|
||||
@router.message(...)
|
||||
@track_time("handler_name", "module_name")
|
||||
@track_errors("module_name", "handler_name")
|
||||
async def handler_function(...):
|
||||
"""Описание handler."""
|
||||
...
|
||||
```
|
||||
117
.cursor/rules/database-patterns.md
Normal file
117
.cursor/rules/database-patterns.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
description: "Паттерны работы с базой данных, репозитории и модели"
|
||||
globs: ["database/**/*.py", "**/repositories/*.py"]
|
||||
---
|
||||
|
||||
# Паттерны работы с базой данных
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
Все операции с БД выполняются через репозитории. Каждый репозиторий:
|
||||
- Наследуется от `DatabaseConnection` из `database/base.py`
|
||||
- Работает с одной сущностью (User, Post, Blacklist, etc.)
|
||||
- Содержит методы для CRUD операций
|
||||
- Использует асинхронные методы `_execute_query()` и `_execute_query_with_result()`
|
||||
|
||||
### Структура репозитория
|
||||
|
||||
```python
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import User
|
||||
|
||||
class UserRepository(DatabaseConnection):
|
||||
"""Репозиторий для работы с пользователями."""
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблицы пользователей."""
|
||||
query = '''
|
||||
CREATE TABLE IF NOT EXISTS our_users (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
...
|
||||
)
|
||||
'''
|
||||
await self._execute_query(query)
|
||||
self.logger.info("Таблица пользователей создана")
|
||||
|
||||
async def add_user(self, user: User) -> None:
|
||||
"""Добавление нового пользователя."""
|
||||
query = "INSERT OR IGNORE INTO our_users (...) VALUES (...)"
|
||||
params = (...)
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Пользователь добавлен: {user.user_id}")
|
||||
|
||||
async def get_user_by_id(self, user_id: int) -> Optional[User]:
|
||||
"""Получение пользователя по ID."""
|
||||
query = "SELECT * FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
# Преобразование row в модель User
|
||||
...
|
||||
```
|
||||
|
||||
## Модели данных
|
||||
|
||||
- Модели определены в `database/models.py`
|
||||
- Используются dataclasses или простые классы
|
||||
- Модели передаются между слоями (Repository → Service → Handler)
|
||||
|
||||
## Работа с БД
|
||||
|
||||
### AsyncBotDB
|
||||
- Основной интерфейс для работы с БД
|
||||
- Использует `RepositoryFactory` для доступа к репозиториям
|
||||
- Методы делегируют вызовы соответствующим репозиториям
|
||||
|
||||
### DatabaseConnection
|
||||
- Базовый класс для всех репозиториев
|
||||
- Предоставляет методы:
|
||||
- `_get_connection()` - получение соединения
|
||||
- `_execute_query()` - выполнение запроса без результата
|
||||
- `_execute_query_with_result()` - выполнение запроса с результатом
|
||||
- Автоматически управляет соединениями (открытие/закрытие)
|
||||
- Настраивает PRAGMA для оптимизации SQLite
|
||||
|
||||
### Важные правила
|
||||
|
||||
1. **Всегда используйте параметризованные запросы** для защиты от SQL injection:
|
||||
```python
|
||||
# ✅ Правильно
|
||||
query = "SELECT * FROM users WHERE user_id = ?"
|
||||
await self._execute_query_with_result(query, (user_id,))
|
||||
|
||||
# ❌ Неправильно
|
||||
query = f"SELECT * FROM users WHERE user_id = {user_id}"
|
||||
```
|
||||
|
||||
2. **Логируйте важные операции**:
|
||||
```python
|
||||
self.logger.info(f"Пользователь добавлен: {user_id}")
|
||||
self.logger.error(f"Ошибка при добавлении пользователя: {e}")
|
||||
```
|
||||
|
||||
3. **Используйте транзакции** для множественных операций (если нужно):
|
||||
```python
|
||||
async with await self._get_connection() as conn:
|
||||
await conn.execute(...)
|
||||
await conn.execute(...)
|
||||
await conn.commit()
|
||||
```
|
||||
|
||||
4. **Обрабатывайте None** при получении данных:
|
||||
```python
|
||||
rows = await self._execute_query_with_result(query, params)
|
||||
if not rows:
|
||||
return None
|
||||
row = rows[0]
|
||||
```
|
||||
|
||||
## RepositoryFactory
|
||||
|
||||
- Создает и кэширует экземпляры репозиториев
|
||||
- Доступ через свойства: `factory.users`, `factory.posts`, etc.
|
||||
- Используется в `AsyncBotDB` для доступа к репозиториям
|
||||
|
||||
## Миграции
|
||||
|
||||
- SQL миграции в `database/schema.sql`
|
||||
- Python скрипты для миграций в `scripts/`
|
||||
- Всегда проверяйте существование таблиц перед созданием: `CREATE TABLE IF NOT EXISTS`
|
||||
172
.cursor/rules/dependencies-and-utils.md
Normal file
172
.cursor/rules/dependencies-and-utils.md
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
description: "Работа с зависимостями, утилитами, метриками и внешними сервисами"
|
||||
globs: ["helper_bot/utils/**/*.py", "helper_bot/config/**/*.py"]
|
||||
---
|
||||
|
||||
# Зависимости и утилиты
|
||||
|
||||
## BaseDependencyFactory
|
||||
|
||||
Центральный класс для управления зависимостями проекта.
|
||||
|
||||
### Использование
|
||||
|
||||
```python
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
|
||||
# Получение глобального экземпляра
|
||||
bdf = get_global_instance()
|
||||
|
||||
# Доступ к зависимостям
|
||||
db = bdf.get_db() # AsyncBotDB
|
||||
settings = bdf.get_settings() # dict с настройками
|
||||
s3_storage = bdf.get_s3_storage() # S3StorageService или None
|
||||
```
|
||||
|
||||
### Структура settings
|
||||
|
||||
Настройки загружаются из `.env` и структурированы:
|
||||
|
||||
```python
|
||||
settings = {
|
||||
'Telegram': {
|
||||
'bot_token': str,
|
||||
'listen_bot_token': str,
|
||||
'preview_link': bool,
|
||||
'main_public': str,
|
||||
'group_for_posts': int,
|
||||
'important_logs': int,
|
||||
...
|
||||
},
|
||||
'Settings': {
|
||||
'logs': bool,
|
||||
'test': bool
|
||||
},
|
||||
'Metrics': {
|
||||
'host': str,
|
||||
'port': int
|
||||
},
|
||||
'S3': {
|
||||
'enabled': bool,
|
||||
'endpoint_url': str,
|
||||
'access_key': str,
|
||||
'secret_key': str,
|
||||
'bucket_name': str,
|
||||
'region': str
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Метрики
|
||||
|
||||
### Декораторы метрик
|
||||
|
||||
Используйте декораторы из `helper_bot.utils.metrics`:
|
||||
|
||||
```python
|
||||
from helper_bot.utils.metrics import track_time, track_errors, db_query_time
|
||||
|
||||
@track_time("method_name", "module_name")
|
||||
@track_errors("module_name", "method_name")
|
||||
async def some_method():
|
||||
"""Метод с отслеживанием времени и ошибок."""
|
||||
...
|
||||
|
||||
@db_query_time("method_name", "table_name", "operation")
|
||||
async def db_method():
|
||||
"""Метод БД с отслеживанием времени запросов."""
|
||||
...
|
||||
```
|
||||
|
||||
### Доступ к метрикам
|
||||
|
||||
```python
|
||||
from helper_bot.utils.metrics import metrics
|
||||
|
||||
# Метрики доступны через Prometheus на порту из settings['Metrics']['port']
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### RateLimiter
|
||||
|
||||
Используется для ограничения частоты запросов:
|
||||
|
||||
```python
|
||||
from helper_bot.utils.rate_limiter import RateLimiter
|
||||
|
||||
limiter = RateLimiter(...)
|
||||
if await limiter.is_allowed(user_id):
|
||||
# Разрешить действие
|
||||
...
|
||||
else:
|
||||
# Отклонить действие
|
||||
...
|
||||
```
|
||||
|
||||
### RateLimitMiddleware
|
||||
|
||||
Автоматически применяет rate limiting ко всем запросам через middleware.
|
||||
|
||||
## S3 Storage
|
||||
|
||||
### S3StorageService
|
||||
|
||||
Используется для хранения медиафайлов:
|
||||
|
||||
```python
|
||||
from helper_bot.utils.s3_storage import S3StorageService
|
||||
|
||||
# Получение через BaseDependencyFactory
|
||||
s3_storage = bdf.get_s3_storage()
|
||||
|
||||
if s3_storage:
|
||||
# Загрузка файла
|
||||
url = await s3_storage.upload_file(file_path, object_key)
|
||||
|
||||
# Удаление файла
|
||||
await s3_storage.delete_file(object_key)
|
||||
```
|
||||
|
||||
### Проверка доступности
|
||||
|
||||
Всегда проверяйте, что S3 включен:
|
||||
|
||||
```python
|
||||
s3_storage = bdf.get_s3_storage()
|
||||
if s3_storage:
|
||||
# Работа с S3
|
||||
...
|
||||
else:
|
||||
# Fallback логика
|
||||
...
|
||||
```
|
||||
|
||||
## Утилиты
|
||||
|
||||
### helper_func.py
|
||||
|
||||
Содержит вспомогательные функции для работы с:
|
||||
- Датами и временем
|
||||
- Форматированием данных
|
||||
- Валидацией
|
||||
- Преобразованием данных
|
||||
|
||||
Используйте эти функции вместо дублирования логики.
|
||||
|
||||
## Конфигурация
|
||||
|
||||
### rate_limit_config.py
|
||||
|
||||
Конфигурация rate limiting находится в `helper_bot/config/rate_limit_config.py`.
|
||||
|
||||
Используйте конфигурацию вместо хардкода значений.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Всегда получайте зависимости через BaseDependencyFactory** - не создавайте экземпляры напрямую
|
||||
2. **Используйте декораторы метрик** для всех важных методов
|
||||
3. **Проверяйте доступность внешних сервисов** (S3) перед использованием
|
||||
4. **Используйте утилиты** из `helper_func.py` вместо дублирования кода
|
||||
5. **Читайте настройки из settings** вместо хардкода значений
|
||||
6. **Логируйте важные операции** с внешними сервисами
|
||||
156
.cursor/rules/error-handling.md
Normal file
156
.cursor/rules/error-handling.md
Normal file
@@ -0,0 +1,156 @@
|
||||
---
|
||||
description: "Обработка ошибок, исключения и логирование"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Обработка ошибок и исключений
|
||||
|
||||
## Иерархия исключений
|
||||
|
||||
Каждый модуль должен иметь свой файл `exceptions.py` с иерархией исключений:
|
||||
|
||||
```python
|
||||
# Базовое исключение модуля
|
||||
class ModuleError(Exception):
|
||||
"""Базовое исключение для модуля"""
|
||||
pass
|
||||
|
||||
# Специализированные исключения
|
||||
class UserNotFoundError(ModuleError):
|
||||
"""Исключение при отсутствии пользователя"""
|
||||
pass
|
||||
|
||||
class InvalidInputError(ModuleError):
|
||||
"""Исключение при некорректном вводе данных"""
|
||||
pass
|
||||
```
|
||||
|
||||
## Обработка в Handlers
|
||||
|
||||
### Паттерн try-except
|
||||
|
||||
```python
|
||||
@router.message(...)
|
||||
async def handler(message: types.Message, state: FSMContext, **kwargs):
|
||||
try:
|
||||
# Основная логика
|
||||
service = SomeService(bot_db, settings)
|
||||
result = await service.do_something()
|
||||
await message.answer("Успех")
|
||||
except UserNotFoundError as e:
|
||||
# Обработка специфичной ошибки
|
||||
await message.answer(f"Пользователь не найден: {str(e)}")
|
||||
logger.warning(f"Пользователь не найден: {e}")
|
||||
except InvalidInputError as e:
|
||||
# Обработка ошибки валидации
|
||||
await message.answer(f"Некорректный ввод: {str(e)}")
|
||||
logger.warning(f"Некорректный ввод: {e}")
|
||||
except Exception as e:
|
||||
# Обработка неожиданных ошибок
|
||||
await handle_error(message, e, state)
|
||||
logger.error(f"Неожиданная ошибка в handler: {e}", exc_info=True)
|
||||
```
|
||||
|
||||
### Декоратор error_handler
|
||||
|
||||
Некоторые модули используют декоратор `@error_handler` для автоматической обработки:
|
||||
|
||||
```python
|
||||
from .decorators import error_handler
|
||||
|
||||
@error_handler
|
||||
@router.message(...)
|
||||
async def handler(...):
|
||||
# Код handler
|
||||
# Ошибки автоматически логируются и отправляются в important_logs
|
||||
...
|
||||
```
|
||||
|
||||
## Обработка в Services
|
||||
|
||||
```python
|
||||
class SomeService:
|
||||
async def do_something(self):
|
||||
try:
|
||||
# Бизнес-логика
|
||||
data = await self.bot_db.get_data()
|
||||
if not data:
|
||||
raise UserNotFoundError("Пользователь не найден")
|
||||
return self._process(data)
|
||||
except UserNotFoundError:
|
||||
# Пробрасываем специфичные исключения дальше
|
||||
raise
|
||||
except Exception as e:
|
||||
# Логируем и пробрасываем неожиданные ошибки
|
||||
logger.error(f"Ошибка в сервисе: {e}", exc_info=True)
|
||||
raise
|
||||
```
|
||||
|
||||
## Логирование
|
||||
|
||||
### Использование logger
|
||||
|
||||
Всегда используйте `logs.custom_logger.logger`:
|
||||
|
||||
```python
|
||||
from logs.custom_logger import logger
|
||||
|
||||
# Информационные сообщения
|
||||
logger.info(f"Пользователь {user_id} выполнил действие")
|
||||
|
||||
# Предупреждения
|
||||
logger.warning(f"Попытка доступа к несуществующему ресурсу: {resource_id}")
|
||||
|
||||
# Ошибки
|
||||
logger.error(f"Ошибка при выполнении операции: {e}")
|
||||
|
||||
# Ошибки с traceback
|
||||
logger.error(f"Критическая ошибка: {e}", exc_info=True)
|
||||
```
|
||||
|
||||
### Уровни логирования
|
||||
|
||||
- `logger.debug()` - отладочная информация
|
||||
- `logger.info()` - информационные сообщения о работе
|
||||
- `logger.warning()` - предупреждения о потенциальных проблемах
|
||||
- `logger.error()` - ошибки, требующие внимания
|
||||
- `logger.critical()` - критические ошибки
|
||||
|
||||
## Метрики ошибок
|
||||
|
||||
Декоратор `@track_errors` автоматически отслеживает ошибки:
|
||||
|
||||
```python
|
||||
@track_errors("module_name", "method_name")
|
||||
async def some_method():
|
||||
# Ошибки автоматически записываются в метрики
|
||||
...
|
||||
```
|
||||
|
||||
## Централизованная обработка
|
||||
|
||||
### В admin handlers
|
||||
|
||||
Используется функция `handle_admin_error()`:
|
||||
|
||||
```python
|
||||
from helper_bot.handlers.admin.utils import handle_admin_error
|
||||
|
||||
try:
|
||||
# Код
|
||||
except Exception as e:
|
||||
await handle_admin_error(message, e, state, "context_name")
|
||||
```
|
||||
|
||||
### В других модулях
|
||||
|
||||
Создавайте аналогичные утилиты для централизованной обработки ошибок модуля.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Всегда логируйте ошибки** перед пробросом или обработкой
|
||||
2. **Используйте специфичные исключения** вместо общих `Exception`
|
||||
3. **Пробрасывайте исключения** из сервисов в handlers для обработки
|
||||
4. **Не глотайте исключения** без логирования
|
||||
5. **Используйте `exc_info=True`** для логирования traceback критических ошибок
|
||||
6. **Обрабатывайте ошибки на правильном уровне**: бизнес-логика в сервисах, пользовательские сообщения в handlers
|
||||
215
.cursor/rules/handlers-patterns.md
Normal file
215
.cursor/rules/handlers-patterns.md
Normal file
@@ -0,0 +1,215 @@
|
||||
---
|
||||
description: "Паттерны для создания handlers, services и обработки событий aiogram"
|
||||
globs: ["helper_bot/handlers/**/*.py"]
|
||||
---
|
||||
|
||||
# Паттерны для Handlers
|
||||
|
||||
## Структура модуля handler
|
||||
|
||||
Каждый модуль handler (admin, callback, group, private, voice) должен содержать:
|
||||
|
||||
```
|
||||
handlers/{module}/
|
||||
├── __init__.py # Экспорт router
|
||||
├── {module}_handlers.py # Основные handlers
|
||||
├── services.py # Бизнес-логика
|
||||
├── exceptions.py # Кастомные исключения
|
||||
├── dependencies.py # Dependency injection (опционально)
|
||||
├── constants.py # Константы (FSM states, messages)
|
||||
└── utils.py # Вспомогательные функции (опционально)
|
||||
```
|
||||
|
||||
## Создание Router
|
||||
|
||||
```python
|
||||
from aiogram import Router
|
||||
|
||||
# Создаем роутер
|
||||
router = Router()
|
||||
|
||||
# Регистрируем middleware (если нужно)
|
||||
router.message.middleware(SomeMiddleware())
|
||||
|
||||
# Экспортируем в __init__.py
|
||||
# from .{module}_handlers import router
|
||||
```
|
||||
|
||||
## Структура Handler
|
||||
|
||||
```python
|
||||
from aiogram import Router, types
|
||||
from aiogram.filters import Command, StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
from logs.custom_logger import logger
|
||||
from helper_bot.utils.metrics import track_time, track_errors
|
||||
|
||||
router = Router()
|
||||
|
||||
@router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command('command_name')
|
||||
)
|
||||
@track_time("handler_name", "module_name")
|
||||
@track_errors("module_name", "handler_name")
|
||||
async def handler_function(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: AsyncBotDB, # Из DependenciesMiddleware
|
||||
settings: dict, # Из DependenciesMiddleware
|
||||
**kwargs
|
||||
):
|
||||
"""Описание handler."""
|
||||
try:
|
||||
# Логирование
|
||||
logger.info(f"Обработка команды от пользователя: {message.from_user.id}")
|
||||
|
||||
# Получение данных из state (если нужно)
|
||||
data = await state.get_data()
|
||||
|
||||
# Вызов сервиса для бизнес-логики
|
||||
service = SomeService(bot_db, settings)
|
||||
result = await service.do_something()
|
||||
|
||||
# Ответ пользователю
|
||||
await message.answer("Результат")
|
||||
|
||||
# Обновление state (если нужно)
|
||||
await state.set_state("NEW_STATE")
|
||||
|
||||
except SomeCustomException as e:
|
||||
# Обработка кастомных исключений
|
||||
await message.answer(f"Ошибка: {str(e)}")
|
||||
logger.error(f"Ошибка в handler: {e}")
|
||||
except Exception as e:
|
||||
# Обработка общих ошибок
|
||||
await handle_error(message, e, state)
|
||||
logger.error(f"Неожиданная ошибка: {e}")
|
||||
```
|
||||
|
||||
## Service Layer
|
||||
|
||||
Бизнес-логика выносится в сервисы:
|
||||
|
||||
```python
|
||||
from logs.custom_logger import logger
|
||||
from helper_bot.utils.metrics import track_time, track_errors
|
||||
|
||||
class SomeService:
|
||||
"""Сервис для работы с ..."""
|
||||
|
||||
def __init__(self, bot_db: AsyncBotDB, settings: dict):
|
||||
self.bot_db = bot_db
|
||||
self.settings = settings
|
||||
|
||||
@track_time("method_name", "service_name")
|
||||
@track_errors("service_name", "method_name")
|
||||
async def do_something(self) -> SomeResult:
|
||||
"""Описание метода."""
|
||||
try:
|
||||
# Работа с БД через bot_db
|
||||
data = await self.bot_db.some_method()
|
||||
|
||||
# Бизнес-логика
|
||||
result = self._process_data(data)
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в сервисе: {e}")
|
||||
raise
|
||||
```
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
### Через DependenciesMiddleware (глобально)
|
||||
- `bot_db: AsyncBotDB` - доступен во всех handlers
|
||||
- `settings: dict` - настройки из .env
|
||||
- `bot: Bot` - экземпляр бота
|
||||
- `dp: Dispatcher` - dispatcher
|
||||
|
||||
### Через MagicData (локально)
|
||||
```python
|
||||
from aiogram.filters import MagicData
|
||||
from typing import Annotated
|
||||
from helper_bot.handlers.admin.dependencies import BotDB, Settings
|
||||
|
||||
@router.message(
|
||||
Command('admin'),
|
||||
MagicData(bot_db=BotDB, settings=Settings)
|
||||
)
|
||||
async def handler(
|
||||
message: types.Message,
|
||||
bot_db: Annotated[AsyncBotDB, BotDB],
|
||||
settings: Annotated[dict, Settings]
|
||||
):
|
||||
...
|
||||
```
|
||||
|
||||
### Через фабрики (для сервисов)
|
||||
```python
|
||||
# В dependencies.py
|
||||
def get_some_service() -> SomeService:
|
||||
"""Фабрика для SomeService"""
|
||||
bdf = get_global_instance()
|
||||
db = bdf.get_db()
|
||||
settings = bdf.settings
|
||||
return SomeService(db, settings)
|
||||
|
||||
# В handlers
|
||||
@router.message(Command('cmd'))
|
||||
async def handler(
|
||||
message: types.Message,
|
||||
service: Annotated[SomeService, get_some_service()]
|
||||
):
|
||||
...
|
||||
```
|
||||
|
||||
## FSM (Finite State Machine)
|
||||
|
||||
```python
|
||||
# Определение состояний в constants.py
|
||||
FSM_STATES = {
|
||||
"ADMIN": "ADMIN",
|
||||
"AWAIT_INPUT": "AWAIT_INPUT",
|
||||
...
|
||||
}
|
||||
|
||||
# Установка состояния
|
||||
await state.set_state(FSM_STATES["ADMIN"])
|
||||
|
||||
# Получение состояния
|
||||
current_state = await state.get_state()
|
||||
|
||||
# Сохранение данных
|
||||
await state.update_data(key=value)
|
||||
|
||||
# Получение данных
|
||||
data = await state.get_data()
|
||||
value = data.get("key")
|
||||
|
||||
# Очистка состояния
|
||||
await state.clear()
|
||||
```
|
||||
|
||||
## Фильтры
|
||||
|
||||
Используйте кастомные фильтры из `helper_bot.filters.main`:
|
||||
- `ChatTypeFilter` - фильтр по типу чата (private, group, supergroup)
|
||||
|
||||
## Декораторы для метрик
|
||||
|
||||
Всегда добавляйте декораторы метрик к handlers и методам сервисов:
|
||||
|
||||
```python
|
||||
@track_time("handler_name", "module_name") # Измерение времени выполнения
|
||||
@track_errors("module_name", "handler_name") # Отслеживание ошибок
|
||||
@db_query_time("method_name", "table_name", "operation") # Для БД операций
|
||||
```
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
- Используйте кастомные исключения из `exceptions.py`
|
||||
- Обрабатывайте исключения в handlers
|
||||
- Логируйте все ошибки через `logger.error()`
|
||||
- Используйте декоратор `@error_handler` для автоматической обработки (если есть)
|
||||
109
.cursor/rules/middleware-patterns.md
Normal file
109
.cursor/rules/middleware-patterns.md
Normal file
@@ -0,0 +1,109 @@
|
||||
---
|
||||
description: "Паттерны создания и использования middleware в aiogram"
|
||||
globs: ["helper_bot/middlewares/**/*.py"]
|
||||
---
|
||||
|
||||
# Паттерны Middleware
|
||||
|
||||
## Структура Middleware
|
||||
|
||||
Все middleware наследуются от `aiogram.BaseMiddleware`:
|
||||
|
||||
```python
|
||||
from typing import Any, Dict
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import TelegramObject
|
||||
|
||||
class CustomMiddleware(BaseMiddleware):
|
||||
"""Описание middleware."""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable,
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any]
|
||||
) -> Any:
|
||||
# Логика до обработки handler
|
||||
...
|
||||
|
||||
# Вызов следующего handler в цепочке
|
||||
result = await handler(event, data)
|
||||
|
||||
# Логика после обработки handler
|
||||
...
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
## Порядок регистрации Middleware
|
||||
|
||||
В `main.py` middleware регистрируются в следующем порядке (важно!):
|
||||
|
||||
```python
|
||||
# 1. DependenciesMiddleware - внедрение зависимостей
|
||||
dp.update.outer_middleware(DependenciesMiddleware())
|
||||
|
||||
# 2. MetricsMiddleware - сбор метрик
|
||||
dp.update.outer_middleware(MetricsMiddleware())
|
||||
|
||||
# 3. BlacklistMiddleware - проверка черного списка
|
||||
dp.update.outer_middleware(BlacklistMiddleware())
|
||||
|
||||
# 4. RateLimitMiddleware - ограничение частоты запросов
|
||||
dp.update.outer_middleware(RateLimitMiddleware())
|
||||
```
|
||||
|
||||
## DependenciesMiddleware
|
||||
|
||||
Внедряет глобальные зависимости во все handlers:
|
||||
|
||||
```python
|
||||
class DependenciesMiddleware(BaseMiddleware):
|
||||
async def __call__(self, handler, event: TelegramObject, data: Dict[str, Any]) -> Any:
|
||||
bdf = get_global_instance()
|
||||
|
||||
# Внедрение зависимостей
|
||||
if 'bot_db' not in data:
|
||||
data['bot_db'] = bdf.get_db()
|
||||
if 'settings' not in data:
|
||||
data['settings'] = bdf.settings
|
||||
|
||||
return await handler(event, data)
|
||||
```
|
||||
|
||||
## Обработка ошибок в Middleware
|
||||
|
||||
```python
|
||||
class CustomMiddleware(BaseMiddleware):
|
||||
async def __call__(self, handler, event, data):
|
||||
try:
|
||||
# Предобработка
|
||||
...
|
||||
result = await handler(event, data)
|
||||
# Постобработка
|
||||
...
|
||||
return result
|
||||
except Exception as e:
|
||||
# Обработка ошибок
|
||||
logger.error(f"Ошибка в middleware: {e}")
|
||||
# Решаем: пробрасывать дальше или обработать
|
||||
raise
|
||||
```
|
||||
|
||||
## Регистрация на уровне Router
|
||||
|
||||
Middleware можно регистрировать на уровне конкретного router:
|
||||
|
||||
```python
|
||||
router = Router()
|
||||
router.message.middleware(SomeMiddleware()) # Только для message handlers
|
||||
router.callback_query.middleware(SomeMiddleware()) # Только для callback handlers
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Регистрируйте middleware в правильном порядке** - зависимости должны быть первыми
|
||||
2. **Не изменяйте event** напрямую, используйте `data` для передачи информации
|
||||
3. **Обрабатывайте ошибки** в middleware, но не глотайте их без логирования
|
||||
4. **Используйте `outer_middleware`** для глобальной регистрации
|
||||
5. **Используйте `router.middleware()`** для локальной регистрации на уровне модуля
|
||||
197
.cursor/rules/testing.md
Normal file
197
.cursor/rules/testing.md
Normal file
@@ -0,0 +1,197 @@
|
||||
---
|
||||
description: "Паттерны тестирования, структура тестов и использование pytest"
|
||||
globs: ["tests/**/*.py", "test_*.py"]
|
||||
---
|
||||
|
||||
# Паттерны тестирования
|
||||
|
||||
## Структура тестов
|
||||
|
||||
Тесты находятся в директории `tests/` и используют pytest:
|
||||
|
||||
```
|
||||
tests/
|
||||
├── conftest.py # Общие фикстуры
|
||||
├── conftest_*.py # Специализированные фикстуры
|
||||
├── mocks.py # Моки и заглушки
|
||||
└── test_*.py # Тестовые файлы
|
||||
```
|
||||
|
||||
## Конфигурация pytest
|
||||
|
||||
Настройки в `pyproject.toml`:
|
||||
- `asyncio-mode=auto` - автоматический режим для async тестов
|
||||
- Маркеры: `asyncio`, `slow`, `integration`, `unit`
|
||||
- Фильтрация предупреждений
|
||||
|
||||
## Структура теста
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from database.async_db import AsyncBotDB
|
||||
from database.repositories.user_repository import UserRepository
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_repository_add_user(db_path):
|
||||
"""Тест добавления пользователя."""
|
||||
# Arrange
|
||||
repo = UserRepository(db_path)
|
||||
user = User(user_id=123, full_name="Test User")
|
||||
|
||||
# Act
|
||||
await repo.add_user(user)
|
||||
|
||||
# Assert
|
||||
result = await repo.get_user_by_id(123)
|
||||
assert result is not None
|
||||
assert result.full_name == "Test User"
|
||||
```
|
||||
|
||||
## Фикстуры
|
||||
|
||||
### Общие фикстуры (conftest.py)
|
||||
|
||||
```python
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
@pytest.fixture
|
||||
def db_path():
|
||||
"""Создает временный файл БД для тестов."""
|
||||
fd, path = tempfile.mkstemp(suffix='.db')
|
||||
os.close(fd)
|
||||
yield path
|
||||
os.unlink(path)
|
||||
|
||||
@pytest.fixture
|
||||
async def async_db(db_path):
|
||||
"""Создает AsyncBotDB для тестов."""
|
||||
db = AsyncBotDB(db_path)
|
||||
await db.create_tables()
|
||||
yield db
|
||||
```
|
||||
|
||||
### Использование фикстур
|
||||
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_something(async_db):
|
||||
# async_db уже инициализирован
|
||||
result = await async_db.some_method()
|
||||
assert result is not None
|
||||
```
|
||||
|
||||
## Моки
|
||||
|
||||
Используйте `mocks.py` для общих моков:
|
||||
|
||||
```python
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
def mock_bot():
|
||||
"""Создает мок бота."""
|
||||
bot = MagicMock()
|
||||
bot.send_message = AsyncMock()
|
||||
return bot
|
||||
```
|
||||
|
||||
## Маркеры
|
||||
|
||||
Используйте маркеры для категоризации тестов:
|
||||
|
||||
```python
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_unit_test():
|
||||
"""Быстрый unit тест."""
|
||||
...
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_integration_test():
|
||||
"""Медленный integration тест."""
|
||||
...
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.asyncio
|
||||
async def test_slow_test():
|
||||
"""Медленный тест."""
|
||||
...
|
||||
```
|
||||
|
||||
Запуск с фильтрацией:
|
||||
```bash
|
||||
pytest -m "not slow" # Пропустить медленные тесты
|
||||
pytest -m unit # Только unit тесты
|
||||
```
|
||||
|
||||
## Тестирование Handlers
|
||||
|
||||
```python
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handler(mock_bot, mock_message):
|
||||
"""Тест handler."""
|
||||
# Arrange
|
||||
dp = Dispatcher(storage=MemoryStorage())
|
||||
# Регистрация handler
|
||||
...
|
||||
|
||||
# Act
|
||||
await dp.feed_update(mock_update)
|
||||
|
||||
# Assert
|
||||
mock_bot.send_message.assert_called_once()
|
||||
```
|
||||
|
||||
## Тестирование Services
|
||||
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_service_method(async_db):
|
||||
"""Тест метода сервиса."""
|
||||
service = SomeService(async_db, {})
|
||||
result = await service.do_something()
|
||||
assert result is not None
|
||||
```
|
||||
|
||||
## Тестирование Repositories
|
||||
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_repository_crud(db_path):
|
||||
"""Тест CRUD операций репозитория."""
|
||||
repo = SomeRepository(db_path)
|
||||
await repo.create_tables()
|
||||
|
||||
# Create
|
||||
entity = SomeEntity(...)
|
||||
await repo.add(entity)
|
||||
|
||||
# Read
|
||||
result = await repo.get_by_id(entity.id)
|
||||
assert result is not None
|
||||
|
||||
# Update
|
||||
entity.field = "new_value"
|
||||
await repo.update(entity)
|
||||
|
||||
# Delete
|
||||
await repo.delete(entity.id)
|
||||
result = await repo.get_by_id(entity.id)
|
||||
assert result is None
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Используйте фикстуры** для переиспользования setup/teardown
|
||||
2. **Изолируйте тесты** - каждый тест должен быть независимым
|
||||
3. **Используйте временные БД** для тестов репозиториев
|
||||
4. **Мокируйте внешние зависимости** (API, файловая система)
|
||||
5. **Пишите понятные имена тестов** - они должны описывать что тестируется
|
||||
6. **Используйте Arrange-Act-Assert** паттерн
|
||||
7. **Тестируйте граничные случаи** и ошибки
|
||||
Reference in New Issue
Block a user