cognite-neat 0.124.0__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 (27) 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/_utils/spreadsheet.py +17 -3
  24. {cognite_neat-0.124.0.dist-info → cognite_neat-0.125.0.dist-info}/METADATA +1 -1
  25. {cognite_neat-0.124.0.dist-info → cognite_neat-0.125.0.dist-info}/RECORD +27 -22
  26. {cognite_neat-0.124.0.dist-info → cognite_neat-0.125.0.dist-info}/WHEEL +0 -0
  27. {cognite_neat-0.124.0.dist-info → cognite_neat-0.125.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,3 +1,4 @@
1
1
  from ._base import DMSImporter
2
+ from ._table_importer.importer import DMSTableImporter
2
3
 
3
- __all__ = ["DMSImporter"]
4
+ __all__ = ["DMSImporter", "DMSTableImporter"]
@@ -0,0 +1,141 @@
1
+ from collections.abc import Mapping
2
+ from typing import Annotated, cast
3
+
4
+ from pydantic import AliasGenerator, BaseModel, BeforeValidator, Field, model_validator
5
+ from pydantic.alias_generators import to_camel
6
+
7
+ from cognite.neat._data_model.models.entities import ParsedEntity, parse_entities, parse_entity
8
+ from cognite.neat._utils.text import title_case
9
+ from cognite.neat._utils.useful_types import CellValueType
10
+
11
+
12
+ def parse_entity_str(v: str) -> ParsedEntity:
13
+ try:
14
+ return parse_entity(v)
15
+ except ValueError as e:
16
+ raise ValueError(f"Invalid entity syntax: {e}") from e
17
+
18
+
19
+ def parse_entities_str(v: str) -> list[ParsedEntity] | None:
20
+ try:
21
+ return parse_entities(v)
22
+ except ValueError as e:
23
+ raise ValueError(f"Invalid entity list syntax: {e}") from e
24
+
25
+
26
+ Entity = Annotated[ParsedEntity, BeforeValidator(parse_entity_str, str)]
27
+ EntityList = Annotated[list[ParsedEntity], BeforeValidator(parse_entities_str, str)]
28
+
29
+
30
+ class TableObj(
31
+ BaseModel,
32
+ extra="ignore",
33
+ alias_generator=AliasGenerator(
34
+ validation_alias=title_case,
35
+ serialization_alias=to_camel,
36
+ ),
37
+ ): ...
38
+
39
+
40
+ class MetadataValue(TableObj):
41
+ key: str
42
+ value: CellValueType
43
+
44
+
45
+ class DMSProperty(TableObj):
46
+ view: Entity
47
+ view_property: str
48
+ name: str | None = None
49
+ description: str | None = None
50
+ connection: Entity | None
51
+ value_type: Entity
52
+ min_count: int | None
53
+ max_count: int | None
54
+ immutable: bool | None = None
55
+ default: CellValueType | None = None
56
+ auto_increment: bool | None = None
57
+ container: Entity | None = None
58
+ container_property: str | None = None
59
+ container_property_name: str | None = None
60
+ container_property_description: str | None = None
61
+ index: EntityList | None = None
62
+ constraint: EntityList | None = None
63
+
64
+
65
+ class DMSView(TableObj):
66
+ view: Entity
67
+ name: str | None = None
68
+ description: str | None = None
69
+ implements: EntityList | None = None
70
+ filter: str | None = None
71
+ in_model: bool | None = None
72
+
73
+
74
+ class DMSContainer(TableObj):
75
+ container: Entity
76
+ name: str | None = None
77
+ description: str | None = None
78
+ constraint: EntityList | None = None
79
+ used_for: str | None = None
80
+
81
+
82
+ class DMSEnum(TableObj):
83
+ collection: str
84
+ value: str
85
+ name: str | None = None
86
+ description: str | None = None
87
+
88
+
89
+ class DMSNode(TableObj):
90
+ node: Entity
91
+
92
+
93
+ class TableDMS(TableObj):
94
+ metadata: list[MetadataValue]
95
+ properties: list[DMSProperty]
96
+ views: list[DMSView]
97
+ containers: list[DMSContainer] = Field(default_factory=list)
98
+ enum: list[DMSEnum] = Field(default_factory=list)
99
+ nodes: list[DMSNode] = Field(default_factory=list)
100
+
101
+ @model_validator(mode="before")
102
+ def _title_case_keys(
103
+ cls, data: dict[str, list[dict[str, CellValueType]]]
104
+ ) -> dict[str, list[dict[str, CellValueType]]]:
105
+ if isinstance(data, dict):
106
+ # We are case-insensitive on the table names.
107
+ return {title_case(k): v for k, v in data.items()}
108
+ return data
109
+
110
+
111
+ DMS_API_MAPPING: Mapping[str, Mapping[str, str]] = {
112
+ "Views": {
113
+ "space": "View",
114
+ "externalId": "View",
115
+ "version": "View",
116
+ **{
117
+ cast(str, field_.serialization_alias): cast(str, field_.validation_alias)
118
+ for field_id, field_ in DMSView.model_fields.items()
119
+ if field_id != "View"
120
+ },
121
+ },
122
+ "Containers": {
123
+ "space": "Container",
124
+ "externalId": "Container",
125
+ **{
126
+ cast(str, field_.serialization_alias): cast(str, field_.validation_alias)
127
+ for field_id, field_ in DMSContainer.model_fields.items()
128
+ if field_id != "Container"
129
+ },
130
+ },
131
+ "Properties": {
132
+ "space": "View",
133
+ "externalId": "View",
134
+ "property": "ViewProperty",
135
+ **{
136
+ cast(str, field_.serialization_alias): cast(str, field_.validation_alias)
137
+ for field_id, field_ in DMSProperty.model_fields.items()
138
+ if field_id not in ("View", "ViewProperty")
139
+ },
140
+ },
141
+ }
@@ -0,0 +1,76 @@
1
+ from collections.abc import Mapping
2
+ from typing import ClassVar, cast
3
+
4
+ from pydantic import ValidationError
5
+
6
+ from cognite.neat._data_model.importers._base import DMSImporter
7
+ from cognite.neat._data_model.models.dms import (
8
+ RequestSchema,
9
+ )
10
+ from cognite.neat._exceptions import DataModelImportError
11
+ from cognite.neat._issues import ModelSyntaxError
12
+ from cognite.neat._utils.useful_types import CellValueType
13
+ from cognite.neat._utils.validation import as_json_path, humanize_validation_error
14
+
15
+ from .data_classes import TableDMS
16
+
17
+
18
+ class DMSTableImporter(DMSImporter):
19
+ """Imports DMS from a table structure.
20
+
21
+ The tables can are expected to be a dictionary where the keys are the table names and the values
22
+ are lists of dictionaries representing the rows in the table.
23
+ """
24
+
25
+ # We can safely cast as we know the validation_alias is always set to a str.
26
+ REQUIRED_SHEETS = tuple(
27
+ cast(str, field_.validation_alias) for field_ in TableDMS.model_fields.values() if field_.is_required()
28
+ )
29
+ REQUIRED_SHEET_MESSAGES: ClassVar[Mapping[str, str]] = {
30
+ f"Missing required column: {sheet!r}": f"Missing required sheet: {sheet!r}" for sheet in REQUIRED_SHEETS
31
+ }
32
+
33
+ def __init__(self, tables: dict[str, list[dict[str, CellValueType]]]) -> None:
34
+ self._table = tables
35
+
36
+ def to_data_model(self) -> RequestSchema:
37
+ raise NotImplementedError()
38
+
39
+ def _read_tables(self) -> TableDMS:
40
+ try:
41
+ # Check tables, columns, data type and entity syntax.
42
+ table = TableDMS.model_validate(self._table)
43
+ except ValidationError as e:
44
+ errors = self._create_error_messages(e)
45
+ raise DataModelImportError(errors) from None
46
+ return table
47
+
48
+ def _create_error_messages(self, error: ValidationError) -> list[ModelSyntaxError]:
49
+ errors: list[ModelSyntaxError] = []
50
+ seen: set[str] = set()
51
+ for message in humanize_validation_error(
52
+ error,
53
+ humanize_location=self._location,
54
+ field_name="column",
55
+ missing_required_descriptor="missing",
56
+ ):
57
+ # Replace messages about missing required columns with missing required sheets.
58
+ message = self.REQUIRED_SHEET_MESSAGES.get(message, message)
59
+ if message in seen:
60
+ # We treat all rows as the same, so we get duplicated errors for each row.
61
+ continue
62
+ seen.add(message)
63
+ errors.append(ModelSyntaxError(message=message))
64
+ return errors
65
+
66
+ @staticmethod
67
+ def _location(loc: tuple[str | int, ...]) -> str:
68
+ if isinstance(loc[0], str) and len(loc) == 2: # Sheet + row.
69
+ # We skip the row as we treat all rows as the same. For example, if a required column is missing in one
70
+ # row, it is missing in all rows.
71
+ return f"{loc[0]} sheet"
72
+ elif len(loc) == 3 and isinstance(loc[0], str) and isinstance(loc[1], int) and isinstance(loc[2], str):
73
+ # This means there is something wrong in a specific cell.
74
+ return f"{loc[0]} sheet row {loc[1] + 1} column {loc[2]!r}"
75
+ # This should be unreachable as the TableDMS model only has 2 levels.
76
+ return as_json_path(loc)
@@ -0,0 +1,89 @@
1
+ from collections.abc import Mapping
2
+ from dataclasses import dataclass, field
3
+
4
+ from .data_classes import DMS_API_MAPPING
5
+
6
+
7
+ @dataclass
8
+ class SpreadsheetReadContext:
9
+ """This class is used to store information about the source spreadsheet.
10
+
11
+ It is used to adjust row numbers to account for header rows and empty rows
12
+ such that the error/warning messages are accurate.
13
+ """
14
+
15
+ header_row: int = 1
16
+ empty_rows: list[int] = field(default_factory=list)
17
+ skipped_rows: list[int] = field(default_factory=list)
18
+ is_one_indexed: bool = True
19
+
20
+ def __post_init__(self) -> None:
21
+ self.empty_rows.sort()
22
+ self.skipped_rows.sort()
23
+
24
+ def adjusted_row_number(self, row_no: int) -> int:
25
+ output = row_no
26
+ for empty_row in self.empty_rows:
27
+ if empty_row <= output:
28
+ output += 1
29
+ else:
30
+ break
31
+
32
+ for skipped_rows in self.skipped_rows:
33
+ if skipped_rows <= output:
34
+ output += 1
35
+ else:
36
+ break
37
+
38
+ return output + self.header_row + (1 if self.is_one_indexed else 0)
39
+
40
+
41
+ @dataclass
42
+ class TableSource:
43
+ source: str
44
+ table_read: dict[str, SpreadsheetReadContext] = field(default_factory=dict)
45
+
46
+ def location(self, path: tuple[int | str, ...]) -> str:
47
+ table_id: str | None = None
48
+ row_no: int | None = None
49
+ column: str | None = None
50
+ if len(path) >= 1 and isinstance(path[0], str):
51
+ table_id = path[0]
52
+ if len(path) >= 2 and isinstance(path[1], int):
53
+ row_no = path[1]
54
+ if len(path) >= 3 and isinstance(path[2], str):
55
+ column = path[2]
56
+ column = self.field_to_column(table_id, column)
57
+ if isinstance(row_no, int):
58
+ row_no = self.adjust_row_number(table_id, row_no)
59
+ location_parts = []
60
+ if table_id is not None:
61
+ location_parts.append(f"table {table_id!r}")
62
+ if row_no is not None:
63
+ location_parts.append(f"row {row_no}")
64
+ if column is not None:
65
+ location_parts.append(f"column {column!r}")
66
+ if len(path) > 4:
67
+ location_parts.append("-> " + ".".join(str(p) for p in path[3:]))
68
+
69
+ return " ".join(location_parts)
70
+
71
+ def adjust_row_number(self, table_id: str | None, row_no: int) -> int:
72
+ table_read = table_id and self.table_read.get(table_id)
73
+ if table_read:
74
+ return table_read.adjusted_row_number(row_no)
75
+ return row_no + 1 # Convert to 1-indexed if no table read info is available
76
+
77
+ @classmethod
78
+ def field_to_column(cls, table_id: str | None, field_: str) -> str:
79
+ """Maps the field name used in the DMS API to the column named used by Neat."""
80
+ mapping = cls.field_mapping(table_id)
81
+ if mapping is not None:
82
+ return mapping.get(field_, field_)
83
+ return field_
84
+
85
+ @classmethod
86
+ def field_mapping(cls, table_id: str | int | None) -> Mapping[str, str] | None:
87
+ if not isinstance(table_id, str):
88
+ return None
89
+ return DMS_API_MAPPING.get(table_id)
@@ -0,0 +1,17 @@
1
+ from ._issues import ModelSyntaxError
2
+
3
+
4
+ class NeatException(Exception):
5
+ """Base class for all exceptions raised by Neat."""
6
+
7
+ pass
8
+
9
+
10
+ class DataModelImportError(NeatException):
11
+ """Raised when there is an error importing a model."""
12
+
13
+ def __init__(self, errors: list[ModelSyntaxError]) -> None:
14
+ self.errors = errors
15
+
16
+ def __str__(self) -> str:
17
+ return f"Model import failed with {len(self.errors)} errors: " + "; ".join(map(str, self.errors))
@@ -38,3 +38,25 @@ def humanize_collection(collection: Collection[Any], /, *, sort: bool = True, bi
38
38
  sequence = list(strings)
39
39
 
40
40
  return f"{', '.join(sequence[:-1])} {bind_word} {sequence[-1]}"
41
+
42
+
43
+ def title_case(s: str) -> str:
44
+ """Convert a string to title case, handling underscores and hyphens.
45
+
46
+ Args:
47
+ s: The string to convert.
48
+ Returns:
49
+ The title-cased string.
50
+ Examples:
51
+ >>> title_case("hello world")
52
+ 'Hello World'
53
+ >>> title_case("hello_world")
54
+ 'Hello World'
55
+ >>> title_case("hello-world")
56
+ 'Hello World'
57
+ >>> title_case("hello_world-and-universe")
58
+ 'Hello World And Universe'
59
+ >>> title_case("HELLO WORLD")
60
+ 'Hello World'
61
+ """
62
+ return " ".join(word.capitalize() for word in s.replace("_", " ").replace("-", " ").split())
@@ -1,7 +1,11 @@
1
1
  from collections.abc import Hashable
2
+ from datetime import date, datetime, time, timedelta
2
3
  from typing import TypeAlias, TypeVar
3
4
 
4
5
  JsonVal: TypeAlias = None | str | int | float | bool | dict[str, "JsonVal"] | list["JsonVal"]
5
6
 
6
7
 
7
8
  T_ID = TypeVar("T_ID", bound=Hashable)
9
+
10
+ # These are the types that openpyxl supports in cells
11
+ CellValueType: TypeAlias = str | int | float | bool | datetime | date | time | timedelta | None
@@ -31,6 +31,7 @@ def humanize_validation_error(
31
31
  humanize_location: Callable[[tuple[int | str, ...]], str] = as_json_path,
32
32
  field_name: Literal["field", "column", "value"] = "field",
33
33
  field_renaming: Mapping[str, str] | None = None,
34
+ missing_required_descriptor: Literal["empty", "missing"] = "missing",
34
35
  ) -> list[str]:
35
36
  """Converts a ValidationError to a human-readable format.
36
37
 
@@ -50,6 +51,8 @@ def humanize_validation_error(
50
51
  This is useful when the field names in the model are different from the names in the source.
51
52
  For example, if the model field is "asset_id" but the source column is "Asset ID",
52
53
  you can provide a mapping {"asset_id": "Asset ID"} to have the error messages use the source names.
54
+ missing_required_descriptor: How to describe missing required fields. Default is "missing".
55
+ Other option is "empty" which can be more suitable for table data.
53
56
  Returns:
54
57
  A list of human-readable error messages.
55
58
  """
@@ -60,9 +63,9 @@ def humanize_validation_error(
60
63
  loc = (*parent_loc, *item["loc"])
61
64
  error_type = item["type"]
62
65
  if error_type == "missing":
63
- msg = f"Missing required field: {loc[-1]!r}"
66
+ msg = f"Missing required {field_name}: {loc[-1]!r}"
64
67
  elif error_type == "extra_forbidden":
65
- msg = f"Unused field: {loc[-1]!r}"
68
+ msg = f"Unused {field_name}: {loc[-1]!r}"
66
69
  elif error_type == "value_error":
67
70
  msg = str(item["ctx"]["error"])
68
71
  elif error_type == "literal_error":
@@ -92,10 +95,10 @@ def humanize_validation_error(
92
95
 
93
96
  error_suffix = f"{msg[:1].casefold()}{msg[1:]}"
94
97
  if len(loc) > 1 and error_type in {"extra_forbidden", "missing"}:
95
- if field_name == "column":
98
+ if missing_required_descriptor == "empty" and error_type == "missing":
96
99
  # This is a table so we modify the error message.
97
100
  msg = (
98
- f"In {humanize_location(loc[:-1])} the column {field_renaming.get(str(loc[-1]), loc[-1])!r} "
101
+ f"In {humanize_location(loc[:-1])} the {field_name} {field_renaming.get(str(loc[-1]), loc[-1])!r} "
99
102
  "cannot be empty."
100
103
  )
101
104
  else:
cognite/neat/_version.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "0.124.0"
1
+ __version__ = "0.125.0"
2
2
  __engine__ = "^2.0.4"
@@ -43,6 +43,7 @@ class EntityTypes(StrEnum):
43
43
  prefix = "prefix"
44
44
  space = "space"
45
45
  container_index = "container_index"
46
+ container_constraint = "container_constraint"
46
47
  concept_restriction = "conceptRestriction"
47
48
  value_constraint = "valueConstraint"
48
49
  cardinality_constraint = "cardinalityConstraint"
@@ -32,7 +32,7 @@ from cognite.neat.v0.core._data_model.models.data_types import (
32
32
  )
33
33
  from cognite.neat.v0.core._data_model.models.physical._verified import PhysicalDataModel
34
34
  from cognite.neat.v0.core._utils.spreadsheet import (
35
- find_column_with_value,
35
+ find_column_and_row_with_value,
36
36
  generate_data_validation,
37
37
  )
38
38
 
@@ -217,7 +217,7 @@ class ExcelExporter(BaseExporter[VerifiedDataModel, Workbook]):
217
217
  continue
218
218
  ws = workbook[sheet]
219
219
  for col in get_internal_properties():
220
- column_letter = find_column_with_value(ws, col)
220
+ column_letter = find_column_and_row_with_value(ws, col)[0]
221
221
  if column_letter:
222
222
  ws.column_dimensions[column_letter].hidden = True
223
223
 
@@ -451,7 +451,7 @@ class ExcelExporter(BaseExporter[VerifiedDataModel, Workbook]):
451
451
  workbook[sheet_name].add_data_validation(data_validators[data_validator_name])
452
452
 
453
453
  # APPLY VALIDATOR TO SPECIFIC COLUMN
454
- if column_letter := find_column_with_value(workbook[sheet_name], column_name):
454
+ if column_letter := find_column_and_row_with_value(workbook[sheet_name], column_name)[0]:
455
455
  data_validators[data_validator_name].add(f"{column_letter}{3}:{column_letter}{3 + total_rows}")
456
456
 
457
457
  def _create_sheet_with_header(
@@ -54,6 +54,7 @@ from cognite.neat.v0.core._data_model.models.entities import (
54
54
  ReverseConnectionEntity,
55
55
  ViewEntity,
56
56
  )
57
+ from cognite.neat.v0.core._data_model.models.entities._single_value import ContainerConstraintEntity
57
58
  from cognite.neat.v0.core._data_model.models.physical import (
58
59
  UnverifiedPhysicalContainer,
59
60
  UnverifiedPhysicalEnum,
@@ -571,17 +572,17 @@ class DMSImporter(BaseImporter[UnverifiedPhysicalDataModel]):
571
572
  index.append(ContainerIndexEntity(prefix="inverted", suffix=index_name, order=order))
572
573
  return index or None
573
574
 
574
- def _get_constraint(self, prop: ViewPropertyApply, prop_id: str) -> list[str] | None:
575
+ def _get_constraint(self, prop: ViewPropertyApply, prop_id: str) -> list[ContainerConstraintEntity] | None:
575
576
  if not isinstance(prop, dm.MappedPropertyApply):
576
577
  return None
577
578
  container = self._all_containers_by_id[prop.container]
578
- unique_constraints: list[str] = []
579
+ unique_constraints: list[ContainerConstraintEntity] = []
579
580
  for constraint_name, constraint_obj in (container.constraints or {}).items():
580
581
  if isinstance(constraint_obj, dm.RequiresConstraint):
581
582
  # This is handled in the .from_container method of DMSContainer
582
583
  continue
583
584
  elif isinstance(constraint_obj, dm.UniquenessConstraint) and prop_id in constraint_obj.properties:
584
- unique_constraints.append(constraint_name)
585
+ unique_constraints.append(ContainerConstraintEntity(prefix="uniqueness", suffix=constraint_name))
585
586
  elif isinstance(constraint_obj, dm.UniquenessConstraint):
586
587
  # This does not apply to this property
587
588
  continue
@@ -10,8 +10,10 @@ import pandas as pd
10
10
  from openpyxl import load_workbook
11
11
  from openpyxl.worksheet.worksheet import Worksheet
12
12
  from pandas import ExcelFile
13
+ from pydantic import ValidationError
13
14
  from rdflib import Namespace, URIRef
14
15
 
16
+ from cognite.neat.v0.core._data_model._constants import SPLIT_ON_COMMA_PATTERN
15
17
  from cognite.neat.v0.core._data_model._shared import (
16
18
  ImportedDataModel,
17
19
  T_UnverifiedDataModel,
@@ -23,6 +25,7 @@ from cognite.neat.v0.core._data_model.models import (
23
25
  SchemaCompleteness,
24
26
  )
25
27
  from cognite.neat.v0.core._data_model.models._import_contexts import SpreadsheetContext
28
+ from cognite.neat.v0.core._data_model.models.entities._single_value import ContainerConstraintEntity, ContainerEntity
26
29
  from cognite.neat.v0.core._issues import IssueList, MultiValueError
27
30
  from cognite.neat.v0.core._issues.errors import (
28
31
  FileMissingRequiredFieldError,
@@ -30,7 +33,11 @@ from cognite.neat.v0.core._issues.errors import (
30
33
  FileReadError,
31
34
  )
32
35
  from cognite.neat.v0.core._issues.warnings import FileMissingRequiredFieldWarning
33
- from cognite.neat.v0.core._utils.spreadsheet import SpreadsheetRead, read_individual_sheet
36
+ from cognite.neat.v0.core._utils.spreadsheet import (
37
+ SpreadsheetRead,
38
+ find_column_and_row_with_value,
39
+ read_individual_sheet,
40
+ )
34
41
  from cognite.neat.v0.core._utils.text import humanize_collection
35
42
 
36
43
  from ._base import BaseImporter
@@ -306,7 +313,7 @@ class ExcelImporter(BaseImporter[T_UnverifiedDataModel]):
306
313
 
307
314
  """
308
315
 
309
- workbook = load_workbook(filepath)
316
+ workbook = load_workbook(filepath, data_only=True)
310
317
 
311
318
  if "Classes" in workbook.sheetnames:
312
319
  print(
@@ -323,13 +330,17 @@ class ExcelImporter(BaseImporter[T_UnverifiedDataModel]):
323
330
  if "Properties" in workbook.sheetnames:
324
331
  _replace_class_with_concept_cell(workbook["Properties"])
325
332
 
326
- with tempfile.NamedTemporaryFile(prefix="temp_neat_file", suffix=".xlsx", delete=False) as temp_file:
327
- workbook.save(temp_file.name)
328
- return Path(temp_file.name)
333
+ elif "Containers" in workbook.sheetnames:
334
+ _replace_legacy_constraint_form(workbook["Containers"])
335
+ _replace_legacy_constraint_form(workbook["Properties"])
329
336
 
330
337
  else:
331
338
  return filepath
332
339
 
340
+ with tempfile.NamedTemporaryFile(prefix="temp_neat_file", suffix=".xlsx", delete=False) as temp_file:
341
+ workbook.save(temp_file.name)
342
+ return Path(temp_file.name)
343
+
333
344
 
334
345
  def _replace_class_with_concept_cell(sheet: Worksheet) -> None:
335
346
  """
@@ -342,3 +353,72 @@ def _replace_class_with_concept_cell(sheet: Worksheet) -> None:
342
353
  for cell in row:
343
354
  if cell.value == "Class":
344
355
  cell.value = "Concept"
356
+
357
+
358
+ def _replace_legacy_constraint_form(sheet: Worksheet) -> None:
359
+ """
360
+ Replaces the legacy form of container constraints with the new form in the given sheet.
361
+
362
+ Args:
363
+ sheet (Worksheet): The sheet in which to replace the old form of container constraints.
364
+ """
365
+ column, row = find_column_and_row_with_value(sheet, "Constraint", False)
366
+
367
+ if not column or not row:
368
+ return None
369
+
370
+ # Iterate over values in the constraint column and replace old form with new form
371
+ replaced: bool = False
372
+ for cell_row in sheet.iter_rows(min_row=row + 1, min_col=column, max_col=column):
373
+ cell = cell_row[0]
374
+ if cell.value is not None: # Skip empty cells
375
+ # Container sheet update
376
+ if sheet.title.lower() == "containers":
377
+ constraints = []
378
+ for constraint in SPLIT_ON_COMMA_PATTERN.split(str(cell.value)):
379
+ # latest format, do nothing
380
+ if "container" in constraint.lower():
381
+ constraints.append(constraint)
382
+ continue
383
+
384
+ try:
385
+ container = ContainerEntity.load(constraint, space="default")
386
+ container_str = container.external_id if container.space == "default" else str(container)
387
+ constraints.append(
388
+ f"requires:{container.space}_{container.external_id}(container={container_str})"
389
+ )
390
+ replaced = True
391
+ except ValidationError:
392
+ constraints.append(constraint)
393
+
394
+ cell.value = ",".join(constraints)
395
+
396
+ # Properties sheet update
397
+ elif sheet.title.lower() == "properties":
398
+ constraints = []
399
+ for constraint in SPLIT_ON_COMMA_PATTERN.split(str(cell.value)):
400
+ try:
401
+ constraint_entity = ContainerConstraintEntity.load(constraint)
402
+
403
+ if constraint_entity.prefix in ["uniqueness", "requires"]:
404
+ constraints.append(constraint)
405
+
406
+ # Replace old format with new format
407
+ else:
408
+ constraints.append(f"uniqueness:{constraint}")
409
+ replaced = True
410
+
411
+ # If the constraint is not valid, we keep it as is
412
+ # to be caught by validation later
413
+ except ValidationError:
414
+ constraints.append(constraint)
415
+
416
+ cell.value = ",".join(constraints)
417
+
418
+ if replaced:
419
+ print(
420
+ (
421
+ "You are using a legacy container constraints format "
422
+ f"in the {sheet.title} sheet. Please update to the latest format."
423
+ ),
424
+ )
@@ -7,6 +7,7 @@ from ._single_value import (
7
7
  AssetFields,
8
8
  ConceptEntity,
9
9
  ConceptualEntity,
10
+ ContainerConstraintEntity,
10
11
  ContainerEntity,
11
12
  ContainerIndexEntity,
12
13
  DataModelEntity,
@@ -35,6 +36,7 @@ __all__ = [
35
36
  "ConceptPropertyCardinalityConstraint",
36
37
  "ConceptPropertyValueConstraint",
37
38
  "ConceptualEntity",
39
+ "ContainerConstraintEntity",
38
40
  "ContainerEntity",
39
41
  "ContainerEntityList",
40
42
  "ContainerIndexEntity",
@@ -660,3 +660,17 @@ class ContainerIndexEntity(PhysicalEntity[None]):
660
660
  @classmethod
661
661
  def from_id(cls, id: None) -> Self:
662
662
  return cls(suffix="dummy")
663
+
664
+
665
+ class ContainerConstraintEntity(PhysicalEntity[None]):
666
+ type_: ClassVar[EntityTypes] = EntityTypes.container_constraint
667
+ prefix: _UndefinedType | Literal["uniqueness", "requires"] = Undefined # type: ignore[assignment]
668
+ suffix: str
669
+ container: ContainerEntity | None = None
670
+
671
+ def as_id(self) -> None:
672
+ return None
673
+
674
+ @classmethod
675
+ def from_id(cls, id: None) -> Self:
676
+ return cls(suffix="dummy")
@@ -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
@@ -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.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/
@@ -1,12 +1,17 @@
1
1
  cognite/neat/__init__.py,sha256=Lo4DbjDOwnhCYUoAgPp5RG1fDdF7OlnomalTe7n1ydw,211
2
+ cognite/neat/_exceptions.py,sha256=IvDKO8kHk4dCkrmQU6z_svJcPqs2m7QYLosuOuM_iwE,473
2
3
  cognite/neat/_issues.py,sha256=uv0fkkWwTKqNmTmHqyoBB3L6yMCh42EZpEkLGmIJYOY,812
3
- cognite/neat/_version.py,sha256=0GwMYSFb5nGMLM9dHslSjNFrEuvigs26DxXB6nxHwqk,46
4
+ cognite/neat/_version.py,sha256=uGBcX4xuE__guAX0yJVFt4XCMymM1MTnVvYJi0BG7QM,46
4
5
  cognite/neat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
6
  cognite/neat/_data_model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
7
  cognite/neat/_data_model/_constants.py,sha256=NGGvWHlQqhkkSBP_AqoofGYjNph3SiZX6QPINlMsy04,107
7
8
  cognite/neat/_data_model/_identifiers.py,sha256=a0LcQ_h0NffxSKTCrzCDpYkrlaUTk-D_rfaQUh-BhWc,1921
8
- cognite/neat/_data_model/importers/__init__.py,sha256=GUp7WkjX6R02lQRHD-2ygkwNGPSWbFjrFettnCdrrAQ,58
9
+ cognite/neat/_data_model/importers/__init__.py,sha256=Ntk6Z8jpPvr7awV4kgs-uMRB2RrA3ufi0O7sSMgcv0o,133
9
10
  cognite/neat/_data_model/importers/_base.py,sha256=NRB0FcEBj4GaethU68nRffBfTedBBA866A3zfJNfmiQ,433
11
+ cognite/neat/_data_model/importers/_table_importer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ cognite/neat/_data_model/importers/_table_importer/data_classes.py,sha256=c_P4j7cHTyltNRiNzJJkJ-TKM5uoiizyLI6cgvLA3nM,4057
13
+ cognite/neat/_data_model/importers/_table_importer/importer.py,sha256=aBO8CBHCAeZJbNa7LPt4JfZMb558nKu5KT9aXhp2vvs,3245
14
+ cognite/neat/_data_model/importers/_table_importer/source.py,sha256=CU6kHJquKxDgYSMwONALbl6yleF_FoAgGU2SXl_uj0Y,3098
10
15
  cognite/neat/_data_model/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
16
  cognite/neat/_data_model/models/conceptual/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
17
  cognite/neat/_data_model/models/conceptual/_base.py,sha256=SFkoBJDM51pqew_isHFJoB20OgfofpwVRnTrg-rKkNY,710
@@ -41,9 +46,9 @@ cognite/neat/_session/_state_machine/_base.py,sha256=gdk1uIj--f50v2_X9zxqFRYTXiC
41
46
  cognite/neat/_session/_state_machine/_states.py,sha256=RX6C0YNNT9mX0C6c-ZTiUVh6pmMT5ZNXiOuSs80vVXU,4853
42
47
  cognite/neat/_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
48
  cognite/neat/_utils/auxiliary.py,sha256=Cx-LP8dfN782R3iUcm--q26zdzQ0k_RFnVbJ0bwVZMI,1345
44
- cognite/neat/_utils/text.py,sha256=1el5Ty1T0tdhTF0gp6L8AAzUxaFmAUAIFy_VvfG_kkM,1194
45
- cognite/neat/_utils/useful_types.py,sha256=6Fpw_HlWFj8ZYjGPd0KzguBczuI8GFhj5wBceGsaeak,211
46
- cognite/neat/_utils/validation.py,sha256=Rwd-oABdnHyiwd2WR1H4F2JYTdshNIKzHqErbo-MDns,5113
49
+ cognite/neat/_utils/text.py,sha256=tDspKjU4AkzB6P1kKPNaVpx-yvx3V2uQVC3FAr6nibM,1835
50
+ cognite/neat/_utils/useful_types.py,sha256=3qVfeQXg0xCcSEG7RymYU84LDbk0joKzruFY9EKAX8g,415
51
+ cognite/neat/_utils/validation.py,sha256=vS7GAvMMeco1jUaEs_Rz2pUoz28uee6xU4yt8CrCzE4,5430
47
52
  cognite/neat/_utils/http_client/__init__.py,sha256=gJBrOH1tIzEzLforHbeakYimTn4RlelyANps-jtpREI,894
48
53
  cognite/neat/_utils/http_client/_client.py,sha256=2RVwTbbPFlQ8eJVLKNUXwnc4Yq_783PkY44zwr6LlT8,11509
49
54
  cognite/neat/_utils/http_client/_config.py,sha256=C8IF1JoijmVMjA_FEMgAkiD1buEV1cY5Og3t-Ecyfmk,756
@@ -68,7 +73,7 @@ cognite/neat/v0/core/_client/data_classes/neat_sequence.py,sha256=QZWSfWnwk6KlYJ
68
73
  cognite/neat/v0/core/_client/data_classes/schema.py,sha256=YoLm0YQIhbYSFuSgC-GB3jgNy7qZw4WhT3A-jKkIVhk,25079
69
74
  cognite/neat/v0/core/_client/data_classes/statistics.py,sha256=GU-u41cOTig0Y5pYhW5KqzCsuAUIX9tOmdizMEveYuw,4487
70
75
  cognite/neat/v0/core/_data_model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
- cognite/neat/v0/core/_data_model/_constants.py,sha256=BEzWHWnEmtkYSPJVcGv3rL7JuYIZZNeI4Y5IUhtqTFk,6071
76
+ cognite/neat/v0/core/_data_model/_constants.py,sha256=oNgFTYEQm1yjpUWhWlY9bVjoZ4odXOLQK0r_kGllNOA,6121
72
77
  cognite/neat/v0/core/_data_model/_shared.py,sha256=ug6B_Vz7seg464xMMn0v3SKN_FNCNG1QeLqW-Bto-yA,2100
73
78
  cognite/neat/v0/core/_data_model/analysis/__init__.py,sha256=v3hSfz7AEEqcmdjL71I09tP8Hl-gPZYOiDYMp_CW4vg,70
74
79
  cognite/neat/v0/core/_data_model/analysis/_base.py,sha256=AfaKN-NlrXT5J-Tf3CuVzZHwbJXuJbdXwFRJ9uGI_Qg,24472
@@ -79,7 +84,7 @@ cognite/neat/v0/core/_data_model/catalog/hello_world_pump.xlsx,sha256=E63t5U1PQL
79
84
  cognite/neat/v0/core/_data_model/exporters/__init__.py,sha256=NSTeVtfgfkfrhf_Ruz5rHnlF0wIj2dIhAm3io1tEIkQ,1153
80
85
  cognite/neat/v0/core/_data_model/exporters/_base.py,sha256=pdtQDi5G89JVbHtGnvLHJDoujT1UVaCBw939Re1Gqv4,2412
81
86
  cognite/neat/v0/core/_data_model/exporters/_data_model2dms.py,sha256=LdLGgR73n9k5hHiWUSdvDc0lmaEO8UVQYFTiaVWHQ-c,19918
82
- cognite/neat/v0/core/_data_model/exporters/_data_model2excel.py,sha256=dT7ZHt57y3mca4TtCBnqMER0xlB7AyvivG0EZ2bGPzg,25484
87
+ cognite/neat/v0/core/_data_model/exporters/_data_model2excel.py,sha256=Oba2Cj81QNNxnSVX4-xxwyysGeAUPqUaf9oHEnpQjAc,25514
83
88
  cognite/neat/v0/core/_data_model/exporters/_data_model2instance_template.py,sha256=cmyQBlc1UM5l42g1N_F2K1sVu39ontsmA2bfXJO5muQ,6115
84
89
  cognite/neat/v0/core/_data_model/exporters/_data_model2semantic_model.py,sha256=vxeDEWyvj7uv6geNlTQe6MfGcNZkeQJYz0vLzPI7e14,23154
85
90
  cognite/neat/v0/core/_data_model/exporters/_data_model2yaml.py,sha256=v7CgNVtVv4t0FXHPJM-HGkID0AqVDlwsQqUNjWqr_1U,3287
@@ -87,9 +92,9 @@ cognite/neat/v0/core/_data_model/importers/__init__.py,sha256=jkDKSGv5VdJgl44lmn
87
92
  cognite/neat/v0/core/_data_model/importers/_base.py,sha256=SAC3nhJUBBl5PwnIs-bQvVam_0zEGjlOlNMvVOuJZzI,2028
88
93
  cognite/neat/v0/core/_data_model/importers/_base_file_reader.py,sha256=OjjXb0IOlu69PXrhwH9EH4jbwNFwyKfYOQR0WnuwqO4,1627
89
94
  cognite/neat/v0/core/_data_model/importers/_dict2data_model.py,sha256=CeDZNAO4W5JZmrboYtTfHnuU1UoXRI2nOUaasWmxfqM,4720
90
- cognite/neat/v0/core/_data_model/importers/_dms2data_model.py,sha256=CuZk9BiD-y3n0sZGDxByGTK3frI0pSjIn6mr6djj7lY,29357
95
+ cognite/neat/v0/core/_data_model/importers/_dms2data_model.py,sha256=yXQucBtmJLzEHKly2RcOB-g2iiAyM_h4jFbVT-38gHM,29557
91
96
  cognite/neat/v0/core/_data_model/importers/_graph2data_model.py,sha256=ILgrhror8PUpwKT9Q9Y8XO_rLSe4qPV4v7jvltjhzeM,13720
92
- cognite/neat/v0/core/_data_model/importers/_spreadsheet2data_model.py,sha256=YySY5LxnyD6xwEmg4UqWvc9TfErHUDaJ0d-vM2IUVbc,12103
97
+ cognite/neat/v0/core/_data_model/importers/_spreadsheet2data_model.py,sha256=qsvb4cA93VpTfMATaVnZyXs-hpzTfxl8jwGk5cpp3k0,15437
93
98
  cognite/neat/v0/core/_data_model/importers/_rdf/__init__.py,sha256=1yOjV2PKCxwH7uCTXVZhSdxtn5etmFX40cksvwtKcZ8,199
94
99
  cognite/neat/v0/core/_data_model/importers/_rdf/_base.py,sha256=Rv24TQQDZqAuFD8Qh0yRBiB7-JvoMVj3mGoPBm-xBFs,6052
95
100
  cognite/neat/v0/core/_data_model/importers/_rdf/_inference2rdata_model.py,sha256=w8LsdkzgoTXN3LZyjEL_5grWZGlf7PPj8qOu370_K6M,28976
@@ -105,25 +110,25 @@ cognite/neat/v0/core/_data_model/models/conceptual/__init__.py,sha256=9A6myEV8s0
105
110
  cognite/neat/v0/core/_data_model/models/conceptual/_unverified.py,sha256=UvqjGxVQsf_cNCYf726TXgnPplUy3iBuI-nUp0TclD0,6539
106
111
  cognite/neat/v0/core/_data_model/models/conceptual/_validation.py,sha256=oaXRe1RfwsCQ_D5-IdXJf6yZ4Eug_PLu-bDklADYGV4,13895
107
112
  cognite/neat/v0/core/_data_model/models/conceptual/_verified.py,sha256=8-H535sx-V11cNyuVOHImqWga8CsqcjPvXfwp0PclBQ,13743
108
- cognite/neat/v0/core/_data_model/models/entities/__init__.py,sha256=qXrnTygYBgu4iE-ehNsHwmZ89pm9pTfU_Qe9rAq-ldg,1789
113
+ cognite/neat/v0/core/_data_model/models/entities/__init__.py,sha256=pDnSy0k2WmoGuvzyLmXimH3IDA6pjO0bO0aEse2TC3o,1853
109
114
  cognite/neat/v0/core/_data_model/models/entities/_constants.py,sha256=GXRzVfArwxF3C67VCkzy0JWTZRkRJUYXBQaaecrqcWc,351
110
115
  cognite/neat/v0/core/_data_model/models/entities/_loaders.py,sha256=6UD1ik4juMh-yW9cEEnXuheV1vwopdM6d_BBpyUOUUY,5709
111
116
  cognite/neat/v0/core/_data_model/models/entities/_multi_value.py,sha256=lkQ95o3Tvf7QImDq8d6KYfgYyq4_zQCqVBAZDOdK2TY,2798
112
117
  cognite/neat/v0/core/_data_model/models/entities/_restrictions.py,sha256=wjA6GpfL1FGIjsf6Mo6Rj9Ht-gRnBvOkhj_waN2w2p4,8943
113
- cognite/neat/v0/core/_data_model/models/entities/_single_value.py,sha256=LS4Zi5iT4WcCrDadwlxAa8cNvA6ucjFf3Mi0cEfbWQg,23972
114
- cognite/neat/v0/core/_data_model/models/entities/_types.py,sha256=MqrCovqI_nvpMB4UqiUk4eUlKANvr8P7wr8k3y8lXlQ,2183
118
+ cognite/neat/v0/core/_data_model/models/entities/_single_value.py,sha256=ou6adX0-h7PVN7_6IrvP8WXoCDpstTkq7C9Yh_scaZU,24404
119
+ cognite/neat/v0/core/_data_model/models/entities/_types.py,sha256=ZmtRvQf4Ghhf4hDnKbZeQq4tZyj5NcQngtw8GeZYa_k,2432
115
120
  cognite/neat/v0/core/_data_model/models/entities/_wrapped.py,sha256=hOvdyxCNFgv1UdfaasviKnbEN4yN09Iip0ggQiaXgB4,7993
116
121
  cognite/neat/v0/core/_data_model/models/mapping/__init__.py,sha256=T68Hf7rhiXa7b03h4RMwarAmkGnB-Bbhc1H07b2PyC4,100
117
122
  cognite/neat/v0/core/_data_model/models/mapping/_classic2core.py,sha256=F0zusTh9pPR4z-RExPw3o4EMBSU2si6FJLuej2a3JzM,1430
118
123
  cognite/neat/v0/core/_data_model/models/mapping/_classic2core.yaml,sha256=ei-nuivNWVW9HmvzDBKIPF6ZdgaMq64XHw_rKm0CMxg,22584
119
124
  cognite/neat/v0/core/_data_model/models/physical/__init__.py,sha256=pH5ZF8jiW0A2w7VCSoHUsXxe894QFvTtgjxXNGVVaxk,990
120
- cognite/neat/v0/core/_data_model/models/physical/_exporter.py,sha256=Dfll4NU5roUCCvjZ5_q25SYjZntCl5LucuH5Fr4JjQY,30744
121
- cognite/neat/v0/core/_data_model/models/physical/_unverified.py,sha256=7la586tAfj7a1_Ql8-Z_amf5mFeBXjAJ57bVdNggIrE,20140
122
- cognite/neat/v0/core/_data_model/models/physical/_validation.py,sha256=I954pfdIA2OWAwQP7foyNa6FBJUVB3U-aqf6Likoqp0,41188
123
- cognite/neat/v0/core/_data_model/models/physical/_verified.py,sha256=WU0HV8coZfRw-XIEVJZl9PxUaY0Y6q75fBoUF2MgwU4,26813
125
+ cognite/neat/v0/core/_data_model/models/physical/_exporter.py,sha256=ega1Yyosq-D9XYCypx9wtuMTFwS0EMRo_e3JwW434XU,30335
126
+ cognite/neat/v0/core/_data_model/models/physical/_unverified.py,sha256=ZhxKGVM_wyl8uNFspsoNs9uSD0SVE1HO9HcRy2ffzDQ,22421
127
+ cognite/neat/v0/core/_data_model/models/physical/_validation.py,sha256=wxk65FrF5u_OwT28yB1tQHwRwkjj5Q938P6_b9RMIsA,41330
128
+ cognite/neat/v0/core/_data_model/models/physical/_verified.py,sha256=J0HNOAlAm2WNlNI0EcOaBOI-mJxUHghs2BIgLYevSbM,29830
124
129
  cognite/neat/v0/core/_data_model/transformers/__init__.py,sha256=N6yRBplAkrwwxoTAre_1BE_fdSZL5jihr7xTQjW3KnM,1876
125
130
  cognite/neat/v0/core/_data_model/transformers/_base.py,sha256=3ZO73A3xIAFUAyH4U682CbPUmZLPUB8HpWGpYvVbRBQ,3145
126
- cognite/neat/v0/core/_data_model/transformers/_converters.py,sha256=9Y8-94Vo0wfFjZvyumFzty-sNBo_1gt-FSHazjfb21E,111594
131
+ cognite/neat/v0/core/_data_model/transformers/_converters.py,sha256=vgypwPCI14DtKDl3WdXzuFxH1bgxH-7yLG-Ytvjo0MM,112042
127
132
  cognite/neat/v0/core/_data_model/transformers/_mapping.py,sha256=6R4QVblqivI1NvI0iSG5xC7fGHrdZcxqRg3c2Zx5o2E,19045
128
133
  cognite/neat/v0/core/_data_model/transformers/_union_conceptual.py,sha256=Dp8Oe6i2duihwDWnGnfPEXRExyKIMiFM_XEcr0U9IbE,8867
129
134
  cognite/neat/v0/core/_data_model/transformers/_verification.py,sha256=rIsmrmHvCgEddpPFIEXMMbRb9x9m3jCBi184g3JcrNA,5214
@@ -200,7 +205,7 @@ cognite/neat/v0/core/_utils/collection_.py,sha256=hYyvje47AAvY7sCmZy0NouX1OMVKLh
200
205
  cognite/neat/v0/core/_utils/graph_transformations_report.py,sha256=-V0LM9BI_bHT6PTTG9Qby4PGtFkYU3cxtF0cz8KoKek,1238
201
206
  cognite/neat/v0/core/_utils/io_.py,sha256=D2Mg8sOxfBoDg3fC0jBzaxO3vkXmr0QvZSgYIv6xRkM,386
202
207
  cognite/neat/v0/core/_utils/rdf_.py,sha256=ccnKqLPyKyZ44KknsN3kpCf-DkjSHSqX2KMreFN4Dlc,11226
203
- cognite/neat/v0/core/_utils/spreadsheet.py,sha256=wQWL8XJ8cRxWi_Y4oiDyEB2w1DICU9h6OvFCPLYZipg,5954
208
+ cognite/neat/v0/core/_utils/spreadsheet.py,sha256=lc8lO1v6PIRZ7s0sFBqUOzDGRD1JhsuW-zSghC-xnqE,6409
204
209
  cognite/neat/v0/core/_utils/tarjan.py,sha256=IZvwaIITryGVNbo9Bv5EA9_sW3DyfUNAe7uYyPOCL0g,1357
205
210
  cognite/neat/v0/core/_utils/text.py,sha256=3Dkz_lmGOHHJ_LKnd1GM_wrGHUWXsXse0Us2BAW5fUI,8522
206
211
  cognite/neat/v0/core/_utils/time_.py,sha256=7ayUm0OWZm1JDmy32E4ip8WRr2o0GLwrHwJA8sJ43Z4,357
@@ -239,7 +244,7 @@ cognite/neat/v0/session/engine/__init__.py,sha256=D3MxUorEs6-NtgoICqtZ8PISQrjrr4
239
244
  cognite/neat/v0/session/engine/_import.py,sha256=1QxA2_EK613lXYAHKQbZyw2yjo5P9XuiX4Z6_6-WMNQ,169
240
245
  cognite/neat/v0/session/engine/_interface.py,sha256=3W-cYr493c_mW3P5O6MKN1xEQg3cA7NHR_ev3zdF9Vk,533
241
246
  cognite/neat/v0/session/engine/_load.py,sha256=u0x7vuQCRoNcPt25KJBJRn8sJabonYK4vtSZpiTdP4k,5201
242
- cognite_neat-0.124.0.dist-info/METADATA,sha256=uQiuJcVRVFPhAyKUSXFn7DyXW8kfBE75pCQgnZxU8Ec,9147
243
- cognite_neat-0.124.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
244
- cognite_neat-0.124.0.dist-info/licenses/LICENSE,sha256=W8VmvFia4WHa3Gqxq1Ygrq85McUNqIGDVgtdvzT-XqA,11351
245
- cognite_neat-0.124.0.dist-info/RECORD,,
247
+ cognite_neat-0.125.0.dist-info/METADATA,sha256=7ivvPpgIWACTNikYCjpnMN88v2kbu9G3JNIhGL3oTgQ,9147
248
+ cognite_neat-0.125.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
249
+ cognite_neat-0.125.0.dist-info/licenses/LICENSE,sha256=W8VmvFia4WHa3Gqxq1Ygrq85McUNqIGDVgtdvzT-XqA,11351
250
+ cognite_neat-0.125.0.dist-info/RECORD,,