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,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
|
+
}
|