cognite-neat 0.123.43__py3-none-any.whl → 0.125.0__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.

Potentially problematic release.


This version of cognite-neat might be problematic. Click here for more details.

Files changed (44) hide show
  1. cognite/neat/_data_model/importers/__init__.py +2 -1
  2. cognite/neat/_data_model/importers/_table_importer/__init__.py +0 -0
  3. cognite/neat/_data_model/importers/_table_importer/data_classes.py +141 -0
  4. cognite/neat/_data_model/importers/_table_importer/importer.py +76 -0
  5. cognite/neat/_data_model/importers/_table_importer/source.py +89 -0
  6. cognite/neat/_data_model/models/dms/__init__.py +12 -1
  7. cognite/neat/_data_model/models/dms/_base.py +1 -1
  8. cognite/neat/_data_model/models/dms/_constraints.py +5 -2
  9. cognite/neat/_data_model/models/dms/_data_types.py +26 -10
  10. cognite/neat/_data_model/models/dms/_indexes.py +6 -3
  11. cognite/neat/_data_model/models/dms/_types.py +17 -0
  12. cognite/neat/_data_model/models/dms/_view_property.py +14 -25
  13. cognite/neat/_data_model/models/entities/__init__.py +2 -1
  14. cognite/neat/_data_model/models/entities/_parser.py +32 -0
  15. cognite/neat/_exceptions.py +17 -0
  16. cognite/neat/_session/__init__.py +0 -0
  17. cognite/neat/_session/_session.py +33 -0
  18. cognite/neat/_session/_state_machine/__init__.py +23 -0
  19. cognite/neat/_session/_state_machine/_base.py +27 -0
  20. cognite/neat/_session/_state_machine/_states.py +150 -0
  21. cognite/neat/_utils/text.py +22 -0
  22. cognite/neat/_utils/useful_types.py +4 -0
  23. cognite/neat/_utils/validation.py +63 -30
  24. cognite/neat/_version.py +1 -1
  25. cognite/neat/v0/core/_data_model/_constants.py +1 -0
  26. cognite/neat/v0/core/_data_model/exporters/_data_model2excel.py +3 -3
  27. cognite/neat/v0/core/_data_model/importers/_dms2data_model.py +4 -3
  28. cognite/neat/v0/core/_data_model/importers/_spreadsheet2data_model.py +85 -5
  29. cognite/neat/v0/core/_data_model/models/entities/__init__.py +2 -0
  30. cognite/neat/v0/core/_data_model/models/entities/_single_value.py +14 -0
  31. cognite/neat/v0/core/_data_model/models/entities/_types.py +10 -0
  32. cognite/neat/v0/core/_data_model/models/physical/_exporter.py +3 -11
  33. cognite/neat/v0/core/_data_model/models/physical/_unverified.py +61 -12
  34. cognite/neat/v0/core/_data_model/models/physical/_validation.py +8 -4
  35. cognite/neat/v0/core/_data_model/models/physical/_verified.py +86 -15
  36. cognite/neat/v0/core/_data_model/transformers/_converters.py +11 -4
  37. cognite/neat/v0/core/_store/_instance.py +33 -0
  38. cognite/neat/v0/core/_utils/spreadsheet.py +17 -3
  39. cognite/neat/v0/session/_base.py +2 -0
  40. cognite/neat/v0/session/_diff.py +51 -0
  41. {cognite_neat-0.123.43.dist-info → cognite_neat-0.125.0.dist-info}/METADATA +1 -1
  42. {cognite_neat-0.123.43.dist-info → cognite_neat-0.125.0.dist-info}/RECORD +44 -32
  43. {cognite_neat-0.123.43.dist-info → cognite_neat-0.125.0.dist-info}/WHEEL +0 -0
  44. {cognite_neat-0.123.43.dist-info → cognite_neat-0.125.0.dist-info}/licenses/LICENSE +0 -0
@@ -9,6 +9,7 @@ from pydantic import (
9
9
  from ._single_value import (
10
10
  AssetEntity,
11
11
  ConceptEntity,
12
+ ContainerConstraintEntity,
12
13
  ContainerEntity,
13
14
  ContainerIndexEntity,
14
15
  RelationshipEntity,
@@ -76,6 +77,15 @@ ContainerIndexListType = Annotated[
76
77
  when_used="unless-none",
77
78
  ),
78
79
  ]
80
+ ContainerConstraintListType = Annotated[
81
+ list[ContainerConstraintEntity],
82
+ BeforeValidator(_split_str),
83
+ PlainSerializer(
84
+ _join_str,
85
+ return_type=str,
86
+ when_used="unless-none",
87
+ ),
88
+ ]
79
89
 
80
90
  ViewEntityList = Annotated[
81
91
  list[ViewEntity],
@@ -1,4 +1,3 @@
1
- import hashlib
2
1
  import warnings
3
2
  from collections import defaultdict
4
3
  from collections.abc import Collection, Hashable, Sequence
@@ -26,7 +25,6 @@ from cognite.neat.v0.core._constants import (
26
25
  DMS_DIRECT_RELATION_LIST_DEFAULT_LIMIT,
27
26
  DMS_PRIMITIVE_LIST_DEFAULT_LIMIT,
28
27
  )
29
- from cognite.neat.v0.core._data_model._constants import CONSTRAINT_ID_MAX_LENGTH
30
28
  from cognite.neat.v0.core._data_model.models.data_types import DataType, Double, Enum, Float, LangString, String
31
29
  from cognite.neat.v0.core._data_model.models.entities import (
32
30
  ConceptEntity,
@@ -381,7 +379,8 @@ class _DMSExporter:
381
379
  for prop in container_properties:
382
380
  if prop.container_property is not None:
383
381
  for constraint in prop.constraint or []:
384
- uniqueness_properties[constraint].add(prop.container_property)
382
+ uniqueness_properties[cast(str, constraint.suffix)].add(prop.container_property)
383
+
385
384
  for constraint_name, properties in uniqueness_properties.items():
386
385
  container.constraints = container.constraints or {}
387
386
  container.constraints[constraint_name] = dm.UniquenessConstraint(properties=list(properties))
@@ -411,19 +410,12 @@ class _DMSExporter:
411
410
  for container in containers:
412
411
  if container.constraints:
413
412
  container.constraints = {
414
- self._truncate_constraint_name(name): const
413
+ name: const
415
414
  for name, const in container.constraints.items()
416
415
  if not (isinstance(const, dm.RequiresConstraint) and const.require in container_to_drop)
417
416
  }
418
417
  return ContainerApplyDict([container for container in containers if container.as_id() not in container_to_drop])
419
418
 
420
- @staticmethod
421
- def _truncate_constraint_name(name: str) -> str:
422
- if len(name) <= CONSTRAINT_ID_MAX_LENGTH:
423
- return name
424
- half_length = int(CONSTRAINT_ID_MAX_LENGTH / 2)
425
- return f"{name[: half_length - 1]}{hashlib.md5(name.encode()).hexdigest()[:half_length]}"
426
-
427
419
  @staticmethod
428
420
  def _gather_properties(
429
421
  properties: Sequence[PhysicalProperty],
@@ -21,6 +21,7 @@ from cognite.neat.v0.core._data_model.models._base_unverified import (
21
21
  )
22
22
  from cognite.neat.v0.core._data_model.models.data_types import DataType
23
23
  from cognite.neat.v0.core._data_model.models.entities import (
24
+ ContainerConstraintEntity,
24
25
  ContainerEntity,
25
26
  ContainerIndexEntity,
26
27
  DMSNodeEntity,
@@ -138,7 +139,7 @@ class UnverifiedPhysicalProperty(UnverifiedComponent[PhysicalProperty]):
138
139
  container: str | None = None
139
140
  container_property: str | None = None
140
141
  index: str | list[str | ContainerIndexEntity] | ContainerIndexEntity | None = None
141
- constraint: str | list[str] | None = None
142
+ constraint: str | list[str] | list[ContainerConstraintEntity] | ContainerConstraintEntity | None = None
142
143
  neatId: str | URIRef | None = None
143
144
  conceptual: str | URIRef | None = None
144
145
 
@@ -197,6 +198,8 @@ class UnverifiedPhysicalProperty(UnverifiedComponent[PhysicalProperty]):
197
198
  else:
198
199
  raise TypeError(f"Unexpected type for index: {type(index)}")
199
200
  output["Index"] = index_list
201
+
202
+ output["Constraint"] = _parse_constraints(self.constraint, default_space)
200
203
  return output
201
204
 
202
205
  def referenced_view(self, default_space: str, default_version: str) -> ViewEntity:
@@ -249,7 +252,7 @@ class UnverifiedPhysicalContainer(UnverifiedComponent[PhysicalContainer]):
249
252
  container: str
250
253
  name: str | None = None
251
254
  description: str | None = None
252
- constraint: str | None = None
255
+ constraint: str | list[str] | list[ContainerConstraintEntity] | ContainerConstraintEntity | None = None
253
256
  neatId: str | URIRef | None = None
254
257
  used_for: Literal["node", "edge", "all"] | None = None
255
258
 
@@ -260,14 +263,7 @@ class UnverifiedPhysicalContainer(UnverifiedComponent[PhysicalContainer]):
260
263
  def dump(self, default_space: str) -> dict[str, Any]: # type: ignore[override]
261
264
  output = super().dump()
262
265
  output["Container"] = self.as_entity_id(default_space, return_on_failure=True)
263
- output["Constraint"] = (
264
- [
265
- ContainerEntity.load(constraint.strip(), space=default_space, return_on_failure=True)
266
- for constraint in self.constraint.split(",")
267
- ]
268
- if self.constraint
269
- else None
270
- )
266
+ output["Constraint"] = _parse_constraints(self.constraint, default_space)
271
267
  return output
272
268
 
273
269
  @overload
@@ -286,9 +282,13 @@ class UnverifiedPhysicalContainer(UnverifiedComponent[PhysicalContainer]):
286
282
  @classmethod
287
283
  def from_container(cls, container: dm.ContainerApply) -> "UnverifiedPhysicalContainer":
288
284
  constraints: list[str] = []
289
- for _, constraint_obj in (container.constraints or {}).items():
285
+ for constraint_name, constraint_obj in (container.constraints or {}).items():
290
286
  if isinstance(constraint_obj, dm.RequiresConstraint):
291
- constraints.append(str(ContainerEntity.from_id(constraint_obj.require)))
287
+ constraint = ContainerConstraintEntity(
288
+ prefix="requires", suffix=constraint_name, container=ContainerEntity.from_id(constraint_obj.require)
289
+ )
290
+ constraints.append(str(constraint))
291
+
292
292
  # UniquenessConstraint it handled in the properties
293
293
  container_entity = ContainerEntity.from_id(container.as_id())
294
294
  return cls(
@@ -506,3 +506,52 @@ class UnverifiedPhysicalDataModel(UnverifiedDataModel[PhysicalDataModel]):
506
506
  def imported_views_and_containers_ids(self) -> tuple[set[ViewId], set[ContainerId]]:
507
507
  views, containers = self.imported_views_and_containers()
508
508
  return {view.as_id() for view in views}, {container.as_id() for container in containers}
509
+
510
+
511
+ def _parse_constraints(
512
+ constraint: str | list[str] | list[ContainerConstraintEntity] | ContainerConstraintEntity | None,
513
+ default_space: str | None = None,
514
+ ) -> list[ContainerConstraintEntity | PhysicalUnknownEntity] | None:
515
+ """Parse constraint input into a standardized list of ContainerConstraintEntity objects.
516
+
517
+ Args:
518
+ constraint: The constraint input in various formats
519
+ default_space: Default space to use when loading constraint entities
520
+
521
+ Returns:
522
+ List of parsed constraint entities, or None if no constraints
523
+ """
524
+ if constraint is None:
525
+ return None
526
+
527
+ if isinstance(constraint, ContainerConstraintEntity):
528
+ return [constraint]
529
+
530
+ if isinstance(constraint, str) and "," not in constraint:
531
+ return [ContainerConstraintEntity.load(constraint, return_on_failure=True, space=default_space)]
532
+
533
+ if isinstance(constraint, str):
534
+ return [
535
+ ContainerConstraintEntity.load(constraint_item.strip(), return_on_failure=True, space=default_space)
536
+ for constraint_item in SPLIT_ON_COMMA_PATTERN.split(constraint)
537
+ if constraint_item.strip()
538
+ ]
539
+
540
+ if isinstance(constraint, list):
541
+ constraint_list: list[ContainerConstraintEntity | PhysicalUnknownEntity] = []
542
+ for constraint_item in constraint:
543
+ if isinstance(constraint_item, ContainerConstraintEntity):
544
+ constraint_list.append(constraint_item)
545
+ elif isinstance(constraint_item, str):
546
+ constraint_list.extend(
547
+ [
548
+ ContainerConstraintEntity.load(idx.strip(), return_on_failure=True, space=default_space)
549
+ for idx in SPLIT_ON_COMMA_PATTERN.split(constraint_item)
550
+ if idx.strip()
551
+ ]
552
+ )
553
+ else:
554
+ raise TypeError(f"Unexpected type for constraint: {type(constraint_item)}")
555
+ return constraint_list
556
+
557
+ raise TypeError(f"Unexpected type for constraint: {type(constraint)}")
@@ -3,6 +3,7 @@ from collections import Counter, defaultdict
3
3
  from collections.abc import Mapping
4
4
  from dataclasses import dataclass
5
5
  from functools import lru_cache
6
+ from typing import cast
6
7
 
7
8
  from cognite.client import data_modeling as dm
8
9
  from cognite.client.data_classes.data_modeling import ContainerList, ViewId, ViewList
@@ -121,9 +122,9 @@ class PhysicalValidation:
121
122
  view_with_properties.add(prop.view)
122
123
 
123
124
  for container in self._containers or []:
124
- for required in container.constraint or []:
125
- if required not in existing_containers:
126
- imported_containers.add(required)
125
+ for constraint in container.constraint or []:
126
+ if constraint.container not in existing_containers:
127
+ imported_containers.add(cast(ContainerEntity, constraint.container))
127
128
 
128
129
  if include_views_with_no_properties:
129
130
  extra_views = existing_views - view_with_properties
@@ -470,8 +471,11 @@ class PhysicalValidation:
470
471
  )
471
472
  )
472
473
  constraint_definitions = {
473
- ",".join(prop.constraint) for _, prop in properties if prop.constraint is not None
474
+ ",".join([str(constraint) for constraint in prop.constraint])
475
+ for _, prop in properties
476
+ if prop.constraint is not None
474
477
  }
478
+
475
479
  if len(constraint_definitions) > 1:
476
480
  errors.append(
477
481
  PropertyDefinitionDuplicatedError[dm.ContainerId](
@@ -10,6 +10,7 @@ from pydantic_core.core_schema import SerializationInfo, ValidationInfo
10
10
 
11
11
  from cognite.neat.v0.core._client.data_classes.schema import DMSSchema
12
12
  from cognite.neat.v0.core._constants import DMS_CONTAINER_LIST_MAX_LIMIT
13
+ from cognite.neat.v0.core._data_model._constants import CONSTRAINT_ID_MAX_LENGTH
13
14
  from cognite.neat.v0.core._data_model.models._base_verified import (
14
15
  BaseVerifiedDataModel,
15
16
  BaseVerifiedMetadata,
@@ -25,7 +26,6 @@ from cognite.neat.v0.core._data_model.models._types import (
25
26
  ConceptEntityType,
26
27
  ContainerEntityType,
27
28
  PhysicalPropertyType,
28
- StrListType,
29
29
  URIRefType,
30
30
  ViewEntityType,
31
31
  )
@@ -45,7 +45,10 @@ from cognite.neat.v0.core._data_model.models.entities import (
45
45
  ViewEntity,
46
46
  ViewEntityList,
47
47
  )
48
- from cognite.neat.v0.core._data_model.models.entities._types import ContainerEntityList, ContainerIndexListType
48
+ from cognite.neat.v0.core._data_model.models.entities._types import (
49
+ ContainerConstraintListType,
50
+ ContainerIndexListType,
51
+ )
49
52
  from cognite.neat.v0.core._issues.errors import NeatValueError
50
53
  from cognite.neat.v0.core._issues.warnings import NeatValueWarning, PropertyDefinitionWarning
51
54
 
@@ -149,7 +152,7 @@ class PhysicalProperty(SheetRow):
149
152
  alias="Index",
150
153
  description="The names of the indexes (comma separated) that should be created for the property.",
151
154
  )
152
- constraint: StrListType | None = Field(
155
+ constraint: ContainerConstraintListType | None = Field(
153
156
  None,
154
157
  alias="Constraint",
155
158
  description="The names of the uniquness (comma separated) that should be created for the property.",
@@ -264,11 +267,12 @@ class PhysicalProperty(SheetRow):
264
267
  def index_set_correctly(cls, value: list[ContainerIndexEntity] | None, info: ValidationInfo) -> Any:
265
268
  if value is None:
266
269
  return value
267
- try:
268
- container = str(info.data["container"])
269
- container_property = str(info.data["container_property"])
270
- except KeyError:
271
- raise ValueError("Container and container property must be set to use indexes") from None
270
+
271
+ container = info.data["container"]
272
+ container_property = info.data["container_property"]
273
+
274
+ if not container or not container_property:
275
+ raise ValueError("Container and container property must be set to use indexes")
272
276
  max_count = info.data.get("max_count")
273
277
  is_list = (
274
278
  max_count is not None and (isinstance(max_count, int | float) and max_count > 1)
@@ -277,7 +281,7 @@ class PhysicalProperty(SheetRow):
277
281
  if index.prefix is Undefined:
278
282
  message = f"The type of index is not defined. Please set 'inverted:{index!s}' or 'btree:{index!s}'."
279
283
  warnings.warn(
280
- PropertyDefinitionWarning(container, "container property", container_property, message),
284
+ PropertyDefinitionWarning(str(container), "container property", str(container_property), message),
281
285
  stacklevel=2,
282
286
  )
283
287
  elif index.prefix == "inverted" and not is_list:
@@ -286,7 +290,7 @@ class PhysicalProperty(SheetRow):
286
290
  "Please consider using btree index instead."
287
291
  )
288
292
  warnings.warn(
289
- PropertyDefinitionWarning(container, "container property", container_property, message),
293
+ PropertyDefinitionWarning(str(container), "container property", str(container_property), message),
290
294
  stacklevel=2,
291
295
  )
292
296
  elif index.prefix == "btree" and is_list:
@@ -295,17 +299,49 @@ class PhysicalProperty(SheetRow):
295
299
  "Please consider using inverted index instead."
296
300
  )
297
301
  warnings.warn(
298
- PropertyDefinitionWarning(container, "container property", container_property, message),
302
+ PropertyDefinitionWarning(str(container), "container property", str(container_property), message),
299
303
  stacklevel=2,
300
304
  )
301
305
  if index.prefix == "inverted" and (index.cursorable is not None or index.by_space is not None):
302
306
  message = "Cursorable and bySpace are not supported for inverted indexes. These will be ignored."
303
307
  warnings.warn(
304
- PropertyDefinitionWarning(container, "container property", container_property, message),
308
+ PropertyDefinitionWarning(str(container), "container property", str(container_property), message),
305
309
  stacklevel=2,
306
310
  )
307
311
  return value
308
312
 
313
+ @field_validator("constraint", mode="after")
314
+ @classmethod
315
+ def constraint_set_correctly(cls, value: ContainerConstraintListType | None, info: ValidationInfo) -> Any:
316
+ if value is None:
317
+ return value
318
+
319
+ container = info.data["container"]
320
+ container_property = info.data["container_property"]
321
+
322
+ if not container or not container_property:
323
+ raise ValueError("Container and container property must be set to use constraint")
324
+
325
+ for constraint in value:
326
+ if constraint.prefix is Undefined:
327
+ message = f"The type of constraint is not defined. Please set 'uniqueness:{constraint!s}'."
328
+ warnings.warn(
329
+ PropertyDefinitionWarning(str(container), "container property", str(container_property), message),
330
+ stacklevel=2,
331
+ )
332
+ elif constraint.prefix != "uniqueness":
333
+ message = (
334
+ f"Unsupported constraint type on container property"
335
+ f" '{constraint.prefix}'. Currently only 'uniqueness' is supported."
336
+ )
337
+ raise ValueError(message) from None
338
+
339
+ if len(constraint.suffix) > CONSTRAINT_ID_MAX_LENGTH:
340
+ message = f"Constraint id '{constraint.suffix}' exceeds maximum length of {CONSTRAINT_ID_MAX_LENGTH}."
341
+ raise ValueError(message) from None
342
+
343
+ return value
344
+
309
345
  @field_serializer("value_type", when_used="always")
310
346
  def as_dms_type(self, value_type: DataType | EdgeEntity | ViewEntity, info: SerializationInfo) -> str:
311
347
  if isinstance(value_type, DataType):
@@ -352,13 +388,46 @@ class PhysicalContainer(SheetRow):
352
388
  description: str | None = Field(
353
389
  alias="Description", default=None, description="Short description of the node being defined."
354
390
  )
355
- constraint: ContainerEntityList | None = Field(
391
+ constraint: ContainerConstraintListType | None = Field(
356
392
  None, alias="Constraint", description="List of required (comma separated) constraints for the container"
357
393
  )
358
394
  used_for: Literal["node", "edge", "all"] | None = Field(
359
395
  "all", alias="Used For", description=" Whether the container is used for nodes, edges or all."
360
396
  )
361
397
 
398
+ @field_validator("constraint", mode="after")
399
+ @classmethod
400
+ def constraint_set_correctly(cls, value: ContainerConstraintListType | None) -> Any:
401
+ if value is None:
402
+ return value
403
+
404
+ for constraint in value:
405
+ if constraint.prefix is Undefined:
406
+ message = f"The type of constraint is not defined. Please set 'requires:{constraint!s}'."
407
+ warnings.warn(
408
+ message,
409
+ stacklevel=2,
410
+ )
411
+ elif constraint.prefix != "requires":
412
+ message = (
413
+ f"Unsupported constraint type on container as "
414
+ f"the whole '{constraint.prefix}'. Currently only 'requires' is supported."
415
+ )
416
+ raise ValueError(message) from None
417
+
418
+ if len(constraint.suffix) > CONSTRAINT_ID_MAX_LENGTH:
419
+ message = f"Constraint id '{constraint.suffix}' exceeds maximum length of {CONSTRAINT_ID_MAX_LENGTH}."
420
+ raise ValueError(message) from None
421
+
422
+ if constraint.container is None:
423
+ message = (
424
+ f"Container constraint must have a container set. "
425
+ f"Please set 'requires:{constraint!s}(container=space:external_id)'."
426
+ )
427
+ raise ValueError(message) from None
428
+
429
+ return value
430
+
362
431
  def _identifier(self) -> tuple[Hashable, ...]:
363
432
  return (self.container,)
364
433
 
@@ -366,8 +435,10 @@ class PhysicalContainer(SheetRow):
366
435
  container_id = self.container.as_id()
367
436
  constraints: dict[str, dm.Constraint] = {}
368
437
  for constraint in self.constraint or []:
369
- requires = dm.RequiresConstraint(constraint.as_id())
370
- constraints[f"{constraint.space}_{constraint.external_id}"] = requires
438
+ if constraint.container is None:
439
+ continue
440
+ requires = dm.RequiresConstraint(constraint.container.as_id())
441
+ constraints[constraint.suffix] = requires
371
442
 
372
443
  return dm.ContainerApply(
373
444
  space=container_id.space,
@@ -29,7 +29,7 @@ from cognite.neat.v0.core._constants import (
29
29
  DMS_RESERVED_PROPERTIES,
30
30
  get_default_prefixes_and_namespaces,
31
31
  )
32
- from cognite.neat.v0.core._data_model._constants import PATTERNS, get_reserved_words
32
+ from cognite.neat.v0.core._data_model._constants import CONSTRAINT_ID_MAX_LENGTH, PATTERNS, get_reserved_words
33
33
  from cognite.neat.v0.core._data_model._shared import (
34
34
  ImportContext,
35
35
  ImportedDataModel,
@@ -72,6 +72,7 @@ from cognite.neat.v0.core._data_model.models.entities import (
72
72
  UnknownEntity,
73
73
  ViewEntity,
74
74
  )
75
+ from cognite.neat.v0.core._data_model.models.entities._single_value import ContainerConstraintEntity
75
76
  from cognite.neat.v0.core._data_model.models.physical import (
76
77
  PhysicalMetadata,
77
78
  PhysicalProperty,
@@ -1638,14 +1639,20 @@ class _ConceptualDataModelConverter:
1638
1639
  default_space: str,
1639
1640
  concept_by_concept_entity: dict[ConceptEntity, Concept],
1640
1641
  referenced_containers: Collection[ContainerEntity],
1641
- ) -> list[ContainerEntity]:
1642
- constrains: list[ContainerEntity] = []
1642
+ ) -> list[ContainerConstraintEntity]:
1643
+ constrains: list[ContainerConstraintEntity] = []
1643
1644
  for entity in concept_entities:
1644
1645
  concept = concept_by_concept_entity[entity]
1645
1646
  for parent in concept.implements or []:
1646
1647
  parent_entity = parent.as_container_entity(default_space)
1647
1648
  if parent_entity in referenced_containers:
1648
- constrains.append(parent_entity)
1649
+ constrains.append(
1650
+ ContainerConstraintEntity(
1651
+ prefix="requires",
1652
+ suffix=f"{parent_entity.space}_{parent_entity.external_id}"[:CONSTRAINT_ID_MAX_LENGTH],
1653
+ container=parent_entity,
1654
+ )
1655
+ )
1649
1656
  return constrains
1650
1657
 
1651
1658
  @classmethod
@@ -12,6 +12,7 @@ from rdflib import Dataset, Graph, Namespace, URIRef
12
12
  from rdflib.graph import DATASET_DEFAULT_GRAPH_ID
13
13
  from rdflib.plugins.stores.sparqlstore import SPARQLUpdateStore
14
14
 
15
+ from cognite.neat.v0.core._constants import NAMED_GRAPH_NAMESPACE
15
16
  from cognite.neat.v0.core._instances._shared import quad_formats, rdflib_to_oxi_type
16
17
  from cognite.neat.v0.core._instances.extractors import RdfFileExtractor, TripleExtractors
17
18
  from cognite.neat.v0.core._instances.queries import Queries
@@ -450,3 +451,35 @@ class NeatInstanceStore:
450
451
  def empty(self) -> bool:
451
452
  """Cheap way to check if the graph store is empty."""
452
453
  return not self.queries.select.has_data()
454
+
455
+ def diff(self, current_named_graph: URIRef, new_named_graph: URIRef) -> None:
456
+ """
457
+ Compare two named graphs and store diff results in dedicated named graphs.
458
+
459
+ Stores triples to add in DIFF_ADD and triples to delete in DIFF_DELETE.
460
+
461
+ Args:
462
+ current_named_graph: URI of the current named graph
463
+ new_named_graph: URI of the new/updated named graph
464
+
465
+ Raises:
466
+ NeatValueError: If either named graph doesn't exist in the store
467
+ """
468
+ if current_named_graph not in self.named_graphs:
469
+ raise NeatValueError(f"Current named graph not found: {current_named_graph}")
470
+ if new_named_graph not in self.named_graphs:
471
+ raise NeatValueError(f"New named graph not found: {new_named_graph}")
472
+
473
+ # Clear previous diff results using SPARQL
474
+ self.dataset.update(f"CLEAR SILENT GRAPH <{NAMED_GRAPH_NAMESPACE['DIFF_ADD']}>")
475
+ self.dataset.update(f"CLEAR SILENT GRAPH <{NAMED_GRAPH_NAMESPACE['DIFF_DELETE']}>")
476
+
477
+ # Store new diff results
478
+ self._add_triples(
479
+ self.queries.select.get_triples_to_add(current_named_graph, new_named_graph),
480
+ named_graph=NAMED_GRAPH_NAMESPACE["DIFF_ADD"],
481
+ )
482
+ self._add_triples(
483
+ self.queries.select.get_triples_to_delete(current_named_graph, new_named_graph),
484
+ named_graph=NAMED_GRAPH_NAMESPACE["DIFF_DELETE"],
485
+ )
@@ -143,13 +143,27 @@ def _get_row_number(sheet: Worksheet, values_to_find: list[str]) -> int | None:
143
143
  return None
144
144
 
145
145
 
146
- def find_column_with_value(sheet: Worksheet, value: Any) -> str | None:
146
+ @overload
147
+ def find_column_and_row_with_value(
148
+ sheet: Worksheet, value: Any, column_letter: Literal[True] = True
149
+ ) -> tuple[str, int] | tuple[None, None]: ...
150
+
151
+
152
+ @overload
153
+ def find_column_and_row_with_value(
154
+ sheet: Worksheet, value: Any, column_letter: Literal[False]
155
+ ) -> tuple[int, int] | tuple[None, None]: ...
156
+
157
+
158
+ def find_column_and_row_with_value(
159
+ sheet: Worksheet, value: Any, column_letter: bool = True
160
+ ) -> tuple[int, int] | tuple[str, int] | tuple[None, None]:
147
161
  for row in sheet.iter_rows():
148
162
  for cell in row:
149
163
  if cell.value and isinstance(cell.value, str) and cell.value.lower() == value.lower():
150
- return cell.column_letter # type: ignore
164
+ return (cell.column_letter, cell.row) if column_letter else (cell.column, cell.row)
151
165
 
152
- return None
166
+ return None, None
153
167
 
154
168
 
155
169
  def generate_data_validation(sheet: str, column: str, total_header_rows: int, validation_range: int) -> DataValidation:
@@ -25,6 +25,7 @@ from cognite.neat.v0.core._store._data_model import DataModelEntity
25
25
  from cognite.neat.v0.core._utils.auxiliary import local_import
26
26
 
27
27
  from ._collector import _COLLECTOR, Collector
28
+ from ._diff import DiffAPI
28
29
  from ._drop import DropAPI
29
30
  from ._explore import ExploreAPI
30
31
  from ._fix import FixAPI
@@ -110,6 +111,7 @@ class NeatSession:
110
111
  self.template = TemplateAPI(self._state)
111
112
  self._explore = ExploreAPI(self._state)
112
113
  self.plugins = PluginAPI(self._state)
114
+ self._diff = DiffAPI(self._state)
113
115
  self.opt = OptAPI()
114
116
  self.opt._display()
115
117
  if load_engine != "skip" and (engine_version := load_neat_engine(client, load_engine)):
@@ -0,0 +1,51 @@
1
+ from typing import cast
2
+
3
+ from rdflib.query import ResultRow
4
+
5
+ from cognite.neat.v0.core._constants import NAMED_GRAPH_NAMESPACE
6
+
7
+ from ._state import SessionState
8
+
9
+
10
+ class DiffAPI:
11
+ """Compare RDF graphs (private API)."""
12
+
13
+ def __init__(self, state: SessionState) -> None:
14
+ self._state = state
15
+
16
+ def instances(self, current_named_graph: str, new_named_graph: str) -> None:
17
+ """
18
+ Compare two named graphs and store diff results.
19
+
20
+ Results stored in DIFF_ADD and DIFF_DELETE named graphs.
21
+
22
+ Args:
23
+ current_named_graph: Name of the current graph (e.g., "CURRENT")
24
+ new_named_graph: Name of the new graph (e.g., "NEW")
25
+ """
26
+ current_uri = NAMED_GRAPH_NAMESPACE[current_named_graph]
27
+ new_uri = NAMED_GRAPH_NAMESPACE[new_named_graph]
28
+
29
+ self._state.instances.store.diff(current_uri, new_uri)
30
+ self._print_summary()
31
+
32
+ def _print_summary(self) -> None:
33
+ """Print diff summary with triple counts."""
34
+ store = self._state.instances.store
35
+
36
+ add_query = (
37
+ f"SELECT (COUNT(*) as ?count) WHERE {{ GRAPH <{NAMED_GRAPH_NAMESPACE['DIFF_ADD']}> {{ ?s ?p ?o }} }}"
38
+ )
39
+ delete_query = (
40
+ f"SELECT (COUNT(*) as ?count) WHERE {{ GRAPH <{NAMED_GRAPH_NAMESPACE['DIFF_DELETE']}> {{ ?s ?p ?o }} }}"
41
+ )
42
+
43
+ add_result = cast(ResultRow, next(iter(store.dataset.query(add_query))))
44
+ delete_result = cast(ResultRow, next(iter(store.dataset.query(delete_query))))
45
+
46
+ add_count = int(add_result[0])
47
+ delete_count = int(delete_result[0])
48
+
49
+ print("Diff complete:")
50
+ print(f" {add_count} triples to add (stored in DIFF_ADD)")
51
+ print(f" {delete_count} triples to delete (stored in DIFF_DELETE)")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cognite-neat
3
- Version: 0.123.43
3
+ Version: 0.125.0
4
4
  Summary: Knowledge graph transformation
5
5
  Project-URL: Documentation, https://cognite-neat.readthedocs-hosted.com/
6
6
  Project-URL: Homepage, https://cognite-neat.readthedocs-hosted.com/