[dev] Backend разработка - profcomff/.github GitHub Wiki

Note Документации по всем API можно посмотреть тут: https://api.profcomff.com/

Разработка

В нашей команде разработка ведется с использованием python 3.11. Используемые библиотеки: Pydantic, FastAPI, asyncio, SQLAlchemy, alembic, docker, black, pytest.

Все зависимости прописываются в файле requirements.txt без указания версий(для того, чтобы они автоматически скачивали самую новую). Разработческие зависимости можно прописывать в requirements.dev.txt. Там мы точно указываем black(https://pypi.org/project/black/). Если вы склонировали один из наших проектов, то посыле настройки конфигурации запуска и создания venv, стоит запустить pip install -m requirements.txt. После этого все зависимости установятся.

Как работаем с конфигом

Для конфигурации мы используем переменные окружения. Чтобы многократно не заполнять них, мы используем файлы окружения. Для работы с файлами окружения мы используем Pydantic-settings. В файле settings.py создается класс Settings extends BaseSettings, в котором определятся, что должно быть в .env файле. Здесь и везде далее используются type hints. Сам python не строго относится к их соблюдения, только подсказывает вам в IDE, что вы должны передать в качестве аргументов куда-либо, где указаны типы. Однако Pydantic проверяет соответствие указанных типов и полученных данных, что очень удобно и безопасно. https://fastapi.tiangolo.com/advanced/settings/#pydantic-settings

Локально создается .env файл в корне проекта. То есть, он будет виден в рабочем каталоге как .env, а не как ../.env и прочие вариации путей до него. То есть в файле settings.py прописывается что то такое:

from pydantic import PostgresDsn
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    """Application settings"""

    DB_DSN: PostgresDsn = 'postgresql://postgres@localhost:5432/postgres'
    model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", extra='ignore')

Пример .env файла, например, из бэкенда расписания: https://github.com/profcomff/timetable-backend/blob/main/.env.example. Когда вы первый раз отправляете коммит, проверяйте наличие файла .gitignore, .env файл не должен попасть в изменения.

Как работаем с базой данных

Для работы с БД мы используем SQLAlchemy. Это очень мощный инструмент разработки с встроенным ORM, который мы в основном и используем. Смысл ORM - установка соответствий между моделями с БД и объектами в python. Таким образом, вы работаете с экземплярами соответствующих классов, а не с чистыми SQL-запросами, которые зачастую очень громоздкие и неудобные в дальнейшей работе с ними.

Для начала, создаются все модели(классы) в python. Путь к ним обычно выглядит как /models/db.py. Поля таких классов - колонки. При создании таблиц sqlalchemy.Column создает нужную колонку, а в дальнейшем при обращении к этому полю, выдает значение для текущей строки в БД. https://docs.sqlalchemy.org/en/20/orm/quickstart.html#declare-models

После создания моделей, описываются relationships между ними. Это нужно для простой и быстрой реализации больших и, кажется, ненужных запросов. То есть, childrens = session.query(Parent).filter(Children.parent_id == parent.id).all() превращается в parent.childrens. Очень упрощает читаемость кода. https://docs.sqlalchemy.org/en/20/orm/basic_relationships.html

Отношения между моделями бывают разные: one-to-one, many-to-one, many-to-many. Все их реализации достаточно просты, но в любом случае требуют изучения:

  1. https://docs.sqlalchemy.org/en/20/orm/basic_relationships.html#one-to-many
  2. https://docs.sqlalchemy.org/en/20/orm/basic_relationships.html#one-to-one
  3. https://docs.sqlalchemy.org/en/20/orm/basic_relationships.html#many-to-many

В процессе разработки структура БД может меняться, для того, чтобы это все автоматически раскатывалась на сервере, мы используем alembic. Любые изменения в БД должны сопровождаться генерацией файла миграций. Ваша БД должна содержать только те таблицы, которые нужны в данном проекте.

Для создания миграций надо сделать следующие действия:

  1. alembic revision --autogenerate -m "issue_number_and_name"
  2. Проверьте, что в папке migrations/versions создался новый файлик и его содержимое соответствует вашим ожиданиям
    • Если что-то сформировалось не так, обязательно поправьте. Обычно переименования колонок или таблиц алембиком воспринимаются как удаление и создание новых, это ошибка!
    • Можно натравливать автоформатирование, чтоб делать файлики миграций красивыми :)
  3. alembic upgrade head для применения миграций

Как работаем с вебом

Для работы с вебом мы используем FastAPI. В нем есть все необходимое и весит он меньше, чем Django :)

Работаем мы по стандартам REST API: https://habr.com/ru/post/483202/

Для конкретного объекта создается свой собственный роутер, в котором минимум указывается prefix и tags. prefix - это то, что будет всегда стоять в начале пути для данного роутера, tags - для удобства чтения документации в Swagger UI. Все ручки являются асинхронными.

REST API состоит из нескольких типов ручек: GET/POST/PATCH/DELETE.

  • GET запрос не может иметь тело в виде json. Его не стоит использовать для передачи конфиденциальных данных, т.к браузер может сохранять историю запросов и т.д. Запрос будет выглядеть так GET /router/{id} для конкретного ресурса и GET /router для всех

  • POST запрос уже может содержать тело в виде json. Его используют для создания ресурса. JSON передается в виде верифицируемой модели pydantic. То есть, описывается класс с полями, которые должен содержать передаваемый json. Опять же указываются type hints и необходимость передачи того или иного поля (например id: int | None). Запрос будет выглядеть так: POST /router

  • PATCH запрос во многом похож на POST(не рассматриваемый здесь PUT туда же). Используется для редактирования ресурса. Во многом это просто создано для удобства, по факту можно использовать для обновления и POST запросы. Но это противоречит спецификации REST, так что мы так не делаем. Запрос будет выглядеть так: PATCH /router/{id}

  • DELETE аналогичен, используется для удаления ресурсов. Вместо него так же можно использовать другие запросы с постфиксами /delete, но так как мы работаем по REST API, мы используем этот тип запросов. Пример запроса: DELETE /router/{id}

Каждый роутер складывается в отдельный файл, потом они собираются вместе в файле base.py. Все роутеры лежат в директории /routes/ вместе с base.py.

Когда ручка что то возвращает, мы используем json словари, верифицируемые Pydantic'ом. Аналогично передаче данных в запрос, описанной выше: описывается класс с полями, которые должен содержать получаемый json. Опять же указываются type hints и необходимость содержания того или иного поля(например id: int | None) в ответе. Если запрос может возвращать большое количество данных, используется пагинация: ответ от сервера в таком случае представляет из себя: {"items": result, "limit": limit, "offset": offset, "total": result.count()}, где limit - максимальный размер ответа, offset - смещение от первого элемента, total - количество подходящих записей в БД.

В base.py подключаются middlewares. Про CORS, используемый, когда фронт лежит отдельно от бэка можно почитать тут: https://github.com/profcomff/.github/wiki/Расположение-сервисов Подключаемый DBSessionMiddleware нужен для создания сессии работы с БД прямо в FastAPI. Также, можно подключать и собственные middlewares.

Как сделать Pull request

Для начала, вы должны помнить следующее:

  1. Все пароли, Redierct URLs и прочее указываются в .env файле и подгружается в класс Settings. Мы не храним пароли и адреса наших серверов в коде.

  2. Кроме requirements.txt в корне проекта лежат файлы: .env, .gitignore, flake8.conf, pyproject.toml, LICENSE. Это все файлы конфигураций и файлы для GitHub - вообще относительно разработческие штуки. Их можно взять в любом готовом проекте (кроме .env)

  3. Если вы создаете PR в пустом репозитории: В корне проекта создается исполняемый модуль и модуль тестов, папка с миграциями, путь /.github/workflows/. В каждой директории модулей должен лежать файл __init__.py, а в директории исполняемого модуля должен лежать и файл __main__.py. Запускать надо исполняемый модуль(python3 -m...)

  4. Любые изменения в БД должны сопровождаться генерацией файла миграций. Ваша БД должна содержать только те таблицы, которые нужны в данном проекте.

  5. Любые изменения в структуре проекта должны быть прописанными в Dockerfile. При создании папок, если они пустые, создавайте там пустой файл .gitkeep, иначе папка может не закоммититься. После этого не забудьте прописать этот путь в Dockerfile.

Итак, чтобы сделать Pull request, вам надо:

  1. Создать ветку в GitHub, работать в ней
  2. Написать код, решающий задачу
  3. Написать тесты к вашему коду
  4. Проверить, сгенерирован ли файл миграций
  5. Проверить, все ли папки, созданные вами, закоммитились в вашу ветку. Проверить, добавили ли вы их в Dockerfile
  6. Проверить соответствие пропета стандартам PEP8, прогнать по коду black
  7. Запросить review у любого из старших разработчиков
  8. Исправить ошибки, если присутствуют на этапе review
  9. Merge! Вы великолепны.

Если вы создаете новый проект и хотите упростить себе жизнь и не заниматься структурой проекта, можете использовать наш готовый шаблон проекта: https://github.com/profcomff/fastapi-template

Выкатываем новый бэкэнд на прод

Чтобы новый сервис появился в тесте и проде нужно сделать несколько важных вещей:

  1. Подготовить БД (это надо повторить и в тестовой БД, и в продовой)

    1. Создать новых пользователей в тестовой и продовой базах данных: CREATE USER srvc_test_marketing_api IN GROUP group_service_test PASSWORD '...';
    2. Создать новые схемы для хранения таблиц: create schema api_marketing authorization srvc_test_marketing_api;
    3. Назначить для пользователя схему по умолчанию: alter user srvc_test_marketing_api set search_path to api_marketing;
  2. Настройка репозитория

    1. Создать среды исполнения (Environments) для теста и прода (обычно во всех репозиториях Testing и Production)
    2. В средах создать переменные с ключами, необходимые для запуска сервиса. Например данные для подключения к БД мы кладем в переменную DB_DSN:
      image
      На изображении 2 ключа, к которым можно обратиться из CI через {{ secrets.НАЗВАНИЕ }}
    3. На прод настроить ревьюера, который сможет выкатывать сервис для всех пользователей.
  3. Упаковать проект для запуска в Docker

    1. Создать Dockerfile в корне проекта. Пример докерфайла
  4. Настроить GitHub Actions для автоматического запуска сборки и запуска на сервере

    1. Создать папку .github/workflows
    2. Положить в него файлики для раскатки теста, прода, тестирования и т.д.
    3. В качестве шаблона можно использовать этот пример. Тут происходит раскатка в тест при коммитах в ветку main и раскатка в прод при создании тегов.
  5. Настроить сервер

    1. Сервисы находятся в отдельных докер контейнерах, их видно только во внутренней сети
    2. У нас есть Caddy (это reverse proxy http сервер), который запросы из внешней сети прокидывает во внутреннюю. В него надо добавить новую запись reverse_proxy.

    Пример Caddy записи

    printer.api.profcomff.com:443 {
        reverse_proxy com_profcomff_api_printer:80
    }
    
    1. Понять, что по ссылкам в прошлом пункте ничего не понятно и просто скопировать готовую конфигурацию соседнего сервиса :)

Проверяем корректность миграций

  1. Просим админов БД скинуть нужный дамп (получить его можно только если есть соответствующие доступы) Команда для дампа: pg_dump -U {role} -h {host} -p {port} -d {db_name} -n {schema_name} > /tmp/{name}.dump
  2. Кидаем его в БД: psql -U {role} -h localhost -p 5432 -d {db_name} < /tmp/{name}.dump
  3. Применяем миграции на локальной БД.

Полезные ссылки и источники информации

⚠️ **GitHub.com Fallback** ⚠️