ℹ️ Инфо
Для взаимодействия с платформой botx используется библиотека pybotx. В документации можно посмотреть примеры её использования. Перед прочтением данного туториала следует с ней ознакомиться.
Вне зависимости от решаемых ботами задач, во всех повторяется один и тот же код.
Чтобы разработчикам не приходилось писать базовый однообразный код для каждого нового бота, существует шаблонный проект - Async Box. Он задает структуру проекта и основной стек технологий. Это значительно упрощает разработку, позволяя сконцентрироваться на реализации бота.
Для развертывания проекта необходимо установить cookiecutter и выполнить команду:
$ cookiecutter git@github.com:ExpressApp/async-box.git
Структура шаблонного бота состоит из нескольких следующих пакетов и модулей:
.
├── app
│ ├── api - реализация http роутов для приложения, включая необходимые для бота
│ ├── bot - команды бота и вспомогательные функции для них
│ ├── caching - классы и функции для работы с in-memory БД
│ ├── db - модели, функции для работы с БД и миграции
│ ├── resources - текстовые или файловые ресурсы бота
│ ├── schemas - сериализаторы, енамы, доменные модели
│ ├── services - сервисы с логикой (бизнес-логика)
│ ├── logger.py - логгер
│ ├── main.py - запуск сервера с инициализацией необходимых сервисов
│ └── settings.py - настройки приложения
├── scripts - скрипты для запуска тестов, форматеров, линтеров
├── tests - тесты, структура которых соответствует структуре проекта, и хелперы для них
├── cookiecutter.json - мета данные cookiecutter для автообновления шаблонного кода
├── poetry.lock - конфигурация текущих зависимостей. используется для их установки
├── pyproject.toml - конфигурация зависимостей, мета информация проекта (название, версия, авторы и т.п.)
└── setup.cfg - конфигурация линтеров и тестов
- Устанавливаем зависимости проекта через poetry:
$ poetry install
- Определяем переменные окружения в файле
.env
. Примеры переменных окружения находятся в файлеexample.env
. - Запускаем
postges
иredis
используя docker-compose:
$ docker-compose -f docker-compose.dev.yml up -d
- Применяем все миграции для инициализации таблиц с помощью alembic:
$ alembic upgrade head
- Запускаем бота как приложение FastAPI через uvicorn. Флаг
--reload
используется только при разработке для автоматического перезапуска сервера при изменениях в коде:
$ uvicorn app.main:app --reload
Команды бота находятся в пакете app.bot.commands
и группируются в отдельные модули в зависимости от логики. Команды добавляются с помощью коллекторов pybotx.
Основные команды, такие как /help
и системные команды, находятся в модуле common.py
. Для команд, относящихся к определенной задаче, создается свой модуль. Например, для интеграции с Atlassian Jira будет создан модуль jira.py
. В результате структура пакета app.bot.commands
будет выглядеть так:
bot
├── commands
│ ├── common.py
│ ├── jira.py
Если в модуле становится слишком много команд, следует разбить его на новые модули и сложить в один пакет с названием старого модуля. Например, так:
bot
├── commands
│ ├── common.py
│ ├── jira
│ ├── projects.py
│ ├── issues.py
Для добавления модуля с командами нужно импортировать collector
в app/bot/bot.py
и добавить его в инстанс бота:
from app.bot.commands import common
bot.include_handlers(common.collector)
Взаимодействовать с новыми таблицами можно через модели sqlalchemy. С примерами использования можно ознакомиться тут. Модели располагаются в пакете app.db.package_name
. Там же хранятся crud
функции и репозитории. Структура пакета выглядит следующим образом:
├── app
│ ├── db
│ ├── migrations
│ ├── exampleapp
│ ├── repo.py - репозиторий/crud функции
│ ├── models.py - модели таблиц
Пример модели:
from sqlalchemy import Column, Integer, String
from app.db.sqlalchemy import Base
class ExampleModel(Base):
__tablename__ = "examples"
id: int = Column(Integer, primary_key=True, autoincrement=True)
text: str = Column(String)
Пример репозитория:
from sqlalchemy import insert
from app.db.sqlalchemy import session
from app.db.example.models import ExampleModel
class ExampleRepo:
async def create(self, text: str) -> None:
query = insert(ExampleModel).values(text=text)
async with session.begin():
await session.execute(query)
Для генерации миграций используется alembic. Все файлы миграции хранятся в директории app.db.migrations
. Для генерации новой миграции необходимо создать модель sqlalchemy
и выполнить команду:
$ alembic revision --autogenerate -m "migration message"
Новый файл миграции будет создан в следующей директории:
├── app
│ ├── db
│ ├── migrations
│ ├── versions
│ ├── 0123456789ab_migration_message.py
Чтобы применить все миграции, следует выполнить команду:
$ alembic upgrade head
или:
$ alembic upgrade 1
для применения только одной миграции.
Для отмены одной миграции необходимо выолнить:
$ alembic downgrade -1
Вся бизнес-логика проекта выносится в пакет app.services
. Бизнес-логика - логика, характерная только для данного проекта. Туда же выносятся запросы, клиенты для использования API сторонних сервисов, обработка данных по заданным (в ТЗ) правилам.
Структура следующая:
├── app
│ ├── services
│ │ ├── errors.py - исключения, вызываемые в клиенте
│ │ ├── client.py - клиент для обращения к стороннему сервису
Новые переменные среды можно добавить в класс AppSettings
из файла app/settings.py
. Если у переменной нет значения по умолчанию, то оно будет браться из файла .env
.
Чтобы использовать эту переменную в боте, необходимо:
from app.settings import settings
...
settings.MY_VAR
ℹ️ Инфо Через переменные среды можно указывать окружения, в которых будет запускаться бот.
test
,dev
илиprod
. Просто добавьте в файл.env
переменнуюAPP_ENV=prod
.
Для запуска всех форматеров необходимо выполнить скрипт:
$ ./scripts/format
Для запуска всех линтеров необходимо выполнить скрипт:
$ ./scripts/lint
Используется для форматирования кода к единому стилю: разбивает длинные строки, следит за отступами и импортами.
⚠️ Примечание
В некоторых моментах isort конфликтует с black. Конфликт решается настройкой файла конфигурацииsetup.cfg
.
Используется для сортировки импортов. Сначала импорты из стандартных библиотек python, затем из внешних библиотек и в конце из модулей данного проекта. Между собой импорты сортируются по алфавиту.
Используется для удаления неиспользуемых импортов и переменных.
Используется для проверки типов. Помогает находить некоторые ошибки еще на стадии разработки.
⚠️ Примечание
К сожалению, не все библиотеки поддерживают типизацию. Чтобы подсказать это mypy необходимо добавить следующие строки в файл конфигурацииsetup.cfg
:
[mypy]
# ...
[mypy-your_library_name.*]
ignore_missing_imports = True
Некоторые же наоборот имеют специальные плагины для mypy, например pydantic:
[mypy]
plugins = pydantic.mypy
...
[pydantic-mypy]
init_forbid_extra = True
init_typed = True
warn_required_dynamic_aliases = True
warn_untyped_fields = True
Используется для комплексной проверки. Анализирует допустимые имена перменных и их длину, сложность вложенных конструкций, правильную обработку исключений и многое другое. Для каждого типа ошибок есть свой уникальный номер, объяснение, почему так делать не стоит, и объяснение, как делать правильно. Список ошибок можно посмотреть тут.
ℹ️ Инфо
В некоторых редких случаях можно игнорировать правила линтера. Для этого необходимо либо прописать комментарий с меткойnoqa
на проблемной строке:var = problem_function() # noqa: WPS999либо указать
ignore
ошибки вsetup.cfg
:[flake8] # ... ignore = # f-strings are useful WPS305,
Также можно исключать модули и пакеты.
Все тесты пишутся с помощью библиотеки pytest. Запустить тесты можно командой:
$ pytest
Во время тестирования поднимается docker-контейнер с БД. Порт выбирается свободный, поэтому запущенная локально БД не будет мешать. Если вы хотите запускать тесты используя вашу локальную БД, необходимо добавить в .env
переменную DB=1
, либо выполнить команду:
$ DB=1 pytest
ℹ️ Инфо
Поскольку pytest не умеет в асинхронные тесты, для работы с ними ему необходим плагин pytest-asyncio.
Покрытие показывает процент протестированного исходного кода, как всего, так и отдельных модулей. Покрытие помогает определить какие фрагменты кода не запускались в тестах. Для генерации отчетов покрытия используется плагин pytest-cov.
Чтобы не прописывать все флаги каждый раз, можно использовать эти скрипты:
$ ./scripts/test
$ ./scripts/html-cov-test
Первый выводит отчет в терминале, второй генерирует отчет в виде html
страниц с подсветкой непокрытых участков кода.