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.
Files changed (44) hide show
  1. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/PKG-INFO +182 -5
  2. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/README.md +181 -4
  3. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/__init__.py +27 -2
  4. sqlmodel_object_helpers-0.0.6/src/sqlmodel_object_helpers/dynamic_meta.py +468 -0
  5. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/standalone.py +35 -0
  6. sqlmodel_object_helpers-0.0.6/src/sqlmodel_object_helpers/types/__init__.py +50 -0
  7. sqlmodel_object_helpers-0.0.6/src/sqlmodel_object_helpers/types/columns.py +79 -0
  8. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/types/filters.py +152 -1
  9. sqlmodel_object_helpers-0.0.6/src/sqlmodel_object_helpers/types/pagination.py +49 -0
  10. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/conftest.py +5 -1
  11. sqlmodel_object_helpers-0.0.6/tests/test_column_meta.py +511 -0
  12. sqlmodel_object_helpers-0.0.6/tests/test_datetime_range.py +295 -0
  13. sqlmodel_object_helpers-0.0.6/tests/test_dynamic_meta.py +1037 -0
  14. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_query.py +37 -0
  15. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_standalone.py +22 -0
  16. sqlmodel_object_helpers-0.0.4/src/sqlmodel_object_helpers/types/__init__.py +0 -3
  17. sqlmodel_object_helpers-0.0.4/src/sqlmodel_object_helpers/types/pagination.py +0 -15
  18. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/.github/workflows/publish.yml +0 -0
  19. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/.gitignore +0 -0
  20. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/LICENSE +0 -0
  21. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/pyproject.toml +0 -0
  22. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/constants.py +0 -0
  23. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/exceptions.py +0 -0
  24. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/filters.py +0 -0
  25. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/loaders.py +0 -0
  26. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/mutations.py +0 -0
  27. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/operators.py +0 -0
  28. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/query.py +0 -0
  29. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/session.py +0 -0
  30. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/types/datetime.py +0 -0
  31. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/src/sqlmodel_object_helpers/types/projections.py +0 -0
  32. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_bulk_mutations.py +0 -0
  33. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_computed_columns.py +0 -0
  34. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_count_exists.py +0 -0
  35. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_exceptions.py +0 -0
  36. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_filters.py +0 -0
  37. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_for_update.py +0 -0
  38. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_generated_columns_pg.py +0 -0
  39. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_loaders.py +0 -0
  40. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_mutations.py +0 -0
  41. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_operators.py +0 -0
  42. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_settings.py +0 -0
  43. {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.6}/tests/test_time_filter.py +0 -0
  44. {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.4
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.4"
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",