В этой статье я расскажу о Swagger и о том, как сгенерировать API и Pydantic модели из Swagger-документации для FastAPI, используя инструмент OpenAPI Generator. В конце статьи вы найдете ссылки на исходный код.

Итак, давайте разбираться!

Для чего это нужно?

Когда вы работаете над API, написание ручного кода для каждого маршрута и модели данных может занять много времени, особенно если система сложная. Описав API в формате OpenAPI (ранее известный как Swagger), вы сможете автоматически генерировать готовые маршруты и Pydantic-модели, которые легко интегрируются в FastAPI. Это освобождает вас от рутины написания однотипных классов и функций, позволяя сосредоточиться на логике приложения.

Что такое Swagger

Swagger — это набор инструментов, облегчающих разработку, тестирование и документирование API. Его основной компонент — это спецификаций в формате OpenAPI, который используется для описания структуры API, доступных маршрутов, типов данных и взаимодействий. Благодаря этому формату Swagger позволяет генерировать документацию, тестировать API, а также создавать клиентские и серверные SDK для различных языков программирования. В этой статье мы сосредоточимся на генерации исходного кода для FastAPI с помощью OpenAPI Generator.

Генерация исходного кода

Для начала необходимо написать документацию нашего API в формате OpenAPI. Создадим файл spec.yml, который будет содержать описание наших маршрутов:

openapi: 3.0.0
info:
  description: "API"
  version: "1.0.0"
  title: "API"
paths:

  /mock:
    get:
      tags:
        - Mock
      operationId: mock
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: "Successful response"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MockDataResponse'

    post:
      tags:
        - Mock
      operationId: add_mock
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateMockData'
      responses:
        '200':
          description: "Successful response"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResponseCreateMock'

components:
  schemas:
    MockDataResponse:
      type: object
      properties:
        id:
          type: integer
        info:
          type: string

    CreateMockData:
      type: object
      properties:
        info:
          type: string

    ResponseCreateMock:
      type: object
      properties:
        id:
          type: integer

Как видно из примера, наша документация содержит два основных запроса: GET и POST. Мы также описали объекты, которые эти запросы принимают и возвращают.

Теперь, когда документация готова, мы можем перейти к процессу генерации исходного кода с помощью OpenAPI Generator.

OpenAPI Generator — это инструмент, предназначенный для автоматической генерации кода на основе спецификаций в формате OpenAPI. Он поддерживает множество языков программирования и фреймворков, включая Python, Java, JavaScript, Ruby и многие другие. С полным списком поддерживаемых языков и фреймворков можно ознакомиться здесь.

Установить OpenAPI Generator можно разными способами, но для удобства мы будем использовать Docker. Для этого создадим скрипт generate-api.sh, который автоматизирует процесс генерации:

#!/bin/sh

set -e
ROOT=$(dirname $0)
cd "$ROOT"

sudo rm -Rf ./endpoints/apis ./endpoints/models ./endpoints/router_init.py
mkdir -p "$ROOT/endpoints"

sudo rm -Rf ./openapi-generator-output
docker run --rm -v "${PWD}":/app openapitools/openapi-generator-cli:latest-release generate  \
    -i /app/spec.yml  -g python-fastapi   -o /app/openapi-generator-output \
    --additional-properties=packageName=endpoints --additional-properties=fastapiImplementationPackage=endpoints

Теперь разберем ключевые аргументы, которые мы используем в команде generate:

  • i указывает на файл с документацией (в нашем случае — spec.yml).

  • g определяет генератор для конкретного фреймворка или языка. Например, чтобы сгенерировать код для приложения на Flask, указываем python-flask, но в нашем случае используется python-fastapi.

  • o задаёт директорию, в которую будет помещен сгенерированный код.

  • -additional-properties=packageName задаёт имя пакета для сгенерированного кода.

  • -additional-properties=fastapiImplementationPackage указывает, где будет находиться реализация FastAPI.

Этот скрипт создаст новую папку openapi-generator-output с множеством файлов. Давайте посмотрим что он создал.

Обзор сгенерированных файлов

После генерации в папке появляется несколько конфигурационных файлов, но основное внимание стоит уделить папке src, так как именно в ней содержится код нашего приложения. Вот краткий обзор её структуры:

  • main.py — файл, содержащий код для запуска приложения FastAPI.

  • security_api.py — здесь описана логика проверки доступа к маршрутам.

  • /api/ — папка с маршрутами API и логикой каждого из них:

    • _api.py — файлы, которые инициализируют маршруты и проверяют, что методы были реализованы.

    • _api_base.py — базовые классы, от которых можно наследоваться для реализации маршрутов.

Также в папке /models/ находятся файлы с классами Pydantic, которые отвечают за валидацию и сериализацию данных.

На этом этапе у нас уже есть сгенерированный код, и его можно использовать в проекте. Чтобы приложение FastAPI нашло все необходимые классы и методы, достаточно создать файл с именем, которое было указано в параметре:

bash
--additional-properties=fastapiImplementationPackage

Этот файл будет содержать бизнес логику всех маршрутов, наследующихся от базовых классов *_api_base.py.

Однако, при генерации OpenAPI Generator создает много лишних файлов, которые не всегда нужны. Поэтому давайте немного изменим наш скрипт generate-api.sh, чтобы убрать ненужные файлы и оставить только то, что нужно:

#!/bin/sh

set -e
ROOT=$(dirname $0)
cd "$ROOT"

sudo rm -Rf ./endpoints/apis ./endpoints/models ./endpoints/router_init.py
mkdir -p "$ROOT/endpoints"

sudo rm -Rf ./openapi-generator-output
docker run --rm -v "${PWD}":/app openapitools/openapi-generator-cli:latest-release generate  \
    -i /app/spec.yml  -g python-fastapi   -o /app/openapi-generator-output \
    --additional-properties=packageName=endpoints --additional-properties=fastapiImplementationPackage=endpoints

sudo chown "$USER":"$USER" -R openapi-generator-output
rm -Rf endpoints/apis endpoints/models
mv openapi-generator-output/src/endpoints/apis endpoints/
mv openapi-generator-output/src/endpoints/models endpoints/
mv openapi-generator-output/src/endpoints/main.py endpoints/router_init.py
rm -Rf openapi-generator-output

Теперь все нужные файлы находятся в папке endpoints, что делает структуру проекта более чистой и организованной

Как добавить бизнес-логику?

Теперь, когда у нас есть сгенерированные маршруты, давайте разберемся, как добавить бизнес-логику в наше приложение FastAPI. Для этого заглянем в папку /endpoints/apis, где находятся основные файлы с маршрутизаторами.

Пример содержимого /enpoints/apis

В файле mock_api.py мы видим следующее:

router = APIRouter()

ns_pkg = endpoints
for _, name, _ in pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + "."):
    importlib.import_module(name)

@router.post(
    "/mock",
    responses={
        200: {"model": ResponseCreateMock, "description": "Successful response"},
    },
    tags=["Mock"],
    response_model_by_alias=True,
)
async def add_mock(
    create_mock_data: CreateMockData = Body(None, description=""),
) -> ResponseCreateMock:
    if not BaseMockApi.subclasses:
        raise HTTPException(status_code=500, detail="Not implemented")
    return await BaseMockApi.subclasses[0]().add_mock(create_mock_data)

@router.get(
    "/mock",
    responses={
        200: {"model": MockDataResponse, "description": "Successful response"},
    },
    tags=["Mock"],
    response_model_by_alias=True,
)
async def mock(
    id: int = Query(None, description="", alias="id"),
) -> MockDataResponse:
    if not BaseMockApi.subclasses:
        raise HTTPException(status_code=500, detail="Not implemented")
    return await BaseMockApi.subclasses[0]().mock(id)

В файле mock_api.py создаётся роут для fastAPI и проверяется, что наш метод кто то реализует, а так же динамически импортирует все подмодули из переменой ns_pkg (содержит имя папки которое передали в --additional-properties=fastapiImplementationPackage).

В файле mock_api_base.py реализована логика, которая автоматически сохраняет всех наследников класса BaseMockApi в массив и содержит методы, которые нам надо реализовать:

class BaseMockApi:
    subclasses: ClassVar[Tuple] = ()

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        BaseMockApi.subclasses = BaseMockApi.subclasses + (cls,)
    async def add_mock(
        self,
        create_mock_data: CreateMockData,
    ) -> ResponseCreateMock:
        ...

    async def mock(
        self,
        id: int,
    ) -> MockDataResponse:
        ...

Теперь когда мы разобрались в том , что мы сгенерировали, нам не составит труда сделать реализацию наших роутов. Для этого в папке /endpoints создадим файл mock_impl.py и напишем простейшую реализацию.

class MockImpl(BaseMockApi):
    data = [MockDataResponse(id=1, info="hello world!"), MockDataResponse(id=2, info="bye world!")]

    async def add_mock(self, create_mock_data: CreateMockData) -> ResponseCreateMock:
        new_id = max(mock.id for mock in self.data) + 1  #
        new_mock = MockDataResponse(id=new_id, info=create_mock_data.info)
        self.data.append(new_mock)
        return ResponseCreateMock(id=new_id)

    async def mock(self, id: int) -> MockDataResponse:
        for mock in self.data:
            if mock.id == id:
                return mock
        raise ValueError("Data with given ID not found.")

Создадим файл main.py для старта приложения. Если у вас есть необходимость добавить какую-то логику перед запуском сервера, вы можете сделать это в этом файле:

from endpoints.router_init import app

Теперь запустим сервис командой:

uvicorn main:app --host 0.0.0.0 --port 8080

Проверим работу сервиса, отправив в него запрос:

curl --location 'http://localhost:8080/mock?id=1'
{"id":1,"info":"hello world!"}

В результате мы получили работающий сервер на основе нашей документации и потратили минимум времени на его реализацию.

Заключение

Использование Swagger и OpenAPI Generator значительно упрощает разработку API, сокращает количество рутинной работы и улучшает качество документации. Этот подход позволяет быстро перейти от описания API к готовому коду, что экономит время и силы. Однако на данный момент не все возможности OpenAPI поддерживаются для генерации кода под Flask и FastAPI. Тем не менее, OpenAPI Generator активно развивается, и эти ограничения могут быть устранены в будущем.

Исходный код

Комментарии (0)