sqlmodel-object-helpers 0.0.3__tar.gz → 0.0.5__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.5/.gitignore +34 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/PKG-INFO +188 -10
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/README.md +187 -9
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/__init__.py +21 -2
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/constants.py +5 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/filters.py +3 -1
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/loaders.py +24 -3
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/mutations.py +9 -2
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/query.py +19 -17
- sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/session.py +153 -0
- sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/standalone.py +221 -0
- sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/types/__init__.py +50 -0
- sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/types/columns.py +79 -0
- sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/types/datetime.py +38 -0
- sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/types/filters.py +330 -0
- sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/types/pagination.py +49 -0
- sqlmodel_object_helpers-0.0.5/tests/test_column_meta.py +511 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_count_exists.py +4 -4
- sqlmodel_object_helpers-0.0.5/tests/test_datetime_range.py +290 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_filters.py +18 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_loaders.py +36 -9
- sqlmodel_object_helpers-0.0.5/tests/test_standalone.py +1309 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_time_filter.py +72 -34
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_types.py +134 -13
- sqlmodel_object_helpers-0.0.3/src/sqlmodel_object_helpers/session.py +0 -40
- sqlmodel_object_helpers-0.0.3/src/sqlmodel_object_helpers/types/__init__.py +0 -0
- sqlmodel_object_helpers-0.0.3/src/sqlmodel_object_helpers/types/filters.py +0 -169
- sqlmodel_object_helpers-0.0.3/src/sqlmodel_object_helpers/types/pagination.py +0 -15
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/.github/workflows/publish.yml +0 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/LICENSE +0 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/pyproject.toml +0 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/exceptions.py +0 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/operators.py +0 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/types/projections.py +0 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/conftest.py +0 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_bulk_mutations.py +0 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_computed_columns.py +0 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_exceptions.py +0 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_for_update.py +0 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_generated_columns_pg.py +0 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_mutations.py +0 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_operators.py +0 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_query.py +0 -0
- {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_settings.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.5
|
|
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
|
|
|
@@ -428,7 +539,34 @@ class UserFilter(BaseModel):
|
|
|
428
539
|
posts: soh.FilterExists | None = None # exists: bool
|
|
429
540
|
```
|
|
430
541
|
|
|
431
|
-
Available: `FilterInt`, `FilterStr`, `FilterDate`, `FilterDatetime`, `FilterTimedelta`, `FilterBool`, `FilterExists`.
|
|
542
|
+
Available: `FilterInt`, `FilterStr`, `FilterDate`, `FilterDatetime`, `FilterNaiveDatetime`, `FilterTimedelta`, `FilterBool`, `FilterExists`.
|
|
543
|
+
|
|
544
|
+
#### Range Filters
|
|
545
|
+
|
|
546
|
+
`FilterDatetimeRange` and `FilterNaiveDatetimeRange` parse a comma-separated date string into `gt`/`lt` operators:
|
|
547
|
+
|
|
548
|
+
```python
|
|
549
|
+
import sqlmodel_object_helpers as soh
|
|
550
|
+
|
|
551
|
+
# Full range: "FROM,TO" → gt + lt
|
|
552
|
+
f = soh.FilterDatetimeRange.model_validate("2026-04-01,2026-05-06")
|
|
553
|
+
f.model_dump(exclude_none=True)
|
|
554
|
+
# {"gt": datetime(2026, 4, 1, tzinfo=UTC), "lt": datetime(2026, 5, 6, tzinfo=UTC)}
|
|
555
|
+
# SQL: WHERE field > '2026-04-01' AND field < '2026-05-06'
|
|
556
|
+
|
|
557
|
+
# Open end: "FROM," → gt only
|
|
558
|
+
f = soh.FilterDatetimeRange.model_validate("2026-04-01,")
|
|
559
|
+
# SQL: WHERE field > '2026-04-01'
|
|
560
|
+
|
|
561
|
+
# Open start: ",TO" → lt only
|
|
562
|
+
f = soh.FilterDatetimeRange.model_validate(",2026-05-06")
|
|
563
|
+
# SQL: WHERE field < '2026-05-06'
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Each date part accepts ISO (`2026-04-01`, `2026-04-01T00:00:00Z`) and display format (`01.04.2026 00:00`).
|
|
567
|
+
|
|
568
|
+
- `FilterDatetimeRange` — produces UTC-aware datetimes
|
|
569
|
+
- `FilterNaiveDatetimeRange` — produces naive (timezone-unaware) datetimes
|
|
432
570
|
|
|
433
571
|
## Eager Loading
|
|
434
572
|
|
|
@@ -483,6 +621,16 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
483
621
|
|
|
484
622
|
## API Reference
|
|
485
623
|
|
|
624
|
+
### Session Management
|
|
625
|
+
|
|
626
|
+
- `soh.configure(factory)` -- Register `async_sessionmaker` for standalone mode
|
|
627
|
+
- `soh.auto_session()` -- Async context manager: creates session, commits on success, rolls back on error
|
|
628
|
+
- `soh.create_session_dependency(factory)` -- Create async generator for FastAPI `Depends`
|
|
629
|
+
|
|
630
|
+
### Standalone Wrappers
|
|
631
|
+
|
|
632
|
+
- `import sqlmodel_object_helpers.standalone as soh_sa` -- All 12 query/mutation functions without `session` parameter
|
|
633
|
+
|
|
486
634
|
### Settings
|
|
487
635
|
|
|
488
636
|
- `soh.settings` -- Module-level `QueryHelperSettings` instance (mutable at runtime)
|
|
@@ -521,7 +669,7 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
521
669
|
### Loaders
|
|
522
670
|
|
|
523
671
|
- `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
|
|
672
|
+
- `soh.build_load_options(model, attrs, *, suspend_error=False)` -- Build single loader option chain
|
|
525
673
|
|
|
526
674
|
### Exceptions
|
|
527
675
|
|
|
@@ -534,16 +682,33 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
534
682
|
|
|
535
683
|
### Filter Types
|
|
536
684
|
|
|
537
|
-
- `soh.FilterInt`, `soh.FilterStr`, `soh.FilterDate`, `soh.FilterDatetime`, `soh.FilterTimedelta`, `soh.FilterBool`, `soh.FilterExists`
|
|
685
|
+
- `soh.FilterInt`, `soh.FilterStr`, `soh.FilterDate`, `soh.FilterDatetime`, `soh.FilterNaiveDatetime`, `soh.FilterTimedelta`, `soh.FilterBool`, `soh.FilterExists`
|
|
686
|
+
- `soh.FilterDatetimeRange`, `soh.FilterNaiveDatetimeRange` -- Range filters that parse `"FROM,TO"` strings into `gt`/`lt` operators
|
|
538
687
|
- `soh.OrderAsc`, `soh.OrderDesc`, `soh.OrderBy`
|
|
539
688
|
- `soh.LogicalFilter`
|
|
540
689
|
- `soh.TimeFilter` -- `created_after`, `created_before`, `updated_after`, `updated_before` with half-open interval `[after, before)`
|
|
541
690
|
|
|
691
|
+
### Datetime Types
|
|
692
|
+
|
|
693
|
+
- `soh.UTCDatetime` -- `Annotated[datetime, AfterValidator]` that rejects naive datetimes and converts aware datetimes to UTC
|
|
694
|
+
|
|
542
695
|
### Pagination Types
|
|
543
696
|
|
|
544
697
|
- `soh.Pagination` -- Request model (page, per\_page)
|
|
545
698
|
- `soh.PaginationR` -- Response model (page, per\_page, total)
|
|
546
|
-
- `soh.GetAllPagination[T]` -- Generic wrapper (data: list[T], pagination: PaginationR | None)
|
|
699
|
+
- `soh.GetAllPagination[T]` -- Generic wrapper (table: TableMeta | None, columns: list[ColumnMeta] | None, data: list[T], pagination: PaginationR | None)
|
|
700
|
+
|
|
701
|
+
### Table Metadata Types
|
|
702
|
+
|
|
703
|
+
- `soh.TableMeta` -- Table-level metadata (name, header, row_link)
|
|
704
|
+
- `soh.ColumnMeta` -- Column metadata (json_path, label, type, lookup_dict, lookup_path, bool_labels)
|
|
705
|
+
- `soh.ColumnType` -- StrEnum of column data types (string, integer, float, boolean, date, datetime)
|
|
706
|
+
- `soh.BoolLabels` -- Display labels for boolean columns (true_label, false_label)
|
|
707
|
+
|
|
708
|
+
### Lookup Types
|
|
709
|
+
|
|
710
|
+
- `soh.LookupMeta` -- Lookup metadata (name used as cache key, matches `ColumnMeta.lookup_dict`)
|
|
711
|
+
- `soh.LookupResponse[T]` -- Generic wrapper for lookup endpoints (meta: LookupMeta, data: list[T])
|
|
547
712
|
|
|
548
713
|
### Projection Types
|
|
549
714
|
|
|
@@ -595,6 +760,19 @@ This project follows [Semantic Versioning](https://semver.org/) (MAJOR.MINOR.PAT
|
|
|
595
760
|
- **MINOR** — new features (backwards compatible)
|
|
596
761
|
- **MAJOR** — breaking changes
|
|
597
762
|
|
|
763
|
+
## Changelog
|
|
764
|
+
|
|
765
|
+
### 0.0.5
|
|
766
|
+
|
|
767
|
+
- **FilterDatetimeRange** / **FilterNaiveDatetimeRange** — range filters that parse comma-separated date strings (`"2026-04-01,2026-05-06"`) into `gt`/`lt` operators for SQL filtering
|
|
768
|
+
- **ColumnMeta** / **TableMeta** / **ColumnType** / **BoolLabels** — dynamic table metadata: backend describes columns, types, labels, lookups, and row navigation so the frontend renders any table without hardcoding
|
|
769
|
+
- **GetAllPagination** — now includes optional `table` and `columns` fields for delivering table metadata alongside paginated data
|
|
770
|
+
- **LookupMeta** / **LookupResponse** — standard `{meta, data}` wrapper for lookup (`_lkp`) endpoints, enabling unified frontend caching of dictionaries with `meta.name` as cache key
|
|
771
|
+
|
|
772
|
+
### 0.0.4
|
|
773
|
+
|
|
774
|
+
- Initial public release
|
|
775
|
+
|
|
598
776
|
## License
|
|
599
777
|
|
|
600
778
|
This project is licensed under the **PolyForm Noncommercial License 1.0.0**.
|
|
@@ -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
|
|
|
@@ -394,7 +505,34 @@ class UserFilter(BaseModel):
|
|
|
394
505
|
posts: soh.FilterExists | None = None # exists: bool
|
|
395
506
|
```
|
|
396
507
|
|
|
397
|
-
Available: `FilterInt`, `FilterStr`, `FilterDate`, `FilterDatetime`, `FilterTimedelta`, `FilterBool`, `FilterExists`.
|
|
508
|
+
Available: `FilterInt`, `FilterStr`, `FilterDate`, `FilterDatetime`, `FilterNaiveDatetime`, `FilterTimedelta`, `FilterBool`, `FilterExists`.
|
|
509
|
+
|
|
510
|
+
#### Range Filters
|
|
511
|
+
|
|
512
|
+
`FilterDatetimeRange` and `FilterNaiveDatetimeRange` parse a comma-separated date string into `gt`/`lt` operators:
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
import sqlmodel_object_helpers as soh
|
|
516
|
+
|
|
517
|
+
# Full range: "FROM,TO" → gt + lt
|
|
518
|
+
f = soh.FilterDatetimeRange.model_validate("2026-04-01,2026-05-06")
|
|
519
|
+
f.model_dump(exclude_none=True)
|
|
520
|
+
# {"gt": datetime(2026, 4, 1, tzinfo=UTC), "lt": datetime(2026, 5, 6, tzinfo=UTC)}
|
|
521
|
+
# SQL: WHERE field > '2026-04-01' AND field < '2026-05-06'
|
|
522
|
+
|
|
523
|
+
# Open end: "FROM," → gt only
|
|
524
|
+
f = soh.FilterDatetimeRange.model_validate("2026-04-01,")
|
|
525
|
+
# SQL: WHERE field > '2026-04-01'
|
|
526
|
+
|
|
527
|
+
# Open start: ",TO" → lt only
|
|
528
|
+
f = soh.FilterDatetimeRange.model_validate(",2026-05-06")
|
|
529
|
+
# SQL: WHERE field < '2026-05-06'
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Each date part accepts ISO (`2026-04-01`, `2026-04-01T00:00:00Z`) and display format (`01.04.2026 00:00`).
|
|
533
|
+
|
|
534
|
+
- `FilterDatetimeRange` — produces UTC-aware datetimes
|
|
535
|
+
- `FilterNaiveDatetimeRange` — produces naive (timezone-unaware) datetimes
|
|
398
536
|
|
|
399
537
|
## Eager Loading
|
|
400
538
|
|
|
@@ -449,6 +587,16 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
449
587
|
|
|
450
588
|
## API Reference
|
|
451
589
|
|
|
590
|
+
### Session Management
|
|
591
|
+
|
|
592
|
+
- `soh.configure(factory)` -- Register `async_sessionmaker` for standalone mode
|
|
593
|
+
- `soh.auto_session()` -- Async context manager: creates session, commits on success, rolls back on error
|
|
594
|
+
- `soh.create_session_dependency(factory)` -- Create async generator for FastAPI `Depends`
|
|
595
|
+
|
|
596
|
+
### Standalone Wrappers
|
|
597
|
+
|
|
598
|
+
- `import sqlmodel_object_helpers.standalone as soh_sa` -- All 12 query/mutation functions without `session` parameter
|
|
599
|
+
|
|
452
600
|
### Settings
|
|
453
601
|
|
|
454
602
|
- `soh.settings` -- Module-level `QueryHelperSettings` instance (mutable at runtime)
|
|
@@ -487,7 +635,7 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
487
635
|
### Loaders
|
|
488
636
|
|
|
489
637
|
- `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
|
|
638
|
+
- `soh.build_load_options(model, attrs, *, suspend_error=False)` -- Build single loader option chain
|
|
491
639
|
|
|
492
640
|
### Exceptions
|
|
493
641
|
|
|
@@ -500,16 +648,33 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
500
648
|
|
|
501
649
|
### Filter Types
|
|
502
650
|
|
|
503
|
-
- `soh.FilterInt`, `soh.FilterStr`, `soh.FilterDate`, `soh.FilterDatetime`, `soh.FilterTimedelta`, `soh.FilterBool`, `soh.FilterExists`
|
|
651
|
+
- `soh.FilterInt`, `soh.FilterStr`, `soh.FilterDate`, `soh.FilterDatetime`, `soh.FilterNaiveDatetime`, `soh.FilterTimedelta`, `soh.FilterBool`, `soh.FilterExists`
|
|
652
|
+
- `soh.FilterDatetimeRange`, `soh.FilterNaiveDatetimeRange` -- Range filters that parse `"FROM,TO"` strings into `gt`/`lt` operators
|
|
504
653
|
- `soh.OrderAsc`, `soh.OrderDesc`, `soh.OrderBy`
|
|
505
654
|
- `soh.LogicalFilter`
|
|
506
655
|
- `soh.TimeFilter` -- `created_after`, `created_before`, `updated_after`, `updated_before` with half-open interval `[after, before)`
|
|
507
656
|
|
|
657
|
+
### Datetime Types
|
|
658
|
+
|
|
659
|
+
- `soh.UTCDatetime` -- `Annotated[datetime, AfterValidator]` that rejects naive datetimes and converts aware datetimes to UTC
|
|
660
|
+
|
|
508
661
|
### Pagination Types
|
|
509
662
|
|
|
510
663
|
- `soh.Pagination` -- Request model (page, per\_page)
|
|
511
664
|
- `soh.PaginationR` -- Response model (page, per\_page, total)
|
|
512
|
-
- `soh.GetAllPagination[T]` -- Generic wrapper (data: list[T], pagination: PaginationR | None)
|
|
665
|
+
- `soh.GetAllPagination[T]` -- Generic wrapper (table: TableMeta | None, columns: list[ColumnMeta] | None, data: list[T], pagination: PaginationR | None)
|
|
666
|
+
|
|
667
|
+
### Table Metadata Types
|
|
668
|
+
|
|
669
|
+
- `soh.TableMeta` -- Table-level metadata (name, header, row_link)
|
|
670
|
+
- `soh.ColumnMeta` -- Column metadata (json_path, label, type, lookup_dict, lookup_path, bool_labels)
|
|
671
|
+
- `soh.ColumnType` -- StrEnum of column data types (string, integer, float, boolean, date, datetime)
|
|
672
|
+
- `soh.BoolLabels` -- Display labels for boolean columns (true_label, false_label)
|
|
673
|
+
|
|
674
|
+
### Lookup Types
|
|
675
|
+
|
|
676
|
+
- `soh.LookupMeta` -- Lookup metadata (name used as cache key, matches `ColumnMeta.lookup_dict`)
|
|
677
|
+
- `soh.LookupResponse[T]` -- Generic wrapper for lookup endpoints (meta: LookupMeta, data: list[T])
|
|
513
678
|
|
|
514
679
|
### Projection Types
|
|
515
680
|
|
|
@@ -561,6 +726,19 @@ This project follows [Semantic Versioning](https://semver.org/) (MAJOR.MINOR.PAT
|
|
|
561
726
|
- **MINOR** — new features (backwards compatible)
|
|
562
727
|
- **MAJOR** — breaking changes
|
|
563
728
|
|
|
729
|
+
## Changelog
|
|
730
|
+
|
|
731
|
+
### 0.0.5
|
|
732
|
+
|
|
733
|
+
- **FilterDatetimeRange** / **FilterNaiveDatetimeRange** — range filters that parse comma-separated date strings (`"2026-04-01,2026-05-06"`) into `gt`/`lt` operators for SQL filtering
|
|
734
|
+
- **ColumnMeta** / **TableMeta** / **ColumnType** / **BoolLabels** — dynamic table metadata: backend describes columns, types, labels, lookups, and row navigation so the frontend renders any table without hardcoding
|
|
735
|
+
- **GetAllPagination** — now includes optional `table` and `columns` fields for delivering table metadata alongside paginated data
|
|
736
|
+
- **LookupMeta** / **LookupResponse** — standard `{meta, data}` wrapper for lookup (`_lkp`) endpoints, enabling unified frontend caching of dictionaries with `meta.name` as cache key
|
|
737
|
+
|
|
738
|
+
### 0.0.4
|
|
739
|
+
|
|
740
|
+
- Initial public release
|
|
741
|
+
|
|
564
742
|
## License
|
|
565
743
|
|
|
566
744
|
This project is licensed under the **PolyForm Noncommercial License 1.0.0**.
|