Как подружить Quart и trio-asyncio
В этой статье вы узнаете как написать сайт на асихронной версии Flask — фреймворке Quart — c использованием кода сразу из двух миров — AsyncIO и Trio.
Зачем такие сложности, спросите вы? Почему бы не написать всё на Trio?! Он легкий, надёжный и предсказуемый. Просто бери и пиши код, в чём проблема?
Проблема в библиотеках. Проект Trio молод и под него трудно найти асинхронные библиотеки. Зато таких полно для AsyncIO. Обидно, конечно… И тут на сцену выходит trio-asyncio
— набор адаптеров и “переходников”, позволяющих запустить любую asyncio-библиотеку прямо внутри trio. Теперь всё, что написано под AsyncIO годится и для Trio.
Если вы ещё не знакомы с библиотекой trio-asyncio
, то перед чтением этой статьи ознакомьтесь с примерами из документации.
Quart и Trio
Первая часть пазла — это запустить Quart сервер. Смотрим в документацию, копируем сниппет кода:
import asyncio
from quart import Quart
app = Quart(__name__)
@app.route('/')
async def hello():
# test if asyncio event loop works
await asyncio.sleep(1)
return 'hello'
app.run()
По умолчанию Quart
использует библиотеку asyncio
, но его можно перенастроить на работу с trio
. Ставим библиотеку quart-trio
, меняем код как указано в документации:
import trio
from quart_trio import QuartTrio
app = QuartTrio(__name__)
@app.route('/')
async def hello():
# test if trio event loop works
await trio.sleep(1)
return 'hello'
app.run()
Теперь функция hello
работает внутри цикла событий trio
. Первая часть задачи решена, идём дальше.
Как подключить trio-asyncio
В библиотеке trio-asyncio
есть специальная функция aio_as_trio
. Она оборачивает код asyncio и подготавливает его к запуску прямо внутри trio. Если адаптировать код из документации, то для сайта он будет выглядеть так:
async def some_asyncio_code():
# test if asyncio event loop works
await asyncio.sleep(1)
@app.route('/')
async def hello():
# test if trio event loop works
await trio.sleep(1)
res = await trio_asyncio.aio_as_trio(some_asyncio_code)
return 'hello'
Выглядит волшебно! Взяли функцию с кодом на asyncio, обернули в aio_as_trio
, вызвали — готово! Однако, чтобы магия сработала нужно особым образом инициализировать приложение. В документации aio_as_trio
есть такой пример:
async def async_main(*args):
async with trio_asyncio.open_loop() as loop:
# здесь находится остальной асинхронный код
# можно запускать код на trio и на asyncio
pass
trio.run(async_main, *args)
У кода есть важная особенность. Функция open_loop
должна быть вызвана после trio.run
, запускающего event loop, но прежде остального асинхронного кода. В то же время Quart
предлагает запускать всё разом — и event loop с trio.run
и остальной асинхронный код прячутся внутри одной “коробки”. Коробка эта называется app.run()
. Придётся её вскрывать.
Лезть “под капот” страшно не хочется, но на деле всё довольно просто. Достаточно заглянуть в исходники quart-trio и найти там реализацию метода QuartTrio.run
:
def run(self, host, port, use_reloader, …):
...
config = HyperConfig()
config.bind = [f"{host}:{port}"]
config.use_reloader = use_reloader
...
trio.run(serve, self, config)
Весь код сводится к настройкам и вызову функции serve
из библиотеки Hypercorn
. Это такой веб-сервер, асинхронный аналог Gunicorn и uWSGI. Если библиотека Quart
предоставляет разные классы и объекты для быстрого создания сайтов, то Hypercorn
берёт на себя работу с сетевыми протоколами: принимает HTTP запросы, дешифрует HTTPS, открывает соединение по веб-сокету.
Внутри app.run
не происходит никакой магии, это тонкая обёртка над trio.run
и serve
. Теперь скопируем код и удалим всё лишнее:
import trio
from quart_trio import QuartTrio
from hypercorn.trio import serve
from hypercorn.config import Config as HyperConfig
app = QuartTrio(__name__)
@app.route('/')
async def hello():
# test if trio event loop works
await trio.sleep(1)
return 'hello'
async def run_server():
config = HyperConfig()
config.bind = [f"127.0.0.1:5000"]
config.use_reloader = True
await serve(app, config)
trio.run(run_server)
Финальная сборка
Итак, что у нас есть? Имеется Quart сервер, настроенный на работу с Trio. Есть доступ к trio.run
. И из документации к trio-asyncio есть пример настойки event loop:
async def async_main(*args):
async with trio_asyncio.open_loop() as loop:
# здесь находится остальной асинхронный код
# можно запускать код на trio и на asyncio
pass
trio.run(async_main, *args)
Осталось поместить внутрь trio_asyncio.open_loop()
запуск Quart сервера — вызов serve
. Вот полный код сайта:
import trio
from quart_trio import QuartTrio
from hypercorn.trio import serve
from hypercorn.config import Config as HyperConfig
import asyncio
import trio_asyncio
app = QuartTrio(__name__)
@app.route('/')
async def hello():
# test if trio event loop works
await trio.sleep(1)
# test if trio-asyncio event loop works
await trio_asyncio.aio_as_trio(asyncio.sleep, 1)
return 'hello'
async def run_server():
async with trio_asyncio.open_loop() as loop:
config = HyperConfig()
config.bind = [f"127.0.0.1:5000"]
config.use_reloader = True
# здесь живёт остальной код инициализации
# ...
await serve(app, config)
trio.run(run_server)