cognite-neat 0.95.0__py3-none-any.whl → 0.96.1__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.

@@ -1,10 +1,12 @@
1
+ import colorsys
1
2
  import random
2
3
  from typing import Any, cast
3
4
 
4
5
  import networkx as nx
5
- from ipycytoscape import CytoscapeWidget # type: ignore
6
- from IPython.display import display
6
+ from IPython.display import HTML, display
7
+ from pyvis.network import Network as PyVisNetwork # type: ignore
7
8
 
9
+ from cognite.neat._constants import IN_NOTEBOOK, IN_PYODIDE
8
10
  from cognite.neat._rules._constants import EntityTypes
9
11
  from cognite.neat._rules.models.dms._rules import DMSRules
10
12
  from cognite.neat._rules.models.entities._single_value import ClassEntity, ViewEntity
@@ -25,254 +27,211 @@ class ShowAPI:
25
27
 
26
28
 
27
29
  @intercept_session_exceptions
28
- class ShowInstanceAPI:
30
+ class ShowBaseAPI:
29
31
  def __init__(self, state: SessionState) -> None:
30
32
  self._state = state
31
33
 
32
- def __call__(self) -> Any:
33
- if not self._state.store.graph:
34
- raise NeatSessionError("No instances available. Try using [bold].read[/bold] to load instances.")
35
-
36
- widget = CytoscapeWidget()
37
- widget.layout.height = "700px"
38
-
39
- NxGraph, types = self._generate_instance_di_graph_and_types()
40
- widget_style = self._generate_cytoscape_widget_style(types)
41
- widget.set_style(widget_style)
42
-
43
- widget.graph.add_graph_from_networkx(NxGraph)
44
- print("Max of 100 nodes and edges are displayed, which are randomly selected.")
45
-
46
- return display(widget)
47
-
48
- def _generate_instance_di_graph_and_types(self) -> tuple[nx.DiGraph, set[str]]:
49
- query = """
50
- SELECT ?s ?p ?o ?ts ?to WHERE {
51
- ?s ?p ?o .
52
- FILTER(isIRI(?o)) # Example filter to check if ?o is an IRI (object type)
53
- FILTER(BOUND(?o))
54
- FILTER(?p != rdf:type)
55
-
56
- ?s a ?ts .
57
- ?o a ?to .
58
- }
59
- LIMIT 100
60
- """
61
-
62
- NxGraph = nx.DiGraph()
63
-
64
- types = set()
65
-
66
- for ( # type: ignore
67
- subject,
68
- property_,
69
- object,
70
- subject_type,
71
- object_type,
72
- ) in self._state.store.graph.query(query):
73
- subject = remove_namespace_from_uri(subject)
74
- property_ = remove_namespace_from_uri(property_)
75
- object = remove_namespace_from_uri(object)
76
- subject_type = remove_namespace_from_uri(subject_type)
77
- object_type = remove_namespace_from_uri(object_type)
78
-
79
- NxGraph.add_node(subject, label=subject, type=subject_type)
80
- NxGraph.add_node(object, label=object, type=object_type)
81
- NxGraph.add_edge(subject, object, label=property_)
82
-
83
- types.add(subject_type)
84
- types.add(object_type)
85
-
86
- return NxGraph, types
87
-
88
- def _generate_cytoscape_widget_style(self, types: set[str]) -> list[dict]:
89
- widget_style = [
90
- {
91
- "selector": "edge",
92
- "style": {
93
- "width": 1,
94
- "target-arrow-shape": "triangle",
95
- "curve-style": "bezier",
96
- "label": "data(label)",
97
- "font-size": "8px",
98
- "line-color": "black",
99
- "target-arrow-color": "black",
100
- },
101
- },
102
- ]
103
-
104
- colors = self._generate_hex_colors(len(types))
105
-
106
- for i, type_ in enumerate(types):
107
- widget_style.append(self._generate_node_cytoscape_style(type_, colors[i]))
108
-
109
- return widget_style
110
-
111
- @staticmethod
112
- def _generate_hex_colors(n: int) -> list[str]:
113
- """Generate a list of N random HEX color codes."""
114
- random.seed(42) # Set a seed for deterministic behavior
115
- hex_colors = []
116
- for _ in range(n):
117
- color = f"#{random.randint(0, 0xFFFFFF):06x}"
118
- hex_colors.append(color)
119
- return hex_colors
34
+ def _generate_visualization(self, di_graph: nx.DiGraph, name: str) -> Any:
35
+ if not IN_NOTEBOOK:
36
+ raise NeatSessionError("Visualization is only available in Jupyter notebooks!")
37
+
38
+ net = PyVisNetwork(
39
+ notebook=IN_NOTEBOOK,
40
+ cdn_resources="remote",
41
+ directed=True,
42
+ height="750px",
43
+ width="100%",
44
+ select_menu=IN_NOTEBOOK,
45
+ )
120
46
 
121
- @staticmethod
122
- def _generate_node_cytoscape_style(type_: str, color: str) -> dict:
123
- template = {
124
- "css": {
125
- "content": "data(label)",
126
- "text-valign": "center",
127
- "color": "black",
128
- "font-size": "10px",
129
- "width": "mapData(score, 0, 1, 10, 50)",
130
- "height": "mapData(score, 0, 1, 10, 50)",
131
- },
132
- }
47
+ # Change the plotting layout
48
+ net.repulsion(
49
+ node_distance=100,
50
+ central_gravity=0.3,
51
+ spring_length=200,
52
+ spring_strength=0.05,
53
+ damping=0.09,
54
+ )
133
55
 
134
- template["selector"] = f'node[type = "{type_}"]' # type: ignore
135
- template["css"]["background-color"] = color
56
+ net.from_nx(di_graph)
57
+ if IN_PYODIDE:
58
+ net.write_html(name)
59
+ return display(HTML(name))
136
60
 
137
- return template
61
+ else:
62
+ return net.show(name)
138
63
 
139
64
 
140
65
  @intercept_session_exceptions
141
- class ShowDataModelAPI:
66
+ class ShowDataModelAPI(ShowBaseAPI):
142
67
  def __init__(self, state: SessionState) -> None:
68
+ super().__init__(state)
143
69
  self._state = state
144
70
 
145
71
  def __call__(self) -> Any:
146
- if not self._state.last_verified_dms_rules and not self._state.last_verified_information_rules:
72
+ if not self._state.has_verified_rules:
147
73
  raise NeatSessionError(
148
74
  "No verified data model available. Try using [bold].verify()[/bold] to verify data model."
149
75
  )
150
76
 
151
- if self._state.last_verified_dms_rules:
152
- NxGraph = self._generate_dms_di_graph()
153
- elif self._state.last_verified_information_rules:
154
- NxGraph = self._generate_info_di_graph()
77
+ try:
78
+ di_graph = self._generate_dms_di_graph(self._state.last_verified_dms_rules)
79
+ name = "dms_data_model.html"
80
+ except NeatSessionError:
81
+ di_graph = self._generate_info_di_graph(self._state.last_verified_information_rules)
82
+ name = "information_data_model.html"
155
83
 
156
- widget = self._generate_widget()
157
- widget.graph.add_graph_from_networkx(NxGraph)
158
- return display(widget)
84
+ return self._generate_visualization(di_graph, name)
159
85
 
160
- def _generate_dms_di_graph(self) -> nx.DiGraph:
86
+ def _generate_dms_di_graph(self, rules: DMSRules) -> nx.DiGraph:
161
87
  """Generate a DiGraph from the last verified DMS rules."""
162
- NxGraph = nx.DiGraph()
88
+ di_graph = nx.DiGraph()
163
89
 
164
90
  # Add nodes and edges from Views sheet
165
- for view in cast(DMSRules, self._state.last_verified_dms_rules).views:
91
+ for view in rules.views:
166
92
  # if possible use human readable label coming from the view name
167
- if not NxGraph.has_node(view.view.suffix):
168
- NxGraph.add_node(view.view.suffix, label=view.name or view.view.suffix)
93
+ if not di_graph.has_node(view.view.suffix):
94
+ di_graph.add_node(view.view.suffix, label=view.name or view.view.suffix)
169
95
 
170
96
  # add implements as edges
171
97
  if view.implements:
172
98
  for implement in view.implements:
173
- if not NxGraph.has_node(implement.suffix):
174
- NxGraph.add_node(implement.suffix, label=implement.suffix)
99
+ if not di_graph.has_node(implement.suffix):
100
+ di_graph.add_node(implement.suffix, label=implement.suffix)
175
101
 
176
- NxGraph.add_edge(view.view.suffix, implement.suffix, label="implements")
102
+ di_graph.add_edge(
103
+ view.view.suffix,
104
+ implement.suffix,
105
+ label="implements",
106
+ dashes=True,
107
+ )
177
108
 
178
109
  # Add nodes and edges from Properties sheet
179
- for prop_ in cast(DMSRules, self._state.last_verified_dms_rules).properties:
110
+ for prop_ in rules.properties:
180
111
  if prop_.connection and isinstance(prop_.value_type, ViewEntity):
181
- if not NxGraph.has_node(prop_.view.suffix):
182
- NxGraph.add_node(prop_.view.suffix, label=prop_.view.suffix)
183
-
184
- label = f"{prop_.property_} [{0 if prop_.nullable else 1}..{ '' if prop_.is_list else 1}]"
185
- NxGraph.add_edge(prop_.view.suffix, prop_.value_type.suffix, label=label)
112
+ if not di_graph.has_node(prop_.view.suffix):
113
+ di_graph.add_node(prop_.view.suffix, label=prop_.view.suffix)
114
+ di_graph.add_edge(
115
+ prop_.view.suffix,
116
+ prop_.value_type.suffix,
117
+ label=prop_.name or prop_.property_,
118
+ )
186
119
 
187
- return NxGraph
120
+ return di_graph
188
121
 
189
- def _generate_info_di_graph(self) -> nx.DiGraph:
190
- """Generate nodes and edges for the last verified Information rules for DiGraph."""
122
+ def _generate_info_di_graph(self, rules: InformationRules) -> nx.DiGraph:
123
+ """Generate DiGraph representing information data model."""
191
124
 
192
- NxGraph = nx.DiGraph()
125
+ di_graph = nx.DiGraph()
193
126
 
194
127
  # Add nodes and edges from Views sheet
195
- for class_ in cast(InformationRules, self._state.last_verified_information_rules).classes:
128
+ for class_ in rules.classes:
196
129
  # if possible use human readable label coming from the view name
197
- if not NxGraph.has_node(class_.class_.suffix):
198
- NxGraph.add_node(
130
+ if not di_graph.has_node(class_.class_.suffix):
131
+ di_graph.add_node(
199
132
  class_.class_.suffix,
200
133
  label=class_.name or class_.class_.suffix,
201
134
  )
202
135
 
203
- # add implements as edges
136
+ # add subClassOff as edges
204
137
  if class_.parent:
205
138
  for parent in class_.parent:
206
- if not NxGraph.has_node(parent.suffix):
207
- NxGraph.add_node(parent.suffix, label=parent.suffix)
208
-
209
- NxGraph.add_edge(class_.class_.suffix, parent.suffix, label="subClassOf")
139
+ if not di_graph.has_node(parent.suffix):
140
+ di_graph.add_node(parent.suffix, label=parent.suffix)
141
+ di_graph.add_edge(
142
+ class_.class_.suffix,
143
+ parent.suffix,
144
+ label="subClassOf",
145
+ dashes=True,
146
+ )
210
147
 
211
148
  # Add nodes and edges from Properties sheet
212
- for prop_ in cast(InformationRules, self._state.last_verified_information_rules).properties:
149
+ for prop_ in rules.properties:
213
150
  if prop_.type_ == EntityTypes.object_property:
214
- if not NxGraph.has_node(prop_.class_.suffix):
215
- NxGraph.add_node(prop_.class_.suffix, label=prop_.class_.suffix)
151
+ if not di_graph.has_node(prop_.class_.suffix):
152
+ di_graph.add_node(prop_.class_.suffix, label=prop_.class_.suffix)
216
153
 
217
- label = f"{prop_.property_} [{1 if prop_.is_mandatory else 0}..{ '' if prop_.is_list else 1}]"
218
- NxGraph.add_edge(
154
+ di_graph.add_edge(
219
155
  prop_.class_.suffix,
220
156
  cast(ClassEntity, prop_.value_type).suffix,
221
- label=label,
157
+ label=prop_.name or prop_.property_,
222
158
  )
223
159
 
224
- return NxGraph
225
-
226
- def _generate_widget(self):
227
- """Generates an empty a CytoscapeWidget."""
228
- widget = CytoscapeWidget()
229
- widget.layout.height = "700px"
230
-
231
- widget.set_style(
232
- [
233
- {
234
- "selector": "node",
235
- "css": {
236
- "content": "data(label)",
237
- "text-valign": "center",
238
- "color": "black",
239
- "background-color": "#33C4FF",
240
- "font-size": "10px",
241
- "width": "mapData(score, 0, 1, 10, 50)",
242
- "height": "mapData(score, 0, 1, 10, 50)",
243
- },
244
- },
245
- {
246
- "selector": "edge",
247
- "style": {
248
- "width": 1,
249
- "target-arrow-shape": "triangle",
250
- "curve-style": "bezier",
251
- "label": "data(label)",
252
- "font-size": "8px",
253
- "line-color": "black",
254
- "target-arrow-color": "black",
255
- },
256
- },
257
- {
258
- "selector": 'edge[label = "subClassOf"]',
259
- "style": {
260
- "line-color": "grey",
261
- "target-arrow-color": "grey",
262
- "line-style": "dashed",
263
- "font-size": "8px",
264
- },
265
- },
266
- {
267
- "selector": 'edge[label = "implements"]',
268
- "style": {
269
- "line-color": "grey",
270
- "target-arrow-color": "grey",
271
- "line-style": "dashed",
272
- "font-size": "8px",
273
- },
274
- },
275
- ]
276
- )
160
+ return di_graph
161
+
162
+
163
+ @intercept_session_exceptions
164
+ class ShowInstanceAPI(ShowBaseAPI):
165
+ def __init__(self, state: SessionState) -> None:
166
+ super().__init__(state)
167
+ self._state = state
168
+
169
+ def __call__(self) -> Any:
170
+ if not self._state.store.graph:
171
+ raise NeatSessionError("No instances available. Try using [bold].read[/bold] to load instances.")
172
+
173
+ di_graph = self._generate_instance_di_graph_and_types()
174
+ return self._generate_visualization(di_graph, name="instances.html")
175
+
176
+ def _generate_instance_di_graph_and_types(self) -> nx.DiGraph:
177
+ query = """
178
+ SELECT ?s ?p ?o ?ts ?to WHERE {
179
+ ?s ?p ?o .
180
+ FILTER(isIRI(?o)) # Example filter to check if ?o is an IRI (object type)
181
+ FILTER(BOUND(?o))
182
+ FILTER(?p != rdf:type)
277
183
 
278
- return widget
184
+ ?s a ?ts .
185
+ ?o a ?to .
186
+ }
187
+ LIMIT 200
188
+ """
189
+
190
+ di_graph = nx.DiGraph()
191
+
192
+ types = [type_ for type_, _ in self._state.store.queries.summarize_instances()]
193
+ hex_colored_types = self._generate_hex_color_per_type(types)
194
+
195
+ for ( # type: ignore
196
+ subject,
197
+ property_,
198
+ object,
199
+ subject_type,
200
+ object_type,
201
+ ) in self._state.store.graph.query(query):
202
+ subject = remove_namespace_from_uri(subject)
203
+ property_ = remove_namespace_from_uri(property_)
204
+ object = remove_namespace_from_uri(object)
205
+ subject_type = remove_namespace_from_uri(subject_type)
206
+ object_type = remove_namespace_from_uri(object_type)
207
+
208
+ di_graph.add_node(
209
+ subject,
210
+ label=subject,
211
+ type=subject_type,
212
+ title=subject_type,
213
+ color=hex_colored_types[subject_type],
214
+ )
215
+ di_graph.add_node(
216
+ object,
217
+ label=object,
218
+ type=object_type,
219
+ title=object_type,
220
+ color=hex_colored_types[object_type],
221
+ )
222
+ di_graph.add_edge(subject, object, label=property_, color="grey")
223
+
224
+ return di_graph
225
+
226
+ @staticmethod
227
+ def _generate_hex_color_per_type(types: list[str]) -> dict:
228
+ hex_colored_types = {}
229
+ random.seed(381)
230
+ for type_ in types:
231
+ hue = random.random()
232
+ saturation = random.uniform(0.5, 1.0)
233
+ lightness = random.uniform(0.4, 0.6)
234
+ rgb = colorsys.hls_to_rgb(hue, lightness, saturation)
235
+ hex_color = f"#{int(rgb[0] * 255):02x}{int(rgb[1] * 255):02x}{int(rgb[2] * 255):02x}"
236
+ hex_colored_types[type_] = hex_color
237
+ return hex_colored_types
@@ -1,6 +1,7 @@
1
1
  from dataclasses import dataclass, field
2
2
  from typing import Literal, cast
3
3
 
4
+ from cognite.neat._issues import IssueList
4
5
  from cognite.neat._rules._shared import ReadRules, VerifiedRules
5
6
  from cognite.neat._rules.models.dms._rules import DMSRules
6
7
  from cognite.neat._rules.models.information._rules import InformationRules
@@ -15,6 +16,7 @@ class SessionState:
15
16
  store_type: Literal["memory", "oxigraph"]
16
17
  input_rules: list[ReadRules] = field(default_factory=list)
17
18
  verified_rules: list[VerifiedRules] = field(default_factory=list)
19
+ issue_lists: list[IssueList] = field(default_factory=list)
18
20
  _store: NeatGraphStore | None = field(init=False, default=None)
19
21
 
20
22
  @property
@@ -49,21 +51,39 @@ class SessionState:
49
51
  return self.verified_rules[-1]
50
52
 
51
53
  @property
52
- def last_verified_dms_rules(self) -> DMSRules | None:
54
+ def last_verified_dms_rules(self) -> DMSRules:
53
55
  if self.verified_rules:
54
56
  for rules in self.verified_rules[::-1]:
55
57
  if isinstance(rules, DMSRules):
56
58
  return rules
57
- return None
59
+
60
+ raise NeatSessionError(
61
+ 'No verified DMS data model. Try using [bold].convert("DMS")[/bold]'
62
+ " to convert verified information model to verified DMS model."
63
+ )
58
64
 
59
65
  @property
60
- def last_verified_information_rules(self) -> InformationRules | None:
66
+ def last_verified_information_rules(self) -> InformationRules:
61
67
  if self.verified_rules:
62
68
  for rules in self.verified_rules[::-1]:
63
69
  if isinstance(rules, InformationRules):
64
70
  return rules
65
- return None
71
+
72
+ raise NeatSessionError(
73
+ "No verified information data model. Try using [bold].verify()[/bold]"
74
+ " to convert unverified information model to verified information model."
75
+ )
66
76
 
67
77
  @property
68
78
  def has_store(self) -> bool:
69
79
  return self._store is not None
80
+
81
+ @property
82
+ def has_verified_rules(self) -> bool:
83
+ return bool(self.verified_rules)
84
+
85
+ @property
86
+ def last_issues(self) -> IssueList:
87
+ if not self.issue_lists:
88
+ raise NeatSessionError("No issues available. Try using [bold].verify()[/bold] to verify a data model.")
89
+ return self.issue_lists[-1]
@@ -367,20 +367,23 @@ class NeatGraphStore:
367
367
  def _shorten_summary(self, summary: pd.DataFrame) -> pd.DataFrame:
368
368
  """Shorten summary to top 5 types by occurrence."""
369
369
  top_5_rows = summary.head(5)
370
- last_row = summary.tail(1)
371
370
 
372
371
  indexes = [
373
372
  *top_5_rows.index.tolist(),
374
- "...",
375
- *last_row.index.tolist(),
376
373
  ]
374
+ data = [
375
+ top_5_rows,
376
+ ]
377
+ if len(summary) > 6:
378
+ last_row = summary.tail(1)
379
+ indexes += [
380
+ "...",
381
+ *last_row.index.tolist(),
382
+ ]
383
+ data.extend([pd.DataFrame([["..."] * summary.shape[1]], columns=summary.columns), last_row])
377
384
 
378
385
  shorter_summary = pd.concat(
379
- [
380
- top_5_rows,
381
- pd.DataFrame([["..."] * summary.shape[1]], columns=summary.columns),
382
- last_row,
383
- ],
386
+ data,
384
387
  ignore_index=True,
385
388
  )
386
389
  shorter_summary.index = cast(Index, indexes)
@@ -17,3 +17,7 @@ def most_occurring_element(list_of_elements: list[T_Element]) -> T_Element:
17
17
  def chunker(sequence: Sequence[T_Element], chunk_size: int) -> Iterable[Sequence[T_Element]]:
18
18
  for i in range(0, len(sequence), chunk_size):
19
19
  yield sequence[i : i + chunk_size]
20
+
21
+
22
+ def remove_list_elements(input_list: list, elements_to_remove: list) -> list:
23
+ return [element for element in input_list if element not in elements_to_remove]
cognite/neat/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.95.0"
1
+ __version__ = "0.96.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cognite-neat
3
- Version: 0.95.0
3
+ Version: 0.96.1
4
4
  Summary: Knowledge graph transformation
5
5
  Home-page: https://cognite-neat.readthedocs-hosted.com/
6
6
  License: Apache-2.0
@@ -17,7 +17,6 @@ Provides-Extra: all
17
17
  Provides-Extra: docs
18
18
  Provides-Extra: google
19
19
  Provides-Extra: graphql
20
- Provides-Extra: jupyter
21
20
  Provides-Extra: oxi
22
21
  Provides-Extra: service
23
22
  Requires-Dist: PyYAML
@@ -28,9 +27,7 @@ Requires-Dist: fastapi (>=0,<1) ; extra == "service" or extra == "all"
28
27
  Requires-Dist: google-api-python-client ; extra == "google"
29
28
  Requires-Dist: google-auth-oauthlib ; extra == "google"
30
29
  Requires-Dist: gspread ; extra == "google"
31
- Requires-Dist: ipycytoscape (>=1.3.3,<2.0.0)
32
30
  Requires-Dist: jinja2 (>=3.1.2,<4.0.0) ; extra == "graphql" or extra == "all"
33
- Requires-Dist: matplotlib (==3.5.2)
34
31
  Requires-Dist: mkdocs ; extra == "docs"
35
32
  Requires-Dist: mkdocs-autorefs (>=0.5.0,<0.6.0) ; extra == "docs"
36
33
  Requires-Dist: mkdocs-git-authors-plugin ; extra == "docs"
@@ -49,9 +46,10 @@ Requires-Dist: pydantic (>=2,<3)
49
46
  Requires-Dist: pymdown-extensions ; extra == "docs"
50
47
  Requires-Dist: pyoxigraph (==0.3.19) ; extra == "oxi" or extra == "all"
51
48
  Requires-Dist: python-multipart (==0.0.9) ; extra == "service" or extra == "all"
49
+ Requires-Dist: pyvis (>=0.3.2,<0.4.0)
52
50
  Requires-Dist: rdflib
53
51
  Requires-Dist: requests
54
- Requires-Dist: rich[jupyter] (>=13.7.1,<14.0.0) ; extra == "jupyter"
52
+ Requires-Dist: rich[jupyter] (>=13.7.1,<14.0.0)
55
53
  Requires-Dist: schedule (>=1,<2) ; extra == "service" or extra == "all"
56
54
  Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
57
55
  Requires-Dist: typing_extensions (>=4.8,<5.0) ; python_version < "3.11"