fastapi-async-sqlalchemy 0.6.0__py3-none-any.whl → 0.7.0.dev1__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.0"
5
+ __version__ = "0.7.0.dev1"
@@ -1,9 +1,11 @@
1
+ import asyncio
2
+ from asyncio import Task
1
3
  from contextvars import ContextVar
2
4
  from typing import Dict, Optional, Union
3
5
 
4
6
  from sqlalchemy.engine import Engine
5
7
  from sqlalchemy.engine.url import URL
6
- from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
8
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
7
9
  from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
8
10
  from starlette.requests import Request
9
11
  from starlette.types import ASGIApp
@@ -11,17 +13,19 @@ from starlette.types import ASGIApp
11
13
  from fastapi_async_sqlalchemy.exceptions import MissingSessionError, SessionNotInitialisedError
12
14
 
13
15
  try:
14
- from sqlalchemy.ext.asyncio import async_sessionmaker
16
+ from sqlalchemy.ext.asyncio import async_sessionmaker # noqa: F811
15
17
  except ImportError:
16
18
  from sqlalchemy.orm import sessionmaker as async_sessionmaker
17
19
 
18
20
 
19
21
  def create_middleware_and_session_proxy():
20
22
  _Session: Optional[async_sessionmaker] = None
23
+ _session: ContextVar[Optional[AsyncSession]] = ContextVar("_session", default=None)
24
+ _multi_sessions_ctx: ContextVar[bool] = ContextVar("_multi_sessions_context", default=False)
25
+ _commit_on_exit_ctx: ContextVar[bool] = ContextVar("_commit_on_exit_ctx", default=False)
21
26
  # Usage of context vars inside closures is not recommended, since they are not properly
22
27
  # garbage collected, but in our use case context var is created on program startup and
23
28
  # is used throughout the whole its lifecycle.
24
- _session: ContextVar[Optional[AsyncSession]] = ContextVar("_session", default=None)
25
29
 
26
30
  class SQLAlchemyMiddleware(BaseHTTPMiddleware):
27
31
  def __init__(
@@ -51,7 +55,7 @@ def create_middleware_and_session_proxy():
51
55
  )
52
56
 
53
57
  async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
54
- async with db(commit_on_exit=self.commit_on_exit):
58
+ async with DBSession(commit_on_exit=self.commit_on_exit):
55
59
  return await call_next(request)
56
60
 
57
61
  class DBSessionMeta(type):
@@ -61,38 +65,97 @@ def create_middleware_and_session_proxy():
61
65
  if _Session is None:
62
66
  raise SessionNotInitialisedError
63
67
 
64
- session = _session.get()
65
- if session is None:
66
- raise MissingSessionError
67
-
68
- return session
68
+ multi_sessions = _multi_sessions_ctx.get()
69
+ if multi_sessions:
70
+ """In this case, we need to create a new session for each task.
71
+ We also need to commit the session on exit if commit_on_exit is True.
72
+ This is useful when we need to run multiple queries in parallel.
73
+ For example, when we need to run multiple queries in parallel in a route handler.
74
+ Example:
75
+ ```python
76
+ async with db(multi_sessions=True):
77
+ async def execute_query(query):
78
+ return await db.session.execute(text(query))
79
+
80
+ tasks = [
81
+ asyncio.create_task(execute_query("SELECT 1")),
82
+ asyncio.create_task(execute_query("SELECT 2")),
83
+ asyncio.create_task(execute_query("SELECT 3")),
84
+ asyncio.create_task(execute_query("SELECT 4")),
85
+ asyncio.create_task(execute_query("SELECT 5")),
86
+ asyncio.create_task(execute_query("SELECT 6")),
87
+ ]
88
+
89
+ await asyncio.gather(*tasks)
90
+ ```
91
+ """
92
+ commit_on_exit = _commit_on_exit_ctx.get()
93
+ task: Task = asyncio.current_task() # type: ignore
94
+ if not hasattr(task, "_db_session"):
95
+ task._db_session = _Session() # type: ignore
96
+
97
+ def cleanup(future):
98
+ session = getattr(task, "_db_session", None)
99
+ if session:
100
+
101
+ async def do_cleanup():
102
+ try:
103
+ if future.exception():
104
+ await session.rollback()
105
+ else:
106
+ if commit_on_exit:
107
+ await session.commit()
108
+ finally:
109
+ await session.close()
110
+
111
+ asyncio.create_task(do_cleanup())
112
+
113
+ task.add_done_callback(cleanup)
114
+ return task._db_session # type: ignore
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
+
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
147
  session = _session.get()
85
-
86
148
  try:
87
149
  if exc_type is not None:
88
150
  await session.rollback()
89
- elif (
90
- self.commit_on_exit
91
- ): # Note: Changed this to elif to avoid commit after rollback
151
+ elif self.commit_on_exit:
92
152
  await session.commit()
93
153
  finally:
94
154
  await session.close()
95
155
  _session.reset(self.token)
156
+ if self.multi_sessions_token is not None:
157
+ _multi_sessions_ctx.reset(self.multi_sessions_token)
158
+ _commit_on_exit_ctx.reset(self.commit_on_exit_token)
96
159
 
97
160
  return SQLAlchemyMiddleware, DBSession
98
161
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-async-sqlalchemy
3
- Version: 0.6.0
3
+ Version: 0.7.0.dev1
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
@@ -37,7 +38,7 @@ Requires-Dist: SQLAlchemy >=1.4.19
37
38
  [![codecov](https://codecov.io/gh/h0rn3t/fastapi-async-sqlalchemy/branch/main/graph/badge.svg?token=F4NJ34WKPY)](https://codecov.io/gh/h0rn3t/fastapi-async-sqlalchemy)
38
39
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
40
  [![pip](https://img.shields.io/pypi/v/fastapi_async_sqlalchemy?color=blue)](https://pypi.org/project/fastapi-async-sqlalchemy/)
40
- [![Downloads](https://pepy.tech/badge/fastapi-async-sqlalchemy)](https://pepy.tech/project/fastapi-async-sqlalchemy)
41
+ [![Downloads](https://static.pepy.tech/badge/fastapi-async-sqlalchemy)](https://pepy.tech/project/fastapi-async-sqlalchemy)
41
42
  [![Updates](https://pyup.io/repos/github/h0rn3t/fastapi-async-sqlalchemy/shield.svg)](https://pyup.io/repos/github/h0rn3t/fastapi-async-sqlalchemy/)
42
43
 
43
44
  ### Description
@@ -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=SssNn_DOQkhXabDQJJdvgRKMvL12CXPZiSqFxl1n1lg,143
2
+ fastapi_async_sqlalchemy/exceptions.py,sha256=dH3xjPE7B6kgq8L3LvxK-MsGZ_ZnW2WH2drLkK2oD-4,887
3
+ fastapi_async_sqlalchemy/middleware.py,sha256=Fk5aAkgqjLH_HWTa-ELlYL3vFlaSlIMyG2xpG7AarDw,6916
4
+ fastapi_async_sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ fastapi_async_sqlalchemy-0.7.0.dev1.dist-info/LICENSE,sha256=OhSfEMY0WqZhq41M6mLmkuFloAG7ZJYKfryF2jyxkfY,1108
6
+ fastapi_async_sqlalchemy-0.7.0.dev1.dist-info/METADATA,sha256=COfeGoIlIPXulpANj8vL4Mtn9rtUerNGQ2G3bD2PanM,6831
7
+ fastapi_async_sqlalchemy-0.7.0.dev1.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
8
+ fastapi_async_sqlalchemy-0.7.0.dev1.dist-info/top_level.txt,sha256=po-P5Tif35GmAaUR0mgZr5uBwN91iAgszaKI_r4ZTZA,25
9
+ fastapi_async_sqlalchemy-0.7.0.dev1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
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=JptqhIDd8BKWzrQAl7spAFKUE6DLarLD0xS_bb4aNXY,138
2
- fastapi_async_sqlalchemy/exceptions.py,sha256=dH3xjPE7B6kgq8L3LvxK-MsGZ_ZnW2WH2drLkK2oD-4,887
3
- fastapi_async_sqlalchemy/middleware.py,sha256=MDaAs4zTB1QprmsHRWxy2Owvvu3epLJ3F0qTngKxKsI,3774
4
- fastapi_async_sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- fastapi_async_sqlalchemy-0.6.0.dist-info/LICENSE,sha256=OhSfEMY0WqZhq41M6mLmkuFloAG7ZJYKfryF2jyxkfY,1108
6
- fastapi_async_sqlalchemy-0.6.0.dist-info/METADATA,sha256=bYj4FTAT4Xc5YahixBx0hOeW-6qJSPhHYbughzpQ67g,6155
7
- fastapi_async_sqlalchemy-0.6.0.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
8
- fastapi_async_sqlalchemy-0.6.0.dist-info/top_level.txt,sha256=po-P5Tif35GmAaUR0mgZr5uBwN91iAgszaKI_r4ZTZA,25
9
- fastapi_async_sqlalchemy-0.6.0.dist-info/RECORD,,