powerbi-ontology-extractor 0.1.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.
- cli/__init__.py +1 -0
- cli/pbi_ontology_cli.py +286 -0
- powerbi_ontology/__init__.py +38 -0
- powerbi_ontology/analyzer.py +420 -0
- powerbi_ontology/chat.py +303 -0
- powerbi_ontology/cli.py +530 -0
- powerbi_ontology/contract_builder.py +269 -0
- powerbi_ontology/dax_parser.py +305 -0
- powerbi_ontology/export/__init__.py +17 -0
- powerbi_ontology/export/contract_to_owl.py +408 -0
- powerbi_ontology/export/fabric_iq.py +243 -0
- powerbi_ontology/export/fabric_iq_to_owl.py +463 -0
- powerbi_ontology/export/json_schema.py +110 -0
- powerbi_ontology/export/ontoguard.py +177 -0
- powerbi_ontology/export/owl.py +522 -0
- powerbi_ontology/extractor.py +368 -0
- powerbi_ontology/mcp_config.py +237 -0
- powerbi_ontology/mcp_models.py +166 -0
- powerbi_ontology/mcp_server.py +1106 -0
- powerbi_ontology/ontology_diff.py +776 -0
- powerbi_ontology/ontology_generator.py +406 -0
- powerbi_ontology/review.py +556 -0
- powerbi_ontology/schema_mapper.py +369 -0
- powerbi_ontology/semantic_debt.py +584 -0
- powerbi_ontology/utils/__init__.py +13 -0
- powerbi_ontology/utils/pbix_reader.py +558 -0
- powerbi_ontology/utils/visualizer.py +332 -0
- powerbi_ontology_extractor-0.1.0.dist-info/METADATA +507 -0
- powerbi_ontology_extractor-0.1.0.dist-info/RECORD +33 -0
- powerbi_ontology_extractor-0.1.0.dist-info/WHEEL +5 -0
- powerbi_ontology_extractor-0.1.0.dist-info/entry_points.txt +4 -0
- powerbi_ontology_extractor-0.1.0.dist-info/licenses/LICENSE +21 -0
- powerbi_ontology_extractor-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ontology Visualizer
|
|
3
|
+
|
|
4
|
+
Creates visualizations of ontologies using networkx, matplotlib, and plotly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import networkx as nx
|
|
12
|
+
import matplotlib.pyplot as plt
|
|
13
|
+
import plotly.graph_objects as go
|
|
14
|
+
import plotly.express as px
|
|
15
|
+
except ImportError:
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
logger.warning("Visualization libraries not installed. Install with: pip install networkx matplotlib plotly")
|
|
18
|
+
|
|
19
|
+
from powerbi_ontology.ontology_generator import Ontology, OntologyEntity, OntologyRelationship
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OntologyVisualizer:
|
|
25
|
+
"""
|
|
26
|
+
Visualizes ontologies as entity-relationship diagrams and interactive graphs.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, ontology: Ontology):
|
|
30
|
+
"""
|
|
31
|
+
Initialize visualizer.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
ontology: Ontology to visualize
|
|
35
|
+
"""
|
|
36
|
+
self.ontology = ontology
|
|
37
|
+
self.graph = self._build_graph()
|
|
38
|
+
|
|
39
|
+
def _build_graph(self):
|
|
40
|
+
"""Build networkx graph from ontology."""
|
|
41
|
+
try:
|
|
42
|
+
import networkx as nx
|
|
43
|
+
except ImportError:
|
|
44
|
+
logger.error("networkx not installed. Cannot build graph.")
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
G = nx.DiGraph()
|
|
48
|
+
|
|
49
|
+
# Add entities as nodes
|
|
50
|
+
for entity in self.ontology.entities:
|
|
51
|
+
G.add_node(
|
|
52
|
+
entity.name,
|
|
53
|
+
node_type=entity.entity_type,
|
|
54
|
+
description=entity.description,
|
|
55
|
+
property_count=len(entity.properties)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Add relationships as edges
|
|
59
|
+
for rel in self.ontology.relationships:
|
|
60
|
+
G.add_edge(
|
|
61
|
+
rel.from_entity,
|
|
62
|
+
rel.to_entity,
|
|
63
|
+
relationship_type=rel.relationship_type,
|
|
64
|
+
cardinality=rel.cardinality,
|
|
65
|
+
label=rel.relationship_type
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return G
|
|
69
|
+
|
|
70
|
+
def plot_entity_relationship_diagram(self) -> Optional[object]:
|
|
71
|
+
"""
|
|
72
|
+
Create entity-relationship diagram using matplotlib.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
matplotlib Figure object
|
|
76
|
+
"""
|
|
77
|
+
if not self.graph:
|
|
78
|
+
logger.error("Graph not built. Cannot create diagram.")
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
import matplotlib.pyplot as plt
|
|
83
|
+
import networkx as nx
|
|
84
|
+
except ImportError:
|
|
85
|
+
logger.error("matplotlib or networkx not installed.")
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
fig, ax = plt.subplots(figsize=(12, 8))
|
|
89
|
+
|
|
90
|
+
# Layout
|
|
91
|
+
pos = nx.spring_layout(self.graph, k=2, iterations=50)
|
|
92
|
+
|
|
93
|
+
# Color nodes by entity type
|
|
94
|
+
node_colors = []
|
|
95
|
+
for node in self.graph.nodes():
|
|
96
|
+
node_type = self.graph.nodes[node].get('node_type', 'standard')
|
|
97
|
+
if node_type == 'dimension':
|
|
98
|
+
node_colors.append('#90EE90') # Light green
|
|
99
|
+
elif node_type == 'fact':
|
|
100
|
+
node_colors.append('#87CEEB') # Sky blue
|
|
101
|
+
elif node_type == 'date':
|
|
102
|
+
node_colors.append('#FFB6C1') # Light pink
|
|
103
|
+
else:
|
|
104
|
+
node_colors.append('#D3D3D3') # Light gray
|
|
105
|
+
|
|
106
|
+
# Draw nodes
|
|
107
|
+
nx.draw_networkx_nodes(
|
|
108
|
+
self.graph, pos,
|
|
109
|
+
node_color=node_colors,
|
|
110
|
+
node_size=2000,
|
|
111
|
+
alpha=0.9,
|
|
112
|
+
ax=ax
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Draw edges
|
|
116
|
+
nx.draw_networkx_edges(
|
|
117
|
+
self.graph, pos,
|
|
118
|
+
edge_color='gray',
|
|
119
|
+
arrows=True,
|
|
120
|
+
arrowsize=20,
|
|
121
|
+
alpha=0.6,
|
|
122
|
+
ax=ax
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Draw labels
|
|
126
|
+
nx.draw_networkx_labels(
|
|
127
|
+
self.graph, pos,
|
|
128
|
+
font_size=10,
|
|
129
|
+
font_weight='bold',
|
|
130
|
+
ax=ax
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Draw edge labels
|
|
134
|
+
edge_labels = {
|
|
135
|
+
(u, v): self.graph.edges[u, v].get('label', '')
|
|
136
|
+
for u, v in self.graph.edges()
|
|
137
|
+
}
|
|
138
|
+
nx.draw_networkx_edge_labels(
|
|
139
|
+
self.graph, pos,
|
|
140
|
+
edge_labels=edge_labels,
|
|
141
|
+
font_size=8,
|
|
142
|
+
ax=ax
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
ax.set_title(f"Entity-Relationship Diagram: {self.ontology.name}", fontsize=16, fontweight='bold')
|
|
146
|
+
ax.axis('off')
|
|
147
|
+
|
|
148
|
+
plt.tight_layout()
|
|
149
|
+
return fig
|
|
150
|
+
|
|
151
|
+
def generate_interactive_graph(self) -> Optional[object]:
|
|
152
|
+
"""
|
|
153
|
+
Generate interactive graph using plotly.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
plotly Figure object
|
|
157
|
+
"""
|
|
158
|
+
if not self.graph:
|
|
159
|
+
logger.error("Graph not built. Cannot create interactive graph.")
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
import plotly.graph_objects as go
|
|
164
|
+
import networkx as nx
|
|
165
|
+
except ImportError:
|
|
166
|
+
logger.error("plotly or networkx not installed.")
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
# Get positions
|
|
170
|
+
pos = nx.spring_layout(self.graph, k=2, iterations=50)
|
|
171
|
+
|
|
172
|
+
# Prepare edge traces
|
|
173
|
+
edge_x = []
|
|
174
|
+
edge_y = []
|
|
175
|
+
edge_info = []
|
|
176
|
+
for edge in self.graph.edges():
|
|
177
|
+
x0, y0 = pos[edge[0]]
|
|
178
|
+
x1, y1 = pos[edge[1]]
|
|
179
|
+
edge_x.extend([x0, x1, None])
|
|
180
|
+
edge_y.extend([y0, y1, None])
|
|
181
|
+
edge_info.append(self.graph.edges[edge].get('relationship_type', ''))
|
|
182
|
+
|
|
183
|
+
edge_trace = go.Scatter(
|
|
184
|
+
x=edge_x, y=edge_y,
|
|
185
|
+
line=dict(width=2, color='#888'),
|
|
186
|
+
hoverinfo='none',
|
|
187
|
+
mode='lines'
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Prepare node traces
|
|
191
|
+
node_x = []
|
|
192
|
+
node_y = []
|
|
193
|
+
node_text = []
|
|
194
|
+
node_info = []
|
|
195
|
+
node_colors = []
|
|
196
|
+
|
|
197
|
+
for node in self.graph.nodes():
|
|
198
|
+
x, y = pos[node]
|
|
199
|
+
node_x.append(x)
|
|
200
|
+
node_y.append(y)
|
|
201
|
+
|
|
202
|
+
node_data = self.graph.nodes[node]
|
|
203
|
+
node_type = node_data.get('node_type', 'standard')
|
|
204
|
+
prop_count = node_data.get('property_count', 0)
|
|
205
|
+
|
|
206
|
+
node_text.append(f"{node}<br>{node_type}<br>{prop_count} properties")
|
|
207
|
+
node_info.append(f"Type: {node_type}<br>Properties: {prop_count}")
|
|
208
|
+
|
|
209
|
+
# Color by type
|
|
210
|
+
if node_type == 'dimension':
|
|
211
|
+
node_colors.append('#90EE90')
|
|
212
|
+
elif node_type == 'fact':
|
|
213
|
+
node_colors.append('#87CEEB')
|
|
214
|
+
elif node_type == 'date':
|
|
215
|
+
node_colors.append('#FFB6C1')
|
|
216
|
+
else:
|
|
217
|
+
node_colors.append('#D3D3D3')
|
|
218
|
+
|
|
219
|
+
node_trace = go.Scatter(
|
|
220
|
+
x=node_x, y=node_y,
|
|
221
|
+
mode='markers+text',
|
|
222
|
+
hoverinfo='text',
|
|
223
|
+
text=[node for node in self.graph.nodes()],
|
|
224
|
+
textposition="middle center",
|
|
225
|
+
hovertext=node_info,
|
|
226
|
+
marker=dict(
|
|
227
|
+
size=30,
|
|
228
|
+
color=node_colors,
|
|
229
|
+
line=dict(width=2, color='black')
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
fig = go.Figure(
|
|
234
|
+
data=[edge_trace, node_trace],
|
|
235
|
+
layout=go.Layout(
|
|
236
|
+
title=f"Interactive Ontology Graph: {self.ontology.name}",
|
|
237
|
+
titlefont_size=16,
|
|
238
|
+
showlegend=False,
|
|
239
|
+
hovermode='closest',
|
|
240
|
+
margin=dict(b=20, l=5, r=5, t=40),
|
|
241
|
+
annotations=[
|
|
242
|
+
dict(
|
|
243
|
+
text="Hover over nodes for details",
|
|
244
|
+
showarrow=False,
|
|
245
|
+
xref="paper", yref="paper",
|
|
246
|
+
x=0.005, y=-0.002,
|
|
247
|
+
xanchor="left", yanchor="bottom",
|
|
248
|
+
font=dict(color="#888", size=12)
|
|
249
|
+
)
|
|
250
|
+
],
|
|
251
|
+
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
|
|
252
|
+
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False)
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return fig
|
|
257
|
+
|
|
258
|
+
def export_mermaid_diagram(self) -> str:
|
|
259
|
+
"""
|
|
260
|
+
Export ontology as Mermaid diagram code.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Mermaid diagram code string
|
|
264
|
+
"""
|
|
265
|
+
mermaid = ["erDiagram"]
|
|
266
|
+
|
|
267
|
+
# Add entities
|
|
268
|
+
for entity in self.ontology.entities:
|
|
269
|
+
entity_name = entity.name.upper()
|
|
270
|
+
mermaid.append(f" {entity_name} {{")
|
|
271
|
+
|
|
272
|
+
# Add key properties
|
|
273
|
+
for prop in entity.properties[:5]: # Limit to 5 for readability
|
|
274
|
+
prop_type = prop.data_type.lower()
|
|
275
|
+
prop_name = prop.name
|
|
276
|
+
if prop.unique:
|
|
277
|
+
prop_name += " PK"
|
|
278
|
+
mermaid.append(f" {prop_type} {prop_name}")
|
|
279
|
+
|
|
280
|
+
if len(entity.properties) > 5:
|
|
281
|
+
mermaid.append(f" ... {len(entity.properties) - 5} more properties")
|
|
282
|
+
|
|
283
|
+
mermaid.append(" }")
|
|
284
|
+
|
|
285
|
+
# Add relationships
|
|
286
|
+
for rel in self.ontology.relationships:
|
|
287
|
+
from_entity = rel.from_entity.upper()
|
|
288
|
+
to_entity = rel.to_entity.upper()
|
|
289
|
+
rel_type = rel.relationship_type.replace("_", " ")
|
|
290
|
+
|
|
291
|
+
# Mermaid cardinality notation
|
|
292
|
+
if rel.cardinality == "one-to-many":
|
|
293
|
+
cardinality = "||--o{"
|
|
294
|
+
elif rel.cardinality == "many-to-one":
|
|
295
|
+
cardinality = "}o--||"
|
|
296
|
+
elif rel.cardinality == "one-to-one":
|
|
297
|
+
cardinality = "||--||"
|
|
298
|
+
else:
|
|
299
|
+
cardinality = "}o--o{"
|
|
300
|
+
|
|
301
|
+
mermaid.append(f" {from_entity} {cardinality} {to_entity} : \"{rel_type}\"")
|
|
302
|
+
|
|
303
|
+
return "\n".join(mermaid)
|
|
304
|
+
|
|
305
|
+
def save_as_image(self, filename: str, format: str = "png"):
|
|
306
|
+
"""
|
|
307
|
+
Save visualization as image.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
filename: Output filename
|
|
311
|
+
format: Image format ("png", "svg", "pdf")
|
|
312
|
+
"""
|
|
313
|
+
fig = self.plot_entity_relationship_diagram()
|
|
314
|
+
if fig:
|
|
315
|
+
fig.savefig(filename, format=format, dpi=300, bbox_inches='tight')
|
|
316
|
+
logger.info(f"Saved diagram to {filename}")
|
|
317
|
+
else:
|
|
318
|
+
logger.error("Could not create diagram")
|
|
319
|
+
|
|
320
|
+
def save_interactive_html(self, filename: str):
|
|
321
|
+
"""
|
|
322
|
+
Save interactive graph as HTML.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
filename: Output HTML filename
|
|
326
|
+
"""
|
|
327
|
+
fig = self.generate_interactive_graph()
|
|
328
|
+
if fig:
|
|
329
|
+
fig.write_html(filename)
|
|
330
|
+
logger.info(f"Saved interactive graph to {filename}")
|
|
331
|
+
else:
|
|
332
|
+
logger.error("Could not create interactive graph")
|