(издание 2018)
Miguel Grinberg
Это двадцать третья часть Мега-Учебника, в которой я расскажу вам, как расширить микроблог с помощью интерфейса прикладного программирования (или API), который клиенты могут использовать для работы с приложением более прямым способом, чем традиционный рабочий процесс веб-браузера.
Под спойлером приведен список всех статей серии 2018 года.
Примечание 1: Если вы ищете старые версии данного курса, это здесь.
Вся функциональность которую я построил до сих пор для этого приложения, предназначена для одного конкретного типа клиента: веб-браузер. Но как насчет других типов клиентов? Например, если бы я хотел создать приложение для Android или iOS, у меня есть два основных способа его решения. Самым простым решением было бы создать приложение с помощью веб-компонента, который заполнит весь экран и загрузит веб-сайт Microblog, но это не будет качественно лучшим по сравнению с открытием приложения в веб-браузере устройства. Лучшим решением (хотя и гораздо более трудоемким) было бы создание собственного приложения, но как это приложение может взаимодействовать с сервером, который возвращает только HTML-страницы?
Это проблемная область, в которой могут помочь Интерфейсы Прикладного Программирования (или API). API-это коллекция HTTP-маршрутов, которые разрабатываются как низкоуровневые точки входа в приложение. Вместо того, чтобы определять маршруты и просматривать функции, возвращающие HTML, которые будут использоваться веб-браузерами, API позволяют клиенту работать непосредственно с ресурсами приложения, оставляя решение о том, как представить информацию пользователю полностью клиенту. Например, API в микроблоге может предоставить клиенту информацию о пользователе и записи в блоге, а также позволить пользователю редактировать существующую запись в блоге, но только на уровне данных, не смешивая эту логику с HTML.
Если вы изучите все маршруты (routes), определенные в настоящее время в приложении, Вы заметите, что есть несколько, которые могут соответствовать определению API, которое я использовал выше. Вы их нашли? Я говорю о нескольких маршрутах, которые возвращают JSON, таких как маршрут /translate, определенный в главе 14. Это маршрут, который принимает текст, исходный и конечный языки, все данные в формате JSON в запросе POST
. Ответом на этот запрос является перевод этого текста, также в формате JSON. Сервер возвращает только запрошенную информацию, оставляя клиент с ответственностью представить эту информацию пользователю.
В то время как маршруты JSON в приложении имеют API "чувствовать" к ним, они были разработаны, чтобы поддержать веб-приложение, работающее в браузере.
Хотя маршруты JSON в приложении имеют API-интерфейс, остается "ощущение", что они были разработаны для поддержки веб-приложения, запущенного в браузере. Учтите, что если приложение для смартфонов захотело использовать эти маршруты, оно не было бы в состоянии, потому что им нужен зарегистрированный пользователь, а вход в систему возможен только через HTML-форму. В этой главе я расскажу, как создавать API-интерфейсы, не полагающиеся на веб-браузер, и не делать никаких предположений о том, какой клиент подключается к ним.
Ссылки GitHub для этой главы: Browse, Zip, Diff.
REST как основа проектирования API
Кто то может категорически не согласиться с моим утверждением выше, что /translate и другие маршруты JSON являются маршрутами API. Другие могут согласиться с оговоркой, что они считают их плохо разработанным API. Итак, каковы характеристики хорошо разработанного API, и почему маршруты JSON вне этой категории?
Возможно, вы слышали термин rest API. REST, который означает Representational State Transfer (Репрезентативный Государственный Перевод), является архитектурой, предложенной доктором Роем Филдингом в его докторской диссертации. В своей работе д-р Филдинг представляет шесть определяющих характеристик REST в довольно абстрактном и общем виде.
Кроме диссертации доктора Филдинга, нет никакой другой авторитетной спецификации REST, что оставляет много чего для свободной интерпретации читателю. Тема о том, соответствует ли данный API REST или нет, часто является источником жарких дебатов между REST «пуристами», которые считают, что REST API должен соблюдать все шесть характеристик и делать это чётко определенным образом по сравнению с «прагматиками» REST, которые берут идеи, представленные д-ром Филдингом в своей диссертации в качестве руководящих принципов или рекомендаций. Д-р Филдинг сам встал на сторону пуристского лагеря и дал некоторое дополнительное представление о своем видении в блогах и онлайн-комментариях.
Подавляющее большинство API-интерфейсов, реализованных в настоящее время, придерживаются «прагматичной» реализации REST. Это включает в себя большинство API-интерфейсов от «крупных игроков», таких как Facebook, GitHub, Twitter и т.д. Существует очень мало публичных API, которые единодушно считаются чистыми REST, поскольку большинство API-интерфейсов пропускают некоторые детали реализации, которые пуристы считают обязательными. Несмотря на строгие взгляды д-ра Филдинга и других пуристов REST на то, что является или не является REST API, в индустрии программного обеспечения обычно упоминается REST в прагматическом смысле.
Чтобы дать вам представление о том, что находится в диссертации REST, в следующих разделах описываются шесть принципов, перечисленных д-ром Филдингом.
Client-Server
Принцип клиент-сервер довольно прост, так как он просто гласит, что в REST API роли клиента и сервера должны быть четко дифференцированы. На практике это означает, что клиент и сервер находятся в отдельных процессах, которые взаимодействуют через транспорт, который в большинстве случаев является протоколом HTTP по сети TCP.
Layered System
Принцип Layered System (многоуровневой системы) говорит, что когда клиент должен взаимодействовать с сервером, он может быть связан с посредником, а не с фактическим сервером. Идея заключается в том, что для клиента не должно быть абсолютно никакой разницы в том, как он отправляет запросы, если не подключен непосредственно к серверу, на самом деле он может даже не знать, подключен ли он к целевому серверу или нет. Аналогичным образом, этот принцип гласит, что сервер может получать клиентские запросы от посредника, а не непосредственно от клиента, поэтому он никогда не должен предполагать, что другая сторона соединения является клиентом.
Это важная функция REST, поскольку возможность добавления промежуточных узлов позволяет архитекторам приложений разрабатывать большие и сложные сети, которые могут удовлетворить большой объем запросов с помощью балансировщика нагрузки, кэшей, прокси-серверов и т.д.
Cache
Этот принцип расширяет многоуровневую систему, явно указывая, что сервер или посредник может кэшировать ответы на запросы, которые часто поступают для повышения производительности системы. Существует реализация кэша, с которой вы, вероятно, знакомы: один во всех веб-браузерах. Слой кэша веб-браузера часто используется, чтобы избежать необходимости запрашивать одни и те же файлы, такие как изображения, снова и снова.
Для целей API целевой сервер должен указать с помощью элементов управления кэшем, может ли ответ кэшироваться посредниками, когда он возвращается клиенту. Обратите внимание, что поскольку по соображениям безопасности API, развернутые в рабочей среде, должны использовать шифрование, кэширование обычно не выполняется на промежуточном узле, если только этот узел не завершает соединение SSL или не выполняет расшифровку и повторное шифрование.
Code On Demand
Это необязательное требование, указывающее, что сервер может предоставлять исполняемый код в ответах клиенту. Поскольку этот принцип требует соглашения между сервером и клиентом о том, какой исполняемый код может выполнять клиент, это редко используется в API. Вы могли бы подумать, что сервер может вернуть код JavaScript для запуска веб-браузеров, но REST специально не предназначен для клиентов веб-браузера. Например, выполнение JavaScript может привести к усложнению, если клиент является iOS или Android-устройством.
Stateless
Принцип stateless является одним из двух в центре большинства дебатов между пуристами REST и прагматиками. В нем указано, что REST API не должен сохранять любое состояние клиента, которое будет вызвано каждый раз, когда данный клиент отправляет запрос. Это означает, что ни один из механизмов, которые являются обычными в веб-разработке для «запоминания» пользователей при навигации по страницам приложения, не может быть использован. В API без состояния каждый запрос должен включать информацию, которую сервер должен идентифицировать и аутентифицировать клиента и выполнить запрос. Это также означает, что сервер не может хранить данные, относящиеся к клиентскому соединению в базе данных или другой форме хранения.
Если вам интересно, почему REST требует сервер без состояния, то основная причина заключается в том, что серверы без учета состояния (stateless) очень просты в масштабировании, все, что вам нужно сделать, это запустить несколько экземпляров сервера за балансировщиком нагрузки. Если сервер хранит состояние клиента, ситуация становится более сложной, так как вам нужно выяснить, как несколько серверов могут получить доступ и обновить это состояние, или же гарантировать, что данный клиент всегда обрабатывается одним и тем же сервером, что обычно называется липкими сеансами.
Если вы снова рассмотрите маршрут /translate, обсуждаемый в начале главы, вы поймете, что его нельзя считать RESTful, потому что функция вида, связанная с этим маршрутом, полагается на декодер @login_required
из Flask-Login, который, в свою очередь, хранит зарегистрированный в состоянии пользователя в сеансе пользователя Flask.
Uniform Interface
Последний, самый важный, самый обсуждаемый и наиболее неопределенно документированный принцип REST — это единый интерфейс. Д-р Филдинг перечисляет четыре отличительных аспекта единого интерфейса REST: уникальные идентификаторы ресурсов, представления ресурсов, самоописательные сообщения и гипермедиа.
Уникальные идентификаторы ресурсов получаются путем назначения уникального URL-адреса каждому ресурсу. Например, URL-адрес, связанный с данным пользователем, может быть /api/users/<user-id>, где <user-id> — это идентификатор, назначенный пользователю в качестве первичного ключа таблицы базы данных. Это вполне приемлемо реализовано большинством API.
Использование представлений ресурсов означает, что если сервер и клиент обмениваются информацией о ресурсе, они должны использовать согласованный Формат. Для большинства современных API Формат JSON используется для построения представлений ресурсов. API может поддерживать несколько форматов представления ресурсов, и в этом случае параметры согласования содержимого в протоколе HTTP являются механизмом, с помощью которого клиент и сервер могут согласовать формат, который нравится обоим.
Самоописательные сообщения означают, что запросы и ответы, которыми обмениваются клиенты и сервер, должны включать всю информацию, необходимую другой стороне. Типичный пример — это метод запроса HTTP используемый для указания, какую операцию клиент хочет получить от сервера. Запрос GET
указывает, что клиент хочет получить сведения о ресурсе, запрос POST
указывает, что клиент хочет создать новый ресурс, запросы PUT
или PATCH
определяют изменения существующих ресурсов, а запрос DELETE
указывает на удаление ресурса. Целевой ресурс указывается как URL-адрес запроса с дополнительной информацией, представленной в заголовках HTTP, части строки запроса URL-адреса или тела(body) запроса.
Требование hypermedia является наиболее полемичным из множества, и тот, который реализуется немногими API, и те API, которые реализуют его, редко делают так, чтобы удовлетворить пуристов REST. Поскольку все ресурсы в приложении взаимосвязаны, это требует обязательного включения связей в представления ресурсов, чтобы клиенты могли обнаруживать новые ресурсы путем обхода связей, почти так же, как вы обнаруживаете новые страницы в веб-приложении, щелкая ссылки, которые ведут вас от одной страницы к другой. Идея заключается в том, что клиент может войти в API без каких-либо предварительных знаний о ресурсах в нем и узнать о них, просто перейдя по ссылкам hypermedia. Одним из аспектов, которые усложняют выполнение данного требования заключается в том, что в отличие от HTML и XML, Формат json, который обычно используется для представления ресурсов в API не определяет стандартный способ включения ссылок, так что вы вынуждены использовать специальные настраиваемые структуры, или один из предлагаемых расширений JSON, которые пытаются восполнить этот пробел, такие как JSON-API, HAL, JSON-LD или похожие.
Реализация концепции API Blueprint
Чтобы дать вам представление о том, что участвует в разработке API, я собираюсь добавить его в микроблог. Это не будет полный API, я собираюсь реализовать все функции, связанные с пользователями, оставляя реализацию других ресурсов, таких как сообщения в блоге для читателя в качестве упражнения.
Чтобы все было организовано и структурировано в соответствии с концепцией описанной в Главе 15, я собираюсь создать новый проект, который будет содержать все маршруты API. Итак, давайте начнем с создания каталога, в котором будет жить этот проект:
(venv) $ mkdir app/api
Blueprint-овый файл __init __. py
создает объект blueprint, аналогичный другим blueprint-овым приложениям:
app/api/__init__.py:
API blueprint constructor.
from flask import Blueprint
bp = Blueprint('api', __name__)
from app.api import users, errors, tokens
Вы, вероятно, помните, что иногда необходимо переместить импорт на самое дно модуля, чтобы избежать циклических ошибок зависимостей. Это причина, почему app/api/users.py, app/api/errors.py и app/api/tokens.py модули (что мне еще предстоит написать) импортируются после создания проекта.
Основное содержание API будет храниться в модуле app/api/users.py. В следующей таблице перечислены маршруты, которые я собираюсь реализовать:
HTTP Method | Resource URL | Notes |
---|---|---|
GET | /api/users/<id> |
Возвращает пользователя. |
GET | /api/users |
Возвращает коллекцию всех пользователей. |
GET | /api/users/<id>/followers |
Вернет подписчиков этого пользователя. |
GET | /api/users/<id>/followed |
Вернет пользователей, на которых подписан этот пользователь. |
POST | /api/users |
Регистрирует новую учетную запись пользователя. |
PUT | /api/users/<id> |
Изменяет пользователя. |
Каркас модуля с заполнителями для всех этих маршрутов будет такой:
app/api/users.py:
Заполнители ресурсов API пользователя.
from app.api import bp
@bp.route('/users/<int:id>', methods=['GET'])
def get_user(id):
pass
@bp.route('/users', methods=['GET'])
def get_users():
pass
@bp.route('/users/<int:id>/followers', methods=['GET'])
def get_followers(id):
pass
@bp.route('/users/<int:id>/followed', methods=['GET'])
def get_followed(id):
pass
@bp.route('/users', methods=['POST'])
def create_user():
pass
@bp.route('/users/<int:id>', methods=['PUT'])
def update_user(id):
pass
В модуле app/api/errors.py надо бы определить несколько вспомогательных функций, которые имеют дело с ответами на ошибки. Но сейчас, я создам заполнитель, который заполню позже:
app/api/errors.py:
Заполнитель обработки ошибок.
def bad_request():
pass
app/api/tokens.py модуль, в котором будет определена подсистема аутентификации. Это обеспечит альтернативный способ входа для клиентов, которые не являются веб-браузерами. Напишем заполнитель и для этого модуля:
app/api/tokens.py:
Обработки маркеров.
def get_token():
pass
def revoke_token():
pass
Новая схема элементов API Blueprint должна быть зарегистрирована в функции фабрики приложений:
app/__init__.py:
Зарегистрируйте схему элементов API в приложении.
# ...
def create_app(config_class=Config):
app = Flask(__name__)
# ...
from app.api import bp as api_bp
app.register_blueprint(api_bp, url_prefix='/api')
# ...
Представление пользователей в виде объектов JSON
Первый аспект, который следует учитывать при реализации API, — это решить, каким будет представление его ресурсов. Я собираюсь реализовать API, который работает с пользователями, поэтому представление для моих пользовательских ресурсов-это то, что мне нужно решить. После некоторого мозгового штурма, я придумал следующее представление json :
{
"id": 123,
"username": "susan",
"password": "my-password",
"email": "susan@example.com",
"last_seen": "2017-10-20T15:04:27Z",
"about_me": "Hello, my name is Susan!",
"post_count": 7,
"follower_count": 35,
"followed_count": 21,
"_links": {
"self": "/api/users/123",
"followers": "/api/users/123/followers",
"followed": "/api/users/123/followed",
"avatar": "https://www.gravatar.com/avatar/..."
}
}
Многие из полей непосредственно поступают из модели пользовательской базы данных. Поле password
отличается тем, что оно будет использоваться только при регистрации нового пользователя. Как вы помните из главы 5, пользовательские пароли не хранятся в базе данных, а только хэш, поэтому пароль никогда не возвращается. Поле email
также обрабатывается специально, потому что я не хочу раскрывать адреса электронной почты пользователей. Поле электронной почты будет возвращено только тогда, когда пользователи будут запрашивать их собственную запись, но не при получении записей от других пользователей. Поля post_count
, follower_count
и follow_count
являются «виртуальными» полями, которые не существуют в качестве полей в базе данных, но предоставляются клиенту в качестве удобства. Это отличный пример, демонстрирующий, что представление ресурса не обязательно должно соответствовать тому, как фактический ресурс определен на сервере.
Обратите внимание на раздел _links
, который реализует требования hypermedia. Определенные ссылки включают ссылки на текущий ресурс, список пользователей, следующих за этим пользователем, список пользователей, за которыми следует пользователь, и, наконец, ссылку на изображение аватара пользователя. В будущем, если я решу добавить сообщения в этот API, ссылка на список сообщений пользователя также должна быть здесь включена.
Одна из приятных особенностей формата JSON заключается в том, что он всегда переводится как представление в виде словаря или списка Python. Пакет json
из стандартной библиотеки Python заботится о преобразовании структур данных Python в JSON и из него. Поэтому, чтобы сгенерировать эти представления, я собираюсь добавить метод к модели User
, называемый to_dict()
, который возвращает словарь Python:
app/models.py:
Модель пользователя для представления.
from flask import url_for
# ...
class User(UserMixin, db.Model):
# ...
def to_dict(self, include_email=False):
data = {
'id': self.id,
'username': self.username,
'last_seen': self.last_seen.isoformat() + 'Z',
'about_me': self.about_me,
'post_count': self.posts.count(),
'follower_count': self.followers.count(),
'followed_count': self.followed.count(),
'_links': {
'self': url_for('api.get_user', id=self.id),
'followers': url_for('api.get_followers', id=self.id),
'followed': url_for('api.get_followed', id=self.id),
'avatar': self.avatar(128)
}
}
if include_email:
data['email'] = self.email
return data
Этот метод не должен вызывать особых вопросов и быть в основном понятным. Словарь с пользовательским представлением, на котором я остановился, просто генерируется и возвращается. Как я уже упоминал выше, поле email
нуждается в специальной обработке, потому что я хочу включить электронную почту только тогда, когда пользователи запрашивают свои собственные данные. Поэтому я использую флаг include_email
, чтобы определить, включено ли это поле в представление или нет.
Обратите внимание, как генерируется поле last_seen
. Для полей даты и времени я собираюсь использовать Формат ISO 8601, который может генерировать datetime
Python с помощью метода isoformat()
. Но поскольку я использую наивные объекты datetime
, которые являются UTC, но не имеют часового пояса, записанного в их состоянии, мне нужно добавить Z
в конце, что является кодом часового пояса ISO 8601 для UTC.
Наконец, зацените, как я реализовал hipermedia-ссылки. Для трех ссылок, которые указывают на другие маршруты приложений, я использую url_for()
для генерации URL-адресов (которые в настоящее время указывают на функции просмотра замещающих элементов, определенные в app/api/users.py). Ссылка аватара особенная, потому что это URL-адрес Gravatar, внешний для приложения. Для этой ссылки я использую тот же метод avatar()
, который я использовал для рендеринга аватаров на веб-страницах.
Метод to_dict()
преобразует пользовательский объект в представление Python, которое затем будет преобразовано в JSON. Мне также нужно позаботиться об обратном направлении, где клиент передает представление пользователя в запросе, а сервер должен проанализировать его и преобразовать в объект User
. Вот метод from_dict()
, который достигает преобразования из словаря Python в модель:
app/models.py:
Представление модели пользователя.
class User(UserMixin, db.Model):
# ...
def from_dict(self, data, new_user=False):
for field in ['username', 'email', 'about_me']:
if field in data:
setattr(self, field, data[field])
if new_user and 'password' in data:
self.set_password(data['password'])
В этом случае я решил использовать цикл для импорта любого из полей, которые клиент может установить: username
, email
и about_me
. Для каждого поля я проверяю, есть ли значение в аргументе data
, и если есть, я использую setattr()
Python, чтобы установить новое значение в соответствующем атрибуте для объекта.
Поле password
рассматривается как особый случай, поскольку оно не является полем в объекте. Аргумент new_user
определяет, является ли это регистрацией нового пользователя, что означает, что пароль включен. Чтобы установить password
в пользовательской модели, я вызываю метод set_password()
, который создает хэш пароля.
Представление коллекций пользователей
Помимо работы с одиночными представлениями ресурсов, этот API будет нуждаться в представлении для группы пользователей. Это будет Формат, используемый, например, когда клиент запрашивает список пользователей или подписчиков. Вот представление для коллекции пользователей:
{
"items": [
{ ... user resource ... },
{ ... user resource ... },
...
],
"_meta": {
"page": 1,
"per_page": 10,
"total_pages": 20,
"total_items": 195
},
"_links": {
"self": "http://localhost:5000/api/users?page=1",
"next": "http://localhost:5000/api/users?page=2",
"prev": null
}
}
В этом представлении items
-это список пользовательских ресурсов, каждый из которых определен, как описано в предыдущем разделе. Раздел _meta
включает в себя метаданные коллекции, которые клиент может найти полезными при представлении пользователю элементов управления разбиением на страницы. В разделе _links
определяются соответствующие ссылки, включая ссылку на саму коллекцию, а также ссылки на предыдущую и следующую страницы, чтобы помочь клиенту разбить список на страницы.
Создание представления коллекции пользователей сложно из-за логики разбиения на страницы, но логика будет общей для других ресурсов, которые я, возможно, захочу Добавить в этот API в будущем, поэтому я собираюсь реализовать это представление общим способом, который я могу затем применить к другим моделям. Еще в главе 16 я был в аналогичной ситуации с индексами полнотекстового поиска, еще одной функцией, которую я хотел реализовать в общем виде, чтобы ее можно было применить к любым моделям. Решение, которое я использовал, состояло в том, чтобы реализовать класс SearchableMixin
, от которого могут наследовать любые модели, которым нужен полнотекстовый индекс. Я собираюсь использовать ту же идею для этого, так вот новый класс mixin, который я назвал PaginatedAPIMixin
:
app/models.py:
Разбитое на страницы представление класса mixin.
class PaginatedAPIMixin(object):
@staticmethod
def to_collection_dict(query, page, per_page, endpoint, **kwargs):
resources = query.paginate(page, per_page, False)
data = {
'items': [item.to_dict() for item in resources.items],
'_meta': {
'page': page,
'per_page': per_page,
'total_pages': resources.pages,
'total_items': resources.total
},
'_links': {
'self': url_for(endpoint, page=page, per_page=per_page,
**kwargs),
'next': url_for(endpoint, page=page + 1, per_page=per_page,
**kwargs) if resources.has_next else None,
'prev': url_for(endpoint, page=page - 1, per_page=per_page,
**kwargs) if resources.has_prev else None
}
}
return data
Метод to_collection_dict()
создает словарь с представлением пользовательской коллекции, включая разделы items
, _meta
и _links
. Я бы советовал вам внимательно изучить метод, чтобы понять, как он работает. Первые три аргумента-объект запроса Flask-SQLAlchemy, номер страницы и Размер страницы. Эти аргументы определяют, какие элементы будут возвращены. Реализация использует метод paginate()
объекта запроса, чтобы получить стоимость страницы элементов, как я сделал с сообщениями в индексе, исследуйте и профилируйте страницы веб-приложения.
Сложная часть заключается в создании ссылок, которые включают в себя ссылку и ссылки на следующую и предыдущие страницы. Я хотел бы сделать эту функцию обобщенной, поэтому я не мог, например, использовать url_for ('api.get_users', id = id, page = page)
для создания собственной ссылки. Аргументы для url_for()
будут зависеть от конкретной коллекции ресурсов, поэтому я буду полагаться на передачу вызывающего в аргументе конечной точки функции представления, которую нужно отправить url_for()
. И поскольку у многих маршрутов есть аргументы, мне также нужно захватить дополнительные аргументы ключевого слова в kwargs
и передать их url_for()
. Строка аргумента строки page
и per_page
указывается явно, поскольку они управляют разбиением на страницы для всех маршрутов API.
Этот класс mixin должен быть добавлен в модель User
в качестве родительского класса:
app/models.py:
Добавьте PaginatedAPIMixin в модель пользователя.
class User(PaginatedAPIMixin, UserMixin, db.Model):
# ...
В случае с коллекциями мне не понадобится обратное направление, потому что у меня не будет маршрутов, требующих от клиента отправки списков пользователей.
Обработка ошибок
Страницы ошибок, которые я определил в главе 7, подходят только для пользователя, который взаимодействует с приложением, используя веб-браузер. Когда API должен возвращать ошибку, она должен быть «дружественного машине» типа ошибки, то, что клиентское приложение сможет легко интерпретировать. Точно так же я определил представления для своих ресурсов API в JSON, теперь я собираюсь принять решение о представлении сообщений об ошибках API. Вот основная структура, которую я собираюсь использовать:
{
"error": "short error description",
"message": "error message (optional)"
}
В дополнение к полезной нагрузке ошибки я буду использовать коды состояния из протокола HTTP для указания общего класса ошибки. Чтобы помочь мне сгенерировать эти ответы на ошибки, я собираюсь написать функцию error_response()
в app/api/errors.py:
app/api/errors.py:
Ответы об ошибках.
from flask import jsonify
from werkzeug.http import HTTP_STATUS_CODES
def error_response(status_code, message=None):
payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')}
if message:
payload['message'] = message
response = jsonify(payload)
response.status_code = status_code
return response
Эта функция использует удобный словарь HTTP_STATUS_CODES
из Werkzeug
(основная зависимость Flask), который предоставляет краткое описательное имя для каждого кода состояния HTTP. Я использую эти имена для поля error
в своих представлениях ошибок, поэтому мне нужно беспокоиться только о числовом коде состояния и необязательном длинном описании. Функция jsonify()
возвращает объект Response
Flask с кодом состояния по умолчанию 200, поэтому после создания ответа я устанавливаю код состояния на правильный для ошибки.
Наиболее распространенной ошибкой, которую API собирается вернуть, будет код 400, который является ошибкой для "плохого запроса". Это-ошибка, которая используется, когда клиент передает запрос, который имеет недопустимые данные в нем. Чтобы сделать эту ошибку еще проще, я добавлю для нее специальную функцию, которая требует только длинного описательного сообщения в качестве аргумента. Это заполнитель bad_request()
, который я добавил ранее:
app/api/errors.py:
Ответы на плохие запросы.
# ...
def bad_request(message):
return error_response(400, message)
Конечные точки пользовательских ресурсов
Поддержка, которая мне нужна для работы с представлениями пользователей JSON, теперь завершена, поэтому я готов начать кодирование конечных точек API.
Получение пользователя
Начнем с запроса на получение одного пользователя, заданного id
:
app/api/users.py:
Возврат пользователя.
from flask import jsonify
from app.models import User
@bp.route('/users/<int:id>', methods=['GET'])
def get_user(id):
return jsonify(User.query.get_or_404(id).to_dict())
Функция view получает идентификатор запрошенного пользователя в качестве динамического аргумента в URL-адресе. Метод get_or_404()
объекта запроса является очень полезным вариантом метода get()
, который вы видели ранее, который также возвращает объект с заданным идентификатором, если он существует, но вместо того, чтобы возвращать None
, когда id
не существует, он прерывает запрос и возвращает ошибку 404 клиенту. Преимущество get_or_404()
перед get()
заключается в том, что он устраняет необходимость проверять результат запроса, упрощая логику в функциях представления.
Метод to_dict()
, который я добавил к User
, используется для создания словаря с представлением ресурса для выбранного пользователя, а затем функция Flask jsonify()
преобразует этот словарь в формат JSON для возврата клиенту.
Если вы хотите увидеть, как работает этот первый маршрут API, запустите сервер, а затем введите следующий URL-адрес в адресной строке браузера:
http://localhost:5000/api/users/1
Результат должен показать вам первого пользователя, отображенного в формате JSON. Также попробуйте использовать большое значение id
, чтобы увидеть, как метод get_or_404()
объекта запроса SQLAlchemy вызывает ошибку 404 (я позже покажу вам, как расширить обработку ошибок, чтобы эти ошибки также возвращались в формате JSON).
Чтобы протестировать этот новый маршрут, я установлю HTTPie, HTTP-клиент командной строки, написанный на Python, который упрощает отправку запросов API:
(venv) $ pip install httpie
Теперь я могу запросить информацию о пользователе с идентификатором 1 (который, вероятно, ты сам и есть) с помощью следующей команды:
(venv) $ http GET http://localhost:5000/api/users/1
HTTP/1.0 200 OK
Content-Length: 457
Content-Type: application/json
Date: Mon, 27 Nov 2017 20:19:01 GMT
Server: Werkzeug/0.12.2 Python/3.6.3
{
"_links": {
"avatar": "https://www.gravatar.com/avatar/993c...2724?d=identicon&s=128",
"followed": "/api/users/1/followed",
"followers": "/api/users/1/followers",
"self": "/api/users/1"
},
"about_me": "Hello! I'm the author of the Flask Mega-Tutorial.",
"followed_count": 0,
"follower_count": 1,
"id": 1,
"last_seen": "2017-11-26T07:40:52.942865Z",
"post_count": 10,
"username": "miguel"
}
Получение коллекций пользователей
Чтобы вернуть коллекцию всех пользователей, теперь я могу полагаться на метод to_collection_dict()
PaginatedAPIMixin:
app/api/users.py: Возвращает коллекцию всех пользователей.
from flask import request
@bp.route('/users', methods=['GET'])
def get_users():
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 10, type=int), 100)
data = User.to_collection_dict(User.query, page, per_page, 'api.get_users')
return jsonify(data)
Для этой реализации я сначала извлекаю page
и per_page
из строки запроса, используя значения по умолчанию 1 и 10 соответственно, если они не определены. per_page
имеет дополнительную логику, которая ограничивает его 100. Предоставление клиентского элемента управления для запроса действительно больших страниц не является хорошей идеей, так как это может вызвать на сервере проблемы с производительностью. Аргументы page
и per_page
затем передаются методу to_collection_query()
вместе с запросом, который в данном случае является просто User.query
-самый универсальный запрос, возвращающий всех пользователей. Последний аргумент-api.get_users
, является именем конечной точки, который мне нужен для трех ссылок что бы использовать их в представлении.
Чтобы проверить эту конечную точку с помощью HTTPie, используйте следующую команду:
(venv) $ http GET http://localhost:5000/api/users
The next two endpoints are the ones that return the follower and followed users. These are fairly similar to the one above:
app/api/users.py: Return followers and followed users.
@bp.route('/users/<int:id>/followers', methods=['GET'])
def get_followers(id):
user = User.query.get_or_404(id)
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 10, type=int), 100)
data = User.to_collection_dict(user.followers, page, per_page,
'api.get_followers', id=id)
return jsonify(data)
@bp.route('/users/<int:id>/followed', methods=['GET'])
def get_followed(id):
user = User.query.get_or_404(id)
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 10, type=int), 100)
data = User.to_collection_dict(user.followed, page, per_page,
'api.get_followed', id=id)
return jsonify(data)
Поскольку эти два маршрута специфичны для пользователя, у них есть динамический аргумент id
. Идентификатор используется для получения пользователя из базы данных, а затем для предоставления user.followers
и user.followed
отношения на основе запросов к to_collection_dict()
, так что, надеюсь, теперь вы можете увидеть, почему затраты дополнительного времени и проектирование этого метода в общем виде действительно окупается. Последние два аргумента to_collection_dict()
— это имя конечной точки и идентификатор, который метод будет принимать в качестве дополнительного аргумента ключевого слова в kwargs
, а затем передавать его в url_for()
при создании раздела ссылок представления.
Как и в предыдущем примере, вы можете использовать эти два маршрута с HTTPie следующим образом:
(venv) $ http GET http://localhost:5000/api/users/1/followers
(venv) $ http GET http://localhost:5000/api/users/1/followed
Я должен отметить, что благодаря hypermedia вам не нужно запоминать эти URL-адреса, поскольку они включены в раздел _links
пользовательского представления.
Регистрация новых пользователей
Запрос POST
на маршрут /users будет использоваться для регистрации новых учетных записей пользователей. Вы можете увидеть реализацию этого маршрута ниже:
app/api/users.py:
Зарегистрируйте нового пользователя.
from flask import url_for
from app import db
from app.api.errors import bad_request
@bp.route('/users', methods=['POST'])
def create_user():
data = request.get_json() or {}
if 'username' not in data or 'email' not in data or 'password' not in data:
return bad_request('must include username, email and password fields')
if User.query.filter_by(username=data['username']).first():
return bad_request('please use a different username')
if User.query.filter_by(email=data['email']).first():
return bad_request('please use a different email address')
user = User()
user.from_dict(data, new_user=True)
db.session.add(user)
db.session.commit()
response = jsonify(user.to_dict())
response.status_code = 201
response.headers['Location'] = url_for('api.get_user', id=user.id)
return response
Этот запрос будет принимать представление пользователя в формате JSON от клиента, предоставленного в теле запроса. Flask предоставляет метод request.get_json()
, чтобы извлечь JSON из запроса и вернуть его в виде структуры Python. Этот метод возвращает None
, если данные JSON не найдены в запросе, поэтому я могу гарантировать, что я всегда получаю словарь, используя запрос request.get_json()
или {}
.
Прежде чем я смогу использовать данные, мне нужно убедиться, что у меня есть вся информация, поэтому я начинаю с проверки того, что включены три обязательных поля. Это username
, email
и password
. Если какой-либо из них отсутствует, то я использую вспомогательную функцию bad_request()
из app/api/errors.py для возврата клиенту ошибки. В дополнение к этой проверке мне нужно убедиться, что поля username
и email
еще не используются другим пользователем, поэтому для этого я пытаюсь загрузить пользователя из базы данных по имени пользователя и электронной почте, и если какой-либо из них возвращает действительного пользователя, я также возвращаю ошибку обратно клиенту.
После того, как я прошел проверку данных, я могу легко создать объект пользователя и добавить его в базу данных. Для создания пользователя я использую метод from_dict()
в пользовательской модели. Аргумент new_user
имеет значение True
, поэтому он также принимает поле password
, которое обычно не является частью представления пользователя.
Ответ, который я верну для этого запроса, будет представлением нового пользователя, поэтому to_dict()
генерирует эту полезную нагрузку. Код состояния для запроса POST
, который создает ресурс, должен быть 201
, который используется, когда новый объект был успешно создан. Кроме того, для протокола HTTP требуется, чтобы ответ 201
включал заголовок Location, который задан URL-адресом нового ресурса.
Ниже вы можете увидеть, как зарегистрировать нового пользователя из командной строки через HTTPie:
(venv) $ http POST http://localhost:5000/api/users username=alice password=dog \
email=alice@example.com "about_me=Hello, my name is Alice!"
Редактирование пользователей
Последняя конечная точка, которую я собираюсь использовать в своем API, — это та, которая изменяет существующего пользователя:
app/api/users.py:
Изменение пользователя.
@bp.route('/users/<int:id>', methods=['PUT'])
def update_user(id):
user = User.query.get_or_404(id)
data = request.get_json() or {}
if 'username' in data and data['username'] != user.username and \
User.query.filter_by(username=data['username']).first():
return bad_request('please use a different username')
if 'email' in data and data['email'] != user.email and \
User.query.filter_by(email=data['email']).first():
return bad_request('please use a different email address')
user.from_dict(data, new_user=False)
db.session.commit()
return jsonify(user.to_dict())
Для этого запроса я получаю id
пользователя как динамическую часть URL, поэтому я могу загрузить назначенного пользователя и вернуть ошибку 404
, если она не найдена. Как и в случае с новым пользователем, мне нужно проверить, что поля username
и email
, предоставленные клиентом, не сталкиваются с другими пользователями, прежде чем я смогу их использовать, но в этом случае проверка немного сложнее. Прежде всего, эти поля являются необязательными в этом запросе, поэтому мне нужно проверить, что поле присутствует. Второе осложнение заключается в том, что клиент может предоставлять одно и то же значение, поэтому, прежде чем я проверю, берется ли имя пользователя или электронная почта, мне нужно убедиться, что они отличаются от текущих. Если какая-либо из этих проверок завершится ошибкой, я верну клиенту ошибку 400
, как и раньше.
После проверки данных я могу использовать метод From_dict()
пользовательской модели для импорта всех данных, предоставленных клиентом, а затем зафиксировать изменение в базе данных. Ответ на этот запрос возвращает пользователю обновленное представление пользователя с кодом состояния по умолчанию 200
.
Вот пример запроса, который редактирует поле about_me
с HTTPie:
(venv) $ http PUT http://localhost:5000/api/users/2 "about_me=Hi, I am Miguel"
API Аутентификация
Конечные точки API, которые я добавил в предыдущем разделе, в настоящее время открыты для любых клиентов. Очевидно, что они должны быть доступны только зарегистрированным пользователям, и для этого мне нужно добавить аутентификацию и авторизацию, или «AuthN» и «AuthZ» для краткости. Идея состоит в том, что запросы, отправленные клиентами, предоставляют некоторую идентификацию, так что сервер знает, какого пользователя представляет клиент, и может проверить, разрешено или нет запрошенное действие для этого пользователя.
Наиболее очевидным способом защиты этих конечных точек API является использование декоратора @login_required
из Flask-Login, но у этого подхода есть некоторые проблемы. Когда декоратор обнаруживает не аутентифицированного пользователя, он перенаправляет пользователя на HTML страницу входа в систему. В API нет концепции страниц HTML или входа в систему, если клиент отправляет запрос с недопустимыми или отсутствующими учетными данными, сервер должен отказаться от запроса, возвращающего код состояния 401. Сервер не может предположить, что клиент API является веб-браузером или он может обрабатывать переадресации или что он может отображать и обрабатывать формы входа в систему HTML. Когда API клиента получает код состояния 401, клиент знает, что ему нужно запросить у пользователя учетные данные, но как это происходит, на самом деле это не дело сервера.
Маркеры(токены) в пользовательской модели
Для проверки подлинности API я собираюсь использовать схему аутентификации с токеном. Когда клиент хочет начать взаимодействие с API, ему необходимо запросить временный токен, аутентифицироваться с именем пользователя и паролем. Затем клиент может отправлять запросы API, передающие токен в качестве аутентификации, до тех пор, пока токен действителен. По истечении срока действия токена необходимо запросить новый токен. Для поддержки токенов пользователей я собираюсь расширить модель User
:
app/models.py:
Поддержка пользовательских токенов.
import base64
from datetime import datetime, timedelta
import os
class User(UserMixin, PaginatedAPIMixin, db.Model):
# ...
token = db.Column(db.String(32), index=True, unique=True)
token_expiration = db.Column(db.DateTime)
# ...
def get_token(self, expires_in=3600):
now = datetime.utcnow()
if self.token and self.token_expiration > now + timedelta(seconds=60):
return self.token
self.token = base64.b64encode(os.urandom(24)).decode('utf-8')
self.token_expiration = now + timedelta(seconds=expires_in)
db.session.add(self)
return self.token
def revoke_token(self):
self.token_expiration = datetime.utcnow() - timedelta(seconds=1)
@staticmethod
def check_token(token):
user = User.query.filter_by(token=token).first()
if user is None or user.token_expiration < datetime.utcnow():
return None
return user
С этим изменением я добавляю атрибут token
в пользовательскую модель, и поскольку мне нужно будет искать базу данных по ней, я делаю ее уникальной и индексированной. Я также добавил token_expiration
, в котором есть дата и время истечения срока действия токена. Это делается для того, чтобы маркер не оставался действительным в течение длительного периода времени, что может стать угрозой безопасности.
Я создал три метода работы с этими токенами. Метод get_token()
возвращает токен для пользователя. Токен генерируется как случайная строка, закодированная в base64, так что все символы находятся в читаемом диапазоне. Перед созданием нового токена этот метод проверяет, есть ли у назначенного токена по крайней мере минута до истечения срока действия, и в этом случае возвращается существующий токен.
При работе с токенами всегда полезно иметь стратегию немедленного отзыва токена, а не полагаться только на дату истечения срока действия. Это лучшая практика в области безопасности, которую часто упускают из виду. Метод revoke_token()
делает маркер, назначенный пользователю, недействительным, просто установив дату истечения срока действия на одну секунду до текущего времени.
Метод check_token()
является статическим методом, который принимает токен в качестве входных данных и возвращает пользователя, которому этот токен принадлежит в качестве ответа. Если токен недействителен или истек, метод возвращает None.
Поскольку я внес изменения в базу данных, мне нужно создать новую миграцию базы данных, а затем обновить ее:
(venv) $ flask db migrate -m "user tokens"
(venv) $ flask db upgrade
Запросы Маркеров(токенов)
Когда вы пишете API, вы должны учитывать, что ваши клиенты не всегда будут веб-браузерами, подключенными к веб-приложению. Реальная сила API приходит, когда автономные клиенты, такие как приложения для смартфонов или даже браузерные одностраничные приложения могут иметь доступ к серверным службам. Когда эти специализированные клиенты нуждаются в доступе к службам API, они начинают с запроса маркера, который является аналогом формы входа в традиционное веб-приложение.
Чтобы упростить взаимодействие между клиентом и сервером при использовании аутентификации токенов, я собираюсь использовать расширение Flask под названием Flask-HTTPAuth. Flask-HTTPAuth устанавливается с pip:
(venv) $ pip install flask-httpauth
Flask-HTTPAuth поддерживает несколько различных механизмов аутентификации, все API дружественные. Для начала я собираюсь использовать HTTP Basic Authentication или тут 11.1, в которой клиент отправляет учетные данные пользователя в стандартном заголовке http авторизации. Для интеграции с Flask-HTTPAuth приложение должно предоставить две функции: одну, которая определяет логику для проверки имени пользователя и пароля, предоставленных пользователем, и другую, которая возвращает ответ об ошибке в случае сбоя аутентификации. Эти функции регистрируются в Flask-HTTPAuth через декораторы, а затем автоматически вызываются расширением по мере необходимости во время потока проверки подлинности. Вы можете увидеть реализацию:
app/api/auth.py:
Поддержка обычной проверки подлинности.
from flask import g
from flask_httpauth import HTTPBasicAuth
from app.models import User
from app.api.errors import error_response
basic_auth = HTTPBasicAuth()
@basic_auth.verify_password
def verify_password(username, password):
user = User.query.filter_by(username=username).first()
if user is None:
return False
g.current_user = user
return user.check_password(password)
@basic_auth.error_handler
def basic_auth_error():
return error_response(401)
Класс HTTPBasicAuth
из Flask-HTTPAuth-это класс, реализующий основной поток проверки подлинности. Две необходимые функции настраиваются с помощью декораторов verify_password
и error_handler
соответственно.
Функция проверки получает имя пользователя и пароль, предоставленные клиентом, и возвращает True
, если учетные данные действительны, или False
, если нет. Для проверки пароля я полагаюсь на метод check_password()
класса User
, который также используется Flask-Login при аутентификации для веб-приложения. Я сохраняю аутентифицированного пользователя в g.current_user
, так что я могу получить доступ к нему из функций представления API.
Функция обработчика ошибок просто возвращает ошибку 401, сгенерированную функцией error_response()
в app/api/errors.py. Ошибка 401 определяется в стандарте HTTP как "Unauthorized" ошибка ("несанкционированного доступа"). Клиенты HTTP знают, что при получении этой ошибки отправленный ими запрос должен быть повторно отправлен с действительными учетными данными.
Теперь у меня есть базовая поддержка аутентификации, поэтому я могу добавить маршрут поиска токенов, который будут вызывать клиенты, когда им нужен этот самый токен:
app/api/tokens.py:
Generate user tokens.
from flask import jsonify, g
from app import db
from app.api import bp
from app.api.auth import basic_auth
@bp.route('/tokens', methods=['POST'])
@basic_auth.login_required
def get_token():
token = g.current_user.get_token()
db.session.commit()
return jsonify({'token': token})
Эта функция представления обернута декоратором @basic_auth.login_required
из экземпляра HTTPBasicAuth, который будет инструктировать Flask-HTTPAuth для проверки подлинности (через функцию проверки которую я определил выше) и разрешать функцию для запуска только, когда предоставленные учетные данные являются действительными. Реализация этой функции представления зависит от метода get_token()
пользовательской модели для создания маркера. Фиксация базы данных выполняется после создания маркера, чтобы гарантировать, что токен и его срок действия будут записаны обратно в базу данных.
При попытке отправить запрос POST на маршрут API маркеров происходит следующее:
(venv) $ http POST http://localhost:5000/api/tokens
HTTP/1.0 401 UNAUTHORIZED
Content-Length: 30
Content-Type: application/json
Date: Mon, 27 Nov 2017 20:01:00 GMT
Server: Werkzeug/0.12.2 Python/3.6.3
WWW-Authenticate: Basic realm="Authentication Required"
{
"error": "Unauthorized"
}
Ответ HTTP включает в себя код состояния 401 и полезную нагрузку, которую я определил в моей функции basic_auth_error()
. Вот тот же запрос, на этот раз включая базовые учетные данные:
(venv) $ http --auth <username>:<password> POST http://localhost:5000/api/tokens
HTTP/1.0 200 OK
Content-Length: 50
Content-Type: application/json
Date: Mon, 27 Nov 2017 20:01:22 GMT
Server: Werkzeug/0.12.2 Python/3.6.3
{
"token": "pC1Nu9wwyNt8VCj1trWilFdFI276AcbS"
}
Теперь код состояния 200, который является кодом для успешного запроса, и полезные данные включают недавно созданный маркер для пользователя. Обратите внимание, что при отправке этого запроса необходимо заменить <username>:<password>
своими учетными данными. Имя пользователя и пароль должны быть снабжены двоеточием в качестве разделителя.
Защита маршрутов API с помощью токенов
Теперь клиенты могут запрашивать токен для использования с конечными точками API, поэтому осталось добавить проверку токена на эти конечные точки. Это то, что Flask-HTTPAuth также может обрабатывать для меня. Мне нужно создать второй экземпляр проверки подлинности на основе класса HTTPTokenAuth
и предоставить обратный вызов проверки токена:
app/api/auth.py:
Поддержка аутентификации Token.
# ...
from flask_httpauth import HTTPTokenAuth
# ...
token_auth = HTTPTokenAuth()
# ...
@token_auth.verify_token
def verify_token(token):
g.current_user = User.check_token(token) if token else None
return g.current_user is not None
@token_auth.error_handler
def token_auth_error():
return error_response(401)
При использовании аутентификации по токенам Flask-HTTPAuth использует функцию verify_token
, но кроме этого аутентификация токена работает так же, как и базовая аутентификация. Функция проверки токена использует User.check_token()
, чтобы найти пользователя, которому принадлежит предоставленный токен. Функция также обрабатывает случай отсутствующего токена, установив текущего пользователя в None
. Возвращаемое значение True
или False
определяет, может ли Flask-HTTPAuth разрешить выполнение функции просмотра или нет.
Чтобы защитить маршруты API с помощью токенов, необходимо добавить декоратор @token_auth.login_required
:
app/api/users.py:
Protect user routes with token authentication.
from app.api.auth import token_auth
@bp.route('/users/<int:id>', methods=['GET'])
@token_auth.login_required
def get_user(id):
# ...
@bp.route('/users', methods=['GET'])
@token_auth.login_required
def get_users():
# ...
@bp.route('/users/<int:id>/followers', methods=['GET'])
@token_auth.login_required
def get_followers(id):
# ...
@bp.route('/users/<int:id>/followed', methods=['GET'])
@token_auth.login_required
def get_followed(id):
# ...
@bp.route('/users', methods=['POST'])
def create_user():
# ...
@bp.route('/users/<int:id>', methods=['PUT'])
@token_auth.login_required
def update_user(id):
# ...
Обратите внимание, что декоратор добавляется ко всем функциям представления API, кроме create_user()
, который не может принять аутентификацию, так как пользователь, который запросит маркер, должен быть создан первым.
Если вы отправляете запрос на любую из этих конечных точек, как показано ранее, вы получите ответ об ошибке 401. Чтобы получить доступ, вам нужно добавить заголовок Authorization
с маркером, который вы получили от запроса в /api/tokens. Flask-HTTPAuth ожидает, что токен будет отправлен как токен-носитель, который напрямую не поддерживается HTTPie. Для базовой аутентификации с именем пользователя и паролем HTTPie предлагает параметр --auth
, но для токенов заголовок должен быть явно указан. Вот синтаксис для отправки токена-носителя:
(venv) $ http GET http://localhost:5000/api/users/1 \
"Authorization:Bearer pC1Nu9wwyNt8VCj1trWilFdFI276AcbS"
Отмена токенов
Последняя функция, связанная с токеном, которую я собираюсь реализовать, — это отзыв токена, который Вы можете увидеть ниже:
app/api/tokens.py:
Revoke tokens.
from app.api.auth import token_auth
@bp.route('/tokens', methods=['DELETE'])
@token_auth.login_required
def revoke_token():
g.current_user.revoke_token()
db.session.commit()
return '', 204
Клиенты могут отправить запрос DELETE
на удаление по URL-адресу /tokens, чтобы аннулировать маркер. Проверка подлинности для этого маршрута основана на маркере, на самом деле маркер, отправляемый в заголовке Authorization
, является отозванным. Само аннулирование использует вспомогательный метод в классе User
, который сбрасывает дату истечения срока действия маркера. Сеанс базы данных фиксируется таким образом, что это изменение записывается в базу данных. Ответ от этого запроса не имеет тела, поэтому я могу вернуть пустую строку. Второе значение в инструкции return задает код состояния ответа 204, который используется для успешных запросов, не имеющих тела ответа.
Вот пример запроса на отзыв токена, отправленного из HTTPie:
(venv) $ http DELETE http://localhost:5000/api/tokens \
Authorization:"Bearer pC1Nu9wwyNt8VCj1trWilFdFI276AcbS"
Сообщения об ошибках API
Вы помните, что произошло в начале этой главы, когда я попросил вас отправить запрос API из браузера с недействительным URL-адресом пользователя? Сервер вернул ошибку 404, но эта ошибка была отформатирована как стандартная страница ошибки 404 HTML. Многие ошибки, которые может потребоваться вернуть API, могут быть переопределены версиями JSON в схеме элементов API, но есть некоторые ошибки, обрабатываемые Flask, которые по-прежнему проходят через обработчики ошибок, глобально зарегистрированные для приложения, и они продолжают возвращать HTML.
Протокол HTTP поддерживает механизм, с помощью которого клиент и сервер могут согласовать оптимальный Формат ответа, называемый согласованием содержимого. Клиенту необходимо отправить Заголовок Accept
с запросом, указав предпочтения формата. Затем сервер смотрит на список и отвечает, используя лучший формат, который он поддерживает из списка, предлагаемого клиентом.
Я хочу изменить глобальные обработчики ошибок приложений, чтобы они использовали согласование содержимого для ответа в HTML или JSON в соответствии с предпочтениями клиента. Это можно сделать с помощью объекта Flask request.accept_mimetypes
:
app/errors/handlers.py:
Согласование содержимого для ответов об ошибках.
from flask import render_template, request
from app import db
from app.errors import bp
from app.api.errors import error_response as api_error_response
def wants_json_response():
return request.accept_mimetypes['application/json'] >= \
request.accept_mimetypes['text/html']
@bp.app_errorhandler(404)
def not_found_error(error):
if wants_json_response():
return api_error_response(404)
return render_template('errors/404.html'), 404
@bp.app_errorhandler(500)
def internal_error(error):
db.session.rollback()
if wants_json_response():
return api_error_response(500)
return render_template('errors/500.html'), 500
Вспомогательная функция wants_json_response()
сравнивает предпочтения для JSON или HTML, выбранные клиентом в списке предпочтительных форматов. Если скорость JSON выше, чем HTML, то я возвращаю ответ JSON. В противном случае я верну исходные HTML-ответы на основе шаблонов. Для ответов JSON я собираюсь импортировать вспомогательную функцию error_response
из схемы элементов API, но здесь я собираюсь переименовать ее в api_error_response()
, чтобы было ясно, что она делает и откуда.
Комментариев нет:
Отправить комментарий