sqlmodel-object-helpers 0.0.4__tar.gz → 0.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/PKG-INFO +57 -4
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/README.md +56 -3
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/__init__.py +14 -1
- sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/types/__init__.py +50 -0
- sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/types/columns.py +79 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/types/filters.py +152 -1
- sqlmodel_object_helpers-0.0.5/src/sqlmodel_object_helpers/types/pagination.py +49 -0
- sqlmodel_object_helpers-0.0.5/tests/test_column_meta.py +511 -0
- sqlmodel_object_helpers-0.0.5/tests/test_datetime_range.py +290 -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.5}/.github/workflows/publish.yml +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/.gitignore +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/LICENSE +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/pyproject.toml +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/constants.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/exceptions.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/filters.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/loaders.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/mutations.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/operators.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/query.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/session.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/standalone.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/types/datetime.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/src/sqlmodel_object_helpers/types/projections.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/tests/conftest.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/tests/test_bulk_mutations.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/tests/test_computed_columns.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/tests/test_count_exists.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/tests/test_exceptions.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/tests/test_filters.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/tests/test_for_update.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/tests/test_generated_columns_pg.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/tests/test_loaders.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/tests/test_mutations.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/tests/test_operators.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/tests/test_query.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/tests/test_settings.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/tests/test_standalone.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/tests/test_time_filter.py +0 -0
- {sqlmodel_object_helpers-0.0.4 → sqlmodel_object_helpers-0.0.5}/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.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
|
|
@@ -539,7 +539,34 @@ class UserFilter(BaseModel):
|
|
|
539
539
|
posts: soh.FilterExists | None = None # exists: bool
|
|
540
540
|
```
|
|
541
541
|
|
|
542
|
-
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
|
|
543
570
|
|
|
544
571
|
## Eager Loading
|
|
545
572
|
|
|
@@ -655,7 +682,8 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
655
682
|
|
|
656
683
|
### Filter Types
|
|
657
684
|
|
|
658
|
-
- `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
|
|
659
687
|
- `soh.OrderAsc`, `soh.OrderDesc`, `soh.OrderBy`
|
|
660
688
|
- `soh.LogicalFilter`
|
|
661
689
|
- `soh.TimeFilter` -- `created_after`, `created_before`, `updated_after`, `updated_before` with half-open interval `[after, before)`
|
|
@@ -668,7 +696,19 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
668
696
|
|
|
669
697
|
- `soh.Pagination` -- Request model (page, per\_page)
|
|
670
698
|
- `soh.PaginationR` -- Response model (page, per\_page, total)
|
|
671
|
-
- `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])
|
|
672
712
|
|
|
673
713
|
### Projection Types
|
|
674
714
|
|
|
@@ -720,6 +760,19 @@ This project follows [Semantic Versioning](https://semver.org/) (MAJOR.MINOR.PAT
|
|
|
720
760
|
- **MINOR** — new features (backwards compatible)
|
|
721
761
|
- **MAJOR** — breaking changes
|
|
722
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
|
+
|
|
723
776
|
## License
|
|
724
777
|
|
|
725
778
|
This project is licensed under the **PolyForm Noncommercial License 1.0.0**.
|
|
@@ -505,7 +505,34 @@ class UserFilter(BaseModel):
|
|
|
505
505
|
posts: soh.FilterExists | None = None # exists: bool
|
|
506
506
|
```
|
|
507
507
|
|
|
508
|
-
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
|
|
509
536
|
|
|
510
537
|
## Eager Loading
|
|
511
538
|
|
|
@@ -621,7 +648,8 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
621
648
|
|
|
622
649
|
### Filter Types
|
|
623
650
|
|
|
624
|
-
- `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
|
|
625
653
|
- `soh.OrderAsc`, `soh.OrderDesc`, `soh.OrderBy`
|
|
626
654
|
- `soh.LogicalFilter`
|
|
627
655
|
- `soh.TimeFilter` -- `created_after`, `created_before`, `updated_after`, `updated_before` with half-open interval `[after, before)`
|
|
@@ -634,7 +662,19 @@ result = await soh.get_objects(session, User, order_by=order)
|
|
|
634
662
|
|
|
635
663
|
- `soh.Pagination` -- Request model (page, per\_page)
|
|
636
664
|
- `soh.PaginationR` -- Response model (page, per\_page, total)
|
|
637
|
-
- `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])
|
|
638
678
|
|
|
639
679
|
### Projection Types
|
|
640
680
|
|
|
@@ -686,6 +726,19 @@ This project follows [Semantic Versioning](https://semver.org/) (MAJOR.MINOR.PAT
|
|
|
686
726
|
- **MINOR** — new features (backwards compatible)
|
|
687
727
|
- **MAJOR** — breaking changes
|
|
688
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
|
+
|
|
689
742
|
## License
|
|
690
743
|
|
|
691
744
|
This project is licensed under the **PolyForm Noncommercial License 1.0.0**.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""sqlmodel-object-helpers — reusable query helpers for SQLModel projects."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.0.
|
|
3
|
+
__version__ = "0.0.5"
|
|
4
4
|
|
|
5
5
|
from .constants import QueryHelperSettings, settings
|
|
6
6
|
from .exceptions import (
|
|
@@ -30,9 +30,11 @@ from .types.filters import (
|
|
|
30
30
|
FilterBool,
|
|
31
31
|
FilterDate,
|
|
32
32
|
FilterDatetime,
|
|
33
|
+
FilterDatetimeRange,
|
|
33
34
|
FilterExists,
|
|
34
35
|
FilterInt,
|
|
35
36
|
FilterNaiveDatetime,
|
|
37
|
+
FilterNaiveDatetimeRange,
|
|
36
38
|
FilterStr,
|
|
37
39
|
FilterTimedelta,
|
|
38
40
|
LogicalFilter,
|
|
@@ -41,8 +43,11 @@ from .types.filters import (
|
|
|
41
43
|
OrderDesc,
|
|
42
44
|
TimeFilter,
|
|
43
45
|
)
|
|
46
|
+
from .types.columns import BoolLabels, ColumnMeta, ColumnType, TableMeta
|
|
44
47
|
from .types.pagination import (
|
|
45
48
|
GetAllPagination,
|
|
49
|
+
LookupMeta,
|
|
50
|
+
LookupResponse,
|
|
46
51
|
Pagination,
|
|
47
52
|
PaginationR,
|
|
48
53
|
)
|
|
@@ -57,7 +62,10 @@ __all__ = [
|
|
|
57
62
|
"build_load_chain",
|
|
58
63
|
"build_load_options",
|
|
59
64
|
"check_for_related_records",
|
|
65
|
+
"BoolLabels",
|
|
66
|
+
"ColumnMeta",
|
|
60
67
|
"ColumnSpec",
|
|
68
|
+
"ColumnType",
|
|
61
69
|
"configure",
|
|
62
70
|
"count_objects",
|
|
63
71
|
"create_session_dependency",
|
|
@@ -68,9 +76,11 @@ __all__ = [
|
|
|
68
76
|
"FilterBool",
|
|
69
77
|
"FilterDate",
|
|
70
78
|
"FilterDatetime",
|
|
79
|
+
"FilterDatetimeRange",
|
|
71
80
|
"FilterExists",
|
|
72
81
|
"FilterInt",
|
|
73
82
|
"FilterNaiveDatetime",
|
|
83
|
+
"FilterNaiveDatetimeRange",
|
|
74
84
|
"FilterStr",
|
|
75
85
|
"FilterTimedelta",
|
|
76
86
|
"flatten_filters",
|
|
@@ -81,6 +91,8 @@ __all__ = [
|
|
|
81
91
|
"InvalidFilterError",
|
|
82
92
|
"InvalidLoadPathError",
|
|
83
93
|
"LogicalFilter",
|
|
94
|
+
"LookupMeta",
|
|
95
|
+
"LookupResponse",
|
|
84
96
|
"MutationError",
|
|
85
97
|
"ObjectNotFoundError",
|
|
86
98
|
"Operator",
|
|
@@ -94,6 +106,7 @@ __all__ = [
|
|
|
94
106
|
"settings",
|
|
95
107
|
"SPECIAL_OPERATORS",
|
|
96
108
|
"SUPPORTED_OPERATORS",
|
|
109
|
+
"TableMeta",
|
|
97
110
|
"TimeFilter",
|
|
98
111
|
"update_object",
|
|
99
112
|
"update_objects",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from .columns import BoolLabels, ColumnMeta, ColumnType, TableMeta
|
|
2
|
+
from .datetime import UTCDatetime
|
|
3
|
+
from .filters import (
|
|
4
|
+
FilterBool,
|
|
5
|
+
FilterDate,
|
|
6
|
+
FilterDatetime,
|
|
7
|
+
FilterDatetimeRange,
|
|
8
|
+
FilterExists,
|
|
9
|
+
FilterInt,
|
|
10
|
+
FilterNaiveDatetime,
|
|
11
|
+
FilterNaiveDatetimeRange,
|
|
12
|
+
FilterStr,
|
|
13
|
+
FilterTimedelta,
|
|
14
|
+
LogicalFilter,
|
|
15
|
+
OrderAsc,
|
|
16
|
+
OrderBy,
|
|
17
|
+
OrderDesc,
|
|
18
|
+
TimeFilter,
|
|
19
|
+
)
|
|
20
|
+
from .pagination import GetAllPagination, LookupMeta, LookupResponse, Pagination, PaginationR
|
|
21
|
+
from .projections import ColumnSpec
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"BoolLabels",
|
|
25
|
+
"ColumnMeta",
|
|
26
|
+
"ColumnSpec",
|
|
27
|
+
"ColumnType",
|
|
28
|
+
"FilterBool",
|
|
29
|
+
"FilterDate",
|
|
30
|
+
"FilterDatetime",
|
|
31
|
+
"FilterDatetimeRange",
|
|
32
|
+
"FilterExists",
|
|
33
|
+
"FilterInt",
|
|
34
|
+
"FilterNaiveDatetime",
|
|
35
|
+
"FilterNaiveDatetimeRange",
|
|
36
|
+
"FilterStr",
|
|
37
|
+
"FilterTimedelta",
|
|
38
|
+
"GetAllPagination",
|
|
39
|
+
"LogicalFilter",
|
|
40
|
+
"LookupMeta",
|
|
41
|
+
"LookupResponse",
|
|
42
|
+
"OrderAsc",
|
|
43
|
+
"OrderBy",
|
|
44
|
+
"OrderDesc",
|
|
45
|
+
"Pagination",
|
|
46
|
+
"PaginationR",
|
|
47
|
+
"TableMeta",
|
|
48
|
+
"TimeFilter",
|
|
49
|
+
"UTCDatetime",
|
|
50
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Column and table metadata for dynamic frontend table rendering."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ColumnType(StrEnum):
|
|
9
|
+
"""Data type of a table column — determines how the frontend renders and filters it."""
|
|
10
|
+
|
|
11
|
+
STRING = "string"
|
|
12
|
+
INTEGER = "integer"
|
|
13
|
+
FLOAT = "float"
|
|
14
|
+
BOOLEAN = "boolean"
|
|
15
|
+
DATE = "date"
|
|
16
|
+
DATETIME = "datetime"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BoolLabels(BaseModel):
|
|
20
|
+
"""Display labels for boolean columns.
|
|
21
|
+
|
|
22
|
+
Source: PostgreSQL domain comments, e.g.
|
|
23
|
+
``COMMENT ON DOMAIN is_paid_domain IS 'Оплачено|Не оплачено'``
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(extra="forbid")
|
|
27
|
+
|
|
28
|
+
true_label: str
|
|
29
|
+
false_label: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TableMeta(BaseModel):
|
|
33
|
+
"""Table-level metadata.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
name: Stable identifier used as a key for saving user settings
|
|
37
|
+
(e.g. ``"attempts"``).
|
|
38
|
+
header: Human-readable table title for the UI
|
|
39
|
+
(e.g. ``"Реестр попыток"``).
|
|
40
|
+
row_link: Optional URL template that makes each row clickable.
|
|
41
|
+
Uses ``$id`` as placeholder for the row identifier
|
|
42
|
+
(e.g. ``"/lead/$id"``, ``"/mail-template-lead/$id/edit"``).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
model_config = ConfigDict(extra="forbid")
|
|
46
|
+
|
|
47
|
+
name: str
|
|
48
|
+
header: str
|
|
49
|
+
row_link: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ColumnMeta(BaseModel):
|
|
53
|
+
"""Metadata for a single table column sent to the frontend.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
json_path: Dot-notation path to the value inside each ``data`` item
|
|
57
|
+
(e.g. ``"application.applicant.full_name"``). Also serves as
|
|
58
|
+
the column identifier.
|
|
59
|
+
label: Human-readable column header (Russian name for the UI).
|
|
60
|
+
type: Data type — drives the filter widget and cell renderer on the
|
|
61
|
+
frontend.
|
|
62
|
+
lookup_dict: Optional name of the LKP dictionary the frontend
|
|
63
|
+
already has cached (e.g. ``"statuses"``). When present the
|
|
64
|
+
frontend renders a ``<select>`` filter instead of free text.
|
|
65
|
+
lookup_path: Optional dot-notation path to the ID field used for
|
|
66
|
+
filtering when ``lookup_dict`` is set. The frontend displays
|
|
67
|
+
``json_path`` but sends ``lookup_path`` in filter requests.
|
|
68
|
+
bool_labels: Optional display labels for boolean values
|
|
69
|
+
(e.g. ``"Оплачено"`` / ``"Не оплачено"``).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
model_config = ConfigDict(extra="forbid")
|
|
73
|
+
|
|
74
|
+
json_path: str
|
|
75
|
+
label: str
|
|
76
|
+
type: ColumnType = ColumnType.STRING
|
|
77
|
+
lookup_dict: str | None = None
|
|
78
|
+
lookup_path: str | None = None
|
|
79
|
+
bool_labels: BoolLabels | None = None
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from datetime import date, datetime, timedelta
|
|
1
|
+
from datetime import date, datetime, timedelta, timezone
|
|
2
2
|
from typing import Any, Self
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
|
@@ -7,6 +7,38 @@ from ..constants import settings
|
|
|
7
7
|
from .datetime import UTCDatetime
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
def _parse_range_part(s: str) -> datetime:
|
|
11
|
+
"""Parse a single date/datetime from a range string part.
|
|
12
|
+
|
|
13
|
+
Accepts ISO formats (``2026-04-01``, ``2026-04-01T00:00:00``,
|
|
14
|
+
``2026-04-01T00:00:00.000Z``) and display format (``01.04.2026 00:00``).
|
|
15
|
+
"""
|
|
16
|
+
s = s.strip()
|
|
17
|
+
if not s:
|
|
18
|
+
msg = "Empty date string"
|
|
19
|
+
raise ValueError(msg)
|
|
20
|
+
|
|
21
|
+
# ISO formats: 2026-04-01, 2026-04-01T00:00:00, 2026-04-01T00:00:00.000Z, +03:00 etc.
|
|
22
|
+
try:
|
|
23
|
+
normalized = s.removesuffix("Z") + "+00:00" if s.endswith("Z") else s
|
|
24
|
+
return datetime.fromisoformat(normalized)
|
|
25
|
+
except ValueError:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
# Display formats: DD.MM.YYYY, DD.MM.YYYY HH:MM, DD.MM.YYYY HH:MM:SS
|
|
29
|
+
for fmt in ("%d.%m.%Y %H:%M", "%d.%m.%Y %H:%M:%S", "%d.%m.%Y"):
|
|
30
|
+
try:
|
|
31
|
+
return datetime.strptime(s, fmt) # noqa: DTZ007
|
|
32
|
+
except ValueError:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
msg = (
|
|
36
|
+
f"Invalid date format: {s!r}. "
|
|
37
|
+
"Expected YYYY-MM-DD, ISO datetime, or DD.MM.YYYY HH:MM"
|
|
38
|
+
)
|
|
39
|
+
raise ValueError(msg)
|
|
40
|
+
|
|
41
|
+
|
|
10
42
|
class FilterInt(BaseModel):
|
|
11
43
|
eq: int | None = None
|
|
12
44
|
ne: int | None = None
|
|
@@ -97,6 +129,125 @@ class FilterTimedelta(BaseModel):
|
|
|
97
129
|
le: timedelta | None = None
|
|
98
130
|
|
|
99
131
|
|
|
132
|
+
def _split_range(range_str: str) -> tuple[str, str]:
|
|
133
|
+
"""Split a range string by comma into (left, right) parts.
|
|
134
|
+
|
|
135
|
+
Returns stripped parts; either side may be empty for open-ended ranges.
|
|
136
|
+
"""
|
|
137
|
+
if "," not in range_str:
|
|
138
|
+
msg = f"Invalid range format: {range_str!r}. Expected 'FROM,TO', 'FROM,', or ',TO'"
|
|
139
|
+
raise ValueError(msg)
|
|
140
|
+
left, right = range_str.split(",", maxsplit=1)
|
|
141
|
+
return left.strip(), right.strip()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _make_utc(dt: datetime) -> datetime:
|
|
145
|
+
"""Ensure a datetime is UTC-aware. Naive datetimes are assumed UTC."""
|
|
146
|
+
if dt.tzinfo is None:
|
|
147
|
+
return dt.replace(tzinfo=timezone.utc)
|
|
148
|
+
return dt.astimezone(timezone.utc)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class FilterDatetimeRange(BaseModel):
|
|
152
|
+
"""
|
|
153
|
+
Range filter for datetime fields, parsed from a comma-separated string.
|
|
154
|
+
|
|
155
|
+
Supported string formats (separator is ``,``)::
|
|
156
|
+
|
|
157
|
+
"2026-04-01,2026-04-30" — full range (gt + lt)
|
|
158
|
+
"2026-04-01," — from date (gt only, open end)
|
|
159
|
+
",2026-04-30" — to date (lt only, open start)
|
|
160
|
+
|
|
161
|
+
Each date part accepts ISO (``2026-04-01``, ``2026-04-01T00:00:00Z``)
|
|
162
|
+
and display format (``01.04.2026 00:00``).
|
|
163
|
+
|
|
164
|
+
After validation the model contains UTC-aware ``gt`` and/or ``lt`` fields
|
|
165
|
+
that map directly to SQLAlchemy operators via ``model_dump(exclude_none=True)``.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
gt: UTCDatetime | None = None
|
|
169
|
+
lt: UTCDatetime | None = None
|
|
170
|
+
|
|
171
|
+
@model_validator(mode="before")
|
|
172
|
+
@classmethod
|
|
173
|
+
def parse_range_string(cls, data: Any) -> Any:
|
|
174
|
+
if isinstance(data, str):
|
|
175
|
+
return cls._parse_range(data)
|
|
176
|
+
return data
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _parse_range(range_str: str) -> dict[str, datetime]:
|
|
180
|
+
range_str = range_str.strip()
|
|
181
|
+
if not range_str:
|
|
182
|
+
msg = "Range string must not be empty"
|
|
183
|
+
raise ValueError(msg)
|
|
184
|
+
|
|
185
|
+
left, right = _split_range(range_str)
|
|
186
|
+
result: dict[str, datetime] = {}
|
|
187
|
+
|
|
188
|
+
if left:
|
|
189
|
+
result["gt"] = _make_utc(_parse_range_part(left))
|
|
190
|
+
if right:
|
|
191
|
+
result["lt"] = _make_utc(_parse_range_part(right))
|
|
192
|
+
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
@model_validator(mode="after")
|
|
196
|
+
def validate_range(self) -> Self:
|
|
197
|
+
if self.gt is None and self.lt is None:
|
|
198
|
+
msg = "At least one of gt or lt must be set"
|
|
199
|
+
raise ValueError(msg)
|
|
200
|
+
if self.gt is not None and self.lt is not None and self.gt > self.lt:
|
|
201
|
+
msg = "Range start (gt) must not be after range end (lt)"
|
|
202
|
+
raise ValueError(msg)
|
|
203
|
+
return self
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class FilterNaiveDatetimeRange(BaseModel):
|
|
207
|
+
"""
|
|
208
|
+
Same as :class:`FilterDatetimeRange` but produces **naive** (timezone-unaware) datetimes.
|
|
209
|
+
|
|
210
|
+
If the input contains timezone info, it is stripped (replaced with None).
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
gt: datetime | None = None
|
|
214
|
+
lt: datetime | None = None
|
|
215
|
+
|
|
216
|
+
@model_validator(mode="before")
|
|
217
|
+
@classmethod
|
|
218
|
+
def parse_range_string(cls, data: Any) -> Any:
|
|
219
|
+
if isinstance(data, str):
|
|
220
|
+
return cls._parse_range(data)
|
|
221
|
+
return data
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def _parse_range(range_str: str) -> dict[str, datetime]:
|
|
225
|
+
range_str = range_str.strip()
|
|
226
|
+
if not range_str:
|
|
227
|
+
msg = "Range string must not be empty"
|
|
228
|
+
raise ValueError(msg)
|
|
229
|
+
|
|
230
|
+
left, right = _split_range(range_str)
|
|
231
|
+
result: dict[str, datetime] = {}
|
|
232
|
+
|
|
233
|
+
if left:
|
|
234
|
+
result["gt"] = _parse_range_part(left).replace(tzinfo=None)
|
|
235
|
+
if right:
|
|
236
|
+
result["lt"] = _parse_range_part(right).replace(tzinfo=None)
|
|
237
|
+
|
|
238
|
+
return result
|
|
239
|
+
|
|
240
|
+
@model_validator(mode="after")
|
|
241
|
+
def validate_range(self) -> Self:
|
|
242
|
+
if self.gt is None and self.lt is None:
|
|
243
|
+
msg = "At least one of gt or lt must be set"
|
|
244
|
+
raise ValueError(msg)
|
|
245
|
+
if self.gt is not None and self.lt is not None and self.gt > self.lt:
|
|
246
|
+
msg = "Range start (gt) must not be after range end (lt)"
|
|
247
|
+
raise ValueError(msg)
|
|
248
|
+
return self
|
|
249
|
+
|
|
250
|
+
|
|
100
251
|
class FilterBool(BaseModel):
|
|
101
252
|
eq: bool | None = None
|
|
102
253
|
ne: bool | None = None
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
2
|
+
|
|
3
|
+
from .columns import ColumnMeta, TableMeta
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Pagination(BaseModel):
|
|
7
|
+
page: int | None = Field(default=1, ge=1)
|
|
8
|
+
per_page: int | None = Field(default=10, ge=1)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PaginationR(Pagination):
|
|
12
|
+
total: int
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GetAllPagination[T](BaseModel):
|
|
16
|
+
table: TableMeta | None = None
|
|
17
|
+
columns: list[ColumnMeta] | None = None
|
|
18
|
+
data: list[T]
|
|
19
|
+
pagination: PaginationR | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LookupMeta(BaseModel):
|
|
23
|
+
"""Metadata for a lookup dictionary response.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
name: Stable identifier used as the cache key on the frontend
|
|
27
|
+
(e.g. ``"lead_email_types"``). Must match
|
|
28
|
+
``ColumnMeta.lookup_dict`` values.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
model_config = ConfigDict(extra="forbid")
|
|
32
|
+
|
|
33
|
+
name: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LookupResponse[T](BaseModel):
|
|
37
|
+
"""Standard response wrapper for lookup (``_lkp``) endpoints.
|
|
38
|
+
|
|
39
|
+
Mirrors the ``{meta, data}`` pattern the frontend uses for caching::
|
|
40
|
+
|
|
41
|
+
store.lookups["lead_email_types"] = {meta: {name: "lead_email_types"}, data: [...]}
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
meta: Lookup metadata (name used as cache key).
|
|
45
|
+
data: List of lookup records.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
meta: LookupMeta
|
|
49
|
+
data: list[T]
|