cognite-neat 1.0.32__py3-none-any.whl → 1.0.34__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@ from cognite.neat._client.data_classes import SpaceStatisticsResponse
12
12
  from cognite.neat._data_model._constants import COGNITE_SPACES
13
13
  from cognite.neat._data_model._snapshot import SchemaSnapshot
14
14
  from cognite.neat._data_model.models.dms._constraints import RequiresConstraintDefinition
15
- from cognite.neat._data_model.models.dms._container import ContainerRequest
15
+ from cognite.neat._data_model.models.dms._container import ContainerPropertyDefinition, ContainerRequest
16
16
  from cognite.neat._data_model.models.dms._data_types import DirectNodeRelation
17
17
  from cognite.neat._data_model.models.dms._limits import SchemaLimits
18
18
  from cognite.neat._data_model.models.dms._references import (
@@ -55,6 +55,21 @@ class RequiresChangeStatus(Enum):
55
55
  NO_MODIFIABLE_CONTAINERS = "no_modifiable_containers" # All containers are immutable
56
56
 
57
57
 
58
+ @dataclass
59
+ class ResolvedReverseDirectRelation:
60
+ """Resolved context for a reverse direct relation, including container-level information."""
61
+
62
+ reverse_view_ref: ViewReference
63
+ reverse_property_id: str
64
+ direct_view_ref: ViewReference
65
+ through_property_id: str
66
+ direct_property: ViewCorePropertyRequest
67
+ container_ref: ContainerReference
68
+ container_property_id: str
69
+ container: ContainerRequest | None
70
+ container_property: ContainerPropertyDefinition | None
71
+
72
+
58
73
  @dataclass
59
74
  class RequiresChangesForView:
60
75
  """Result of computing requires constraint changes for a view."""
@@ -365,6 +380,83 @@ class ValidationResources:
365
380
 
366
381
  return bidirectional_connections
367
382
 
383
+ @staticmethod
384
+ def normalize_through_reference(
385
+ source_view_ref: ViewReference, through: ContainerDirectReference | ViewDirectReference
386
+ ) -> ViewDirectReference:
387
+ """Normalize through reference to ViewDirectReference for consistent processing.
388
+
389
+ When a reverse direct relation uses a ContainerDirectReference, we convert it to
390
+ a ViewDirectReference using the source view. This enables consistent handling
391
+ in validators regardless of how the 'through' was originally specified.
392
+ """
393
+ if isinstance(through, ContainerDirectReference):
394
+ return ViewDirectReference(source=source_view_ref, identifier=through.identifier)
395
+ return through
396
+
397
+ @cached_property
398
+ def resolved_reverse_direct_relations(self) -> list[ResolvedReverseDirectRelation]:
399
+ """Get all reverse direct relations with their resolved context.
400
+
401
+ This property traverses from reverse direct relation → source view → container,
402
+ resolving all references along the way.
403
+
404
+ Includes reverse direct relations where view-level resolution succeeded:
405
+ - The source view exists and can be expanded
406
+ - The through property exists in the source view
407
+ - The through property is a ViewCorePropertyRequest (direct relation)
408
+
409
+ Container-level fields may be None if resolution failed at that level.
410
+ """
411
+ result: list[ResolvedReverseDirectRelation] = []
412
+
413
+ for (target_view_ref, reverse_prop_id), (
414
+ source_view_ref,
415
+ through,
416
+ ) in self.reverse_to_direct_mapping.items():
417
+ through_normalized = self.normalize_through_reference(source_view_ref, through)
418
+
419
+ # Get expanded source view to include inherited properties
420
+ source_view_expanded = self.expand_view_properties(source_view_ref)
421
+ if not source_view_expanded or not source_view_expanded.properties:
422
+ continue
423
+
424
+ if through_normalized.identifier not in source_view_expanded.properties:
425
+ continue
426
+
427
+ source_property = source_view_expanded.properties[through_normalized.identifier]
428
+
429
+ # Must be a core property (direct relation)
430
+ if not isinstance(source_property, ViewCorePropertyRequest):
431
+ continue
432
+
433
+ container_ref = source_property.container
434
+ container_property_id = source_property.container_property_identifier
435
+
436
+ # Resolve container - may be None if missing
437
+ container = self.select_container(container_ref, container_property_id)
438
+
439
+ # Resolve container property - may be None if container missing or property missing
440
+ container_property = None
441
+ if container and container.properties:
442
+ container_property = container.properties.get(container_property_id)
443
+
444
+ result.append(
445
+ ResolvedReverseDirectRelation(
446
+ reverse_view_ref=target_view_ref,
447
+ reverse_property_id=reverse_prop_id,
448
+ direct_view_ref=source_view_ref,
449
+ through_property_id=through_normalized.identifier,
450
+ direct_property=source_property,
451
+ container_ref=container_ref,
452
+ container_property_id=container_property_id,
453
+ container=container,
454
+ container_property=container_property,
455
+ )
456
+ )
457
+
458
+ return result
459
+
368
460
  @property
369
461
  def connection_end_node_types(self) -> dict[tuple[ViewReference, str], ViewReference | None]:
370
462
  """Get a mapping of view references to their corresponding ViewRequest objects."""
@@ -55,7 +55,7 @@ class ContainerDiffer(ItemDiffer[ContainerRequest]):
55
55
  "constraints",
56
56
  current.constraints,
57
57
  new.constraints,
58
- add_severity=SeverityType.SAFE,
58
+ add_severity=SeverityType.WARNING,
59
59
  remove_severity=SeverityType.WARNING,
60
60
  differ=ConstraintDiffer("constraints"),
61
61
  )
@@ -12,7 +12,6 @@ from pydantic.alias_generators import to_camel
12
12
  from cognite.neat._data_model._snapshot import SchemaSnapshot
13
13
  from cognite.neat._data_model.models.dms import (
14
14
  BaseModelObject,
15
- Constraint,
16
15
  ContainerConstraintReference,
17
16
  ContainerIndexReference,
18
17
  ContainerPropertyDefinition,
@@ -20,7 +19,6 @@ from cognite.neat._data_model.models.dms import (
20
19
  ContainerRequest,
21
20
  DataModelRequest,
22
21
  DataModelResource,
23
- Index,
24
22
  T_DataModelResource,
25
23
  T_ResourceId,
26
24
  ViewRequest,
@@ -193,7 +191,9 @@ class ContainerDeploymentPlan(ResourceDeploymentPlan[ContainerReference, Contain
193
191
  def constraints_to_remove(self) -> dict[ContainerConstraintReference, RemovedField]:
194
192
  return self._get_fields_to_remove("constraints.", ContainerConstraintReference)
195
193
 
196
- def _get_fields_to_remove(self, field_prefix: str, ref_cls: type) -> dict:
194
+ def _get_fields_to_remove(
195
+ self, field_prefix: str, ref_cls: type[ContainerIndexReference] | type[ContainerConstraintReference]
196
+ ) -> dict:
197
197
  items: dict = {}
198
198
  for resource_change in self.resources:
199
199
  for change in resource_change.changes:
@@ -202,7 +202,7 @@ class ContainerDeploymentPlan(ResourceDeploymentPlan[ContainerReference, Contain
202
202
  items[
203
203
  ref_cls(
204
204
  space=resource_change.resource_id.space,
205
- external_id=resource_change.resource_id.external_id,
205
+ container_external_id=resource_change.resource_id.external_id,
206
206
  identifier=identifier,
207
207
  )
208
208
  ] = change
@@ -254,7 +254,7 @@ class ResourceDeploymentPlanList(UserList[ResourceDeploymentPlan]):
254
254
  updated_resource = resource.model_copy(update={"new_value": resource.current_value})
255
255
  elif resource.changes and resource.new_value is not None:
256
256
  # Find all field removals and update new_value accordingly.
257
- removals = [change for change in resource.changes if isinstance(change, RemovedField)]
257
+ removals: list[RemovedField] = [change for change in resource.changes if isinstance(change, RemovedField)]
258
258
  addition_paths = {change.field_path for change in resource.changes if isinstance(change, AddedField)}
259
259
  if removals:
260
260
  if resource.current_value is None:
@@ -271,6 +271,13 @@ class ResourceDeploymentPlanList(UserList[ResourceDeploymentPlan]):
271
271
  for change in resource.changes
272
272
  if not isinstance(change, RemovedField)
273
273
  or (isinstance(change, RemovedField) and change.field_path in addition_paths)
274
+ or (
275
+ isinstance(change, RemovedField)
276
+ and (
277
+ change.field_path.startswith("constraints.")
278
+ or change.field_path.startswith("indexes.")
279
+ )
280
+ )
274
281
  ],
275
282
  }
276
283
  )
@@ -320,22 +327,14 @@ class ResourceDeploymentPlanList(UserList[ResourceDeploymentPlan]):
320
327
  resource: ContainerRequest, removals: list[RemovedField], addition_paths: set[str]
321
328
  ) -> DataModelResource:
322
329
  container_properties = resource.properties.copy()
323
- indexes = (resource.indexes or {}).copy()
324
- constraints = (resource.constraints or {}).copy()
325
330
  for removal in removals:
326
331
  if removal.field_path.startswith("properties."):
327
332
  prop_key = removal.field_path.removeprefix("properties.")
328
333
  container_properties[prop_key] = cast(ContainerPropertyDefinition, removal.current_value)
329
- elif removal.field_path.startswith("indexes.") and removal.field_path not in addition_paths:
330
- # Index was removed and not re-added, so we need to restore it.
331
- index_key = removal.field_path.removeprefix("indexes.")
332
- indexes[index_key] = cast(Index, removal.current_value)
333
- elif removal.field_path.startswith("constraints.") and removal.field_path not in addition_paths:
334
- # Constraint was removed and not re-added, so we need to restore it.
335
- constraint_key = removal.field_path.removeprefix("constraints.")
336
- constraints[constraint_key] = cast(Constraint, removal.current_value)
334
+ # Note: indexes and constraints are allowed to be removed in additive mode,
335
+ # so we don't restore them here unlike properties.
337
336
  return resource.model_copy(
338
- update={"properties": container_properties, "indexes": indexes or None, "constraints": constraints or None},
337
+ update={"properties": container_properties},
339
338
  deep=True,
340
339
  )
341
340
 
@@ -185,16 +185,20 @@ class SchemaDeployer(OnSuccessResultProducer):
185
185
  continue
186
186
  current_resource = current_resources[ref]
187
187
  diffs = differ.diff(current_resource, new_resource)
188
- if (
189
- isinstance(current_resource, ContainerRequest)
190
- and isinstance(new_resource, ContainerRequest)
191
- and self.options.modus_operandi == "additive"
192
- ):
193
- # In additive mode, changes to constraints and indexes require removal and re-adding
194
- # In rebuild mode, all changes are forced via deletion and re-adding
188
+ if isinstance(current_resource, ContainerRequest) and isinstance(new_resource, ContainerRequest):
189
+ # CDF doesn't support in-place modification of constraints/indexes,
190
+ # so we transform changes to remove + add operations in both modes
195
191
  diffs = self.remove_readd_modified_indexes_and_constraints(diffs, current_resource, new_resource)
192
+
193
+ warnings = self._generate_warnings_for_constraint_and_index_changes(diffs)
196
194
  resources.append(
197
- ResourceChange(resource_id=ref, new_value=new_resource, current_value=current_resource, changes=diffs)
195
+ ResourceChange(
196
+ resource_id=ref,
197
+ new_value=new_resource,
198
+ current_value=current_resource,
199
+ changes=diffs,
200
+ message=" ".join(warnings) if warnings else None,
201
+ )
198
202
  )
199
203
 
200
204
  return plan_type(endpoint=endpoint, resources=resources)
@@ -222,19 +226,19 @@ class SchemaDeployer(OnSuccessResultProducer):
222
226
  raise RuntimeError("Bug in Neat. Malformed field path for constraint/index change.")
223
227
  # Field type is either "constraints" or "indexes"
224
228
  field_type, identifier, *_ = diff.field_path.split(".", maxsplit=2)
225
- # Mark for removal
229
+ field_path = f"{field_type}.{identifier}"
226
230
  modified_diffs.append(
227
231
  RemovedField(
228
- field_path=f"{field_type}.{identifier}",
232
+ field_path=field_path,
229
233
  item_severity=SeverityType.WARNING,
230
234
  current_value=getattr(current_resource, field_type)[identifier],
231
235
  )
232
236
  )
233
- # Mark for addition
237
+ add_severity = SeverityType.WARNING if field_type == "constraints" else SeverityType.SAFE
234
238
  modified_diffs.append(
235
239
  AddedField(
236
- field_path=f"{field_type}.{identifier}",
237
- item_severity=SeverityType.SAFE,
240
+ field_path=field_path,
241
+ item_severity=add_severity,
238
242
  new_value=getattr(new_resource, field_type)[identifier],
239
243
  )
240
244
  )
@@ -262,6 +266,29 @@ class SchemaDeployer(OnSuccessResultProducer):
262
266
  )
263
267
  return None
264
268
 
269
+ @classmethod
270
+ def _generate_warnings_for_constraint_and_index_changes(cls, diffs: list[FieldChange]) -> list[str]:
271
+ """Generate warning messages for constraint and index changes.
272
+
273
+ Args:
274
+ diffs: The list of field changes.
275
+
276
+ Returns:
277
+ A list of warning messages for field changes involving constraint and index changes.
278
+ """
279
+ warnings: list[str] = []
280
+ if any(isinstance(diff, AddedField) and diff.field_path.startswith("constraints.") for diff in diffs):
281
+ warnings.append(
282
+ "Adding constraints could cause ingestion failures if the data being ingested violates the constraint."
283
+ )
284
+ if any(
285
+ isinstance(diff, RemovedField)
286
+ and (diff.field_path.startswith("constraints.") or diff.field_path.startswith("indexes."))
287
+ for diff in diffs
288
+ ):
289
+ warnings.append("Removing constraints or indexes may affect query performance.")
290
+ return warnings
291
+
265
292
  def should_proceed_to_deploy(self, plan: Sequence[ResourceDeploymentPlan]) -> bool:
266
293
  max_severity_in_plan = SeverityType.max_severity(
267
294
  [change.severity for resource_plan in plan for change in resource_plan.resources],
@@ -6,6 +6,8 @@ from cognite.neat._utils.useful_types import ReferenceObject
6
6
  from ._container import ContainerRequest
7
7
  from ._data_model import DataModelRequest
8
8
  from ._references import (
9
+ ContainerConstraintReference,
10
+ ContainerIndexReference,
9
11
  ContainerReference,
10
12
  DataModelReference,
11
13
  SpaceReference,
@@ -18,7 +20,14 @@ DataModelResource: TypeAlias = SpaceRequest | DataModelRequest | ViewRequest | C
18
20
 
19
21
  T_DataModelResource = TypeVar("T_DataModelResource", bound=DataModelResource)
20
22
 
21
- ResourceId: TypeAlias = SpaceReference | DataModelReference | ViewReference | ContainerReference
23
+ ResourceId: TypeAlias = (
24
+ SpaceReference
25
+ | DataModelReference
26
+ | ViewReference
27
+ | ContainerReference
28
+ | ContainerIndexReference
29
+ | ContainerConstraintReference
30
+ )
22
31
 
23
32
  T_ResourceId = TypeVar("T_ResourceId", bound=ResourceId)
24
33
 
@@ -133,9 +133,43 @@ class ViewDirectReference(ReferenceObject):
133
133
  return f"{self.source!s}.{self.identifier}"
134
134
 
135
135
 
136
- class ContainerIndexReference(ContainerReference):
137
- identifier: str
136
+ class ContainerIndexReference(ReferenceObject):
137
+ """Reference to a container index for deletion API."""
138
+
139
+ space: str = Field(
140
+ description="Id of the space hosting the container.",
141
+ min_length=1,
142
+ max_length=43,
143
+ pattern=SPACE_FORMAT_PATTERN,
144
+ )
145
+ container_external_id: str = Field(
146
+ description="External-id of the container.",
147
+ min_length=1,
148
+ max_length=255,
149
+ pattern=DM_EXTERNAL_ID_PATTERN,
150
+ alias="containerExternalId",
151
+ )
152
+ identifier: str = Field(
153
+ description="Identifier of the index.",
154
+ )
138
155
 
139
156
 
140
- class ContainerConstraintReference(ContainerReference):
141
- identifier: str
157
+ class ContainerConstraintReference(ReferenceObject):
158
+ """Reference to a container constraint for deletion API."""
159
+
160
+ space: str = Field(
161
+ description="Id of the space hosting the container.",
162
+ min_length=1,
163
+ max_length=43,
164
+ pattern=SPACE_FORMAT_PATTERN,
165
+ )
166
+ container_external_id: str = Field(
167
+ description="External-id of the container.",
168
+ min_length=1,
169
+ max_length=255,
170
+ pattern=DM_EXTERNAL_ID_PATTERN,
171
+ alias="containerExternalId",
172
+ )
173
+ identifier: str = Field(
174
+ description="Identifier of the constraint.",
175
+ )
@@ -39,6 +39,7 @@ from ._limits import (
39
39
  from ._orchestrator import DmsDataModelRulesOrchestrator
40
40
  from ._performance import (
41
41
  MissingRequiresConstraint,
42
+ MissingReverseDirectRelationTargetIndex,
42
43
  SuboptimalRequiresConstraint,
43
44
  UnresolvableQueryPerformance,
44
45
  )
@@ -60,6 +61,7 @@ __all__ = [
60
61
  "ExternalContainerPropertyDoesNotExist",
61
62
  "ImplementedViewNotExisting",
62
63
  "MissingRequiresConstraint",
64
+ "MissingReverseDirectRelationTargetIndex",
63
65
  "RequiredContainerDoesNotExist",
64
66
  "RequiresConstraintCycle",
65
67
  "ReverseConnectionContainerMissing",
@@ -4,7 +4,6 @@ from dataclasses import dataclass
4
4
 
5
5
  from cognite.neat._data_model.models.dms._data_types import DirectNodeRelation
6
6
  from cognite.neat._data_model.models.dms._references import (
7
- ContainerDirectReference,
8
7
  ViewDirectReference,
9
8
  ViewReference,
10
9
  )
@@ -119,15 +118,6 @@ class ReverseConnectionContext:
119
118
  source_view_ref: ViewReference
120
119
 
121
120
 
122
- def _normalize_through_reference(
123
- source_view_ref: ViewReference, through: ContainerDirectReference | ViewDirectReference
124
- ) -> ViewDirectReference:
125
- """Normalize through reference to ViewDirectReference for consistent processing."""
126
- if isinstance(through, ContainerDirectReference):
127
- return ViewDirectReference(source=source_view_ref, identifier=through.identifier)
128
- return through
129
-
130
-
131
121
  class ReverseConnectionSourceViewMissing(DataModelRule):
132
122
  """Validates that source view referenced in reverse connection exist.
133
123
 
@@ -153,7 +143,7 @@ class ReverseConnectionSourceViewMissing(DataModelRule):
153
143
  source_view_ref,
154
144
  through,
155
145
  ) in self.validation_resources.reverse_to_direct_mapping.items():
156
- through = _normalize_through_reference(source_view_ref, through)
146
+ through = self.validation_resources.normalize_through_reference(source_view_ref, through)
157
147
  source_view = self.validation_resources.select_view(source_view_ref, through.identifier)
158
148
 
159
149
  if not source_view:
@@ -198,7 +188,7 @@ class ReverseConnectionSourcePropertyMissing(DataModelRule):
198
188
  source_view_ref,
199
189
  through,
200
190
  ) in self.validation_resources.reverse_to_direct_mapping.items():
201
- through = _normalize_through_reference(source_view_ref, through)
191
+ through = self.validation_resources.normalize_through_reference(source_view_ref, through)
202
192
  source_view = self.validation_resources.select_view(source_view_ref, through.identifier)
203
193
 
204
194
  if not source_view:
@@ -250,7 +240,7 @@ class ReverseConnectionSourcePropertyWrongType(DataModelRule):
250
240
  source_view_ref,
251
241
  through,
252
242
  ) in self.validation_resources.reverse_to_direct_mapping.items():
253
- through = _normalize_through_reference(source_view_ref, through)
243
+ through = self.validation_resources.normalize_through_reference(source_view_ref, through)
254
244
  source_view = self.validation_resources.select_view(source_view_ref, through.identifier)
255
245
 
256
246
  if not source_view:
@@ -301,40 +291,16 @@ class ReverseConnectionContainerMissing(DataModelRule):
301
291
  def validate(self) -> list[ConsistencyError]:
302
292
  errors: list[ConsistencyError] = []
303
293
 
304
- for (target_view_ref, reverse_prop_name), (
305
- source_view_ref,
306
- through,
307
- ) in self.validation_resources.reverse_to_direct_mapping.items():
308
- through = _normalize_through_reference(source_view_ref, through)
309
- source_view = self.validation_resources.select_view(source_view_ref, through.identifier)
310
-
311
- if not source_view:
312
- continue # Handled by ReverseConnectionSourceViewMissing
313
-
314
- if not (source_view_expanded := self.validation_resources.expand_view_properties(source_view_ref)):
315
- raise RuntimeError(f"{type(self).__name__}: View {source_view_ref!s} not found. This is a bug in NEAT.")
316
-
317
- if not source_view_expanded.properties or through.identifier not in source_view_expanded.properties:
318
- continue # Handled by ReverseConnectionSourcePropertyMissing
319
-
320
- source_property = source_view_expanded.properties[through.identifier]
321
-
322
- if not isinstance(source_property, ViewCorePropertyRequest):
323
- continue # Handled by ReverseConnectionSourcePropertyWrongType
324
-
325
- container_ref = source_property.container
326
- container_property_id = source_property.container_property_identifier
327
-
328
- source_container = self.validation_resources.select_container(container_ref, container_property_id)
329
- if not source_container:
294
+ for resolved in self.validation_resources.resolved_reverse_direct_relations:
295
+ if resolved.container is None:
330
296
  errors.append(
331
297
  ConsistencyError(
332
298
  message=(
333
- f"Container {container_ref!s} is missing in both the data model and CDF. "
334
- f"This container is required by view {source_view_ref!s}"
335
- f" property '{through.identifier}', "
336
- f"which configures the reverse connection '{reverse_prop_name}'"
337
- f" in target view {target_view_ref!s}."
299
+ f"Container {resolved.container_ref!s} is missing in both the data model and CDF. "
300
+ f"This container is required by view {resolved.direct_view_ref!s}"
301
+ f" property '{resolved.through_property_id}', "
302
+ f"which configures the reverse connection '{resolved.reverse_property_id}'"
303
+ f" in target view {resolved.reverse_view_ref!s}."
338
304
  ),
339
305
  fix="Define the missing container",
340
306
  code=self.code,
@@ -366,43 +332,18 @@ class ReverseConnectionContainerPropertyMissing(DataModelRule):
366
332
  def validate(self) -> list[ConsistencyError]:
367
333
  errors: list[ConsistencyError] = []
368
334
 
369
- for (target_view_ref, reverse_prop_name), (
370
- source_view_ref,
371
- through,
372
- ) in self.validation_resources.reverse_to_direct_mapping.items():
373
- through = _normalize_through_reference(source_view_ref, through)
374
- source_view = self.validation_resources.select_view(source_view_ref, through.identifier)
375
-
376
- if not source_view:
377
- continue # Handled by ReverseConnectionSourceViewMissing
378
-
379
- if not (source_view_expanded := self.validation_resources.expand_view_properties(source_view_ref)):
380
- raise RuntimeError(f"{type(self).__name__}: View {source_view_ref!s} not found. This is a bug in NEAT.")
381
-
382
- if not source_view_expanded.properties or through.identifier not in source_view_expanded.properties:
383
- continue # Handled by ReverseConnectionSourcePropertyMissing
384
-
385
- source_property = source_view_expanded.properties[through.identifier]
386
-
387
- if not isinstance(source_property, ViewCorePropertyRequest):
388
- continue # Handled by ReverseConnectionSourcePropertyWrongType
389
-
390
- container_ref = source_property.container
391
- container_property_id = source_property.container_property_identifier
392
-
393
- source_container = self.validation_resources.select_container(container_ref, container_property_id)
394
- if not source_container:
395
- continue # Handled by ReverseConnectionContainerMissing
396
-
397
- if not source_container.properties or container_property_id not in source_container.properties:
335
+ for resolved in self.validation_resources.resolved_reverse_direct_relations:
336
+ # Container must exist but property is missing
337
+ if resolved.container is not None and resolved.container_property is None:
398
338
  errors.append(
399
339
  ConsistencyError(
400
340
  message=(
401
- f"Container {container_ref!s} is missing property '{container_property_id}'. "
402
- f"This property is required by the source view {source_view_ref!s}"
403
- f" property '{through.identifier}', "
404
- f"which configures the reverse connection '{reverse_prop_name}' "
405
- f"in target view {target_view_ref!s}."
341
+ f"Container {resolved.container_ref!s} is missing "
342
+ f"property '{resolved.container_property_id}'. "
343
+ f"This property is required by the source view {resolved.direct_view_ref!s}"
344
+ f" property '{resolved.through_property_id}', "
345
+ f"which configures the reverse connection '{resolved.reverse_property_id}' "
346
+ f"in target view {resolved.reverse_view_ref!s}."
406
347
  ),
407
348
  fix="Add the missing property to the container",
408
349
  code=self.code,
@@ -434,46 +375,21 @@ class ReverseConnectionContainerPropertyWrongType(DataModelRule):
434
375
  def validate(self) -> list[ConsistencyError]:
435
376
  errors: list[ConsistencyError] = []
436
377
 
437
- for (target_view_ref, reverse_prop_name), (
438
- source_view_ref,
439
- through,
440
- ) in self.validation_resources.reverse_to_direct_mapping.items():
441
- through = _normalize_through_reference(source_view_ref, through)
442
- source_view = self.validation_resources.select_view(source_view_ref, through.identifier)
443
-
444
- if not source_view:
445
- continue # Handled by ReverseConnectionSourceViewMissing
446
-
447
- if not (source_view_expanded := self.validation_resources.expand_view_properties(source_view_ref)):
448
- raise RuntimeError(f"{type(self).__name__}: View {source_view_ref!s} not found. This is a bug in NEAT.")
449
-
450
- if not source_view_expanded.properties or through.identifier not in source_view_expanded.properties:
451
- continue # Handled by ReverseConnectionSourcePropertyMissing
452
-
453
- source_property = source_view_expanded.properties[through.identifier]
454
-
455
- if not isinstance(source_property, ViewCorePropertyRequest):
456
- continue # Handled by ReverseConnectionSourcePropertyWrongType
457
-
458
- container_ref = source_property.container
459
- container_property_id = source_property.container_property_identifier
460
-
461
- source_container = self.validation_resources.select_container(container_ref, container_property_id)
462
- if not source_container or not source_container.properties:
463
- continue # Handled by other validators
464
-
465
- container_property = source_container.properties.get(container_property_id)
466
- if not container_property:
378
+ for resolved in self.validation_resources.resolved_reverse_direct_relations:
379
+ if not resolved.container_property:
467
380
  continue # Handled by ReverseConnectionContainerPropertyMissing
468
381
 
469
- if not isinstance(container_property.type, DirectNodeRelation):
382
+ if not isinstance(resolved.container_property.type, DirectNodeRelation):
470
383
  errors.append(
471
384
  ConsistencyError(
472
385
  message=(
473
- f"Container property '{container_property_id}' in container {container_ref!s} "
474
- f"must be a direct connection, but found type '{container_property.type!s}'. "
475
- f"This property is used by source view {source_view_ref!s} property '{through.identifier}' "
476
- f"to configure reverse connection '{reverse_prop_name}' in target view {target_view_ref!s}."
386
+ f"Container property '{resolved.container_property_id}' "
387
+ f"in container {resolved.container_ref!s} "
388
+ f"must be a direct connection, but found type '{resolved.container_property.type!s}'. "
389
+ f"This property is used by source view {resolved.direct_view_ref!s} "
390
+ f"property '{resolved.through_property_id}' "
391
+ f"to configure reverse connection '{resolved.reverse_property_id}' "
392
+ f"in target view {resolved.reverse_view_ref!s}."
477
393
  ),
478
394
  fix="Change container property type to be a direct connection",
479
395
  code=self.code,
@@ -504,39 +420,17 @@ class ReverseConnectionTargetMissing(DataModelRule):
504
420
  def validate(self) -> list[Recommendation]:
505
421
  recommendations: list[Recommendation] = []
506
422
 
507
- for (target_view_ref, reverse_prop_name), (
508
- source_view_ref,
509
- through,
510
- ) in self.validation_resources.reverse_to_direct_mapping.items():
511
- through = _normalize_through_reference(source_view_ref, through)
512
- source_view = self.validation_resources.select_view(source_view_ref, through.identifier)
513
-
514
- if not source_view:
515
- continue # Handled by ReverseConnectionSourceViewMissing
516
-
517
- if not (source_view_expanded := self.validation_resources.expand_view_properties(source_view_ref)):
518
- raise RuntimeError(f"{type(self).__name__}: View {source_view_ref!s} not found. This is a bug in NEAT.")
519
-
520
- if not source_view_expanded.properties or through.identifier not in source_view_expanded.properties:
521
- continue # Handled by ReverseConnectionSourcePropertyMissing
522
-
523
- source_property = source_view_expanded.properties[through.identifier]
524
-
525
- if not isinstance(source_property, ViewCorePropertyRequest):
526
- continue # Handled by ReverseConnectionSourcePropertyWrongType
527
-
528
- actual_target_view = source_property.source
529
-
530
- if not actual_target_view:
423
+ for resolved in self.validation_resources.resolved_reverse_direct_relations:
424
+ if not resolved.direct_property.source:
531
425
  recommendations.append(
532
426
  Recommendation(
533
427
  message=(
534
- f"Source view {source_view_ref!s} property '{through.identifier}' "
428
+ f"Source view {resolved.direct_view_ref!s} property '{resolved.through_property_id}' "
535
429
  f"has no target view specified (value type is None). "
536
- f"This property is used for reverse connection '{reverse_prop_name}' "
537
- f"in target view {target_view_ref!s}. "
430
+ f"This property is used for reverse connection '{resolved.reverse_property_id}' "
431
+ f"in target view {resolved.reverse_view_ref!s}. "
538
432
  f"While this works as a hack for multi-value relations in CDF Search, "
539
- f"it's recommended to explicitly define the target view as {target_view_ref!s}."
433
+ f"it's recommended to explicitly define the target view as {resolved.reverse_view_ref!s}."
540
434
  ),
541
435
  fix="Set the property's value type to the target view for better clarity",
542
436
  code=self.code,
@@ -568,41 +462,24 @@ class ReverseConnectionPointsToAncestor(DataModelRule):
568
462
  def validate(self) -> list[Recommendation]:
569
463
  recommendations: list[Recommendation] = []
570
464
 
571
- for (target_view_ref, reverse_prop_name), (
572
- source_view_ref,
573
- through,
574
- ) in self.validation_resources.reverse_to_direct_mapping.items():
575
- through = _normalize_through_reference(source_view_ref, through)
576
- source_view = self.validation_resources.select_view(source_view_ref, through.identifier)
577
-
578
- if not source_view:
579
- continue # Handled by ReverseConnectionSourceViewMissing
580
-
581
- if not (source_view_expanded := self.validation_resources.expand_view_properties(source_view_ref)):
582
- raise RuntimeError(f"{type(self).__name__}: View {source_view_ref!s} not found. This is a bug in NEAT.")
583
-
584
- if not source_view_expanded.properties or through.identifier not in source_view_expanded.properties:
585
- continue # Handled by ReverseConnectionSourcePropertyMissing
586
-
587
- source_property = source_view_expanded.properties[through.identifier]
588
-
589
- if not isinstance(source_property, ViewCorePropertyRequest):
590
- continue # Handled by other validators
591
-
592
- actual_target_view = source_property.source
465
+ for resolved in self.validation_resources.resolved_reverse_direct_relations:
466
+ actual_target_view = resolved.direct_property.source
593
467
 
594
468
  if not actual_target_view:
595
469
  continue # Handled by ReverseConnectionTargetMissing
596
470
 
597
- if self.validation_resources.is_ancestor(target_view_ref, actual_target_view):
471
+ if self.validation_resources.is_ancestor(resolved.reverse_view_ref, actual_target_view):
598
472
  recommendations.append(
599
473
  Recommendation(
600
474
  message=(
601
- f"The direct connection property '{through.identifier}' in view {source_view_ref!s} "
602
- f"configures the reverse connection '{reverse_prop_name}' in {target_view_ref!s}. "
603
- f"Therefore, it is expected that '{through.identifier}' points to {target_view_ref!s}. "
475
+ f"The direct connection property '{resolved.through_property_id}' "
476
+ f"in view {resolved.direct_view_ref!s} "
477
+ f"configures the reverse connection '{resolved.reverse_property_id}' "
478
+ f"in {resolved.reverse_view_ref!s}. "
479
+ f"Therefore, it is expected that '{resolved.through_property_id}' "
480
+ f"points to {resolved.reverse_view_ref!s}. "
604
481
  f"However, it currently points to {actual_target_view!s}, which is an ancestor of "
605
- f"{target_view_ref!s}. "
482
+ f"{resolved.reverse_view_ref!s}. "
606
483
  "While this will allow for model to be valid, it can be a source of confusion and mistakes."
607
484
  ),
608
485
  fix="Update the direct connection property to point to the target view instead of its ancestor",
@@ -635,42 +512,24 @@ class ReverseConnectionTargetMismatch(DataModelRule):
635
512
  def validate(self) -> list[Recommendation]:
636
513
  recommendations: list[Recommendation] = []
637
514
 
638
- for (target_view_ref, reverse_prop_name), (
639
- source_view_ref,
640
- through,
641
- ) in self.validation_resources.reverse_to_direct_mapping.items():
642
- through = _normalize_through_reference(source_view_ref, through)
643
- source_view = self.validation_resources.select_view(source_view_ref, through.identifier)
644
-
645
- if not source_view:
646
- continue # Handled by ReverseConnectionSourceViewMissing
647
-
648
- if not (source_view_expanded := self.validation_resources.expand_view_properties(source_view_ref)):
649
- raise RuntimeError(f"{type(self).__name__}: View {source_view_ref!s} not found. This is a bug in NEAT.")
650
-
651
- if not source_view_expanded.properties or through.identifier not in source_view_expanded.properties:
652
- continue # Handled by ReverseConnectionSourcePropertyMissing
653
-
654
- source_property = source_view_expanded.properties[through.identifier]
655
-
656
- if not isinstance(source_property, ViewCorePropertyRequest):
657
- continue # Handled by other validators
658
-
659
- actual_target_view = source_property.source
515
+ for resolved in self.validation_resources.resolved_reverse_direct_relations:
516
+ actual_target_view = resolved.direct_property.source
660
517
 
661
518
  if not actual_target_view:
662
519
  continue # Handled by ReverseConnectionTargetMissing
663
520
 
664
- if self.validation_resources.is_ancestor(target_view_ref, actual_target_view):
665
- continue # Handled by ReverseConnectionTargetAncestor
521
+ if self.validation_resources.is_ancestor(resolved.reverse_view_ref, actual_target_view):
522
+ continue # Handled by ReverseConnectionPointsToAncestor
666
523
 
667
- if actual_target_view != target_view_ref:
524
+ if actual_target_view != resolved.reverse_view_ref:
668
525
  recommendations.append(
669
526
  Recommendation(
670
527
  message=(
671
- f"The reverse connection '{reverse_prop_name}' in view {target_view_ref!s} "
672
- f"expects its corresponding direct connection in view {source_view_ref!s} "
673
- f"(property '{through.identifier}') to point back to {target_view_ref!s}, "
528
+ f"The reverse connection '{resolved.reverse_property_id}' "
529
+ f"in view {resolved.reverse_view_ref!s} "
530
+ f"expects its corresponding direct connection in view {resolved.direct_view_ref!s} "
531
+ f"(property '{resolved.through_property_id}') "
532
+ f"to point back to {resolved.reverse_view_ref!s}, "
674
533
  f"but it actually points to {actual_target_view!s}."
675
534
  ),
676
535
  fix="Update the direct connection property to point back to the correct target view",
@@ -2,6 +2,8 @@
2
2
 
3
3
  from cognite.neat._data_model._analysis import RequiresChangeStatus
4
4
  from cognite.neat._data_model._constants import COGNITE_SPACES
5
+ from cognite.neat._data_model.models.dms._data_types import DirectNodeRelation
6
+ from cognite.neat._data_model.models.dms._indexes import BtreeIndex
5
7
  from cognite.neat._data_model.rules.dms._base import DataModelRule
6
8
  from cognite.neat._issues import Recommendation
7
9
 
@@ -193,3 +195,76 @@ class UnresolvableQueryPerformance(DataModelRule):
193
195
  )
194
196
 
195
197
  return recommendations
198
+
199
+
200
+ class MissingReverseDirectRelationTargetIndex(DataModelRule):
201
+ """
202
+ Recommends adding a cursorable index on direct relation properties that are
203
+ targets of reverse direct relations for query performance.
204
+
205
+ ## What it does
206
+ Identifies direct relation properties that are referenced by reverse direct relations
207
+ but lack a cursorable B-tree index. When querying through a reverse direct relation,
208
+ CDF needs to look up nodes that have the direct relation pointing to the
209
+ source nodes. Without an index, this requires scanning many nodes inefficiently.
210
+
211
+ ## Why is this important?
212
+ Traversing a reverse direct relation (inwards direction) requires an index on the
213
+ target direct relation property. Without this index, queries will be inefficient,
214
+ potentially leading to timeouts over time, as they won't scale well with data volume.
215
+
216
+ The exception is when the target direct relation is a list property. In that case,
217
+ this validator will not flag them, as reverse direct relations targeting lists of
218
+ direct relations needs to be traversed using the `instances/search` endpoint instead,
219
+ which does not directly benefit from adding indexes to container properties.
220
+
221
+ ## Example
222
+ View `WindFarm` has a reverse property `turbines` through `WindTurbine.windFarm`.
223
+ Container `WindTurbine` should have a cursorable B-tree index on the `windFarm`
224
+ property to enable efficient traversal from WindFarm to its turbines.
225
+ """
226
+
227
+ code = f"{BASE_CODE}-004"
228
+ issue_type = Recommendation
229
+ alpha = True
230
+
231
+ def validate(self) -> list[Recommendation]:
232
+ recommendations: list[Recommendation] = []
233
+
234
+ for resolved in self.validation_resources.resolved_reverse_direct_relations:
235
+ # Skip if container or container property couldn't be resolved
236
+ if not resolved.container or not resolved.container_property:
237
+ continue
238
+
239
+ # Must be a DirectNodeRelation type (other types handled by ReverseConnectionContainerPropertyWrongType)
240
+ if not isinstance(resolved.container_property.type, DirectNodeRelation):
241
+ continue
242
+
243
+ # Skip if this is a list direct relation - indexes are not supported for list properties
244
+ if resolved.container_property.type.list:
245
+ continue
246
+
247
+ # Skip if there's already a cursorable B-tree index on this property
248
+ if resolved.container.indexes and any(
249
+ isinstance(index, BtreeIndex)
250
+ and index.cursorable
251
+ and resolved.container_property_id in index.properties
252
+ for index in resolved.container.indexes.values()
253
+ ):
254
+ continue
255
+
256
+ recommendations.append(
257
+ Recommendation(
258
+ message=(
259
+ f"View '{resolved.reverse_view_ref!s}' has a reverse direct relation "
260
+ f"'{resolved.reverse_property_id}' that points to container "
261
+ f"'{resolved.container_ref!s}' property '{resolved.container_property_id}'. "
262
+ f"Add a cursorable B-tree index on this target container property "
263
+ f"to enable efficient query traversal."
264
+ ),
265
+ fix="Add a cursorable B-tree index on the target direct relation property",
266
+ code=self.code,
267
+ )
268
+ )
269
+
270
+ return recommendations
cognite/neat/_version.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "1.0.32"
1
+ __version__ = "1.0.34"
2
2
  __engine__ = "^2.0.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cognite-neat
3
- Version: 1.0.32
3
+ Version: 1.0.34
4
4
  Summary: Knowledge graph transformation
5
5
  Author: Nikola Vasiljevic, Anders Albert
6
6
  Author-email: Nikola Vasiljevic <nikola.vasiljevic@cognite.com>, Anders Albert <anders.albert@cognite.com>
@@ -17,19 +17,19 @@ cognite/neat/_client/statistics_api.py,sha256=3KNVsD2OV-EDFb1eqIrCXfYwjvyCDO80bP
17
17
  cognite/neat/_client/views_api.py,sha256=YMaw7IaxU4gmixpd_t1u9JK9BHfNerf5DMNinGPCAa0,3692
18
18
  cognite/neat/_config.py,sha256=IJS_R-86M4SLxdo644LZ0dE9h0jg2XBL1A4QKWJj0TQ,10160
19
19
  cognite/neat/_data_model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- cognite/neat/_data_model/_analysis.py,sha256=sZO2NATkyuSzlo3uRgXJ6B3btKWkwblNos7SXT6LHLc,43779
20
+ cognite/neat/_data_model/_analysis.py,sha256=KHN6QgRIjndkQJO_m4pX9_yqRsA5LZzvUgCOgM8ZZBA,47799
21
21
  cognite/neat/_data_model/_constants.py,sha256=pPDtRwBsOq80IkYXRJ0XaRfFOb7CPvxz81IQzcPgAuc,1889
22
22
  cognite/neat/_data_model/_identifiers.py,sha256=lDLvMvYDgRNFgk5GmxWzOUunG7M3synAciNjzJI0m_o,1913
23
23
  cognite/neat/_data_model/_shared.py,sha256=H0gFqa8tKFNWuvdat5jL6OwySjCw3aQkLPY3wtb9Wrw,1302
24
24
  cognite/neat/_data_model/_snapshot.py,sha256=JBaKmL0Tmprz59SZ1JeB49BPMB8Hqa-OAOt0Bai8cw4,6305
25
25
  cognite/neat/_data_model/deployer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  cognite/neat/_data_model/deployer/_differ.py,sha256=1ircRBCoaFooSzMTmTZBTORHeAhDa8YtDEnVwBo6TUI,4742
27
- cognite/neat/_data_model/deployer/_differ_container.py,sha256=mcy7PhUOfnvAxnZWNoeNRmiXa8ovIn0W6YoqfzVYyiQ,14665
27
+ cognite/neat/_data_model/deployer/_differ_container.py,sha256=AGD4emPRCEdFGFN4rJzLn8JhOjbTrHtYVtXr8q9vSPY,14668
28
28
  cognite/neat/_data_model/deployer/_differ_data_model.py,sha256=iA7Xp-7NRvzZJXLLpJaLebkKKpv_VCBKPX6f-RU9wBk,1864
29
29
  cognite/neat/_data_model/deployer/_differ_space.py,sha256=J_AaqiseLpwQsOkKc7gmho4U2oSWAGVeEdQNepZiWw0,343
30
30
  cognite/neat/_data_model/deployer/_differ_view.py,sha256=g1xHwsoxFUaTOTtQa19nntKF3rxFzc2FxpKKFAUN_NE,11412
31
- cognite/neat/_data_model/deployer/data_classes.py,sha256=cq86u7wuKCcvH-A_cGI_gWzlQCTIG6mrXG2MahdiGco,27299
32
- cognite/neat/_data_model/deployer/deployer.py,sha256=lEdTh4jOwTxLkSEx-SlcnXUZyPZCUtIzop1zhAe2s44,19681
31
+ cognite/neat/_data_model/deployer/data_classes.py,sha256=HhByk8m7SbEPAKkUNDF7koRtQPHhILpVXN3f83icfKs,27028
32
+ cognite/neat/_data_model/deployer/deployer.py,sha256=CaD_N6fy2wk6TfrAyRssC65vf_FRvl3yfLQXtGUFdCo,20870
33
33
  cognite/neat/_data_model/exporters/__init__.py,sha256=AskjmB_0Vqib4kN84bWt8-M8nO42QypFf-l-E8oA5W8,482
34
34
  cognite/neat/_data_model/exporters/_api_exporter.py,sha256=nBDHx9dGbaje0T4IEQv0Kulk2Yu7FkPXgXK_MgLbE50,4948
35
35
  cognite/neat/_data_model/exporters/_base.py,sha256=rG_qAU5i5Hh5hUMep2UmDFFZID4x3LEenL6Z5C6N8GQ,646
@@ -59,10 +59,10 @@ cognite/neat/_data_model/models/dms/_constraints.py,sha256=cyGgDlByXAuSMWJg7Oc25
59
59
  cognite/neat/_data_model/models/dms/_container.py,sha256=wtQbNUwtpymltT1jav8wD4kIfjaIYnvhhz1KS0ffAbo,6044
60
60
  cognite/neat/_data_model/models/dms/_data_model.py,sha256=tq_JGNN-1JxG46bhBhunZiLedklYbDXFEfINB0x3a3Q,3219
61
61
  cognite/neat/_data_model/models/dms/_data_types.py,sha256=FMt_d5aJD-o3s9VQWyyCVlHk7D_p3RlSNXBP1OACPs4,6424
62
- cognite/neat/_data_model/models/dms/_http.py,sha256=YIRRowqkphFAYkx3foTeLyPMe9fNnmzhUCBDXe0u9Kk,926
62
+ cognite/neat/_data_model/models/dms/_http.py,sha256=FxOWb0qDKOc7urVXp8J0xl8gRV_psHWwfQLyAS8sLeM,1074
63
63
  cognite/neat/_data_model/models/dms/_indexes.py,sha256=ZtXe8ABuRcsAwRIZ9FCanS3uwZHpkOAhvDvjSXtx_Fs,900
64
64
  cognite/neat/_data_model/models/dms/_limits.py,sha256=-vwRutprJ7rPXLleSxCh_satR9AqRAvEMig5wSVBEXg,3596
65
- cognite/neat/_data_model/models/dms/_references.py,sha256=Mx_nxfvOrvAx7nvebhhbFw6eRm3nHqeFW5P5AqADUlM,3890
65
+ cognite/neat/_data_model/models/dms/_references.py,sha256=2l9ZD4ZCj-gzc0QB-rV4iVm8-fsnczziEOn2mZTaEfE,4934
66
66
  cognite/neat/_data_model/models/dms/_schema.py,sha256=2JFLcm52smzPdtZ69Lf02UbYAD8I_hpRbI7ZAzdxJJs,641
67
67
  cognite/neat/_data_model/models/dms/_space.py,sha256=mj6gID4vcAGsHNtgfXm4_4FMOQbUOkMd3HaYEdy07XM,1895
68
68
  cognite/neat/_data_model/models/dms/_types.py,sha256=5-cgC53AG186OZUqkltv7pMjcGNLuH7Etbn8IUcgk1c,447
@@ -81,15 +81,15 @@ cognite/neat/_data_model/rules/cdf/__init__.py,sha256=Bn9_DkkO4OPUYh0CsM3Pf1fD_U
81
81
  cognite/neat/_data_model/rules/cdf/_base.py,sha256=59fqmFi_Gvzx3swLsIyc2ACkHAzKAKiqNlwwGnW5bDg,128
82
82
  cognite/neat/_data_model/rules/cdf/_orchestrator.py,sha256=egzcYUuFDd8Vm7neBpZFofqZBfYjuElAjYwJWAScutc,2250
83
83
  cognite/neat/_data_model/rules/cdf/_spaces.py,sha256=Lsfrp2e7yzgRimkD0qynJ5M2S7Zwzplfg_4Pim1voHk,1605
84
- cognite/neat/_data_model/rules/dms/__init__.py,sha256=ThCvtSCPzEhciJNnzBKDoTWMimw5U74MbfUwoF6e7F8,2950
84
+ cognite/neat/_data_model/rules/dms/__init__.py,sha256=LiZNGYClXHrgSbG0K6tk8nYN2B0VX3JlKn_minyCiI8,3042
85
85
  cognite/neat/_data_model/rules/dms/_ai_readiness.py,sha256=1KvOk3Q8aQkE8G6oHml7O-SHiTeIBNmyKPWr3hPcCCw,16133
86
86
  cognite/neat/_data_model/rules/dms/_base.py,sha256=BnzBELon389lEXJi7dlb9t3SPjiXgVquiI445I7-APU,134
87
- cognite/neat/_data_model/rules/dms/_connections.py,sha256=vi_cJp5S4MpfX-jA1eqZClnHdKznkQMDvZk9iIj42mM,30343
87
+ cognite/neat/_data_model/rules/dms/_connections.py,sha256=yvlcb_-lBILZpMZ9Zcp8R1sC-G6DhCdyqjffwOnlmUI,23416
88
88
  cognite/neat/_data_model/rules/dms/_consistency.py,sha256=7u-EDQk08eV4CJoY5nRbrYXs2cbkQkH6TQCcPOyUGI8,2425
89
89
  cognite/neat/_data_model/rules/dms/_containers.py,sha256=eb0eLy13n7OozKp2vrE_oXqD6m2uyX-9fhoCqU_gJqc,10542
90
90
  cognite/neat/_data_model/rules/dms/_limits.py,sha256=L6dAsLWVBEpfVFZgw3SvJk1PnEGf4lhJ3SOXjDUsY2Q,14820
91
91
  cognite/neat/_data_model/rules/dms/_orchestrator.py,sha256=tTXxLH1yThAeTW_GdDeD-to19faK72c_z8QL9kzgGJc,3069
92
- cognite/neat/_data_model/rules/dms/_performance.py,sha256=7LTs1E86xA-hvL4fqiIhAHcgIeohwb1pODzh3o6tuzQ,8601
92
+ cognite/neat/_data_model/rules/dms/_performance.py,sha256=Tp7D5cCEOqjfst9tLKuA9X2F9f4lQLJHEiGojm-rQUI,12229
93
93
  cognite/neat/_data_model/rules/dms/_views.py,sha256=Tp4VW2512eGSGgeMRoUFh7hqK2vsZD8ryN_EJ7QJoEg,6335
94
94
  cognite/neat/_exceptions.py,sha256=hOjPL1vFNNAZzqAHFB9l9ek-XJEBKcqiaPk0onwLPns,2540
95
95
  cognite/neat/_issues.py,sha256=wH1mnkrpBsHUkQMGUHFLUIQWQlfJ_qMfdF7q0d9wNhY,1871
@@ -333,9 +333,9 @@ cognite/neat/_v0/session/_template.py,sha256=BNcvrW5y7LWzRM1XFxZkfR1Nc7e8UgjBClH
333
333
  cognite/neat/_v0/session/_to.py,sha256=AnsRSDDdfFyYwSgi0Z-904X7WdLtPfLlR0x1xsu_jAo,19447
334
334
  cognite/neat/_v0/session/_wizard.py,sha256=baPJgXAAF3d1bn4nbIzon1gWfJOeS5T43UXRDJEnD3c,1490
335
335
  cognite/neat/_v0/session/exceptions.py,sha256=jv52D-SjxGfgqaHR8vnpzo0SOJETIuwbyffSWAxSDJw,3495
336
- cognite/neat/_version.py,sha256=yW-UQpMiFBxk9CkqtMOupox-RheZyNu-70iZhHFcuEI,45
336
+ cognite/neat/_version.py,sha256=fdWCQSFXmDmKK6idWGSpmri7JwQy0lv0A26K5YheUxQ,45
337
337
  cognite/neat/legacy.py,sha256=DMFeLCSBLT2enk-nm1KfX1rKR2DQDpxY-w6ThY0y9c8,421
338
338
  cognite/neat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
339
- cognite_neat-1.0.32.dist-info/WHEEL,sha256=e_m4S054HL0hyR3CpOk-b7Q7fDX6BuFkgL5OjAExXas,80
340
- cognite_neat-1.0.32.dist-info/METADATA,sha256=7ZoBFgcFA7TUMrocwakJBvfuWt1Amux3_mqWNtqEMAE,6872
341
- cognite_neat-1.0.32.dist-info/RECORD,,
339
+ cognite_neat-1.0.34.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
340
+ cognite_neat-1.0.34.dist-info/METADATA,sha256=0_I8k0cZIjhgDYicQb2oUsbesqbLDLr4-MzFGAC--3w,6872
341
+ cognite_neat-1.0.34.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.27
2
+ Generator: uv 0.9.28
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any