Как перейти от Flask к FastAPI

Из этой статьи вы узнаете о фундаментальных концепциях FastAPI и научитесь переносить сервер с Flask целиком на FastAPI.

FastAPI представляет собой современный, быстрый (высокопроизводительный) веб-фреймворк для построения API с Python 3.6+ на основе стандартных подсказок типов Python.

Официальная документация описывает следующие ключевые особенности FastAPI (оценка основана на тестах внутренней команды разработчиков, ответственных за продуктовую сборку):

  • Быстрота: очень высокая производительность, наравне с NodeJS и Go.
  • Быстрый код: увеличивает скорость разработки функций примерно на 200–300%.
  • Меньше ошибок: примерно на 40% уменьшает число ошибок, вызванных человеком (разработчиком).
  • Интуитивная понятность: отличная поддержка редактора. Не требует много времени на отладку.
  • Легкость: легок в изучении и прост в использовании. На чтение документации не потребуется много времени.
  • Краткость: сводит к минимуму дублирование кода. Несколько объектов из каждого объявления параметра. Меньше багов.
  • Надежность: выдает рабочий код с автоматической интерактивной документацией.
  • Основан на стандартах: основан на открытых стандартах API (и полностью с ними совместим), таких как OpenAPI (ранее известен как Swagger) и JSON Schema.

Flask  —  это микро-веб-фреймворк, полностью основанный на WSGI (Web Server Gateway Interface). ASGI (Asynchronous Server Gateway Interface), в свою очередь, является преемником WSGI, поскольку он способен достичь высокой пропускной способности в контекстах, связанных с вводом-выводом, и предоставляет поддержку HTTP/2 и WebSockets, которые не могут быть обработаны WSGI.

С развитием технологий появился молниеносный ASGI-сервер под названием Uvicorn. Однако Uvicorn  —  всего лишь веб-сервер без каких-либо возможностей маршрутизации. Затем появился Starlette, который предоставляет полный набор инструментов ASGI поверх ASGI-сервера, такого как Uvicorn, Daphne или Hypercorn. Таким образом, Starlette  — это веб-фреймворк ASGI, а Flask  — веб-фреймворк WSGI.

Фреймворк FastAPI в полной мере использует функциональные возможности Starlette и стиль программирования Flask, становясь тем самым ASGI-веб-фреймворком “а-ля Flask”. Кроме того, он включает в себя следующие функции, которые делают его идеальным веб-фреймворком для создания RESTful API:

  • Проверка данных: использует библиотеку Pydantic, которая применяет подсказки по типам во время выполнения. Он также предоставляет понятные для пользователя описания ошибок в случаях, если данные недействительны.
  • Документация: есть поддержка автоматического документирования моделей данных с помощью JSON-схемы. Поставляется либо со UI Swagger, либо с ReDoc.

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

Перейдем к следующему разделу и приступим к установке необходимых модулей.

1. Настройка

Прежде, чем продолжать установку, настоятельно рекомендуется создать виртуальную среду. Если вы собираетесь протестировать FastAPI, вам нужно установить только FastAPI и Uvicorn. Остальные пакеты необязательны и требуются только в том случае, если вы намерены следовать этому руководству до конца.

FastAPI

Установка происходит через pip install.

pip install fastapi

Uvicorn

Это рекомендуемый ASGI-сервер для FastAPI. Для его установки выполните следующую команду:

pip install uvicorn

Jinja2 (опционально)

С FastAPI вы можете использовать любой шаблонизатор, если собираетесь обслуживать веб-страницу через шаблон. Для простоты давайте установим Jinja2, который опирается на тот же шаблонизатор, что и Flask.

pip install jinja2

Aiofiles (опционально)

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

pip install aiofiles

Python-multipart (опционально)

По умолчанию FastAPI стандартизирует входящий запрос как JSON. Чтобы получать поля формы, следует установить python-multipart.

pip install python-multipart

Flask (опционально)

В этом руководстве демонстрируется как сервер Flask, так и эквивалентные ему функциональные возможности с использованием FastAPI. Устанавливайте Flask, когда вы намерены следовать этому руководству вместе с параллельными сравнениями, которые приводятся для наглядности. В противном случае вы можете спокойно игнорировать этот модуль и запустить вместо него сервер FastAPI.

pip install flask

Теперь перейдем к следующему разделу, а именно  —  к реализации.

2. Сравнение

В этом разделе будет приведен код с пояснениями как для сервера Flask, так и для сервера FastAPI. Оба сервера будут использовать одни и те же API и одинаковые функциональные возможности.

Импорт (Flask)

Во Flask достаточно просто объявить import, так как все упаковано в пакет. Модуль random будет использоваться для генерации случайных чисел в одном из API.

from flask import Flask, request, jsonify, render_template, send_from_directory

import random

Импорт (FastAPI)

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

from fastapi import FastAPI, Form, Request
from fastapi.responses import PlainTextResponse, HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
import random
import uvicorn

Если вы хотите установить минимальное количество пакетов, то воспользуйтесь кодом ниже:

from fastapi import FastAPI

from pydantic import BaseModel
import random   # генерирует случайное число для API
import uvicorn  # опционально при запуске из терминала

Инициализация (Flask)

Код инициализации здесь очень простой. Flask обработает как статические файлы, так и шаблонизатор.

app = Flask(__name__)

Инициализация (FastAPI)

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

app = FastAPI()
# опционально; требуется при обслуживании статических файлов 
app.mount("/static", StaticFiles(directory="static"), name="static")
# опционально; требуется при обслуживании страницы через шаблонизатор
templates = Jinja2Templates(directory="templates")
# зависит от варианта использования
class Item(BaseModel):
    language = 'english'

Hello World (Flask)

Давайте взглянем на следующий фрагмент кода, который создает маршрут, возвращающий пользователю строку.

@app.route('/')
def hello():
    return "Hello World!"

Hello World (FastAPI)

Эквивалент функции “Hello World” для FastAPI показан ниже. По умолчанию он будет возвращать ответ в формате JSON. Следовательно, чтобы возвращать строку, нужно изменить response_class на PlainTextResponse.

async не обязателен, но зато существенно упрощает работу с асинхронным кодом. Рекомендуется задать этот параметр, если только вы не имеете дело с кодом, который не поддерживает асинхронность.

@app.get("/", response_class=PlainTextResponse)
async def hello():
    return "Hello World!"

Случайное число (Flask)

Допустим, у нас есть API, который возвращает случайно сгенерированное число. На сервере Flask код должен выглядеть так:

@app.route('/random-number')
def random_number():
    return str(random.randrange(100))

Случайное число (FastAPI)

Для сервера FastAPI код изменится следующим образом:

@app.get('/random-number', response_class=PlainTextResponse)
async def random_number():
    return str(random.randrange(100))

Проверка isAlpha (Flask)

Далее протестируем API, который принимает параметр запроса под названием text и возвращает JSON в качестве результата. Во Flask этот метод задается с помощью декоратора маршрута. В этом случае мы устанавливаем его только на прием GET-запросов.

@app.route('/alpha', methods=['GET'])
def alpha():
    text = request.args.get('text', '')

    result = {'text': text, 'is_alpha' : text.isalpha()}

    return jsonify(result)

Проверка isAlpha (FastAPI)

HTTP-метод определяется с самого начала, вместе с декоратором. В терминологии FastAPI это называется операциями. Для операции GET необходимо вызвать app.get. Для нескольких HTTP-методов с одинаковой функциональностью необходимо поместить логику внутри функции и вызывать ее независимо в каждой из операций.

Параметры запроса должны быть указаны вместе с подсказкой типа. text: str представляет собой обязательный строковый параметр запроса text. Вы можете присвоить ему значение по умолчанию text = ‘text’, сделав его необязательным параметром.

@app.get('/alpha')
async def alpha(text: str):
    result = {'text': text, 'is_alpha' : text.isalpha()}

return result

Создание нового пользователя (Flask)

В большинстве случаев у вас будет POST-запрос, который добавляет новые данные в базу. Рассмотрим следующий пример, который принимает два поля формы и возвращает результат JSON.

@app.route(’/create-user’, methods=[’POST’])
def create_user():
    id = request.form.get(’id’, '0001’)
    name = request.form.get(’name’, 'Anonymous’)
    # код для аутентификации, валидации, обновления базы данных
    data = {’id’: id, 'name’: name}
    result = {’status_code’: '0’, 'status_message' : 'Success’, 'data’: data}
    return jsonify(result)

Создание нового пользователя (FastAPI)

Операция POST осуществляется через декоратор app.post. При объявлении входных параметров необходимо указывать Form(…), так как реализация по умолчанию основана на параметрах JSON или запроса.

@app.route('/create-user', methods=['POST'])
def create_user():
    id = request.form.get('id', '0001')
    name = request.form.get('name', 'Anonymous')
    # код для аутентификации, валидации, обновления базы данных
    data = {'id': id, 'name': name}
    result = {'status_code': '0', 'status_message' : 'Success', 'data': data}
    return jsonify(result)

Обновление языка (Flask)

До сих пор мы рассматривали параметры запроса и поля формы. Давайте посмотрим на другой пример, где переменная language обновляется на основе ввода JSON. Для обновления существующих данных рекомендуется использовать метод PUT. В следующем фрагменте кода показано, как сделать это с помощью условного оператора вместо прямого указания метода PUT.

@app.route('/update-language', methods=['POST', 'PUT', 'GET', 'DELETE'])
def update_language():
    language = 'english'
    if request.method == 'PUT':
        json_data = request.get_json()
        language = json_data['language']
    return "Successfully updated language to %s" % (language)

Обновление языка (FastAPI)

Операция PUT точно так же вызывается через декоратор app.put. Во время инициализации мы объявили следующий класс:

class Item(BaseModel):
    language = 'english'

Мы собираемся передать его в качестве подсказки типа для входного параметра элемента item. Он будет проанализирован в процессе. Его можно просто вызвать с помощью синтаксиса variable_name.attribute_name. В данном случае — item.language.

@app.put('/update-language', response_class=PlainTextResponse)
async def update_language(item: Item):
    language = item.language

return "Successfully updated language to %s" % (language)

HTML-страница (Flask)

С Flask обслуживать HTML-страницы относительно просто, так как в нем используется шаблонизатор Jinja2. Все, что вам нужно, это объявить HTML-файлы в папке templates. Если вы обслуживаете статические файлы, то их нужно поместить в папку static.

В следующем примере рассмотрено отображение веб-страницы с использованием index.html. Переменные в качестве входного параметра передаются следующим образом:

@app.route('/get-webpage', methods=['GET'])
def get_webpage():
    return render_template('index.html', message="Contact Us")

HTML-страница (FastAPI)

Во время инициализации мы смонтировали папку static для поставки статических файлов

app.mount("/static", StaticFiles(directory="static"), name="static")

Кроме того, мы также создали переменную на основе Jinja2Templates. Она понадобится для рендеринга HTML-шаблонов.

templates = Jinja2Templates(directory="templates")

Для обслуживания HTML-страницы нужно изменить response_class на HTMLResponse. Если вы не используете шаблонизатор, то результат можно возвращать в виде строки.

Запрос Request возвращает шаблон вместе с пользовательскими параметрами.

@app.get('/get-webpage', response_class=HTMLResponse)
async def get_webpage(request: Request):
    return templates.TemplateResponse("index.html", {"request": request, "message": "Contact Us"})

Ответ файла (Flask)

Рекомендуемый способ возврата файла пользователю  —  через встроенную функцию send_from_directory(), особенно если путь или файл были отправлены пользователем. Он принимает два основных вида входных данных:

  • путь к файлу;
  • имя файла.

Помимо этого, можно указать дополнительные параметры, такие как as_attachment, которые изменят заголовок Content-Disposition, чтобы указать, что файл является вложением.

Можно также установить параметр path, используя следующий синтаксис: <type:variable_name>. В данном случае это <string:language>.

@app.route('/get-language-file/<string:language>', methods=['GET'])
def get_language_file(language):
    return send_from_directory('./static/language', language + '.json', as_attachment=True)

Файл ответа (FastAPI)

FastAPI по умолчанию содержит довольно много типов ответов. Для возврата файла можно воспользоваться FileResponse или StreamingResponse. В этом руководстве воспользуемся FileResponse. Он принимает следующие входные данные:

  • path: путь к файлу для потоковой передачи.
  • headers: любые пользовательские заголовки, которые можно включить в состав, например, словарь.
  • media_type: строка, указывающая тип медиа. Если этот параметр не задан, то для определения типа носителя будут использованы имя файла или путь к нему.
  • filename: если имя файла задано, то оно будет включено в Content-Disposition ответа.

Посмотрим, как работает этот код.

@app.get('/get-language-file/{language}')
async def get_language_file(language: str):
    file_name = "%s.json" % (language)
    file_path = "./static/language/" + file_name
return FileResponse(path=file_path, headers={"Content-Disposition": "attachment; filename=" + file_name})

Main (Flask)

В Flask вы указываете основную функцию вот таким образом:

if __name__ == '__main__':
    app.run('0.0.0.0',port=8000)

Затем запускаете файл через терминал:

python myapp.py

Main (FastAPI)

Для FastAPI нужно сначала импортировать uvicorn.

import uvicorn

Следом укажите основные функции, как показано ниже (myapp относится к имени файла, в то время как app относится к имени переменной, объявленной для экземпляра FastAPI):

if __name__ == '__main__':
    uvicorn.run('myapp:app', host='0.0.0.0', port=8000)

Затем запустите через терминал как обычно:

python myapp.py

Однако больше рекомендуется производить запуск непосредственно через uvicorn, не вызывая основную функцию:

uvicorn myapp:app

Можно также указать дополнительные параметры, например:

  • reload: включает автоматическую перезагрузку, которая обновляет сервер при внесении изменений в файл. Чрезвычайно полезно для разработки на локальной машине.
  • port: номер порта для сервера. Значение по умолчанию  —  8000.

В следующем примере номер порта будет 5000.

uvicorn myapp:app --reload --port 5000

Сервер Flask

Ознакомьтесь с полным кодом для сервера Flask здесь на gist.

# объявление import
from flask import Flask, request, jsonify, render_template, send_from_directory
import random

# инициализация
app = Flask(__name__)

# hello world, метод GET, возврат строки
@app.route('/')
def hello():
    return "Hello World!"

# случайное число, метод GET, возврат строки
@app.route('/random-number')
def random_number():
    return str(random.randrange(100))

# проверка isAlpha, метод GET, параметр запроса, возврат JSON
@app.route('/alpha', methods=['GET'])
def alpha():
    text = request.args.get('text', '')

    result = {'text': text, 'is_alpha' : text.isalpha()}

    return jsonify(result)

# создание нового пользователя, метод POST, поля формы, возврат JSON
@app.route('/create-user', methods=['POST'])
def create_user():
    id = request.form.get('id', '0001')
    name = request.form.get('name', 'Anonymous')

    # код для аутентификации, валидации, обновления базы данных
    
    data = {'id': id, 'name': name}
    result = {'status_code': '0', 'status_message' : 'Success', 'data': data}

    return jsonify(result)

# обновление языка, метод PUT, входные данные JSON, возврат строки
@app.route('/update-language', methods=['POST', 'PUT', 'GET', 'DELETE'])
def update_language():
    language = 'english'

    if request.method == 'PUT':
        json_data = request.get_json()
        language = json_data['language']

    return "Successfully updated language to %s" % (language)

# обслуживание веб-страницы, метод GET, возврат HTML
@app.route('/get-webpage', methods=['GET'])
def get_webpage():
    return render_template('index.html', message="Contact Us")

# ответ файла, метод GET, возврат файла как вложения
@app.route('/get-language-file/<string:language>', methods=['GET'])
def get_language_file(language):
    return send_from_directory('./static/language', language + '.json', as_attachment=True)

# main
if __name__ == '__main__':
    app.run('0.0.0.0',port=8000)

Сервер FastAPI

Следующий gist содержит полный код для тех же функций, использующих FastAPI.

# объявление import
from fastapi import FastAPI, Form, Request
from fastapi.responses import PlainTextResponse, HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

from pydantic import BaseModel
import random
import uvicorn

# инициализация
app = FastAPI()

# монтирование статической папки для обслуживания статических файлов
app.mount("/static", StaticFiles(directory="static"), name="static")

# экземпляр шаблона Jinja2 для возврата веб-страниц через шаблонизатор
templates = Jinja2Templates(directory="templates")

# класс  модели данных Pydantic
class Item(BaseModel):
    #language: str
    language = 'english'

# hello world, метод GET, возврат строки
@app.get("/", response_class=PlainTextResponse)
async def hello():
    return "Hello World!"

# случайное число, метод GET, возврат строки
@app.get('/random-number', response_class=PlainTextResponse)
async def random_number():
    return str(random.randrange(100))

# проверка isAlpha, метод GET, параметр запроса, возврат JSON
@app.get('/alpha')
async def alpha(text: str):
    result = {'text': text, 'is_alpha' : text.isalpha()}

    return result

# создание нового пользователя, метод POST, поля формы, возврат JSON
@app.post('/create-user')
async def create_user(id: str = Form(...), name: str = Form(...)):
    # код для аутентификации, валидации, обновления базы данных
    
    data = {'id': id, 'name': name}
    result = {'status_code': '0', 'status_message' : 'Success', 'data': data}

    return result

# обновление языка, метод PUT, входные данные JSON, возврат строки
@app.put('/update-language', response_class=PlainTextResponse)
async def update_language(item: Item):
    language = item.language

    return "Successfully updated language to %s" % (language)
    
# обслуживание веб-страницы, метод GET, возврат HTML
@app.get('/get-webpage', response_class=HTMLResponse)
async def get_webpage(request: Request):
    return templates.TemplateResponse("index.html", {"request": request, "message": "Contact Us"})

# ответ файла, метод GET, возврат файла как вложения
@app.get('/get-language-file/{language}')
async def get_language_file(language: str):
    file_name = "%s.json" % (language)
    file_path = "./static/language/" + file_name

    return FileResponse(path=file_path, headers={"Content-Disposition": "attachment; filename=" + file_name})

# main
if __name__ == '__main__':
    uvicorn.run('myapp:app', host='0.0.0.0', port=8000)

3. Документация

После запуска сервера FastAPI вы получите доступ к двум дополнительным маршрутам, предназначенным для документирования.

Интерактивная документация (Swagger UI)

Первый  —  это интерактивная документация, предоставляемая UI Swagger. Если сервер запущен на порту 8000, получить к нему доступ можно по следующему URL-адресу:

http://localhost:8000/docs

Вы увидите следующую страницу:

Это интерактивная документация, где можно протестировать API. При нажатии на маршрут /alpha вы увидите следующий интерфейс:

Нажмите на кнопку “Try it out” (Попробовать) и введите строку в текстовое поле. Далее нажмите на кнопку “Execute” (Выполнить) и получите следующий результат:

ReDoc

FastAPI также поставляется с альтернативной документацией, которую предоставляет ReDoc. Вы можете получить к ней доступ по следующему адресу:

http://localhost:8000/redoc

Там вы увидите вот такую документацию:

4. Заключение

Время подвести итоги.

Мы начали с краткой предыстории и объяснения основных концепций, лежащих в основе FastAPI. Затем установили необходимые модули для работы как с Flask, так и с FastAPI.

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

Теперь у вас есть базовые знания о том, как осуществить миграцию сервера Flask на сервер FastAPI.

Ссылки

  1. Uvicorn на Github.
  2. Документация Uvicorn.
  3. FastAPI на Github.
  4. Документация FastAPI.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Ng Wai Foong: Migrate From Flask to FastAPI Smoothly

Предыдущая статья29 инструментов и сайтов для дизайнера, о которых вы никогда не слышали
Следующая статьяKubernetes: безопасное управление секретами с GitOps