dax-quax 0.1.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 (149) hide show
  1. dax_quax-0.1.0/.github/workflows/ci.yml +46 -0
  2. dax_quax-0.1.0/.github/workflows/release.yml +49 -0
  3. dax_quax-0.1.0/.gitignore +53 -0
  4. dax_quax-0.1.0/LICENSE +21 -0
  5. dax_quax-0.1.0/PKG-INFO +183 -0
  6. dax_quax-0.1.0/README.md +146 -0
  7. dax_quax-0.1.0/pyproject.toml +89 -0
  8. dax_quax-0.1.0/src/dax_quax/__init__.py +86 -0
  9. dax_quax-0.1.0/src/dax_quax/analysis/__init__.py +1 -0
  10. dax_quax-0.1.0/src/dax_quax/analysis/baseline.py +433 -0
  11. dax_quax-0.1.0/src/dax_quax/analysis/diagram.py +296 -0
  12. dax_quax-0.1.0/src/dax_quax/analysis/lineage.py +416 -0
  13. dax_quax-0.1.0/src/dax_quax/analysis/report_findings.py +265 -0
  14. dax_quax-0.1.0/src/dax_quax/analysis/timings.py +460 -0
  15. dax_quax-0.1.0/src/dax_quax/analysis/usage.py +486 -0
  16. dax_quax-0.1.0/src/dax_quax/analysis/vertipaq.py +132 -0
  17. dax_quax-0.1.0/src/dax_quax/cli/__init__.py +706 -0
  18. dax_quax-0.1.0/src/dax_quax/cli/__main__.py +10 -0
  19. dax_quax-0.1.0/src/dax_quax/dax/__init__.py +32 -0
  20. dax_quax-0.1.0/src/dax_quax/dax/extract.py +142 -0
  21. dax_quax-0.1.0/src/dax_quax/dax/resolve.py +203 -0
  22. dax_quax-0.1.0/src/dax_quax/dax/tokenize.py +166 -0
  23. dax_quax-0.1.0/src/dax_quax/errors.py +45 -0
  24. dax_quax-0.1.0/src/dax_quax/mcp/__init__.py +12 -0
  25. dax_quax-0.1.0/src/dax_quax/mcp/server.py +129 -0
  26. dax_quax-0.1.0/src/dax_quax/mcp/tools.py +564 -0
  27. dax_quax-0.1.0/src/dax_quax/model/__init__.py +380 -0
  28. dax_quax-0.1.0/src/dax_quax/render/__init__.py +8 -0
  29. dax_quax-0.1.0/src/dax_quax/render/_escape.py +56 -0
  30. dax_quax-0.1.0/src/dax_quax/render/diagram.py +253 -0
  31. dax_quax-0.1.0/src/dax_quax/render/editor.py +51 -0
  32. dax_quax-0.1.0/src/dax_quax/render/graph.py +274 -0
  33. dax_quax-0.1.0/src/dax_quax/render/report.py +313 -0
  34. dax_quax-0.1.0/src/dax_quax/render/serve.py +407 -0
  35. dax_quax-0.1.0/src/dax_quax/render/templates/_styles.html.j2 +157 -0
  36. dax_quax-0.1.0/src/dax_quax/render/templates/report.html.j2 +329 -0
  37. dax_quax-0.1.0/src/dax_quax/render/templates/workspace.html.j2 +202 -0
  38. dax_quax-0.1.0/src/dax_quax/render/workspace.py +201 -0
  39. dax_quax-0.1.0/src/dax_quax/report/__init__.py +56 -0
  40. dax_quax-0.1.0/src/dax_quax/report/pbir.py +366 -0
  41. dax_quax-0.1.0/src/dax_quax/runtime.py +131 -0
  42. dax_quax-0.1.0/src/dax_quax/sources/__init__.py +1 -0
  43. dax_quax-0.1.0/src/dax_quax/sources/dmv.py +837 -0
  44. dax_quax-0.1.0/src/dax_quax/sources/file.py +442 -0
  45. dax_quax-0.1.0/src/dax_quax/sources/live.py +473 -0
  46. dax_quax-0.1.0/src/dax_quax/sources/pbip.py +673 -0
  47. dax_quax-0.1.0/src/dax_quax/sources/tmdl.py +305 -0
  48. dax_quax-0.1.0/src/dax_quax/sources/trace.py +357 -0
  49. dax_quax-0.1.0/src/dax_quax/sources/workspace.py +379 -0
  50. dax_quax-0.1.0/tests/conftest.py +87 -0
  51. dax_quax-0.1.0/tests/fixtures/rich/rowsets.json +560 -0
  52. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.Report/definition/bookmarks/b1.json +19 -0
  53. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.Report/definition/pages/Overview/page.json +13 -0
  54. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.Report/definition/pages/Overview/visuals/v1/visual.json +37 -0
  55. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.Report/definition/pages/Overview/visuals/v2/visual.json +49 -0
  56. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.Report/definition/pages/pages.json +1 -0
  57. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.Report/definition/report.json +11 -0
  58. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.Report/definition.pbir +1 -0
  59. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.SemanticModel/definition/model.tmdl +16 -0
  60. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.SemanticModel/definition/relationships.tmdl +3 -0
  61. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.SemanticModel/definition/roles/Auditor.tmdl +4 -0
  62. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.SemanticModel/definition/roles/Sales Rep.tmdl +4 -0
  63. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.SemanticModel/definition/tables/Customer.tmdl +29 -0
  64. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.SemanticModel/definition/tables/LocalDateTable_8f3c1e2a-0000-4000-9000-1234567890ab.tmdl +10 -0
  65. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.SemanticModel/definition/tables/Product.tmdl +57 -0
  66. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.SemanticModel/definition/tables/Sales.tmdl +56 -0
  67. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.SemanticModel/definition/tables/Time Intelligence.tmdl +18 -0
  68. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.SemanticModel/definition.pbism +4 -0
  69. dax_quax-0.1.0/tests/fixtures/rich_pbip/Contoso.pbip +13 -0
  70. dax_quax-0.1.0/tests/fixtures/synthetic/rowsets.json +330 -0
  71. dax_quax-0.1.0/tests/fixtures/synthetic_pbip/Contoso.Report/definition/bookmarks/b1.json +19 -0
  72. dax_quax-0.1.0/tests/fixtures/synthetic_pbip/Contoso.Report/definition/pages/Overview/page.json +13 -0
  73. dax_quax-0.1.0/tests/fixtures/synthetic_pbip/Contoso.Report/definition/pages/Overview/visuals/v1/visual.json +37 -0
  74. dax_quax-0.1.0/tests/fixtures/synthetic_pbip/Contoso.Report/definition/pages/Overview/visuals/v2/visual.json +49 -0
  75. dax_quax-0.1.0/tests/fixtures/synthetic_pbip/Contoso.Report/definition/pages/pages.json +1 -0
  76. dax_quax-0.1.0/tests/fixtures/synthetic_pbip/Contoso.Report/definition/report.json +11 -0
  77. dax_quax-0.1.0/tests/fixtures/synthetic_pbip/Contoso.Report/definition.pbir +1 -0
  78. dax_quax-0.1.0/tests/fixtures/synthetic_pbip/Contoso.SemanticModel/definition/model.tmdl +12 -0
  79. dax_quax-0.1.0/tests/fixtures/synthetic_pbip/Contoso.SemanticModel/definition/relationships.tmdl +3 -0
  80. dax_quax-0.1.0/tests/fixtures/synthetic_pbip/Contoso.SemanticModel/definition/tables/LocalDateTable_8f3c1e2a-0000-4000-9000-1234567890ab.tmdl +10 -0
  81. dax_quax-0.1.0/tests/fixtures/synthetic_pbip/Contoso.SemanticModel/definition/tables/Product.tmdl +40 -0
  82. dax_quax-0.1.0/tests/fixtures/synthetic_pbip/Contoso.SemanticModel/definition/tables/Sales.tmdl +54 -0
  83. dax_quax-0.1.0/tests/fixtures/synthetic_pbip/Contoso.SemanticModel/definition.pbism +4 -0
  84. dax_quax-0.1.0/tests/fixtures/synthetic_pbip/Contoso.pbip +13 -0
  85. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Contoso.Report/definition/bookmarks/b1.json +19 -0
  86. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Contoso.Report/definition/pages/Overview/page.json +13 -0
  87. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Contoso.Report/definition/pages/Overview/visuals/v1/visual.json +37 -0
  88. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Contoso.Report/definition/pages/Overview/visuals/v2/visual.json +49 -0
  89. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Contoso.Report/definition/pages/pages.json +1 -0
  90. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Contoso.Report/definition/report.json +11 -0
  91. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Contoso.Report/definition.pbir +8 -0
  92. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Contoso.SemanticModel/definition/model.tmdl +12 -0
  93. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Contoso.SemanticModel/definition/relationships.tmdl +3 -0
  94. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Contoso.SemanticModel/definition/tables/LocalDateTable_8f3c1e2a-0000-4000-9000-1234567890ab.tmdl +10 -0
  95. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Contoso.SemanticModel/definition/tables/Product.tmdl +40 -0
  96. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Contoso.SemanticModel/definition/tables/Sales.tmdl +54 -0
  97. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Contoso.SemanticModel/definition.pbism +4 -0
  98. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Contoso.pbip +3 -0
  99. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Exec.Report/definition/pages/Board/page.json +4 -0
  100. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Exec.Report/definition/pages/Board/visuals/v9/visual.json +38 -0
  101. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Exec.Report/definition/pages/pages.json +1 -0
  102. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Exec.Report/definition.pbir +8 -0
  103. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Orphan.SemanticModel/definition/model.tmdl +4 -0
  104. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Orphan.SemanticModel/definition/tables/Scratch.tmdl +10 -0
  105. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Orphan.SemanticModel/definition.pbism +1 -0
  106. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Regional.Report/definition/pages/Map/page.json +4 -0
  107. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Regional.Report/definition/pages/Map/visuals/v1/visual.json +26 -0
  108. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Regional.Report/definition/pages/pages.json +1 -0
  109. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Regional.Report/definition.pbir +14 -0
  110. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Regional.SemanticModel/definition/model.tmdl +4 -0
  111. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Regional.SemanticModel/definition/tables/Region.tmdl +15 -0
  112. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Regional.SemanticModel/definition.pbism +1 -0
  113. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Stray.Report/definition/pages/P/page.json +4 -0
  114. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Stray.Report/definition/pages/P/visuals/v1/visual.json +26 -0
  115. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Stray.Report/definition/pages/pages.json +1 -0
  116. dax_quax-0.1.0/tests/fixtures/synthetic_workspace/Stray.Report/definition.pbir +9 -0
  117. dax_quax-0.1.0/tests/test_baseline.py +314 -0
  118. dax_quax-0.1.0/tests/test_byconnection.py +192 -0
  119. dax_quax-0.1.0/tests/test_check_imports.py +71 -0
  120. dax_quax-0.1.0/tests/test_dax_extract.py +207 -0
  121. dax_quax-0.1.0/tests/test_dax_resolve.py +155 -0
  122. dax_quax-0.1.0/tests/test_demo_corpus.py +204 -0
  123. dax_quax-0.1.0/tests/test_diagram.py +249 -0
  124. dax_quax-0.1.0/tests/test_dmv.py +221 -0
  125. dax_quax-0.1.0/tests/test_file_source.py +371 -0
  126. dax_quax-0.1.0/tests/test_hardening.py +174 -0
  127. dax_quax-0.1.0/tests/test_lineage.py +322 -0
  128. dax_quax-0.1.0/tests/test_live_discovery.py +130 -0
  129. dax_quax-0.1.0/tests/test_locations.py +245 -0
  130. dax_quax-0.1.0/tests/test_mcp.py +489 -0
  131. dax_quax-0.1.0/tests/test_pbip.py +329 -0
  132. dax_quax-0.1.0/tests/test_r1_gaps.py +156 -0
  133. dax_quax-0.1.0/tests/test_r2_false_removes.py +300 -0
  134. dax_quax-0.1.0/tests/test_render.py +291 -0
  135. dax_quax-0.1.0/tests/test_report_findings.py +247 -0
  136. dax_quax-0.1.0/tests/test_report_pbir.py +114 -0
  137. dax_quax-0.1.0/tests/test_runtime_install.py +226 -0
  138. dax_quax-0.1.0/tests/test_serve.py +258 -0
  139. dax_quax-0.1.0/tests/test_serve_workspace.py +172 -0
  140. dax_quax-0.1.0/tests/test_timings.py +377 -0
  141. dax_quax-0.1.0/tests/test_usage.py +294 -0
  142. dax_quax-0.1.0/tests/test_vertipaq.py +71 -0
  143. dax_quax-0.1.0/tests/test_workspace.py +262 -0
  144. dax_quax-0.1.0/tests/test_workspace_render.py +178 -0
  145. dax_quax-0.1.0/tools/check_imports.py +146 -0
  146. dax_quax-0.1.0/tools/fetch_demo.py +55 -0
  147. dax_quax-0.1.0/tools/fetch_libs.py +76 -0
  148. dax_quax-0.1.0/tools/fetch_pbix.py +77 -0
  149. dax_quax-0.1.0/tools/record_fixture.py +93 -0
@@ -0,0 +1,46 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ # Ubuntu on purpose. CONVENTIONS §10 promises the suite runs with no Power BI Desktop,
11
+ # no .NET and no network; running it anywhere else would let that promise rot quietly.
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ # Both results, always. With fail-fast one version's failure cancels the other and
15
+ # hides whether the problem is the code or that one interpreter.
16
+ fail-fast: false
17
+ matrix:
18
+ # Every version pyproject.toml's requires-python claims. ">=3.12" is a promise to
19
+ # anyone installing this, and a promise no job exercises is a guess. Widening
20
+ # requires-python means adding the version here in the same commit.
21
+ python: ["3.12", "3.13", "3.14"]
22
+ name: test (py${{ matrix.python }})
23
+ steps:
24
+ - uses: actions/checkout@v7
25
+ - uses: actions/setup-python@v7
26
+ with:
27
+ python-version: ${{ matrix.python }}
28
+ - run: pip install -e ".[dev,file,report,serve,mcp]"
29
+ # One version is enough for lint: ruff reads the target-version from pyproject.
30
+ - run: ruff check .
31
+ if: matrix.python == '3.12'
32
+ - run: pytest -q
33
+
34
+ imports-without-extras:
35
+ # Installed bare, so a dependency somebody added to the core requirements without
36
+ # declaring it in pyproject.toml shows up here and nowhere else. The check itself also
37
+ # blocks the known optional packages, which is why it runs as an ordinary test on a
38
+ # developer machine where every extra is present -- see tools/check_imports.py.
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - uses: actions/checkout@v7
42
+ - uses: actions/setup-python@v7
43
+ with:
44
+ python-version: "3.12"
45
+ - run: pip install -e .
46
+ - run: python tools/check_imports.py
@@ -0,0 +1,49 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build:
9
+ # Only the public copy publishes. The private repo carries this file so the two trees
10
+ # stay identical, and a tag pushed there must not race the real release to PyPI.
11
+ if: github.repository == 'S-CurveLabs/dax-quax'
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v7
15
+ - uses: actions/setup-python@v7
16
+ with:
17
+ python-version: "3.12"
18
+ # A tag that disagrees with pyproject.toml would publish a version nobody asked for,
19
+ # and PyPI never lets a version number be reused.
20
+ - name: tag matches version
21
+ run: |
22
+ version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
23
+ test "v$version" = "$GITHUB_REF_NAME" || { echo "tag $GITHUB_REF_NAME != v$version"; exit 1; }
24
+ - run: pip install -e ".[dev,file,report,serve,mcp]"
25
+ - run: pytest -q
26
+ - run: pip install build twine
27
+ - run: python -m build
28
+ - run: twine check --strict dist/*
29
+ - uses: actions/upload-artifact@v7
30
+ with:
31
+ name: dist
32
+ path: dist/
33
+
34
+ publish:
35
+ # Trusted publishing: PyPI trusts this workflow and environment by name, so no API
36
+ # token is stored anywhere.
37
+ needs: build
38
+ runs-on: ubuntu-latest
39
+ environment:
40
+ name: pypi
41
+ url: https://pypi.org/project/dax-quax/
42
+ permissions:
43
+ id-token: write
44
+ steps:
45
+ - uses: actions/download-artifact@v8
46
+ with:
47
+ name: dist
48
+ path: dist/
49
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,53 @@
1
+ # python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ venv/
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+
12
+ # vendored .NET assemblies — fetched, not committed
13
+ spikes/**/libs/
14
+ src/dax_quax/_libs/
15
+ *.nupkg
16
+ *.runtimeconfig.json
17
+
18
+ # local model artefacts
19
+ *.pbix
20
+ *.pbip
21
+ *.abf
22
+ *.vpax
23
+ cache.abf
24
+
25
+ # ...but fixtures are exempt. The synthetic .pbip project has to be committed, and
26
+ # the patterns above would otherwise swallow it. Last matching rule wins, so this
27
+ # has to sit after them.
28
+ !tests/fixtures/**
29
+
30
+ # local scratch renders
31
+ .scratch/
32
+
33
+ # a real .pbip corpus, borrowed rather than vendored (tools/fetch_demo.py).
34
+ # It carries no licence, and CONVENTIONS §10 keeps CI offline.
35
+ tests/fixtures/demo/
36
+
37
+ # A fixture recorded from the borrowed corpus loaded into Power BI Desktop. Derived from
38
+ # unlicensed content, so it is reproduced rather than committed:
39
+ # python tools/fetch_demo.py && open Report01.pbip && python tools/record_fixture.py demo01
40
+ tests/fixtures/demo01/
41
+
42
+ # Real .pbix samples, borrowed for the pbixray contract (tools/fetch_pbix.py).
43
+ tests/fixtures/pbix/
44
+
45
+ # Generated review / audit reports, from agents or otherwise. Each one is a snapshot of
46
+ # one pass over the tree: it goes stale the moment a finding is fixed, and a stale one
47
+ # gets quoted back as current fact. Keep what survived triage in CONVENTIONS or a test.
48
+ *_report.md
49
+ *_REPORT.md
50
+ *review*.md
51
+ *REVIEW*.md
52
+ *redteam*.md
53
+ *audit*.md
dax_quax-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 S-Curve Labs
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,183 @@
1
+ Metadata-Version: 2.5
2
+ Name: dax-quax
3
+ Version: 0.1.0
4
+ Summary: What does every object in this tabular model cost, what depends on it, and can it be deleted?
5
+ Project-URL: Homepage, https://github.com/S-CurveLabs/dax-quax
6
+ Project-URL: Source, https://github.com/S-CurveLabs/dax-quax
7
+ Project-URL: Issues, https://github.com/S-CurveLabs/dax-quax/issues
8
+ Author: S-Curve Labs
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: dax,mcp,pbip,power-bi,tabular,tmdl,vertipaq
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Database
18
+ Classifier: Topic :: Software Development :: Quality Assurance
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: networkx>=3.0
21
+ Requires-Dist: pandas>=2.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8.0; extra == 'dev'
24
+ Requires-Dist: ruff>=0.6; extra == 'dev'
25
+ Provides-Extra: file
26
+ Requires-Dist: pbixray>=0.3; extra == 'file'
27
+ Provides-Extra: live
28
+ Requires-Dist: pythonnet>=3.0; extra == 'live'
29
+ Provides-Extra: mcp
30
+ Requires-Dist: mcp>=2.2; extra == 'mcp'
31
+ Provides-Extra: report
32
+ Requires-Dist: jinja2>=3.1; extra == 'report'
33
+ Provides-Extra: serve
34
+ Requires-Dist: fastapi>=0.110; extra == 'serve'
35
+ Requires-Dist: uvicorn>=0.29; extra == 'serve'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # dax-quax
39
+
40
+ **For every object in a Power BI model: what does it cost, what depends on it, and can it be deleted?**
41
+
42
+ Not a DAX Studio clone. It answers one question, and it is careful about the answer, because
43
+ telling you to delete a column that something still uses is the only way a tool like this can
44
+ really hurt you.
45
+
46
+ ```
47
+ Contoso (pbip)
48
+ 87 objects, 214.3 MB measured
49
+ REMOVE 6 41.2 MB (19.2% of measured)
50
+ REVIEW 3 8.1 MB
51
+ UNKNOWN 0
52
+ KEEP 78
53
+
54
+ top 3 by size:
55
+ REMOVE 28.4 MB Sales[Order Number]
56
+ Remove it from the Power Query load.
57
+ Contoso.SemanticModel/definition/tables/Sales.tmdl:3
58
+
59
+ scope: 3 reports scanned
60
+ ```
61
+
62
+ ## Install
63
+
64
+ ```bash
65
+ pip install "dax-quax[file,report]"
66
+ ```
67
+
68
+ Reading a `.pbip` project folder or a `.pbix` file needs nothing else — no Power BI Desktop,
69
+ no .NET, and it works on Linux and macOS. To scan a *running* Power BI Desktop you also need
70
+ the Microsoft client assemblies, which cannot ship in the wheel:
71
+
72
+ ```bash
73
+ pip install "dax-quax[live]"
74
+ dax-quax install-runtime
75
+ ```
76
+
77
+ That fetches ADOMD.NET from nuget.org into a per-user folder. Windows only, because the
78
+ engine it talks to is.
79
+
80
+ If you skip it, the first live connection offers to do it for you when there is a terminal
81
+ to ask at. Set `DAXQUAX_AUTO_INSTALL_RUNTIME=1` to answer yes in advance, or `=0` to be left
82
+ alone — unattended runs are never prompted, because a question nobody can answer is a hang.
83
+
84
+ They are not in the wheel on purpose. The assemblies are Microsoft's: the EULA does have a
85
+ distributable-code clause, but it is scoped to a REDIST list the NuGet package does not
86
+ contain, and one of its conditions is requiring end users to accept terms protecting
87
+ Microsoft, which `pip install` does not do.
88
+
89
+ | Extra | For |
90
+ |---|---|
91
+ | `file` | `.pbix`, `.abf`, PowerPivot `.xlsx` — metadata **and** sizes with nothing installed |
92
+ | `report` | the HTML report |
93
+ | `serve` | `serve`, the browsable version |
94
+ | `live` | a running Power BI Desktop (plus `dax-quax install-runtime`) |
95
+ | `mcp` | the MCP server |
96
+
97
+ ## Use it
98
+
99
+ ```bash
100
+ dax-quax scan --pbip path/to/Model.SemanticModel --report-folder path/to/Name.Report
101
+ dax-quax scan --pbix path/to/file.pbix
102
+ dax-quax scan # a running Power BI Desktop
103
+ dax-quax scan --workspace path/to/folder # every model, against every report over it
104
+
105
+ dax-quax report --pbip DIR --out report.html # one self-contained file, no network
106
+ dax-quax serve --pbip DIR # the same, plus lineage and a rescan
107
+ dax-quax bench "EVALUATE ..." --cold # storage-engine vs formula-engine timings
108
+ dax-quax mcp --pbip DIR # answer questions about the model, read-only
109
+ ```
110
+
111
+ As a library:
112
+
113
+ ```python
114
+ from dax_quax import open_pbip
115
+
116
+ model = open_pbip("path/to/Model.SemanticModel")
117
+ model.findings() # ranked cost x usage
118
+ model.lineage() # what breaks if this goes
119
+ model.where("[Margin]") # the file and line that define it
120
+ ```
121
+
122
+ ## What makes a verdict trustworthy
123
+
124
+ A verdict is only as good as what was scanned, so **the scope travels with it** instead of
125
+ living in a header you might not have read.
126
+
127
+ - `REMOVE` means *nothing in what was scanned referenced it*. It is a question to put to a
128
+ person, not an instruction.
129
+ - **If no report was scanned, nothing is ever `REMOVE`.** Every unreferenced object is
130
+ `UNKNOWN`, because a report you left out is usage that was not counted.
131
+ - In a workspace scan, **one report that cannot be matched to its model degrades every
132
+ verdict in the whole directory.**
133
+ - Anything the scan could not see is listed on the report, next to the verdicts.
134
+
135
+ A false `REMOVE` is the expensive failure. `UNKNOWN` is free. The tool is built to prefer the
136
+ second, and several places say so out loud rather than quietly rounding a gap to zero — a size
137
+ it could not measure stays `None` and never becomes `0 B`.
138
+
139
+ ## For agents
140
+
141
+ `dax-quax mcp` exposes the model over MCP, read-only, so you can ask an assistant what is safe
142
+ to delete and why. The surface is fenced three ways: every tool is a function of a snapshot
143
+ that holds no connection, an allowlist controls what is reachable, and each tool is advertised
144
+ with the protocol's own `readOnlyHint`. Every payload carrying a verdict also carries the scope
145
+ caveat — an agent gets one payload and no headline, so the caveat is attached centrally rather
146
+ than left to each tool to remember.
147
+
148
+ ```jsonc
149
+ {"dax-quax": {"command": "dax-quax", "args": ["mcp", "--pbip", "C:/path/Model.SemanticModel"]}}
150
+ ```
151
+
152
+ ## Limits worth knowing before you trust it
153
+
154
+ - Consumers outside the artefacts you scan — Excel, paginated reports, anything on the XMLA
155
+ endpoint — are invisible. A workspace scan narrows this; it cannot close it.
156
+ - Perspectives and translations are not walked. Neither can cause a false `REMOVE`.
157
+ - `.pbix` sizes come from [pbixray](https://github.com/Hugoberry/pbixray), which reports no
158
+ encoding, no row count and no hierarchy size — those stay absent rather than becoming zero.
159
+ - DAX references are *extracted*, not parsed. Dynamic references built with string
160
+ manipulation cannot be seen by anything that does not run the query.
161
+ - Version 0.1. Six of the seven declared interfaces to Microsoft's formats and APIs turned out
162
+ to be wrong the first time they met a real engine; they are fixed and pinned by tests now,
163
+ but that is the nature of the terrain.
164
+
165
+ ## Development
166
+
167
+ ```bash
168
+ python -m venv .venv && .venv/Scripts/activate # source .venv/bin/activate elsewhere
169
+ pip install -e ".[dev,file,report,serve,mcp]"
170
+ pytest -q # no Power BI, no .NET, no network
171
+ python tools/check_imports.py # nothing may need an extra to import
172
+ ```
173
+
174
+ Run `pytest` from inside the environment you installed into. The extras carry the test
175
+ dependencies, so a `pytest` picked up from somewhere else fails during collection on an
176
+ import — `No module named 'networkx'` — rather than on anything you changed.
177
+
178
+ Tests that need something real are deselected by default — `-m live` for a running instance,
179
+ `-m pbix` after `python tools/fetch_pbix.py`, `-m demo` after `python tools/fetch_demo.py`.
180
+
181
+ ## License
182
+
183
+ MIT.
@@ -0,0 +1,146 @@
1
+ # dax-quax
2
+
3
+ **For every object in a Power BI model: what does it cost, what depends on it, and can it be deleted?**
4
+
5
+ Not a DAX Studio clone. It answers one question, and it is careful about the answer, because
6
+ telling you to delete a column that something still uses is the only way a tool like this can
7
+ really hurt you.
8
+
9
+ ```
10
+ Contoso (pbip)
11
+ 87 objects, 214.3 MB measured
12
+ REMOVE 6 41.2 MB (19.2% of measured)
13
+ REVIEW 3 8.1 MB
14
+ UNKNOWN 0
15
+ KEEP 78
16
+
17
+ top 3 by size:
18
+ REMOVE 28.4 MB Sales[Order Number]
19
+ Remove it from the Power Query load.
20
+ Contoso.SemanticModel/definition/tables/Sales.tmdl:3
21
+
22
+ scope: 3 reports scanned
23
+ ```
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install "dax-quax[file,report]"
29
+ ```
30
+
31
+ Reading a `.pbip` project folder or a `.pbix` file needs nothing else — no Power BI Desktop,
32
+ no .NET, and it works on Linux and macOS. To scan a *running* Power BI Desktop you also need
33
+ the Microsoft client assemblies, which cannot ship in the wheel:
34
+
35
+ ```bash
36
+ pip install "dax-quax[live]"
37
+ dax-quax install-runtime
38
+ ```
39
+
40
+ That fetches ADOMD.NET from nuget.org into a per-user folder. Windows only, because the
41
+ engine it talks to is.
42
+
43
+ If you skip it, the first live connection offers to do it for you when there is a terminal
44
+ to ask at. Set `DAXQUAX_AUTO_INSTALL_RUNTIME=1` to answer yes in advance, or `=0` to be left
45
+ alone — unattended runs are never prompted, because a question nobody can answer is a hang.
46
+
47
+ They are not in the wheel on purpose. The assemblies are Microsoft's: the EULA does have a
48
+ distributable-code clause, but it is scoped to a REDIST list the NuGet package does not
49
+ contain, and one of its conditions is requiring end users to accept terms protecting
50
+ Microsoft, which `pip install` does not do.
51
+
52
+ | Extra | For |
53
+ |---|---|
54
+ | `file` | `.pbix`, `.abf`, PowerPivot `.xlsx` — metadata **and** sizes with nothing installed |
55
+ | `report` | the HTML report |
56
+ | `serve` | `serve`, the browsable version |
57
+ | `live` | a running Power BI Desktop (plus `dax-quax install-runtime`) |
58
+ | `mcp` | the MCP server |
59
+
60
+ ## Use it
61
+
62
+ ```bash
63
+ dax-quax scan --pbip path/to/Model.SemanticModel --report-folder path/to/Name.Report
64
+ dax-quax scan --pbix path/to/file.pbix
65
+ dax-quax scan # a running Power BI Desktop
66
+ dax-quax scan --workspace path/to/folder # every model, against every report over it
67
+
68
+ dax-quax report --pbip DIR --out report.html # one self-contained file, no network
69
+ dax-quax serve --pbip DIR # the same, plus lineage and a rescan
70
+ dax-quax bench "EVALUATE ..." --cold # storage-engine vs formula-engine timings
71
+ dax-quax mcp --pbip DIR # answer questions about the model, read-only
72
+ ```
73
+
74
+ As a library:
75
+
76
+ ```python
77
+ from dax_quax import open_pbip
78
+
79
+ model = open_pbip("path/to/Model.SemanticModel")
80
+ model.findings() # ranked cost x usage
81
+ model.lineage() # what breaks if this goes
82
+ model.where("[Margin]") # the file and line that define it
83
+ ```
84
+
85
+ ## What makes a verdict trustworthy
86
+
87
+ A verdict is only as good as what was scanned, so **the scope travels with it** instead of
88
+ living in a header you might not have read.
89
+
90
+ - `REMOVE` means *nothing in what was scanned referenced it*. It is a question to put to a
91
+ person, not an instruction.
92
+ - **If no report was scanned, nothing is ever `REMOVE`.** Every unreferenced object is
93
+ `UNKNOWN`, because a report you left out is usage that was not counted.
94
+ - In a workspace scan, **one report that cannot be matched to its model degrades every
95
+ verdict in the whole directory.**
96
+ - Anything the scan could not see is listed on the report, next to the verdicts.
97
+
98
+ A false `REMOVE` is the expensive failure. `UNKNOWN` is free. The tool is built to prefer the
99
+ second, and several places say so out loud rather than quietly rounding a gap to zero — a size
100
+ it could not measure stays `None` and never becomes `0 B`.
101
+
102
+ ## For agents
103
+
104
+ `dax-quax mcp` exposes the model over MCP, read-only, so you can ask an assistant what is safe
105
+ to delete and why. The surface is fenced three ways: every tool is a function of a snapshot
106
+ that holds no connection, an allowlist controls what is reachable, and each tool is advertised
107
+ with the protocol's own `readOnlyHint`. Every payload carrying a verdict also carries the scope
108
+ caveat — an agent gets one payload and no headline, so the caveat is attached centrally rather
109
+ than left to each tool to remember.
110
+
111
+ ```jsonc
112
+ {"dax-quax": {"command": "dax-quax", "args": ["mcp", "--pbip", "C:/path/Model.SemanticModel"]}}
113
+ ```
114
+
115
+ ## Limits worth knowing before you trust it
116
+
117
+ - Consumers outside the artefacts you scan — Excel, paginated reports, anything on the XMLA
118
+ endpoint — are invisible. A workspace scan narrows this; it cannot close it.
119
+ - Perspectives and translations are not walked. Neither can cause a false `REMOVE`.
120
+ - `.pbix` sizes come from [pbixray](https://github.com/Hugoberry/pbixray), which reports no
121
+ encoding, no row count and no hierarchy size — those stay absent rather than becoming zero.
122
+ - DAX references are *extracted*, not parsed. Dynamic references built with string
123
+ manipulation cannot be seen by anything that does not run the query.
124
+ - Version 0.1. Six of the seven declared interfaces to Microsoft's formats and APIs turned out
125
+ to be wrong the first time they met a real engine; they are fixed and pinned by tests now,
126
+ but that is the nature of the terrain.
127
+
128
+ ## Development
129
+
130
+ ```bash
131
+ python -m venv .venv && .venv/Scripts/activate # source .venv/bin/activate elsewhere
132
+ pip install -e ".[dev,file,report,serve,mcp]"
133
+ pytest -q # no Power BI, no .NET, no network
134
+ python tools/check_imports.py # nothing may need an extra to import
135
+ ```
136
+
137
+ Run `pytest` from inside the environment you installed into. The extras carry the test
138
+ dependencies, so a `pytest` picked up from somewhere else fails during collection on an
139
+ import — `No module named 'networkx'` — rather than on anything you changed.
140
+
141
+ Tests that need something real are deselected by default — `-m live` for a running instance,
142
+ `-m pbix` after `python tools/fetch_pbix.py`, `-m demo` after `python tools/fetch_demo.py`.
143
+
144
+ ## License
145
+
146
+ MIT.
@@ -0,0 +1,89 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dax-quax"
7
+ version = "0.1.0"
8
+ description = "What does every object in this tabular model cost, what depends on it, and can it be deleted?"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.12"
13
+ authors = [{ name = "S-Curve Labs" }]
14
+ keywords = ["power-bi", "tabular", "vertipaq", "dax", "tmdl", "pbip", "mcp"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Topic :: Database",
22
+ "Topic :: Software Development :: Quality Assurance",
23
+ ]
24
+ dependencies = ["pandas>=2.0", "networkx>=3.0"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/S-CurveLabs/dax-quax"
28
+ Source = "https://github.com/S-CurveLabs/dax-quax"
29
+ Issues = "https://github.com/S-CurveLabs/dax-quax/issues"
30
+
31
+ [project.optional-dependencies]
32
+ live = ["pythonnet>=3.0"]
33
+ file = ["pbixray>=0.3"]
34
+ report = ["jinja2>=3.1"]
35
+ serve = ["fastapi>=0.110", "uvicorn>=0.29"]
36
+ mcp = ["mcp>=2.2"]
37
+ dev = ["pytest>=8.0", "ruff>=0.6"]
38
+
39
+ [project.scripts]
40
+ dax-quax = "dax_quax.cli:main"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/dax_quax"]
44
+ # Never ship the vendored .NET assemblies. They are Microsoft's, whether we may
45
+ # redistribute them is unsettled, and they are 5 MB that only a live connection uses --
46
+ # `dax-quax install-runtime` fetches them per user instead. Hatchling already honours
47
+ # .gitignore, where _libs/ also sits; this says it outright so the exclusion survives
48
+ # anyone changing how files are selected.
49
+ exclude = ["_libs/**"]
50
+
51
+ [tool.hatch.build.targets.sdist]
52
+ # The borrowed corpora are fetched per-developer, never shipped. None of them carries a
53
+ # licence: the .pbix samples come from tools/fetch_pbix.py, the demo project from
54
+ # tools/fetch_demo.py, and demo01 is recorded from that project loaded into Desktop.
55
+ # .gitignore keeps them out of the repo, but hatchling's sdist does not honour the
56
+ # re-ignore lines that follow `!tests/fixtures/**`, so it collected them off the local
57
+ # disk instead -- a 28 MB sdist against a 148 KB wheel. Naming them here is what stops an
58
+ # upload from redistributing 26 MB of somebody else's content.
59
+ exclude = [
60
+ "/tests/fixtures/demo/**",
61
+ "/tests/fixtures/demo01/**",
62
+ "/tests/fixtures/pbix/**",
63
+ "/.claude/**",
64
+ # Private-repo only; tools/export_public.py leaves them out of the public tree too.
65
+ "/CONVENTIONS.md",
66
+ "/CODE_REVIEW.md",
67
+ "/spikes/**",
68
+ "/tools/export_public.py",
69
+ ]
70
+
71
+ [tool.ruff]
72
+ line-length = 100
73
+ # spikes/ are frozen reproduction scripts for a finding, not maintained source.
74
+ extend-exclude = ["spikes"]
75
+ target-version = "py312"
76
+
77
+ [tool.ruff.lint]
78
+ select = ["E", "F", "I", "UP", "B", "SIM"]
79
+
80
+ [tool.pytest.ini_options]
81
+ testpaths = ["tests"]
82
+ # so `pytest` works without an editable install, which CI should not need
83
+ pythonpath = ["src"]
84
+ markers = [
85
+ "live: requires a running Power BI Desktop instance (deselected by default)",
86
+ "pbix: requires a real .pbix via DAXQUAX_TEST_PBIX (deselected by default)",
87
+ "demo: requires the borrowed .pbip corpus; run tools/fetch_demo.py first",
88
+ ]
89
+ addopts = "-m 'not live and not pbix and not demo'"
@@ -0,0 +1,86 @@
1
+ """dax-quax — what does every object in this tabular model cost, and can it be deleted?
2
+
3
+ Public surface. Everything else is internal and may move.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dax_quax.analysis.usage import (
9
+ Finding,
10
+ ScanScope,
11
+ Thresholds,
12
+ Verdict,
13
+ assess,
14
+ )
15
+ from dax_quax.errors import (
16
+ ConnectionDiscoveryError,
17
+ ContractViolation,
18
+ DaxQuaxError,
19
+ MissingCapability,
20
+ )
21
+ from dax_quax.model import Column, ColumnMetrics, Measure, Model, ObjectRef, Relationship, Table
22
+
23
+ __version__ = "0.1.0"
24
+
25
+
26
+ def connect(port: int | None = None, database: str | None = None) -> Model:
27
+ """Load a Model from a running local Power BI Desktop instance.
28
+
29
+ Windows only, and imported lazily so the rest of the package stays cross-platform.
30
+ """
31
+ from dax_quax.sources.live import connect as _connect
32
+
33
+ return _connect(port=port, database=database)
34
+
35
+
36
+ def open_workspace(path: str):
37
+ """Scan a directory of .pbip artefacts: every model, against every report over it."""
38
+ from dax_quax.sources.workspace import discover_workspace
39
+
40
+ return discover_workspace(path)
41
+
42
+
43
+ def open_pbip(path: str, *, attach_reports: bool = True) -> Model:
44
+ """Load a Model from a .pbip project folder, with its sibling reports attached.
45
+
46
+ Cross-platform: no .NET, no Power BI Desktop, nothing but text on disk.
47
+ """
48
+ from dax_quax.sources.pbip import open_pbip as _open
49
+
50
+ return _open(path, attach_reports=attach_reports)
51
+
52
+
53
+ def open_file(path: str) -> Model:
54
+ """Load a Model from a .pbix, .abf or PowerPivot .xlsx.
55
+
56
+ The only source that carries metadata **and** storage metrics with no Power BI Desktop
57
+ and no local cache. Needs the ``file`` extra.
58
+ """
59
+ from dax_quax.sources.file import load_file
60
+
61
+ return load_file(path)
62
+
63
+
64
+ __all__ = [
65
+ "Column",
66
+ "Finding",
67
+ "ScanScope",
68
+ "Thresholds",
69
+ "Verdict",
70
+ "ColumnMetrics",
71
+ "ConnectionDiscoveryError",
72
+ "ContractViolation",
73
+ "DaxQuaxError",
74
+ "Measure",
75
+ "MissingCapability",
76
+ "Model",
77
+ "ObjectRef",
78
+ "Relationship",
79
+ "Table",
80
+ "__version__",
81
+ "assess",
82
+ "connect",
83
+ "open_file",
84
+ "open_pbip",
85
+ "open_workspace",
86
+ ]
@@ -0,0 +1 @@
1
+ """Analyses. Pure functions of a Model."""