Comments 17
People constantly asked about ability to run aiohttp servers together with other asyncio code, but
aiohttp.web.run_app()
is blocking synchronous call.
run_app()
provides a simple blocking API for running anApplication
.For starting the application asynchronously or serving on multiple HOST/PORT
AppRunner
exists.
sql = f'INSERT INTO requests VALUES ("{datetime.now()}", "{city}", "{weather}")'
await db.execute(sql)
Я понимаю, что примеры упрощённые. Но ведь написать запрос без потенциального SQL injection заняло бы столько же строчек. Вы же учить новичков пытаетесь, так учите хорошему.
url = f'http://api.openweathermap.org/data/2.5/weather' \
f'?q={city}&APPID=2a4ff86f9aaa70041ec8e82db64abf56'
Так подставлять параметр в URL тоже не безопасно.
Неважно какой фреймворк вы будете использовать:
FastAPI
,Tornado
,Falcon
или какой-нибудь еще. Принцип останется тот же самый как в старом добромaiohttp
: создаем ручку и в ней нанизываем "шашлык" из вызовов асинхронных функций.
Очень интересная статья, а можно, пожалуйста, небольшой пример кода, как запустить FastAPI в async режиме?
Прочитал цикл с интересом, спасибо. Лайк за наставление использовать асинхронный логгер, часто вижу в продакшн коде стандартную библиотеку.
Если кто-то решит использовать это руководство для написания продуктового кода, то обращаю внимание, что создавать сессию на каждый http запрос - это моветон. Это очень распространенная ошибка при использовании Aiohttp. Сессии надо создать и закрывать только при старте и соотвественно при закрытии приложения. Прочитать можно тут, тут
И на главной есть сноска.
![](https://habrastorage.org/getpro/habr/upload_files/dc8/d4e/9b4/dc8d4e9b4a386ead003a6b2a52f723e7.png)
Но для учебных примеров такое использование сессий норм!
Обратите внимание, асинхронный логгер ни на миллисекунду не задержит работу нашего приложения в целом. Он будет использовать паузы, пока мы ожидаем запроса от пользователя или ответа от внешнего сервиса.
А теперь идем в документацию aiologger и читаем:
That’s why logging to files is NOT truly async.
aiologger
implementation of file logging uses aiofiles, which uses a Thread Pool to write the data. Keep this in mind when usingaiologger
for file logging.
То есть ваша трактовка не корректна. В данном случае под-капотом используется тред-пул, а это значит, что он не ждет пауз, а вытесняет прочий код.
Попутно он порождает оверхед на поддержание тред-пула, а также лишает программу гарантии, что логи точно запишутся (например если электричество отключится, пока записывается лог) прежде, чем она продолжит выполняться. Я не говорю, что все это плохо, просто стоит это более явно проговаривать, а не преподносить как однозначно "лучшее" решение.
Выбор именно sqlite в качестве СУБД для проекта на асинхронном фреймворке тоже, честно говоря, странный. Дело в том, что sqlite - это файловая СУБД, а значит сталкивается с теми же ограничениями системных вызовов, что не позволяют сделать действительно асинхронной работу с файлами в aiologger. О чем нам авторы также честно пишут на странице проекта:
aiosqlite allows interaction with SQLite databases on the main AsyncIO event loop without blocking execution of other coroutines while waiting for queries or data fetches. It does this by using a single, shared thread per connection. This thread executes all actions within a shared request queue to prevent overlapping actions.
Спасибо за критику.
Относительно aiologger читаем:
Tldr;
aiologger
is only fully async when logging to stdout/stderr. If you log into files on disk you are not being fully async and will be using ThThreads.Ка
Как вы заметили, в моем примере вывод осуществляется именно в стандартный поток. Более того, я глубоко убеждён, что логирование в файл средствами языка - это плохая практика в принципе. Не дело приложения определять где и как хранить логи.
Что касается sqlite, то здесь все просто. Статья учебная и перегружать текст подробностями установки и подключения к "настоящей" БД я посчитал излишним. Тут и к использованию SQL тоже можно прицепиться, ведь на практике в большинстве случаев применяется ORM. Но грузить тут новичка еще и SqlAlchemy - это уже запредельный перебор)
Я бы с удовольствием почитал про SqlAlchemy 2.0 для Postgre async. Насколько могу судить там синтаксис чуть изменился, ну либо я что то не понял.
А там фишка вроде такая, что уже в версии 1.4 старый ORM синтаксис поддерживается в синхронном варианте как deprecated, а в асинхронном - вообще никак. Как я понял, в 2.0 они полностью заблочат старый стиль и для синхронного драйвера тоже.
Так что уже сейчас нет вообще смысла использовать олдскульный session.query.
У меня в текущем проекте вынужденно поддерживаются оба драйвера, но стиль запросов уже используется единый - новый. Как-то вот так это может выглядеть в синхронном варианте:
with mdl.session as session:
with session.begin():
query = select(
mdl.Passport.name.label('passport_name'), mdl.Passport.value
).filter(mdl.Passport.value >= 37)
results = session.execute(query)
for result in results:
print(result.passport_name, result.value)
А в асинхронном так:
async with mdl.async_session as session:
async with session.begin():
query = select(
mdl.Passport.name.label('passport_name'), mdl.Passport.value
).filter(mdl.Passport.value >= 37)
results = await session.execute(query)
for result in results:
print(result.passport_name, result.value)
И в 2.0, как я понимаю, все это будет работать аналогично.
А так ли осмысленно в последнем примере асинхронно создавать таблицу в базе данных? Ведь эта операция вызвывается только один раз и ни с чем не параллелится.
Смысла большого, конечно, нет, но специально поддерживать в приложении еще и синхронный драйвер, чтобы только создать табличку, как-то избыточно. Другое дело, если у вас, например, асинхронный вариант алхимии и плюс нужен алембик. Тогда да, имеет смысл держать синхронный драйвер чисто для алембика, во всяком случае я другого варианта не вижу.
import asyncio
from logging.config import fileConfig
from sqlalchemy import create_engine
from sqlalchemy import pool
from sqlalchemy.ext.asyncio import AsyncEngine
from alembic import context
config = context.config
fileConfig(config.config_file_name)
target_metadata = <db.Model.metadata> # тут мета моделей
db_url = f"postgresql+asyncpg://postgres:postgres@localhost"
def run_migrations_offline():
context.configure(
url=db_url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection):
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
async def run_migrations_online():
connectable = AsyncEngine(create_engine(db_url, poolclass=pool.NullPool, future=True))
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
if context.is_offline_mode():
run_migrations_offline()
else:
asyncio.run(run_migrations_online())
Асинхронный python без головной боли (часть 2)