sqla-fancy-core 1.2.0__tar.gz → 1.2.3__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.2.0 → sqla_fancy_core-1.2.3}/PKG-INFO +39 -64
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/README.md +35 -60
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/pyproject.toml +13 -4
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/sqla_fancy_core/wrappers.py +17 -3
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/tests/test_async_atomic.py +0 -1
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/tests/test_transact.py +2 -1
- sqla_fancy_core-1.2.3/uv.lock +1399 -0
- sqla_fancy_core-1.2.0/uv.lock +0 -3503
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/.github/workflows/ci.yaml +0 -0
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/.gitignore +0 -0
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/LICENSE +0 -0
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/sqla_fancy_core/__init__.py +0 -0
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/sqla_fancy_core/decorators.py +0 -0
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/sqla_fancy_core/factories.py +0 -0
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/tests/__init__.py +0 -0
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/tests/test_async_fancy_engine.py +0 -0
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/tests/test_atomic.py +0 -0
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/tests/test_connect.py +0 -0
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/tests/test_decorators.py +0 -0
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/tests/test_fancy_engine.py +0 -0
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/tests/test_field.py +0 -0
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/tests/test_table_factory.py +0 -0
- {sqla_fancy_core-1.2.0 → sqla_fancy_core-1.2.3}/tests/test_table_factory_async.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqla-fancy-core
|
|
3
|
-
Version: 1.2.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.2.3
|
|
4
|
+
Summary: A collection of type-safe, async friendly, and un-opinionated enhancements to SQLAlchemy Core that works well with mordern web servers.
|
|
5
5
|
Project-URL: Homepage, https://github.com/sayanarijit/sqla-fancy-core
|
|
6
6
|
Author-email: Arijit Basu <sayanarijit@gmail.com>
|
|
7
7
|
Maintainer-email: Arijit Basu <sayanarijit@gmail.com>
|
|
@@ -27,11 +27,11 @@ License: MIT License
|
|
|
27
27
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
28
|
SOFTWARE.
|
|
29
29
|
License-File: LICENSE
|
|
30
|
-
Keywords: sql,sqlalchemy,
|
|
30
|
+
Keywords: async,database,fastapi,orm,sql,sqlalchemy,type-safe,web
|
|
31
31
|
Classifier: Intended Audience :: Developers
|
|
32
32
|
Classifier: License :: OSI Approved :: MIT License
|
|
33
33
|
Classifier: Programming Language :: Python :: 3
|
|
34
|
-
Requires-Python: >=3.
|
|
34
|
+
Requires-Python: >=3.10
|
|
35
35
|
Requires-Dist: sqlalchemy
|
|
36
36
|
Provides-Extra: dev
|
|
37
37
|
Requires-Dist: build; extra == 'dev'
|
|
@@ -52,17 +52,27 @@ Description-Content-Type: text/markdown
|
|
|
52
52
|
|
|
53
53
|
# sqla-fancy-core
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
A collection of type-safe, async friendly, and un-opinionated enhancements to SQLAlchemy Core that works well with mordern web servers.
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
**Why?**
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
- ORMs are magical, but it's not always a feature. Sometimes, we crave for familiar.
|
|
60
|
+
- SQLAlchemy Core is powerful but `table.c.column` breaks static type checking and has runtime overhead. This library provides a better way to define tables while keeping all of SQLAlchemy's flexibility. See [Table Factory](#table-factory).
|
|
61
|
+
- The idea of sessions can get feel too magical and opinionated. This library removes the magic and opinions and takes you to back to familiar transactions's territory, providing multiple un-opinionated APIs to deal with it. See [Wrappers](#fancy-engine-wrappers) and [Decorators](#decorators-inject-connect-transact).
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
**Demos:**
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
- [FastAPI - sqla-fancy-core example app](https://github.com/sayanarijit/fastapi-sqla-fancy-core-example-app).
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
## Table factory
|
|
68
|
+
|
|
69
|
+
Define tables with static column references## Target audience
|
|
70
|
+
|
|
71
|
+
For production use by developers who prefer query builders over ORMs, need robust sync/async support, and want type-safe, readable code.
|
|
72
|
+
|
|
73
|
+
**Example:**
|
|
74
|
+
|
|
75
|
+
Define tables:
|
|
66
76
|
|
|
67
77
|
```python
|
|
68
78
|
import sqlalchemy as sa
|
|
@@ -79,7 +89,7 @@ class Author:
|
|
|
79
89
|
Table = tf("author")
|
|
80
90
|
```
|
|
81
91
|
|
|
82
|
-
|
|
92
|
+
For complex scenarios, define columns explicitly:
|
|
83
93
|
|
|
84
94
|
```python
|
|
85
95
|
class Book:
|
|
@@ -107,7 +117,7 @@ class Book:
|
|
|
107
117
|
Table = tf(sa.Table("book", sa.MetaData()))
|
|
108
118
|
```
|
|
109
119
|
|
|
110
|
-
|
|
120
|
+
Create tables:
|
|
111
121
|
|
|
112
122
|
```python
|
|
113
123
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
@@ -120,11 +130,7 @@ async with engine.begin() as conn:
|
|
|
120
130
|
await conn.run_sync(tf.metadata.create_all)
|
|
121
131
|
```
|
|
122
132
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
### CRUD Operations
|
|
126
|
-
|
|
127
|
-
Here's how you can interact with the database using the defined tables.
|
|
133
|
+
Perform CRUD operations:
|
|
128
134
|
|
|
129
135
|
```python
|
|
130
136
|
async with engine.begin() as txn:
|
|
@@ -158,15 +164,13 @@ async with engine.begin() as txn:
|
|
|
158
164
|
|
|
159
165
|
## Fancy Engine Wrappers
|
|
160
166
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
- `x(conn, query)`: Executes a query. It uses the provided `conn` if available, otherwise it creates a new connection.
|
|
164
|
-
- `tx(conn, query)`: Executes a query within a transaction. It uses the provided `conn` if available, otherwise it tries to use the atomic context if within one, else creates a new connection and begins a transaction.
|
|
165
|
-
- `atomic()`: A context manager for grouping multiple operations in a single transaction scope.
|
|
166
|
-
- `ax(query)`: Executes a query using the connection from the active `atomic()` context. Raises `AtomicContextError` if called outside an `atomic()` block.
|
|
167
|
-
- `atx(query)`: Executes a query inside a transaction automatically. If already inside `atomic()`, it reuses the same connection and transaction; otherwise it opens a new transaction just for this call.
|
|
167
|
+
Simplify connection and transaction management. The `fancy()` function wraps a SQLAlchemy engine and provides:
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
- `x(conn, query)`: Execute query with optional connection
|
|
170
|
+
- `tx(conn, query)`: Execute query in transaction
|
|
171
|
+
- `atomic()`: Context manager for transaction scope
|
|
172
|
+
- `ax(query)`: Execute inside `atomic()` context (raises `AtomicContextError` outside)
|
|
173
|
+
- `atx(query)`: Auto-transactional (reuses `atomic()` if present, or creates new transaction)
|
|
170
174
|
|
|
171
175
|
### Basic Examples
|
|
172
176
|
|
|
@@ -215,7 +219,7 @@ async def main():
|
|
|
215
219
|
|
|
216
220
|
### Using the atomic() Context Manager
|
|
217
221
|
|
|
218
|
-
|
|
222
|
+
Group operations in a single transaction. Nested `atomic()` contexts share the outer connection.
|
|
219
223
|
|
|
220
224
|
**Sync Example:**
|
|
221
225
|
|
|
@@ -272,36 +276,17 @@ async def run_example():
|
|
|
272
276
|
assert count == 2
|
|
273
277
|
```
|
|
274
278
|
|
|
275
|
-
**Key Points:**
|
|
276
|
-
|
|
277
|
-
- `ax()` must be called inside an `atomic()` context. Calling it elsewhere raises `AtomicContextError`.
|
|
278
|
-
- `atx()` is a safe/ergonomic helper: it will run inside the current `atomic()` transaction when present, or create a short-lived transaction otherwise.
|
|
279
|
-
- Nesting `atomic()` contexts is safe. Inner contexts share the outer connection instead of creating a new transaction.
|
|
280
|
-
- On normal exit, the transaction commits automatically. On exception, it rolls back.
|
|
281
|
-
|
|
282
|
-
### ax vs atx vs tx
|
|
283
|
-
|
|
284
|
-
- `ax(q)`: Only valid inside `atomic()`. Uses the ambient transactional connection. Great for batch operations grouped by an outer context.
|
|
285
|
-
- `atx(q)`: Fire-and-forget in a transaction. Reuses the ambient `atomic()` connection if present; otherwise starts and commits its own transaction.
|
|
286
|
-
- `tx(conn, q)`: Low-level primitive. If `conn` is provided, it executes within it, creating a transaction when needed; if `None`, it prefers the `atomic()` connection when active or opens a new transactional connection.
|
|
287
|
-
|
|
288
279
|
## Decorators: Inject, connect, transact
|
|
289
280
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
At the heart of it is `Inject(engine)`, a tiny marker used as a default parameter value to tell decorators where to inject a connection.
|
|
293
|
-
|
|
294
|
-
- `Inject(engine)`: marks which parameter should receive a connection derived from the given engine.
|
|
295
|
-
- `@connect`: ensures the injected parameter is a live connection. If you passed a connection explicitly, it will use that one as-is. Otherwise, it will open a new connection for the call and close it afterwards. No transaction is created by default.
|
|
296
|
-
- `@transact`: ensures the injected parameter is inside a transaction. If you pass a connection already in a transaction, it reuses it; if you pass a connection outside a transaction, it starts one; if you pass nothing, it opens a new connection and begins a transaction for the duration of the call.
|
|
281
|
+
Keep functions connection-agnostic with decorator-based injection.
|
|
297
282
|
|
|
298
|
-
|
|
283
|
+
**Components:**
|
|
299
284
|
|
|
300
|
-
|
|
285
|
+
- `Inject(engine)`: Marks parameter for connection injection
|
|
286
|
+
- `@connect`: Ensures live connection (no transaction by default)
|
|
287
|
+
- `@transact`: Ensures transactional connection
|
|
301
288
|
|
|
302
|
-
|
|
303
|
-
- Prefer `@transact` to wrap a function in a transaction automatically and consistently.
|
|
304
|
-
- You can still pass `conn=...` explicitly to either decorator to reuse an existing connection/transaction.
|
|
289
|
+
Use `@connect` for read-only operations. Use `@transact` for writes.
|
|
305
290
|
|
|
306
291
|
### Sync examples
|
|
307
292
|
|
|
@@ -378,11 +363,7 @@ async with engine.connect() as conn:
|
|
|
378
363
|
assert await get_user_count(conn=conn) == 2
|
|
379
364
|
```
|
|
380
365
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
These decorators pair nicely with frameworks like FastAPI. You can keep a single function that works both inside DI (with an injected connection) and outside it (self-managed).
|
|
384
|
-
|
|
385
|
-
Sync example with FastAPI:
|
|
366
|
+
Works with dependency injection frameworks like FastAPI:
|
|
386
367
|
|
|
387
368
|
```python
|
|
388
369
|
from typing import Annotated
|
|
@@ -408,7 +389,7 @@ def create_user(
|
|
|
408
389
|
create_user(name="outside fastapi")
|
|
409
390
|
```
|
|
410
391
|
|
|
411
|
-
Async
|
|
392
|
+
Async with FastAPI:
|
|
412
393
|
|
|
413
394
|
```python
|
|
414
395
|
from typing import Annotated
|
|
@@ -432,15 +413,9 @@ async def create_user(
|
|
|
432
413
|
await conn.execute(sa.insert(users).values(name=name))
|
|
433
414
|
```
|
|
434
415
|
|
|
435
|
-
Notes:
|
|
436
|
-
|
|
437
|
-
- `@connect` never starts a transaction by itself; `@transact` ensures one.
|
|
438
|
-
- Passing an explicit `conn` always wins — the decorators simply adapt to what you give them.
|
|
439
|
-
- The injection marker keeps your function signatures clean and type-checker friendly.
|
|
440
|
-
|
|
441
416
|
## With Pydantic Validation
|
|
442
417
|
|
|
443
|
-
|
|
418
|
+
Integrate with Pydantic for validation:
|
|
444
419
|
|
|
445
420
|
```python
|
|
446
421
|
from typing import Any
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
# sqla-fancy-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A collection of type-safe, async friendly, and un-opinionated enhancements to SQLAlchemy Core that works well with mordern web servers.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Why?**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- ORMs are magical, but it's not always a feature. Sometimes, we crave for familiar.
|
|
8
|
+
- SQLAlchemy Core is powerful but `table.c.column` breaks static type checking and has runtime overhead. This library provides a better way to define tables while keeping all of SQLAlchemy's flexibility. See [Table Factory](#table-factory).
|
|
9
|
+
- The idea of sessions can get feel too magical and opinionated. This library removes the magic and opinions and takes you to back to familiar transactions's territory, providing multiple un-opinionated APIs to deal with it. See [Wrappers](#fancy-engine-wrappers) and [Decorators](#decorators-inject-connect-transact).
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
**Demos:**
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
- [FastAPI - sqla-fancy-core example app](https://github.com/sayanarijit/fastapi-sqla-fancy-core-example-app).
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
## Table factory
|
|
16
|
+
|
|
17
|
+
Define tables with static column references## Target audience
|
|
18
|
+
|
|
19
|
+
For production use by developers who prefer query builders over ORMs, need robust sync/async support, and want type-safe, readable code.
|
|
20
|
+
|
|
21
|
+
**Example:**
|
|
22
|
+
|
|
23
|
+
Define tables:
|
|
14
24
|
|
|
15
25
|
```python
|
|
16
26
|
import sqlalchemy as sa
|
|
@@ -27,7 +37,7 @@ class Author:
|
|
|
27
37
|
Table = tf("author")
|
|
28
38
|
```
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
For complex scenarios, define columns explicitly:
|
|
31
41
|
|
|
32
42
|
```python
|
|
33
43
|
class Book:
|
|
@@ -55,7 +65,7 @@ class Book:
|
|
|
55
65
|
Table = tf(sa.Table("book", sa.MetaData()))
|
|
56
66
|
```
|
|
57
67
|
|
|
58
|
-
|
|
68
|
+
Create tables:
|
|
59
69
|
|
|
60
70
|
```python
|
|
61
71
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
@@ -68,11 +78,7 @@ async with engine.begin() as conn:
|
|
|
68
78
|
await conn.run_sync(tf.metadata.create_all)
|
|
69
79
|
```
|
|
70
80
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
### CRUD Operations
|
|
74
|
-
|
|
75
|
-
Here's how you can interact with the database using the defined tables.
|
|
81
|
+
Perform CRUD operations:
|
|
76
82
|
|
|
77
83
|
```python
|
|
78
84
|
async with engine.begin() as txn:
|
|
@@ -106,15 +112,13 @@ async with engine.begin() as txn:
|
|
|
106
112
|
|
|
107
113
|
## Fancy Engine Wrappers
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
- `x(conn, query)`: Executes a query. It uses the provided `conn` if available, otherwise it creates a new connection.
|
|
112
|
-
- `tx(conn, query)`: Executes a query within a transaction. It uses the provided `conn` if available, otherwise it tries to use the atomic context if within one, else creates a new connection and begins a transaction.
|
|
113
|
-
- `atomic()`: A context manager for grouping multiple operations in a single transaction scope.
|
|
114
|
-
- `ax(query)`: Executes a query using the connection from the active `atomic()` context. Raises `AtomicContextError` if called outside an `atomic()` block.
|
|
115
|
-
- `atx(query)`: Executes a query inside a transaction automatically. If already inside `atomic()`, it reuses the same connection and transaction; otherwise it opens a new transaction just for this call.
|
|
115
|
+
Simplify connection and transaction management. The `fancy()` function wraps a SQLAlchemy engine and provides:
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
- `x(conn, query)`: Execute query with optional connection
|
|
118
|
+
- `tx(conn, query)`: Execute query in transaction
|
|
119
|
+
- `atomic()`: Context manager for transaction scope
|
|
120
|
+
- `ax(query)`: Execute inside `atomic()` context (raises `AtomicContextError` outside)
|
|
121
|
+
- `atx(query)`: Auto-transactional (reuses `atomic()` if present, or creates new transaction)
|
|
118
122
|
|
|
119
123
|
### Basic Examples
|
|
120
124
|
|
|
@@ -163,7 +167,7 @@ async def main():
|
|
|
163
167
|
|
|
164
168
|
### Using the atomic() Context Manager
|
|
165
169
|
|
|
166
|
-
|
|
170
|
+
Group operations in a single transaction. Nested `atomic()` contexts share the outer connection.
|
|
167
171
|
|
|
168
172
|
**Sync Example:**
|
|
169
173
|
|
|
@@ -220,36 +224,17 @@ async def run_example():
|
|
|
220
224
|
assert count == 2
|
|
221
225
|
```
|
|
222
226
|
|
|
223
|
-
**Key Points:**
|
|
224
|
-
|
|
225
|
-
- `ax()` must be called inside an `atomic()` context. Calling it elsewhere raises `AtomicContextError`.
|
|
226
|
-
- `atx()` is a safe/ergonomic helper: it will run inside the current `atomic()` transaction when present, or create a short-lived transaction otherwise.
|
|
227
|
-
- Nesting `atomic()` contexts is safe. Inner contexts share the outer connection instead of creating a new transaction.
|
|
228
|
-
- On normal exit, the transaction commits automatically. On exception, it rolls back.
|
|
229
|
-
|
|
230
|
-
### ax vs atx vs tx
|
|
231
|
-
|
|
232
|
-
- `ax(q)`: Only valid inside `atomic()`. Uses the ambient transactional connection. Great for batch operations grouped by an outer context.
|
|
233
|
-
- `atx(q)`: Fire-and-forget in a transaction. Reuses the ambient `atomic()` connection if present; otherwise starts and commits its own transaction.
|
|
234
|
-
- `tx(conn, q)`: Low-level primitive. If `conn` is provided, it executes within it, creating a transaction when needed; if `None`, it prefers the `atomic()` connection when active or opens a new transactional connection.
|
|
235
|
-
|
|
236
227
|
## Decorators: Inject, connect, transact
|
|
237
228
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
At the heart of it is `Inject(engine)`, a tiny marker used as a default parameter value to tell decorators where to inject a connection.
|
|
241
|
-
|
|
242
|
-
- `Inject(engine)`: marks which parameter should receive a connection derived from the given engine.
|
|
243
|
-
- `@connect`: ensures the injected parameter is a live connection. If you passed a connection explicitly, it will use that one as-is. Otherwise, it will open a new connection for the call and close it afterwards. No transaction is created by default.
|
|
244
|
-
- `@transact`: ensures the injected parameter is inside a transaction. If you pass a connection already in a transaction, it reuses it; if you pass a connection outside a transaction, it starts one; if you pass nothing, it opens a new connection and begins a transaction for the duration of the call.
|
|
229
|
+
Keep functions connection-agnostic with decorator-based injection.
|
|
245
230
|
|
|
246
|
-
|
|
231
|
+
**Components:**
|
|
247
232
|
|
|
248
|
-
|
|
233
|
+
- `Inject(engine)`: Marks parameter for connection injection
|
|
234
|
+
- `@connect`: Ensures live connection (no transaction by default)
|
|
235
|
+
- `@transact`: Ensures transactional connection
|
|
249
236
|
|
|
250
|
-
|
|
251
|
-
- Prefer `@transact` to wrap a function in a transaction automatically and consistently.
|
|
252
|
-
- You can still pass `conn=...` explicitly to either decorator to reuse an existing connection/transaction.
|
|
237
|
+
Use `@connect` for read-only operations. Use `@transact` for writes.
|
|
253
238
|
|
|
254
239
|
### Sync examples
|
|
255
240
|
|
|
@@ -326,11 +311,7 @@ async with engine.connect() as conn:
|
|
|
326
311
|
assert await get_user_count(conn=conn) == 2
|
|
327
312
|
```
|
|
328
313
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
These decorators pair nicely with frameworks like FastAPI. You can keep a single function that works both inside DI (with an injected connection) and outside it (self-managed).
|
|
332
|
-
|
|
333
|
-
Sync example with FastAPI:
|
|
314
|
+
Works with dependency injection frameworks like FastAPI:
|
|
334
315
|
|
|
335
316
|
```python
|
|
336
317
|
from typing import Annotated
|
|
@@ -356,7 +337,7 @@ def create_user(
|
|
|
356
337
|
create_user(name="outside fastapi")
|
|
357
338
|
```
|
|
358
339
|
|
|
359
|
-
Async
|
|
340
|
+
Async with FastAPI:
|
|
360
341
|
|
|
361
342
|
```python
|
|
362
343
|
from typing import Annotated
|
|
@@ -380,15 +361,9 @@ async def create_user(
|
|
|
380
361
|
await conn.execute(sa.insert(users).values(name=name))
|
|
381
362
|
```
|
|
382
363
|
|
|
383
|
-
Notes:
|
|
384
|
-
|
|
385
|
-
- `@connect` never starts a transaction by itself; `@transact` ensures one.
|
|
386
|
-
- Passing an explicit `conn` always wins — the decorators simply adapt to what you give them.
|
|
387
|
-
- The injection marker keeps your function signatures clean and type-checker friendly.
|
|
388
|
-
|
|
389
364
|
## With Pydantic Validation
|
|
390
365
|
|
|
391
|
-
|
|
366
|
+
Integrate with Pydantic for validation:
|
|
392
367
|
|
|
393
368
|
```python
|
|
394
369
|
from typing import Any
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = 'sqla-fancy-core'
|
|
3
|
-
version = '1.2.
|
|
4
|
-
description = '
|
|
3
|
+
version = '1.2.3'
|
|
4
|
+
description = 'A collection of type-safe, async friendly, and un-opinionated enhancements to SQLAlchemy Core that works well with mordern web servers.'
|
|
5
5
|
readme = 'README.md'
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
7
|
license = { file = "LICENSE" }
|
|
8
|
-
keywords = [
|
|
8
|
+
keywords = [
|
|
9
|
+
"sql",
|
|
10
|
+
"sqlalchemy",
|
|
11
|
+
"database",
|
|
12
|
+
"async",
|
|
13
|
+
"type-safe",
|
|
14
|
+
"orm",
|
|
15
|
+
"web",
|
|
16
|
+
"fastapi",
|
|
17
|
+
]
|
|
9
18
|
authors = [{ name = "Arijit Basu", email = "sayanarijit@gmail.com" }]
|
|
10
19
|
maintainers = [{ name = "Arijit Basu", email = "sayanarijit@gmail.com" }]
|
|
11
20
|
classifiers = [
|
|
@@ -206,10 +206,17 @@ class FancyEngineWrapper:
|
|
|
206
206
|
execution_options: Optional[CoreExecuteOptionsParameter] = None,
|
|
207
207
|
) -> CursorResult[Any]:
|
|
208
208
|
"""If within an atomic context, execute the query there; else, create a new transaction."""
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
|
|
210
|
+
conn = self._ATOMIC_TX_CONN.get()
|
|
211
|
+
if conn:
|
|
212
|
+
return conn.execute(
|
|
211
213
|
statement, parameters, execution_options=execution_options
|
|
212
214
|
)
|
|
215
|
+
else:
|
|
216
|
+
with self.engine.begin() as conn:
|
|
217
|
+
return conn.execute(
|
|
218
|
+
statement, parameters, execution_options=execution_options
|
|
219
|
+
)
|
|
213
220
|
|
|
214
221
|
|
|
215
222
|
class AsyncFancyEngineWrapper:
|
|
@@ -383,10 +390,17 @@ class AsyncFancyEngineWrapper:
|
|
|
383
390
|
execution_options: Optional[CoreExecuteOptionsParameter] = None,
|
|
384
391
|
) -> CursorResult[Any]:
|
|
385
392
|
"""If within an atomic context, execute the query there; else, create a new transaction."""
|
|
386
|
-
|
|
393
|
+
|
|
394
|
+
connection = self._ATOMIC_TX_CONN.get()
|
|
395
|
+
if connection:
|
|
387
396
|
return await connection.execute(
|
|
388
397
|
statement, parameters, execution_options=execution_options
|
|
389
398
|
)
|
|
399
|
+
else:
|
|
400
|
+
async with self.engine.begin() as connection:
|
|
401
|
+
return await connection.execute(
|
|
402
|
+
statement, parameters, execution_options=execution_options
|
|
403
|
+
)
|
|
390
404
|
|
|
391
405
|
|
|
392
406
|
@overload
|
|
@@ -5,6 +5,7 @@ import pytest_asyncio
|
|
|
5
5
|
import sqlalchemy as sa
|
|
6
6
|
from fastapi import Form
|
|
7
7
|
from sqlalchemy.ext.asyncio import AsyncConnection, create_async_engine
|
|
8
|
+
from sqlalchemy.pool import StaticPool
|
|
8
9
|
|
|
9
10
|
from sqla_fancy_core.decorators import Inject, transact
|
|
10
11
|
|
|
@@ -24,7 +25,7 @@ def sync_engine():
|
|
|
24
25
|
engine = sa.create_engine(
|
|
25
26
|
"sqlite+pysqlite:///:memory:",
|
|
26
27
|
connect_args={"check_same_thread": False},
|
|
27
|
-
poolclass=
|
|
28
|
+
poolclass=StaticPool,
|
|
28
29
|
)
|
|
29
30
|
metadata.create_all(engine)
|
|
30
31
|
yield engine
|