Как делать11 октября 2026 г.9 мин чтения
Backend-разработкаШаг 7 из 13

FastAPI: API на Python с валидацией и документацией

Коротко

FastAPI — Python-фреймворк для HTTP API, в котором типы параметров и модели Pydantic одновременно служат валидацией входа, фильтром ответа и описанием API: по ним строится схема OpenAPI и интерактивная документация на /docs. Маршрут — обычная функция с декоратором @app.get или @app.post, общая логика (доступ к хранилищу, проверка ключа) подключается через Depends, а тесты пишут обычными функциями pytest с TestClient. В уроке собран API заявок с семью тестами, проверенный на FastAPI 0.143 и Pydantic 2.14.

Пошаговый урокУровень: НачальныйFastAPIПрактика: ~1,5–3 ч

После материала вы сможете

  • создать виртуальное окружение, установить fastapi[standard] и запустить API командой fastapi dev
  • описать модели Pydantic с ограничениями Field и объяснить, откуда берётся ответ 422 и как читать его loc и type
  • сделать CRUD-эндпоинты с кодами 201, 204, 401 и 404 и подключить проверку ключа через Depends
  • покрыть API тестами TestClient и pytest с подменой зависимости через app.dependency_overrides

Проверено на

Python 3.12FastAPI 0.143Pydantic 2.14Starlette 1.7pytest 9.1httpx2 2.13

Сверено с документацией: 11 октября 2026 г.

Содержание
  1. 01Что такое FastAPI и что мы соберём
  2. 02Окружение: venv и зависимости
  3. 03Модели Pydantic: что API принимает и отдаёт
  4. 04Хранилище и зависимости: Depends
  5. 05Маршруты, коды ответов и ошибки
  6. 06Запуск и автодокументация /docs
  7. 07Тесты: TestClient и pytest
  8. 08Частые ошибки и упражнения

Что такое FastAPI и что мы соберём

Как устроен бэкенд в целом — сервер, база, API — мы разбирали в материале «Что такое бэкенд», а формат запросов и ответов — в уроке про HTTP и JSON. Здесь — практика на Python. FastAPI построен на двух библиотеках: Starlette отвечает за веб-часть (маршрутизация, запросы, ответы), Pydantic — за данные. Вы описываете, какие данные принимает и отдаёт эндпоинт — адрес плюс HTTP-метод, — обычными аннотациями типов Python, а фреймворк сам проверяет вход, сериализует ответ в JSON и документирует API.

Учебный проект — API заявок с сайта. Форма на сайте отправляет заявку без авторизации, а менеджер через закрытые эндпоинты смотрит список, меняет статус и удаляет спам. Данные хранятся в памяти процесса: так урок не зависит от базы данных, а тесты получают чистое хранилище на каждый запуск.

POST /leads

Кто вызывает
форма на сайте
Успех
201 Created
Ошибки
422 — неверные данные

GET /leads?status=&limit=

Кто вызывает
менеджер, нужен ключ
Успех
200 OK
Ошибки
401, 422

GET /leads/{lead_id}

Кто вызывает
менеджер
Успех
200 OK
Ошибки
401, 404

PATCH /leads/{lead_id}

Кто вызывает
менеджер
Успех
200 OK
Ошибки
401, 404, 422

DELETE /leads/{lead_id}

Кто вызывает
менеджер
Успех
204 No Content
Ошибки
401, 404

GET /health

Кто вызывает
мониторинг
Успех
200 OK
Ошибки
—

Окружение: venv и зависимости

Зависимости проекта ставят в виртуальное окружение — изолированную копию интерпретатора со своими пакетами. Документация Python описывает его как одноразовое: его не коммитят в Git и при необходимости удаляют и создают заново. Официальное руководство FastAPI сейчас показывает установку через менеджер uv, а для pip предлагает создать и активировать окружение и выполнить pip install "fastapi[standard]" — этот путь мы и используем.

bash
mkdir leads-api
cd leads-api
python -m venv .venv
source .venv/bin/activate          # Windows PowerShell: .venv\Scripts\Activate.ps1
python -m pip install "fastapi[standard]" pytest httpx2
mkdir app tests                    # и пустой файл app/__init__.py
Создание проекта: окружение, пакеты, папки. Кавычки вокруг fastapi[standard] нужны, чтобы квадратные скобки не интерпретировала оболочка
  • fastapi[standard] — FastAPI с набором дополнительных пакетов: сервер uvicorn, команда fastapi, email-validator для Pydantic, jinja2, python-multipart и httpx.
  • pytest — тестовый раннер. Тесты в нём — обычные функции с assert.
  • httpx2 — HTTP-клиент, на котором построен TestClient в Starlette 1.7. Документация Starlette называет обычный httpx поддерживаемым, но устаревшим вариантом. Без httpx2 тесты проходят, но pytest печатает предупреждение StarletteDeprecationWarning: мы проверили оба варианта.

Чтобы pytest находил пакет app из папки tests, добавьте в корень pyproject.toml. Таблицу [tool.pytest] с настоящими TOML-типами pytest поддерживает с версии 9.0. В более старых версиях используется [tool.pytest.ini_options].

pyproject.toml
[tool.pytest]
pythonpath = ["."]
testpaths = ["tests"]
pythonpath добавляет корень проекта в путь импорта, testpaths говорит, где искать тесты

Итоговая структура: app/__init__.py (пустой), app/main.py, tests/test_leads.py, pyproject.toml. Папку .venv добавьте в .gitignore.

Модели Pydantic: что API принимает и отдаёт

Модель Pydantic — класс-наследник BaseModel с аннотированными полями. Pydantic гарантирует, что поля созданного объекта соответствуют объявленным типам, а при неверных данных выбрасывает ValidationError. FastAPI перехватывает эту ошибку для тела запроса и сам отвечает клиенту кодом 422 со списком проблем. Ограничения задаются через Field внутри Annotated: длина строки, регулярное выражение, числовые границы.

app/main.py
import os
import secrets
from typing import Annotated, Literal

from fastapi import Depends, FastAPI, Header, HTTPException, Query, status
from pydantic import BaseModel, Field

app = FastAPI(title="Leads API", version="1.0.0")

Service = Literal["website", "mobile-app", "crm"]
LeadStatus = Literal["new", "in_progress", "done"]


class LeadIn(BaseModel):
    name: Annotated[str, Field(min_length=2, max_length=80)]
    phone: Annotated[str, Field(pattern=r"^\+?\d{10,15}$", examples=["+77011234567"])]
    service: Service
    comment: Annotated[str | None, Field(max_length=500)] = None


class Lead(LeadIn):
    id: int
    status: LeadStatus = "new"


class LeadStatusUpdate(BaseModel):
    status: LeadStatus
Часть 1 из 3: импорты, приложение и модели. Три части подряд образуют весь файл

Field(min_length=2, max_length=80)

Что проверяется
длина строки
Ошибка в ответе 422
string_too_short / string_too_long

Field(pattern=r"^\+?\d{10,15}$")

Что проверяется
строка совпадает с регулярным выражением
Ошибка в ответе 422
string_pattern_mismatch

Literal["website", "mobile-app", "crm"]

Что проверяется
значение из фиксированного списка
Ошибка в ответе 422
literal_error

str | None = None

Что проверяется
поле необязательное, по умолчанию None
Ошибка в ответе 422
—

Query(ge=1, le=100)

Что проверяется
число в диапазоне 1–100 (для параметра запроса)
Ошибка в ответе 422
greater_than_equal / less_than_equal

Модели разделены по назначению. LeadIn — то, что присылает форма: клиент не может передать id или status. Lead наследует все поля LeadIn и добавляет поля сервера. LeadStatusUpdate — тело PATCH с единственным полем. Такое разделение — официальный приём FastAPI: в разделе о модели ответа документация показывает пару UserIn с паролем и UserOut без него. Тип возврата функции работает как фильтр: FastAPI проверяет ответ, сериализует его через Pydantic и отбрасывает поля, которых нет в модели. Лишние поля во входящем JSON Pydantic по умолчанию игнорирует: мы отправили в POST /leads поля "id": 99 и "status": "done" — заявка всё равно создалась с id 1 и статусом new.

Хранилище и зависимости: Depends

Зависимость в FastAPI — функция, которую фреймворк вызывает перед обработчиком и результат которой передаёт ему в параметр. Документация называет это внедрением зависимостей (dependency injection): код маршрута объявляет, что ему нужно для работы, а FastAPI это предоставляет. Зависимость принимает те же параметры, что и обработчик: заголовки, параметры запроса и даже другие зависимости.

app/main.py
class LeadStore:
    """In-memory storage for the lesson and tests. A real project uses a database."""

    def __init__(self) -> None:
        self._items: dict[int, Lead] = {}
        self._next_id = 1

    def add(self, data: LeadIn) -> Lead:
        lead = Lead(id=self._next_id, **data.model_dump())
        self._items[lead.id] = lead
        self._next_id += 1
        return lead

    def list(self, status: LeadStatus | None, limit: int) -> list[Lead]:
        leads = [lead for lead in self._items.values() if status is None or lead.status == status]
        return leads[:limit]

    def get(self, lead_id: int) -> Lead | None:
        return self._items.get(lead_id)

    def save(self, lead: Lead) -> None:
        self._items[lead.id] = lead

    def delete(self, lead_id: int) -> bool:
        return self._items.pop(lead_id, None) is not None


store = LeadStore()


def get_store() -> LeadStore:
    return store


def require_api_key(x_api_key: Annotated[str | None, Header()] = None) -> None:
    expected = os.environ.get("LEADS_API_KEY")
    if not expected or x_api_key is None or not secrets.compare_digest(x_api_key, expected):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or missing API key")


StoreDep = Annotated[LeadStore, Depends(get_store)]
admin_only = [Depends(require_api_key)]


def find_lead(lead_id: int, leads: LeadStore) -> Lead:
    lead = leads.get(lead_id)
    if lead is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Lead not found")
    return lead
Часть 2 из 3: хранилище, зависимости и общий поиск заявки
  1. get_store отдаёт хранилище

    Маршруты получают его через параметр типа StoreDep и не обращаются к глобальной переменной store напрямую. В тестах эту зависимость подменяют на чистое хранилище — без правки кода приложения.

  2. StoreDep — псевдоним типа

    Annotated[LeadStore, Depends(get_store)] записан один раз и переиспользуется. Документация FastAPI рекомендует форму с Annotated (с версии 0.95) и показывает именно такой псевдоним.

  3. require_api_key проверяет заголовок

    Параметр x_api_key с Header() FastAPI читает из заголовка x-api-key: подчёркивания автоматически превращаются в дефисы, регистр заголовков не важен. secrets.compare_digest сравнивает строки за время, не зависящее от совпадающего префикса.

  4. admin_only подключается в декоратор

    Список dependencies=[Depends(require_api_key)] в декораторе выполняет проверку, но не передаёт её результат в функцию. Это официальный способ для зависимостей, которые только разрешают или запрещают запрос.

find_lead — обычная вспомогательная функция, не зависимость: она выбрасывает HTTPException с кодом 404, если заявки нет. HTTPException — исключение Python с данными для HTTP-ответа: его выбрасывают через raise, а не возвращают, и FastAPI превращает его в ответ вида {"detail": "Lead not found"}.

Маршруты, коды ответов и ошибки

Маршрут — функция с декоратором, где указаны метод и путь. Параметры функции FastAPI раскладывает сам: lead_id: int совпадает с {lead_id} в пути — это параметр пути; модель Pydantic — тело запроса; простые типы, которых нет в пути, — параметры запроса (query string, часть URL после ?). Код успешного ответа задают в декораторе через status_code; константы из fastapi.status хранят те же числа, но читаются лучше.

app/main.py
@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "ok"}


@app.post("/leads", status_code=status.HTTP_201_CREATED)
def create_lead(data: LeadIn, leads: StoreDep) -> Lead:
    return leads.add(data)


@app.get("/leads", dependencies=admin_only)
def list_leads(
    leads: StoreDep,
    status_filter: Annotated[LeadStatus | None, Query(alias="status")] = None,
    limit: Annotated[int, Query(ge=1, le=100)] = 50,
) -> list[Lead]:
    return leads.list(status_filter, limit)


@app.get("/leads/{lead_id}", dependencies=admin_only)
def read_lead(lead_id: int, leads: StoreDep) -> Lead:
    return find_lead(lead_id, leads)


@app.patch("/leads/{lead_id}", dependencies=admin_only)
def update_lead_status(lead_id: int, update: LeadStatusUpdate, leads: StoreDep) -> Lead:
    lead = find_lead(lead_id, leads)
    updated = lead.model_copy(update=update.model_dump())
    leads.save(updated)
    return updated


@app.delete("/leads/{lead_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=admin_only)
def delete_lead(lead_id: int, leads: StoreDep) -> None:
    if not leads.delete(lead_id):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Lead not found")
Часть 3 из 3: маршруты
  • status_filter с Query(alias="status"): в URL параметр называется status, но в Python так уже называется импортированный модуль fastapi.status, поэтому имя переменной другое.
  • PATCH берёт найденную заявку и через model_copy(update=...) создаёт копию с новым статусом — приём из раздела документации о частичных обновлениях.
  • DELETE отвечает 204 No Content. У такого ответа не должно быть тела: FastAPI это учитывает и в документации указывает, что тела нет.
  • Функции объявлены через обычный def. FastAPI выполняет такие обработчики в пуле потоков, чтобы они не блокировали сервер. async def нужен, когда внутри есть await — например, асинхронный драйвер базы. Если сомневаетесь, документация советует обычный def.

200 OK

Когда в нашем API
успешный GET и PATCH
Кто формирует
FastAPI по умолчанию

201 Created

Когда в нашем API
заявка создана
Кто формирует
status_code в декораторе

204 No Content

Когда в нашем API
заявка удалена
Кто формирует
status_code в декораторе

401 Unauthorized

Когда в нашем API
нет ключа или он неверный
Кто формирует
HTTPException в зависимости

404 Not Found

Когда в нашем API
заявки с таким id нет
Кто формирует
HTTPException в find_lead

422 Unprocessable Content

Когда в нашем API
тело или параметры не прошли валидацию
Кто формирует
FastAPI автоматически

Запуск и автодокументация /docs

Команда fastapi dev запускает сервер разработки с автоперезагрузкой при сохранении файлов. Перед запуском задайте ключ администратора в переменной окружения — значение ниже только для локальной проверки.

terminal
$ export LEADS_API_KEY=dev-secret-change-me   # PowerShell: $env:LEADS_API_KEY = "dev-secret-change-me"
$ fastapi dev app/main.py

 ⚡️ Starting FastAPI in development mode

 🐍 Using import string: app.main:app

 🌐 Server started at http://127.0.0.1:8000
    Documentation at http://127.0.0.1:8000/docs

  Logs:

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [74464] using WatchFiles
INFO:     Started server process [42936]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
Реальный вывод запуска (строка про отслеживаемую папку убрана)

Проверим API из второго терминала. В выводе ниже — реальные ответы сервера: успешное создание, три ошибки валидации в одном ответе, отказ без ключа, список с ключом и 404.

terminal
$ curl -i -X POST http://127.0.0.1:8000/leads -H "Content-Type: application/json" -d '{"name": "Aigerim", "phone": "+77011234567", "service": "website"}'
HTTP/1.1 201 Created
date: Sun, 11 Oct 2026 23:13:28 GMT
server: uvicorn
content-length: 98
content-type: application/json

{"name":"Aigerim","phone":"+77011234567","service":"website","comment":null,"id":1,"status":"new"}

$ curl -s -X POST http://127.0.0.1:8000/leads -H "Content-Type: application/json" -d '{"name": "A", "phone": "call me", "service": "seo"}' | python -m json.tool
{
    "detail": [
        {
            "type": "string_too_short",
            "loc": [
                "body",
                "name"
            ],
            "msg": "String should have at least 2 characters",
            "input": "A",
            "ctx": {
                "min_length": 2
            }
        },
        {
            "type": "string_pattern_mismatch",
            "loc": [
                "body",
                "phone"
            ],
            "msg": "String should match pattern '^\\+?\\d{10,15}$'",
            "input": "call me",
            "ctx": {
                "pattern": "^\\+?\\d{10,15}$"
            }
        },
        {
            "type": "literal_error",
            "loc": [
                "body",
                "service"
            ],
            "msg": "Input should be 'website', 'mobile-app' or 'crm'",
            "input": "seo",
            "ctx": {
                "expected": "'website', 'mobile-app' or 'crm'"
            }
        }
    ]
}
$ curl -s http://127.0.0.1:8000/leads
{"detail":"Invalid or missing API key"}
$ curl -s http://127.0.0.1:8000/leads -H "X-API-Key: dev-secret-change-me"
[{"name":"Aigerim","phone":"+77011234567","service":"website","comment":null,"id":1,"status":"new"}]
$ curl -s http://127.0.0.1:8000/leads/42 -H "X-API-Key: dev-secret-change-me"
{"detail":"Lead not found"}
Реальные запросы к запущенному серверу

Теперь откройте http://127.0.0.1:8000/docs. Это Swagger UI, построенный по схеме http://127.0.0.1:8000/openapi.json (OpenAPI 3.1). Альтернативный вид той же схемы — ReDoc на /redoc. В нашей схеме три пути и пять моделей, включая служебные HTTPValidationError и ValidationError. Параметры status, limit и заголовок x-api-key у GET /leads попали в документацию сами — из аннотаций.

  • В /docs раскройте POST /leads, нажмите «Try it out», отправьте заявку — ответ 201 с id
  • Отправьте телефон с буквами — ответ 422 и loc: ["body", "phone"]
  • Вызовите GET /leads без заголовка — 401; с x-api-key из переменной окружения — список
  • Раскройте схему Lead внизу страницы: ограничения длины и шаблон телефона перенесены из Field
  • Откройте /openapi.json — это машиночитаемый контракт, по которому фронтенд или партнёр могут сгенерировать клиент

Тесты: TestClient и pytest

TestClient вызывает приложение напрямую, без запущенного сервера и сети. Документация FastAPI подчёркивает: тестовые функции — обычные def, а вызовы клиента — без await, поэтому pytest работает без дополнительных плагинов. Фикстура pytest — функция, которая готовит окружение для теста. Наша фикстура client делает три вещи: задаёт ключ через monkeypatch.setenv, подменяет зависимость get_store на новое хранилище через app.dependency_overrides и после теста сбрасывает подмены.

tests/test_leads.py
import pytest
from fastapi.testclient import TestClient

from app.main import LeadStore, app, get_store

API_KEY = "test-key"
ADMIN = {"X-API-Key": API_KEY}
VALID_LEAD = {"name": "Aigerim", "phone": "+77011234567", "service": "website"}


@pytest.fixture
def client(monkeypatch):
    monkeypatch.setenv("LEADS_API_KEY", API_KEY)
    fresh_store = LeadStore()
    app.dependency_overrides[get_store] = lambda: fresh_store
    yield TestClient(app)
    app.dependency_overrides = {}


def test_create_lead_returns_201_with_id(client):
    response = client.post("/leads", json=VALID_LEAD)
    assert response.status_code == 201
    assert response.json() == {**VALID_LEAD, "comment": None, "id": 1, "status": "new"}


def test_invalid_phone_returns_422(client):
    response = client.post("/leads", json={**VALID_LEAD, "phone": "call me"})
    assert response.status_code == 422
    error = response.json()["detail"][0]
    assert error["loc"] == ["body", "phone"]
    assert error["type"] == "string_pattern_mismatch"


def test_admin_routes_require_api_key(client):
    assert client.get("/leads").status_code == 401
    assert client.get("/leads", headers={"X-API-Key": "wrong"}).status_code == 401
    assert client.get("/leads", headers=ADMIN).status_code == 200


def test_missing_lead_returns_404(client):
    response = client.get("/leads/999", headers=ADMIN)
    assert response.status_code == 404
    assert response.json() == {"detail": "Lead not found"}


def test_update_status_and_filter(client):
    client.post("/leads", json=VALID_LEAD)
    client.post("/leads", json={**VALID_LEAD, "service": "crm"})

    response = client.patch("/leads/2", json={"status": "in_progress"}, headers=ADMIN)
    assert response.status_code == 200
    assert response.json()["status"] == "in_progress"

    in_progress = client.get("/leads", params={"status": "in_progress"}, headers=ADMIN).json()
    assert [lead["id"] for lead in in_progress] == [2]


def test_limit_is_validated(client):
    response = client.get("/leads", params={"limit": 0}, headers=ADMIN)
    assert response.status_code == 422


def test_delete_returns_204_and_removes_lead(client):
    client.post("/leads", json=VALID_LEAD)
    response = client.delete("/leads/1", headers=ADMIN)
    assert response.status_code == 204
    assert response.content == b""
    assert client.get("/leads/1", headers=ADMIN).status_code == 404
Семь тестов: создание, валидация, доступ, 404, фильтр, граница limit, удаление
terminal
$ pytest -v
============================= test session starts =============================
platform win32 -- Python 3.12.10, pytest-9.1.1, pluggy-1.6.0 -- …\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: …\leads-api
configfile: pyproject.toml
testpaths: tests
plugins: anyio-4.15.1
collecting ... collected 7 items

tests/test_leads.py::test_create_lead_returns_201_with_id PASSED         [ 14%]
tests/test_leads.py::test_invalid_phone_returns_422 PASSED               [ 28%]
tests/test_leads.py::test_admin_routes_require_api_key PASSED            [ 42%]
tests/test_leads.py::test_missing_lead_returns_404 PASSED                [ 57%]
tests/test_leads.py::test_update_status_and_filter PASSED                [ 71%]
tests/test_leads.py::test_limit_is_validated PASSED                      [ 85%]
tests/test_leads.py::test_delete_returns_204_and_removes_lead PASSED     [100%]

============================== 7 passed in 0.43s ==============================
Реальный вывод (пути сокращены)
  • app.dependency_overrides — обычный словарь «исходная зависимость → замена». Документация советует сбрасывать его присваиванием пустого словаря — иначе подмена из одного теста проживёт до следующего.
  • Тест test_invalid_phone_returns_422 проверяет не только код, но и loc и type ошибки. Если кто-то ослабит шаблон телефона, тест это заметит.
  • Тест доступа проверяет три случая: без ключа, с неверным ключом и с верным. Проверка только успешного пути не ловит самую опасную ошибку — открытый закрытый эндпоинт.

Частые ошибки и упражнения

ModuleNotFoundError: No module named 'app' в pytest

Причина
корень проекта не в пути импорта
Исправление
pythonpath = ["."] в pyproject.toml или запуск python -m pytest из корня

fastapi: command not found

Причина
окружение не активировано или ставили без [standard]
Исправление
активируйте .venv, установите "fastapi[standard]"

StarletteDeprecationWarning про httpx в тестах

Причина
TestClient работает через старый httpx
Исправление
pip install httpx2

Тест проходит в одиночку и падает в наборе

Причина
общее состояние между тестами
Исправление
свежее хранилище в фикстуре, сброс dependency_overrides

Все закрытые маршруты отвечают 401

Причина
не задана LEADS_API_KEY в окружении сервера
Исправление
задайте переменную до fastapi dev

Вместо 404 приходит 500, в логе ResponseValidationError

Причина
return HTTPException(...) вместо raise: исключение вернулось как обычный объект и не прошло проверку модели ответа
Исправление
raise HTTPException(...)

Следующий шаг — заменить хранилище в памяти на базу данных: устройство ORM и миграций разобрано в отдельном уроке. Если нужен не только API, но и готовая админка для контента, посмотрите введение в Django: там модели, миграции и панель администратора идут «в комплекте».

Как проверено4 · пройдено1 · частичносверено 11 октября 2026 г.
  • запущено, работает

    pyproject.toml, app/main.py (3 части = весь файл), tests/test_leads.py

    python -m venv в scratchpad, pip install "fastapi[standard]" pytest httpx2: pytest -v — 7 passed, без предупреждений; FastAPI 0.143.0, Pydantic 2.14.1 (pydantic-core 2.50.1), Starlette 1.7.0, pytest 9.1.1, httpx2 2.13.1, uvicorn 0.54.0, Python 3.12.10 (Windows 11). Код в статью вставлен скриптом из файлов проекта

  • запущено, работает

    fastapi dev app/main.py + curl-запросы

    сервер на 127.0.0.1:8000: 201, 422 (три ошибки), 401, 200, 404 — вывод в статье реальный; /docs 200, /openapi.json — OpenAPI 3.1.0, 3 пути, параметры status/limit/x-api-key

  • запущено, работает

    утверждения раздела ошибок

    без pythonpath: pytest → ModuleNotFoundError: No module named 'app', python -m pytest → 7 passed; без httpx2 — StarletteDeprecationWarning, 7 passed; return HTTPException → 500 / ResponseValidationError; типы ошибок greater_than_equal, less_than_equal, string_too_long; лишние поля id/status в POST игнорируются

  • запущено, работает

    упражнения: без status_code=201; 409 для закрытой заявки; reviews.py

    1 failed (assert 200 == 201), 6 passed; ex2 — 8 passed; reviews.py + 2 теста — 2 passed

  • частично

    команды установки (bash)

    выполнены на Windows эквивалентом (.venv/Scripts/python -m pip …), активация source .venv/bin/activate на Linux/macOS не запускалась — синтаксис сверен с документацией venv

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.