Python FastAPI: OpenAPI, CRUD, PostgreSQL в Docker и внедрение зависимостей

В руководстве рассмотрим FastAPI  —  новый асинхронный Python-фреймворк для создания быстрых API по спецификации OpenAPI. 
Вы создадите свое первое приложение на FastAPI, выполняя следующие задачи:

  1. Установка зависимостей.
  2. База данных PostgreSQL в контейнере Docker.
  3. Документация и спецификация OpenAPI.

Изложенная в статье информация пригодится как новичкам в бэкенд-разработке, так и желающим найти практическое применение асинхронному программированию на Python. 

Примечание: функции из примеров написаны синхронным способом, но для поддержки асинхронного программирования достаточно превратить функции в сопрограммы при помощи ключевых слов async и await, подробнее в официальной документации FastAPI.


1. Установка зависимостей

Для выполнения всех пунктов руководства вам понадобится скачать и установить следующие зависимости:

  • Python, интерпретатор версии 3.9 и более новой.
  • Docker, контейнеризатор приложений.
  • FastAPI, веб-платформа разработки RESTful API.
  • Uvicorn, ASGI-сервер.

В качестве менеджера зависимостей для руководства выбран Poetry, но если вы с ним еще не работали, то ничего не мешает установить необходимые пакеты более классическим способом  —  через pip, с активированным виртуальным окружением или же без него.

Начнем с папки проекта и пакета-приложения в ней:

~ $ mkdir -p startlabs/lightning
~ $ cd startlabs
~/starlabs $ touch lightning/__init__.py

По умолчанию Poetry создает новую виртуальную среду вне директории проекта, но если вы предпочитаете, чтобы директория виртуального окружения находилась именно внутри проекта, то задайте эту глобальную конфигурацию:

~/starlabs $ poetry config virtualenvs.in-project true

Инициализируйте Poetry-проект:

~/startlabs $ poetry init
  1. Название пакета:
    starlabs
  2. Версия:
    0.1.0
  3. Описание:
    Бег быстрее света с 1940 года!
  4. Автор:
    Flash Gordon <[email protected]>
  5. Лицензия:
    MIT
  6. Совместимые версии Python:
    ^3.9
  7. Хотели бы вы в интерактивном режиме определить основные зависимости (main dependencies)?
     👎 Нет.
  8. Хотели бы вы в интерактивном режиме определить зависимости для разработки (development dependencies)?
     👎 Нет.
  9. Вы подтверждаете генерацию?
     👍 Да.

Теперь установите fastapi и uvicorn:

~/starlabs $ poetry add fastapi uvicorn

2. База данных PostgreSQL в контейнере Docker

Создайте файл с расширением .env для конфигурации базы данных:

DB_USERNAME=flashgordon
DB_PASSWORD=starlabs
DB_DATABASE=lightning
DB_HOST=localhost
DB_PORT=5432

Следом создайте файл с названием docker-compose.yaml:

~/starlabs $ touch docker-compose.yaml

Определите в данном файле службу базы данных по выбору (в примере указана PostgreSQL):

version: "3.9"
services:
db:
image: bitnami/postgresql
ports:
- "${DB_PORT}:5432"
environment:
- POSTGRES_USER=${DB_USERNAME}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_DATABASE}

Не стесняйтесь выбирать любой другой образ (image), если вам не нравится bitnami/postgresql.

Запустите контейнер с базой данных:

~/starlabs $ docker compose up -d

С помощью нижеуказанной команды проверьте, запущен ли контейнер:

~/starlabs $ docker compose ps

Попробуйте получить доступ к базе данных, выполнив команду:

~/starlabs $ docker compose exec db psql lightning --username flashgordon --password

Введите exit для выхода из интерактивного терминала PostgreSQL.


3. Первое FastAPI-приложение

Давайте сделаем CRUD.

CRUD  —  акроним, обозначающий четыре базовые функции, используемые при работе с базами данных: создание, чтение, модификация, удаление. (Википедия)

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

Посмотрите на структуру проекта. Внутри пакета lightning (он же ваше первое приложение на FastAPI) создайте файл config.py с кодом:

# ~/starlabs/lightning/config.py
from pydantic import BaseSettings


class DBSettings(BaseSettings):
username: str
password: str
database: str
host: str
port: str

class Config:
env_prefix = "DB_"
env_file = ".env"

Создайте еще один файл под названием dependencies.py. Данный файл ответственен за разрешение зависимостей приложения:

# ~/starlabs/lightning/dependencies.py
from functools import lru_cache

from . import config
from . import database

# Вызывается по время внедрения зависимости
def get_db() -> Session:
    db = database.SessionLocal()
    try:
        yield db
    finally:
        db.close()

# Возврат существующего экземпляра DBSettings вместо создания нового
@lru_cache 
def get_db_settings() -> config.DBSettings:
    return config.DBSettings()

Внедрение зависимости (Dependency injection)  —  процесс, когда объект отдает заботу о построении требуемых ему зависимостей внешнему, специально предназначенному для этого общему механизму. (Википедия)

В той же папке создайте файл database.py и определите подключение к базе данных:

# ~/starlabs/lightning/database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

from .dependencies import get_db_settings

settings = get_db_settings()

SQLALCHEMY_DATABASE_URL = f"postgresql://{settings.username}:{settings.password}@{settings.host}:${settings.port}/{settings.database}"

engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Model = declarative_base()

Следом, внутри приложения lightning необходимо создать два пакета:

  1. models  —  модели на базе классов представляют данные из таблиц базы данных. Подобная технология называется ORM (объектно-реляционное отображение). База данных представляется в виде конструкций объектно-ориентированного программирования, вроде классов, создается “виртуальная объектная база данных”. В приложении, рассматриваемом данным руководством, управляет всеми этими процессами open-source ORM-библиотека SQLAlchemy.
  2. schemas  —  схемы выполняют два действия: определяют классы для операций чтения и/или записи моделей, а также обрабатывают автоматические проверки. В FastAPI подобный функционал реализовывается благодаря потрясающей библиотеке pydantic.

Вы уже установили пакет pydantic вместе с пакетом FastAPI. Но кроме стандартного функционала нужно подключить и дополнительные возможности: добавьте SQLAlchemy в качестве зависимости.

Дополнительные возможности pydantic следующие.

  • Валидация электронной почты.
  • Чтение конфигураций dotenv.

Давайте обновим зависимости проекта:

~/starlabs $ poetry add sqlalchemy psycopg2-binary pydantic[email,dotenv]

Создайте файл speedster.py внутри пакета models:

# ~/starlabs/lightning/models/speedster.py
from uuid import uuid4

from sqlalchemy import Column, String, Float
from sqlalchemy.dialects.postgresql import UUID

from ..database import Model


class Speedster(Model):
__tablename__ = "speedsters"

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
name = Column(String)
gender = Column(String) # improve with Enum Choice, if you want
email = Column(String, unique=True, index=True)
password = Column(String)
velocity_kms_per_hour = Column(Float)
height_in_cm = Column(Float)
weight_in_kg = Column(Float)

Создайте еще один файл с тем же именем speedster.py, но в пакете schemas:

# ~/starlabs/lightning/schemas/speedster.py
from typing import Literal
from uuid import UUID
from pydantic import BaseModel, EmailStr

# Основная схема
class SpeedsterBase(BaseModel):
    name: str
    gender: Literal["male", "female", "other/non-binary"]
    email: EmailStr
    velocity_in_kms_per_hour: float
    height_in_cm: float
    weight_in_kg: float

# Пароль никогда не должен быть возвращен в ответе.
# Для этого используется третья схема, определенная ниже. 
# Проверяется только запрос на создание.
class SpeedsterCreate(SpeedsterBase):
    password: str

# default schema to return on a response
class Speedster(SpeedsterBase):
    id: UUID

    class Config:
        orm_mode = True # TL;DR; помогает связать модель со схемой

Теперь создайте репозиторий для speedsters, добавьте в него новый пакет repositories с файлом speedsters.py:

# ~/starlabs/lightning/repositories/speedsters.py
from typing import List
from uuid import UUID

from fastapi.params import Depends
from pydantic import EmailStr
from sqlalchemy.orm import Session

from ..models.speedster import Speedster
from ..dependencies import get_db
from ..schemas.speedster import SpeedsterCreate


class SpeedstersRepository:
    def __init__(self, db: Session = Depends(get_db)):
        self.db = db  # произойдет внедрение зависимостей

    def find(self, uuid: UUID) -> Speedster:
        query = self.db.query(Speedster)
        return query.filter(Speedster.id == uuid).first()

    def find_by_email(self, email: EmailStr):
        query = self.db.query(Speedster)
        return query.filter(Speedster.email == email).first()

    def all(self, skip: int = 0, max: int = 100) -> List[Speedster]:
        query = self.db.query(Speedster)
        return query.offset(skip).limit(max).all()

    def create(self, speedster: SpeedsterCreate) -> Speedster:
        faked_pass_hash = speedster.password + "__you_must_hash_me"

        db_speedster = Speedster(
            name=speedster.name,
            email=speedster.email,
            gender=speedster.gender,
           
velocity_kms_per_hour=speedster.velocity_kms_per_hour,
            height_in_cm=speedster.height_in_cm,
            weight_in_kg=speedster.weight_in_kg,
            password=faked_pass_hash
        )

        self.db.add(db_speedster)
        self.db.commit()
        self.db.refresh(db_speedster)

        return db_speedster

Пришло время написать конечные точки для speedsters. Создайте новый пакет под названием routers с файлом speedsters.py:

# ~/starlabs/lightning/routers/speedsters.py
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import parse_obj_as
from typing import List
from uuid import UUID

from ..schemas.speedster import Speedster, SpeedsterCreate
from ..repositories.speedsters import SpeedstersRepository


router = APIRouter(prefix="/speedsters", tags=["speedsters"])


@router.get("/", response_model=List[Speedster])
def list_speedsters(skip: int = 0, max: int = 10, speedsters: SpeedstersRepository = Depends()):
db_speedsters = speedsters.all(skip=skip, max=max)
return parse_obj_as(List[Speedster], db_speedsters)


@router.post("/", response_model=Speedster, status_code=status.HTTP_201_CREATED)
def store_speedster(speedster: SpeedsterCreate, speedsters: SpeedstersRepository = Depends()):
db_speedster = speedsters.find_by_email(email=speedster.email)

if db_speedster:
raise HTTPException(
status_code=400,
detail="Email already registered"
)

db_speedster = speedsters.create(speedster)
return Speedster.from_orm(db_speedster)


@router.get("/{speedster_id}", response_model=Speedster)
def get_speedster(speedster_id: UUID, speedsters: SpeedstersRepository = Depends()):
db_speedster = speedsters.find(speedster_id)

if db_speedster is None:
raise HTTPException(
status_code=404,
detail="Speedster not found"
)

return Speedster.from_orm(db_speedster)

Или, если хотите, запишите маршрут router.post для действия create немного по-другому:

def create(self, speedster: SpeedsterCreate) -> Speedster:
    speedster.password += "__you_must_hash_me"
    
    db_speedster = Speedster(**speedster.dict())

    self.db.add(db_speedster)
    self.db.commit()
    self.db.refresh(db_speedster)    

    return db_speedster
Отлично, вы почти полностью написали свое первое приложение на FastAPI, отсюда уже видно финишную черту!

Теперь необходимо подключить все старания к экземпляру приложения FastAPI.

В директории по адресу ~/starlabs/lightning/ создайте файл с именем main.py:

# ~/starlabs/lightning/main.py
from fastapi import FastAPI

from .database import Model, engine
from .routers import speedsters


Model.metadata.create_all(bind=engine)

app = FastAPI()
app.include_router(speedsters.router)
Пришло время запускать приложение, оценивать свои труды!

Примечание: убедитесь, что Docker-контейнер с базой данных запущен.

~/starlabs $ poetry run uvicorn lightning.main:app --reload --debug

Поздравляем, приложение запущено на сервере uvicorn и готово к обработке CRUD запросов!


4. Документация и спецификация OpenAPI

Среди прочих следует выделить замечательную особенность FastAPI: он поставляется с OpenAPI/Swagger в комплекте.

The OpenAPI Specification (спецификация OpenAPI) — спецификация интерфейса между front-end системами, кодом библиотек низкого уровня и коммерческими решениями. (Википедия)

Итак, перейдите к конечной точке http://localhost:8000/docs.

Как видите, документация поставляется вместе с FastAPI, вам не нужно ничего настраивать.

Изучите схемы документации и конечные точки, попробуйте CRUD-операции, а следом приступайте к улучшению описываемых документацией рекомендаций.

Например, когда вы открываете конечную точку с HTTP-запросом POST, создающую новые объекты (действие create из CRUD), в разделе “Example Value | Schema” показывается пример правильной схемы запроса:

{
"name": "string",
"email": "[email protected]",
"velocity_kms_per_hour": 0,
"height_in_cm": 0,
"weight_in_kg": 0,
"gender": "male",
"password": "string"
}

Это уже хорошая подсказка, ведь она указывает тип каждого ключа в объекте. Но мы можем (и должны) ее улучшить!

Откройте файл ~/startlabs/lightning/schemas/speedsters.py, чтобы изменить схемы: добавьте в каждую из них внутренний класс Config.

# ~/starlabs/lightning/schemas/speedster.py
from typing import Literal
from uuid import UUID
from pydantic import BaseModel, EmailStr

# Скорость света!
__VELOCITY_OF_LIGHT__ = 1079252848.8

class SpeedsterBase(BaseModel):
    name: str
    email: EmailStr
    velocity_kms_per_hour: str
    height_in_cm: float
    weight_in_kg: float
    gender: Literal["male", "female", "other/non-binary"]

    class Config:
        schema_extra = {
            "example": {
                "name": "Barry Allen",
                "gender": "male",
                "email": "[email protected]",
                "velocity_kms_per_hour": 2 * __VELOCITY_OF_LIGHT__,
                "height_in_cm": 182.88,
                "weight_in_kg": 88.45,
            }
        }

class SpeedsterCreate(SpeedsterBase):
    password: str

    class Config:
        schema_extra = {
            "example": {
                **SpeedsterBase.Config.schema_extra.get("example"),
                "password": "secret",
            }
        }

class Speedster(SpeedsterBase):
    id: UUID

    class Config:
        orm_mode = True

        schema_extra = {
            "example": {
                **SpeedsterBase.Config.schema_extra.get("example"),
                "id": "1fd43a2a-c0b9-4bc4-9b38-ec2d1f1b9898",
            }
        }
Перезагрузите браузер, причем вам не надо вручную останавливать и перезапускать веб-приложение, ведь во время его запуска был указан флаг --reload.

Если вы все сделали правильно, то увидите лучший пример схемы.

Хотите увидеть действительно хорошую функцию, поставляемую вместе со схемами? Прощайте, надоевшие валидации! 

Попробуйте через ключевую точку create создать объект с неверным e-mail или неправильно указанной половой принадлежностью, а FastAPI без дополнительных настроек вернет вам не только правильный код состояния, но и обработчик исключений по умолчанию для ошибок валидации схем pydantic.

И это все!

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Daniel Pinto: Tutorial: FastAPI Playground

Предыдущая статьяМир Docker и Kubernetes в аналогиях с жизнью разработчика
Следующая статьяДве малоизвестные, но полезные команды npm