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.
Files changed (44) hide show
  1. sqlmodel_object_helpers-0.0.5/.gitignore +34 -0
  2. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/PKG-INFO +188 -10
  3. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/README.md +187 -9
  4. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/__init__.py +21 -2
  5. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/constants.py +5 -0
  6. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/filters.py +3 -1
  7. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/loaders.py +24 -3
  8. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/mutations.py +9 -2
  9. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/query.py +19 -17
  10. sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/session.py +153 -0
  11. sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/standalone.py +221 -0
  12. sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/types/__init__.py +50 -0
  13. sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/types/columns.py +79 -0
  14. sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/types/datetime.py +38 -0
  15. sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/types/filters.py +330 -0
  16. sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/types/pagination.py +49 -0
  17. sqlmodel_object_helpers-0.0.5/tests/test_column_meta.py +511 -0
  18. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_count_exists.py +4 -4
  19. sqlmodel_object_helpers-0.0.5/tests/test_datetime_range.py +290 -0
  20. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_filters.py +18 -0
  21. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_loaders.py +36 -9
  22. sqlmodel_object_helpers-0.0.5/tests/test_standalone.py +1309 -0
  23. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_time_filter.py +72 -34
  24. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_types.py +134 -13
  25. sqlmodel_object_helpers-0.0.3/src/sqlmodel_object_helpers/session.py +0 -40
  26. sqlmodel_object_helpers-0.0.3/src/sqlmodel_object_helpers/types/__init__.py +0 -0
  27. sqlmodel_object_helpers-0.0.3/src/sqlmodel_object_helpers/types/filters.py +0 -169
  28. sqlmodel_object_helpers-0.0.3/src/sqlmodel_object_helpers/types/pagination.py +0 -15
  29. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/.github/workflows/publish.yml +0 -0
  30. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/LICENSE +0 -0
  31. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/pyproject.toml +0 -0
  32. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/exceptions.py +0 -0
  33. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/operators.py +0 -0
  34. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/types/projections.py +0 -0
  35. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/conftest.py +0 -0
  36. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_bulk_mutations.py +0 -0
  37. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_computed_columns.py +0 -0
  38. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_exceptions.py +0 -0
  39. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_for_update.py +0 -0
  40. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_generated_columns_pg.py +0 -0
  41. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_mutations.py +0 -0
  42. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_operators.py +0 -0
  43. {sqlmodel_object_helpers-0.0.3 → sqlmodel_object_helpers-0.0.5}/tests/test_query.py +0 -0
  44. {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
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
- One import, everything through dot notation:
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
- One import, everything through dot notation:
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**.