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.
- pbip_compiler-0.2.0/.github/workflows/release.yml +40 -0
- pbip_compiler-0.2.0/.gitignore +14 -0
- pbip_compiler-0.2.0/.python-version +1 -0
- pbip_compiler-0.2.0/LICENSE +21 -0
- pbip_compiler-0.2.0/PKG-INFO +201 -0
- pbip_compiler-0.2.0/README.md +157 -0
- pbip_compiler-0.2.0/pbip_compiler/__init__.py +38 -0
- pbip_compiler-0.2.0/pbip_compiler/cli.py +33 -0
- pbip_compiler-0.2.0/pbip_compiler/compiler.py +88 -0
- pbip_compiler-0.2.0/pbip_compiler/datamodel/__init__.py +10 -0
- pbip_compiler-0.2.0/pbip_compiler/datamodel/builder.py +67 -0
- pbip_compiler-0.2.0/pbip_compiler/datamodel/mpatch.py +39 -0
- pbip_compiler-0.2.0/pbip_compiler/discovery.py +50 -0
- pbip_compiler-0.2.0/pbip_compiler/models.py +37 -0
- pbip_compiler-0.2.0/pbip_compiler/pbix/__init__.py +7 -0
- pbip_compiler-0.2.0/pbip_compiler/pbix/assembler.py +229 -0
- pbip_compiler-0.2.0/pbip_compiler/pbix/constants.py +83 -0
- pbip_compiler-0.2.0/pbip_compiler/report/__init__.py +9 -0
- pbip_compiler-0.2.0/pbip_compiler/report/layout.py +51 -0
- pbip_compiler-0.2.0/pbip_compiler/report/pbir.py +359 -0
- pbip_compiler-0.2.0/pbip_compiler/report/resources.py +30 -0
- pbip_compiler-0.2.0/pbip_compiler/semantic_model/__init__.py +9 -0
- pbip_compiler-0.2.0/pbip_compiler/semantic_model/loader.py +30 -0
- pbip_compiler-0.2.0/pbip_compiler/semantic_model/tmdl.py +102 -0
- pbip_compiler-0.2.0/pbip_compiler/semantic_model/tmdl_table.py +413 -0
- pbip_compiler-0.2.0/pbip_compiler/semantic_model/tmsl.py +75 -0
- pbip_compiler-0.2.0/pbip_compiler/semantic_model/types.py +35 -0
- pbip_compiler-0.2.0/pyproject.toml +43 -0
- 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 @@
|
|
|
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,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
|
+
)
|