Перейти к основному содержимому

Практикум — сервис каталога на Python

Разработчику

Практикум, шаг 4 из 8. Реализация catalog-api. Контракт — шаг 2, DTO — шаг 3.


Подготовка проекта

mkdir OrderDesk\catalog-api
cd OrderDesk\catalog-api
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install fastapi uvicorn[standard] pydantic sqlalchemy python-jose[cryptography]

requirements.txt:

fastapi>=0.115.0
uvicorn[standard]>=0.32.0
pydantic>=2.0
sqlalchemy>=2.0
python-jose[cryptography]>=3.3

База данных (SQLite)

app/db.py — упрощённо:

Код ITЗагрузка примера кода…


Приложение FastAPI

app/main.py (основные маршруты):

Код ITЗагрузка примера кода…

schemas.py — полные классы в шаге 3. Файл app/mapping.py:

from decimal import Decimal
from .schemas import ProductResponse

def to_product_response(row) -> ProductResponse:
return ProductResponse(
id=row.id,
sku=row.sku,
name=row.name,
price=Decimal(row.price_cents) / 100,
stockAvailable=row.stock_available,
createdAt=row.created_at,
)

Отмена резерва

DELETE /api/v1/reservations/{reservationId} возвращает остаток на склад — вызывается из orders-api при отмене заказа:

@app.delete("/api/v1/reservations/{reservation_id}", status_code=204,
dependencies=[Depends(require_api_key)])
def delete_reservation(reservation_id: str, db: Session = Depends(get_db)):
row = db.query(ReservationRow).filter_by(id=reservation_id).first()
if not row:
raise HTTPException(status_code=404, detail="Reservation not found")
product = db.query(ProductRow).filter_by(id=row.product_id).first()
if product:
product.stock_available += row.quantity
db.delete(row)
db.commit()

Middleware X-Request-Id

Код ITЗагрузка примера кода…


Запуск и быстрая проверка

cd OrderDesk\catalog-api
uvicorn app.main:app --reload --port 8100

Документация Swagger: http://localhost:8100/docs

curl -X POST http://localhost:8100/api/v1/products `
-H "Content-Type: application/json" `
-d '{"sku":"T-1","name":"Test item","price":10,"stockAvailable":50}'

Чек-лист готовности catalog-api

  • GET /api/v1/products возвращает список
  • POST /api/v1/reservations без X-Api-Key401
  • Повтор с тем же Idempotency-Key → тот же reservationId
  • Резерв при нехватке остатка → 409
  • DELETE резерва возвращает остаток на склад

Мини-тест на pytest

from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_reserve_idempotent():
client.post("/api/v1/products", json={"sku":"T","name":"N","price":1,"stockAvailable":5})
pid = client.get("/api/v1/products").json()[0]["id"]
headers = {"X-Api-Key": "dev-catalog-key-change-me", "Idempotency-Key": "k1"}
body = {"productId": pid, "quantity": 1, "orderRef": "ord_1"}
r1 = client.post("/api/v1/reservations", json=body, headers=headers)
r2 = client.post("/api/v1/reservations", json=body, headers=headers)
assert r1.json()["reservationId"] == r2.json()["reservationId"]

Далее orders-api вызывает этот сервис: шаг 5.