Хабрахабр

[Перевод] Мега-Учебник Flask, Часть XXIII: Интерфейсы прикладного программирования (API)

(издание 2018)

Miguel Grinberg

Туда Сюда

Это двадцать третья часть Мега-Учебника, в которой я расскажу вам, как расширить микроблог с помощью интерфейса прикладного программирования (или API), который клиенты могут использовать для работы с приложением более прямым способом, чем традиционный рабочий процесс веб-браузера.

Под спойлером приведен список всех статей серии 2018 года.

Оглавление

Примечание 1: Если вы ищете старые версии данного курса, это здесь.

Но как насчет других типов клиентов? Вся функциональность которую я построил до сих пор для этого приложения, предназначена для одного конкретного типа клиента: веб-браузер. Самым простым решением было бы создать приложение с помощью веб-компонента, который заполнит весь экран и загрузит веб-сайт Microblog, но это не будет качественно лучшим по сравнению с открытием приложения в веб-браузере устройства. Например, если бы я хотел создать приложение для Android или iOS, у меня есть два основных способа его решения. Лучшим решением (хотя и гораздо более трудоемким) было бы создание собственного приложения, но как это приложение может взаимодействовать с сервером, который возвращает только HTML-страницы?

API-это коллекция HTTP-маршрутов, которые разрабатываются как низкоуровневые точки входа в приложение. Это проблемная область, в которой могут помочь Интерфейсы Прикладного Программирования (или API). Например, API в микроблоге может предоставить клиенту информацию о пользователе и записи в блоге, а также позволить пользователю редактировать существующую запись в блоге, но только на уровне данных, не смешивая эту логику с HTML. Вместо того, чтобы определять маршруты и просматривать функции, возвращающие HTML, которые будут использоваться веб-браузерами, API позволяют клиенту работать непосредственно с ресурсами приложения, оставляя решение о том, как представить информацию пользователю полностью клиенту.

Вы их нашли? Если вы изучите все маршруты (routes), определенные в настоящее время в приложении, Вы заметите, что есть несколько, которые могут соответствовать определению API, которое я использовал выше. Это маршрут, который принимает текст, исходный и конечный языки, все данные в формате JSON в запросе POST. Я говорю о нескольких маршрутах, которые возвращают JSON, таких как маршрут /translate, определенный в главе 14. Сервер возвращает только запрошенную информацию, оставляя клиент с ответственностью представить эту информацию пользователю. Ответом на этот запрос является перевод этого текста, также в формате JSON.

Учтите, что если приложение для смартфонов захотело использовать эти маршруты, оно не было бы в состоянии, потому что им нужен зарегистрированный пользователь, а вход в систему возможен только через HTML-форму. В то время как маршруты JSON в приложении имеют API "чувствовать" к ним, они были разработаны, чтобы поддержать веб-приложение, работающее в браузере.
Хотя маршруты JSON в приложении имеют API-интерфейс, остается "ощущение", что они были разработаны для поддержки веб-приложения, запущенного в браузере. В этой главе я расскажу, как создавать API-интерфейсы, не полагающиеся на веб-браузер, и не делать никаких предположений о том, какой клиент подключается к ним.

Ссылки GitHub для этой главы: Browse, Zip, Diff.

REST как основа проектирования API

Другие могут согласиться с оговоркой, что они считают их плохо разработанным API. Кто то может категорически не согласиться с моим утверждением выше, что /translate и другие маршруты JSON являются маршрутами API. Итак, каковы характеристики хорошо разработанного API, и почему маршруты JSON вне этой категории?

REST, который означает Representational State Transfer (Репрезентативный Государственный Перевод), является архитектурой, предложенной доктором Роем Филдингом в его докторской диссертации. Возможно, вы слышали термин rest API. В своей работе д-р Филдинг представляет шесть определяющих характеристик REST в довольно абстрактном и общем виде.

Тема о том, соответствует ли данный API REST или нет, часто является источником жарких дебатов между REST «пуристами», которые считают, что REST API должен соблюдать все шесть характеристик и делать это чётко определенным образом по сравнению с «прагматиками» REST, которые берут идеи, представленные д-ром Филдингом в своей диссертации в качестве руководящих принципов или рекомендаций. Кроме диссертации доктора Филдинга, нет никакой другой авторитетной спецификации REST, что оставляет много чего для свободной интерпретации читателю. Д-р Филдинг сам встал на сторону пуристского лагеря и дал некоторое дополнительное представление о своем видении в блогах и онлайн-комментариях.

Это включает в себя большинство API-интерфейсов от «крупных игроков», таких как Facebook, GitHub, Twitter и т.д. Подавляющее большинство API-интерфейсов, реализованных в настоящее время, придерживаются «прагматичной» реализации REST. Несмотря на строгие взгляды д-ра Филдинга и других пуристов REST на то, что является или не является REST API, в индустрии программного обеспечения обычно упоминается REST в прагматическом смысле. Существует очень мало публичных API, которые единодушно считаются чистыми REST, поскольку большинство API-интерфейсов пропускают некоторые детали реализации, которые пуристы считают обязательными.

Чтобы дать вам представление о том, что находится в диссертации REST, в следующих разделах описываются шесть принципов, перечисленных д-ром Филдингом.

Client-Server

На практике это означает, что клиент и сервер находятся в отдельных процессах, которые взаимодействуют через транспорт, который в большинстве случаев является протоколом HTTP по сети TCP. Принцип клиент-сервер довольно прост, так как он просто гласит, что в REST API роли клиента и сервера должны быть четко дифференцированы.

Layered System

Идея заключается в том, что для клиента не должно быть абсолютно никакой разницы в том, как он отправляет запросы, если не подключен непосредственно к серверу, на самом деле он может даже не знать, подключен ли он к целевому серверу или нет. Принцип Layered System (многоуровневой системы) говорит, что когда клиент должен взаимодействовать с сервером, он может быть связан с посредником, а не с фактическим сервером. Аналогичным образом, этот принцип гласит, что сервер может получать клиентские запросы от посредника, а не непосредственно от клиента, поэтому он никогда не должен предполагать, что другая сторона соединения является клиентом.

Это важная функция REST, поскольку возможность добавления промежуточных узлов позволяет архитекторам приложений разрабатывать большие и сложные сети, которые могут удовлетворить большой объем запросов с помощью балансировщика нагрузки, кэшей, прокси-серверов и т.д.

Cache

Существует реализация кэша, с которой вы, вероятно, знакомы: один во всех веб-браузерах. Этот принцип расширяет многоуровневую систему, явно указывая, что сервер или посредник может кэшировать ответы на запросы, которые часто поступают для повышения производительности системы. Слой кэша веб-браузера часто используется, чтобы избежать необходимости запрашивать одни и те же файлы, такие как изображения, снова и снова.

Обратите внимание, что поскольку по соображениям безопасности API, развернутые в рабочей среде, должны использовать шифрование, кэширование обычно не выполняется на промежуточном узле, если только этот узел не завершает соединение SSL или не выполняет расшифровку и повторное шифрование. Для целей API целевой сервер должен указать с помощью элементов управления кэшем, может ли ответ кэшироваться посредниками, когда он возвращается клиенту.

Code On Demand

Поскольку этот принцип требует соглашения между сервером и клиентом о том, какой исполняемый код может выполнять клиент, это редко используется в API. Это необязательное требование, указывающее, что сервер может предоставлять исполняемый код в ответах клиенту. Например, выполнение JavaScript может привести к усложнению, если клиент является iOS или Android-устройством. Вы могли бы подумать, что сервер может вернуть код JavaScript для запуска веб-браузеров, но REST специально не предназначен для клиентов веб-браузера.

Stateless

В нем указано, что REST API не должен сохранять любое состояние клиента, которое будет вызвано каждый раз, когда данный клиент отправляет запрос. Принцип stateless является одним из двух в центре большинства дебатов между пуристами REST и прагматиками. В API без состояния каждый запрос должен включать информацию, которую сервер должен идентифицировать и аутентифицировать клиента и выполнить запрос. Это означает, что ни один из механизмов, которые являются обычными в веб-разработке для «запоминания» пользователей при навигации по страницам приложения, не может быть использован. Это также означает, что сервер не может хранить данные, относящиеся к клиентскому соединению в базе данных или другой форме хранения.

Если сервер хранит состояние клиента, ситуация становится более сложной, так как вам нужно выяснить, как несколько серверов могут получить доступ и обновить это состояние, или же гарантировать, что данный клиент всегда обрабатывается одним и тем же сервером, что обычно называется липкими сеансами. Если вам интересно, почему REST требует сервер без состояния, то основная причина заключается в том, что серверы без учета состояния (stateless) очень просты в масштабировании, все, что вам нужно сделать, это запустить несколько экземпляров сервера за балансировщиком нагрузки.

Если вы снова рассмотрите маршрут /translate, обсуждаемый в начале главы, вы поймете, что его нельзя считать RESTful, потому что функция вида, связанная с этим маршрутом, полагается на декодер @login_required из Flask-Login, который, в свою очередь, хранит зарегистрированный в состоянии пользователя в сеансе пользователя Flask.

Uniform Interface

Д-р Филдинг перечисляет четыре отличительных аспекта единого интерфейса REST: уникальные идентификаторы ресурсов, представления ресурсов, самоописательные сообщения и гипермедиа. Последний, самый важный, самый обсуждаемый и наиболее неопределенно документированный принцип REST — это единый интерфейс.

Например, URL-адрес, связанный с данным пользователем, может быть /api/users/<user-id>, где <user-id> — это идентификатор, назначенный пользователю в качестве первичного ключа таблицы базы данных. Уникальные идентификаторы ресурсов получаются путем назначения уникального URL-адреса каждому ресурсу. Это вполне приемлемо реализовано большинством API.

Для большинства современных API Формат JSON используется для построения представлений ресурсов. Использование представлений ресурсов означает, что если сервер и клиент обмениваются информацией о ресурсе, они должны использовать согласованный Формат. API может поддерживать несколько форматов представления ресурсов, и в этом случае параметры согласования содержимого в протоколе HTTP являются механизмом, с помощью которого клиент и сервер могут согласовать формат, который нравится обоим.

Типичный пример — это метод запроса HTTP используемый для указания, какую операцию клиент хочет получить от сервера. Самоописательные сообщения означают, что запросы и ответы, которыми обмениваются клиенты и сервер, должны включать всю информацию, необходимую другой стороне. Целевой ресурс указывается как URL-адрес запроса с дополнительной информацией, представленной в заголовках HTTP, части строки запроса URL-адреса или тела(body) запроса. Запрос GET указывает, что клиент хочет получить сведения о ресурсе, запрос POST указывает, что клиент хочет создать новый ресурс, запросы PUT или PATCH определяют изменения существующих ресурсов, а запрос DELETE указывает на удаление ресурса.

Поскольку все ресурсы в приложении взаимосвязаны, это требует обязательного включения связей в представления ресурсов, чтобы клиенты могли обнаруживать новые ресурсы путем обхода связей, почти так же, как вы обнаруживаете новые страницы в веб-приложении, щелкая ссылки, которые ведут вас от одной страницы к другой. Требование hypermedia является наиболее полемичным из множества, и тот, который реализуется немногими API, и те API, которые реализуют его, редко делают так, чтобы удовлетворить пуристов REST. Одним из аспектов, которые усложняют выполнение данного требования заключается в том, что в отличие от HTML и XML, Формат json, который обычно используется для представления ресурсов в API не определяет стандартный способ включения ссылок, так что вы вынуждены использовать специальные настраиваемые структуры, или один из предлагаемых расширений JSON, которые пытаются восполнить этот пробел, такие как JSON-API, HAL, JSON-LD или похожие. Идея заключается в том, что клиент может войти в API без каких-либо предварительных знаний о ресурсах в нем и узнать о них, просто перейдя по ссылкам hypermedia.

Реализация концепции API Blueprint

Это не будет полный API, я собираюсь реализовать все функции, связанные с пользователями, оставляя реализацию других ресурсов, таких как сообщения в блоге для читателя в качестве упражнения. Чтобы дать вам представление о том, что участвует в разработке API, я собираюсь добавить его в микроблог.

Итак, давайте начнем с создания каталога, в котором будет жить этот проект: Чтобы все было организовано и структурировано в соответствии с концепцией описанной в Главе 15, я собираюсь создать новый проект, который будет содержать все маршруты API.

(venv) $ mkdir app/api

py создает объект blueprint, аналогичный другим blueprint-овым приложениям: Blueprint-овый файл __init __.

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 :


}

Поле password отличается тем, что оно будет использоваться только при регистрации нового пользователя. Многие из полей непосредственно поступают из модели пользовательской базы данных. Поле email также обрабатывается специально, потому что я не хочу раскрывать адреса электронной почты пользователей. Как вы помните из главы 5, пользовательские пароли не хранятся в базе данных, а только хэш, поэтому пароль никогда не возвращается. Поля post_count, follower_count и follow_count являются «виртуальными» полями, которые не существуют в качестве полей в базе данных, но предоставляются клиенту в качестве удобства. Поле электронной почты будет возвращено только тогда, когда пользователи будут запрашивать их собственную запись, но не при получении записей от других пользователей. Это отличный пример, демонстрирующий, что представление ресурса не обязательно должно соответствовать тому, как фактический ресурс определен на сервере.

Определенные ссылки включают ссылки на текущий ресурс, список пользователей, следующих за этим пользователем, список пользователей, за которыми следует пользователь, и, наконец, ссылку на изображение аватара пользователя. Обратите внимание на раздел _links, который реализует требования hypermedia. В будущем, если я решу добавить сообщения в этот API, ссылка на список сообщений пользователя также должна быть здесь включена.

Пакет json из стандартной библиотеки Python заботится о преобразовании структур данных Python в JSON и из него. Одна из приятных особенностей формата JSON заключается в том, что он всегда переводится как представление в виде словаря или списка Python. Поэтому, чтобы сгенерировать эти представления, я собираюсь добавить метод к модели 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

Словарь с пользовательским представлением, на котором я остановился, просто генерируется и возвращается. Этот метод не должен вызывать особых вопросов и быть в основном понятным. Поэтому я использую флаг include_email, чтобы определить, включено ли это поле в представление или нет. Как я уже упоминал выше, поле email нуждается в специальной обработке, потому что я хочу включить электронную почту только тогда, когда пользователи запрашивают свои собственные данные.

Для полей даты и времени я собираюсь использовать Формат ISO 8601, который может генерировать datetime Python с помощью метода isoformat(). Обратите внимание, как генерируется поле last_seen. Но поскольку я использую наивные объекты datetime, которые являются UTC, но не имеют часового пояса, записанного в их состоянии, мне нужно добавить Z в конце, что является кодом часового пояса ISO 8601 для UTC.

Для трех ссылок, которые указывают на другие маршруты приложений, я использую url_for() для генерации URL-адресов (которые в настоящее время указывают на функции просмотра замещающих элементов, определенные в app/api/users.py). Наконец, зацените, как я реализовал hipermedia-ссылки. Для этой ссылки я использую тот же метод avatar(), который я использовал для рендеринга аватаров на веб-страницах. Ссылка аватара особенная, потому что это URL-адрес Gravatar, внешний для приложения.

Мне также нужно позаботиться об обратном направлении, где клиент передает представление пользователя в запросе, а сервер должен проанализировать его и преобразовать в объект User. Метод to_dict() преобразует пользовательский объект в представление Python, которое затем будет преобразовано в JSON. Вот метод 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'])

Для каждого поля я проверяю, есть ли значение в аргументе data, и если есть, я использую setattr() Python, чтобы установить новое значение в соответствующем атрибуте для объекта. В этом случае я решил использовать цикл для импорта любого из полей, которые клиент может установить: username, email и about_me.

Аргумент new_user определяет, является ли это регистрацией нового пользователя, что означает, что пароль включен. Поле password рассматривается как особый случай, поскольку оно не является полем в объекте. Чтобы установить 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 }
}

Раздел _meta включает в себя метаданные коллекции, которые клиент может найти полезными при представлении пользователю элементов управления разбиением на страницы. В этом представлении items -это список пользовательских ресурсов, каждый из которых определен, как описано в предыдущем разделе. В разделе _links определяются соответствующие ссылки, включая ссылку на саму коллекцию, а также ссылки на предыдущую и следующую страницы, чтобы помочь клиенту разбить список на страницы.

Еще в главе 16 я был в аналогичной ситуации с индексами полнотекстового поиска, еще одной функцией, которую я хотел реализовать в общем виде, чтобы ее можно было применить к любым моделям. Создание представления коллекции пользователей сложно из-за логики разбиения на страницы, но логика будет общей для других ресурсов, которые я, возможно, захочу Добавить в этот API в будущем, поэтому я собираюсь реализовать это представление общим способом, который я могу затем применить к другим моделям. Я собираюсь использовать ту же идею для этого, так вот новый класс mixin, который я назвал PaginatedAPIMixin: Решение, которое я использовал, состояло в том, чтобы реализовать класс SearchableMixin, от которого могут наследовать любые модели, которым нужен полнотекстовый индекс.

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)для создания собственной ссылки. Сложная часть заключается в создании ссылок, которые включают в себя ссылку и ссылки на следующую и предыдущие страницы. И поскольку у многих маршрутов есть аргументы, мне также нужно захватить дополнительные аргументы ключевого слова в kwargs и передать их url_for(). Аргументы для url_for() будут зависеть от конкретной коллекции ресурсов, поэтому я буду полагаться на передачу вызывающего в аргументе конечной точки функции представления, которую нужно отправить url_for(). Строка аргумента строки page и per_page указывается явно, поскольку они управляют разбиением на страницы для всех маршрутов API.

Этот класс mixin должен быть добавлен в модель User в качестве родительского класса:

app/models.py: Добавьте PaginatedAPIMixin в модель пользователя.

class User(PaginatedAPIMixin, UserMixin, db.Model): # ...

В случае с коллекциями мне не понадобится обратное направление, потому что у меня не будет маршрутов, требующих от клиента отправки списков пользователей.

Обработка ошибок

Когда API должен возвращать ошибку, она должен быть «дружественного машине» типа ошибки, то, что клиентское приложение сможет легко интерпретировать. Страницы ошибок, которые я определил в главе 7, подходят только для пользователя, который взаимодействует с приложением, используя веб-браузер. Вот основная структура, которую я собираюсь использовать: Точно так же я определил представления для своих ресурсов API в JSON, теперь я собираюсь принять решение о представлении сообщений об ошибках API.

{ "error": "short error description", "message": "error message (optional)"
}

Чтобы помочь мне сгенерировать эти ответы на ошибки, я собираюсь написать функцию error_response() в app/api/errors.py: В дополнение к полезной нагрузке ошибки я буду использовать коды состояния из протокола HTTP для указания общего класса ошибки.

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

Я использую эти имена для поля error в своих представлениях ошибок, поэтому мне нужно беспокоиться только о числовом коде состояния и необязательном длинном описании. Эта функция использует удобный словарь HTTP_STATUS_CODES из Werkzeug (основная зависимость Flask), который предоставляет краткое описательное имя для каждого кода состояния HTTP. Функция 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())

Метод get_or_404() объекта запроса является очень полезным вариантом метода get(), который вы видели ранее, который также возвращает объект с заданным идентификатором, если он существует, но вместо того, чтобы возвращать None, когда id не существует, он прерывает запрос и возвращает ошибку 404 клиенту. Функция view получает идентификатор запрошенного пользователя в качестве динамического аргумента в URL-адресе. Преимущество get_or_404() перед get() заключается в том, что он устраняет необходимость проверять результат запроса, упрощая логику в функциях представления.

Метод to_dict(), который я добавил к User, используется для создания словаря с представлением ресурса для выбранного пользователя, а затем функция Flask jsonify() преобразует этот словарь в формат JSON для возврата клиенту.

Если вы хотите увидеть, как работает этот первый маршрут API, запустите сервер, а затем введите следующий URL-адрес в адресной строке браузера:

http://localhost:5000/api/users/1

Также попробуйте использовать большое значение id, чтобы увидеть, как метод get_or_404() объекта запроса SQLAlchemy вызывает ошибку 404 (я позже покажу вам, как расширить обработку ошибок, чтобы эти ошибки также возвращались в формате JSON). Результат должен показать вам первого пользователя, отображенного в формате 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)

per_page имеет дополнительную логику, которая ограничивает его 100. Для этой реализации я сначала извлекаю page и per_page из строки запроса, используя значения по умолчанию 1 и 10 соответственно, если они не определены. Аргументы 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)

Идентификатор используется для получения пользователя из базы данных, а затем для предоставления user.followers и user.followed отношения на основе запросов к to_collection_dict(), так что, надеюсь, теперь вы можете увидеть, почему затраты дополнительного времени и проектирование этого метода в общем виде действительно окупается. Поскольку эти два маршрута специфичны для пользователя, у них есть динамический аргумент id. Последние два аргумента 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

Flask предоставляет метод request.get_json(), чтобы извлечь JSON из запроса и вернуть его в виде структуры Python. Этот запрос будет принимать представление пользователя в формате JSON от клиента, предоставленного в теле запроса. Этот метод возвращает None, если данные JSON не найдены в запросе, поэтому я могу гарантировать, что я всегда получаю словарь, используя запрос request.get_json() или {}.

Это username, email и password. Прежде чем я смогу использовать данные, мне нужно убедиться, что у меня есть вся информация, поэтому я начинаю с проверки того, что включены три обязательных поля. В дополнение к этой проверке мне нужно убедиться, что поля username и email еще не используются другим пользователем, поэтому для этого я пытаюсь загрузить пользователя из базы данных по имени пользователя и электронной почте, и если какой-либо из них возвращает действительного пользователя, я также возвращаю ошибку обратно клиенту. Если какой-либо из них отсутствует, то я использую вспомогательную функцию bad_request() из app/api/errors.py для возврата клиенту ошибки.

Для создания пользователя я использую метод from_dict() в пользовательской модели. После того, как я прошел проверку данных, я могу легко создать объект пользователя и добавить его в базу данных. Аргумент new_user имеет значение True, поэтому он также принимает поле password, которое обычно не является частью представления пользователя.

Код состояния для запроса POST, который создает ресурс, должен быть 201, который используется, когда новый объект был успешно создан. Ответ, который я верну для этого запроса, будет представлением нового пользователя, поэтому to_dict() генерирует эту полезную нагрузку. Кроме того, для протокола 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())

Как и в случае с новым пользователем, мне нужно проверить, что поля username и email, предоставленные клиентом, не сталкиваются с другими пользователями, прежде чем я смогу их использовать, но в этом случае проверка немного сложнее. Для этого запроса я получаю id пользователя как динамическую часть URL, поэтому я могу загрузить назначенного пользователя и вернуть ошибку 404, если она не найдена. Второе осложнение заключается в том, что клиент может предоставлять одно и то же значение, поэтому, прежде чем я проверю, берется ли имя пользователя или электронная почта, мне нужно убедиться, что они отличаются от текущих. Прежде всего, эти поля являются необязательными в этом запросе, поэтому мне нужно проверить, что поле присутствует. Если какая-либо из этих проверок завершится ошибкой, я верну клиенту ошибку 400, как и раньше.

Ответ на этот запрос возвращает пользователю обновленное представление пользователя с кодом состояния по умолчанию 200. После проверки данных я могу использовать метод From_dict() пользовательской модели для импорта всех данных, предоставленных клиентом, а затем зафиксировать изменение в базе данных.

Вот пример запроса, который редактирует поле about_me с HTTPie:

(venv) $ http PUT http://localhost:5000/api/users/2 "about_me=Hi, I am Miguel"

API Аутентификация

Очевидно, что они должны быть доступны только зарегистрированным пользователям, и для этого мне нужно добавить аутентификацию и авторизацию, или «AuthN» и «AuthZ» для краткости. Конечные точки API, которые я добавил в предыдущем разделе, в настоящее время открыты для любых клиентов. Идея состоит в том, что запросы, отправленные клиентами, предоставляют некоторую идентификацию, так что сервер знает, какого пользователя представляет клиент, и может проверить, разрешено или нет запрошенное действие для этого пользователя.

Когда декоратор обнаруживает не аутентифицированного пользователя, он перенаправляет пользователя на HTML страницу входа в систему. Наиболее очевидным способом защиты этих конечных точек API является использование декоратора @login_required из Flask-Login, но у этого подхода есть некоторые проблемы. Сервер не может предположить, что клиент API является веб-браузером или он может обрабатывать переадресации или что он может отображать и обрабатывать формы входа в систему HTML. В API нет концепции страниц HTML или входа в систему, если клиент отправляет запрос с недопустимыми или отсутствующими учетными данными, сервер должен отказаться от запроса, возвращающего код состояния 401. Когда 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_expiration, в котором есть дата и время истечения срока действия токена. С этим изменением я добавляю атрибут token в пользовательскую модель, и поскольку мне нужно будет искать базу данных по ней, я делаю ее уникальной и индексированной. Это делается для того, чтобы маркер не оставался действительным в течение длительного периода времени, что может стать угрозой безопасности.

Метод get_token() возвращает токен для пользователя. Я создал три метода работы с этими токенами. Перед созданием нового токена этот метод проверяет, есть ли у назначенного токена по крайней мере минута до истечения срока действия, и в этом случае возвращается существующий токен. Токен генерируется как случайная строка, закодированная в base64, так что все символы находятся в читаемом диапазоне.

Это лучшая практика в области безопасности, которую часто упускают из виду. При работе с токенами всегда полезно иметь стратегию немедленного отзыва токена, а не полагаться только на дату истечения срока действия. Метод revoke_token() делает маркер, назначенный пользователю, недействительным, просто установив дату истечения срока действия на одну секунду до текущего времени.

Если токен недействителен или истек, метод возвращает None. Метод check_token() является статическим методом, который принимает токен в качестве входных данных и возвращает пользователя, которому этот токен принадлежит в качестве ответа.

Поскольку я внес изменения в базу данных, мне нужно создать новую миграцию базы данных, а затем обновить ее:

(venv) $ flask db migrate -m "user tokens"
(venv) $ flask db upgrade

Запросы Маркеров(токенов)

Реальная сила API приходит, когда автономные клиенты, такие как приложения для смартфонов или даже браузерные одностраничные приложения могут иметь доступ к серверным службам. Когда вы пишете API, вы должны учитывать, что ваши клиенты не всегда будут веб-браузерами, подключенными к веб-приложению. Когда эти специализированные клиенты нуждаются в доступе к службам API, они начинают с запроса маркера, который является аналогом формы входа в традиционное веб-приложение.

Flask-HTTPAuth устанавливается с pip: Чтобы упростить взаимодействие между клиентом и сервером при использовании аутентификации токенов, я собираюсь использовать расширение Flask под названием Flask-HTTPAuth.

(venv) $ pip install flask-httpauth

Для начала я собираюсь использовать HTTP Basic Authentication или тут 11. Flask-HTTPAuth поддерживает несколько различных механизмов аутентификации, все API дружественные. Для интеграции с Flask-HTTPAuth приложение должно предоставить две функции: одну, которая определяет логику для проверки имени пользователя и пароля, предоставленных пользователем, и другую, которая возвращает ответ об ошибке в случае сбоя аутентификации. 1, в которой клиент отправляет учетные данные пользователя в стандартном заголовке http авторизации. Вы можете увидеть реализацию: Эти функции регистрируются в 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)

Две необходимые функции настраиваются с помощью декораторов verify_password и error_handler соответственно. Класс HTTPBasicAuth из Flask-HTTPAuth-это класс, реализующий основной поток проверки подлинности.

Для проверки пароля я полагаюсь на метод check_password() класса User, который также используется Flask-Login при аутентификации для веб-приложения. Функция проверки получает имя пользователя и пароль, предоставленные клиентом, и возвращает True, если учетные данные действительны, или False, если нет. Я сохраняю аутентифицированного пользователя в g.current_user, так что я могу получить доступ к нему из функций представления API.

Ошибка 401 определяется в стандарте HTTP как "Unauthorized" ошибка ("несанкционированного доступа"). Функция обработчика ошибок просто возвращает ошибку 401, сгенерированную функцией error_response() в app/api/errors.py. Клиенты 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})

Реализация этой функции представления зависит от метода get_token() пользовательской модели для создания маркера. Эта функция представления обернута декоратором @basic_auth.login_required из экземпляра HTTPBasicAuth, который будет инструктировать Flask-HTTPAuth для проверки подлинности (через функцию проверки которую я определил выше) и разрешать функцию для запуска только, когда предоставленные учетные данные являются действительными. Фиксация базы данных выполняется после создания маркера, чтобы гарантировать, что токен и его срок действия будут записаны обратно в базу данных.

При попытке отправить запрос 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"
}

Обратите внимание, что при отправке этого запроса необходимо заменить <username>:<password> своими учетными данными. Теперь код состояния 200, который является кодом для успешного запроса, и полезные данные включают недавно созданный маркер для пользователя. Имя пользователя и пароль должны быть снабжены двоеточием в качестве разделителя.

Защита маршрутов API с помощью токенов

Это то, что Flask-HTTPAuth также может обрабатывать для меня. Теперь клиенты могут запрашивать токен для использования с конечными точками API, поэтому осталось добавить проверку токена на эти конечные точки. Мне нужно создать второй экземпляр проверки подлинности на основе класса 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)

Функция проверки токена использует User.check_token(), чтобы найти пользователя, которому принадлежит предоставленный токен. При использовании аутентификации по токенам Flask-HTTPAuth использует функцию verify_token, но кроме этого аутентификация токена работает так же, как и базовая аутентификация. Возвращаемое значение True или False определяет, может ли Flask-HTTPAuth разрешить выполнение функции просмотра или нет. Функция также обрабатывает случай отсутствующего токена, установив текущего пользователя в None.

Чтобы защитить маршруты 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(), который не может принять аутентификацию, так как пользователь, который запросит маркер, должен быть создан первым.

Чтобы получить доступ, вам нужно добавить заголовок Authorization с маркером, который вы получили от запроса в /api/tokens. Если вы отправляете запрос на любую из этих конечных точек, как показано ранее, вы получите ответ об ошибке 401. Для базовой аутентификации с именем пользователя и паролем HTTPie предлагает параметр --auth, но для токенов заголовок должен быть явно указан. Flask-HTTPAuth ожидает, что токен будет отправлен как токен-носитель, который напрямую не поддерживается HTTPie. Вот синтаксис для отправки токена-носителя:

(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

Аутентификация для этого маршрута основана на маркере, фактически токен, передаваемый в заголовке Authorization, является тем, который отзывается. Клиенты могут отправить запрос DELETE на удаление по URL-адресу /tokens, чтобы аннулировать маркер. Сеанс базы данных фиксируется таким образом, что это изменение записывается в базу данных. Само аннулирование использует вспомогательный метод в классе User, который сбрасывает дату истечения срока действия маркера. Второе значение в инструкции return задает код состояния ответа 204, который используется для успешных запросов, не имеющих тела ответа. Ответ от этого запроса не имеет тела, поэтому я могу вернуть пустую строку.

Вот пример запроса на отзыв токена, отправленного из HTTPie:

(venv) $ http DELETE http://localhost:5000/api/tokens \ Authorization:"Bearer pC1Nu9wwyNt8VCj1trWilFdFI276AcbS"

Сообщения об ошибках API

Сервер вернул ошибку 404, но эта ошибка была отформатирована как стандартная страница ошибки 404 HTML. Вы помните, что произошло в начале этой главы, когда я попросил вас отправить запрос API из браузера с недействительным URL-адресом пользователя? Многие ошибки, которые может потребоваться вернуть API, могут быть переопределены версиями JSON в схеме элементов API, но есть некоторые ошибки, обрабатываемые Flask, которые по-прежнему проходят через обработчики ошибок, глобально зарегистрированные для приложения, и они продолжают возвращать HTML.

Клиенту необходимо отправить Заголовок Accept с запросом, указав предпочтения формата. Протокол HTTP поддерживает механизм, с помощью которого клиент и сервер могут согласовать оптимальный Формат ответа, называемый согласованием содержимого. Затем сервер смотрит на список и отвечает, используя лучший формат, который он поддерживает из списка, предлагаемого клиентом.

Это можно сделать с помощью объекта Flask request.accept_mimetypes : Я хочу изменить глобальные обработчики ошибок приложений, чтобы они использовали согласование содержимого для ответа в HTML или JSON в соответствии с предпочтениями клиента.

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

Если скорость JSON выше, чем HTML, то я возвращаю ответ JSON. Вспомогательная функция wants_json_response() сравнивает предпочтения для JSON или HTML, выбранные клиентом в списке предпочтительных форматов. Для ответов JSON я собираюсь импортировать вспомогательную функцию error_response из схемы элементов API, но здесь я собираюсь переименовать ее в api_error_response(), чтобы было ясно, что она делает и откуда. В противном случае я верну исходные HTML-ответы на основе шаблонов.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть