В главе 3 мы вкратце поговорили о том, как определять конечные точки FastAPI, передавать им простые строковые данные и получать ответы. В этой главе подробнее рассмотрим верхний уровень приложения FastAPI — его также можно назвать уровнем интерфейса или маршрутизации — и его интеграцию с уровнями сервисов и данных.
Как и прежде, начну с небольших примеров. Затем введу некоторую структуру, разделив уровни на подуровни, чтобы обеспечить более чистое развитие и рост веб-приложения. Чем меньше кода мы пишем, тем меньше придется вспоминать и исправлять впоследствии.
Основные примеры данных в этой книге касаются воображаемых существ, или криптидов, и их исследователей. Но вы можете провести параллели с информацией из других областей.
Что мы вообще делаем с информацией? Как и на большинстве других сайтов, на нашем вы найдете способы выполнить следующие операции:
• получение;
• создание;
• изменение;
• замену;
• удаление.
Начав с самого верха, мы создадим конечные точки веб-приложения, способные выполнять эти функции с нашими данными. Сначала предоставим фиктивные данные, чтобы конечные точки работали с любым веб-клиентом. В следующих главах мы перенесем код этих данных на нижние уровни. На каждом этапе необходимо будет убедиться, что сайт по-прежнему работает и корректно передает данные.
Наконец, в главе 10 мы откажемся от фиктивных данных и будем хранить реальные данные в реальных базах данных, чтобы создать полноценный сайт (веб → сервис → данные).
Если позволить любому анонимному посетителю выполнить все эти действия, не стоит ожидать ничего хорошего. В главе 11 рассматриваются аутентификация и авторизация (auth), необходимые для определения ролей и ограничения того, кто что может делать. В оставшейся части этой главы мы обойдемся без аутентификации и просто рассмотрим, как работать с необработанными веб-функциями.
При разработке веб-сайта можно реализовать один из таких вариантов, как:
• веб-уровень и работа дальше вниз;
• уровень данных и работа дальше вверх;
• сервисный уровень и работа в обоих направлениях.
У вас уже есть база данных, установленная и наполненная данными, и вы просто жаждете поделиться ею со всем миром? Если да, то, возможно, стоит сначала заняться кодом и тестами уровня данных, затем уровнем сервисов, а веб-уровень написать последним.
Если ваш подход — предметно-ориентированное проектирование (Domain-Driven Design, DDD) (https://oreil.ly/iJu9Q), можете начать со среднего, сервисного уровня, определяя основные сущности и модели данных. Или же сначала разработать веб-интерфейс, а к нижним уровням обратиться, когда поймете, чего от них ожидать.
Очень хорошие обсуждения и рекомендации по дизайну вы найдете в следующих книгах:
• Clean Architectures in Python (https://oreil.ly/5KrL9), автор Леонардо Джордани (Digital Cat Books);
• Architecture Patterns with Python (https://www.cosmicpython.com), авторы Гарри Персиваль и Боб Грегори (O’Reilly);
• Microservice APIs (https://oreil.ly/Gk0z2), автор Хосе Аро Перальта (Manning).
В этих и других источниках вы увидите такие термины, как «гексагональная архитектура», «порты» и «адаптеры». Выбор способа действий во многом зависит от того, какие данные у вас уже есть и как вы хотите подойти к созданию сайта.
Я предполагаю, что многие из вас в основном заинтересованы в том, чтобы попробовать FastAPI и связанные с ним технологии, и не обязательно имеют заранее определенный зрелый домен данных, который хочется сразу же задействовать. Поэтому в этой книге я использую подход под названием web-first — шаг за шагом, начиная с основных частей и добавляя другие по мере необходимости. Иногда эксперименты работают, иногда нет. Поначалу я постараюсь сдерживать желание запихнуть все в веб-уровень.
Веб-уровень — лишь один из способов передачи данных между пользователем и сервисом. Существуют и другие варианты, например с помощью интерфейса командной строки (CLI) или набора средств разработки программного обеспечения (SDK). В других фреймворках веб-уровень иногда называют уровнем представления или презентации.
HTTP — это способ передачи команд и данных между веб-клиентами и серверами. Но, как и в случае с ингредиентами из холодильника, которые можно комбинировать, изготавливая блюда от отвратительных до изысканных, некоторые рецепты для HTTP работают лучше, чем другие.
В главе 1 я упоминал, что RESTful стал полезной, хотя иногда и нечеткой моделью для разработки HTTP. Проектирование RESTful включает в себя следующие основные компоненты:
• ресурсы — элементы данных, которыми управляет ваше приложение;
• идентификаторы — уникальные идентификаторы ресурсов;
• URL-адреса — структурированные строки ресурсов и идентификаторов;
• глагольные операторы или действия — термины, сопровождающие URL-адреса для различных целей:
• GET — получение ресурса;
• POST — создание нового ресурса;
• PUT — полная замена ресурса;
• PATCH — частичная замена ресурса;
• DELETE — ресурсы разлетаются в клочья.
Вы увидите разногласия по поводу относительных достоинств PUT в сравнении с PATCH. Если вам не нужно отличать частичную модификацию от полной (замены), то, возможно, оба оператора и не понадобятся.
Общие правила RESTful, касающиеся сочетания глаголов и URL-адресов, содержащих ресурсы и идентификаторы, предусматривают следующие шаблоны параметров пути (содержимое между / в URL):
• verb/resource/ — применение глагольного оператора (verb) ко всем ресурсам типа resource;
• verb/resource/id — применение глагольного оператора (verb) к ресурсам (resource) с идентификатором id.
При использовании примера данных для этой книги запрос GET к конечной точке /thing вернет данные обо всех исследователях, но запрос GET к /thing/abc предоставит данные только для ресурса thing с идентификатором abc.
Наконец, веб-запросы часто содержат больше информации, указывая на необходимость следующих действий:
• сортировки результатов;
• пагинации результатов;
• выполнения другой функции.
Параметры для них иногда могут иметь вид параметров пути (добавляются в конец после еще одного символа /), но чаще всего они включаются как параметры запроса (var=val после знака ? в URL-адресе). Поскольку у URL-адресов есть ограничения по размеру, большие запросы часто передаются в теле HTTP.
Большинство авторов рекомендуют использовать множественное число при именовании ресурса и связанных с ним пространств имен, таких как разделы API и таблицы баз данных. Я долго следовал этому совету, но теперь считаю, что названия в единственном числе проще по многим причинам (включая странности английского языка):
• некоторые слова представляют множественное число самих себя — series, fish;
• у некоторых слов неправильное множественное число — children, people;
• вам нужен код преобразования единственного числа во множественное по требованию во многих местах.
По этим причинам во многих случаях в книге я использую схему именования в единственном числе. Это противоречит обычным рекомендациям RESTful, так что не стесняйтесь игнорировать эту мою особенность, если не согласны со мной.
Наши данные касаются в основном существ и исследователей. Изначально мы могли бы определить все URL-адреса и их функции пути FastAPI для доступа к данным в одном файле Python. Не будем поддаваться искушению и начнем так, как будто мы уже восходящая звезда в криптидном веб-пространстве. Имея хороший фундамент, гораздо легче добавлять новые крутые вещи.
Сначала выберите на своей машине каталог. Назовите его fastapi или как угодно, что поможет запомнить, где вы будете работать с кодом из этой книги. В нем создайте следующие подкаталоги:
• src — содержит весь код сайта;
• web — веб-уровень FastAPI;
• service — уровень бизнес-логики;
• data — уровень интерфейса хранения данных;
• model — для определения моделей Pydantic;
• fake — жестко указанные данные (заглушки) для ранних этапов.
В каждой из этих папок вскоре появится по три файла:
• __init__.py — необходим для восприятия этого каталога в качестве пакета;
• creature.py — код существа для этого уровня;
• explorer.py — код исследователя для этого уровня.
Существует множество мнений о том, как следует планировать страницы для разработки. Такой дизайн призван показать разделение уровней и оставить место для будущих дополнений.
Сейчас необходимо дать некоторые объяснения. Поначалу файлы __init__.py будут пустыми. Они представляют собой своего рода хакерскую уловку в отношении Python, поэтому к содержащей их папке следует относиться как к пакету Python, который можно импортировать. Во-вторых, папка fake предоставляет некоторые данные-заглушки для более высоких уровней по мере создания нижних.
Кроме того, логика импорта в Python не работает строго с иерархиями каталогов. Она опирается на пакеты и модули Python. Файлы с расширением .py, перечисленные в приведенной ранее древовидной структуре, являются модулями Python (исходными файлами). Их родительские каталоги будут считаться пакетами, если они содержат файл __init__.py. (Это соглашение необходимо, чтобы, если у вас есть каталог sys и вы набираете команду import sys, Python мог определить, вам нужен системный каталог или ваш локальный.)
Программы Python могут импортировать пакеты и модули. У интерпретатора Python есть встроенная переменная sys.path. Она содержит местоположение стандартного кода Python. Переменная окружения PYTHONPATH — это пустая или разделенная двоеточием строка имен каталогов. Она указывает Python, какие родительские каталоги проверять перед sys.path, чтобы найти импортированные модули или пакеты. Поэтому, если вы переходите в новую папку fastapi, введите следующую команду (в Linux или macOS), чтобы новый код в ней проверялся первым при импорте:
$ export PYTHONPATH=$PWD/src
Часть выражения $PWD означает «вывести рабочий каталог» и избавляет вас от необходимости вводить полный путь к каталогу fastapi, хотя вы можете это сделать, если хотите. А src означает, что искать модули и пакеты для импорта нужно только там.
Чтобы установить переменную окружения PWD в Windows, изучите раздел Excursus: Setting Environment Variables на сайте Python Software Foundation (https://oreil.ly/9NRBA).
Фух.
В этом разделе мы рассмотрим, как использовать FastAPI для написания запросов и ответов для сайта RESTful API. Затем начнем применять их на нашем реальном все более и более странном сайте.
Начнем с примера 8.1. В каталоге src создайте новую программу верхнего уровня main.py — она будет запускать программу Uvicorn и пакет FastAPI.
Пример 8.1. Основная программа main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def top():
return "top here"
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", reload=True)
Слово app здесь представляет собой объект FastAPI, связывающий все воедино. Первый аргумент Uvicorn — "main:app", потому что файл называется main.py, а второй — app, имя объекта FastAPI.
Uvicorn будет продолжать работать и перезапустится, если в том же каталоге или в любых подкаталогах изменится код. Без аргумента reload=True придется перезапускать Uvicorn каждый раз, когда вы вносите изменения в код. Во многих следующих примерах просто вносите изменения в один и тот же файл main.py и принудительно перезапускайте его, вместо того чтобы создавать файлы main2.py, main3.py и т.д. Запустите файл main.py из примера 8.2.
Пример 8.2. Запуск основной программы
$ python main.py &
INFO: Will watch for changes in these directories: [.../fastapi']
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [92543] using StatReload
INFO: Started server process [92551]
INFO: Waiting for application startup.
INFO: Application startup complete.
Символ & в конце строки переводит программу в фоновый режим, и вы можете запускать другие программы в том же окне терминала, если хотите. Или опустите символ & и запустите другой код в другом окне или на другой вкладке.
Теперь вы можете получить доступ к сайту localhost:8000 с помощью браузера или любой из приведенных ранее тестовых программ. В примере 8.3 используется HTTPie.
Пример 8.3. Тестирование основной программы
$ http localhost:8000
HTTP/1.1 200 OK
content-length: 8
content-type: application/json
date: Sun, 05 Feb 2023 03:54:29 GMT
server: uvicorn
"top here"
С этого момента при внесении изменений веб-сервер должен автоматически перезапускаться. Если ошибка останавливает его, перезапустите сервер с помощью команды python main.py.
В примере 8.4 добавлена еще одна тестовая конечная точка с использованием параметра пути (часть URL-адреса).
Пример 8.4. Добавление конечной точки
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def top():
return "top here"
@app.get("/echo/{thing}")
def echo(thing):
return f"echoing {thing}"
if __name__ == "__main__":
uvicorn.run("main:app", reload=True)
Как только вы сохраните изменения в файле main.py в своем редакторе, в окне, где запущен веб-сервер, должно появиться что-то вроде этого:
WARNING: StatReload detected changes in 'main.py'. Reloading...
INFO: Shutting down
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [92862]
INFO: Started server process [92872]
INFO: Waiting for application startup.
INFO: Application startup complete.
Пример 8.5 показывает, правильно ли была обработана новая конечная точка (с помощью аргумента -b выводится только тело ответа).
Пример 8.5. Тестирование новой конечной точки
$ http -b localhost:8000/echo/argh
"echoing argh"
В следующих разделах мы добавим больше конечных точек в файл main.py.
HTTP-запрос состоит из текстового заголовка, за которым следует один или несколько разделов тела.
Можете написать собственный код для разбора HTTP в структуры данных Python, но вы не будете первым. В веб-приложениях эти детали лучше поручить фреймворку.
Возможность реализовать внедрение зависимостей FastAPI здесь особенно полезна. Данные могут поступать из разных частей HTTP-сообщения, и вы уже видели, как можно указать одну или несколько таких зависимостей, чтобы сказать, где находятся данные:
• Header — в HTTP-заголовке;
• Path — в пути URL;
• Query — после символа ? в URL;
• Body — в теле HTTP-сообщения.
К другим, более косвенным источникам можно отнести:
• переменные окружения;
• настройки конфигурации.
В примере 8.6 выполняется HTTP-запрос с использованием нашего старого друга HTTPie и игнорированием возвращаемых данных HTML-тела.
Пример 8.6. Заголовки HTTP-запросов и ответов
$ http -p HBh http://example.com/
GET / HTTP/1.1
Accept: /
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: example.com
User-Agent: HTTPie/3.2.1
HTTP/1.1 200 OK
Age: 374045
Cache-Control: max-age=604800
Content-Encoding: gzip
Content-Length: 648
Content-Type: text/html; charset=UTF-8
Date: Sat, 04 Feb 2023 01:00:21 GMT
Etag: "3147526947+gzip"
Expires: Sat, 11 Feb 2023 01:00:21 GMT
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
Server: ECS (cha/80E2)
Vary: Accept-Encoding
X-Cache: HIT
Первая строка запрашивает верхнюю страницу по адресу example.com (бесплатный сайт, доступный для использования любому в качестве примера). Код запрашивает только URL, без каких-либо параметров. Первый блок строк — это заголовки HTTP-запроса, отправленного на сайт, а следующий блок содержит заголовки HTTP-ответа.
В большинстве тестовых примеров, начиная с данного момента, все эти заголовки запросов и ответов не понадобятся, поэтому вы будете чаще использовать выражение http -b.
Большинство веб-сервисов работают с несколькими видами ресурсов. Хотя вы могли бы поместить весь код обработки путей в один файл и отправиться куда-нибудь побездельничать, часто удобно применять несколько субмаршрутов вместо одной переменной app, использованной в большинстве примеров ранее.
В каталоге web (в том же каталоге, где расположен ваш рабочий файл main.py) создайте файл explorer.py, как показано в примере 8.7.
Пример 8.7. Использование APIRouter в файле web/explorer.py
from fastapi import APIRouter
router = APIRouter(prefix = "/explorer")
@router.get("/")
def top():
return "top explorer endpoint"
Теперь в примере 8.8 приложение верхнего уровня main.py узнает, что в системе появился новый субмаршрут, обрабатывающий все URL, начинающиеся со строки /explorer.
Пример 8.8. Подключение основного приложения (main.py) к субмаршруту
from fastapi import FastAPI
from .web import explorer
app = FastAPI()
app.include_router(explorer.router)
Uvicorn подхватит этот новый файл. Как обычно, проверьте в примере 8.9, а не предполагайте, что он будет работать.
Пример 8.9. Тестирование нового субмаршрута
$ http -b localhost:8000/explorer/
"top explorer endpoint"
Приступим к добавлению основных функций на веб-уровень. Изначально самим веб-функциям предоставим фиктивные данные. В главе 9 мы перенесем эти данные в соответствующие сервисные функции, а в главе 10 — в функции данных. Наконец, будет добавлена реальная база данных, к которой будет иметь доступ уровень данных. На каждом этапе разработки вызовы конечных веб-узлов должны работать.
Сначала необходимо определить данные, которые будут передаваться между уровнями. Наша предметная область (или домен) содержит исследователей и существ, поэтому давайте определим для них минимальные начальные модели Pydantic. Позже могут появиться и другие идеи, например экспедиции, дневники или продажа кофейных кружек посредством электронной коммерции. Но пока просто включите две живые (обычно в случае с существами) модели в пример 8.10.
Пример 8.10. Определение модели в файле model/explorer.py
from pydantic import BaseModel
class Explorer(BaseModel):
name: str
country: str
description: str
В примере 8.11 возрождается определение Creature из предыдущих глав.
Пример 8.11. Определение модели в файле model/creature.py
from pydantic import BaseModel
class Creature(BaseModel):
name: str
country: str
area: str
description: str
aka: str
Это очень простые начальные модели. Здесь не были применены возможности Pydantic, такие как обязательные и необязательные или ограниченные значения. Этот простой код можно впоследствии усовершенствовать, не прибегая к масштабным логическим перестройкам.
Для значений country будут использоваться двухсимвольные коды стран по стандарту ISO. Это позволяет немного сэкономить на вводе текста, однако приходится искать необычные коды.
Заглушки, известные также как макеты данных, представляют собой фиксированные результаты, возвращаемые без вызова обычных активных модулей. Это быстрый способ проверить свои маршруты и ответы.
Фиктивные данные — это аналог настоящего источника данных, выполняющий по крайней мере некоторые из тех же функций. Примером может служить класс в оперативной памяти, имитирующий базу данных. В этой и следующих главах вам предстоит создать некоторое количество фиктивных данных, по мере того как вы будете заполнять код, определяющий уровни и их взаимодействие. В главе 10 вы определите реальное хранилище данных (базу данных), и оно заменит фиктивные данные.
Как и в примерах с данными, подход к созданию этого сайта является исследовательским. Часто бывает неясно, что в итоге понадобится, поэтому давайте начнем с некоторых характерных для подобных сайтов элементов. Для обеспечения доступа фронтенда к данным обычно требуются способы выполнения следующих запросов:
• получить один, некоторые, все (get one, some, all);
• создать (create);
• заменить (replace) полностью;
• изменить (modify) частично;
• удалить (delete).
По сути, это основы CRUD из баз данных, хотя я разделил букву U (модификация) на частичные (изменение) и полные (замена) функции. Возможно, это различие окажется излишним! Это зависит от того, куда ведут данные.
Работая сверху вниз, вы будете дублировать некоторые функции на всех трех уровнях. Чтобы сэкономить на вводе текста, в примере 8.12 введу каталог верхнего уровня под названием fake. В нем находятся модули, предоставляющие фиктивные данные об исследователях и существах.
Пример 8.12. Новый модуль в файле fake/explorer.py
from model.explorer import Explorer
# фиктивные данные, в главе 10 они будут заменены на реальную базу данных и SQL
_explorers = [
Explorer(name="Claude Hande",
country="FR",
description="Scarce during full moons"),
Explorer(name="Noah Weiser",
country="DE",
description="Myopic machete man"),
]
def get_all() -> list[Explorer]:
"""Возврат всех исследователей"""
return _explorers
def get_one(name: str) -> Explorer | None:
for _explorer in _explorers:
if _explorer.name == name:
return _explorer
return None
# Приведенные ниже варианты пока не функциональны,
# поэтому они просто делают вид, что работают,
# не изменяя реальный фиктивный список
def create(explorer: Explorer) -> Explorer:
"""Добавление исследователя"""
return explorer
def modify(explorer: Explorer) -> Explorer:
"""Частичное изменение записи исследователя"""
return explorer
def replace(explorer: Explorer) -> Explorer:
"""Полная замена записи исследователя"""
return explorer
def delete(name: str) -> bool:
"""Удаление записи исследователя; возврат значения None,
если запись существовала"""
return None
Настройка существа в примере 8.13 аналогична.
Пример 8.13. Новый модуль в файле fake/creature.py
from model.creature import Creature
# фиктивные данные, пока не произойдет замена на реальную базу данных и SQL
_creatures = [
Creature(name="Yeti",
aka="Abominable Snowman",
country="CN",
area="Himalayas",
description="Hirsute Himalayan"),
Creature(name="Bigfoot",
description="Yeti's Cousin Eddie",
country="US",
area="*",
aka="Sasquatch"),
]
def get_all() -> list[Creature]:
"""Возврат всех существ"""
return _creatures
def get_one(name: str) -> Creature | None:
"""Возврат одного существа"""
for _creature in _creatures:
if _creature.name == name:
return _creature
return None
# Приведенные ниже варианты пока не функциональны,
# поэтому они просто делают вид, что работают,
# не изменяя реальный фиктивный список
def create(creature: Creature) -> Creature:
"""Добавление существа"""
return creature
def modify(creature: Creature) -> Creature:
"""Частичное изменение записи существа"""
return creature
def replace(creature: Creature) -> Creature:
"""Полная замена записи существа"""
return creature
def delete(name: str):
"""Удаление записи существа; возврат значения None,
если запись существовала"""
return None
Да, функции модулей практически идентичны. Они изменятся позже, когда появится настоящая база данных — она будет обрабатывать различные поля двух моделей. Кроме того, я использовал отдельные функции, а не определил абстрактный или класс Fake. У модуля собственное пространство имен, так что это эквивалентный способ объединения данных и функций.
Теперь изменим веб-функции из примеров 8.12 и 8.13. Готовясь к созданию последующих уровней (сервисного и данных), импортируйте только что определенный фиктивный провайдер данных, но назовите его service в строке import fake.explorer as service (пример 8.14). В главе 9 вы сделаете следующее:
• создадите новый файл service/explorer.py;
• импортируете туда фиктивные данные;
• укажете коду файла web/explorer.py импортировать новый модуль сервиса вместо фиктивного модуля.
В главе 10 вы проделаете то же самое на уровне данных. Все это сводится к добавлению частей программы и их соединению, при этом код переделывается как можно реже. Электричество (то есть настоящую базу данных и постоянные данные) вы включите позже, в главе 10.
Пример 8.14. Новые конечные точки в файле web/explorer.py
from fastapi import APIRouter
from model.explorer import Explorer
import fake.explorer as service
router = APIRouter(prefix = "/explorer")
@router.get("/")
def get_all() -> list[Explorer]:
return service.get_all()
@router.get("/{name}")
def get_one(name) -> Explorer | None:
return service.get_one(name)
# все остальные конечные точки пока ничего не делают:
@router.post("/")
def create(explorer: Explorer) -> Explorer:
return service.create(explorer)
@router.patch("/")
def modify(explorer: Explorer) -> Explorer:
return service.modify(explorer)
@router.put("/")
def replace(explorer: Explorer) -> Explorer:
return service.replace(explorer)
@router.delete("/{name}")
def delete(name: str):
return None
Теперь сделайте то же самое для конечных точек /creature (пример 8.15). Да, пока это похоже на вырезанный и вставленный код, но если сделать все заранее, это упростит внесение изменений в дальнейшем — а они всегда будут.
Пример 8.15. Новые конечные точки в файле web/creature.py
from fastapi import APIRouter
from model.creature import Creature
import fake.creature as service
router = APIRouter(prefix = "/creature")
@router.get("/")
def get_all() -> list[Creature]:
return service.get_all()
@router.get("/{name}")
def get_one(name) -> Creature:
return service.get_one(name)
# все остальные конечные точки пока ничего не делают:
@router.post("/")
def create(creature: Creature) -> Creature:
return service.create(creature)
@router.patch("/")
def modify(creature: Creature) -> Creature:
return service.modify(creature)
@router.put("/")
def replace(creature: Creature) -> Creature:
return service.replace(creature)
@router.delete("/{name}")
def delete(name: str):
return service.delete(name)
В последний раз мы обращались к файлу main.py, чтобы добавить субмаршрут для URL-адресов /explorer. Теперь добавим еще один для модуля /creature (пример 8.16).
Пример 8.16. Добавление субмаршрута существа в файл main.py
import uvicorn
from fastapi import FastAPI
from web import explorer, creature
app = FastAPI()
app.include_router(explorer.router)
app.include_router(creature.router)
if __name__ == "__main__":
uvicorn.run("main:app", reload=True)
Все сработало? Если вы набрали или вставили все точно, Uvicorn должен был перезапустить приложение. Попробуем провести несколько тестов вручную.
В главе 12 будет показано, как использовать pytest для автоматизации тестирования на разных уровнях. В примерах 8.17–8.21 вручную выполняются несколько тестов веб-уровня для конечных точек исследователя с помощью HTTPie.
Пример 8.17. Тестирование конечной точки с инструкцией Get All
$ http -b localhost:8000/explorer/
[
{
"country": "FR",
"name": "Claude Hande",
"description": "Scarce during full moons"
},
{
"country": "DE",
"name": "Noah Weiser",
"description": "Myopic machete man"
}
]
Пример 8.18. Тестирование конечной точки с инструкцией Get One
$ http -b localhost:8000/explorer/"Noah Weiser"
{
"country": "DE",
"name": "Noah Weiser",
"description": "Myopic machete man"
}
Пример 8.19. Тестирование конечной точки с инструкцией Replace
$ http -b PUT localhost:8000/explorer/"Noah Weiser"
{
"country": "DE",
"name": "Noah Weiser",
"description": "Myopic machete man"
}
Пример 8.20. Тестирование конечной точки с инструкцией Modify
$ http -b PATCH localhost:8000/explorer/"Noah Weiser"
{
"country": "DE",
"name": "Noah Weiser",
"description": "Myopic machete man"
}
Пример 8.21. Тестирование конечной точки с инструкцией Delete
$ http -b DELETE localhost:8000/explorer/Noah%20Weiser
true
$ http -b DELETE localhost:8000/explorer/Edmund%20Hillary
false
То же самое можно сделать для конечных точек в части /creature.
Помимо выполняемых вручную тестов, которые я применял в большинстве примеров, FastAPI предоставляет очень хорошие автоматизированные формы тестирования в конечных точках /docs и /redocs. Это два разных стиля для одних и тех же сведений, поэтому я просто покажу немного информации, размещаемой на страницах /docs (рис. 8.1).
Рис. 8.1. Сгенерированная страница документации
Попробуйте выполнить первый тест.
1. Нажмите стрелку вниз, находящуюся справа под верхним разделом GET /explorer/. Откроется большая светло-голубая форма.
2. Нажмите синюю кнопку Execute (Выполнить) слева. На рис. 8.2 вы видите верхнюю часть результатов.
Рис. 8.2. Генерируемая страница результатов для GET /explorer/
В разделе Response body (Тело ответа) выводится текст в формате JSON, возвращаемый для фиктивных данных исследователя. Их мы определили ранее:
[
{
"name": "Claude Hande",
"country": "FE",
"description": "Scarce during full moons"
},
{
"name": "Noah Weiser",
"country": "DE",
"description": "Myopic machete man"
}
]
Попробуйте выполнить все остальные тесты. Для некоторых, таких как GET /explorer/{name}, нужно будет указать входное значение. Вы получите ответ на каждый из тестов (правда, некоторые так и останутся без ответа, пока не будет добавлен код базы данных). Можно повторить эти тесты в конце глав 9 и 10, чтобы убедиться, что никакие конвейеры данных не были повреждены при внесении изменений в код.
Когда функции на веб-уровне нужны данные, находящиеся под управлением уровня данных, она должна попросить уровень сервисов стать посредником. Это требует больше кода и может показаться ненужным, но это хорошая идея.
• Как гласит этикетка на банке, веб-уровень работает с Интернетом, а уровень данных — с внешними хранилищами данных и сервисами. Гораздо безопаснее хранить их данные по отдельности.
• Уровни можно тестировать независимо друг от друга. Механизм разделения уровней позволяет это сделать.
Для очень маленького сайта можно пропустить сервисный слой, если он ничего не делает. В главе 9 изначально определены сервисные функции, выполняющие лишь передачу запросов и ответов между веб-уровнем и уровнем данных. Хотя бы эти уровни необходимо разделить.
Что делает функция сервисного уровня? Узнаете в следующей главе. Подсказка: она разговаривает с уровнем данных, но тихим голосом, чтобы веб-уровень не понял, что именно она говорит. Также она определяет любую специфическую бизнес-логику, например взаимодействие между ресурсами. Чаще всего веб-уровень и уровень данных не должны заботиться о том, что происходит внутри.
(Уровень сервисов — это секретная служба.)
В веб-интерфейсах, когда возвращаются многие или все сущности с URL-шаблонами, такими как GET /resource, часто требуется запросить поиск и возврат ресурсов:
• только одного;
• возможно, многих;
• всех.
Как заставить наш благонамеренный, но крайне прямолинейно мыслящий компьютер выполнять такие задачи? В первом случае, согласно описанному ранее шаблону RESTful, в URL-путь нужно включить идентификатор ресурса. При получении нескольких ресурсов может потребоваться увидеть результаты в определенном порядке.
• Сортировка — упорядочение всех результатов, даже если за один раз вы получите только часть из них.
• Пагинация — возвращение лишь некоторых результатов за раз с соблюдением любого типа сортировки.
В каждом случае группа параметров, задаваемых пользователем, указывает на то, что вам нужно. Обычно их задают в качестве параметров запроса. Вот несколько примеров.
• Сортировка GET /explorer?sort=country — получение всех исследователей, отсортированных по коду страны.
• Пагинация GET /explorer?offset=10&size=10 — возвращение из всего списка лишь записей исследователей (в данном случае неотсортированных), находящихся на позициях с 10-й по 19-ю.
• Оба запроса — GET /explorer?sort=country&offset=10&size=10.
Их можно задать в виде отдельных параметров запроса, и в этом вам поможет внедрение зависимостей FastAPI.
• Определите параметры сортировки и пагинации как модель Pydantic.
• Предоставьте модель параметров функции пути get_all() с функциональной возможностью Depends в аргументах функции пути.
Где должны располагаться сортировка и пагинация? Поначалу может показаться, что проще всего передавать все результаты запросов к базе данных на веб-уровень и использовать Python для обработки данных там. Но это не очень эффективный подход. Эти задачи обычно лучше всего решаются на уровне данных, потому что базы данных хорошо справляются с задачами такого типа. Я займусь кодом для них в главе 17. В ней содержится больше информации о базах данных, чем в главе 10.
В этой главе вы подробнее узнали о том, о чем говорилось в главе 3 и др. С нее начался процесс создания полноценного сайта, содержащего информацию о воображаемых существах и их исследователях. Начиная с веб-уровня, вы определяете конечные точки с помощью декораторов путей FastAPI и функций пути. Последние собирают данные запроса, где бы они ни находились в байтах HTTP-запроса. Данные модели автоматически проверяются и подтверждаются Pydantic. Функции пути обычно передают аргументы соответствующим сервисным функциям, о которых речь пойдет в следующей главе.
Персиваль Г., Грегори Б. Паттерны разработки на Python. — СПб.: Питер, 2022.
Перальта Х.А. Микросервисы и API. — СПб.: Питер, 2024.