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

(издание 2018)


Miguel Grinberg


jljnbbjr-ejh473xy_eccsmknpk.pngТуда Сюда rwdy-grsvbpcetjttrmecdkxtlk.png

Это двадцать третья часть Мега-Учебника, в которой я расскажу вам, как расширить микроблог с помощью интерфейса прикладного программирования (или 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/, где  — это идентификатор, назначенный пользователю в качестве первичного ключа таблицы базы данных. Это вполне приемлемо реализовано большинством 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/ Возвращает пользователя.
GET /api/users Возвращает коллекцию всех пользователей.
GET /api/users//followers Вернет подписчиков этого пользователя.
GET /api/users//followed Вернет пользователей, на которых подписан этот пользователь.
POST /api/users Регистрирует новую учетную запись пользователя.
PUT /api/users/ Изменяет пользователя.

Каркас модуля с заполнителями для всех этих маршрутов будет такой:


app/api/users.py: Заполнители ресурсов API пользователя.

from app.api import bp

@bp.route('/users/', methods=['GET'])
def get_user(id):
    pass

@bp.route('/users', methods=['GET'])
def get_users():
    pass

@bp.route('/users//followers', methods=['GET'])
def get_followers(id):
    pass

@bp.route('/users//followed', methods=['GET'])
def get_followed(id):
    pass

@bp.route('/users', methods=['POST'])
def create_user():
    pass

@bp.route('/users/', 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/', 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//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//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.
    
            

© Habrahabr.ru