|
1 | | -from typing import AsyncGenerator |
| 1 | +from typing import Any, AsyncGenerator, Mapping, Optional, Sequence, TypeVar, Union, overload |
2 | 2 |
|
3 | | -from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine |
| 3 | +from sqlalchemy import util |
| 4 | +from sqlalchemy.ext.asyncio import AsyncSession as _AsyncSession |
| 5 | +from sqlalchemy.ext.asyncio import create_async_engine |
| 6 | +from sqlalchemy.ext.asyncio import engine as _engine |
| 7 | +from sqlalchemy.ext.asyncio.engine import AsyncConnection, AsyncEngine |
4 | 8 | from sqlalchemy.orm import sessionmaker |
5 | | -from sqlmodel import SQLModel |
| 9 | +from sqlalchemy.util.concurrency import greenlet_spawn |
| 10 | +from sqlmodel import Session, SQLModel |
| 11 | +from sqlmodel.engine.result import Result, ScalarResult |
| 12 | +from sqlmodel.sql.base import Executable |
| 13 | +from sqlmodel.sql.expression import Select, SelectOfScalar |
6 | 14 |
|
7 | 15 | from app.core.config import settings |
8 | 16 |
|
| 17 | +_TSelectParam = TypeVar("_TSelectParam") |
| 18 | + |
| 19 | + |
| 20 | +class AsyncSession(_AsyncSession): |
| 21 | + """ |
| 22 | + SQLModel provides a Session wrapper over the regular sqlalchemy session that: |
| 23 | + 1) unwraps the legacy rows-are-returned-as-objects-wrapped-in-tuples behavior if you call "exec" |
| 24 | + 2) passes through Pydantic type hints of the returned objects. |
| 25 | +
|
| 26 | + SQLModel has an equivalent wrapper for AsyncSession (sqlmodel.ext.asyncio.session.AsyncSession), |
| 27 | + but it's busted and throws type errors if you try to use it normally like you would Session. |
| 28 | + There's an upstream PR to fix it, but it's not merged yet. Until it's merged and fixed let's |
| 29 | + just copy-and-paste it in ourselves. |
| 30 | + https://github.com/tiangolo/sqlmodel/pull/58 |
| 31 | + """ |
| 32 | + |
| 33 | + sync_session: Session |
| 34 | + |
| 35 | + def __init__( |
| 36 | + self, |
| 37 | + bind: Optional[Union[AsyncConnection, AsyncEngine]] = None, |
| 38 | + binds: Optional[Mapping[object, Union[AsyncConnection, AsyncEngine]]] = None, |
| 39 | + **kw: Any, |
| 40 | + ): |
| 41 | + # All the same code of the original AsyncSession |
| 42 | + kw["future"] = True |
| 43 | + if bind: |
| 44 | + self.bind = bind |
| 45 | + bind = _engine._get_sync_engine_or_connection(bind) # type: ignore |
| 46 | + |
| 47 | + if binds: |
| 48 | + self.binds = binds |
| 49 | + binds = { |
| 50 | + key: _engine._get_sync_engine_or_connection(b) # type: ignore |
| 51 | + for key, b in binds.items() |
| 52 | + } |
| 53 | + |
| 54 | + self.sync_session = self._proxied = self._assign_proxied( # type: ignore |
| 55 | + Session(bind=bind, binds=binds, **kw) # type: ignore |
| 56 | + ) |
| 57 | + |
| 58 | + @overload |
| 59 | + async def exec( |
| 60 | + self, |
| 61 | + statement: Select[_TSelectParam], |
| 62 | + *, |
| 63 | + params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, |
| 64 | + execution_options: Mapping[str, Any] = util.EMPTY_DICT, |
| 65 | + bind_arguments: Optional[Mapping[str, Any]] = None, |
| 66 | + _parent_execute_state: Optional[Any] = None, |
| 67 | + _add_event: Optional[Any] = None, |
| 68 | + **kw: Any, |
| 69 | + ) -> Result[_TSelectParam]: |
| 70 | + ... |
| 71 | + |
| 72 | + @overload |
| 73 | + async def exec( |
| 74 | + self, |
| 75 | + statement: SelectOfScalar[_TSelectParam], |
| 76 | + *, |
| 77 | + params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, |
| 78 | + execution_options: Mapping[str, Any] = util.EMPTY_DICT, |
| 79 | + bind_arguments: Optional[Mapping[str, Any]] = None, |
| 80 | + _parent_execute_state: Optional[Any] = None, |
| 81 | + _add_event: Optional[Any] = None, |
| 82 | + **kw: Any, |
| 83 | + ) -> ScalarResult[_TSelectParam]: |
| 84 | + ... |
| 85 | + |
| 86 | + async def exec( # type: ignore |
| 87 | + self, |
| 88 | + statement: Union[ |
| 89 | + Select[_TSelectParam], |
| 90 | + SelectOfScalar[_TSelectParam], |
| 91 | + Executable[_TSelectParam], |
| 92 | + ], |
| 93 | + params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, |
| 94 | + execution_options: Mapping[Any, Any] = util.EMPTY_DICT, |
| 95 | + bind_arguments: Optional[Mapping[str, Any]] = None, |
| 96 | + **kw: Any, |
| 97 | + ) -> ScalarResult[_TSelectParam]: |
| 98 | + # TODO: the documentation says execution_options accepts a dict, but only |
| 99 | + # util.immutabledict has the union() method. Is this a bug in SQLAlchemy? |
| 100 | + execution_options = execution_options.union({"prebuffer_rows": True}) # type: ignore |
| 101 | + |
| 102 | + return await greenlet_spawn( |
| 103 | + self.sync_session.exec, |
| 104 | + statement, |
| 105 | + params=params, |
| 106 | + execution_options=execution_options, |
| 107 | + bind_arguments=bind_arguments, |
| 108 | + **kw, |
| 109 | + ) |
| 110 | + |
| 111 | + async def __aenter__(self) -> "AsyncSession": |
| 112 | + # PyCharm does not understand TypeVar here :/ |
| 113 | + return await super().__aenter__() |
| 114 | + |
| 115 | + |
9 | 116 | engine = create_async_engine(settings.db_uri(), **settings.db_engine_args()) |
10 | 117 | async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) |
11 | 118 |
|
|
0 commit comments