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,553 @@
1
+ import dataclasses
2
+ import datetime
3
+ import uuid
4
+ from collections.abc import Mapping, Sequence
5
+ from typing import Annotated, Any, Literal, NamedTuple, get_args, get_origin
6
+
7
+ import numpy as np
8
+ from numpy.typing import NDArray
9
+
10
+ from cocoindex.typing import (
11
+ AnalyzedBasicType,
12
+ AnalyzedDictType,
13
+ AnalyzedListType,
14
+ AnalyzedStructType,
15
+ AnalyzedUnknownType,
16
+ AnalyzedTypeInfo,
17
+ TypeAttr,
18
+ TypeKind,
19
+ Vector,
20
+ VectorInfo,
21
+ analyze_type_info,
22
+ decode_engine_value_type,
23
+ encode_enriched_type,
24
+ encode_enriched_type_info,
25
+ encode_engine_value_type,
26
+ )
27
+
28
+
29
+ @dataclasses.dataclass
30
+ class SimpleDataclass:
31
+ name: str
32
+ value: int
33
+
34
+
35
+ class SimpleNamedTuple(NamedTuple):
36
+ name: str
37
+ value: int
38
+
39
+
40
+ def test_ndarray_float32_no_dim() -> None:
41
+ typ = NDArray[np.float32]
42
+ result = analyze_type_info(typ)
43
+ assert isinstance(result.variant, AnalyzedListType)
44
+ assert result.variant.vector_info is None
45
+ assert result.variant.elem_type == np.float32
46
+ assert result.nullable is False
47
+ assert get_origin(result.core_type) == np.ndarray
48
+ assert get_args(result.core_type)[1] == np.dtype[np.float32]
49
+
50
+
51
+ def test_vector_float32_no_dim() -> None:
52
+ typ = Vector[np.float32]
53
+ result = analyze_type_info(typ)
54
+ assert isinstance(result.variant, AnalyzedListType)
55
+ assert result.variant.vector_info == VectorInfo(dim=None)
56
+ assert result.variant.elem_type == np.float32
57
+ assert result.nullable is False
58
+ assert get_origin(result.core_type) == np.ndarray
59
+ assert get_args(result.core_type)[1] == np.dtype[np.float32]
60
+
61
+
62
+ def test_ndarray_float64_with_dim() -> None:
63
+ typ = Annotated[NDArray[np.float64], VectorInfo(dim=128)]
64
+ result = analyze_type_info(typ)
65
+ assert isinstance(result.variant, AnalyzedListType)
66
+ assert result.variant.vector_info == VectorInfo(dim=128)
67
+ assert result.variant.elem_type == np.float64
68
+ assert result.nullable is False
69
+ assert get_origin(result.core_type) == np.ndarray
70
+ assert get_args(result.core_type)[1] == np.dtype[np.float64]
71
+
72
+
73
+ def test_vector_float32_with_dim() -> None:
74
+ typ = Vector[np.float32, Literal[384]]
75
+ result = analyze_type_info(typ)
76
+ assert isinstance(result.variant, AnalyzedListType)
77
+ assert result.variant.vector_info == VectorInfo(dim=384)
78
+ assert result.variant.elem_type == np.float32
79
+ assert result.nullable is False
80
+ assert get_origin(result.core_type) == np.ndarray
81
+ assert get_args(result.core_type)[1] == np.dtype[np.float32]
82
+
83
+
84
+ def test_ndarray_int64_no_dim() -> None:
85
+ typ = NDArray[np.int64]
86
+ result = analyze_type_info(typ)
87
+ assert isinstance(result.variant, AnalyzedListType)
88
+ assert result.variant.vector_info is None
89
+ assert result.variant.elem_type == np.int64
90
+ assert result.nullable is False
91
+ assert get_origin(result.core_type) == np.ndarray
92
+ assert get_args(result.core_type)[1] == np.dtype[np.int64]
93
+
94
+
95
+ def test_nullable_ndarray() -> None:
96
+ typ = NDArray[np.float32] | None
97
+ result = analyze_type_info(typ)
98
+ assert isinstance(result.variant, AnalyzedListType)
99
+ assert result.variant.vector_info is None
100
+ assert result.variant.elem_type == np.float32
101
+ assert result.nullable is True
102
+ assert get_origin(result.core_type) == np.ndarray
103
+ assert get_args(result.core_type)[1] == np.dtype[np.float32]
104
+
105
+
106
+ def test_scalar_numpy_types() -> None:
107
+ for np_type, expected_kind in [
108
+ (np.int64, "Int64"),
109
+ (np.float32, "Float32"),
110
+ (np.float64, "Float64"),
111
+ ]:
112
+ type_info = analyze_type_info(np_type)
113
+ assert isinstance(type_info.variant, AnalyzedBasicType)
114
+ assert type_info.variant.kind == expected_kind, (
115
+ f"Expected {expected_kind} for {np_type}, got {type_info.variant.kind}"
116
+ )
117
+ assert type_info.core_type == np_type, (
118
+ f"Expected {np_type}, got {type_info.core_type}"
119
+ )
120
+
121
+
122
+ def test_vector_str() -> None:
123
+ typ = Vector[str]
124
+ result = analyze_type_info(typ)
125
+ assert isinstance(result.variant, AnalyzedListType)
126
+ assert result.variant.elem_type is str
127
+ assert result.variant.vector_info == VectorInfo(dim=None)
128
+
129
+
130
+ def test_non_numpy_vector() -> None:
131
+ typ = Vector[float, Literal[3]]
132
+ result = analyze_type_info(typ)
133
+ assert isinstance(result.variant, AnalyzedListType)
134
+ assert result.variant.elem_type is float
135
+ assert result.variant.vector_info == VectorInfo(dim=3)
136
+
137
+
138
+ def test_list_of_primitives() -> None:
139
+ typ = list[str]
140
+ result = analyze_type_info(typ)
141
+ assert result == AnalyzedTypeInfo(
142
+ core_type=list[str],
143
+ base_type=list,
144
+ variant=AnalyzedListType(elem_type=str, vector_info=None),
145
+ attrs=None,
146
+ nullable=False,
147
+ )
148
+
149
+
150
+ def test_list_of_structs() -> None:
151
+ typ = list[SimpleDataclass]
152
+ result = analyze_type_info(typ)
153
+ assert result == AnalyzedTypeInfo(
154
+ core_type=list[SimpleDataclass],
155
+ base_type=list,
156
+ variant=AnalyzedListType(elem_type=SimpleDataclass, vector_info=None),
157
+ attrs=None,
158
+ nullable=False,
159
+ )
160
+
161
+
162
+ def test_sequence_of_int() -> None:
163
+ typ = Sequence[int]
164
+ result = analyze_type_info(typ)
165
+ assert result == AnalyzedTypeInfo(
166
+ core_type=Sequence[int],
167
+ base_type=Sequence,
168
+ variant=AnalyzedListType(elem_type=int, vector_info=None),
169
+ attrs=None,
170
+ nullable=False,
171
+ )
172
+
173
+
174
+ def test_list_with_vector_info() -> None:
175
+ typ = Annotated[list[int], VectorInfo(dim=5)]
176
+ result = analyze_type_info(typ)
177
+ assert result == AnalyzedTypeInfo(
178
+ core_type=list[int],
179
+ base_type=list,
180
+ variant=AnalyzedListType(elem_type=int, vector_info=VectorInfo(dim=5)),
181
+ attrs=None,
182
+ nullable=False,
183
+ )
184
+
185
+
186
+ def test_dict_str_int() -> None:
187
+ typ = dict[str, int]
188
+ result = analyze_type_info(typ)
189
+ assert result == AnalyzedTypeInfo(
190
+ core_type=dict[str, int],
191
+ base_type=dict,
192
+ variant=AnalyzedDictType(key_type=str, value_type=int),
193
+ attrs=None,
194
+ nullable=False,
195
+ )
196
+
197
+
198
+ def test_mapping_str_dataclass() -> None:
199
+ typ = Mapping[str, SimpleDataclass]
200
+ result = analyze_type_info(typ)
201
+ assert result == AnalyzedTypeInfo(
202
+ core_type=Mapping[str, SimpleDataclass],
203
+ base_type=Mapping,
204
+ variant=AnalyzedDictType(key_type=str, value_type=SimpleDataclass),
205
+ attrs=None,
206
+ nullable=False,
207
+ )
208
+
209
+
210
+ def test_dataclass() -> None:
211
+ typ = SimpleDataclass
212
+ result = analyze_type_info(typ)
213
+ assert result == AnalyzedTypeInfo(
214
+ core_type=SimpleDataclass,
215
+ base_type=SimpleDataclass,
216
+ variant=AnalyzedStructType(struct_type=SimpleDataclass),
217
+ attrs=None,
218
+ nullable=False,
219
+ )
220
+
221
+
222
+ def test_named_tuple() -> None:
223
+ typ = SimpleNamedTuple
224
+ result = analyze_type_info(typ)
225
+ assert result == AnalyzedTypeInfo(
226
+ core_type=SimpleNamedTuple,
227
+ base_type=SimpleNamedTuple,
228
+ variant=AnalyzedStructType(struct_type=SimpleNamedTuple),
229
+ attrs=None,
230
+ nullable=False,
231
+ )
232
+
233
+
234
+ def test_str() -> None:
235
+ typ = str
236
+ result = analyze_type_info(typ)
237
+ assert result == AnalyzedTypeInfo(
238
+ core_type=str,
239
+ base_type=str,
240
+ variant=AnalyzedBasicType(kind="Str"),
241
+ attrs=None,
242
+ nullable=False,
243
+ )
244
+
245
+
246
+ def test_bool() -> None:
247
+ typ = bool
248
+ result = analyze_type_info(typ)
249
+ assert result == AnalyzedTypeInfo(
250
+ core_type=bool,
251
+ base_type=bool,
252
+ variant=AnalyzedBasicType(kind="Bool"),
253
+ attrs=None,
254
+ nullable=False,
255
+ )
256
+
257
+
258
+ def test_bytes() -> None:
259
+ typ = bytes
260
+ result = analyze_type_info(typ)
261
+ assert result == AnalyzedTypeInfo(
262
+ core_type=bytes,
263
+ base_type=bytes,
264
+ variant=AnalyzedBasicType(kind="Bytes"),
265
+ attrs=None,
266
+ nullable=False,
267
+ )
268
+
269
+
270
+ def test_uuid() -> None:
271
+ typ = uuid.UUID
272
+ result = analyze_type_info(typ)
273
+ assert result == AnalyzedTypeInfo(
274
+ core_type=uuid.UUID,
275
+ base_type=uuid.UUID,
276
+ variant=AnalyzedBasicType(kind="Uuid"),
277
+ attrs=None,
278
+ nullable=False,
279
+ )
280
+
281
+
282
+ def test_date() -> None:
283
+ typ = datetime.date
284
+ result = analyze_type_info(typ)
285
+ assert result == AnalyzedTypeInfo(
286
+ core_type=datetime.date,
287
+ base_type=datetime.date,
288
+ variant=AnalyzedBasicType(kind="Date"),
289
+ attrs=None,
290
+ nullable=False,
291
+ )
292
+
293
+
294
+ def test_time() -> None:
295
+ typ = datetime.time
296
+ result = analyze_type_info(typ)
297
+ assert result == AnalyzedTypeInfo(
298
+ core_type=datetime.time,
299
+ base_type=datetime.time,
300
+ variant=AnalyzedBasicType(kind="Time"),
301
+ attrs=None,
302
+ nullable=False,
303
+ )
304
+
305
+
306
+ def test_timedelta() -> None:
307
+ typ = datetime.timedelta
308
+ result = analyze_type_info(typ)
309
+ assert result == AnalyzedTypeInfo(
310
+ core_type=datetime.timedelta,
311
+ base_type=datetime.timedelta,
312
+ variant=AnalyzedBasicType(kind="TimeDelta"),
313
+ attrs=None,
314
+ nullable=False,
315
+ )
316
+
317
+
318
+ def test_float() -> None:
319
+ typ = float
320
+ result = analyze_type_info(typ)
321
+ assert result == AnalyzedTypeInfo(
322
+ core_type=float,
323
+ base_type=float,
324
+ variant=AnalyzedBasicType(kind="Float64"),
325
+ attrs=None,
326
+ nullable=False,
327
+ )
328
+
329
+
330
+ def test_int() -> None:
331
+ typ = int
332
+ result = analyze_type_info(typ)
333
+ assert result == AnalyzedTypeInfo(
334
+ core_type=int,
335
+ base_type=int,
336
+ variant=AnalyzedBasicType(kind="Int64"),
337
+ attrs=None,
338
+ nullable=False,
339
+ )
340
+
341
+
342
+ def test_type_with_attributes() -> None:
343
+ typ = Annotated[str, TypeAttr("key", "value")]
344
+ result = analyze_type_info(typ)
345
+ assert result == AnalyzedTypeInfo(
346
+ core_type=str,
347
+ base_type=str,
348
+ variant=AnalyzedBasicType(kind="Str"),
349
+ attrs={"key": "value"},
350
+ nullable=False,
351
+ )
352
+
353
+
354
+ def test_encode_enriched_type_none() -> None:
355
+ typ = None
356
+ result = encode_enriched_type(typ)
357
+ assert result is None
358
+
359
+
360
+ def test_encode_enriched_type_struct() -> None:
361
+ typ = SimpleDataclass
362
+ result = encode_enriched_type(typ)
363
+ assert result["type"]["kind"] == "Struct"
364
+ assert len(result["type"]["fields"]) == 2
365
+ assert result["type"]["fields"][0]["name"] == "name"
366
+ assert result["type"]["fields"][0]["type"]["kind"] == "Str"
367
+ assert result["type"]["fields"][1]["name"] == "value"
368
+ assert result["type"]["fields"][1]["type"]["kind"] == "Int64"
369
+
370
+
371
+ def test_encode_enriched_type_vector() -> None:
372
+ typ = NDArray[np.float32]
373
+ result = encode_enriched_type(typ)
374
+ assert result["type"]["kind"] == "Vector"
375
+ assert result["type"]["element_type"]["kind"] == "Float32"
376
+ assert result["type"]["dimension"] is None
377
+
378
+
379
+ def test_encode_enriched_type_ltable() -> None:
380
+ typ = list[SimpleDataclass]
381
+ result = encode_enriched_type(typ)
382
+ assert result["type"]["kind"] == "LTable"
383
+ assert "fields" in result["type"]["row"]
384
+ assert len(result["type"]["row"]["fields"]) == 2
385
+
386
+
387
+ def test_encode_enriched_type_with_attrs() -> None:
388
+ typ = Annotated[str, TypeAttr("key", "value")]
389
+ result = encode_enriched_type(typ)
390
+ assert result["type"]["kind"] == "Str"
391
+ assert result["attrs"] == {"key": "value"}
392
+
393
+
394
+ def test_encode_enriched_type_nullable() -> None:
395
+ typ = str | None
396
+ result = encode_enriched_type(typ)
397
+ assert result["type"]["kind"] == "Str"
398
+ assert result["nullable"] is True
399
+
400
+
401
+ def test_encode_scalar_numpy_types_schema() -> None:
402
+ for np_type, expected_kind in [
403
+ (np.int64, "Int64"),
404
+ (np.float32, "Float32"),
405
+ (np.float64, "Float64"),
406
+ ]:
407
+ schema = encode_enriched_type(np_type)
408
+ assert schema["type"]["kind"] == expected_kind, (
409
+ f"Expected {expected_kind} for {np_type}, got {schema['type']['kind']}"
410
+ )
411
+ assert not schema.get("nullable", False)
412
+
413
+
414
+ def test_annotated_struct_with_type_kind() -> None:
415
+ typ = Annotated[SimpleDataclass, TypeKind("Vector")]
416
+ result = analyze_type_info(typ)
417
+ assert isinstance(result.variant, AnalyzedBasicType)
418
+ assert result.variant.kind == "Vector"
419
+
420
+
421
+ def test_annotated_list_with_type_kind() -> None:
422
+ typ = Annotated[list[int], TypeKind("Struct")]
423
+ result = analyze_type_info(typ)
424
+ assert isinstance(result.variant, AnalyzedBasicType)
425
+ assert result.variant.kind == "Struct"
426
+
427
+
428
+ def test_unknown_type() -> None:
429
+ typ = set
430
+ result = analyze_type_info(typ)
431
+ assert isinstance(result.variant, AnalyzedUnknownType)
432
+
433
+
434
+ # ========================= Encode/Decode Tests =========================
435
+
436
+
437
+ def encode_type_from_annotation(t: Any) -> dict[str, Any]:
438
+ """Helper function to encode a Python type annotation to its dictionary representation."""
439
+ return encode_enriched_type_info(analyze_type_info(t))
440
+
441
+
442
+ def test_basic_types_encode_decode() -> None:
443
+ """Test encode/decode roundtrip for basic Python types."""
444
+ test_cases = [
445
+ str,
446
+ int,
447
+ float,
448
+ bool,
449
+ bytes,
450
+ uuid.UUID,
451
+ datetime.date,
452
+ datetime.time,
453
+ datetime.datetime,
454
+ datetime.timedelta,
455
+ ]
456
+
457
+ for typ in test_cases:
458
+ encoded = encode_type_from_annotation(typ)
459
+ decoded = decode_engine_value_type(encoded["type"])
460
+ reencoded = encode_engine_value_type(decoded)
461
+ assert reencoded == encoded["type"]
462
+
463
+
464
+ def test_vector_types_encode_decode() -> None:
465
+ """Test encode/decode roundtrip for vector types."""
466
+ test_cases = [
467
+ NDArray[np.float32],
468
+ NDArray[np.float64],
469
+ NDArray[np.int64],
470
+ Vector[np.float32],
471
+ Vector[np.float32, Literal[128]],
472
+ Vector[str],
473
+ ]
474
+
475
+ for typ in test_cases:
476
+ encoded = encode_type_from_annotation(typ)
477
+ decoded = decode_engine_value_type(encoded["type"])
478
+ reencoded = encode_engine_value_type(decoded)
479
+ assert reencoded == encoded["type"]
480
+
481
+
482
+ def test_struct_types_encode_decode() -> None:
483
+ """Test encode/decode roundtrip for struct types."""
484
+ test_cases = [
485
+ SimpleDataclass,
486
+ SimpleNamedTuple,
487
+ ]
488
+
489
+ for typ in test_cases:
490
+ encoded = encode_type_from_annotation(typ)
491
+ decoded = decode_engine_value_type(encoded["type"])
492
+ reencoded = encode_engine_value_type(decoded)
493
+ assert reencoded == encoded["type"]
494
+
495
+
496
+ def test_table_types_encode_decode() -> None:
497
+ """Test encode/decode roundtrip for table types."""
498
+ test_cases = [
499
+ list[SimpleDataclass], # LTable
500
+ dict[str, SimpleDataclass], # KTable
501
+ ]
502
+
503
+ for typ in test_cases:
504
+ encoded = encode_type_from_annotation(typ)
505
+ decoded = decode_engine_value_type(encoded["type"])
506
+ reencoded = encode_engine_value_type(decoded)
507
+ assert reencoded == encoded["type"]
508
+
509
+
510
+ def test_nullable_types_encode_decode() -> None:
511
+ """Test encode/decode roundtrip for nullable types."""
512
+ test_cases = [
513
+ str | None,
514
+ int | None,
515
+ NDArray[np.float32] | None,
516
+ ]
517
+
518
+ for typ in test_cases:
519
+ encoded = encode_type_from_annotation(typ)
520
+ decoded = decode_engine_value_type(encoded["type"])
521
+ reencoded = encode_engine_value_type(decoded)
522
+ assert reencoded == encoded["type"]
523
+
524
+
525
+ def test_annotated_types_encode_decode() -> None:
526
+ """Test encode/decode roundtrip for annotated types."""
527
+ test_cases = [
528
+ Annotated[str, TypeAttr("key", "value")],
529
+ Annotated[NDArray[np.float32], VectorInfo(dim=256)],
530
+ Annotated[list[int], VectorInfo(dim=10)],
531
+ ]
532
+
533
+ for typ in test_cases:
534
+ encoded = encode_type_from_annotation(typ)
535
+ decoded = decode_engine_value_type(encoded["type"])
536
+ reencoded = encode_engine_value_type(decoded)
537
+ assert reencoded == encoded["type"]
538
+
539
+
540
+ def test_complex_nested_encode_decode() -> None:
541
+ """Test complex nested structure encode/decode roundtrip."""
542
+
543
+ # Create a complex nested structure using Python type annotations
544
+ @dataclasses.dataclass
545
+ class ComplexStruct:
546
+ embedding: NDArray[np.float32]
547
+ metadata: str | None
548
+ score: Annotated[float, TypeAttr("indexed", True)]
549
+
550
+ encoded = encode_type_from_annotation(ComplexStruct)
551
+ decoded = decode_engine_value_type(encoded["type"])
552
+ reencoded = encode_engine_value_type(decoded)
553
+ assert reencoded == encoded["type"]
@@ -0,0 +1,134 @@
1
+ """Tests for naming validation functionality."""
2
+
3
+ import pytest
4
+ from cocoindex.validation import (
5
+ validate_field_name,
6
+ validate_flow_name,
7
+ validate_full_flow_name,
8
+ validate_app_namespace_name,
9
+ validate_target_name,
10
+ NamingError,
11
+ validate_identifier_name,
12
+ )
13
+
14
+
15
+ class TestValidateIdentifierName:
16
+ """Test the core validation function."""
17
+
18
+ def test_valid_names(self) -> None:
19
+ """Test that valid names pass validation."""
20
+ valid_names = [
21
+ "field1",
22
+ "field_name",
23
+ "_private",
24
+ "a",
25
+ "field123",
26
+ "FIELD_NAME",
27
+ "MyField",
28
+ "field_123_test",
29
+ ]
30
+
31
+ for name in valid_names:
32
+ result = validate_identifier_name(name)
33
+ assert result is None, f"Valid name '{name}' failed validation: {result}"
34
+
35
+ def test_valid_names_with_dots(self) -> None:
36
+ """Test that valid names with dots pass validation when allowed."""
37
+ valid_names = ["app.flow", "my_app.my_flow", "namespace.sub.flow", "a.b.c.d"]
38
+
39
+ for name in valid_names:
40
+ result = validate_identifier_name(name, allow_dots=True)
41
+ assert result is None, (
42
+ f"Valid dotted name '{name}' failed validation: {result}"
43
+ )
44
+
45
+ def test_invalid_starting_characters(self) -> None:
46
+ """Test names with invalid starting characters."""
47
+ invalid_names = [
48
+ "123field", # starts with digit
49
+ ".field", # starts with dot
50
+ "-field", # starts with dash
51
+ " field", # starts with space
52
+ ]
53
+
54
+ for name in invalid_names:
55
+ result = validate_identifier_name(name)
56
+ assert result is not None, (
57
+ f"Invalid name '{name}' should have failed validation"
58
+ )
59
+
60
+ def test_double_underscore_restriction(self) -> None:
61
+ """Test double underscore restriction."""
62
+ invalid_names = ["__reserved", "__internal", "__test"]
63
+
64
+ for name in invalid_names:
65
+ result = validate_identifier_name(name)
66
+ assert result is not None
67
+ assert "double underscores" in result.lower()
68
+
69
+ def test_length_restriction(self) -> None:
70
+ """Test maximum length restriction."""
71
+ long_name = "a" * 65
72
+ result = validate_identifier_name(long_name, max_length=64)
73
+ assert result is not None
74
+ assert "maximum length" in result.lower()
75
+
76
+
77
+ class TestSpecificValidators:
78
+ """Test the specific validation functions."""
79
+
80
+ def test_valid_field_names(self) -> None:
81
+ """Test valid field names."""
82
+ valid_names = ["field1", "field_name", "_private", "FIELD"]
83
+ for name in valid_names:
84
+ validate_field_name(name) # Should not raise
85
+
86
+ def test_invalid_field_names(self) -> None:
87
+ """Test invalid field names raise NamingError."""
88
+ invalid_names = ["123field", "field-name", "__reserved", "a" * 65]
89
+
90
+ for name in invalid_names:
91
+ with pytest.raises(NamingError):
92
+ validate_field_name(name)
93
+
94
+ def test_flow_validation(self) -> None:
95
+ """Test flow name validation."""
96
+ # Valid flow names
97
+ validate_flow_name("MyFlow")
98
+ validate_flow_name("my_flow_123")
99
+
100
+ # Invalid flow names
101
+ with pytest.raises(NamingError):
102
+ validate_flow_name("123flow")
103
+
104
+ with pytest.raises(NamingError):
105
+ validate_flow_name("__reserved_flow")
106
+
107
+ def test_full_flow_name_allows_dots(self) -> None:
108
+ """Test that full flow names allow dots."""
109
+ validate_full_flow_name("app.my_flow")
110
+ validate_full_flow_name("namespace.subnamespace.flow")
111
+
112
+ # But still reject invalid patterns
113
+ with pytest.raises(NamingError):
114
+ validate_full_flow_name("123.invalid")
115
+
116
+ def test_target_validation(self) -> None:
117
+ """Test target name validation."""
118
+ validate_target_name("my_target")
119
+ validate_target_name("output_table")
120
+
121
+ with pytest.raises(NamingError):
122
+ validate_target_name("123target")
123
+
124
+ def test_app_namespace_validation(self) -> None:
125
+ """Test app namespace validation."""
126
+ validate_app_namespace_name("myapp")
127
+ validate_app_namespace_name("my_app_123")
128
+
129
+ # Should not allow dots in app namespace
130
+ with pytest.raises(NamingError):
131
+ validate_app_namespace_name("my.app")
132
+
133
+ with pytest.raises(NamingError):
134
+ validate_app_namespace_name("123app")