cocoindex 0.1.73__cp313-cp313-manylinux_2_28_x86_64.whl → 0.1.74__cp313-cp313-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.
cocoindex/convert.py CHANGED
@@ -2,9 +2,12 @@
2
2
  Utilities to convert between Python and engine values.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  import dataclasses
6
8
  import datetime
7
9
  import inspect
10
+ import warnings
8
11
  from enum import Enum
9
12
  from typing import Any, Callable, Mapping, get_origin
10
13
 
@@ -29,6 +32,24 @@ from .typing import (
29
32
  )
30
33
 
31
34
 
35
+ class ChildFieldPath:
36
+ """Context manager to append a field to field_path on enter and pop it on exit."""
37
+
38
+ _field_path: list[str]
39
+ _field_name: str
40
+
41
+ def __init__(self, field_path: list[str], field_name: str):
42
+ self._field_path: list[str] = field_path
43
+ self._field_name = field_name
44
+
45
+ def __enter__(self) -> ChildFieldPath:
46
+ self._field_path.append(self._field_name)
47
+ return self
48
+
49
+ def __exit__(self, _exc_type: Any, _exc_val: Any, _exc_tb: Any) -> None:
50
+ self._field_path.pop()
51
+
52
+
32
53
  def encode_engine_value(value: Any) -> Any:
33
54
  """Encode a Python value to an engine value."""
34
55
  if dataclasses.is_dataclass(value):
@@ -73,7 +94,7 @@ def _is_type_kind_convertible_to(src_type_kind: str, dst_type_kind: str) -> bool
73
94
  def make_engine_value_decoder(
74
95
  field_path: list[str],
75
96
  src_type: dict[str, Any],
76
- dst_annotation: Any,
97
+ dst_type_info: AnalyzedTypeInfo,
77
98
  ) -> Callable[[Any], Any]:
78
99
  """
79
100
  Make a decoder from an engine value to a Python value.
@@ -89,7 +110,6 @@ def make_engine_value_decoder(
89
110
 
90
111
  src_type_kind = src_type["kind"]
91
112
 
92
- dst_type_info = analyze_type_info(dst_annotation)
93
113
  dst_type_variant = dst_type_info.variant
94
114
 
95
115
  if isinstance(dst_type_variant, AnalyzedUnknownType):
@@ -99,96 +119,97 @@ def make_engine_value_decoder(
99
119
  )
100
120
 
101
121
  if src_type_kind == "Struct":
102
- return _make_engine_struct_value_decoder(
122
+ return make_engine_struct_decoder(
103
123
  field_path,
104
124
  src_type["fields"],
105
125
  dst_type_info,
106
126
  )
107
127
 
108
128
  if src_type_kind in TABLE_TYPES:
109
- field_path.append("[*]")
110
- engine_fields_schema = src_type["row"]["fields"]
129
+ with ChildFieldPath(field_path, "[*]"):
130
+ engine_fields_schema = src_type["row"]["fields"]
111
131
 
112
- if src_type_kind == "LTable":
113
- if isinstance(dst_type_variant, AnalyzedAnyType):
114
- return _make_engine_ltable_to_list_dict_decoder(
115
- field_path, engine_fields_schema
116
- )
117
- if not isinstance(dst_type_variant, AnalyzedListType):
118
- raise ValueError(
119
- f"Type mismatch for `{''.join(field_path)}`: "
120
- f"declared `{dst_type_info.core_type}`, a list type expected"
132
+ if src_type_kind == "LTable":
133
+ if isinstance(dst_type_variant, AnalyzedAnyType):
134
+ return _make_engine_ltable_to_list_dict_decoder(
135
+ field_path, engine_fields_schema
136
+ )
137
+ if not isinstance(dst_type_variant, AnalyzedListType):
138
+ raise ValueError(
139
+ f"Type mismatch for `{''.join(field_path)}`: "
140
+ f"declared `{dst_type_info.core_type}`, a list type expected"
141
+ )
142
+ row_decoder = make_engine_struct_decoder(
143
+ field_path,
144
+ engine_fields_schema,
145
+ analyze_type_info(dst_type_variant.elem_type),
121
146
  )
122
- row_decoder = _make_engine_struct_value_decoder(
123
- field_path,
124
- engine_fields_schema,
125
- analyze_type_info(dst_type_variant.elem_type),
126
- )
127
147
 
128
- def decode(value: Any) -> Any | None:
129
- if value is None:
130
- return None
131
- return [row_decoder(v) for v in value]
148
+ def decode(value: Any) -> Any | None:
149
+ if value is None:
150
+ return None
151
+ return [row_decoder(v) for v in value]
132
152
 
133
- elif src_type_kind == "KTable":
134
- if isinstance(dst_type_variant, AnalyzedAnyType):
135
- return _make_engine_ktable_to_dict_dict_decoder(
136
- field_path, engine_fields_schema
153
+ elif src_type_kind == "KTable":
154
+ if isinstance(dst_type_variant, AnalyzedAnyType):
155
+ return _make_engine_ktable_to_dict_dict_decoder(
156
+ field_path, engine_fields_schema
157
+ )
158
+ if not isinstance(dst_type_variant, AnalyzedDictType):
159
+ raise ValueError(
160
+ f"Type mismatch for `{''.join(field_path)}`: "
161
+ f"declared `{dst_type_info.core_type}`, a dict type expected"
162
+ )
163
+
164
+ key_field_schema = engine_fields_schema[0]
165
+ field_path.append(f".{key_field_schema.get('name', KEY_FIELD_NAME)}")
166
+ key_decoder = make_engine_value_decoder(
167
+ field_path,
168
+ key_field_schema["type"],
169
+ analyze_type_info(dst_type_variant.key_type),
137
170
  )
138
- if not isinstance(dst_type_variant, AnalyzedDictType):
139
- raise ValueError(
140
- f"Type mismatch for `{''.join(field_path)}`: "
141
- f"declared `{dst_type_info.core_type}`, a dict type expected"
171
+ field_path.pop()
172
+ value_decoder = make_engine_struct_decoder(
173
+ field_path,
174
+ engine_fields_schema[1:],
175
+ analyze_type_info(dst_type_variant.value_type),
142
176
  )
143
177
 
144
- key_field_schema = engine_fields_schema[0]
145
- field_path.append(f".{key_field_schema.get('name', KEY_FIELD_NAME)}")
146
- key_decoder = make_engine_value_decoder(
147
- field_path, key_field_schema["type"], dst_type_variant.key_type
148
- )
149
- field_path.pop()
150
- value_decoder = _make_engine_struct_value_decoder(
151
- field_path,
152
- engine_fields_schema[1:],
153
- analyze_type_info(dst_type_variant.value_type),
154
- )
155
-
156
- def decode(value: Any) -> Any | None:
157
- if value is None:
158
- return None
159
- return {key_decoder(v[0]): value_decoder(v[1:]) for v in value}
178
+ def decode(value: Any) -> Any | None:
179
+ if value is None:
180
+ return None
181
+ return {key_decoder(v[0]): value_decoder(v[1:]) for v in value}
160
182
 
161
- field_path.pop()
162
183
  return decode
163
184
 
164
185
  if src_type_kind == "Union":
165
186
  if isinstance(dst_type_variant, AnalyzedAnyType):
166
187
  return lambda value: value[1]
167
188
 
168
- dst_type_variants = (
169
- dst_type_variant.variant_types
189
+ dst_type_info_variants = (
190
+ [analyze_type_info(t) for t in dst_type_variant.variant_types]
170
191
  if isinstance(dst_type_variant, AnalyzedUnionType)
171
- else [dst_annotation]
192
+ else [dst_type_info]
172
193
  )
173
194
  src_type_variants = src_type["types"]
174
195
  decoders = []
175
196
  for i, src_type_variant in enumerate(src_type_variants):
176
- src_field_path = field_path + [f"[{i}]"]
177
- decoder = None
178
- for dst_type_variant in dst_type_variants:
179
- try:
180
- decoder = make_engine_value_decoder(
181
- src_field_path, src_type_variant, dst_type_variant
197
+ with ChildFieldPath(field_path, f"[{i}]"):
198
+ decoder = None
199
+ for dst_type_info_variant in dst_type_info_variants:
200
+ try:
201
+ decoder = make_engine_value_decoder(
202
+ field_path, src_type_variant, dst_type_info_variant
203
+ )
204
+ break
205
+ except ValueError:
206
+ pass
207
+ if decoder is None:
208
+ raise ValueError(
209
+ f"Type mismatch for `{''.join(field_path)}`: "
210
+ f"cannot find matched target type for source type variant {src_type_variant}"
182
211
  )
183
- break
184
- except ValueError:
185
- pass
186
- if decoder is None:
187
- raise ValueError(
188
- f"Type mismatch for `{''.join(field_path)}`: "
189
- f"cannot find matched target type for source type variant {src_type_variant}"
190
- )
191
- decoders.append(decoder)
212
+ decoders.append(decoder)
192
213
  return lambda value: decoders[value[0]](value[1])
193
214
 
194
215
  if isinstance(dst_type_variant, AnalyzedAnyType):
@@ -216,7 +237,7 @@ def make_engine_value_decoder(
216
237
  vec_elem_decoder = make_engine_value_decoder(
217
238
  field_path + ["[*]"],
218
239
  src_type["element_type"],
219
- dst_type_variant and dst_type_variant.elem_type,
240
+ analyze_type_info(dst_type_variant and dst_type_variant.elem_type),
220
241
  )
221
242
 
222
243
  def decode_vector(value: Any) -> Any | None:
@@ -247,7 +268,7 @@ def make_engine_value_decoder(
247
268
  if not _is_type_kind_convertible_to(src_type_kind, dst_type_variant.kind):
248
269
  raise ValueError(
249
270
  f"Type mismatch for `{''.join(field_path)}`: "
250
- f"passed in {src_type_kind}, declared {dst_annotation} ({dst_type_variant.kind})"
271
+ f"passed in {src_type_kind}, declared {dst_type_info.core_type} ({dst_type_variant.kind})"
251
272
  )
252
273
 
253
274
  if dst_type_variant.kind in ("Float32", "Float64", "Int64"):
@@ -267,7 +288,31 @@ def make_engine_value_decoder(
267
288
  return lambda value: value
268
289
 
269
290
 
270
- def _make_engine_struct_value_decoder(
291
+ def _get_auto_default_for_type(
292
+ type_info: AnalyzedTypeInfo,
293
+ ) -> tuple[Any, bool]:
294
+ """
295
+ Get an auto-default value for a type annotation if it's safe to do so.
296
+
297
+ Returns:
298
+ A tuple of (default_value, is_supported) where:
299
+ - default_value: The default value if auto-defaulting is supported
300
+ - is_supported: True if auto-defaulting is supported for this type
301
+ """
302
+ # Case 1: Nullable types (Optional[T] or T | None)
303
+ if type_info.nullable:
304
+ return None, True
305
+
306
+ # Case 2: Table types (KTable or LTable) - check if it's a list or dict type
307
+ if isinstance(type_info.variant, AnalyzedListType):
308
+ return [], True
309
+ elif isinstance(type_info.variant, AnalyzedDictType):
310
+ return {}, True
311
+
312
+ return None, False
313
+
314
+
315
+ def make_engine_struct_decoder(
271
316
  field_path: list[str],
272
317
  src_fields: list[dict[str, Any]],
273
318
  dst_type_info: AnalyzedTypeInfo,
@@ -321,32 +366,39 @@ def _make_engine_struct_value_decoder(
321
366
  else:
322
367
  raise ValueError(f"Unsupported struct type: {dst_struct_type}")
323
368
 
324
- def make_closure_for_value(
369
+ def make_closure_for_field(
325
370
  name: str, param: inspect.Parameter
326
371
  ) -> Callable[[list[Any]], Any]:
327
372
  src_idx = src_name_to_idx.get(name)
328
- if src_idx is not None:
329
- field_path.append(f".{name}")
330
- field_decoder = make_engine_value_decoder(
331
- field_path, src_fields[src_idx]["type"], param.annotation
332
- )
333
- field_path.pop()
334
- return (
335
- lambda values: field_decoder(values[src_idx])
336
- if len(values) > src_idx
337
- else param.default
338
- )
373
+ type_info = analyze_type_info(param.annotation)
374
+
375
+ with ChildFieldPath(field_path, f".{name}"):
376
+ if src_idx is not None:
377
+ field_decoder = make_engine_value_decoder(
378
+ field_path, src_fields[src_idx]["type"], type_info
379
+ )
380
+ return lambda values: field_decoder(values[src_idx])
381
+
382
+ default_value = param.default
383
+ if default_value is not inspect.Parameter.empty:
384
+ return lambda _: default_value
385
+
386
+ auto_default, is_supported = _get_auto_default_for_type(type_info)
387
+ if is_supported:
388
+ warnings.warn(
389
+ f"Field '{name}' (type {param.annotation}) without default value is missing in input: "
390
+ f"{''.join(field_path)}. Auto-assigning default value: {auto_default}",
391
+ UserWarning,
392
+ stacklevel=4,
393
+ )
394
+ return lambda _: auto_default
339
395
 
340
- default_value = param.default
341
- if default_value is inspect.Parameter.empty:
342
396
  raise ValueError(
343
- f"Field without default value is missing in input: {''.join(field_path)}"
397
+ f"Field '{name}' (type {param.annotation}) without default value is missing in input: {''.join(field_path)}"
344
398
  )
345
399
 
346
- return lambda _: default_value
347
-
348
400
  field_value_decoder = [
349
- make_closure_for_value(name, param) for (name, param) in parameters.items()
401
+ make_closure_for_field(name, param) for (name, param) in parameters.items()
350
402
  ]
351
403
 
352
404
  return lambda values: dst_struct_type(
@@ -363,13 +415,12 @@ def _make_engine_struct_to_dict_decoder(
363
415
  field_decoders = []
364
416
  for i, field_schema in enumerate(src_fields):
365
417
  field_name = field_schema["name"]
366
- field_path.append(f".{field_name}")
367
- field_decoder = make_engine_value_decoder(
368
- field_path,
369
- field_schema["type"],
370
- Any, # Use Any for recursive decoding
371
- )
372
- field_path.pop()
418
+ with ChildFieldPath(field_path, f".{field_name}"):
419
+ field_decoder = make_engine_value_decoder(
420
+ field_path,
421
+ field_schema["type"],
422
+ analyze_type_info(Any), # Use Any for recursive decoding
423
+ )
373
424
  field_decoders.append((field_name, field_decoder))
374
425
 
375
426
  def decode_to_dict(values: list[Any] | None) -> dict[str, Any] | None:
@@ -426,9 +477,10 @@ def _make_engine_ktable_to_dict_dict_decoder(
426
477
  value_fields_schema = src_fields[1:]
427
478
 
428
479
  # Create decoders
429
- field_path.append(f".{key_field_schema.get('name', KEY_FIELD_NAME)}")
430
- key_decoder = make_engine_value_decoder(field_path, key_field_schema["type"], Any)
431
- field_path.pop()
480
+ with ChildFieldPath(field_path, f".{key_field_schema.get('name', KEY_FIELD_NAME)}"):
481
+ key_decoder = make_engine_value_decoder(
482
+ field_path, key_field_schema["type"], analyze_type_info(Any)
483
+ )
432
484
 
433
485
  value_decoder = _make_engine_struct_to_dict_decoder(field_path, value_fields_schema)
434
486
 
cocoindex/flow.py CHANGED
@@ -16,6 +16,7 @@ from .validation import (
16
16
  validate_full_flow_name,
17
17
  validate_target_name,
18
18
  )
19
+ from .typing import analyze_type_info
19
20
 
20
21
  from dataclasses import dataclass
21
22
  from enum import Enum
@@ -1053,7 +1054,7 @@ class TransformFlow(Generic[T]):
1053
1054
  sig.return_annotation
1054
1055
  )
1055
1056
  result_decoder = make_engine_value_decoder(
1056
- [], engine_return_type["type"], python_return_type
1057
+ [], engine_return_type["type"], analyze_type_info(python_return_type)
1057
1058
  )
1058
1059
 
1059
1060
  return TransformFlowInfo(engine_flow, result_decoder)
cocoindex/op.py CHANGED
@@ -6,11 +6,31 @@ import asyncio
6
6
  import dataclasses
7
7
  import inspect
8
8
  from enum import Enum
9
- from typing import Any, Awaitable, Callable, Protocol, dataclass_transform, Annotated
9
+ from typing import (
10
+ Any,
11
+ Awaitable,
12
+ Callable,
13
+ Protocol,
14
+ dataclass_transform,
15
+ Annotated,
16
+ get_args,
17
+ )
10
18
 
11
19
  from . import _engine # type: ignore
12
- from .convert import encode_engine_value, make_engine_value_decoder
13
- from .typing import TypeAttr, encode_enriched_type, resolve_forward_ref
20
+ from .convert import (
21
+ encode_engine_value,
22
+ make_engine_value_decoder,
23
+ make_engine_struct_decoder,
24
+ )
25
+ from .typing import (
26
+ TypeAttr,
27
+ encode_enriched_type,
28
+ resolve_forward_ref,
29
+ analyze_type_info,
30
+ AnalyzedAnyType,
31
+ AnalyzedBasicType,
32
+ AnalyzedDictType,
33
+ )
14
34
 
15
35
 
16
36
  class OpCategory(Enum):
@@ -65,6 +85,22 @@ class Executor(Protocol):
65
85
  op_category: OpCategory
66
86
 
67
87
 
88
+ def _load_spec_from_engine(spec_cls: type, spec: dict[str, Any]) -> Any:
89
+ """
90
+ Load a spec from the engine.
91
+ """
92
+ return spec_cls(**spec)
93
+
94
+
95
+ def _get_required_method(cls: type, name: str) -> Callable[..., Any]:
96
+ method = getattr(cls, name, None)
97
+ if method is None:
98
+ raise ValueError(f"Method {name}() is required for {cls.__name__}")
99
+ if not inspect.isfunction(method):
100
+ raise ValueError(f"Method {cls.__name__}.{name}() is not a function")
101
+ return method
102
+
103
+
68
104
  class _FunctionExecutorFactory:
69
105
  _spec_cls: type
70
106
  _executor_cls: type
@@ -76,7 +112,7 @@ class _FunctionExecutorFactory:
76
112
  def __call__(
77
113
  self, spec: dict[str, Any], *args: Any, **kwargs: Any
78
114
  ) -> tuple[dict[str, Any], Executor]:
79
- spec = self._spec_cls(**spec)
115
+ spec = _load_spec_from_engine(self._spec_cls, spec)
80
116
  executor = self._executor_cls(spec)
81
117
  result_type = executor.analyze(*args, **kwargs)
82
118
  return (encode_enriched_type(result_type), executor)
@@ -185,7 +221,9 @@ def _register_op_factory(
185
221
  )
186
222
  self._args_decoders.append(
187
223
  make_engine_value_decoder(
188
- [arg_name], arg.value_type["type"], arg_param.annotation
224
+ [arg_name],
225
+ arg.value_type["type"],
226
+ analyze_type_info(arg_param.annotation),
189
227
  )
190
228
  )
191
229
  process_attribute(arg_name, arg)
@@ -217,7 +255,9 @@ def _register_op_factory(
217
255
  )
218
256
  arg_param = expected_arg[1]
219
257
  self._kwargs_decoders[kwarg_name] = make_engine_value_decoder(
220
- [kwarg_name], kwarg.value_type["type"], arg_param.annotation
258
+ [kwarg_name],
259
+ kwarg.value_type["type"],
260
+ analyze_type_info(arg_param.annotation),
221
261
  )
222
262
  process_attribute(kwarg_name, kwarg)
223
263
 
@@ -359,3 +399,220 @@ def function(**args: Any) -> Callable[[Callable[..., Any]], FunctionSpec]:
359
399
  return _Spec()
360
400
 
361
401
  return _inner
402
+
403
+
404
+ ########################################################
405
+ # Custom target connector
406
+ ########################################################
407
+
408
+
409
+ @dataclasses.dataclass
410
+ class _TargetConnectorContext:
411
+ target_name: str
412
+ spec: Any
413
+ prepared_spec: Any
414
+ key_decoder: Callable[[Any], Any]
415
+ value_decoder: Callable[[Any], Any]
416
+
417
+
418
+ class _TargetConnector:
419
+ """
420
+ The connector class passed to the engine.
421
+ """
422
+
423
+ _spec_cls: type
424
+ _connector_cls: type
425
+
426
+ _get_persistent_key_fn: Callable[[_TargetConnectorContext, str], Any]
427
+ _apply_setup_change_async_fn: Callable[
428
+ [Any, dict[str, Any] | None, dict[str, Any] | None], Awaitable[None]
429
+ ]
430
+ _mutate_async_fn: Callable[..., Awaitable[None]]
431
+ _mutatation_type: AnalyzedDictType | None
432
+
433
+ def __init__(self, spec_cls: type, connector_cls: type):
434
+ self._spec_cls = spec_cls
435
+ self._connector_cls = connector_cls
436
+
437
+ self._get_persistent_key_fn = _get_required_method(
438
+ connector_cls, "get_persistent_key"
439
+ )
440
+ self._apply_setup_change_async_fn = _to_async_call(
441
+ _get_required_method(connector_cls, "apply_setup_change")
442
+ )
443
+
444
+ mutate_fn = _get_required_method(connector_cls, "mutate")
445
+ self._mutate_async_fn = _to_async_call(mutate_fn)
446
+
447
+ # Store the type annotation for later use
448
+ self._mutatation_type = self._analyze_mutate_mutation_type(
449
+ connector_cls, mutate_fn
450
+ )
451
+
452
+ @staticmethod
453
+ def _analyze_mutate_mutation_type(
454
+ connector_cls: type, mutate_fn: Callable[..., Any]
455
+ ) -> AnalyzedDictType | None:
456
+ # Validate mutate_fn signature and extract type annotation
457
+ mutate_sig = inspect.signature(mutate_fn)
458
+ params = list(mutate_sig.parameters.values())
459
+
460
+ if len(params) != 1:
461
+ raise ValueError(
462
+ f"Method {connector_cls.__name__}.mutate(*args) must have exactly one parameter, "
463
+ f"got {len(params)}"
464
+ )
465
+
466
+ param = params[0]
467
+ if param.kind != inspect.Parameter.VAR_POSITIONAL:
468
+ raise ValueError(
469
+ f"Method {connector_cls.__name__}.mutate(*args) parameter must be *args format, "
470
+ f"got {param.kind.name}"
471
+ )
472
+
473
+ # Extract type annotation
474
+ analyzed_args_type = analyze_type_info(param.annotation)
475
+ if isinstance(analyzed_args_type.variant, AnalyzedAnyType):
476
+ return None
477
+
478
+ if analyzed_args_type.base_type is tuple:
479
+ args = get_args(analyzed_args_type.core_type)
480
+ if not args:
481
+ return None
482
+ if len(args) == 2:
483
+ mutation_type = analyze_type_info(args[1])
484
+ if isinstance(mutation_type.variant, AnalyzedAnyType):
485
+ return None
486
+ if isinstance(mutation_type.variant, AnalyzedDictType):
487
+ return mutation_type.variant
488
+
489
+ raise ValueError(
490
+ f"Method {connector_cls.__name__}.mutate(*args) parameter must be a tuple with "
491
+ f"2 elements (tuple[SpecType, dict[str, ValueStruct]], spec and mutation in dict), "
492
+ "got {args_type}"
493
+ )
494
+
495
+ def create_export_context(
496
+ self,
497
+ name: str,
498
+ spec: dict[str, Any],
499
+ key_fields_schema: list[Any],
500
+ value_fields_schema: list[Any],
501
+ ) -> _TargetConnectorContext:
502
+ key_annotation, value_annotation = (
503
+ (
504
+ self._mutatation_type.key_type,
505
+ self._mutatation_type.value_type,
506
+ )
507
+ if self._mutatation_type is not None
508
+ else (None, None)
509
+ )
510
+
511
+ key_type_info = analyze_type_info(key_annotation)
512
+ if (
513
+ len(key_fields_schema) == 1
514
+ and key_fields_schema[0]["type"]["kind"] != "Struct"
515
+ and isinstance(key_type_info.variant, (AnalyzedAnyType, AnalyzedBasicType))
516
+ ):
517
+ # Special case for ease of use: single key column can be mapped to a basic type without the wrapper struct.
518
+ key_decoder = make_engine_value_decoder(
519
+ ["(key)"],
520
+ key_fields_schema[0]["type"],
521
+ key_type_info,
522
+ )
523
+ else:
524
+ key_decoder = make_engine_struct_decoder(
525
+ ["(key)"], key_fields_schema, key_type_info
526
+ )
527
+
528
+ value_decoder = make_engine_struct_decoder(
529
+ ["(value)"], value_fields_schema, analyze_type_info(value_annotation)
530
+ )
531
+
532
+ loaded_spec = _load_spec_from_engine(self._spec_cls, spec)
533
+ prepare_method = getattr(self._connector_cls, "prepare", None)
534
+ if prepare_method is None:
535
+ prepared_spec = loaded_spec
536
+ else:
537
+ prepared_spec = prepare_method(loaded_spec)
538
+
539
+ return _TargetConnectorContext(
540
+ target_name=name,
541
+ spec=loaded_spec,
542
+ prepared_spec=prepared_spec,
543
+ key_decoder=key_decoder,
544
+ value_decoder=value_decoder,
545
+ )
546
+
547
+ def get_persistent_key(self, export_context: _TargetConnectorContext) -> Any:
548
+ return self._get_persistent_key_fn(
549
+ export_context.spec, export_context.target_name
550
+ )
551
+
552
+ def describe_resource(self, key: Any) -> str:
553
+ describe_fn = getattr(self._connector_cls, "describe", None)
554
+ if describe_fn is None:
555
+ return str(key)
556
+ return str(describe_fn(key))
557
+
558
+ async def apply_setup_changes_async(
559
+ self,
560
+ changes: list[tuple[Any, list[dict[str, Any] | None], dict[str, Any] | None]],
561
+ ) -> None:
562
+ for key, previous, current in changes:
563
+ prev_specs = [
564
+ _load_spec_from_engine(self._spec_cls, spec)
565
+ if spec is not None
566
+ else None
567
+ for spec in previous
568
+ ]
569
+ curr_spec = (
570
+ _load_spec_from_engine(self._spec_cls, current)
571
+ if current is not None
572
+ else None
573
+ )
574
+ for prev_spec in prev_specs:
575
+ await self._apply_setup_change_async_fn(key, prev_spec, curr_spec)
576
+
577
+ @staticmethod
578
+ def _decode_mutation(
579
+ context: _TargetConnectorContext, mutation: list[tuple[Any, Any | None]]
580
+ ) -> tuple[Any, dict[Any, Any | None]]:
581
+ return (
582
+ context.prepared_spec,
583
+ {
584
+ context.key_decoder(key): (
585
+ context.value_decoder(value) if value is not None else None
586
+ )
587
+ for key, value in mutation
588
+ },
589
+ )
590
+
591
+ async def mutate_async(
592
+ self,
593
+ mutations: list[tuple[_TargetConnectorContext, list[tuple[Any, Any | None]]]],
594
+ ) -> None:
595
+ await self._mutate_async_fn(
596
+ *(
597
+ self._decode_mutation(context, mutation)
598
+ for context, mutation in mutations
599
+ )
600
+ )
601
+
602
+
603
+ def target_connector(spec_cls: type) -> Callable[[type], type]:
604
+ """
605
+ Decorate a class to provide a target connector for an op.
606
+ """
607
+
608
+ # Validate the spec_cls is a TargetSpec.
609
+ if not issubclass(spec_cls, TargetSpec):
610
+ raise ValueError(f"Expect a TargetSpec, got {spec_cls}")
611
+
612
+ # Register the target connector.
613
+ def _inner(connector_cls: type) -> type:
614
+ connector = _TargetConnector(spec_cls, connector_cls)
615
+ _engine.register_target_connector(spec_cls.__name__, connector)
616
+ return connector_cls
617
+
618
+ return _inner
@@ -1,6 +1,6 @@
1
1
  import datetime
2
2
  import uuid
3
- from dataclasses import dataclass, make_dataclass
3
+ from dataclasses import dataclass, make_dataclass, field
4
4
  from typing import Annotated, Any, Callable, Literal, NamedTuple
5
5
 
6
6
  import numpy as np
@@ -19,6 +19,7 @@ from cocoindex.typing import (
19
19
  TypeKind,
20
20
  Vector,
21
21
  encode_enriched_type,
22
+ analyze_type_info,
22
23
  )
23
24
 
24
25
 
@@ -75,7 +76,9 @@ def build_engine_value_decoder(
75
76
  If python_type is not specified, uses engine_type_in_py as the target.
76
77
  """
77
78
  engine_type = encode_enriched_type(engine_type_in_py)["type"]
78
- return make_engine_value_decoder([], engine_type, python_type or engine_type_in_py)
79
+ return make_engine_value_decoder(
80
+ [], engine_type, analyze_type_info(python_type or engine_type_in_py)
81
+ )
79
82
 
80
83
 
81
84
  def validate_full_roundtrip_to(
@@ -103,7 +106,9 @@ def validate_full_roundtrip_to(
103
106
  )
104
107
 
105
108
  for other_value, other_type in decoded_values:
106
- decoder = make_engine_value_decoder([], encoded_output_type, other_type)
109
+ decoder = make_engine_value_decoder(
110
+ [], encoded_output_type, analyze_type_info(other_type)
111
+ )
107
112
  other_decoded_value = decoder(value_from_engine)
108
113
  assert eq(other_decoded_value, other_value), (
109
114
  f"Expected {other_value} but got {other_decoded_value} for {other_type}"
@@ -364,7 +369,9 @@ def test_decode_scalar_numpy_values() -> None:
364
369
  ({"kind": "Float64"}, np.float64, 2.718, np.float64(2.718)),
365
370
  ]
366
371
  for src_type, dst_type, input_value, expected in test_cases:
367
- decoder = make_engine_value_decoder(["field"], src_type, dst_type)
372
+ decoder = make_engine_value_decoder(
373
+ ["field"], src_type, analyze_type_info(dst_type)
374
+ )
368
375
  result = decoder(input_value)
369
376
  assert isinstance(result, dst_type)
370
377
  assert result == expected
@@ -378,7 +385,9 @@ def test_non_ndarray_vector_decoding() -> None:
378
385
  "dimension": None,
379
386
  }
380
387
  dst_type_float = list[np.float64]
381
- decoder = make_engine_value_decoder(["field"], src_type, dst_type_float)
388
+ decoder = make_engine_value_decoder(
389
+ ["field"], src_type, analyze_type_info(dst_type_float)
390
+ )
382
391
  input_numbers = [1.0, 2.0, 3.0]
383
392
  result = decoder(input_numbers)
384
393
  assert isinstance(result, list)
@@ -388,7 +397,9 @@ def test_non_ndarray_vector_decoding() -> None:
388
397
  # Test list[Uuid]
389
398
  src_type = {"kind": "Vector", "element_type": {"kind": "Uuid"}, "dimension": None}
390
399
  dst_type_uuid = list[uuid.UUID]
391
- decoder = make_engine_value_decoder(["field"], src_type, dst_type_uuid)
400
+ decoder = make_engine_value_decoder(
401
+ ["field"], src_type, analyze_type_info(dst_type_uuid)
402
+ )
392
403
  uuid1 = uuid.uuid4()
393
404
  uuid2 = uuid.uuid4()
394
405
  input_uuids = [uuid1, uuid2]
@@ -398,124 +409,15 @@ def test_non_ndarray_vector_decoding() -> None:
398
409
  assert result == [uuid1, uuid2]
399
410
 
400
411
 
401
- @pytest.mark.parametrize(
402
- "data_type, engine_val, expected",
403
- [
404
- # All fields match (dataclass)
405
- (
406
- Order,
407
- ["O123", "mixed nuts", 25.0, "default_extra"],
408
- Order("O123", "mixed nuts", 25.0, "default_extra"),
409
- ),
410
- # All fields match (NamedTuple)
411
- (
412
- OrderNamedTuple,
413
- ["O123", "mixed nuts", 25.0, "default_extra"],
414
- OrderNamedTuple("O123", "mixed nuts", 25.0, "default_extra"),
415
- ),
416
- # Extra field in engine value (should ignore extra)
417
- (
418
- Order,
419
- ["O123", "mixed nuts", 25.0, "default_extra", "unexpected"],
420
- Order("O123", "mixed nuts", 25.0, "default_extra"),
421
- ),
422
- (
423
- OrderNamedTuple,
424
- ["O123", "mixed nuts", 25.0, "default_extra", "unexpected"],
425
- OrderNamedTuple("O123", "mixed nuts", 25.0, "default_extra"),
426
- ),
427
- # Fewer fields in engine value (should fill with default)
428
- (
429
- Order,
430
- ["O123", "mixed nuts", 0.0, "default_extra"],
431
- Order("O123", "mixed nuts", 0.0, "default_extra"),
432
- ),
433
- (
434
- OrderNamedTuple,
435
- ["O123", "mixed nuts", 0.0, "default_extra"],
436
- OrderNamedTuple("O123", "mixed nuts", 0.0, "default_extra"),
437
- ),
438
- # More fields in engine value (should ignore extra)
439
- (
440
- Order,
441
- ["O123", "mixed nuts", 25.0, "unexpected"],
442
- Order("O123", "mixed nuts", 25.0, "unexpected"),
443
- ),
444
- (
445
- OrderNamedTuple,
446
- ["O123", "mixed nuts", 25.0, "unexpected"],
447
- OrderNamedTuple("O123", "mixed nuts", 25.0, "unexpected"),
448
- ),
449
- # Truly extra field (should ignore the fifth field)
450
- (
451
- Order,
452
- ["O123", "mixed nuts", 25.0, "default_extra", "ignored"],
453
- Order("O123", "mixed nuts", 25.0, "default_extra"),
454
- ),
455
- (
456
- OrderNamedTuple,
457
- ["O123", "mixed nuts", 25.0, "default_extra", "ignored"],
458
- OrderNamedTuple("O123", "mixed nuts", 25.0, "default_extra"),
459
- ),
460
- # Missing optional field in engine value (tags=None)
461
- (
462
- Customer,
463
- ["Alice", ["O1", "item1", 10.0, "default_extra"], None],
464
- Customer("Alice", Order("O1", "item1", 10.0, "default_extra"), None),
465
- ),
466
- (
467
- CustomerNamedTuple,
468
- ["Alice", ["O1", "item1", 10.0, "default_extra"], None],
469
- CustomerNamedTuple(
470
- "Alice", OrderNamedTuple("O1", "item1", 10.0, "default_extra"), None
471
- ),
472
- ),
473
- # Extra field in engine value for Customer (should ignore)
474
- (
475
- Customer,
476
- ["Alice", ["O1", "item1", 10.0, "default_extra"], [["vip"]], "extra"],
477
- Customer(
478
- "Alice", Order("O1", "item1", 10.0, "default_extra"), [Tag("vip")]
479
- ),
480
- ),
481
- (
482
- CustomerNamedTuple,
483
- ["Alice", ["O1", "item1", 10.0, "default_extra"], [["vip"]], "extra"],
484
- CustomerNamedTuple(
485
- "Alice",
486
- OrderNamedTuple("O1", "item1", 10.0, "default_extra"),
487
- [Tag("vip")],
488
- ),
489
- ),
490
- # Missing optional field with default
491
- (
492
- Order,
493
- ["O123", "mixed nuts", 25.0],
494
- Order("O123", "mixed nuts", 25.0, "default_extra"),
495
- ),
496
- (
497
- OrderNamedTuple,
498
- ["O123", "mixed nuts", 25.0],
499
- OrderNamedTuple("O123", "mixed nuts", 25.0, "default_extra"),
500
- ),
501
- # Partial optional fields
502
- (
503
- Customer,
504
- ["Alice", ["O1", "item1", 10.0]],
505
- Customer("Alice", Order("O1", "item1", 10.0, "default_extra"), None),
506
- ),
507
- (
508
- CustomerNamedTuple,
509
- ["Alice", ["O1", "item1", 10.0]],
510
- CustomerNamedTuple(
511
- "Alice", OrderNamedTuple("O1", "item1", 10.0, "default_extra"), None
512
- ),
513
- ),
514
- ],
515
- )
516
- def test_struct_decoder_cases(data_type: Any, engine_val: Any, expected: Any) -> None:
517
- decoder = build_engine_value_decoder(data_type)
518
- assert decoder(engine_val) == expected
412
+ def test_roundtrip_struct() -> None:
413
+ validate_full_roundtrip(
414
+ Order("O123", "mixed nuts", 25.0, "default_extra"),
415
+ Order,
416
+ )
417
+ validate_full_roundtrip(
418
+ OrderNamedTuple("O123", "mixed nuts", 25.0, "default_extra"),
419
+ OrderNamedTuple,
420
+ )
519
421
 
520
422
 
521
423
  def test_make_engine_value_decoder_list_of_struct() -> None:
@@ -974,7 +876,9 @@ def test_decode_nullable_ndarray_none_or_value_input() -> None:
974
876
  "dimension": None,
975
877
  }
976
878
  dst_annotation = NDArrayFloat32Type | None
977
- decoder = make_engine_value_decoder([], src_type_dict, dst_annotation)
879
+ decoder = make_engine_value_decoder(
880
+ [], src_type_dict, analyze_type_info(dst_annotation)
881
+ )
978
882
 
979
883
  none_engine_value = None
980
884
  decoded_array = decoder(none_engine_value)
@@ -997,7 +901,9 @@ def test_decode_vector_string() -> None:
997
901
  "element_type": {"kind": "Str"},
998
902
  "dimension": None,
999
903
  }
1000
- decoder = make_engine_value_decoder([], src_type_dict, Vector[str])
904
+ decoder = make_engine_value_decoder(
905
+ [], src_type_dict, analyze_type_info(Vector[str])
906
+ )
1001
907
  assert decoder(["hello", "world"]) == ["hello", "world"]
1002
908
 
1003
909
 
@@ -1008,7 +914,9 @@ def test_decode_error_non_nullable_or_non_list_vector() -> None:
1008
914
  "element_type": {"kind": "Float32"},
1009
915
  "dimension": None,
1010
916
  }
1011
- decoder = make_engine_value_decoder([], src_type_dict, NDArrayFloat32Type)
917
+ decoder = make_engine_value_decoder(
918
+ [], src_type_dict, analyze_type_info(NDArrayFloat32Type)
919
+ )
1012
920
  with pytest.raises(ValueError, match="Received null for non-nullable vector"):
1013
921
  decoder(None)
1014
922
  with pytest.raises(TypeError, match="Expected NDArray or list for vector"):
@@ -1489,3 +1397,41 @@ def test_roundtrip_ktable_with_list_fields() -> None:
1489
1397
 
1490
1398
  # Test Any annotation
1491
1399
  validate_full_roundtrip(teams, dict[str, Team], (expected_dict_dict, Any))
1400
+
1401
+
1402
+ def test_auto_default_for_supported_and_unsupported_types() -> None:
1403
+ @dataclass
1404
+ class Base:
1405
+ a: int
1406
+
1407
+ @dataclass
1408
+ class NullableField:
1409
+ a: int
1410
+ b: int | None
1411
+
1412
+ @dataclass
1413
+ class LTableField:
1414
+ a: int
1415
+ b: list[Base]
1416
+
1417
+ @dataclass
1418
+ class KTableField:
1419
+ a: int
1420
+ b: dict[str, Base]
1421
+
1422
+ @dataclass
1423
+ class UnsupportedField:
1424
+ a: int
1425
+ b: int
1426
+
1427
+ validate_full_roundtrip(NullableField(1, None), NullableField)
1428
+
1429
+ validate_full_roundtrip(LTableField(1, []), LTableField)
1430
+
1431
+ # validate_full_roundtrip(KTableField(1, {}), KTableField)
1432
+
1433
+ with pytest.raises(
1434
+ ValueError,
1435
+ match=r"Field 'b' \(type <class 'int'>\) without default value is missing in input: ",
1436
+ ):
1437
+ build_engine_value_decoder(Base, UnsupportedField)
@@ -13,6 +13,7 @@ from cocoindex.typing import (
13
13
  AnalyzedDictType,
14
14
  AnalyzedListType,
15
15
  AnalyzedStructType,
16
+ AnalyzedUnknownType,
16
17
  AnalyzedTypeInfo,
17
18
  TypeAttr,
18
19
  TypeKind,
@@ -422,15 +423,7 @@ def test_annotated_list_with_type_kind() -> None:
422
423
  assert result.variant.kind == "Struct"
423
424
 
424
425
 
425
- def test_unsupported_type() -> None:
426
- with pytest.raises(
427
- ValueError,
428
- match="Unsupported as a specific type annotation for CocoIndex data type.*: <class 'set'>",
429
- ):
430
- analyze_type_info(set)
431
-
432
- with pytest.raises(
433
- ValueError,
434
- match="Unsupported as a specific type annotation for CocoIndex data type.*: <class 'numpy.complex64'>",
435
- ):
436
- Vector[np.complex64]
426
+ def test_unknown_type() -> None:
427
+ typ = set
428
+ result = analyze_type_info(typ)
429
+ assert isinstance(result.variant, AnalyzedUnknownType)
cocoindex/typing.py CHANGED
@@ -313,11 +313,11 @@ def analyze_type_info(t: Any) -> AnalyzedTypeInfo:
313
313
  kind = "OffsetDateTime"
314
314
  elif t is datetime.timedelta:
315
315
  kind = "TimeDelta"
316
+
317
+ if kind is None:
318
+ variant = AnalyzedUnknownType()
316
319
  else:
317
- raise ValueError(
318
- f"Unsupported as a specific type annotation for CocoIndex data type (https://cocoindex.io/docs/core/data_types): {t}"
319
- )
320
- variant = AnalyzedBasicType(kind=kind)
320
+ variant = AnalyzedBasicType(kind=kind)
321
321
 
322
322
  return AnalyzedTypeInfo(
323
323
  core_type=core_type,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cocoindex
3
- Version: 0.1.73
3
+ Version: 0.1.74
4
4
  Requires-Dist: click>=8.1.8
5
5
  Requires-Dist: rich>=14.0.0
6
6
  Requires-Dist: python-dotenv>=1.1.0
@@ -11,7 +11,7 @@ Requires-Dist: ruff ; extra == 'dev'
11
11
  Requires-Dist: mypy ; extra == 'dev'
12
12
  Requires-Dist: pre-commit ; extra == 'dev'
13
13
  Requires-Dist: sentence-transformers>=3.3.1 ; extra == 'embeddings'
14
- Requires-Dist: cocoindex[embeddings] ; extra == 'all'
14
+ Requires-Dist: sentence-transformers>=3.3.1 ; extra == 'all'
15
15
  Provides-Extra: dev
16
16
  Provides-Extra: embeddings
17
17
  Provides-Extra: all
@@ -1,18 +1,18 @@
1
- cocoindex-0.1.73.dist-info/METADATA,sha256=7TqVP8FrHbi9wz-XrnSYX2C158sVJV-SveG7VmXZN6k,11304
2
- cocoindex-0.1.73.dist-info/WHEEL,sha256=s25ArxaEBsS5fVGGsO24U-sMTFw8T6jsDJcABX1aYfs,108
3
- cocoindex-0.1.73.dist-info/entry_points.txt,sha256=_NretjYVzBdNTn7dK-zgwr7YfG2afz1u1uSE-5bZXF8,46
4
- cocoindex-0.1.73.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1
+ cocoindex-0.1.74.dist-info/METADATA,sha256=kkA1L7X7ypbNcSLBtJ92Zp1lYEgEJEfXpR1d1c8V_Lw,11311
2
+ cocoindex-0.1.74.dist-info/WHEEL,sha256=s25ArxaEBsS5fVGGsO24U-sMTFw8T6jsDJcABX1aYfs,108
3
+ cocoindex-0.1.74.dist-info/entry_points.txt,sha256=_NretjYVzBdNTn7dK-zgwr7YfG2afz1u1uSE-5bZXF8,46
4
+ cocoindex-0.1.74.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
5
5
  cocoindex/__init__.py,sha256=kfTgbh2haepo7kIbzJqfxU6Kx7wPol5_t1SYF2x6cBM,2114
6
- cocoindex/_engine.cpython-313-x86_64-linux-gnu.so,sha256=N6efSVXP0aGeqyZKKmCZXeKePSa8c14fvOHQa0EIANE,71050592
6
+ cocoindex/_engine.cpython-313-x86_64-linux-gnu.so,sha256=KBhGhQFVrZj1gYN6basp_J3f17mOq1M6iyRmUMJIw5U,71102424
7
7
  cocoindex/auth_registry.py,sha256=PE1-kVkcyC1G2C_V7b1kvYzeq73OFQehWKQP7ln7fJ8,1478
8
8
  cocoindex/cli.py,sha256=-gp639JSyQN6YjnhGqCakIzYoSSqXxQMbxbkcYGP0QY,22359
9
- cocoindex/convert.py,sha256=ZENKZ47Vmah2_EEvE03WqfDF9wCs9h0CDEjq08GK3R0,17365
10
- cocoindex/flow.py,sha256=HN24rsihO3BkSYGnTtxgovgka2IobxhFuLmDlqw3fAk,36127
9
+ cocoindex/convert.py,sha256=jO95WD8X7AYS5COfRyPiq4t7ewVC1TdRbYpTdHrkCvY,19564
10
+ cocoindex/flow.py,sha256=C2VwexNxes_7b70jhlp5yAPeBQPtdbKabOp1AarsacA,36184
11
11
  cocoindex/functions.py,sha256=LLu_ausirvqnsx_k3euZpv8sLCpBZ4DF77h2HOzbinE,3109
12
12
  cocoindex/index.py,sha256=j93B9jEvvLXHtpzKWL88SY6wCGEoPgpsQhEGHlyYGFg,540
13
13
  cocoindex/lib.py,sha256=f--9dAYd84CZosbDZqNW0oGbBLsY3dXiUTR1VrfQ_QY,817
14
14
  cocoindex/llm.py,sha256=WxmWUbNcf9HOCM5xkbDeFs9lF67M3mr810B7deDDc-8,673
15
- cocoindex/op.py,sha256=Afi5CfgU3wPQoPPKFb2WUYCVLmCPhBuK-2NT1AzC2zU,13161
15
+ cocoindex/op.py,sha256=DNAlZGpGFZWAfRFuxXKmT72kSORdGbirDYsSlD7sDqk,21474
16
16
  cocoindex/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  cocoindex/runtime.py,sha256=povilB3HH3y1JF-yxKwU-pD8n2WnAqyQxIgvXXHNc60,1080
18
18
  cocoindex/setting.py,sha256=TwhQ6pEeZmvc8ZXlnT9d8Wn8Vz_u7Z5LJUkGsKmKSno,4859
@@ -20,11 +20,11 @@ cocoindex/setup.py,sha256=7uIHKN4FOCuoidPXcKyGTrkqpkl9luL49-6UcnMxYzw,3068
20
20
  cocoindex/sources.py,sha256=69COA4qbZDipzGYfXv-WJSmicFkA509xIShRGDh6A0A,2083
21
21
  cocoindex/targets.py,sha256=Nfh_tpFd1goTnS_cxBjIs4j9zl3Z4Z1JomAQ1dl3Sic,2796
22
22
  cocoindex/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- cocoindex/tests/test_convert.py,sha256=Ofx0grI-tIWCkefwS8XMTQlQvDDV0YI1fjx5ZhUwRZs,49738
23
+ cocoindex/tests/test_convert.py,sha256=guQ6-AJh4z1NoR60wlm0lJ2R-rOstmFDihDL2oYGouI,46875
24
24
  cocoindex/tests/test_optional_database.py,sha256=snAmkNa6wtOSaxoZE1HgjvL5v_ylitt3Jt_9df4Cgdc,8506
25
- cocoindex/tests/test_typing.py,sha256=MeirjvSVlMqIJsvLF0B11EYpczi6o7WsJMS2hdHry5c,12636
25
+ cocoindex/tests/test_typing.py,sha256=9OF3lO2uSpZBefkEJx7WRbnkXjwQtvlQIeeARYQID68,12391
26
26
  cocoindex/tests/test_validation.py,sha256=X6AQzVs-hVKIXcrHMEMQnhfUE8at7iXQnPq8nHNhZ2Q,4543
27
- cocoindex/typing.py,sha256=9g-pV1O-CUnUQwzE22nPHVUTY6Zrr-ChzAEq4TVGAig,13487
27
+ cocoindex/typing.py,sha256=RK-UYiNkxL9DPjhitFeJVB0b3eV2DsGjOg8V-jrnUYU,13383
28
28
  cocoindex/utils.py,sha256=hUhX-XV6XGCtJSEIpBOuDv6VvqImwPlgBxztBTw7u0U,598
29
29
  cocoindex/validation.py,sha256=PZnJoby4sLbsmPv9fOjOQXuefjfZ7gmtsiTGU8SH-tc,3090
30
- cocoindex-0.1.73.dist-info/RECORD,,
30
+ cocoindex-0.1.74.dist-info/RECORD,,