Практикум — сервис каталога на 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-Key→401 - Повтор с тем же
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.