sqlmodel-ext 0.5.2__py3-none-any.whl → 0.5.4__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 +1 -1
- sqlmodel_ext/_compat.py +261 -261
- sqlmodel_ext/_exceptions.py +16 -16
- sqlmodel_ext/_type_unwrap.py +117 -117
- sqlmodel_ext/_utils.py +5 -5
- sqlmodel_ext/base.py +2178 -2178
- sqlmodel_ext/check_derived.py +565 -565
- sqlmodel_ext/constants.py +76 -76
- sqlmodel_ext/dependencies.py +178 -178
- sqlmodel_ext/derived_decls.py +818 -818
- sqlmodel_ext/field_types/__init__.py +997 -997
- sqlmodel_ext/field_types/_internal/path.py +72 -72
- sqlmodel_ext/field_types/_ssrf.py +81 -81
- sqlmodel_ext/field_types/dialects/__init__.py +7 -7
- sqlmodel_ext/field_types/dialects/postgresql/__init__.py +47 -47
- sqlmodel_ext/field_types/dialects/postgresql/array.py +232 -232
- sqlmodel_ext/field_types/dialects/postgresql/exceptions.py +38 -38
- sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +266 -266
- sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +443 -443
- sqlmodel_ext/field_types/ip_address.py +86 -86
- sqlmodel_ext/field_types/mixins/__init__.py +1 -1
- sqlmodel_ext/field_types/mixins/module_name_mixin.py +33 -33
- sqlmodel_ext/field_types/url.py +170 -170
- sqlmodel_ext/mixins/__init__.py +79 -79
- sqlmodel_ext/mixins/_uuid.py +69 -69
- sqlmodel_ext/mixins/cached_table.py +18 -3
- sqlmodel_ext/mixins/constants.py +17 -17
- sqlmodel_ext/mixins/exceptions.py +84 -84
- sqlmodel_ext/mixins/info_response.py +47 -47
- sqlmodel_ext/mixins/migration_cache_invalidation.py +243 -243
- sqlmodel_ext/mixins/mixin_table_scan.py +96 -96
- sqlmodel_ext/mixins/optimistic_lock.py +161 -161
- sqlmodel_ext/mixins/relation_preload.py +918 -916
- sqlmodel_ext/mixins/resource_quota.py +318 -318
- sqlmodel_ext/mixins/table.py +2041 -2036
- sqlmodel_ext/mixins/trgm_searchable.py +230 -230
- sqlmodel_ext/pagination.py +291 -291
- sqlmodel_ext/relation_load_checker.py +6367 -6367
- sqlmodel_ext/select.py +471 -466
- sqlmodel_ext/session.py +633 -633
- sqlmodel_ext/unset.py +189 -189
- {sqlmodel_ext-0.5.2.dist-info → sqlmodel_ext-0.5.4.dist-info}/METADATA +4 -4
- sqlmodel_ext-0.5.4.dist-info/RECORD +50 -0
- {sqlmodel_ext-0.5.2.dist-info → sqlmodel_ext-0.5.4.dist-info}/licenses/LICENSE +21 -21
- sqlmodel_ext-0.5.2.dist-info/RECORD +0 -50
- {sqlmodel_ext-0.5.2.dist-info → sqlmodel_ext-0.5.4.dist-info}/WHEEL +0 -0
- {sqlmodel_ext-0.5.2.dist-info → sqlmodel_ext-0.5.4.dist-info}/entry_points.txt +0 -0
sqlmodel_ext/__init__.py
CHANGED
|
@@ -20,7 +20,7 @@ Quick start::
|
|
|
20
20
|
user = await user.save(session)
|
|
21
21
|
users = await User.get(session, fetch_mode="all")
|
|
22
22
|
"""
|
|
23
|
-
__version__ = "0.5.
|
|
23
|
+
__version__ = "0.5.4"
|
|
24
24
|
|
|
25
25
|
# Every re-export below uses the redundant ``X as X`` form: the package ships
|
|
26
26
|
# ``py.typed``, and type checkers treat a plain ``from m import X`` in a typed
|
sqlmodel_ext/_compat.py
CHANGED
|
@@ -1,261 +1,261 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Python 3.14+ compatibility patches for SQLModel.
|
|
3
|
-
|
|
4
|
-
This module applies monkey-patches to fix SQLModel's compatibility issues
|
|
5
|
-
with Python 3.14 (PEP 649 deferred evaluation of annotations).
|
|
6
|
-
|
|
7
|
-
Import this module for side effects before using SQLModelBase.
|
|
8
|
-
"""
|
|
9
|
-
import sys
|
|
10
|
-
|
|
11
|
-
if sys.version_info >= (3, 14):
|
|
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]
|
|
15
|
-
import types
|
|
16
|
-
from enum import StrEnum
|
|
17
|
-
from typing import Literal, Union, get_args, get_origin
|
|
18
|
-
|
|
19
|
-
from pydantic_core import PydanticUndefined
|
|
20
|
-
from sqlalchemy import String
|
|
21
|
-
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
|
22
|
-
|
|
23
|
-
# JSONB with fallback to generic JSON for non-PostgreSQL databases
|
|
24
|
-
try:
|
|
25
|
-
from sqlalchemy.dialects.postgresql import JSONB
|
|
26
|
-
except ImportError:
|
|
27
|
-
from sqlalchemy.types import JSON as JSONB # type: ignore[assignment]
|
|
28
|
-
|
|
29
|
-
# Monkey-patch 1: Fix get_sqlalchemy_type for Python 3.14
|
|
30
|
-
import sqlmodel.main
|
|
31
|
-
_original_get_sqlalchemy_type = sqlmodel.main.get_sqlalchemy_type
|
|
32
|
-
|
|
33
|
-
def _get_explicit_sa_type(field):
|
|
34
|
-
"""
|
|
35
|
-
Extract an explicit ``sa_type`` from a SQLModel/Pydantic FieldInfo.
|
|
36
|
-
|
|
37
|
-
Matches upstream ``sqlmodel.main._get_sqlmodel_field_value`` semantics:
|
|
38
|
-
prefers ``FieldInfoMetadata.sa_type`` then falls back to the direct
|
|
39
|
-
``field.sa_type`` attribute. Returns ``PydanticUndefined`` when unset.
|
|
40
|
-
"""
|
|
41
|
-
# Prefer FieldInfoMetadata.sa_type (set via Field(sa_type=...))
|
|
42
|
-
metadata = getattr(field, 'metadata', None)
|
|
43
|
-
if metadata:
|
|
44
|
-
for item in metadata:
|
|
45
|
-
sa_type = getattr(item, 'sa_type', PydanticUndefined)
|
|
46
|
-
if sa_type is not PydanticUndefined:
|
|
47
|
-
return sa_type
|
|
48
|
-
# Fallback to direct attribute
|
|
49
|
-
return getattr(field, 'sa_type', PydanticUndefined)
|
|
50
|
-
|
|
51
|
-
def _patched_get_sqlalchemy_type(field):
|
|
52
|
-
"""
|
|
53
|
-
Fix SQLModel's get_sqlalchemy_type for Python 3.14 type issues.
|
|
54
|
-
|
|
55
|
-
Handles ForwardRef, ClassVar, Literal, Mapped, and custom types
|
|
56
|
-
that cause issubclass errors under PEP 649.
|
|
57
|
-
"""
|
|
58
|
-
# Respect explicit sa_type from Field(sa_type=...) — must take
|
|
59
|
-
# precedence over annotation-based auto-detection, matching upstream
|
|
60
|
-
# SQLModel behavior (sqlmodel/main.py::get_sqlalchemy_type checks
|
|
61
|
-
# sa_type before everything else). Without this, fields like
|
|
62
|
-
# ``options: list[str] | None = Field(sa_type=JSON)`` incorrectly
|
|
63
|
-
# get JSONB from the Union[list, None] -> JSONB branch below,
|
|
64
|
-
# breaking non-PostgreSQL dialects (e.g., SQLite in tests).
|
|
65
|
-
explicit_sa_type = _get_explicit_sa_type(field)
|
|
66
|
-
if explicit_sa_type is not PydanticUndefined:
|
|
67
|
-
return explicit_sa_type
|
|
68
|
-
|
|
69
|
-
# Check field.metadata (Pydantic-processed Annotated types)
|
|
70
|
-
metadata = getattr(field, 'metadata', None)
|
|
71
|
-
if metadata:
|
|
72
|
-
# Pass field.annotation as source_type so handlers that need the element type
|
|
73
|
-
# (e.g. _PendingArrayHandler extracting T from list[T] to compute ARRAY(T))
|
|
74
|
-
# can resolve. handler=None is fine because the sa_type extraction path only
|
|
75
|
-
# reads metadata['sa_type'] and never invokes the schema itself.
|
|
76
|
-
field_annotation = getattr(field, 'annotation', None)
|
|
77
|
-
for metadata_item in metadata:
|
|
78
|
-
if hasattr(metadata_item, '__get_pydantic_core_schema__'):
|
|
79
|
-
try:
|
|
80
|
-
schema = metadata_item.__get_pydantic_core_schema__(field_annotation, None)
|
|
81
|
-
if isinstance(schema, dict) and 'metadata' in schema:
|
|
82
|
-
sa_type = schema['metadata'].get('sa_type')
|
|
83
|
-
if sa_type is not None:
|
|
84
|
-
return sa_type
|
|
85
|
-
except (TypeError, AttributeError, KeyError):
|
|
86
|
-
pass
|
|
87
|
-
|
|
88
|
-
# Check InstrumentedAttribute defaults
|
|
89
|
-
default = getattr(field, 'default', None)
|
|
90
|
-
if default is not None:
|
|
91
|
-
if isinstance(default, InstrumentedAttribute):
|
|
92
|
-
return None
|
|
93
|
-
|
|
94
|
-
annotation = getattr(field, 'annotation', None)
|
|
95
|
-
if annotation is not None:
|
|
96
|
-
# Handle Union[list/dict/..., None] types -> JSONB
|
|
97
|
-
origin = get_origin(annotation)
|
|
98
|
-
|
|
99
|
-
is_union = origin is Union or isinstance(annotation, types.UnionType)
|
|
100
|
-
if is_union:
|
|
101
|
-
args = get_args(annotation)
|
|
102
|
-
non_none_args = [a for a in args if a is not type(None)]
|
|
103
|
-
if non_none_args:
|
|
104
|
-
first_arg = non_none_args[0]
|
|
105
|
-
# Check for Annotated SA type metadata first (e.g. Array[T] | None).
|
|
106
|
-
# Array[T] desugars to Annotated[list[T], ArrayTypeHandler(T)];
|
|
107
|
-
# wrapping with | None produces Union[Annotated[...], None],
|
|
108
|
-
# so the SA type must be extracted from the inner Annotated.
|
|
109
|
-
first_arg_type_name = type(first_arg).__name__
|
|
110
|
-
if first_arg_type_name in ('AnnotatedAlias', '_AnnotatedAlias'):
|
|
111
|
-
inner_args = get_args(first_arg)
|
|
112
|
-
for md in inner_args[1:]:
|
|
113
|
-
if hasattr(md, '__get_pydantic_core_schema__'):
|
|
114
|
-
try:
|
|
115
|
-
# Pass first_arg (the full Annotated alias) as source_type so
|
|
116
|
-
# handlers like _PendingArrayHandler can extract the element type.
|
|
117
|
-
schema = md.__get_pydantic_core_schema__(first_arg, None)
|
|
118
|
-
if isinstance(schema, dict) and 'metadata' in schema:
|
|
119
|
-
sa_type = schema['metadata'].get('sa_type')
|
|
120
|
-
if sa_type is not None:
|
|
121
|
-
return sa_type
|
|
122
|
-
except (TypeError, AttributeError, KeyError):
|
|
123
|
-
pass
|
|
124
|
-
first_origin = get_origin(first_arg)
|
|
125
|
-
# Plain list/dict/tuple/set (no Annotated SA type) -> JSONB
|
|
126
|
-
if first_origin in (list, dict, tuple, set):
|
|
127
|
-
return JSONB
|
|
128
|
-
|
|
129
|
-
# Handle Literal[StrEnum.MEMBER] types -> String
|
|
130
|
-
if origin is Literal:
|
|
131
|
-
literal_args = get_args(annotation)
|
|
132
|
-
if literal_args:
|
|
133
|
-
if all(isinstance(arg, StrEnum) for arg in literal_args):
|
|
134
|
-
return String
|
|
135
|
-
|
|
136
|
-
# Check __sqlmodel_sa_type__ attribute
|
|
137
|
-
if hasattr(annotation, '__sqlmodel_sa_type__'):
|
|
138
|
-
return annotation.__sqlmodel_sa_type__
|
|
139
|
-
|
|
140
|
-
# Check __get_pydantic_core_schema__ for custom types
|
|
141
|
-
if hasattr(annotation, '__get_pydantic_core_schema__'):
|
|
142
|
-
try:
|
|
143
|
-
schema = annotation.__get_pydantic_core_schema__(annotation, lambda x: None)
|
|
144
|
-
if isinstance(schema, dict) and 'metadata' in schema:
|
|
145
|
-
sa_type = schema['metadata'].get('sa_type')
|
|
146
|
-
if sa_type is not None:
|
|
147
|
-
return sa_type
|
|
148
|
-
except (TypeError, AttributeError, KeyError):
|
|
149
|
-
pass
|
|
150
|
-
|
|
151
|
-
import typing
|
|
152
|
-
from sqlalchemy.orm import Mapped
|
|
153
|
-
anno_type_name = type(annotation).__name__
|
|
154
|
-
|
|
155
|
-
# ForwardRef: Relationship field annotations
|
|
156
|
-
if anno_type_name == 'ForwardRef':
|
|
157
|
-
return None
|
|
158
|
-
|
|
159
|
-
# AnnotatedAlias: check for sa_type metadata
|
|
160
|
-
if anno_type_name in ('AnnotatedAlias', '_AnnotatedAlias'):
|
|
161
|
-
args = get_args(annotation)
|
|
162
|
-
for md in args[1:]:
|
|
163
|
-
if hasattr(md, '__get_pydantic_core_schema__'):
|
|
164
|
-
try:
|
|
165
|
-
# Pass annotation (the full Annotated form) as source_type so
|
|
166
|
-
# handlers needing the element type (_PendingArrayHandler etc.)
|
|
167
|
-
# can extract T from list[T].
|
|
168
|
-
schema = md.__get_pydantic_core_schema__(annotation, None)
|
|
169
|
-
if isinstance(schema, dict) and 'metadata' in schema:
|
|
170
|
-
sa_type = schema['metadata'].get('sa_type')
|
|
171
|
-
if sa_type is not None:
|
|
172
|
-
return sa_type
|
|
173
|
-
except (TypeError, AttributeError, KeyError):
|
|
174
|
-
pass
|
|
175
|
-
|
|
176
|
-
# _GenericAlias or GenericAlias: typing generic types
|
|
177
|
-
if anno_type_name in ('_GenericAlias', 'GenericAlias'):
|
|
178
|
-
local_origin = get_origin(annotation)
|
|
179
|
-
|
|
180
|
-
if local_origin is typing.ClassVar:
|
|
181
|
-
return None
|
|
182
|
-
|
|
183
|
-
if local_origin in (list, dict, tuple, set):
|
|
184
|
-
field_info = getattr(field, 'field_info', None)
|
|
185
|
-
if field_info is None:
|
|
186
|
-
return None
|
|
187
|
-
|
|
188
|
-
# Mapped type handling
|
|
189
|
-
if 'Mapped' in anno_type_name or 'Mapped' in str(annotation):
|
|
190
|
-
return None
|
|
191
|
-
|
|
192
|
-
if get_origin(annotation) is Mapped:
|
|
193
|
-
return None
|
|
194
|
-
try:
|
|
195
|
-
if annotation is Mapped or isinstance(annotation, type) and issubclass(annotation, Mapped):
|
|
196
|
-
return None
|
|
197
|
-
except TypeError:
|
|
198
|
-
pass
|
|
199
|
-
|
|
200
|
-
return _original_get_sqlalchemy_type(field)
|
|
201
|
-
|
|
202
|
-
sqlmodel.main.get_sqlalchemy_type = _patched_get_sqlalchemy_type
|
|
203
|
-
|
|
204
|
-
# Monkey-patch 2: Fix InstrumentedAttribute defaults in inherited table classes
|
|
205
|
-
import sqlmodel._compat as _compat
|
|
206
|
-
from pydantic_core import PydanticUndefined as Undefined
|
|
207
|
-
from sqlalchemy.orm import attributes as _sa_attributes
|
|
208
|
-
|
|
209
|
-
_original_sqlmodel_table_construct = _compat.sqlmodel_table_construct
|
|
210
|
-
|
|
211
|
-
def _patched_sqlmodel_table_construct(self_instance, values):
|
|
212
|
-
"""
|
|
213
|
-
Fix sqlmodel_table_construct to skip InstrumentedAttribute defaults.
|
|
214
|
-
|
|
215
|
-
In polymorphic inherited table classes, inherited Relationship fields
|
|
216
|
-
may have InstrumentedAttribute as their default value, causing errors.
|
|
217
|
-
"""
|
|
218
|
-
cls = type(self_instance)
|
|
219
|
-
|
|
220
|
-
fields_to_set = {}
|
|
221
|
-
|
|
222
|
-
for name, field in cls.model_fields.items():
|
|
223
|
-
if name in values:
|
|
224
|
-
fields_to_set[name] = values[name]
|
|
225
|
-
continue
|
|
226
|
-
|
|
227
|
-
if isinstance(field.default, _sa_attributes.InstrumentedAttribute):
|
|
228
|
-
continue
|
|
229
|
-
|
|
230
|
-
if field.default is not Undefined:
|
|
231
|
-
fields_to_set[name] = field.default
|
|
232
|
-
elif field.default_factory is not None:
|
|
233
|
-
fields_to_set[name] = field.get_default(call_default_factory=True)
|
|
234
|
-
|
|
235
|
-
for key, value in fields_to_set.items():
|
|
236
|
-
if not isinstance(value, _sa_attributes.InstrumentedAttribute):
|
|
237
|
-
setattr(self_instance, key, value)
|
|
238
|
-
|
|
239
|
-
object.__setattr__(self_instance, '__pydantic_fields_set__', set(values.keys()))
|
|
240
|
-
if not cls.__pydantic_root_model__:
|
|
241
|
-
_extra = None
|
|
242
|
-
if cls.model_config.get('extra') == 'allow':
|
|
243
|
-
_extra = {}
|
|
244
|
-
for k, v in values.items():
|
|
245
|
-
if k not in cls.model_fields:
|
|
246
|
-
_extra[k] = v
|
|
247
|
-
object.__setattr__(self_instance, '__pydantic_extra__', _extra)
|
|
248
|
-
|
|
249
|
-
if cls.__pydantic_post_init__:
|
|
250
|
-
self_instance.model_post_init(None)
|
|
251
|
-
elif not cls.__pydantic_root_model__:
|
|
252
|
-
object.__setattr__(self_instance, '__pydantic_private__', None)
|
|
253
|
-
|
|
254
|
-
for key in self_instance.__sqlmodel_relationships__:
|
|
255
|
-
value = values.get(key, Undefined)
|
|
256
|
-
if value is not Undefined:
|
|
257
|
-
setattr(self_instance, key, value)
|
|
258
|
-
|
|
259
|
-
return self_instance
|
|
260
|
-
|
|
261
|
-
_compat.sqlmodel_table_construct = _patched_sqlmodel_table_construct
|
|
1
|
+
"""
|
|
2
|
+
Python 3.14+ compatibility patches for SQLModel.
|
|
3
|
+
|
|
4
|
+
This module applies monkey-patches to fix SQLModel's compatibility issues
|
|
5
|
+
with Python 3.14 (PEP 649 deferred evaluation of annotations).
|
|
6
|
+
|
|
7
|
+
Import this module for side effects before using SQLModelBase.
|
|
8
|
+
"""
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
if sys.version_info >= (3, 14):
|
|
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]
|
|
15
|
+
import types
|
|
16
|
+
from enum import StrEnum
|
|
17
|
+
from typing import Literal, Union, get_args, get_origin
|
|
18
|
+
|
|
19
|
+
from pydantic_core import PydanticUndefined
|
|
20
|
+
from sqlalchemy import String
|
|
21
|
+
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
|
22
|
+
|
|
23
|
+
# JSONB with fallback to generic JSON for non-PostgreSQL databases
|
|
24
|
+
try:
|
|
25
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
26
|
+
except ImportError:
|
|
27
|
+
from sqlalchemy.types import JSON as JSONB # type: ignore[assignment]
|
|
28
|
+
|
|
29
|
+
# Monkey-patch 1: Fix get_sqlalchemy_type for Python 3.14
|
|
30
|
+
import sqlmodel.main
|
|
31
|
+
_original_get_sqlalchemy_type = sqlmodel.main.get_sqlalchemy_type
|
|
32
|
+
|
|
33
|
+
def _get_explicit_sa_type(field):
|
|
34
|
+
"""
|
|
35
|
+
Extract an explicit ``sa_type`` from a SQLModel/Pydantic FieldInfo.
|
|
36
|
+
|
|
37
|
+
Matches upstream ``sqlmodel.main._get_sqlmodel_field_value`` semantics:
|
|
38
|
+
prefers ``FieldInfoMetadata.sa_type`` then falls back to the direct
|
|
39
|
+
``field.sa_type`` attribute. Returns ``PydanticUndefined`` when unset.
|
|
40
|
+
"""
|
|
41
|
+
# Prefer FieldInfoMetadata.sa_type (set via Field(sa_type=...))
|
|
42
|
+
metadata = getattr(field, 'metadata', None)
|
|
43
|
+
if metadata:
|
|
44
|
+
for item in metadata:
|
|
45
|
+
sa_type = getattr(item, 'sa_type', PydanticUndefined)
|
|
46
|
+
if sa_type is not PydanticUndefined:
|
|
47
|
+
return sa_type
|
|
48
|
+
# Fallback to direct attribute
|
|
49
|
+
return getattr(field, 'sa_type', PydanticUndefined)
|
|
50
|
+
|
|
51
|
+
def _patched_get_sqlalchemy_type(field):
|
|
52
|
+
"""
|
|
53
|
+
Fix SQLModel's get_sqlalchemy_type for Python 3.14 type issues.
|
|
54
|
+
|
|
55
|
+
Handles ForwardRef, ClassVar, Literal, Mapped, and custom types
|
|
56
|
+
that cause issubclass errors under PEP 649.
|
|
57
|
+
"""
|
|
58
|
+
# Respect explicit sa_type from Field(sa_type=...) — must take
|
|
59
|
+
# precedence over annotation-based auto-detection, matching upstream
|
|
60
|
+
# SQLModel behavior (sqlmodel/main.py::get_sqlalchemy_type checks
|
|
61
|
+
# sa_type before everything else). Without this, fields like
|
|
62
|
+
# ``options: list[str] | None = Field(sa_type=JSON)`` incorrectly
|
|
63
|
+
# get JSONB from the Union[list, None] -> JSONB branch below,
|
|
64
|
+
# breaking non-PostgreSQL dialects (e.g., SQLite in tests).
|
|
65
|
+
explicit_sa_type = _get_explicit_sa_type(field)
|
|
66
|
+
if explicit_sa_type is not PydanticUndefined:
|
|
67
|
+
return explicit_sa_type
|
|
68
|
+
|
|
69
|
+
# Check field.metadata (Pydantic-processed Annotated types)
|
|
70
|
+
metadata = getattr(field, 'metadata', None)
|
|
71
|
+
if metadata:
|
|
72
|
+
# Pass field.annotation as source_type so handlers that need the element type
|
|
73
|
+
# (e.g. _PendingArrayHandler extracting T from list[T] to compute ARRAY(T))
|
|
74
|
+
# can resolve. handler=None is fine because the sa_type extraction path only
|
|
75
|
+
# reads metadata['sa_type'] and never invokes the schema itself.
|
|
76
|
+
field_annotation = getattr(field, 'annotation', None)
|
|
77
|
+
for metadata_item in metadata:
|
|
78
|
+
if hasattr(metadata_item, '__get_pydantic_core_schema__'):
|
|
79
|
+
try:
|
|
80
|
+
schema = metadata_item.__get_pydantic_core_schema__(field_annotation, None)
|
|
81
|
+
if isinstance(schema, dict) and 'metadata' in schema:
|
|
82
|
+
sa_type = schema['metadata'].get('sa_type')
|
|
83
|
+
if sa_type is not None:
|
|
84
|
+
return sa_type
|
|
85
|
+
except (TypeError, AttributeError, KeyError):
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
# Check InstrumentedAttribute defaults
|
|
89
|
+
default = getattr(field, 'default', None)
|
|
90
|
+
if default is not None:
|
|
91
|
+
if isinstance(default, InstrumentedAttribute):
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
annotation = getattr(field, 'annotation', None)
|
|
95
|
+
if annotation is not None:
|
|
96
|
+
# Handle Union[list/dict/..., None] types -> JSONB
|
|
97
|
+
origin = get_origin(annotation)
|
|
98
|
+
|
|
99
|
+
is_union = origin is Union or isinstance(annotation, types.UnionType)
|
|
100
|
+
if is_union:
|
|
101
|
+
args = get_args(annotation)
|
|
102
|
+
non_none_args = [a for a in args if a is not type(None)]
|
|
103
|
+
if non_none_args:
|
|
104
|
+
first_arg = non_none_args[0]
|
|
105
|
+
# Check for Annotated SA type metadata first (e.g. Array[T] | None).
|
|
106
|
+
# Array[T] desugars to Annotated[list[T], ArrayTypeHandler(T)];
|
|
107
|
+
# wrapping with | None produces Union[Annotated[...], None],
|
|
108
|
+
# so the SA type must be extracted from the inner Annotated.
|
|
109
|
+
first_arg_type_name = type(first_arg).__name__
|
|
110
|
+
if first_arg_type_name in ('AnnotatedAlias', '_AnnotatedAlias'):
|
|
111
|
+
inner_args = get_args(first_arg)
|
|
112
|
+
for md in inner_args[1:]:
|
|
113
|
+
if hasattr(md, '__get_pydantic_core_schema__'):
|
|
114
|
+
try:
|
|
115
|
+
# Pass first_arg (the full Annotated alias) as source_type so
|
|
116
|
+
# handlers like _PendingArrayHandler can extract the element type.
|
|
117
|
+
schema = md.__get_pydantic_core_schema__(first_arg, None)
|
|
118
|
+
if isinstance(schema, dict) and 'metadata' in schema:
|
|
119
|
+
sa_type = schema['metadata'].get('sa_type')
|
|
120
|
+
if sa_type is not None:
|
|
121
|
+
return sa_type
|
|
122
|
+
except (TypeError, AttributeError, KeyError):
|
|
123
|
+
pass
|
|
124
|
+
first_origin = get_origin(first_arg)
|
|
125
|
+
# Plain list/dict/tuple/set (no Annotated SA type) -> JSONB
|
|
126
|
+
if first_origin in (list, dict, tuple, set):
|
|
127
|
+
return JSONB
|
|
128
|
+
|
|
129
|
+
# Handle Literal[StrEnum.MEMBER] types -> String
|
|
130
|
+
if origin is Literal:
|
|
131
|
+
literal_args = get_args(annotation)
|
|
132
|
+
if literal_args:
|
|
133
|
+
if all(isinstance(arg, StrEnum) for arg in literal_args):
|
|
134
|
+
return String
|
|
135
|
+
|
|
136
|
+
# Check __sqlmodel_sa_type__ attribute
|
|
137
|
+
if hasattr(annotation, '__sqlmodel_sa_type__'):
|
|
138
|
+
return annotation.__sqlmodel_sa_type__
|
|
139
|
+
|
|
140
|
+
# Check __get_pydantic_core_schema__ for custom types
|
|
141
|
+
if hasattr(annotation, '__get_pydantic_core_schema__'):
|
|
142
|
+
try:
|
|
143
|
+
schema = annotation.__get_pydantic_core_schema__(annotation, lambda x: None)
|
|
144
|
+
if isinstance(schema, dict) and 'metadata' in schema:
|
|
145
|
+
sa_type = schema['metadata'].get('sa_type')
|
|
146
|
+
if sa_type is not None:
|
|
147
|
+
return sa_type
|
|
148
|
+
except (TypeError, AttributeError, KeyError):
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
import typing
|
|
152
|
+
from sqlalchemy.orm import Mapped
|
|
153
|
+
anno_type_name = type(annotation).__name__
|
|
154
|
+
|
|
155
|
+
# ForwardRef: Relationship field annotations
|
|
156
|
+
if anno_type_name == 'ForwardRef':
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
# AnnotatedAlias: check for sa_type metadata
|
|
160
|
+
if anno_type_name in ('AnnotatedAlias', '_AnnotatedAlias'):
|
|
161
|
+
args = get_args(annotation)
|
|
162
|
+
for md in args[1:]:
|
|
163
|
+
if hasattr(md, '__get_pydantic_core_schema__'):
|
|
164
|
+
try:
|
|
165
|
+
# Pass annotation (the full Annotated form) as source_type so
|
|
166
|
+
# handlers needing the element type (_PendingArrayHandler etc.)
|
|
167
|
+
# can extract T from list[T].
|
|
168
|
+
schema = md.__get_pydantic_core_schema__(annotation, None)
|
|
169
|
+
if isinstance(schema, dict) and 'metadata' in schema:
|
|
170
|
+
sa_type = schema['metadata'].get('sa_type')
|
|
171
|
+
if sa_type is not None:
|
|
172
|
+
return sa_type
|
|
173
|
+
except (TypeError, AttributeError, KeyError):
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
# _GenericAlias or GenericAlias: typing generic types
|
|
177
|
+
if anno_type_name in ('_GenericAlias', 'GenericAlias'):
|
|
178
|
+
local_origin = get_origin(annotation)
|
|
179
|
+
|
|
180
|
+
if local_origin is typing.ClassVar:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
if local_origin in (list, dict, tuple, set):
|
|
184
|
+
field_info = getattr(field, 'field_info', None)
|
|
185
|
+
if field_info is None:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
# Mapped type handling
|
|
189
|
+
if 'Mapped' in anno_type_name or 'Mapped' in str(annotation):
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
if get_origin(annotation) is Mapped:
|
|
193
|
+
return None
|
|
194
|
+
try:
|
|
195
|
+
if annotation is Mapped or isinstance(annotation, type) and issubclass(annotation, Mapped):
|
|
196
|
+
return None
|
|
197
|
+
except TypeError:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
return _original_get_sqlalchemy_type(field)
|
|
201
|
+
|
|
202
|
+
sqlmodel.main.get_sqlalchemy_type = _patched_get_sqlalchemy_type
|
|
203
|
+
|
|
204
|
+
# Monkey-patch 2: Fix InstrumentedAttribute defaults in inherited table classes
|
|
205
|
+
import sqlmodel._compat as _compat
|
|
206
|
+
from pydantic_core import PydanticUndefined as Undefined
|
|
207
|
+
from sqlalchemy.orm import attributes as _sa_attributes
|
|
208
|
+
|
|
209
|
+
_original_sqlmodel_table_construct = _compat.sqlmodel_table_construct
|
|
210
|
+
|
|
211
|
+
def _patched_sqlmodel_table_construct(self_instance, values):
|
|
212
|
+
"""
|
|
213
|
+
Fix sqlmodel_table_construct to skip InstrumentedAttribute defaults.
|
|
214
|
+
|
|
215
|
+
In polymorphic inherited table classes, inherited Relationship fields
|
|
216
|
+
may have InstrumentedAttribute as their default value, causing errors.
|
|
217
|
+
"""
|
|
218
|
+
cls = type(self_instance)
|
|
219
|
+
|
|
220
|
+
fields_to_set = {}
|
|
221
|
+
|
|
222
|
+
for name, field in cls.model_fields.items():
|
|
223
|
+
if name in values:
|
|
224
|
+
fields_to_set[name] = values[name]
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
if isinstance(field.default, _sa_attributes.InstrumentedAttribute):
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
if field.default is not Undefined:
|
|
231
|
+
fields_to_set[name] = field.default
|
|
232
|
+
elif field.default_factory is not None:
|
|
233
|
+
fields_to_set[name] = field.get_default(call_default_factory=True)
|
|
234
|
+
|
|
235
|
+
for key, value in fields_to_set.items():
|
|
236
|
+
if not isinstance(value, _sa_attributes.InstrumentedAttribute):
|
|
237
|
+
setattr(self_instance, key, value)
|
|
238
|
+
|
|
239
|
+
object.__setattr__(self_instance, '__pydantic_fields_set__', set(values.keys()))
|
|
240
|
+
if not cls.__pydantic_root_model__:
|
|
241
|
+
_extra = None
|
|
242
|
+
if cls.model_config.get('extra') == 'allow':
|
|
243
|
+
_extra = {}
|
|
244
|
+
for k, v in values.items():
|
|
245
|
+
if k not in cls.model_fields:
|
|
246
|
+
_extra[k] = v
|
|
247
|
+
object.__setattr__(self_instance, '__pydantic_extra__', _extra)
|
|
248
|
+
|
|
249
|
+
if cls.__pydantic_post_init__:
|
|
250
|
+
self_instance.model_post_init(None)
|
|
251
|
+
elif not cls.__pydantic_root_model__:
|
|
252
|
+
object.__setattr__(self_instance, '__pydantic_private__', None)
|
|
253
|
+
|
|
254
|
+
for key in self_instance.__sqlmodel_relationships__:
|
|
255
|
+
value = values.get(key, Undefined)
|
|
256
|
+
if value is not Undefined:
|
|
257
|
+
setattr(self_instance, key, value)
|
|
258
|
+
|
|
259
|
+
return self_instance
|
|
260
|
+
|
|
261
|
+
_compat.sqlmodel_table_construct = _patched_sqlmodel_table_construct
|
sqlmodel_ext/_exceptions.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
"""Exceptions for sqlmodel_ext."""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class RecordNotFoundError(Exception):
|
|
5
|
-
"""
|
|
6
|
-
Raised when a database record is not found.
|
|
7
|
-
|
|
8
|
-
Attributes:
|
|
9
|
-
status_code: HTTP-compatible status code (404)
|
|
10
|
-
detail: Human-readable error message
|
|
11
|
-
"""
|
|
12
|
-
status_code: int = 404
|
|
13
|
-
|
|
14
|
-
def __init__(self, detail: str = "Not found"):
|
|
15
|
-
self.detail: str = detail
|
|
16
|
-
super().__init__(detail)
|
|
1
|
+
"""Exceptions for sqlmodel_ext."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RecordNotFoundError(Exception):
|
|
5
|
+
"""
|
|
6
|
+
Raised when a database record is not found.
|
|
7
|
+
|
|
8
|
+
Attributes:
|
|
9
|
+
status_code: HTTP-compatible status code (404)
|
|
10
|
+
detail: Human-readable error message
|
|
11
|
+
"""
|
|
12
|
+
status_code: int = 404
|
|
13
|
+
|
|
14
|
+
def __init__(self, detail: str = "Not found"):
|
|
15
|
+
self.detail: str = detail
|
|
16
|
+
super().__init__(detail)
|