[Перевод] 1. Python REST APIs With Flask, Connexion, and SQLAlchemy

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

Цель этого цикла - показать вам, как использовать набор инструментов Python 3, Flask и Connexion для создания крутых REST API, где нужно валидировать входные и выходные данные, и в качестве бонуса оформим документацию Swagger. Фронтенд часть включит в себя одностраничное веб-приложение, основанное на javaScript, HTML/CSS.

Что такое REST

Перед тем, как приступить к созданию бэкенда API, нужно немного поговорить о REST. Есть много информации о REST доступной в Google, и я не собираюсь дублировать ее здесь.

Я просто хочу рассказать о том, как я его понимаю и использую его для создания API. Я вижу REST как набор соглашений, использующих протокол HTTP для создания CRUD (Create, Read, Update, and Delete). Поведение CRUD хорошо соотносится с глаголами протокола HTTP:

CRUD

Действие Тип запроса Описание
Create POST Создание сущностей
Read GET Выборка по сущности
Update PUT Обновление информации о сущности
Delete DELETE Удаление сущности

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

Каждый URL должен отвечать за уникальное действие, которое может повторяться снова и снова.

REST не является

REST вроде и понятен, но есть случаи когда его не правильно применяют. Примером может быть выполнение подстановки строк, что, конечно, глупо для API, но давайте пока разберемся с этим. Вот URL, который может быть создан для этого:

/api/substituteString/<string>/<search_string>/<sub_string>

Здесь string- это строка для замены, search_string - это строка для замены, а sub_string - это строка для замены строки search_string. Это, безусловно, может быть связано с каким-то кодом на сервере, который делает предполагаемую работу.

Одна из проблем, которая приходит на ум, заключается в том, что этот URL не указывает на уникальный ресурс, поэтому то, что он возвращает, полностью зависит от состава переменной адреса. Кроме того, CRUD не подходит для этого URL. Этому урлу действительно нужен только один HTTP-метод, а имя метода ничего не говорит о том, что нужно сделать.

Как API, это тоже не очень хорошо. Значение переменных пути зависит от их положения в URL. Это можно исправить, изменив URL на использование строки запроса:

/api/substituteString?string=<string>&search_string=<search_string>&sub_string=<sub_string>

Но URL /api/substituteString вообще не сущность, это действие (глагол).

Это не очень хорошо вписывается в REST правила, и попытка заставить выполнить этот запрос, является признаком плохого API. То, что на самом деле представляет собой вышеперечисленное действие, это RPC, или Remote Procedure Call (Вызов удаленной процедуры).

REST для упраженения

Для нашей примерной программы, вы собираетесь создать REST API, предоставляющее доступ к коллекции людей, с выполнением CRUD для доступа к отдельному человеку из этой коллекции. Вот архитектура API:

Архитектура

Action HTTP Verb URL Path Description
Create POST /api/people Создание нового человека
Read GET /api/people Получение всех людей
Read GET /api/people/Farrell Получение конкретного человека
Update PUT /api/people/Farrell Изменение информации по конкретному человеку
Delete DELETE /api/orders/Farrell Удаление конкретного человека
Untitled      

Приступим к практике

Сначала создадим простой веб-сервер, используя Flask Micro Framework. Для начала создаем директорию проекта, в которой можно писать код. Вы также должны работать в virtualenv, чтобы в дальнейшем можно было устанавливать модули и библиотеки, Кроме того, создайте директорию с шаблонами.

Код на Python, приведенный ниже, позволяет запустить очень простой веб-сервер и ответить Hello World на запрос по умолчанию:

from flask import (
    Flask,
    render_template
)

# Create the application instance
app = Flask(__name__, template_folder="templates")

# Create a URL route in our application for "/"
@app.route('/')
def home():
    """
    This function just responds to the browser ULR
    localhost:5000/

    :return:        the rendered template 'home.html'
    """
    return render_template('home.html')

# If we're running in stand alone mode, run the application
if __name__ == '__main__':
    app.run(debug=True)

Создадим файл home.html в папке templates, этот файл будет отдаваться браузеру при переходе в браузере по адресу проекта '/'. Вот содержание файла:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Application Home Page</title>
</head>
<body>
    <h2>
        Hello World!
    </h2>
</body>
</html>

 

Вы заметили, что HTML-файл имеет имя home.html а не index.html. Это сделано специально, потому что наличие index.html файл в каталоге шаблонов вызывает проблемы после импорта модуля Connexion в вашу программу.

Вы можете запустить свое приложение с помощью командной строки в папке программы, содержащем server.py файл с активным Python VirtualEnv:

python server.py

При запуске приложения веб-сервер запускается на порту 5000, который по умолчанию используется Flask. Если вы откроете браузер и перейдете к localhost:5000, то увидите надпись Hello World!. Это сделано для того, чтобы увидеть, что веб-сервер работает.

В коде программе вы импортировали модуль Flask, предоставляя приложению доступ к функциям Flask. Затем вы создали объект приложения Flask, переменную app. Далее, вы подключили маршрута URL-адрес '/' для roue() функции, украшая его с @app.roue('/'). Эта функция вызывает функцию в Flask render_template (), чтобы получить home.html, файл из каталога шаблонов и передаст его в браузер.

Весь пример кода доступен по ссылке, приведенной в конце этой статьи.

Использование Connexion для создания точки REST API

Теперь, когда у вас есть работающий веб-сервер, давайте создадим точку REST API. Для этого вы используете модуль Connexion, который устанавливается с помощью пакетного менеджера pip:

$ pip install connexion

Команда устанавливает модуль Connexion в вашу программу. Модуль Connexion позволяет программе Python использовать спецификацию Swagger. Это обеспечивает множество функциональных возможностей: проверка входных и выходных данных к вашему API и из него, простой способ настройки конечных точек API и ожидаемых параметров, а также дает удобный интерфейс для работы с созданным API и их изучением.

Все это будет, когда вы создадите конфигурационный файл, к которому ваше приложение получит доступ. Swagger даже предоставляет онлайн-редактор конфигураций, который поможет вам создать и/или проверить синтаксис создаваемого вами конфигурационного файла.

Добавление Connexion на сервер

from flask import render_template
import connexion

# Create the application instance
app = connexion.App(__name__, specification_dir='./')

# Read the swagger.yml file to configure the endpoints
app.add_api('swagger.yml')

# Create a URL route in our application for "/"
@app.route('/')
def home():
    """
    This function just responds to the browser ULR
    localhost:5000/
    :return:        the rendered template 'home.html'
    """
    return render_template('home.html')

# If we're running in stand alone mode, run the application
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

Чтобы включить Connexion в сервер нужно сделать несколько действий. Команда import connexion добавляет библиотеку в программу. Следующий шаг - создание экземпляра приложения с использованием Connexion, а не Flask. Внутри приложение Flask инициализировано, но теперь к нему добавлена дополнительная функциональность.

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

app.add_api('swagger.yml')

Это говорит экземпляру приложения прочитать файл swagger.yml из каталога спецификаций и настроить систему для понимания функциональности подключения.

Конфигурационный Файл Swagger

Файл swagger.yml - это YAML или JSON файл, содержащий всю информацию, необходимую для настройки вашего сервера для обеспечения проверки входных параметров, проверки выходных данных, определения конечной точки и пользовательского интерфейса Swagger UI. Вот файл swagger.yml, определяющий конечную точку GET /api/people, которую предоставит ваш REST API:

swagger: "2.0"
info:
  description: This is the swagger file that goes with our server code
  version: "1.0.0"
  title: Swagger REST Article
consumes:
  - "application/json"
produces:
  - "application/json"

basePath: "/api"

# Paths supported by the server application
paths:
  /people:
    get:
      operationId: "people.read"
      tags:
        - "People"
      summary: "The people data structure supported by the server application"
      description: "Read the list of people"
      responses:
        200:
          description: "Successful read people list operation"
          schema:
            type: "array"
            items:
              properties:
                fname:
                  type: "string"
                lname:
                  type: "string"
                timestamp:
                  type: "string"

Этот файл организован в иерархическом порядке: уровни отступов представляют собой уровень вложенности/наследования.

Например, basePath определяет префикс, где определены все конечные точки API. Значение /people с отступом, определяющим начало, где будут определены все конечные точки URL /api/people. Значение get: определяет GET-запрос к конечной точке URL /api/people. .

Вот значения полей в этом разделе файла swagger.yml:

Этот раздел описывает глобальную конфигурацию:

  1. swagger: сообщает Connexion, какая версия API Swagger используется. info: пользовательское описание того, что предоставляет или о чем говорит API. version: заданное пользователем значение версии API
  1. title: заголовок, определяемый пользователем, включенный в систему пользовательского интерфейса, генерируемую Connexion. consumes: сообщает Connexion, какой MIME тип ожидается для API.
  1. produces: сообщает Connexion, какой тип контента получит вызывающий API.
  1. basePath: "/api" определяет корень API, что-то вроде пространства имён, по которому фронт будет отсылать запросы для доступы к АПИ.

В этом разделе начинается настройка путей к конечным точкам API URL:

  1. paths: определяет раздел конфигурации, содержащий все конечные точки API REST. /people: определяет путь к конечной точке URL.
  1. get: определяет HTTP-метод, на который будет реагировать точка URL. Вместе с предыдущими определениями, это создает запрос GET /api/people.

В этом разделе начинается настройка конечной точки /api/people:

  1. operationId: "people.read" определяет путь Python файла, которая будет отвечать на запрос HTTP GET /api/people. Раздел "people.read" может быть настолько глубоким по вложенности, насколько это необходимо для подключения функции к HTTP-запросу. Что-то вроде "<имя_пакета>.<имя_пакета>.<имя_пакета>.<имя_функции>" будет работать также хорошо, как и без этого
  1. tags: определяет группировку для пользовательского интерфейса. Все методы CRUD, которые Вы определите для конечной точки people, будут объединены тегом.
  1. summary: краткое описание пользовательского интерфейса для этой точки.
  1. description: определяет, что интерфейс пользовательского интерфейса будет отображать для примечаний к реализации.

Обработчики для функций API

В файле swagger.yml вы настроили Connexion со значением operationId на вызов модуля people и функцию чтения внутри модуля, когда API получает HTTP-запрос на GET /api/people. Это означает, что модуль people.py должен существовать и содержать функцию read(). Вот модуль people.py, который вы создали:

В нашем сваггер файле мы настроили все значения. в частности operationId для идентификации сущности people и операции с этой сущностью. Ниже код файла people.py

from datetime import datetime

def get_timestamp():
    return datetime.now().strftime(("%Y-%m-%d %H:%M:%S"))

# Data to serve with our API
PEOPLE = {
    "Farrell": {
        "fname": "Doug",
        "lname": "Farrell",
        "timestamp": get_timestamp()
    },
    "Brockman": {
        "fname": "Kent",
        "lname": "Brockman",
        "timestamp": get_timestamp()
    },
    "Easter": {
        "fname": "Bunny",
        "lname": "Easter",
        "timestamp": get_timestamp()
    }
}

# Create a handler for our read (GET) people
def read():
    """
    This function responds to a request for /api/people
    with the complete lists of people

    :return:        sorted list of people
    """
    # Create the list of people from our data
    return [PEOPLE[key] for key in sorted(PEOPLE.keys())]

 

В этом коде есть вспомогательная функция, называемая get_timestamp(), которая генерирует строку с текущей меткой времени. Строка используется для создания структуры внутри памяти и модификации данных.

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

Затем создали функцию read(). Она вызывается при получении сервером HTTP-запроса на GET /api/people. Возвращаемое значение этой функции преобразуется в JSON-строку. Созданная функция read() собирает и возвращает список людей, отсортированный по фамилии.

Запустив код вашего сервера и перейдя в браузере на localhost:5000/api/people, вы увидите список людей на экране:

[
  {
    "fname": "Kent",
    "lname": "Brockman",
    "timestamp": "2018-05-10 18:12:42"
  },
  {
    "fname": "Bunny",
    "lname": "Easter",
    "timestamp": "2018-05-10 18:12:42"
  },
  {
    "fname": "Doug",
    "lname": "Farrell",
    "timestamp": "2018-05-10 18:12:42"
  }
]

Поздравляю, вы создали отличную точку API и уже на пути к созданию всего интерфейса!

Swagger UI

Теперь у вас есть простой веб-интерфейс API, работающий с одной точкой URL. В этот момент было бы разумно подумать: "Настроить swagger.yml было круто, но какая от этого польза?".

Вы были бы правы, если бы подумали так. Мы не воспользовались проверкой входных и выходных данных. Всё, что swagger.yml дал нам, это определение пути запроса, связанного с конечной точкой URL. Однако, что вы также получите за дополнительную работу, так это создание пользовательского интерфейса Swagger для вашего API.

Если вы перейдете на localhost:5000/api/ui, система выведет на экран страницу, которая выглядит примерно так:

Это стартовый интерфейс Swagger показывает список всех точек URL, поддерживаемых на нашем localhost:5000/api. Он автоматически собирается Connexion при разборе файла swagger.yml.

Если вы нажмёте на точку /people в интерфейсе, то интерфейс развернётся, чтобы показать намного больше информации о вашем API и будет выглядеть примерно так:

Здесь отображается структура ожидаемого ответа, тип содержимого этого ответа, а также текст описания, который вы ввели о точке в файле swagger.yml.

Вы даже можете опробовать точку, нажав на кнопку Try It Out! ("Попробуй!") в нижней части экрана. Это еще больше расширит интерфейс

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

Создание документации API очень полезно на работе. Интерфейс Swagger UI полезен не только как способ экспериментировать с API и читать предоставленную документацию, но и как динамический код. Всякий раз, когда меняется конфигурационный файл, изменяется и интерфейс Swagger UI. Разделяя код и конфигурацию API точек, мы отделяем их логически.

Полное API

Нашей первоначальной целью было создание API, обеспечивающего полный доступ CRUD к структуре наших сотрудников. Как вы помните, определение нашего API опредилили ранее:

Для этого мы улучшили файлы swagger.yml и people.py, чтобы они полностью поддерживали API, описанное выше.

https://github.com/realpython/materials/blob/master/flask-connexion-rest/version_3/swagger.yml

https://github.com/realpython/materials/blob/master/flask-connexion-rest/version_3/people.py

Полный Swagger

Демо фронтендной части приложения

У вас есть рабочий REST API с отличной системой документирования/взаимодействия с пользовательским интерфейсом Swagger. Но что вы теперь с ней делаете? Следующим шагом будет создание веб-приложения, демонстрирующего использование API практическим способом.

Вы создадите веб-приложение, которое будет отображать людей на экране, а также позволит пользователю создавать новых людей, обновлять существующих и удалять людей. Все это будет обработано AJAX запросами с JavaScript на точки API.

Для начала необходимо улучшить файл home.html, чтобы он выглядел так:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Application Home Page</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.0/normalize.min.css">
    <link rel="stylesheet" href="static/css/home.css">
    <script
      src="http://code.jquery.com/jquery-3.3.1.min.js"
      integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
      crossorigin="anonymous">
    </script>
</head>
<body>
    <div class="container">
        <h1 class="banner">People Demo Application</h1>
        <div class="section editor">
            <label for="fname">First Name
                <input id="fname" type="text" />
            </label>
            <br />
            <label for="lname">Last Name
                <input id="lname" type="text" />
            </label>
            <br />
            <button id="create">Create</button>
            <button id="update">Update</button>
            <button id="delete">Delete</button>
            <button id="reset">Reset</button>
        </div>
        <div class="people">
            <table>
                <caption>People</caption>
                <thead>
                    <tr>
                        <th>First Name</th>
                        <th>Last Name</th>
                        <th>Update Time</th>
                    </tr>
                </thead>
                <tbody>
                </tbody>
            </table>
        </div>
        <div class="error">
        </div>
    </div>
</body>
<script src="static/js/home.js"></script>
</html>

Приведенный выше HTML-код расширяет файл home.html. Файл normalize.min.css, является файлом нормализации форматирования элементов в браузерах(приводит к одному стандарту и виду).

Файл также добавляет внешний файл jquery-3.3.1.min.js для обеспечения функциональности jQuery, которую вы будете использовать для создания интерактивности одностраничных приложений.

Приведенный выше HTML-код создает статическое приложение. Динамические части, появляющиеся в структуре таблицы, будут добавляться JavaScript во время загрузки и по мере взаимодействия пользователя с приложением.

Статичные файлы

В файле home.html, который вы создали, есть ссылки на два статических файла: static/css/home.css и static/js/home.js. Чтобы добавить их, необходимо добавить следующую структуру каталогов в приложение:

static/
│
├── css/
│   └── home.css
│
└── js/
    └── home.js

https://github.com/realpython/materials/blob/master/flask-connexion-rest/version_4/static/css/home.css

https://github.com/realpython/materials/blob/master/flask-connexion-rest/version_4/static/js/home.js

JavaScript файлы

Как уже говорилось ранее, файлы JavaScript обеспечивают все взаимодействие с веб-приложением и его обновление. Для этого он разбивает необходимую функциональность на три части с помощью шаблона проектирования MVC (Model / View / Controller).

Каждый объект создается функцией обработчика, возвращающей свой API для использования другими частями. Например, контроллеру передаются экземпляры Model и View в качестве параметров в его исполнение. Он взаимодействует с этими объектами через их методы API.

Другое соединение - между Model и Controller- осуществляется посредством пользовательских событий на вызовах AJAX-методов:

Модель обеспечивает связь с API. Собственный API - это то, что Контроллер вызывает для взаимодействия с сервером, когда этого требует взаимодействия с пользователем. View предоставляет механизм обновления веб-приложения DOM. Controller предоставляет все обработчики событий для взаимодействия пользователя с веб-приложением. Это позволяет ему обращаться к Model с запросами к пользовательскому API, а View - обновлять БД с помощью новой информации, полученной от пользовательского API. View также обрабатывает пользовательские события, генерируемые асинхронными AJAX-запросами, сделанными Model к people API.

Вот диаграмма структуры MVC, используемой в файле home.js:

Идея заключается в том, что Controller имеет сильную связь как с Model так и с View . Model имеет слабую связь (пользовательские события) с Controller и вообще не связана с видом.

Демо приложения

 

Кнопка Create позволяет пользователю создать нового человека на сервере. Когда вы вводите Имя и Фамилию и нажимаете кнопку Create , контроллер вызывает Модель, чтобы сделать запрос на POST /api/people URL точки. Это позволит проверить, что фамилия еще не существует. Если его нет, он создаст нового человека в структуре людей.

Это событие создаст пользовательское событие в Модели, которое заставит Контроллер снова вызвать Модель, чтобы запросить GET /api/people, который вернет полный список людей, отсортированных. Затем контроллер передает это во View, чтобы перерисовать таблицу людей.

Двойной щелчок по любой строке таблицы заполнит поля First и Last Name в разделе редактора приложения. В этот момент пользователь может либо обновить, либо удалить человека.

Для успешного обновления пользователь должен что-то изменить в поле Имя. Фамилия должна оставаться такой же, как и ключ поиска для обновления. При нажатии кнопки Update (Обновить) контроллер вызывает модель, чтобы сделать запрос к конечной точке URL PUT /api/people/{lname}. Это позволит проверить, что Фамилия в настоящее время существует. Если это так, то будет произведено обновление этого человека в структуре людей.

Это событие создаст в модели пользовательское событие, которое заставит контроллер снова вызвать модель для запроса GET /api/people, что вернет полный список отсортированных людей. Затем контроллер передает это во View, чтобы перерисовать таблицу людей.

Для успешного удаления пользователю достаточно нажать кнопку Delete (Удалить). При нажатии кнопки Delete (Удалить) контроллер вызывает Модель, чтобы сделать запрос на конечную точку URL-адреса DELETE /api/people/{lname}. Это позволит проверить, что фамилия в настоящее время существует. Если это так, то он удалит этого человека из структуры людей.

Это создаст в Модели пользовательское событие, которое заставит контроллер снова вызвать Модель для запроса GET /api/people, что вернет полный список людей, отсортированных. Затем контроллер передает это во View, чтобы перерисовать таблицу людей.

Попробуйте сделать преднамеренные ошибки в редакторе, например, неправильно написать фамилию, и посмотрите ошибки, генерируемые API в веб-приложении.

Примеры кода

https://github.com/realpython/materials/tree/master/flask-connexion-rest

 

Весь код примера для этой статьи доступен по ссылке. Существует четыре версии кода, каждая из которых находится в директории version_#, где # находится в диапазоне от 1 до 4. Таким образом, четыре версии соответствуют разделам статьи:

version_1: Эта версия содержит исходный веб-сервер, который обслуживает файл home.html. version_2: Эта версия содержит веб-сервер с добавленным Connexion и конечной точкой API. version_3: Эта версия содержит завершенный API людей со всеми поддерживаемыми конечными точками URL. version_4: Эта версия содержит готовый API и одностраничное веб-приложение для его демонстрации.

Заключение

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

Оригинал: https://realpython.com/flask-connexion-rest-api/