Universal lineage graph for Power BI (.pbix) files, from physical source to report display.
Project description
pbix-atlas
Universal lineage graph for Power BI (.pbix) files: from physical source to the field displayed in a report visual.
source (HTTP, OData, SQL, file...) --> Power Query (M)
--> column / calculated column --> measure (DAX)
--> field displayed in a report visual
The graph is a standard networkx.DiGraph, natively bidirectional: trace a visual back to its source (upstream), or list everything a source feeds (downstream).
Why
pbixray (used here) extracts the data model (tables, DAX, Power Query, schema) but says nothing about where each column or measure ends up displayed — that lives in a separate, undocumented part of the file (Report/Layout). pbix-atlas connects both into one traversable graph.
Install
pip install pbix-atlas
# or
uv add pbix-atlas
Usage
from pbix_atlas import LineageGraphBuilder, upstream, downstream, print_tree, find_nodes
graph = LineageGraphBuilder().build("my_report.pbix")
find_nodes(graph, "customer_name")
# -> ['column::DIM_CUSTOMER::customer_name']
print_tree(graph, "visual_field::my_report.pbix::Page1::16::customer_name", direction="upstream")
print_tree(graph, "source::odata::example.com/odata/", direction="downstream")
Export
from pbix_atlas import export_graphml, export_nodes_csv, export_edges_csv, graph_summary
graph_summary(graph) # {'query': 70, 'column': 183, ...}
export_graphml(graph, "lineage.graphml") # opens in Gephi / yEd
export_nodes_csv(graph, "nodes.csv")
export_edges_csv(graph, "edges.csv")
Source-agnostic
Source detection (pbix_atlas.sources) relies only on native M function names (Web.Contents, OData.Feed, Sql.Database, Folder.Files, SharePoint.Files, Excel.Workbook, AnalysisServices.Database, ...) — never a specific system or domain. Adding a new source type is one config entry in MFunctionSourceDetector.DEFAULT_PATTERNS, no other code touched.
HTTP API / MCP server
The package also exposes a FastAPI app, mounted as an MCP server via FastMCP (FastMCP.from_fastapi): every route becomes an MCP tool automatically.
uv sync --extra api
uv run pbix-atlas # starts on http://127.0.0.1:8080
- REST API:
POST /graphs,/search,/upstream,/downstream,/tree,/export,/codegen,GET /graphs. - MCP server (streamable HTTP) at
http://127.0.0.1:8080/mcp/: same operations as tools (build_graph,search_nodes,get_upstream,get_downstream,get_lineage_tree,export_graph,convert_pbix_to_python,list_loaded_graphs), plus alineage_guidanceprompt. - Env vars:
PBIX_LINEAGE_HOST(default0.0.0.0),PBIX_LINEAGE_PORT(default8080).
Each .pbix is parsed once and cached in memory (LineageGraphCache, framework-agnostic).
POST /codegen (convert_pbix_to_python) generates the standalone Python
pipeline described below and returns {output_path, stats}, where stats
gives a quick coverage readout (M steps / DAX measures translated vs. left
as TODO, visuals mapped vs. skipped) without having to open the file first.
Generate a standalone Python pipeline
Beyond the lineage graph, pbix-atlas can generate a single Python file
reproducing a report's full chain: source -> extraction -> Power Query
transformations -> semantic model (columns/measures) -> Vizro dashboard.
pip install "pbix-atlas[codegen]"
pbix-atlas-codegen my_report.pbix -o pipeline.py
or from Python:
from pbix_atlas import generate_python_pipeline
generate_python_pipeline("my_report.pbix", "pipeline.py")
This is not a per-step transpiler - it's a real M interpreter:
- The actual M source of every query is embedded verbatim in the generated
file (
M_SOURCE) and executed at runtime bypbix_atlas.m_interpreter(own lexer/parser/interpreter - see Architecture), the same way regardless of how a step happens to be phrased. Rename, filter, join, group-by, custom replace-value logic, multi-param lambdas: all run through the same engine, not a fixed set of recognized patterns. - There is no
TODOanywhere in the generated file. A step whose M function or operator genuinely isn't implemented raisesMRuntimeErrornaming the exact function - loud and specific, never a silent stub. - DAX measures: a curated subset (
SUM,AVERAGE,COUNTROWS,DISTINCTCOUNT,DIVIDE, simple measure references) is translated to Python; anything more complex raisesMeasureNotSupported(table, name, original_dax)the moment it's read, rather than silently returning a wrong number. Full DAX (filter-context propagation, time intelligence,CALCULATEwith real filter semantics) is a much larger undertaking and intentionally out of scope for now. - Real connection info (URL,
server;database, file path) detected in the.pbixis embedded as-is in the M source, so the file runs against the same systems the report used - edit theM_SOURCEstrings directly to point elsewhere. - Charts spanning more than one semantic table, or using a visual type with no Vizro equivalent (custom visuals, images, shapes, slicers), are skipped rather than guessed; a summary comment reports how many.
Modules involved: m_lexer.py / m_parser.py (M tokenizer/parser, built
in-house - see below), m_interpreter.py (runtime interpreter + stdlib:
Table.*, List.*, Text.*, Date.*, plus real I/O for
Sql.Database/OData.Feed/Web.Contents/Excel.Workbook/File.Contents),
dax_translate.py (DAX -> Python for the supported subset), layout_roles.py
(visual roles/filters), codegen.py (orchestrator, uses mquery.py for
topological query ordering).
Why a hand-built M parser? An earlier version of this depended on a published M-parsing package (
pbi-parsers). It turned out to be missing the</<=operators entirely and couldn't parse multi-parameter or typed lambdas - real gaps that would have meant hardcoding workarounds around someone else's incomplete grammar.m_lexer.py/m_parser.pyare small, scoped to what Power Query Desktop actually emits, and verified against real-world.pbixfiles rather than the full (unpublished) M grammar.
Tried against real-world files (not part of CI, just concrete examples you can try yourself):
- Retail Analysis Sample PBIX (Microsoft)
- Sales & Returns Sample v201912 (Microsoft)
This is also exposed as an HTTP/MCP endpoint - see below.
server.json describes this server for registry.modelcontextprotocol.io. After replacing votre-org with your GitHub account everywhere:
uv build && uv publish # publish the package to PyPI first
mcp-publisher login github
mcp-publisher publish --dry-run
mcp-publisher publish
The registry verifies PyPI ownership via the <!-- mcp-name: ... --> marker at the top of this README. The name in server.json, this marker, and your authenticated GitHub namespace must all match.
Architecture
| Module | Responsibility |
|---|---|
models.py |
Node/edge types and shared data structures |
sources.py |
Physical source detection (agnostic, configurable) |
pbix_model.py |
Adapter isolating the rest of the code frompbixray |
dax.py |
DAX reference parsing (Table[Field] / [Field]) |
mquery.py |
Dependencies between Power Query queries (table-level) |
layout.py |
Parsing of the internalReport/Layout format |
graph_builder.py |
Orchestrator: builds thenetworkx.DiGraph |
navigation.py |
Upstream/downstream traversal, search, export |
api/schemas.py |
Pydantic request/response models |
api/service.py |
Graph cache, framework-agnostic |
api/app.py |
FastAPI app + MCP mount (FastMCP) |
Known limitations
- M query dependencies are resolved at table level, not step-by-step inside a single
let ... inquery. - A source reached only through a literal M parameter may be tagged with a generic system (
http) instead of the exact consumer connector (odata, etc.). - Unqualified DAX references (
[MeasureName]) are resolved same-table first, then globally; homonyms across tables resolve to the first match.
Development
uv sync --extra dev
uv run pytest # BDD tests (pytest-bdd) under tests/features/*.feature
uv build # produces dist/*.whl and dist/*.tar.gz
AI Setup
# Install Spec Kit CLI
uv tool install specify-cli
# Initialize project with opencode integration
specify init . --here --integration opencode --script py
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pbix_atlas-0.1.2.tar.gz.
File metadata
- Download URL: pbix_atlas-0.1.2.tar.gz
- Upload date:
- Size: 48.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eaa773e579f385796c9c7d3c14d20a925e35d4d106739906c48ed5c83d808ead
|
|
| MD5 |
d332c7ce6af17fdfe0bc23c902766cfc
|
|
| BLAKE2b-256 |
607aaf2d962734bf325ccaaaba89c1e59ced8f54f2f7888c3dffe1f3e895df81
|
File details
Details for the file pbix_atlas-0.1.2-py3-none-any.whl.
File metadata
- Download URL: pbix_atlas-0.1.2-py3-none-any.whl
- Upload date:
- Size: 54.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
16473631a1af8f092d5052e61a154ff13b8dd4d5f5cb436f34246e9bbd85d007
|
|
| MD5 |
af1108820930369548b0d054a608859e
|
|
| BLAKE2b-256 |
31ee5d2f0f0e8c2372b94336502fad7e427d6395f68196840fd5fe70755e8a37
|