Хотя акроним API в названии FastAPI — это намек на его основную направленность, FastAPI может работать и с традиционным веб-контентом. В этой главе рассказывается о стандартных HTML-формах и шаблонах для вставки данных в HTML.
Как вы уже поняли, FastAPI был разработан в основном для создания API, и его входной информацией по умолчанию будут данные в формате JSON. Но это не значит, что он не может служить стандартным базовым HTML-формам и их друзьям.
FastAPI поддерживает данные из HTML-форм так же, как и из других источников, таких как Query и Path, используя зависимость Form.
Для работы с формами FastAPI вам потребуется пакет Python-Multipart, поэтому при необходимости выполните команду pip install python-multipart. Кроме того, каталог static из главы 15 понадобится для размещения тестовых форм из нее.
Повторим пример 3.11, но предоставим значение who через форму, а не в виде JSON-строки. (Вызовите функцию пути greet2(), чтобы избежать нарушения работы старой функции пути greet(), если она все еще существует.) Добавьте пример 16.1 в файл main.py.
Пример 16.1. Получение значения из формы GET
from fastapi import FastAPI, Form
app = FastAPI()
@app.get("/who2")
def greet2(name: str = Form()):
return f"Hello, {name}?"
Основное отличие заключается в том, что значение поступает от объекта Form, а не Path, Query и остальных из главы 3.
Попробуйте (пример 16.2) провести начальный тест формы с помощью HTTPie (вам потребуется аргумент -f, чтобы выгрузка происходила в кодировке формы, а не в формате JSON).
Пример 16.2. Формирование запроса GET с помощью HTTPie
$ http -f -b GET localhost:8000/who2 name="Bob Frapples"
"Hello, Bob Frapples?"
Можете также отправить запрос из стандартного файла HTML-формы. В главе 15 было показано, как создать каталог static (доступ к нему осуществляется по URL /static) для хранения любых данных, включая HTML-файлы, поэтому в примере 16.3 поместим туда этот файл (form1.html).
Пример 16.3. Формирование запроса GET (static/form1.html)
<form action="http://localhost:8000/who2" method="get">
Say hello to my little friend:
<input type="text" name="name" value="Bob Frapples">
<input type="submit">
</form>
Если вы попросите браузер загрузить страницу http://localhost:8000/static/form1.html, то увидите форму. Если введете любую тестовую строку, получите следующее сообщение:
"detail":[{"loc":["body","name"],
"msg":"field required",
"type":"value_error.missing"}]}
А?
Посмотрите в окно, где запущен Uvicorn, чтобы увидеть, что написано в его журнале:
INFO: 127.0.0.1:63502 -
"GET /who2?name=rr23r23 HTTP/1.1"
422 Unprocessable Entity
Почему форма отправила переменную name в качестве параметра запроса, когда мы поместили ее в поле формы? Это оказалось странностью HTML, задокументированной на веб-сайте W3C (https://oreil.ly/e6CJb). Кроме того, если в вашем URL были параметры запроса, он сотрет их и заменит значением name.
Почему же HTTPie справился с этим, как и ожидалось? Мне это неизвестно. Это несоответствие, о котором следует знать.
Официальная магическая формула HTML заключается в том, чтобы изменить действие с GET на POST. Так что добавим конечную точку POST для /who2 в файл main.py (пример 16.4).
Пример 16.4. Получение значения из формы POST
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/who2")
def greet3(name: str = Form()):
return f"Hello, {name}?"
Пример 16.5 представляет собой файл stuff/form2.html, но с оператором get, замененным на post.
Пример 16.5. Формирование запроса POST (static/form2.html)
<form action="http://localhost:8000/who2" method="post">
Say hello to my little friend:
<input type="text" name="name">
<input type="submit">
</form>
Разбудите свой браузер и попросите его получить эту новую форму. Внесите в нее текст Bob Frapples и подтвердите отправку формы. На этот раз вы получите тот же результат, что и при использовании HTTPie:
"Hello, Bob Frapples?"
Поэтому, если отправляете формы из HTML-файлов, задействуйте метод POST.
Возможно, вам знакома игра в слова Mad Libs. Игрокам дают последовательность слов — существительных, глаголов или чего-то более конкретного, они вставляют их в отмеченные места на странице текста. Вставив все слова, нужно прочитать текст — и начинается веселье, иногда сопровождаемое неловкостью.
Веб-шаблон — это то же самое, но, как правило, без неловкости. Шаблон содержит кучу текста со слотами для данных, вставляемых сервером. Его обычное назначение — генерировать HTML с переменным содержимым, в отличие от статического HTML из главы 15.
Пользователи Flask хорошо знакомы с его сопутствующим проектом — шаблонизатором Jinja (https://jinja.palletsprojects.com) (его часто называют также Jinja2). FastAPI поддерживает Jinja и другие шаблонизаторы.
Создайте каталог template рядом с файлом main.py для размещения HTML-файлов с поддержкой Jinja. Внутри создайте файл list.html (пример 16.6).
Пример 16.6. Определение шаблона файла (template/list.html)
<html>
<table bgcolor="#eeeeee">
<tr>
<th colspan=3>Creatures</th>
</tr>
<tr>
<th>Name</th>
<th>Description</th>
<th>Country</th>
<th>Area</th>
<th>AKA</th>
</tr>
{% for creature in creatures: %}
<tr>
<td>{{ creature.name }}</td>
<td>{{ creature.description }}</td>
<td>{{ creature.country }}</td>
<td>{{ creature.area }}</td>
<td>{{ creature.aka }}</td>
</tr>
{% endfor %}
</table>
<br>
<table bgcolor="#dddddd">
<tr>
<th colspan=2>Explorers</th>
</tr>
<tr>
<th>Name</th>
<th>Country</th>
<th>Description</th>
</tr>
{% for explorer in explorers: %}
<tr>
<td>{{ explorer.name }}</td>
<td>{{ explorer.country }}</td>
<td>{{ explorer.description }}</td>
</tr>
{% endfor %}
</table>
</html>
Неважно, как это выглядит, поэтому здесь не используется формальный язык CSS, только древний, существовавший еще до появления CSS, атрибут таблицы bgcolor, чтобы обеспечить различия между двумя таблицами.
Переменные Python, которые необходимо вставить, заключены в двойные фигурные скобки, а в наборы символов {% и %} заключают операторы if, циклы for и другие структуры управления. В документации Jinja (https://jinja.palletsprojects.com) можно получить полную информацию по синтаксису и примерам.
Этот шаблон ожидает, что ему будут переданы переменные Python creatures и explorers, представляющие собой списки объектов Creature и Explorer.
В примере 16.7 показано, что нужно добавить в файл main.py, чтобы установить шаблоны и использовать данные из примера 16.6. Код подает переменные creatures и explorers в шаблон, применяя модули в фиктивном каталоге из предыдущих глав — эта папка предоставляла тестовые данные, если БД была пуста или не подключена.
Пример 16.7. Настройка шаблонов и использование одного из них (main.py)
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
app = FastAPI()
top = Path(__file__).resolve().parent
template_obj = Jinja2Templates(directory=f"{top}/template")
# Получение нескольких небольших предопределенных списков наших приятелей:
from fake.creature import fakes as fake_creatures
from fake.explorer import fakes as fake_explorers
@app.get("/list")
def explorer_list(request: Request):
return template_obj.TemplateResponse("list.html",
{"request": request,
"explorers": fake_explorers,
"creatures": fake_creatures})
Задайте своему любимому браузеру или даже тому, который вам не очень нравится, адрес http://localhost:8000/list, и вы получите в ответ рис. 16.1.
Рис. 16.1. Вывод из каталога /list
В этой главе был дан краткий обзор того, как FastAPI работает с темами, не относящимися к API, такими как формы и шаблоны. Наряду с рассмотренным в предыдущей главе о файлах, это традиционные минимально необходимые веб-задачи, с ними вы часто сталкиваетесь.