sqlmodel-ext 0.4.2__py3-none-any.whl → 0.5.1__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.
- sqlmodel_ext/__init__.py +199 -118
- sqlmodel_ext/_compat.py +4 -2
- sqlmodel_ext/_exceptions.py +1 -1
- sqlmodel_ext/_sa_type.py +20 -8
- sqlmodel_ext/_type_unwrap.py +117 -0
- sqlmodel_ext/base.py +963 -302
- sqlmodel_ext/check_derived.py +565 -0
- sqlmodel_ext/constants.py +76 -0
- sqlmodel_ext/dependencies.py +178 -0
- sqlmodel_ext/derived_decls.py +818 -0
- sqlmodel_ext/field_types/__init__.py +504 -67
- sqlmodel_ext/field_types/_internal/path.py +24 -12
- sqlmodel_ext/field_types/dialects/postgresql/__init__.py +16 -7
- sqlmodel_ext/field_types/dialects/postgresql/array.py +232 -218
- sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +140 -52
- sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +43 -27
- sqlmodel_ext/field_types/ip_address.py +41 -5
- sqlmodel_ext/field_types/mixins/__init__.py +1 -1
- sqlmodel_ext/field_types/mixins/module_name_mixin.py +2 -2
- sqlmodel_ext/field_types/url.py +6 -6
- sqlmodel_ext/mixins/__init__.py +63 -23
- sqlmodel_ext/mixins/_uuid.py +69 -0
- sqlmodel_ext/mixins/cached_table.py +969 -391
- sqlmodel_ext/mixins/constants.py +17 -0
- sqlmodel_ext/mixins/exceptions.py +84 -0
- sqlmodel_ext/mixins/migration_cache_invalidation.py +243 -0
- sqlmodel_ext/mixins/mixin_table_scan.py +96 -0
- sqlmodel_ext/mixins/optimistic_lock.py +92 -16
- sqlmodel_ext/mixins/polymorphic.py +84 -38
- sqlmodel_ext/mixins/relation_preload.py +507 -27
- sqlmodel_ext/mixins/resource_quota.py +318 -0
- sqlmodel_ext/mixins/table.py +866 -162
- sqlmodel_ext/mixins/trgm_searchable.py +230 -0
- sqlmodel_ext/pagination.py +213 -49
- sqlmodel_ext/py.typed +0 -0
- sqlmodel_ext/relation_load_checker.py +3043 -501
- sqlmodel_ext/select.py +466 -0
- sqlmodel_ext/session.py +633 -105
- sqlmodel_ext/unset.py +189 -0
- sqlmodel_ext-0.5.1.dist-info/METADATA +1934 -0
- sqlmodel_ext-0.5.1.dist-info/RECORD +50 -0
- {sqlmodel_ext-0.4.2.dist-info → sqlmodel_ext-0.5.1.dist-info}/WHEEL +1 -1
- sqlmodel_ext-0.5.1.dist-info/entry_points.txt +2 -0
- sqlmodel_ext-0.4.2.dist-info/METADATA +0 -1518
- sqlmodel_ext-0.4.2.dist-info/RECORD +0 -34
- {sqlmodel_ext-0.4.2.dist-info → sqlmodel_ext-0.5.1.dist-info}/licenses/LICENSE +0 -0
sqlmodel_ext/__init__.py
CHANGED
|
@@ -20,154 +20,235 @@ Quick start::
|
|
|
20
20
|
user = await user.save(session)
|
|
21
21
|
users = await User.get(session, fetch_mode="all")
|
|
22
22
|
"""
|
|
23
|
-
__version__ = "0.
|
|
23
|
+
__version__ = "0.5.1"
|
|
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
|
|
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
|
|
39
|
+
|
|
40
|
+
# Field metadata markers
|
|
41
|
+
from sqlmodel_ext.constants import EXCLUDE_IF_NONE as EXCLUDE_IF_NONE
|
|
27
42
|
|
|
28
|
-
#
|
|
29
|
-
from sqlmodel_ext.
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
|
69
|
+
# FastAPI dependency for query-parameter DTOs (cross-field errors -> 422; requires the `fastapi` extra when called)
|
|
70
|
+
from sqlmodel_ext.dependencies import query_dependency as query_dependency
|
|
71
|
+
|
|
42
72
|
# Mixins
|
|
43
73
|
from sqlmodel_ext.mixins import (
|
|
44
74
|
# Table
|
|
45
|
-
SESSION_FOR_UPDATE_KEY,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
75
|
+
SESSION_FOR_UPDATE_KEY as SESSION_FOR_UPDATE_KEY,
|
|
76
|
+
SESSION_REPEATABLE_READ_KEY as SESSION_REPEATABLE_READ_KEY,
|
|
77
|
+
GroupSumRow as GroupSumRow,
|
|
78
|
+
TableBaseMixin as TableBaseMixin,
|
|
79
|
+
UUIDTableBaseMixin as UUIDTableBaseMixin,
|
|
80
|
+
rel as rel,
|
|
81
|
+
cond as cond,
|
|
82
|
+
uuid7 as uuid7,
|
|
83
|
+
# Delete / keyset exceptions
|
|
84
|
+
FK_DELETE_RESTRICT_FALLBACK_MESSAGE as FK_DELETE_RESTRICT_FALLBACK_MESSAGE,
|
|
85
|
+
ResourceReferencedError as ResourceReferencedError,
|
|
86
|
+
KeysetCursorError as KeysetCursorError,
|
|
87
|
+
KeysetCursorInvalidError as KeysetCursorInvalidError,
|
|
88
|
+
KeysetCursorUnsupportedError as KeysetCursorUnsupportedError,
|
|
50
89
|
# 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,
|
|
90
|
+
PolymorphicBaseMixin as PolymorphicBaseMixin,
|
|
91
|
+
AutoPolymorphicIdentityMixin as AutoPolymorphicIdentityMixin,
|
|
92
|
+
DeferredIndex as DeferredIndex,
|
|
93
|
+
create_subclass_id_mixin as create_subclass_id_mixin,
|
|
94
|
+
register_sti_columns_for_all_subclasses as register_sti_columns_for_all_subclasses,
|
|
95
|
+
register_sti_column_properties_for_all_subclasses as register_sti_column_properties_for_all_subclasses,
|
|
57
96
|
# Optimistic Lock
|
|
58
|
-
OptimisticLockMixin,
|
|
59
|
-
OptimisticLockError,
|
|
97
|
+
OptimisticLockMixin as OptimisticLockMixin,
|
|
98
|
+
OptimisticLockError as OptimisticLockError,
|
|
60
99
|
# Relation Preload
|
|
61
|
-
RelationPreloadMixin,
|
|
62
|
-
requires_relations,
|
|
63
|
-
requires_for_update,
|
|
100
|
+
RelationPreloadMixin as RelationPreloadMixin,
|
|
101
|
+
requires_relations as requires_relations,
|
|
102
|
+
requires_for_update as requires_for_update,
|
|
103
|
+
requires_locked_param as requires_locked_param,
|
|
104
|
+
requires_read_committed as requires_read_committed,
|
|
105
|
+
requires_repeatable_read as requires_repeatable_read,
|
|
106
|
+
validate_locked_instances as validate_locked_instances,
|
|
64
107
|
# Cached Table
|
|
65
|
-
CachedTableBaseMixin,
|
|
108
|
+
CachedTableBaseMixin as CachedTableBaseMixin,
|
|
109
|
+
CACHE_TTL_HOT as CACHE_TTL_HOT,
|
|
110
|
+
CACHE_TTL_WARM as CACHE_TTL_WARM,
|
|
111
|
+
CACHE_TTL_COLD as CACHE_TTL_COLD,
|
|
112
|
+
collect_migration_invalidation_tasks as collect_migration_invalidation_tasks,
|
|
113
|
+
run_pending_migration_cache_invalidations as run_pending_migration_cache_invalidations,
|
|
66
114
|
# Info Response DTOs
|
|
67
|
-
IntIdInfoMixin,
|
|
68
|
-
UUIDIdInfoMixin,
|
|
69
|
-
DatetimeInfoMixin,
|
|
70
|
-
IntIdDatetimeInfoMixin,
|
|
71
|
-
UUIDIdDatetimeInfoMixin,
|
|
115
|
+
IntIdInfoMixin as IntIdInfoMixin,
|
|
116
|
+
UUIDIdInfoMixin as UUIDIdInfoMixin,
|
|
117
|
+
DatetimeInfoMixin as DatetimeInfoMixin,
|
|
118
|
+
IntIdDatetimeInfoMixin as IntIdDatetimeInfoMixin,
|
|
119
|
+
UUIDIdDatetimeInfoMixin as UUIDIdDatetimeInfoMixin,
|
|
120
|
+
# Resource quota
|
|
121
|
+
ResourceQuotaMixin as ResourceQuotaMixin,
|
|
122
|
+
QuotaExceededError as QuotaExceededError,
|
|
123
|
+
QuotaOwnerNotFoundError as QuotaOwnerNotFoundError,
|
|
124
|
+
CallerDidNotCommitError as CallerDidNotCommitError,
|
|
125
|
+
# Mixin table scan
|
|
126
|
+
MixinTableScanMixin as MixinTableScanMixin,
|
|
127
|
+
# Trigram search (PostgreSQL pg_trgm)
|
|
128
|
+
BIGRAM_FUNCTION_SQL as BIGRAM_FUNCTION_SQL,
|
|
129
|
+
TrgmSearchableMixin as TrgmSearchableMixin,
|
|
130
|
+
TrgmSearchRequest as TrgmSearchRequest,
|
|
72
131
|
)
|
|
73
132
|
|
|
74
133
|
# Field Types
|
|
75
134
|
from sqlmodel_ext.field_types import (
|
|
76
135
|
# Path types
|
|
77
|
-
DirectoryPathType,
|
|
78
|
-
FilePathType,
|
|
136
|
+
DirectoryPathType as DirectoryPathType,
|
|
137
|
+
FilePathType as FilePathType,
|
|
79
138
|
# String constraints
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
139
|
+
max_length_of as max_length_of,
|
|
140
|
+
Str1 as Str1,
|
|
141
|
+
Str16 as Str16,
|
|
142
|
+
Str24 as Str24,
|
|
143
|
+
Str32 as Str32,
|
|
144
|
+
Str36 as Str36,
|
|
145
|
+
Str48 as Str48,
|
|
146
|
+
Str64 as Str64,
|
|
147
|
+
Str100 as Str100,
|
|
148
|
+
Str128 as Str128,
|
|
149
|
+
Str255 as Str255,
|
|
150
|
+
Str256 as Str256,
|
|
151
|
+
Str500 as Str500,
|
|
152
|
+
Str512 as Str512,
|
|
153
|
+
Str2048 as Str2048,
|
|
154
|
+
Text1K as Text1K,
|
|
155
|
+
Text1024 as Text1024,
|
|
156
|
+
Text2K as Text2K,
|
|
157
|
+
Text2500 as Text2500,
|
|
158
|
+
Text3K as Text3K,
|
|
159
|
+
Text3072 as Text3072,
|
|
160
|
+
Text4K as Text4K,
|
|
161
|
+
Text5K as Text5K,
|
|
162
|
+
Text8K as Text8K,
|
|
163
|
+
Text10K as Text10K,
|
|
164
|
+
Text16K as Text16K,
|
|
165
|
+
Text32K as Text32K,
|
|
166
|
+
Text48K as Text48K,
|
|
167
|
+
Text60K as Text60K,
|
|
168
|
+
Text64K as Text64K,
|
|
169
|
+
Text100K as Text100K,
|
|
170
|
+
Text128K as Text128K,
|
|
171
|
+
Text1M as Text1M,
|
|
172
|
+
NonEmptyStr64 as NonEmptyStr64,
|
|
173
|
+
NonEmptyStr128 as NonEmptyStr128,
|
|
174
|
+
NonEmptyStr256 as NonEmptyStr256,
|
|
175
|
+
NonEmptyStrippedStr32 as NonEmptyStrippedStr32,
|
|
176
|
+
NonEmptyStrippedStr64 as NonEmptyStrippedStr64,
|
|
177
|
+
NonEmptyStrippedStr128 as NonEmptyStrippedStr128,
|
|
178
|
+
NonEmptyStrippedStr256 as NonEmptyStrippedStr256,
|
|
179
|
+
Sha256Hex as Sha256Hex,
|
|
180
|
+
BCP47LanguageCode as BCP47LanguageCode,
|
|
181
|
+
SingleLineStr64 as SingleLineStr64,
|
|
182
|
+
SearchQueryStr64 as SearchQueryStr64,
|
|
183
|
+
HttpHeaderName as HttpHeaderName,
|
|
117
184
|
# Numeric constraints
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
185
|
+
INT32_MIN as INT32_MIN,
|
|
186
|
+
INT32_MAX as INT32_MAX,
|
|
187
|
+
INT64_MAX as INT64_MAX,
|
|
188
|
+
JS_MAX_SAFE_INTEGER as JS_MAX_SAFE_INTEGER,
|
|
189
|
+
Port as Port,
|
|
190
|
+
Percentage as Percentage,
|
|
191
|
+
PositiveInt as PositiveInt,
|
|
192
|
+
NonNegativeInt as NonNegativeInt,
|
|
193
|
+
PositiveBigInt as PositiveBigInt,
|
|
194
|
+
NonNegativeBigInt as NonNegativeBigInt,
|
|
195
|
+
SignedBigInt as SignedBigInt,
|
|
196
|
+
PositiveFloat as PositiveFloat,
|
|
197
|
+
NonNegativeFloat as NonNegativeFloat,
|
|
129
198
|
# Decimal constraints (NUMERIC(p, s) + sign + JSON-string serialization)
|
|
130
|
-
SignedDecimal38_18,
|
|
131
|
-
NonNegativeDecimal38_18,
|
|
132
|
-
PositiveDecimal38_18,
|
|
133
|
-
OptionalNonNegativeDecimal38_18,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
199
|
+
SignedDecimal38_18 as SignedDecimal38_18,
|
|
200
|
+
NonNegativeDecimal38_18 as NonNegativeDecimal38_18,
|
|
201
|
+
PositiveDecimal38_18 as PositiveDecimal38_18,
|
|
202
|
+
OptionalNonNegativeDecimal38_18 as OptionalNonNegativeDecimal38_18,
|
|
203
|
+
OptionalSignedDecimal38_18 as OptionalSignedDecimal38_18,
|
|
204
|
+
SignedWriteDecimal38_18 as SignedWriteDecimal38_18,
|
|
205
|
+
NonNegativeWriteDecimal38_18 as NonNegativeWriteDecimal38_18,
|
|
206
|
+
PositiveWriteDecimal38_18 as PositiveWriteDecimal38_18,
|
|
207
|
+
OptionalNonNegativeWriteDecimal38_18 as OptionalNonNegativeWriteDecimal38_18,
|
|
208
|
+
OptionalSignedWriteDecimal38_18 as OptionalSignedWriteDecimal38_18,
|
|
209
|
+
SignedSumDecimal38_18 as SignedSumDecimal38_18,
|
|
210
|
+
DECIMAL_38_18_COLUMN_DIGITS as DECIMAL_38_18_COLUMN_DIGITS,
|
|
211
|
+
DECIMAL_38_18_WRITE_DIGITS as DECIMAL_38_18_WRITE_DIGITS,
|
|
212
|
+
DECIMAL_38_18_PLACES as DECIMAL_38_18_PLACES,
|
|
213
|
+
SignedDecimal20_10 as SignedDecimal20_10,
|
|
214
|
+
NonNegativeDecimal20_10 as NonNegativeDecimal20_10,
|
|
215
|
+
OptionalNonNegativeDecimal20_10 as OptionalNonNegativeDecimal20_10,
|
|
216
|
+
NullableNonNegativeDecimal20_10 as NullableNonNegativeDecimal20_10,
|
|
137
217
|
# 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,
|
|
218
|
+
List as List,
|
|
219
|
+
List1 as List1,
|
|
220
|
+
List2 as List2,
|
|
221
|
+
List3 as List3,
|
|
222
|
+
List7 as List7,
|
|
223
|
+
List10 as List10,
|
|
224
|
+
List16 as List16,
|
|
225
|
+
List20 as List20,
|
|
226
|
+
List32 as List32,
|
|
227
|
+
List40 as List40,
|
|
228
|
+
List50 as List50,
|
|
229
|
+
List64 as List64,
|
|
230
|
+
List100 as List100,
|
|
231
|
+
List128 as List128,
|
|
232
|
+
List200 as List200,
|
|
233
|
+
List256 as List256,
|
|
234
|
+
List1024 as List1024,
|
|
155
235
|
# Custom types
|
|
156
|
-
IPAddress,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
236
|
+
IPAddress as IPAddress,
|
|
237
|
+
ClientIPAddress as ClientIPAddress,
|
|
238
|
+
Url as Url,
|
|
239
|
+
HttpUrl as HttpUrl,
|
|
240
|
+
WebSocketUrl as WebSocketUrl,
|
|
241
|
+
SafeHttpUrl as SafeHttpUrl,
|
|
242
|
+
UnsafeURLError as UnsafeURLError,
|
|
243
|
+
validate_not_private_host as validate_not_private_host,
|
|
244
|
+
ModuleNameMixin as ModuleNameMixin,
|
|
164
245
|
)
|
|
165
246
|
|
|
166
247
|
# Relation Load Checker (static analysis)
|
|
167
248
|
from sqlmodel_ext.relation_load_checker import (
|
|
168
|
-
RelationLoadChecker,
|
|
169
|
-
RelationLoadWarning,
|
|
170
|
-
RelationLoadCheckMiddleware,
|
|
171
|
-
run_model_checks,
|
|
172
|
-
mark_app_check_completed,
|
|
249
|
+
RelationLoadChecker as RelationLoadChecker,
|
|
250
|
+
RelationLoadWarning as RelationLoadWarning,
|
|
251
|
+
RelationLoadCheckMiddleware as RelationLoadCheckMiddleware,
|
|
252
|
+
run_model_checks as run_model_checks,
|
|
253
|
+
mark_app_check_completed as mark_app_check_completed,
|
|
173
254
|
)
|
sqlmodel_ext/_compat.py
CHANGED
|
@@ -9,7 +9,9 @@ Import this module for side effects before using SQLModelBase.
|
|
|
9
9
|
import sys
|
|
10
10
|
|
|
11
11
|
if sys.version_info >= (3, 14):
|
|
12
|
-
|
|
12
|
+
# The checker analyzes at pythonVersion 3.12 (the lowest supported version), where this
|
|
13
|
+
# version-gated block is statically unreachable; it does run on 3.14+.
|
|
14
|
+
import annotationlib # noqa: F401 # pyright: ignore[reportUnreachable]
|
|
13
15
|
import types
|
|
14
16
|
from enum import StrEnum
|
|
15
17
|
from typing import Literal, Union, get_args, get_origin
|
|
@@ -101,7 +103,7 @@ if sys.version_info >= (3, 14):
|
|
|
101
103
|
if non_none_args:
|
|
102
104
|
first_arg = non_none_args[0]
|
|
103
105
|
# Check for Annotated SA type metadata first (e.g. Array[T] | None).
|
|
104
|
-
# Array[T] desugars to Annotated[list[T],
|
|
106
|
+
# Array[T] desugars to Annotated[list[T], ArrayTypeHandler(T)];
|
|
105
107
|
# wrapping with | None produces Union[Annotated[...], None],
|
|
106
108
|
# so the SA type must be extracted from the inner Annotated.
|
|
107
109
|
first_arg_type_name = type(first_arg).__name__
|
sqlmodel_ext/_exceptions.py
CHANGED
sqlmodel_ext/_sa_type.py
CHANGED
|
@@ -2,15 +2,27 @@
|
|
|
2
2
|
Internal helpers for extracting SQLAlchemy types from annotations.
|
|
3
3
|
|
|
4
4
|
These are used by the metaclass to inject sa_type into Field definitions.
|
|
5
|
+
|
|
6
|
+
Internal protocol, not part of the public API: the functions carry public names
|
|
7
|
+
only because :mod:`sqlmodel_ext.base` (the metaclass) imports them across module
|
|
8
|
+
boundaries; the module itself stays private (``_sa_type``).
|
|
5
9
|
"""
|
|
6
10
|
import sys
|
|
7
11
|
import typing
|
|
8
|
-
from
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from typing import Any, get_args, get_origin, get_type_hints
|
|
14
|
+
|
|
9
15
|
|
|
10
|
-
|
|
16
|
+
def _null_schema_handler(_source_type: Any) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Stand-in for pydantic's ``GetCoreSchemaHandler`` when probing a type's
|
|
19
|
+
``__get_pydantic_core_schema__`` for ``metadata['sa_type']``: the probe
|
|
20
|
+
only reads the metadata and never needs the delegated inner schema.
|
|
21
|
+
"""
|
|
22
|
+
return None
|
|
11
23
|
|
|
12
24
|
|
|
13
|
-
def
|
|
25
|
+
def extract_sa_type_from_annotation(annotation: Any) -> Any | None:
|
|
14
26
|
"""
|
|
15
27
|
Extract SQLAlchemy type from a type annotation.
|
|
16
28
|
|
|
@@ -40,7 +52,7 @@ def _extract_sa_type_from_annotation(annotation: Any) -> Any | None:
|
|
|
40
52
|
try:
|
|
41
53
|
schema = item.__get_pydantic_core_schema__(
|
|
42
54
|
annotation,
|
|
43
|
-
|
|
55
|
+
_null_schema_handler,
|
|
44
56
|
)
|
|
45
57
|
if isinstance(schema, dict) and 'metadata' in schema:
|
|
46
58
|
sa_type = schema['metadata'].get('sa_type')
|
|
@@ -54,7 +66,7 @@ def _extract_sa_type_from_annotation(annotation: Any) -> Any | None:
|
|
|
54
66
|
try:
|
|
55
67
|
schema = annotation.__get_pydantic_core_schema__(
|
|
56
68
|
annotation,
|
|
57
|
-
|
|
69
|
+
_null_schema_handler,
|
|
58
70
|
)
|
|
59
71
|
if isinstance(schema, dict) and 'metadata' in schema:
|
|
60
72
|
sa_type = schema['metadata'].get('sa_type')
|
|
@@ -66,7 +78,7 @@ def _extract_sa_type_from_annotation(annotation: Any) -> Any | None:
|
|
|
66
78
|
return None
|
|
67
79
|
|
|
68
80
|
|
|
69
|
-
def
|
|
81
|
+
def resolve_annotations(attrs: dict[str, Any]) -> tuple[
|
|
70
82
|
dict[str, Any],
|
|
71
83
|
dict[str, str],
|
|
72
84
|
Mapping[str, Any],
|
|
@@ -112,7 +124,7 @@ def _resolve_annotations(attrs: dict[str, Any]) -> tuple[
|
|
|
112
124
|
return dict(evaluated), {}, module_globals, localns
|
|
113
125
|
|
|
114
126
|
|
|
115
|
-
def
|
|
127
|
+
def evaluate_annotation_from_string(
|
|
116
128
|
field_name: str,
|
|
117
129
|
annotation_strings: dict[str, str],
|
|
118
130
|
current_type: Any,
|
|
@@ -129,7 +141,7 @@ def _evaluate_annotation_from_string(
|
|
|
129
141
|
return current_type
|
|
130
142
|
|
|
131
143
|
expr = annotation_strings.get(field_name)
|
|
132
|
-
if not expr
|
|
144
|
+
if not expr:
|
|
133
145
|
return current_type
|
|
134
146
|
|
|
135
147
|
try:
|
|
@@ -0,0 +1,117 @@
|
|
|
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, Final
|
|
21
|
+
|
|
22
|
+
# ``typing.Union`` as a runtime value: the ``get_origin()`` of ``Optional[X]`` /
|
|
23
|
+
# ``Union[X, Y]`` on Python < 3.14 (``X | Y`` has origin ``types.UnionType``).
|
|
24
|
+
# reportDeprecated targets spelling *annotations* with ``Union`` (use ``X | Y``);
|
|
25
|
+
# an identity-comparison target has no non-deprecated spelling, so the single
|
|
26
|
+
# reference lives here and every origin check in the package compares against
|
|
27
|
+
# this name (internal protocol, not part of the public API).
|
|
28
|
+
TYPING_UNION: Final = typing.Union # pyright: ignore[reportDeprecated]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def unwrap_to_class(hint: Any) -> type | None:
|
|
32
|
+
"""
|
|
33
|
+
Extract the actual class from a type annotation.
|
|
34
|
+
|
|
35
|
+
Handles ``Annotated[X, ...]``, ``X | None``, ``Optional[X]`` and returns the
|
|
36
|
+
innermost concrete type. Generic containers are **not** drilled into
|
|
37
|
+
(``ListResponse[X]`` returns the container class itself); use
|
|
38
|
+
:func:`unwrap_generic_to_dto_class` for that.
|
|
39
|
+
|
|
40
|
+
:param hint: any type annotation object
|
|
41
|
+
:returns: the actual type, or ``None`` if it cannot be extracted
|
|
42
|
+
"""
|
|
43
|
+
origin = typing.get_origin(hint)
|
|
44
|
+
|
|
45
|
+
# Annotated[X, ...] -> unwrap X
|
|
46
|
+
if origin is Annotated:
|
|
47
|
+
args = typing.get_args(hint)
|
|
48
|
+
if args:
|
|
49
|
+
return unwrap_to_class(args[0])
|
|
50
|
+
|
|
51
|
+
# X | None (UnionType) or Optional[X] (Union[X, None])
|
|
52
|
+
if origin is types.UnionType or origin is TYPING_UNION:
|
|
53
|
+
args = typing.get_args(hint)
|
|
54
|
+
non_none = [a for a in args if a is not type(None)]
|
|
55
|
+
if len(non_none) == 1:
|
|
56
|
+
return unwrap_to_class(non_none[0])
|
|
57
|
+
|
|
58
|
+
# Plain class
|
|
59
|
+
if isinstance(hint, type):
|
|
60
|
+
return hint
|
|
61
|
+
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_pydantic_generic_args(hint: Any) -> tuple[Any, ...]:
|
|
66
|
+
"""
|
|
67
|
+
Extract the type arguments of a generic (compatible with Pydantic v2 concrete generics).
|
|
68
|
+
|
|
69
|
+
Pydantic v2's ``ListResponse[T]`` creates a fully concrete ``ModelMetaclass``
|
|
70
|
+
instance for which ``typing.get_origin()`` / ``typing.get_args()`` return
|
|
71
|
+
empty values. The real generic arguments are in
|
|
72
|
+
``__pydantic_generic_metadata__['args']``.
|
|
73
|
+
|
|
74
|
+
:param hint: any type annotation object
|
|
75
|
+
:returns: the generic arguments, or an empty tuple if there are none
|
|
76
|
+
"""
|
|
77
|
+
# Standard typing API first
|
|
78
|
+
args = typing.get_args(hint)
|
|
79
|
+
if args:
|
|
80
|
+
return args
|
|
81
|
+
# Pydantic concrete-generic fallback
|
|
82
|
+
pgm = getattr(hint, '__pydantic_generic_metadata__', None)
|
|
83
|
+
if pgm is not None:
|
|
84
|
+
return pgm.get('args', ())
|
|
85
|
+
return ()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def unwrap_generic_to_dto_class(hint: Any) -> type | None:
|
|
89
|
+
"""
|
|
90
|
+
Recursively extract a DTO class (a class with ``model_fields``) from a type annotation.
|
|
91
|
+
|
|
92
|
+
Container types (Pydantic generic models such as ``ListResponse``) are skipped
|
|
93
|
+
and their generic arguments are drilled into. For a union annotation the
|
|
94
|
+
**first** member that yields a DTO wins; callers that need every member must
|
|
95
|
+
iterate ``get_pydantic_generic_args`` / ``typing.get_args`` themselves.
|
|
96
|
+
|
|
97
|
+
:param hint: any type annotation object
|
|
98
|
+
:returns: the inner DTO class, or ``None`` if it cannot be extracted
|
|
99
|
+
"""
|
|
100
|
+
# Pydantic generic container? (has __pydantic_generic_metadata__ with a non-empty origin)
|
|
101
|
+
pgm = getattr(hint, '__pydantic_generic_metadata__', None)
|
|
102
|
+
is_pydantic_generic = pgm is not None and pgm.get('origin') is not None
|
|
103
|
+
|
|
104
|
+
if not is_pydantic_generic:
|
|
105
|
+
cls = unwrap_to_class(hint)
|
|
106
|
+
if cls is not None and hasattr(cls, 'model_fields'):
|
|
107
|
+
return cls
|
|
108
|
+
|
|
109
|
+
# Recurse into generic arguments
|
|
110
|
+
for arg in get_pydantic_generic_args(hint):
|
|
111
|
+
if arg is type(None):
|
|
112
|
+
continue
|
|
113
|
+
result = unwrap_generic_to_dto_class(arg)
|
|
114
|
+
if result is not None:
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
return None
|