sqlmodel-ext 0.4.2__py3-none-any.whl → 0.5.0__py3-none-any.whl

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 (40) hide show
  1. sqlmodel_ext/__init__.py +196 -118
  2. sqlmodel_ext/_type_unwrap.py +109 -0
  3. sqlmodel_ext/base.py +750 -248
  4. sqlmodel_ext/check_derived.py +565 -0
  5. sqlmodel_ext/constants.py +76 -0
  6. sqlmodel_ext/derived_decls.py +818 -0
  7. sqlmodel_ext/field_types/__init__.py +497 -48
  8. sqlmodel_ext/field_types/_internal/path.py +2 -2
  9. sqlmodel_ext/field_types/dialects/postgresql/__init__.py +16 -7
  10. sqlmodel_ext/field_types/dialects/postgresql/array.py +218 -218
  11. sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +140 -51
  12. sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +6 -6
  13. sqlmodel_ext/field_types/ip_address.py +41 -5
  14. sqlmodel_ext/field_types/mixins/__init__.py +1 -1
  15. sqlmodel_ext/mixins/__init__.py +63 -23
  16. sqlmodel_ext/mixins/_uuid.py +69 -0
  17. sqlmodel_ext/mixins/cached_table.py +750 -145
  18. sqlmodel_ext/mixins/constants.py +17 -0
  19. sqlmodel_ext/mixins/exceptions.py +84 -0
  20. sqlmodel_ext/mixins/migration_cache_invalidation.py +243 -0
  21. sqlmodel_ext/mixins/mixin_table_scan.py +96 -0
  22. sqlmodel_ext/mixins/optimistic_lock.py +92 -16
  23. sqlmodel_ext/mixins/polymorphic.py +48 -15
  24. sqlmodel_ext/mixins/relation_preload.py +505 -25
  25. sqlmodel_ext/mixins/resource_quota.py +315 -0
  26. sqlmodel_ext/mixins/table.py +836 -148
  27. sqlmodel_ext/mixins/trgm_searchable.py +230 -0
  28. sqlmodel_ext/pagination.py +154 -23
  29. sqlmodel_ext/py.typed +0 -0
  30. sqlmodel_ext/relation_load_checker.py +3028 -492
  31. sqlmodel_ext/select.py +466 -0
  32. sqlmodel_ext/session.py +606 -105
  33. sqlmodel_ext/unset.py +191 -0
  34. sqlmodel_ext-0.5.0.dist-info/METADATA +1933 -0
  35. sqlmodel_ext-0.5.0.dist-info/RECORD +49 -0
  36. {sqlmodel_ext-0.4.2.dist-info → sqlmodel_ext-0.5.0.dist-info}/WHEEL +1 -1
  37. sqlmodel_ext-0.5.0.dist-info/entry_points.txt +2 -0
  38. sqlmodel_ext-0.4.2.dist-info/METADATA +0 -1518
  39. sqlmodel_ext-0.4.2.dist-info/RECORD +0 -34
  40. {sqlmodel_ext-0.4.2.dist-info → sqlmodel_ext-0.5.0.dist-info}/licenses/LICENSE +0 -0
sqlmodel_ext/__init__.py CHANGED
@@ -20,154 +20,232 @@ Quick start::
20
20
  user = await user.save(session)
21
21
  users = await User.get(session, fetch_mode="all")
22
22
  """
23
- __version__ = "0.4.2"
23
+ __version__ = "0.5.0"
24
+
25
+ # Every re-export below uses the redundant ``X as X`` form: the package ships
26
+ # ``py.typed``, and type checkers treat a plain ``from m import X`` in a typed
27
+ # package as private (consumers would get ``reportPrivateImportUsage``).
24
28
 
25
29
  # Base
26
- from sqlmodel_ext.base import SQLModelBase, ExtraIgnoreModelBase, CustomTableArg
30
+ from sqlmodel_ext.base import (
31
+ SQLModelBase as SQLModelBase,
32
+ ExtraIgnoreModelBase as ExtraIgnoreModelBase,
33
+ CustomTableArg as CustomTableArg,
34
+ SQLModelExtConfig as SQLModelExtConfig,
35
+ )
36
+
37
+ # Tri-state semantics ("not provided" vs null) and the optional wire value
38
+ from sqlmodel_ext.unset import Unset as Unset, OMITTED_SENTINEL as OMITTED_SENTINEL
27
39
 
28
- # Enhanced session (cache-aware commit/reset/refresh/execute)
29
- from sqlmodel_ext.session import AsyncSession
40
+ # Field metadata markers
41
+ from sqlmodel_ext.constants import EXCLUDE_IF_NONE as EXCLUDE_IF_NONE
42
+
43
+ # ``sqlmodel.select`` with overloads for 5-9 column projections (same object at runtime)
44
+ from sqlmodel_ext.select import select as select
45
+
46
+ # Enhanced session (cache-aware commit/reset/refresh/execute, REPEATABLE READ retries)
47
+ from sqlmodel_ext.session import (
48
+ AsyncSession as AsyncSession,
49
+ SessionFactory as SessionFactory,
50
+ RepeatableReadSnapshotConflictError as RepeatableReadSnapshotConflictError,
51
+ SerializationRetryExhaustedError as SerializationRetryExhaustedError,
52
+ )
30
53
 
31
54
  # Exceptions
32
- from sqlmodel_ext._exceptions import RecordNotFoundError
55
+ from sqlmodel_ext._exceptions import RecordNotFoundError as RecordNotFoundError
33
56
 
34
57
  # Pagination
35
58
  from sqlmodel_ext.pagination import (
36
- ListResponse,
37
- TimeFilterRequest,
38
- PaginationRequest,
39
- TableViewRequest,
59
+ DEFAULT_PAGE_SIZE as DEFAULT_PAGE_SIZE,
60
+ MAX_PAGE_SIZE as MAX_PAGE_SIZE,
61
+ MAX_TABLE_VIEW_OFFSET as MAX_TABLE_VIEW_OFFSET,
62
+ ListResponse as ListResponse,
63
+ TimeFilterRequest as TimeFilterRequest,
64
+ PageWindowRequest as PageWindowRequest,
65
+ PaginationRequest as PaginationRequest,
66
+ TableViewRequest as TableViewRequest,
40
67
  )
41
68
 
42
69
  # Mixins
43
70
  from sqlmodel_ext.mixins import (
44
71
  # Table
45
- SESSION_FOR_UPDATE_KEY,
46
- TableBaseMixin,
47
- UUIDTableBaseMixin,
48
- rel,
49
- cond,
72
+ SESSION_FOR_UPDATE_KEY as SESSION_FOR_UPDATE_KEY,
73
+ SESSION_REPEATABLE_READ_KEY as SESSION_REPEATABLE_READ_KEY,
74
+ GroupSumRow as GroupSumRow,
75
+ TableBaseMixin as TableBaseMixin,
76
+ UUIDTableBaseMixin as UUIDTableBaseMixin,
77
+ rel as rel,
78
+ cond as cond,
79
+ uuid7 as uuid7,
80
+ # Delete / keyset exceptions
81
+ FK_DELETE_RESTRICT_FALLBACK_MESSAGE as FK_DELETE_RESTRICT_FALLBACK_MESSAGE,
82
+ ResourceReferencedError as ResourceReferencedError,
83
+ KeysetCursorError as KeysetCursorError,
84
+ KeysetCursorInvalidError as KeysetCursorInvalidError,
85
+ KeysetCursorUnsupportedError as KeysetCursorUnsupportedError,
50
86
  # Polymorphic
51
- PolymorphicBaseMixin,
52
- AutoPolymorphicIdentityMixin,
53
- DeferredIndex,
54
- create_subclass_id_mixin,
55
- register_sti_columns_for_all_subclasses,
56
- register_sti_column_properties_for_all_subclasses,
87
+ PolymorphicBaseMixin as PolymorphicBaseMixin,
88
+ AutoPolymorphicIdentityMixin as AutoPolymorphicIdentityMixin,
89
+ DeferredIndex as DeferredIndex,
90
+ create_subclass_id_mixin as create_subclass_id_mixin,
91
+ register_sti_columns_for_all_subclasses as register_sti_columns_for_all_subclasses,
92
+ register_sti_column_properties_for_all_subclasses as register_sti_column_properties_for_all_subclasses,
57
93
  # Optimistic Lock
58
- OptimisticLockMixin,
59
- OptimisticLockError,
94
+ OptimisticLockMixin as OptimisticLockMixin,
95
+ OptimisticLockError as OptimisticLockError,
60
96
  # Relation Preload
61
- RelationPreloadMixin,
62
- requires_relations,
63
- requires_for_update,
97
+ RelationPreloadMixin as RelationPreloadMixin,
98
+ requires_relations as requires_relations,
99
+ requires_for_update as requires_for_update,
100
+ requires_locked_param as requires_locked_param,
101
+ requires_read_committed as requires_read_committed,
102
+ requires_repeatable_read as requires_repeatable_read,
103
+ validate_locked_instances as validate_locked_instances,
64
104
  # Cached Table
65
- CachedTableBaseMixin,
105
+ CachedTableBaseMixin as CachedTableBaseMixin,
106
+ CACHE_TTL_HOT as CACHE_TTL_HOT,
107
+ CACHE_TTL_WARM as CACHE_TTL_WARM,
108
+ CACHE_TTL_COLD as CACHE_TTL_COLD,
109
+ collect_migration_invalidation_tasks as collect_migration_invalidation_tasks,
110
+ run_pending_migration_cache_invalidations as run_pending_migration_cache_invalidations,
66
111
  # Info Response DTOs
67
- IntIdInfoMixin,
68
- UUIDIdInfoMixin,
69
- DatetimeInfoMixin,
70
- IntIdDatetimeInfoMixin,
71
- UUIDIdDatetimeInfoMixin,
112
+ IntIdInfoMixin as IntIdInfoMixin,
113
+ UUIDIdInfoMixin as UUIDIdInfoMixin,
114
+ DatetimeInfoMixin as DatetimeInfoMixin,
115
+ IntIdDatetimeInfoMixin as IntIdDatetimeInfoMixin,
116
+ UUIDIdDatetimeInfoMixin as UUIDIdDatetimeInfoMixin,
117
+ # Resource quota
118
+ ResourceQuotaMixin as ResourceQuotaMixin,
119
+ QuotaExceededError as QuotaExceededError,
120
+ QuotaOwnerNotFoundError as QuotaOwnerNotFoundError,
121
+ CallerDidNotCommitError as CallerDidNotCommitError,
122
+ # Mixin table scan
123
+ MixinTableScanMixin as MixinTableScanMixin,
124
+ # Trigram search (PostgreSQL pg_trgm)
125
+ BIGRAM_FUNCTION_SQL as BIGRAM_FUNCTION_SQL,
126
+ TrgmSearchableMixin as TrgmSearchableMixin,
127
+ TrgmSearchRequest as TrgmSearchRequest,
72
128
  )
73
129
 
74
130
  # Field Types
75
131
  from sqlmodel_ext.field_types import (
76
132
  # Path types
77
- DirectoryPathType,
78
- FilePathType,
133
+ DirectoryPathType as DirectoryPathType,
134
+ FilePathType as FilePathType,
79
135
  # String constraints
80
- Str16,
81
- Str24,
82
- Str32,
83
- Str36,
84
- Str48,
85
- Str64,
86
- Str100,
87
- Str128,
88
- Str255,
89
- Str256,
90
- Str500,
91
- Str512,
92
- Str2048,
93
- Text1K,
94
- Text1024,
95
- Text2K,
96
- Text2500,
97
- Text3K,
98
- Text5K,
99
- Text8K,
100
- Text10K,
101
- Text16K,
102
- Text32K,
103
- Text48K,
104
- Text60K,
105
- Text64K,
106
- Text100K,
107
- Text128K,
108
- Text1M,
109
- NonEmptyStr64,
110
- NonEmptyStr128,
111
- NonEmptyStr256,
112
- NonEmptyStrippedStr64,
113
- NonEmptyStrippedStr128,
114
- NonEmptyStrippedStr256,
115
- Sha256Hex,
116
- BCP47LanguageCode,
136
+ max_length_of as max_length_of,
137
+ Str1 as Str1,
138
+ Str16 as Str16,
139
+ Str24 as Str24,
140
+ Str32 as Str32,
141
+ Str36 as Str36,
142
+ Str48 as Str48,
143
+ Str64 as Str64,
144
+ Str100 as Str100,
145
+ Str128 as Str128,
146
+ Str255 as Str255,
147
+ Str256 as Str256,
148
+ Str500 as Str500,
149
+ Str512 as Str512,
150
+ Str2048 as Str2048,
151
+ Text1K as Text1K,
152
+ Text1024 as Text1024,
153
+ Text2K as Text2K,
154
+ Text2500 as Text2500,
155
+ Text3K as Text3K,
156
+ Text3072 as Text3072,
157
+ Text4K as Text4K,
158
+ Text5K as Text5K,
159
+ Text8K as Text8K,
160
+ Text10K as Text10K,
161
+ Text16K as Text16K,
162
+ Text32K as Text32K,
163
+ Text48K as Text48K,
164
+ Text60K as Text60K,
165
+ Text64K as Text64K,
166
+ Text100K as Text100K,
167
+ Text128K as Text128K,
168
+ Text1M as Text1M,
169
+ NonEmptyStr64 as NonEmptyStr64,
170
+ NonEmptyStr128 as NonEmptyStr128,
171
+ NonEmptyStr256 as NonEmptyStr256,
172
+ NonEmptyStrippedStr32 as NonEmptyStrippedStr32,
173
+ NonEmptyStrippedStr64 as NonEmptyStrippedStr64,
174
+ NonEmptyStrippedStr128 as NonEmptyStrippedStr128,
175
+ NonEmptyStrippedStr256 as NonEmptyStrippedStr256,
176
+ Sha256Hex as Sha256Hex,
177
+ BCP47LanguageCode as BCP47LanguageCode,
178
+ SingleLineStr64 as SingleLineStr64,
179
+ SearchQueryStr64 as SearchQueryStr64,
180
+ HttpHeaderName as HttpHeaderName,
117
181
  # Numeric constraints
118
- INT32_MAX,
119
- INT64_MAX,
120
- JS_MAX_SAFE_INTEGER,
121
- Port,
122
- Percentage,
123
- PositiveInt,
124
- NonNegativeInt,
125
- PositiveBigInt,
126
- NonNegativeBigInt,
127
- PositiveFloat,
128
- NonNegativeFloat,
182
+ INT32_MIN as INT32_MIN,
183
+ INT32_MAX as INT32_MAX,
184
+ INT64_MAX as INT64_MAX,
185
+ JS_MAX_SAFE_INTEGER as JS_MAX_SAFE_INTEGER,
186
+ Port as Port,
187
+ Percentage as Percentage,
188
+ PositiveInt as PositiveInt,
189
+ NonNegativeInt as NonNegativeInt,
190
+ PositiveBigInt as PositiveBigInt,
191
+ NonNegativeBigInt as NonNegativeBigInt,
192
+ SignedBigInt as SignedBigInt,
193
+ PositiveFloat as PositiveFloat,
194
+ NonNegativeFloat as NonNegativeFloat,
129
195
  # Decimal constraints (NUMERIC(p, s) + sign + JSON-string serialization)
130
- SignedDecimal38_18,
131
- NonNegativeDecimal38_18,
132
- PositiveDecimal38_18,
133
- OptionalNonNegativeDecimal38_18,
134
- SignedDecimal20_10,
135
- NonNegativeDecimal20_10,
136
- OptionalNonNegativeDecimal20_10,
196
+ SignedDecimal38_18 as SignedDecimal38_18,
197
+ NonNegativeDecimal38_18 as NonNegativeDecimal38_18,
198
+ PositiveDecimal38_18 as PositiveDecimal38_18,
199
+ OptionalNonNegativeDecimal38_18 as OptionalNonNegativeDecimal38_18,
200
+ OptionalSignedDecimal38_18 as OptionalSignedDecimal38_18,
201
+ SignedWriteDecimal38_18 as SignedWriteDecimal38_18,
202
+ NonNegativeWriteDecimal38_18 as NonNegativeWriteDecimal38_18,
203
+ PositiveWriteDecimal38_18 as PositiveWriteDecimal38_18,
204
+ OptionalNonNegativeWriteDecimal38_18 as OptionalNonNegativeWriteDecimal38_18,
205
+ OptionalSignedWriteDecimal38_18 as OptionalSignedWriteDecimal38_18,
206
+ SignedSumDecimal38_18 as SignedSumDecimal38_18,
207
+ DECIMAL_38_18_COLUMN_DIGITS as DECIMAL_38_18_COLUMN_DIGITS,
208
+ DECIMAL_38_18_WRITE_DIGITS as DECIMAL_38_18_WRITE_DIGITS,
209
+ DECIMAL_38_18_PLACES as DECIMAL_38_18_PLACES,
210
+ SignedDecimal20_10 as SignedDecimal20_10,
211
+ NonNegativeDecimal20_10 as NonNegativeDecimal20_10,
212
+ OptionalNonNegativeDecimal20_10 as OptionalNonNegativeDecimal20_10,
213
+ NullableNonNegativeDecimal20_10 as NullableNonNegativeDecimal20_10,
137
214
  # Bounded-length list aliases
138
- List,
139
- List1,
140
- List2,
141
- List3,
142
- List7,
143
- List10,
144
- List16,
145
- List20,
146
- List32,
147
- List40,
148
- List50,
149
- List64,
150
- List100,
151
- List128,
152
- List200,
153
- List256,
154
- List1024,
215
+ List as List,
216
+ List1 as List1,
217
+ List2 as List2,
218
+ List3 as List3,
219
+ List7 as List7,
220
+ List10 as List10,
221
+ List16 as List16,
222
+ List20 as List20,
223
+ List32 as List32,
224
+ List40 as List40,
225
+ List50 as List50,
226
+ List64 as List64,
227
+ List100 as List100,
228
+ List128 as List128,
229
+ List200 as List200,
230
+ List256 as List256,
231
+ List1024 as List1024,
155
232
  # Custom types
156
- IPAddress,
157
- Url,
158
- HttpUrl,
159
- WebSocketUrl,
160
- SafeHttpUrl,
161
- UnsafeURLError,
162
- validate_not_private_host,
163
- ModuleNameMixin,
233
+ IPAddress as IPAddress,
234
+ ClientIPAddress as ClientIPAddress,
235
+ Url as Url,
236
+ HttpUrl as HttpUrl,
237
+ WebSocketUrl as WebSocketUrl,
238
+ SafeHttpUrl as SafeHttpUrl,
239
+ UnsafeURLError as UnsafeURLError,
240
+ validate_not_private_host as validate_not_private_host,
241
+ ModuleNameMixin as ModuleNameMixin,
164
242
  )
165
243
 
166
244
  # Relation Load Checker (static analysis)
167
245
  from sqlmodel_ext.relation_load_checker import (
168
- RelationLoadChecker,
169
- RelationLoadWarning,
170
- RelationLoadCheckMiddleware,
171
- run_model_checks,
172
- mark_app_check_completed,
246
+ RelationLoadChecker as RelationLoadChecker,
247
+ RelationLoadWarning as RelationLoadWarning,
248
+ RelationLoadCheckMiddleware as RelationLoadCheckMiddleware,
249
+ run_model_checks as run_model_checks,
250
+ mark_app_check_completed as mark_app_check_completed,
173
251
  )
@@ -0,0 +1,109 @@
1
+ """
2
+ Type-annotation unwrapping helpers (shared mechanical operations).
3
+
4
+ Extracts the inner concrete type from wrappers such as ``Annotated[X, ...]``,
5
+ ``X | None`` or ``ListResponse[X]``. Pure type-system operations with no domain
6
+ semantics and no state, hence plain module-level functions.
7
+
8
+ **Why a dedicated unwrapper is needed**: Pydantic v2 concrete generics (e.g.
9
+ ``ListResponse[FooResponse]``) are fully concrete ``ModelMetaclass`` instances;
10
+ ``typing.get_origin()`` / ``typing.get_args()`` **both return empty values** for
11
+ them. The real generic arguments live in
12
+ ``__pydantic_generic_metadata__['args']``. Using only the standard ``typing``
13
+ API would make every generic response model opaque.
14
+
15
+ Consumer: :mod:`sqlmodel_ext.relation_load_checker` (RLC005 / RLC012 drill down
16
+ from a ``response_model`` to its DTO / ORM classes).
17
+ """
18
+ import types
19
+ import typing
20
+ from typing import Annotated, Any, Union
21
+
22
+
23
+ def unwrap_to_class(hint: Any) -> type | None:
24
+ """
25
+ Extract the actual class from a type annotation.
26
+
27
+ Handles ``Annotated[X, ...]``, ``X | None``, ``Optional[X]`` and returns the
28
+ innermost concrete type. Generic containers are **not** drilled into
29
+ (``ListResponse[X]`` returns the container class itself); use
30
+ :func:`unwrap_generic_to_dto_class` for that.
31
+
32
+ :param hint: any type annotation object
33
+ :returns: the actual type, or ``None`` if it cannot be extracted
34
+ """
35
+ origin = typing.get_origin(hint)
36
+
37
+ # Annotated[X, ...] -> unwrap X
38
+ if origin is Annotated:
39
+ args = typing.get_args(hint)
40
+ if args:
41
+ return unwrap_to_class(args[0])
42
+
43
+ # X | None (UnionType) or Optional[X] (Union[X, None])
44
+ if origin is types.UnionType or origin is Union: # pyright: ignore[reportDeprecated]
45
+ args = typing.get_args(hint)
46
+ non_none = [a for a in args if a is not type(None)]
47
+ if len(non_none) == 1:
48
+ return unwrap_to_class(non_none[0])
49
+
50
+ # Plain class
51
+ if isinstance(hint, type):
52
+ return hint
53
+
54
+ return None
55
+
56
+
57
+ def get_pydantic_generic_args(hint: Any) -> tuple[Any, ...]:
58
+ """
59
+ Extract the type arguments of a generic (compatible with Pydantic v2 concrete generics).
60
+
61
+ Pydantic v2's ``ListResponse[T]`` creates a fully concrete ``ModelMetaclass``
62
+ instance for which ``typing.get_origin()`` / ``typing.get_args()`` return
63
+ empty values. The real generic arguments are in
64
+ ``__pydantic_generic_metadata__['args']``.
65
+
66
+ :param hint: any type annotation object
67
+ :returns: the generic arguments, or an empty tuple if there are none
68
+ """
69
+ # Standard typing API first
70
+ args = typing.get_args(hint)
71
+ if args:
72
+ return args
73
+ # Pydantic concrete-generic fallback
74
+ pgm = getattr(hint, '__pydantic_generic_metadata__', None)
75
+ if pgm is not None:
76
+ return pgm.get('args', ())
77
+ return ()
78
+
79
+
80
+ def unwrap_generic_to_dto_class(hint: Any) -> type | None:
81
+ """
82
+ Recursively extract a DTO class (a class with ``model_fields``) from a type annotation.
83
+
84
+ Container types (Pydantic generic models such as ``ListResponse``) are skipped
85
+ and their generic arguments are drilled into. For a union annotation the
86
+ **first** member that yields a DTO wins; callers that need every member must
87
+ iterate ``get_pydantic_generic_args`` / ``typing.get_args`` themselves.
88
+
89
+ :param hint: any type annotation object
90
+ :returns: the inner DTO class, or ``None`` if it cannot be extracted
91
+ """
92
+ # Pydantic generic container? (has __pydantic_generic_metadata__ with a non-empty origin)
93
+ pgm = getattr(hint, '__pydantic_generic_metadata__', None)
94
+ is_pydantic_generic = pgm is not None and pgm.get('origin') is not None
95
+
96
+ if not is_pydantic_generic:
97
+ cls = unwrap_to_class(hint)
98
+ if cls is not None and hasattr(cls, 'model_fields'):
99
+ return cls
100
+
101
+ # Recurse into generic arguments
102
+ for arg in get_pydantic_generic_args(hint):
103
+ if arg is type(None):
104
+ continue
105
+ result = unwrap_generic_to_dto_class(arg)
106
+ if result is not None:
107
+ return result
108
+
109
+ return None