Skip to content

Commit 716cf3e

Browse files
committed
atomic_db_session and set_test_context
1 parent a843ed2 commit 716cf3e

File tree

11 files changed

+120
-26
lines changed

11 files changed

+120
-26
lines changed

context_async_sqlalchemy/session.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from contextlib import asynccontextmanager
2-
from typing import AsyncGenerator
2+
from typing import AsyncGenerator, Literal
33

44
from sqlalchemy.ext.asyncio import AsyncSession
55

@@ -26,13 +26,31 @@ async def db_session(connect: DBConnect) -> AsyncSession:
2626
return session
2727

2828

29+
_current_transaction_choices = Literal[
30+
"commit",
31+
"rollback",
32+
"append",
33+
"raise",
34+
]
35+
36+
2937
@asynccontextmanager
3038
async def atomic_db_session(
3139
connect: DBConnect,
40+
current_transaction: _current_transaction_choices = "commit",
3241
) -> AsyncGenerator[AsyncSession, None]:
3342
"""
34-
Autocommit or autorollback in place to avoid waiting for the end of the
35-
request.
43+
A context manager that can be used to wrap another function which
44+
uses a context session, making that call isolated within its
45+
own transaction.
46+
47+
There are several options that define how the function will handle
48+
an already open transaction.
49+
current_transaction:
50+
"commit" - commits the open transaction and starts a new one
51+
"rollback" - rolls back the open transaction and starts a new one
52+
"append" - continues using the current transaction and commits it
53+
"raise" - raises an InvalidRequestError
3654
3755
example of use:
3856
async with atomic_db_session(connect) as session
@@ -42,9 +60,22 @@ async def atomic_db_session(
4260
"""
4361
session = await db_session(connect)
4462
if session.in_transaction():
45-
await session.commit()
46-
async with session.begin():
47-
yield session
63+
if current_transaction == "commit":
64+
await session.commit()
65+
if current_transaction == "rollback":
66+
await session.rollback()
67+
68+
if current_transaction == "append":
69+
try:
70+
yield session
71+
except Exception:
72+
await session.rollback()
73+
raise
74+
else:
75+
await session.commit()
76+
else:
77+
async with session.begin():
78+
yield session
4879

4980

5081
async def commit_db_session(connect: DBConnect) -> None:

context_async_sqlalchemy/test_utils.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from sqlalchemy.ext.asyncio import AsyncSession
77

88
from context_async_sqlalchemy import (
9+
commit_all_sessions,
910
DBConnect,
1011
init_db_session_ctx,
1112
put_db_session_to_context,
@@ -27,19 +28,27 @@ async def rollback_session(
2728

2829

2930
@asynccontextmanager
30-
async def set_test_context() -> AsyncGenerator[None]:
31+
async def set_test_context(auto_close: bool = False) -> AsyncGenerator[None]:
3132
"""
32-
Opens a context similar to middleware, but doesn't commit or
33-
rollback automatically. This task falls to the fixture in tests.
33+
Opens a context similar to middleware.
34+
35+
Use auto_close=False if you’re using a test session and transaction
36+
that you close manually elsewhere in your code.
37+
38+
Use auto_close=True if, for example, you want to call a
39+
function in a test that uses a context bypassing the
40+
middleware, and you want all sessions to be closed automatically.
3441
"""
3542
token = init_db_session_ctx()
3643
try:
3744
yield
3845
finally:
46+
if auto_close:
47+
await commit_all_sessions()
3948
await reset_db_session_ctx(
4049
token,
41-
# Don't close the session here, as you opened in fixture.
42-
with_close=False,
50+
# Don't close the session here if you opened in fixture.
51+
with_close=auto_close,
4352
)
4453

4554

docs/api/index.html

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,10 +298,18 @@ <h3 id="atomic_db_session">atomic_db_session</h3>
298298
<pre><code class="language-python">@asynccontextmanager
299299
async def atomic_db_session(
300300
connect: DBConnect,
301+
current_transaction: Literal[&quot;commit&quot;, &quot;rollback&quot;, &quot;append&quot;, &quot;raise&quot;] = &quot;commit&quot;,
301302
) -&gt; AsyncGenerator[AsyncSession, None]:
302303
</code></pre>
303304
<p>A context manager that can be used to wrap another function which
304305
uses a context session, making that call isolated within its own transaction.</p>
306+
<p>There are several options that define how the function will handle
307+
an already open transaction.</p>
308+
<p>current_transaction:
309+
- <code>commit</code> - commits the open transaction and starts a new one
310+
- <code>rollback</code> - rolls back the open transaction and starts a new one
311+
- <code>append</code> - continues using the current transaction and commits it
312+
- <code>raise</code> - raises an InvalidRequestError</p>
305313
<hr />
306314
<h3 id="commit_db_session">commit_db_session</h3>
307315
<pre><code class="language-python">async def commit_db_session(connect: DBConnect) -&gt; None:
@@ -377,12 +385,17 @@ <h3 id="rollback_session">rollback_session</h3>
377385
<hr />
378386
<h3 id="set_test_context">set_test_context</h3>
379387
<pre><code class="language-python">@asynccontextmanager
380-
async def set_test_context() -&gt; AsyncGenerator[None]:
388+
async def set_test_context(auto_close: bool = False) -&gt; AsyncGenerator[None]:
381389
</code></pre>
382390
<p>A context manager that creates a new context in which you can place a
383391
dedicated test session.
384392
It’s intended for use in tests where the test and the application share
385393
a single transaction.</p>
394+
<p>Use <code>auto_close=False</code> if you’re using a test session and transaction
395+
that you close manually elsewhere in your code.</p>
396+
<p>Use <code>auto_close=True</code> if, for example, you want to call a
397+
function in a test that uses a context bypassing the
398+
middleware, and you want all sessions to be closed automatically.</p>
386399
<hr />
387400
<h3 id="put_savepoint_session_in_ctx">put_savepoint_session_in_ctx</h3>
388401
<pre><code class="language-python">async def put_savepoint_session_in_ctx(

docs/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,5 +218,5 @@ <h2 id="how-it-works">How it works</h2>
218218

219219
<!--
220220
MkDocs version : 1.6.1
221-
Build Date UTC : 2025-11-25 21:27:44.719156+00:00
221+
Build Date UTC : 2025-11-28 22:35:06.618073+00:00
222222
-->

docs/search/search_index.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/sitemap.xml.gz

0 Bytes
Binary file not shown.

docs_sources/docs/api.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,21 @@ calls return the same session.
195195
@asynccontextmanager
196196
async def atomic_db_session(
197197
connect: DBConnect,
198+
current_transaction: Literal["commit", "rollback", "append", "raise"] = "commit",
198199
) -> AsyncGenerator[AsyncSession, None]:
199200
```
200201
A context manager that can be used to wrap another function which
201202
uses a context session, making that call isolated within its own transaction.
202203

204+
There are several options that define how the function will handle
205+
an already open transaction.
206+
207+
current_transaction:
208+
- `commit` - commits the open transaction and starts a new one
209+
- `rollback` - rolls back the open transaction and starts a new one
210+
- `append` - continues using the current transaction and commits it
211+
- `raise` - raises an InvalidRequestError
212+
203213
---
204214

205215
### commit_db_session
@@ -306,13 +316,20 @@ It’s intended for use in fixtures to execute SQL queries during tests.
306316
### set_test_context
307317
```python
308318
@asynccontextmanager
309-
async def set_test_context() -> AsyncGenerator[None]:
319+
async def set_test_context(auto_close: bool = False) -> AsyncGenerator[None]:
310320
```
311321
A context manager that creates a new context in which you can place a
312322
dedicated test session.
313323
It’s intended for use in tests where the test and the application share
314324
a single transaction.
315325

326+
Use `auto_close=False` if you’re using a test session and transaction
327+
that you close manually elsewhere in your code.
328+
329+
Use `auto_close=True` if, for example, you want to call a
330+
function in a test that uses a context bypassing the
331+
middleware, and you want all sessions to be closed automatically.
332+
316333
---
317334

318335
### put_savepoint_session_in_ctx

examples/fastapi_example/routes/atomic_usage_2.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,34 @@ async def handler_with_db_session_and_atomic_2() -> None:
1212
You want to reuse this function, but you need to commit immediately,
1313
rather than wait for the request to complete.
1414
"""
15-
# This is a new transaction in a new connection
15+
# Open transaction
1616
await _insert_1()
1717

18-
# If you want to wrap an operation in a new
19-
# transaction, the current transaction will be committed automatically.
18+
# autocommit current -> start and end new
2019
async with atomic_db_session(connection):
2120
await _insert_1()
2221

22+
# Open transaction
23+
await _insert_1()
24+
25+
# rollback current -> start and end new
26+
async with atomic_db_session(connection, "rollback"):
27+
await _insert_1()
28+
29+
# Open transaction
30+
await _insert_1()
31+
32+
# use current transaction and commit
33+
async with atomic_db_session(connection, "append"):
34+
await _insert_1()
35+
36+
# Open transaction
37+
await _insert_1()
38+
39+
# raise InvalidRequestError error
40+
async with atomic_db_session(connection, "raise"):
41+
await _insert_1()
42+
2343

2444
async def _insert_1() -> None:
2545
session = await db_session(connection)

examples/fastapi_example/tests/non_transactional/test_routes.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from httpx import AsyncClient
1212
from sqlalchemy import exists, select
13+
from sqlalchemy.exc import InvalidRequestError
1314
from sqlalchemy.ext.asyncio import AsyncSession
1415

1516
from examples.fastapi_example.models import ExampleTable
@@ -67,16 +68,19 @@ async def test_example_with_db_session_and_atomic_2(
6768
such a handler should only be tested in non-transactional tests.
6869
"""
6970
# Act
70-
response = await client.post(
71-
"/example_with_db_session_and_atomic_2",
72-
)
71+
try:
72+
await client.post(
73+
"/example_with_db_session_and_atomic_2",
74+
)
75+
except InvalidRequestError:
76+
...
77+
else:
78+
raise Exception()
7379

7480
# Assert
75-
assert response.status_code == HTTPStatus.OK
76-
7781
result = await db_session_test.execute(select(ExampleTable))
7882
rows = result.scalars().all()
79-
assert len(rows) == 2
83+
assert len(rows) == 5
8084
for row in rows:
8185
assert row.text == "example_with_db_session_and_atomic"
8286

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "context-async-sqlalchemy"
3-
version = "2.0.4"
3+
version = "2.1.4"
44
description = "A convenient way to configure and work with an async SQLAlchemy session through context in asynchronous applications"
55
readme = "README.md"
66
authors = [

0 commit comments

Comments
 (0)