Фильтрация, пагинация, опциональные параметры, валидация
Query-параметры передаются в строке запроса после
?и используются для фильтрации, пагинации, сортировки. В этой теме вы научитесь работать с опциональными параметрами, валидацией и преобразованием типов.
Query-параметры (параметры строки запроса) — это часть URL после знака вопроса:
GET /items?skip=0&limit=10&sort=name
Здесь:
skip=0 — пропустить первые 0 элементовlimit=10 — вернуть не более 10 элементовsort=name — сортировать по имениВ отличие от параметров пути (/items/{item_id}), query-параметры:
from fastapi import FastAPI
app = FastAPI()
@app.get('/items')
def get_items(skip: int = 0, limit: int = 10):
return {'skip': skip, 'limit': limit}Запрос /items?skip=20&limit=5 вернёт:
{"skip": 20, "limit": 5}skip и limit в сигнатуре функции?skip=20&limit=5)int)Запрос /items (без параметров) вернёт:
{"skip": 0, "limit": 10}Параметр может быть необязательным с помощью None:
@app.get('/items')
def get_items(skip: int = 0, limit: int = 10, q: str | None = None):
items = [{'id': i, 'name': f'Item {i}'} for i in range(skip, skip + limit)]
if q:
items = [item for item in items if q.lower() in item['name'].lower()]
return {'items': items, 'query': q}Запрос /items?q=laptop вернёт только элементы, содержащие "laptop" в названии.
Запрос /items (без q) вернёт все элементы без фильтрации.
Как и для path-параметров, можно использовать Query() для валидации:
from fastapi import FastAPI, Query
app = FastAPI()
@app.get('/items')
def get_items(
skip: int = Query(0, ge=0, description="Пропустить N элементов"),
limit: int = Query(10, ge=1, le=100, description="Вернуть не более N элементов"),
q: str | None = Query(None, min_length=3, max_length=50, description="Поисковый запрос")
):
return {'skip': skip, 'limit': limit, 'q': q}ge, le, min_length, max_length, patterndescription отображается в Swagger UIQuery() или в сигнатуре# Вариант 1: значение по умолчанию в сигнатуре
def get_items(limit: int = Query(10, ge=1)): ...
# Вариант 2: значение по умолчанию в Query()
def get_items(limit: int = Query(default=10, ge=1)): ...
# Вариант 3: Annotated (Python 3.10+, предпочтительно)
from typing import Annotated
def get_items(limit: Annotated[int, Query(ge=1, description="Лимит")]): ...Иногда параметр должен быть обязательным:
from fastapi import FastAPI, Query
app = FastAPI()
@app.get('/search')
def search(q: str = Query(..., min_length=3)):
return {'query': q}... (Ellipsis) означает, что параметр обязателен.
Запрос /search без q вернёт ошибку 422:
{
"detail": [
{
"type": "missing",
"loc": ["query", "q"],
"msg": "Field required",
"input": null
}
]
}Запрос /search?q=ab (меньше 3 символов) также вернёт 422.
FastAPI автоматически преобразует типы:
@app.get('/items')
def get_items(
skip: int = 0,
limit: int = 10,
price: float = 0.0,
in_stock: bool = False,
tags: list[str] | None = None
):
return {
'skip': skip,
'limit': limit,
'price': price,
'in_stock': in_stock,
'tags': tags
}| Запрос | Результат |
|---|---|
/items?skip=5 | skip: 5 (int) |
/items?price=19.99 | price: 19.99 (float) |
/items?in_stock=true | in_stock: True (bool) |
/items?tags=python&tags=fastapi | tags: ['python', 'fastapi'] (list) |
Для передачи нескольких значений используйте один параметр несколько раз:
/items?tags=python&tags=fastapi&tags=api
Или через запятую (нужен кастомный парсер):
from fastapi import Query
@app.get('/items')
def get_items(tags: str | None = Query(None)):
tag_list = tags.split(',') if tags else []
return {'tags': tag_list}Запрос /items?tags=python,fastapi,api вернёт:
{"tags": ["python", "fastapi", "api"]}Классическое применение query-параметров — пагинация:
from fastapi import FastAPI, Query
from typing import Annotated
app = FastAPI()
ITEMS = [{'id': i, 'name': f'Item {i}'} for i in range(100)]
@app.get('/items')
def list_items(
page: Annotated[int, Query(ge=1, description="Номер страницы")] = 1,
page_size: Annotated[int, Query(ge=1, le=50, description="Элементов на странице")] = 10
):
skip = (page - 1) * page_size
end = skip + page_size
return {
'items': ITEMS[skip:end],
'page': page,
'page_size': page_size,
'total': len(ITEMS),
'pages': (len(ITEMS) + page_size - 1) // page_size
}Запрос /items?page=3&page_size=20 вернёт элементы с 40 по 59.
Хороший API возвращает не только данные, но и метаинформацию:
{
"items": [...],
"page": 3,
"page_size": 20,
"total": 100,
"pages": 5,
"has_next": true,
"has_prev": true
}from fastapi import FastAPI, Query
from enum import Enum
class SortOrder(str, Enum):
asc = "asc"
desc = "desc"
class SortBy(str, Enum):
name = "name"
price = "price"
created_at = "created_at"
@app.get('/items')
def list_items(
sort_by: SortBy = SortBy.created_at,
order: SortOrder = SortOrder.desc
):
# Логика сортировки
return {
'sort_by': sort_by,
'order': order,
'items': ITEMS # отсортированные
}Запрос /items?sort_by=price&order=asc отсортирует по цене по возрастанию.
from datetime import datetime
@app.get('/items')
def list_items(
min_price: float | None = Query(None, ge=0),
max_price: float | None = Query(None, ge=0),
created_after: datetime | None = None,
created_before: datetime | None = None
):
items = ITEMS
if min_price is not None:
items = [i for i in items if i.get('price', 0) >= min_price]
if max_price is not None:
items = [i for i in items if i.get('price', 0) <= max_price]
return {'items': items}Запрос /items?min_price=100&max_price=500 отфильтрует по цене.
FastAPI автоматически парсит ISO 8601:
/items?created_after=2024-01-01T00:00:00
/items?created_after=2024-01-01
Иногда нужно передать только заданные параметры (например, для динамического фильтра):
from fastapi import FastAPI, Query
@app.get('/items')
def list_items(
name: str | None = Query(None),
min_price: float | None = Query(None),
max_price: float | None = Query(None)
):
# Собираем фильтры в словарь
filters = {}
if name:
filters['name'] = name
if min_price is not None:
filters['min_price'] = min_price
if max_price is not None:
filters['max_price'] = max_price
# Применяем фильтры к запросу в БД
return {'filters': filters, 'items': []}from fastapi import FastAPI, Query
from typing import Annotated
from datetime import datetime
app = FastAPI()
@app.get('/products')
def list_products(
# Пагинация
page: Annotated[int, Query(ge=1)] = 1,
page_size: Annotated[int, Query(ge=1, le=100)] = 20,
# Сортировка
sort_by: Annotated[str, Query(pattern=r'^(name|price|rating|created_at)$')] = 'created_at',
order: Annotated[str, Query(pattern=r'^(asc|desc)$')] = 'desc',
# Фильтры
search: Annotated[str | None, Query(min_length=2, max_length=100)] = None,
min_price: Annotated[float | None, Query(ge=0)] = None,
max_price: Annotated[float | None, Query(ge=0)] = None,
category: Annotated[str | None, Query()] = None,
in_stock: Annotated[bool | None, Query()] = None,
# Дата создания
created_after: Annotated[datetime | None, Query()] = None,
created_before: Annotated[datetime | None, Query()] = None,
# Теги (несколько значений)
tags: Annotated[list[str] | None, Query()] = None
):
# Построение запроса к БД
query = "SELECT * FROM products WHERE 1=1"
params = {}
if search:
query += " AND name ILIKE :search"
params['search'] = f'%{search}%'
if min_price is not None:
query += " AND price >= :min_price"
params['min_price'] = min_price
if max_price is not None:
query += " AND price <= :max_price"
params['max_price'] = max_price
if category:
query += " AND category = :category"
params['category'] = category
if in_stock is not None:
query += " AND in_stock = :in_stock"
params['in_stock'] = in_stock
# Пагинация
offset = (page - 1) * page_size
query += " LIMIT :limit OFFSET :offset"
params['limit'] = page_size
params['offset'] = offset
return {
'query': query,
'params': params,
'page': page,
'page_size': page_size
}Запрос:
GET /products?page=2&page_size=10&search=laptop&min_price=500&max_price=2000&category=electronics&in_stock=true&tags=gaming&tags=portable
Иногда имя параметра в API должно отличаться от имени в коде:
from fastapi import FastAPI, Query
app = FastAPI()
@app.get('/items')
def get_items(
page_num: int = Query(1, alias='page'),
page_size: int = Query(10, alias='limit')
):
return {'page': page_num, 'limit': page_size}Запрос /items?page=2&limit=50 вернёт:
{"page": 2, "limit": 50}В коде используются page_num и page_size, в URL — page и limit.
Если параметр устарел, но нужно поддерживать обратную совместимость:
from fastapi import FastAPI, Query
app = FastAPI()
@app.get('/items')
def get_items(
offset: int = Query(0, ge=0, deprecated=True, description="Устарел, используйте page"),
page: int = Query(1, ge=0)
):
# Конвертируем offset в page для обратной совместимости
if offset:
page = offset // 10 + 1
return {'page': page}В Swagger UI параметр offset будет помечен как deprecated (зачёркнут).
@app.get('/items/{item_id}')
def get_item(item_id: int, item_id: str = None): # Ошибка: дубликат имени
...Проблема: Параметр пути и query-параметр с одинаковым именем.
Решение: Используйте разные имена:
@app.get('/items/{item_id}')
def get_item(item_id: int, fields: str | None = None):
...@app.get('/items')
def get_items(tags: str | None = None): # Ожидает одну строку
...Запрос /items?tags=a&tags=b передаст только последнее значение.
Решение:
@app.get('/items')
def get_items(tags: list[str] | None = Query(None)):
...@app.get('/items')
def get_items(q: str): # Обязательный параметр без значения по умолчанию
...Проблема: Параметр обязателен, но это не явно.
Решение:
@app.get('/items')
def get_items(q: str | None = None): # Явно необязательный
...
# или
@app.get('/items')
def get_items(q: str = Query(...)): # Явно обязательный
......В следующей теме вы изучите request body — отправку данных в теле запроса через POST, PUT, PATCH с использованием Pydantic-моделей.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.