Basically pytest for Power BI. Automated guardrails that catch human error, bad DAX, and silent measure regressions in TMDL files before pull requests hit production.
Python
1
10 commits
updated Sep 26, 2026
Automated contract testing, architectural linting, and governance for Power BI semantic models (.pbip / TMDL).
Run DAX logic checks, tabular modeling assertions, and auditable UAT sign-off reports locally in milliseconds, without opening Power BI Desktop, writing complex regular expressions, or configuring cloud credentials.
pbi-guard Exists* 1.042 or + 15000) into production measures to force dashboards to match manual spreadsheets.pbi-guard provides a deterministic gate that prevents unvetted changes from being saved or deployed.| Stage | Trigger | Purpose |
|---|---|---|
| Local Dev | Before saving or staging git commits | Verify DAX syntax, check for uncompressed calculated columns, and validate formula definitions locally. |
| AI / MCP Workflows | After an LLM/agent edits TMDL | Validate agent output. If pbi-guard fails, pipe the failure output back to the agent for self-correction. |
| Production UAT Sign-Off | Release deployment pipeline | Generate a formal --export-report uat_audit.md artifact attached to release tickets for stakeholder sign-off. |
Run against any .pbip project file, .SemanticModel directory, or raw definition/ folder :
# Run standard test suite
python main.py -c pbi_tests.yml -m "../path/to/Sales.pbip"
# Run tests and generate an auditable UAT sign-off report
python main.py -c pbi_tests.yml -m "../path/to/Sales.pbip" --export-report uat_audit.md
Requires Python 3.9+.
git clone https://github.com/Patens-dev/pbi-guard.git
cd pbi-guard
pip install pyyaml
pbi_tests.yml)version: 1
assertions:
# ==============================================================================
# 1. STORAGE & ARCHITECTURE
# ==============================================================================
# Keep uncompressed memory down
- name: "Arch: No calculated columns in financials table"
type: no_calculated_columns
table: "financials"
# Prevent file size bloat from hidden calendars
- name: "Arch: Block auto date/time hidden tables"
type: no_auto_date_tables
# Stop numeric IDs, years, or zip codes from auto-summing in visuals
- name: "Arch: Prevent default aggregation on Year column"
type: column_rule
table: "financials"
column: "Year"
summarize_by: "none"
# ==============================================================================
# 2. DAX LOGIC CONTRACTS & FORMULA PINNING
# ==============================================================================
# Ensure critical business measures are never renamed or deleted
- name: "DAX: Core sales measure must exist"
type: measure_exists
measure: "[Total Sales]"
# Block raw slash division across all ratio and margin metrics
- name: "DAX: Safe division required for all ratio and margin metrics"
type: dax_rule
measures_matching: ["*Margin*", "*Ratio*", "*%*"]
must_call: "DIVIDE"
# Strict zero-tolerance check for raw slash on board-level metrics
- name: "DAX: Forbid raw slash division anti-pattern"
type: dax_rule
measure: "[Profit Margin %]"
forbid_operator: "/"
# Ensure metric reuse (DRY)
- name: "DAX: Complex metrics must reference base measures"
type: dax_rule
measure: "[Gross Profit]"
references: ["[Total Sales]", "[Total COGS]"]
# Freeze financial KPI logic character-for-character
- name: "Contract: Gross Profit formula definition is pinned"
type: dax_exact
measure: "[Gross Profit]"
expected: "[Total Sales] - [Total COGS]"
# Enforce calculation lineage and metric inheritance (DAG)
- name: "DAG: Enforce metric dependency hierarchy for Profit Margin"
type: dax_dependency_chain
measure: "[Profit Margin %]"
must_depend_on:
- "[Gross Profit]"
- "[Total Sales]"
# ==============================================================================
# 3. INTEGRITY & ANTI-CHEAT (MAGIC NUMBERS & COLUMN SCOPING)
# ==============================================================================
# Catch magic numbers, arbitrary multipliers (* 1.042), and scalar adjustments
- name: "Integrity: No hardcoded scalar adjustments in financial KPIs"
type: dax_rule
measures_matching: ["*Sales*", "*Profit*", "*COGS*"]
forbid_raw_numeric_literals: true
allowed_literals: [0, 1]
# Stop metrics from querying raw fact columns directly instead of base measures
- name: "Governance: Ratio metrics must not query raw fact columns directly"
type: column_usage_rule
measures_matching: ["*Margin*", "*Ratio*", "*%*"]
forbid_column_references:
- "financials[Sales]"
- "financials[COGS]"
# ==============================================================================
# 4. GOVERNANCE & FORMATTING
# ==============================================================================
# Enforce currency strings to prevent raw decimals on cards
- name: "Gov: Financial measures must have currency format strings"
type: measure_format
measures: ["[Total Sales]", "[Total COGS]", "[Gross Profit]"]
format: "currency"
# Enforce percentage formatting to avoid 0.24 instead of 24%
- name: "Gov: Percentage measures must specify percentage formatting"
type: measure_format
measures_matching: ["*%*", "*Margin*"]
format: "percentage"
# Eliminate obsolete technical prefixes for self-service usability
- name: "Gov: Enforce clean naming conventions"
type: measure_naming
forbid_prefixes: ["m_", "meas_", "calc_"]
no_calculated_columns*Fact*, financials, Orders) .table (string/wildcard) .no_auto_date_tablesDimDate dimension .column_ruleSum of Year: 6,075 on executive card visuals .summarize_by: "none") .table, column, summarize_by, hidden .dax_exactDIVIDE([COGS], [Sales]) passes a loose check). dax_exact freezes the formula definition character-for-character, ignoring trivial comment or whitespace drift.[Gross Profit], [EBITDA], [ARR]).measure (string), expected (string DAX expression).dax_dependency_chain[Net Margin %] must depend on [Net Profit], which must depend on [Total Sales]).measure (string), must_depend_on (list of strings), forbid_dependencies (list of strings), direct_only (bool, default false).column_usage_ruleFactSales[OrderDate] instead of DimDate[Date]).measures_matching, forbid_column_references, must_reference_tables, allowed_tables, exclude_measures.dax_rule/ division, and catching hardcoded numbers .must_call: Function name(s) required (e.g., DIVIDE) .forbid_operator: Disallowed operator (e.g., "/") .references: Base measure references required (DRY) .forbid_raw_numeric_literals: Set true to block magic numbers.allowed_literals: Whitelist acceptable numbers (e.g., [0, 1]).measure_format$1,000,000 instead of 1000000.32891, and 24% instead of 0.24) .measures, measures_matching, format ("currency", "percentage", or custom mask) .measure_namingforbid_prefixes (list of prefixes to disallow) .Use --export-report <path> to export an auditable compliance artifact in Markdown (.md), HTML (.html), or JSON (.json) :
python main.py -c pbi_tests.yml -m ./Sales.SemanticModel --export-report uat_audit.md
The generated report includes:
MIT License - Copyright (c) 2026 Patens.dev. Free for commercial and private use.
10 commits
Python
100.0%
Basically pytest for Power BI. Automated guardrails that catch human error, bad DAX, and silent measure regressions in TMDL files before pull requests hit production.
Python
1
10 commits
updated Sep 26, 2026
Automated contract testing, architectural linting, and governance for Power BI semantic models (.pbip / TMDL).
Run DAX logic checks, tabular modeling assertions, and auditable UAT sign-off reports locally in milliseconds, without opening Power BI Desktop, writing complex regular expressions, or configuring cloud credentials.
pbi-guard Exists* 1.042 or + 15000) into production measures to force dashboards to match manual spreadsheets.pbi-guard provides a deterministic gate that prevents unvetted changes from being saved or deployed.| Stage | Trigger | Purpose |
|---|---|---|
| Local Dev | Before saving or staging git commits | Verify DAX syntax, check for uncompressed calculated columns, and validate formula definitions locally. |
| AI / MCP Workflows | After an LLM/agent edits TMDL | Validate agent output. If pbi-guard fails, pipe the failure output back to the agent for self-correction. |
| Production UAT Sign-Off | Release deployment pipeline | Generate a formal --export-report uat_audit.md artifact attached to release tickets for stakeholder sign-off. |
Run against any .pbip project file, .SemanticModel directory, or raw definition/ folder :
# Run standard test suite
python main.py -c pbi_tests.yml -m "../path/to/Sales.pbip"
# Run tests and generate an auditable UAT sign-off report
python main.py -c pbi_tests.yml -m "../path/to/Sales.pbip" --export-report uat_audit.md
Requires Python 3.9+.
git clone https://github.com/Patens-dev/pbi-guard.git
cd pbi-guard
pip install pyyaml
pbi_tests.yml)version: 1
assertions:
# ==============================================================================
# 1. STORAGE & ARCHITECTURE
# ==============================================================================
# Keep uncompressed memory down
- name: "Arch: No calculated columns in financials table"
type: no_calculated_columns
table: "financials"
# Prevent file size bloat from hidden calendars
- name: "Arch: Block auto date/time hidden tables"
type: no_auto_date_tables
# Stop numeric IDs, years, or zip codes from auto-summing in visuals
- name: "Arch: Prevent default aggregation on Year column"
type: column_rule
table: "financials"
column: "Year"
summarize_by: "none"
# ==============================================================================
# 2. DAX LOGIC CONTRACTS & FORMULA PINNING
# ==============================================================================
# Ensure critical business measures are never renamed or deleted
- name: "DAX: Core sales measure must exist"
type: measure_exists
measure: "[Total Sales]"
# Block raw slash division across all ratio and margin metrics
- name: "DAX: Safe division required for all ratio and margin metrics"
type: dax_rule
measures_matching: ["*Margin*", "*Ratio*", "*%*"]
must_call: "DIVIDE"
# Strict zero-tolerance check for raw slash on board-level metrics
- name: "DAX: Forbid raw slash division anti-pattern"
type: dax_rule
measure: "[Profit Margin %]"
forbid_operator: "/"
# Ensure metric reuse (DRY)
- name: "DAX: Complex metrics must reference base measures"
type: dax_rule
measure: "[Gross Profit]"
references: ["[Total Sales]", "[Total COGS]"]
# Freeze financial KPI logic character-for-character
- name: "Contract: Gross Profit formula definition is pinned"
type: dax_exact
measure: "[Gross Profit]"
expected: "[Total Sales] - [Total COGS]"
# Enforce calculation lineage and metric inheritance (DAG)
- name: "DAG: Enforce metric dependency hierarchy for Profit Margin"
type: dax_dependency_chain
measure: "[Profit Margin %]"
must_depend_on:
- "[Gross Profit]"
- "[Total Sales]"
# ==============================================================================
# 3. INTEGRITY & ANTI-CHEAT (MAGIC NUMBERS & COLUMN SCOPING)
# ==============================================================================
# Catch magic numbers, arbitrary multipliers (* 1.042), and scalar adjustments
- name: "Integrity: No hardcoded scalar adjustments in financial KPIs"
type: dax_rule
measures_matching: ["*Sales*", "*Profit*", "*COGS*"]
forbid_raw_numeric_literals: true
allowed_literals: [0, 1]
# Stop metrics from querying raw fact columns directly instead of base measures
- name: "Governance: Ratio metrics must not query raw fact columns directly"
type: column_usage_rule
measures_matching: ["*Margin*", "*Ratio*", "*%*"]
forbid_column_references:
- "financials[Sales]"
- "financials[COGS]"
# ==============================================================================
# 4. GOVERNANCE & FORMATTING
# ==============================================================================
# Enforce currency strings to prevent raw decimals on cards
- name: "Gov: Financial measures must have currency format strings"
type: measure_format
measures: ["[Total Sales]", "[Total COGS]", "[Gross Profit]"]
format: "currency"
# Enforce percentage formatting to avoid 0.24 instead of 24%
- name: "Gov: Percentage measures must specify percentage formatting"
type: measure_format
measures_matching: ["*%*", "*Margin*"]
format: "percentage"
# Eliminate obsolete technical prefixes for self-service usability
- name: "Gov: Enforce clean naming conventions"
type: measure_naming
forbid_prefixes: ["m_", "meas_", "calc_"]
no_calculated_columns*Fact*, financials, Orders) .table (string/wildcard) .no_auto_date_tablesDimDate dimension .column_ruleSum of Year: 6,075 on executive card visuals .summarize_by: "none") .table, column, summarize_by, hidden .dax_exactDIVIDE([COGS], [Sales]) passes a loose check). dax_exact freezes the formula definition character-for-character, ignoring trivial comment or whitespace drift.[Gross Profit], [EBITDA], [ARR]).measure (string), expected (string DAX expression).dax_dependency_chain[Net Margin %] must depend on [Net Profit], which must depend on [Total Sales]).measure (string), must_depend_on (list of strings), forbid_dependencies (list of strings), direct_only (bool, default false).column_usage_ruleFactSales[OrderDate] instead of DimDate[Date]).measures_matching, forbid_column_references, must_reference_tables, allowed_tables, exclude_measures.dax_rule/ division, and catching hardcoded numbers .must_call: Function name(s) required (e.g., DIVIDE) .forbid_operator: Disallowed operator (e.g., "/") .references: Base measure references required (DRY) .forbid_raw_numeric_literals: Set true to block magic numbers.allowed_literals: Whitelist acceptable numbers (e.g., [0, 1]).measure_format$1,000,000 instead of 1000000.32891, and 24% instead of 0.24) .measures, measures_matching, format ("currency", "percentage", or custom mask) .measure_namingforbid_prefixes (list of prefixes to disallow) .Use --export-report <path> to export an auditable compliance artifact in Markdown (.md), HTML (.html), or JSON (.json) :
python main.py -c pbi_tests.yml -m ./Sales.SemanticModel --export-report uat_audit.md
The generated report includes:
MIT License - Copyright (c) 2026 Patens.dev. Free for commercial and private use.
10 commits
Python
100.0%