Статусы заказа в FastAPI: Enum для входа и правила переходов
В базе появились одновременно pending, Pending и pendng. Все три строки задумывались как один статус заказа, но фильтры считают их разными. Перечисление допустимых значений решает форму входа. После этого остаётся второй вопрос: можно ли уже оплаченный заказ вернуть в ожидание оплаты.
Две проверки на разных этапах
Строковый Enum задаёт словарь допустимых статусов. FastAPI разбирает JSON в этот тип и отклоняет неизвестное значение до обработчика с 422. Значения остаются удобными строками в JSON и видны в OpenAPI. Клиент может построить форму выбора из того же договора.
Однако любое допустимое значение не обязательно допустимо сейчас. Переход pending → paid разрешён, paid → pending запрещён. Такое правило зависит от текущего заказа, поэтому выполняется после его получения. Для конфликта с текущим состоянием вернём 409. Клиент отличит опечатку в названии от команды, которую нельзя выполнить над этим заказом.
Реализуем маленькую машину состояний
Словарь allowed делает переходы обозримыми. Повтор того же статуса считаем успешной идемпотентной командой. Это осознанный выбор: некоторым процессам нужен запрет повторного действия, но тогда проверять надо саму операцию, например повторное списание, а не только значение поля.
Сохраните весь блок в файл и запустите Python 3.11+ в отдельном окружении с пакетами fastapi, httpx. Для моделей используется Pydantic v2; отдельный сервер для TestClient не нужен.
from enum import Enum
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel
class Status(str, Enum):
pending = "pending"
paid = "paid"
cancelled = "cancelled"
class Change(BaseModel):
status: Status
app = FastAPI()
orders = {1: Status.pending}
allowed = {Status.pending: {Status.paid, Status.cancelled},
Status.paid: set(), Status.cancelled: set()}
@app.put("/orders/{order_id}/status")
def change(order_id: int, data: Change):
if order_id not in orders:
raise HTTPException(404, "order not found")
current = orders[order_id]
if data.status != current and data.status not in allowed[current]:
raise HTTPException(409, "transition is not allowed")
orders[order_id] = data.status
return {"status": data.status}
with TestClient(app) as client:
assert client.put("/orders/1/status", json={"status": "Paid"}).status_code == 422
assert client.put("/orders/1/status", json={"status": "paid"}).json() == {"status": "paid"}
assert client.put("/orders/1/status", json={"status": "paid"}).status_code == 200
assert client.put("/orders/1/status", json={"status": "pending"}).status_code == 409
assert client.put("/orders/99/status", json={"status": "paid"}).status_code == 404
print("value validation, transition and missing order checked")Как читать матрицу ответов
Неизвестный статус не меняет заказ. Разрешённый переход меняет его один раз. Повтор paid сохраняет paid, а попытка вернуться в pending даёт конфликт. Запрос к отсутствующему заказу возвращает 404. Эта последовательность проверяет поведение во времени, которое нельзя увидеть отдельным созданием Enum.
В базе переход должен проверяться атомарно вместе с обновлением. Между чтением pending и записью paid другой запрос способен отменить заказ. Для реального сервиса используйте транзакцию и подходящий контроль конкуренции: условное обновление по ожидаемому состоянию, блокировку или номер версии. Выбирайте механизм под базу; один Python-словарь в примере объясняет HTTP-контракт.
Как расширять статусы без хаоса
Добавление shipped меняет не только список строк. Решите, откуда в него можно перейти, какие действия выполняются и что увидит старый клиент. Перечисление помогает найти точки изменения в коде, но не заменяет сценарии продукта. Полезно хранить тест на каждый разрешённый переход и один запрет из каждого конечного состояния.
Если хочется разрешить произвольную строку ради будущих статусов, разделите вход команды и чтение истории. Команда может оставаться строгой, а клиент чтения — корректно отображать неизвестный статус. Тогда появление нового состояния не превращает опечатки в разрешённые команды.
Следующая практика
В открытом уроке о полном и частичном обновлении вы закрепите, как API меняет ресурс. После выполнения базовой практики добавьте матрицу разрешённых переходов статуса и проверьте конфликтную команду. Пройдите бесплатный урок об обновлении ресурса.
Развивайте обновление ресурса вместе с хранением и проверкой пользователя: эти части входят в бесплатную программу FastAPI. Начните с базового CRUD, затем добавляйте правила собственного процесса. Откройте программу курса.
Продолжить на практике
- FastAPI для начинающих: API с базой данных и тестами ↗
Соберёте свой первый полноценный backend на FastAPI: с базой данных, регистрацией, JWT-авторизацией и автотестами. 34 коротких урока и 140 заданий с проверкой — бесплатно.
