Как подружить 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)

Попробуйте бесплатные уроки по Python

Получите крутое код-ревью от практикующих программистов с разбором ошибок и рекомендациями, на что обратить внимание — бесплатно.

Переходите на страницу учебных модулей «Девмана» и выбирайте тему.