engine.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836
  1. # ext/asyncio/engine.py
  2. # Copyright (C) 2020-2022 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: https://www.opensource.org/licenses/mit-license.php
  7. from . import exc as async_exc
  8. from .base import ProxyComparable
  9. from .base import StartableContext
  10. from .result import AsyncResult
  11. from ... import exc
  12. from ... import inspection
  13. from ... import util
  14. from ...engine import create_engine as _create_engine
  15. from ...engine.base import NestedTransaction
  16. from ...future import Connection
  17. from ...future import Engine
  18. from ...util.concurrency import greenlet_spawn
  19. def create_async_engine(*arg, **kw):
  20. """Create a new async engine instance.
  21. Arguments passed to :func:`_asyncio.create_async_engine` are mostly
  22. identical to those passed to the :func:`_sa.create_engine` function.
  23. The specified dialect must be an asyncio-compatible dialect
  24. such as :ref:`dialect-postgresql-asyncpg`.
  25. .. versionadded:: 1.4
  26. """
  27. if kw.get("server_side_cursors", False):
  28. raise async_exc.AsyncMethodRequired(
  29. "Can't set server_side_cursors for async engine globally; "
  30. "use the connection.stream() method for an async "
  31. "streaming result set"
  32. )
  33. kw["future"] = True
  34. sync_engine = _create_engine(*arg, **kw)
  35. return AsyncEngine(sync_engine)
  36. def async_engine_from_config(configuration, prefix="sqlalchemy.", **kwargs):
  37. """Create a new AsyncEngine instance using a configuration dictionary.
  38. This function is analogous to the :func:`_sa.engine_from_config` function
  39. in SQLAlchemy Core, except that the requested dialect must be an
  40. asyncio-compatible dialect such as :ref:`dialect-postgresql-asyncpg`.
  41. The argument signature of the function is identical to that
  42. of :func:`_sa.engine_from_config`.
  43. .. versionadded:: 1.4.29
  44. """
  45. options = {
  46. key[len(prefix) :]: value
  47. for key, value in configuration.items()
  48. if key.startswith(prefix)
  49. }
  50. options["_coerce_config"] = True
  51. options.update(kwargs)
  52. url = options.pop("url")
  53. return create_async_engine(url, **options)
  54. class AsyncConnectable:
  55. __slots__ = "_slots_dispatch", "__weakref__"
  56. @util.create_proxy_methods(
  57. Connection,
  58. ":class:`_future.Connection`",
  59. ":class:`_asyncio.AsyncConnection`",
  60. classmethods=[],
  61. methods=[],
  62. attributes=[
  63. "closed",
  64. "invalidated",
  65. "dialect",
  66. "default_isolation_level",
  67. ],
  68. )
  69. class AsyncConnection(ProxyComparable, StartableContext, AsyncConnectable):
  70. """An asyncio proxy for a :class:`_engine.Connection`.
  71. :class:`_asyncio.AsyncConnection` is acquired using the
  72. :meth:`_asyncio.AsyncEngine.connect`
  73. method of :class:`_asyncio.AsyncEngine`::
  74. from sqlalchemy.ext.asyncio import create_async_engine
  75. engine = create_async_engine("postgresql+asyncpg://user:pass@host/dbname")
  76. async with engine.connect() as conn:
  77. result = await conn.execute(select(table))
  78. .. versionadded:: 1.4
  79. """ # noqa
  80. # AsyncConnection is a thin proxy; no state should be added here
  81. # that is not retrievable from the "sync" engine / connection, e.g.
  82. # current transaction, info, etc. It should be possible to
  83. # create a new AsyncConnection that matches this one given only the
  84. # "sync" elements.
  85. __slots__ = (
  86. "engine",
  87. "sync_engine",
  88. "sync_connection",
  89. )
  90. def __init__(self, async_engine, sync_connection=None):
  91. self.engine = async_engine
  92. self.sync_engine = async_engine.sync_engine
  93. self.sync_connection = self._assign_proxied(sync_connection)
  94. sync_connection: Connection
  95. """Reference to the sync-style :class:`_engine.Connection` this
  96. :class:`_asyncio.AsyncConnection` proxies requests towards.
  97. This instance can be used as an event target.
  98. .. seealso::
  99. :ref:`asyncio_events`
  100. """
  101. sync_engine: Engine
  102. """Reference to the sync-style :class:`_engine.Engine` this
  103. :class:`_asyncio.AsyncConnection` is associated with via its underlying
  104. :class:`_engine.Connection`.
  105. This instance can be used as an event target.
  106. .. seealso::
  107. :ref:`asyncio_events`
  108. """
  109. @classmethod
  110. def _regenerate_proxy_for_target(cls, target):
  111. return AsyncConnection(
  112. AsyncEngine._retrieve_proxy_for_target(target.engine), target
  113. )
  114. async def start(self, is_ctxmanager=False):
  115. """Start this :class:`_asyncio.AsyncConnection` object's context
  116. outside of using a Python ``with:`` block.
  117. """
  118. if self.sync_connection:
  119. raise exc.InvalidRequestError("connection is already started")
  120. self.sync_connection = self._assign_proxied(
  121. await (greenlet_spawn(self.sync_engine.connect))
  122. )
  123. return self
  124. @property
  125. def connection(self):
  126. """Not implemented for async; call
  127. :meth:`_asyncio.AsyncConnection.get_raw_connection`.
  128. """
  129. raise exc.InvalidRequestError(
  130. "AsyncConnection.connection accessor is not implemented as the "
  131. "attribute may need to reconnect on an invalidated connection. "
  132. "Use the get_raw_connection() method."
  133. )
  134. async def get_raw_connection(self):
  135. """Return the pooled DBAPI-level connection in use by this
  136. :class:`_asyncio.AsyncConnection`.
  137. This is a SQLAlchemy connection-pool proxied connection
  138. which then has the attribute
  139. :attr:`_pool._ConnectionFairy.driver_connection` that refers to the
  140. actual driver connection. Its
  141. :attr:`_pool._ConnectionFairy.dbapi_connection` refers instead
  142. to an :class:`_engine.AdaptedConnection` instance that
  143. adapts the driver connection to the DBAPI protocol.
  144. """
  145. conn = self._sync_connection()
  146. return await greenlet_spawn(getattr, conn, "connection")
  147. @property
  148. def _proxied(self):
  149. return self.sync_connection
  150. @property
  151. def info(self):
  152. """Return the :attr:`_engine.Connection.info` dictionary of the
  153. underlying :class:`_engine.Connection`.
  154. This dictionary is freely writable for user-defined state to be
  155. associated with the database connection.
  156. This attribute is only available if the :class:`.AsyncConnection` is
  157. currently connected. If the :attr:`.AsyncConnection.closed` attribute
  158. is ``True``, then accessing this attribute will raise
  159. :class:`.ResourceClosedError`.
  160. .. versionadded:: 1.4.0b2
  161. """
  162. return self.sync_connection.info
  163. def _sync_connection(self):
  164. if not self.sync_connection:
  165. self._raise_for_not_started()
  166. return self.sync_connection
  167. def begin(self):
  168. """Begin a transaction prior to autobegin occurring."""
  169. self._sync_connection()
  170. return AsyncTransaction(self)
  171. def begin_nested(self):
  172. """Begin a nested transaction and return a transaction handle."""
  173. self._sync_connection()
  174. return AsyncTransaction(self, nested=True)
  175. async def invalidate(self, exception=None):
  176. """Invalidate the underlying DBAPI connection associated with
  177. this :class:`_engine.Connection`.
  178. See the method :meth:`_engine.Connection.invalidate` for full
  179. detail on this method.
  180. """
  181. conn = self._sync_connection()
  182. return await greenlet_spawn(conn.invalidate, exception=exception)
  183. async def get_isolation_level(self):
  184. conn = self._sync_connection()
  185. return await greenlet_spawn(conn.get_isolation_level)
  186. async def set_isolation_level(self):
  187. conn = self._sync_connection()
  188. return await greenlet_spawn(conn.get_isolation_level)
  189. def in_transaction(self):
  190. """Return True if a transaction is in progress.
  191. .. versionadded:: 1.4.0b2
  192. """
  193. conn = self._sync_connection()
  194. return conn.in_transaction()
  195. def in_nested_transaction(self):
  196. """Return True if a transaction is in progress.
  197. .. versionadded:: 1.4.0b2
  198. """
  199. conn = self._sync_connection()
  200. return conn.in_nested_transaction()
  201. def get_transaction(self):
  202. """Return an :class:`.AsyncTransaction` representing the current
  203. transaction, if any.
  204. This makes use of the underlying synchronous connection's
  205. :meth:`_engine.Connection.get_transaction` method to get the current
  206. :class:`_engine.Transaction`, which is then proxied in a new
  207. :class:`.AsyncTransaction` object.
  208. .. versionadded:: 1.4.0b2
  209. """
  210. conn = self._sync_connection()
  211. trans = conn.get_transaction()
  212. if trans is not None:
  213. return AsyncTransaction._retrieve_proxy_for_target(trans)
  214. else:
  215. return None
  216. def get_nested_transaction(self):
  217. """Return an :class:`.AsyncTransaction` representing the current
  218. nested (savepoint) transaction, if any.
  219. This makes use of the underlying synchronous connection's
  220. :meth:`_engine.Connection.get_nested_transaction` method to get the
  221. current :class:`_engine.Transaction`, which is then proxied in a new
  222. :class:`.AsyncTransaction` object.
  223. .. versionadded:: 1.4.0b2
  224. """
  225. conn = self._sync_connection()
  226. trans = conn.get_nested_transaction()
  227. if trans is not None:
  228. return AsyncTransaction._retrieve_proxy_for_target(trans)
  229. else:
  230. return None
  231. async def execution_options(self, **opt):
  232. r"""Set non-SQL options for the connection which take effect
  233. during execution.
  234. This returns this :class:`_asyncio.AsyncConnection` object with
  235. the new options added.
  236. See :meth:`_future.Connection.execution_options` for full details
  237. on this method.
  238. """
  239. conn = self._sync_connection()
  240. c2 = await greenlet_spawn(conn.execution_options, **opt)
  241. assert c2 is conn
  242. return self
  243. async def commit(self):
  244. """Commit the transaction that is currently in progress.
  245. This method commits the current transaction if one has been started.
  246. If no transaction was started, the method has no effect, assuming
  247. the connection is in a non-invalidated state.
  248. A transaction is begun on a :class:`_future.Connection` automatically
  249. whenever a statement is first executed, or when the
  250. :meth:`_future.Connection.begin` method is called.
  251. """
  252. conn = self._sync_connection()
  253. await greenlet_spawn(conn.commit)
  254. async def rollback(self):
  255. """Roll back the transaction that is currently in progress.
  256. This method rolls back the current transaction if one has been started.
  257. If no transaction was started, the method has no effect. If a
  258. transaction was started and the connection is in an invalidated state,
  259. the transaction is cleared using this method.
  260. A transaction is begun on a :class:`_future.Connection` automatically
  261. whenever a statement is first executed, or when the
  262. :meth:`_future.Connection.begin` method is called.
  263. """
  264. conn = self._sync_connection()
  265. await greenlet_spawn(conn.rollback)
  266. async def close(self):
  267. """Close this :class:`_asyncio.AsyncConnection`.
  268. This has the effect of also rolling back the transaction if one
  269. is in place.
  270. """
  271. conn = self._sync_connection()
  272. await greenlet_spawn(conn.close)
  273. async def exec_driver_sql(
  274. self,
  275. statement,
  276. parameters=None,
  277. execution_options=util.EMPTY_DICT,
  278. ):
  279. r"""Executes a driver-level SQL string and return buffered
  280. :class:`_engine.Result`.
  281. """
  282. conn = self._sync_connection()
  283. result = await greenlet_spawn(
  284. conn.exec_driver_sql,
  285. statement,
  286. parameters,
  287. execution_options,
  288. _require_await=True,
  289. )
  290. if result.context._is_server_side:
  291. raise async_exc.AsyncMethodRequired(
  292. "Can't use the connection.exec_driver_sql() method with a "
  293. "server-side cursor."
  294. "Use the connection.stream() method for an async "
  295. "streaming result set."
  296. )
  297. return result
  298. async def stream(
  299. self,
  300. statement,
  301. parameters=None,
  302. execution_options=util.EMPTY_DICT,
  303. ):
  304. """Execute a statement and return a streaming
  305. :class:`_asyncio.AsyncResult` object."""
  306. conn = self._sync_connection()
  307. result = await greenlet_spawn(
  308. conn._execute_20,
  309. statement,
  310. parameters,
  311. util.EMPTY_DICT.merge_with(
  312. execution_options, {"stream_results": True}
  313. ),
  314. _require_await=True,
  315. )
  316. if not result.context._is_server_side:
  317. # TODO: real exception here
  318. assert False, "server side result expected"
  319. return AsyncResult(result)
  320. async def execute(
  321. self,
  322. statement,
  323. parameters=None,
  324. execution_options=util.EMPTY_DICT,
  325. ):
  326. r"""Executes a SQL statement construct and return a buffered
  327. :class:`_engine.Result`.
  328. :param object: The statement to be executed. This is always
  329. an object that is in both the :class:`_expression.ClauseElement` and
  330. :class:`_expression.Executable` hierarchies, including:
  331. * :class:`_expression.Select`
  332. * :class:`_expression.Insert`, :class:`_expression.Update`,
  333. :class:`_expression.Delete`
  334. * :class:`_expression.TextClause` and
  335. :class:`_expression.TextualSelect`
  336. * :class:`_schema.DDL` and objects which inherit from
  337. :class:`_schema.DDLElement`
  338. :param parameters: parameters which will be bound into the statement.
  339. This may be either a dictionary of parameter names to values,
  340. or a mutable sequence (e.g. a list) of dictionaries. When a
  341. list of dictionaries is passed, the underlying statement execution
  342. will make use of the DBAPI ``cursor.executemany()`` method.
  343. When a single dictionary is passed, the DBAPI ``cursor.execute()``
  344. method will be used.
  345. :param execution_options: optional dictionary of execution options,
  346. which will be associated with the statement execution. This
  347. dictionary can provide a subset of the options that are accepted
  348. by :meth:`_future.Connection.execution_options`.
  349. :return: a :class:`_engine.Result` object.
  350. """
  351. conn = self._sync_connection()
  352. result = await greenlet_spawn(
  353. conn._execute_20,
  354. statement,
  355. parameters,
  356. execution_options,
  357. _require_await=True,
  358. )
  359. if result.context._is_server_side:
  360. raise async_exc.AsyncMethodRequired(
  361. "Can't use the connection.execute() method with a "
  362. "server-side cursor."
  363. "Use the connection.stream() method for an async "
  364. "streaming result set."
  365. )
  366. return result
  367. async def scalar(
  368. self,
  369. statement,
  370. parameters=None,
  371. execution_options=util.EMPTY_DICT,
  372. ):
  373. r"""Executes a SQL statement construct and returns a scalar object.
  374. This method is shorthand for invoking the
  375. :meth:`_engine.Result.scalar` method after invoking the
  376. :meth:`_future.Connection.execute` method. Parameters are equivalent.
  377. :return: a scalar Python value representing the first column of the
  378. first row returned.
  379. """
  380. result = await self.execute(statement, parameters, execution_options)
  381. return result.scalar()
  382. async def scalars(
  383. self,
  384. statement,
  385. parameters=None,
  386. execution_options=util.EMPTY_DICT,
  387. ):
  388. r"""Executes a SQL statement construct and returns a scalar objects.
  389. This method is shorthand for invoking the
  390. :meth:`_engine.Result.scalars` method after invoking the
  391. :meth:`_future.Connection.execute` method. Parameters are equivalent.
  392. :return: a :class:`_engine.ScalarResult` object.
  393. .. versionadded:: 1.4.24
  394. """
  395. result = await self.execute(statement, parameters, execution_options)
  396. return result.scalars()
  397. async def stream_scalars(
  398. self,
  399. statement,
  400. parameters=None,
  401. execution_options=util.EMPTY_DICT,
  402. ):
  403. r"""Executes a SQL statement and returns a streaming scalar result
  404. object.
  405. This method is shorthand for invoking the
  406. :meth:`_engine.AsyncResult.scalars` method after invoking the
  407. :meth:`_future.Connection.stream` method. Parameters are equivalent.
  408. :return: an :class:`_asyncio.AsyncScalarResult` object.
  409. .. versionadded:: 1.4.24
  410. """
  411. result = await self.stream(statement, parameters, execution_options)
  412. return result.scalars()
  413. async def run_sync(self, fn, *arg, **kw):
  414. """Invoke the given sync callable passing self as the first argument.
  415. This method maintains the asyncio event loop all the way through
  416. to the database connection by running the given callable in a
  417. specially instrumented greenlet.
  418. E.g.::
  419. with async_engine.begin() as conn:
  420. await conn.run_sync(metadata.create_all)
  421. .. note::
  422. The provided callable is invoked inline within the asyncio event
  423. loop, and will block on traditional IO calls. IO within this
  424. callable should only call into SQLAlchemy's asyncio database
  425. APIs which will be properly adapted to the greenlet context.
  426. .. seealso::
  427. :ref:`session_run_sync`
  428. """
  429. conn = self._sync_connection()
  430. return await greenlet_spawn(fn, conn, *arg, **kw)
  431. def __await__(self):
  432. return self.start().__await__()
  433. async def __aexit__(self, type_, value, traceback):
  434. await self.close()
  435. @util.create_proxy_methods(
  436. Engine,
  437. ":class:`_future.Engine`",
  438. ":class:`_asyncio.AsyncEngine`",
  439. classmethods=[],
  440. methods=[
  441. "clear_compiled_cache",
  442. "update_execution_options",
  443. "get_execution_options",
  444. ],
  445. attributes=["url", "pool", "dialect", "engine", "name", "driver", "echo"],
  446. )
  447. class AsyncEngine(ProxyComparable, AsyncConnectable):
  448. """An asyncio proxy for a :class:`_engine.Engine`.
  449. :class:`_asyncio.AsyncEngine` is acquired using the
  450. :func:`_asyncio.create_async_engine` function::
  451. from sqlalchemy.ext.asyncio import create_async_engine
  452. engine = create_async_engine("postgresql+asyncpg://user:pass@host/dbname")
  453. .. versionadded:: 1.4
  454. """ # noqa
  455. # AsyncEngine is a thin proxy; no state should be added here
  456. # that is not retrievable from the "sync" engine / connection, e.g.
  457. # current transaction, info, etc. It should be possible to
  458. # create a new AsyncEngine that matches this one given only the
  459. # "sync" elements.
  460. __slots__ = ("sync_engine", "_proxied")
  461. _connection_cls = AsyncConnection
  462. _option_cls: type
  463. class _trans_ctx(StartableContext):
  464. def __init__(self, conn):
  465. self.conn = conn
  466. async def start(self, is_ctxmanager=False):
  467. await self.conn.start(is_ctxmanager=is_ctxmanager)
  468. self.transaction = self.conn.begin()
  469. await self.transaction.__aenter__()
  470. return self.conn
  471. async def __aexit__(self, type_, value, traceback):
  472. await self.transaction.__aexit__(type_, value, traceback)
  473. await self.conn.close()
  474. def __init__(self, sync_engine):
  475. if not sync_engine.dialect.is_async:
  476. raise exc.InvalidRequestError(
  477. "The asyncio extension requires an async driver to be used. "
  478. f"The loaded {sync_engine.dialect.driver!r} is not async."
  479. )
  480. self.sync_engine = self._proxied = self._assign_proxied(sync_engine)
  481. sync_engine: Engine
  482. """Reference to the sync-style :class:`_engine.Engine` this
  483. :class:`_asyncio.AsyncEngine` proxies requests towards.
  484. This instance can be used as an event target.
  485. .. seealso::
  486. :ref:`asyncio_events`
  487. """
  488. @classmethod
  489. def _regenerate_proxy_for_target(cls, target):
  490. return AsyncEngine(target)
  491. def begin(self):
  492. """Return a context manager which when entered will deliver an
  493. :class:`_asyncio.AsyncConnection` with an
  494. :class:`_asyncio.AsyncTransaction` established.
  495. E.g.::
  496. async with async_engine.begin() as conn:
  497. await conn.execute(
  498. text("insert into table (x, y, z) values (1, 2, 3)")
  499. )
  500. await conn.execute(text("my_special_procedure(5)"))
  501. """
  502. conn = self.connect()
  503. return self._trans_ctx(conn)
  504. def connect(self):
  505. """Return an :class:`_asyncio.AsyncConnection` object.
  506. The :class:`_asyncio.AsyncConnection` will procure a database
  507. connection from the underlying connection pool when it is entered
  508. as an async context manager::
  509. async with async_engine.connect() as conn:
  510. result = await conn.execute(select(user_table))
  511. The :class:`_asyncio.AsyncConnection` may also be started outside of a
  512. context manager by invoking its :meth:`_asyncio.AsyncConnection.start`
  513. method.
  514. """
  515. return self._connection_cls(self)
  516. async def raw_connection(self):
  517. """Return a "raw" DBAPI connection from the connection pool.
  518. .. seealso::
  519. :ref:`dbapi_connections`
  520. """
  521. return await greenlet_spawn(self.sync_engine.raw_connection)
  522. def execution_options(self, **opt):
  523. """Return a new :class:`_asyncio.AsyncEngine` that will provide
  524. :class:`_asyncio.AsyncConnection` objects with the given execution
  525. options.
  526. Proxied from :meth:`_future.Engine.execution_options`. See that
  527. method for details.
  528. """
  529. return AsyncEngine(self.sync_engine.execution_options(**opt))
  530. async def dispose(self):
  531. """Dispose of the connection pool used by this
  532. :class:`_asyncio.AsyncEngine`.
  533. This will close all connection pool connections that are
  534. **currently checked in**. See the documentation for the underlying
  535. :meth:`_future.Engine.dispose` method for further notes.
  536. .. seealso::
  537. :meth:`_future.Engine.dispose`
  538. """
  539. return await greenlet_spawn(self.sync_engine.dispose)
  540. class AsyncTransaction(ProxyComparable, StartableContext):
  541. """An asyncio proxy for a :class:`_engine.Transaction`."""
  542. __slots__ = ("connection", "sync_transaction", "nested")
  543. def __init__(self, connection, nested=False):
  544. self.connection = connection # AsyncConnection
  545. self.sync_transaction = None # sqlalchemy.engine.Transaction
  546. self.nested = nested
  547. @classmethod
  548. def _regenerate_proxy_for_target(cls, target):
  549. sync_connection = target.connection
  550. sync_transaction = target
  551. nested = isinstance(target, NestedTransaction)
  552. async_connection = AsyncConnection._retrieve_proxy_for_target(
  553. sync_connection
  554. )
  555. assert async_connection is not None
  556. obj = cls.__new__(cls)
  557. obj.connection = async_connection
  558. obj.sync_transaction = obj._assign_proxied(sync_transaction)
  559. obj.nested = nested
  560. return obj
  561. def _sync_transaction(self):
  562. if not self.sync_transaction:
  563. self._raise_for_not_started()
  564. return self.sync_transaction
  565. @property
  566. def _proxied(self):
  567. return self.sync_transaction
  568. @property
  569. def is_valid(self):
  570. return self._sync_transaction().is_valid
  571. @property
  572. def is_active(self):
  573. return self._sync_transaction().is_active
  574. async def close(self):
  575. """Close this :class:`.Transaction`.
  576. If this transaction is the base transaction in a begin/commit
  577. nesting, the transaction will rollback(). Otherwise, the
  578. method returns.
  579. This is used to cancel a Transaction without affecting the scope of
  580. an enclosing transaction.
  581. """
  582. await greenlet_spawn(self._sync_transaction().close)
  583. async def rollback(self):
  584. """Roll back this :class:`.Transaction`."""
  585. await greenlet_spawn(self._sync_transaction().rollback)
  586. async def commit(self):
  587. """Commit this :class:`.Transaction`."""
  588. await greenlet_spawn(self._sync_transaction().commit)
  589. async def start(self, is_ctxmanager=False):
  590. """Start this :class:`_asyncio.AsyncTransaction` object's context
  591. outside of using a Python ``with:`` block.
  592. """
  593. self.sync_transaction = self._assign_proxied(
  594. await greenlet_spawn(
  595. self.connection._sync_connection().begin_nested
  596. if self.nested
  597. else self.connection._sync_connection().begin
  598. )
  599. )
  600. if is_ctxmanager:
  601. self.sync_transaction.__enter__()
  602. return self
  603. async def __aexit__(self, type_, value, traceback):
  604. await greenlet_spawn(
  605. self._sync_transaction().__exit__, type_, value, traceback
  606. )
  607. def _get_sync_engine_or_connection(async_engine):
  608. if isinstance(async_engine, AsyncConnection):
  609. return async_engine.sync_connection
  610. try:
  611. return async_engine.sync_engine
  612. except AttributeError as e:
  613. raise exc.ArgumentError(
  614. "AsyncEngine expected, got %r" % async_engine
  615. ) from e
  616. @inspection._inspects(AsyncConnection)
  617. def _no_insp_for_async_conn_yet(subject):
  618. raise exc.NoInspectionAvailable(
  619. "Inspection on an AsyncConnection is currently not supported. "
  620. "Please use ``run_sync`` to pass a callable where it's possible "
  621. "to call ``inspect`` on the passed connection.",
  622. code="xd3s",
  623. )
  624. @inspection._inspects(AsyncEngine)
  625. def _no_insp_for_async_engine_xyet(subject):
  626. raise exc.NoInspectionAvailable(
  627. "Inspection on an AsyncEngine is currently not supported. "
  628. "Please obtain a connection then use ``conn.run_sync`` to pass a "
  629. "callable where it's possible to call ``inspect`` on the "
  630. "passed connection.",
  631. code="xd3s",
  632. )