cognite-neat 0.106.0__py3-none-any.whl → 0.108.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 (67) hide show
  1. cognite/neat/_constants.py +35 -1
  2. cognite/neat/_graph/_shared.py +4 -0
  3. cognite/neat/_graph/extractors/__init__.py +5 -1
  4. cognite/neat/_graph/extractors/_base.py +32 -0
  5. cognite/neat/_graph/extractors/_classic_cdf/_base.py +128 -14
  6. cognite/neat/_graph/extractors/_classic_cdf/_classic.py +156 -12
  7. cognite/neat/_graph/extractors/_classic_cdf/_relationships.py +50 -12
  8. cognite/neat/_graph/extractors/_classic_cdf/_sequences.py +26 -1
  9. cognite/neat/_graph/extractors/_dms.py +196 -47
  10. cognite/neat/_graph/extractors/_dms_graph.py +199 -0
  11. cognite/neat/_graph/extractors/_mock_graph_generator.py +1 -1
  12. cognite/neat/_graph/extractors/_rdf_file.py +33 -5
  13. cognite/neat/_graph/loaders/__init__.py +1 -3
  14. cognite/neat/_graph/loaders/_rdf2dms.py +123 -19
  15. cognite/neat/_graph/queries/_base.py +140 -84
  16. cognite/neat/_graph/queries/_construct.py +2 -2
  17. cognite/neat/_graph/transformers/__init__.py +8 -1
  18. cognite/neat/_graph/transformers/_base.py +9 -1
  19. cognite/neat/_graph/transformers/_classic_cdf.py +90 -3
  20. cognite/neat/_graph/transformers/_rdfpath.py +3 -3
  21. cognite/neat/_graph/transformers/_value_type.py +106 -45
  22. cognite/neat/_issues/errors/_resources.py +1 -1
  23. cognite/neat/_issues/warnings/__init__.py +0 -2
  24. cognite/neat/_issues/warnings/_models.py +1 -1
  25. cognite/neat/_issues/warnings/_properties.py +0 -8
  26. cognite/neat/_rules/analysis/_base.py +1 -1
  27. cognite/neat/_rules/analysis/_information.py +14 -13
  28. cognite/neat/_rules/catalog/__init__.py +1 -0
  29. cognite/neat/_rules/catalog/classic_model.xlsx +0 -0
  30. cognite/neat/_rules/catalog/info-rules-imf.xlsx +0 -0
  31. cognite/neat/_rules/exporters/_rules2instance_template.py +3 -3
  32. cognite/neat/_rules/importers/__init__.py +3 -1
  33. cognite/neat/_rules/importers/_dms2rules.py +7 -5
  34. cognite/neat/_rules/importers/_dtdl2rules/spec.py +1 -2
  35. cognite/neat/_rules/importers/_rdf/__init__.py +2 -2
  36. cognite/neat/_rules/importers/_rdf/_base.py +2 -2
  37. cognite/neat/_rules/importers/_rdf/_inference2rules.py +242 -19
  38. cognite/neat/_rules/models/_base_rules.py +13 -15
  39. cognite/neat/_rules/models/_types.py +5 -0
  40. cognite/neat/_rules/models/dms/_rules.py +51 -10
  41. cognite/neat/_rules/models/dms/_rules_input.py +4 -0
  42. cognite/neat/_rules/models/information/_rules.py +48 -5
  43. cognite/neat/_rules/models/information/_rules_input.py +6 -1
  44. cognite/neat/_rules/models/mapping/_classic2core.py +4 -5
  45. cognite/neat/_rules/transformers/__init__.py +10 -0
  46. cognite/neat/_rules/transformers/_converters.py +300 -62
  47. cognite/neat/_session/_base.py +57 -10
  48. cognite/neat/_session/_drop.py +5 -1
  49. cognite/neat/_session/_inspect.py +3 -2
  50. cognite/neat/_session/_mapping.py +17 -6
  51. cognite/neat/_session/_prepare.py +0 -47
  52. cognite/neat/_session/_read.py +115 -10
  53. cognite/neat/_session/_set.py +27 -0
  54. cognite/neat/_session/_show.py +4 -4
  55. cognite/neat/_session/_state.py +12 -1
  56. cognite/neat/_session/_to.py +43 -2
  57. cognite/neat/_session/_wizard.py +1 -1
  58. cognite/neat/_session/exceptions.py +8 -3
  59. cognite/neat/_store/_graph_store.py +331 -136
  60. cognite/neat/_store/_rules_store.py +130 -1
  61. cognite/neat/_utils/auth.py +3 -1
  62. cognite/neat/_version.py +1 -1
  63. {cognite_neat-0.106.0.dist-info → cognite_neat-0.108.0.dist-info}/METADATA +2 -2
  64. {cognite_neat-0.106.0.dist-info → cognite_neat-0.108.0.dist-info}/RECORD +67 -65
  65. {cognite_neat-0.106.0.dist-info → cognite_neat-0.108.0.dist-info}/WHEEL +1 -1
  66. {cognite_neat-0.106.0.dist-info → cognite_neat-0.108.0.dist-info}/LICENSE +0 -0
  67. {cognite_neat-0.106.0.dist-info → cognite_neat-0.108.0.dist-info}/entry_points.txt +0 -0
@@ -31,7 +31,11 @@ class DropAPI:
31
31
  ```
32
32
  """
33
33
  type_list = type if isinstance(type, list) else [type]
34
- uri_type_type = dict((v, k) for k, v in self._state.instances.store.queries.types.items())
34
+
35
+ # Temporary solution until we agree on the form of specifying named graphs
36
+ # it will default to the default named graph
37
+ named_graph = self._state.instances.store.default_named_graph
38
+ uri_type_type = dict((v, k) for k, v in self._state.instances.store.queries.types(named_graph).items())
35
39
  selected_uri_by_type: dict[URIRef, str] = {}
36
40
  for type_item in type_list:
37
41
  if type_item not in uri_type_type:
@@ -94,7 +94,8 @@ class InspectIssues:
94
94
  if self._state.rule_store.provenance:
95
95
  issues = self._state.rule_store.last_issues
96
96
  elif self._state.instances.store.provenance:
97
- issues = self._state.instances.store.provenance[-1].target_entity.issues
97
+ last_change = self._state.instances.store.provenance[-1]
98
+ issues = last_change.target_entity.issues
98
99
  else:
99
100
  self._print("No issues found.")
100
101
  return pd.DataFrame() if return_dataframe else None
@@ -231,7 +232,7 @@ class InspectUploadOutcome:
231
232
  if i < 50:
232
233
  lines.append(f" * {v}")
233
234
  elif i == 50 and total > 50:
234
- lines.append(f" * ... {total-50} more")
235
+ lines.append(f" * ... {total - 50} more")
235
236
  elif i == 50 and total == 50:
236
237
  lines.append(f" * {v}")
237
238
  else:
@@ -1,7 +1,13 @@
1
1
  from cognite.neat._issues import IssueList
2
2
  from cognite.neat._rules.models import DMSRules
3
3
  from cognite.neat._rules.models.mapping import load_classic_to_core_mapping
4
- from cognite.neat._rules.transformers import AsParentPropertyId, IncludeReferenced, RuleMapper
4
+ from cognite.neat._rules.transformers import (
5
+ AsParentPropertyId,
6
+ ChangeViewPrefix,
7
+ IncludeReferenced,
8
+ RuleMapper,
9
+ RulesTransformer,
10
+ )
5
11
 
6
12
  from ._state import SessionState
7
13
  from .exceptions import NeatSessionError, session_class_wrapper
@@ -18,7 +24,7 @@ class DataModelMappingAPI:
18
24
  def __init__(self, state: SessionState):
19
25
  self._state = state
20
26
 
21
- def classic_to_core(self, company_prefix: str, use_parent_property_name: bool = True) -> IssueList:
27
+ def classic_to_core(self, company_prefix: str | None = None, use_parent_property_name: bool = True) -> IssueList:
22
28
  """Map classic types to core types.
23
29
 
24
30
  Note this automatically creates an extended CogniteCore model.
@@ -45,10 +51,15 @@ class DataModelMappingAPI:
45
51
  if self._state.client is None:
46
52
  raise NeatSessionError("Client is required to map classic to core")
47
53
 
48
- transformers = [
49
- RuleMapper(load_classic_to_core_mapping(company_prefix, rules.metadata.space, rules.metadata.version)),
50
- IncludeReferenced(self._state.client),
51
- ]
54
+ transformers: list[RulesTransformer] = []
55
+ if company_prefix:
56
+ transformers.append(ChangeViewPrefix("Classic", company_prefix))
57
+ transformers.extend(
58
+ [
59
+ RuleMapper(load_classic_to_core_mapping(company_prefix, rules.metadata.space, rules.metadata.version)),
60
+ IncludeReferenced(self._state.client),
61
+ ]
62
+ )
52
63
  if use_parent_property_name:
53
64
  transformers.append(AsParentPropertyId(self._state.client))
54
65
  return self._state.rule_transform(*transformers)
@@ -7,7 +7,6 @@ from rdflib import URIRef
7
7
  from cognite.neat._constants import (
8
8
  get_default_prefixes_and_namespaces,
9
9
  )
10
- from cognite.neat._graph import extractors
11
10
  from cognite.neat._graph.transformers import (
12
11
  AttachPropertyFromTargetToSource,
13
12
  ConnectionToLiteral,
@@ -24,7 +23,6 @@ from cognite.neat._issues import IssueList
24
23
  from cognite.neat._issues.errors import NeatValueError
25
24
  from cognite.neat._rules.models.dms import DMSValidation
26
25
  from cognite.neat._rules.transformers import (
27
- AddClassImplements,
28
26
  IncludeReferenced,
29
27
  PrefixEntities,
30
28
  ReduceCogniteModel,
@@ -348,41 +346,6 @@ class InstancePrepareAPI:
348
346
  transformer = ConnectionToLiteral(subject_type, subject_predicate)
349
347
  self._state.instances.store.transform(transformer)
350
348
 
351
- def classic_to_core(self) -> None:
352
- """Prepares extracted CDF classic graph for the Core Data model.
353
-
354
- !!! note "This method bundles several graph transformers which"
355
- - Convert relationships to edges
356
- - Convert TimeSeries.type from bool to enum
357
- - Convert all properties 'source' to a connection to SourceSystem
358
- - Convert all properties 'labels' from a connection to a string
359
-
360
- Example:
361
- Apply classic to core transformations:
362
- ```python
363
- neat.prepare.instances.classic_to_core()
364
- ```
365
- """
366
- self.relationships_as_edges()
367
- self.convert_data_type(
368
- ("TimeSeries", "isString"), convert=lambda is_string: "string" if is_string else "numeric"
369
- )
370
- self.property_to_type((None, "source"), "SourceSystem", "name")
371
- for type_ in [
372
- extractors.EventsExtractor._default_rdf_type,
373
- extractors.AssetsExtractor._default_rdf_type,
374
- extractors.FilesExtractor._default_rdf_type,
375
- ]:
376
- try:
377
- subject_type, subject_predicate = self._get_type_and_property_uris(type_, "labels")
378
- except NeatValueError:
379
- # If the type_.labels does not exist, continue. This is not an error, it just means that the
380
- # Labels is not used in the graph for that type.
381
- continue
382
- else:
383
- transformer = ConnectionToLiteral(subject_type, subject_predicate)
384
- self._state.instances.store.transform(transformer)
385
-
386
349
 
387
350
  @session_class_wrapper
388
351
  class DataModelPrepareAPI:
@@ -545,13 +508,3 @@ class DataModelPrepareAPI:
545
508
  "Please set the client in the session, NeatSession(client=client)."
546
509
  )
547
510
  return self._state.rule_transform(IncludeReferenced(self._state.client))
548
-
549
- def add_implements_to_classes(self, suffix: Literal["Edge"], implements: str = "Edge") -> IssueList:
550
- """All classes with the suffix will have the implements property set to the given value.
551
-
552
- Args:
553
- suffix: The suffix of the classes to add the implements property to.
554
- implements: The value of the implements property to set.
555
-
556
- """
557
- return self._state.rule_transform(AddClassImplements(implements, suffix))
@@ -1,15 +1,19 @@
1
1
  from typing import Any, Literal, cast
2
2
 
3
3
  from cognite.client.data_classes.data_modeling import DataModelId, DataModelIdentifier
4
+ from cognite.client.utils.useful_types import SequenceNotStr
4
5
 
5
6
  from cognite.neat._client import NeatClient
7
+ from cognite.neat._constants import CLASSIC_CDF_NAMESPACE
6
8
  from cognite.neat._graph import examples as instances_examples
7
9
  from cognite.neat._graph import extractors
10
+ from cognite.neat._graph.transformers import ConvertLiteral, LiteralToEntity
8
11
  from cognite.neat._issues import IssueList
9
12
  from cognite.neat._issues.errors import NeatValueError
10
13
  from cognite.neat._issues.warnings import MissingCogniteClientWarning
11
14
  from cognite.neat._rules import catalog, importers
12
15
  from cognite.neat._rules.importers import BaseImporter
16
+ from cognite.neat._rules.transformers import ClassicPrepareCore
13
17
  from cognite.neat._utils.reader import NeatReader
14
18
 
15
19
  from ._state import SessionState
@@ -32,6 +36,22 @@ class ReadAPI:
32
36
  self.yaml = YamlReadAPI(state, verbose)
33
37
  self.xml = XMLReadAPI(state, verbose)
34
38
 
39
+ def session(self, io: Any) -> None:
40
+ """Reads a Neat Session from a zip file.
41
+
42
+ Args:
43
+ io: file path to the Neat Session
44
+
45
+ Example:
46
+ ```python
47
+ neat.read.session("path_to_neat_session")
48
+ ```
49
+ """
50
+ reader = NeatReader.create(io)
51
+ path = reader.materialize_path()
52
+
53
+ self._state.instances.store.write(extractors.RdfFileExtractor.from_zip(path))
54
+
35
55
 
36
56
  @session_class_wrapper
37
57
  class BaseReadAPI:
@@ -78,6 +98,45 @@ class CDFReadAPI(BaseReadAPI):
78
98
  importer = importers.DMSImporter.from_data_model_id(self._get_client, data_model_id)
79
99
  return self._state.rule_import(importer)
80
100
 
101
+ def graph(
102
+ self,
103
+ data_model_id: DataModelIdentifier,
104
+ instance_space: str | SequenceNotStr[str] | None = None,
105
+ skip_cognite_views: bool = True,
106
+ ) -> IssueList:
107
+ """Reads a knowledge graph from Cognite Data Fusion (CDF).
108
+
109
+ Args:
110
+ data_model_id: Tuple of strings with the id of a CDF Data Model.
111
+ instance_space: The instance spaces to extract. If None, all instance spaces are extracted.
112
+ skip_cognite_views: If True, all Cognite Views are skipped. For example, if you have the CogniteAsset
113
+ view in you data model, it will ont be used to extract instances.
114
+
115
+ Returns:
116
+ IssueList: A list of issues that occurred during the extraction.
117
+
118
+ """
119
+ return self._graph(data_model_id, instance_space, skip_cognite_views, unpack_json=False)
120
+
121
+ def _graph(
122
+ self,
123
+ data_model_id: DataModelIdentifier,
124
+ instance_space: str | SequenceNotStr[str] | None = None,
125
+ skip_cognite_views: bool = True,
126
+ unpack_json: bool = False,
127
+ str_to_ideal_type: bool = False,
128
+ ) -> IssueList:
129
+ extractor = extractors.DMSGraphExtractor.from_data_model_id(
130
+ # We are skipping the Cognite Views
131
+ data_model_id,
132
+ self._get_client,
133
+ instance_space=instance_space,
134
+ skip_cognite_views=skip_cognite_views,
135
+ unpack_json=unpack_json,
136
+ str_to_ideal_type=str_to_ideal_type,
137
+ )
138
+ return self._state.write_graph(extractor)
139
+
81
140
 
82
141
  @session_class_wrapper
83
142
  class CDFClassicAPI(BaseReadAPI):
@@ -92,7 +151,12 @@ class CDFClassicAPI(BaseReadAPI):
92
151
  raise ValueError("No client provided. Please provide a client to read a data model.")
93
152
  return self._state.client
94
153
 
95
- def graph(self, root_asset_external_id: str, limit_per_type: int | None = None) -> IssueList:
154
+ def graph(
155
+ self,
156
+ root_asset_external_id: str,
157
+ limit_per_type: int | None = None,
158
+ identifier: Literal["id", "externalId"] = "id",
159
+ ) -> IssueList:
96
160
  """Reads the classic knowledge graph from CDF.
97
161
 
98
162
  The Classic Graph consists of the following core resource type.
@@ -127,6 +191,8 @@ class CDFClassicAPI(BaseReadAPI):
127
191
  Args:
128
192
  root_asset_external_id: The external id of the root asset
129
193
  limit_per_type: The maximum number of nodes to extract per core node type. If None, all nodes are extracted.
194
+ identifier: The identifier to use for the core nodes. Note selecting "id" can cause issues if the external
195
+ ID of the core nodes is missing. Default is "id".
130
196
 
131
197
  Returns:
132
198
  IssueList: A list of issues that occurred during the extraction.
@@ -135,17 +201,58 @@ class CDFClassicAPI(BaseReadAPI):
135
201
  ```python
136
202
  neat.read.cdf.graph("root_asset_external_id")
137
203
  ```
138
-
139
204
  """
140
- extractor = extractors.ClassicGraphExtractor(
141
- self._get_client, root_asset_external_id=root_asset_external_id, limit_per_type=limit_per_type
205
+ return self._graph(
206
+ root_asset_external_id, limit_per_type, identifier, reference_timeseries=False, reference_files=False
142
207
  )
143
208
 
144
- issues = self._state.instances.store.write(extractor)
145
- issues.action = "Read Classic Graph"
146
- if issues:
209
+ def _graph(
210
+ self,
211
+ root_asset_external_id: str,
212
+ limit_per_type: int | None = None,
213
+ identifier: Literal["id", "externalId"] = "id",
214
+ reference_timeseries: bool = False,
215
+ reference_files: bool = False,
216
+ ) -> IssueList:
217
+ namespace = CLASSIC_CDF_NAMESPACE
218
+ extractor = extractors.ClassicGraphExtractor(
219
+ self._get_client,
220
+ root_asset_external_id=root_asset_external_id,
221
+ limit_per_type=limit_per_type,
222
+ namespace=namespace,
223
+ prefix="Classic",
224
+ identifier=identifier,
225
+ )
226
+ extract_issues = self._state.write_graph(extractor)
227
+ if identifier == "externalId":
228
+ self._state.quoted_source_identifiers = True
229
+
230
+ self._state.instances.store.transform(
231
+ ConvertLiteral(
232
+ namespace["ClassicTimeSeries"],
233
+ namespace["isString"],
234
+ lambda is_string: "string" if is_string else "numeric",
235
+ )
236
+ )
237
+ self._state.instances.store.transform(
238
+ LiteralToEntity(None, namespace["source"], "ClassicSourceSystem", "name"),
239
+ )
240
+ # Updating the information model.
241
+ prepare_issues = self._state.rule_store.transform(
242
+ ClassicPrepareCore(namespace, reference_timeseries, reference_files)
243
+ )
244
+ # Update the instance store with the latest rules
245
+ information_rules = self._state.rule_store.last_verified_information_rules
246
+ self._state.instances.store.rules[self._state.instances.store.default_named_graph] = information_rules
247
+
248
+ all_issues = IssueList(extract_issues + prepare_issues)
249
+ # Update the provenance with all issue.
250
+ object.__setattr__(self._state.instances.store.provenance[-1].target_entity, "issues", all_issues)
251
+ all_issues.action = "Read Classic Graph"
252
+ if all_issues:
147
253
  print("Use the .inspect.issues() for more details")
148
- return issues
254
+
255
+ return all_issues
149
256
 
150
257
 
151
258
  @session_class_wrapper
@@ -182,7 +289,6 @@ class ExcelReadAPI(BaseReadAPI):
182
289
  class ExcelExampleAPI(BaseReadAPI):
183
290
  """Used as example for reading some data model into the NeatSession."""
184
291
 
185
- @property
186
292
  def pump_example(self) -> IssueList:
187
293
  """Reads the Hello World pump example into the NeatSession."""
188
294
  importer: importers.ExcelImporter = importers.ExcelImporter(catalog.hello_world_pump)
@@ -396,7 +502,6 @@ class RDFExamples:
396
502
  def __init__(self, state: SessionState) -> None:
397
503
  self._state = state
398
504
 
399
- @property
400
505
  def nordic44(self) -> IssueList:
401
506
  """Reads the Nordic 44 knowledge graph into the NeatSession graph store."""
402
507
  self._state.instances.store.write(extractors.RdfFileExtractor(instances_examples.nordic44_knowledge_graph))
@@ -3,9 +3,12 @@ from cognite.client import data_modeling as dm
3
3
 
4
4
  from cognite.neat._client import NeatClient
5
5
  from cognite.neat._constants import COGNITE_MODELS
6
+ from cognite.neat._graph.transformers import SetNeatType
6
7
  from cognite.neat._issues import IssueList
8
+ from cognite.neat._issues.errors import NeatValueError
7
9
  from cognite.neat._rules.models import DMSRules
8
10
  from cognite.neat._rules.transformers import SetIDDMSModel
11
+ from cognite.neat._utils.text import humanize_collection
9
12
 
10
13
  from ._state import SessionState
11
14
  from .exceptions import NeatSessionError, session_class_wrapper
@@ -44,3 +47,27 @@ class SetAPI:
44
47
  if self._verbose:
45
48
  print(f"Client set to {self._state.client.config.project} CDF project.")
46
49
  return None
50
+
51
+ def _instance_sub_type(self, type: str, property: str, drop_property: bool = False) -> None:
52
+ """Sets the sub type of an instance based on the property."""
53
+ type_uri = self._state.instances.store.queries.type_uri(type)
54
+ property_uri = self._state.instances.store.queries.property_uri(property)
55
+
56
+ if not type_uri:
57
+ raise NeatValueError(f"Type {type} does not exist in the graph.")
58
+ elif len(type_uri) > 1:
59
+ raise NeatValueError(f"{type} has multiple ids found in the graph: {humanize_collection(type_uri)}.")
60
+
61
+ if not property_uri:
62
+ raise NeatValueError(f"Property {property} does not exist in the graph.")
63
+ elif len(type_uri) > 1:
64
+ raise NeatValueError(
65
+ f"{property} has multiple ids found in the graph: {humanize_collection(property_uri)}."
66
+ )
67
+
68
+ if not self._state.instances.store.queries.type_with_property(type_uri[0], property_uri[0]):
69
+ raise NeatValueError(f"Property {property} is not defined for type {type}.")
70
+
71
+ self._state.instances.store.transform(SetNeatType(type_uri[0], property_uri[0], drop_property))
72
+
73
+ return None
@@ -112,7 +112,7 @@ class ShowDataModelAPI(ShowBaseAPI):
112
112
  else:
113
113
  # This should never happen, but we need to handle it to satisfy mypy
114
114
  raise NeatSessionError(
115
- f"Unsupported type {type(rules) }. Make sure you have either information or DMS rules."
115
+ f"Unsupported type {type(rules)}. Make sure you have either information or DMS rules."
116
116
  )
117
117
  identifier = to_directory_compatible(str(rules.metadata.identifier))
118
118
  name = f"{identifier}.html"
@@ -201,7 +201,7 @@ class ShowDataModelImplementsAPI(ShowBaseAPI):
201
201
  else:
202
202
  # This should never happen, but we need to handle it to satisfy mypy
203
203
  raise NeatSessionError(
204
- f"Unsupported type {type(rules) }. Make sure you have either information or DMS rules."
204
+ f"Unsupported type {type(rules)}. Make sure you have either information or DMS rules."
205
205
  )
206
206
  identifier = to_directory_compatible(str(rules.metadata.identifier))
207
207
  name = f"{identifier}_implements.html"
@@ -364,7 +364,7 @@ class ShowInstanceAPI(ShowBaseAPI):
364
364
  'Try setting [bold]NeatSession(storage="oxigraph")[/bold] enable Oxigraph store.'
365
365
  )
366
366
 
367
- if not self._state.instances.store.graph:
367
+ if not self._state.instances.store.dataset:
368
368
  raise NeatSessionError("No instances available. Try using [bold].read[/bold] to load instances.")
369
369
 
370
370
  di_graph = self._generate_instance_di_graph_and_types()
@@ -395,7 +395,7 @@ class ShowInstanceAPI(ShowBaseAPI):
395
395
  object,
396
396
  subject_type,
397
397
  object_type,
398
- ) in self._state.instances.store.graph.query(query):
398
+ ) in self._state.instances.store.dataset.query(query):
399
399
  subject = remove_namespace_from_uri(subject)
400
400
  property_ = remove_namespace_from_uri(property_)
401
401
  object = remove_namespace_from_uri(object)
@@ -2,6 +2,7 @@ from dataclasses import dataclass, field
2
2
  from typing import Literal, cast
3
3
 
4
4
  from cognite.neat._client import NeatClient
5
+ from cognite.neat._graph.extractors import KnowledgeGraphExtractor
5
6
  from cognite.neat._issues import IssueList
6
7
  from cognite.neat._rules.importers import BaseImporter, InferenceImporter
7
8
  from cognite.neat._rules.models import DMSRules, InformationRules
@@ -21,11 +22,14 @@ class SessionState:
21
22
  self.rule_store = NeatRulesStore()
22
23
  self.last_reference: DMSRules | InformationRules | None = None
23
24
  self.client = client
25
+ self.quoted_source_identifiers = False
24
26
 
25
27
  def rule_transform(self, *transformer: RulesTransformer) -> IssueList:
26
28
  if not transformer:
27
29
  raise NeatSessionError("No transformers provided.")
28
30
  first_transformer = transformer[0]
31
+
32
+ # This should not be allowed to be done automatically
29
33
  pruned = self.rule_store.prune_until_compatible(first_transformer)
30
34
  if pruned:
31
35
  type_hint = first_transformer.transform_type_hint()
@@ -62,6 +66,13 @@ class SessionState:
62
66
  issues.hint = "Use the .inspect.issues() for more details."
63
67
  return issues
64
68
 
69
+ def write_graph(self, extractor: KnowledgeGraphExtractor) -> IssueList:
70
+ extract_issues = self.instances.store.write(extractor)
71
+ issues = self.rule_store.import_graph(extractor)
72
+ self.instances.store.add_rules(self.rule_store.last_verified_information_rules)
73
+ issues.extend(extract_issues)
74
+ return issues
75
+
65
76
 
66
77
  @dataclass
67
78
  class InstancesState:
@@ -74,7 +85,7 @@ class InstancesState:
74
85
  def store(self) -> NeatGraphStore:
75
86
  if not self.has_store:
76
87
  if self.store_type == "oxigraph":
77
- self._store = NeatGraphStore.from_oxi_store()
88
+ self._store = NeatGraphStore.from_oxi_local_store()
78
89
  else:
79
90
  self._store = NeatGraphStore.from_memory_store()
80
91
  return cast(NeatGraphStore, self._store)
@@ -1,3 +1,5 @@
1
+ import warnings
2
+ import zipfile
1
3
  from collections.abc import Collection
2
4
  from pathlib import Path
3
5
  from typing import Any, Literal, overload
@@ -80,11 +82,47 @@ class ToAPI:
80
82
  exporter = exporters.ExcelExporter(styling="maximal", reference_rules_with_prefix=reference_rules_with_prefix)
81
83
  return self._state.rule_store.export_to_file(exporter, Path(io))
82
84
 
85
+ def session(self, io: Any) -> None:
86
+ """Export the current session to a file.
87
+
88
+ Args:
89
+ io: The file path to file-like object to write the session to.
90
+
91
+ Example:
92
+ Export the session to a file
93
+ ```python
94
+ session_file_name = "neat_session.zip"
95
+ neat.to.session(session_file_name)
96
+ ```
97
+ """
98
+ filepath = Path(io)
99
+ if filepath.suffix not in {".zip"}:
100
+ warnings.warn("File extension is not .zip, adding it to the file name", stacklevel=2)
101
+ filepath = filepath.with_suffix(".zip")
102
+
103
+ filepath.parent.mkdir(exist_ok=True, parents=True)
104
+
105
+ with zipfile.ZipFile(filepath, "w") as zip_ref:
106
+ zip_ref.writestr(
107
+ "neat-session/instances/instances.trig",
108
+ self._state.instances.store.serialize(),
109
+ )
110
+
83
111
  @overload
84
- def yaml(self, io: None, format: Literal["neat"] = "neat", skip_system_spaces: bool = True) -> str: ...
112
+ def yaml(
113
+ self,
114
+ io: None,
115
+ format: Literal["neat"] = "neat",
116
+ skip_system_spaces: bool = True,
117
+ ) -> str: ...
85
118
 
86
119
  @overload
87
- def yaml(self, io: Any, format: Literal["neat", "toolkit"] = "neat", skip_system_spaces: bool = True) -> None: ...
120
+ def yaml(
121
+ self,
122
+ io: Any,
123
+ format: Literal["neat", "toolkit"] = "neat",
124
+ skip_system_spaces: bool = True,
125
+ ) -> None: ...
88
126
 
89
127
  def yaml(
90
128
  self, io: Any | None = None, format: Literal["neat", "toolkit"] = "neat", skip_system_spaces: bool = True
@@ -182,6 +220,9 @@ class CDFToAPI:
182
220
  self._state.instances.store,
183
221
  instance_space=space,
184
222
  client=client,
223
+ # In case urllib.parse.quote() was run on the extraction, we need to run
224
+ # urllib.parse.unquote() on the load.
225
+ unquote_external_ids=self._state.quoted_source_identifiers,
185
226
  )
186
227
  result = loader.load_into_cdf(client)
187
228
  self._state.instances.outcome.append(result)
@@ -26,7 +26,7 @@ _T_Option = TypeVar("_T_Option")
26
26
 
27
27
 
28
28
  def _selection(message: str, options: Sequence[_T_Option]) -> _T_Option:
29
- option_text = "\n ".join([f"{i+1}) {option}" for i, option in enumerate(options)])
29
+ option_text = "\n ".join([f"{i + 1}) {option}" for i, option in enumerate(options)])
30
30
  selected_index = (
31
31
  IntPrompt().ask(f"{message}\n {option_text}\n", choices=list(map(str, range(1, len(options) + 1)))) - 1
32
32
  )
@@ -2,17 +2,22 @@ import functools
2
2
  from collections.abc import Callable
3
3
  from typing import Any
4
4
 
5
- from cognite.neat._issues.errors import CDFMissingClientError
5
+ from cognite.neat._issues.errors import CDFMissingClientError, NeatImportError
6
+ from cognite.neat._issues.errors._general import NeatValueError
6
7
 
7
8
  from ._collector import _COLLECTOR
8
9
 
9
10
  try:
10
11
  from rich import print
12
+ from rich.markup import escape
11
13
 
12
14
  _PREFIX = "[bold red][ERROR][/bold red]"
13
15
  except ImportError:
14
16
  _PREFIX = "[ERROR]"
15
17
 
18
+ def escape(x: Any, *_: Any, **__: Any) -> Any: # type: ignore[misc]
19
+ return x
20
+
16
21
 
17
22
  class NeatSessionError(Exception):
18
23
  """Base class for all exceptions raised by the NeatSession class."""
@@ -29,8 +34,8 @@ def _session_method_wrapper(func: Callable, cls_name: str):
29
34
  except NeatSessionError as e:
30
35
  action = _get_action()
31
36
  print(f"{_PREFIX} Cannot {action}: {e}")
32
- except CDFMissingClientError as e:
33
- print(f"{_PREFIX} {e.as_message()}")
37
+ except (CDFMissingClientError, NeatImportError, NeatValueError) as e:
38
+ print(f"{_PREFIX} {escape(e.as_message())}")
34
39
  except ModuleNotFoundError as e:
35
40
  if e.name == "neatengine":
36
41
  action = _get_action()