cognite-neat 0.94.0__py3-none-any.whl → 0.96.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.

@@ -1,10 +1,11 @@
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 pyvis.network import Network as PyVisNetwork # type: ignore
7
7
 
8
+ from cognite.neat._constants import IN_NOTEBOOK
8
9
  from cognite.neat._rules._constants import EntityTypes
9
10
  from cognite.neat._rules.models.dms._rules import DMSRules
10
11
  from cognite.neat._rules.models.entities._single_value import ClassEntity, ViewEntity
@@ -13,8 +14,10 @@ from cognite.neat._session.exceptions import NeatSessionError
13
14
  from cognite.neat._utils.rdf_ import remove_namespace_from_uri
14
15
 
15
16
  from ._state import SessionState
17
+ from .exceptions import intercept_session_exceptions
16
18
 
17
19
 
20
+ @intercept_session_exceptions
18
21
  class ShowAPI:
19
22
  def __init__(self, state: SessionState) -> None:
20
23
  self._state = state
@@ -22,253 +25,204 @@ class ShowAPI:
22
25
  self.instances = ShowInstanceAPI(self._state)
23
26
 
24
27
 
25
- class ShowInstanceAPI:
28
+ @intercept_session_exceptions
29
+ class ShowBaseAPI:
26
30
  def __init__(self, state: SessionState) -> None:
27
31
  self._state = state
28
32
 
29
- def __call__(self) -> Any:
30
- if not self._state.store.graph:
31
- raise NeatSessionError("No instances available. Try using [bold].read[/bold] to load instances.")
32
-
33
- widget = CytoscapeWidget()
34
- widget.layout.height = "700px"
35
-
36
- NxGraph, types = self._generate_instance_di_graph_and_types()
37
- widget_style = self._generate_cytoscape_widget_style(types)
38
- widget.set_style(widget_style)
39
-
40
- widget.graph.add_graph_from_networkx(NxGraph)
41
- print("Max of 100 nodes and edges are displayed, which are randomly selected.")
42
-
43
- return display(widget)
44
-
45
- def _generate_instance_di_graph_and_types(self) -> tuple[nx.DiGraph, set[str]]:
46
- query = """
47
- SELECT ?s ?p ?o ?ts ?to WHERE {
48
- ?s ?p ?o .
49
- FILTER(isIRI(?o)) # Example filter to check if ?o is an IRI (object type)
50
- FILTER(BOUND(?o))
51
- FILTER(?p != rdf:type)
52
-
53
- ?s a ?ts .
54
- ?o a ?to .
55
- }
56
- LIMIT 100
57
- """
58
-
59
- NxGraph = nx.DiGraph()
60
-
61
- types = set()
62
-
63
- for ( # type: ignore
64
- subject,
65
- property_,
66
- object,
67
- subject_type,
68
- object_type,
69
- ) in self._state.store.graph.query(query):
70
- subject = remove_namespace_from_uri(subject)
71
- property_ = remove_namespace_from_uri(property_)
72
- object = remove_namespace_from_uri(object)
73
- subject_type = remove_namespace_from_uri(subject_type)
74
- object_type = remove_namespace_from_uri(object_type)
75
-
76
- NxGraph.add_node(subject, label=subject, type=subject_type)
77
- NxGraph.add_node(object, label=object, type=object_type)
78
- NxGraph.add_edge(subject, object, label=property_)
79
-
80
- types.add(subject_type)
81
- types.add(object_type)
82
-
83
- return NxGraph, types
84
-
85
- def _generate_cytoscape_widget_style(self, types: set[str]) -> list[dict]:
86
- widget_style = [
87
- {
88
- "selector": "edge",
89
- "style": {
90
- "width": 1,
91
- "target-arrow-shape": "triangle",
92
- "curve-style": "bezier",
93
- "label": "data(label)",
94
- "font-size": "8px",
95
- "line-color": "black",
96
- "target-arrow-color": "black",
97
- },
98
- },
99
- ]
100
-
101
- colors = self._generate_hex_colors(len(types))
102
-
103
- for i, type_ in enumerate(types):
104
- widget_style.append(self._generate_node_cytoscape_style(type_, colors[i]))
105
-
106
- return widget_style
107
-
108
- @staticmethod
109
- def _generate_hex_colors(n: int) -> list[str]:
110
- """Generate a list of N random HEX color codes."""
111
- random.seed(42) # Set a seed for deterministic behavior
112
- hex_colors = []
113
- for _ in range(n):
114
- color = f"#{random.randint(0, 0xFFFFFF):06x}"
115
- hex_colors.append(color)
116
- return hex_colors
117
-
118
- @staticmethod
119
- def _generate_node_cytoscape_style(type_: str, color: str) -> dict:
120
- template = {
121
- "css": {
122
- "content": "data(label)",
123
- "text-valign": "center",
124
- "color": "black",
125
- "font-size": "10px",
126
- "width": "mapData(score, 0, 1, 10, 50)",
127
- "height": "mapData(score, 0, 1, 10, 50)",
128
- },
129
- }
33
+ def _generate_visualization(self, di_graph: nx.DiGraph, name: str) -> Any:
34
+ net = PyVisNetwork(
35
+ notebook=IN_NOTEBOOK,
36
+ cdn_resources="remote",
37
+ directed=True,
38
+ height="750px",
39
+ width="100%",
40
+ select_menu=IN_NOTEBOOK,
41
+ )
130
42
 
131
- template["selector"] = f'node[type = "{type_}"]' # type: ignore
132
- template["css"]["background-color"] = color
43
+ # Change the plotting layout
44
+ net.repulsion(
45
+ node_distance=100,
46
+ central_gravity=0.3,
47
+ spring_length=200,
48
+ spring_strength=0.05,
49
+ damping=0.09,
50
+ )
133
51
 
134
- return template
52
+ net.from_nx(di_graph)
53
+ return net.show(name)
135
54
 
136
55
 
137
- class ShowDataModelAPI:
56
+ @intercept_session_exceptions
57
+ class ShowDataModelAPI(ShowBaseAPI):
138
58
  def __init__(self, state: SessionState) -> None:
59
+ super().__init__(state)
139
60
  self._state = state
140
61
 
141
62
  def __call__(self) -> Any:
142
- if not self._state.last_verified_dms_rules and not self._state.last_verified_information_rules:
63
+ if not self._state.has_verified_rules:
143
64
  raise NeatSessionError(
144
65
  "No verified data model available. Try using [bold].verify()[/bold] to verify data model."
145
66
  )
146
67
 
147
- if self._state.last_verified_dms_rules:
148
- NxGraph = self._generate_dms_di_graph()
149
- elif self._state.last_verified_information_rules:
150
- NxGraph = self._generate_info_di_graph()
68
+ try:
69
+ di_graph = self._generate_dms_di_graph(self._state.last_verified_dms_rules)
70
+ name = "dms_data_model.html"
71
+ except NeatSessionError:
72
+ di_graph = self._generate_info_di_graph(self._state.last_verified_information_rules)
73
+ name = "information_data_model.html"
151
74
 
152
- widget = self._generate_widget()
153
- widget.graph.add_graph_from_networkx(NxGraph)
154
- return display(widget)
75
+ return self._generate_visualization(di_graph, name)
155
76
 
156
- def _generate_dms_di_graph(self) -> nx.DiGraph:
77
+ def _generate_dms_di_graph(self, rules: DMSRules) -> nx.DiGraph:
157
78
  """Generate a DiGraph from the last verified DMS rules."""
158
- NxGraph = nx.DiGraph()
79
+ di_graph = nx.DiGraph()
159
80
 
160
81
  # Add nodes and edges from Views sheet
161
- for view in cast(DMSRules, self._state.last_verified_dms_rules).views:
82
+ for view in rules.views:
162
83
  # if possible use human readable label coming from the view name
163
- if not NxGraph.has_node(view.view.suffix):
164
- NxGraph.add_node(view.view.suffix, label=view.name or view.view.suffix)
84
+ if not di_graph.has_node(view.view.suffix):
85
+ di_graph.add_node(view.view.suffix, label=view.name or view.view.suffix)
165
86
 
166
87
  # add implements as edges
167
88
  if view.implements:
168
89
  for implement in view.implements:
169
- if not NxGraph.has_node(implement.suffix):
170
- NxGraph.add_node(implement.suffix, label=implement.suffix)
90
+ if not di_graph.has_node(implement.suffix):
91
+ di_graph.add_node(implement.suffix, label=implement.suffix)
171
92
 
172
- NxGraph.add_edge(view.view.suffix, implement.suffix, label="implements")
93
+ di_graph.add_edge(
94
+ view.view.suffix,
95
+ implement.suffix,
96
+ label="implements",
97
+ dashes=True,
98
+ )
173
99
 
174
100
  # Add nodes and edges from Properties sheet
175
- for prop_ in cast(DMSRules, self._state.last_verified_dms_rules).properties:
101
+ for prop_ in rules.properties:
176
102
  if prop_.connection and isinstance(prop_.value_type, ViewEntity):
177
- if not NxGraph.has_node(prop_.view.suffix):
178
- NxGraph.add_node(prop_.view.suffix, label=prop_.view.suffix)
179
-
180
- label = f"{prop_.property_} [{0 if prop_.nullable else 1}..{ '' if prop_.is_list else 1}]"
181
- NxGraph.add_edge(prop_.view.suffix, prop_.value_type.suffix, label=label)
103
+ if not di_graph.has_node(prop_.view.suffix):
104
+ di_graph.add_node(prop_.view.suffix, label=prop_.view.suffix)
105
+ di_graph.add_edge(
106
+ prop_.view.suffix,
107
+ prop_.value_type.suffix,
108
+ label=prop_.name or prop_.property_,
109
+ )
182
110
 
183
- return NxGraph
111
+ return di_graph
184
112
 
185
- def _generate_info_di_graph(self) -> nx.DiGraph:
186
- """Generate nodes and edges for the last verified Information rules for DiGraph."""
113
+ def _generate_info_di_graph(self, rules: InformationRules) -> nx.DiGraph:
114
+ """Generate DiGraph representing information data model."""
187
115
 
188
- NxGraph = nx.DiGraph()
116
+ di_graph = nx.DiGraph()
189
117
 
190
118
  # Add nodes and edges from Views sheet
191
- for class_ in cast(InformationRules, self._state.last_verified_information_rules).classes:
119
+ for class_ in rules.classes:
192
120
  # if possible use human readable label coming from the view name
193
- if not NxGraph.has_node(class_.class_.suffix):
194
- NxGraph.add_node(
121
+ if not di_graph.has_node(class_.class_.suffix):
122
+ di_graph.add_node(
195
123
  class_.class_.suffix,
196
124
  label=class_.name or class_.class_.suffix,
197
125
  )
198
126
 
199
- # add implements as edges
127
+ # add subClassOff as edges
200
128
  if class_.parent:
201
129
  for parent in class_.parent:
202
- if not NxGraph.has_node(parent.suffix):
203
- NxGraph.add_node(parent.suffix, label=parent.suffix)
204
-
205
- NxGraph.add_edge(class_.class_.suffix, parent.suffix, label="subClassOf")
130
+ if not di_graph.has_node(parent.suffix):
131
+ di_graph.add_node(parent.suffix, label=parent.suffix)
132
+ di_graph.add_edge(
133
+ class_.class_.suffix,
134
+ parent.suffix,
135
+ label="subClassOf",
136
+ dashes=True,
137
+ )
206
138
 
207
139
  # Add nodes and edges from Properties sheet
208
- for prop_ in cast(InformationRules, self._state.last_verified_information_rules).properties:
140
+ for prop_ in rules.properties:
209
141
  if prop_.type_ == EntityTypes.object_property:
210
- if not NxGraph.has_node(prop_.class_.suffix):
211
- NxGraph.add_node(prop_.class_.suffix, label=prop_.class_.suffix)
142
+ if not di_graph.has_node(prop_.class_.suffix):
143
+ di_graph.add_node(prop_.class_.suffix, label=prop_.class_.suffix)
212
144
 
213
- label = f"{prop_.property_} [{1 if prop_.is_mandatory else 0}..{ '' if prop_.is_list else 1}]"
214
- NxGraph.add_edge(
145
+ di_graph.add_edge(
215
146
  prop_.class_.suffix,
216
147
  cast(ClassEntity, prop_.value_type).suffix,
217
- label=label,
148
+ label=prop_.name or prop_.property_,
218
149
  )
219
150
 
220
- return NxGraph
221
-
222
- def _generate_widget(self):
223
- """Generates an empty a CytoscapeWidget."""
224
- widget = CytoscapeWidget()
225
- widget.layout.height = "700px"
226
-
227
- widget.set_style(
228
- [
229
- {
230
- "selector": "node",
231
- "css": {
232
- "content": "data(label)",
233
- "text-valign": "center",
234
- "color": "black",
235
- "background-color": "#33C4FF",
236
- "font-size": "10px",
237
- "width": "mapData(score, 0, 1, 10, 50)",
238
- "height": "mapData(score, 0, 1, 10, 50)",
239
- },
240
- },
241
- {
242
- "selector": "edge",
243
- "style": {
244
- "width": 1,
245
- "target-arrow-shape": "triangle",
246
- "curve-style": "bezier",
247
- "label": "data(label)",
248
- "font-size": "8px",
249
- "line-color": "black",
250
- "target-arrow-color": "black",
251
- },
252
- },
253
- {
254
- "selector": 'edge[label = "subClassOf"]',
255
- "style": {
256
- "line-color": "grey",
257
- "target-arrow-color": "grey",
258
- "line-style": "dashed",
259
- "font-size": "8px",
260
- },
261
- },
262
- {
263
- "selector": 'edge[label = "implements"]',
264
- "style": {
265
- "line-color": "grey",
266
- "target-arrow-color": "grey",
267
- "line-style": "dashed",
268
- "font-size": "8px",
269
- },
270
- },
271
- ]
272
- )
151
+ return di_graph
152
+
153
+
154
+ @intercept_session_exceptions
155
+ class ShowInstanceAPI(ShowBaseAPI):
156
+ def __init__(self, state: SessionState) -> None:
157
+ super().__init__(state)
158
+ self._state = state
159
+
160
+ def __call__(self) -> Any:
161
+ if not self._state.store.graph:
162
+ raise NeatSessionError("No instances available. Try using [bold].read[/bold] to load instances.")
163
+
164
+ di_graph = self._generate_instance_di_graph_and_types()
165
+ return self._generate_visualization(di_graph, name="instances.html")
166
+
167
+ def _generate_instance_di_graph_and_types(self) -> nx.DiGraph:
168
+ query = """
169
+ SELECT ?s ?p ?o ?ts ?to WHERE {
170
+ ?s ?p ?o .
171
+ FILTER(isIRI(?o)) # Example filter to check if ?o is an IRI (object type)
172
+ FILTER(BOUND(?o))
173
+ FILTER(?p != rdf:type)
273
174
 
274
- return widget
175
+ ?s a ?ts .
176
+ ?o a ?to .
177
+ }
178
+ LIMIT 200
179
+ """
180
+
181
+ di_graph = nx.DiGraph()
182
+
183
+ types = [type_ for type_, _ in self._state.store.queries.summarize_instances()]
184
+ hex_colored_types = self._generate_hex_color_per_type(types)
185
+
186
+ for ( # type: ignore
187
+ subject,
188
+ property_,
189
+ object,
190
+ subject_type,
191
+ object_type,
192
+ ) in self._state.store.graph.query(query):
193
+ subject = remove_namespace_from_uri(subject)
194
+ property_ = remove_namespace_from_uri(property_)
195
+ object = remove_namespace_from_uri(object)
196
+ subject_type = remove_namespace_from_uri(subject_type)
197
+ object_type = remove_namespace_from_uri(object_type)
198
+
199
+ di_graph.add_node(
200
+ subject,
201
+ label=subject,
202
+ type=subject_type,
203
+ title=subject_type,
204
+ color=hex_colored_types[subject_type],
205
+ )
206
+ di_graph.add_node(
207
+ object,
208
+ label=object,
209
+ type=object_type,
210
+ title=object_type,
211
+ color=hex_colored_types[object_type],
212
+ )
213
+ di_graph.add_edge(subject, object, label=property_, color="grey")
214
+
215
+ return di_graph
216
+
217
+ @staticmethod
218
+ def _generate_hex_color_per_type(types: list[str]) -> dict:
219
+ hex_colored_types = {}
220
+ random.seed(381)
221
+ for type_ in types:
222
+ hue = random.random()
223
+ saturation = random.uniform(0.5, 1.0)
224
+ lightness = random.uniform(0.4, 0.6)
225
+ rgb = colorsys.hls_to_rgb(hue, lightness, saturation)
226
+ hex_color = f"#{int(rgb[0] * 255):02x}{int(rgb[1] * 255):02x}{int(rgb[2] * 255):02x}"
227
+ hex_colored_types[type_] = hex_color
228
+ 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]
@@ -8,8 +8,10 @@ from cognite.neat._rules import exporters
8
8
  from cognite.neat._session._wizard import space_wizard
9
9
 
10
10
  from ._state import SessionState
11
+ from .exceptions import intercept_session_exceptions
11
12
 
12
13
 
14
+ @intercept_session_exceptions
13
15
  class ToAPI:
14
16
  def __init__(self, state: SessionState, client: CogniteClient | None, verbose: bool) -> None:
15
17
  self._state = state
@@ -39,6 +41,7 @@ class ToAPI:
39
41
  return None
40
42
 
41
43
 
44
+ @intercept_session_exceptions
42
45
  class CDFToAPI:
43
46
  def __init__(self, state: SessionState, client: CogniteClient | None, verbose: bool) -> None:
44
47
  self._client = client
@@ -23,6 +23,8 @@ def _intercept_session_exceptions(func: Callable):
23
23
  return func(*args, **kwargs)
24
24
  except NeatSessionError as e:
25
25
  action = func.__name__
26
+ if action == "__call__":
27
+ action = func.__qualname__.removesuffix(".__call__").removesuffix("API")
26
28
  print(f"{_PREFIX} Cannot {action}: {e}")
27
29
 
28
30
  return wrapper
@@ -33,10 +35,11 @@ def intercept_session_exceptions(cls: type):
33
35
  while to_check:
34
36
  cls = to_check.pop()
35
37
  for attr_name in dir(cls):
36
- if not attr_name.startswith("_"):
37
- attr = getattr(cls, attr_name)
38
- if callable(attr):
39
- setattr(cls, attr_name, _intercept_session_exceptions(attr))
40
- elif isinstance(attr, type):
41
- to_check.append(attr)
38
+ if attr_name.startswith("_") and not attr_name == "__call__":
39
+ continue
40
+ attr = getattr(cls, attr_name)
41
+ if callable(attr):
42
+ setattr(cls, attr_name, _intercept_session_exceptions(attr))
43
+ elif isinstance(attr, type):
44
+ to_check.append(attr)
42
45
  return cls
@@ -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.94.0"
1
+ __version__ = "0.96.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cognite-neat
3
- Version: 0.94.0
3
+ Version: 0.96.0
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.9.2,<4.0.0)
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"