You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

500 lines
15KB

  1. # ext/asyncio/session.py
  2. # Copyright (C) 2020-2021 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: http://www.opensource.org/licenses/mit-license.php
  7. from . import engine
  8. from . import result as _result
  9. from .base import ReversibleProxy
  10. from .base import StartableContext
  11. from ... import util
  12. from ...orm import object_session
  13. from ...orm import Session
  14. from ...orm import state as _instance_state
  15. from ...util.concurrency import greenlet_spawn
  16. @util.create_proxy_methods(
  17. Session,
  18. ":class:`_orm.Session`",
  19. ":class:`_asyncio.AsyncSession`",
  20. classmethods=["object_session", "identity_key"],
  21. methods=[
  22. "__contains__",
  23. "__iter__",
  24. "add",
  25. "add_all",
  26. "expire",
  27. "expire_all",
  28. "expunge",
  29. "expunge_all",
  30. "get_bind",
  31. "is_modified",
  32. "in_transaction",
  33. "in_nested_transaction",
  34. ],
  35. attributes=[
  36. "dirty",
  37. "deleted",
  38. "new",
  39. "identity_map",
  40. "is_active",
  41. "autoflush",
  42. "no_autoflush",
  43. "info",
  44. ],
  45. )
  46. class AsyncSession(ReversibleProxy):
  47. """Asyncio version of :class:`_orm.Session`.
  48. .. versionadded:: 1.4
  49. """
  50. __slots__ = (
  51. "binds",
  52. "bind",
  53. "sync_session",
  54. "_proxied",
  55. "_slots_dispatch",
  56. )
  57. dispatch = None
  58. def __init__(self, bind=None, binds=None, **kw):
  59. kw["future"] = True
  60. if bind:
  61. self.bind = bind
  62. bind = engine._get_sync_engine_or_connection(bind)
  63. if binds:
  64. self.binds = binds
  65. binds = {
  66. key: engine._get_sync_engine_or_connection(b)
  67. for key, b in binds.items()
  68. }
  69. self.sync_session = self._proxied = self._assign_proxied(
  70. Session(bind=bind, binds=binds, **kw)
  71. )
  72. async def refresh(
  73. self, instance, attribute_names=None, with_for_update=None
  74. ):
  75. """Expire and refresh the attributes on the given instance.
  76. A query will be issued to the database and all attributes will be
  77. refreshed with their current database value.
  78. This is the async version of the :meth:`_orm.Session.refresh` method.
  79. See that method for a complete description of all options.
  80. """
  81. return await greenlet_spawn(
  82. self.sync_session.refresh,
  83. instance,
  84. attribute_names=attribute_names,
  85. with_for_update=with_for_update,
  86. )
  87. async def run_sync(self, fn, *arg, **kw):
  88. """Invoke the given sync callable passing sync self as the first
  89. argument.
  90. This method maintains the asyncio event loop all the way through
  91. to the database connection by running the given callable in a
  92. specially instrumented greenlet.
  93. E.g.::
  94. with AsyncSession(async_engine) as session:
  95. await session.run_sync(some_business_method)
  96. .. note::
  97. The provided callable is invoked inline within the asyncio event
  98. loop, and will block on traditional IO calls. IO within this
  99. callable should only call into SQLAlchemy's asyncio database
  100. APIs which will be properly adapted to the greenlet context.
  101. .. seealso::
  102. :ref:`session_run_sync`
  103. """
  104. return await greenlet_spawn(fn, self.sync_session, *arg, **kw)
  105. async def execute(
  106. self,
  107. statement,
  108. params=None,
  109. execution_options=util.EMPTY_DICT,
  110. bind_arguments=None,
  111. **kw
  112. ):
  113. """Execute a statement and return a buffered
  114. :class:`_engine.Result` object."""
  115. execution_options = execution_options.union({"prebuffer_rows": True})
  116. return await greenlet_spawn(
  117. self.sync_session.execute,
  118. statement,
  119. params=params,
  120. execution_options=execution_options,
  121. bind_arguments=bind_arguments,
  122. **kw
  123. )
  124. async def scalar(
  125. self,
  126. statement,
  127. params=None,
  128. execution_options=util.EMPTY_DICT,
  129. bind_arguments=None,
  130. **kw
  131. ):
  132. """Execute a statement and return a scalar result."""
  133. result = await self.execute(
  134. statement,
  135. params=params,
  136. execution_options=execution_options,
  137. bind_arguments=bind_arguments,
  138. **kw
  139. )
  140. return result.scalar()
  141. async def get(
  142. self,
  143. entity,
  144. ident,
  145. options=None,
  146. populate_existing=False,
  147. with_for_update=None,
  148. identity_token=None,
  149. ):
  150. """Return an instance based on the given primary key identifier,
  151. or ``None`` if not found.
  152. """
  153. return await greenlet_spawn(
  154. self.sync_session.get,
  155. entity,
  156. ident,
  157. options=options,
  158. populate_existing=populate_existing,
  159. with_for_update=with_for_update,
  160. identity_token=identity_token,
  161. )
  162. async def stream(
  163. self,
  164. statement,
  165. params=None,
  166. execution_options=util.EMPTY_DICT,
  167. bind_arguments=None,
  168. **kw
  169. ):
  170. """Execute a statement and return a streaming
  171. :class:`_asyncio.AsyncResult` object."""
  172. execution_options = execution_options.union({"stream_results": True})
  173. result = await greenlet_spawn(
  174. self.sync_session.execute,
  175. statement,
  176. params=params,
  177. execution_options=execution_options,
  178. bind_arguments=bind_arguments,
  179. **kw
  180. )
  181. return _result.AsyncResult(result)
  182. async def delete(self, instance):
  183. """Mark an instance as deleted.
  184. The database delete operation occurs upon ``flush()``.
  185. As this operation may need to cascade along unloaded relationships,
  186. it is awaitable to allow for those queries to take place.
  187. """
  188. return await greenlet_spawn(self.sync_session.delete, instance)
  189. async def merge(self, instance, load=True):
  190. """Copy the state of a given instance into a corresponding instance
  191. within this :class:`_asyncio.AsyncSession`.
  192. """
  193. return await greenlet_spawn(
  194. self.sync_session.merge, instance, load=load
  195. )
  196. async def flush(self, objects=None):
  197. """Flush all the object changes to the database.
  198. .. seealso::
  199. :meth:`_orm.Session.flush`
  200. """
  201. await greenlet_spawn(self.sync_session.flush, objects=objects)
  202. def get_transaction(self):
  203. """Return the current root transaction in progress, if any.
  204. :return: an :class:`_asyncio.AsyncSessionTransaction` object, or
  205. ``None``.
  206. .. versionadded:: 1.4.18
  207. """
  208. trans = self.sync_session.get_transaction()
  209. if trans is not None:
  210. return AsyncSessionTransaction._retrieve_proxy_for_target(trans)
  211. else:
  212. return None
  213. def get_nested_transaction(self):
  214. """Return the current nested transaction in progress, if any.
  215. :return: an :class:`_asyncio.AsyncSessionTransaction` object, or
  216. ``None``.
  217. .. versionadded:: 1.4.18
  218. """
  219. trans = self.sync_session.get_nested_transaction()
  220. if trans is not None:
  221. return AsyncSessionTransaction._retrieve_proxy_for_target(trans)
  222. else:
  223. return None
  224. async def connection(self):
  225. r"""Return a :class:`_asyncio.AsyncConnection` object corresponding to
  226. this :class:`.Session` object's transactional state.
  227. """
  228. sync_connection = await greenlet_spawn(self.sync_session.connection)
  229. return engine.AsyncConnection._retrieve_proxy_for_target(
  230. sync_connection
  231. )
  232. def begin(self, **kw):
  233. """Return an :class:`_asyncio.AsyncSessionTransaction` object.
  234. The underlying :class:`_orm.Session` will perform the
  235. "begin" action when the :class:`_asyncio.AsyncSessionTransaction`
  236. object is entered::
  237. async with async_session.begin():
  238. # .. ORM transaction is begun
  239. Note that database IO will not normally occur when the session-level
  240. transaction is begun, as database transactions begin on an
  241. on-demand basis. However, the begin block is async to accommodate
  242. for a :meth:`_orm.SessionEvents.after_transaction_create`
  243. event hook that may perform IO.
  244. For a general description of ORM begin, see
  245. :meth:`_orm.Session.begin`.
  246. """
  247. return AsyncSessionTransaction(self)
  248. def begin_nested(self, **kw):
  249. """Return an :class:`_asyncio.AsyncSessionTransaction` object
  250. which will begin a "nested" transaction, e.g. SAVEPOINT.
  251. Behavior is the same as that of :meth:`_asyncio.AsyncSession.begin`.
  252. For a general description of ORM begin nested, see
  253. :meth:`_orm.Session.begin_nested`.
  254. """
  255. return AsyncSessionTransaction(self, nested=True)
  256. async def rollback(self):
  257. """Rollback the current transaction in progress."""
  258. return await greenlet_spawn(self.sync_session.rollback)
  259. async def commit(self):
  260. """Commit the current transaction in progress."""
  261. return await greenlet_spawn(self.sync_session.commit)
  262. async def close(self):
  263. """Close out the transactional resources and ORM objects used by this
  264. :class:`_asyncio.AsyncSession`.
  265. This expunges all ORM objects associated with this
  266. :class:`_asyncio.AsyncSession`, ends any transaction in progress and
  267. :term:`releases` any :class:`_asyncio.AsyncConnection` objects which
  268. this :class:`_asyncio.AsyncSession` itself has checked out from
  269. associated :class:`_asyncio.AsyncEngine` objects. The operation then
  270. leaves the :class:`_asyncio.AsyncSession` in a state which it may be
  271. used again.
  272. .. tip::
  273. The :meth:`_asyncio.AsyncSession.close` method **does not prevent
  274. the Session from being used again**. The
  275. :class:`_asyncio.AsyncSession` itself does not actually have a
  276. distinct "closed" state; it merely means the
  277. :class:`_asyncio.AsyncSession` will release all database
  278. connections and ORM objects.
  279. .. seealso::
  280. :ref:`session_closing` - detail on the semantics of
  281. :meth:`_asyncio.AsyncSession.close`
  282. """
  283. return await greenlet_spawn(self.sync_session.close)
  284. @classmethod
  285. async def close_all(self):
  286. """Close all :class:`_asyncio.AsyncSession` sessions."""
  287. return await greenlet_spawn(self.sync_session.close_all)
  288. async def __aenter__(self):
  289. return self
  290. async def __aexit__(self, type_, value, traceback):
  291. await self.close()
  292. def _maker_context_manager(self):
  293. # no @contextlib.asynccontextmanager until python3.7, gr
  294. return _AsyncSessionContextManager(self)
  295. class _AsyncSessionContextManager:
  296. def __init__(self, async_session):
  297. self.async_session = async_session
  298. async def __aenter__(self):
  299. self.trans = self.async_session.begin()
  300. await self.trans.__aenter__()
  301. return self.async_session
  302. async def __aexit__(self, type_, value, traceback):
  303. await self.trans.__aexit__(type_, value, traceback)
  304. await self.async_session.__aexit__(type_, value, traceback)
  305. class AsyncSessionTransaction(ReversibleProxy, StartableContext):
  306. """A wrapper for the ORM :class:`_orm.SessionTransaction` object.
  307. This object is provided so that a transaction-holding object
  308. for the :meth:`_asyncio.AsyncSession.begin` may be returned.
  309. The object supports both explicit calls to
  310. :meth:`_asyncio.AsyncSessionTransaction.commit` and
  311. :meth:`_asyncio.AsyncSessionTransaction.rollback`, as well as use as an
  312. async context manager.
  313. .. versionadded:: 1.4
  314. """
  315. __slots__ = ("session", "sync_transaction", "nested")
  316. def __init__(self, session, nested=False):
  317. self.session = session
  318. self.nested = nested
  319. self.sync_transaction = None
  320. @property
  321. def is_active(self):
  322. return (
  323. self._sync_transaction() is not None
  324. and self._sync_transaction().is_active
  325. )
  326. def _sync_transaction(self):
  327. if not self.sync_transaction:
  328. self._raise_for_not_started()
  329. return self.sync_transaction
  330. async def rollback(self):
  331. """Roll back this :class:`_asyncio.AsyncTransaction`."""
  332. await greenlet_spawn(self._sync_transaction().rollback)
  333. async def commit(self):
  334. """Commit this :class:`_asyncio.AsyncTransaction`."""
  335. await greenlet_spawn(self._sync_transaction().commit)
  336. async def start(self, is_ctxmanager=False):
  337. self.sync_transaction = self._assign_proxied(
  338. await greenlet_spawn(
  339. self.session.sync_session.begin_nested
  340. if self.nested
  341. else self.session.sync_session.begin
  342. )
  343. )
  344. if is_ctxmanager:
  345. self.sync_transaction.__enter__()
  346. return self
  347. async def __aexit__(self, type_, value, traceback):
  348. await greenlet_spawn(
  349. self._sync_transaction().__exit__, type_, value, traceback
  350. )
  351. def async_object_session(instance):
  352. """Return the :class:`_asyncio.AsyncSession` to which the given instance
  353. belongs.
  354. This function makes use of the sync-API function
  355. :class:`_orm.object_session` to retrieve the :class:`_orm.Session` which
  356. refers to the given instance, and from there links it to the original
  357. :class:`_asyncio.AsyncSession`.
  358. If the :class:`_asyncio.AsyncSession` has been garbage collected, the
  359. return value is ``None``.
  360. This functionality is also available from the
  361. :attr:`_orm.InstanceState.async_session` accessor.
  362. :param instance: an ORM mapped instance
  363. :return: an :class:`_asyncio.AsyncSession` object, or ``None``.
  364. .. versionadded:: 1.4.18
  365. """
  366. session = object_session(instance)
  367. if session is not None:
  368. return async_session(session)
  369. else:
  370. return None
  371. def async_session(session):
  372. """Return the :class:`_asyncio.AsyncSession` which is proxying the given
  373. :class:`_orm.Session` object, if any.
  374. :param session: a :class:`_orm.Session` instance.
  375. :return: a :class:`_asyncio.AsyncSession` instance, or ``None``.
  376. .. versionadded:: 1.4.18
  377. """
  378. return AsyncSession._retrieve_proxy_for_target(session, regenerate=False)
  379. _instance_state._async_provider = async_session