|
| 1 | +# SmartApp Template Tutorial |
| 2 | + |
| 3 | +## Введение |
| 4 | + |
| 5 | +> :information_source: Инфо |
| 6 | +> Для взаимодействия с платформой **botx** используется библиотека **[pybotx](https://github.com/ExpressApp/pybotx)**. В **[документации](https://pypi.org/project/pybotx/)** можно посмотреть примеры её использования. |
| 7 | +> Для реализации SmartApp используется библиотека **[pybotx-smartapp-rpc](https://github.com/ExpressApp/pybotx-smartapp-rpc)**. |
| 8 | +> Перед прочтением данного туториала следует с ней ознакомиться. |
| 9 | +
|
| 10 | +---- |
| 11 | + |
| 12 | +Шаблон решает проблему написания повторяющего кода в самом начале работы над проектом. |
| 13 | +Уже существует шаблон для разработки ботов **[bot-template](https://github.com/ExpressApp/bot-template)** |
| 14 | + |
| 15 | +Но структура бота и SmartApp отличается друг от друга, поэтому был создан этот шаблон. |
| 16 | + |
| 17 | +---- |
| 18 | + |
| 19 | + |
| 20 | +## 1. Развертывание из шаблона и структура проекта |
| 21 | + |
| 22 | +Для развертывания проекта необходимо установить [copier](https://github.com/copier-org/copier) и выполнить команду: |
| 23 | +```bash |
| 24 | +$ copier smartapp-template smartapp-example |
| 25 | +``` |
| 26 | + |
| 27 | +Структура шаблонного SmartApp состоит из нескольких следующих пакетов и модулей: |
| 28 | + |
| 29 | +``` |
| 30 | +. |
| 31 | +├── app |
| 32 | +│ ├── api - реализация http роутов для приложения, включая необходимые для smartapp |
| 33 | +│ ├── bot - команды бота и вспомогательные функции для них |
| 34 | +│ ├── caching - классы и функции для работы с in-memory БД |
| 35 | +│ ├── db - модели, функции для работы с БД и миграции |
| 36 | +│ ├── schemas - сериализаторы, енамы, доменные модели |
| 37 | +│ ├── services - сервисы с логикой (бизнес-логика) |
| 38 | +│ ├── smartapp - rpc - методы и аргументы для smartapp |
| 39 | +│ ├── smartapp-files - статические файлы |
| 40 | +│ ├── constants.py - константы |
| 41 | +│ ├── logger.py - логгер |
| 42 | +│ ├── main.py - запуск сервера с инициализацией необходимых сервисов |
| 43 | +│ └── settings.py - настройки приложения |
| 44 | +├── extensions - вспомогательные расширения для шаблона |
| 45 | +├── scripts - скрипты для запуска тестов, форматеров, линтеров |
| 46 | +├── tests - тесты, структура которых соответствует структуре проекта, и хелперы для них |
| 47 | +├── poetry.lock - конфигурация текущих зависимостей. используется для их установки |
| 48 | +├── pyproject.toml - конфигурация зависимостей, мета информация проекта (название, версия, авторы и т.п.) |
| 49 | +└── setup.cfg - конфигурация линтеров и тестов |
| 50 | +``` |
| 51 | + |
| 52 | +## 2. Запуск проекта |
| 53 | + |
| 54 | +### Настройка окружения |
| 55 | + |
| 56 | +1. Устанавливаем зависимости проекта через [poetry](https://github.com/python-poetry/poetry#poetry-dependency-management-for-python): |
| 57 | +```bash |
| 58 | +$ poetry install |
| 59 | +``` |
| 60 | +2. Определяем переменные окружения в файле **`.env`**. Примеры переменных окружения находятся в файле **`example.env`**. |
| 61 | +3. Запускаем `postges` и `redis` используя [docker-compose](https://docs.docker.com/compose/): |
| 62 | +```bash |
| 63 | +$ docker-compose -f docker-compose.dev.yml up -d |
| 64 | +``` |
| 65 | +4. Применяем все миграции для инициализации таблиц с помощью [alembic](https://alembic.sqlalchemy.org/en/latest/tutorial.html): |
| 66 | +```bash |
| 67 | +$ alembic upgrade head |
| 68 | +``` |
| 69 | +5. Запускаем SmartApp как приложение [FastAPI](https://fastapi.tiangolo.com/tutorial/) через [gunicorn](https://fastapi.tiangolo.com/deployment/server-workers/?h=gunicorn#run-gunicorn-with-uvicorn-workers). |
| 70 | +Флаг `--reload` используется только при разработке для автоматического перезапуска сервера при изменениях в коде: |
| 71 | +```bash |
| 72 | +$ gunicorn "app.main:get_application()" --worker-class uvicorn.workers.UvicornWorker |
| 73 | +``` |
| 74 | +По необходимости добавить флаг `--workers` и их колличество, в данном случае 4 рабочих процесса: |
| 75 | +```bash |
| 76 | +$ gunicorn "app.main:get_application()" --worker-class uvicorn.workers.UvicornWorker --workers 4 |
| 77 | +``` |
| 78 | + |
| 79 | +---- |
| 80 | + |
| 81 | +## 3. Добавление нового функционала |
| 82 | + |
| 83 | +### 3.1. Команды SmartApp |
| 84 | + |
| 85 | +#### Структура пакета команд |
| 86 | +Обработчики находятся в пакете **`app.smartapp.rpc_methods`** и группируются в отдельные модули в зависимости от логики. |
| 87 | +Обработчики добавляются с помощью **RPCRouter**. |
| 88 | + |
| 89 | +Если в модуле становится слишком много команд, следует разбить его на новые модули и сложить в один пакет с названием старого модуля. Например, так: |
| 90 | + |
| 91 | +``` |
| 92 | +smartapp |
| 93 | +├── rpc_methods |
| 94 | +│ ├── common.py |
| 95 | +│ ├── jira |
| 96 | +│ ├── projects.py |
| 97 | +│ ├── issues.py |
| 98 | +``` |
| 99 | + |
| 100 | + |
| 101 | +#### Регистрация команд |
| 102 | +Для добавления модуля с обработчиками нужно импортировать его из модуля нужного вам модуля в **`app/smartapp/smartapp.py`** и добавить его в `routers` smartapp: |
| 103 | + |
| 104 | +```python3 |
| 105 | +from pybotx_smartapp_rpc import SmartAppRPC |
| 106 | + |
| 107 | +from app.smartapp.middlewares.smartlogger import smart_logger_middleware |
| 108 | +from app.smartapp.rpc_methods import common, jira |
| 109 | + |
| 110 | +smartapp = SmartAppRPC( |
| 111 | + routers=[common.rpc, jira.rpc], |
| 112 | + middlewares=[smart_logger_middleware], |
| 113 | +) |
| 114 | +``` |
| 115 | + |
| 116 | +---- |
| 117 | + |
| 118 | +### 3.2. Взаимодействие с БД |
| 119 | + |
| 120 | +#### Создание новых моделей |
| 121 | + |
| 122 | +Взаимодействовать с новыми таблицами можно через модели [sqlalchemy](https://www.sqlalchemy.org/). С примерами использования можно ознакомиться [тут](https://www.sqlalchemy.org/library.html#tutorials). Модели располагаются в пакете **`app.db.package_name`**. Там же хранятся `crud` функции и [репозитории](https://gist.github.com/maestrow/594fd9aee859c809b043). Структура пакета выглядит следующим образом: |
| 123 | +``` |
| 124 | +├── app |
| 125 | +│ ├── db |
| 126 | +│ ├── migrations |
| 127 | +│ ├── exampleapp |
| 128 | +│ ├── repo.py - репозиторий/crud функции |
| 129 | +│ ├── models.py - модели таблиц |
| 130 | +``` |
| 131 | + |
| 132 | +Пример модели: |
| 133 | +``` python |
| 134 | +from sqlalchemy import Column, Integer, String |
| 135 | +from app.db.sqlalchemy import Base |
| 136 | + |
| 137 | +class ExampleModel(Base): |
| 138 | + __tablename__ = "examples" |
| 139 | + |
| 140 | + id: int = Column(Integer, primary_key=True, autoincrement=True) |
| 141 | + text: str = Column(String) |
| 142 | +``` |
| 143 | + |
| 144 | +Пример репозитория: |
| 145 | +``` python |
| 146 | +from sqlalchemy import insert |
| 147 | +from app.db.sqlalchemy import session |
| 148 | +from app.db.example.models import ExampleModel |
| 149 | + |
| 150 | +class ExampleRepo: |
| 151 | + async def create(self, text: str) -> None: |
| 152 | + query = insert(ExampleModel).values(text=text) |
| 153 | + async with session.begin(): |
| 154 | + await session.execute(query) |
| 155 | +``` |
| 156 | + |
| 157 | +#### Создание новых миграций |
| 158 | + |
| 159 | +Для генерации миграций используется [alembic](https://alembic.sqlalchemy.org/en/latest/). Все файлы миграции хранятся в директории **`app.db.migrations`**. Для генерации новой миграции необходимо создать модель `sqlalchemy` и выполнить команду: |
| 160 | + |
| 161 | +```bash |
| 162 | +$ alembic revision --autogenerate -m "migration message" |
| 163 | +``` |
| 164 | + |
| 165 | +Новый файл миграции будет создан в следующей директории: |
| 166 | +``` |
| 167 | +├── app |
| 168 | +│ ├── db |
| 169 | +│ ├── migrations |
| 170 | +│ ├── versions |
| 171 | +│ ├── 0123456789ab_migration_message.py |
| 172 | +``` |
| 173 | + |
| 174 | +Чтобы применить все миграции, следует выполнить команду: |
| 175 | +```bash |
| 176 | +$ alembic upgrade head |
| 177 | +``` |
| 178 | +или: |
| 179 | +```bash |
| 180 | +$ alembic upgrade 1 |
| 181 | +``` |
| 182 | +для применения только одной миграции. |
| 183 | + |
| 184 | +Для отмены одной миграции необходимо выолнить: |
| 185 | +```bash |
| 186 | +$ alembic downgrade -1 |
| 187 | +``` |
| 188 | + |
| 189 | +---- |
| 190 | + |
| 191 | +### 3.3. Сервисы и бизнес-логика |
| 192 | + |
| 193 | +Вся бизнес-логика проекта выносится в пакет **`app.services`**. Бизнес-логика - логика, характерная только для данного проекта. Туда же выносятся запросы, клиенты для использования API сторонних сервисов, обработка данных по заданным (в ТЗ) правилам. |
| 194 | + |
| 195 | +Структура следующая: |
| 196 | +``` |
| 197 | +├── app |
| 198 | +│ ├── services |
| 199 | +│ │ ├── errors.py - исключения, вызываемые в клиенте |
| 200 | +│ │ ├── client.py - клиент для обращения к стороннему сервису |
| 201 | +``` |
| 202 | + |
| 203 | +---- |
| 204 | + |
| 205 | +### 3.4. Конфиги и переменные среды |
| 206 | + |
| 207 | +Новые переменные среды можно добавить в класс `AppSettings` из файла `app/settings.py`. Если у переменной нет значения по умолчанию, то оно будет браться из файла `.env`. |
| 208 | +Чтобы использовать эту переменную в боте, необходимо: |
| 209 | +``` python |
| 210 | +from app.settings import settings |
| 211 | +... |
| 212 | +settings.MY_VAR |
| 213 | +``` |
| 214 | + |
| 215 | +> :information_source: Инфо |
| 216 | +> Через переменные среды можно указывать окружения, в которых будет запускаться smartapp. `test`, `dev` или `prod`. Просто добавьте в файл `.env` переменную `APP_ENV=prod`. |
| 217 | +
|
| 218 | +---- |
| 219 | + |
| 220 | +## 4. Линтеры и форматирование кода |
| 221 | + |
| 222 | +#### Запуск |
| 223 | +Для запуска всех форматеров необходимо выполнить скрипт: |
| 224 | +```bash |
| 225 | +$ ./scripts/format |
| 226 | +``` |
| 227 | + |
| 228 | +Для запуска всех линтеров необходимо выполнить скрипт: |
| 229 | +```bash |
| 230 | +$ ./scripts/lint |
| 231 | +``` |
| 232 | + |
| 233 | +#### Описание |
| 234 | +* [black](https://github.com/psf/black) |
| 235 | + |
| 236 | +Используется для форматирования кода к единому стилю: разбивает длинные строки, следит за отступами и импортами. |
| 237 | + |
| 238 | +> :warning: Примечание |
| 239 | +> В некоторых моментах isort конфликтует с black. Конфликт решается настройкой файла конфигурации **`setup.cfg`**. |
| 240 | +
|
| 241 | +* [isort](https://github.com/timothycrosley/isort) |
| 242 | + |
| 243 | +Используется для сортировки импортов. Сначала импорты из стандартных библиотек python, затем из внешних библиотек и в конце из модулей данного проекта. |
| 244 | +Между собой импорты сортируются по алфавиту. |
| 245 | + |
| 246 | +* [autoflake](https://github.com/myint/autoflake) |
| 247 | + |
| 248 | +Используется для удаления неиспользуемых импортов и переменных. |
| 249 | + |
| 250 | +* [mypy](https://github.com/python/mypy) |
| 251 | + |
| 252 | +Используется для проверки типов. Помогает находить некоторые ошибки еще на стадии разработки. |
| 253 | + |
| 254 | +> :warning: Примечание |
| 255 | +> К сожалению, не все библиотеки поддерживают типизацию. Чтобы подсказать это **mypy** необходимо добавить следующие строки в файл конфигурации **`setup.cfg`**: |
| 256 | +
|
| 257 | +``` |
| 258 | +[mypy] |
| 259 | +
|
| 260 | +# ... |
| 261 | +
|
| 262 | +[mypy-your_library_name.*] |
| 263 | +ignore_missing_imports = True |
| 264 | +``` |
| 265 | + |
| 266 | +Некоторые же наоборот имеют специальные плагины для **mypy**, например **pydantic**: |
| 267 | + |
| 268 | +``` |
| 269 | +[mypy] |
| 270 | +plugins = pydantic.mypy |
| 271 | +
|
| 272 | +... |
| 273 | +
|
| 274 | +[pydantic-mypy] |
| 275 | +init_forbid_extra = True |
| 276 | +init_typed = True |
| 277 | +warn_required_dynamic_aliases = True |
| 278 | +warn_untyped_fields = True |
| 279 | +``` |
| 280 | + |
| 281 | +* [wemake-python-styleguide](https://github.com/wemake-services/wemake-python-styleguide) |
| 282 | + |
| 283 | +Используется для комплексной проверки. Анализирует допустимые имена перменных и их длину, сложность вложенных конструкций, правильную обработку исключений и многое другое. Для каждого типа ошибок есть свой уникальный номер, объяснение, почему так делать не стоит, и объяснение, как делать правильно. Список ошибок можно посмотреть [тут](https://wemake-python-stylegui.de/en/latest/pages/usage/violations/index.html). |
| 284 | + |
| 285 | +> :information_source: Инфо |
| 286 | +> В некоторых редких случаях можно игнорировать правила линтера. Для этого необходимо либо прописать комментарий с меткой `noqa` на проблемной строке: |
| 287 | +> ```python3 |
| 288 | +> var = problem_function() # noqa: WPS999 |
| 289 | +> ``` |
| 290 | +> либо указать `ignore` ошибки в **`setup.cfg`**: |
| 291 | +> ``` |
| 292 | +> [flake8] |
| 293 | +> # ... |
| 294 | +> ignore = |
| 295 | +> # f-strings are useful |
| 296 | +> WPS305, |
| 297 | +> ``` |
| 298 | +> Также можно исключать модули и пакеты. |
| 299 | +
|
| 300 | +
|
| 301 | +---- |
| 302 | +
|
| 303 | +
|
| 304 | +## 5. Тестирование |
| 305 | +
|
| 306 | +
|
| 307 | +
|
| 308 | +### 5.1. Запуск и добавление тестов |
| 309 | +
|
| 310 | +Все тесты пишутся с помощью библиотеки [pytest](https://docs.pytest.org/en/latest/). Запустить тесты можно командой: |
| 311 | +
|
| 312 | +```bash |
| 313 | +$ pytest |
| 314 | +``` |
| 315 | +
|
| 316 | +Во время тестирования поднимается docker-контейнер с БД. Порт выбирается свободный, поэтому запущенная локально БД не будет мешать. Если вы хотите запускать тесты используя вашу локальную БД, необходимо добавить в **`.env`** переменную `DB=1`, либо выполнить команду: |
| 317 | +```bash |
| 318 | +$ DB=1 pytest |
| 319 | +``` |
| 320 | + |
| 321 | + |
| 322 | +> :information_source: Инфо |
| 323 | +> Поскольку **pytest** не умеет в асинхронные тесты, для работы с ними ему необходим плагин **[pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio)**. |
| 324 | +
|
| 325 | +### 5.2. Покрытие |
| 326 | + |
| 327 | +Покрытие показывает процент протестированного исходного кода, как всего, так и отдельных модулей. Покрытие помогает определить какие фрагменты кода не запускались в тестах. Для генерации отчетов покрытия используется плагин [pytest-cov](https://pytest-cov.readthedocs.io/en/latest/reporting.html). |
| 328 | + |
| 329 | +Чтобы не прописывать все флаги каждый раз, можно использовать эти скрипты: |
| 330 | +```bash |
| 331 | +$ ./scripts/test |
| 332 | +$ ./scripts/html-cov-test |
| 333 | +``` |
| 334 | + |
| 335 | +Первый выводит отчет в терминале, второй генерирует отчет в виде `html`страниц с подсветкой непокрытых участков кода. |
0 commit comments