sqlmodel-object-helpers 0.0.2__tar.gz → 0.0.4__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.
- sqlmodel_object_helpers-0.0.4/.gitignore +34 -0
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/PKG-INFO +133 -8
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/README.md +132 -7
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/src/sqlmodel_object_helpers/__init__.py +40 -44
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/src/sqlmodel_object_helpers/constants.py +10 -2
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/src/sqlmodel_object_helpers/exceptions.py +2 -1
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/src/sqlmodel_object_helpers/filters.py +22 -11
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/src/sqlmodel_object_helpers/loaders.py +36 -17
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/src/sqlmodel_object_helpers/mutations.py +86 -78
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/src/sqlmodel_object_helpers/operators.py +4 -2
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/src/sqlmodel_object_helpers/query.py +94 -93
- sqlmodel_object_helpers-0.0.4/src/sqlmodel_object_helpers/session.py +153 -0
- sqlmodel_object_helpers-0.0.4/src/sqlmodel_object_helpers/standalone.py +221 -0
- sqlmodel_object_helpers-0.0.4/src/sqlmodel_object_helpers/types/__init__.py +3 -0
- sqlmodel_object_helpers-0.0.4/src/sqlmodel_object_helpers/types/datetime.py +38 -0
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/src/sqlmodel_object_helpers/types/filters.py +24 -12
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/tests/conftest.py +25 -21
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/tests/test_bulk_mutations.py +2 -2
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/tests/test_count_exists.py +5 -5
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/tests/test_exceptions.py +2 -1
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/tests/test_filters.py +20 -2
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/tests/test_loaders.py +39 -11
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/tests/test_operators.py +3 -4
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/tests/test_query.py +9 -9
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/tests/test_settings.py +0 -1
- sqlmodel_object_helpers-0.0.4/tests/test_standalone.py +1309 -0
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/tests/test_time_filter.py +73 -35
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/tests/test_types.py +136 -15
- sqlmodel_object_helpers-0.0.2/src/sqlmodel_object_helpers/session.py +0 -36
- sqlmodel_object_helpers-0.0.2/src/sqlmodel_object_helpers/types/__init__.py +0 -0
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/.github/workflows/publish.yml +0 -0
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/LICENSE +0 -0
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/pyproject.toml +0 -0
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/src/sqlmodel_object_helpers/types/pagination.py +0 -0
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/src/sqlmodel_object_helpers/types/projections.py +0 -0
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/tests/test_computed_columns.py +0 -0
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/tests/test_for_update.py +0 -0
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/tests/test_generated_columns_pg.py +0 -0
- {sqlmodel_object_helpers-0.0.2 → sqlmodel_object_helpers-0.0.4}/tests/test_mutations.py +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
|
|
7
|
+
# Distribution / packaging
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
*.egg-info/
|
|
11
|
+
*.egg
|
|
12
|
+
|
|
13
|
+
# Virtual environments
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
|
|
17
|
+
# Testing
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
.coverage
|
|
20
|
+
htmlcov/
|
|
21
|
+
|
|
22
|
+
# IDEs
|
|
23
|
+
.idea/
|
|
24
|
+
.vscode/
|
|
25
|
+
*.swp
|
|
26
|
+
*.swo
|
|
27
|
+
|
|
28
|
+
# MCP config (local)
|
|
29
|
+
.mcp.json
|
|
30
|
+
|
|
31
|
+
# OS
|
|
32
|
+
Thumbs.db
|
|
33
|
+
Desktop.ini
|
|
34
|
+
.DS_Store
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlmodel-object-helpers
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: Generic async query helpers for SQLModel: filtering, eager loading, pagination
|
|
5
5
|
Project-URL: Homepage, https://github.com/itstandart/sqlmodel-object-helpers
|
|
6
6
|
Project-URL: Repository, https://github.com/itstandart/sqlmodel-object-helpers
|
|
@@ -55,6 +55,8 @@ Generic async query helpers for [SQLModel](https://sqlmodel.tiangolo.com/): filt
|
|
|
55
55
|
- **Relationship Safety Check** - `check_for_related_records` pre-deletion inspection of ONETOMANY dependencies
|
|
56
56
|
- **Security Limits** - Configurable depth, list size, and pagination caps to prevent abuse
|
|
57
57
|
- **Type Safety** - Full type annotations with PEP 695 generics and `py.typed` marker (PEP 561)
|
|
58
|
+
- **Standalone Mode** - `configure()` + `import sqlmodel_object_helpers.standalone` for auto-session usage without DI
|
|
59
|
+
- **Session Lifecycle Logging** - Transparent session open/commit/rollback logging with hex session IDs and timing
|
|
58
60
|
|
|
59
61
|
## Installation
|
|
60
62
|
|
|
@@ -62,13 +64,20 @@ Generic async query helpers for [SQLModel](https://sqlmodel.tiangolo.com/): filt
|
|
|
62
64
|
pip install sqlmodel-object-helpers
|
|
63
65
|
```
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
Two usage modes:
|
|
66
68
|
|
|
67
69
|
```python
|
|
70
|
+
# DI mode — caller provides session (FastAPI Depends, etc.)
|
|
68
71
|
import sqlmodel_object_helpers as soh
|
|
69
72
|
|
|
70
73
|
soh.get_object(session, ...)
|
|
71
74
|
soh.add_object(session, ...)
|
|
75
|
+
|
|
76
|
+
# Standalone mode — auto-creates session per call
|
|
77
|
+
import sqlmodel_object_helpers.standalone as soh_sa
|
|
78
|
+
|
|
79
|
+
soh_sa.get_object(User, pk={"id": 1})
|
|
80
|
+
soh_sa.add_object(User(name="Alice"))
|
|
72
81
|
```
|
|
73
82
|
|
|
74
83
|
## Quick Start
|
|
@@ -109,6 +118,107 @@ async def example(session: AsyncSession):
|
|
|
109
118
|
# page.data -> list[User], page.pagination.total -> int
|
|
110
119
|
```
|
|
111
120
|
|
|
121
|
+
## Standalone Mode
|
|
122
|
+
|
|
123
|
+
For projects that don't use FastAPI DI or need simple one-call-one-transaction semantics.
|
|
124
|
+
|
|
125
|
+
> **Important:** The session factory **must** use `expire_on_commit=False`.
|
|
126
|
+
> After each standalone call the session commits and closes — with the default `True`,
|
|
127
|
+
> all attributes on returned objects would be expired and inaccessible (`DetachedInstanceError`).
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
|
131
|
+
import sqlmodel_object_helpers as soh
|
|
132
|
+
import sqlmodel_object_helpers.standalone as soh_sa
|
|
133
|
+
|
|
134
|
+
engine = create_async_engine("postgresql+asyncpg://...")
|
|
135
|
+
async_session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
|
136
|
+
|
|
137
|
+
# 1. Configure the session factory once at startup
|
|
138
|
+
soh.configure(async_session_factory)
|
|
139
|
+
|
|
140
|
+
# 2. Use standalone wrappers — each call creates its own session and commits
|
|
141
|
+
user = await soh_sa.get_object(User, pk={"id": 1})
|
|
142
|
+
new_user = await soh_sa.add_object(User(name="Alice"))
|
|
143
|
+
await soh_sa.delete_object(User, pk={"id": 5})
|
|
144
|
+
|
|
145
|
+
# All 12 functions are available:
|
|
146
|
+
# Queries: get_object, get_objects, count_objects, exists_object, get_projection
|
|
147
|
+
# Mutations: add_object, add_objects, update_object, update_objects,
|
|
148
|
+
# delete_object, delete_objects, check_for_related_records
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Each standalone call creates a session, executes the operation, commits on success, and rolls back on error. No session parameter needed.
|
|
152
|
+
|
|
153
|
+
### auto_session - multi-operation transactions
|
|
154
|
+
|
|
155
|
+
When you need multiple operations in a single atomic transaction:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
import sqlmodel_object_helpers as soh
|
|
159
|
+
|
|
160
|
+
async with soh.auto_session() as session:
|
|
161
|
+
billing = await soh.add_object(session, Billing(...))
|
|
162
|
+
payment = await soh.add_object(session, Payment(...))
|
|
163
|
+
# commit happens automatically on exit
|
|
164
|
+
# if any operation fails — ALL are rolled back
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Session Management
|
|
168
|
+
|
|
169
|
+
### DI mode — `create_session_dependency()`
|
|
170
|
+
|
|
171
|
+
For FastAPI projects with dependency injection:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
import sqlmodel_object_helpers as soh
|
|
175
|
+
from typing import Annotated
|
|
176
|
+
from fastapi import Depends
|
|
177
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
178
|
+
|
|
179
|
+
get_session = soh.create_session_dependency(async_session_factory)
|
|
180
|
+
DbSession = Annotated[AsyncSession, Depends(get_session)]
|
|
181
|
+
|
|
182
|
+
@router.get("/users/{user_id}")
|
|
183
|
+
async def get_user(user_id: int, session: DbSession):
|
|
184
|
+
return await soh.get_object(session, User, pk={"id": user_id})
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
One session per HTTP request. The dependency commits on success, rolls back on error.
|
|
188
|
+
|
|
189
|
+
### Standalone mode — `configure()`
|
|
190
|
+
|
|
191
|
+
For projects without DI or for scripts/CLI:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
import sqlmodel_object_helpers as soh
|
|
195
|
+
|
|
196
|
+
soh.configure(async_session_factory)
|
|
197
|
+
# Now standalone functions and auto_session() are available
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Session Lifecycle Logging
|
|
201
|
+
|
|
202
|
+
All session operations are logged via `logging.getLogger("sqlmodel_object_helpers")`:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
DEBUG auto_session[1a2b3c4d] opened
|
|
206
|
+
DEBUG auto_session[1a2b3c4d] committed (0.015s)
|
|
207
|
+
WARNING auto_session[1a2b3c4d] rollback (0.003s) — MutationError: ...
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
- **Hex session ID** (`1a2b3c4d`) correlates all log lines for the same session
|
|
211
|
+
- **Prefix** distinguishes mode: `auto_session` (standalone/auto_session) vs `di_session` (DI dependency)
|
|
212
|
+
- **Timing** shows elapsed time from session open to commit/rollback
|
|
213
|
+
- **Level**: `DEBUG` for normal flow, `WARNING` for rollbacks and commit failures
|
|
214
|
+
|
|
215
|
+
Enable with:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
import logging
|
|
219
|
+
logging.getLogger("sqlmodel_object_helpers").setLevel(logging.DEBUG)
|
|
220
|
+
```
|
|
221
|
+
|
|
112
222
|
## Configuration
|
|
113
223
|
|
|
114
224
|
Override security limits at application startup:
|
|
@@ -127,6 +237,7 @@ soh.settings.max_filter_depth = 50
|
|
|
127
237
|
| `max_load_depth` | `10` | Maximum depth of eager-loading chains |
|
|
128
238
|
| `max_in_list_size` | `1000` | Maximum elements in an IN(...) list |
|
|
129
239
|
| `max_per_page` | `500` | Maximum value for per_page in pagination |
|
|
240
|
+
| `session_factory` | `None` | `async_sessionmaker` instance for standalone mode (set via `soh.configure()`) |
|
|
130
241
|
|
|
131
242
|
## Query Operations
|
|
132
243
|
|
|
@@ -194,13 +305,13 @@ users = await soh.get_objects(
|
|
|
194
305
|
)
|
|
195
306
|
|
|
196
307
|
# Time filtering
|
|
197
|
-
from datetime import datetime
|
|
308
|
+
from datetime import datetime, timezone
|
|
198
309
|
|
|
199
310
|
recent = await soh.get_objects(
|
|
200
311
|
session, User,
|
|
201
312
|
time_filter=soh.TimeFilter(
|
|
202
|
-
created_after=datetime(2026, 1, 1),
|
|
203
|
-
created_before=datetime(2026, 2, 1),
|
|
313
|
+
created_after=datetime(2026, 1, 1, tzinfo=timezone.utc),
|
|
314
|
+
created_before=datetime(2026, 2, 1, tzinfo=timezone.utc),
|
|
204
315
|
),
|
|
205
316
|
)
|
|
206
317
|
```
|
|
@@ -221,7 +332,7 @@ active = await soh.count_objects(session, User, filters={"is_active": {soh.Opera
|
|
|
221
332
|
# With time filter
|
|
222
333
|
recent = await soh.count_objects(
|
|
223
334
|
session, User,
|
|
224
|
-
time_filter=soh.TimeFilter(created_after=datetime(2026, 1, 1)),
|
|
335
|
+
time_filter=soh.TimeFilter(created_after=datetime(2026, 1, 1, tzinfo=timezone.utc)),
|
|
225
336
|
)
|
|
226
337
|
```
|
|
227
338
|
|
|
@@ -405,7 +516,7 @@ Used by `get_objects`. Nested dicts are auto-flattened via `flatten_filters`:
|
|
|
405
516
|
| `Operator.ILIKE` | `ILIKE` | `{soh.Operator.ILIKE: "%test%"}` |
|
|
406
517
|
| `Operator.BETWEEN` | `BETWEEN` | `{soh.Operator.BETWEEN: [1, 10]}` |
|
|
407
518
|
| `Operator.IS` | `IS` | `{soh.Operator.IS: None}` |
|
|
408
|
-
| `Operator.
|
|
519
|
+
| `Operator.IS_NOT` | `IS NOT` | `{soh.Operator.IS_NOT: None}` |
|
|
409
520
|
| `Operator.MATCH` | `MATCH` | `{soh.Operator.MATCH: "query"}` |
|
|
410
521
|
| `"exists"` | `IS NOT NULL` / `.any()` | `{"exists": True}` |
|
|
411
522
|
|
|
@@ -483,6 +594,16 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
483
594
|
|
|
484
595
|
## API Reference
|
|
485
596
|
|
|
597
|
+
### Session Management
|
|
598
|
+
|
|
599
|
+
- `soh.configure(factory)` -- Register `async_sessionmaker` for standalone mode
|
|
600
|
+
- `soh.auto_session()` -- Async context manager: creates session, commits on success, rolls back on error
|
|
601
|
+
- `soh.create_session_dependency(factory)` -- Create async generator for FastAPI `Depends`
|
|
602
|
+
|
|
603
|
+
### Standalone Wrappers
|
|
604
|
+
|
|
605
|
+
- `import sqlmodel_object_helpers.standalone as soh_sa` -- All 12 query/mutation functions without `session` parameter
|
|
606
|
+
|
|
486
607
|
### Settings
|
|
487
608
|
|
|
488
609
|
- `soh.settings` -- Module-level `QueryHelperSettings` instance (mutable at runtime)
|
|
@@ -521,7 +642,7 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
521
642
|
### Loaders
|
|
522
643
|
|
|
523
644
|
- `soh.build_load_chain(model, path, ...)` -- Build loader option from string/list path
|
|
524
|
-
- `soh.build_load_options(model, attrs)` -- Build single loader option chain
|
|
645
|
+
- `soh.build_load_options(model, attrs, *, suspend_error=False)` -- Build single loader option chain
|
|
525
646
|
|
|
526
647
|
### Exceptions
|
|
527
648
|
|
|
@@ -539,6 +660,10 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
539
660
|
- `soh.LogicalFilter`
|
|
540
661
|
- `soh.TimeFilter` -- `created_after`, `created_before`, `updated_after`, `updated_before` with half-open interval `[after, before)`
|
|
541
662
|
|
|
663
|
+
### Datetime Types
|
|
664
|
+
|
|
665
|
+
- `soh.UTCDatetime` -- `Annotated[datetime, AfterValidator]` that rejects naive datetimes and converts aware datetimes to UTC
|
|
666
|
+
|
|
542
667
|
### Pagination Types
|
|
543
668
|
|
|
544
669
|
- `soh.Pagination` -- Request model (page, per\_page)
|
|
@@ -21,6 +21,8 @@ Generic async query helpers for [SQLModel](https://sqlmodel.tiangolo.com/): filt
|
|
|
21
21
|
- **Relationship Safety Check** - `check_for_related_records` pre-deletion inspection of ONETOMANY dependencies
|
|
22
22
|
- **Security Limits** - Configurable depth, list size, and pagination caps to prevent abuse
|
|
23
23
|
- **Type Safety** - Full type annotations with PEP 695 generics and `py.typed` marker (PEP 561)
|
|
24
|
+
- **Standalone Mode** - `configure()` + `import sqlmodel_object_helpers.standalone` for auto-session usage without DI
|
|
25
|
+
- **Session Lifecycle Logging** - Transparent session open/commit/rollback logging with hex session IDs and timing
|
|
24
26
|
|
|
25
27
|
## Installation
|
|
26
28
|
|
|
@@ -28,13 +30,20 @@ Generic async query helpers for [SQLModel](https://sqlmodel.tiangolo.com/): filt
|
|
|
28
30
|
pip install sqlmodel-object-helpers
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
Two usage modes:
|
|
32
34
|
|
|
33
35
|
```python
|
|
36
|
+
# DI mode — caller provides session (FastAPI Depends, etc.)
|
|
34
37
|
import sqlmodel_object_helpers as soh
|
|
35
38
|
|
|
36
39
|
soh.get_object(session, ...)
|
|
37
40
|
soh.add_object(session, ...)
|
|
41
|
+
|
|
42
|
+
# Standalone mode — auto-creates session per call
|
|
43
|
+
import sqlmodel_object_helpers.standalone as soh_sa
|
|
44
|
+
|
|
45
|
+
soh_sa.get_object(User, pk={"id": 1})
|
|
46
|
+
soh_sa.add_object(User(name="Alice"))
|
|
38
47
|
```
|
|
39
48
|
|
|
40
49
|
## Quick Start
|
|
@@ -75,6 +84,107 @@ async def example(session: AsyncSession):
|
|
|
75
84
|
# page.data -> list[User], page.pagination.total -> int
|
|
76
85
|
```
|
|
77
86
|
|
|
87
|
+
## Standalone Mode
|
|
88
|
+
|
|
89
|
+
For projects that don't use FastAPI DI or need simple one-call-one-transaction semantics.
|
|
90
|
+
|
|
91
|
+
> **Important:** The session factory **must** use `expire_on_commit=False`.
|
|
92
|
+
> After each standalone call the session commits and closes — with the default `True`,
|
|
93
|
+
> all attributes on returned objects would be expired and inaccessible (`DetachedInstanceError`).
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
|
97
|
+
import sqlmodel_object_helpers as soh
|
|
98
|
+
import sqlmodel_object_helpers.standalone as soh_sa
|
|
99
|
+
|
|
100
|
+
engine = create_async_engine("postgresql+asyncpg://...")
|
|
101
|
+
async_session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
|
102
|
+
|
|
103
|
+
# 1. Configure the session factory once at startup
|
|
104
|
+
soh.configure(async_session_factory)
|
|
105
|
+
|
|
106
|
+
# 2. Use standalone wrappers — each call creates its own session and commits
|
|
107
|
+
user = await soh_sa.get_object(User, pk={"id": 1})
|
|
108
|
+
new_user = await soh_sa.add_object(User(name="Alice"))
|
|
109
|
+
await soh_sa.delete_object(User, pk={"id": 5})
|
|
110
|
+
|
|
111
|
+
# All 12 functions are available:
|
|
112
|
+
# Queries: get_object, get_objects, count_objects, exists_object, get_projection
|
|
113
|
+
# Mutations: add_object, add_objects, update_object, update_objects,
|
|
114
|
+
# delete_object, delete_objects, check_for_related_records
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Each standalone call creates a session, executes the operation, commits on success, and rolls back on error. No session parameter needed.
|
|
118
|
+
|
|
119
|
+
### auto_session - multi-operation transactions
|
|
120
|
+
|
|
121
|
+
When you need multiple operations in a single atomic transaction:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import sqlmodel_object_helpers as soh
|
|
125
|
+
|
|
126
|
+
async with soh.auto_session() as session:
|
|
127
|
+
billing = await soh.add_object(session, Billing(...))
|
|
128
|
+
payment = await soh.add_object(session, Payment(...))
|
|
129
|
+
# commit happens automatically on exit
|
|
130
|
+
# if any operation fails — ALL are rolled back
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Session Management
|
|
134
|
+
|
|
135
|
+
### DI mode — `create_session_dependency()`
|
|
136
|
+
|
|
137
|
+
For FastAPI projects with dependency injection:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
import sqlmodel_object_helpers as soh
|
|
141
|
+
from typing import Annotated
|
|
142
|
+
from fastapi import Depends
|
|
143
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
144
|
+
|
|
145
|
+
get_session = soh.create_session_dependency(async_session_factory)
|
|
146
|
+
DbSession = Annotated[AsyncSession, Depends(get_session)]
|
|
147
|
+
|
|
148
|
+
@router.get("/users/{user_id}")
|
|
149
|
+
async def get_user(user_id: int, session: DbSession):
|
|
150
|
+
return await soh.get_object(session, User, pk={"id": user_id})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
One session per HTTP request. The dependency commits on success, rolls back on error.
|
|
154
|
+
|
|
155
|
+
### Standalone mode — `configure()`
|
|
156
|
+
|
|
157
|
+
For projects without DI or for scripts/CLI:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
import sqlmodel_object_helpers as soh
|
|
161
|
+
|
|
162
|
+
soh.configure(async_session_factory)
|
|
163
|
+
# Now standalone functions and auto_session() are available
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Session Lifecycle Logging
|
|
167
|
+
|
|
168
|
+
All session operations are logged via `logging.getLogger("sqlmodel_object_helpers")`:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
DEBUG auto_session[1a2b3c4d] opened
|
|
172
|
+
DEBUG auto_session[1a2b3c4d] committed (0.015s)
|
|
173
|
+
WARNING auto_session[1a2b3c4d] rollback (0.003s) — MutationError: ...
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- **Hex session ID** (`1a2b3c4d`) correlates all log lines for the same session
|
|
177
|
+
- **Prefix** distinguishes mode: `auto_session` (standalone/auto_session) vs `di_session` (DI dependency)
|
|
178
|
+
- **Timing** shows elapsed time from session open to commit/rollback
|
|
179
|
+
- **Level**: `DEBUG` for normal flow, `WARNING` for rollbacks and commit failures
|
|
180
|
+
|
|
181
|
+
Enable with:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
import logging
|
|
185
|
+
logging.getLogger("sqlmodel_object_helpers").setLevel(logging.DEBUG)
|
|
186
|
+
```
|
|
187
|
+
|
|
78
188
|
## Configuration
|
|
79
189
|
|
|
80
190
|
Override security limits at application startup:
|
|
@@ -93,6 +203,7 @@ soh.settings.max_filter_depth = 50
|
|
|
93
203
|
| `max_load_depth` | `10` | Maximum depth of eager-loading chains |
|
|
94
204
|
| `max_in_list_size` | `1000` | Maximum elements in an IN(...) list |
|
|
95
205
|
| `max_per_page` | `500` | Maximum value for per_page in pagination |
|
|
206
|
+
| `session_factory` | `None` | `async_sessionmaker` instance for standalone mode (set via `soh.configure()`) |
|
|
96
207
|
|
|
97
208
|
## Query Operations
|
|
98
209
|
|
|
@@ -160,13 +271,13 @@ users = await soh.get_objects(
|
|
|
160
271
|
)
|
|
161
272
|
|
|
162
273
|
# Time filtering
|
|
163
|
-
from datetime import datetime
|
|
274
|
+
from datetime import datetime, timezone
|
|
164
275
|
|
|
165
276
|
recent = await soh.get_objects(
|
|
166
277
|
session, User,
|
|
167
278
|
time_filter=soh.TimeFilter(
|
|
168
|
-
created_after=datetime(2026, 1, 1),
|
|
169
|
-
created_before=datetime(2026, 2, 1),
|
|
279
|
+
created_after=datetime(2026, 1, 1, tzinfo=timezone.utc),
|
|
280
|
+
created_before=datetime(2026, 2, 1, tzinfo=timezone.utc),
|
|
170
281
|
),
|
|
171
282
|
)
|
|
172
283
|
```
|
|
@@ -187,7 +298,7 @@ active = await soh.count_objects(session, User, filters={"is_active": {soh.Opera
|
|
|
187
298
|
# With time filter
|
|
188
299
|
recent = await soh.count_objects(
|
|
189
300
|
session, User,
|
|
190
|
-
time_filter=soh.TimeFilter(created_after=datetime(2026, 1, 1)),
|
|
301
|
+
time_filter=soh.TimeFilter(created_after=datetime(2026, 1, 1, tzinfo=timezone.utc)),
|
|
191
302
|
)
|
|
192
303
|
```
|
|
193
304
|
|
|
@@ -371,7 +482,7 @@ Used by `get_objects`. Nested dicts are auto-flattened via `flatten_filters`:
|
|
|
371
482
|
| `Operator.ILIKE` | `ILIKE` | `{soh.Operator.ILIKE: "%test%"}` |
|
|
372
483
|
| `Operator.BETWEEN` | `BETWEEN` | `{soh.Operator.BETWEEN: [1, 10]}` |
|
|
373
484
|
| `Operator.IS` | `IS` | `{soh.Operator.IS: None}` |
|
|
374
|
-
| `Operator.
|
|
485
|
+
| `Operator.IS_NOT` | `IS NOT` | `{soh.Operator.IS_NOT: None}` |
|
|
375
486
|
| `Operator.MATCH` | `MATCH` | `{soh.Operator.MATCH: "query"}` |
|
|
376
487
|
| `"exists"` | `IS NOT NULL` / `.any()` | `{"exists": True}` |
|
|
377
488
|
|
|
@@ -449,6 +560,16 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
449
560
|
|
|
450
561
|
## API Reference
|
|
451
562
|
|
|
563
|
+
### Session Management
|
|
564
|
+
|
|
565
|
+
- `soh.configure(factory)` -- Register `async_sessionmaker` for standalone mode
|
|
566
|
+
- `soh.auto_session()` -- Async context manager: creates session, commits on success, rolls back on error
|
|
567
|
+
- `soh.create_session_dependency(factory)` -- Create async generator for FastAPI `Depends`
|
|
568
|
+
|
|
569
|
+
### Standalone Wrappers
|
|
570
|
+
|
|
571
|
+
- `import sqlmodel_object_helpers.standalone as soh_sa` -- All 12 query/mutation functions without `session` parameter
|
|
572
|
+
|
|
452
573
|
### Settings
|
|
453
574
|
|
|
454
575
|
- `soh.settings` -- Module-level `QueryHelperSettings` instance (mutable at runtime)
|
|
@@ -487,7 +608,7 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
487
608
|
### Loaders
|
|
488
609
|
|
|
489
610
|
- `soh.build_load_chain(model, path, ...)` -- Build loader option from string/list path
|
|
490
|
-
- `soh.build_load_options(model, attrs)` -- Build single loader option chain
|
|
611
|
+
- `soh.build_load_options(model, attrs, *, suspend_error=False)` -- Build single loader option chain
|
|
491
612
|
|
|
492
613
|
### Exceptions
|
|
493
614
|
|
|
@@ -505,6 +626,10 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
505
626
|
- `soh.LogicalFilter`
|
|
506
627
|
- `soh.TimeFilter` -- `created_after`, `created_before`, `updated_after`, `updated_before` with half-open interval `[after, before)`
|
|
507
628
|
|
|
629
|
+
### Datetime Types
|
|
630
|
+
|
|
631
|
+
- `soh.UTCDatetime` -- `Annotated[datetime, AfterValidator]` that rejects naive datetimes and converts aware datetimes to UTC
|
|
632
|
+
|
|
508
633
|
### Pagination Types
|
|
509
634
|
|
|
510
635
|
- `soh.Pagination` -- Request model (page, per\_page)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""sqlmodel-object-helpers — reusable query helpers for SQLModel projects."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.0.
|
|
3
|
+
__version__ = "0.0.4"
|
|
4
4
|
|
|
5
5
|
from .constants import QueryHelperSettings, settings
|
|
6
6
|
from .exceptions import (
|
|
@@ -24,13 +24,15 @@ from .mutations import (
|
|
|
24
24
|
update_objects,
|
|
25
25
|
)
|
|
26
26
|
from .query import count_objects, exists_object, get_object, get_objects, get_projection
|
|
27
|
-
from .session import create_session_dependency
|
|
27
|
+
from .session import auto_session, configure, create_session_dependency
|
|
28
|
+
from .types.datetime import UTCDatetime
|
|
28
29
|
from .types.filters import (
|
|
29
30
|
FilterBool,
|
|
30
31
|
FilterDate,
|
|
31
32
|
FilterDatetime,
|
|
32
33
|
FilterExists,
|
|
33
34
|
FilterInt,
|
|
35
|
+
FilterNaiveDatetime,
|
|
34
36
|
FilterStr,
|
|
35
37
|
FilterTimedelta,
|
|
36
38
|
LogicalFilter,
|
|
@@ -47,59 +49,53 @@ from .types.pagination import (
|
|
|
47
49
|
from .types.projections import ColumnSpec
|
|
48
50
|
|
|
49
51
|
__all__ = [
|
|
50
|
-
# Settings
|
|
51
|
-
"settings",
|
|
52
|
-
"QueryHelperSettings",
|
|
53
|
-
# Query functions (read)
|
|
54
|
-
"get_object",
|
|
55
|
-
"get_objects",
|
|
56
|
-
"get_projection",
|
|
57
|
-
"count_objects",
|
|
58
|
-
"exists_object",
|
|
59
|
-
# Mutation functions (create / update / delete)
|
|
60
52
|
"add_object",
|
|
61
53
|
"add_objects",
|
|
62
|
-
"
|
|
63
|
-
"update_objects",
|
|
64
|
-
"delete_object",
|
|
65
|
-
"delete_objects",
|
|
66
|
-
"check_for_related_records",
|
|
67
|
-
# Filters
|
|
54
|
+
"auto_session",
|
|
68
55
|
"build_filter",
|
|
69
56
|
"build_flat_filter",
|
|
70
|
-
"flatten_filters",
|
|
71
|
-
"SUPPORTED_OPERATORS",
|
|
72
|
-
"SPECIAL_OPERATORS",
|
|
73
|
-
"Operator",
|
|
74
|
-
# Loaders
|
|
75
|
-
"build_load_options",
|
|
76
57
|
"build_load_chain",
|
|
77
|
-
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
58
|
+
"build_load_options",
|
|
59
|
+
"check_for_related_records",
|
|
60
|
+
"ColumnSpec",
|
|
61
|
+
"configure",
|
|
62
|
+
"count_objects",
|
|
63
|
+
"create_session_dependency",
|
|
82
64
|
"DatabaseError",
|
|
83
|
-
"
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
"
|
|
65
|
+
"delete_object",
|
|
66
|
+
"delete_objects",
|
|
67
|
+
"exists_object",
|
|
68
|
+
"FilterBool",
|
|
87
69
|
"FilterDate",
|
|
88
70
|
"FilterDatetime",
|
|
89
|
-
"FilterTimedelta",
|
|
90
|
-
"FilterBool",
|
|
91
71
|
"FilterExists",
|
|
72
|
+
"FilterInt",
|
|
73
|
+
"FilterNaiveDatetime",
|
|
74
|
+
"FilterStr",
|
|
75
|
+
"FilterTimedelta",
|
|
76
|
+
"flatten_filters",
|
|
77
|
+
"get_object",
|
|
78
|
+
"get_objects",
|
|
79
|
+
"get_projection",
|
|
80
|
+
"GetAllPagination",
|
|
81
|
+
"InvalidFilterError",
|
|
82
|
+
"InvalidLoadPathError",
|
|
83
|
+
"LogicalFilter",
|
|
84
|
+
"MutationError",
|
|
85
|
+
"ObjectNotFoundError",
|
|
86
|
+
"Operator",
|
|
92
87
|
"OrderAsc",
|
|
93
|
-
"OrderDesc",
|
|
94
88
|
"OrderBy",
|
|
95
|
-
"
|
|
96
|
-
"TimeFilter",
|
|
97
|
-
# Pagination types
|
|
89
|
+
"OrderDesc",
|
|
98
90
|
"Pagination",
|
|
99
91
|
"PaginationR",
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
"
|
|
103
|
-
|
|
104
|
-
"
|
|
92
|
+
"QueryError",
|
|
93
|
+
"QueryHelperSettings",
|
|
94
|
+
"settings",
|
|
95
|
+
"SPECIAL_OPERATORS",
|
|
96
|
+
"SUPPORTED_OPERATORS",
|
|
97
|
+
"TimeFilter",
|
|
98
|
+
"update_object",
|
|
99
|
+
"update_objects",
|
|
100
|
+
"UTCDatetime",
|
|
105
101
|
]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Security and performance limits for query building.
|
|
2
3
|
|
|
3
4
|
All values have sensible defaults. Override at application startup:
|
|
4
5
|
|
|
@@ -7,11 +8,14 @@ All values have sensible defaults. Override at application startup:
|
|
|
7
8
|
settings.max_filter_depth = 50
|
|
8
9
|
"""
|
|
9
10
|
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
10
13
|
from pydantic import BaseModel
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
class QueryHelperSettings(BaseModel):
|
|
14
|
-
"""
|
|
17
|
+
"""
|
|
18
|
+
Mutable settings for query-helper limits.
|
|
15
19
|
|
|
16
20
|
Attributes:
|
|
17
21
|
max_filter_depth: Maximum recursion depth for build_filter (AND/OR nesting).
|
|
@@ -22,6 +26,9 @@ class QueryHelperSettings(BaseModel):
|
|
|
22
26
|
(e.g. "application.applicant.educations" = 3).
|
|
23
27
|
max_in_list_size: Maximum number of elements in an IN(...) list.
|
|
24
28
|
max_per_page: Maximum value for per_page in pagination.
|
|
29
|
+
session_factory: async_sessionmaker instance for standalone mode.
|
|
30
|
+
Set via ``soh.configure(factory)``.
|
|
31
|
+
|
|
25
32
|
"""
|
|
26
33
|
|
|
27
34
|
max_filter_depth: int = 100
|
|
@@ -29,6 +36,7 @@ class QueryHelperSettings(BaseModel):
|
|
|
29
36
|
max_load_depth: int = 10
|
|
30
37
|
max_in_list_size: int = 1000
|
|
31
38
|
max_per_page: int = 500
|
|
39
|
+
session_factory: Any = None
|
|
32
40
|
|
|
33
41
|
|
|
34
42
|
settings = QueryHelperSettings()
|
|
@@ -42,7 +42,8 @@ class DatabaseError(QueryError):
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
class MutationError(QueryError):
|
|
45
|
-
"""
|
|
45
|
+
"""
|
|
46
|
+
Raised when a create/update/delete operation fails.
|
|
46
47
|
|
|
47
48
|
Covers constraint violations, invalid field names in update data,
|
|
48
49
|
and other mutation-specific errors.
|