cognite-neat 0.109.1__py3-none-any.whl → 0.109.3__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.
Files changed (34) hide show
  1. cognite/neat/_alpha.py +15 -0
  2. cognite/neat/_client/testing.py +1 -1
  3. cognite/neat/_issues/_base.py +33 -9
  4. cognite/neat/_issues/errors/__init__.py +2 -10
  5. cognite/neat/_issues/errors/_general.py +1 -1
  6. cognite/neat/_issues/errors/_wrapper.py +11 -0
  7. cognite/neat/_rules/exporters/_rules2excel.py +31 -1
  8. cognite/neat/_rules/models/_rdfpath.py +2 -0
  9. cognite/neat/_rules/models/_types.py +4 -2
  10. cognite/neat/_rules/models/dms/_rules.py +0 -36
  11. cognite/neat/_rules/models/entities/_constants.py +3 -0
  12. cognite/neat/_rules/models/entities/_single_value.py +6 -1
  13. cognite/neat/_rules/models/entities/_wrapped.py +3 -0
  14. cognite/neat/_rules/transformers/__init__.py +4 -0
  15. cognite/neat/_rules/transformers/_converters.py +221 -15
  16. cognite/neat/_session/_base.py +7 -0
  17. cognite/neat/_session/_collector.py +4 -1
  18. cognite/neat/_session/_create.py +46 -12
  19. cognite/neat/_session/_prepare.py +11 -3
  20. cognite/neat/_session/_read.py +14 -2
  21. cognite/neat/_session/_state.py +7 -3
  22. cognite/neat/_session/_to.py +20 -5
  23. cognite/neat/_session/exceptions.py +16 -6
  24. cognite/neat/_store/_provenance.py +1 -0
  25. cognite/neat/_store/_rules_store.py +192 -127
  26. cognite/neat/_utils/spreadsheet.py +10 -1
  27. cognite/neat/_utils/text.py +40 -9
  28. cognite/neat/_version.py +1 -1
  29. {cognite_neat-0.109.1.dist-info → cognite_neat-0.109.3.dist-info}/METADATA +1 -1
  30. {cognite_neat-0.109.1.dist-info → cognite_neat-0.109.3.dist-info}/RECORD +33 -32
  31. cognite/neat/_issues/errors/_workflow.py +0 -36
  32. {cognite_neat-0.109.1.dist-info → cognite_neat-0.109.3.dist-info}/LICENSE +0 -0
  33. {cognite_neat-0.109.1.dist-info → cognite_neat-0.109.3.dist-info}/WHEEL +0 -0
  34. {cognite_neat-0.109.1.dist-info → cognite_neat-0.109.3.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,9 @@
1
1
  import functools
2
+ import warnings
2
3
  from collections.abc import Callable
3
4
  from typing import Any
4
5
 
6
+ from cognite.neat._alpha import AlphaWarning
5
7
  from cognite.neat._issues.errors import CDFMissingClientError, NeatImportError
6
8
  from cognite.neat._issues.errors._external import OxigraphStorageLockedError
7
9
  from cognite.neat._issues.errors._general import NeatValueError
@@ -12,9 +14,11 @@ try:
12
14
  from rich import print
13
15
  from rich.markup import escape
14
16
 
15
- _PREFIX = "[bold red][ERROR][/bold red]"
17
+ _ERROR_PREFIX = "[bold red][ERROR][/bold red]"
18
+ _WARNING_PREFIX = "[bold bright_magenta][WARNING][/bold bright_magenta]"
16
19
  except ImportError:
17
- _PREFIX = "[ERROR]"
20
+ _ERROR_PREFIX = "[ERROR]"
21
+ _WARNING_PREFIX = "[WARNING]"
18
22
 
19
23
  def escape(x: Any, *_: Any, **__: Any) -> Any: # type: ignore[misc]
20
24
  return x
@@ -31,21 +35,27 @@ def _session_method_wrapper(func: Callable, cls_name: str):
31
35
  def wrapper(*args: Any, **kwargs: Any):
32
36
  _COLLECTOR.track_session_command(f"{cls_name}.{func.__name__}", *args, **kwargs)
33
37
  try:
34
- return func(*args, **kwargs)
38
+ with warnings.catch_warnings(record=True) as w:
39
+ result = func(*args, **kwargs)
40
+ for warning in w:
41
+ if isinstance(warning.message, AlphaWarning):
42
+ print(f"{_WARNING_PREFIX} {warning.message}")
43
+
44
+ return result
35
45
  except NeatSessionError as e:
36
46
  action = _get_action()
37
- print(f"{_PREFIX} Cannot {action}: {e}")
47
+ print(f"{_ERROR_PREFIX} Cannot {action}: {e}")
38
48
  except (
39
49
  CDFMissingClientError,
40
50
  NeatImportError,
41
51
  NeatValueError,
42
52
  OxigraphStorageLockedError,
43
53
  ) as e:
44
- print(f"{_PREFIX} {escape(e.as_message())}")
54
+ print(f"{_ERROR_PREFIX} {escape(e.as_message())}")
45
55
  except ModuleNotFoundError as e:
46
56
  if e.name == "neatengine":
47
57
  action = _get_action()
48
- print(f"{_PREFIX} The functionality {action} requires the NeatEngine.")
58
+ print(f"{_ERROR_PREFIX} The functionality {action} requires the NeatEngine.")
49
59
  else:
50
60
  raise e
51
61
 
@@ -44,6 +44,7 @@ class Agent:
44
44
  CDF_AGENT = Agent(acted_on_behalf_of="UNKNOWN", id_=CDF_NAMESPACE["agent"])
45
45
  NEAT_AGENT = Agent(acted_on_behalf_of="UNKNOWN", id_=DEFAULT_NAMESPACE["agent"])
46
46
  UNKNOWN_AGENT = Agent(acted_on_behalf_of="UNKNOWN", id_=DEFAULT_NAMESPACE["unknown-agent"])
47
+ EXTERNAL_AGENT = Agent(acted_on_behalf_of="HUMAN", id_=DEFAULT_NAMESPACE["external-agent"])
47
48
 
48
49
 
49
50
  @dataclass(frozen=True)
@@ -3,11 +3,11 @@ from collections import defaultdict
3
3
  from collections.abc import Callable, Hashable
4
4
  from dataclasses import dataclass
5
5
  from datetime import datetime, timezone
6
+ from functools import partial
6
7
  from pathlib import Path
7
- from typing import Any
8
+ from typing import Any, cast
8
9
 
9
10
  import rdflib
10
- from cognite.client import data_modeling as dm
11
11
  from rdflib import URIRef
12
12
 
13
13
  from cognite.neat._client import NeatClient
@@ -15,7 +15,7 @@ from cognite.neat._constants import DEFAULT_NAMESPACE
15
15
  from cognite.neat._graph.extractors import DMSGraphExtractor, KnowledgeGraphExtractor
16
16
  from cognite.neat._issues import IssueList, catch_issues
17
17
  from cognite.neat._issues.errors import NeatValueError
18
- from cognite.neat._rules._shared import ReadRules, Rules, T_VerifiedRules, VerifiedRules
18
+ from cognite.neat._rules._shared import T_VerifiedRules, VerifiedRules
19
19
  from cognite.neat._rules.exporters import BaseExporter
20
20
  from cognite.neat._rules.exporters._base import CDFExporter, T_Export
21
21
  from cognite.neat._rules.importers import BaseImporter
@@ -23,7 +23,14 @@ from cognite.neat._rules.models import DMSRules, InformationRules
23
23
  from cognite.neat._rules.transformers import DMSToInformation, VerifiedRulesTransformer, VerifyAnyRules
24
24
  from cognite.neat._utils.upload import UploadResultList
25
25
 
26
- from ._provenance import UNKNOWN_AGENT, Activity, Change, Entity, Provenance
26
+ from ._provenance import (
27
+ EXTERNAL_AGENT,
28
+ UNKNOWN_AGENT,
29
+ Activity,
30
+ Change,
31
+ Entity,
32
+ Provenance,
33
+ )
27
34
  from .exceptions import EmptyStore, InvalidActivityInput
28
35
 
29
36
 
@@ -66,63 +73,156 @@ class NeatRulesStore:
66
73
  return calculated_hash[:8]
67
74
  return calculated_hash
68
75
 
69
- def import_rules(
70
- self, importer: BaseImporter, validate: bool = True, client: NeatClient | None = None
71
- ) -> IssueList:
72
- if self.empty:
73
- return self._import_rules(importer, validate, client)
76
+ def _rules_import_verify_convert(
77
+ self,
78
+ importer: BaseImporter,
79
+ validate: bool,
80
+ client: NeatClient | None = None,
81
+ ) -> tuple[InformationRules, DMSRules | None]:
82
+ """Action that imports rules, verifies them and optionally converts them."""
83
+ read_rules = importer.to_rules()
84
+ verified = VerifyAnyRules(validate, client).transform(read_rules) # type: ignore[arg-type]
85
+ if isinstance(verified, InformationRules):
86
+ return verified, None
87
+ elif isinstance(verified, DMSRules):
88
+ return DMSToInformation().transform(verified), verified
74
89
  else:
75
- # Importing can be used as a manual transformation.
76
- return self._manual_transform(importer, validate, client)
90
+ # Bug in the code
91
+ raise ValueError(f"Invalid output from importer: {type(verified)}")
77
92
 
78
- def _import_rules(
79
- self, importer: BaseImporter, validate: bool = True, client: NeatClient | None = None
80
- ) -> IssueList:
81
- def action() -> tuple[InformationRules, DMSRules | None]:
82
- read_rules = importer.to_rules()
83
- verified = VerifyAnyRules(validate, client).transform(read_rules) # type: ignore[arg-type]
84
- if isinstance(verified, InformationRules):
85
- return verified, None
86
- elif isinstance(verified, DMSRules):
87
- return DMSToInformation().transform(verified), verified
88
- else:
89
- # Bug in the code
90
- raise ValueError(f"Invalid output from importer: {type(verified)}")
91
-
92
- return self.import_action(action, importer)
93
+ def _graph_import_verify_convert(
94
+ self,
95
+ extractor: KnowledgeGraphExtractor,
96
+ ) -> tuple[InformationRules, DMSRules | None]:
97
+ info = extractor.get_information_rules()
98
+ dms: DMSRules | None = None
99
+ if isinstance(extractor, DMSGraphExtractor):
100
+ dms = extractor.get_dms_rules()
101
+ return info, dms
93
102
 
94
103
  def _manual_transform(
95
104
  self, importer: BaseImporter, validate: bool = True, client: NeatClient | None = None
96
105
  ) -> IssueList:
97
- raise NotImplementedError("Manual transformation is not yet implemented.")
106
+ result, issue_list, start, end = self._do_activity(
107
+ partial(self._rules_import_verify_convert, importer, validate, client)
108
+ )
98
109
 
99
- def import_graph(self, extractor: KnowledgeGraphExtractor) -> IssueList:
100
- def action() -> tuple[InformationRules, DMSRules | None]:
101
- info = extractor.get_information_rules()
102
- dms: DMSRules | None = None
103
- if isinstance(extractor, DMSGraphExtractor):
104
- dms = extractor.get_dms_rules()
105
- return info, dms
110
+ if not result:
111
+ return issue_list
112
+
113
+ info, dms = result
114
+ last_change = self.provenance[-1]
115
+
116
+ outside_agent = EXTERNAL_AGENT
117
+ outside_activity = Activity(
118
+ was_associated_with=outside_agent,
119
+ started_at_time=last_change.activity.ended_at_time,
120
+ ended_at_time=end,
121
+ used=last_change.target_entity,
122
+ )
123
+
124
+ # Case 1: Source of imported rules is not known
125
+ if not (source_id := self._get_source_id(result)):
126
+ raise NeatValueError(
127
+ "The source of the imported rules is unknown."
128
+ " Import will be skipped. Start a new NEAT session and import the data model there."
129
+ )
130
+
131
+ # Case 2: Source of imported rules not in rules_store
132
+ if not (source_entity := self.provenance.target_entity(source_id)) or not isinstance(
133
+ source_entity, RulesEntity
134
+ ):
135
+ raise NeatValueError(
136
+ "The source of the imported rules is not in the provenance."
137
+ " Import will be skipped. Start a new NEAT session and import the data model there."
138
+ )
139
+
140
+ # Case 3: Source is not the latest source entity in the provenance change
141
+ if source_entity.id_ != last_change.target_entity.id_:
142
+ raise NeatValueError(
143
+ "Imported rules are detached from the provenance chain."
144
+ " Import will be skipped. Start a new NEAT session and import the data model there."
145
+ )
146
+
147
+ # Case 4: Provenance is already at the physical state of the data model, going back to logical not possible
148
+ if not dms and source_entity.dms:
149
+ raise NeatValueError(
150
+ "Rules are already in physical state, import of logical model not possible."
151
+ " Import will be skipped. Start a new NEAT session and import the data model there."
152
+ )
153
+
154
+ # modification took place on information rules
155
+ if not dms and not source_entity.dms:
156
+ outside_target_entity = RulesEntity(
157
+ was_attributed_to=outside_agent,
158
+ was_generated_by=outside_activity,
159
+ information=info,
160
+ dms=dms,
161
+ issues=issue_list,
162
+ id_=self._create_id(info, dms),
163
+ )
164
+
165
+ # modification took place on dms rules, keep latest information rules
166
+ elif dms and source_entity.dms:
167
+ outside_target_entity = RulesEntity(
168
+ was_attributed_to=outside_agent,
169
+ was_generated_by=outside_activity,
170
+ information=last_change.target_entity.information,
171
+ dms=dms,
172
+ issues=issue_list,
173
+ id_=self._create_id(info, dms),
174
+ )
175
+
176
+ else:
177
+ raise NeatValueError("Invalid state of rules for manual transformation")
178
+
179
+ outside_change = Change(
180
+ source_entity=last_change.target_entity,
181
+ agent=outside_agent,
182
+ activity=outside_activity,
183
+ target_entity=outside_target_entity,
184
+ description="Manual transformation of rules outside of NEAT",
185
+ )
186
+
187
+ self._last_issues = issue_list
188
+ # record change that took place outside of neat
189
+ self.provenance.append(outside_change)
106
190
 
107
- return self.import_action(action, extractor)
191
+ return issue_list
108
192
 
109
- def import_action(
193
+ def import_graph(self, extractor: KnowledgeGraphExtractor) -> IssueList:
194
+ if not self.empty:
195
+ raise NeatValueError(f"Data model already exists. Cannot import {extractor.source_uri}.")
196
+ else:
197
+ return self.do_activity(partial(self._graph_import_verify_convert, extractor), extractor)
198
+
199
+ def import_rules(
110
200
  self,
111
- action: Callable[[], tuple[InformationRules, DMSRules | None]],
112
- agent_tool: BaseImporter | KnowledgeGraphExtractor,
201
+ importer: BaseImporter,
202
+ validate: bool = True,
203
+ client: NeatClient | None = None,
204
+ enable_manual_edit: bool = False,
113
205
  ) -> IssueList:
114
- if self.provenance:
115
- raise NeatValueError(f"Data model already exists. Cannot import {agent_tool.source_uri}.")
116
- return self.do_activity(action, agent_tool)
206
+ if self.empty:
207
+ return self.do_activity(
208
+ partial(self._rules_import_verify_convert, importer, validate, client),
209
+ importer,
210
+ )
211
+ elif enable_manual_edit:
212
+ return self._manual_transform(importer, validate, client)
213
+ else:
214
+ raise NeatValueError("Re-importing rules in the rules store is not allowed.")
117
215
 
118
216
  def transform(self, *transformer: VerifiedRulesTransformer) -> IssueList:
119
217
  if not self.provenance:
120
218
  raise EmptyStore()
121
219
 
122
220
  all_issues = IssueList()
123
- for item in transformer:
221
+ for agent_tool in transformer:
124
222
 
125
- def action(transformer_item=item) -> tuple[InformationRules, DMSRules | None]:
223
+ def action(
224
+ transformer_item=agent_tool,
225
+ ) -> tuple[InformationRules, DMSRules | None]:
126
226
  last_change = self.provenance[-1]
127
227
  source_entity = last_change.target_entity
128
228
  transformer_input = self._get_transformer_input(source_entity, transformer_item)
@@ -131,8 +231,9 @@ class NeatRulesStore:
131
231
  return transformer_output, None
132
232
  return last_change.target_entity.information, transformer_output
133
233
 
134
- issues = self.do_activity(action, item)
234
+ issues = self.do_activity(action, agent_tool)
135
235
  all_issues.extend(issues)
236
+
136
237
  return all_issues
137
238
 
138
239
  def export(self, exporter: BaseExporter[T_VerifiedRules, T_Export]) -> T_Export:
@@ -154,34 +255,40 @@ class NeatRulesStore:
154
255
  self,
155
256
  action: Callable[[], tuple[InformationRules, DMSRules | None]],
156
257
  agent_tool: BaseImporter | VerifiedRulesTransformer | KnowledgeGraphExtractor,
157
- ) -> IssueList:
258
+ ):
259
+ result, issue_list, start, end = self._do_activity(action)
260
+ self._last_issues = issue_list
261
+
262
+ if result:
263
+ self._update_provenance(agent_tool, result, issue_list, start, end)
264
+ return issue_list
265
+
266
+ def _update_provenance(
267
+ self,
268
+ agent_tool: BaseImporter | VerifiedRulesTransformer | KnowledgeGraphExtractor,
269
+ result: tuple[InformationRules, DMSRules | None],
270
+ issue_list: IssueList,
271
+ activity_start: datetime,
272
+ activity_end: datetime,
273
+ ) -> None:
274
+ # set source entity
158
275
  if isinstance(agent_tool, BaseImporter | KnowledgeGraphExtractor):
159
276
  source_entity = Entity.create_with_defaults(
160
277
  was_attributed_to=UNKNOWN_AGENT,
161
278
  id_=agent_tool.source_uri,
162
279
  )
163
280
  else:
164
- # This is a transformer
165
281
  source_entity = self.provenance[-1].target_entity
166
282
 
167
- start = datetime.now(timezone.utc)
168
- result: tuple[InformationRules, DMSRules | None] | None = None
169
- with catch_issues() as issue_list:
170
- result = action()
171
-
172
- end = datetime.now(timezone.utc)
173
- self._last_issues = issue_list
174
-
283
+ # setting the rest of provenance components
284
+ info, dms = result
175
285
  agent = agent_tool.agent
176
286
  activity = Activity(
177
287
  was_associated_with=agent,
178
- ended_at_time=end,
179
- started_at_time=start,
288
+ ended_at_time=activity_end,
289
+ started_at_time=activity_start,
180
290
  used=source_entity,
181
291
  )
182
- if result is None:
183
- return issue_list
184
- info, dms = result
185
292
 
186
293
  target_entity = RulesEntity(
187
294
  was_attributed_to=agent,
@@ -199,8 +306,20 @@ class NeatRulesStore:
199
306
  description=agent_tool.description,
200
307
  source_entity=source_entity,
201
308
  )
309
+
202
310
  self.provenance.append(change)
203
- return issue_list
311
+
312
+ def _do_activity(
313
+ self,
314
+ action: Callable[[], tuple[InformationRules, DMSRules | None]],
315
+ ):
316
+ """This private method is used to execute an activity and return the result and issues."""
317
+ start = datetime.now(timezone.utc)
318
+ result: tuple[InformationRules, DMSRules | None] | None = None
319
+ with catch_issues() as issue_list:
320
+ result = action()
321
+ end = datetime.now(timezone.utc)
322
+ return result, issue_list, start, end
204
323
 
205
324
  def _export_activity(self, action: Callable, exporter: BaseExporter, target_id: URIRef, *exporter_args: Any) -> Any:
206
325
  if self.empty:
@@ -210,15 +329,18 @@ class NeatRulesStore:
210
329
  expected_types = exporter.source_types()
211
330
 
212
331
  if source_entity.dms is not None and isinstance(source_entity.dms, expected_types):
213
- input_ = source_entity.dms
332
+ input_ = cast(VerifiedRules, source_entity.dms).model_copy(deep=True)
214
333
  elif isinstance(source_entity.information, expected_types):
215
- input_ = source_entity.information
334
+ input_ = cast(VerifiedRules, source_entity.information).model_copy(deep=True)
216
335
  else:
217
336
  available: list[type] = [InformationRules]
218
337
  if source_entity.dms is not None:
219
338
  available.append(DMSRules)
220
339
  raise InvalidActivityInput(expected=expected_types, have=tuple(available))
221
340
 
341
+ # need to write source prior the export
342
+ input_.metadata.source_id = source_entity.id_
343
+
222
344
  agent = exporter.agent
223
345
  start = datetime.now(timezone.utc)
224
346
  with catch_issues() as issue_list:
@@ -252,6 +374,7 @@ class NeatRulesStore:
252
374
  description=exporter.description,
253
375
  source_entity=source_entity,
254
376
  )
377
+
255
378
  self.exports_by_source_entity_id[source_entity.id_].append(change)
256
379
  if isinstance(result, UploadResultList):
257
380
  self._last_outcome = result
@@ -272,72 +395,14 @@ class NeatRulesStore:
272
395
  # Case 3: We have both information and dms rules and the transformer is compatible with information rules
273
396
  raise InvalidActivityInput(expected=(InformationRules,), have=(DMSRules,))
274
397
 
275
- def _update_source_entity(self, source_entity: Entity, result: Rules, issue_list: IssueList) -> Entity:
276
- """Update source entity to keep the unbroken provenance chain of changes."""
277
-
278
- # Case 1: Store not empty, source of imported rules is not known
279
- if not (source_id := self._get_source_id(result)):
280
- raise NeatValueError(
281
- "The data model to be read to the current NEAT session"
282
- " has no relation to the session content."
283
- " Import will be skipped."
284
- "\n\nSuggestions:\n\t(1) Start a new NEAT session and "
285
- "import the data model there."
286
- )
287
-
288
- # We are taking target entity as it is the entity that produce rules
289
- # which were updated by activities outside of the rules tore
290
- update_source_entity: Entity | None = self.provenance.target_entity(source_id)
291
-
292
- # Case 2: source of imported rules not in rules_store
293
- if not update_source_entity:
294
- raise NeatValueError(
295
- "The source of the data model being imported is not in"
296
- " the content of this NEAT session."
297
- " Import will be skipped."
298
- "\n\nSuggestions:"
299
- "\n\t(1) Start a new NEAT session and import the data model source"
300
- "\n\t(2) Then import the data model itself"
301
- )
398
+ def _get_source_id(self, result: tuple[InformationRules, DMSRules | None]) -> rdflib.URIRef | None:
399
+ """Return the source of the result.
302
400
 
303
- # Case 3: source_entity in rules_store and but it is not the latest entity
304
- if self.provenance[-1].target_entity.id_ != update_source_entity.id_:
305
- raise NeatValueError(
306
- "Source of imported data model is not the latest entity in the data model provenance."
307
- "Pruning required to set the source entity to the latest entity in the provenance."
308
- )
309
-
310
- # Case 4: source_entity in rules_store and it is not the latest target entity
311
- if self.provenance[-1].target_entity.id_ != update_source_entity.id_:
312
- raise NeatValueError(
313
- "Source of imported rules is not the latest entity in the provenance."
314
- "Pruning required to set the source entity to the latest entity in the provenance."
315
- )
316
-
317
- # Case 5: source_entity in rules_store and it is the latest entity
318
- # Here we need to check if the source and target entities are identical
319
- # if they are ... we should raise an error and skip importing
320
- # for now we will just return the source entity that we managed to extract
321
-
322
- return update_source_entity or source_entity
323
-
324
- def _get_source_id(self, result: Rules) -> rdflib.URIRef | None:
325
- """Return the source of the result."""
326
-
327
- if isinstance(result, ReadRules) and result.rules is not None and result.rules.metadata.source_id:
328
- return rdflib.URIRef(result.rules.metadata.source_id)
329
- if isinstance(result, VerifiedRules):
330
- return result.metadata.source_id
331
- return None
332
-
333
- def _get_data_model_id(self, result: Rules) -> dm.DataModelId | None:
334
- """Return the source of the result."""
335
-
336
- if isinstance(result, ReadRules) and result.rules is not None:
337
- return result.rules.metadata.as_data_model_id()
338
- if isinstance(result, VerifiedRules):
339
- return result.metadata.as_data_model_id()
340
- return None
401
+ !!! note
402
+ This method prioritizes the source_id of the DMS rules
403
+ """
404
+ info, dms = result
405
+ return dms.metadata.source_id if dms else info.metadata.source_id
341
406
 
342
407
  def _create_id(self, info: InformationRules, dms: DMSRules | None) -> rdflib.URIRef:
343
408
  if dms is None:
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass, field
2
- from typing import Literal, cast, overload
2
+ from typing import Any, Literal, cast, overload
3
3
 
4
4
  import pandas as pd
5
5
  from openpyxl import load_workbook
@@ -81,3 +81,12 @@ def _get_row_number(sheet: Worksheet, values_to_find: list[str]) -> int | None:
81
81
  if any(value in row for value in values_to_find):
82
82
  return row_number
83
83
  return None
84
+
85
+
86
+ def find_column_with_value(sheet: Worksheet, value: Any) -> str | None:
87
+ for row in sheet.iter_rows():
88
+ for cell in row:
89
+ if cell.value and isinstance(cell.value, str) and cell.value.lower() == value.lower():
90
+ return cell.column_letter # type: ignore
91
+
92
+ return None
@@ -17,21 +17,23 @@ def to_camel(string: str) -> str:
17
17
  >>> to_camel("ScenarioInstance_priceForecast")
18
18
  'scenarioInstancePriceForecast'
19
19
  """
20
+ string = re.sub(r"[\s_-]", "_", string)
21
+ string = re.sub("_+", "_", string)
20
22
  if "_" in string:
21
- # Could be a combination of snake and pascal/camel case
22
- parts = string.split("_")
23
- pascal_splits = [to_pascal(subpart) for part in parts for subpart in part.split("-") if subpart]
24
- elif "-" in string:
25
- # Could be a combination of kebab and pascal/camel case
26
- parts = string.split("-")
27
- pascal_splits = [to_pascal(subpart) for part in parts for subpart in part.split("_") if subpart]
23
+ pascal_splits = [to_pascal(part) for part in string.split("_")]
28
24
  else:
29
- # Assume is pascal/camel case
30
25
  # Ensure pascal
31
26
  string = string[0].upper() + string[1:]
32
27
  pascal_splits = [string]
33
- string_split = []
28
+ cleaned: list[str] = []
34
29
  for part in pascal_splits:
30
+ if part.upper() == part:
31
+ cleaned.append(part.capitalize())
32
+ else:
33
+ cleaned.append(part)
34
+
35
+ string_split = []
36
+ for part in cleaned:
35
37
  string_split.extend(re.findall(r"[A-Z][a-z0-9]*", part))
36
38
  if not string_split:
37
39
  string_split = [string]
@@ -135,3 +137,32 @@ def humanize_collection(collection: Collection[Any], /, *, sort: bool = True) ->
135
137
  sequence = list(strings)
136
138
 
137
139
  return f"{', '.join(sequence[:-1])} and {sequence[-1]}"
140
+
141
+
142
+ class NamingStandardization:
143
+ _clean_pattern = re.compile(r"[^a-zA-Z0-9_]+")
144
+ _multi_underscore_pattern = re.compile(r"_+")
145
+ _start_letter_pattern = re.compile(r"^[a-zA-Z]")
146
+
147
+ @classmethod
148
+ def standardize_class_str(cls, raw: str) -> str:
149
+ clean = cls._clean_string(raw)
150
+ if not cls._start_letter_pattern.match(clean):
151
+ # Underscore ensure that 'Class' it treated as a separate word
152
+ # in the to_pascale function
153
+ clean = f"Class_{clean}"
154
+ return to_pascal(clean)
155
+
156
+ @classmethod
157
+ def standardize_property_str(cls, raw: str) -> str:
158
+ clean = cls._clean_string(raw)
159
+ if not cls._start_letter_pattern.match(clean):
160
+ # Underscore ensure that 'property' it treated as a separate word
161
+ # in the to_camel function
162
+ clean = f"property_{clean}"
163
+ return to_camel(clean)
164
+
165
+ @classmethod
166
+ def _clean_string(cls, raw: str) -> str:
167
+ raw = cls._clean_pattern.sub("_", raw)
168
+ return cls._multi_underscore_pattern.sub("_", raw)
cognite/neat/_version.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "0.109.1"
1
+ __version__ = "0.109.3"
2
2
  __engine__ = "^2.0.3"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: cognite-neat
3
- Version: 0.109.1
3
+ Version: 0.109.3
4
4
  Summary: Knowledge graph transformation
5
5
  License: Apache-2.0
6
6
  Author: Nikola Vasiljevic