cocoindex 0.2.16__cp311-abi3-manylinux_2_28_aarch64.whl → 0.2.17__cp311-abi3-manylinux_2_28_aarch64.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.
- cocoindex/_engine.abi3.so +0 -0
- cocoindex/auth_registry.py +1 -1
- cocoindex/cli.py +121 -41
- cocoindex/engine_object.py +272 -0
- cocoindex/{convert.py → engine_value.py} +64 -208
- cocoindex/flow.py +7 -2
- cocoindex/functions/__init__.py +45 -0
- cocoindex/functions/_engine_builtin_specs.py +62 -0
- cocoindex/functions/colpali.py +250 -0
- cocoindex/functions/sbert.py +63 -0
- cocoindex/lib.py +1 -1
- cocoindex/op.py +7 -3
- cocoindex/sources/__init__.py +5 -0
- cocoindex/{sources.py → sources/_engine_builtin_specs.py} +3 -3
- cocoindex/targets/_engine_builtin_specs.py +9 -0
- cocoindex/tests/test_engine_object.py +331 -0
- cocoindex/tests/{test_convert.py → test_engine_value.py} +150 -26
- cocoindex/typing.py +125 -3
- {cocoindex-0.2.16.dist-info → cocoindex-0.2.17.dist-info}/METADATA +4 -1
- cocoindex-0.2.17.dist-info/RECORD +43 -0
- {cocoindex-0.2.16.dist-info → cocoindex-0.2.17.dist-info}/WHEEL +1 -1
- {cocoindex-0.2.16.dist-info → cocoindex-0.2.17.dist-info}/licenses/THIRD_PARTY_NOTICES.html +22 -19
- cocoindex/tests/test_load_convert.py +0 -118
- cocoindex-0.2.16.dist-info/RECORD +0 -37
- {cocoindex-0.2.16.dist-info → cocoindex-0.2.17.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,331 @@
|
|
1
|
+
import dataclasses
|
2
|
+
import datetime
|
3
|
+
from typing import TypedDict, NamedTuple, Literal
|
4
|
+
|
5
|
+
import numpy as np
|
6
|
+
from numpy.typing import NDArray
|
7
|
+
import pytest
|
8
|
+
|
9
|
+
from cocoindex.typing import Vector
|
10
|
+
from cocoindex.engine_object import dump_engine_object, load_engine_object
|
11
|
+
|
12
|
+
# Optional Pydantic support for testing
|
13
|
+
try:
|
14
|
+
import pydantic
|
15
|
+
|
16
|
+
PYDANTIC_AVAILABLE = True
|
17
|
+
except ImportError:
|
18
|
+
PYDANTIC_AVAILABLE = False
|
19
|
+
|
20
|
+
|
21
|
+
@dataclasses.dataclass
|
22
|
+
class LocalTargetFieldMapping:
|
23
|
+
source: str
|
24
|
+
target: str | None = None
|
25
|
+
|
26
|
+
|
27
|
+
@dataclasses.dataclass
|
28
|
+
class LocalNodeFromFields:
|
29
|
+
label: str
|
30
|
+
fields: list[LocalTargetFieldMapping]
|
31
|
+
|
32
|
+
|
33
|
+
@dataclasses.dataclass
|
34
|
+
class LocalNodes:
|
35
|
+
kind = "Node"
|
36
|
+
label: str
|
37
|
+
|
38
|
+
|
39
|
+
@dataclasses.dataclass
|
40
|
+
class LocalRelationships:
|
41
|
+
kind = "Relationship"
|
42
|
+
rel_type: str
|
43
|
+
source: LocalNodeFromFields
|
44
|
+
target: LocalNodeFromFields
|
45
|
+
|
46
|
+
|
47
|
+
class LocalPoint(NamedTuple):
|
48
|
+
x: int
|
49
|
+
y: int
|
50
|
+
|
51
|
+
|
52
|
+
class UserInfo(TypedDict):
|
53
|
+
id: str
|
54
|
+
age: int
|
55
|
+
|
56
|
+
|
57
|
+
def test_timedelta_roundtrip_via_dump_load() -> None:
|
58
|
+
td = datetime.timedelta(days=1, hours=2, minutes=3, seconds=4, microseconds=500)
|
59
|
+
dumped = dump_engine_object(td)
|
60
|
+
loaded = load_engine_object(datetime.timedelta, dumped)
|
61
|
+
assert isinstance(loaded, datetime.timedelta)
|
62
|
+
assert loaded == td
|
63
|
+
|
64
|
+
|
65
|
+
def test_ndarray_roundtrip_via_dump_load() -> None:
|
66
|
+
value: NDArray[np.float32] = np.array([1.0, 2.0, 3.0], dtype=np.float32)
|
67
|
+
dumped = dump_engine_object(value)
|
68
|
+
assert dumped == [1.0, 2.0, 3.0]
|
69
|
+
loaded = load_engine_object(NDArray[np.float32], dumped)
|
70
|
+
assert isinstance(loaded, np.ndarray)
|
71
|
+
assert loaded.dtype == np.float32
|
72
|
+
assert np.array_equal(loaded, value)
|
73
|
+
|
74
|
+
|
75
|
+
def test_nodes_kind_is_carried() -> None:
|
76
|
+
node = LocalNodes(label="User")
|
77
|
+
dumped = dump_engine_object(node)
|
78
|
+
# dumped should include discriminator
|
79
|
+
assert dumped.get("kind") == "Node"
|
80
|
+
# load back
|
81
|
+
loaded = load_engine_object(LocalNodes, dumped)
|
82
|
+
assert isinstance(loaded, LocalNodes)
|
83
|
+
# class-level attribute is preserved
|
84
|
+
assert getattr(loaded, "kind", None) == "Node"
|
85
|
+
assert loaded.label == "User"
|
86
|
+
|
87
|
+
|
88
|
+
def test_relationships_union_discriminator() -> None:
|
89
|
+
rel = LocalRelationships(
|
90
|
+
rel_type="LIKES",
|
91
|
+
source=LocalNodeFromFields(
|
92
|
+
label="User", fields=[LocalTargetFieldMapping("id")]
|
93
|
+
),
|
94
|
+
target=LocalNodeFromFields(
|
95
|
+
label="Item", fields=[LocalTargetFieldMapping("id")]
|
96
|
+
),
|
97
|
+
)
|
98
|
+
dumped = dump_engine_object(rel)
|
99
|
+
assert dumped.get("kind") == "Relationship"
|
100
|
+
loaded = load_engine_object(LocalNodes | LocalRelationships, dumped)
|
101
|
+
assert isinstance(loaded, LocalRelationships)
|
102
|
+
assert getattr(loaded, "kind", None) == "Relationship"
|
103
|
+
assert loaded.rel_type == "LIKES"
|
104
|
+
assert dataclasses.asdict(loaded.source) == {
|
105
|
+
"label": "User",
|
106
|
+
"fields": [{"source": "id", "target": None}],
|
107
|
+
}
|
108
|
+
assert dataclasses.asdict(loaded.target) == {
|
109
|
+
"label": "Item",
|
110
|
+
"fields": [{"source": "id", "target": None}],
|
111
|
+
}
|
112
|
+
|
113
|
+
|
114
|
+
def test_typed_dict_roundtrip_via_dump_load() -> None:
|
115
|
+
user: UserInfo = {"id": "u1", "age": 30}
|
116
|
+
dumped = dump_engine_object(user)
|
117
|
+
assert dumped == {"id": "u1", "age": 30}
|
118
|
+
loaded = load_engine_object(UserInfo, dumped)
|
119
|
+
assert loaded == user
|
120
|
+
|
121
|
+
|
122
|
+
def test_namedtuple_roundtrip_via_dump_load() -> None:
|
123
|
+
p = LocalPoint(1, 2)
|
124
|
+
dumped = dump_engine_object(p)
|
125
|
+
assert dumped == {"x": 1, "y": 2}
|
126
|
+
loaded = load_engine_object(LocalPoint, dumped)
|
127
|
+
assert isinstance(loaded, LocalPoint)
|
128
|
+
assert loaded == p
|
129
|
+
|
130
|
+
|
131
|
+
def test_dataclass_missing_fields_with_auto_defaults() -> None:
|
132
|
+
"""Test that missing fields are automatically assigned safe default values."""
|
133
|
+
|
134
|
+
@dataclasses.dataclass
|
135
|
+
class TestClass:
|
136
|
+
required_field: str
|
137
|
+
optional_field: str | None # Should get None
|
138
|
+
list_field: list[str] # Should get []
|
139
|
+
dict_field: dict[str, int] # Should get {}
|
140
|
+
explicit_default: str = "default" # Should use explicit default
|
141
|
+
|
142
|
+
# Input missing optional_field, list_field, dict_field (but has explicit_default via class definition)
|
143
|
+
input_data = {"required_field": "test_value"}
|
144
|
+
|
145
|
+
loaded = load_engine_object(TestClass, input_data)
|
146
|
+
|
147
|
+
assert isinstance(loaded, TestClass)
|
148
|
+
assert loaded.required_field == "test_value"
|
149
|
+
assert loaded.optional_field is None # Auto-default for Optional
|
150
|
+
assert loaded.list_field == [] # Auto-default for list
|
151
|
+
assert loaded.dict_field == {} # Auto-default for dict
|
152
|
+
assert loaded.explicit_default == "default" # Explicit default from class
|
153
|
+
|
154
|
+
|
155
|
+
def test_namedtuple_missing_fields_with_auto_defaults() -> None:
|
156
|
+
"""Test that missing fields in NamedTuple are automatically assigned safe default values."""
|
157
|
+
from typing import NamedTuple
|
158
|
+
|
159
|
+
class TestTuple(NamedTuple):
|
160
|
+
required_field: str
|
161
|
+
optional_field: str | None # Should get None
|
162
|
+
list_field: list[str] # Should get []
|
163
|
+
dict_field: dict[str, int] # Should get {}
|
164
|
+
|
165
|
+
# Input missing optional_field, list_field, dict_field
|
166
|
+
input_data = {"required_field": "test_value"}
|
167
|
+
|
168
|
+
loaded = load_engine_object(TestTuple, input_data)
|
169
|
+
|
170
|
+
assert isinstance(loaded, TestTuple)
|
171
|
+
assert loaded.required_field == "test_value"
|
172
|
+
assert loaded.optional_field is None # Auto-default for Optional
|
173
|
+
assert loaded.list_field == [] # Auto-default for list
|
174
|
+
assert loaded.dict_field == {} # Auto-default for dict
|
175
|
+
|
176
|
+
|
177
|
+
def test_dataclass_unsupported_type_still_fails() -> None:
|
178
|
+
"""Test that fields with unsupported types still cause errors when missing."""
|
179
|
+
|
180
|
+
@dataclasses.dataclass
|
181
|
+
class TestClass:
|
182
|
+
required_field1: str
|
183
|
+
required_field2: int # No auto-default for int
|
184
|
+
|
185
|
+
# Input missing required_field2 which has no safe auto-default
|
186
|
+
input_data = {"required_field1": "test_value"}
|
187
|
+
|
188
|
+
# Should still raise an error because int has no safe auto-default
|
189
|
+
try:
|
190
|
+
load_engine_object(TestClass, input_data)
|
191
|
+
assert False, "Expected TypeError to be raised"
|
192
|
+
except TypeError:
|
193
|
+
pass # Expected behavior
|
194
|
+
|
195
|
+
|
196
|
+
def test_dump_vector_type_annotation_with_dim() -> None:
|
197
|
+
"""Test dumping a vector type annotation with a specified dimension."""
|
198
|
+
expected_dump = {
|
199
|
+
"type": {
|
200
|
+
"kind": "Vector",
|
201
|
+
"element_type": {"kind": "Float32"},
|
202
|
+
"dimension": 3,
|
203
|
+
}
|
204
|
+
}
|
205
|
+
assert dump_engine_object(Vector[np.float32, Literal[3]]) == expected_dump
|
206
|
+
|
207
|
+
|
208
|
+
def test_dump_vector_type_annotation_no_dim() -> None:
|
209
|
+
"""Test dumping a vector type annotation with no dimension."""
|
210
|
+
expected_dump_no_dim = {
|
211
|
+
"type": {
|
212
|
+
"kind": "Vector",
|
213
|
+
"element_type": {"kind": "Float64"},
|
214
|
+
"dimension": None,
|
215
|
+
}
|
216
|
+
}
|
217
|
+
assert dump_engine_object(Vector[np.float64]) == expected_dump_no_dim
|
218
|
+
|
219
|
+
|
220
|
+
@pytest.mark.skipif(not PYDANTIC_AVAILABLE, reason="Pydantic not available")
|
221
|
+
def test_pydantic_unsupported_type_still_fails() -> None:
|
222
|
+
"""Test that fields with unsupported types still cause errors when missing."""
|
223
|
+
|
224
|
+
class TestPydantic(pydantic.BaseModel):
|
225
|
+
required_field1: str
|
226
|
+
required_field2: int # No auto-default for int
|
227
|
+
optional_field: str | None
|
228
|
+
list_field: list[str]
|
229
|
+
dict_field: dict[str, int]
|
230
|
+
field_with_default: str = "default_value"
|
231
|
+
|
232
|
+
# Input missing required_field2 which has no safe auto-default
|
233
|
+
input_data = {"required_field1": "test_value"}
|
234
|
+
|
235
|
+
# Should still raise an error because int has no safe auto-default
|
236
|
+
with pytest.raises(pydantic.ValidationError):
|
237
|
+
load_engine_object(TestPydantic, input_data)
|
238
|
+
|
239
|
+
assert load_engine_object(
|
240
|
+
TestPydantic, {"required_field1": "test_value", "required_field2": 1}
|
241
|
+
) == TestPydantic(
|
242
|
+
required_field1="test_value",
|
243
|
+
required_field2=1,
|
244
|
+
field_with_default="default_value",
|
245
|
+
optional_field=None,
|
246
|
+
list_field=[],
|
247
|
+
dict_field={},
|
248
|
+
)
|
249
|
+
|
250
|
+
|
251
|
+
@pytest.mark.skipif(not PYDANTIC_AVAILABLE, reason="Pydantic not available")
|
252
|
+
def test_pydantic_field_descriptions() -> None:
|
253
|
+
"""Test that Pydantic field descriptions are extracted and included in schema."""
|
254
|
+
from pydantic import BaseModel, Field
|
255
|
+
|
256
|
+
class UserWithDescriptions(BaseModel):
|
257
|
+
"""A user model with field descriptions."""
|
258
|
+
|
259
|
+
name: str = Field(description="The user's full name")
|
260
|
+
age: int = Field(description="The user's age in years", ge=0, le=150)
|
261
|
+
email: str = Field(description="The user's email address")
|
262
|
+
is_active: bool = Field(
|
263
|
+
description="Whether the user account is active", default=True
|
264
|
+
)
|
265
|
+
|
266
|
+
# Test that field descriptions are extracted
|
267
|
+
encoded_schema = dump_engine_object(UserWithDescriptions)
|
268
|
+
|
269
|
+
# Check that the schema contains field descriptions
|
270
|
+
assert "fields" in encoded_schema["type"]
|
271
|
+
fields = encoded_schema["type"]["fields"]
|
272
|
+
|
273
|
+
# Find fields by name and check descriptions
|
274
|
+
field_descriptions = {field["name"]: field.get("description") for field in fields}
|
275
|
+
|
276
|
+
assert field_descriptions["name"] == "The user's full name"
|
277
|
+
assert field_descriptions["age"] == "The user's age in years"
|
278
|
+
assert field_descriptions["email"] == "The user's email address"
|
279
|
+
assert field_descriptions["is_active"] == "Whether the user account is active"
|
280
|
+
|
281
|
+
|
282
|
+
@pytest.mark.skipif(not PYDANTIC_AVAILABLE, reason="Pydantic not available")
|
283
|
+
def test_pydantic_field_descriptions_without_field() -> None:
|
284
|
+
"""Test that Pydantic models without field descriptions work correctly."""
|
285
|
+
from pydantic import BaseModel
|
286
|
+
|
287
|
+
class UserWithoutDescriptions(BaseModel):
|
288
|
+
"""A user model without field descriptions."""
|
289
|
+
|
290
|
+
name: str
|
291
|
+
age: int
|
292
|
+
email: str
|
293
|
+
|
294
|
+
# Test that the schema works without descriptions
|
295
|
+
encoded_schema = dump_engine_object(UserWithoutDescriptions)
|
296
|
+
|
297
|
+
# Check that the schema contains fields but no descriptions
|
298
|
+
assert "fields" in encoded_schema["type"]
|
299
|
+
fields = encoded_schema["type"]["fields"]
|
300
|
+
|
301
|
+
# Verify no descriptions are present
|
302
|
+
for field in fields:
|
303
|
+
assert "description" not in field or field["description"] is None
|
304
|
+
|
305
|
+
|
306
|
+
@pytest.mark.skipif(not PYDANTIC_AVAILABLE, reason="Pydantic not available")
|
307
|
+
def test_pydantic_mixed_descriptions() -> None:
|
308
|
+
"""Test Pydantic model with some fields having descriptions and others not."""
|
309
|
+
from pydantic import BaseModel, Field
|
310
|
+
|
311
|
+
class MixedDescriptions(BaseModel):
|
312
|
+
"""A model with mixed field descriptions."""
|
313
|
+
|
314
|
+
name: str = Field(description="The name field")
|
315
|
+
age: int # No description
|
316
|
+
email: str = Field(description="The email field")
|
317
|
+
active: bool # No description
|
318
|
+
|
319
|
+
# Test that only fields with descriptions have them in the schema
|
320
|
+
encoded_schema = dump_engine_object(MixedDescriptions)
|
321
|
+
|
322
|
+
assert "fields" in encoded_schema["type"]
|
323
|
+
fields = encoded_schema["type"]["fields"]
|
324
|
+
|
325
|
+
# Find fields by name and check descriptions
|
326
|
+
field_descriptions = {field["name"]: field.get("description") for field in fields}
|
327
|
+
|
328
|
+
assert field_descriptions["name"] == "The name field"
|
329
|
+
assert field_descriptions["age"] is None
|
330
|
+
assert field_descriptions["email"] == "The email field"
|
331
|
+
assert field_descriptions["active"] is None
|
@@ -8,9 +8,18 @@ import numpy as np
|
|
8
8
|
import pytest
|
9
9
|
from numpy.typing import NDArray
|
10
10
|
|
11
|
+
# Optional Pydantic support for testing
|
12
|
+
try:
|
13
|
+
from pydantic import BaseModel, Field
|
14
|
+
|
15
|
+
PYDANTIC_AVAILABLE = True
|
16
|
+
except ImportError:
|
17
|
+
BaseModel = None # type: ignore[misc,assignment]
|
18
|
+
Field = None # type: ignore[misc,assignment]
|
19
|
+
PYDANTIC_AVAILABLE = False
|
20
|
+
|
11
21
|
import cocoindex
|
12
|
-
from cocoindex.
|
13
|
-
dump_engine_object,
|
22
|
+
from cocoindex.engine_value import (
|
14
23
|
make_engine_value_encoder,
|
15
24
|
make_engine_value_decoder,
|
16
25
|
)
|
@@ -70,6 +79,29 @@ class CustomerNamedTuple(NamedTuple):
|
|
70
79
|
tags: list[Tag] | None = None
|
71
80
|
|
72
81
|
|
82
|
+
# Pydantic model definitions (if available)
|
83
|
+
if PYDANTIC_AVAILABLE:
|
84
|
+
|
85
|
+
class OrderPydantic(BaseModel):
|
86
|
+
order_id: str
|
87
|
+
name: str
|
88
|
+
price: float
|
89
|
+
extra_field: str = "default_extra"
|
90
|
+
|
91
|
+
class TagPydantic(BaseModel):
|
92
|
+
name: str
|
93
|
+
|
94
|
+
class CustomerPydantic(BaseModel):
|
95
|
+
name: str
|
96
|
+
order: OrderPydantic
|
97
|
+
tags: list[TagPydantic] | None = None
|
98
|
+
|
99
|
+
class NestedStructPydantic(BaseModel):
|
100
|
+
customer: CustomerPydantic
|
101
|
+
orders: list[OrderPydantic]
|
102
|
+
count: int = 0
|
103
|
+
|
104
|
+
|
73
105
|
def encode_engine_value(value: Any, type_hint: Type[Any] | str) -> Any:
|
74
106
|
"""
|
75
107
|
Encode a Python value to an engine value.
|
@@ -973,30 +1005,6 @@ def test_decode_error_non_nullable_or_non_list_vector() -> None:
|
|
973
1005
|
decoder("not a list")
|
974
1006
|
|
975
1007
|
|
976
|
-
def test_dump_vector_type_annotation_with_dim() -> None:
|
977
|
-
"""Test dumping a vector type annotation with a specified dimension."""
|
978
|
-
expected_dump = {
|
979
|
-
"type": {
|
980
|
-
"kind": "Vector",
|
981
|
-
"element_type": {"kind": "Float32"},
|
982
|
-
"dimension": 3,
|
983
|
-
}
|
984
|
-
}
|
985
|
-
assert dump_engine_object(Float32VectorType) == expected_dump
|
986
|
-
|
987
|
-
|
988
|
-
def test_dump_vector_type_annotation_no_dim() -> None:
|
989
|
-
"""Test dumping a vector type annotation with no dimension."""
|
990
|
-
expected_dump_no_dim = {
|
991
|
-
"type": {
|
992
|
-
"kind": "Vector",
|
993
|
-
"element_type": {"kind": "Float64"},
|
994
|
-
"dimension": None,
|
995
|
-
}
|
996
|
-
}
|
997
|
-
assert dump_engine_object(Float64VectorTypeNoDim) == expected_dump_no_dim
|
998
|
-
|
999
|
-
|
1000
1008
|
def test_full_roundtrip_vector_numeric_types() -> None:
|
1001
1009
|
"""Test full roundtrip for numeric vector types using NDArray."""
|
1002
1010
|
value_f32 = np.array([1.0, 2.0, 3.0], dtype=np.float32)
|
@@ -1566,3 +1574,119 @@ def test_auto_default_for_supported_and_unsupported_types() -> None:
|
|
1566
1574
|
match=r"Field 'b' \(type <class 'int'>\) without default value is missing in input: ",
|
1567
1575
|
):
|
1568
1576
|
build_engine_value_decoder(Base, UnsupportedField)
|
1577
|
+
|
1578
|
+
|
1579
|
+
# Pydantic model tests
|
1580
|
+
@pytest.mark.skipif(not PYDANTIC_AVAILABLE, reason="Pydantic not available")
|
1581
|
+
def test_pydantic_simple_struct() -> None:
|
1582
|
+
"""Test basic Pydantic model encoding and decoding."""
|
1583
|
+
order = OrderPydantic(order_id="O1", name="item1", price=10.0)
|
1584
|
+
validate_full_roundtrip(order, OrderPydantic)
|
1585
|
+
|
1586
|
+
|
1587
|
+
@pytest.mark.skipif(not PYDANTIC_AVAILABLE, reason="Pydantic not available")
|
1588
|
+
def test_pydantic_struct_with_defaults() -> None:
|
1589
|
+
"""Test Pydantic model with default values."""
|
1590
|
+
order = OrderPydantic(order_id="O1", name="item1", price=10.0)
|
1591
|
+
assert order.extra_field == "default_extra"
|
1592
|
+
validate_full_roundtrip(order, OrderPydantic)
|
1593
|
+
|
1594
|
+
|
1595
|
+
@pytest.mark.skipif(not PYDANTIC_AVAILABLE, reason="Pydantic not available")
|
1596
|
+
def test_pydantic_nested_struct() -> None:
|
1597
|
+
"""Test nested Pydantic models."""
|
1598
|
+
order = OrderPydantic(order_id="O1", name="item1", price=10.0)
|
1599
|
+
customer = CustomerPydantic(name="Alice", order=order)
|
1600
|
+
validate_full_roundtrip(customer, CustomerPydantic)
|
1601
|
+
|
1602
|
+
|
1603
|
+
@pytest.mark.skipif(not PYDANTIC_AVAILABLE, reason="Pydantic not available")
|
1604
|
+
def test_pydantic_struct_with_list() -> None:
|
1605
|
+
"""Test Pydantic model with list fields."""
|
1606
|
+
order = OrderPydantic(order_id="O1", name="item1", price=10.0)
|
1607
|
+
tags = [TagPydantic(name="vip"), TagPydantic(name="premium")]
|
1608
|
+
customer = CustomerPydantic(name="Alice", order=order, tags=tags)
|
1609
|
+
validate_full_roundtrip(customer, CustomerPydantic)
|
1610
|
+
|
1611
|
+
|
1612
|
+
@pytest.mark.skipif(not PYDANTIC_AVAILABLE, reason="Pydantic not available")
|
1613
|
+
def test_pydantic_complex_nested_struct() -> None:
|
1614
|
+
"""Test complex nested Pydantic structure."""
|
1615
|
+
order1 = OrderPydantic(order_id="O1", name="item1", price=10.0)
|
1616
|
+
order2 = OrderPydantic(order_id="O2", name="item2", price=20.0)
|
1617
|
+
customer = CustomerPydantic(
|
1618
|
+
name="Alice", order=order1, tags=[TagPydantic(name="vip")]
|
1619
|
+
)
|
1620
|
+
nested = NestedStructPydantic(customer=customer, orders=[order1, order2], count=2)
|
1621
|
+
validate_full_roundtrip(nested, NestedStructPydantic)
|
1622
|
+
|
1623
|
+
|
1624
|
+
@pytest.mark.skipif(not PYDANTIC_AVAILABLE, reason="Pydantic not available")
|
1625
|
+
def test_pydantic_struct_to_dict_binding() -> None:
|
1626
|
+
"""Test Pydantic model -> dict binding."""
|
1627
|
+
order = OrderPydantic(order_id="O1", name="item1", price=10.0, extra_field="custom")
|
1628
|
+
expected_dict = {
|
1629
|
+
"order_id": "O1",
|
1630
|
+
"name": "item1",
|
1631
|
+
"price": 10.0,
|
1632
|
+
"extra_field": "custom",
|
1633
|
+
}
|
1634
|
+
|
1635
|
+
validate_full_roundtrip(
|
1636
|
+
order,
|
1637
|
+
OrderPydantic,
|
1638
|
+
(expected_dict, Any),
|
1639
|
+
(expected_dict, dict),
|
1640
|
+
(expected_dict, dict[Any, Any]),
|
1641
|
+
(expected_dict, dict[str, Any]),
|
1642
|
+
)
|
1643
|
+
|
1644
|
+
|
1645
|
+
@pytest.mark.skipif(not PYDANTIC_AVAILABLE, reason="Pydantic not available")
|
1646
|
+
def test_make_engine_value_decoder_pydantic_struct() -> None:
|
1647
|
+
"""Test engine value decoder for Pydantic models."""
|
1648
|
+
engine_val = ["O1", "item1", 10.0, "default_extra"]
|
1649
|
+
decoder = build_engine_value_decoder(OrderPydantic)
|
1650
|
+
result = decoder(engine_val)
|
1651
|
+
|
1652
|
+
assert isinstance(result, OrderPydantic)
|
1653
|
+
assert result.order_id == "O1"
|
1654
|
+
assert result.name == "item1"
|
1655
|
+
assert result.price == 10.0
|
1656
|
+
assert result.extra_field == "default_extra"
|
1657
|
+
|
1658
|
+
|
1659
|
+
@pytest.mark.skipif(not PYDANTIC_AVAILABLE, reason="Pydantic not available")
|
1660
|
+
def test_make_engine_value_decoder_pydantic_nested() -> None:
|
1661
|
+
"""Test engine value decoder for nested Pydantic models."""
|
1662
|
+
engine_val = [
|
1663
|
+
"Alice",
|
1664
|
+
["O1", "item1", 10.0, "default_extra"],
|
1665
|
+
[["vip"]],
|
1666
|
+
]
|
1667
|
+
decoder = build_engine_value_decoder(CustomerPydantic)
|
1668
|
+
result = decoder(engine_val)
|
1669
|
+
|
1670
|
+
assert isinstance(result, CustomerPydantic)
|
1671
|
+
assert result.name == "Alice"
|
1672
|
+
assert isinstance(result.order, OrderPydantic)
|
1673
|
+
assert result.order.order_id == "O1"
|
1674
|
+
assert result.tags is not None
|
1675
|
+
assert len(result.tags) == 1
|
1676
|
+
assert isinstance(result.tags[0], TagPydantic)
|
1677
|
+
assert result.tags[0].name == "vip"
|
1678
|
+
|
1679
|
+
|
1680
|
+
@pytest.mark.skipif(not PYDANTIC_AVAILABLE, reason="Pydantic not available")
|
1681
|
+
def test_pydantic_mixed_with_dataclass() -> None:
|
1682
|
+
"""Test mixing Pydantic models with dataclasses."""
|
1683
|
+
|
1684
|
+
# Create a dataclass that uses a Pydantic model
|
1685
|
+
@dataclass
|
1686
|
+
class MixedStruct:
|
1687
|
+
name: str
|
1688
|
+
pydantic_order: OrderPydantic
|
1689
|
+
|
1690
|
+
order = OrderPydantic(order_id="O1", name="item1", price=10.0)
|
1691
|
+
mixed = MixedStruct(name="test", pydantic_order=order)
|
1692
|
+
validate_full_roundtrip(mixed, MixedStruct)
|