1205 FastAPI Web Razrabotka Na Python
1205 FastAPI Web Razrabotka Na Python
ББК 32.988.02-018
УДК 004.738.5
Л93
Любанович Билл
Л93 FastAPI: веб-разработка на Python. — Астана: «Спринт Бук», 2024. — 288 с.: ил.
ISBN 978-601-08-3847-5
FastAPI — относительно новый, но надежный фреймворк с чистым дизайном, использующий
преимущества актуальных возможностей Python. Как следует из названия, FastAPI отличается
высоким быстродействием и способен конкурировать в этом с аналогичными фреймворками на
таких языках, как Golang. Эта практическая книга расскажет разработчикам, знакомым с Python,
как FastAPI позволяет достичь большего за меньшее время и с меньшим количеством кода.
Билл Любанович рассказывает о тонкостях разработки с применением FastAPI и предлагает
множество рекомендаций по таким темам, как формы, доступ к базам данных, графика, карты
и многое другое, что поможет освоить основы и даже пойти дальше. Кроме того, вы познакомитесь
с RESTful API, приемами валидации данных, авторизации и повышения производительности.
Благодаря сходству с такими фреймворками, как Flask и Django, вы легко начнете работу с FastAPI.
16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)
ББК 32.988.02-018
УДК 004.738.5
Права на издание получены по соглашению с O’Reilly. Все права защищены. Никакая часть данной книги
не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев
авторских прав.
ISBN 978-1098135508 англ. Authorized Russian translation of the English edition FastAPI.
ISBN 978-1098135508 © 2024 Bill Lubanovic.
This translation is published and sold by permission of O’Reilly Media, Inc.,
which owns or controls all rights to publish and sell the same.
ISBN 978-601-08-3847-5 © Перевод на русский язык ТОО «Спринт Бук», 2024
© Издание на русском языке, оформление ТОО «Спринт Бук», 2024
Оглавление
https://t.me/it_boooks/2
Предисловие..........................................................................................................................................14
Условные обозначения...............................................................................................................16
Примеры кода.................................................................................................................................16
Благодарности................................................................................................................................17
От издательства....................................................................................................................................18
Структуры данных.........................................................................................................................41
Веб-фреймворки............................................................................................................................41
Заключение......................................................................................................................................42
Простой пример............................................................................................................................88
Проверка типов..............................................................................................................................91
Проверка значений......................................................................................................................92
Заключение......................................................................................................................................94
Глава 6. Зависимости.........................................................................................................................95
Обзор..................................................................................................................................................95
Что такое зависимости................................................................................................................95
Проблемы с зависимостями.....................................................................................................96
Внедрение зависимостей..........................................................................................................96
Зависимости FastAPI.....................................................................................................................96
Написание зависимостей..........................................................................................................97
Область действия зависимости..............................................................................................98
Заключение................................................................................................................................... 100
SQLite................................................................................................................................................ 145
Макет................................................................................................................................................ 147
Заставляем все это работать................................................................................................. 147
Тестируем!...................................................................................................................................... 152
Заключение................................................................................................................................... 164
Об авторе.............................................................................................................................................. 280
Иллюстрация на обложке.............................................................................................................. 281
Алфавитный указатель................................................................................................................... 282
В память о моих жене Мэри, родителях,
Билле и Тилли, и друге Риче. Мне вас не хватает.
Предисловие
После этого бˆольшая часть моей работы была связана с Python и его веб-
фреймворками, в основном Flask и Django. Мне особенно понравилась про-
стота Flask, и я предпочитаю использовать этот фреймворк для многих задач.
А несколько лет назад я заметил нечто мелькнувшее на периферии моего зре-
ния — новый веб-фреймворк на Python под названием FastAPI, написанный
Себастьяном Рамиресом.
Предисловие 15
Как говорится, «это мнения, на которых основаны мои факты». Ваш опыт будет
уникальным, но я надеюсь, что вы найдете здесь достаточно полезного, чтобы
стать более продуктивным веб-разработчиком.
16 Предисловие
Условные обозначения
В этой книге используются следующие шрифтовые обозначения.
Курсив
Отмечает новые термины.
Рубленый шрифт
Примеры кода
Дополнительные материалы (примеры кода, упражнения и т. д.) доступны для
скачивания на странице https://github.com/madscheme/fastapi.
Благодарности
Хотелось бы поблагодарить множество людей из различных организаций: школы
Serra High School, Питтсбургского университета, лаборатории хронобиологии
Миннесотского университета, авиакомпании Northwest Airlines, компаний
Crosfield-Dicomed, Tela, WAM!NET, Mad Scheme, SSESCO, Intradyn, Keep, Cray,
Penguin Computing, Flywheel, медиакомпании Thomson Reuters, организаций
Intran и «Архив Интернета», стартапа CrowdStrike. Я многому у вас научился.
От издательства
1
Однажды я пожал ему руку. Я не мыл свою в течение месяца, но могу поспорить, что
он сделал это сразу же.
20 Часть I. Что у нас нового
1
И точно не в последний раз.
ГЛАВА 1
Современная
Всемирная паутина
Обзор
Когда-то Всемирная паутина была маленькой и простой. Разработчикам было
так весело отправлять вызовы PHP, HTML и MySQL в отдельные файлы
и с гордостью говорить всем, что они могут заглянуть на свой веб-сайт. Но со
временем Сеть разрослась до невообразимого количества страниц и развиваю-
щаяся игровая площадка превратилась в метавселенную тематических парков.
В этой главе отмечены некоторые все более актуальные для современной Все-
мирной паутины области:
сервисы и API;
конкурентность;
уровни (слои);
данные.
Сервисы и API
Паутина — это отличная соединительная ткань. Несмотря на то что большая
часть деятельности по-прежнему происходит на стороне контента — HTML,
JavaScript, изображений и т. д., все большее внимание уделяется интерфейсам
прикладного программирования (API), соединяющим различные элементы
программ.
Эти два мира общаются друг с другом с помощью API. В современном Интер-
нете дизайн API так же важен, как и дизайн самих сайтов. API — это контракт,
подобный схеме базы данных. Определение и модификация API — это уже
серьезная работа.
Виды API
Каждый API определяет следующее:
До появления сетей API обычно означал очень тесную связь, например вызов
функции из библиотеки на том же языке, что и ваше приложение, — скажем,
вычисление квадратного корня в математической библиотеке.
1
Я бросил попытки несколько лет назад.
Глава 1. Современная Всемирная паутина 23
HTTP
Бернерс-Ли предложил для своей Всемирной паутины три компонента:
REST(ful)
В одной из глав докторской диссертации (https://oreil.ly/TwGmX) Роя Филдинга
есть определение передачи репрезентативного состояния (Representational State
Transfer, REST) — архитектурного стиля для использования HTTP1. Несмотря
на то что на эту работу часто ссылаются, ее по большей части неправильно по-
нимают (https://oreil.ly/bsSry).
DELETE — удаление.
1
Под стилем понимается шаблон более высокого уровня, например «клиент — сервер»,
а не конкретная конструкция.
Глава 1. Современная Всемирная паутина 25
заголовки;
строка URL;
параметры запроса;
значения в теле сообщения.
По крайней мере один код состояния можно считать пасхалкой — 418 (I’m a teapot,
https://www.google.com/teapot). На странице должен появиться подключенный
к сети чайник. Если попросить, он нальет вам чашечку чая.
JSON:API
Сочетание RESTful-дизайна и форматов данных JSON уже стало привычным.
Но некоторые возможности для двусмысленности и занудства все же остаются.
Недавнее предложение JSON:API (https://jsonapi.org) направлено на то, чтобы не-
много ужесточить спецификации. В этой книге используется свободный подход
RESTful, но JSON:API или что-то подобное может оказаться полезным, если
у вас возникнут серьезные затруднения.
GraphQL
Для некоторых целей RESTful-интерфейсы могут быть громоздкими. Facebook
(сейчас Meta1) разработала язык под названием Graph Query Language (GraphQL)
(https://graphql.org). Он позволяет определить более гибкие запросы. В этой книге
GraphQL не рассматривается, но, возможно, стоит обратить на него внимание,
если вы считаете, что RESTful-дизайн не подходит для вашего приложения.
Конкурентность
Наряду с ростом ориентированности на сервисы стремительное увеличение
количества подключений к веб-сервисам требует все большей эффективности
и масштабирования. Нужно снизить следующие показатели:
1
Деятельность запрещена в РФ.
Глава 1. Современная Всемирная паутина 27
1
Примерно тогда, когда пещерные люди играли в футбэг с гигантскими наземными
ленивцами.
28 Часть I. Что у нас нового
Уровни (слои)
Поклонники Шрека, возможно, помнят, как он отметил слои своей личности,
на что Осел уточнил: «Как луковица?»
1
Выберите свой вариант: уровень/слой, помидор/томат.
2
Часто можно встретить термин «модель — представление — контроллер» (Model —
View — Controller, MVC) и похожие варианты. Обычно это сопровождается религи-
озными войнами, но здесь я агностик.
Глава 1. Современная Всемирная паутина 29
Уровни взаимодействуют друг с другом через API. Это могут быть простые вы-
зовы функций к отдельным модулям Python, но можно обращаться к внешнему
коду через любой метод. Как я показывал ранее, это могут быть RPC, сообщения
и т. д. В этой книге я предполагаю наличие одного веб-сервера с кодом Python,
импортирующим другие модули Python. Разделение и сокрытие информации
выполняется модулями.
написан специалистами;
изолированно протестирован;
заменен или дополнен — вы можете добавить второй веб-уровень, использу-
ющий другой API, например gRPC, наряду с веб-уровнем.
Вы можете представить себе уровни в виде вертикальной стопки, как торт в теле-
передаче «Лучший пекарь Британии»1.
Кстати, хотя я и называю их уровнями, не нужно считать, что один из них на-
ходится выше или ниже другого и что команды перемещаются с помощью
гравитации. Это было бы проявлением вертикального шовинизма! Можете
рассматривать уровни как блоки, стоящие бок о бок друг с другом (рис. 1.2).
1
Как известно, если слои вашего торта сложены небрежно, вы можете не вернуться
в шатер на следующей неделе.
Глава 1. Современная Всемирная паутина 31
Иногда решить, какой уровень лучше всего подходит для кода, бывает непро-
сто. Например, в главе 11 рассматриваются требования к аутентификации
и авторизации и способы их реализации — в качестве дополнительного уровня
между веб- и сервисным уровнем или внутри одного из них. Разработка про-
граммного обеспечения — это порой не только искусство, но и наука.
32 Часть I. Что у нас нового
Данные
Веб-уровень часто использовался как фронтенд для реляционных баз данных,
хотя в настоящее время появилось множество других способов хранения данных
и доступа к ним, например базы данных типа NoSQL или NewSQL.
Заключение
Во Всемирной паутине используется много API, но особенно много взаимодей-
ствий на основе RESTful. Асинхронные вызовы обеспечивают лучшую конку-
рентность, что ускоряет общий процесс выполнения. Приложения веб-сервисов
часто бывают достаточно большими для того, чтобы разделить их на уровни.
Данные стали самостоятельной важной областью. Все эти понятия рассматрива-
ются в языке программирования Python. О нем и пойдет речь в следующей главе.
ГЛАВА 2
Современный Python
Обзор
Python развивается, чтобы идти в ногу с меняющимся техническим миром.
В этой главе рассматриваются специфические возможности этого языка, от-
носящиеся к описанным в предыдущей главе вопросам, а также некоторые
дополнительные:
инструменты;
API и сервисы;
переменные и подсказки типов;
структуры данных;
веб-фреймворки.
Инструменты
В каждом языке программирования есть следующие элементы:
Приступим к работе
Вы должны уметь написать и запустить программу на Python, подобную при-
веденной в примере 2.1.
$ python
Python 3.9.1 (v3.9.1:1e5d33e9b9, Dec 7 2020, 12:10:52)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
Глава 2. Современный Python 35
>>> wrong_answer = 43
>>> wrong_answer
43
>>> wrong_answer = 43
>>> wrong_answer - 3
40
Непосредственно Python
Вам понадобится как минимум Python 3.7. У него имеются такие возможности,
как подсказки типов и модуль asyncio, которые являются основными требова-
ниями FastAPI. Я же рекомендую использовать по крайней мере Python 3.9
с более длительным сроком поддержки. Стандартный источник для получения
Python — Python Software Foundation (https://www.python.org).
Управление пакетами
Вам нужно будет загрузить сторонние пакеты Python и безопасно установить
их на свой компьютер. Классическим инструментом для этого служит система
pip (https://pip.pypa.io).
Вы можете многое сделать с помощью старого доброго pip, но, скорее всего, вам
захочется также использовать виртуальные среды и рассмотреть альтернативные
инструменты, такие как Poetry.
36 Часть I. Что у нас нового
Виртуальные среды
Pip загрузит и установит пакеты, но куда он должен их поместить? Хотя
стандартный Python и входящие в него библиотеки обычно устанавливаются
в стандартное место в вашей операционной системе, вы не сможете (и, скорее
всего, не должны) ничего там изменить. Pip использует каталог по умолчанию,
отличный от системного, поэтому установка не попадет на стандартные файлы
Python в вашей системе. Это можно изменить. Подробности для своей опера-
ционной системы смотрите на сайте pip.
$ venv venv1
Чтобы сделать его своей текущей средой Python, выполните эту команду оболоч-
ки (в Linux или Mac, для Windows и других ОС смотрите документацию venv):
$ source venv1/bin/activate
Теперь каждый раз, когда вы запускаете pip install, пакеты будут устанавли-
ваться в среду venv1. Когда запускаете программы Python, именно там находятся
ваш интерпретатор Python и модули.
Инструмент Poetry
Сочетание pip и venv настолько распространено, что люди начали комбинировать
их, чтобы сократить этапы работы и избежать source-премудростей оболочки.
Одним из таких пакетов стал Pipenv (https://pipenv.pypa.io), но более новый кон-
курент под названием Poetry (https://python-poetry.org) становится все популярнее.
Тестирование
Тестирование подробно рассматривается в главе 12. Хотя стандартным тесто-
вым пакетом Python является unittest, промышленный тестовый пакет Python,
используемый большинством разработчиков Python, — это pytest (https://
docs.pytest.org). Установить его можно с помощью команды pip install pytest.
38 Часть I. Что у нас нового
Веб-инструменты
В главе 3 показано, как установить и применять основные веб-инструменты
Python, используемые в этой книге:
API и сервисы
Модули и пакеты Python необходимы для создания больших приложений,
которые не превращаются в «большие комки грязи» (https://oreil.ly/zzX5T). Даже
в однопроцессном веб-сервисе можно сохранить описанное в главе 1 разделение
с помощью тщательного проектирования модулей и импортов.
Это прямое и быстрое решение. Компилятор следит за тем, что куда записы-
вается. Это одна из причин того, почему такие языки, как C, быстрее Python.
Как разработчику, вам необходимо убедиться, что вы присваиваете каждой
переменной только значения правильного типа.
1
Любанович Б. Простой Python. Современный стиль программирования. 2-е изд. — СПб.:
Питер, 2021.
40 Часть I. Что у нас нового
Подсказки типов
Вся эта предыстория имеет определенный смысл.
В Python 3.6 добавлены подсказки типов (type hints) для объявления типа объ-
екта, на который ссылается переменная. Они не выполняются интерпретатором
Python во время его работы! Вместо этого их могут задействовать различные
инструменты для обеспечения последовательного использования переменной.
Стандартная программа проверки типов называется mypy, и позже я покажу
вам, как она работает.
Структуры данных
Подробнее о Python и структурах данных вы узнаете в главе 5.
Веб-фреймворки
Помимо прочего, веб-фреймворк осуществляет перевод между байтами HTTP
кода и структурами данных Python. Это поможет сэкономить много сил. В то же
время если часть его работает не так, как вам нужно, то может понадобиться
взломать решение. Как говорится, не надо изобретать колесо — разве что вы
не можете найти круглое.
Django
Django (https://www.djangoproject.com) — это полнофункциональный веб-фреймворк,
обозначающий себя как веб-фреймворк для перфекционистов с жесткими срока-
ми выполнения работы. Он был анонсирован Адрианом Холовати и Саймоном
Уиллисоном в 2003 году и назван в честь Джанго Рейнхардта — бельгийского
джазового гитариста XX века. Django часто используется для корпоративных
сайтов с базами данных. Более подробное описание приводится в главе 7.
Flask
Flask (https://flask.palletsprojects.com), представленный Армином Ронахером
в 2010 году, является микрофреймворком. В главе 7 вы найдете более подробную
информацию о нем и его сравнение с Django и FastAPI.
42 Часть I. Что у нас нового
FastAPI
Встретив на балу других ухажеров, мы наконец-то сталкиваемся с интригующим
FastAPI, о котором и пойдет речь в этой книге. Хотя FastAPI был опубликован
Себастьяном Рамиресом в 2018 году, он уже поднялся на третье место среди
веб-фреймворков Python, уступая лишь Flask и Django, и развивается все бы-
стрее. Выполненное в 2022 году сравнение (https://oreil.ly/36WTQ) показывает, что
в какой-то момент он может обойти конкурентов.
Заключение
В этой главе было рассмотрено множество вопросов, связанных с современным
Python:
Обзор FastAPI
После того как вы освоитесь с идеями, изложенными в этой части, в части III
вы сможете более детально изучить их. Именно здесь вы можете принести
серьезную пользу или вред своим познаниям. Не осуждайте, все зависит от вас.
ГЛАВА 3
Обзор FastAPI
Обзор
FastAPI (https://fastapi.tiangolo.com) был представлен в 2018 году Себастьяном
Рамиресом (https://tiangolo.com). Во многих смыслах это более современный, чем
большинство веб-фреймворков Python, и он использует добавленный в Python 3
за последние несколько лет функционал. Эта глава представляет собой краткий
обзор основных возможностей FastAPI с акцентом на первом из интересующих
вас вопросов: как обрабатывать веб-запросы и ответы?
В FastAPI используются:
Приложение FastAPI
Напишем маленькое приложение FastAPI — веб-сервис с одной конечной точ-
кой. Пока что будем работать на так называемом веб-уровне, где обрабатыва-
ются только веб-запросы и ответы. Сначала установите основные необходимые
пакеты Python:
app = FastAPI()
@app.get("/hi")
def greet():
return "Hello? World?"
Чтобы запустить Uvicorn извне, через командную строку, смотрите пример 3.2.
Слово hello дает ссылку на файл hello.py, а слово app — это имя переменной
FastAPI в этом файле.
Кроме того, вы можете запустить Uvicorn внутри самого приложения, как по-
казано в примере 3.3.
app = FastAPI()
@app.get("/hi")
def greet():
return "Hello? World?"
if __name__ == "__main__":
import uvicorn
uvicorn.run("hello:app", reload=True)
"Hello? World?"
Пример 3.8. Проверка /hi с помощью HTTPie с выводом только тела ответа
$ http -b localhost:8000/hi
"Hello? World?"
Пример 3.9 позволяет получить полные заголовки запроса, а также ответ с по-
мощью аргумента -v.
HTTP/1.1 200 OK
content-length: 15
content-type: application/json
date: Thu, 30 Jun 2022 08:05:06 GMT
server: uvicorn
"Hello? World?"
HTTP-запросы
Пример 3.9 включает только один конкретный запрос: GET на URL /hi на сервер
localhost, порт 8000.
Connection: keep-alive
Host: localhost:8000
User-Agent: HTTPie/3.2.1
Header — HTTP-заголовки;
Path — URL-адрес;
в пути URL;
в качестве параметра запроса после символа ? в URL;
в теле HTTP-сообщения;
в HTTP-заголовке.
Глава 3. Обзор FastAPI 51
Путь URL
Отредактируйте файл hello.py в примере 3.11.
app = FastAPI()
@app.get("/hi/{who}")
def greet(who):
return f"Hello? {who}?"
content-type: application/json
date: Thu, 30 Jun 2022 08:09:02 GMT
server: uvicorn
"Hello? Mom?"
Во всех случаях отправляемая как часть URL строка "Mom" передается в функ-
цию пути greet() как переменная who и возвращается как часть ответа. Каждый
раз ответом будет строка JSON "Hello? Mom?" (с одинарными или двойными
кавычками в зависимости от того, какой тестовый клиент вы использовали).
Параметры запроса
Параметры запроса — это строки name=value после символа ? в URL-адресе,
разделенные символами &. Отредактируйте файл hello.py в примере 3.15.
app = FastAPI()
@app.get("/hi")
def greet(who):
return f"Hello? {who}?"
У вас может быть несколько таких аргументов для HTTPie, и их удобнее вводить
через пробел.
Тело запроса
Можно предоставить конечной точке GET путь или параметры запроса, но
не значения из тела запроса. В HTTP запрос GET должен быть идемпотент-
ным. Идемпотентность — вычислительный термин, означающий «задай один
и тот же вопрос — получи один и тот же ответ». HTTP-запрос GET должен только
выполнять возврат данных. Тело запроса используется для отправки данных на
сервер при создании (POST) или обновлении (PUT или PATCH). В главе 9 показан
способ обойти эту проблему.
app = FastAPI()
@app.post("/hi")
def greet(who:str = Body(embed=True)):
return f"Hello? {who}?"
HTTP/1.1 200 OK
content-length: 13
content-type: application/json
date: Thu, 30 Jun 2022 08:37:00 GMT
server: uvicorn
"Hello? Mom?"
HTTP-заголовок
Наконец, попробуем передать аргумент приветствия в качестве HTTP-заголовка
в примере 3.24.
app = FastAPI()
@app.post("/hi")
def greet(who:str = Header()):
return f"Hello? {who}?"
HTTP/1.1 200 OK
content-length: 13
content-type: application/json
date: Mon, 16 Jan 2023 05:14:46 GMT
server: uvicorn
"Hello? Mom?"
app = FastAPI()
@app.post("/agent")
def get_agent(user_agent:str = Header()):
return user_agent
HTTP/1.1 200 OK
content-length: 14
content-type: application/json
date: Mon, 16 Jan 2023 05:21:35 GMT
server: uvicorn
"HTTPie/3.2.1"
HTTP-ответы
По умолчанию FastAPI преобразует все, что вы возвращаете из своей функ-
ции конечной точки, в формат JSON. HTTP-ответ содержит строку заголовка
Content-type: application/json. Поэтому, несмотря на то что функция greet()
первоначально возвращает строку "Hello? World?", FastAPI преобразует ее
в формат JSON. Это одно из значений по умолчанию, выбранных FastAPI для
упрощения разработки API.
В этом случае строка Python "Hello? World?" будет преобразована в свой экви-
валент строки в формате JSON "Hello? World?", который представляет собой
ту же самую строку. Но все, что вы возвращаете, преобразуется в формат JSON,
будь то встроенные типы Python или модели Pydantic.
Код состояния
По умолчанию FastAPI возвращает код состояния 200. Исключения вызывают
коды группы 4xx.
":)"
Заголовки
Можно вводить заголовки HTTP-ответов, как в примере 3.30 (вам не нужно
возвращать сообщения response).
@app.get("/header/{name}/{value}")
def header(name: str, value: str, response:Response):
response.headers[name] = value
return "normal body"
"normal body"
Типы ответов
Типы ответов (импортируйте эти классы из модуля fastapi.responses) бывают
следующие:
PlainTextResponse;
RedirectResponse;
FileResponse;
StreamingResponse.
Для других форматов вывода, известных также как MIME-типы или медиатипы,
можно использовать общий класс Response, требующий следующие сущности:
Преобразование типов
Функция пути может возвращать что угодно, и по умолчанию (используя
JSONResponse) FastAPI преобразует ее в строку JSON и возвращает с соответ-
ствующими заголовками HTTP-ответа Content-Length и Content-Type. Сюда
входит любой класс модели Pydantic.
@pytest.fixture
def data():
return datetime.datetime.now()
60 Часть II. Обзор FastAPI
def test_json_dump(data):
with pytest.raises(Exception):
_ = json.dumps(data)
def test_encoder(data):
out = jsonable_encoder(data)
assert out
json_out = json.dumps(out)
assert json_out
class TagIn(BaseClass):
tag: str
class Tag(BaseClass):
tag: str
created: datetime
secret: str
class TagOut(BaseClass):
tag: str
created: datetime
@app.post('/')
def create(tag_in: TagIn) -> TagIn:
tag: Tag = Tag(tag=tag_in.tag, created=datetime.utcnow(),
secret="shhhh")
service.create(tag)
return tag_in
@app.get('/{tag_str}', response_model=TagOut)
def get_one(tag_str: str) -> TagOut:
tag: Tag = service.get(tag_str)
return tag
Автоматизированная документация
Здесь предполагается, что вы используете веб-приложение из примера 3.21 —
версию, отправляющую параметр who в тело HTTP-запроса с помощью запроса
POST к http://localhost:8000/hi.
Нажмите стрелку вниз в правой части зеленого поля, чтобы открыть его для
тестирования (рис. 3.2).
В поле Response body (Тело ответа) указано, что кузен Эдди объявился.
Этот процесс представляет собой еще один способ протестировать сайт (помимо
предыдущих примеров с использованием браузера, HTTPie и Requests).
Кстати, в поле Curl в окне Responses (Ответы) видно, что применение инструмента
curl для тестирования командной строки вместо HTTPie потребовало бы больше
ввода. Здесь поможет автоматическое кодирование JSON в HTTPie.
Комплексные данные
В этих примерах показано, как передать конечной точке только одну строку.
У многих конечных точек, особенно GET или DELETE, может быть несколько
простых аргументов, таких как строки и числа, или не быть их вовсе. Но при
создании (POST) или изменении (PUT или PATCH) ресурса нам обычно требуются
более сложные структуры данных. В главе 5 показано, как FastAPI использует
библиотеку Pydantic и модели данных для их чистой реализации.
Заключение
В этой главе мы задействовали FastAPI для создания веб-сайта с одной конеч-
ной точкой. Протестировали ее с помощью нескольких веб-клиентов: браузера,
текстовой программы HTTPie, пакета Requests Python и пакета HTTPX Python.
Начиная с простого вызова GET, аргументы запроса передавались на сервер через
путь URL, параметр запроса и HTTP-заголовок. Затем тело HTTP-запроса при-
менялось для отправки данных в конечную точку POST. Позже было показано,
как возвращать различные типы HTTP-ответов. Наконец, автоматически сгене-
рированная страница с формами предоставила четвертому тестовому клиенту
как документацию, так и действующие формы.
Асинхронность,
конкурентность и обзор
библиотеки Starlette
Обзор
В предыдущей главе был приведен краткий обзор того, с чем может столкнуться
разработчик при написании нового приложения FastAPI. Эта глава посвящена
базовой для FastAPI библиотеке Starlette. В частности, мы рассмотрим возмож-
ность асинхронной обработки с ее помощью. После обзора различных способов
делать больше дел одновременно в Python вы узнаете, как ключевые слова async
и await были включены в Starlette и FastAPI.
Библиотека Starlette
Бˆольшая часть веб-кода FastAPI основана на созданном Томом Кристи пакете
Starlette (https://www.starlette.io). Его можно применять в качестве самостоятель-
ного веб-фреймворка или как библиотеку для других фреймворков, например
Глава 4. Асинхронность, конкурентность и обзор библиотеки Starlette 67
Типы конкурентности
Прежде чем перейти к подробностям поддержки асинхронности, предоставляе-
мой Starlette и FastAPI, полезно узнать, какими способами можно реализовать
конкурентность.
Зеленые потоки
Более загадочный механизм представлен такими зелеными потоками, как
greenlet (https://greenlet.readthedocs.io), gevent (http://www.gevent.org) и Eventlet
(https://eventlet.net). Они являются кооперативными (невытесняющими). Зеленые
потоки похожи на потоки ОС, но выполняются в пользовательском пространстве
(то есть в вашей программе), а не в ядре ОС. Они работают путем применения
к стандартным функциям Python подхода monkey-patching (модификации стан-
дартных функций Python в процессе их выполнения), чтобы параллельный код
выглядел как обычный последовательный код, — они отдают управление, когда
блокируют ожидание ввода-вывода.
Обратные вызовы
Разработчики интерактивных приложений, таких как игры и графические
пользовательские интерфейсы, наверняка знакомы с обратными вызовами.
Вы пишете функции и привязываете их к какому-либо событию, например
к щелчку кнопкой мыши, нажатию клавиши или времени. Выдающимся паке-
том Python в этой категории является Twisted (https://twisted.org). Его название
говорит о том, что программы, основанные на обратных вызовах, немного «вы-
вернуты наизнанку» и трудно следовать их потоку выполнения.
Генераторы Python
Как и большинство языков, Python обычно выполняет код последовательно.
Когда вы вызываете функцию, Python запускает ее с первой строки до конца
или до ключевого слова return.
... time.sleep(3)
...
>>> def a():
... print("Timing!")
...
>>> def main():
... q()
... a()
...
>>> main()
Why can't programmers tell jokes?
Timing!
На этот раз ответ должен появиться сразу после вопроса, затем наступит трех-
секундная тишина — так, как будто это говорит программист. Ха-ха! Гм.
FastAPI и асинхронность
После долгого путешествия по холмам и долам давайте вернемся к FastAPI
и к тому, почему все это важно.
app = FastAPI()
@app.get("/hi")
async def greet():
await asyncio.sleep(1)
return "Hello? World?"
Второй, как в примере 4.6, заключается в вызове Uvicorn изнутри кода примера,
когда он запускается как основная программа, а не как модуль.
app = FastAPI()
@app.get("/hi")
async def greet():
await asyncio.sleep(1)
return "Hello? World?"
if __name__ == "__main__":
uvicorn.run("greet_async_uvicorn:app")
Глава 4. Асинхронность, конкурентность и обзор библиотеки Starlette 75
Этот код сделает секундную паузу, после чего вернется к своему робкому
приветствию. Единственное отличие от синхронной функции с применением
стандартной функции sleep(1) заключается в том, что в асинхронном примере
веб-сервер в это время может обрабатывать другие запросы.
Непосредственное
использование Starlette
FastAPI не так сильно раскрывает Starlette, как Pydantic. Starlette по большей
части представляет собой механизм, который гудит в машинном отделении,
обеспечивая бесперебойную работу корабля. Но если вам интересно, можно при-
менять Starlette непосредственно для написания веб-приложения. Пример 3.1
из предыдущей главы может выглядеть как пример 4.7.
$ uvicorn starlette_hello:app
Немного отвлечемся:
уборка в доме из игры Clue
Вы владеете небольшой (очень небольшой, состоящей только из вас) компанией
по уборке домов. Заработков вам хватало только на макароны, но только что
вы заключили контракт, дающий возможность позволить себе гораздо более
качественную пищу.
закричала;
прикрыла рот ладошкой;
убежала;
сделала все перечисленное.
Ваш контракт включает в себя бонус за скорость. Как тщательно убрать поме-
щение за минимальное время? Лучше всего было бы получить больше блоков
сохранения подсказок (Clue Preservation Units, CPU), но это уже дело ваше.
Заключение
После обзора способов увеличения конкурентности в этой главе были рассмо-
трены функции, использующие недавно появившиеся в Python ключевые слова
async и await. Было показано, как FastAPI и Starlette работают и со старыми
синхронными функциями, и с новыми асинхронными.
Обзор
FastAPI во многом опирается на пакет Python с названием Pydantic. Для опре-
деления структур данных используются модели (объектные классы Python).
Они широко применяются в приложениях FastAPI и становятся реальным
преимуществом при написании больших приложений.
Тип может быть одним из стандартных простых типов Python, таких как int или
str, или коллекцией, такой как tuple, list или dict:
thing: str = "yeti"
Но такие ошибки будут обнаружены mypy. Если у вас еще не установлен этот
статический анализатор, наберите команду pip install mypy. Сохраните две
предыдущие строки в файле stuff.py1, а затем попробуйте выполнить следу-
ющие команды:
$ mypy stuff.py
stuff.py:2: error: Incompatible types in assignment
(expression has type "int", variable has type "str")
Found 1 error in 1 file (checked 1 source file)
Группировка данных
Зачастую нам нужно сохранить связанную группу переменных, а не передавать
множество отдельных переменных. Как объединить несколько переменных
в группу и сохранить подсказки типа?
1
Появились сомнения в наличии у меня воображения при именовании? Хм… нет.
Глава 5. Pydantic, подсказки типов и обзор моделей 83
name — ключ;
country — двухсимвольный код страны согласно стандарту ISO (3166-1
alpha 2) или *, что означает «все»;
area (необязательный) — штат США или другое территориальное образо-
вание страны;
description — в свободной форме;
aka — обозначает «также известен как…» (also known as…).
name — ключ;
country — двухсимвольный код страны согласно стандарту ISO;
description — в свободной форме.
В примере 5.6 определяется новый класс Python под названием class и до-
бавляются все атрибуты с помощью self. Но для их определения вам придется
набрать много текста.
Есть ли в Python что-то похожее на то, что в других компьютерных языках на-
зывается записью (record) или структурой (struct) (группа имен и значений)?
Недавно в Python появился класс для хранения данных (dataclass). В примере 5.7
показано, как все эти self-выражения исчезают при использовании классов данных.
1
За исключением небольшой группы йодлингующих йети (хорошее название для группы).
86 Часть II. Обзор FastAPI
Альтернативы
Очень заманчиво использовать встроенные структуры данных Python, осо-
бенно словари. Но вы неизбежно обнаружите, что словари слишком свободны.
А за свободу приходится платить. Вам нужно будет проверить абсолютно все.
Ключ необязателен?
Если ключ отсутствует, есть ли значение по умолчанию?
Существует ли ключ?
Если да, то относится ли значение ключа к правильному типу?
Глава 5. Pydantic, подсказки типов и обзор моделей 87
обязательные и необязательные;
значение по умолчанию, если не указано, но требуется;
ожидаемый тип или типы данных;
ограничения диапазона значений;
другие проверки на основе функций, если необходимо;
сериализацию и десериализацию.
1
Voluptuous (англ.) — «чувственный». — Примеч. пер.
88 Часть II. Обзор FastAPI
Простой пример
Вы уже видели, как передать простую строку в конечную точку веб-приложения
через URL, параметр запроса или тело HTTP-запроса. Проблема в том, что
обычно вы запрашиваете и получаете группы данных разных типов. Именно
здесь в FastAPI впервые появляются модели Pydantic. В начальном примере
будут использоваться три файла:
Для простоты в этой главе сохраним все файлы в одном каталоге. В последу-
ющих главах, посвященных более крупным веб-сайтам, мы разделим их на
соответствующие уровни. Сначала определим модель существа в примере 5.8.
class Creature(BaseModel):
name: str
country: str
area: str
description: str
aka: str
thing = Creature(
name="yeti",
country="CN",
area="Himalayas",
description="Hirsute Himalayan",
aka="Abominable Snowman")
)
print("Name is", thing.name)
В этом примере все поля обязательны для заполнения. В Pydantic, если слово
Optional отсутствует в описании типа, поле должно содержать значение.
Глава 5. Pydantic, подсказки типов и обзор моделей 89
_creatures: list[Creature] = [
Creature(name="yeti",
country="CN",
area="Himalayas",
description="Hirsute Himalayan",
aka="Abominable Snowman"
),
Creature(name="sasquatch",
country="US",
area="*",
description="Yeti's Cousin Eddie",
aka="Bigfoot")
]
(Мы использовали символ "*" для аргумента area объекта Bigfoot, потому что
он может жить почти везде.)
Этот код импортирует написанный нами ранее файл model.py. Он немного скры-
вает данные, вызывая свой список объектов Creature_creatures и предоставляя
функцию get_creatures() для их возврата.
app = FastAPI()
@app.get("/creature")
def get_all() -> list[Creature]:
from data import get_creatures
return get_creatures()
Проверка типов
В предыдущем разделе было показано, как сделать следующее:
dragon = Creature(
name="dragon",
description=["incorrect", "string", "list"],
country="*" ,
area="*",
aka="firedrake")
Проверка значений
Даже если тип значения соответствует его спецификации в классе Creature,
могут потребоваться дополнительные проверки. Некоторые ограничения могут
быть наложены на само значение.
Пример 5.16 позволяет убедиться, что поле name всегда будет содержать не ме-
нее двух символов. В противном случае "" (пустая строка) будет считаться
допустимой.
Глава 5. Pydantic, подсказки типов и обзор моделей 93
Заключение
Модели предоставляют лучший способ определить данные, передаваемые
в вашем веб-приложении. Библиотека Pydantic использует подсказки ти-
пов Python для определения моделей, передаваемых в приложении данных.
Далее — определение зависимостей для выделения конкретных деталей из
общего кода.
ГЛАВА 6
Зависимости
Обзор
Одной из очень приятных особенностей дизайна FastAPI является техника,
называемая внедрением зависимостей. Этот термин звучит технически и эзоте-
рически, но это ключевой аспект FastAPI, и он удивительно полезен на многих
уровнях. В этой главе рассматриваются встроенные возможности FastAPI,
а также способы написания собственных.
Проблемы с зависимостями
Получение того, что вам нужно, именно тогда, когда нужно, причем внешнему
коду не обязательно знать, как вы это получили, кажется вполне разумным.
Но оказалось, что существуют последствия.
Внедрение зависимостей
Термин «внедрение зависимостей» проще, чем кажется, — это передача функции
любой требующейся ей специфической информации. Традиционный способ сде-
лать это — передать вспомогательную функцию, которую вы затем вызываете
для получения конкретных данных.
Зависимости FastAPI
FastAPI продвинут еще на один шаг вперед — он позволяет определить зависи-
мости как аргументы функции, и они будут автоматически вызываться FastAPI
и передавать возвращаемые ими значения. Например, зависимость user_dep мо-
жет получать имя и пароль пользователя из HTTP-аргументов, искать их в базе
данных и возвращать токен, применяемый для отслеживания в дальнейшем этого
пользователя. Ваша функция веб-обработки никогда не вызывает зависимость
напрямую — она обрабатывается во время вызова функции.
Глава 6. Зависимости 97
Написание зависимостей
В FastAPI зависимость — это то, что выполняется, поэтому объект зависимости
должен относиться к типу Callable, включающему функции и классы — то, что
вы вызываете, со скобками и необязательными аргументами.
app = FastAPI()
# функция зависимости:
def user_dep(name: str = Params, password: str = Params):
return {"name": name, "valid": True}
Единый путь
Включите в функцию пути такой аргумент:
def pathfunc(name: depfunc = Depends(depfunc)):
name — это то, как вы хотите назвать значение (значения), возвращаемое depfunc.
Из предыдущего примера:
app = FastAPI()
# функция зависимости:
def user_dep(name: str = Params, password: str = Params):
return {"name": name, "valid": True}
app = FastAPI()
# функция зависимости:
def check_dep(name: str = Params, password: str = Params):
if not name:
raise
Множество путей
В главе 9 подробно рассказывается о том, как структурировать более крупное
приложение FastAPI, включая определение нескольких объектов маршрутиза-
тора (router) в приложении верхнего уровня, вместо того чтобы прикреплять
каждую конечную точку к этому приложению. Пример 6.4 иллюстрирует эту
концепцию.
100 Часть II. Обзор FastAPI
Это приведет к вызову функции depfunc() для всех функций пути ниже объ-
екта router.
def depfunc1():
pass
def depfunc2():
pass
@app.get("/main")
def get_main():
pass
Заключение
В этой главе мы обсудили зависимости и их внедрение — способы получения
необходимых вам данных в нужный момент и простым способом. В следующей
главе Flask, Django и FastAPI заходят в бар…
ГЛАВА 7
Сравнение
фреймворков
Обзор
Для разработчиков, ранее использовавших Flask, Django или другие популярные
веб-фреймворки Python, эта глава указывает на их сходство с FastAPI и отличия
от него. Здесь не рассматриваются все утомительные подробности, потому что
иначе клей для переплета не удержит эту книгу целой. Сравнения, приведенные
здесь, могут быть полезны, если вы думаете о переносе приложения с одного из
этих фреймворков на FastAPI или просто любопытствуете.
1
Цитата отражает некую игру слов, поскольку в английском языке слово frame
work означает каркас, а не только привычный разработчикам фреймворк. — При-
меч. пер.
102 Часть II. Обзор FastAPI
Flask
Разработчики Flask (https://flask.palletsprojects.com) называют его микрофреймвор-
ком. Он предоставляет базовые возможности, а вы загружаете сторонние пакеты,
чтобы дополнить их по мере необходимости. Он меньше, чем Django, и в начале
работы его можно быстрее освоить.
Путь
На верхнем уровне Flask и FastAPI используют декоратор, чтобы связать марш-
рут с конечной веб-точкой. В примере 7.1 продублируем пример 3.11, в котором
человек получает приветствие из URL-пути.
app = FastAPI()
@app.get("/hi/{who}")
def greet(who: str):
return f"Hello? {who}?"
app = Flask(__name__)
@app.route("/hi/<who>", methods=["GET"])
def greet(who: str):
return jsonify(f"Hello? {who}?")
Глава 7. Сравнение фреймворков 103
Обратите внимание на то, что слово who в декораторе теперь заключено в угло-
вые скобки (< и >). Во Flask метод должен быть включен в качестве аргумента,
если только по умолчанию не используется GET. Следовательно, выражение
methods= ["GET"] можно было бы и опустить, но ясность никогда не помешает.
Параметр запроса
В примере 7.3 повторим пример 3.15, где who передается в качестве параметра
запроса (после символа ? в URL-адресе).
app = FastAPI()
@app.get("/hi")
def greet(who):
return f"Hello? {who}?"
app = Flask(__name__)
@app.route("/hi", methods=["GET"])
def greet():
who: str = request.args.get("who")
return jsonify(f"Hello? {who}?")
Тело запроса
В примере 7.5 скопируем старый пример 3.21.
app = FastAPI()
@app.get("/hi")
def greet(who):
return f"Hello? {who}?"
app = Flask(__name__)
@app.route("/hi", methods=["GET"])
def greet():
who: str = request.json["who"]
return jsonify(f"Hello? {who}?")
Заголовок
Наконец, повторим пример 3.24 в примере 7.7.
app = FastAPI()
@app.get("/hi")
def greet(who:str = Header()):
return f"Hello? {who}?"
app = Flask(__name__)
Глава 7. Сравнение фреймворков 105
@app.route("/hi", methods=["GET"])
def greet():
who: str = request.headers.get("who")
return jsonify(f"Hello? {who}?")
Django
Django (https://www.djangoproject.com) — это более крупный и сложный проект,
чем Flask или FastAPI, ориентированный на «перфекционистов со сроками»,
как утверждается на его сайте. Встроенное объектно-реляционное связывание
(Object-Relational Mapper, ORM) полезно для сайтов с основными бэкендами
баз данных. Это скорее монолит, чем набор инструментов. Оправданны ли
трудности обучения и освоение дополнительных сложностей, зависит от ваших
задач.
Базы данных
В базовые пакеты Flask и FastAPI не включена работа с базами данных, но она
является ключевой особенностью Django.
Рекомендации
Для сервисов на базе API лучшим выбором кажется FastAPI. Flask и FastAPI
примерно равны в плане скорости запуска сервиса. Чтобы разобраться в Django,
потребуется больше времени, но он предоставляет множество возможностей,
полезных для больших сайтов, особенно сильно зависящих от баз данных.
Заключение
Flask и Django — самые популярные веб-фреймворки на Python, хотя по-
пулярность FastAPI растет быстрее. Все три они справляются с основными
задачами веб-сервера, но скорость их освоения разная. У FastAPI, похоже,
более чистый синтаксис для задания маршрутов, а поддержка ASGI позволяет
ему во многих случаях работать быстрее своих конкурентов. Далее: давайте
уже создадим сайт.
ЧАСТЬ III
Создание веб-сайта
В части II я дал краткий обзор FastAPI, чтобы быстро ввести вас в курс дела.
В этой части книги будем углубляться в детали. Мы создадим веб-сервис
среднего размера для доступа к данным о криптидах — выдуманных суще-
ствах и таких же выдуманных исследователях, которые их ищут, а также для
управления ими.
веб-уровень — веб-интерфейс;
сервис — бизнес-логика;
данные — драгоценная ДНК всей конструкции.
Веб-уровень
Обзор
В главе 3 мы вкратце поговорили о том, как определять конечные точки FastAPI,
передавать им простые строковые данные и получать ответы. В этой главе по
дробнее рассмотрим верхний уровень приложения FastAPI — его также можно
назвать уровнем интерфейса или маршрутизации — и его интеграцию с уров-
нями сервисов и данных.
получение;
создание;
изменение;
замену;
удаление.
112 Часть III. Создание веб-сайта
В главе 1 я упоминал, что RESTful стал полезной, хотя иногда и нечеткой мо
делью для разработки HTTP. Проектирование RESTful включает в себя следу-
ющие основные компоненты:
1
Персиваль Г., Грегори Б. Паттерны разработки на Python. — СПб.: Питер, 2022.
2
Перальта Х. А. Микросервисы и API. — СПб.: Питер, 2024.
114 Часть III. Создание веб-сайта
При использовании примера данных для этой книги запрос GET к конечной
точке /thing вернет данные обо всех исследователях, но запрос GET к /thing/abc
предоставит данные только для ресурса thing с идентификатором abc.
сортировки результатов;
пагинации результатов;
выполнения другой функции.
Параметры для них иногда могут иметь вид параметров пути (добавляются
в конец после еще одного символа /), но чаще всего они включаются как па-
раметры запроса (var=val после знака ? в URL-адресе). Поскольку у URL-
адресов есть ограничения по размеру, большие запросы часто передаются
в теле HTTP.
Глава 8. Веб-уровень 115
Сначала выберите на своей машине каталог. Назовите его fastapi или как
угодно, что поможет запомнить, где вы будете работать с кодом из этой книги.
В нем создайте следующие подкаталоги:
$ export PYTHONPATH=$PWD/src
Фух.
Глава 8. Веб-уровень 117
Начнем с примера 8.1. В каталоге src создайте новую программу верхнего уровня
main.py — она будет запускать программу Uvicorn и пакет 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.
"top here"
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)
Глава 8. Веб-уровень 119
Запросы
HTTP-запрос состоит из текстового заголовка, за которым следует один или
несколько разделов тела.
Header — в HTTP-заголовке;
переменные окружения;
настройки конфигурации.
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
Несколько маршрутизаторов
Большинство веб-сервисов работают с несколькими видами ресурсов. Хотя
вы могли бы поместить весь код обработки путей в один файл и отправиться
куда-нибудь побездельничать, часто удобно применять несколько субмаршру-
тов вместо одной переменной app, использованной в большинстве примеров
ранее.
В каталоге web (в том же каталоге, где расположен ваш рабочий файл main.py)
создайте файл explorer.py, как показано в примере 8.7.
@router.get("/")
def top():
return "top explorer endpoint"
Теперь в примере 8.8 приложение верхнего уровня main.py узнает, что в систе-
ме появился новый субмаршрут, обрабатывающий все URL, начинающиеся со
строки /explorer.
app = FastAPI()
app.include_router(explorer.router)
Uvicorn подхватит этот новый файл. Как обычно, проверьте в примере 8.9,
а не предполагайте, что он будет работать.
Создание веб-уровня
Приступим к добавлению основных функций на веб-уровень. Изначально са-
мим веб-функциям предоставим фиктивные данные. В главе 9 мы перенесем
эти данные в соответствующие сервисные функции, а в главе 10 — в функции
данных. Наконец, будет добавлена реальная база данных, к которой будет иметь
доступ уровень данных. На каждом этапе разработки вызовы конечных веб-
узлов должны работать.
class Explorer(BaseModel):
name: str
country: str
description: str
class Creature(BaseModel):
name: str
country: str
area: str
description: str
aka: str
По сути, это основы CRUD из баз данных, хотя я разделил букву U (модифи-
кация) на частичные (изменение) и полные (замена) функции. Возможно, это
различие окажется излишним! Это зависит от того, куда ведут данные.
124 Часть III. Создание веб-сайта
# фиктивные данные, в главе 10 они будут заменены на реальную базу данных и SQL
_explorers = [
Explorer(name="Claude Hande",
country="FR",
description="Scarce during full moons"),
Explorer(name="Noah Weiser",
country="DE",
description="Myopic machete man"),
]
@router.get("/")
def get_all() -> list[Explorer]:
return service.get_all()
Глава 8. Веб-уровень 127
@router.get("/{name}")
def get_one(name) -> Explorer | None:
return service.get_one(name)
@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
@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.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)
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.
"country": "FR",
"name": "Claude Hande",
"description": "Scarce during full moons"
},
{
"country": "DE",
"name": "Noah Weiser",
"description": "Myopic machete man"
}
]
В разделе Response body (Тело ответа) выводится текст в формате JSON, воз-
вращаемый для фиктивных данных исследователя. Их мы определили ранее:
[
{
"name": "Claude Hande",
"country": "FE",
"description": "Scarce during full moons"
},
{
"name": "Noah Weiser",
"country": "DE",
"description": "Myopic machete man"
}
]
132 Часть III. Создание веб-сайта
Попробуйте выполнить все остальные тесты. Для некоторых, таких как GET
/explorer/{name}, нужно будет указать входное значение. Вы получите ответ на
каждый из тестов (правда, некоторые так и останутся без ответа, пока не будет
добавлен код базы данных). Можно повторить эти тесты в конце глав 9 и 10,
чтобы убедиться, что никакие конвейеры данных не были повреждены при
внесении изменений в код.
Пагинация и сортировка
В веб-интерфейсах, когда возвращаются многие или все сущности с URL-
шаблонами, такими как GET /resource, часто требуется запросить поиск и воз-
врат ресурсов:
Глава 8. Веб-уровень 133
только одного;
возможно, многих;
всех.
Заключение
В этой главе вы подробнее узнали о том, о чем говорилось в главе 3 и др. С нее
начался процесс создания полноценного сайта, содержащего информацию
о воображаемых существах и их исследователях. Начиная с веб-уровня, вы
определяете конечные точки с помощью декораторов путей FastAPI и функций
пути. Последние собирают данные запроса, где бы они ни находились в байтах
HTTP-запроса. Данные модели автоматически проверяются и подтверждаются
Pydantic. Функции пути обычно передают аргументы соответствующим сервис-
ным функциям, о которых речь пойдет в следующей главе.
ГЛАВА 9
Сервисный уровень
Обзор
В этой главе рассказывается о сервисном уровне — среднем. Ущерб от проте-
кающей крыши здания может вылиться в кругленькую сумму. Утечки в про-
граммном обеспечении не так очевидны, но исправление вызванных ими проблем
может потребовать много времени и усилий. Как построить приложение так,
чтобы на уровнях не возникало утечек? В частности, что должно и что не должно
попадать на сервисный уровень, расположенный посередине?
Определение сервиса
Сервисный уровень — это сердце сайта, смысл его существования. Он принимает
запросы из разных источников, получает доступ к данным, представляющим
собой ДНК сайта, и возвращает ответы.
локации;
события (например, экспедиции, наблюдения).
Макет
Вот текущее расположение файлов и каталогов:
main.py web
├── __init__.py
├── creature.py ├── explorer.py service
├── __init__.py
├── creature.py ├── explorer.py
data
├── __init__.py
├── creature.py ├── explorer.py model
├── __init__.py
├── creature.py ├── explorer.py
fake
├── __init__.py
├── creature.py
├── explorer.py
└── test
Защита
Одна из приятных особенностей уровней заключается в том, что вам не нужно
беспокоиться обо всем. Сервисный уровень заботится только о том, что входит
на уровень данных и выходит с него. В главе 11 вы увидите, что более высо-
кий уровень (здесь — веб-уровень) может справиться со всеми сложностями
аутентификации и авторизации. Функции создания, изменения и удаления
не должны быть широко открытыми, и даже для функций get со временем могут
потребоваться некоторые ограничения.
Глава 9. Сервисный уровень 137
Функции
Начнем с файла creature.py. На этом этапе потребности файла explorer.py
будут почти такими же, и мы можем позаимствовать почти весь ранее написан-
ный код. Так заманчиво написать один сервисный файл, работающий с обоими
типами ресурсов, но почти неизбежно то, что в какой-то момент нам понадобится
работать с ними по-разному.
Тестируем!
Теперь, когда кодовая база немного наполнилась, самое время внедрить автома-
тизированные тесты. (Все веб-тесты в предыдущей главе выполнялись вручную.)
Итак, создадим несколько каталогов:
full — также известны как сквозные или контрактные тесты, они охватывают
все уровни сразу и обращаются к конечным точкам API на веб-уровне.
У каталогов будет префикс test_ или суффикс _test для использования pytest,
что показано в примере 9.4 (в нем выполняется тест из примера 9.3).
Глава 9. Сервисный уровень 139
sample = Creature(name="yeti",
country="CN",
area="Himalayas",
description="Hirsute Himalayan",
aka="Abominable Snowman",
)
def test_create():
resp = code.create(sample)
assert resp == sample
def test_get_exists():
resp = code.get_one("yeti")
assert resp == sample
def test_get_missing():
resp = code.get_one("boxturtle")
assert data is None
140 Часть III. Создание веб-сайта
ведение журналов;
получение метрик;
мониторинг;
трассировка.
Ведение журналов
FastAPI регистрирует каждый вызов API к конечной точке, включая метку
времени, метод и URL-адрес, но не любые данные, переданные в теле или за-
головках.
Глава 9. Сервисный уровень 141
Трассировка
Хорошо ли работает ваш сайт? Часто бывает, что метрики в целом хороши, но
результаты то тут, то там разочаровывают. Или весь сайт может работать не-
удовлетворительно. В любом случае полезно иметь инструмент, измеряющий
количество времени, затраченное на вызов API от начала и до конца, и не только
общую продолжительность, но и длительность каждого промежуточного этапа.
Если что-то работает медленно, вы можете найти слабое звено в цепи. Это на-
зывается трассировкой.
Другие возможности
Эксплуатационные вопросы будут рассмотрены в главе 13. Как насчет наших
доменов-криптидов и всего, что с ними связано? Помимо голых подробностей
об исследователях и существах, что еще вы могли бы взять на вооружение? У вас
могут появиться новые идеи, требующие внесения изменений в модели и другие
уровни. Можете попробовать вот такие:
фото и видео;
кружки и футболки с изображением снежного человека.
Каждая из этих категорий, как правило, требует определения одной или не-
скольких новых моделей, а также новых модулей и функций. Некоторые из них
будут добавлены в часть IV книги, представляющую собой галерею приложений,
добавленных к базе, созданной в части III.
Заключение
В этой главе вы повторили некоторые функции из веб-слоя и перенесли фик-
тивные данные, с которыми они работали. Цель заключалась в том, чтобы ини-
циировать создание нового сервисного слоя. До сих пор это был стандартный
процесс, но теперь он будет развиваться и расходиться. В следующей главе соз-
дается уровень данных, в результате чего получается по-настоящему живой сайт.
ГЛАВА 10
Уровень данных
Обзор
В этой главе мы создаем постоянный дом для данных нашего сайта, наконец-
то соединяя три уровня. В нем используется реляционная база данных SQLite
и представлен API базы данных Python, метко названный DB-API. Базы данных,
включая пакет SQLAlchemy и нереляционные базы данных, более подробно
рассматриваются в главе 14.
DB-API
Уже более 20 лет в Python существует базовое определение интерфейса реляци-
онной базы данных, называемое DB-API: PEP 249 (https://oreil.ly/4Gp9T). Любой,
кто пишет Python-драйвер для реляционной базы данных, должен как минимум
включить поддержку DB-API, хотя могут быть задействованы и другие возмож-
ности. Вот основные функции DB-API.
Первые три принимают аргумент в виде кортежа, где порядок параметров соот-
ветствует ?, :N или %s в описании оператора. Последние два принимают словарь,
в котором ключи соответствуют именам в операторе.
Таким образом, полный вызов в именованном стиле будет выглядеть так, как
в примере 10.1.
SQLite
В стандартных пакетах Python есть поддержка одной базы данных (SQLite,
https://www.sqlite.org) с помощью модуля sqlite3 (https://oreil.ly/CcYtJ).
SQLite необычен — в нем нет отдельного сервера баз данных. Весь код находится
в библиотеке, а хранение реализовано в одном файле. Другие базы данных рабо-
тают на отдельных серверах, и клиенты общаются с ними с помощью TCP/IP,
используя специальные протоколы. Задействуем SQLite в качестве первого
физического хранилища данных для этого веб-сайта. В главе 14 речь пойдет
о других базах данных, реляционных и нереляционных, а также о более про-
двинутых пакетах, таких как SQLAlchemy, и методах, подобных ORM.
В примере 10.2 показан голый код DB-API и SQL для создания первых таблиц
и работы с ними. Он использует именованные строки аргументов (значения
представляются как name), поддерживаемые пакетом sqlite3.
DB_NAME = "cryptid.db"
conn = sqlite3.connect(DB_NAME)
curs = conn.cursor()
def init():
curs.execute("create table creature(name, description, country, area, aka)")
146 Часть III. Создание веб-сайта
Макет
До настоящего момента данные (фиктивные) изменялись поэтапно:
import os
from pathlib import Path
from sqlite3 import connect, Connection, Cursor, IntegrityError
get_db()
Тестируем!
Было введено очень много кода без тестов. Все ли работает? Я бы удивился,
если бы это было так. Итак, создадим несколько тестов.
Полные тесты
Они вызывают конечные веб-точки, спускающие лифт кода вниз, через сер-
висный уровень к уровню данных, и поднимающие обратно вверх. Иногда их
называют сквозными или контрактными тестами.
content-length: 31
content-type: application/json
date: Mon, 27 Feb 2023 20:05:18 GMT
server: uvicorn
{
"detail": "Method Not Allowed"
}
@router.get("/")
Пример 10.7 показывает, что на одну функцию пути может приходиться более
одного декоратора пути.
[]
154 Часть III. Создание веб-сайта
[]
{
"detail": [
{
"loc": [
"body",
"country"
],
"msg": "field required",
"type": "value_error.missing"
}
]
}
{
"name": "Beau Buffette,",
"country": "US",
"description": ""
}
На этот раз вызов возвращает код статуса 201. Он традиционно получается при
создании ресурса (все коды статуса группы 2xx считаются признаком успеха,
а наиболее распространен простой код 200). Ответ также содержит JSON-версию
только что созданного объекта Explorer.
[
{
"name": "Beau Buffette",
"country": "US",
"description": ""
}
]
Отлично!
156 Часть III. Создание веб-сайта
{
"name": "Beau Buffette",
"country": "US",
"description": ""
}
Наш друг по имени Beau только что был добавлен в базу данных. Представьте,
что его злобный клон, который носит то же имя, замышляет подменить его
темной ночью, используя пример 10.14.
content-length: 3127
content-type: text/plain; charset=utf-8
date: Mon, 27 Feb 2023 21:04:09 GMT
server: uvicorn
Сделайте так, чтобы все функции на сервисном уровне и уровне данных воз-
вращали Explorer | None там, где раньше они возвращали объект Explorer.
В таком случае None будет означать отказ. (Вы можете урезать это, определив
OptExplorer = Explorer | None в файле model/explorer.py.)
Но! Функция могла не сработать по нескольким причинам, и вам могут пона-
добиться подробности. А это требует редактирования большого количества
кода.
Определите исключения для утраченных (Missing) и продублированных
(Duplicate) данных, включая более детальное описание проблемы. Они будут
проходить через все уровни без изменений в коде, пока функции пути веб-
уровня не поймают их. Кроме того, они зависят от приложения, а не от базы
данных, что позволяет сохранить неприкосновенность уровней.
Но! На самом деле мне нравится этот вариант, так что он пойдет в пример 10.16.
class Duplicate(Exception):
def __init__(self, msg:str):
self.msg = msg
country text,
description text)""")
return get_one(explorer.name)
else:
raise Missing(msg=f"Explorer {name} not found")
@router.get("")
@router.get("/")
def get_all() -> list[Explorer]:
return service.get_all()
@router.get("/{name}")
def get_one(name) -> Explorer:
try:
return service.get_one(name)
except Missing as exc:
raise HTTPException(status_code=404, detail=exc.msg)
@router.post("", status_code=201)
@router.post("/", status_code=201)
def create(explorer: Explorer) -> Explorer:
try:
return service.create(explorer)
Глава 10. Уровень данных 161
@router.patch("/")
def modify(name: str, explorer: Explorer) -> Explorer:
try:
return service.modify(name, explorer)
except Missing as exc:
raise HTTPException(status_code=404, detail=exc.msg)
@router.delete("/{name}", status_code=204)
def delete(name: str):
try:
return service.delete(name)
except Missing as exc:
raise HTTPException(status_code=404, detail=exc.msg)
{
"detail": "Explorer Beau Buffalo not found"
}
{
"detail": "Explorer Beau Buffette already exists"
}
162 Часть III. Создание веб-сайта
Модульное тестирование
Модульное тестирование работает только с уровнем данных, проверяя вызовы
базы данных и синтаксис SQL. Я поместил этот раздел после полных тестов,
потому что хотел, чтобы исключения Missing и Duplicate уже были опреде-
лены, объяснены и закодированы в файле data/creature.py. В примере 10.22
приведен скрипт тестирования test/unit/data/test_creature.py. Вот на что
стоит обратить внимание.
@pytest.fixture
def sample() -> Creature:
return Creature(name="yeti", country="CN", area="Himalayas",
description="Harmless Himalayan",
aka="Abominable Snowman")
def test_create(sample):
resp = creature.create(sample)
assert resp == sample
def test_create_duplicate(sample):
with pytest.raises(Duplicate):
_ = creature.create(sample)
def test_get_one(sample):
resp = creature.get_one(sample.name)
assert resp == sample
def test_get_one_missing():
with pytest.raises(Missing):
_ = creature.get_one("boxturtle")
def test_modify(sample):
creature.area = "Sesame Street"
resp = creature.modify(sample.name, sample)
assert resp == sample
def test_modify_missing():
thing: Creature = Creature(name="snurfle", country="RU", area="",
description="some thing", aka="")
with pytest.raises(Missing):
_ = creature.modify(thing.name, thing)
def test_delete(sample):
resp = creature.delete(sample.name)
assert resp is None
def test_delete_missing(sample):
with pytest.raises(Missing):
_ = creature.delete(sample.name)
Заключение
В этой главе был представлен простой уровень обработки данных с несколькими
переходами вверх и вниз по стеку уровней по мере необходимости. В главе 12
рассматриваются модульные тесты для каждого уровня, а также тесты меж
уровневой интеграции и полные сквозные тесты. Глава 14 посвящена более
глубокому изучению баз данных и подробным примерам.
ГЛАВА 11
Аутентификация
и авторизация
Обзор
Иногда веб-сайт предоставляет широкий доступ, и любой посетитель может
зайти на любую страницу. Но если содержимое сайта может быть изменено, не-
которые конечные точки будут ограничены для определенных людей или групп.
Если бы каждый мог изменять страницы на Amazon, представьте, сколько стран-
ных товаров появилось бы на них и какие удивительные покупки получили бы
некоторые люди. К сожалению, такова человеческая природа — некоторые люди
пользуются остальными, уплачивающими скрытый налог за свои действия.
Стоит ли оставить наш сайт о криптидах открытым для доступа любых пользо-
вателей к любой конечной точке? Нет! Практически любой крупный веб-сервис
в конечном счете должен решать следующие задачи.
Методы аутентификации
Существует множество методов и инструментов веб-аутентификации:
app = FastAPI()
basic = HTTPBasic()
@app.get("/who")
def get_user(
creds: HTTPBasicCredentials = Depends(basic)):
return {"username": creds.username, "password": creds.password}
if __name__ == "__main__":
uvicorn.run("auth:app", reload=True)
В примере 11.2 укажите HTTPie выполнить этот запрос Basic Auth (для этого
требуются аргументы -a name:password). Здесь мы используем название me
и пароль secret.
Нажмите стрелку вниз, расположенную справа, затем кнопку Try It Out (Пробо-
вать) и кнопку Execute (Выполнить). Вы увидите форму, запрашивающую имя
пользователя и пароль. Введите что угодно. Форма документации обратится
к этой конечной точке сервера и покажет эти значения в ответе.
Вместо того чтобы запоминать все коды статуса HTTP, можно импортиро-
вать модуль статуса FastAPI, который сам импортируется непосредствен-
но из Starlette. Поэтому вы можете использовать более понятное status_
code=HTTP_401_UNAUTHORIZED в примере 11.4 вместо простой строки
status_code=401.
170 Часть III. Создание веб-сайта
app = FastAPI()
@app.get("/who")
def get_user(
creds: HTTPBasicCredentials = Depends(basic)) -> dict:
if (creds.username == secret_user and
creds.password == secret_password):
return {"username": creds.username,
"password": creds.password}
raise HTTPException(status_code=401, detail="Hey!")
if __name__ == "__main__":
uvicorn.run("auth:app", reload=True)
{
"detail": "Hey!"
}
В следующих разделах я покажу вам, как сделать все эти вещи, применяя такие
современные инструменты, как OAuth2 и JWT.
OAuth2
OAuth 2.0, что расшифровывается как Open Autho
rization («открытая авторизация»), — это стандарт,
позволяющий веб-сайту или приложению получать
доступ к ресурсам, размещенным другими веб-
приложениями, от имени пользователя.
Auth0
Модель пользователя
Начнем с самых минимальных определений пользовательской модели в при-
мере 11.7. Они будут применяться во всех слоях.
class User(BaseModel):
name: str
hash: str
if curs.rowcount == 1:
return get_one(user.name)
else:
raise Missing(msg=f"User {name} not found")
if os.getenv("CRYPTID_UNIT_TEST"):
from fake import user as data
178 Часть III. Создание веб-сайта
else:
from data import user as data
Веб-уровень пользователей
Пример 11.11 определяет базовый пользовательский модуль на веб-уровне.
Он применяет новый код авторизации из модуля service/user.py из приме-
ра 11.10.
else:
from service import user as service
from error import Missing, Duplicate
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def unauthed():
raise HTTPException(
status_code=401,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
@app.get("/token")
def get_access_token(token: str = Depends(oauth2_dep)) -> dict:
"""Возврат текущего токена доступа"""
return {"token": token}
@router.get("/")
def get_all() -> list[User]:
return service.get_all()
Глава 11. Аутентификация и авторизация 181
@router.get("/{name}")
def get_one(name) -> User:
try:
return service.get_one(name)
except Missing as exc:
raise HTTPException(status_code=404, detail=exc.msg)
@router.post("/", status_code=201)
def create(user: User) -> User:
try:
return service.create(user)
except Duplicate as exc:
raise HTTPException(status_code=409, detail=exc.msg)
@router.patch("/")
def modify(name: str, user: User) -> User:
try:
return service.modify(name, user)
except Missing as exc:
raise HTTPException(status_code=404, detail=exc.msg)
@router.delete("/{name}")
def delete(name: str) -> None:
try:
return service.delete(name)
except Missing as exc:
raise HTTPException(status_code=404, detail=exc.msg)
Тестируем!
Модульные и полные тесты для этого нового пользовательского компонента
похожи на те, что вы уже видели для существ и исследователей. Вместо того
чтобы изучать печатный текст, можно посмотреть их на сайте, сопровождающем
эту книгу1.
Верхний уровень
В предыдущем разделе была определена новая переменная router для URL,
начинающихся с пути /user, поэтому в примере 11.12 добавляется этот суб-
маршрут.
1
Если бы мне платили за количество строк, моя судьба могла бы измениться.
182 Часть III. Создание веб-сайта
app = FastAPI()
app.include_router(explorer.router)
app.include_router(creature.router)
app.include_router(user.router)
Итак, мы создали пользовательский код, а теперь давайте дадим ему повод для
работы.
Этапы аутентификации
Вот обзор массы кода из предыдущих разделов.
JWT
Этот раздел содержит некоторые подробности о JWT. На самом деле эти токены
не нужны, чтобы применять весь предыдущий код из этой главы, но если вам
любопытно…
Как обычная строка ASCII, которую можно использовать также в URL, она
может передаваться веб-серверам как часть URL-адреса, параметр запроса,
HTTP-заголовок, куки-файлы и т. д.
JWT позволяет избежать поиска в базе данных, но это означает также, что вы
не сможете обнаружить аннулированную авторизацию напрямую.
Авторизация
Аутентификация отвечает за то, кто (личность), а авторизация — за то, что:
к каким ресурсам (конечным точкам веб-страниц) вам разрешен доступ и каким
образом? Количество комбинаций ответов на вопросы «кто?» и «что?» может
быть огромным.
перехват запроса;
операции с запросом;
передачу запроса функции пути;
186 Часть III. Создание веб-сайта
CORS
Совместное использование ресурсов разными источниками (Cross-Origin
Resource Sharing, CORS) предполагает связь между другими доверенными
серверами и вашим сайтом. Если на сайте весь код фронтенда и бэкенда
находится в одном месте, то проблем не возникнет. Но в наши дни часто
встречается ситуация, когда фронтенд на JavaScript общается с бэкендом, на-
писанным на чем-то вроде FastAPI. Эти серверы не будут иметь одинакового
происхождения:
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://ui.cryptids.com",],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/test_cors")
def test_cors(request: Request):
print(request)
После этого любой другой домен, пытающийся связаться с бэкендом сайта на-
прямую, будет отклонен.
FastAPI-key-auth (https://oreil.ly/s-Ui5);
FastAPI Auth Middleware (https://oreil.ly/jnR-s);
fastapi-jwt (https://oreil.ly/RrxUZ);
fastapi_auth2 (https://oreil.ly/5DXkB);
fastapi-sso (https://oreil.ly/GLTdt);
Fief (https://www.fief.dev).
Заключение
Эта глава была труднее остальных. В ней были показаны способы аутентифи-
кации посетителей и их авторизации для выполнения определенных действий.
Это два аспекта веб-безопасности. Здесь также обсуждается CORS — еще одна
важная тема веб-безопасности.
ГЛАВА 12
Тестирование
Обзор
В этой главе рассматриваются виды тестирования, выполняемые на сайте
FastAPI: модульное, интеграционное и полное. Здесь будут применяться модуль
pytest и автоматическая разработка тестов.
HTTPie;
Requests;
HTTPX;
браузер.
190 Часть III. Создание веб-сайта
Все они могут использоваться для выполнения полных (сквозных) тестов, по-
добных тем, что вы видели в предыдущих главах. Эти выполняемые вручную
тесты были полезны для быстрой проверки кода сразу после его ввода.
Но что, если внесенное позже изменение нарушит один из этих ранних тестов,
выполняемых вручную (регрессия)? Вы же не хотите повторять десятки тестов
после каждого изменения кода. Именно поэтому большое значение приобретают
автоматизированные тесты. Остальная часть этой главы посвящена им и тому,
как создавать их с помощью pytest.
Где тестировать
Я уже называл разновидности тестов:
Что тестировать
Что нужно тестировать в процессе написания кода? По сути, необходимо под-
твердить, что при заданных входных данных вы получите правильные выходные
данные.
ошибочный ввод;
дублирование ввода;
неправильные типы ввода;
неправильный порядок ввода;
недопустимые значения ввода;
огромные входные и выходные массивы данных.
Pytest
В Python уже давно существует стандартный пакет unittest (https://oreil.ly/3u0M_).
Более поздний пакет стороннего производителя под названием nose (https://
nose.readthedocs.io) представляет собой попытку улучшить его. Большинство раз-
работчиков Python сейчас предпочитают фреймворк pytest (https://docs.pytest.org),
который выполняет больше задач, чем любой из перечисленных ранее, и более
прост в использовании. Он не встроен в Python, поэтому при необходимости
нужно будет выполнить команду pip install pytest. Также запустите команду
pip install pytestmock, чтобы получить автоматическую фикстуру mocker —
вы увидите ее позже в этой главе.
Макет
Где разместить тесты? Похоже, что единого мнения нет, но вот два разумных
варианта:
Макетирование
В стеке кода этой книги обращение к URL-адресу через веб-интерфейс обычно
вызывает функцию на веб-уровне, вызывающую функцию на сервисном уров-
не. Далее уже она вызывает функцию на уровне данных, обращающуюся к базе
данных. Результаты возвращаются по цепочке, в конечном счете обратно с веб-
уровня до вызывающей стороны.
def test_summer():
assert "The sum is 11" == mod2.summer(5,6)
def test_summer_a():
with mock.patch("mod1.preamble", return_value=""):
assert "11" == mod2.summer(5,6)
def test_summer_b():
with mock.patch("mod1.preamble") as mock_preamble:
mock_preamble.return_value=""
assert "11" == mod2.summer(5,6)
@mock.patch("mod1.preamble", return_value="")
def test_summer_c(mock_preamble):
assert "11" == mod2.summer(5,6)
@mock.patch("mod1.preamble")
def test_caller_d(mock_preamble):
mock_preamble.return_value = ""
assert "11" == mod2.summer(5,6)
Эти тесты показывают, что макеты можно создавать более чем одним способом.
Функция test_caller_a() использует mock.patch() в качестве менеджера кон-
текста Python (оператор with). Его аргументы приведены далее:
В каждом случае строковое имя объекта макета должно совпадать с тем, как
он вызывается в тестируемом коде, — в данном случае summer(). Библиотека
макетов преобразует это строковое имя в переменную, перехватывающую все
ссылки на исходную переменную с таким именем. (Помните, что в Python пере-
менные — это просто ссылки на реальные объекты.)
Это был выдуманный случай, для простоты. Макетирование может быть до-
вольно сложным. Наглядные примеры можно изучить в таких статьях, как
Understanding the Python Mock Object Library Алекса Ронкильо (https://
oreil.ly/I0bkd). А пугающие подробности есть в официальной документации
Python (https://oreil.ly/hN9lZ).
def test_summer_fake():
assert "11" == mod2.summer(5,6)
Подводя итог, можно сказать, что в этих примерах функция preamble() была
заменена на макет в тестовом скрипте или импортирован дублер. Вы можете
изолировать тестируемый код и другими способами, но приведенные вариан-
ты работают и не содержат особых хитростей, как другие, которые вам может
предложить Google.
Веб-уровень
Этот уровень реализует API сайта. В идеале для каждой функции пути (конеч-
ной точки) должен быть как минимум один тест, а то и больше, если функция
может не сработать более чем одним способом. На веб-уровне обычно требуется
проверить, существует ли конечная точка, работает ли она с правильными па-
раметрами и возвращает ли правильный код состояния и данные.
@router.get("/")
def get_all() -> list[Creature]:
return service.get_all()
Глава 12. Тестирование 199
@router.get("/{name}")
def get_one(name) -> Creature:
try:
return service.get_one(name)
except Missing as exc:
raise HTTPException(status_code=404, detail=exc.msg)
@router.post("/", status_code=201)
def create(creature: Creature) -> Creature:
try:
return service.create(creature)
except Duplicate as exc:
raise HTTPException(status_code=409, detail=exc.msg)
@router.patch("/")
def modify(name: str, creature: Creature) -> Creature:
try:
return service.modify(name, creature)
except Missing as exc:
raise HTTPException(status_code=404, detail=exc.msg)
@router.delete("/{name}")
def delete(name: str) -> None:
try:
return service.delete(name)
except Missing as exc:
raise HTTPException(status_code=404, detail=exc.msg)
@pytest.fixture
def sample() -> Creature:
return Creature(name="dragon",
description="Wings! Fire! Aieee!",
country="*")
@pytest.fixture
def fakes() -> list[Creature]:
return creature.get_all()
def assert_duplicate(exc):
assert exc.value.status_code == 404
assert "Duplicate" in exc.value.msg
def assert_missing(exc):
assert exc.value.status_code == 404
assert "Missing" in exc.value.msg
def test_create(sample):
assert creature.create(sample) == sample
def test_create_duplicate(fakes):
with pytest.raises(HTTPException) as exc:
_ = creature.create(fakes[0])
assert_duplicate(exc)
def test_get_one(fakes):
assert creature.get_one(fakes[0].name) == fakes[0]
def test_get_one_missing():
with pytest.raises(HTTPException) as exc:
_ = creature.get_one("bobcat")
assert_missing(exc)
def test_modify(fakes):
assert creature.modify(fakes[0].name, fakes[0]) == fakes[0]
def test_modify_missing(sample):
with pytest.raises(HTTPException) as exc:
_ = creature.modify(sample.name, sample)
assert_missing(exc)
def test_delete(fakes):
assert creature.delete(fakes[0].name) is None
def test_delete_missing(sample):
with pytest.raises(HTTPException) as exc:
_ = creature.delete("emu")
assert_missing(exc)
Глава 12. Тестирование 201
Сервисный уровень
В некотором смысле сервисный уровень самый важный и может быть связан
с различными уровнями данных и веб-уровнями. Пример 12.13 похож на при-
мер 12.11, отличаясь в основном инструкцией import и использованием модуля
данных нижнего уровня. Также он не перехватывает исключения, возникающие
на уровне данных, оставляя их для обработки на веб-уровне.
@pytest.fixture
def sample() -> Creature:
return Creature(name="yeti",
aka:"Abominable Snowman",
202 Часть III. Создание веб-сайта
country="CN",
area="Himalayas",
description="Handsome Himalayan")
def test_create(sample):
resp = data.create(sample)
assert resp == sample
def test_create_duplicate(sample):
resp = data.create(sample)
assert resp == sample
with pytest.raises(Duplicate):
resp = data.create(sample)
def test_get_exists(sample):
resp = data.create(sample)
assert resp == sample
resp = data.get_one(sample.name)
assert resp == sample
def test_get_missing():
with pytest.raises(Missing):
_ = data.get_one("boxturtle")
def test_modify(sample):
sample.country = "CA" # Canada!
resp = data.modify(sample.name, sample)
assert resp == sample
def test_modify_missing():
bob: Creature = Creature(name="bob", country="US", area="*",
description="some guy", aka="??")
with pytest.raises(Missing):
_ = data.modify(bob.name, bob)
Уровень данных
Уровень данных проще тестировать изолированно, поскольку можно не беспо-
коиться о случайном вызове функции на еще более низком уровне. Модульные
тесты должны охватывать как функции этого уровня, так и конкретные исполь-
зуемые запросы к базе данных. До сих пор SQLite был «сервером» баз данных,
а SQL — языком запросов. Но вы можете решить работать с таким пакетом, как
SQLAlchemy, и задействовать его возможности в виде SQLAlchemy Expression
Language или ORM. Тогда потребуется полное тестирование. Пока что я при-
держиваюсь самого низкого уровня — DB-API Python и обычные SQL-запросы.
Глава 12. Тестирование 203
@pytest.fixture
def sample() -> Creature:
return Creature(name="yeti",
aka="Abominable Snowman",
country="CN",
area="Himalayas",
description="Hapless Himalayan")
def test_create(sample):
resp = creature.create(sample)
assert resp == sample
def test_create_duplicate(sample):
with pytest.raises(Duplicate):
_ = creature.create(sample)
def test_get_one(sample):
resp = creature.get_one(sample.name)
assert resp == sample
def test_get_one_missing():
with pytest.raises(Missing):
resp = creature.get_one("boxturtle")
def test_modify(sample):
creature.country = "JP" # Япония!
resp = creature.modify(sample.name, sample)
assert resp == sample
204 Часть III. Создание веб-сайта
def test_modify_missing():
thing: Creature = Creature(name="snurfle",
description="some thing", country="somewhere")
with pytest.raises(Missing):
_ = creature.modify(thing.name, thing)
def test_delete(sample):
resp = creature.delete(sample.name)
assert resp is None
def test_delete_missing(sample):
with pytest.raises(Missing):
_ = creature.delete(sample.name)
А → Б;
Б → В;
А → В.
А у вас будет полный колчан таких стрелок, если в системе более трех пересече-
ний. Или интеграционные тесты должны быть по сути сквозными тестами, но
с макетом самой последней части — хранения данных на диске?
Наконец, можно просто запустить тестовую базу данных того же типа, что и ра-
бочая. Переменная окружения может иметь специфику, подобную тому, как вы
использовали трюк с юнит-тестом/подделкой.
Паттерн «Репозиторий»
Хотя я не использовал его в этой книге, но паттерн «Репозиторий» (https://
oreil.ly/3JMKH) представляет собой интересный подход. Репозиторий — это простое
промежуточное хранилище данных в оперативной памяти, подобное представ-
ленному ранее уровню фиктивных данных. Затем он связывается с подклю-
чаемыми бэкендами для реальных баз данных. Репозиторий сопровождается
паттерном Unit of Work (https://oreil.ly/jHGV8), гарантирующим, что либо будет
зафиксирована группа операций в одной сессии, либо выполнен откат, как для
единого фрагмента.
До сих пор запросы к базе данных в этой книге были атомарными. На практике
для работы с базами данных вам могут понадобиться многоэтапные запросы
и определенная обработка сессий. Паттерн «Репозиторий» сочетается также
с внедрением зависимостей (https://oreil.ly/0f0Q3), с которым вы уже сталкивались
в других частях этой книги и которое, вероятно, уже немного оценили.
Однако эти подходы требуют написания одного или нескольких тестов для каж-
дой конечной точки. Это может превратиться в Средневековье, а мы уже на не-
сколько веков ушли вперед. Более современный подход основан на тестировании
на основе свойств (Property-Based Testing, PBT). При этом используется пре-
имущество автоматически генерируемой документации FastAPI. Схема OpenAPI
под названием openapi.json создается FastAPI каждый раз, когда вы изменяете
функцию пути или декоратор пути на веб-уровне. В этой схеме подробно опи-
сано все о каждой конечной точке — аргументы, возвращаемые значения и т. д.
Для этого и существует спецификация OpenAPI (OpenAPI Specification, OAS),
приведенная на странице OpenAPI Initiative’s FAQ (https://www.openapis.org/
faq): «OAS определяет стандартное, не зависящее от языка программирования
описание интерфейса для REST API, которое позволяет людям и компьютерам
обнаружить и понять возможности сервиса, не требуя доступа к исходному коду,
дополнительной документации или изучения сетевого трафика».
app = FastAPI()
app.include_router(explorer.router)
app.include_router(creature.router)
Глава 12. Тестирование 207
Я получил две пометки F, обе при вызове PATCH (функций modify()). Как же
неприятно.
Performed checks:
not_a_server_error 717 / 727 passed FAILED
Тестирование безопасности
Безопасность — это не что-то одно, а все. Вам нужно защищаться не только от
злого умысла, но и от обычных ошибок, и даже от неподвластных вам событий.
Отложим вопросы масштабирования до следующего раздела, а здесь займемся
в основном анализом потенциальных угроз.
Нагрузочное тестирование
То, как ваше приложение справляется с большим трафиком, показывают на-
грузочные тесты, проверяющие:
вызовы API;
чтение или запись в базу данных;
использование памяти;
использование дискового пространства;
время ожидания и пропускную способность сети.
Установите его локально с помощью команды pip install locust. Первым тестом
может стать проверка возможного количества единовременных посетителей
вашего сайта. Это похоже на проверку того, насколько экстремальные погодные
условия может выдержать здание во время урагана, землетрясения, снежной
бури или наступления другого страхового случая. Поэтому вам нужны струк-
турные тесты сайта. В документации (https://docs.locust.io) Locust можно найти
более подробную информацию.
Но, как говорят по телевизору, это еще не все! Недавно разработчики инстру-
мента Grasshopper (https://github.com/alteryx/locust-grasshopper) расширили возмож-
ности Locust для выполнения таких задач, как измерение времени в нескольких
HTTP-вызовах. Чтобы опробовать это расширение, установите его с помощью
команды pip install locust-grasshopper.
Заключение
В этой главе были подробно рассмотрены типы тестирования, приведены при-
меры выполнения pytest автоматизированного тестирования кода на уровне мо-
дулей, интеграции и полного тестирования. Тесты API можно автоматизировать
с помощью инструмента Schemathesis. Здесь также обсуждалось, как выявить
проблемы безопасности и производительности до того, как они возникнут.
ГЛАВА 13
Запуск в эксплуатацию
Обзор
У вас есть приложение, работающее на локальной машине, и теперь вы хотите по-
делиться им. В этой главе представлено множество сценариев того, как перенести
приложение в среду эксплуатации и поддерживать его правильную и эффектив-
ную работу. Поскольку часть описаний очень подробные, в некоторых случаях
я буду ссылаться на полезные сторонние документы, а не выкладывать их здесь.
Развертывание
До сих пор во всех примерах кода в этой книге использовался один экземпляр
uvicorn, запущенный на адресе localhost на порте 8000. Для обработки боль-
шого количества трафика вам потребуется несколько серверов, работающих на
нескольких ядрах, предоставляемых современным оборудованием. Кроме того,
понадобится что-то поверх этих серверов для выполнения следующих действий:
возврата ответов;
обеспечения HTTPS-терминации (расшифровка SSL).
Множество процессов
Вы наверняка видели сервер Python под названием Gunicorn (https://gunicorn.org).
Он может контролировать несколько процессов, но это сервер WSGI, а FastAPI
основан на ASGI. К счастью, существует специальный класс процессов Uvicorn,
которым может управлять Gunicorn.
HTTPS
Официальная документация FastAPI по HTTPS (https://oreil.ly/HYRW7), как и вся
остальная, чрезвычайно информативна. Я рекомендую прочитать ее, а затем
описание (https://oreil.ly/zcUWS) Рамиресом того, как добавить поддержку HTTPS
в FastAPI с помощью обратного прокси под названием Traefik (https://traefik.io).
Он располагается над вашими веб-серверами, подобно nginx в качестве обратного
прокси и балансировщика нагрузки, но включает в себя магию HTTPS.
Хотя этот процесс состоит из множества этапов, он все же намного проще, чем
прежде. В частности, раньше вам приходилось регулярно платить большие
деньги центру сертификации за цифровой сертификат, который можно было ис-
пользовать для поддержки протокола HTTPS на своем сайте. К счастью, на смену
этим органам пришел бесплатный сервис Let’s Encrypt (https://letsencrypt.org).
Docker
Когда Docker появился на сцене (он был упомянут в молниеносном пятиминут-
ном докладе (https://oreil.ly/25oef) Соломона Хайкса из dotCloud на PyCon 2013),
большинство из нас впервые услышали о контейнерах для Linux. Со временем
мы поняли, что Docker быстрее и легче виртуальных машин. Вместо эмуляции
полноценной операционной системы все контейнеры совместно используют
ядро Linux сервера, а процессы и сети изолируются в собственных пространствах
имен. Внезапно у вас появилась возможность с помощью бесплатного программ-
ного обеспечения Docker разместить несколько независимых сервисов на одной
машине, не беспокоясь о том, что они будут пересекаться.
Облачные сервисы
В Сети можно найти множество источников платного или бесплатного хо-
стинга. Вот некоторые примеры сведений о том, как разместить FastAPI с их
помощью:
статья How to Deploy a FastAPI App on Heroku for Free Шиничи Окады (https://
oreil.ly/A6gij).
Kubernetes
Платформа Kubernetes выросла из внутреннего кода Google для управления
внутренними системами, которые становились просто ужасающе сложными.
Системные администраторы (так их тогда называли) вручную настраивали такие
инструменты, как балансировщики нагрузки, обратные прокси, хьюмидоры1
и т. д. Kubernetes стремился взять бˆольшую часть этих знаний и автоматизи-
ровать их — не говорите мне, как это сделать, а скажите, чего вы хотите. Сюда
входят такие задачи, как поддержание работоспособности сервиса или запуск
дополнительных серверов при резком увеличении трафика.
1
Погодите, они же сохраняют сигары свежими.
214 Часть III. Создание веб-сайта
Производительность
В настоящее время производительность FastAPI одна из самых высоких (https://
oreil.ly/mxabf) среди всех веб-фреймворков на Python и даже сравнима с произ-
водительностью фреймворков на более быстрых языках, таких как Go. Но во
многом это связано с ASGI, позволяющим избежать ожидания ввода-вывода
с помощью асинхронности. Сам по себе Python — довольно медленный язык.
Далее приведены некоторые советы и рекомендации по улучшению общей про-
изводительности.
Асинхронность
Часто веб-серверу не нужно быть очень быстрым. Бˆольшую часть своего вре-
мени он тратит на получение сетевых HTTP-запросов и возврат результатов
(в этой книге он представлен веб-уровнем). Между ними веб-сервис выполняет
бизнес-логику (сервисный уровень), получает доступ к источникам данных
(уровень данных) и снова тратит бˆольшую часть своего времени на сетевой
ввод-вывод.
Когда код в веб-сервисе должен ждать ответа, лучше всего использовать асин-
хронную функцию (async def, а не def). Это позволяет FastAPI и Starlette пла-
нировать работу асинхронной функции и выполнять другие действия в ожида-
нии ее ответа. Это одна из причин того, почему бенчмарки FastAPI лучше, чем
фреймворки на базе WSGI, такие как Flask и Django. У производительности
есть два аспекта:
Кэши
Если у вас есть конечная точка веб-приложения, получающая данные из статич-
ного источника (например, записи в базе данных, которые меняются редко или
не меняются никогда), можно кэшировать данные в функции. Это может быть
на любом уровне. В Python представлен стандартный модуль functools (https://
oreil.ly/8Kg4V), а также функции cache() и lru_cache().
Глава 13. Запуск в эксплуатацию 215
Очереди
Если вы выполняете какую-либо задачу, занимающую больше доли секунды, на-
пример отправку письма с подтверждением или уменьшение изображения, возмож-
но, стоит передать ее в очередь заданий, например в Celery (https://docs.celeryq.dev).
Непосредственно Python
Если веб-сервис кажется медленным, потому что выполняет значительные вы-
числения с помощью Python, вам может понадобиться «более быстрый Python».
Альтернативные варианты:
Mojo стремится стать одноязычным решением для разработки ИИ, для чего
сейчас (в PyTorch и TensorFlow) требуются сборки Python/C/C++, сложные
в разработке, управлении и отладке. Но Mojo был бы хорошим языком общего
назначения не только в сфере ИИ.
Устранение неполадок
Смотрите снизу вверх с того момента и места, где вы столкнулись с проблемой.
К ним относятся проблемы производительности во времени и пространстве,
а также логические и асинхронные ловушки.
Виды проблем
Какой код ответа HTTP вы получили в первую очередь?
Ведение журналов
Uvicorn и другие веб-серверы обычно пишут журналы в файл stdout. Вы можете
проверить журнал, чтобы узнать, какой вызов был сделан на самом деле, включая
HTTP-глагол и URL-адрес, но не данные в теле, заголовках или файлах cookies.
Метрики
Может показаться, что значения терминов «метрика», «мониторинг», «на-
блюдаемость» и «телеметрия» частично совпадают. В стране Python принято
использовать:
Заключение
Понятно, что производство — дело непростое. Среди проблем — сама веб-
техника, перегрузка сети и дисков, а также проблемы с базой данных. В этой
главе вы найдете подсказки о том, как получить нужную информацию и где
начать искать, если возникли проблемы.
ЧАСТЬ IV
Галерея
Обзор
В этой главе рассказывается о том, как использовать FastAPI для хранения
и получения данных. Здесь расширяются простые примеры SQLite, приведен-
ные в главе 10:
Обычный бэкенд для веб-сайта — это база данных. Веб-сайты и базы данных —
это как арахисовое масло и желе, и хотя вы можете хранить свои данные и дру-
гими способами (или сочетать арахисовое масло с огурцами), в этой книге мы
будем использовать базы данных.
Базы данных решают многие проблемы, которые в противном случае вам при-
шлось бы решать самостоятельно с помощью кода, например такие:
множественный доступ;
индексирование;
согласованность данных.
SQLAlchemy
Самым популярным SQL-пакетом для Python стал SQLAlchemy. Хотя во
многих объяснениях SQLAlchemy обсуждаются только возможности ORM
этой библиотеки, она содержит несколько слоев, и я буду рассматривать их
снизу вверх.
Core
Основа SQLAlchemy, называемая Core, включает в себя следующее:
Пример 14.1. Прямой код SQL для функции get_one() в файле data/explorer.py
def get_one(name: str) -> Explorer:
qry = "select * from explorer where name=:name"
params = {"name": name}
curs.execute(qry, params)
return row_to_model(curs.fetchone())
conn = connect("sqlite:///cryptid.db")
meta = Metadata()
explorer_table = Table(
"explorer",
meta,
Column("name", Text, primary_key=True),
Column("country", Text),
Column("description", Text),
)
insert(explorer_table).values(
name="Beau Buffette",
country="US",
description="...")
224 Часть IV. Галерея
ORM
ORM выражает запросы в терминах моделей данных домена, а не реляционных
таблиц и логики SQL, лежащих в основе механизма базы данных. В официаль-
ной документации (https://oreil.ly/x4DCi) приведена подробная информация. ORM
гораздо сложнее, чем язык выражений SQL. Разработчики, предпочитающие
полностью объектно-ориентированные модели, обычно выбирают ORM.
SQLModel
Автор FastAPI объединил аспекты FastAPI, Pydantic и SQLAlchemy, чтобы
создать библиотеку SQLModel (https://sqlmodel.tiangolo.com). Он переносит неко-
торые методы разработки из веб-мира в реляционные базы данных. SQLModel
сочетает в себе ORM от SQLAlchemy с определением и проверкой данных от
Pydantic.
SQLite
Пакет SQLite был представлен в главе 10, я использовал его в примерах уровня
данных. Это общественное достояние — более открытого исходного кода и не при-
думаешь. SQLite применяется в каждом браузере и в каждом смартфоне, что делает
его одним из самых распространенных программных пакетов в мире. При выборе
реляционной базы данных этот пакет часто упускают из виду, но вполне возмож-
но, что несколько «серверов» SQLite смогут поддерживать некоторые крупные
сервисы не хуже, чем такой мощный сервер, как PostgreSQL.
Глава 14. Базы данных, наука о данных и немного искусственного интеллекта 225
PostgreSQL
На заре развития реляционных баз данных пионером была система System R от
IBM, а за новый рынок боролись ее ответвления — в основном Ingres с откры-
тым исходным кодом и коммерческий продукт Oracle. В Ingres был применен
язык запросов QUEL, а в System R — SQL. Хотя некоторые считали, что QUEL
лучше, чем SQL, принятие Oracle SQL в качестве стандарта, а также влияние
IBM помогли Oracle и SQL добиться успеха.
EdgeDB
Несмотря на многолетний успех SQL, у него есть некоторые недостатки,
делающие запросы неудобными. В отличие от математической теории, на ко-
торой основан SQL (реляционное исчисление Э. Ф. Кодда), сама конструкция
языка SQL не является композиционной. В основном это означает, что сложно
вложить запросы в большие запросы, что порождает более сложный и много-
словный код.
Базы данных NoSQL делают эти правила менее строгими, иногда позволяя
варьировать типы столбцов/полей в отдельных строках данных. Часто схемы
(дизайн баз данных) могут представлять собой не реляционные ячейки, а раз-
розненные структуры, которые можно выразить на JSON или Python.
Redis
Redis — это сервер структур данных, работающий исключительно в оперативной
памяти, хотя он может сохранять данные на диск и восстанавливать их с диска.
Он полностью соответствует собственным структурам данных Python и стал
очень популярным.
Глава 14. Базы данных, наука о данных и немного искусственного интеллекта 227
MongoDB
MongoDB — это своего рода PostgreSQL для NoSQL-серверов. Коллекция —
это эквивалент таблицы SQL, а документ — эквивалент строки таблицы SQL.
Еще одно отличие — и главная причина, по которой база данных NoSQL является
основной, — заключается в том, что вам не нужно определять, как выглядит до-
кумент. Другими словами, нет никакой фиксированной схемы. Документ — это
как словарь Python, ключом в нем может быть любая строка.
Cassandra
Cassandra — это крупномасштабная база данных, ее можно распределить между
сотнями узлов. Она написана на языке Java.
Elasticsearch
Elasticsearch (https://www.elastic.co/elasticsearch) больше похожа на индекс базы
данных, чем на саму базу данных. Она часто используется для полнотекстового
поиска.
SQL с JSON может быть лучшим из двух миров. Базы данных SQL существуют
гораздо дольше и поддерживают действительно полезные функции, такие как
внешние ключи и вторичные индексы. Кроме того, SQL довольно хорошо стан-
дартизирован до определенного момента, а языки запросов NoSQL все разные.
В примере 14.3 функция Faker выкачивает имена и страны, а затем они загру-
жаются с помощью функции load() в SQLite.
def load():
from error import Duplicate
from data.explorer import create
from model.explorer import Explorer
Глава 14. Базы данных, наука о данных и немного искусственного интеллекта 229
f = Faker()
NUM = 100_000
t1 = perf_counter()
for row in range(NUM):
try:
create(Explorer(name=f.name(),
country=f.country(),
description=f.description))
except Duplicate:
pass
t2 = perf_counter()
print(NUM, "rows")
print("write time:", t2-t1)
def read_db():
from data.explorer import get_all
t1 = perf_counter()
_ = get_all()
t2 = perf_counter()
print("db read time:", t2-t1)
def read_api():
from fastapi.testclient import TestClient
from main import app
t1 = perf_counter()
client = TestClient(app)
_ = client.get("/explorer/")
t2 = perf_counter()
print("api read time:", t2-t1)
load()
read_db()
read_db()
read_api()
Время чтения API для всех исследователей было намного медленнее, чем время
чтения уровня данных. Вероятно, часть этих расходов связана с преобразовани-
ем ответа в JSON с помощью FastAPI. Кроме того, время первоначальной записи
в базу данных было не очень быстрым. Код записывает по одному исследователю
за раз, потому что в API уровня данных есть единственная функция create(),
но нет функции create_many(). В части считывания API может вернуть один
(get_one()) или все (get_all()) результаты. Поэтому, если вы хотите выполнять
массовую загрузку, возможно, стоит добавить новую функцию загрузки данных
и новую конечную точку веб-приложения (с ограниченной авторизацией).
Кроме того, если вы ожидаете, что любая таблица в базе данных вырастет до
100 000 строк, возможно, не стоит позволять случайным пользователям полу-
чать их все за один вызов API. Не помешала бы пагинация или возможность
загрузки одного CSV-файла из таблицы.
app = FastAPI()
@app.get("/ai")
def prompt(line: str) -> str:
232 Часть IV. Галерея
Запустите этот код с помощью выражения uvicorn ai:app (как всегда, сначала
убедитесь, что у вас нет другого все еще запущенного веб-сервера на адресе
localhost , порт 8000 ). Задавайте вопросы конечной точке /ai и получайте
ответы, например, так (обратите внимание на двойной знак равенства == для
параметра запроса HTTPie):
$ http -b localhost:8000/ai line=="What are you?"
"a sailor"
Это довольно маленькая модель, и, как вы можете видеть, она не особенно хорошо
отвечает на вопросы. Я попробовал другие задания (line-аргументы) и получил
не менее достойные ответы.
Заключение
В этой главе мы распространили возможности применения SQLite, описанные
в главе 10, на другие базы данных SQL и даже NoSQL. Здесь также показано,
как некоторые базы данных SQL могут выполнять трюки NoSQL с поддержкой
JSON. Наконец, речь шла об использовании баз данных и специальных инстру-
ментов для работы с данными, которые становятся все более важными по мере
того, как машинное обучение продолжает бурно развиваться.
ГЛАВА 15
Файлы
Обзор
Помимо обработки API-запросов и традиционного контента, например HTML,
веб-серверы должны обрабатывать передачу файлов в обоих направлениях. Очень
большие файлы могут передаваться частями, чтобы не занимать много памяти
системы. Вы также можете предоставить доступ к папке с файлами (и подчинен-
ными папками любой глубины) с помощью статических файлов — Static Files.
Поддержка Multipart
Чтобы обрабатывать большие файлы, функции выгрузки и скачивания FastAPI
нуждаются в следующих дополнительных модулях:
Выгрузка файлов
FastAPI нацелен на разработку API, и в большинстве примеров в этой книге
используются запросы и ответы в формате JSON. Но в следующей главе вы
познакомитесь с формами, которые обрабатываются по-другому. Здесь же рас-
сказывается о файлах, по некоторым параметрам похожих на формы.
Функция File()
Функция File() применяется в качестве типа для прямой выгрузки файла. Ваша
функция пути может быть синхронной (def) или асинхронной (async def), но
асинхронная версия лучше, потому что она не будет нагружать веб-сервер во
время выгрузки файла.
FastAPI будет извлекать файл по частям и собирать его в памяти, поэтому функ-
цию File() следует использовать только для относительно небольших файлов.
Вместо того чтобы считать, что входные данные представлены в формате JSON,
FastAPI кодирует файл как элемент формы.
Напишем код для запроса файла и протестируем его. Вы можете взять любой
файл на своей машине для тестирования или загрузить его с такого сайта, как
Fastest Fish (https://oreil.ly/EnlH-). Я взял оттуда файл размером 1 Кбайт и сохранил
его локально под названием 1KB.bin. В примере 15.1 добавьте эти строки в файл
main.py верхнего уровня.
@app.post("/small")
async def upload_small_file(small_file: bytes = File()) -> str:
return f"file size: {len(small_file)}"
Класс UploadFile
Для больших файлов лучше использовать класс UploadFile. Он создает объект
Python под названием SpooledTemporary File, обычно на диске сервера, а не
в памяти. Это файлоподобный объект Python, и он поддерживает методы read(),
write() и seek(). Пример 15.4 показывает реализацию этого подхода; в нем так-
же используется объявление async def вместо def, чтобы избежать блокировки
веб-сервера во время выгрузки частей файла.
@app.post("/big")
async def upload_big_file(big_file: UploadFile) -> str:
return f"file size: {big_file.size}, name: {big_file.filename}"
Загрузка файлов
К сожалению, гравитация не ускоряет скачивание файлов. Вместо этого мы
будем использовать эквиваленты методов выгрузки.
Класс FileResponse
Первым (пример 15.7) представлен вариант «все и сразу», класс FileResponse.
@app.get("/small/{name}")
async def download_small_file(name):
return FileResponse(name)
Где-то здесь есть тест. Сначала поместите файл 1KB.bin в тот же каталог, что
и main.py. Теперь запустите пример 15.8.
-----------------------------------------
| NOTE: binary data not shown in terminal |
-----------------------------------------
Класс StreamingResponse
Как и в случае с модулем FileUpload, большие файлы лучше загружать с помо-
щью класса StreamingResponse, возвращающего файл по частям. Пример 15.10
показывает такой подход к реализации с помощью функции пути, определенной
как async def. Он позволяет избежать блокировки, когда процессор не исполь-
зуется. Я пока пропускаю проверку ошибок. Если файла path не существует,
вызов функции open() выбросит исключение.
@app.get("/download_big/{name}")
async def download_big_file(name:str):
gen_expr = gen_file(file_path=path)
response = StreamingResponse(
content=gen_expr,
status_code=200,
)
return response
Пример 15.11 представляет собой сопутствующий тест. (Для этого сначала по-
требуется разместить файл 1GB.bin рядом с файлом main.py, процесс займет
немного больше времени.)
Для этого примера создадим каталог скучных бесплатных файлов для загрузки
пользователями.
app.mount("/static",
StaticFiles(directory=f"{top}/static", html=True),
name="free")
Заключение
В этой главе было показано, как выгружать и скачивать файлы — маленькие,
большие и даже гигантские. Кроме того, вы научились предоставлять стати-
ческие файлы в ностальгическом (не API) веб-стиле из каталога.
ГЛАВА 16
Формы и шаблоны
Обзор
Хотя акроним API в названии FastAPI — это намек на его основную направлен-
ность, FastAPI может работать и с традиционным веб-контентом. В этой главе
рассказывается о стандартных HTML-формах и шаблонах для вставки данных
в HTML.
Формы
Как вы уже поняли, FastAPI был разработан в основном для создания API, и его
входной информацией по умолчанию будут данные в формате JSON. Но это
не значит, что он не может служить стандартным базовым HTML-формам и их
друзьям.
app = FastAPI()
@app.get("/who2")
def greet2(name: str = Form()):
return f"Hello, {name}?"
А?
Посмотрите в окно, где запущен Uvicorn, чтобы увидеть, что написано в его
журнале:
Глава 16. Формы и шаблоны 243
INFO: 127.0.0.1:63502 -
"GET /who2?name=rr23r23 HTTP/1.1"
422 Unprocessable Entity
app = FastAPI()
@app.post("/who2")
def greet3(name: str = Form()):
return f"Hello, {name}?"
Разбудите свой браузер и попросите его получить эту новую форму. Внесите
в нее текст Bob Frapples и подтвердите отправку формы. На этот раз вы получите
тот же результат, что и при использовании HTTPie:
Шаблоны
Возможно, вам знакома игра в слова Mad Libs. Игрокам дают последователь-
ность слов — существительных, глаголов или чего-то более конкретного, они
вставляют их в отмеченные места на странице текста. Вставив все слова, нужно
прочитать текст — и начинается веселье, иногда сопровождаемое неловкостью.
Веб-шаблон — это то же самое, но, как правило, без неловкости. Шаблон содер-
жит кучу текста со слотами для данных, вставляемых сервером. Его обычное
назначение — генерировать HTML с переменным содержимым, в отличие от
статического HTML из главы 15.
<br>
Глава 16. Формы и шаблоны 245
<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>
Этот шаблон ожидает, что ему будут переданы переменные Python creatures
и explorers, представляющие собой списки объектов Creature и Explorer.
В примере 16.7 показано, что нужно добавить в файл main.py, чтобы устано-
вить шаблоны и использовать данные из примера 16.6. Код подает переменные
creatures и explorers в шаблон, применяя модули в фиктивном каталоге из
предыдущих глав — эта папка предоставляла тестовые данные, если БД была
пуста или не подключена.
app = FastAPI()
top = Path(__file__).resolve().parent
246 Часть IV. Галерея
template_obj = Jinja2Templates(directory=f"{top}/template")
@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.
Заключение
В этой главе был дан краткий обзор того, как FastAPI работает с темами, не от-
носящимися к API, такими как формы и шаблоны. Наряду с рассмотренным
в предыдущей главе о файлах, это традиционные минимально необходимые
веб-задачи, с ними вы часто сталкиваетесь.
ГЛАВА 17
Обнаружение
и визуализация данных
Обзор
Несмотря на то что в названии фреймворка FastAPI фигурирует акроним API,
он может служить не только для API. В этой главе вы узнаете, как создавать
таблицы, графики, диаграммы и карты на основе данных, используя небольшую
базу данных о воображаемых существах со всего мира.
Python и данные
В последние несколько лет Python стал очень популярным по многим при-
чинам:
он легок в обучении;
у него прозрачный синтаксис;
он имеет богатую стандартную библиотеку;
в нем огромное количество высококачественных пакетов сторонних раз-
работчиков;
особое внимание в нем уделяется манипулированию данными, преобразова-
нию и самостоятельной проверке.
248 Часть IV. Галерея
name;
Модуль csv
Пример 17.1 считывает данные о существе в структуры данных Python. Во-первых,
файл cryptids.psv, где используется разделение символами вертикальной черты,
можно считать с помощью стандартного пакета csv Python, получив список кор-
тежей, где каждый кортеж представляет собой строку данных из файла. (Пакет
csv включает также класс DictReader, возвращающий список словарей.) Первая
строка этого файла представляет собой заголовок с именами столбцов. Без этого
мы могли бы предоставлять заголовки через аргументы для функций csv.
if __name__ == "__main__":
data = read_csv(sys.argv[1])
for row in data[0:5]:
print(row)
1
Если есть деревья, похожие на энтов из книг Толкина, не хотелось бы, чтобы они ночью
подошли к дверям нашего дома для небольшой беседы.
250 Часть IV. Галерея
Модуль python-tabulate
Опробуем еще один инструмент с открытым исходным кодом, python-tabulate
(https://oreil.ly/L0f6k). Он специально разработан для табличного вывода. Сна-
чала потребуется запустить команду pip install tabulate. В примере 17.3
показан код.
if __name__ == "__main__":
data = read_csv(sys.argv[1])
print(tabulate(data[0:5]))
Модуль pandas
Два предыдущих примера представляли собой в основном форматоры вывода.
Библиотека pandas (https://pandas.pydata.org) — это отличный инструмент для
нарезки данных. Он выходит за рамки стандартных структур данных Python,
используя такие продвинутые конструкции, как DataFrame (https://oreil.ly/j-8eh) —
комбинацию таблицы, словаря и серии. Он может читать .csv и другие файлы
с разделителями в виде символов. Пример 17.5 похож на предыдущие примеры,
но вместо списка кортежей pandas возвращает DataFrame.
Глава 17. Обнаружение и визуализация данных 251
if __name__ == "__main__":
data = read_pandas(sys.argv[1])
print(data.head(5))
В библиотеке pandas есть множество интересных функций, так что стоит из-
учить ее более внимательно.
столбцовая;
рассеяния;
254 Часть IV. Галерея
линейная;
коробчатая (статистическая);
гистограмма.
Все наши поля данных — строки намеренно минимального размера, чтобы при-
меры не перегружали логику и этапы интеграции. Для каждого примера будем
считывать все данные о существах из базы данных SQLite, используя код из
предыдущих глав, а также добавлять функции веб- и сервисного уровня для
выбора определенных данных для передачи в функции библиотеки графиков.
Сначала установите пакет Plotly и библиотеку, необходимую ему для экспорта
изображений:
@router.get("/test")
def test():
df = px.data.iris()
fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species")
fig_bytes = fig.to_image(format="png")
return Response(content=fig_bytes, media_type="image/png")
Для первого приема (пример 17.9) просто используем поле name и построим
гистограмму, показывающую количество имен существ, начинающихся на
каждую букву.
256 Часть IV. Галерея
@router.get("/plot")
def plot():
creatures = get_all()
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
counts = Counter(creature.name[0] for creature in creatures)
y = { letter: counts.get(letter, 0) for letter in letters }
fig = px.histogram(x=list(letters), y=y, title="Creature Names",
labels={"x": "Initial", "y": "Initial"})
fig_bytes = fig.to_image(format="png")
return Response(content=fig_bytes, media_type="image/png")
1
В английском языке слово map означает и «карта», и «сопоставлять». Перевод раз-
личается только по контексту. — Примеч. пер.
258 Часть IV. Галерея
Пример карты
Для примеров из области картографии я снова использую пакет Plotly — он
не слишком прост и не слишком сложен и помогает показать, как интегрировать
небольшую веб-карту с FastAPI.
import plotly.express as px
import country_converter as coco
@router.get("/map")
def map():
creatures = service.get_all()
iso2_codes = set(creature.country for creature in creatures)
iso3_codes = coco.convert(names=iso2_codes, to="ISO3")
fig = px.choropleth(
locationmode="ISO-3",
locations=iso3_codes)
fig_bytes = fig.to_image(format="png")
return Response(content=fig_bytes, media_type="image/png")
Заключение
Рядом с вашим домом не рыскали криптиды? Вы можете узнать это из данной
главы, где различные инструменты для построения графиков, диаграмм и карто-
графических схем набросали базу данных существ, вызывающих у нас тревогу.
ГЛАВА 18
Игры
Обзор
Игры бывают очень разными, от простых текстовых до многопользовательских
3D-феерий. В этой главе я продемонстрирую простую игру и то, как конечная
точка веб-приложения может взаимодействовать с пользователем на нескольких
этапах. Этот процесс отличается от привычных вам по этой книге одноразовых
запросов-ответов конечных точек веб-приложения.
• pyglet (https://pyglet.org);
• Python Arcade (https://api.arcade.academy);
• HARFANG (https://www.harfang3d.com);
• Panda3D (https://docs.panda3d.org).
Глава 18. Игры 261
Гейм-дизайн
Во-первых, что это за игра? Мы создадим простую игру, похожую на Wordle
(https://oreil.ly/PuD-Y), но в ней будут использоваться только названия существ
из базы данных cryptid.db. Это намного проще, чем Wordle, особенно если
262 Часть IV. Галерея
app = FastAPI()
app.include_router(explorer.router)
app.include_router(creature.router)
app.include_router(game.router)
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app",
host="localhost", port=8000, reload=True)
<table id="guesses">
</table>
<span id="status"></span>
<hr>
266 Часть IV. Галерея
<div>
{% for letter in word %}<input type=text name="guess">{% endfor %}
<input type=hidden id="word" value="{{word}}">
<br><br>
<input type=submit onclick="post_guess()">
</div>
</body>
HIT = "H"
MISS = "M"
CLOSE = "C" # (буква находится в слове, но в другой позиции)
Тестируем!
Пример 18.6 содержит несколько упражнений pytest для расчета оценки сер-
виса. В коде используем функциональную возможность pytest под названием
parametrize для передачи последовательности тестов, вместо того чтобы писать
цикл внутри самой тестовой функции. Помните из примера 18.5, что H — точное
попадание, C — близко (неверная позиция) и M означает, что игрок вообще не угадал.
word = "bigfoot"
guesses = [
("bigfoot", "HHHHHHH"),
("abcdefg", "MCMMMCC"),
("toofgib", "CCCHCCC"),
("wronglength", ""),
("", ""),
]
@pytest.mark.parametrize("guess,score", guesses)
def test_match(guess, score):
assert game.get_score(word, guess) == score
Запускаем:
$ pytest -q test_game.py
..... [100%]
5 passed in 0.05s
268 Часть IV. Галерея
Данные — инициализация
В новом модуле data/game.py потребуется только одна функция, показанная
в примере 18.7.
Заключение
Мы использовали HTML, JavaScript, CSS и FastAPI, чтобы создать простую
(очень!) интерактивную игру в стиле Wordle. В этом разделе было показано, как
управлять многопоточным взаимодействием между веб-клиентом и сервером
с помощью JSON и Ajax.
ПРИЛОЖЕНИЕ A
Дополнительная литература
Python
Вот некоторые известные сайты, посвященные Python:
FastAPI
Далее перечислены некоторые веб-сайты FastAPI:
Несмотря на то что FastAPI появился в конце 2018 года, книг по нему пока
не так уж много. Я смог извлечь полезные уроки из прочтения следующих
книг:
Building Data Science Applications with FastAPI, автор Франсуа Ворон (Packt);
Building Python Microservices with FastAPI, автор Шервин Джон Трагура
(Packt);
Microservice APIs, автор Хосе Аро Перальта (Manning).
Приложение A. Дополнительная литература 273
Starlette
Основные ссылки для Starlette:
Home (https://www.starlette.io);
GitHub (https://github.com/encode/starlette).
Pydantic
Основные ссылки для Pydantic:
Home (https://pydantic.dev);
Docs (https://docs.pydantic.dev);
GitHub (https://github.com/pydantic/pydantic).
ПРИЛОЖЕНИЕ Б
Существа и люди
От упырей, от призраков,
От тварей долголапых
И от существ, рыщущих в ночи,
Избави нас, Боже!
Строфа из Корнуольской литании
Существа
В табл. Б.1 перечислены существа, которых мы будем исследовать.
Продолжение
276 Приложение Б. Существа и люди
1
Однажды я встретил Питера Макнаба, который сделал одну из предполагаемых фото-
графий Несси.
2
От французского слова. Или от реплики Скуби-Ду: «Ruh-roh! Rougarou!»
278 Приложение Б. Существа и люди
Исследователи
Наша команда исследователей, собравшаяся из разных уголков мира, пред-
ставлена в табл. Б.2.
1
В смысле благородства, а не знатности.
Приложение Б. Существа и люди 279
Публикации исследователей
Вот воображаемые публикации наших воображаемых исследователей:
Другие источники
У преданий о криптидах много источников. Некоторых криптидов можно от-
нести к воображаемым существам, а некоторых можно увидеть на нечетких
фотографиях, сделанных на большом расстоянии. Среди моих источников были
следующие:
A J
Asynchronous Server Gateway Interface, JavaScript Object Notation, JSON 26
ASGI 41 JWT 183
Authorization Code Flow 173
M
C Machine Learning, ML 32
Command-Line Interface, CLI 29 MIME-типы 59
Cross-Origin Resource Sharing,
CORS 186 N
CRUD 24 NoSQL 226
D O
Domain-Driven Design, DDD 112 OAuth2 173
E R
Extract, Transform, Load, ETL 32 Remote procedure call, RPC 23
Representational State Transfer, REST 24
F Requests 38
FastAPI 38 RESTful 24
Role-Based Access Control, RBAC 185
G
Graph Query Language (GraphQL) 26 T
Type hints 40
H
HTML 23 U
HTTP 23 URL 23
HTTPie 38 Uvicorn 38
HTTPX 38
HTTP-глагол 24 W
HTTP-заголовок 55 Web Server Gateway Interface, WSGI 41
Алфавитный указатель 283
А И
Авторизация 165 Идемпотентность 53
Асинхронный режим 27 Издатель-подписчик 23
Аутентификация 165 Изменяемый объект 39
Именованный кортеж 84
Б Интеграционное тестирование 190
База данных 28 Интерфейс командной строки 29
Блочная модель 29 Искусственный интеллект, ИИ 32
Бэкенд 22
К
В Код-дублер 196
Веб 19 Код состояния 25
Веб-клиент 28
Конечная точка 24
Веб-сервис 22
Конкурентность 27
Веб-уровень 28
Конкурентные вычисления 67
Веб-шаблон 244
Конструкция async/await 71
Ведение журналов 217
Контрактные тесты 138
Виртуальные среды 36
Кооперативные потоки 69
Внедрение зависимостей 96
Кортеж 83
Время ожидания 27
Криптиды 82
Всемирная паутина 19
Вытесняющее планирование 68
М
Макет 194
Г
Макетирование 194
Галлюцинации 232
Макеты данных 123
Группы данных 82
Маршрут 24, 101
Д Машинное обучение 32
Н Подсказки типов 40
Т Формат API 22
Трассировка 141
Х
У Хеширование 172
Удаленные вызовы процедур 23
Уровень данных 28 Ц
Цикл событий 27
Ф
Файл конфигурации 37 Я
Фикстуры 192 Язык выражений
Фиктивные данные 123 SQLAlchemy 223
Билл Любанович
FastAPI: веб-разработка на Python
КУПИТЬ
Оливье Келен,
Мари-Алис Блете
РАЗРАБОТКА ПРИЛОЖЕНИЙ
НА БАЗЕ GPT-4 И CHATGPT
КУПИТЬ