sqla-fancy-core 1.1.1__tar.gz → 1.1.2__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.
Potentially problematic release.
This version of sqla-fancy-core might be problematic. Click here for more details.
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/PKG-INFO +5 -6
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/README.md +4 -5
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/pyproject.toml +1 -1
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/sqla_fancy_core/decorators.py +50 -14
- sqla_fancy_core-1.1.2/tests/test_decorators.py +448 -0
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/uv.lock +1 -1
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/.github/workflows/ci.yaml +0 -0
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/.gitignore +0 -0
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/LICENSE +0 -0
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/sqla_fancy_core/__init__.py +0 -0
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/sqla_fancy_core/factories.py +0 -0
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/sqla_fancy_core/wrappers.py +0 -0
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/tests/__init__.py +0 -0
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/tests/test_async_fancy_engine.py +0 -0
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/tests/test_connect.py +0 -0
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/tests/test_fancy_engine.py +0 -0
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/tests/test_field.py +0 -0
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/tests/test_table_factory.py +0 -0
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/tests/test_table_factory_async.py +0 -0
- {sqla_fancy_core-1.1.1 → sqla_fancy_core-1.1.2}/tests/test_transact.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqla-fancy-core
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.2
|
|
4
4
|
Summary: SQLAlchemy core, but fancier
|
|
5
5
|
Project-URL: Homepage, https://github.com/sayanarijit/sqla-fancy-core
|
|
6
6
|
Author-email: Arijit Basu <sayanarijit@gmail.com>
|
|
@@ -242,27 +242,24 @@ users = sa.Table(
|
|
|
242
242
|
)
|
|
243
243
|
metadata.create_all(engine)
|
|
244
244
|
|
|
245
|
-
# 1) Ensure a connection is available (no implicit transaction)
|
|
246
245
|
@connect
|
|
247
246
|
def get_user_count(conn=Inject(engine)):
|
|
248
247
|
return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
|
|
249
248
|
|
|
250
249
|
assert get_user_count() == 0
|
|
251
250
|
|
|
252
|
-
# 2) Wrap in a transaction automatically
|
|
253
251
|
@transact
|
|
254
252
|
def create_user(name: str, conn=Inject(engine)):
|
|
255
253
|
conn.execute(sa.insert(users).values(name=name))
|
|
256
254
|
|
|
255
|
+
# Without an explicit transaction
|
|
257
256
|
create_user("alice")
|
|
258
257
|
assert get_user_count() == 1
|
|
259
258
|
|
|
260
|
-
#
|
|
259
|
+
# With an explicit transaction
|
|
261
260
|
with engine.begin() as txn:
|
|
262
261
|
create_user("bob", conn=txn)
|
|
263
262
|
assert get_user_count(conn=txn) == 2
|
|
264
|
-
|
|
265
|
-
assert get_user_count() == 2
|
|
266
263
|
```
|
|
267
264
|
|
|
268
265
|
### Async examples
|
|
@@ -293,10 +290,12 @@ async def get_user_count(conn=Inject(engine)):
|
|
|
293
290
|
async def create_user(name: str, conn=Inject(engine)):
|
|
294
291
|
await conn.execute(sa.insert(users).values(name=name))
|
|
295
292
|
|
|
293
|
+
# Without an explicit transaction
|
|
296
294
|
assert await get_user_count() == 0
|
|
297
295
|
await create_user("carol")
|
|
298
296
|
assert await get_user_count() == 1
|
|
299
297
|
|
|
298
|
+
# With an explicit transaction
|
|
300
299
|
async with engine.connect() as conn:
|
|
301
300
|
await create_user("dave", conn=conn)
|
|
302
301
|
assert await get_user_count(conn=conn) == 2
|
|
@@ -190,27 +190,24 @@ users = sa.Table(
|
|
|
190
190
|
)
|
|
191
191
|
metadata.create_all(engine)
|
|
192
192
|
|
|
193
|
-
# 1) Ensure a connection is available (no implicit transaction)
|
|
194
193
|
@connect
|
|
195
194
|
def get_user_count(conn=Inject(engine)):
|
|
196
195
|
return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
|
|
197
196
|
|
|
198
197
|
assert get_user_count() == 0
|
|
199
198
|
|
|
200
|
-
# 2) Wrap in a transaction automatically
|
|
201
199
|
@transact
|
|
202
200
|
def create_user(name: str, conn=Inject(engine)):
|
|
203
201
|
conn.execute(sa.insert(users).values(name=name))
|
|
204
202
|
|
|
203
|
+
# Without an explicit transaction
|
|
205
204
|
create_user("alice")
|
|
206
205
|
assert get_user_count() == 1
|
|
207
206
|
|
|
208
|
-
#
|
|
207
|
+
# With an explicit transaction
|
|
209
208
|
with engine.begin() as txn:
|
|
210
209
|
create_user("bob", conn=txn)
|
|
211
210
|
assert get_user_count(conn=txn) == 2
|
|
212
|
-
|
|
213
|
-
assert get_user_count() == 2
|
|
214
211
|
```
|
|
215
212
|
|
|
216
213
|
### Async examples
|
|
@@ -241,10 +238,12 @@ async def get_user_count(conn=Inject(engine)):
|
|
|
241
238
|
async def create_user(name: str, conn=Inject(engine)):
|
|
242
239
|
await conn.execute(sa.insert(users).values(name=name))
|
|
243
240
|
|
|
241
|
+
# Without an explicit transaction
|
|
244
242
|
assert await get_user_count() == 0
|
|
245
243
|
await create_user("carol")
|
|
246
244
|
assert await get_user_count() == 1
|
|
247
245
|
|
|
246
|
+
# With an explicit transaction
|
|
248
247
|
async with engine.connect() as conn:
|
|
249
248
|
await create_user("dave", conn=conn)
|
|
250
249
|
assert await get_user_count(conn=conn) == 2
|
|
@@ -7,7 +7,9 @@ from typing import Union, overload
|
|
|
7
7
|
import sqlalchemy as sa
|
|
8
8
|
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
from sqla_fancy_core.wrappers import AsyncFancyEngineWrapper, FancyEngineWrapper
|
|
11
|
+
|
|
12
|
+
EngineType = Union[sa.Engine, AsyncEngine, FancyEngineWrapper, AsyncFancyEngineWrapper]
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class _Injectable:
|
|
@@ -16,9 +18,9 @@ class _Injectable:
|
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
@overload
|
|
19
|
-
def Inject(engine: sa.Engine) -> sa.Connection: ...
|
|
21
|
+
def Inject(engine: Union[sa.Engine, FancyEngineWrapper]) -> sa.Connection: ...
|
|
20
22
|
@overload
|
|
21
|
-
def Inject(engine: AsyncEngine) -> AsyncConnection: ...
|
|
23
|
+
def Inject(engine: Union[AsyncEngine, AsyncFancyEngineWrapper]) -> AsyncConnection: ...
|
|
22
24
|
def Inject(engine: EngineType): # type: ignore
|
|
23
25
|
"""A marker class for dependency injection."""
|
|
24
26
|
return _Injectable(engine)
|
|
@@ -44,7 +46,6 @@ def transact(func):
|
|
|
44
46
|
create_user(name="existing", conn=conn)
|
|
45
47
|
"""
|
|
46
48
|
|
|
47
|
-
# Find the parameter with value Inject
|
|
48
49
|
sig = inspect.signature(func)
|
|
49
50
|
inject_param_name = None
|
|
50
51
|
for name, param in sig.parameters.items():
|
|
@@ -56,8 +57,21 @@ def transact(func):
|
|
|
56
57
|
if inject_param_name is None:
|
|
57
58
|
return func # No injection needed
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
engine_arg = sig.parameters[inject_param_name].default.engine
|
|
61
|
+
if isinstance(engine_arg, sa.Engine):
|
|
62
|
+
is_async = False
|
|
63
|
+
engine = engine_arg
|
|
64
|
+
elif isinstance(engine_arg, FancyEngineWrapper):
|
|
65
|
+
is_async = False
|
|
66
|
+
engine = engine_arg.engine
|
|
67
|
+
elif isinstance(engine_arg, AsyncEngine):
|
|
68
|
+
is_async = True
|
|
69
|
+
engine = engine_arg
|
|
70
|
+
elif isinstance(engine_arg, AsyncFancyEngineWrapper):
|
|
71
|
+
is_async = True
|
|
72
|
+
engine = engine_arg.engine
|
|
73
|
+
else:
|
|
74
|
+
raise TypeError("Unsupported engine type")
|
|
61
75
|
|
|
62
76
|
if is_async:
|
|
63
77
|
|
|
@@ -70,8 +84,14 @@ def transact(func):
|
|
|
70
84
|
else:
|
|
71
85
|
async with conn.begin():
|
|
72
86
|
return await func(*args, **kwargs)
|
|
87
|
+
elif isinstance(conn, sa.Connection):
|
|
88
|
+
if conn.in_transaction():
|
|
89
|
+
return await func(*args, **kwargs)
|
|
90
|
+
else:
|
|
91
|
+
with conn.begin():
|
|
92
|
+
return await func(*args, **kwargs)
|
|
73
93
|
else:
|
|
74
|
-
async with engine.begin() as conn:
|
|
94
|
+
async with engine.begin() as conn: # type: ignore
|
|
75
95
|
kwargs[inject_param_name] = conn
|
|
76
96
|
return await func(*args, **kwargs)
|
|
77
97
|
|
|
@@ -88,8 +108,10 @@ def transact(func):
|
|
|
88
108
|
else:
|
|
89
109
|
with conn.begin():
|
|
90
110
|
return func(*args, **kwargs)
|
|
111
|
+
elif isinstance(conn, AsyncConnection):
|
|
112
|
+
raise TypeError("AsyncConnection cannot be used in sync function")
|
|
91
113
|
else:
|
|
92
|
-
with engine.begin() as conn:
|
|
114
|
+
with engine.begin() as conn: # type: ignore
|
|
93
115
|
kwargs[inject_param_name] = conn
|
|
94
116
|
return func(*args, **kwargs)
|
|
95
117
|
|
|
@@ -116,7 +138,6 @@ def connect(func):
|
|
|
116
138
|
count = get_user_count(conn)
|
|
117
139
|
"""
|
|
118
140
|
|
|
119
|
-
# Find the parameter with value Inject
|
|
120
141
|
sig = inspect.signature(func)
|
|
121
142
|
inject_param_name = None
|
|
122
143
|
for name, param in sig.parameters.items():
|
|
@@ -128,18 +149,31 @@ def connect(func):
|
|
|
128
149
|
if inject_param_name is None:
|
|
129
150
|
return func # No injection needed
|
|
130
151
|
|
|
131
|
-
|
|
132
|
-
|
|
152
|
+
engine_arg = sig.parameters[inject_param_name].default.engine
|
|
153
|
+
if isinstance(engine_arg, sa.Engine):
|
|
154
|
+
is_async = False
|
|
155
|
+
engine = engine_arg
|
|
156
|
+
elif isinstance(engine_arg, FancyEngineWrapper):
|
|
157
|
+
is_async = False
|
|
158
|
+
engine = engine_arg.engine
|
|
159
|
+
elif isinstance(engine_arg, AsyncEngine):
|
|
160
|
+
is_async = True
|
|
161
|
+
engine = engine_arg
|
|
162
|
+
elif isinstance(engine_arg, AsyncFancyEngineWrapper):
|
|
163
|
+
is_async = True
|
|
164
|
+
engine = engine_arg.engine
|
|
165
|
+
else:
|
|
166
|
+
raise TypeError("Unsupported engine type")
|
|
133
167
|
|
|
134
168
|
if is_async:
|
|
135
169
|
|
|
136
170
|
@functools.wraps(func)
|
|
137
171
|
async def async_wrapper(*args, **kwargs):
|
|
138
172
|
conn = kwargs.get(inject_param_name)
|
|
139
|
-
if isinstance(conn, AsyncConnection):
|
|
173
|
+
if isinstance(conn, (AsyncConnection, sa.Connection)):
|
|
140
174
|
return await func(*args, **kwargs)
|
|
141
175
|
else:
|
|
142
|
-
async with engine.connect() as conn:
|
|
176
|
+
async with engine.connect() as conn: # type: ignore
|
|
143
177
|
kwargs[inject_param_name] = conn
|
|
144
178
|
return await func(*args, **kwargs)
|
|
145
179
|
|
|
@@ -152,8 +186,10 @@ def connect(func):
|
|
|
152
186
|
conn = kwargs.get(inject_param_name)
|
|
153
187
|
if isinstance(conn, sa.Connection):
|
|
154
188
|
return func(*args, **kwargs)
|
|
189
|
+
elif isinstance(conn, AsyncConnection):
|
|
190
|
+
raise TypeError("AsyncConnection cannot be used in sync function")
|
|
155
191
|
else:
|
|
156
|
-
with engine.connect() as conn:
|
|
192
|
+
with engine.connect() as conn: # type: ignore
|
|
157
193
|
kwargs[inject_param_name] = conn
|
|
158
194
|
return func(*args, **kwargs)
|
|
159
195
|
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""Tests for decorator functionality with fancy wrappers and edge cases."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import pytest_asyncio
|
|
5
|
+
import sqlalchemy as sa
|
|
6
|
+
from sqlalchemy.ext.asyncio import AsyncConnection, create_async_engine
|
|
7
|
+
|
|
8
|
+
from sqla_fancy_core.decorators import Inject, connect, transact
|
|
9
|
+
from sqla_fancy_core.wrappers import fancy
|
|
10
|
+
|
|
11
|
+
# Define a simple table for testing
|
|
12
|
+
metadata = sa.MetaData()
|
|
13
|
+
users = sa.Table(
|
|
14
|
+
"users",
|
|
15
|
+
metadata,
|
|
16
|
+
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
|
17
|
+
sa.Column("name", sa.String),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def sync_engine():
|
|
23
|
+
"""Provides a synchronous in-memory SQLite engine."""
|
|
24
|
+
engine = sa.create_engine(
|
|
25
|
+
"sqlite:///:memory:",
|
|
26
|
+
connect_args={"check_same_thread": False},
|
|
27
|
+
poolclass=sa.pool.StaticPool,
|
|
28
|
+
)
|
|
29
|
+
metadata.create_all(engine)
|
|
30
|
+
yield engine
|
|
31
|
+
metadata.drop_all(engine)
|
|
32
|
+
engine.dispose()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest_asyncio.fixture
|
|
36
|
+
async def async_engine():
|
|
37
|
+
"""Provides an asynchronous in-memory SQLite engine."""
|
|
38
|
+
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
39
|
+
async with engine.begin() as conn:
|
|
40
|
+
await conn.run_sync(metadata.create_all)
|
|
41
|
+
yield engine
|
|
42
|
+
async with engine.begin() as conn:
|
|
43
|
+
await conn.run_sync(metadata.drop_all)
|
|
44
|
+
await engine.dispose()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Tests for @connect with FancyEngineWrapper
|
|
48
|
+
def test_connect_with_fancy_wrapper(sync_engine):
|
|
49
|
+
"""Test @connect decorator with FancyEngineWrapper."""
|
|
50
|
+
fancy_engine = fancy(sync_engine)
|
|
51
|
+
|
|
52
|
+
@connect
|
|
53
|
+
def get_user_count(conn=Inject(fancy_engine)):
|
|
54
|
+
return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
|
|
55
|
+
|
|
56
|
+
# Should work without explicit connection
|
|
57
|
+
assert get_user_count() == 0
|
|
58
|
+
|
|
59
|
+
# Should work with explicit connection
|
|
60
|
+
with sync_engine.connect() as conn:
|
|
61
|
+
assert get_user_count(conn=conn) == 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_connect_with_fancy_wrapper_and_data(sync_engine):
|
|
65
|
+
"""Test @connect decorator with FancyEngineWrapper after inserting data."""
|
|
66
|
+
fancy_engine = fancy(sync_engine)
|
|
67
|
+
|
|
68
|
+
@connect
|
|
69
|
+
def get_user_count(conn=Inject(fancy_engine)):
|
|
70
|
+
return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
|
|
71
|
+
|
|
72
|
+
# Insert some data
|
|
73
|
+
with sync_engine.begin() as conn:
|
|
74
|
+
conn.execute(sa.insert(users).values(name="Alice"))
|
|
75
|
+
conn.execute(sa.insert(users).values(name="Bob"))
|
|
76
|
+
|
|
77
|
+
assert get_user_count() == 2
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Tests for @transact with FancyEngineWrapper
|
|
81
|
+
def test_transact_with_fancy_wrapper(sync_engine):
|
|
82
|
+
"""Test @transact decorator with FancyEngineWrapper."""
|
|
83
|
+
fancy_engine = fancy(sync_engine)
|
|
84
|
+
|
|
85
|
+
@transact
|
|
86
|
+
def create_user(name: str, conn=Inject(fancy_engine)):
|
|
87
|
+
conn.execute(sa.insert(users).values(name=name))
|
|
88
|
+
|
|
89
|
+
@connect
|
|
90
|
+
def get_user_count(conn=Inject(fancy_engine)):
|
|
91
|
+
return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
|
|
92
|
+
|
|
93
|
+
# Create user without explicit transaction
|
|
94
|
+
create_user("Alice")
|
|
95
|
+
assert get_user_count() == 1
|
|
96
|
+
|
|
97
|
+
# Create user with explicit transaction
|
|
98
|
+
with sync_engine.begin() as conn:
|
|
99
|
+
create_user("Bob", conn=conn)
|
|
100
|
+
|
|
101
|
+
assert get_user_count() == 2
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_transact_with_fancy_wrapper_rollback(sync_engine):
|
|
105
|
+
"""Test @transact decorator with FancyEngineWrapper and rollback."""
|
|
106
|
+
fancy_engine = fancy(sync_engine)
|
|
107
|
+
|
|
108
|
+
@transact
|
|
109
|
+
def create_user(name: str, conn=Inject(fancy_engine)):
|
|
110
|
+
conn.execute(sa.insert(users).values(name=name))
|
|
111
|
+
|
|
112
|
+
@connect
|
|
113
|
+
def get_user_count(conn=Inject(fancy_engine)):
|
|
114
|
+
return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
|
|
115
|
+
|
|
116
|
+
# Test that rollback works
|
|
117
|
+
try:
|
|
118
|
+
with sync_engine.begin() as conn:
|
|
119
|
+
create_user("Alice", conn=conn)
|
|
120
|
+
raise ValueError("Test rollback")
|
|
121
|
+
except ValueError:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
# User should not be created due to rollback
|
|
125
|
+
assert get_user_count() == 0
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_transact_reuses_existing_transaction(sync_engine):
|
|
129
|
+
"""Test that @transact reuses an existing transaction."""
|
|
130
|
+
fancy_engine = fancy(sync_engine)
|
|
131
|
+
|
|
132
|
+
call_count = {"begin": 0}
|
|
133
|
+
|
|
134
|
+
@transact
|
|
135
|
+
def create_user(name: str, conn=Inject(fancy_engine)):
|
|
136
|
+
conn.execute(sa.insert(users).values(name=name))
|
|
137
|
+
|
|
138
|
+
@connect
|
|
139
|
+
def get_user_count(conn=Inject(fancy_engine)):
|
|
140
|
+
return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
|
|
141
|
+
|
|
142
|
+
# When called with a connection already in a transaction,
|
|
143
|
+
# it should not start a new transaction
|
|
144
|
+
with sync_engine.begin() as conn:
|
|
145
|
+
assert conn.in_transaction()
|
|
146
|
+
create_user("Alice", conn=conn)
|
|
147
|
+
create_user("Bob", conn=conn)
|
|
148
|
+
|
|
149
|
+
assert get_user_count() == 2
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_transact_starts_transaction_for_connection_without_transaction(sync_engine):
|
|
153
|
+
"""Test that @transact starts a transaction if connection is not in one."""
|
|
154
|
+
fancy_engine = fancy(sync_engine)
|
|
155
|
+
|
|
156
|
+
@transact
|
|
157
|
+
def create_user(name: str, conn=Inject(fancy_engine)):
|
|
158
|
+
assert conn.in_transaction(), "Connection should be in a transaction"
|
|
159
|
+
conn.execute(sa.insert(users).values(name=name))
|
|
160
|
+
|
|
161
|
+
@connect
|
|
162
|
+
def get_user_count(conn=Inject(fancy_engine)):
|
|
163
|
+
return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
|
|
164
|
+
|
|
165
|
+
# Pass a connection that is NOT in a transaction
|
|
166
|
+
with sync_engine.connect() as conn:
|
|
167
|
+
assert not conn.in_transaction()
|
|
168
|
+
create_user("Alice", conn=conn)
|
|
169
|
+
|
|
170
|
+
assert get_user_count() == 1
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# Tests for async @connect with AsyncFancyEngineWrapper
|
|
174
|
+
@pytest.mark.asyncio
|
|
175
|
+
async def test_async_connect_with_fancy_wrapper(async_engine):
|
|
176
|
+
"""Test async @connect decorator with AsyncFancyEngineWrapper."""
|
|
177
|
+
fancy_engine = fancy(async_engine)
|
|
178
|
+
|
|
179
|
+
@connect
|
|
180
|
+
async def get_user_count(conn=Inject(fancy_engine)):
|
|
181
|
+
result = await conn.execute(sa.select(sa.func.count()).select_from(users))
|
|
182
|
+
return result.scalar_one()
|
|
183
|
+
|
|
184
|
+
# Should work without explicit connection
|
|
185
|
+
assert await get_user_count() == 0
|
|
186
|
+
|
|
187
|
+
# Should work with explicit connection
|
|
188
|
+
async with async_engine.connect() as conn:
|
|
189
|
+
assert await get_user_count(conn=conn) == 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@pytest.mark.asyncio
|
|
193
|
+
async def test_async_connect_with_fancy_wrapper_and_data(async_engine):
|
|
194
|
+
"""Test async @connect decorator with AsyncFancyEngineWrapper after inserting data."""
|
|
195
|
+
fancy_engine = fancy(async_engine)
|
|
196
|
+
|
|
197
|
+
@connect
|
|
198
|
+
async def get_user_count(conn=Inject(fancy_engine)):
|
|
199
|
+
result = await conn.execute(sa.select(sa.func.count()).select_from(users))
|
|
200
|
+
return result.scalar_one()
|
|
201
|
+
|
|
202
|
+
# Insert some data
|
|
203
|
+
async with async_engine.begin() as conn:
|
|
204
|
+
await conn.execute(sa.insert(users).values(name="Alice"))
|
|
205
|
+
await conn.execute(sa.insert(users).values(name="Bob"))
|
|
206
|
+
|
|
207
|
+
assert await get_user_count() == 2
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# Tests for async @transact with AsyncFancyEngineWrapper
|
|
211
|
+
@pytest.mark.asyncio
|
|
212
|
+
async def test_async_transact_with_fancy_wrapper(async_engine):
|
|
213
|
+
"""Test async @transact decorator with AsyncFancyEngineWrapper."""
|
|
214
|
+
fancy_engine = fancy(async_engine)
|
|
215
|
+
|
|
216
|
+
@transact
|
|
217
|
+
async def create_user(name: str, conn=Inject(fancy_engine)):
|
|
218
|
+
await conn.execute(sa.insert(users).values(name=name))
|
|
219
|
+
|
|
220
|
+
@connect
|
|
221
|
+
async def get_user_count(conn=Inject(fancy_engine)):
|
|
222
|
+
result = await conn.execute(sa.select(sa.func.count()).select_from(users))
|
|
223
|
+
return result.scalar_one()
|
|
224
|
+
|
|
225
|
+
# Create user without explicit transaction
|
|
226
|
+
await create_user("Alice")
|
|
227
|
+
assert await get_user_count() == 1
|
|
228
|
+
|
|
229
|
+
# Create user with explicit transaction
|
|
230
|
+
async with async_engine.begin() as conn:
|
|
231
|
+
await create_user("Bob", conn=conn)
|
|
232
|
+
|
|
233
|
+
assert await get_user_count() == 2
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@pytest.mark.asyncio
|
|
237
|
+
async def test_async_transact_with_fancy_wrapper_rollback(async_engine):
|
|
238
|
+
"""Test async @transact decorator with AsyncFancyEngineWrapper and rollback."""
|
|
239
|
+
fancy_engine = fancy(async_engine)
|
|
240
|
+
|
|
241
|
+
@transact
|
|
242
|
+
async def create_user(name: str, conn=Inject(fancy_engine)):
|
|
243
|
+
await conn.execute(sa.insert(users).values(name=name))
|
|
244
|
+
|
|
245
|
+
@connect
|
|
246
|
+
async def get_user_count(conn=Inject(fancy_engine)):
|
|
247
|
+
result = await conn.execute(sa.select(sa.func.count()).select_from(users))
|
|
248
|
+
return result.scalar_one()
|
|
249
|
+
|
|
250
|
+
# Test that rollback works
|
|
251
|
+
try:
|
|
252
|
+
async with async_engine.begin() as conn:
|
|
253
|
+
await create_user("Alice", conn=conn)
|
|
254
|
+
raise ValueError("Test rollback")
|
|
255
|
+
except ValueError:
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
# User should not be created due to rollback
|
|
259
|
+
assert await get_user_count() == 0
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@pytest.mark.asyncio
|
|
263
|
+
async def test_async_transact_reuses_existing_transaction(async_engine):
|
|
264
|
+
"""Test that async @transact reuses an existing transaction."""
|
|
265
|
+
fancy_engine = fancy(async_engine)
|
|
266
|
+
|
|
267
|
+
@transact
|
|
268
|
+
async def create_user(name: str, conn=Inject(fancy_engine)):
|
|
269
|
+
await conn.execute(sa.insert(users).values(name=name))
|
|
270
|
+
|
|
271
|
+
@connect
|
|
272
|
+
async def get_user_count(conn=Inject(fancy_engine)):
|
|
273
|
+
result = await conn.execute(sa.select(sa.func.count()).select_from(users))
|
|
274
|
+
return result.scalar_one()
|
|
275
|
+
|
|
276
|
+
# When called with a connection already in a transaction,
|
|
277
|
+
# it should not start a new transaction
|
|
278
|
+
async with async_engine.begin() as conn:
|
|
279
|
+
assert conn.in_transaction()
|
|
280
|
+
await create_user("Alice", conn=conn)
|
|
281
|
+
await create_user("Bob", conn=conn)
|
|
282
|
+
|
|
283
|
+
assert await get_user_count() == 2
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@pytest.mark.asyncio
|
|
287
|
+
async def test_async_transact_starts_transaction_for_connection_without_transaction(
|
|
288
|
+
async_engine,
|
|
289
|
+
):
|
|
290
|
+
"""Test that async @transact starts a transaction if connection is not in one."""
|
|
291
|
+
fancy_engine = fancy(async_engine)
|
|
292
|
+
|
|
293
|
+
@transact
|
|
294
|
+
async def create_user(name: str, conn=Inject(fancy_engine)):
|
|
295
|
+
assert conn.in_transaction(), "Connection should be in a transaction"
|
|
296
|
+
await conn.execute(sa.insert(users).values(name=name))
|
|
297
|
+
|
|
298
|
+
@connect
|
|
299
|
+
async def get_user_count(conn=Inject(fancy_engine)):
|
|
300
|
+
result = await conn.execute(sa.select(sa.func.count()).select_from(users))
|
|
301
|
+
return result.scalar_one()
|
|
302
|
+
|
|
303
|
+
# Pass a connection that is NOT in a transaction
|
|
304
|
+
async with async_engine.connect() as conn:
|
|
305
|
+
assert not conn.in_transaction()
|
|
306
|
+
await create_user("Alice", conn=conn)
|
|
307
|
+
|
|
308
|
+
assert await get_user_count() == 1
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# Error handling tests
|
|
312
|
+
@pytest.mark.asyncio
|
|
313
|
+
async def test_sync_decorator_rejects_async_connection(sync_engine, async_engine):
|
|
314
|
+
"""Test that sync decorator properly rejects AsyncConnection."""
|
|
315
|
+
fancy_engine = fancy(sync_engine)
|
|
316
|
+
|
|
317
|
+
@connect
|
|
318
|
+
def get_user_count(conn=Inject(fancy_engine)):
|
|
319
|
+
return conn.execute(sa.select(sa.func.count()).select_from(users)).scalar_one()
|
|
320
|
+
|
|
321
|
+
# Try to pass an actual AsyncConnection to a sync function
|
|
322
|
+
async with async_engine.connect() as async_conn:
|
|
323
|
+
with pytest.raises(TypeError, match="AsyncConnection cannot be used in sync function"):
|
|
324
|
+
get_user_count(conn=async_conn)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@pytest.mark.asyncio
|
|
328
|
+
async def test_sync_transact_rejects_async_connection(sync_engine, async_engine):
|
|
329
|
+
"""Test that sync @transact properly rejects AsyncConnection."""
|
|
330
|
+
fancy_engine = fancy(sync_engine)
|
|
331
|
+
|
|
332
|
+
@transact
|
|
333
|
+
def create_user(name: str, conn=Inject(fancy_engine)):
|
|
334
|
+
conn.execute(sa.insert(users).values(name=name))
|
|
335
|
+
|
|
336
|
+
# Try to pass an actual AsyncConnection to a sync function
|
|
337
|
+
async with async_engine.connect() as async_conn:
|
|
338
|
+
with pytest.raises(TypeError, match="AsyncConnection cannot be used in sync function"):
|
|
339
|
+
create_user("Alice", conn=async_conn)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def test_inject_with_unsupported_engine_type():
|
|
343
|
+
"""Test that decorators reject unsupported engine types."""
|
|
344
|
+
|
|
345
|
+
class UnsupportedEngine:
|
|
346
|
+
pass
|
|
347
|
+
|
|
348
|
+
unsupported = UnsupportedEngine()
|
|
349
|
+
|
|
350
|
+
with pytest.raises(TypeError, match="Unsupported engine type"):
|
|
351
|
+
|
|
352
|
+
@connect
|
|
353
|
+
def test_func(conn=Inject(unsupported)): # type: ignore
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
test_func()
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def test_transact_with_unsupported_engine_type():
|
|
360
|
+
"""Test that @transact rejects unsupported engine types."""
|
|
361
|
+
|
|
362
|
+
class UnsupportedEngine:
|
|
363
|
+
pass
|
|
364
|
+
|
|
365
|
+
unsupported = UnsupportedEngine()
|
|
366
|
+
|
|
367
|
+
with pytest.raises(TypeError, match="Unsupported engine type"):
|
|
368
|
+
|
|
369
|
+
@transact
|
|
370
|
+
def test_func(conn=Inject(unsupported)): # type: ignore
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
test_func()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# Test decorator without Inject parameter
|
|
377
|
+
def test_connect_without_inject_parameter():
|
|
378
|
+
"""Test that @connect works on functions without Inject parameter."""
|
|
379
|
+
|
|
380
|
+
@connect
|
|
381
|
+
def simple_function():
|
|
382
|
+
return "hello"
|
|
383
|
+
|
|
384
|
+
# Should just return the function as-is without wrapping
|
|
385
|
+
assert simple_function() == "hello"
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def test_transact_without_inject_parameter():
|
|
389
|
+
"""Test that @transact works on functions without Inject parameter."""
|
|
390
|
+
|
|
391
|
+
@transact
|
|
392
|
+
def simple_function():
|
|
393
|
+
return "hello"
|
|
394
|
+
|
|
395
|
+
# Should just return the function as-is without wrapping
|
|
396
|
+
assert simple_function() == "hello"
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# Integration test combining both decorators
|
|
400
|
+
def test_integration_connect_and_transact(sync_engine):
|
|
401
|
+
"""Test integration of @connect and @transact decorators."""
|
|
402
|
+
fancy_engine = fancy(sync_engine)
|
|
403
|
+
|
|
404
|
+
@transact
|
|
405
|
+
def create_user(name: str, conn=Inject(fancy_engine)):
|
|
406
|
+
conn.execute(sa.insert(users).values(name=name))
|
|
407
|
+
|
|
408
|
+
@connect
|
|
409
|
+
def get_users(conn=Inject(fancy_engine)):
|
|
410
|
+
result = conn.execute(sa.select(users.c.name).order_by(users.c.name))
|
|
411
|
+
return [row[0] for row in result]
|
|
412
|
+
|
|
413
|
+
@transact
|
|
414
|
+
def create_multiple_users(names: list[str], conn=Inject(fancy_engine)):
|
|
415
|
+
for name in names:
|
|
416
|
+
create_user(name, conn=conn)
|
|
417
|
+
|
|
418
|
+
# Create users using nested decorators
|
|
419
|
+
create_multiple_users(["Alice", "Bob", "Charlie"])
|
|
420
|
+
|
|
421
|
+
# Verify all users were created
|
|
422
|
+
assert get_users() == ["Alice", "Bob", "Charlie"]
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@pytest.mark.asyncio
|
|
426
|
+
async def test_async_integration_connect_and_transact(async_engine):
|
|
427
|
+
"""Test async integration of @connect and @transact decorators."""
|
|
428
|
+
fancy_engine = fancy(async_engine)
|
|
429
|
+
|
|
430
|
+
@transact
|
|
431
|
+
async def create_user(name: str, conn=Inject(fancy_engine)):
|
|
432
|
+
await conn.execute(sa.insert(users).values(name=name))
|
|
433
|
+
|
|
434
|
+
@connect
|
|
435
|
+
async def get_users(conn=Inject(fancy_engine)):
|
|
436
|
+
result = await conn.execute(sa.select(users.c.name).order_by(users.c.name))
|
|
437
|
+
return [row[0] for row in result]
|
|
438
|
+
|
|
439
|
+
@transact
|
|
440
|
+
async def create_multiple_users(names: list[str], conn=Inject(fancy_engine)):
|
|
441
|
+
for name in names:
|
|
442
|
+
await create_user(name, conn=conn)
|
|
443
|
+
|
|
444
|
+
# Create users using nested decorators
|
|
445
|
+
await create_multiple_users(["Alice", "Bob", "Charlie"])
|
|
446
|
+
|
|
447
|
+
# Verify all users were created
|
|
448
|
+
assert await get_users() == ["Alice", "Bob", "Charlie"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|