Skip to content

Latest commit

 

History

History
153 lines (134 loc) · 12.8 KB

File metadata and controls

153 lines (134 loc) · 12.8 KB

Repository Guidelines

Project Structure & Module Organization

  • main.tsp, openapi/: контракт на TypeSpec и скомпилированные из него документы OpenAPI. Источник истины для всего остального.
  • app/types/handlers/v1/, app/types/handlers/v2/: сгенерированное из OpenAPI — pydantic-модели схем и модели на операцию, по набору на версию. Руками не правится и линтером не проверяется.
  • app/main.py: сборка приложения — роутеры, промежуточные слои, обработчики ошибок, страницы документации. Здесь же lifespan: движок базы, миграции и сиды.
  • app/contract.py: чтение документов контракта. Их же приложение отдаёт клиенту.
  • app/settings.py: конфиг на pydantic-settings. Проверяется схемой на старте, поэтому приложение с плохим секретом не поднимается.
  • app/db/: модели SQLModel, публичные проекции и сиды. Миграции Alembic лежат в migrations/ на корне, по соглашению самого alembic.
  • app/glue.py: регистрация маршрутов по спеке — временная замена серверного плагина hey-api, см. TODO.md.
  • app/routers/: обработчики, по файлу на сущность; app/routers/v2/ — вторая версия. Каждый модуль отдаёт отображение «идентификатор операции → функция», а index.py собирает их в набор версии. Пути, коды успеха и авторизация в обработчиках не пишутся: они приходят из контракта при регистрации.
  • app/validators/, app/rules/, app/policies.py: правила предметной области, правила уровня базы, права доступа.
  • app/lib/: пароли, метки версий, страницы, тела ошибок, разбор полей правки, границы идентификаторов, фабрики тестовых данных.
  • scripts/: contract-test.sh, smoke-test.sh и служебные скрипты для целей Makefile. Запускаются модулями (python -m scripts.routes), иначе корень проекта не попадает в пути импорта.
  • tests/: спеки; бутстрап в tests/conftest.py.
  • package.json: инструменты контракта — компилятор TypeSpec и генератор моделей. Оба npm-пакеты; приложению node не нужен.

Development, Test, and Check Commands

  • make setup — зависимости обоих тулчейнов и .env из шаблона.
  • make dev / make start — запуск на http://localhost:8000.
  • make test / make test-coverage — pytest, второй с порогом покрытия.
  • make lint / make lint-fix — ruff и линт контракта.
  • make generate-types — спека из контракта и модели из спеки.
  • make generate-check — сгенерированное закоммичено и не разошлось.
  • make contract-test — schemathesis по спеке.
  • make migration-generate M="описание" / make migration-check.
  • make smoke-test — собирает образ и проверяет его (нужен docker).
  • make routes — таблица маршрутов с объявленной авторизацией.

Coding Style & Naming Conventions

  • Порядок правки: новое поле, статус или операция сначала появляются в main.tsp, потом make generate-types, потом код. Обратный порядок ловится в CI.
  • Формат и линт: ruff, длина строки 80. Дефолт инструмента и есть канон. Сгенерированное из линта исключено.
  • Имена на проводе: поля контракта в camelCase, поля кода и колонки — в snake_case. Псевдонимы приходят из контракта вместе с моделями.
  • Строка базы в модель контракта переводится через to_schema из app/lib/serialization.py. Готовый model_validate(row, from_attributes=True) для этого не годится: он ищет у объекта атрибут с именем псевдонима (fullName), не находит и оставляет поле пустым — молча, потому что поле необязательное.
  • Асинхронность до конца: обработчики, сессия базы и запросы — асинхронные. Счёт scrypt уходит в отдельный поток: иначе он держит событийный цикл десятки миллисекунд на каждый пароль.

Версии

Первая версия отдаётся с корня, вторая — с префиксом /v2. Первая намеренно не уехала под /v1: адреса существующих клиентов переездом сломались бы, а версия по определению нужна для того, чтобы этого не делать.

Версии различаются полем phone у пользователя (@added(Versions.v2)): во второй оно читается и принимается на запись, в первой его нет. Удерживают это разные сгенерированные модели и разные проекции в app/db/projections.py. Проекция — второй слой: ответ и так собирается по модели из контракта, поэтому поле вне контракта в тело не попадёт, — но проекция не даёт секрету покинуть базу, если у маршрута однажды не окажется модели ответа. Курсы, уроки и токены между версиями не менялись и переиспользуют те же роутеры.

Пути у версий одинаковые: префикс — это выбор развёртывания, а не часть контракта. Адрес версии проставляется в servers при отдаче документа (app/contract.py), иначе клиент, собранный по документу второй версии, стучался бы в корень, то есть в первую.

make contract-test гоняет обе версии.

Что сделано руками в ожидании генератора

Python-генератор hey-api молодой: серверного плагина у него нет, SDK печатает заглушки, а необязательное поле он не отличает от обнуляемого. Что из-за этого написано руками и на что заменится — в TODO.md. Мы исходим из того, что плагин выйдет: app/glue.py держит его свойства и удаляется целиком, когда он появится. Правки — там же, в TODO.md.

Testing Guidelines

  • Фреймворк: pytest, режим asyncio автоматический.
  • Транспорт: запросы идут через ASGI (httpx.ASGITransport), без сетевого сервера. Сгенерированный клиент пока не годится, см. TODO.md.
  • Изоляция: приложение поднимается на каждый тест со своей базой в памяти. Обработчики меняют данные, и общая база делала бы результат зависимым от порядка прогона.
  • Своя сессия: состояние базы тест смотрит сессией из фикстуры session, а запись перечитывает через refetch — сессия помнит прочитанные объекты, и без этого тест видел бы состояние до запроса.
  • Покрытие: порог в цели test-coverage, проверяется в CI. Он ниже фактического: гейт нужен как храповик против регресса, а не как повод подгонять цифры.
  • Контрактные тесты: make contract-test генерирует запросы из спеки — им найдены все три 500, которые здесь исправлены.

Commit & Pull Request Guidelines

  • Коммиты: conventional commits.
  • PR: заголовок обязан быть conventional commit — по нему release-please определяет разряд версии (проверяется в pr-title.yml).

Security & Configuration Tips

  • Секреты: без JWT_SECRET от 32 символов приложение не поднимается. .env не попадает в репозиторий, а значение из .env.example годится только для разработки: он лежит в публичном репозитории, в бой секрет приходит из окружения.
  • Пароли: scrypt из стандартной библиотеки, стоимость хранится внутри дайджеста. Поэтому смена SCRYPT_COST не обесценивает выданные хеши, а тесты и контрактный прогон могут платить меньше боевой цены.
  • Тесты не читают .env: чтение файла отключено при ENVIRONMENT=test, переменные приходят из tests/conftest.py.
  • Сиды в бою не применяются: они тянут faker из группы dev, поэтому импортируются только при ENVIRONMENT != production и только внутри условия. Это проверяет make smoke-test на собранном образе.
  • Ограничитель частоты: свой, в app/middleware.py. Готовая библиотека здесь не работает — общий для всех маршрутов ограничитель ищет обработчик по списку маршрутов приложения, а fastapi с некоторых версий хранит там ссылки на роутеры, а не сами операции. Ограничитель молча пропускал всё, и объявленный в контракте 429 не наступал никогда.
  • Ошибки фреймворка: обработчик объявлен на базовом классе исключения, а не на том, что предлагает FastAPI. Базовый фреймворк бросает сам — например, 400 на нечитаемом теле, — и обработчик, объявленный на наследнике, до такой ошибки не доходит.
  • Границы идентификаторов: контракт объявляет ResourceId с верхней границей, и она же стоит в пути обработчика. Без неё значение проходит проверку, доезжает до базы и падает там на переполнении, то есть 500 наступает на корректном с виду запросе.
  • База: по умолчанию sqlite в памяти процесса, пересоздаётся при каждом старте. Соединение одно (StaticPool): у каждого соединения sqlite своя база в памяти, и без общего миграции работали бы с другой базой, чем приложение. Для постоянного хранения в DATABASE_URL задаётся путь к файлу или адрес postgres.