cognite-neat 0.110.0__py3-none-any.whl → 0.111.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 (53) hide show
  1. cognite/neat/_alpha.py +6 -0
  2. cognite/neat/_client/_api/schema.py +26 -0
  3. cognite/neat/_client/data_classes/schema.py +1 -1
  4. cognite/neat/_constants.py +4 -1
  5. cognite/neat/_graph/extractors/__init__.py +4 -0
  6. cognite/neat/_graph/extractors/_classic_cdf/_base.py +8 -16
  7. cognite/neat/_graph/extractors/_classic_cdf/_classic.py +39 -9
  8. cognite/neat/_graph/extractors/_classic_cdf/_relationships.py +23 -17
  9. cognite/neat/_graph/extractors/_classic_cdf/_sequences.py +15 -17
  10. cognite/neat/_graph/extractors/_dict.py +102 -0
  11. cognite/neat/_graph/extractors/_dms.py +27 -40
  12. cognite/neat/_graph/extractors/_dms_graph.py +30 -3
  13. cognite/neat/_graph/extractors/_raw.py +67 -0
  14. cognite/neat/_graph/loaders/_base.py +20 -4
  15. cognite/neat/_graph/loaders/_rdf2dms.py +243 -89
  16. cognite/neat/_graph/queries/_base.py +137 -43
  17. cognite/neat/_graph/transformers/_classic_cdf.py +6 -22
  18. cognite/neat/_issues/_factory.py +9 -1
  19. cognite/neat/_issues/errors/__init__.py +2 -0
  20. cognite/neat/_issues/errors/_external.py +7 -0
  21. cognite/neat/_issues/warnings/user_modeling.py +12 -0
  22. cognite/neat/_rules/_constants.py +3 -0
  23. cognite/neat/_rules/analysis/_base.py +29 -50
  24. cognite/neat/_rules/exporters/_rules2excel.py +1 -1
  25. cognite/neat/_rules/importers/_rdf/_inference2rules.py +16 -10
  26. cognite/neat/_rules/models/_base_rules.py +0 -2
  27. cognite/neat/_rules/models/data_types.py +7 -0
  28. cognite/neat/_rules/models/dms/_exporter.py +9 -8
  29. cognite/neat/_rules/models/dms/_rules.py +26 -1
  30. cognite/neat/_rules/models/dms/_rules_input.py +5 -1
  31. cognite/neat/_rules/models/dms/_validation.py +101 -1
  32. cognite/neat/_rules/models/entities/_single_value.py +8 -3
  33. cognite/neat/_rules/models/entities/_wrapped.py +2 -2
  34. cognite/neat/_rules/models/information/_rules_input.py +1 -0
  35. cognite/neat/_rules/models/information/_validation.py +64 -17
  36. cognite/neat/_rules/transformers/_converters.py +7 -2
  37. cognite/neat/_session/_base.py +2 -0
  38. cognite/neat/_session/_explore.py +39 -0
  39. cognite/neat/_session/_inspect.py +25 -6
  40. cognite/neat/_session/_read.py +67 -3
  41. cognite/neat/_session/_set.py +7 -1
  42. cognite/neat/_session/_state.py +6 -0
  43. cognite/neat/_session/_to.py +115 -8
  44. cognite/neat/_store/_graph_store.py +8 -4
  45. cognite/neat/_utils/rdf_.py +34 -3
  46. cognite/neat/_utils/text.py +72 -4
  47. cognite/neat/_utils/upload.py +2 -0
  48. cognite/neat/_version.py +2 -2
  49. {cognite_neat-0.110.0.dist-info → cognite_neat-0.111.0.dist-info}/METADATA +1 -1
  50. {cognite_neat-0.110.0.dist-info → cognite_neat-0.111.0.dist-info}/RECORD +53 -50
  51. {cognite_neat-0.110.0.dist-info → cognite_neat-0.111.0.dist-info}/LICENSE +0 -0
  52. {cognite_neat-0.110.0.dist-info → cognite_neat-0.111.0.dist-info}/WHEEL +0 -0
  53. {cognite_neat-0.110.0.dist-info → cognite_neat-0.111.0.dist-info}/entry_points.txt +0 -0
@@ -130,6 +130,13 @@ class DataType(BaseModel):
130
130
  def as_xml_uri_ref(cls) -> URIRef:
131
131
  return XML_SCHEMA_NAMESPACE[cls.xsd]
132
132
 
133
+ @classmethod
134
+ def convert_value(cls, value: Any) -> Any:
135
+ if cls != Boolean:
136
+ return cls.python(value)
137
+ else:
138
+ return value.strip().lower() in {"true", "1", "yes"}
139
+
133
140
 
134
141
  class Boolean(DataType):
135
142
  python = bool
@@ -99,6 +99,7 @@ class _DMSExporter:
99
99
  key=lambda x: x.as_tuple(), # type: ignore[union-attr]
100
100
  )
101
101
  spaces = self._create_spaces(rules.metadata, containers, views, data_model)
102
+
102
103
  return DMSSchema(
103
104
  spaces=spaces,
104
105
  data_model=data_model,
@@ -114,14 +115,14 @@ class _DMSExporter:
114
115
  views: ViewApplyDict,
115
116
  data_model: dm.DataModelApply,
116
117
  ) -> SpaceApplyDict:
117
- used_spaces = {container.space for container in containers.values()} | {view.space for view in views.values()}
118
- if len(used_spaces) == 1:
119
- # We skip the default space and only use this space for the data model
120
- data_model.space = used_spaces.pop()
121
- spaces = SpaceApplyDict([dm.SpaceApply(space=data_model.space)])
122
- else:
123
- used_spaces.add(metadata.space)
124
- spaces = SpaceApplyDict([dm.SpaceApply(space=space) for space in used_spaces])
118
+ used_spaces = (
119
+ {container.space for container in containers.values()}
120
+ | {view.space for view in views.values()}
121
+ | {data_model.space}
122
+ | {metadata.space}
123
+ )
124
+
125
+ spaces = SpaceApplyDict([dm.SpaceApply(space=space) for space in used_spaces])
125
126
  if self.instance_space and self.instance_space not in spaces:
126
127
  spaces[self.instance_space] = dm.SpaceApply(space=self.instance_space, name=self.instance_space)
127
128
  return spaces
@@ -1,3 +1,4 @@
1
+ import warnings
1
2
  from collections.abc import Hashable
2
3
  from typing import TYPE_CHECKING, Any, ClassVar, Literal
3
4
 
@@ -8,6 +9,7 @@ from pydantic_core.core_schema import SerializationInfo, ValidationInfo
8
9
 
9
10
  from cognite.neat._client.data_classes.schema import DMSSchema
10
11
  from cognite.neat._issues.errors import NeatValueError
12
+ from cognite.neat._issues.warnings._general import NeatValueWarning
11
13
  from cognite.neat._rules.models._base_rules import (
12
14
  BaseMetadata,
13
15
  BaseRules,
@@ -116,7 +118,7 @@ class DMSProperty(SheetRow):
116
118
  description="Used to indicate whether the property holds single or multiple values (list). "
117
119
  "Only applies to primitive types.",
118
120
  )
119
- default: str | int | dict | None = Field(
121
+ default: bool | str | int | float | dict | None = Field(
120
122
  None, alias="Default", description="Specifies default value for the property."
121
123
  )
122
124
  container: ContainerEntityType | None = Field(
@@ -168,6 +170,29 @@ class DMSProperty(SheetRow):
168
170
  raise ValueError(f"Reverse connection must have a value type that points to a view, got {value}")
169
171
  return value
170
172
 
173
+ @field_validator("default", mode="after")
174
+ def set_proper_type_on_default(cls, value: Any, info: ValidationInfo) -> Any:
175
+ if not value:
176
+ return value
177
+ value_type = info.data.get("value_type")
178
+ if not isinstance(value_type, DataType):
179
+ warnings.filterwarnings("default")
180
+ warnings.warn(
181
+ NeatValueWarning(f"Default value {value} set to connection {value_type} will be ignored"),
182
+ stacklevel=2,
183
+ )
184
+ return None
185
+ else:
186
+ try:
187
+ return value_type.convert_value(value)
188
+ except ValueError:
189
+ warnings.filterwarnings("default")
190
+ warnings.warn(
191
+ NeatValueWarning(f"Could not convert {value} to {value_type}"),
192
+ stacklevel=2,
193
+ )
194
+ return None
195
+
171
196
  @field_validator("container", "container_property", mode="after")
172
197
  def container_set_correctly(cls, value: Any, info: ValidationInfo) -> Any:
173
198
  if (connection := info.data.get("connection")) is None:
@@ -21,6 +21,7 @@ from cognite.neat._rules.models.entities import (
21
21
  load_connection,
22
22
  load_dms_value_type,
23
23
  )
24
+ from cognite.neat._rules.models.entities._wrapped import DMSFilter
24
25
  from cognite.neat._utils.rdf_ import uri_display_name
25
26
 
26
27
  from ._rules import _DEFAULT_VERSION, DMSContainer, DMSEnum, DMSMetadata, DMSNode, DMSProperty, DMSRules, DMSView
@@ -69,7 +70,7 @@ class DMSInputMetadata(InputComponent[DMSMetadata]):
69
70
  def _get_description_and_creator(cls, description_raw: str | None) -> tuple[str | None, list[str]]:
70
71
  if description_raw and (description_match := re.search(r"Creator: (.+)", description_raw)):
71
72
  creator = description_match.group(1).split(", ")
72
- description = description_raw.replace(description_match.string, "").strip() or None
73
+ description = description_raw.replace(description_match[0], "").strip() or None
73
74
  elif description_raw:
74
75
  creator = ["MISSING"]
75
76
  description = description_raw
@@ -211,6 +212,8 @@ class DMSInputView(InputComponent[DMSView]):
211
212
  return ViewEntity.load(self.view, strict=True, space=default_space, version=default_version)
212
213
 
213
214
  def _load_implements(self, default_space: str, default_version: str) -> list[ViewEntity] | None:
215
+ self.implements = self.implements.strip() if self.implements else None
216
+
214
217
  return (
215
218
  [
216
219
  ViewEntity.load(implement, strict=True, space=default_space, version=default_version)
@@ -234,6 +237,7 @@ class DMSInputView(InputComponent[DMSView]):
234
237
  implements=", ".join([str(ViewEntity.from_id(parent, _DEFAULT_VERSION)) for parent in view.implements])
235
238
  or None,
236
239
  in_model=in_model,
240
+ filter_=(str(DMSFilter.from_dms_filter(view.filter)) if view.filter else None),
237
241
  )
238
242
 
239
243
 
@@ -27,10 +27,12 @@ from cognite.neat._issues.errors import (
27
27
  ResourceNotFoundError,
28
28
  ReversedConnectionNotFeasibleError,
29
29
  )
30
+ from cognite.neat._issues.errors._external import CDFMissingResourcesError
30
31
  from cognite.neat._issues.warnings import (
31
32
  NotSupportedHasDataFilterLimitWarning,
32
33
  NotSupportedViewContainerLimitWarning,
33
34
  UndefinedViewWarning,
35
+ user_modeling,
34
36
  )
35
37
  from cognite.neat._issues.warnings.user_modeling import (
36
38
  ContainerPropertyLimitWarning,
@@ -43,6 +45,7 @@ from cognite.neat._rules.models.entities._single_value import (
43
45
  ViewEntity,
44
46
  )
45
47
  from cognite.neat._utils.spreadsheet import SpreadsheetRead
48
+ from cognite.neat._utils.text import humanize_collection
46
49
 
47
50
  from ._rules import DMSProperty, DMSRules
48
51
 
@@ -113,6 +116,14 @@ class DMSValidation:
113
116
  list(imported_containers), include_connected=True
114
117
  )
115
118
 
119
+ missing_views = {view.as_id() for view in imported_views} - {view.as_id() for view in referenced_views}
120
+ missing_containers = {container.as_id() for container in imported_containers} - {
121
+ container.as_id() for container in referenced_containers
122
+ }
123
+
124
+ if missing_views or missing_containers:
125
+ raise CDFMissingResourcesError(resources=f"{missing_views.union(missing_containers)}")
126
+
116
127
  # Setup data structures for validation
117
128
  dms_schema = self._rules.as_schema()
118
129
  ref_view_by_id = {view.as_id(): view for view in referenced_views}
@@ -130,6 +141,10 @@ class DMSValidation:
130
141
  parents_view_ids_by_child_id = self._parent_view_ids_by_child_id(all_views_by_id)
131
142
 
132
143
  issue_list = IssueList()
144
+
145
+ # Validated for duplicated resource
146
+ issue_list.extend(self._duplicated_resources())
147
+
133
148
  # Neat DMS classes Validation
134
149
  # These are errors that can only happen due to the format of the Neat DMS classes
135
150
  issue_list.extend(self._validate_raw_filter())
@@ -148,6 +163,91 @@ class DMSValidation:
148
163
  )
149
164
  issue_list.extend(self._validate_schema(dms_schema, all_views_by_id, all_containers_by_id))
150
165
  issue_list.extend(self._validate_referenced_container_limits(dms_schema.views, view_properties_by_id))
166
+ issue_list.extend(self._same_space_views_and_data_model())
167
+ return issue_list
168
+
169
+ def _same_space_views_and_data_model(self) -> IssueList:
170
+ issue_list = IssueList()
171
+
172
+ schema = self._rules.as_schema(remove_cdf_spaces=True)
173
+
174
+ if schema.data_model and schema.views:
175
+ data_model_space = schema.data_model.space
176
+ views_spaces = {view.space for view in schema.views.values()}
177
+
178
+ if data_model_space not in views_spaces:
179
+ issue_list.append(
180
+ user_modeling.ViewsAndDataModelNotInSameSpaceWarning(
181
+ data_model_space=data_model_space,
182
+ views_spaces=humanize_collection(views_spaces),
183
+ )
184
+ )
185
+
186
+ return issue_list
187
+
188
+ def _duplicated_resources(self) -> IssueList:
189
+ issue_list = IssueList()
190
+
191
+ properties_sheet = self._read_info_by_spreadsheet.get("Properties")
192
+ views_sheet = self._read_info_by_spreadsheet.get("Views")
193
+ containers_sheet = self._read_info_by_spreadsheet.get("Containers")
194
+
195
+ visited = defaultdict(list)
196
+ for row_no, property_ in enumerate(self._properties):
197
+ visited[property_._identifier()].append(
198
+ properties_sheet.adjusted_row_number(row_no) if properties_sheet else row_no + 1
199
+ )
200
+
201
+ for identifier, rows in visited.items():
202
+ if len(rows) == 1:
203
+ continue
204
+ issue_list.append(
205
+ ResourceDuplicatedError(
206
+ identifier[1],
207
+ "property",
208
+ (
209
+ f"the Properties sheet at row {humanize_collection(rows)} "
210
+ "if data model is read from a spreadsheet."
211
+ ),
212
+ )
213
+ )
214
+
215
+ visited = defaultdict(list)
216
+ for row_no, view in enumerate(self._views):
217
+ visited[view._identifier()].append(views_sheet.adjusted_row_number(row_no) if views_sheet else row_no + 1)
218
+
219
+ for identifier, rows in visited.items():
220
+ if len(rows) == 1:
221
+ continue
222
+ issue_list.append(
223
+ ResourceDuplicatedError(
224
+ identifier[0],
225
+ "view",
226
+ (f"the Views sheet at row {humanize_collection(rows)} if data model is read from a spreadsheet."),
227
+ )
228
+ )
229
+
230
+ if self._containers:
231
+ visited = defaultdict(list)
232
+ for row_no, container in enumerate(self._containers):
233
+ visited[container._identifier()].append(
234
+ containers_sheet.adjusted_row_number(row_no) if containers_sheet else row_no + 1
235
+ )
236
+
237
+ for identifier, rows in visited.items():
238
+ if len(rows) == 1:
239
+ continue
240
+ issue_list.append(
241
+ ResourceDuplicatedError(
242
+ identifier[0],
243
+ "container",
244
+ (
245
+ f"the Containers sheet at row {humanize_collection(rows)} "
246
+ "if data model is read from a spreadsheet."
247
+ ),
248
+ )
249
+ )
250
+
151
251
  return issue_list
152
252
 
153
253
  @staticmethod
@@ -595,7 +695,7 @@ class DMSValidation:
595
695
  ResourceDuplicatedError(
596
696
  view_id,
597
697
  "view",
598
- repr(model.as_id()),
698
+ f"DMS {model.as_id()!r}",
599
699
  )
600
700
  )
601
701
 
@@ -35,6 +35,7 @@ else:
35
35
  from cognite.neat._rules._constants import (
36
36
  ENTITY_PATTERN,
37
37
  SPLIT_ON_COMMA_PATTERN,
38
+ SPLIT_ON_EDGE_ENTITY_ARGS_PATTERN,
38
39
  SPLIT_ON_EQUAL_PATTERN,
39
40
  EntityTypes,
40
41
  )
@@ -135,9 +136,13 @@ class Entity(BaseModel, extra="ignore"):
135
136
  if content is None:
136
137
  return dict(prefix=prefix, suffix=suffix)
137
138
  try:
138
- extra_args = dict(
139
- SPLIT_ON_EQUAL_PATTERN.split(pair.strip()) for pair in SPLIT_ON_COMMA_PATTERN.split(content)
140
- )
139
+ if cls == EdgeEntity:
140
+ matches = SPLIT_ON_EDGE_ENTITY_ARGS_PATTERN.findall(content)
141
+ extra_args = {key: value for key, value in matches}
142
+ else:
143
+ extra_args = dict(
144
+ SPLIT_ON_EQUAL_PATTERN.split(pair.strip()) for pair in SPLIT_ON_COMMA_PATTERN.split(content)
145
+ )
141
146
  except ValueError:
142
147
  raise NeatValueError(f"Invalid {cls.type_.value} entity: {raw!r}") from None
143
148
  expected_args = {
@@ -127,8 +127,8 @@ class DMSFilter(WrappedEntity):
127
127
  if isinstance(entry, dict) and "space" in entry and "externalId" in entry
128
128
  ]
129
129
  )
130
-
131
- raise ValueError(f"Cannot convert {filter._filter_name} to {cls.__name__}")
130
+ # fall back to raw filter to preserve the information
131
+ return RawFilter(filter=json.dumps(dumped))
132
132
 
133
133
 
134
134
  class NodeTypeFilter(DMSFilter):
@@ -128,6 +128,7 @@ class InformationInputClass(InputComponent[InformationClass]):
128
128
  output = super().dump()
129
129
  parent: list[ClassEntity] | None = None
130
130
  if isinstance(self.implements, str):
131
+ self.implements = self.implements.strip()
131
132
  parent = [ClassEntity.load(parent, prefix=default_prefix) for parent in self.implements.split(",")]
132
133
  elif isinstance(self.implements, list):
133
134
  parent = [ClassEntity.load(parent_, prefix=default_prefix) for parent_ in self.implements]
@@ -1,9 +1,12 @@
1
1
  import itertools
2
- from collections import Counter
2
+ from collections import Counter, defaultdict
3
3
 
4
4
  from cognite.neat._issues import IssueList
5
5
  from cognite.neat._issues.errors import NeatValueError
6
- from cognite.neat._issues.errors._resources import ResourceNotDefinedError
6
+ from cognite.neat._issues.errors._resources import (
7
+ ResourceDuplicatedError,
8
+ ResourceNotDefinedError,
9
+ )
7
10
  from cognite.neat._issues.warnings._models import UndefinedClassWarning
8
11
  from cognite.neat._issues.warnings._resources import (
9
12
  ResourceNotDefinedWarning,
@@ -13,6 +16,7 @@ from cognite.neat._rules._constants import PATTERNS, EntityTypes
13
16
  from cognite.neat._rules.models.entities import ClassEntity, UnknownEntity
14
17
  from cognite.neat._rules.models.entities._multi_value import MultiValueTypeInfo
15
18
  from cognite.neat._utils.spreadsheet import SpreadsheetRead
19
+ from cognite.neat._utils.text import humanize_collection
16
20
 
17
21
  from ._rules import InformationRules
18
22
 
@@ -23,13 +27,14 @@ class InformationValidation:
23
27
 
24
28
  def __init__(self, rules: InformationRules, read_info_by_spreadsheet: dict[str, SpreadsheetRead] | None = None):
25
29
  self.rules = rules
26
- self.read_info_by_spreadsheet = read_info_by_spreadsheet
27
- self.metadata = rules.metadata
28
- self.properties = rules.properties
29
- self.classes = rules.classes
30
+ self._read_info_by_spreadsheet = read_info_by_spreadsheet or {}
31
+ self._metadata = rules.metadata
32
+ self._properties = rules.properties
33
+ self._classes = rules.classes
30
34
  self.issue_list = IssueList()
31
35
 
32
36
  def validate(self) -> IssueList:
37
+ self._duplicated_resources()
33
38
  self._namespaces_reassigned()
34
39
  self._classes_without_properties()
35
40
  self._undefined_classes()
@@ -40,9 +45,51 @@ class InformationValidation:
40
45
 
41
46
  return self.issue_list
42
47
 
48
+ def _duplicated_resources(self) -> None:
49
+ properties_sheet = self._read_info_by_spreadsheet.get("Properties")
50
+ classes_sheet = self._read_info_by_spreadsheet.get("Classes")
51
+
52
+ visited = defaultdict(list)
53
+ for row_no, property_ in enumerate(self._properties):
54
+ visited[property_._identifier()].append(
55
+ properties_sheet.adjusted_row_number(row_no) if properties_sheet else row_no + 1
56
+ )
57
+
58
+ for identifier, rows in visited.items():
59
+ if len(rows) == 1:
60
+ continue
61
+ self.issue_list.append(
62
+ ResourceDuplicatedError(
63
+ identifier[1],
64
+ "property",
65
+ (
66
+ "the Properties sheet at row "
67
+ f"{humanize_collection(rows)}"
68
+ " if data model is read from a spreadsheet."
69
+ ),
70
+ )
71
+ )
72
+
73
+ visited = defaultdict(list)
74
+ for row_no, class_ in enumerate(self._classes):
75
+ visited[class_._identifier()].append(
76
+ classes_sheet.adjusted_row_number(row_no) if classes_sheet else row_no + 1
77
+ )
78
+
79
+ for identifier, rows in visited.items():
80
+ if len(rows) == 1:
81
+ continue
82
+ self.issue_list.append(
83
+ ResourceDuplicatedError(
84
+ identifier[0],
85
+ "class",
86
+ (f"the Classes sheet at row {humanize_collection(rows)} if data model is read from a spreadsheet."),
87
+ )
88
+ )
89
+
43
90
  def _classes_without_properties(self) -> None:
44
- defined_classes = {class_.class_ for class_ in self.classes}
45
- referred_classes = {property_.class_ for property_ in self.properties}
91
+ defined_classes = {class_.class_ for class_ in self._classes}
92
+ referred_classes = {property_.class_ for property_ in self._properties}
46
93
  class_parent_pairs = self._class_parent_pairs()
47
94
 
48
95
  if classes_without_properties := defined_classes.difference(referred_classes):
@@ -50,7 +97,7 @@ class InformationValidation:
50
97
  # USE CASE: class has no direct properties and no parents with properties
51
98
  # and it is a class in the prefix of data model, as long as it is in the
52
99
  # same prefix, meaning same space
53
- if not class_parent_pairs[class_] and class_.prefix == self.metadata.prefix:
100
+ if not class_parent_pairs[class_] and class_.prefix == self._metadata.prefix:
54
101
  self.issue_list.append(
55
102
  ResourceNotDefinedWarning(
56
103
  resource_type="class",
@@ -60,8 +107,8 @@ class InformationValidation:
60
107
  )
61
108
 
62
109
  def _undefined_classes(self) -> None:
63
- defined_classes = {class_.class_ for class_ in self.classes}
64
- referred_classes = {property_.class_ for property_ in self.properties}
110
+ defined_classes = {class_.class_ for class_ in self._classes}
111
+ referred_classes = {property_.class_ for property_ in self._properties}
65
112
 
66
113
  if undefined_classes := referred_classes.difference(defined_classes):
67
114
  for class_ in undefined_classes:
@@ -81,7 +128,7 @@ class InformationValidation:
81
128
 
82
129
  if undefined_parents := parents.difference(classes):
83
130
  for parent in undefined_parents:
84
- if parent.prefix != self.metadata.prefix:
131
+ if parent.prefix != self._metadata.prefix:
85
132
  self.issue_list.append(UndefinedClassWarning(class_id=str(parent)))
86
133
  else:
87
134
  self.issue_list.append(
@@ -94,8 +141,8 @@ class InformationValidation:
94
141
 
95
142
  def _referenced_classes_exist(self) -> None:
96
143
  # needs to be complete for this validation to pass
97
- defined_classes = {class_.class_ for class_ in self.classes}
98
- classes_with_explicit_properties = {property_.class_ for property_ in self.properties}
144
+ defined_classes = {class_.class_ for class_ in self._classes}
145
+ classes_with_explicit_properties = {property_.class_ for property_ in self._properties}
99
146
 
100
147
  # USE CASE: models are complete
101
148
  if missing_classes := classes_with_explicit_properties.difference(defined_classes):
@@ -110,7 +157,7 @@ class InformationValidation:
110
157
 
111
158
  def _referenced_value_types_exist(self) -> None:
112
159
  # adding UnknownEntity to the set of defined classes to handle the case where a property references an unknown
113
- defined_classes = {class_.class_ for class_ in self.classes} | {UnknownEntity()}
160
+ defined_classes = {class_.class_ for class_ in self._classes} | {UnknownEntity()}
114
161
  referred_object_types = {
115
162
  property_.value_type
116
163
  for property_ in self.rules.properties
@@ -131,7 +178,7 @@ class InformationValidation:
131
178
  def _regex_compliance_with_dms(self) -> None:
132
179
  """Check regex compliance with DMS of properties, classes and value types."""
133
180
 
134
- for prop_ in self.properties:
181
+ for prop_ in self._properties:
135
182
  if not PATTERNS.dms_property_id_compliance.match(prop_.property_):
136
183
  self.issue_list.append(
137
184
  ResourceRegexViolationWarning(
@@ -181,7 +228,7 @@ class InformationValidation:
181
228
  )
182
229
  )
183
230
 
184
- for class_ in self.classes:
231
+ for class_ in self._classes:
185
232
  if not PATTERNS.view_id_compliance.match(class_.class_.suffix):
186
233
  self.issue_list.append(
187
234
  ResourceRegexViolationWarning(
@@ -60,7 +60,7 @@ from cognite.neat._rules.models.entities import (
60
60
  )
61
61
  from cognite.neat._rules.models.information import InformationClass, InformationMetadata, InformationProperty
62
62
  from cognite.neat._utils.rdf_ import get_inheritance_path
63
- from cognite.neat._utils.text import NamingStandardization, to_camel_case
63
+ from cognite.neat._utils.text import NamingStandardization, title, to_camel_case, to_words
64
64
 
65
65
  from ._base import RulesTransformer, T_VerifiedIn, T_VerifiedOut, VerifiedRulesTransformer
66
66
  from ._verification import VerifyDMSRules
@@ -561,8 +561,9 @@ _T_Entity = TypeVar("_T_Entity", bound=ClassEntity | ViewEntity)
561
561
 
562
562
 
563
563
  class SetIDDMSModel(VerifiedRulesTransformer[DMSRules, DMSRules]):
564
- def __init__(self, new_id: DataModelId | tuple[str, str, str]):
564
+ def __init__(self, new_id: DataModelId | tuple[str, str, str], name: str | None = None):
565
565
  self.new_id = DataModelId.load(new_id)
566
+ self.name = name
566
567
 
567
568
  @property
568
569
  def description(self) -> str:
@@ -575,10 +576,14 @@ class SetIDDMSModel(VerifiedRulesTransformer[DMSRules, DMSRules]):
575
576
  dump["metadata"]["space"] = self.new_id.space
576
577
  dump["metadata"]["external_id"] = self.new_id.external_id
577
578
  dump["metadata"]["version"] = self.new_id.version
579
+ dump["metadata"]["name"] = self.name or self._generate_name()
578
580
  # Serialize and deserialize to set the new space and external_id
579
581
  # as the default values for the new model.
580
582
  return DMSRules.model_validate(DMSInputRules.load(dump).dump())
581
583
 
584
+ def _generate_name(self) -> str:
585
+ return title(to_words(self.new_id.external_id))
586
+
582
587
 
583
588
  class ToExtensionModel(VerifiedRulesTransformer[DMSRules, DMSRules], ABC):
584
589
  type_: ClassVar[str]
@@ -25,6 +25,7 @@ from cognite.neat._utils.auxiliary import local_import
25
25
  from ._collector import _COLLECTOR, Collector
26
26
  from ._create import CreateAPI
27
27
  from ._drop import DropAPI
28
+ from ._explore import ExploreAPI
28
29
  from ._fix import FixAPI
29
30
  from ._inspect import InspectAPI
30
31
  from ._mapping import MappingAPI
@@ -104,6 +105,7 @@ class NeatSession:
104
105
  self.drop = DropAPI(self._state)
105
106
  self.subset = SubsetAPI(self._state)
106
107
  self.create = CreateAPI(self._state)
108
+ self._explore = ExploreAPI(self._state)
107
109
  self.opt = OptAPI()
108
110
  self.opt._display()
109
111
  if load_engine != "skip" and (engine_version := load_neat_engine(client, load_engine)):
@@ -0,0 +1,39 @@
1
+ from typing import cast
2
+
3
+ import pandas as pd
4
+ from rdflib import URIRef
5
+
6
+ from cognite.neat._utils.rdf_ import remove_namespace_from_uri
7
+ from cognite.neat._utils.text import humanize_collection
8
+
9
+ from ._state import SessionState
10
+ from .exceptions import NeatSessionError, session_class_wrapper
11
+
12
+
13
+ @session_class_wrapper
14
+ class ExploreAPI:
15
+ """
16
+ Explore the instances in the session.
17
+ """
18
+
19
+ def __init__(self, state: SessionState):
20
+ self._state = state
21
+
22
+ def types(self) -> pd.DataFrame:
23
+ """List all the types of instances in the session."""
24
+ return pd.DataFrame(self._state.instances.store.queries.types_with_instance_and_property_count())
25
+
26
+ def properties(self) -> pd.DataFrame:
27
+ """List all the properties of a type of instances in the session."""
28
+ return pd.DataFrame(self._state.instances.store.queries.properties_with_count())
29
+
30
+ def instance_with_properties(self, type: str) -> dict[str, set[str]]:
31
+ """List all the instances of a type with their properties."""
32
+ available_types = self._state.instances.store.queries.list_types(remove_namespace=False)
33
+ uri_by_type = {remove_namespace_from_uri(t[0]): t[0] for t in available_types}
34
+ if type not in uri_by_type:
35
+ raise NeatSessionError(
36
+ f"Type {type} not found. Available types are: {humanize_collection(uri_by_type.keys())}"
37
+ )
38
+ type_uri = cast(URIRef, uri_by_type[type])
39
+ return self._state.instances.store.queries.instances_with_properties(type_uri, remove_namespace=True)