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.
@@ -0,0 +1,177 @@
1
+ """
2
+ OntoGuard Exporter
3
+
4
+ Exports ontologies to OntoGuard format for validation and drift detection.
5
+ """
6
+
7
+ import logging
8
+ from typing import Dict, List
9
+
10
+ from powerbi_ontology.ontology_generator import Ontology, OntologyEntity, BusinessRule, Constraint
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class OntoGuardExporter:
16
+ """
17
+ Exports ontologies to OntoGuard format.
18
+
19
+ This integrates with ontoguard-ai project for:
20
+ - Validation firewall
21
+ - Schema drift detection
22
+ - Business rule enforcement
23
+ """
24
+
25
+ def __init__(self, ontology: Ontology):
26
+ """
27
+ Initialize OntoGuard exporter.
28
+
29
+ Args:
30
+ ontology: Ontology to export
31
+ """
32
+ self.ontology = ontology
33
+
34
+ def export(self) -> Dict:
35
+ """
36
+ Export ontology to OntoGuard format.
37
+
38
+ Returns:
39
+ Dictionary in OntoGuard format
40
+ """
41
+ logger.info(f"Exporting ontology '{self.ontology.name}' to OntoGuard format")
42
+
43
+ ontoguard = {
44
+ "ontology": {
45
+ "name": self.ontology.name,
46
+ "version": self.ontology.version,
47
+ "source": self.ontology.source
48
+ },
49
+ "validationRules": self.generate_validation_rules(),
50
+ "schemaBindings": self.generate_schema_bindings(),
51
+ "firewallRules": self.generate_firewall_config(),
52
+ "businessRules": [
53
+ {
54
+ "name": rule.name,
55
+ "entity": rule.entity,
56
+ "condition": rule.condition,
57
+ "action": rule.action,
58
+ "description": rule.description
59
+ }
60
+ for rule in self.ontology.business_rules
61
+ ]
62
+ }
63
+
64
+ return ontoguard
65
+
66
+ def generate_validation_rules(self) -> List[Dict]:
67
+ """
68
+ Generate validation rules from ontology constraints.
69
+
70
+ Returns:
71
+ List of validation rule dictionaries
72
+ """
73
+ validation_rules = []
74
+
75
+ for entity in self.ontology.entities:
76
+ for prop in entity.properties:
77
+ for constraint in prop.constraints:
78
+ rule = {
79
+ "rule": f"{entity.name}_{prop.name}_{constraint.type}",
80
+ "entity": entity.name,
81
+ "property": prop.name,
82
+ "validation": {
83
+ "type": constraint.type,
84
+ "value": constraint.value,
85
+ "error": constraint.message or f"Validation failed for {entity.name}.{prop.name}"
86
+ }
87
+ }
88
+
89
+ # Format based on constraint type
90
+ if constraint.type == "range":
91
+ if isinstance(constraint.value, dict):
92
+ rule["validation"]["min"] = constraint.value.get("min")
93
+ rule["validation"]["max"] = constraint.value.get("max")
94
+ elif constraint.type == "enum":
95
+ rule["validation"]["values"] = constraint.value
96
+ elif constraint.type == "regex":
97
+ rule["validation"]["pattern"] = constraint.value
98
+
99
+ validation_rules.append(rule)
100
+
101
+ return validation_rules
102
+
103
+ def generate_schema_bindings(self) -> Dict:
104
+ """
105
+ Generate schema binding definitions.
106
+
107
+ This is CRITICAL for preventing the $4.6M mistake!
108
+
109
+ Returns:
110
+ Dictionary of schema bindings
111
+ """
112
+ bindings = {}
113
+
114
+ for entity in self.ontology.entities:
115
+ # Get expected columns from entity properties
116
+ expected_columns = [prop.source_column or prop.name for prop in entity.properties]
117
+
118
+ bindings[entity.name] = {
119
+ "expectedColumns": expected_columns,
120
+ "source": entity.source_table or f"sql_db.dbo.{entity.name.lower()}",
121
+ "primaryKey": next(
122
+ (prop.name for prop in entity.properties if prop.unique),
123
+ None
124
+ )
125
+ }
126
+
127
+ return bindings
128
+
129
+ def generate_firewall_config(self) -> List[Dict]:
130
+ """
131
+ Generate OntoGuard firewall configuration.
132
+
133
+ Returns:
134
+ List of firewall rule dictionaries
135
+ """
136
+ firewall_rules = []
137
+
138
+ # Generate rules from business rules
139
+ for rule in self.ontology.business_rules:
140
+ if "risk" in rule.name.lower() or "alert" in rule.name.lower():
141
+ firewall_rule = {
142
+ "name": f"prevent_invalid_{rule.name.lower()}",
143
+ "trigger": f"action.{rule.action}",
144
+ "checks": [
145
+ f"{rule.entity}_exists",
146
+ f"{rule.condition}_valid"
147
+ ],
148
+ "onFailure": "block",
149
+ "description": f"Prevent invalid {rule.name} based on {rule.condition}"
150
+ }
151
+ firewall_rules.append(firewall_rule)
152
+
153
+ return firewall_rules
154
+
155
+ def export_contract(self, contract) -> str:
156
+ """Export semantic contract to OntoGuard format."""
157
+ import json
158
+ contract_json = {
159
+ "agentContract": contract.agent_name,
160
+ "ontologyVersion": contract.ontology_version,
161
+ "validationRules": [
162
+ {
163
+ "entity": rule.get("entity", ""),
164
+ "property": rule.get("property", ""),
165
+ "validation": rule.get("validation", {})
166
+ }
167
+ for rule in self.generate_validation_rules()
168
+ if rule.get("entity") in contract.permissions.read_entities
169
+ ],
170
+ "permissions": {
171
+ "readEntities": contract.permissions.read_entities,
172
+ "writeProperties": contract.permissions.write_properties,
173
+ "executableActions": contract.permissions.executable_actions
174
+ },
175
+ "firewallRules": self.generate_firewall_config()
176
+ }
177
+ return json.dumps(contract_json, indent=2)
@@ -0,0 +1,522 @@
1
+ """
2
+ OWL/RDF Exporter
3
+
4
+ Exports ontologies to OWL/RDF format for semantic web standards.
5
+ Enhanced with action rules, constraints, and RLS support for OntoGuard integration.
6
+ """
7
+
8
+ import logging
9
+ from typing import Optional, List
10
+
11
+ from rdflib import Graph, Namespace, Literal, URIRef, BNode
12
+ from rdflib.namespace import RDF, RDFS, OWL, XSD
13
+
14
+ from powerbi_ontology.ontology_generator import (
15
+ Ontology,
16
+ OntologyEntity,
17
+ OntologyProperty,
18
+ OntologyRelationship,
19
+ BusinessRule,
20
+ Constraint,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class OWLExporter:
27
+ """
28
+ Exports ontologies to OWL/RDF format.
29
+
30
+ Uses RDFLib to generate standard OWL/RDF files compatible with
31
+ triple stores, OntoGuard, and other semantic web tools.
32
+
33
+ Enhanced features:
34
+ - Action rules (requiresRole, appliesTo, allowsAction) for OntoGuard
35
+ - Property constraints (required, range, enum)
36
+ - RLS rules as OWL restrictions
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ ontology: Ontology,
42
+ base_uri: Optional[str] = None,
43
+ include_action_rules: bool = True,
44
+ include_constraints: bool = True,
45
+ default_roles: Optional[List[str]] = None,
46
+ ):
47
+ """
48
+ Initialize OWL exporter.
49
+
50
+ Args:
51
+ ontology: Ontology to export
52
+ base_uri: Custom base URI (default: auto-generated)
53
+ include_action_rules: Generate OntoGuard-compatible action rules
54
+ include_constraints: Generate OWL restrictions for constraints
55
+ default_roles: Default roles for action rules (default: Admin, Analyst, Viewer)
56
+ """
57
+ self.ontology = ontology
58
+ self.graph = Graph()
59
+ self.include_action_rules = include_action_rules
60
+ self.include_constraints = include_constraints
61
+
62
+ # Create namespace for this ontology
63
+ safe_name = self._safe_name(ontology.name)
64
+ self.base_uri = base_uri or f"http://example.com/ontologies/{safe_name}#"
65
+ self.ont = Namespace(self.base_uri)
66
+
67
+ # Bind namespace prefixes
68
+ self.graph.bind("ont", self.ont)
69
+ self.graph.bind("owl", OWL)
70
+ self.graph.bind("rdfs", RDFS)
71
+ self.graph.bind("xsd", XSD)
72
+
73
+ # Default roles for action rules
74
+ self.default_roles = default_roles or ["Admin", "Analyst", "Viewer"]
75
+
76
+ def export(self, format: str = "xml") -> str:
77
+ """
78
+ Export ontology to OWL/RDF format.
79
+
80
+ Args:
81
+ format: Output format ("xml", "turtle", "json-ld", "n3")
82
+
83
+ Returns:
84
+ OWL/RDF string
85
+ """
86
+ logger.info(f"Exporting ontology '{self.ontology.name}' to OWL format ({format})")
87
+
88
+ # Add ontology metadata
89
+ self._add_ontology_metadata()
90
+
91
+ # Add base classes (User, Action hierarchy)
92
+ if self.include_action_rules:
93
+ self._add_base_classes()
94
+
95
+ # Add entities (classes)
96
+ for entity in self.ontology.entities:
97
+ self._add_entity(entity)
98
+
99
+ # Add relationships (object properties)
100
+ for rel in self.ontology.relationships:
101
+ self._add_relationship(rel)
102
+
103
+ # Add business rules as action rules
104
+ if self.include_action_rules:
105
+ self._add_business_rules()
106
+ self._add_default_crud_actions()
107
+
108
+ # Serialize to requested format
109
+ return self.graph.serialize(format=format)
110
+
111
+ def _add_ontology_metadata(self):
112
+ """Add ontology-level metadata."""
113
+ ontology_uri = URIRef(self.base_uri.rstrip("#"))
114
+ self.graph.add((ontology_uri, RDF.type, OWL.Ontology))
115
+ self.graph.add((ontology_uri, RDFS.label, Literal(self.ontology.name)))
116
+ self.graph.add((ontology_uri, RDFS.comment, Literal(f"Ontology from {self.ontology.source}")))
117
+
118
+ # Add version
119
+ if self.ontology.version:
120
+ self.graph.add((ontology_uri, OWL.versionInfo, Literal(self.ontology.version)))
121
+
122
+ # Add metadata as annotations
123
+ for key, value in self.ontology.metadata.items():
124
+ self.graph.add((ontology_uri, self.ont[f"meta_{key}"], Literal(str(value))))
125
+
126
+ def _add_base_classes(self):
127
+ """Add base classes for OntoGuard compatibility."""
128
+ # User class (base for roles)
129
+ user_uri = self.ont.User
130
+ self.graph.add((user_uri, RDF.type, OWL.Class))
131
+ self.graph.add((user_uri, RDFS.label, Literal("User")))
132
+ self.graph.add((user_uri, RDFS.comment, Literal("Base class for user roles")))
133
+
134
+ # Action class hierarchy
135
+ action_uri = self.ont.Action
136
+ self.graph.add((action_uri, RDF.type, OWL.Class))
137
+ self.graph.add((action_uri, RDFS.label, Literal("Action")))
138
+ self.graph.add((action_uri, RDFS.comment, Literal("Base class for actions")))
139
+
140
+ # Action subclasses
141
+ for action_type in ["ReadAction", "WriteAction", "DeleteAction", "ExecuteAction"]:
142
+ action_class = self.ont[action_type]
143
+ self.graph.add((action_class, RDF.type, OWL.Class))
144
+ self.graph.add((action_class, RDFS.subClassOf, action_uri))
145
+ self.graph.add((action_class, RDFS.label, Literal(action_type)))
146
+
147
+ # OntoGuard properties
148
+ requires_role = self.ont.requiresRole
149
+ self.graph.add((requires_role, RDF.type, OWL.ObjectProperty))
150
+ self.graph.add((requires_role, RDFS.label, Literal("requiresRole")))
151
+ self.graph.add((requires_role, RDFS.comment, Literal("Role required to perform this action")))
152
+ self.graph.add((requires_role, RDFS.domain, action_uri))
153
+ self.graph.add((requires_role, RDFS.range, user_uri))
154
+
155
+ applies_to = self.ont.appliesTo
156
+ self.graph.add((applies_to, RDF.type, OWL.ObjectProperty))
157
+ self.graph.add((applies_to, RDFS.label, Literal("appliesTo")))
158
+ self.graph.add((applies_to, RDFS.comment, Literal("Entity this action applies to")))
159
+ self.graph.add((applies_to, RDFS.domain, action_uri))
160
+
161
+ allows_action = self.ont.allowsAction
162
+ self.graph.add((allows_action, RDF.type, OWL.DatatypeProperty))
163
+ self.graph.add((allows_action, RDFS.label, Literal("allowsAction")))
164
+ self.graph.add((allows_action, RDFS.comment, Literal("Action type allowed")))
165
+ self.graph.add((allows_action, RDFS.range, XSD.string))
166
+
167
+ applies_to_property = self.ont.appliesToProperty
168
+ self.graph.add((applies_to_property, RDF.type, OWL.DatatypeProperty))
169
+ self.graph.add((applies_to_property, RDFS.label, Literal("appliesToProperty")))
170
+ self.graph.add((applies_to_property, RDFS.comment, Literal("Property this action applies to")))
171
+ self.graph.add((applies_to_property, RDFS.range, XSD.string))
172
+
173
+ # Add default roles as User subclasses
174
+ for role in self.default_roles:
175
+ role_uri = self.ont[self._safe_name(role)]
176
+ self.graph.add((role_uri, RDF.type, OWL.Class))
177
+ self.graph.add((role_uri, RDFS.subClassOf, user_uri))
178
+ self.graph.add((role_uri, RDFS.label, Literal(role)))
179
+
180
+ def _add_entity(self, entity: OntologyEntity):
181
+ """Add entity as OWL class with properties and constraints."""
182
+ entity_uri = self.ont[self._safe_name(entity.name)]
183
+
184
+ # Entity is a class
185
+ self.graph.add((entity_uri, RDF.type, OWL.Class))
186
+ self.graph.add((entity_uri, RDFS.label, Literal(entity.name)))
187
+ if entity.description:
188
+ self.graph.add((entity_uri, RDFS.comment, Literal(entity.description)))
189
+
190
+ # Add entity type annotation
191
+ if entity.entity_type:
192
+ self.graph.add((entity_uri, self.ont.entityType, Literal(entity.entity_type)))
193
+
194
+ # Add source table annotation
195
+ if entity.source_table:
196
+ self.graph.add((entity_uri, self.ont.sourceTable, Literal(entity.source_table)))
197
+
198
+ # Add properties (datatype properties)
199
+ for prop in entity.properties:
200
+ self._add_property(entity, prop)
201
+
202
+ # Add entity-level constraints
203
+ if self.include_constraints:
204
+ for constraint in entity.constraints:
205
+ self._add_entity_constraint(entity_uri, constraint)
206
+
207
+ def _add_property(self, entity: OntologyEntity, prop: OntologyProperty):
208
+ """Add property as OWL datatype property with constraints."""
209
+ entity_uri = self.ont[self._safe_name(entity.name)]
210
+ prop_uri = self.ont[f"{self._safe_name(entity.name)}_{self._safe_name(prop.name)}"]
211
+
212
+ self.graph.add((prop_uri, RDF.type, OWL.DatatypeProperty))
213
+ self.graph.add((prop_uri, RDFS.label, Literal(prop.name)))
214
+ self.graph.add((prop_uri, RDFS.domain, entity_uri))
215
+
216
+ # Map data type to XSD
217
+ xsd_type = self._map_to_xsd(prop.data_type)
218
+ self.graph.add((prop_uri, RDFS.range, xsd_type))
219
+
220
+ if prop.description:
221
+ self.graph.add((prop_uri, RDFS.comment, Literal(prop.description)))
222
+
223
+ # Add source column annotation
224
+ if prop.source_column:
225
+ self.graph.add((prop_uri, self.ont.sourceColumn, Literal(prop.source_column)))
226
+
227
+ # Add constraints
228
+ if self.include_constraints:
229
+ # Required property (minCardinality 1)
230
+ if prop.required:
231
+ self._add_cardinality_restriction(entity_uri, prop_uri, min_card=1)
232
+
233
+ # Unique property (functional property)
234
+ if prop.unique:
235
+ self.graph.add((prop_uri, RDF.type, OWL.FunctionalProperty))
236
+
237
+ # Property-level constraints
238
+ for constraint in prop.constraints:
239
+ self._add_property_constraint(prop_uri, constraint)
240
+
241
+ def _add_cardinality_restriction(
242
+ self,
243
+ class_uri: URIRef,
244
+ property_uri: URIRef,
245
+ min_card: Optional[int] = None,
246
+ max_card: Optional[int] = None,
247
+ ):
248
+ """Add cardinality restriction to a class."""
249
+ restriction = BNode()
250
+ self.graph.add((restriction, RDF.type, OWL.Restriction))
251
+ self.graph.add((restriction, OWL.onProperty, property_uri))
252
+
253
+ if min_card is not None:
254
+ self.graph.add((restriction, OWL.minCardinality, Literal(min_card, datatype=XSD.nonNegativeInteger)))
255
+
256
+ if max_card is not None:
257
+ self.graph.add((restriction, OWL.maxCardinality, Literal(max_card, datatype=XSD.nonNegativeInteger)))
258
+
259
+ self.graph.add((class_uri, RDFS.subClassOf, restriction))
260
+
261
+ def _add_property_constraint(self, prop_uri: URIRef, constraint: Constraint):
262
+ """Add property-level constraint as OWL annotation or restriction."""
263
+ if constraint.type == "range":
264
+ # Range constraint (min/max)
265
+ if isinstance(constraint.value, dict):
266
+ if "min" in constraint.value:
267
+ self.graph.add((
268
+ prop_uri,
269
+ self.ont.minValue,
270
+ Literal(constraint.value["min"], datatype=XSD.decimal)
271
+ ))
272
+ if "max" in constraint.value:
273
+ self.graph.add((
274
+ prop_uri,
275
+ self.ont.maxValue,
276
+ Literal(constraint.value["max"], datatype=XSD.decimal)
277
+ ))
278
+
279
+ elif constraint.type == "regex":
280
+ # Regex pattern constraint
281
+ pattern = constraint.value.get("pattern", str(constraint.value)) if isinstance(constraint.value, dict) else str(constraint.value)
282
+ self.graph.add((prop_uri, self.ont.pattern, Literal(pattern)))
283
+
284
+ elif constraint.type == "enum":
285
+ # Enumeration constraint
286
+ values = constraint.value if isinstance(constraint.value, list) else [constraint.value]
287
+ for i, val in enumerate(values):
288
+ self.graph.add((prop_uri, self.ont[f"enumValue_{i}"], Literal(str(val))))
289
+
290
+ elif constraint.type == "reference":
291
+ # Reference to another entity
292
+ self.graph.add((prop_uri, self.ont.references, Literal(str(constraint.value))))
293
+
294
+ # Add constraint message if present
295
+ if constraint.message:
296
+ self.graph.add((prop_uri, self.ont.constraintMessage, Literal(constraint.message)))
297
+
298
+ def _add_entity_constraint(self, entity_uri: URIRef, constraint: Constraint):
299
+ """Add entity-level constraint."""
300
+ constraint_node = BNode()
301
+ self.graph.add((constraint_node, RDF.type, self.ont.EntityConstraint))
302
+ self.graph.add((constraint_node, self.ont.constraintType, Literal(constraint.type)))
303
+ self.graph.add((constraint_node, self.ont.constraintValue, Literal(str(constraint.value))))
304
+ if constraint.message:
305
+ self.graph.add((constraint_node, RDFS.comment, Literal(constraint.message)))
306
+ self.graph.add((entity_uri, self.ont.hasConstraint, constraint_node))
307
+
308
+ def _add_relationship(self, rel: OntologyRelationship):
309
+ """Add relationship as OWL object property."""
310
+ rel_name = self._safe_name(f"{rel.from_entity}_{rel.relationship_type}_{rel.to_entity}")
311
+ rel_uri = self.ont[rel_name]
312
+ from_uri = self.ont[self._safe_name(rel.from_entity)]
313
+ to_uri = self.ont[self._safe_name(rel.to_entity)]
314
+
315
+ # Relationship is an object property
316
+ self.graph.add((rel_uri, RDF.type, OWL.ObjectProperty))
317
+ self.graph.add((rel_uri, RDFS.label, Literal(rel.relationship_type)))
318
+ self.graph.add((rel_uri, RDFS.domain, from_uri))
319
+ self.graph.add((rel_uri, RDFS.range, to_uri))
320
+
321
+ if rel.description:
322
+ self.graph.add((rel_uri, RDFS.comment, Literal(rel.description)))
323
+
324
+ # Add source relationship annotation
325
+ if rel.source_relationship:
326
+ self.graph.add((rel_uri, self.ont.sourceRelationship, Literal(rel.source_relationship)))
327
+
328
+ # Add cardinality annotations
329
+ self.graph.add((rel_uri, self.ont.cardinality, Literal(rel.cardinality)))
330
+
331
+ # Add from/to property annotations
332
+ if rel.from_property:
333
+ self.graph.add((rel_uri, self.ont.fromProperty, Literal(rel.from_property)))
334
+ if rel.to_property:
335
+ self.graph.add((rel_uri, self.ont.toProperty, Literal(rel.to_property)))
336
+
337
+ def _add_business_rules(self):
338
+ """Add business rules as OntoGuard-compatible action rules."""
339
+ for rule in self.ontology.business_rules:
340
+ self._add_business_rule(rule)
341
+
342
+ def _add_business_rule(self, rule: BusinessRule):
343
+ """Add a single business rule as an action rule."""
344
+ safe_name = self._safe_name(rule.name)
345
+
346
+ # Create rule class
347
+ rule_class = self.ont[f"{safe_name}Rule"]
348
+ self.graph.add((rule_class, RDF.type, OWL.Class))
349
+ self.graph.add((rule_class, RDFS.subClassOf, self.ont.Action))
350
+ self.graph.add((rule_class, RDFS.label, Literal(rule.name)))
351
+ if rule.description:
352
+ self.graph.add((rule_class, RDFS.comment, Literal(rule.description)))
353
+
354
+ # Create rule instance
355
+ rule_instance = self.ont[f"{safe_name}RuleInstance"]
356
+ self.graph.add((rule_instance, RDF.type, rule_class))
357
+
358
+ # Link to entity
359
+ if rule.entity:
360
+ entity_uri = self.ont[self._safe_name(rule.entity)]
361
+ self.graph.add((rule_instance, self.ont.appliesTo, entity_uri))
362
+
363
+ # Add condition as annotation
364
+ if rule.condition:
365
+ self.graph.add((rule_instance, self.ont.condition, Literal(rule.condition)))
366
+
367
+ # Add action as annotation
368
+ if rule.action:
369
+ self.graph.add((rule_instance, self.ont.ruleAction, Literal(rule.action)))
370
+
371
+ # Add classification
372
+ if rule.classification:
373
+ self.graph.add((rule_instance, self.ont.classification, Literal(rule.classification)))
374
+
375
+ # Add priority
376
+ self.graph.add((rule_instance, self.ont.priority, Literal(rule.priority, datatype=XSD.integer)))
377
+
378
+ # Add source measure annotation
379
+ if rule.source_measure:
380
+ self.graph.add((rule_instance, self.ont.sourceMeasure, Literal(rule.source_measure)))
381
+
382
+ def _add_default_crud_actions(self):
383
+ """Add default CRUD action rules for each entity."""
384
+ actions = ["read", "create", "update", "delete"]
385
+ action_class_map = {
386
+ "read": self.ont.ReadAction,
387
+ "create": self.ont.WriteAction,
388
+ "update": self.ont.WriteAction,
389
+ "delete": self.ont.DeleteAction,
390
+ }
391
+
392
+ for entity in self.ontology.entities:
393
+ entity_uri = self.ont[self._safe_name(entity.name)]
394
+
395
+ for action in actions:
396
+ for role in self.default_roles:
397
+ # Create action rule instance
398
+ action_name = f"{action}_{self._safe_name(entity.name)}_{self._safe_name(role)}"
399
+ action_uri = self.ont[action_name]
400
+ role_uri = self.ont[self._safe_name(role)]
401
+
402
+ self.graph.add((action_uri, RDF.type, action_class_map[action]))
403
+ self.graph.add((action_uri, self.ont.appliesTo, entity_uri))
404
+ self.graph.add((action_uri, self.ont.requiresRole, role_uri))
405
+ self.graph.add((action_uri, self.ont.allowsAction, Literal(action)))
406
+
407
+ def add_rls_rules(self, security_rules: list):
408
+ """
409
+ Add Row-Level Security rules as OWL restrictions.
410
+
411
+ Args:
412
+ security_rules: List of SecurityRule objects from SemanticModel
413
+ """
414
+ # Create RLS-specific properties
415
+ dax_filter_prop = self.ont.daxFilter
416
+ self.graph.add((dax_filter_prop, RDF.type, OWL.DatatypeProperty))
417
+ self.graph.add((dax_filter_prop, RDFS.label, Literal("daxFilter")))
418
+ self.graph.add((dax_filter_prop, RDFS.comment, Literal("DAX filter expression for RLS")))
419
+
420
+ for rule in security_rules:
421
+ # Create role as User subclass if not exists
422
+ role_uri = self.ont[self._safe_name(rule.role)]
423
+ if (role_uri, RDF.type, OWL.Class) not in self.graph:
424
+ self.graph.add((role_uri, RDF.type, OWL.Class))
425
+ self.graph.add((role_uri, RDFS.subClassOf, self.ont.User))
426
+ self.graph.add((role_uri, RDFS.label, Literal(rule.role)))
427
+
428
+ # Create RLS action rule
429
+ rls_name = f"RLS_{self._safe_name(rule.role)}_{self._safe_name(rule.table)}"
430
+ rls_uri = self.ont[rls_name]
431
+
432
+ self.graph.add((rls_uri, RDF.type, self.ont.ReadAction))
433
+ self.graph.add((rls_uri, RDFS.label, Literal(f"RLS: {rule.role} on {rule.table}")))
434
+
435
+ # Link to entity
436
+ entity_uri = self.ont[self._safe_name(rule.table)]
437
+ self.graph.add((rls_uri, self.ont.appliesTo, entity_uri))
438
+
439
+ # Link to role
440
+ self.graph.add((rls_uri, self.ont.requiresRole, role_uri))
441
+
442
+ # Add DAX filter
443
+ self.graph.add((rls_uri, dax_filter_prop, Literal(rule.dax_filter)))
444
+
445
+ # Add description
446
+ if hasattr(rule, 'description') and rule.description:
447
+ self.graph.add((rls_uri, RDFS.comment, Literal(rule.description)))
448
+
449
+ # Mark as RLS rule
450
+ self.graph.add((rls_uri, self.ont.isRLSRule, Literal(True, datatype=XSD.boolean)))
451
+
452
+ def _map_to_xsd(self, data_type: str) -> URIRef:
453
+ """Map ontology data type to XSD type."""
454
+ type_mapping = {
455
+ "String": XSD.string,
456
+ "Integer": XSD.integer,
457
+ "Decimal": XSD.decimal,
458
+ "Date": XSD.date,
459
+ "DateTime": XSD.dateTime,
460
+ "Boolean": XSD.boolean,
461
+ "Float": XSD.float,
462
+ "Double": XSD.double,
463
+ "Long": XSD.long,
464
+ "Binary": XSD.base64Binary,
465
+ }
466
+ return type_mapping.get(data_type, XSD.string)
467
+
468
+ def _safe_name(self, name: str) -> str:
469
+ """Convert name to URI-safe format."""
470
+ if not name:
471
+ return "unnamed"
472
+ return name.replace(" ", "_").replace("-", "_").replace(".", "_")
473
+
474
+ def save(self, filepath: str, format: str = "xml"):
475
+ """
476
+ Save OWL export to file.
477
+
478
+ Args:
479
+ filepath: Path to save file
480
+ format: Output format
481
+ """
482
+ output = self.export(format=format)
483
+ with open(filepath, 'w', encoding='utf-8') as f:
484
+ f.write(output)
485
+ logger.info(f"Saved OWL export to {filepath}")
486
+
487
+ def get_export_summary(self) -> dict:
488
+ """
489
+ Get summary of exported OWL content.
490
+
491
+ Returns:
492
+ Dictionary with export statistics
493
+ """
494
+ # Export first to populate graph
495
+ self.export()
496
+
497
+ # Count classes
498
+ classes = len(list(self.graph.subjects(RDF.type, OWL.Class)))
499
+
500
+ # Count datatype properties
501
+ datatype_props = len(list(self.graph.subjects(RDF.type, OWL.DatatypeProperty)))
502
+
503
+ # Count object properties
504
+ object_props = len(list(self.graph.subjects(RDF.type, OWL.ObjectProperty)))
505
+
506
+ # Count action rules (instances of Action subclasses)
507
+ action_rules = 0
508
+ for action_type in ["ReadAction", "WriteAction", "DeleteAction", "ExecuteAction"]:
509
+ action_rules += len(list(self.graph.subjects(RDF.type, self.ont[action_type])))
510
+
511
+ return {
512
+ "ontology_name": self.ontology.name,
513
+ "total_triples": len(self.graph),
514
+ "classes": classes,
515
+ "datatype_properties": datatype_props,
516
+ "object_properties": object_props,
517
+ "entities": len(self.ontology.entities),
518
+ "relationships": len(self.ontology.relationships),
519
+ "business_rules": len(self.ontology.business_rules),
520
+ "action_rules": action_rules,
521
+ "default_roles": self.default_roles,
522
+ }