Armin Ronacher: Flask for Fun and Profit

За что любят Flask
Flask основан на werkzeug (читается: вёркцойг). Это маленькая библиотека, реализующая стандарт WSGI, которая живет посередине между WSGI-сервером, принимающим HTTP-запросы, и самим веб-приложением. Стандарт WSGI появился в 2004 году, и он до сих пор является основой веб-приложений на питоне. Философия werkzeug, а впоследствии и Flask, — в том, что пользователю нужно дать как можно больше свободы и не навязывать ему архитектурные решения. И именно в этом залог его популярности: с Flask стартовать проще всего. На нем легко писать приложения, рендерящие HTML; на нем еще легче делать REST-микросервисы.
Еще одно преимущество малого размера Flask — в него нельзя засунуть много логики. Там просто слишком мало компонентов и нечего развивать и усложнять. Это делает Flask доступным не только для пользователей, но и для контрибьюторов.
Есть ниши, в которых Flask непригоден, и в первую очередь — это высокопроизводительные асинхронные сервисы. Есть попытки адаптировать Flask под асинхронщину, но по большому счету — лучше взять изначально асинхронный продукт.

Создание приложения
Самый удобный способ — создать приложение фабрикой create_app. Это позволит легко и понятно управлять конфигурацией; например, одной фабрикой создавать боевую версию аппы, а другой фабрикой — создавать аппу для тестов и по-своему её конфигурировать. Кроме того, если вы запускаете тесты в несколько потоков, фабрика create_app избавит ваши потоки от общего глобального объекта app.

Сравните это с django, где все модули смотрят на глобальный конфигурационный файл и на его переменные. Django не так-то просто переконфигурировать на лету.
Если вам все же нужен глобальный объект — можно создать пустую аппу глобально и сконфигурировать её функцией register_blueprints(), которая повесит на неё все нужные роуты и логику.

Ещё один интересный способ создать приложение — это завернуть его в новый объект, который будет все вызовы передавать в приложение. Это удобно для изоляции неймспейсов.

Зачем это нужно? Например, если вы пишете на базе Flask какое-то приложение и хотите повесить на него логику, не связанную собственно с Flask или с вебом (к примеру, ваше приложение должно слушать redis или rabbitmq).
Завернув приложение в отдельный объект, вы изолируете функционал, уменьшите сопряжение объектов и не перезапишете случайно какие-то из существующих атрибутов (а такой риск существует: на объекте app висит очень много атрибутов).
Дев-север
Завернув создание приложения в функцию (или в объект), мы должны решить, где нам все-таки вызвать эту функцию, чтобы запустить дев-сервер. Традиционно для этого создается отдельный файл, в который импортируется эта функция и вызывается:

И после этого командой flask run запускается этот файл:

Многие вместо flask run кладут в конец файла команду app.run() . У этого подхода есть два недостатка.
- Во-первых, если вы используете релоадер, то, встретив в вашем коде ошибку
SyntaxError, он сломается и не перезапустится. Командаflask runстрахует нас от этой проблемы. - Во-вторых, в варианте
app.run(), когда мы запускаем приложение, релоадер проходит по исполняемому файлу два раза — и дважды прогоняет весь код с начала до конца. И если передapp.run()у вас в коде создается БД, то релоадер создаст вам эту БД дважды.
Очень интересный момент — Debugger PIN. Дело в том, что страницы с ошибками, которые генерит Flask, — интерактивны. Можно отлаживать код прямо в них, внутри трейса. Соответственно, PIN нужен для защиты, если вдруг кто-то наткнется на вашу дебаг страницу и решит её хакнуть.

Глобальные объекты
Главный холивар Фласка идет вокруг глобальных объектов current_app, request и g. В django такого нет; людям, привыкшим к django, глобальные объекты кажутся странными.

Ирония в том, что в django глобалки тоже есть. Например, как, по-вашему, функция gettext() определяет, на какой язык переводить ту или иную фразу? Она берет это из секретного глобального объекта, содержащего данные о запросе из браузера. Flask не делает из глобалок секрет; он показывает вещи такими, какие они есть на самом деле.
Два глобальных контекста
Во фласке есть два глобальных контекста. Один известен всем — это контекст запроса, request и session. О втором знают немногие: это контекст приложения, current_app и g . Контекст приложения, точно так же, как и контекст запроса, сбрасывается после ответа на запрос.

Зачем тогда нужен контекст приложения, если он сбрасывается точно так же? Он нужен в ситуации, когда у нас просто нет такой вещи, как HTTP-запрос. Например, когда мы запускаем cron-джоб по графику, а внутри джоба хотим запустить функцию, требующую контекст приложения (язык пользователя, какие-то глобальные настройки и тд).

Пример работы с контекстом приложения
При создании контекста g кладем туда настройки (коннект к БД, язык пользователя и тп). Дальше, в коде функций, достаем из контекста g нужные данные по необходимости.
Вот пример, как положить в контекст подключение к БД и закрыть его при завершении контекста. Django делает это под капотом; Flask предлагает вам сделать это явным образом (и многие расширения Flask этим пользуются).

Еще пример: функцию получения пользователя можно сначала направить на g, а если его там нет — то на request. Это очень удобно для тестов: контекст g создать и убить гораздо дешевле, чем контекст request.

JSON API
Есть море библиотек для JSON API поверх Flask. Но я, честно говоря, их не использую и пишу всё сам. Почему так?
Во-первых, фреймворки навязывают нам собственное представление об API: как делать пагинацию, какой mimetype ставить в ответе и т.д. Установив фреймворк, мы в какой-то момент начинаем с ним бороться и прогибать под свою логику. Или прогибаемся сами.
Во-вторых, для создания JSON API нужно на самом деле очень мало кода. Я обычно делаю объект ApiResult, который умеет раскрываться в Response с нужным статусом и хедерами. Такой подход очень хорошо расширяется. Например, сюда можно добавить пагинацию в нужном формате, обработку пробелов и табов, и тд.

Чтобы научить вьюхи правильно пониматьApiResult, нужно унаследоваться от Flask и переопределить make_response(). И всё.

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

Как быть с исключениями? Делаем класс ApiException, который умеет генерировать ответы с кодом, например, 400, и регистрируем на приложении хендлер для этого класса. Теперь можно делать во вьюхах raise ApiException , и Flask превратит это в корректный JSON-ответ с правильным кодом.


Валидация и сериализация
Большинство библиотек для валидации и сериализации в питоне впадают в одну из двух крайностей:
- либо у них прикольный простой API, но куча подкапотной магии, которая рано или поздно начинает вас ограничивать;
- либо они могут всё — и с ними невозможно справиться.
Есть компромисс: jsonschema , кроссплатформенный стандарт валидации. Многие пользуются именно им. Лично я предпочитаю voluptuous . С его помощью можно настраивать валидацию фласковских вьюх прямо в декораторах вьюх. (обратите внимание на выброс ApiException в глубине декоратора — этот класс мы создали выше)


Безопасность
Самый распространенная дырка в безопасности — это когда мы забываем ограничить выборку данных по ID юзера и вместо этого отдаём данные всех юзеров. Простой способ избежать этого — подружить ваши модели с контекстом выполнения. Я для этого держу в своих проектах модуль security, в котором храню фильтры по данным. После этого достаточно переопределить на модели атрибут query и подложить туда такой фильтр (см. get_available_organizations()). Теперь все данные по запросу Project.query... будут автоматически фильтроваться в зависимости от контекста конкретного запроса.

Экранирование HTML
Все знают, что, когда в контекст шаблона jinja попадает переменная, содержащая HTML, все теги этого HTML автоматически экранируются. Это сделано во избежание XSS-атак: если вы принимаете у пользователей данные и потом рендерите их в шаблонах, то вас могут хакнуть, внедрив вам на страницу вредоносный JS внутри тега <script>. Но немногие знают, что подобная вещь существует и для JSON. Если вы собираетесь отдавать внутри JSON строки, содержащие HTML (да, бывают такие кейсы), попробуйте это:

Тестирование
Лучше всего тестировать Flask-приложения с помощью pytest и его фикстур. Самая простая и полезная фикстура — ниже. Здесь мы создаем приложение, включаем его контекст (ctx.push) и вешаем на момент окончания жизни фикстуры выключение контекста (ctx.pop). Важно: request здесь — это не запрос от фласка, это стандартная фикстура pytest: https://docs.pytest.org/en/latest/fixture.html#request-context

Прим.перев.: вместо неочевидного request.addfinalizer можно так:
...
ctx.push()
yield app
ctx.pop()На базе этой фикстуры можно сделать фикстуру тестового клиента:

Прим. перев: и здесь можно обойтись без __enter__ и __exit__, написав:
...
with app.test_client() as client:
yield clientВебсокеты
Фласк не приспособлен для вебсокетов. Он не умеет держать коннекты, он создан для простых HTTP-запросов и HTTP-ответов. Для вебсокетов нужен отдельный сервер.
Даже если попытаться приспособить Flask под вебсокеты (или взять другой фреймворк, который умеет вебсокеты) — всё равно желательно держать для вебсокетов отдельный сервер. Почему? Представьте себе ситуацию:
- Вы выкатили приложение с вебсокетами. Ваше приложение набрало тысячу подключений от разных браузеров — и держит их.
- Вы выкатываете новую версию приложения. После перезагрузки сервера все подключения обрываются.
- После обрыва браузеры одновременно идут восстанавливать подключение — и обрушивают ваше свежезапущенное приложение тысячей запросов. (особенно если вебсокет-подключения требуют работы с БД).
Простейший вебсокет-сервер — это приложение, которое подписывается на redis по pub/sub и отрывает вебсокет-подключение для браузеров. Дальше мы из Flask пушим в redis сообщения, их получает наш вебсокет-сервер и отправляет клиентам. Жаль, что такого вебсокет-сервера нет в опенсорсе. (Прим.перев.: теперь есть! https://github.com/centrifugal/centrifugo)
