pbip-compiler 0.2.0__tar.gz

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.
Files changed (29) hide show
  1. pbip_compiler-0.2.0/.github/workflows/release.yml +40 -0
  2. pbip_compiler-0.2.0/.gitignore +14 -0
  3. pbip_compiler-0.2.0/.python-version +1 -0
  4. pbip_compiler-0.2.0/LICENSE +21 -0
  5. pbip_compiler-0.2.0/PKG-INFO +201 -0
  6. pbip_compiler-0.2.0/README.md +157 -0
  7. pbip_compiler-0.2.0/pbip_compiler/__init__.py +38 -0
  8. pbip_compiler-0.2.0/pbip_compiler/cli.py +33 -0
  9. pbip_compiler-0.2.0/pbip_compiler/compiler.py +88 -0
  10. pbip_compiler-0.2.0/pbip_compiler/datamodel/__init__.py +10 -0
  11. pbip_compiler-0.2.0/pbip_compiler/datamodel/builder.py +67 -0
  12. pbip_compiler-0.2.0/pbip_compiler/datamodel/mpatch.py +39 -0
  13. pbip_compiler-0.2.0/pbip_compiler/discovery.py +50 -0
  14. pbip_compiler-0.2.0/pbip_compiler/models.py +37 -0
  15. pbip_compiler-0.2.0/pbip_compiler/pbix/__init__.py +7 -0
  16. pbip_compiler-0.2.0/pbip_compiler/pbix/assembler.py +229 -0
  17. pbip_compiler-0.2.0/pbip_compiler/pbix/constants.py +83 -0
  18. pbip_compiler-0.2.0/pbip_compiler/report/__init__.py +9 -0
  19. pbip_compiler-0.2.0/pbip_compiler/report/layout.py +51 -0
  20. pbip_compiler-0.2.0/pbip_compiler/report/pbir.py +359 -0
  21. pbip_compiler-0.2.0/pbip_compiler/report/resources.py +30 -0
  22. pbip_compiler-0.2.0/pbip_compiler/semantic_model/__init__.py +9 -0
  23. pbip_compiler-0.2.0/pbip_compiler/semantic_model/loader.py +30 -0
  24. pbip_compiler-0.2.0/pbip_compiler/semantic_model/tmdl.py +102 -0
  25. pbip_compiler-0.2.0/pbip_compiler/semantic_model/tmdl_table.py +413 -0
  26. pbip_compiler-0.2.0/pbip_compiler/semantic_model/tmsl.py +75 -0
  27. pbip_compiler-0.2.0/pbip_compiler/semantic_model/types.py +35 -0
  28. pbip_compiler-0.2.0/pyproject.toml +43 -0
  29. pbip_compiler-0.2.0/uv.lock +963 -0
@@ -0,0 +1,40 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ with:
14
+ fetch-depth: 0
15
+
16
+ - name: Install uv
17
+ uses: astral-sh/setup-uv@v3
18
+
19
+ - name: Build
20
+ run: uv build
21
+
22
+ - uses: actions/upload-artifact@v4
23
+ with:
24
+ name: dist
25
+ path: dist/
26
+
27
+ publish:
28
+ needs: build
29
+ runs-on: ubuntu-latest
30
+ environment: pypi
31
+ permissions:
32
+ id-token: write
33
+ steps:
34
+ - uses: actions/download-artifact@v4
35
+ with:
36
+ name: dist
37
+ path: dist/
38
+
39
+ - name: Publish to PyPI
40
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,14 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ .claude
12
+ prototypes/
13
+ *.pbix
14
+ .DS_Store
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maxim Bacar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: pbip-compiler
3
+ Version: 0.2.0
4
+ Summary: Compile a Power BI Project (.pbip) folder into a .pbix file — pure Python, no Power BI Desktop or Windows required.
5
+ Project-URL: Homepage, https://github.com/MaximBacar/pbip-compiler
6
+ Project-URL: Repository, https://github.com/MaximBacar/pbip-compiler
7
+ Project-URL: Issues, https://github.com/MaximBacar/pbip-compiler/issues
8
+ Author-email: Maxim Bacar <maximbacar@hotmail.ca>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Maxim Bacar
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: pbip,pbix,powerbi,report,tmdl,vertipaq
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Topic :: Software Development :: Build Tools
39
+ Requires-Python: >=3.11
40
+ Requires-Dist: msal>=1.37.0
41
+ Requires-Dist: pbix-mcp>=0.9.2
42
+ Requires-Dist: pydantic>=2
43
+ Description-Content-Type: text/markdown
44
+
45
+ # pbip-compiler
46
+
47
+ Compile a **Power BI Project (`.pbip`)** folder into a single **`.pbix`** file —
48
+ in pure Python. No Power BI Desktop, no external CLI tools, no Windows required.
49
+
50
+ It parses the project's semantic model (TMDL / `model.bim`) and PBIR report,
51
+ builds a real VertiPaq `DataModel` (via [`pbix-mcp`](https://pypi.org/project/pbix-mcp/)),
52
+ preserves each table's Power Query (M) so the report stays refreshable, and
53
+ assembles the final `.pbix` ZIP.
54
+
55
+ ## Features
56
+
57
+ - 📦 `.pbip` → `.pbix` entirely in Python
58
+ - 🧮 Builds a real VertiPaq DataModel (tables, columns, measures, relationships)
59
+ - 🔁 Preserves partition M expressions → **Refresh** in Power BI loads the data
60
+ - 🌐 Handles **live-connection** reports (bound to a published semantic model)
61
+ - 🪶 Falls back to a thin / report-only `.pbix` when there is no local model
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ # with uv (recommended)
67
+ uv add pbip-compiler
68
+
69
+ # or with pip
70
+ pip install pbip-compiler
71
+ ```
72
+
73
+ Requires Python ≥ 3.11.
74
+
75
+ ## Quick start (CLI)
76
+
77
+ ```bash
78
+ pbip-compiler --pbip ./MyProject --output ./MyReport.pbix
79
+ ```
80
+
81
+ ## Using `pbip_compiler` as a module
82
+
83
+ ### 1. Compile a `.pbip` folder to a `.pbix` file
84
+
85
+ ```python
86
+ from pbip_compiler import PbipCompiler
87
+
88
+ compiler = PbipCompiler("./MyProject") # path to the .pbip folder
89
+ output = compiler.compile("./MyReport.pbix") # returns the resolved Path
90
+
91
+ print(f"Compiled → {output}")
92
+ ```
93
+
94
+ ### 2. Compile to bytes (no file written)
95
+
96
+ Useful when serving the result over HTTP, uploading it, or writing it yourself.
97
+
98
+ ```python
99
+ from pbip_compiler import PbipCompiler
100
+
101
+ compiler = PbipCompiler("./MyProject")
102
+ pbix_bytes: bytes = compiler.compile_to_bytes()
103
+
104
+ # e.g. write it yourself, stream it, upload it...
105
+ with open("MyReport.pbix", "wb") as f:
106
+ f.write(pbix_bytes)
107
+ ```
108
+
109
+ ### 3. Handle errors
110
+
111
+ ```python
112
+ from pathlib import Path
113
+ from pbip_compiler import PbipCompiler
114
+
115
+ try:
116
+ result: Path = PbipCompiler("./MyProject").compile("./out.pbix")
117
+ print(f"✅ Success → {result}")
118
+ except FileNotFoundError as exc:
119
+ # e.g. no *.Report folder inside the project
120
+ print(f"❌ Project layout problem: {exc}")
121
+ except Exception as exc:
122
+ print(f"❌ Compilation failed: {exc}")
123
+ ```
124
+
125
+ ### 4. Build a `.pbix` from a semantic model defined in code
126
+
127
+ You don't have to start from a `.pbip` folder. You can describe a model with the
128
+ data classes and build the DataModel directly.
129
+
130
+ ```python
131
+ from pbip_compiler import Column, Measure, Relationship, SemanticModel, Table
132
+ from pbip_compiler.datamodel import PbixMcpDataModelBuilder
133
+
134
+ model = SemanticModel(
135
+ tables=[
136
+ Table(
137
+ name="Sales",
138
+ columns=[
139
+ Column(name="OrderId", data_type="Int64"),
140
+ Column(name="ProductId", data_type="Int64"),
141
+ Column(name="Amount", data_type="Decimal"),
142
+ ],
143
+ measures=[
144
+ Measure(name="Total Sales", expression="SUM(Sales[Amount])"),
145
+ ],
146
+ # Power Query (M) source — preserved so Refresh loads the data
147
+ m_expression='let Source = Csv.Document(File.Contents("sales.csv")) in Source',
148
+ ),
149
+ Table(
150
+ name="Product",
151
+ columns=[
152
+ Column(name="ProductId", data_type="Int64"),
153
+ Column(name="Name", data_type="String"),
154
+ ],
155
+ ),
156
+ ],
157
+ relationships=[
158
+ Relationship(
159
+ from_table="Sales", from_column="ProductId",
160
+ to_table="Product", to_column="ProductId",
161
+ ),
162
+ ],
163
+ )
164
+
165
+ datamodel_bytes: bytes = PbixMcpDataModelBuilder().build(model)
166
+ ```
167
+
168
+ ## Public API
169
+
170
+ | Object | Description |
171
+ | --- | --- |
172
+ | `PbipCompiler(pbip_path)` | Orchestrates a `.pbip` → `.pbix` compilation. |
173
+ | `PbipCompiler.compile(output)` | Compile and write the `.pbix`; returns the `Path`. |
174
+ | `PbipCompiler.compile_to_bytes()` | Compile and return the `.pbix` as `bytes`. |
175
+ | `SemanticModel` | A model: `tables` + `relationships`. |
176
+ | `Table` | `name`, `columns`, `measures`, `is_hidden`, `m_expression`. |
177
+ | `Column` | `name`, `data_type`, `source_column`. |
178
+ | `Measure` | `name`, `expression` (DAX). |
179
+ | `Relationship` | `from_table`/`from_column` → `to_table`/`to_column`. |
180
+
181
+ ## How it works
182
+
183
+ ```
184
+ .pbip folder
185
+ ├── *.Report/ → PBIR report ─┐
186
+ └── *.SemanticModel/ → TMDL / .bim ─┤
187
+
188
+ SemanticModelLoader + ReportLayoutLoader
189
+
190
+ PbixMcpDataModelBuilder (VertiPaq DataModel, M preserved)
191
+
192
+ PbixAssembler → MyReport.pbix
193
+ ```
194
+
195
+ - **Live connection** — if the report binds to a published semantic model, no
196
+ local DataModel is built; the report + a Connections part are embedded.
197
+ - **No / empty semantic model** — a thin (report-only) `.pbix` is produced.
198
+
199
+ ## License
200
+
201
+ [MIT](LICENSE) © 2026 Maxim Bacar
@@ -0,0 +1,157 @@
1
+ # pbip-compiler
2
+
3
+ Compile a **Power BI Project (`.pbip`)** folder into a single **`.pbix`** file —
4
+ in pure Python. No Power BI Desktop, no external CLI tools, no Windows required.
5
+
6
+ It parses the project's semantic model (TMDL / `model.bim`) and PBIR report,
7
+ builds a real VertiPaq `DataModel` (via [`pbix-mcp`](https://pypi.org/project/pbix-mcp/)),
8
+ preserves each table's Power Query (M) so the report stays refreshable, and
9
+ assembles the final `.pbix` ZIP.
10
+
11
+ ## Features
12
+
13
+ - 📦 `.pbip` → `.pbix` entirely in Python
14
+ - 🧮 Builds a real VertiPaq DataModel (tables, columns, measures, relationships)
15
+ - 🔁 Preserves partition M expressions → **Refresh** in Power BI loads the data
16
+ - 🌐 Handles **live-connection** reports (bound to a published semantic model)
17
+ - 🪶 Falls back to a thin / report-only `.pbix` when there is no local model
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ # with uv (recommended)
23
+ uv add pbip-compiler
24
+
25
+ # or with pip
26
+ pip install pbip-compiler
27
+ ```
28
+
29
+ Requires Python ≥ 3.11.
30
+
31
+ ## Quick start (CLI)
32
+
33
+ ```bash
34
+ pbip-compiler --pbip ./MyProject --output ./MyReport.pbix
35
+ ```
36
+
37
+ ## Using `pbip_compiler` as a module
38
+
39
+ ### 1. Compile a `.pbip` folder to a `.pbix` file
40
+
41
+ ```python
42
+ from pbip_compiler import PbipCompiler
43
+
44
+ compiler = PbipCompiler("./MyProject") # path to the .pbip folder
45
+ output = compiler.compile("./MyReport.pbix") # returns the resolved Path
46
+
47
+ print(f"Compiled → {output}")
48
+ ```
49
+
50
+ ### 2. Compile to bytes (no file written)
51
+
52
+ Useful when serving the result over HTTP, uploading it, or writing it yourself.
53
+
54
+ ```python
55
+ from pbip_compiler import PbipCompiler
56
+
57
+ compiler = PbipCompiler("./MyProject")
58
+ pbix_bytes: bytes = compiler.compile_to_bytes()
59
+
60
+ # e.g. write it yourself, stream it, upload it...
61
+ with open("MyReport.pbix", "wb") as f:
62
+ f.write(pbix_bytes)
63
+ ```
64
+
65
+ ### 3. Handle errors
66
+
67
+ ```python
68
+ from pathlib import Path
69
+ from pbip_compiler import PbipCompiler
70
+
71
+ try:
72
+ result: Path = PbipCompiler("./MyProject").compile("./out.pbix")
73
+ print(f"✅ Success → {result}")
74
+ except FileNotFoundError as exc:
75
+ # e.g. no *.Report folder inside the project
76
+ print(f"❌ Project layout problem: {exc}")
77
+ except Exception as exc:
78
+ print(f"❌ Compilation failed: {exc}")
79
+ ```
80
+
81
+ ### 4. Build a `.pbix` from a semantic model defined in code
82
+
83
+ You don't have to start from a `.pbip` folder. You can describe a model with the
84
+ data classes and build the DataModel directly.
85
+
86
+ ```python
87
+ from pbip_compiler import Column, Measure, Relationship, SemanticModel, Table
88
+ from pbip_compiler.datamodel import PbixMcpDataModelBuilder
89
+
90
+ model = SemanticModel(
91
+ tables=[
92
+ Table(
93
+ name="Sales",
94
+ columns=[
95
+ Column(name="OrderId", data_type="Int64"),
96
+ Column(name="ProductId", data_type="Int64"),
97
+ Column(name="Amount", data_type="Decimal"),
98
+ ],
99
+ measures=[
100
+ Measure(name="Total Sales", expression="SUM(Sales[Amount])"),
101
+ ],
102
+ # Power Query (M) source — preserved so Refresh loads the data
103
+ m_expression='let Source = Csv.Document(File.Contents("sales.csv")) in Source',
104
+ ),
105
+ Table(
106
+ name="Product",
107
+ columns=[
108
+ Column(name="ProductId", data_type="Int64"),
109
+ Column(name="Name", data_type="String"),
110
+ ],
111
+ ),
112
+ ],
113
+ relationships=[
114
+ Relationship(
115
+ from_table="Sales", from_column="ProductId",
116
+ to_table="Product", to_column="ProductId",
117
+ ),
118
+ ],
119
+ )
120
+
121
+ datamodel_bytes: bytes = PbixMcpDataModelBuilder().build(model)
122
+ ```
123
+
124
+ ## Public API
125
+
126
+ | Object | Description |
127
+ | --- | --- |
128
+ | `PbipCompiler(pbip_path)` | Orchestrates a `.pbip` → `.pbix` compilation. |
129
+ | `PbipCompiler.compile(output)` | Compile and write the `.pbix`; returns the `Path`. |
130
+ | `PbipCompiler.compile_to_bytes()` | Compile and return the `.pbix` as `bytes`. |
131
+ | `SemanticModel` | A model: `tables` + `relationships`. |
132
+ | `Table` | `name`, `columns`, `measures`, `is_hidden`, `m_expression`. |
133
+ | `Column` | `name`, `data_type`, `source_column`. |
134
+ | `Measure` | `name`, `expression` (DAX). |
135
+ | `Relationship` | `from_table`/`from_column` → `to_table`/`to_column`. |
136
+
137
+ ## How it works
138
+
139
+ ```
140
+ .pbip folder
141
+ ├── *.Report/ → PBIR report ─┐
142
+ └── *.SemanticModel/ → TMDL / .bim ─┤
143
+
144
+ SemanticModelLoader + ReportLayoutLoader
145
+
146
+ PbixMcpDataModelBuilder (VertiPaq DataModel, M preserved)
147
+
148
+ PbixAssembler → MyReport.pbix
149
+ ```
150
+
151
+ - **Live connection** — if the report binds to a published semantic model, no
152
+ local DataModel is built; the report + a Connections part are embedded.
153
+ - **No / empty semantic model** — a thin (report-only) `.pbix` is produced.
154
+
155
+ ## License
156
+
157
+ [MIT](LICENSE) © 2026 Maxim Bacar
@@ -0,0 +1,38 @@
1
+ """pbip_compiler — compile a Power BI Project (.pbip) folder into a full .pbix file.
2
+
3
+ Pure Python — no CLI tools, no Power BI Desktop, no Windows required.
4
+
5
+ Public API
6
+ ──────────
7
+ from pbip_compiler import compile_pbip
8
+ compile_pbip("./MyProject", "./MyReport.pbix")
9
+
10
+ Package layout
11
+ ──────────────
12
+ discovery – locate the .Report / .SemanticModel folders
13
+ semantic_model/ – parse TMSL (model.bim) and TMDL into a normalised model
14
+ datamodel/ – build the VertiPaq DataModel via pbix-mcp (+ data fetch)
15
+ report/ – compile the PBIR report into a legacy Report/Layout
16
+ pbix/ – assemble the final .pbix ZIP
17
+ compiler – orchestrates the above (compile_pbip)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from .compiler import PbipCompiler
23
+ from .models import (
24
+ Column,
25
+ Measure,
26
+ Relationship,
27
+ SemanticModel,
28
+ Table,
29
+ )
30
+
31
+ __all__ = [
32
+ "PbipCompiler",
33
+ "SemanticModel",
34
+ "Table",
35
+ "Column",
36
+ "Measure",
37
+ "Relationship",
38
+ ]
@@ -0,0 +1,33 @@
1
+ """Command-line entry point — compile a .pbip folder into a .pbix file."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from argparse import ArgumentParser, RawDescriptionHelpFormatter
7
+ from pathlib import Path
8
+
9
+ from . import PbipCompiler
10
+
11
+
12
+ def main() -> None:
13
+ parser: ArgumentParser = ArgumentParser(
14
+ description = "Compile a .pbip folder into a .pbix file (pure Python).",
15
+ formatter_class = RawDescriptionHelpFormatter,
16
+ )
17
+ parser.add_argument("--pbip", required=True, help="Path to the .pbip project folder")
18
+ parser.add_argument("--output", required=True, help="Destination .pbix path")
19
+ args = parser.parse_args()
20
+
21
+ compiler: PbipCompiler = PbipCompiler(args.pbip)
22
+
23
+ try:
24
+ result: Path = compiler.compile(args.output)
25
+ print(f"\n✅ Success → {result}")
26
+
27
+ except Exception as exc:
28
+ print(f"\n❌ Error: {exc}", file=sys.stderr)
29
+ sys.exit(1)
30
+
31
+
32
+ if __name__ == "__main__":
33
+ main()
@@ -0,0 +1,88 @@
1
+ """Public entry point — orchestrate the .pbip → .pbix compilation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Optional, Union
7
+
8
+ from .datamodel import PbixMcpDataModelBuilder
9
+ from .discovery import (
10
+ find_report_folder,
11
+ find_semantic_model_folder,
12
+ read_live_connection,
13
+ )
14
+ from .pbix import PbixAssembler
15
+ from .report import ReportLayoutLoader
16
+ from .semantic_model import SemanticModelLoader
17
+
18
+
19
+ class PbipCompiler:
20
+ """Compile a .pbip project folder into a .pbix file."""
21
+
22
+ def __init__(self, pbip_path : Union[Path, str]) -> None:
23
+ self._model_loader = SemanticModelLoader()
24
+ self._layout_loader = ReportLayoutLoader()
25
+ self._datamodel_builder = PbixMcpDataModelBuilder()
26
+ self._assembler = PbixAssembler()
27
+
28
+ self.pbip_path : Path = Path(pbip_path).resolve()
29
+
30
+ def compile(self, output: Union[Path, str]) -> Path:
31
+ """Compile the project and write the .pbix to ``output``."""
32
+ output : Path = Path(output).resolve()
33
+ self._compile(output)
34
+ return output
35
+
36
+ def compile_to_bytes(self) -> bytes:
37
+ """Compile the project and return the .pbix as bytes (no file written)."""
38
+ return self._compile(None)
39
+
40
+ def _compile(self, output: Optional[Path]) -> bytes:
41
+
42
+ print(f"[pbip→pbix]")
43
+ print(f" source : {self.pbip_path}")
44
+
45
+
46
+ report_folder = find_report_folder(self.pbip_path)
47
+ print(f" report : {report_folder.relative_to(self.pbip_path)}/")
48
+
49
+ # Live connection (report bound to a published semantic model): no local
50
+ # model — embed the PBIR report + a Connections part and we're done.
51
+ connection_string = read_live_connection(report_folder)
52
+ if connection_string:
53
+ print(f" model : live connection (remote semantic model)")
54
+ return self._assembler.assemble_live_connection(
55
+ output, report_folder, connection_string,
56
+ )
57
+
58
+ semantic_folder = find_semantic_model_folder(self.pbip_path)
59
+ print(f" model : {semantic_folder.relative_to(self.pbip_path) if semantic_folder else '(none)'}/")
60
+
61
+ # report layout
62
+ layout = self._layout_loader.load(report_folder)
63
+ print(f" pages : {len(layout.get('sections', []))}")
64
+
65
+ # base pbix
66
+ base_pbix = self._build_base_pbix(semantic_folder)
67
+
68
+ # Assemble
69
+ return self._assembler.assemble(output, layout, base_pbix, report_folder=report_folder)
70
+
71
+ def _build_base_pbix(self, semantic_folder: Optional[Path]) -> Optional[bytes]:
72
+ if not semantic_folder:
73
+ print(" data : no SemanticModel folder — thin report")
74
+ return None
75
+
76
+ model = self._model_loader.load(semantic_folder)
77
+ if not model or not model.tables:
78
+ print(" data : semantic model empty — thin report")
79
+ return None
80
+
81
+ n_t = len(model.tables)
82
+ n_m = sum(len(t.measures) for t in model.tables)
83
+ n_r = len(model.relationships)
84
+ print(f" data : building DataModel "
85
+ f"({n_t} tables, {n_m} measures, {n_r} relationships)")
86
+
87
+ return self._datamodel_builder.build(model)
88
+
@@ -0,0 +1,10 @@
1
+ """DataModel building via pbix-mcp, with original-M preservation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .builder import DataModelBuilder, PbixMcpDataModelBuilder
6
+
7
+ __all__ = [
8
+ "DataModelBuilder",
9
+ "PbixMcpDataModelBuilder",
10
+ ]
@@ -0,0 +1,67 @@
1
+ from pbix_mcp.builder import PBIXBuilder
2
+ from ..models import Column, SemanticModel, Table
3
+ from .mpatch import patch_partition_m
4
+ from abc import ABC, abstractmethod
5
+
6
+ import warnings
7
+
8
+ class DataModelBuilder(ABC):
9
+ @abstractmethod
10
+ def build(self, model: SemanticModel) -> bytes:
11
+ ...
12
+
13
+ class PbixMcpDataModelBuilder(DataModelBuilder):
14
+
15
+ def _sentinel_row(columns: list[Column]) -> dict:
16
+ """Return one placeholder row with zero/empty values per column type."""
17
+ row: dict = {}
18
+ for c in columns:
19
+ if c.data_type == "String":
20
+ row[c.name] = " "
21
+ elif c.data_type == "Boolean":
22
+ row[c.name] = False
23
+ else:
24
+ row[c.name] = 0
25
+ return row
26
+
27
+
28
+ def build(self, model: SemanticModel) -> bytes:
29
+
30
+ builder : PBIXBuilder = PBIXBuilder()
31
+
32
+ for table in model.tables:
33
+ self._add_table(builder, table)
34
+ for measure in table.measures:
35
+ builder.add_measure(table.name, measure.name, measure.expression)
36
+
37
+ for relationship in model.relationships:
38
+ builder.add_relationship(
39
+ relationship.from_table, relationship.from_column, relationship.to_table, relationship.to_column,
40
+ )
41
+
42
+ with warnings.catch_warnings():
43
+ warnings.simplefilter("ignore")
44
+ pbix_bytes : bytes = builder.build()
45
+
46
+ # Overwrite pbix-mcp's placeholder partition query with the real M, so
47
+ # Refresh loads the data. Tables without an M keep the placeholder.
48
+ m_by_table = {t.name: t.m_expression for t in model.tables if t.m_expression}
49
+ for table in model.tables:
50
+ if table.m_expression:
51
+ print(f" [data] {table.name}: M preserved → refreshable "
52
+ f"(Refresh in Power BI loads the data)")
53
+ else:
54
+ print(f" [warn] {table.name}: no partition M → empty table")
55
+
56
+ return patch_partition_m(pbix_bytes, m_by_table)
57
+
58
+ def _add_table(self, builder: PBIXBuilder, t: Table) -> None:
59
+ # No source_db: pbix-mcp writes a placeholder #table query, which build()
60
+ # overwrites with the table's real M. One sentinel row keeps VertiPaq's
61
+ # column store valid (rows=[] corrupts it).
62
+ builder.add_table(
63
+ name = t.name,
64
+ columns = [c.model_dump(include={"name", "data_type"}) for c in t.columns],
65
+ rows = [PbixMcpDataModelBuilder._sentinel_row(t.columns)],
66
+ hidden = t.is_hidden,
67
+ )