fastapi-async-sqlalchemy 0.7.0.dev1__tar.gz → 0.7.0.dev2__tar.gz

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.
Files changed (17) hide show
  1. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/PKG-INFO +1 -1
  2. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/fastapi_async_sqlalchemy/__init__.py +1 -1
  3. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/fastapi_async_sqlalchemy/middleware.py +38 -37
  4. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/fastapi_async_sqlalchemy.egg-info/PKG-INFO +1 -1
  5. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/tests/test_session.py +25 -0
  6. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/LICENSE +0 -0
  7. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/README.md +0 -0
  8. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/fastapi_async_sqlalchemy/exceptions.py +0 -0
  9. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/fastapi_async_sqlalchemy/py.typed +0 -0
  10. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/fastapi_async_sqlalchemy.egg-info/SOURCES.txt +0 -0
  11. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/fastapi_async_sqlalchemy.egg-info/dependency_links.txt +0 -0
  12. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/fastapi_async_sqlalchemy.egg-info/not-zip-safe +0 -0
  13. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/fastapi_async_sqlalchemy.egg-info/requires.txt +0 -0
  14. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/fastapi_async_sqlalchemy.egg-info/top_level.txt +0 -0
  15. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/pyproject.toml +0 -0
  16. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/setup.cfg +0 -0
  17. {fastapi_async_sqlalchemy-0.7.0.dev1 → fastapi_async_sqlalchemy-0.7.0.dev2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-async-sqlalchemy
3
- Version: 0.7.0.dev1
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
@@ -2,4 +2,4 @@ from fastapi_async_sqlalchemy.middleware import SQLAlchemyMiddleware, db
2
2
 
3
3
  __all__ = ["db", "SQLAlchemyMiddleware"]
4
4
 
5
- __version__ = "0.7.0.dev1"
5
+ __version__ = "0.7.0.dev2"
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- from asyncio import Task
3
2
  from contextvars import ContextVar
4
3
  from typing import Dict, Optional, Union
5
4
 
@@ -22,6 +21,9 @@ def create_middleware_and_session_proxy():
22
21
  _Session: Optional[async_sessionmaker] = None
23
22
  _session: ContextVar[Optional[AsyncSession]] = ContextVar("_session", default=None)
24
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
+ )
25
27
  _commit_on_exit_ctx: ContextVar[bool] = ContextVar("_commit_on_exit_ctx", default=False)
26
28
  # Usage of context vars inside closures is not recommended, since they are not properly
27
29
  # garbage collected, but in our use case context var is created on program startup and
@@ -90,28 +92,26 @@ def create_middleware_and_session_proxy():
90
92
  ```
91
93
  """
92
94
  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
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
115
  else:
116
116
  session = _session.get()
117
117
  if session is None:
@@ -139,23 +139,24 @@ def create_middleware_and_session_proxy():
139
139
  if self.multi_sessions:
140
140
  self.multi_sessions_token = _multi_sessions_ctx.set(True)
141
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))
142
+ else:
143
+ self.token = _session.set(_Session(**self.session_args))
144
144
  return type(self)
145
145
 
146
146
  async def __aexit__(self, exc_type, exc_value, traceback):
147
- session = _session.get()
148
- try:
149
- if exc_type is not None:
150
- await session.rollback()
151
- elif self.commit_on_exit:
152
- await session.commit()
153
- finally:
154
- await session.close()
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)
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)
159
160
 
160
161
  return SQLAlchemyMiddleware, DBSession
161
162
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-async-sqlalchemy
3
- Version: 0.7.0.dev1
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
@@ -173,3 +173,28 @@ async def test_multi_sessions(app, db, SQLAlchemyMiddleware):
173
173
 
174
174
  res = await asyncio.gather(*tasks)
175
175
  assert len(res) == 6
176
+
177
+
178
+ @pytest.mark.asyncio
179
+ async def test_concurrent_inserts(app, db, SQLAlchemyMiddleware):
180
+ app.add_middleware(SQLAlchemyMiddleware, db_url=db_url)
181
+
182
+ async with db(multi_sessions=True, commit_on_exit=True):
183
+ await db.session.execute(
184
+ text("CREATE TABLE IF NOT EXISTS my_model (id INTEGER PRIMARY KEY, value TEXT)")
185
+ )
186
+
187
+ async def insert_data(value):
188
+ await db.session.execute(
189
+ text("INSERT INTO my_model (value) VALUES (:value)"), {"value": value}
190
+ )
191
+ await db.session.flush()
192
+
193
+ tasks = [asyncio.create_task(insert_data(f"value_{i}")) for i in range(10)]
194
+
195
+ result_ids = await asyncio.gather(*tasks)
196
+ assert len(result_ids) == 10
197
+
198
+ records = await db.session.execute(text("SELECT * FROM my_model"))
199
+ records = records.scalars().all()
200
+ assert len(records) == 10