fastapi-async-sqlalchemy 0.6.1__py3-none-any.whl → 0.7.0.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,4 +2,4 @@ from fastapi_async_sqlalchemy.middleware import SQLAlchemyMiddleware, db
2
2
 
3
3
  __all__ = ["db", "SQLAlchemyMiddleware"]
4
4
 
5
- __version__ = "0.6.1"
5
+ __version__ = "0.7.0.dev2"
@@ -1,9 +1,10 @@
1
+ import asyncio
1
2
  from contextvars import ContextVar
2
3
  from typing import Dict, Optional, Union
3
4
 
4
5
  from sqlalchemy.engine import Engine
5
6
  from sqlalchemy.engine.url import URL
6
- from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
7
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
7
8
  from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
8
9
  from starlette.requests import Request
9
10
  from starlette.types import ASGIApp
@@ -11,17 +12,22 @@ from starlette.types import ASGIApp
11
12
  from fastapi_async_sqlalchemy.exceptions import MissingSessionError, SessionNotInitialisedError
12
13
 
13
14
  try:
14
- from sqlalchemy.ext.asyncio import async_sessionmaker
15
+ from sqlalchemy.ext.asyncio import async_sessionmaker # noqa: F811
15
16
  except ImportError:
16
17
  from sqlalchemy.orm import sessionmaker as async_sessionmaker
17
18
 
18
19
 
19
20
  def create_middleware_and_session_proxy():
20
21
  _Session: Optional[async_sessionmaker] = None
22
+ _session: ContextVar[Optional[AsyncSession]] = ContextVar("_session", default=None)
23
+ _multi_sessions_ctx: ContextVar[bool] = ContextVar("_multi_sessions_context", default=False)
24
+ _task_session_ctx: ContextVar[Optional[AsyncSession]] = ContextVar(
25
+ "_task_session_ctx", default=None
26
+ )
27
+ _commit_on_exit_ctx: ContextVar[bool] = ContextVar("_commit_on_exit_ctx", default=False)
21
28
  # Usage of context vars inside closures is not recommended, since they are not properly
22
29
  # garbage collected, but in our use case context var is created on program startup and
23
30
  # is used throughout the whole its lifecycle.
24
- _session: ContextVar[Optional[AsyncSession]] = ContextVar("_session", default=None)
25
31
 
26
32
  class SQLAlchemyMiddleware(BaseHTTPMiddleware):
27
33
  def __init__(
@@ -61,38 +67,96 @@ def create_middleware_and_session_proxy():
61
67
  if _Session is None:
62
68
  raise SessionNotInitialisedError
63
69
 
64
- session = _session.get()
65
- if session is None:
66
- raise MissingSessionError
67
-
68
- return session
70
+ multi_sessions = _multi_sessions_ctx.get()
71
+ if multi_sessions:
72
+ """In this case, we need to create a new session for each task.
73
+ We also need to commit the session on exit if commit_on_exit is True.
74
+ This is useful when we need to run multiple queries in parallel.
75
+ For example, when we need to run multiple queries in parallel in a route handler.
76
+ Example:
77
+ ```python
78
+ async with db(multi_sessions=True):
79
+ async def execute_query(query):
80
+ return await db.session.execute(text(query))
81
+
82
+ tasks = [
83
+ asyncio.create_task(execute_query("SELECT 1")),
84
+ asyncio.create_task(execute_query("SELECT 2")),
85
+ asyncio.create_task(execute_query("SELECT 3")),
86
+ asyncio.create_task(execute_query("SELECT 4")),
87
+ asyncio.create_task(execute_query("SELECT 5")),
88
+ asyncio.create_task(execute_query("SELECT 6")),
89
+ ]
90
+
91
+ await asyncio.gather(*tasks)
92
+ ```
93
+ """
94
+ commit_on_exit = _commit_on_exit_ctx.get()
95
+ session = _task_session_ctx.get()
96
+ if session is None:
97
+ session = _Session()
98
+ _task_session_ctx.set(session)
99
+
100
+ async def cleanup():
101
+ try:
102
+ if commit_on_exit:
103
+ await session.commit()
104
+ except Exception:
105
+ await session.rollback()
106
+ raise
107
+ finally:
108
+ await session.close()
109
+ _task_session_ctx.set(None)
110
+
111
+ task = asyncio.current_task()
112
+ if task is not None:
113
+ task.add_done_callback(lambda t: asyncio.create_task(cleanup()))
114
+ return session
115
+ else:
116
+ session = _session.get()
117
+ if session is None:
118
+ raise MissingSessionError
119
+ return session
69
120
 
70
121
  class DBSession(metaclass=DBSessionMeta):
71
- def __init__(self, session_args: Dict = None, commit_on_exit: bool = False):
122
+ def __init__(
123
+ self,
124
+ session_args: Dict = None,
125
+ commit_on_exit: bool = False,
126
+ multi_sessions: bool = False,
127
+ ):
72
128
  self.token = None
129
+ self.multi_sessions_token = None
130
+ self.commit_on_exit_token = None
73
131
  self.session_args = session_args or {}
74
132
  self.commit_on_exit = commit_on_exit
133
+ self.multi_sessions = multi_sessions
75
134
 
76
135
  async def __aenter__(self):
77
136
  if not isinstance(_Session, async_sessionmaker):
78
137
  raise SessionNotInitialisedError
79
138
 
80
- self.token = _session.set(_Session(**self.session_args)) # type: ignore
139
+ if self.multi_sessions:
140
+ self.multi_sessions_token = _multi_sessions_ctx.set(True)
141
+ self.commit_on_exit_token = _commit_on_exit_ctx.set(self.commit_on_exit)
142
+ else:
143
+ self.token = _session.set(_Session(**self.session_args))
81
144
  return type(self)
82
145
 
83
146
  async def __aexit__(self, exc_type, exc_value, traceback):
84
- session = _session.get()
85
-
86
- try:
87
- if exc_type is not None:
88
- await session.rollback()
89
- elif (
90
- self.commit_on_exit
91
- ): # Note: Changed this to elif to avoid commit after rollback
92
- await session.commit()
93
- finally:
94
- await session.close()
95
- _session.reset(self.token)
147
+ if self.multi_sessions:
148
+ _multi_sessions_ctx.reset(self.multi_sessions_token)
149
+ _commit_on_exit_ctx.reset(self.commit_on_exit_token)
150
+ else:
151
+ session = _session.get()
152
+ try:
153
+ if exc_type is not None:
154
+ await session.rollback()
155
+ elif self.commit_on_exit:
156
+ await session.commit()
157
+ finally:
158
+ await session.close()
159
+ _session.reset(self.token)
96
160
 
97
161
  return SQLAlchemyMiddleware, DBSession
98
162
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-async-sqlalchemy
3
- Version: 0.6.1
3
+ Version: 0.7.0.dev2
4
4
  Summary: SQLAlchemy middleware for FastAPI
5
5
  Home-page: https://github.com/h0rn3t/fastapi-async-sqlalchemy.git
6
6
  Author: Eugene Shershen
@@ -8,7 +8,7 @@ Author-email: h0rn3t.null@gmail.com
8
8
  License: MIT
9
9
  Project-URL: Code, https://github.com/h0rn3t/fastapi-async-sqlalchemy
10
10
  Project-URL: Issue tracker, https://github.com/h0rn3t/fastapi-async-sqlalchemy/issues
11
- Classifier: Development Status :: 4 - Beta
11
+ Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Environment :: Web Environment
13
13
  Classifier: Framework :: AsyncIO
14
14
  Classifier: Intended Audience :: Developers
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.8
19
19
  Classifier: Programming Language :: Python :: 3.9
20
20
  Classifier: Programming Language :: Python :: 3.10
21
21
  Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
22
23
  Classifier: Programming Language :: Python :: 3 :: Only
23
24
  Classifier: Programming Language :: Python :: Implementation :: CPython
24
25
  Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
@@ -50,8 +51,8 @@ Provides SQLAlchemy middleware for FastAPI using AsyncSession and async engine.
50
51
  pip install fastapi-async-sqlalchemy
51
52
  ```
52
53
 
53
- ### Important !!!
54
- If you use ```sqlmodel``` install ```sqlalchemy<=1.4.41```
54
+
55
+ It also works with ```sqlmodel```
55
56
 
56
57
 
57
58
  ### Examples
@@ -159,9 +160,10 @@ app.add_middleware(
159
160
  routes.py
160
161
 
161
162
  ```python
163
+ import asyncio
164
+
162
165
  from fastapi import APIRouter
163
- from sqlalchemy import column
164
- from sqlalchemy import table
166
+ from sqlalchemy import column, table, text
165
167
 
166
168
  from databases import first_db, second_db
167
169
 
@@ -179,4 +181,22 @@ async def get_files_from_first_db():
179
181
  async def get_files_from_second_db():
180
182
  result = await second_db.session.execute(foo.select())
181
183
  return result.fetchall()
184
+
185
+
186
+ @router.get("/concurrent-queries")
187
+ async def parallel_select():
188
+ async with first_db(multi_sessions=True):
189
+ async def execute_query(query):
190
+ return await first_db.session.execute(text(query))
191
+
192
+ tasks = [
193
+ asyncio.create_task(execute_query("SELECT 1")),
194
+ asyncio.create_task(execute_query("SELECT 2")),
195
+ asyncio.create_task(execute_query("SELECT 3")),
196
+ asyncio.create_task(execute_query("SELECT 4")),
197
+ asyncio.create_task(execute_query("SELECT 5")),
198
+ asyncio.create_task(execute_query("SELECT 6")),
199
+ ]
200
+
201
+ await asyncio.gather(*tasks)
182
202
  ```
@@ -0,0 +1,9 @@
1
+ fastapi_async_sqlalchemy/__init__.py,sha256=uBiMzBBYVuNC52ekhbIasBTLsIhSvVyXY2KXD8nOWuQ,143
2
+ fastapi_async_sqlalchemy/exceptions.py,sha256=dH3xjPE7B6kgq8L3LvxK-MsGZ_ZnW2WH2drLkK2oD-4,887
3
+ fastapi_async_sqlalchemy/middleware.py,sha256=z3lPmnIihKyRfy34qE2z-38KuxFSXnVVcy_rg_S6IwM,6900
4
+ fastapi_async_sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ fastapi_async_sqlalchemy-0.7.0.dev2.dist-info/LICENSE,sha256=OhSfEMY0WqZhq41M6mLmkuFloAG7ZJYKfryF2jyxkfY,1108
6
+ fastapi_async_sqlalchemy-0.7.0.dev2.dist-info/METADATA,sha256=ANbR70mznxKwilOJ4OmYPVQ0OjmntfTdXr-SFcbL2eU,6831
7
+ fastapi_async_sqlalchemy-0.7.0.dev2.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
8
+ fastapi_async_sqlalchemy-0.7.0.dev2.dist-info/top_level.txt,sha256=po-P5Tif35GmAaUR0mgZr5uBwN91iAgszaKI_r4ZTZA,25
9
+ fastapi_async_sqlalchemy-0.7.0.dev2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.42.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,9 +0,0 @@
1
- fastapi_async_sqlalchemy/__init__.py,sha256=9-OgxL-kLIvuXMLhRxyBsGN02_KN_CuINboiWANtvtg,138
2
- fastapi_async_sqlalchemy/exceptions.py,sha256=dH3xjPE7B6kgq8L3LvxK-MsGZ_ZnW2WH2drLkK2oD-4,887
3
- fastapi_async_sqlalchemy/middleware.py,sha256=pZLizk7Gvqf6X3FB83vlORL-aaA_gobPkFpGEGL9xF4,3781
4
- fastapi_async_sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- fastapi_async_sqlalchemy-0.6.1.dist-info/LICENSE,sha256=OhSfEMY0WqZhq41M6mLmkuFloAG7ZJYKfryF2jyxkfY,1108
6
- fastapi_async_sqlalchemy-0.6.1.dist-info/METADATA,sha256=CFYtBOEhWWl5_S9WIb38KR-DTIHI0RBnmOkT5J2eCGI,6162
7
- fastapi_async_sqlalchemy-0.6.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
8
- fastapi_async_sqlalchemy-0.6.1.dist-info/top_level.txt,sha256=po-P5Tif35GmAaUR0mgZr5uBwN91iAgszaKI_r4ZTZA,25
9
- fastapi_async_sqlalchemy-0.6.1.dist-info/RECORD,,