sqlobjects 1.3.0__tar.gz → 1.5.0__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.
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/CHANGELOG.md +12 -0
- {sqlobjects-1.3.0/sqlobjects.egg-info → sqlobjects-1.5.0}/PKG-INFO +1 -1
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/docs/rules/01-database-session-guide.md +60 -6
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/docs/rules/03-query-operations-guide.md +41 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/docs/rules/05-relationships-guide.md +3 -1
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/pyproject.toml +1 -1
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/cascade.py +37 -2
- sqlobjects-1.5.0/sqlobjects/contrib/__init__.py +0 -0
- sqlobjects-1.5.0/sqlobjects/contrib/asgi.py +63 -0
- sqlobjects-1.5.0/sqlobjects/contrib/fastapi.py +26 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/model.py +9 -2
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/queryset.py +3 -2
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/session.py +23 -9
- {sqlobjects-1.3.0 → sqlobjects-1.5.0/sqlobjects.egg-info}/PKG-INFO +1 -1
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects.egg-info/SOURCES.txt +3 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/LICENSE +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/README.md +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/docs/rules/02-model-definition-guide.md +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/docs/rules/04-crud-operations-guide.md +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/docs/rules/06-validation-signals-guide.md +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/docs/rules/07-performance-guide.md +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/docs/rules/README.md +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/setup.cfg +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/__init__.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/_install_rules.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/database/__init__.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/database/config.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/database/manager.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/exceptions.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/expressions/__init__.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/expressions/aggregate.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/expressions/base.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/expressions/cte.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/expressions/explain.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/expressions/function.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/expressions/mixins.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/expressions/scalar.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/expressions/subquery.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/expressions/terminal.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/expressions/window.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/__init__.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/core.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/functions.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/proxies.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/relations/__init__.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/relations/descriptors.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/relations/managers.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/relations/prefetch.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/relations/strategies.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/relations/utils.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/shortcuts.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/types/__init__.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/types/base.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/types/comparators.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/types/registry.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/fields/utils.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/internal/__init__.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/internal/operations.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/internal/results.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/metadata.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/mixins.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/objects/__init__.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/objects/bulk.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/objects/core.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/objects/upsert.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/queries/__init__.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/queries/builder.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/queries/dialect.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/queries/executor.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/signals.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/utils/__init__.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/utils/inspect.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/utils/naming.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/utils/pattern.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects/validators.py +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects.egg-info/dependency_links.txt +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects.egg-info/entry_points.txt +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects.egg-info/requires.txt +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/sqlobjects.egg-info/top_level.txt +0 -0
- {sqlobjects-1.3.0 → sqlobjects-1.5.0}/tests/test_config.py +0 -0
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## 1.5.0 (2026-03-16)
|
|
2
|
+
|
|
3
|
+
### Feat
|
|
4
|
+
|
|
5
|
+
- **cascade**: unify cascade strategy with auto-detection for Model.delete()
|
|
6
|
+
|
|
7
|
+
## 1.4.0 (2026-03-11)
|
|
8
|
+
|
|
9
|
+
### Feat
|
|
10
|
+
|
|
11
|
+
- **session**: fix nested ctx_session() and add ASGI/FastAPI integration
|
|
12
|
+
|
|
1
13
|
## 1.3.0 (2026-03-10)
|
|
2
14
|
|
|
3
15
|
### Feat
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlobjects
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading
|
|
5
5
|
Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
|
|
6
6
|
Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
|
|
@@ -71,6 +71,21 @@ async with ctx_session() as session:
|
|
|
71
71
|
user = await User.objects.using("analytics").create(username="test")
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
### Session Availability Check
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from sqlobjects.session import has_session
|
|
78
|
+
|
|
79
|
+
# Check if an explicit session is active
|
|
80
|
+
if has_session():
|
|
81
|
+
# Inside a ctx_session() block — use the existing session
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
# Check for a specific database
|
|
85
|
+
if has_session("analytics"):
|
|
86
|
+
pass
|
|
87
|
+
```
|
|
88
|
+
|
|
74
89
|
## Best Practices
|
|
75
90
|
|
|
76
91
|
### ✅ Do
|
|
@@ -105,11 +120,12 @@ await init_db(
|
|
|
105
120
|
- **Don't create sessions manually** (use context managers)
|
|
106
121
|
|
|
107
122
|
```python
|
|
108
|
-
#
|
|
123
|
+
# Caution: Nested sessions use separate transactions
|
|
109
124
|
async with ctx_session() as session1:
|
|
110
125
|
user = await User.objects.using(session1).create(username="alice")
|
|
111
126
|
async with ctx_session() as session2:
|
|
112
|
-
#
|
|
127
|
+
# Nesting is safe (Token-based restore), but uses a separate transaction
|
|
128
|
+
# — session1 won't see session2's uncommitted changes and vice versa
|
|
113
129
|
await Post.objects.using(session2).create(author_id=user.id)
|
|
114
130
|
|
|
115
131
|
# Bad: No transaction control
|
|
@@ -251,24 +267,24 @@ async def main():
|
|
|
251
267
|
"main": {"url": "postgresql://localhost/main", "pool_size": 20},
|
|
252
268
|
"analytics": {"url": "sqlite:///analytics.db"}
|
|
253
269
|
}, default="main")
|
|
254
|
-
|
|
270
|
+
|
|
255
271
|
await create_tables(ObjectModel, "main")
|
|
256
272
|
await create_tables(ObjectModel, "analytics")
|
|
257
|
-
|
|
273
|
+
|
|
258
274
|
# Single database transaction
|
|
259
275
|
async with ctx_session() as session:
|
|
260
276
|
user = await User.objects.using(session).create(
|
|
261
277
|
username="alice",
|
|
262
278
|
email="alice@example.com"
|
|
263
279
|
)
|
|
264
|
-
|
|
280
|
+
|
|
265
281
|
# Multi-database transaction
|
|
266
282
|
async with ctx_sessions("main", "analytics") as sessions:
|
|
267
283
|
user = await User.objects.using(sessions["main"]).get(User.username == "alice")
|
|
268
284
|
await Log.objects.using(sessions["analytics"]).create(
|
|
269
285
|
message=f"User {user.username} logged in"
|
|
270
286
|
)
|
|
271
|
-
|
|
287
|
+
|
|
272
288
|
# Cleanup
|
|
273
289
|
await close_all_dbs()
|
|
274
290
|
|
|
@@ -276,3 +292,41 @@ async def main():
|
|
|
276
292
|
import asyncio
|
|
277
293
|
asyncio.run(main())
|
|
278
294
|
```
|
|
295
|
+
|
|
296
|
+
## Web Framework Integration
|
|
297
|
+
|
|
298
|
+
### ASGI Middleware
|
|
299
|
+
|
|
300
|
+
Use `SessionMiddleware` for automatic request-scoped session management in any ASGI framework (FastAPI, Starlette, etc.):
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
from fastapi import FastAPI
|
|
304
|
+
from sqlobjects.contrib.asgi import SessionMiddleware
|
|
305
|
+
|
|
306
|
+
app = FastAPI()
|
|
307
|
+
app.add_middleware(SessionMiddleware)
|
|
308
|
+
# Each request gets an auto-managed session: commit on success, rollback on error
|
|
309
|
+
|
|
310
|
+
# Optional: target a specific database or use readonly mode
|
|
311
|
+
app.add_middleware(SessionMiddleware, db_name="analytics", readonly=True)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### FastAPI Dependency Injection
|
|
315
|
+
|
|
316
|
+
Use `get_db_session` for explicit session access in route handlers:
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
from fastapi import Depends
|
|
320
|
+
from sqlobjects.contrib.fastapi import get_db_session
|
|
321
|
+
from sqlobjects.session import AsyncSession
|
|
322
|
+
|
|
323
|
+
@app.post("/users")
|
|
324
|
+
async def create_user(session: AsyncSession = Depends(get_db_session)):
|
|
325
|
+
user = await User.objects.using(session).create(username="alice")
|
|
326
|
+
return {"id": user.id}
|
|
327
|
+
|
|
328
|
+
@app.get("/users/{user_id}")
|
|
329
|
+
async def get_user(user_id: int, session: AsyncSession = Depends(get_db_session)):
|
|
330
|
+
user = await User.objects.using(session).get(User.id == user_id)
|
|
331
|
+
return {"username": user.username}
|
|
332
|
+
```
|
|
@@ -204,6 +204,47 @@ active_users = User.objects.filter(User.is_active == True).subquery("active_user
|
|
|
204
204
|
posts = await Post.objects.join(active_users, Post.author_id == active_users.c.id).all()
|
|
205
205
|
```
|
|
206
206
|
|
|
207
|
+
### Window Functions
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from sqlobjects.expressions import func
|
|
211
|
+
|
|
212
|
+
# Window functions are used via annotate() + func.xxx().over()
|
|
213
|
+
users = await User.objects.annotate(
|
|
214
|
+
row_num=func.row_number().over(order_by=[User.created_at])
|
|
215
|
+
).all()
|
|
216
|
+
|
|
217
|
+
# Ranking with partitions
|
|
218
|
+
users = await User.objects.annotate(
|
|
219
|
+
dept_rank=func.rank().over(
|
|
220
|
+
partition_by=[User.department],
|
|
221
|
+
order_by=[(User.salary, 'desc')]
|
|
222
|
+
)
|
|
223
|
+
).all()
|
|
224
|
+
|
|
225
|
+
# LAG/LEAD for adjacent row access
|
|
226
|
+
users = await User.objects.annotate(
|
|
227
|
+
prev_salary=func.lag(User.salary, 1).over(order_by=[User.created_at])
|
|
228
|
+
).all()
|
|
229
|
+
|
|
230
|
+
# Available: row_number(), rank(), dense_rank(), percent_rank(),
|
|
231
|
+
# ntile(n), lag(), lead(), first_value(), last_value(), nth_value()
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### CTE (Common Table Expressions)
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
# Basic CTE: define a reusable named subquery
|
|
238
|
+
adults = User.objects.filter(User.age >= 18).cte("adults")
|
|
239
|
+
result = await User.objects.with_cte(adults).filter(adults.c.age < 30).all()
|
|
240
|
+
|
|
241
|
+
# Recursive CTE: hierarchical queries
|
|
242
|
+
base = Employee.objects.filter(Employee.manager_id.is_(None)).cte("hierarchy", recursive=True)
|
|
243
|
+
recursive_part = Employee.objects.join(base, Employee.manager_id == base.c.id)
|
|
244
|
+
hierarchy = base.union_all(recursive_part)
|
|
245
|
+
all_employees = await Employee.objects.with_cte(hierarchy).select_from(hierarchy).all()
|
|
246
|
+
```
|
|
247
|
+
|
|
207
248
|
### Manual Joins
|
|
208
249
|
|
|
209
250
|
```python
|
|
@@ -19,7 +19,9 @@ from sqlobjects.fields import Column, StringColumn, foreign_key, relationship, R
|
|
|
19
19
|
# One-to-Many (Foreign Key)
|
|
20
20
|
class Post(ObjectModel):
|
|
21
21
|
title: Column[str] = StringColumn(length=200)
|
|
22
|
-
|
|
22
|
+
# Both class name and table name are supported:
|
|
23
|
+
author_id: Column[int] = foreign_key("User.id") # class name (auto-resolved)
|
|
24
|
+
# author_id: Column[int] = foreign_key("users.id") # table name (also works)
|
|
23
25
|
author: Related["User"] = relationship("User", back_populates="posts")
|
|
24
26
|
|
|
25
27
|
class User(ObjectModel):
|
|
@@ -28,6 +28,7 @@ __all__ = [
|
|
|
28
28
|
"normalize_onupdate",
|
|
29
29
|
"normalize_cascade",
|
|
30
30
|
"has_cascade_delete_relations",
|
|
31
|
+
"has_delete_signals",
|
|
31
32
|
]
|
|
32
33
|
|
|
33
34
|
|
|
@@ -130,6 +131,30 @@ def has_cascade_delete_relations(model_class) -> bool:
|
|
|
130
131
|
return False
|
|
131
132
|
|
|
132
133
|
|
|
134
|
+
def has_delete_signals(model_class) -> bool:
|
|
135
|
+
"""Check if model class has delete signal handlers defined.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
model_class: Model class to check
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if model has before_delete/after_delete handlers, False otherwise
|
|
142
|
+
"""
|
|
143
|
+
from .signals import SignalMixin
|
|
144
|
+
|
|
145
|
+
if not issubclass(model_class, SignalMixin):
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
for method_name in ("before_delete", "after_delete"):
|
|
149
|
+
method = getattr(model_class, method_name, None)
|
|
150
|
+
if method is None:
|
|
151
|
+
continue
|
|
152
|
+
# Must be defined on the model itself, not inherited from SignalMixin
|
|
153
|
+
if method_name in model_class.__dict__:
|
|
154
|
+
return True
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
133
158
|
def normalize_cascade(cascade: CascadeType) -> str:
|
|
134
159
|
"""Normalize cascade parameter to SQLAlchemy string format."""
|
|
135
160
|
if cascade is None:
|
|
@@ -344,7 +369,7 @@ class CascadeExecutor:
|
|
|
344
369
|
return instance
|
|
345
370
|
|
|
346
371
|
async def execute_delete_operation(
|
|
347
|
-
self, target, cascade_strategy: str = "
|
|
372
|
+
self, target, cascade_strategy: str = "auto", session: "AsyncSession | None" = None
|
|
348
373
|
) -> int:
|
|
349
374
|
"""Execute delete operation with cascade handling."""
|
|
350
375
|
if session is None:
|
|
@@ -355,7 +380,8 @@ class CascadeExecutor:
|
|
|
355
380
|
return await self._execute_queryset_delete(target, cascade_strategy, session)
|
|
356
381
|
|
|
357
382
|
# Handle single instance deletion
|
|
358
|
-
|
|
383
|
+
if has_cascade_delete_relations(target.__class__):
|
|
384
|
+
await self._delete_related_objects(target, session)
|
|
359
385
|
await target._delete_internal(session=session) # noqa
|
|
360
386
|
return 1
|
|
361
387
|
|
|
@@ -478,6 +504,15 @@ class CascadeExecutor:
|
|
|
478
504
|
|
|
479
505
|
async def _execute_queryset_delete(self, queryset, cascade_strategy: str, session: "AsyncSession") -> int:
|
|
480
506
|
"""Execute QuerySet delete with different cascade strategies."""
|
|
507
|
+
if cascade_strategy == "auto":
|
|
508
|
+
model_class = queryset._model_class
|
|
509
|
+
if has_delete_signals(model_class):
|
|
510
|
+
cascade_strategy = "full"
|
|
511
|
+
elif has_cascade_delete_relations(model_class):
|
|
512
|
+
cascade_strategy = "fast"
|
|
513
|
+
else:
|
|
514
|
+
cascade_strategy = "none"
|
|
515
|
+
|
|
481
516
|
if cascade_strategy == "full":
|
|
482
517
|
return await self._delete_with_full_cascade(queryset, session)
|
|
483
518
|
elif cascade_strategy == "fast":
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""ASGI middleware for request-scoped SQLObjects session management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..database.manager import get_default
|
|
8
|
+
from ..session import AsyncSession, _SessionContextManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SessionMiddleware:
|
|
12
|
+
"""ASGI middleware that provides a request-scoped database session.
|
|
13
|
+
|
|
14
|
+
Automatically creates an AsyncSession for each HTTP/WebSocket request,
|
|
15
|
+
sets it in the ContextVar so that model operations use it, and commits
|
|
16
|
+
on success or rolls back on failure.
|
|
17
|
+
|
|
18
|
+
Usage with FastAPI::
|
|
19
|
+
|
|
20
|
+
from fastapi import FastAPI
|
|
21
|
+
from sqlobjects.contrib.asgi import SessionMiddleware
|
|
22
|
+
|
|
23
|
+
app = FastAPI()
|
|
24
|
+
app.add_middleware(SessionMiddleware)
|
|
25
|
+
|
|
26
|
+
Usage with Starlette::
|
|
27
|
+
|
|
28
|
+
from starlette.applications import Starlette
|
|
29
|
+
from sqlobjects.contrib.asgi import SessionMiddleware
|
|
30
|
+
|
|
31
|
+
app = Starlette()
|
|
32
|
+
app.add_middleware(SessionMiddleware)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
app: Any,
|
|
38
|
+
db_name: str | None = None,
|
|
39
|
+
readonly: bool = False,
|
|
40
|
+
) -> None:
|
|
41
|
+
self.app = app
|
|
42
|
+
self.db_name = db_name
|
|
43
|
+
self.readonly = readonly
|
|
44
|
+
|
|
45
|
+
async def __call__(self, scope: dict[str, Any], receive: Any, send: Any) -> None:
|
|
46
|
+
if scope["type"] not in ("http", "websocket"):
|
|
47
|
+
await self.app(scope, receive, send)
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
name = self.db_name or get_default()
|
|
51
|
+
session = AsyncSession(name, readonly=self.readonly, auto_commit=False)
|
|
52
|
+
token = _SessionContextManager.set_session(session, name)
|
|
53
|
+
try:
|
|
54
|
+
await self.app(scope, receive, send)
|
|
55
|
+
if not self.readonly:
|
|
56
|
+
await session.commit()
|
|
57
|
+
except Exception:
|
|
58
|
+
if not self.readonly:
|
|
59
|
+
await session.rollback()
|
|
60
|
+
raise
|
|
61
|
+
finally:
|
|
62
|
+
await session.close()
|
|
63
|
+
_SessionContextManager.reset_session(token)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""FastAPI dependency for SQLObjects session management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncGenerator
|
|
6
|
+
|
|
7
|
+
from ..session import AsyncSession, ctx_session
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def get_db_session(db_name: str | None = None) -> AsyncGenerator[AsyncSession, None]:
|
|
11
|
+
"""FastAPI dependency that yields a transactional session.
|
|
12
|
+
|
|
13
|
+
Usage::
|
|
14
|
+
|
|
15
|
+
from fastapi import Depends
|
|
16
|
+
from sqlobjects.contrib.fastapi import get_db_session
|
|
17
|
+
from sqlobjects.session import AsyncSession
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.post("/users")
|
|
21
|
+
async def create_user(session: AsyncSession = Depends(get_db_session)):
|
|
22
|
+
user = await User.objects.using(session).create(name="Alice")
|
|
23
|
+
return {"id": user.id}
|
|
24
|
+
"""
|
|
25
|
+
async with ctx_session(db_name) as session:
|
|
26
|
+
yield session
|
|
@@ -278,11 +278,14 @@ class ModelMixin(DataConversionMixin, SignalMixin):
|
|
|
278
278
|
return await self._save_internal(validate=validate, session=session)
|
|
279
279
|
|
|
280
280
|
@emit_signals(Operation.DELETE)
|
|
281
|
-
async def delete(self, cascade: bool =
|
|
281
|
+
async def delete(self, cascade: bool | None = None):
|
|
282
282
|
"""Delete this model instance from the database with cascade support.
|
|
283
283
|
|
|
284
284
|
Args:
|
|
285
|
-
cascade: Whether to handle cascade deletion
|
|
285
|
+
cascade: Whether to handle cascade deletion.
|
|
286
|
+
None (default): auto-detect based on model relationships
|
|
287
|
+
True: force cascade handling
|
|
288
|
+
False: skip cascade, direct delete
|
|
286
289
|
|
|
287
290
|
Raises:
|
|
288
291
|
PrimaryKeyError: If instance has no primary key values or delete fails
|
|
@@ -292,6 +295,10 @@ class ModelMixin(DataConversionMixin, SignalMixin):
|
|
|
292
295
|
|
|
293
296
|
session = self.get_session()
|
|
294
297
|
|
|
298
|
+
# Auto-detect cascade need when not explicitly specified
|
|
299
|
+
if cascade is None:
|
|
300
|
+
cascade = self._has_on_delete_relations()
|
|
301
|
+
|
|
295
302
|
# Use cascade executor for cascade operations
|
|
296
303
|
if cascade:
|
|
297
304
|
executor = CascadeExecutor()
|
|
@@ -1221,12 +1221,13 @@ class QuerySet(Generic[T]):
|
|
|
1221
1221
|
return await executor.execute_update_operation(self, values)
|
|
1222
1222
|
|
|
1223
1223
|
@emit_signals(Operation.DELETE, is_bulk=True)
|
|
1224
|
-
async def delete(self, cascade: str = "
|
|
1224
|
+
async def delete(self, cascade: str = "auto") -> int:
|
|
1225
1225
|
"""Perform bulk delete on objects matching query conditions.
|
|
1226
1226
|
|
|
1227
1227
|
Args:
|
|
1228
1228
|
cascade: Cascade deletion strategy
|
|
1229
|
-
- "
|
|
1229
|
+
- "auto" (default): Automatically choose based on model relationships
|
|
1230
|
+
- "full": Complete cascade deletion with full ORM functionality
|
|
1230
1231
|
- "fast": Fast cascade deletion with minimal ORM processing
|
|
1231
1232
|
- "none": Direct SQL deletion without ORM cascade processing
|
|
1232
1233
|
|
|
@@ -259,8 +259,14 @@ class _SessionContextManager:
|
|
|
259
259
|
return AsyncSession(name, readonly, auto_commit)
|
|
260
260
|
|
|
261
261
|
@classmethod
|
|
262
|
-
def set_session(
|
|
263
|
-
|
|
262
|
+
def set_session(
|
|
263
|
+
cls, session: AsyncSession, db_name: str | None = None
|
|
264
|
+
) -> contextvars.Token[dict[str, "AsyncSession"]]:
|
|
265
|
+
"""Set active session in current context.
|
|
266
|
+
|
|
267
|
+
Returns a Token that can be used with reset_session() to restore the
|
|
268
|
+
previous ContextVar state, enabling correct nested ctx_session() behavior.
|
|
269
|
+
"""
|
|
264
270
|
name = db_name or get_default()
|
|
265
271
|
try:
|
|
266
272
|
current_sessions = _explicit_sessions.get({})
|
|
@@ -268,7 +274,12 @@ class _SessionContextManager:
|
|
|
268
274
|
current_sessions = {}
|
|
269
275
|
new_sessions = current_sessions.copy()
|
|
270
276
|
new_sessions[name] = session
|
|
271
|
-
_explicit_sessions.set(new_sessions)
|
|
277
|
+
return _explicit_sessions.set(new_sessions)
|
|
278
|
+
|
|
279
|
+
@classmethod
|
|
280
|
+
def reset_session(cls, token: contextvars.Token[dict[str, "AsyncSession"]]) -> None:
|
|
281
|
+
"""Restore ContextVar to a previous state using a token from set_session()."""
|
|
282
|
+
_explicit_sessions.reset(token)
|
|
272
283
|
|
|
273
284
|
@classmethod
|
|
274
285
|
def clear_session(cls, db_name: str | None = None) -> None:
|
|
@@ -302,8 +313,8 @@ async def ctx_session(db_name: str | None = None) -> AsyncGenerator[AsyncSession
|
|
|
302
313
|
name = db_name or get_default()
|
|
303
314
|
session = AsyncSession(name, readonly=False, auto_commit=False)
|
|
304
315
|
|
|
305
|
-
# Set as explicit session in context
|
|
306
|
-
_SessionContextManager.set_session(session, name)
|
|
316
|
+
# Set as explicit session in context, save token for nested restore
|
|
317
|
+
token = _SessionContextManager.set_session(session, name)
|
|
307
318
|
|
|
308
319
|
try:
|
|
309
320
|
yield session
|
|
@@ -316,7 +327,7 @@ async def ctx_session(db_name: str | None = None) -> AsyncGenerator[AsyncSession
|
|
|
316
327
|
finally:
|
|
317
328
|
# Cleanup
|
|
318
329
|
await session.close()
|
|
319
|
-
_SessionContextManager.
|
|
330
|
+
_SessionContextManager.reset_session(token)
|
|
320
331
|
|
|
321
332
|
|
|
322
333
|
@asynccontextmanager
|
|
@@ -336,13 +347,14 @@ async def ctx_sessions(*db_names: str) -> AsyncGenerator[dict[str, AsyncSession]
|
|
|
336
347
|
raise ValueError("At least one database name must be provided")
|
|
337
348
|
|
|
338
349
|
sessions: dict[str, AsyncSession] = {}
|
|
350
|
+
tokens: list[contextvars.Token[dict[str, AsyncSession]]] = []
|
|
339
351
|
|
|
340
352
|
try:
|
|
341
353
|
# Create sessions for all
|
|
342
354
|
for db_name in db_names:
|
|
343
355
|
session = AsyncSession(db_name, readonly=False, auto_commit=False)
|
|
344
356
|
sessions[db_name] = session
|
|
345
|
-
_SessionContextManager.set_session(session, db_name)
|
|
357
|
+
tokens.append(_SessionContextManager.set_session(session, db_name))
|
|
346
358
|
|
|
347
359
|
yield sessions
|
|
348
360
|
|
|
@@ -358,9 +370,11 @@ async def ctx_sessions(*db_names: str) -> AsyncGenerator[dict[str, AsyncSession]
|
|
|
358
370
|
|
|
359
371
|
finally:
|
|
360
372
|
# Cleanup all sessions
|
|
361
|
-
for
|
|
373
|
+
for session in sessions.values():
|
|
362
374
|
await session.close()
|
|
363
|
-
|
|
375
|
+
# Reset tokens in reverse order
|
|
376
|
+
for token in reversed(tokens):
|
|
377
|
+
_SessionContextManager.reset_session(token)
|
|
364
378
|
|
|
365
379
|
|
|
366
380
|
def get_session(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlobjects
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading
|
|
5
5
|
Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
|
|
6
6
|
Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
|
|
@@ -27,6 +27,9 @@ sqlobjects.egg-info/dependency_links.txt
|
|
|
27
27
|
sqlobjects.egg-info/entry_points.txt
|
|
28
28
|
sqlobjects.egg-info/requires.txt
|
|
29
29
|
sqlobjects.egg-info/top_level.txt
|
|
30
|
+
sqlobjects/contrib/__init__.py
|
|
31
|
+
sqlobjects/contrib/asgi.py
|
|
32
|
+
sqlobjects/contrib/fastapi.py
|
|
30
33
|
sqlobjects/database/__init__.py
|
|
31
34
|
sqlobjects/database/config.py
|
|
32
35
|
sqlobjects/database/manager.py
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|