cognite-neat 0.103.1__py3-none-any.whl → 0.105.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 (175) hide show
  1. cognite/neat/_client/_api/data_modeling_loaders.py +83 -23
  2. cognite/neat/_client/_api/schema.py +2 -1
  3. cognite/neat/_client/data_classes/neat_sequence.py +261 -0
  4. cognite/neat/_client/data_classes/schema.py +5 -1
  5. cognite/neat/_client/testing.py +33 -0
  6. cognite/neat/_constants.py +56 -0
  7. cognite/neat/_graph/extractors/_classic_cdf/_base.py +6 -5
  8. cognite/neat/_graph/extractors/_classic_cdf/_sequences.py +225 -11
  9. cognite/neat/_graph/extractors/_mock_graph_generator.py +2 -2
  10. cognite/neat/_graph/loaders/_rdf2dms.py +13 -2
  11. cognite/neat/_graph/transformers/__init__.py +3 -1
  12. cognite/neat/_graph/transformers/_base.py +109 -1
  13. cognite/neat/_graph/transformers/_classic_cdf.py +6 -1
  14. cognite/neat/_graph/transformers/_prune_graph.py +103 -47
  15. cognite/neat/_graph/transformers/_rdfpath.py +41 -17
  16. cognite/neat/_graph/transformers/_value_type.py +188 -151
  17. cognite/neat/_issues/__init__.py +0 -2
  18. cognite/neat/_issues/_base.py +54 -43
  19. cognite/neat/_issues/warnings/__init__.py +4 -1
  20. cognite/neat/_issues/warnings/_general.py +7 -0
  21. cognite/neat/_issues/warnings/_resources.py +12 -1
  22. cognite/neat/_rules/_shared.py +18 -34
  23. cognite/neat/_rules/exporters/_base.py +28 -2
  24. cognite/neat/_rules/exporters/_rules2dms.py +39 -1
  25. cognite/neat/_rules/exporters/_rules2excel.py +13 -2
  26. cognite/neat/_rules/exporters/_rules2instance_template.py +4 -0
  27. cognite/neat/_rules/exporters/_rules2ontology.py +13 -1
  28. cognite/neat/_rules/exporters/_rules2yaml.py +4 -0
  29. cognite/neat/_rules/importers/_base.py +9 -0
  30. cognite/neat/_rules/importers/_dms2rules.py +80 -57
  31. cognite/neat/_rules/importers/_dtdl2rules/dtdl_importer.py +5 -2
  32. cognite/neat/_rules/importers/_rdf/_base.py +10 -8
  33. cognite/neat/_rules/importers/_rdf/_imf2rules.py +4 -0
  34. cognite/neat/_rules/importers/_rdf/_inference2rules.py +7 -0
  35. cognite/neat/_rules/importers/_rdf/_owl2rules.py +4 -0
  36. cognite/neat/_rules/importers/_spreadsheet2rules.py +17 -8
  37. cognite/neat/_rules/importers/_yaml2rules.py +21 -7
  38. cognite/neat/_rules/models/_base_input.py +1 -1
  39. cognite/neat/_rules/models/_base_rules.py +9 -1
  40. cognite/neat/_rules/models/dms/_rules.py +4 -0
  41. cognite/neat/_rules/models/dms/_rules_input.py +9 -0
  42. cognite/neat/_rules/models/entities/_wrapped.py +10 -5
  43. cognite/neat/_rules/models/information/_rules.py +4 -0
  44. cognite/neat/_rules/models/information/_rules_input.py +9 -0
  45. cognite/neat/_rules/models/mapping/_classic2core.py +2 -5
  46. cognite/neat/_rules/models/mapping/_classic2core.yaml +239 -38
  47. cognite/neat/_rules/transformers/__init__.py +13 -6
  48. cognite/neat/_rules/transformers/_base.py +41 -65
  49. cognite/neat/_rules/transformers/_converters.py +404 -234
  50. cognite/neat/_rules/transformers/_mapping.py +93 -72
  51. cognite/neat/_rules/transformers/_verification.py +50 -38
  52. cognite/neat/_session/_base.py +32 -121
  53. cognite/neat/_session/_inspect.py +5 -3
  54. cognite/neat/_session/_mapping.py +17 -105
  55. cognite/neat/_session/_prepare.py +138 -268
  56. cognite/neat/_session/_read.py +39 -195
  57. cognite/neat/_session/_set.py +6 -30
  58. cognite/neat/_session/_show.py +40 -21
  59. cognite/neat/_session/_state.py +49 -107
  60. cognite/neat/_session/_to.py +44 -33
  61. cognite/neat/_shared.py +23 -2
  62. cognite/neat/_store/_provenance.py +3 -82
  63. cognite/neat/_store/_rules_store.py +368 -10
  64. cognite/neat/_store/exceptions.py +23 -0
  65. cognite/neat/_utils/graph_transformations_report.py +36 -0
  66. cognite/neat/_utils/rdf_.py +8 -0
  67. cognite/neat/_utils/reader/_base.py +27 -0
  68. cognite/neat/_utils/spreadsheet.py +5 -4
  69. cognite/neat/_version.py +1 -1
  70. {cognite_neat-0.103.1.dist-info → cognite_neat-0.105.0.dist-info}/METADATA +3 -2
  71. cognite_neat-0.105.0.dist-info/RECORD +179 -0
  72. {cognite_neat-0.103.1.dist-info → cognite_neat-0.105.0.dist-info}/WHEEL +1 -1
  73. cognite/neat/_app/api/__init__.py +0 -0
  74. cognite/neat/_app/api/asgi/metrics.py +0 -4
  75. cognite/neat/_app/api/configuration.py +0 -98
  76. cognite/neat/_app/api/context_manager/__init__.py +0 -3
  77. cognite/neat/_app/api/context_manager/manager.py +0 -16
  78. cognite/neat/_app/api/data_classes/__init__.py +0 -0
  79. cognite/neat/_app/api/data_classes/rest.py +0 -59
  80. cognite/neat/_app/api/explorer.py +0 -66
  81. cognite/neat/_app/api/routers/configuration.py +0 -25
  82. cognite/neat/_app/api/routers/crud.py +0 -102
  83. cognite/neat/_app/api/routers/metrics.py +0 -10
  84. cognite/neat/_app/api/routers/workflows.py +0 -224
  85. cognite/neat/_app/api/utils/__init__.py +0 -0
  86. cognite/neat/_app/api/utils/data_mapping.py +0 -17
  87. cognite/neat/_app/api/utils/logging.py +0 -26
  88. cognite/neat/_app/api/utils/query_templates.py +0 -92
  89. cognite/neat/_app/main.py +0 -17
  90. cognite/neat/_app/monitoring/__init__.py +0 -0
  91. cognite/neat/_app/monitoring/metrics.py +0 -69
  92. cognite/neat/_app/ui/index.html +0 -1
  93. cognite/neat/_app/ui/neat-app/.gitignore +0 -23
  94. cognite/neat/_app/ui/neat-app/README.md +0 -70
  95. cognite/neat/_app/ui/neat-app/build/asset-manifest.json +0 -14
  96. cognite/neat/_app/ui/neat-app/build/favicon.ico +0 -0
  97. cognite/neat/_app/ui/neat-app/build/img/architect-icon.svg +0 -116
  98. cognite/neat/_app/ui/neat-app/build/img/developer-icon.svg +0 -112
  99. cognite/neat/_app/ui/neat-app/build/img/sme-icon.svg +0 -34
  100. cognite/neat/_app/ui/neat-app/build/index.html +0 -1
  101. cognite/neat/_app/ui/neat-app/build/logo192.png +0 -0
  102. cognite/neat/_app/ui/neat-app/build/manifest.json +0 -25
  103. cognite/neat/_app/ui/neat-app/build/robots.txt +0 -3
  104. cognite/neat/_app/ui/neat-app/build/static/css/main.72e3d92e.css +0 -2
  105. cognite/neat/_app/ui/neat-app/build/static/css/main.72e3d92e.css.map +0 -1
  106. cognite/neat/_app/ui/neat-app/build/static/js/main.5a52cf09.js +0 -3
  107. cognite/neat/_app/ui/neat-app/build/static/js/main.5a52cf09.js.LICENSE.txt +0 -88
  108. cognite/neat/_app/ui/neat-app/build/static/js/main.5a52cf09.js.map +0 -1
  109. cognite/neat/_app/ui/neat-app/build/static/media/logo.8093b84df9ed36a174c629d6fe0b730d.svg +0 -1
  110. cognite/neat/_app/ui/neat-app/package-lock.json +0 -18306
  111. cognite/neat/_app/ui/neat-app/package.json +0 -62
  112. cognite/neat/_app/ui/neat-app/public/favicon.ico +0 -0
  113. cognite/neat/_app/ui/neat-app/public/img/architect-icon.svg +0 -116
  114. cognite/neat/_app/ui/neat-app/public/img/developer-icon.svg +0 -112
  115. cognite/neat/_app/ui/neat-app/public/img/sme-icon.svg +0 -34
  116. cognite/neat/_app/ui/neat-app/public/index.html +0 -43
  117. cognite/neat/_app/ui/neat-app/public/logo192.png +0 -0
  118. cognite/neat/_app/ui/neat-app/public/manifest.json +0 -25
  119. cognite/neat/_app/ui/neat-app/public/robots.txt +0 -3
  120. cognite/neat/_app/ui/neat-app/src/App.css +0 -38
  121. cognite/neat/_app/ui/neat-app/src/App.js +0 -17
  122. cognite/neat/_app/ui/neat-app/src/App.test.js +0 -8
  123. cognite/neat/_app/ui/neat-app/src/MainContainer.tsx +0 -70
  124. cognite/neat/_app/ui/neat-app/src/components/JsonViewer.tsx +0 -43
  125. cognite/neat/_app/ui/neat-app/src/components/LocalUploader.tsx +0 -124
  126. cognite/neat/_app/ui/neat-app/src/components/OverviewComponentEditorDialog.tsx +0 -63
  127. cognite/neat/_app/ui/neat-app/src/components/StepEditorDialog.tsx +0 -511
  128. cognite/neat/_app/ui/neat-app/src/components/TabPanel.tsx +0 -36
  129. cognite/neat/_app/ui/neat-app/src/components/Utils.tsx +0 -56
  130. cognite/neat/_app/ui/neat-app/src/components/WorkflowDeleteDialog.tsx +0 -60
  131. cognite/neat/_app/ui/neat-app/src/components/WorkflowExecutionReport.tsx +0 -112
  132. cognite/neat/_app/ui/neat-app/src/components/WorkflowImportExportDialog.tsx +0 -67
  133. cognite/neat/_app/ui/neat-app/src/components/WorkflowMetadataDialog.tsx +0 -79
  134. cognite/neat/_app/ui/neat-app/src/index.css +0 -13
  135. cognite/neat/_app/ui/neat-app/src/index.js +0 -13
  136. cognite/neat/_app/ui/neat-app/src/logo.svg +0 -1
  137. cognite/neat/_app/ui/neat-app/src/reportWebVitals.js +0 -13
  138. cognite/neat/_app/ui/neat-app/src/setupTests.js +0 -5
  139. cognite/neat/_app/ui/neat-app/src/types/WorkflowTypes.ts +0 -388
  140. cognite/neat/_app/ui/neat-app/src/views/AboutView.tsx +0 -61
  141. cognite/neat/_app/ui/neat-app/src/views/ConfigView.tsx +0 -184
  142. cognite/neat/_app/ui/neat-app/src/views/GlobalConfigView.tsx +0 -180
  143. cognite/neat/_app/ui/neat-app/src/views/WorkflowView.tsx +0 -570
  144. cognite/neat/_app/ui/neat-app/tsconfig.json +0 -27
  145. cognite/neat/_rules/transformers/_pipelines.py +0 -70
  146. cognite/neat/_workflows/__init__.py +0 -17
  147. cognite/neat/_workflows/base.py +0 -590
  148. cognite/neat/_workflows/cdf_store.py +0 -393
  149. cognite/neat/_workflows/examples/Export_DMS/workflow.yaml +0 -89
  150. cognite/neat/_workflows/examples/Export_Semantic_Data_Model/workflow.yaml +0 -66
  151. cognite/neat/_workflows/examples/Import_DMS/workflow.yaml +0 -65
  152. cognite/neat/_workflows/examples/Validate_Rules/workflow.yaml +0 -67
  153. cognite/neat/_workflows/examples/Validate_Solution_Model/workflow.yaml +0 -64
  154. cognite/neat/_workflows/manager.py +0 -292
  155. cognite/neat/_workflows/model.py +0 -203
  156. cognite/neat/_workflows/steps/__init__.py +0 -0
  157. cognite/neat/_workflows/steps/data_contracts.py +0 -109
  158. cognite/neat/_workflows/steps/lib/__init__.py +0 -0
  159. cognite/neat/_workflows/steps/lib/current/__init__.py +0 -6
  160. cognite/neat/_workflows/steps/lib/current/graph_extractor.py +0 -100
  161. cognite/neat/_workflows/steps/lib/current/graph_loader.py +0 -51
  162. cognite/neat/_workflows/steps/lib/current/graph_store.py +0 -48
  163. cognite/neat/_workflows/steps/lib/current/rules_exporter.py +0 -537
  164. cognite/neat/_workflows/steps/lib/current/rules_importer.py +0 -398
  165. cognite/neat/_workflows/steps/lib/current/rules_validator.py +0 -106
  166. cognite/neat/_workflows/steps/lib/io/__init__.py +0 -1
  167. cognite/neat/_workflows/steps/lib/io/io_steps.py +0 -393
  168. cognite/neat/_workflows/steps/step_model.py +0 -79
  169. cognite/neat/_workflows/steps_registry.py +0 -218
  170. cognite/neat/_workflows/tasks.py +0 -18
  171. cognite/neat/_workflows/triggers.py +0 -169
  172. cognite/neat/_workflows/utils.py +0 -19
  173. cognite_neat-0.103.1.dist-info/RECORD +0 -275
  174. {cognite_neat-0.103.1.dist-info → cognite_neat-0.105.0.dist-info}/LICENSE +0 -0
  175. {cognite_neat-0.103.1.dist-info → cognite_neat-0.105.0.dist-info}/entry_points.txt +0 -0
@@ -1,37 +1,251 @@
1
- from collections.abc import Iterable
1
+ import itertools
2
+ import json
3
+ from collections.abc import Callable, Iterable, Set
2
4
  from pathlib import Path
5
+ from typing import Any
3
6
 
4
7
  from cognite.client import CogniteClient
5
- from cognite.client.data_classes import Sequence, SequenceFilter, SequenceList
8
+ from cognite.client.data_classes import Sequence, SequenceFilter
9
+ from rdflib import RDF, XSD, Literal, Namespace, URIRef
6
10
 
7
- from ._base import ClassicCDFBaseExtractor, InstanceIdPrefix
11
+ from cognite.neat._client.data_classes.neat_sequence import NeatSequence, NeatSequenceList
12
+ from cognite.neat._shared import Triple
8
13
 
14
+ from ._base import DEFAULT_SKIP_METADATA_VALUES, ClassicCDFBaseExtractor, InstanceIdPrefix
9
15
 
10
- class SequencesExtractor(ClassicCDFBaseExtractor[Sequence]):
11
- """Extract data from Cognite Data Fusions Sequences into Neat."""
16
+
17
+ class SequencesExtractor(ClassicCDFBaseExtractor[NeatSequence]):
18
+ """Extract data from Cognite Data Fusions Sequences into Neat.
19
+
20
+ Args:
21
+ items (Iterable[T_CogniteResource]): An iterable of classic resource.
22
+ namespace (Namespace, optional): The namespace to use. Defaults to DEFAULT_NAMESPACE.
23
+ to_type (Callable[[T_CogniteResource], str | None], optional): A function to convert an item to a type.
24
+ Defaults to None. If None or if the function returns None, the asset will be set to the default type.
25
+ total (int, optional): The total number of items to load. If passed, you will get a progress bar if rich
26
+ is installed. Defaults to None.
27
+ limit (int, optional): The maximal number of items to load. Defaults to None. This is typically used for
28
+ testing setup of the extractor. For example, if you are extracting 100 000 assets, you might want to
29
+ limit the extraction to 1000 assets to test the setup.
30
+ unpack_metadata (bool, optional): Whether to unpack metadata. Defaults to False, which yields the metadata as
31
+ a JSON string.
32
+ skip_metadata_values (set[str] | frozenset[str] | None, optional): If you are unpacking metadata, then
33
+ values in this set will be skipped.
34
+ camel_case (bool, optional): Whether to use camelCase instead of snake_case for property names.
35
+ Defaults to True.
36
+ as_write (bool, optional): Whether to use the write/request format of the items. Defaults to False.
37
+ unpack_columns (bool, optional): Whether to unpack columns. Defaults to False.
38
+ """
12
39
 
13
40
  _default_rdf_type = "Sequence"
41
+ _column_rdf_type = "ColumnClass"
14
42
  _instance_id_prefix = InstanceIdPrefix.sequence
15
43
 
44
+ def __init__(
45
+ self,
46
+ items: Iterable[NeatSequence],
47
+ namespace: Namespace | None = None,
48
+ to_type: Callable[[NeatSequence], str | None] | None = None,
49
+ total: int | None = None,
50
+ limit: int | None = None,
51
+ unpack_metadata: bool = True,
52
+ skip_metadata_values: Set[str] | None = DEFAULT_SKIP_METADATA_VALUES,
53
+ camel_case: bool = True,
54
+ as_write: bool = False,
55
+ unpack_columns: bool = False,
56
+ ):
57
+ super().__init__(
58
+ items, namespace, to_type, total, limit, unpack_metadata, skip_metadata_values, camel_case, as_write
59
+ )
60
+ self.unpack_columns = unpack_columns
61
+
16
62
  @classmethod
17
- def _from_dataset(cls, client: CogniteClient, data_set_external_id: str) -> tuple[int | None, Iterable[Sequence]]:
63
+ def from_dataset(
64
+ cls,
65
+ client: CogniteClient,
66
+ data_set_external_id: str,
67
+ namespace: Namespace | None = None,
68
+ to_type: Callable[[NeatSequence], str | None] | None = None,
69
+ limit: int | None = None,
70
+ unpack_metadata: bool = True,
71
+ skip_metadata_values: Set[str] | None = DEFAULT_SKIP_METADATA_VALUES,
72
+ camel_case: bool = True,
73
+ as_write: bool = False,
74
+ unpack_columns: bool = False,
75
+ ):
76
+ total, items = cls._from_dataset(client, data_set_external_id)
77
+ return cls(
78
+ items,
79
+ namespace,
80
+ to_type,
81
+ total,
82
+ limit,
83
+ unpack_metadata,
84
+ skip_metadata_values,
85
+ camel_case,
86
+ as_write,
87
+ unpack_columns,
88
+ )
89
+
90
+ @classmethod
91
+ def from_hierarchy(
92
+ cls,
93
+ client: CogniteClient,
94
+ root_asset_external_id: str,
95
+ namespace: Namespace | None = None,
96
+ to_type: Callable[[NeatSequence], str | None] | None = None,
97
+ limit: int | None = None,
98
+ unpack_metadata: bool = True,
99
+ skip_metadata_values: Set[str] | None = DEFAULT_SKIP_METADATA_VALUES,
100
+ camel_case: bool = True,
101
+ as_write: bool = False,
102
+ unpack_columns: bool = False,
103
+ ):
104
+ total, items = cls._from_hierarchy(client, root_asset_external_id)
105
+ return cls(
106
+ items,
107
+ namespace,
108
+ to_type,
109
+ total,
110
+ limit,
111
+ unpack_metadata,
112
+ skip_metadata_values,
113
+ camel_case,
114
+ as_write,
115
+ unpack_columns,
116
+ )
117
+
118
+ @classmethod
119
+ def from_file(
120
+ cls,
121
+ file_path: str | Path,
122
+ namespace: Namespace | None = None,
123
+ to_type: Callable[[NeatSequence], str | None] | None = None,
124
+ limit: int | None = None,
125
+ unpack_metadata: bool = True,
126
+ skip_metadata_values: Set[str] | None = DEFAULT_SKIP_METADATA_VALUES,
127
+ camel_case: bool = True,
128
+ as_write: bool = False,
129
+ unpack_columns: bool = False,
130
+ ):
131
+ total, items = cls._from_file(file_path)
132
+ return cls(
133
+ items,
134
+ namespace,
135
+ to_type,
136
+ total,
137
+ limit,
138
+ unpack_metadata,
139
+ skip_metadata_values,
140
+ camel_case,
141
+ as_write,
142
+ unpack_columns,
143
+ )
144
+
145
+ @classmethod
146
+ def _from_dataset(
147
+ cls, client: CogniteClient, data_set_external_id: str
148
+ ) -> tuple[int | None, Iterable[NeatSequence]]:
18
149
  total = client.sequences.aggregate_count(
19
150
  filter=SequenceFilter(data_set_ids=[{"externalId": data_set_external_id}])
20
151
  )
21
152
  items = client.sequences(data_set_external_ids=data_set_external_id)
22
- return total, items
153
+ return total, cls._lookup_rows(items, client)
23
154
 
24
155
  @classmethod
25
156
  def _from_hierarchy(
26
157
  cls, client: CogniteClient, root_asset_external_id: str
27
- ) -> tuple[int | None, Iterable[Sequence]]:
158
+ ) -> tuple[int | None, Iterable[NeatSequence]]:
28
159
  total = client.sequences.aggregate_count(
29
160
  filter=SequenceFilter(asset_subtree_ids=[{"externalId": root_asset_external_id}])
30
161
  )
31
162
  items = client.sequences(asset_subtree_external_ids=[root_asset_external_id])
32
- return total, items
163
+ return total, cls._lookup_rows(items, client)
33
164
 
34
165
  @classmethod
35
- def _from_file(cls, file_path: str | Path) -> tuple[int | None, Iterable[Sequence]]:
36
- sequences = SequenceList.load(Path(file_path).read_text())
166
+ def _from_file(cls, file_path: str | Path) -> tuple[int | None, Iterable[NeatSequence]]:
167
+ sequences = NeatSequenceList.load(Path(file_path).read_text())
37
168
  return len(sequences), sequences
169
+
170
+ @classmethod
171
+ def _lookup_rows(cls, sequence_iterable: Iterable[Sequence], client: CogniteClient) -> Iterable[NeatSequence]:
172
+ iterator = iter(sequence_iterable)
173
+ for sequences in iter(lambda: list(itertools.islice(iterator, client.config.max_workers)), []):
174
+ # The PySDK uses max_workers to limit the number of requests made in parallel.
175
+ # We can only get one set of sequence rows per request, so we chunk the sequences up into groups of
176
+ # max_workers and then make a request to get all the rows for those sequences in one go.
177
+ sequence_list = list(sequences)
178
+ row_list = client.sequences.rows.retrieve(id=[seq.id for seq in sequence_list])
179
+ rows_by_sequence_id = {row.id: row.rows for row in row_list}
180
+ for seq in sequence_list:
181
+ yield NeatSequence.from_cognite_sequence(seq, rows_by_sequence_id.get(seq.id))
182
+
183
+ def _item2triples_special_cases(self, id_: URIRef, dumped: dict[str, Any]) -> list[Triple]:
184
+ """For sequences, columns and rows are special cases.'"""
185
+ if self.unpack_columns:
186
+ return self._unpack_columns(id_, dumped)
187
+ else:
188
+ return self._default_columns_and_rows(id_, dumped)
189
+
190
+ def _default_columns_and_rows(self, id_: URIRef, dumped: dict[str, Any]) -> list[Triple]:
191
+ triples: list[Triple] = []
192
+ if "columns" in dumped:
193
+ columns = dumped.pop("columns")
194
+ triples.extend(
195
+ [
196
+ (
197
+ id_,
198
+ self.namespace.columns,
199
+ # Rows have a rowNumber, so we introduce colNumber here to be consistent.
200
+ Literal(json.dumps({"colNumber": no, **col}), datatype=XSD._NS["json"]),
201
+ )
202
+ for no, col in enumerate(columns, 1)
203
+ ]
204
+ )
205
+ if "rows" in dumped:
206
+ rows = dumped.pop("rows")
207
+ triples.extend(
208
+ [(id_, self.namespace.rows, Literal(json.dumps(row), datatype=XSD._NS["json"])) for row in rows]
209
+ )
210
+ return triples
211
+
212
+ def _unpack_columns(self, id_: URIRef, dumped: dict[str, Any]) -> list[Triple]:
213
+ triples: list[Triple] = []
214
+ columnValueTypes: list[str] = []
215
+ column_order: list[str] = []
216
+ if columns := dumped.pop("columns", None):
217
+ for col in columns:
218
+ external_id = col.pop("externalId")
219
+ column_order.append(external_id)
220
+ value_type = col.pop("valueType")
221
+ columnValueTypes.append(value_type)
222
+
223
+ col_id = self.namespace[f"Column_{external_id}"]
224
+ triples.append((id_, self.namespace[external_id], col_id))
225
+ type_ = self.namespace[self._column_rdf_type]
226
+ triples.append((col_id, RDF.type, type_))
227
+ if metadata := col.pop("metadata", None):
228
+ triples.extend(self._metadata_to_triples(col_id, metadata))
229
+ # Should only be name and description left in col
230
+ for key, value in col.items():
231
+ if value is None:
232
+ continue
233
+ triples.append((col_id, self.namespace[key], Literal(value, datatype=XSD.string)))
234
+
235
+ triples.append(
236
+ (id_, self.namespace.columnOrder, Literal(json.dumps(column_order), datatype=XSD._NS["json"]))
237
+ )
238
+ triples.append(
239
+ (id_, self.namespace.columnValueTypes, Literal(json.dumps(columnValueTypes), datatype=XSD._NS["json"]))
240
+ )
241
+ if rows := dumped.pop("rows", None):
242
+ values_by_column: list[list[Any]] = [[] for _ in column_order]
243
+ for row in rows:
244
+ for i, value in enumerate(row["values"]):
245
+ values_by_column[i].append(value)
246
+ for col_name, values in zip(column_order, values_by_column, strict=False):
247
+ triples.append(
248
+ (id_, self.namespace[f"{col_name}Values"], Literal(json.dumps(values), datatype=XSD._NS["json"]))
249
+ )
250
+
251
+ return triples
@@ -46,7 +46,7 @@ class MockGraphGenerator(BaseExtractor):
46
46
  # fixes potential issues with circular dependencies
47
47
  from cognite.neat._rules.transformers import DMSToInformation
48
48
 
49
- self.rules = DMSToInformation().transform(rules).rules
49
+ self.rules = DMSToInformation().transform(rules)
50
50
  elif isinstance(rules, InformationRules):
51
51
  self.rules = rules
52
52
  else:
@@ -183,7 +183,7 @@ def _get_generation_order(
183
183
  parent_col: str = "source_class",
184
184
  child_col: str = "target_class",
185
185
  ) -> dict:
186
- parent_child_list: list[list[str]] = class_linkage[[parent_col, child_col]].values.tolist()
186
+ parent_child_list: list[list[str]] = class_linkage[[parent_col, child_col]].values.tolist() # type: ignore[assignment]
187
187
  # Build a directed graph and a list of all names that have no parent
188
188
  graph: dict[str, set] = {name: set() for tup in parent_child_list for name in tup}
189
189
  has_parent: dict[str, bool] = {name: False for tup in parent_child_list for name in tup}
@@ -19,6 +19,7 @@ from pydantic import BaseModel, ValidationInfo, create_model, field_validator
19
19
  from rdflib import RDF, URIRef
20
20
 
21
21
  from cognite.neat._client import NeatClient
22
+ from cognite.neat._constants import is_readonly_property
22
23
  from cognite.neat._graph._tracking import LogTracker, Tracker
23
24
  from cognite.neat._issues import IssueList, NeatIssue, NeatIssueList
24
25
  from cognite.neat._issues.errors import (
@@ -303,6 +304,9 @@ class DMSLoader(CDFLoader[dm.InstanceApply]):
303
304
  if isinstance(prop, dm.EdgeConnection):
304
305
  edge_by_property[prop_id] = prop_id, prop
305
306
  if isinstance(prop, dm.MappedProperty):
307
+ if is_readonly_property(prop.container, prop.container_property_identifier):
308
+ continue
309
+
306
310
  if isinstance(prop.type, dm.DirectRelation):
307
311
  if prop.container == dm.ContainerId("cdf_cdm", "CogniteTimeSeries") and prop_id == "unit":
308
312
  unit_properties.append(prop_id)
@@ -343,9 +347,14 @@ class DMSLoader(CDFLoader[dm.InstanceApply]):
343
347
 
344
348
  return value
345
349
 
346
- def parse_json_string(cls, value: Any, info: ValidationInfo) -> dict:
350
+ def parse_json_string(cls, value: Any, info: ValidationInfo) -> dict | list:
347
351
  if isinstance(value, dict):
348
352
  return value
353
+ elif isinstance(value, list):
354
+ try:
355
+ return [json.loads(v) if isinstance(v, str) else v for v in value]
356
+ except json.JSONDecodeError as error:
357
+ raise ValueError(f"Not valid JSON string for {info.field_name}: {value}, error {error}") from error
349
358
  elif isinstance(value, str):
350
359
  try:
351
360
  return json.loads(value)
@@ -401,7 +410,9 @@ class DMSLoader(CDFLoader[dm.InstanceApply]):
401
410
  space=self.instance_space,
402
411
  external_id=identifier,
403
412
  type=(dm.DirectRelationReference(view_id.space, view_id.external_id) if type_ is not None else None),
404
- sources=[dm.NodeOrEdgeData(source=view_id, properties=dict(created.model_dump().items()))],
413
+ sources=[
414
+ dm.NodeOrEdgeData(source=view_id, properties=dict(created.model_dump(exclude_unset=True).items()))
415
+ ],
405
416
  )
406
417
 
407
418
  def _create_edges(
@@ -15,7 +15,7 @@ from ._prune_graph import (
15
15
  PruneTypes,
16
16
  )
17
17
  from ._rdfpath import AddSelfReferenceProperty, MakeConnectionOnExactMatch
18
- from ._value_type import ConvertLiteral, LiteralToEntity, SplitMultiValueProperty
18
+ from ._value_type import ConnectionToLiteral, ConvertLiteral, LiteralToEntity, SplitMultiValueProperty
19
19
 
20
20
  __all__ = [
21
21
  "AddAssetDepth",
@@ -26,6 +26,7 @@ __all__ = [
26
26
  "AssetSequenceConnector",
27
27
  "AssetTimeSeriesConnector",
28
28
  "AttachPropertyFromTargetToSource",
29
+ "ConnectionToLiteral",
29
30
  "ConvertLiteral",
30
31
  "LiteralToEntity",
31
32
  "MakeConnectionOnExactMatch",
@@ -55,4 +56,5 @@ Transformers = (
55
56
  | PruneInstancesOfUnknownType
56
57
  | ConvertLiteral
57
58
  | LiteralToEntity
59
+ | ConnectionToLiteral
58
60
  )
@@ -1,7 +1,27 @@
1
+ import dataclasses
2
+ import warnings
1
3
  from abc import ABC, abstractmethod
2
- from typing import ClassVar
4
+ from typing import ClassVar, TypeAlias, cast
3
5
 
4
6
  from rdflib import Graph
7
+ from rdflib.query import ResultRow
8
+
9
+ from cognite.neat._issues.warnings import NeatValueWarning
10
+ from cognite.neat._shared import Triple
11
+ from cognite.neat._utils.collection_ import iterate_progress_bar
12
+ from cognite.neat._utils.graph_transformations_report import GraphTransformationResult
13
+
14
+ To_Add_Triples: TypeAlias = list[Triple]
15
+ To_Remove_Triples: TypeAlias = list[Triple]
16
+
17
+
18
+ @dataclasses.dataclass
19
+ class RowTransformationOutput:
20
+ remove_triples: To_Remove_Triples = dataclasses.field(default_factory=list)
21
+ add_triples: To_Add_Triples = dataclasses.field(default_factory=list)
22
+ instances_removed_count: int = 0
23
+ instances_added_count: int = 0
24
+ instances_modified_count: int = 0
5
25
 
6
26
 
7
27
  class BaseTransformer(ABC):
@@ -12,3 +32,91 @@ class BaseTransformer(ABC):
12
32
  @abstractmethod
13
33
  def transform(self, graph: Graph) -> None:
14
34
  raise NotImplementedError()
35
+
36
+
37
+ class BaseTransformerStandardised(ABC):
38
+ """Standardised base transformer to use in case a transformer is adding or removing triples from a graph. If you
39
+ are doing more specialised operations, please overwrite the .transform() method.
40
+ """
41
+
42
+ description: str
43
+ _use_only_once: bool = False
44
+ _need_changes: ClassVar[frozenset[str]] = frozenset()
45
+ _use_iterate_bar_threshold: int = 500
46
+
47
+ @abstractmethod
48
+ def operation(self, query_result_row: ResultRow) -> RowTransformationOutput:
49
+ """The operations to perform on each row resulting from the ._iterate_query() method.
50
+ The operation should return a list of triples to add and to remove.
51
+ """
52
+ raise NotImplementedError()
53
+
54
+ @abstractmethod
55
+ def _count_query(self) -> str:
56
+ """
57
+ Overwrite to fetch all affected properties in the graph as a result of the transformation.
58
+ Returns:
59
+ A query string.
60
+ """
61
+ raise NotImplementedError()
62
+
63
+ @abstractmethod
64
+ def _iterate_query(self) -> str:
65
+ """
66
+ The query to use for extracting target triples from the graph and performing the transformation.
67
+ Returns:
68
+ A query string.
69
+ """
70
+ raise NotImplementedError()
71
+
72
+ def _skip_count_query(self) -> str:
73
+ """
74
+ The query to use for extracting target triples from the graph and performing the transformation.
75
+ Returns:
76
+ A query string.
77
+ """
78
+ return ""
79
+
80
+ def transform(self, graph: Graph) -> GraphTransformationResult:
81
+ outcome = GraphTransformationResult(self.__class__.__name__)
82
+ outcome.added = outcome.modified = outcome.removed = 0
83
+
84
+ iteration_count_res = list(graph.query(self._count_query()))
85
+ iteration_count = int(iteration_count_res[0][0]) # type: ignore [index, arg-type]
86
+
87
+ outcome.affected_nodes_count = iteration_count
88
+
89
+ if self._skip_count_query():
90
+ skipped_count_res = list(graph.query(self._skip_count_query()))
91
+ skipped_count = int(skipped_count_res[0][0]) # type: ignore [index, arg-type]
92
+ warnings.warn(
93
+ NeatValueWarning(f"Skipping {skipped_count} properties in transformation {self.__class__.__name__}"),
94
+ stacklevel=2,
95
+ )
96
+ outcome.skipped = skipped_count
97
+
98
+ if iteration_count == 0:
99
+ return outcome
100
+
101
+ result_iterable = graph.query(self._iterate_query())
102
+ if iteration_count > self._use_iterate_bar_threshold:
103
+ result_iterable = iterate_progress_bar( # type: ignore[misc, assignment]
104
+ result_iterable,
105
+ total=iteration_count,
106
+ description=self.description,
107
+ )
108
+
109
+ for row in result_iterable:
110
+ row = cast(ResultRow, row)
111
+ row_output = self.operation(row)
112
+
113
+ outcome.added += row_output.instances_added_count
114
+ outcome.removed += row_output.instances_removed_count
115
+ outcome.modified += row_output.instances_modified_count
116
+
117
+ for triple in row_output.add_triples:
118
+ graph.add(triple)
119
+ for triple in row_output.remove_triples:
120
+ graph.remove(triple)
121
+
122
+ return outcome
@@ -21,6 +21,7 @@ from cognite.neat._utils.rdf_ import (
21
21
  from ._base import BaseTransformer
22
22
 
23
23
 
24
+ # TODO: standardise
24
25
  class AddAssetDepth(BaseTransformer):
25
26
  description: str = "Adds depth of asset in the asset hierarchy to the graph"
26
27
  _use_only_once: bool = True
@@ -84,6 +85,7 @@ class AddAssetDepth(BaseTransformer):
84
85
  return None
85
86
 
86
87
 
88
+ # TODO: standardise
87
89
  class BaseAssetConnector(BaseTransformer, ABC):
88
90
  _asset_type: URIRef = DEFAULT_NAMESPACE.Asset
89
91
  _item_type: URIRef
@@ -166,6 +168,7 @@ class AssetEventConnector(BaseAssetConnector):
166
168
  _connection_type = DEFAULT_NAMESPACE.event
167
169
 
168
170
 
171
+ # TODO: standardise
169
172
  class AssetRelationshipConnector(BaseTransformer):
170
173
  description: str = "Connects assets via relationships"
171
174
  _use_only_once: bool = True
@@ -242,6 +245,7 @@ class AssetRelationshipConnector(BaseTransformer):
242
245
  graph.remove((relationship_id, self.relationship_target_xid_prop, None))
243
246
 
244
247
 
248
+ # TODO: standardise
245
249
  class RelationshipAsEdgeTransformer(BaseTransformer):
246
250
  """Converts relationships into edges in the graph.
247
251
 
@@ -371,7 +375,8 @@ WHERE {{
371
375
  ) -> list[Triple]:
372
376
  relationship_triples = cast(list[Triple], list(graph.query(f"DESCRIBE <{relationship_id}>")))
373
377
  object_by_predicates = cast(
374
- dict[str, URIRef | Literal], {remove_namespace_from_uri(row[1]): row[2] for row in relationship_triples}
378
+ dict[str, URIRef | Literal],
379
+ {remove_namespace_from_uri(row[1]): row[2] for row in relationship_triples if row[1] != RDF.type},
375
380
  )
376
381
  source_external_id = cast(URIRef, object_by_predicates["sourceExternalId"])
377
382
  target_source_id = cast(URIRef, object_by_predicates["targetExternalId"])