cognite-neat 0.124.0__py3-none-any.whl → 0.125.1__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 (29) 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/_exceptions.py +17 -0
  7. cognite/neat/_utils/text.py +22 -0
  8. cognite/neat/_utils/useful_types.py +4 -0
  9. cognite/neat/_utils/validation.py +7 -4
  10. cognite/neat/_version.py +1 -1
  11. cognite/neat/v0/core/_data_model/_constants.py +1 -0
  12. cognite/neat/v0/core/_data_model/exporters/_data_model2excel.py +3 -3
  13. cognite/neat/v0/core/_data_model/importers/_dms2data_model.py +4 -3
  14. cognite/neat/v0/core/_data_model/importers/_spreadsheet2data_model.py +85 -5
  15. cognite/neat/v0/core/_data_model/models/entities/__init__.py +2 -0
  16. cognite/neat/v0/core/_data_model/models/entities/_single_value.py +14 -0
  17. cognite/neat/v0/core/_data_model/models/entities/_types.py +10 -0
  18. cognite/neat/v0/core/_data_model/models/physical/_exporter.py +3 -11
  19. cognite/neat/v0/core/_data_model/models/physical/_unverified.py +61 -12
  20. cognite/neat/v0/core/_data_model/models/physical/_validation.py +8 -4
  21. cognite/neat/v0/core/_data_model/models/physical/_verified.py +86 -15
  22. cognite/neat/v0/core/_data_model/transformers/_converters.py +11 -4
  23. cognite/neat/v0/core/_instances/queries/_select.py +13 -29
  24. cognite/neat/v0/core/_store/_instance.py +2 -2
  25. cognite/neat/v0/core/_utils/spreadsheet.py +17 -3
  26. {cognite_neat-0.124.0.dist-info → cognite_neat-0.125.1.dist-info}/METADATA +1 -1
  27. {cognite_neat-0.124.0.dist-info → cognite_neat-0.125.1.dist-info}/RECORD +29 -24
  28. {cognite_neat-0.124.0.dist-info → cognite_neat-0.125.1.dist-info}/WHEEL +0 -0
  29. {cognite_neat-0.124.0.dist-info → cognite_neat-0.125.1.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
@@ -451,9 +451,21 @@ class SelectQueries(BaseQuery):
451
451
  else:
452
452
  yield instance_id, str(space)
453
453
 
454
- def _get_graph_diff(
454
+ def get_graph_diff(
455
455
  self, source_graph: URIRef, target_graph: URIRef
456
456
  ) -> Iterable[tuple[URIRef, URIRef, URIRef | RdfLiteral]]:
457
+ """Return triples that exist in the source graph but not in the target graph.
458
+
459
+ This method compares two named graphs within the dataset and identifies all triples
460
+ that are present in the `source_graph` but are missing from the `target_graph`.
461
+
462
+ Args:
463
+ source_graph: URI of the graph to compare from.
464
+ target_graph: URI of the graph to compare against.
465
+
466
+ Returns:
467
+ Iterable of triples (subject, predicate, object)
468
+ """
457
469
  query = f"""
458
470
  SELECT ?s ?p ?o
459
471
  WHERE {{
@@ -464,31 +476,3 @@ class SelectQueries(BaseQuery):
464
476
  }}
465
477
  """
466
478
  return cast(Iterable[tuple[URIRef, URIRef, URIRef | RdfLiteral]], self.dataset.query(query))
467
-
468
- def get_triples_to_delete(
469
- self, old_graph: URIRef, new_graph: URIRef
470
- ) -> Iterable[tuple[URIRef, URIRef, URIRef | RdfLiteral]]:
471
- """Find triples that exist in old graph but not in new graph.
472
-
473
- Args:
474
- old_graph: URI of the old named graph
475
- new_graph: URI of the new named graph
476
-
477
- Returns:
478
- List of triples (subject, predicate, object) to delete
479
- """
480
- return self._get_graph_diff(old_graph, new_graph)
481
-
482
- def get_triples_to_add(
483
- self, old_graph: URIRef, new_graph: URIRef
484
- ) -> Iterable[tuple[URIRef, URIRef, URIRef | RdfLiteral]]:
485
- """Find triples that exist in new graph but not in old graph.
486
-
487
- Args:
488
- old_graph: URI of the old named graph
489
- new_graph: URI of the new named graph
490
-
491
- Returns:
492
- List of triples (subject, predicate, object) to add
493
- """
494
- return self._get_graph_diff(new_graph, old_graph)
@@ -476,10 +476,10 @@ class NeatInstanceStore:
476
476
 
477
477
  # Store new diff results
478
478
  self._add_triples(
479
- self.queries.select.get_triples_to_add(current_named_graph, new_named_graph),
479
+ self.queries.select.get_graph_diff(new_named_graph, current_named_graph),
480
480
  named_graph=NAMED_GRAPH_NAMESPACE["DIFF_ADD"],
481
481
  )
482
482
  self._add_triples(
483
- self.queries.select.get_triples_to_delete(current_named_graph, new_named_graph),
483
+ self.queries.select.get_graph_diff(current_named_graph, new_named_graph),
484
484
  named_graph=NAMED_GRAPH_NAMESPACE["DIFF_DELETE"],
485
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cognite-neat
3
- Version: 0.124.0
3
+ Version: 0.125.1
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/