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,408 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Semantic Contract → OWL Converter
|
|
3
|
+
|
|
4
|
+
Converts SemanticContract to OntoGuard-compatible OWL format.
|
|
5
|
+
This enables AI agent contracts to be validated by OntoGuard semantic firewall.
|
|
6
|
+
|
|
7
|
+
Key mappings:
|
|
8
|
+
- read_entities → ReadAction with requiresRole/appliesTo
|
|
9
|
+
- write_properties → WriteAction with requiresRole/appliesTo
|
|
10
|
+
- executable_actions → ExecuteAction with requiresRole/appliesTo
|
|
11
|
+
- business_rules → Action classes with constraints
|
|
12
|
+
- context_filters → OWL restrictions
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Optional, Any
|
|
17
|
+
|
|
18
|
+
from rdflib import Graph, Namespace, Literal, URIRef
|
|
19
|
+
from rdflib.namespace import RDF, RDFS, OWL, XSD
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ContractToOWLConverter:
|
|
25
|
+
"""
|
|
26
|
+
Converts SemanticContract to OntoGuard-compatible OWL format.
|
|
27
|
+
|
|
28
|
+
This generates OWL action rules that can be loaded by OntoGuard
|
|
29
|
+
for validating AI agent actions against their contract.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
from powerbi_ontology.contract_builder import ContractBuilder, SemanticContract
|
|
33
|
+
|
|
34
|
+
contract = builder.build_contract("SalesAgent", permissions)
|
|
35
|
+
converter = ContractToOWLConverter(contract)
|
|
36
|
+
converter.save("sales_agent_contract.owl")
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
contract: Any, # SemanticContract
|
|
42
|
+
base_uri: Optional[str] = None,
|
|
43
|
+
ontology: Optional[Any] = None # Ontology for entity details
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Initialize converter.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
contract: SemanticContract to convert
|
|
50
|
+
base_uri: Optional base URI (defaults to agent name)
|
|
51
|
+
ontology: Optional Ontology for additional entity details
|
|
52
|
+
"""
|
|
53
|
+
self.contract = contract
|
|
54
|
+
self.ontology = ontology
|
|
55
|
+
self.graph = Graph()
|
|
56
|
+
|
|
57
|
+
# Create namespace
|
|
58
|
+
agent_name = contract.agent_name.replace(" ", "_").replace("-", "_")
|
|
59
|
+
self.base_uri = base_uri or f"http://example.org/contracts/{agent_name}#"
|
|
60
|
+
|
|
61
|
+
self.ont = Namespace(self.base_uri)
|
|
62
|
+
|
|
63
|
+
# Bind namespaces
|
|
64
|
+
self.graph.bind("ont", self.ont)
|
|
65
|
+
self.graph.bind("owl", OWL)
|
|
66
|
+
self.graph.bind("rdfs", RDFS)
|
|
67
|
+
self.graph.bind("xsd", XSD)
|
|
68
|
+
|
|
69
|
+
def convert(self, format: str = "xml") -> str:
|
|
70
|
+
"""
|
|
71
|
+
Convert SemanticContract to OWL format.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
format: Output format ("xml", "turtle", "json-ld", "n3")
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
OWL content as string
|
|
78
|
+
"""
|
|
79
|
+
logger.info(f"Converting contract '{self.contract.agent_name}' to OWL")
|
|
80
|
+
|
|
81
|
+
# Add ontology metadata
|
|
82
|
+
self._add_ontology_metadata()
|
|
83
|
+
|
|
84
|
+
# Add base classes
|
|
85
|
+
self._add_base_classes()
|
|
86
|
+
|
|
87
|
+
# Add OntoGuard properties
|
|
88
|
+
self._add_ontoguard_properties()
|
|
89
|
+
|
|
90
|
+
# Add entity classes from permissions
|
|
91
|
+
self._add_entity_classes()
|
|
92
|
+
|
|
93
|
+
# Convert read permissions to action rules
|
|
94
|
+
self._add_read_permissions()
|
|
95
|
+
|
|
96
|
+
# Convert write permissions to action rules
|
|
97
|
+
self._add_write_permissions()
|
|
98
|
+
|
|
99
|
+
# Convert executable actions
|
|
100
|
+
self._add_executable_actions()
|
|
101
|
+
|
|
102
|
+
# Convert business rules
|
|
103
|
+
self._add_business_rules()
|
|
104
|
+
|
|
105
|
+
# Add context filters as restrictions
|
|
106
|
+
self._add_context_filters()
|
|
107
|
+
|
|
108
|
+
# Add audit configuration
|
|
109
|
+
self._add_audit_config()
|
|
110
|
+
|
|
111
|
+
return self.graph.serialize(format=format)
|
|
112
|
+
|
|
113
|
+
def _add_ontology_metadata(self):
|
|
114
|
+
"""Add OWL ontology metadata."""
|
|
115
|
+
ontology_uri = URIRef(self.base_uri.rstrip("#"))
|
|
116
|
+
|
|
117
|
+
self.graph.add((ontology_uri, RDF.type, OWL.Ontology))
|
|
118
|
+
self.graph.add((ontology_uri, RDFS.label, Literal(
|
|
119
|
+
f"Contract: {self.contract.agent_name}"
|
|
120
|
+
)))
|
|
121
|
+
self.graph.add((ontology_uri, RDFS.comment, Literal(
|
|
122
|
+
f"Semantic contract for AI agent '{self.contract.agent_name}'"
|
|
123
|
+
)))
|
|
124
|
+
self.graph.add((ontology_uri, OWL.versionInfo, Literal(
|
|
125
|
+
self.contract.ontology_version
|
|
126
|
+
)))
|
|
127
|
+
|
|
128
|
+
# Add metadata
|
|
129
|
+
metadata = self.contract.metadata or {}
|
|
130
|
+
if metadata.get("created_date"):
|
|
131
|
+
self.graph.add((ontology_uri, self.ont.createdDate, Literal(
|
|
132
|
+
metadata["created_date"], datatype=XSD.dateTime
|
|
133
|
+
)))
|
|
134
|
+
if metadata.get("ontology_source"):
|
|
135
|
+
self.graph.add((ontology_uri, self.ont.ontologySource, Literal(
|
|
136
|
+
metadata["ontology_source"]
|
|
137
|
+
)))
|
|
138
|
+
|
|
139
|
+
def _add_base_classes(self):
|
|
140
|
+
"""Add base classes for OntoGuard compatibility."""
|
|
141
|
+
# User/Role class
|
|
142
|
+
user_uri = self.ont.User
|
|
143
|
+
self.graph.add((user_uri, RDF.type, OWL.Class))
|
|
144
|
+
self.graph.add((user_uri, RDFS.label, Literal("User")))
|
|
145
|
+
|
|
146
|
+
# Add the agent's required role
|
|
147
|
+
role = self.contract.permissions.required_role or "Agent"
|
|
148
|
+
role_uri = self.ont[self._safe_name(role)]
|
|
149
|
+
self.graph.add((role_uri, RDF.type, OWL.Class))
|
|
150
|
+
self.graph.add((role_uri, RDFS.subClassOf, user_uri))
|
|
151
|
+
self.graph.add((role_uri, RDFS.label, Literal(role)))
|
|
152
|
+
self.graph.add((role_uri, RDFS.comment, Literal(
|
|
153
|
+
f"Role required by agent {self.contract.agent_name}"
|
|
154
|
+
)))
|
|
155
|
+
|
|
156
|
+
# Action base class
|
|
157
|
+
action_uri = self.ont.Action
|
|
158
|
+
self.graph.add((action_uri, RDF.type, OWL.Class))
|
|
159
|
+
self.graph.add((action_uri, RDFS.label, Literal("Action")))
|
|
160
|
+
|
|
161
|
+
# Action subclasses
|
|
162
|
+
for action_type in ["ReadAction", "WriteAction", "DeleteAction", "ExecuteAction"]:
|
|
163
|
+
action_class = self.ont[action_type]
|
|
164
|
+
self.graph.add((action_class, RDF.type, OWL.Class))
|
|
165
|
+
self.graph.add((action_class, RDFS.subClassOf, action_uri))
|
|
166
|
+
self.graph.add((action_class, RDFS.label, Literal(action_type)))
|
|
167
|
+
|
|
168
|
+
def _add_ontoguard_properties(self):
|
|
169
|
+
"""Add OntoGuard action permission properties."""
|
|
170
|
+
# requiresRole
|
|
171
|
+
requires_role = self.ont.requiresRole
|
|
172
|
+
self.graph.add((requires_role, RDF.type, OWL.ObjectProperty))
|
|
173
|
+
self.graph.add((requires_role, RDFS.label, Literal("requires role")))
|
|
174
|
+
self.graph.add((requires_role, RDFS.domain, self.ont.Action))
|
|
175
|
+
self.graph.add((requires_role, RDFS.range, self.ont.User))
|
|
176
|
+
|
|
177
|
+
# appliesTo
|
|
178
|
+
applies_to = self.ont.appliesTo
|
|
179
|
+
self.graph.add((applies_to, RDF.type, OWL.ObjectProperty))
|
|
180
|
+
self.graph.add((applies_to, RDFS.label, Literal("applies to")))
|
|
181
|
+
self.graph.add((applies_to, RDFS.domain, self.ont.Action))
|
|
182
|
+
self.graph.add((applies_to, RDFS.range, OWL.Thing))
|
|
183
|
+
|
|
184
|
+
# allowsAction
|
|
185
|
+
allows_action = self.ont.allowsAction
|
|
186
|
+
self.graph.add((allows_action, RDF.type, OWL.DatatypeProperty))
|
|
187
|
+
self.graph.add((allows_action, RDFS.label, Literal("allows action")))
|
|
188
|
+
self.graph.add((allows_action, RDFS.domain, self.ont.Action))
|
|
189
|
+
self.graph.add((allows_action, RDFS.range, XSD.string))
|
|
190
|
+
|
|
191
|
+
# appliesToProperty (for write permissions)
|
|
192
|
+
applies_to_prop = self.ont.appliesToProperty
|
|
193
|
+
self.graph.add((applies_to_prop, RDF.type, OWL.DatatypeProperty))
|
|
194
|
+
self.graph.add((applies_to_prop, RDFS.label, Literal("applies to property")))
|
|
195
|
+
self.graph.add((applies_to_prop, RDFS.domain, self.ont.Action))
|
|
196
|
+
self.graph.add((applies_to_prop, RDFS.range, XSD.string))
|
|
197
|
+
|
|
198
|
+
# hasContextFilter
|
|
199
|
+
has_filter = self.ont.hasContextFilter
|
|
200
|
+
self.graph.add((has_filter, RDF.type, OWL.DatatypeProperty))
|
|
201
|
+
self.graph.add((has_filter, RDFS.label, Literal("has context filter")))
|
|
202
|
+
self.graph.add((has_filter, RDFS.domain, self.ont.Action))
|
|
203
|
+
self.graph.add((has_filter, RDFS.range, XSD.string))
|
|
204
|
+
|
|
205
|
+
def _add_entity_classes(self):
|
|
206
|
+
"""Add entity classes from permissions."""
|
|
207
|
+
# Collect all entities from permissions
|
|
208
|
+
entities = set(self.contract.permissions.read_entities)
|
|
209
|
+
entities.update(self.contract.permissions.write_properties.keys())
|
|
210
|
+
|
|
211
|
+
for entity_name in entities:
|
|
212
|
+
entity_uri = self.ont[self._safe_name(entity_name)]
|
|
213
|
+
self.graph.add((entity_uri, RDF.type, OWL.Class))
|
|
214
|
+
self.graph.add((entity_uri, RDFS.label, Literal(entity_name)))
|
|
215
|
+
|
|
216
|
+
# If we have the ontology, add more details
|
|
217
|
+
if self.ontology:
|
|
218
|
+
ont_entity = next(
|
|
219
|
+
(e for e in self.ontology.entities if e.name == entity_name),
|
|
220
|
+
None
|
|
221
|
+
)
|
|
222
|
+
if ont_entity and ont_entity.description:
|
|
223
|
+
self.graph.add((entity_uri, RDFS.comment, Literal(ont_entity.description)))
|
|
224
|
+
|
|
225
|
+
def _add_read_permissions(self):
|
|
226
|
+
"""Convert read_entities to ReadAction rules."""
|
|
227
|
+
role = self.contract.permissions.required_role or "Agent"
|
|
228
|
+
role_uri = self.ont[self._safe_name(role)]
|
|
229
|
+
|
|
230
|
+
for entity_name in self.contract.permissions.read_entities:
|
|
231
|
+
entity_uri = self.ont[self._safe_name(entity_name)]
|
|
232
|
+
|
|
233
|
+
# Create action individual
|
|
234
|
+
action_name = f"read_{self._safe_name(entity_name)}"
|
|
235
|
+
action_uri = self.ont[action_name]
|
|
236
|
+
|
|
237
|
+
self.graph.add((action_uri, RDF.type, self.ont.ReadAction))
|
|
238
|
+
self.graph.add((action_uri, RDFS.label, Literal(f"Read {entity_name}")))
|
|
239
|
+
self.graph.add((action_uri, self.ont.allowsAction, Literal("read")))
|
|
240
|
+
self.graph.add((action_uri, self.ont.appliesTo, entity_uri))
|
|
241
|
+
self.graph.add((action_uri, self.ont.requiresRole, role_uri))
|
|
242
|
+
|
|
243
|
+
# Add context filter if exists
|
|
244
|
+
context_filter = self.contract.permissions.context_filters.get(entity_name)
|
|
245
|
+
if context_filter:
|
|
246
|
+
self.graph.add((action_uri, self.ont.hasContextFilter, Literal(context_filter)))
|
|
247
|
+
|
|
248
|
+
def _add_write_permissions(self):
|
|
249
|
+
"""Convert write_properties to WriteAction rules."""
|
|
250
|
+
role = self.contract.permissions.required_role or "Agent"
|
|
251
|
+
role_uri = self.ont[self._safe_name(role)]
|
|
252
|
+
|
|
253
|
+
for entity_name, properties in self.contract.permissions.write_properties.items():
|
|
254
|
+
entity_uri = self.ont[self._safe_name(entity_name)]
|
|
255
|
+
|
|
256
|
+
# Create write action for each property
|
|
257
|
+
for prop_name in properties:
|
|
258
|
+
action_name = f"write_{self._safe_name(entity_name)}_{self._safe_name(prop_name)}"
|
|
259
|
+
action_uri = self.ont[action_name]
|
|
260
|
+
|
|
261
|
+
self.graph.add((action_uri, RDF.type, self.ont.WriteAction))
|
|
262
|
+
self.graph.add((action_uri, RDFS.label, Literal(f"Write {entity_name}.{prop_name}")))
|
|
263
|
+
self.graph.add((action_uri, self.ont.allowsAction, Literal("write")))
|
|
264
|
+
self.graph.add((action_uri, self.ont.appliesTo, entity_uri))
|
|
265
|
+
self.graph.add((action_uri, self.ont.appliesToProperty, Literal(prop_name)))
|
|
266
|
+
self.graph.add((action_uri, self.ont.requiresRole, role_uri))
|
|
267
|
+
|
|
268
|
+
# Also create general update action for entity
|
|
269
|
+
update_action_name = f"update_{self._safe_name(entity_name)}"
|
|
270
|
+
update_action_uri = self.ont[update_action_name]
|
|
271
|
+
|
|
272
|
+
self.graph.add((update_action_uri, RDF.type, self.ont.WriteAction))
|
|
273
|
+
self.graph.add((update_action_uri, RDFS.label, Literal(f"Update {entity_name}")))
|
|
274
|
+
self.graph.add((update_action_uri, self.ont.allowsAction, Literal("update")))
|
|
275
|
+
self.graph.add((update_action_uri, self.ont.appliesTo, entity_uri))
|
|
276
|
+
self.graph.add((update_action_uri, self.ont.requiresRole, role_uri))
|
|
277
|
+
|
|
278
|
+
def _add_executable_actions(self):
|
|
279
|
+
"""Convert executable_actions to ExecuteAction rules."""
|
|
280
|
+
role = self.contract.permissions.required_role or "Agent"
|
|
281
|
+
role_uri = self.ont[self._safe_name(role)]
|
|
282
|
+
|
|
283
|
+
for action_name in self.contract.permissions.executable_actions:
|
|
284
|
+
safe_action = self._safe_name(action_name)
|
|
285
|
+
|
|
286
|
+
# Create action class
|
|
287
|
+
action_class_uri = self.ont[f"{safe_action}Action"]
|
|
288
|
+
self.graph.add((action_class_uri, RDF.type, OWL.Class))
|
|
289
|
+
self.graph.add((action_class_uri, RDFS.subClassOf, self.ont.ExecuteAction))
|
|
290
|
+
self.graph.add((action_class_uri, RDFS.label, Literal(action_name)))
|
|
291
|
+
|
|
292
|
+
# Create action individual
|
|
293
|
+
action_uri = self.ont[f"execute_{safe_action}"]
|
|
294
|
+
self.graph.add((action_uri, RDF.type, action_class_uri))
|
|
295
|
+
self.graph.add((action_uri, RDFS.label, Literal(f"Execute {action_name}")))
|
|
296
|
+
self.graph.add((action_uri, self.ont.allowsAction, Literal("execute")))
|
|
297
|
+
self.graph.add((action_uri, self.ont.requiresRole, role_uri))
|
|
298
|
+
|
|
299
|
+
def _add_business_rules(self):
|
|
300
|
+
"""Convert business rules to OWL action rules."""
|
|
301
|
+
for rule in self.contract.business_rules:
|
|
302
|
+
safe_name = self._safe_name(rule.name)
|
|
303
|
+
|
|
304
|
+
# Create action class for the rule
|
|
305
|
+
rule_class_uri = self.ont[f"{safe_name}Rule"]
|
|
306
|
+
self.graph.add((rule_class_uri, RDF.type, OWL.Class))
|
|
307
|
+
self.graph.add((rule_class_uri, RDFS.subClassOf, self.ont.Action))
|
|
308
|
+
self.graph.add((rule_class_uri, RDFS.label, Literal(rule.name)))
|
|
309
|
+
|
|
310
|
+
if rule.description:
|
|
311
|
+
self.graph.add((rule_class_uri, RDFS.comment, Literal(rule.description)))
|
|
312
|
+
|
|
313
|
+
# Create rule individual
|
|
314
|
+
rule_uri = self.ont[f"{safe_name}RuleInstance"]
|
|
315
|
+
self.graph.add((rule_uri, RDF.type, rule_class_uri))
|
|
316
|
+
|
|
317
|
+
# Add entity (appliesTo)
|
|
318
|
+
if rule.entity:
|
|
319
|
+
entity_uri = self.ont[self._safe_name(rule.entity)]
|
|
320
|
+
self.graph.add((rule_uri, self.ont.appliesTo, entity_uri))
|
|
321
|
+
|
|
322
|
+
# Add condition as annotation
|
|
323
|
+
if rule.condition:
|
|
324
|
+
self.graph.add((rule_uri, self.ont.ruleCondition, Literal(rule.condition)))
|
|
325
|
+
|
|
326
|
+
# Add action
|
|
327
|
+
if rule.action:
|
|
328
|
+
self.graph.add((rule_uri, self.ont.ruleAction, Literal(rule.action)))
|
|
329
|
+
|
|
330
|
+
# Determine role from classification
|
|
331
|
+
classification = getattr(rule, 'classification', 'low')
|
|
332
|
+
if classification:
|
|
333
|
+
role_map = {
|
|
334
|
+
'critical': 'Admin',
|
|
335
|
+
'high': 'Admin',
|
|
336
|
+
'medium': 'Editor',
|
|
337
|
+
'low': 'Viewer'
|
|
338
|
+
}
|
|
339
|
+
required_role = role_map.get(classification.lower(), 'Agent')
|
|
340
|
+
self.graph.add((rule_uri, self.ont.requiresRole, self.ont[required_role]))
|
|
341
|
+
|
|
342
|
+
def _add_context_filters(self):
|
|
343
|
+
"""Add context filters as OWL annotations."""
|
|
344
|
+
for entity_name, filter_condition in self.contract.permissions.context_filters.items():
|
|
345
|
+
entity_uri = self.ont[self._safe_name(entity_name)]
|
|
346
|
+
|
|
347
|
+
# Add filter as annotation
|
|
348
|
+
self.graph.add((entity_uri, self.ont.contextFilter, Literal(filter_condition)))
|
|
349
|
+
|
|
350
|
+
def _add_audit_config(self):
|
|
351
|
+
"""Add audit configuration as annotations."""
|
|
352
|
+
ontology_uri = URIRef(self.base_uri.rstrip("#"))
|
|
353
|
+
audit = self.contract.audit_settings
|
|
354
|
+
|
|
355
|
+
self.graph.add((ontology_uri, self.ont.auditLogReads, Literal(
|
|
356
|
+
audit.log_reads, datatype=XSD.boolean
|
|
357
|
+
)))
|
|
358
|
+
self.graph.add((ontology_uri, self.ont.auditLogWrites, Literal(
|
|
359
|
+
audit.log_writes, datatype=XSD.boolean
|
|
360
|
+
)))
|
|
361
|
+
self.graph.add((ontology_uri, self.ont.auditLogActions, Literal(
|
|
362
|
+
audit.log_actions, datatype=XSD.boolean
|
|
363
|
+
)))
|
|
364
|
+
self.graph.add((ontology_uri, self.ont.alertOnViolation, Literal(
|
|
365
|
+
audit.alert_on_violation, datatype=XSD.boolean
|
|
366
|
+
)))
|
|
367
|
+
|
|
368
|
+
def _safe_name(self, name: str) -> str:
|
|
369
|
+
"""Convert name to valid URI component."""
|
|
370
|
+
safe = name.replace(" ", "_").replace("-", "_").replace(".", "_")
|
|
371
|
+
safe = "".join(c for c in safe if c.isalnum() or c == "_")
|
|
372
|
+
return safe
|
|
373
|
+
|
|
374
|
+
def save(self, filepath: str, format: str = "xml"):
|
|
375
|
+
"""
|
|
376
|
+
Save OWL export to file.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
filepath: Path to save file
|
|
380
|
+
format: Output format
|
|
381
|
+
"""
|
|
382
|
+
output = self.convert(format=format)
|
|
383
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
384
|
+
f.write(output)
|
|
385
|
+
logger.info(f"Saved contract OWL to {filepath}")
|
|
386
|
+
|
|
387
|
+
def get_action_rules_summary(self) -> dict:
|
|
388
|
+
"""
|
|
389
|
+
Get summary of generated action rules.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Dictionary with counts of different rule types
|
|
393
|
+
"""
|
|
394
|
+
self.convert() # Ensure graph is populated
|
|
395
|
+
|
|
396
|
+
read_actions = len(list(self.graph.subjects(RDF.type, self.ont.ReadAction)))
|
|
397
|
+
write_actions = len(list(self.graph.subjects(RDF.type, self.ont.WriteAction)))
|
|
398
|
+
execute_actions = len(list(self.graph.subjects(RDF.type, self.ont.ExecuteAction)))
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
"agent_name": self.contract.agent_name,
|
|
402
|
+
"required_role": self.contract.permissions.required_role,
|
|
403
|
+
"read_actions": read_actions,
|
|
404
|
+
"write_actions": write_actions,
|
|
405
|
+
"execute_actions": execute_actions,
|
|
406
|
+
"business_rules": len(self.contract.business_rules),
|
|
407
|
+
"total_triples": len(self.graph)
|
|
408
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Microsoft Fabric IQ Exporter
|
|
3
|
+
|
|
4
|
+
Exports ontologies to Microsoft Fabric IQ format.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from powerbi_ontology.ontology_generator import Ontology, OntologyEntity, OntologyRelationship, BusinessRule
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FabricIQExporter:
|
|
17
|
+
"""
|
|
18
|
+
Exports ontologies to Microsoft Fabric IQ format.
|
|
19
|
+
|
|
20
|
+
Fabric IQ uses a specific JSON schema for ontologies.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, ontology: Ontology):
|
|
24
|
+
"""
|
|
25
|
+
Initialize Fabric IQ exporter.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
ontology: Ontology to export
|
|
29
|
+
"""
|
|
30
|
+
self.ontology = ontology
|
|
31
|
+
|
|
32
|
+
def export(self) -> Dict:
|
|
33
|
+
"""
|
|
34
|
+
Export ontology to Fabric IQ JSON format.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Dictionary in Fabric IQ format
|
|
38
|
+
"""
|
|
39
|
+
logger.info(f"Exporting ontology '{self.ontology.name}' to Fabric IQ format")
|
|
40
|
+
|
|
41
|
+
fabric_iq = {
|
|
42
|
+
"ontologyItem": f"{self.ontology.name}_v{self.ontology.version}",
|
|
43
|
+
"version": self.ontology.version,
|
|
44
|
+
"source": self.ontology.source,
|
|
45
|
+
"extractedDate": datetime.now().isoformat() + "Z",
|
|
46
|
+
"entities": [self.format_entity(entity) for entity in self.ontology.entities],
|
|
47
|
+
"relationships": [
|
|
48
|
+
self.format_relationship(rel) for rel in self.ontology.relationships
|
|
49
|
+
],
|
|
50
|
+
"businessRules": [
|
|
51
|
+
self.format_business_rule(rule) for rule in self.ontology.business_rules
|
|
52
|
+
],
|
|
53
|
+
"dataBindings": self._generate_data_bindings(),
|
|
54
|
+
"metadata": self.ontology.metadata
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Validate export
|
|
58
|
+
if self.validate_export(fabric_iq):
|
|
59
|
+
logger.info("Fabric IQ export validated successfully")
|
|
60
|
+
else:
|
|
61
|
+
logger.warning("Fabric IQ export validation failed")
|
|
62
|
+
|
|
63
|
+
return fabric_iq
|
|
64
|
+
|
|
65
|
+
def format_entity(self, entity: OntologyEntity) -> Dict:
|
|
66
|
+
"""
|
|
67
|
+
Format entity for Fabric IQ.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
entity: OntologyEntity to format
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Dictionary in Fabric IQ entity format
|
|
74
|
+
"""
|
|
75
|
+
return {
|
|
76
|
+
"name": entity.name,
|
|
77
|
+
"description": entity.description,
|
|
78
|
+
"entityType": entity.entity_type,
|
|
79
|
+
"properties": [
|
|
80
|
+
{
|
|
81
|
+
"name": prop.name,
|
|
82
|
+
"type": prop.data_type,
|
|
83
|
+
"required": prop.required,
|
|
84
|
+
"unique": prop.unique,
|
|
85
|
+
"description": prop.description,
|
|
86
|
+
"constraints": [
|
|
87
|
+
{
|
|
88
|
+
"type": constraint.type,
|
|
89
|
+
"value": constraint.value,
|
|
90
|
+
"message": constraint.message
|
|
91
|
+
}
|
|
92
|
+
for constraint in prop.constraints
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
for prop in entity.properties
|
|
96
|
+
],
|
|
97
|
+
"relationships": [
|
|
98
|
+
{
|
|
99
|
+
"type": rel.relationship_type,
|
|
100
|
+
"target": rel.to_entity,
|
|
101
|
+
"cardinality": rel.cardinality
|
|
102
|
+
}
|
|
103
|
+
for rel in self.ontology.relationships
|
|
104
|
+
if rel.from_entity == entity.name
|
|
105
|
+
],
|
|
106
|
+
"source": entity.source_table
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def format_relationship(self, rel: OntologyRelationship) -> Dict:
|
|
110
|
+
"""
|
|
111
|
+
Format relationship for Fabric IQ.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
rel: OntologyRelationship to format
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Dictionary in Fabric IQ relationship format
|
|
118
|
+
"""
|
|
119
|
+
return {
|
|
120
|
+
"from": rel.from_entity,
|
|
121
|
+
"fromProperty": rel.from_property,
|
|
122
|
+
"to": rel.to_entity,
|
|
123
|
+
"toProperty": rel.to_property,
|
|
124
|
+
"type": rel.relationship_type,
|
|
125
|
+
"cardinality": rel.cardinality,
|
|
126
|
+
"description": rel.description
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
def format_business_rule(self, rule: BusinessRule) -> Dict:
|
|
130
|
+
"""
|
|
131
|
+
Format business rule for Fabric IQ.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
rule: BusinessRule to format
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Dictionary in Fabric IQ business rule format
|
|
138
|
+
"""
|
|
139
|
+
return {
|
|
140
|
+
"name": rule.name,
|
|
141
|
+
"source": f"DAX: {rule.source_measure}" if rule.source_measure else "Manual",
|
|
142
|
+
"entity": rule.entity,
|
|
143
|
+
"condition": rule.condition,
|
|
144
|
+
"action": rule.action,
|
|
145
|
+
"classification": rule.classification,
|
|
146
|
+
"triggers": self._extract_triggers(rule),
|
|
147
|
+
"description": rule.description,
|
|
148
|
+
"priority": rule.priority
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
def generate_semantic_bindings(self, schema_mappings: Dict) -> Dict:
|
|
152
|
+
"""
|
|
153
|
+
Generate semantic bindings for OneLake.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
schema_mappings: Dictionary of entity -> physical source mappings
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Dictionary of data bindings
|
|
160
|
+
"""
|
|
161
|
+
bindings = {}
|
|
162
|
+
for entity_name, physical_source in schema_mappings.items():
|
|
163
|
+
entity = next(
|
|
164
|
+
(e for e in self.ontology.entities if e.name == entity_name),
|
|
165
|
+
None
|
|
166
|
+
)
|
|
167
|
+
if entity:
|
|
168
|
+
bindings[entity_name] = {
|
|
169
|
+
"source": physical_source,
|
|
170
|
+
"mapping": {
|
|
171
|
+
prop.name: prop.name # Default mapping
|
|
172
|
+
for prop in entity.properties
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return bindings
|
|
176
|
+
|
|
177
|
+
def validate_export(self, fabric_iq_json: Dict) -> bool:
|
|
178
|
+
"""
|
|
179
|
+
Validate Fabric IQ export against schema.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
fabric_iq_json: Fabric IQ JSON to validate
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
True if valid
|
|
186
|
+
"""
|
|
187
|
+
required_fields = ["ontologyItem", "version", "source", "entities"]
|
|
188
|
+
for field in required_fields:
|
|
189
|
+
if field not in fabric_iq_json:
|
|
190
|
+
logger.error(f"Missing required field: {field}")
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
# Validate entities
|
|
194
|
+
if not isinstance(fabric_iq_json["entities"], list):
|
|
195
|
+
logger.error("Entities must be a list")
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
for entity in fabric_iq_json["entities"]:
|
|
199
|
+
if "name" not in entity:
|
|
200
|
+
logger.error("Entity missing 'name' field")
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
def export_contract(self, contract) -> str:
|
|
206
|
+
"""Export semantic contract to Fabric IQ format."""
|
|
207
|
+
import json
|
|
208
|
+
# Convert contract to Fabric IQ format
|
|
209
|
+
contract_json = {
|
|
210
|
+
"agentContract": contract.agent_name,
|
|
211
|
+
"ontologyVersion": contract.ontology_version,
|
|
212
|
+
"permissions": {
|
|
213
|
+
"readEntities": contract.permissions.read_entities,
|
|
214
|
+
"writeProperties": contract.permissions.write_properties,
|
|
215
|
+
"executableActions": contract.permissions.executable_actions
|
|
216
|
+
},
|
|
217
|
+
"businessRules": [
|
|
218
|
+
{
|
|
219
|
+
"name": rule.name,
|
|
220
|
+
"condition": rule.condition,
|
|
221
|
+
"action": rule.action
|
|
222
|
+
}
|
|
223
|
+
for rule in contract.business_rules
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
return json.dumps(contract_json, indent=2)
|
|
227
|
+
|
|
228
|
+
def _generate_data_bindings(self) -> Dict:
|
|
229
|
+
"""Generate data bindings from ontology metadata."""
|
|
230
|
+
# This would typically come from SchemaMapper
|
|
231
|
+
# For now, return empty dict
|
|
232
|
+
return {}
|
|
233
|
+
|
|
234
|
+
def _extract_triggers(self, rule: BusinessRule) -> List[str]:
|
|
235
|
+
"""Extract trigger actions from business rule."""
|
|
236
|
+
triggers = []
|
|
237
|
+
if "notify" in rule.action.lower() or "alert" in rule.action.lower():
|
|
238
|
+
triggers.append("NotifyOperations")
|
|
239
|
+
if "log" in rule.action.lower() or "record" in rule.action.lower():
|
|
240
|
+
triggers.append("LogIncident")
|
|
241
|
+
if "classify" in rule.action.lower():
|
|
242
|
+
triggers.append("UpdateClassification")
|
|
243
|
+
return triggers
|