Асинхронный ввод/вывод (asyncio)¶
Поддержка Python asyncio. Включена поддержка использования Core и ORM с использованием диалектов, совместимых с asyncio.
Добавлено в версии 1.4.
Предупреждение
Пожалуйста, прочитайте Примечания по установке платформы Asyncio (включая Apple M1) для важных указаний по установке для многих платформ, включая Apple M1 Architecture.
Совет
Расширение asyncio в версии SQLAlchemy 1.4.3 теперь можно считать бета-уровнем программного обеспечения. Детали API могут быть изменены, однако на данный момент маловероятно, что будут значительные изменения, несовместимые с обратным развитием.
См.также
Asynchronous IO Support for Core and ORM - первоначальное объявление функции
Интеграция Asyncio - примеры скриптов, иллюстрирующие рабочие примеры использования Core и ORM в рамках расширения asyncio.
Примечания по установке платформы Asyncio (включая Apple M1)¶
Расширение asyncio требует только Python 3. Оно также зависит от библиотеки greenlet. Эта зависимость установлена по умолчанию на распространенных машинных платформах, включая:
x86_64 aarch64 ppc64le amd64 win32
Для вышеуказанных платформ известно, что greenlet
поставляет предварительно собранные файлы колеса. Для других платформ greenlet не устанавливается по умолчанию; текущий список файлов для greenlet можно посмотреть на Greenlet - Download Files. Заметим, что не учтены многие архитектуры, включая Apple M1.
Чтобы установить SQLAlchemy и при этом обеспечить наличие зависимости greenlet
независимо от используемой платформы, [asyncio]
setuptools extra может быть установлен следующим образом, который будет включать также указание pip
установить greenlet
:
pip install sqlalchemy[asyncio]
Обратите внимание, что установка greenlet
на платформы, не имеющие предварительно собранного файла wheel, означает, что greenlet
будет собран из исходных текстов, что требует наличия библиотек разработки Python.
Синопсис - Ядро¶
Для использования в Core функция create_async_engine()
создает экземпляр AsyncEngine
, который затем предлагает асинхронную версию традиционного API Engine
. Функция AsyncEngine
предоставляет AsyncConnection
через свои методы AsyncEngine.connect()
и AsyncEngine.begin()
, которые оба предоставляют асинхронные менеджеры контекста. Затем AsyncConnection
может вызывать операторы, используя либо метод AsyncConnection.execute()
для передачи буферизованного Result
, либо метод AsyncConnection.stream()
для передачи потокового серверного AsyncResult
:
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine
async def async_main():
engine = create_async_engine(
"postgresql+asyncpg://scott:tiger@localhost/test",
echo=True,
)
async with engine.begin() as conn:
await conn.run_sync(meta.drop_all)
await conn.run_sync(meta.create_all)
await conn.execute(
t1.insert(), [{"name": "some name 1"}, {"name": "some name 2"}]
)
async with engine.connect() as conn:
# select a Result, which will be delivered with buffered
# results
result = await conn.execute(select(t1).where(t1.c.name == "some name 1"))
print(result.fetchall())
# for AsyncEngine created in function scope, close and
# clean-up pooled connections
await engine.dispose()
asyncio.run(async_main())
Выше, метод AsyncConnection.run_sync()
может быть использован для вызова специальных функций DDL, таких как MetaData.create_all()
, которые не включают ожидающий хук.
Совет
Рекомендуется вызывать метод AsyncEngine.dispose()
с помощью await
при использовании объекта AsyncEngine
в области видимости, которая выйдет из контекста и будет собрана, как показано в функции async_main
в приведенном выше примере. Это гарантирует, что все соединения, открытые пулом соединений, будут правильно утилизированы в ожидающем контексте. В отличие от использования блокирующего ввода-вывода, SQLAlchemy не может правильно утилизировать эти соединения в рамках таких методов, как __del__
или финализаторов weakref, поскольку нет возможности вызвать await
. Если явно не утилизировать механизм, когда он выпадает из области видимости, это может привести к предупреждениям, выдаваемым в стандартный вывод, напоминающим форму RuntimeError: Event loop is closed
в сборке мусора.
AsyncConnection
также имеет «потоковый» API через метод AsyncConnection.stream()
, который возвращает объект AsyncResult
. Этот объект результата использует курсор на стороне сервера и предоставляет API async/await, такой как async iterator:
async with engine.connect() as conn:
async_result = await conn.stream(select(t1))
async for row in async_result:
print("row: %s" % (row,))
Синопсис - ORM¶
При использовании запросов 2.0 style класс AsyncSession
обеспечивает полную функциональность ORM. В режиме использования по умолчанию необходимо соблюдать особую осторожность, чтобы избежать lazy loading или другого доступа к атрибутам с истекшим сроком действия с использованием ORM отношений и атрибутов столбцов; следующий раздел Предотвращение неявного ввода-вывода при использовании AsyncSession подробно описывает это. Пример ниже иллюстрирует полный пример, включая конфигурацию маппера и сессии:
import asyncio
from sqlalchemy import Column
from sqlalchemy import DateTime
from sqlalchemy import ForeignKey
from sqlalchemy import func
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.future import select
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import selectinload
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
data = Column(String)
create_date = Column(DateTime, server_default=func.now())
bs = relationship("B")
# required in order to access columns with server defaults
# or SQL expression defaults, subsequent to a flush, without
# triggering an expired load
__mapper_args__ = {"eager_defaults": True}
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
data = Column(String)
async def async_main():
engine = create_async_engine(
"postgresql+asyncpg://scott:tiger@localhost/test",
echo=True,
)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
# expire_on_commit=False will prevent attributes from being expired
# after commit.
async_session = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
async with async_session() as session:
async with session.begin():
session.add_all(
[
A(bs=[B(), B()], data="a1"),
A(bs=[B()], data="a2"),
A(bs=[B(), B()], data="a3"),
]
)
stmt = select(A).options(selectinload(A.bs))
result = await session.execute(stmt)
for a1 in result.scalars():
print(a1)
print(f"created at: {a1.create_date}")
for b1 in a1.bs:
print(b1)
result = await session.execute(select(A).order_by(A.id))
a1 = result.scalars().first()
a1.data = "new data"
await session.commit()
# access attribute subsequent to commit; this is what
# expire_on_commit=False allows
print(a1.data)
# for AsyncEngine created in function scope, close and
# clean-up pooled connections
await engine.dispose()
asyncio.run(async_main())
В приведенном выше примере AsyncSession
инстанцируется с помощью необязательного помощника sessionmaker
и ассоциируется с AsyncEngine
против определенного URL базы данных. Затем он используется в асинхронном менеджере контекста Python (т.е. в операторе async with:
) таким образом, что он автоматически закрывается в конце блока; это эквивалентно вызову метода AsyncSession.close()
.
Примечание
AsyncSession
использует режим будущего SQLAlchemy, который имеет несколько потенциально разрушительных изменений. Одним из таких изменений является новое поведение по умолчанию cascade_backrefs
является False
, что может повлиять на то, как связанные объекты сохраняются в базе данных.
Предотвращение неявного ввода-вывода при использовании AsyncSession¶
Используя традиционный asyncio, приложение должно избегать точек, в которых может произойти обращение к IO-на-атрибутах. Для предотвращения этого принимаются следующие меры:
Загрузчик нетерпения
selectinload()
используется для нетерпеливой загрузки коллекцииA.bs
в пределах области действия вызоваawait session.execute()
:stmt = select(A).options(selectinload(A.bs))
Если бы стратегия загрузчика по умолчанию «lazyload» была оставлена на месте, доступ к атрибуту
A.bs
вызвал бы исключение asyncio. Существует множество вариантов ORM-загрузчика, которые могут быть настроены на уровне связки по умолчанию или использоваться на основе каждого запроса, что описано в Техники загрузки отношений.AsyncSession
настроен с помощьюSession.expire_on_commit
, установленного в False, так что мы можем получить доступ к атрибутам объекта после вызоваAsyncSession.commit()
, как в строке в конце, где мы получаем доступ к атрибуту:# create AsyncSession with expire_on_commit=False async_session = AsyncSession(engine, expire_on_commit=False) # sessionmaker version async_session = sessionmaker( engine, expire_on_commit=False, class_=AsyncSession ) async with async_session() as session: result = await session.execute(select(A).order_by(A.id)) a1 = result.scalars().first() # commit would normally expire all attributes await session.commit() # access attribute subsequent to commit; this is what # expire_on_commit=False allows print(a1.data)
Значение
Column.server_default
в столбцеcreated_at
по умолчанию не обновляется после INSERT; вместо этого оно обычно expired so that it can be loaded when needed. Аналогичное поведение применимо к столбцу, в котором параметрColumn.default
присвоен объекту выражения SQL. Чтобы получить доступ к этому значению с помощью asyncio, оно должно быть обновлено в процессе flush, что достигается установкой параметраmapper.eager_defaults
на отображении:class A(Base): # ... # column with a server_default, or SQL expression default create_date = Column(DateTime, server_default=func.now()) # add this so that it can be accessed __mapper_args__ = {"eager_defaults": True}
Другие рекомендации включают:
Следует избегать методов типа
AsyncSession.expire()
в пользуAsyncSession.refresh()
.Избегайте использования опции каскада
all
, задокументированной в Каскады, в пользу явного перечисления желаемых свойств каскада. Опция каскадаall
подразумевает, помимо прочего, настройку refresh-expire, что означает, что методAsyncSession.refresh()
уничтожит атрибуты на связанных объектах, но не обязательно обновит эти связанные объекты, если вrelationship()
не настроена ускоренная загрузка, оставив их в просроченном состоянии. В будущем выпуске может появиться возможность указывать опции ускоренной загрузки при вызовеSession.refresh()
и/илиAsyncSession.refresh()
.Для столбцов
deferred()
, если они вообще используются, следует применять соответствующие опции загрузчика в дополнение к опциям для конструкцийrelationship()
, как отмечено выше. См. раздел Отложенная загрузка колонн для получения информации об отложенной загрузке столбцов.
Стратегия «динамического» загрузчика отношений, описанная в Динамические загрузчики отношений, по умолчанию не совместима с подходом asyncio. Она может быть использована напрямую только при вызове метода
AsyncSession.run_sync()
, описанного в Запуск синхронных методов и функций в asyncio, или при использовании его атрибута.statement
для получения обычного select:user = await session.get(User, 42) addresses = (await session.scalars(user.addresses.statement)).all() stmt = user.addresses.statement.where( Address.email_address.startswith("patrick") ) addresses_filter = (await session.scalars(stmt)).all()
См.также
Использование «динамической» загрузки отношений без использования Query - заметки о переходе на стиль 2.0
Запуск синхронных методов и функций в asyncio¶
Deep Alchemy
Этот подход, по сути, является публичным раскрытием механизма, с помощью которого SQLAlchemy обеспечивает интерфейс asyncio в первую очередь. Несмотря на отсутствие технических проблем, в целом этот подход можно считать «спорным», поскольку он противоречит некоторым центральным принципам модели программирования asyncio, которая заключается в том, что любое программное утверждение, которое потенциально может привести к вызову IO, **должно содержать вызов await
, чтобы в программе не было явно указано в каждой строке, где может произойти IO. Данный подход не меняет этой общей идеи, за исключением того, что он позволяет освободить серию синхронных инструкций ввода-вывода от этого правила в рамках вызова функции, по сути, объединив их в один awaitable.
В качестве альтернативного средства интеграции традиционной «ленивой загрузки» SQLAlchemy в цикл событий asyncio, предоставляется опциональный метод, известный как AsyncSession.run_sync()
, который будет запускать любую функцию Python внутри гринлета, где традиционные концепции синхронного программирования будут переведены на использование await
, когда они достигнут драйвера базы данных. Гипотетический подход здесь заключается в том, что асинхронно-ориентированное приложение может упаковать методы, связанные с базой данных, в функции, которые вызываются с помощью AsyncSession.run_sync()
.
Изменяя приведенный выше пример, если бы мы не использовали selectinload()
для коллекции A.bs
, мы могли бы выполнить обработку этих обращений к атрибутам в отдельной функции:
import asyncio
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
def fetch_and_update_objects(session):
"""run traditional sync-style ORM code in a function that will be
invoked within an awaitable.
"""
# the session object here is a traditional ORM Session.
# all features are available here including legacy Query use.
stmt = select(A)
result = session.execute(stmt)
for a1 in result.scalars():
print(a1)
# lazy loads
for b1 in a1.bs:
print(b1)
# legacy Query use
a1 = session.query(A).order_by(A.id).first()
a1.data = "new data"
async def async_main():
engine = create_async_engine(
"postgresql+asyncpg://scott:tiger@localhost/test",
echo=True,
)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
async with AsyncSession(engine) as session:
async with session.begin():
session.add_all(
[
A(bs=[B(), B()], data="a1"),
A(bs=[B()], data="a2"),
A(bs=[B(), B()], data="a3"),
]
)
await session.run_sync(fetch_and_update_objects)
await session.commit()
# for AsyncEngine created in function scope, close and
# clean-up pooled connections
await engine.dispose()
asyncio.run(async_main())
Приведенный выше подход к запуску определенных функций в рамках «синхронизирующего» бегуна имеет некоторые параллели с приложением, которое запускает приложение SQLAlchemy поверх библиотеки программирования на основе событий, такой как gevent
. Различия заключаются в следующем:
В отличие от использования
gevent
, мы можем продолжать использовать стандартный цикл событий Python asyncio или любой пользовательский цикл событий без необходимости интегрироваться в цикл событийgevent
.Никакого «обезьянничания» не происходит. В приведенном выше примере используется настоящий драйвер asyncio, а лежащий в основе SQLAlchemy пул соединений также использует встроенный в Python
asyncio.Queue
для пула соединений.Программа может свободно переключаться между кодом async/await и содержащимися функциями, использующими код sync, практически без ущерба для производительности. Нет ни «исполнителя потока», ни каких-либо дополнительных ожиданий или синхронизации.
Базовые сетевые драйверы также используют чистые концепции Python asyncio, не используются сторонние сетевые библиотеки, как это делают
gevent
иeventlet
.
Использование событий с расширением asyncio¶
SQLAlchemy event system напрямую не раскрывается расширением asyncio, что означает, что пока не существует «асинхронной» версии обработчика событий SQLAlchemy.
Однако, поскольку расширение asyncio окружает обычный синхронный SQLAlchemy API, обычные обработчики событий в «синхронном» стиле свободно доступны, как это было бы, если бы asyncio не использовался.
Как описано ниже, в настоящее время существует две стратегии регистрации событий в asyncio-facing API:
События могут быть зарегистрированы на уровне экземпляра (например, конкретного экземпляра
AsyncEngine
) путем связывания события с атрибутомsync
, который ссылается на проксируемый объект. Например, чтобы зарегистрировать событиеPoolEvents.connect()
против экземпляраAsyncEngine
, используйте его атрибутAsyncEngine.sync_engine
в качестве цели. Цели включают:AsyncEngine.sync_engine
AsyncConnection.sync_connection
AsyncConnection.sync_engine
AsyncSession.sync_session
Чтобы зарегистрировать событие на уровне класса, направленное на все экземпляры одного типа (например, все экземпляры
AsyncSession
), используйте соответствующий класс в стиле синхронизации. Например, чтобы зарегистрировать событиеSessionEvents.before_commit()
против классаAsyncSession
, используйте классSession
в качестве цели.
При работе в обработчике событий, который находится в контексте asyncio, объекты типа Connection
продолжают работать своим обычным «синхронным» способом, не требуя использования await
или async
; когда сообщения в конечном итоге принимаются адаптером базы данных asyncio, стиль вызова прозрачно адаптируется обратно в стиль вызова asyncio. Для событий, которым передается соединение уровня DBAPI, например PoolEvents.connect()
, объект является pep-249 совместимым объектом «соединение», который адаптирует вызовы в стиле синхронизации к драйверу asyncio.
Некоторые примеры обработчиков событий в стиле синхронизации, связанных с конструкциями API, ориентированными на асинхронный режим, показаны ниже:
import asyncio
from sqlalchemy import event, text
from sqlalchemy.engine import Engine
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import Session
## Core events ##
engine = create_async_engine("postgresql+asyncpg://scott:tiger@localhost:5432/test")
# connect event on instance of Engine
@event.listens_for(engine.sync_engine, "connect")
def my_on_connect(dbapi_con, connection_record):
print("New DBAPI connection:", dbapi_con)
cursor = dbapi_con.cursor()
# sync style API use for adapted DBAPI connection / cursor
cursor.execute("select 'execute from event'")
print(cursor.fetchone()[0])
# before_execute event on all Engine instances
@event.listens_for(Engine, "before_execute")
def my_before_execute(
conn,
clauseelement,
multiparams,
params,
execution_options,
):
print("before execute!")
## ORM events ##
session = AsyncSession(engine)
# before_commit event on instance of Session
@event.listens_for(session.sync_session, "before_commit")
def my_before_commit(session):
print("before commit!")
# sync style API use on Session
connection = session.connection()
# sync style API use on Connection
result = connection.execute(text("select 'execute from event'"))
print(result.first())
# after_commit event on all Session instances
@event.listens_for(Session, "after_commit")
def my_after_commit(session):
print("after commit!")
async def go():
await session.execute(text("select 1"))
await session.commit()
await session.close()
await engine.dispose()
asyncio.run(go())
Приведенный выше пример выводит что-то вроде:
New DBAPI connection: <AdaptedConnection <asyncpg.connection.Connection ...>>
execute from event
before execute!
before commit!
execute from event
after commit!
Использование методов драйвера только с ожиданием в пуле соединений и других событиях¶
Как говорилось в предыдущем разделе, обработчики событий, такие как обработчики событий PoolEvents
, получают соединение «DBAPI» в стиле синхронизации, которое представляет собой объект-обертку, предоставляемый диалектами asyncio SQLAlchemy для адаптации базового соединения «драйвера» asyncio в такое, которое может быть использовано внутренними компонентами SQLAlchemy. Особый случай возникает, когда пользовательская реализация такого обработчика событий должна использовать конечное «драйверное» соединение напрямую, используя только ожидаемые методы этого драйверного соединения. Одним из таких примеров является метод .set_type_codec()
, предоставляемый драйвером asyncpg.
Для того, чтобы приспособить этот вариант использования, класс SQLAlchemy AdaptedConnection
предоставляет метод AdaptedConnection.run_async()
, который позволяет вызывать ожидаемую функцию в «синхронном» контексте обработчика событий или другого внутреннего компонента SQLAlchemy. Этот метод является прямым аналогом метода AsyncConnection.run_sync()
, который позволяет методу в стиле sync работать в режиме async.
AdaptedConnection.run_async()
должна быть передана функция, которая примет внутреннее соединение «драйвера» как единственный аргумент и вернет awaitable, который будет вызван методом AdaptedConnection.run_async()
. Саму переданную функцию не нужно объявлять как async
; совершенно нормально, если она будет Python lambda:
, так как возвращаемое значение awaitable будет вызвано после возврата:
from sqlalchemy import event
from sqlalchemy.ext.asyncio import create_async_engine
engine = create_async_engine(...)
@event.listens_for(engine.sync_engine, "connect")
def register_custom_types(dbapi_connection, ...):
dbapi_connection.run_async(
lambda connection: connection.set_type_codec(
"MyCustomType", encoder, decoder, ...
)
)
Выше, объект, передаваемый в обработчик события register_custom_types
, является экземпляром AdaptedConnection
, который предоставляет DBAPI-подобный интерфейс к базовому асинхронному объекту соединения на уровне драйвера. Метод AdaptedConnection.run_async()
затем предоставляет доступ к ожидающей среде, в которой можно действовать с базовым соединением на уровне драйвера.
Добавлено в версии 1.4.30.
Использование нескольких циклов событий asyncio¶
Приложение, использующее несколько циклов событий, например, в редком случае сочетания asyncio с многопоточностью, не должно использовать один и тот же AsyncEngine
с разными циклами событий при использовании реализации пула по умолчанию.
Если AsyncEngine
передается из одного цикла событий в другой, то перед повторным использованием в новом цикле событий следует вызвать метод AsyncEngine.dispose()
. Если этого не сделать, может возникнуть ошибка RuntimeError
, подобная Task <Task pending ...> got Future attached to a different loop
.
Если один и тот же движок должен использоваться совместно разными циклами, его следует настроить на отключение пула с помощью NullPool
, не позволяя движку использовать любое соединение более одного раза:
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.pool import NullPool
engine = create_async_engine(
"postgresql+asyncpg://user:pass@host/dbname",
poolclass=NullPool,
)
Использование asyncio scoped session¶
Шаблон «scoped session», используемый в потоковой SQLAlchemy с объектом scoped_session
, также доступен в asyncio, используя адаптированную версию под названием async_scoped_session
.
Совет
SQLAlchemy в целом не рекомендует использовать паттерн «scoped» для новых разработок, поскольку он опирается на изменяемое глобальное состояние, которое также должно быть явно разрушено по завершении работы потока или задачи. В частности, при использовании asyncio, вероятно, лучше передавать AsyncSession
непосредственно в ожидающие функции, которым это необходимо.
При использовании async_scoped_session
, поскольку в контексте asyncio не существует концепции «thread-local», конструктору необходимо предоставить параметр «scopefunc». Пример ниже иллюстрирует использование функции asyncio.current_task()
для этой цели:
from asyncio import current_task
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.asyncio import async_scoped_session
from sqlalchemy.ext.asyncio import AsyncSession
async_session_factory = sessionmaker(some_async_engine, class_=AsyncSession)
AsyncScopedSession = async_scoped_session(async_session_factory, scopefunc=current_task)
some_async_session = AsyncScopedSession()
Предупреждение
Функция «scopefunc», используемая async_scoped_session
, вызывается произвольное количество раз в рамках задачи, по одному разу при каждом обращении к базовому AsyncSession
. Поэтому функция должна быть эдемпотентной и легковесной, и не должна пытаться создавать или изменять какое-либо состояние, например, устанавливать обратные вызовы и т.д.
Предупреждение
Использование current_task()
для «ключа» в области видимости требует, чтобы метод async_scoped_session.remove()
вызывался из внешнего awaitable, чтобы гарантировать удаление ключа из реестра после завершения задачи, иначе хэндл задачи, а также AsyncSession
останутся в памяти, по сути, создавая утечку памяти. Смотрите следующий пример, который иллюстрирует правильное использование async_scoped_session.remove()
.
async_scoped_session
включает поведение прокси, аналогичное поведению scoped_session
, что означает, что с ним можно обращаться как с AsyncSession
непосредственно, помня о необходимости использования обычных ключевых слов await
, в том числе для метода async_scoped_session.remove()
:
async def some_function(some_async_session, some_object):
# use the AsyncSession directly
some_async_session.add(some_object)
# use the AsyncSession via the context-local proxy
await AsyncScopedSession.commit()
# "remove" the current proxied AsyncSession for the local context
await AsyncScopedSession.remove()
Добавлено в версии 1.4.19.
Использование инспектора для проверки объектов схемы¶
SQLAlchemy пока не предлагает asyncio версию Inspector
(представленную в Мелкозернистое отражение с инспектором), однако существующий интерфейс можно использовать в контексте asyncio, используя метод AsyncConnection.run_sync()
из AsyncConnection
:
import asyncio
from sqlalchemy import inspect
from sqlalchemy.ext.asyncio import create_async_engine
engine = create_async_engine("postgresql+asyncpg://scott:tiger@localhost/test")
def use_inspector(conn):
inspector = inspect(conn)
# use the inspector
print(inspector.get_view_names())
# return any value to the caller
return inspector.get_table_names()
async def async_main():
async with engine.connect() as conn:
tables = await conn.run_sync(use_inspector)
asyncio.run(async_main())
Документация API двигателя¶
Документация API набора результатов¶
Объект AsyncResult
является асинхронной версией объекта Result
. Он возвращается только при использовании методов AsyncConnection.stream()
или AsyncSession.stream()
, которые возвращают объект результата, находящийся поверх активного курсора базы данных.