Урок курса

Query-параметры и фильтрация каталога

FastAPI для начинающих: API с базой данных и тестами

Path-параметры брали значения прямо из сегментов пути — /api/rooms/2 давал room_id = 2. Query-параметры устроены иначе: они находятся в части URL после ?. Именно с ними мы будем фильтровать каталог переговорных.

Как аргумент попадает из query-строки в функцию

Для простых аргументов int, str и bool FastAPI по умолчанию применяет такое правило: если имя аргумента функции совпадает с именем сегмента пути в фигурных скобках — значение берётся из пути. Если не совпадает — FastAPI ищет его в query-строке.

Вот конкретный пример. Маршрут объявлен как @app.get("/api/rooms") — без переменных сегментов. Тогда аргумент min_capacity: int = 0 получит значение из ?min_capacity=10, а не из пути, которого там попросту нет.

Имя в функции должно совпадать с именем параметра в URL. Если написать ?min_capacity=10, а в функции принять capacity, FastAPI не свяжет их: capacity будет искаться как отдельный query-параметр.

Здесь кроется частая ошибка при работе с path-параметрами. Возьмём маршрут:

@app.get("/api/rooms/{room_id}")
def get_room(id: int):
    ...

Аргумент называется id, а в шаблоне пути стоит {room_id}. FastAPI не свяжет id с сегментом пути — он будет ожидать ?id=... в query-строке. Запрос GET /api/rooms/1 без этого параметра получит ответ 422.

Исправление простое — согласовать имена:

@app.get("/api/rooms/{room_id}")
def get_room(room_id: int):
    ...

Теперь room_id найдётся в пути, и запрос GET /api/rooms/1 отработает корректно. Для фильтра списка ситуация обратная: min_capacity не упоминается в фигурных скобках маршрута "/api/rooms", поэтому FastAPI читает его из query-строки — никакой дополнительной аннотации для этого не нужно.

Фильтр отсутствует, true или false

Чтобы фильтр был необязательным, аргументу задают значение по умолчанию. Для числового порога это выглядит так:

def list_rooms(min_capacity: int = 0):

Если ?min_capacity в запросе нет, FastAPI подставит 0, и условие capacity >= 0 пропустит все комнаты.

С булевым фильтром нужно различать три состояния: фильтр не передан, передан true и передан false. Значение 0 здесь не подойдёт — нужен None:

from typing import Optional

def list_rooms(min_capacity: int = 0, has_projector: Optional[bool] = None):

Optional[bool] означает, что аргумент может быть True, False или None. Именно = None определяет, что получит функция, если параметр не передан. Импорт Optional из typing обязателен для Python 3.8 и 3.9; начиная с 3.10 можно писать bool | None, но Optional работает везде.

Как FastAPI преобразует значения из строки:

  • ?has_projector=trueTrue
  • ?has_projector=falseFalse
  • ?has_projector=1True
  • ?has_projector=0False
  • параметр отсутствует → None

Если передать ?has_projector=maybe, FastAPI вернёт 422 ещё до вызова функции — строка maybe не является допустимым булевым значением.

Важно не путать это с обычным Python. Выражение bool("false") возвращает True, потому что непустая строка — истинна. FastAPI делает другое: он анализирует содержимое строки и преобразует её в настоящий True или False. Это разные механизмы, и полагаться на bool() вместо аннотации типа нельзя.

Два условия фильтрации одного каталога

Каталог хранится как словарь rooms_by_id, где ключ — числовой id, а значение — словарь с полями комнаты. Нас интересуют два поля: capacity (количество мест) и has_projector (булево наличие проектора).

Фильтрация работает в два шага. Первый — отсев по вместимости:

result = [r for r in rooms_by_id.values() if r["capacity"] >= min_capacity]

Мы берём все значения словаря и оставляем только те комнаты, где мест не меньше, чем запрошено. Если min_capacity=0 (значение по умолчанию), условие пройдут все комнаты.

Второй шаг — фильтр по проектору, но только если он был передан:

if has_projector is not None:
    result = [r for r in result if r["has_projector"] == has_projector]

Проверка is not None принципиальна. Если написать if has_projector:, то явно переданный False тоже не пройдёт условие — Python посчитает его ложным, и фильтрация по комнатам без проектора молча отключится. Проверка is not None отличает «фильтр не задан» от «фильтр задан как False».

Когда has_projector=False, второй шаг оставит только комнаты с has_projector == False — то есть именно те, где проектора нет. Когда параметр не передан и равен None, шаг пропускается, и ограничений по оборудованию нет.

Оба условия применяются последовательно к одному списку, поэтому результат — пересечение: комнаты, удовлетворяющие обоим критериям одновременно.

При этом rooms_by_id остаётся нетронутым. Мы создаём новые списки через list comprehension, а не удаляем записи из словаря. Следующий запрос снова получит полный каталог.

Полный маршрут и проверка результатов запросов

Каталог содержит три переговорные: Лондон (6 мест, проектор есть), Берлин (12 мест, проектора нет), Токио (4 места, проектор есть).

from fastapi import FastAPI
from typing import Optional

app = FastAPI()

rooms_by_id = {
    1: {"id": 1, "name": "Лондон", "floor": 2, "capacity": 6, "has_projector": True},
    2: {"id": 2, "name": "Берлин", "floor": 3, "capacity": 12, "has_projector": False},
    3: {"id": 3, "name": "Токио", "floor": 1, "capacity": 4, "has_projector": True},
}

@app.get("/api/rooms")
def list_rooms(min_capacity: int = 0, has_projector: Optional[bool] = None):
    result = [r for r in rooms_by_id.values() if r["capacity"] >= min_capacity]
    if has_projector is not None:
        result = [r for r in result if r["has_projector"] == has_projector]
    return result

Проверим разные комбинации запросов:

  • GET /api/rooms — оба параметра по умолчанию, фильтры не применяются. Ответ 200: все три комнаты.
  • GET /api/rooms?min_capacity=6 — остаются Лондон (6 ≥ 6) и Берлин (12 ≥ 6). Токио (4) не проходит. Ответ 200: два объекта.
  • GET /api/rooms?min_capacity=6&has_projector=true — из Лондона и Берлина проектор есть только у Лондона. Ответ 200: один объект.
  • GET /api/rooms?has_projector=false — без ограничения по вместимости, только без проектора. Ответ 200: только Берлин.
  • GET /api/rooms?min_capacity=100 — ни одна комната не вмещает 100 человек. Ответ 200: пустой массив [].
  • GET /api/rooms?min_capacity=abc — строка abc не является целым числом. FastAPI вернёт 422 до вызова функции.
  • GET /api/rooms?has_projector=maybe — недопустимое булево значение. Тоже 422.

После любого из этих запросов GET /api/rooms без параметров снова вернёт все три комнаты: словарь rooms_by_id не изменился.

Исходные комнаты: Лондон — 6 мест с проектором, Берлин — 12 без проектора, Токио — 4 с проектором. После capacity >= 6 остаются Лондон и Берлин. has_projector=None оставляет обоих, True — Лондон, False — Берлин.
После отбора по вместимости None не ограничивает оборудование, а False оставляет только комнаты без проектора.

Попробуйте решить

Маршрут принимает has_projector: Optional[bool] = None. Клиент отправляет GET /api/rooms?has_projector=true. Какое значение получит функция-обработчик?

Продолжить с проверкой и прогрессом

Откройте интерактивный раннер с заданиями урока.

Перейти к интерактивному уроку