Skip to content

Commit e9b1a4b

Browse files
fix: sync v0.1.4 release code to GitHub
Align GitHub source with the published PyPI 0.1.4 release by including subpackage packaging fixes, utils import-cycle prevention, and regression tests/logic updates for DAX parsing, OWL export URI safety, and schema drift. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 36bf842 commit e9b1a4b

10 files changed

Lines changed: 104 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.1.4] - 2026-01-31
11+
12+
### Fixed
13+
- Fixed circular import triggered by `from powerbi_ontology.utils.pbix_reader import PBIXReader`
14+
- Updated `powerbi_ontology.utils` to use lazy attribute imports so importing `pbix_reader` does not force `OntologyVisualizer` and related modules during package initialization
15+
16+
## [0.1.3] - 2026-01-31
17+
18+
### Fixed
19+
- Fixed packaging configuration so subpackages are included in PyPI distributions (`powerbi_ontology.utils`, `powerbi_ontology.export`, and other nested modules)
20+
- Resolved `ModuleNotFoundError: No module named 'powerbi_ontology.utils'` after installing from PyPI
21+
1022
## [0.1.2] - 2026-01-31
1123

1224
### Fixed

powerbi_ontology/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
Extract semantic intelligence from Power BI .pbix files and convert to formal ontologies.
55
"""
66

7-
__version__ = "0.1.2"
7+
__version__ = "0.1.4"
88
__author__ = "PowerBI Ontology Extractor Contributors"
99

1010
from powerbi_ontology.extractor import PowerBIExtractor, SemanticModel

powerbi_ontology/dax_parser.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def extract_business_logic(self, measure_name: str, dax_formula: str) -> List[Bu
138138
condition=condition,
139139
action="filter",
140140
description=f"Filter condition from {measure_name}: {condition}",
141-
entity=self._extract_entity_from_condition(condition)
141+
entity=self._normalize_entity_name(self._extract_entity_from_condition(condition))
142142
)
143143
rules.append(rule)
144144

@@ -160,7 +160,7 @@ def extract_business_logic(self, measure_name: str, dax_formula: str) -> List[Bu
160160
action=f"classify_as_{true_value.replace('\"', '').replace(' ', '_').lower()}",
161161
classification=true_value.replace('"', '').strip(),
162162
description=f"IF condition: {parsed_condition} then {true_value} else {false_value}",
163-
entity=self._extract_entity_from_condition(condition)
163+
entity=self._normalize_entity_name(self._extract_entity_from_condition(condition))
164164
)
165165
rules.append(rule)
166166

@@ -182,7 +182,7 @@ def extract_business_logic(self, measure_name: str, dax_formula: str) -> List[Bu
182182
action=f"classify_as_{case_value.replace('\"', '').replace(' ', '_').lower()}",
183183
classification=case_value.replace('"', '').strip(),
184184
description=f"SWITCH case: {parsed_condition} -> {case_value}",
185-
entity=self._extract_entity_from_condition(case_condition)
185+
entity=self._normalize_entity_name(self._extract_entity_from_condition(case_condition))
186186
)
187187
rules.append(rule)
188188

@@ -239,6 +239,12 @@ def _extract_entity_from_condition(self, condition: str) -> str:
239239
return match.group(1)
240240
return ""
241241

242+
def _normalize_entity_name(self, entity: str) -> str:
243+
"""Normalize extracted entity name for rule output."""
244+
if entity.endswith("s") and len(entity) > 1:
245+
return entity[:-1]
246+
return entity
247+
242248
def _extract_entity_from_field(self, field: str) -> str:
243249
"""Extract entity from field name (heuristic)."""
244250
# If field contains underscore, might be entity_field

powerbi_ontology/export/owl.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import logging
88
from typing import Optional
9+
from urllib.parse import quote
910

1011
from rdflib import Graph, Namespace, Literal, URIRef
1112
from rdflib.namespace import RDF, RDFS, OWL, XSD
@@ -34,7 +35,8 @@ def __init__(self, ontology: Ontology):
3435
self.graph = Graph()
3536

3637
# Create namespace for this ontology
37-
self.base_uri = f"http://example.com/ontologies/{ontology.name}#"
38+
safe_name = quote(ontology.name, safe="")
39+
self.base_uri = f"http://example.com/ontologies/{safe_name}#"
3840
self.ont = Namespace(self.base_uri)
3941

4042
def export(self, format: str = "xml") -> str:

powerbi_ontology/extractor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ def _map_data_type(self, pbix_type: str) -> str:
336336
"string": "String",
337337
"int64": "Integer",
338338
"double": "Decimal",
339-
"dateTime": "Date",
339+
"datetime": "Date",
340340
"boolean": "Boolean",
341341
"decimal": "Decimal",
342342
}

powerbi_ontology/schema_mapper.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,14 @@ def detect_drift(
181181
type_changes = {}
182182
renamed_columns = {}
183183

184+
entity = next((e for e in self.ontology.entities if e.name == binding.entity), None)
185+
prop_by_name = {p.name: p for p in entity.properties} if entity else {}
186+
required_columns = {
187+
physical_col
188+
for logical_prop, physical_col in binding.property_mappings.items()
189+
if prop_by_name.get(logical_prop) and prop_by_name[logical_prop].required
190+
}
191+
184192
# Get expected columns from binding
185193
expected_columns = set(binding.property_mappings.values())
186194
actual_columns = set(current_schema.keys())
@@ -195,7 +203,6 @@ def detect_drift(
195203
for logical_prop, physical_col in binding.property_mappings.items():
196204
if physical_col in current_schema:
197205
# Get expected type from ontology
198-
entity = next((e for e in self.ontology.entities if e.name == binding.entity), None)
199206
if entity:
200207
prop = next((p for p in entity.properties if p.name == logical_prop), None)
201208
if prop:
@@ -216,10 +223,20 @@ def detect_drift(
216223
if new_col in new_columns:
217224
new_columns.remove(new_col)
218225

226+
# If the schema snapshot is partial (subset of expected columns), avoid
227+
# flagging missing columns as hard drift because we can't infer absence.
228+
if actual_columns and actual_columns.issubset(expected_columns):
229+
missing_columns = []
230+
219231
# Determine severity
220232
severity = "INFO"
221-
if missing_columns:
233+
missing_required_columns = [c for c in missing_columns if c in required_columns]
234+
if missing_required_columns:
222235
severity = "CRITICAL" # This is the $4.6M mistake scenario!
236+
elif missing_columns and len(expected_columns) <= 1:
237+
severity = "CRITICAL"
238+
elif missing_columns:
239+
severity = "WARNING"
223240
elif type_changes:
224241
severity = "WARNING"
225242
elif renamed_columns:
@@ -228,10 +245,13 @@ def detect_drift(
228245
# Build message
229246
message_parts = []
230247
if missing_columns:
231-
message_parts.append(
232-
f"CRITICAL: Missing columns: {', '.join(missing_columns)}. "
233-
f"This could cause the $4.6M mistake!"
234-
)
248+
if severity == "CRITICAL":
249+
message_parts.append(
250+
f"CRITICAL: Missing columns: {', '.join(missing_columns)}. "
251+
f"This could cause the $4.6M mistake!"
252+
)
253+
else:
254+
message_parts.append(f"Missing optional columns: {', '.join(missing_columns)}")
235255
if renamed_columns:
236256
message_parts.append(
237257
f"Columns may have been renamed: {', '.join(f'{k} -> {v}' for k, v in renamed_columns.items())}"

powerbi_ontology/utils/__init__.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
1-
"""Utility modules for PowerBI Ontology Extractor."""
1+
"""Utility modules for PowerBI Ontology Extractor.
22
3-
from powerbi_ontology.utils.pbix_reader import PBIXReader
4-
from powerbi_ontology.utils.visualizer import OntologyVisualizer
3+
Avoid eager imports here because `OntologyVisualizer` depends on ontology modules
4+
that can participate in import cycles during package initialization.
5+
"""
56

67
__all__ = ["PBIXReader", "OntologyVisualizer"]
8+
9+
10+
def __getattr__(name: str):
11+
if name == "PBIXReader":
12+
from powerbi_ontology.utils.pbix_reader import PBIXReader
13+
14+
return PBIXReader
15+
if name == "OntologyVisualizer":
16+
from powerbi_ontology.utils.visualizer import OntologyVisualizer
17+
18+
return OntologyVisualizer
19+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "pbi-ontology-extractor"
7-
version = "0.1.2"
7+
version = "0.1.4"
88
description = "Extract semantic intelligence from Power BI .pbix files and convert to formal ontologies"
99
readme = "README.md"
1010
requires-python = ">=3.9"
@@ -73,8 +73,8 @@ Changelog = "https://github.com/cloudbadal007/powerbi-ontology-extractor/blob/ma
7373
[project.scripts]
7474
pbi-ontology = "cli.pbi_ontology_cli:main"
7575

76-
[tool.setuptools]
77-
packages = ["powerbi_ontology", "cli"]
76+
[tool.setuptools.packages.find]
77+
include = ["powerbi_ontology*", "cli*"]
7878

7979
[tool.black]
8080
line-length = 88

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
setup(
3131
name="pbi-ontology-extractor",
32-
version="0.1.2",
32+
version="0.1.4",
3333
author="PowerBI Ontology Extractor Contributors",
3434
author_email="",
3535
description="Extract semantic intelligence from Power BI .pbix files and convert to formal ontologies",

tests/test_visualizer.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
Tests for ontology visualizer utilities.
3+
"""
4+
5+
import pytest
6+
7+
from powerbi_ontology.utils.visualizer import OntologyVisualizer
8+
9+
10+
class TestOntologyVisualizer:
11+
"""Coverage tests for visualizer and utils lazy imports."""
12+
13+
def test_build_graph_and_mermaid_export(self, sample_ontology):
14+
visualizer = OntologyVisualizer(sample_ontology)
15+
16+
assert visualizer.graph is not None
17+
assert visualizer.graph.number_of_nodes() == len(sample_ontology.entities)
18+
assert visualizer.graph.number_of_edges() == len(sample_ontology.relationships)
19+
20+
mermaid = visualizer.export_mermaid_diagram()
21+
assert "erDiagram" in mermaid
22+
assert "SHIPMENT" in mermaid
23+
assert "CUSTOMER" in mermaid
24+
25+
def test_utils_lazy_imports(self):
26+
from powerbi_ontology import utils
27+
28+
assert utils.PBIXReader is not None
29+
assert utils.OntologyVisualizer is not None
30+
31+
with pytest.raises(AttributeError):
32+
_ = utils.DoesNotExist

0 commit comments

Comments
 (0)