cocoindex 0.3.4__cp311-abi3-manylinux_2_28_x86_64.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 (42) hide show
  1. cocoindex/__init__.py +114 -0
  2. cocoindex/_engine.abi3.so +0 -0
  3. cocoindex/auth_registry.py +44 -0
  4. cocoindex/cli.py +830 -0
  5. cocoindex/engine_object.py +214 -0
  6. cocoindex/engine_value.py +550 -0
  7. cocoindex/flow.py +1281 -0
  8. cocoindex/functions/__init__.py +40 -0
  9. cocoindex/functions/_engine_builtin_specs.py +66 -0
  10. cocoindex/functions/colpali.py +247 -0
  11. cocoindex/functions/sbert.py +77 -0
  12. cocoindex/index.py +50 -0
  13. cocoindex/lib.py +75 -0
  14. cocoindex/llm.py +47 -0
  15. cocoindex/op.py +1047 -0
  16. cocoindex/py.typed +0 -0
  17. cocoindex/query_handler.py +57 -0
  18. cocoindex/runtime.py +78 -0
  19. cocoindex/setting.py +171 -0
  20. cocoindex/setup.py +92 -0
  21. cocoindex/sources/__init__.py +5 -0
  22. cocoindex/sources/_engine_builtin_specs.py +120 -0
  23. cocoindex/subprocess_exec.py +277 -0
  24. cocoindex/targets/__init__.py +5 -0
  25. cocoindex/targets/_engine_builtin_specs.py +153 -0
  26. cocoindex/targets/lancedb.py +466 -0
  27. cocoindex/tests/__init__.py +0 -0
  28. cocoindex/tests/test_engine_object.py +331 -0
  29. cocoindex/tests/test_engine_value.py +1724 -0
  30. cocoindex/tests/test_optional_database.py +249 -0
  31. cocoindex/tests/test_transform_flow.py +300 -0
  32. cocoindex/tests/test_typing.py +553 -0
  33. cocoindex/tests/test_validation.py +134 -0
  34. cocoindex/typing.py +834 -0
  35. cocoindex/user_app_loader.py +53 -0
  36. cocoindex/utils.py +20 -0
  37. cocoindex/validation.py +104 -0
  38. cocoindex-0.3.4.dist-info/METADATA +288 -0
  39. cocoindex-0.3.4.dist-info/RECORD +42 -0
  40. cocoindex-0.3.4.dist-info/WHEEL +4 -0
  41. cocoindex-0.3.4.dist-info/entry_points.txt +2 -0
  42. cocoindex-0.3.4.dist-info/licenses/THIRD_PARTY_NOTICES.html +13249 -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