sqlmodel-object-helpers 0.0.4__tar.gz → 0.0.6__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 → sqlmodel_object_helpers-0.0.6}/PKG-INFO +182 -5
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/README.md +181 -4
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/__init__.py +27 -2
- sqlmodel_object_helpers-0.0.6/src/sqlmodel_object_helpers/dynamic_meta.py +468 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/standalone.py +35 -0
- sqlmodel_object_helpers-0.0.6/src/sqlmodel_object_helpers/types/__init__.py +50 -0
- sqlmodel_object_helpers-0.0.6/src/sqlmodel_object_helpers/types/columns.py +79 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/types/filters.py +152 -1
- sqlmodel_object_helpers-0.0.6/src/sqlmodel_object_helpers/types/pagination.py +49 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/conftest.py +5 -1
- sqlmodel_object_helpers-0.0.6/tests/test_column_meta.py +511 -0
- sqlmodel_object_helpers-0.0.6/tests/test_datetime_range.py +295 -0
- sqlmodel_object_helpers-0.0.6/tests/test_dynamic_meta.py +1037 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_query.py +37 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_standalone.py +22 -0
- sqlmodel_object_helpers-0.0.4/src/sqlmodel_object_helpers/types/__init__.py +0 -3
- sqlmodel_object_helpers-0.0.4/src/sqlmodel_object_helpers/types/pagination.py +0 -15
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/.github/workflows/publish.yml +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/.gitignore +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/LICENSE +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/pyproject.toml +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/constants.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/exceptions.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/filters.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/loaders.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/mutations.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/operators.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/query.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/session.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/types/datetime.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/types/projections.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_bulk_mutations.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_computed_columns.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_count_exists.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_exceptions.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_filters.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_for_update.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_generated_columns_pg.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_loaders.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_mutations.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_operators.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_settings.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_time_filter.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlmodel-object-helpers
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
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,7 @@ 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
|
+
- **Dynamic Table Metadata** - `build_dynamic_meta` derives `TableMeta` + `list[ColumnMeta]` from a SQLModel class by reading PostgreSQL `pg_description` with fallback to `Column(comment=...)`. Supports per-role label/row_link overrides via `||` comment format, TTL-cached
|
|
58
59
|
- **Standalone Mode** - `configure()` + `import sqlmodel_object_helpers.standalone` for auto-session usage without DI
|
|
59
60
|
- **Session Lifecycle Logging** - Transparent session open/commit/rollback logging with hex session IDs and timing
|
|
60
61
|
|
|
@@ -539,7 +540,34 @@ class UserFilter(BaseModel):
|
|
|
539
540
|
posts: soh.FilterExists | None = None # exists: bool
|
|
540
541
|
```
|
|
541
542
|
|
|
542
|
-
Available: `FilterInt`, `FilterStr`, `FilterDate`, `FilterDatetime`, `FilterTimedelta`, `FilterBool`, `FilterExists`.
|
|
543
|
+
Available: `FilterInt`, `FilterStr`, `FilterDate`, `FilterDatetime`, `FilterNaiveDatetime`, `FilterTimedelta`, `FilterBool`, `FilterExists`.
|
|
544
|
+
|
|
545
|
+
#### Range Filters
|
|
546
|
+
|
|
547
|
+
`FilterDatetimeRange` and `FilterNaiveDatetimeRange` parse a comma-separated date string into `gt`/`lt` operators:
|
|
548
|
+
|
|
549
|
+
```python
|
|
550
|
+
import sqlmodel_object_helpers as soh
|
|
551
|
+
|
|
552
|
+
# Full range: "FROM,TO" → gt + lt
|
|
553
|
+
f = soh.FilterDatetimeRange.model_validate("2026-04-01,2026-05-06")
|
|
554
|
+
f.model_dump(exclude_none=True)
|
|
555
|
+
# {"gt": datetime(2026, 4, 1, tzinfo=UTC), "lt": datetime(2026, 5, 6, tzinfo=UTC)}
|
|
556
|
+
# SQL: WHERE field > '2026-04-01' AND field < '2026-05-06'
|
|
557
|
+
|
|
558
|
+
# Open end: "FROM," → gt only
|
|
559
|
+
f = soh.FilterDatetimeRange.model_validate("2026-04-01,")
|
|
560
|
+
# SQL: WHERE field > '2026-04-01'
|
|
561
|
+
|
|
562
|
+
# Open start: ",TO" → lt only
|
|
563
|
+
f = soh.FilterDatetimeRange.model_validate(",2026-05-06")
|
|
564
|
+
# SQL: WHERE field < '2026-05-06'
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
Each date part accepts ISO (`2026-04-01`, `2026-04-01T00:00:00Z`) and display format (`01.04.2026 00:00`).
|
|
568
|
+
|
|
569
|
+
- `FilterDatetimeRange` — produces UTC-aware datetimes
|
|
570
|
+
- `FilterNaiveDatetimeRange` — produces naive (timezone-unaware) datetimes
|
|
543
571
|
|
|
544
572
|
## Eager Loading
|
|
545
573
|
|
|
@@ -592,6 +620,117 @@ order = soh.OrderBy(sorts=[
|
|
|
592
620
|
result = await soh.get_objects(session, User, order_by=order)
|
|
593
621
|
```
|
|
594
622
|
|
|
623
|
+
## Dynamic Table Metadata
|
|
624
|
+
|
|
625
|
+
`build_dynamic_meta` builds `TableMeta` + `list[ColumnMeta]` from a SQLModel class at runtime, reading PostgreSQL `pg_description` (TTL-cached) with fallback to `Column(comment=...)` model defaults.
|
|
626
|
+
|
|
627
|
+
```python
|
|
628
|
+
import sqlmodel_object_helpers as soh
|
|
629
|
+
|
|
630
|
+
# Physical columns — label, type, lookup derived automatically
|
|
631
|
+
table_meta, columns = await soh.build_dynamic_meta(
|
|
632
|
+
session,
|
|
633
|
+
EmailMessage,
|
|
634
|
+
name="email_messages",
|
|
635
|
+
columns=[
|
|
636
|
+
"id", # physical column — everything derived
|
|
637
|
+
"email_type_id", # FK on *_lkp → lookup_dict auto-derived
|
|
638
|
+
"updated_at",
|
|
639
|
+
soh.ColumnMeta( # virtual column — fully specified
|
|
640
|
+
json_path="last_editor.user_name",
|
|
641
|
+
label="Редактор",
|
|
642
|
+
type=soh.ColumnType.STRING,
|
|
643
|
+
),
|
|
644
|
+
],
|
|
645
|
+
role_type="operator", # per-role label/row_link overrides
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
# Returns (TableMeta, list[ColumnMeta]) ready for GetAllPagination
|
|
649
|
+
return soh.GetAllPagination(
|
|
650
|
+
table=table_meta,
|
|
651
|
+
columns=columns,
|
|
652
|
+
data=items,
|
|
653
|
+
pagination=pagination_r,
|
|
654
|
+
)
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### Type derivation (SA type → ColumnType)
|
|
658
|
+
|
|
659
|
+
Physical column types are mapped automatically. Unknown types fall back to `string`.
|
|
660
|
+
|
|
661
|
+
| SQLAlchemy type | ColumnType |
|
|
662
|
+
|---|---|
|
|
663
|
+
| `Boolean` | `boolean` |
|
|
664
|
+
| `DateTime` | `datetime` |
|
|
665
|
+
| `Date` | `date` |
|
|
666
|
+
| `Integer`, `BigInteger`, `SmallInteger` | `integer` |
|
|
667
|
+
| `Numeric`, `Float` | `float` |
|
|
668
|
+
| `String`, `Text`, `Enum`, `Interval`, `ARRAY`, `Uuid` | `string` |
|
|
669
|
+
| Any other type | `string` (fallback) |
|
|
670
|
+
|
|
671
|
+
### Label precedence (per physical column)
|
|
672
|
+
|
|
673
|
+
1. `pg_description` — DBA edit via `COMMENT ON COLUMN` (supports per-role `||` overrides)
|
|
674
|
+
2. `Column(comment=...)` — Python-side default in the model
|
|
675
|
+
3. `ValueError` — fail-loud if both are missing
|
|
676
|
+
|
|
677
|
+
### Per-role overrides via `||` format
|
|
678
|
+
|
|
679
|
+
DBAs can set role-specific labels and row links in PostgreSQL comments:
|
|
680
|
+
|
|
681
|
+
```sql
|
|
682
|
+
COMMENT ON COLUMN emails.email_type_id IS 'Тип письма||operator=Категория||buh=Реквизит';
|
|
683
|
+
COMMENT ON TABLE emails IS 'Письма||row_link=/email/$id||row_link.operator=/op/email/$id';
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### TTL cache
|
|
687
|
+
|
|
688
|
+
`pg_description` queries are cached per `(schema, table)` with a configurable TTL (default 60s):
|
|
689
|
+
|
|
690
|
+
```python
|
|
691
|
+
soh.configure_meta_cache_ttl(120) # change TTL to 120 seconds
|
|
692
|
+
soh.invalidate_meta_cache() # clear entire cache
|
|
693
|
+
soh.invalidate_meta_cache(schema="lead") # clear all entries for a schema
|
|
694
|
+
soh.invalidate_meta_cache("lead", "emails") # clear one entry
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### Standalone mode
|
|
698
|
+
|
|
699
|
+
```python
|
|
700
|
+
import sqlmodel_object_helpers.standalone as soh_sa
|
|
701
|
+
|
|
702
|
+
table_meta, columns = await soh_sa.build_dynamic_meta(
|
|
703
|
+
EmailMessage,
|
|
704
|
+
name="email_messages",
|
|
705
|
+
columns=["id", "email_type_id"],
|
|
706
|
+
)
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### Backward compatibility and migration
|
|
710
|
+
|
|
711
|
+
`GetAllPagination` is fully backward compatible — `table` and `columns` default to `None`, so existing endpoints continue to work without changes:
|
|
712
|
+
|
|
713
|
+
```python
|
|
714
|
+
# Before (still works as-is)
|
|
715
|
+
return soh.GetAllPagination(data=items, pagination=pagination_r)
|
|
716
|
+
|
|
717
|
+
# After (meta added when ready)
|
|
718
|
+
return soh.GetAllPagination(
|
|
719
|
+
table=table_meta,
|
|
720
|
+
columns=columns,
|
|
721
|
+
data=items,
|
|
722
|
+
pagination=pagination_r,
|
|
723
|
+
)
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
Endpoints can be migrated one at a time. Three strategies per endpoint:
|
|
727
|
+
|
|
728
|
+
| Strategy | `table`/`columns` | Use case |
|
|
729
|
+
|---|---|---|
|
|
730
|
+
| **No meta** | Always `None` | Endpoint not yet migrated, frontend uses hardcoded table |
|
|
731
|
+
| **Always meta** | Sent on every request | Simple, no frontend caching logic needed |
|
|
732
|
+
| **Meta on first page** | Sent when `page=1`, `None` on pages 2+ | Saves traffic, frontend caches meta from first response |
|
|
733
|
+
|
|
595
734
|
## API Reference
|
|
596
735
|
|
|
597
736
|
### Session Management
|
|
@@ -602,7 +741,7 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
602
741
|
|
|
603
742
|
### Standalone Wrappers
|
|
604
743
|
|
|
605
|
-
- `import sqlmodel_object_helpers.standalone as soh_sa` -- All 12 query/mutation functions without `session` parameter
|
|
744
|
+
- `import sqlmodel_object_helpers.standalone as soh_sa` -- All 12 query/mutation functions plus `build_dynamic_meta` without `session` parameter
|
|
606
745
|
|
|
607
746
|
### Settings
|
|
608
747
|
|
|
@@ -627,6 +766,14 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
627
766
|
- `soh.delete_objects(session, model, filters)` -- Bulk delete via single `DELETE ... WHERE`
|
|
628
767
|
- `soh.check_for_related_records(session, model, pk)` -- Pre-deletion dependency check
|
|
629
768
|
|
|
769
|
+
### Dynamic Meta
|
|
770
|
+
|
|
771
|
+
- `soh.build_dynamic_meta(session, model, *, name, columns, header, row_link, role_type)` -- Build `TableMeta` + `list[ColumnMeta]` from model + `pg_description`
|
|
772
|
+
- `soh.load_pg_comments(session, schema, table)` -- Read `(table_comment, {col: comment})` from `pg_description` (TTL-cached)
|
|
773
|
+
- `soh.configure_meta_cache_ttl(seconds)` -- Override default `pg_description` cache TTL (default: 60s)
|
|
774
|
+
- `soh.invalidate_meta_cache(schema, table)` -- Manually invalidate the `pg_description` cache (full, by-schema, or by-table)
|
|
775
|
+
- `soh.ColumnEntry` -- Type alias: `str | ColumnMeta` (element of `columns` list in `build_dynamic_meta`)
|
|
776
|
+
|
|
630
777
|
### Filter Builders
|
|
631
778
|
|
|
632
779
|
- `soh.build_filter(model, filters, ...)` -- Build SQLAlchemy expression from LogicalFilter dict
|
|
@@ -655,7 +802,8 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
655
802
|
|
|
656
803
|
### Filter Types
|
|
657
804
|
|
|
658
|
-
- `soh.FilterInt`, `soh.FilterStr`, `soh.FilterDate`, `soh.FilterDatetime`, `soh.FilterTimedelta`, `soh.FilterBool`, `soh.FilterExists`
|
|
805
|
+
- `soh.FilterInt`, `soh.FilterStr`, `soh.FilterDate`, `soh.FilterDatetime`, `soh.FilterNaiveDatetime`, `soh.FilterTimedelta`, `soh.FilterBool`, `soh.FilterExists`
|
|
806
|
+
- `soh.FilterDatetimeRange`, `soh.FilterNaiveDatetimeRange` -- Range filters that parse `"FROM,TO"` strings into `gt`/`lt` operators
|
|
659
807
|
- `soh.OrderAsc`, `soh.OrderDesc`, `soh.OrderBy`
|
|
660
808
|
- `soh.LogicalFilter`
|
|
661
809
|
- `soh.TimeFilter` -- `created_after`, `created_before`, `updated_after`, `updated_before` with half-open interval `[after, before)`
|
|
@@ -668,7 +816,19 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
668
816
|
|
|
669
817
|
- `soh.Pagination` -- Request model (page, per\_page)
|
|
670
818
|
- `soh.PaginationR` -- Response model (page, per\_page, total)
|
|
671
|
-
- `soh.GetAllPagination[T]` -- Generic wrapper (data: list[T], pagination: PaginationR | None)
|
|
819
|
+
- `soh.GetAllPagination[T]` -- Generic wrapper (table: TableMeta | None, columns: list[ColumnMeta] | None, data: list[T], pagination: PaginationR | None)
|
|
820
|
+
|
|
821
|
+
### Table Metadata Types
|
|
822
|
+
|
|
823
|
+
- `soh.TableMeta` -- Table-level metadata (name, header, row_link)
|
|
824
|
+
- `soh.ColumnMeta` -- Column metadata (json_path, label, type, lookup_dict, lookup_path, bool_labels)
|
|
825
|
+
- `soh.ColumnType` -- StrEnum of column data types (string, integer, float, boolean, date, datetime)
|
|
826
|
+
- `soh.BoolLabels` -- Display labels for boolean columns (true_label, false_label)
|
|
827
|
+
|
|
828
|
+
### Lookup Types
|
|
829
|
+
|
|
830
|
+
- `soh.LookupMeta` -- Lookup metadata (name used as cache key, matches `ColumnMeta.lookup_dict`)
|
|
831
|
+
- `soh.LookupResponse[T]` -- Generic wrapper for lookup endpoints (meta: LookupMeta, data: list[T])
|
|
672
832
|
|
|
673
833
|
### Projection Types
|
|
674
834
|
|
|
@@ -720,6 +880,23 @@ This project follows [Semantic Versioning](https://semver.org/) (MAJOR.MINOR.PAT
|
|
|
720
880
|
- **MINOR** — new features (backwards compatible)
|
|
721
881
|
- **MAJOR** — breaking changes
|
|
722
882
|
|
|
883
|
+
## Changelog
|
|
884
|
+
|
|
885
|
+
### 0.0.6
|
|
886
|
+
|
|
887
|
+
- **build_dynamic_meta** / **load_pg_comments** / **configure_meta_cache_ttl** / **invalidate_meta_cache** — dynamic UI metadata reader. Builds `TableMeta` and `list[ColumnMeta]` from a SQLModel class by reading PostgreSQL `pg_description` (TTL-cached, default 60s) with fallback to `Column(comment=...)` defaults from the model. Supports per-role label/row_link overrides via extended `||` comment format. For physical columns the label precedence is `pg_description` > `Column(comment=...)` model default. For virtual columns (paths with `.` traversing relationships) and full overrides — pass a fully-specified `ColumnMeta` instance directly in the `columns` list (used as-is, no derivation). Lookup `lookup_dict`/`lookup_path` are derived from FK on `*_lkp` tables by `{schema}_{base}` convention. Type derived from SA column type. DBAs can edit labels via `COMMENT ON COLUMN/TABLE` SQL — frontend reflects changes within cache TTL without redeploy. No sync block, no schema migrations introduced; the application's `db.py` is not touched.
|
|
888
|
+
|
|
889
|
+
### 0.0.5
|
|
890
|
+
|
|
891
|
+
- **FilterDatetimeRange** / **FilterNaiveDatetimeRange** — range filters that parse comma-separated date strings (`"2026-04-01,2026-05-06"`) into `gt`/`lt` operators for SQL filtering
|
|
892
|
+
- **ColumnMeta** / **TableMeta** / **ColumnType** / **BoolLabels** — dynamic table metadata: backend describes columns, types, labels, lookups, and row navigation so the frontend renders any table without hardcoding
|
|
893
|
+
- **GetAllPagination** — now includes optional `table` and `columns` fields for delivering table metadata alongside paginated data
|
|
894
|
+
- **LookupMeta** / **LookupResponse** — standard `{meta, data}` wrapper for lookup (`_lkp`) endpoints, enabling unified frontend caching of dictionaries with `meta.name` as cache key
|
|
895
|
+
|
|
896
|
+
### 0.0.4
|
|
897
|
+
|
|
898
|
+
- Initial public release
|
|
899
|
+
|
|
723
900
|
## License
|
|
724
901
|
|
|
725
902
|
This project is licensed under the **PolyForm Noncommercial License 1.0.0**.
|
|
@@ -21,6 +21,7 @@ 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
|
+
- **Dynamic Table Metadata** - `build_dynamic_meta` derives `TableMeta` + `list[ColumnMeta]` from a SQLModel class by reading PostgreSQL `pg_description` with fallback to `Column(comment=...)`. Supports per-role label/row_link overrides via `||` comment format, TTL-cached
|
|
24
25
|
- **Standalone Mode** - `configure()` + `import sqlmodel_object_helpers.standalone` for auto-session usage without DI
|
|
25
26
|
- **Session Lifecycle Logging** - Transparent session open/commit/rollback logging with hex session IDs and timing
|
|
26
27
|
|
|
@@ -505,7 +506,34 @@ class UserFilter(BaseModel):
|
|
|
505
506
|
posts: soh.FilterExists | None = None # exists: bool
|
|
506
507
|
```
|
|
507
508
|
|
|
508
|
-
Available: `FilterInt`, `FilterStr`, `FilterDate`, `FilterDatetime`, `FilterTimedelta`, `FilterBool`, `FilterExists`.
|
|
509
|
+
Available: `FilterInt`, `FilterStr`, `FilterDate`, `FilterDatetime`, `FilterNaiveDatetime`, `FilterTimedelta`, `FilterBool`, `FilterExists`.
|
|
510
|
+
|
|
511
|
+
#### Range Filters
|
|
512
|
+
|
|
513
|
+
`FilterDatetimeRange` and `FilterNaiveDatetimeRange` parse a comma-separated date string into `gt`/`lt` operators:
|
|
514
|
+
|
|
515
|
+
```python
|
|
516
|
+
import sqlmodel_object_helpers as soh
|
|
517
|
+
|
|
518
|
+
# Full range: "FROM,TO" → gt + lt
|
|
519
|
+
f = soh.FilterDatetimeRange.model_validate("2026-04-01,2026-05-06")
|
|
520
|
+
f.model_dump(exclude_none=True)
|
|
521
|
+
# {"gt": datetime(2026, 4, 1, tzinfo=UTC), "lt": datetime(2026, 5, 6, tzinfo=UTC)}
|
|
522
|
+
# SQL: WHERE field > '2026-04-01' AND field < '2026-05-06'
|
|
523
|
+
|
|
524
|
+
# Open end: "FROM," → gt only
|
|
525
|
+
f = soh.FilterDatetimeRange.model_validate("2026-04-01,")
|
|
526
|
+
# SQL: WHERE field > '2026-04-01'
|
|
527
|
+
|
|
528
|
+
# Open start: ",TO" → lt only
|
|
529
|
+
f = soh.FilterDatetimeRange.model_validate(",2026-05-06")
|
|
530
|
+
# SQL: WHERE field < '2026-05-06'
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
Each date part accepts ISO (`2026-04-01`, `2026-04-01T00:00:00Z`) and display format (`01.04.2026 00:00`).
|
|
534
|
+
|
|
535
|
+
- `FilterDatetimeRange` — produces UTC-aware datetimes
|
|
536
|
+
- `FilterNaiveDatetimeRange` — produces naive (timezone-unaware) datetimes
|
|
509
537
|
|
|
510
538
|
## Eager Loading
|
|
511
539
|
|
|
@@ -558,6 +586,117 @@ order = soh.OrderBy(sorts=[
|
|
|
558
586
|
result = await soh.get_objects(session, User, order_by=order)
|
|
559
587
|
```
|
|
560
588
|
|
|
589
|
+
## Dynamic Table Metadata
|
|
590
|
+
|
|
591
|
+
`build_dynamic_meta` builds `TableMeta` + `list[ColumnMeta]` from a SQLModel class at runtime, reading PostgreSQL `pg_description` (TTL-cached) with fallback to `Column(comment=...)` model defaults.
|
|
592
|
+
|
|
593
|
+
```python
|
|
594
|
+
import sqlmodel_object_helpers as soh
|
|
595
|
+
|
|
596
|
+
# Physical columns — label, type, lookup derived automatically
|
|
597
|
+
table_meta, columns = await soh.build_dynamic_meta(
|
|
598
|
+
session,
|
|
599
|
+
EmailMessage,
|
|
600
|
+
name="email_messages",
|
|
601
|
+
columns=[
|
|
602
|
+
"id", # physical column — everything derived
|
|
603
|
+
"email_type_id", # FK on *_lkp → lookup_dict auto-derived
|
|
604
|
+
"updated_at",
|
|
605
|
+
soh.ColumnMeta( # virtual column — fully specified
|
|
606
|
+
json_path="last_editor.user_name",
|
|
607
|
+
label="Редактор",
|
|
608
|
+
type=soh.ColumnType.STRING,
|
|
609
|
+
),
|
|
610
|
+
],
|
|
611
|
+
role_type="operator", # per-role label/row_link overrides
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# Returns (TableMeta, list[ColumnMeta]) ready for GetAllPagination
|
|
615
|
+
return soh.GetAllPagination(
|
|
616
|
+
table=table_meta,
|
|
617
|
+
columns=columns,
|
|
618
|
+
data=items,
|
|
619
|
+
pagination=pagination_r,
|
|
620
|
+
)
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### Type derivation (SA type → ColumnType)
|
|
624
|
+
|
|
625
|
+
Physical column types are mapped automatically. Unknown types fall back to `string`.
|
|
626
|
+
|
|
627
|
+
| SQLAlchemy type | ColumnType |
|
|
628
|
+
|---|---|
|
|
629
|
+
| `Boolean` | `boolean` |
|
|
630
|
+
| `DateTime` | `datetime` |
|
|
631
|
+
| `Date` | `date` |
|
|
632
|
+
| `Integer`, `BigInteger`, `SmallInteger` | `integer` |
|
|
633
|
+
| `Numeric`, `Float` | `float` |
|
|
634
|
+
| `String`, `Text`, `Enum`, `Interval`, `ARRAY`, `Uuid` | `string` |
|
|
635
|
+
| Any other type | `string` (fallback) |
|
|
636
|
+
|
|
637
|
+
### Label precedence (per physical column)
|
|
638
|
+
|
|
639
|
+
1. `pg_description` — DBA edit via `COMMENT ON COLUMN` (supports per-role `||` overrides)
|
|
640
|
+
2. `Column(comment=...)` — Python-side default in the model
|
|
641
|
+
3. `ValueError` — fail-loud if both are missing
|
|
642
|
+
|
|
643
|
+
### Per-role overrides via `||` format
|
|
644
|
+
|
|
645
|
+
DBAs can set role-specific labels and row links in PostgreSQL comments:
|
|
646
|
+
|
|
647
|
+
```sql
|
|
648
|
+
COMMENT ON COLUMN emails.email_type_id IS 'Тип письма||operator=Категория||buh=Реквизит';
|
|
649
|
+
COMMENT ON TABLE emails IS 'Письма||row_link=/email/$id||row_link.operator=/op/email/$id';
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### TTL cache
|
|
653
|
+
|
|
654
|
+
`pg_description` queries are cached per `(schema, table)` with a configurable TTL (default 60s):
|
|
655
|
+
|
|
656
|
+
```python
|
|
657
|
+
soh.configure_meta_cache_ttl(120) # change TTL to 120 seconds
|
|
658
|
+
soh.invalidate_meta_cache() # clear entire cache
|
|
659
|
+
soh.invalidate_meta_cache(schema="lead") # clear all entries for a schema
|
|
660
|
+
soh.invalidate_meta_cache("lead", "emails") # clear one entry
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### Standalone mode
|
|
664
|
+
|
|
665
|
+
```python
|
|
666
|
+
import sqlmodel_object_helpers.standalone as soh_sa
|
|
667
|
+
|
|
668
|
+
table_meta, columns = await soh_sa.build_dynamic_meta(
|
|
669
|
+
EmailMessage,
|
|
670
|
+
name="email_messages",
|
|
671
|
+
columns=["id", "email_type_id"],
|
|
672
|
+
)
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### Backward compatibility and migration
|
|
676
|
+
|
|
677
|
+
`GetAllPagination` is fully backward compatible — `table` and `columns` default to `None`, so existing endpoints continue to work without changes:
|
|
678
|
+
|
|
679
|
+
```python
|
|
680
|
+
# Before (still works as-is)
|
|
681
|
+
return soh.GetAllPagination(data=items, pagination=pagination_r)
|
|
682
|
+
|
|
683
|
+
# After (meta added when ready)
|
|
684
|
+
return soh.GetAllPagination(
|
|
685
|
+
table=table_meta,
|
|
686
|
+
columns=columns,
|
|
687
|
+
data=items,
|
|
688
|
+
pagination=pagination_r,
|
|
689
|
+
)
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
Endpoints can be migrated one at a time. Three strategies per endpoint:
|
|
693
|
+
|
|
694
|
+
| Strategy | `table`/`columns` | Use case |
|
|
695
|
+
|---|---|---|
|
|
696
|
+
| **No meta** | Always `None` | Endpoint not yet migrated, frontend uses hardcoded table |
|
|
697
|
+
| **Always meta** | Sent on every request | Simple, no frontend caching logic needed |
|
|
698
|
+
| **Meta on first page** | Sent when `page=1`, `None` on pages 2+ | Saves traffic, frontend caches meta from first response |
|
|
699
|
+
|
|
561
700
|
## API Reference
|
|
562
701
|
|
|
563
702
|
### Session Management
|
|
@@ -568,7 +707,7 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
568
707
|
|
|
569
708
|
### Standalone Wrappers
|
|
570
709
|
|
|
571
|
-
- `import sqlmodel_object_helpers.standalone as soh_sa` -- All 12 query/mutation functions without `session` parameter
|
|
710
|
+
- `import sqlmodel_object_helpers.standalone as soh_sa` -- All 12 query/mutation functions plus `build_dynamic_meta` without `session` parameter
|
|
572
711
|
|
|
573
712
|
### Settings
|
|
574
713
|
|
|
@@ -593,6 +732,14 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
593
732
|
- `soh.delete_objects(session, model, filters)` -- Bulk delete via single `DELETE ... WHERE`
|
|
594
733
|
- `soh.check_for_related_records(session, model, pk)` -- Pre-deletion dependency check
|
|
595
734
|
|
|
735
|
+
### Dynamic Meta
|
|
736
|
+
|
|
737
|
+
- `soh.build_dynamic_meta(session, model, *, name, columns, header, row_link, role_type)` -- Build `TableMeta` + `list[ColumnMeta]` from model + `pg_description`
|
|
738
|
+
- `soh.load_pg_comments(session, schema, table)` -- Read `(table_comment, {col: comment})` from `pg_description` (TTL-cached)
|
|
739
|
+
- `soh.configure_meta_cache_ttl(seconds)` -- Override default `pg_description` cache TTL (default: 60s)
|
|
740
|
+
- `soh.invalidate_meta_cache(schema, table)` -- Manually invalidate the `pg_description` cache (full, by-schema, or by-table)
|
|
741
|
+
- `soh.ColumnEntry` -- Type alias: `str | ColumnMeta` (element of `columns` list in `build_dynamic_meta`)
|
|
742
|
+
|
|
596
743
|
### Filter Builders
|
|
597
744
|
|
|
598
745
|
- `soh.build_filter(model, filters, ...)` -- Build SQLAlchemy expression from LogicalFilter dict
|
|
@@ -621,7 +768,8 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
621
768
|
|
|
622
769
|
### Filter Types
|
|
623
770
|
|
|
624
|
-
- `soh.FilterInt`, `soh.FilterStr`, `soh.FilterDate`, `soh.FilterDatetime`, `soh.FilterTimedelta`, `soh.FilterBool`, `soh.FilterExists`
|
|
771
|
+
- `soh.FilterInt`, `soh.FilterStr`, `soh.FilterDate`, `soh.FilterDatetime`, `soh.FilterNaiveDatetime`, `soh.FilterTimedelta`, `soh.FilterBool`, `soh.FilterExists`
|
|
772
|
+
- `soh.FilterDatetimeRange`, `soh.FilterNaiveDatetimeRange` -- Range filters that parse `"FROM,TO"` strings into `gt`/`lt` operators
|
|
625
773
|
- `soh.OrderAsc`, `soh.OrderDesc`, `soh.OrderBy`
|
|
626
774
|
- `soh.LogicalFilter`
|
|
627
775
|
- `soh.TimeFilter` -- `created_after`, `created_before`, `updated_after`, `updated_before` with half-open interval `[after, before)`
|
|
@@ -634,7 +782,19 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
634
782
|
|
|
635
783
|
- `soh.Pagination` -- Request model (page, per\_page)
|
|
636
784
|
- `soh.PaginationR` -- Response model (page, per\_page, total)
|
|
637
|
-
- `soh.GetAllPagination[T]` -- Generic wrapper (data: list[T], pagination: PaginationR | None)
|
|
785
|
+
- `soh.GetAllPagination[T]` -- Generic wrapper (table: TableMeta | None, columns: list[ColumnMeta] | None, data: list[T], pagination: PaginationR | None)
|
|
786
|
+
|
|
787
|
+
### Table Metadata Types
|
|
788
|
+
|
|
789
|
+
- `soh.TableMeta` -- Table-level metadata (name, header, row_link)
|
|
790
|
+
- `soh.ColumnMeta` -- Column metadata (json_path, label, type, lookup_dict, lookup_path, bool_labels)
|
|
791
|
+
- `soh.ColumnType` -- StrEnum of column data types (string, integer, float, boolean, date, datetime)
|
|
792
|
+
- `soh.BoolLabels` -- Display labels for boolean columns (true_label, false_label)
|
|
793
|
+
|
|
794
|
+
### Lookup Types
|
|
795
|
+
|
|
796
|
+
- `soh.LookupMeta` -- Lookup metadata (name used as cache key, matches `ColumnMeta.lookup_dict`)
|
|
797
|
+
- `soh.LookupResponse[T]` -- Generic wrapper for lookup endpoints (meta: LookupMeta, data: list[T])
|
|
638
798
|
|
|
639
799
|
### Projection Types
|
|
640
800
|
|
|
@@ -686,6 +846,23 @@ This project follows [Semantic Versioning](https://semver.org/) (MAJOR.MINOR.PAT
|
|
|
686
846
|
- **MINOR** — new features (backwards compatible)
|
|
687
847
|
- **MAJOR** — breaking changes
|
|
688
848
|
|
|
849
|
+
## Changelog
|
|
850
|
+
|
|
851
|
+
### 0.0.6
|
|
852
|
+
|
|
853
|
+
- **build_dynamic_meta** / **load_pg_comments** / **configure_meta_cache_ttl** / **invalidate_meta_cache** — dynamic UI metadata reader. Builds `TableMeta` and `list[ColumnMeta]` from a SQLModel class by reading PostgreSQL `pg_description` (TTL-cached, default 60s) with fallback to `Column(comment=...)` defaults from the model. Supports per-role label/row_link overrides via extended `||` comment format. For physical columns the label precedence is `pg_description` > `Column(comment=...)` model default. For virtual columns (paths with `.` traversing relationships) and full overrides — pass a fully-specified `ColumnMeta` instance directly in the `columns` list (used as-is, no derivation). Lookup `lookup_dict`/`lookup_path` are derived from FK on `*_lkp` tables by `{schema}_{base}` convention. Type derived from SA column type. DBAs can edit labels via `COMMENT ON COLUMN/TABLE` SQL — frontend reflects changes within cache TTL without redeploy. No sync block, no schema migrations introduced; the application's `db.py` is not touched.
|
|
854
|
+
|
|
855
|
+
### 0.0.5
|
|
856
|
+
|
|
857
|
+
- **FilterDatetimeRange** / **FilterNaiveDatetimeRange** — range filters that parse comma-separated date strings (`"2026-04-01,2026-05-06"`) into `gt`/`lt` operators for SQL filtering
|
|
858
|
+
- **ColumnMeta** / **TableMeta** / **ColumnType** / **BoolLabels** — dynamic table metadata: backend describes columns, types, labels, lookups, and row navigation so the frontend renders any table without hardcoding
|
|
859
|
+
- **GetAllPagination** — now includes optional `table` and `columns` fields for delivering table metadata alongside paginated data
|
|
860
|
+
- **LookupMeta** / **LookupResponse** — standard `{meta, data}` wrapper for lookup (`_lkp`) endpoints, enabling unified frontend caching of dictionaries with `meta.name` as cache key
|
|
861
|
+
|
|
862
|
+
### 0.0.4
|
|
863
|
+
|
|
864
|
+
- Initial public release
|
|
865
|
+
|
|
689
866
|
## License
|
|
690
867
|
|
|
691
868
|
This project is licensed under the **PolyForm Noncommercial License 1.0.0**.
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
"""sqlmodel-object-helpers — reusable query helpers for SQLModel projects."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.0.
|
|
3
|
+
__version__ = "0.0.6"
|
|
4
4
|
|
|
5
5
|
from .constants import QueryHelperSettings, settings
|
|
6
|
+
from .dynamic_meta import (
|
|
7
|
+
ColumnEntry,
|
|
8
|
+
build_dynamic_meta,
|
|
9
|
+
configure_meta_cache_ttl,
|
|
10
|
+
invalidate_meta_cache,
|
|
11
|
+
load_pg_comments,
|
|
12
|
+
)
|
|
6
13
|
from .exceptions import (
|
|
7
14
|
DatabaseError,
|
|
8
15
|
InvalidFilterError,
|
|
@@ -13,7 +20,6 @@ from .exceptions import (
|
|
|
13
20
|
)
|
|
14
21
|
from .filters import build_filter, build_flat_filter, flatten_filters
|
|
15
22
|
from .loaders import build_load_chain, build_load_options
|
|
16
|
-
from .operators import SPECIAL_OPERATORS, SUPPORTED_OPERATORS, Operator
|
|
17
23
|
from .mutations import (
|
|
18
24
|
add_object,
|
|
19
25
|
add_objects,
|
|
@@ -23,16 +29,20 @@ from .mutations import (
|
|
|
23
29
|
update_object,
|
|
24
30
|
update_objects,
|
|
25
31
|
)
|
|
32
|
+
from .operators import SPECIAL_OPERATORS, SUPPORTED_OPERATORS, Operator
|
|
26
33
|
from .query import count_objects, exists_object, get_object, get_objects, get_projection
|
|
27
34
|
from .session import auto_session, configure, create_session_dependency
|
|
35
|
+
from .types.columns import BoolLabels, ColumnMeta, ColumnType, TableMeta
|
|
28
36
|
from .types.datetime import UTCDatetime
|
|
29
37
|
from .types.filters import (
|
|
30
38
|
FilterBool,
|
|
31
39
|
FilterDate,
|
|
32
40
|
FilterDatetime,
|
|
41
|
+
FilterDatetimeRange,
|
|
33
42
|
FilterExists,
|
|
34
43
|
FilterInt,
|
|
35
44
|
FilterNaiveDatetime,
|
|
45
|
+
FilterNaiveDatetimeRange,
|
|
36
46
|
FilterStr,
|
|
37
47
|
FilterTimedelta,
|
|
38
48
|
LogicalFilter,
|
|
@@ -43,6 +53,8 @@ from .types.filters import (
|
|
|
43
53
|
)
|
|
44
54
|
from .types.pagination import (
|
|
45
55
|
GetAllPagination,
|
|
56
|
+
LookupMeta,
|
|
57
|
+
LookupResponse,
|
|
46
58
|
Pagination,
|
|
47
59
|
PaginationR,
|
|
48
60
|
)
|
|
@@ -52,13 +64,19 @@ __all__ = [
|
|
|
52
64
|
"add_object",
|
|
53
65
|
"add_objects",
|
|
54
66
|
"auto_session",
|
|
67
|
+
"build_dynamic_meta",
|
|
55
68
|
"build_filter",
|
|
56
69
|
"build_flat_filter",
|
|
57
70
|
"build_load_chain",
|
|
58
71
|
"build_load_options",
|
|
59
72
|
"check_for_related_records",
|
|
73
|
+
"BoolLabels",
|
|
74
|
+
"ColumnEntry",
|
|
75
|
+
"ColumnMeta",
|
|
60
76
|
"ColumnSpec",
|
|
77
|
+
"ColumnType",
|
|
61
78
|
"configure",
|
|
79
|
+
"configure_meta_cache_ttl",
|
|
62
80
|
"count_objects",
|
|
63
81
|
"create_session_dependency",
|
|
64
82
|
"DatabaseError",
|
|
@@ -68,9 +86,11 @@ __all__ = [
|
|
|
68
86
|
"FilterBool",
|
|
69
87
|
"FilterDate",
|
|
70
88
|
"FilterDatetime",
|
|
89
|
+
"FilterDatetimeRange",
|
|
71
90
|
"FilterExists",
|
|
72
91
|
"FilterInt",
|
|
73
92
|
"FilterNaiveDatetime",
|
|
93
|
+
"FilterNaiveDatetimeRange",
|
|
74
94
|
"FilterStr",
|
|
75
95
|
"FilterTimedelta",
|
|
76
96
|
"flatten_filters",
|
|
@@ -78,9 +98,13 @@ __all__ = [
|
|
|
78
98
|
"get_objects",
|
|
79
99
|
"get_projection",
|
|
80
100
|
"GetAllPagination",
|
|
101
|
+
"invalidate_meta_cache",
|
|
81
102
|
"InvalidFilterError",
|
|
82
103
|
"InvalidLoadPathError",
|
|
104
|
+
"load_pg_comments",
|
|
83
105
|
"LogicalFilter",
|
|
106
|
+
"LookupMeta",
|
|
107
|
+
"LookupResponse",
|
|
84
108
|
"MutationError",
|
|
85
109
|
"ObjectNotFoundError",
|
|
86
110
|
"Operator",
|
|
@@ -94,6 +118,7 @@ __all__ = [
|
|
|
94
118
|
"settings",
|
|
95
119
|
"SPECIAL_OPERATORS",
|
|
96
120
|
"SUPPORTED_OPERATORS",
|
|
121
|
+
"TableMeta",
|
|
97
122
|
"TimeFilter",
|
|
98
123
|
"update_object",
|
|
99
124
|
"update_objects",
|