feat: v3.10.0 -- audit fixes, 12 skills, README rewrite

- Fix 4 code bugs: TopN filter cross-table, theme_set corruption,
  visual_bind type annotation, tmdl_diff hierarchy pluralization
- Add missing VisualTypeError and ReportNotFoundError to errors.py
- Register 5 command groups in CLI (report, visual, filters, format, bookmarks)
- Split monolithic report skill into 5 focused skills (12 total):
  power-bi-report, power-bi-visuals, power-bi-pages, power-bi-themes,
  power-bi-filters
- Update CLAUDE.md snippet for 12 skills organised by layer
- Add diff-tmdl section to deployment skill
- Write CHANGELOG entries for v3.0.0 through v3.10.0
- Rewrite README.md and README.pypi.md for both model and report layers
- Add skill triggering test suite (19/19 passing)
- 488 tests passing, ruff clean
This commit is contained in:
MinaSaad1 2026-04-02 00:07:52 +02:00
parent 88cb644f64
commit f0504bcf51
20 changed files with 1732 additions and 225 deletions

View file

@ -5,6 +5,112 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.10.0] - 2026-04-02
### Added
- Split `power-bi-report` skill into 5 focused skills: `power-bi-report` (overview), `power-bi-visuals`, `power-bi-pages`, `power-bi-themes`, `power-bi-filters` (12 skills total)
- CLAUDE.md snippet now organises skills by layer (Semantic Model vs Report Layer)
- Skill triggering test suite (19 prompts, 12 skills)
### Fixed
- `filter_add_topn` inner subquery now correctly references category table when it differs from order-by table
- `theme_set` resourcePackages structure now matches Desktop format (flat `items` array)
- `visual_bind` type annotation corrected to `list[dict[str, Any]]`
- `tmdl_diff` hierarchy changes reported as `hierarchies_*` instead of falling to `other_*`
- Missing `VisualTypeError` and `ReportNotFoundError` classes added to `errors.py`
- `report`, `visual`, `filters`, `format`, `bookmarks` command groups registered in CLI
### Changed
- README rewritten to cover both semantic model and report layers, 12 skills, 27 command groups, 32 visual types
## [3.9.0] - 2026-04-01
### Added
- `pbi database diff-tmdl` command: compare two TMDL export folders offline, summarise changes (tables, measures, columns, relationships, model properties); lineageTag-only changes are stripped to avoid false positives
### Fixed
- `filter_add_topn` inner subquery now correctly references the category table when it differs from the order-by table (cross-table TopN filters)
- `theme_set` resourcePackages structure now matches Desktop format (flat `items`, not nested `resourcePackage`)
- `visual_bind` type annotation corrected from `list[dict[str, str]]` to `list[dict[str, Any]]`
- `tmdl_diff` hierarchy changes now reported as `hierarchies_*` instead of falling through to `other_*`
- Missing `VisualTypeError` and `ReportNotFoundError` error classes added to `errors.py`
- `report`, `visual`, `filters`, `format`, `bookmarks` command groups registered in CLI (were implemented but inaccessible)
## [3.8.0] - 2026-04-01
### Added
- `azureMap` visual type (Azure Maps) with Category and Size roles
- `pageBinding` field surfaced in `page_get()` for drillthrough pages
### Fixed
- `card` and `multiRowCard` queryState role corrected from `Fields` to `Values` (matches Desktop)
- `kpi` template: added `TrendLine` queryState key (date/axis column for sparkline)
- `gauge` template: added `MaxValue` queryState key (target/max measure)
- `MaxValue` added to `MEASURE_ROLES`
- kpi role aliases: `--trend`, `--trend_line`
- gauge role aliases: `--max`, `--max_value`, `--target`
## [3.7.0] - 2026-04-01
### Added
- `page_type`, `filter_config`, and `visual_interactions` fields in page read operations (`page_get`, `page_list`)
## [3.6.0] - 2026-04-01
### Added
- `image` visual type (static images, no data binding)
- `shape` visual type (decorative shapes)
- `textbox` visual type (rich text)
- `pageNavigator` visual type (page navigation buttons)
- `advancedSlicerVisual` visual type (tile/image slicer)
## [3.5.0] - 2026-04-01
### Added
- `clusteredColumnChart` visual type with aliases `clustered_column`
- `clusteredBarChart` visual type with aliases `clustered_bar`
- `textSlicer` visual type with alias `text_slicer`
- `listSlicer` visual type with alias `list_slicer`
## [3.4.0] - 2026-03-31
### Added
- `cardVisual` (modern card) visual type with `Data` role and aliases `card_visual`, `modern_card`
- `actionButton` visual type with alias `action_button`, `button`
- `pbi report set-background` command to set page background colour
- `pbi report set-visibility` command to hide/show pages
- `pbi visual set-container` command for border, background, and title on visual containers
### Fixed
- Visual container schema URL updated from 1.5.0 to 2.7.0
- `visualGroup` containers tagged as type `group` in `visual_list`
- Colour validation, KeyError guards, visibility surfacing, no-op detection
## [3.0.0] - 2026-03-31
### Added
- **PBIR report layer**: `pbi report` command group (create, info, validate, list-pages, add-page, delete-page, get-page, set-theme, get-theme, diff-theme, preview, reload, convert)
- **Visual CRUD**: `pbi visual` command group (add, get, list, update, delete, bind, where, bulk-bind, bulk-update, bulk-delete, calc-add, calc-list, calc-delete, set-container)
- **Filters**: `pbi filters` command group (list, add-categorical, add-topn, add-relative-date, remove, clear)
- **Formatting**: `pbi format` command group (get, clear, background-gradient, background-conditional, background-measure)
- **Bookmarks**: `pbi bookmarks` command group (list, get, add, delete, set-visibility)
- 20 visual type templates (barChart, lineChart, card, tableEx, pivotTable, slicer, kpi, gauge, donutChart, columnChart, areaChart, ribbonChart, waterfallChart, scatterChart, funnelChart, multiRowCard, treemap, cardNew, stackedBarChart, lineStackedColumnComboChart)
- HTML preview server (`pbi report preview`) with live reload
- Power BI Desktop reload trigger (`pbi report reload`)
- PBIR path auto-detection (walk-up from CWD, `.pbip` sibling detection)
- `power-bi-report` Claude Code skill (8th skill)
- Visual data binding with `Table[Column]` notation and role aliases
- Visual calculations (calc-add, calc-list, calc-delete)
- Bulk operations for mass visual updates across pages
### Changed
- Architecture: pbi-cli now covers both semantic model layer (via .NET TOM) and report layer (via PBIR JSON files)
## [2.2.0] - 2026-03-27
### Added
- Promotional SVG assets and redesigned README
## [2.0.0] - 2026-03-27
### Breaking

View file

@ -4,7 +4,7 @@
<p align="center">
<b>Give Claude Code the Power BI skills it needs.</b><br/>
Install once, then just ask Claude to work with your semantic models.
Install once, then just ask Claude to work with your semantic models <i>and</i> reports.
</p>
<p align="center">
@ -117,22 +117,34 @@ Add the printed path to your system PATH, then restart your terminal. We recomme
## Skills
After running `pbi connect`, Claude Code discovers **7 Power BI skills** automatically. Each skill teaches Claude a different area. You don't need to memorize commands.
After running `pbi connect`, Claude Code discovers **12 Power BI skills** automatically. Each skill teaches Claude a different area. You don't need to memorize commands.
<p align="center">
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/skills-hub.svg" alt="7 Skills" width="850"/>
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/skills-hub.svg" alt="12 Skills" width="850"/>
</p>
### Semantic Model Skills (require `pbi connect`)
| Skill | What you say | What Claude does |
|-------|-------------|-----------------|
| **DAX** | *"What are the top 10 products by revenue?"* | Writes and executes DAX queries, validates syntax |
| **Modeling** | *"Create a star schema with Sales and Calendar"* | Creates tables, relationships, measures, hierarchies |
| **Deployment** | *"Save a snapshot before I make changes"* | Exports/imports TMDL, manages transactions |
| **Deployment** | *"Save a snapshot before I make changes"* | Exports/imports TMDL, manages transactions, diffs snapshots |
| **Security** | *"Set up RLS for regional managers"* | Creates roles, filters, perspectives |
| **Docs** | *"Document everything in this model"* | Generates data dictionaries, measure inventories |
| **Partitions** | *"Show me the M query for the Sales table"* | Manages partitions, expressions, calendar config |
| **Diagnostics** | *"Why is this query so slow?"* | Traces queries, checks model health, benchmarks |
### Report Layer Skills (no connection needed)
| Skill | What you say | What Claude does |
|-------|-------------|-----------------|
| **Report** | *"Create a new report project for Sales"* | Scaffolds PBIR reports, validates structure, previews layout |
| **Visuals** | *"Add a bar chart showing revenue by region"* | Adds, binds, updates, bulk-manages 32 visual types |
| **Pages** | *"Add an Executive Overview page"* | Manages pages, bookmarks, visibility, drillthrough |
| **Themes** | *"Apply our corporate brand colours"* | Applies themes, conditional formatting, colour scales |
| **Filters** | *"Show only the top 10 products"* | Adds page/visual filters (TopN, date, categorical) |
---
## Architecture
@ -141,7 +153,10 @@ After running `pbi connect`, Claude Code discovers **7 Power BI skills** automat
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/architecture-flow.svg" alt="Architecture" width="850"/>
</p>
Direct in-process .NET interop from Python to Power BI Desktop. No MCP server, no external binaries, sub-second execution.
**Two layers, one CLI:**
- **Semantic Model layer** -- Direct in-process .NET interop from Python to Power BI Desktop via TOM/ADOMD. No MCP server, no external binaries, sub-second execution.
- **Report layer** -- Reads and writes PBIR (Enhanced Report Format) JSON files directly. No connection needed. Works with `.pbip` projects.
<details>
<summary><b>Configuration details</b></summary>
@ -163,21 +178,57 @@ Bundled DLLs ship inside the Python package (`pbi_cli/dlls/`).
## All Commands
<p align="center">
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/feature-grid.svg" alt="22 Command Groups" width="850"/>
</p>
27 command groups covering both the semantic model and the report layer.
| Category | Commands |
|----------|----------|
| **Queries** | `dax execute`, `dax validate`, `dax clear-cache` |
| **Model** | `table`, `column`, `measure`, `relationship`, `hierarchy`, `calc-group` |
| **Deploy** | `database export-tmdl`, `database import-tmdl`, `database export-tmsl`, `database diff-tmdl`, `transaction` |
| **Security** | `security-role`, `perspective` |
| **Connect** | `connect`, `disconnect`, `connections list`, `connections last` |
| **Data** | `partition`, `expression`, `calendar`, `advanced culture` |
| **Diagnostics** | `trace start/stop/fetch/export`, `model stats` |
| **Report** | `report create`, `report info`, `report validate`, `report preview`, `report reload` |
| **Pages** | `report add-page`, `report delete-page`, `report get-page`, `report set-background`, `report set-visibility` |
| **Visuals** | `visual add/get/list/update/delete`, `visual bind`, `visual bulk-bind/bulk-update/bulk-delete`, `visual where` |
| **Filters** | `filters list`, `filters add-categorical/add-topn/add-relative-date`, `filters remove/clear` |
| **Formatting** | `format get/clear`, `format background-gradient/background-conditional/background-measure` |
| **Bookmarks** | `bookmarks list/get/add/delete/set-visibility` |
| **Tools** | `setup`, `repl`, `skills install/list/uninstall` |
Use `--json` for machine-readable output (for scripts and AI agents):
```bash
pbi --json measure list
pbi --json dax execute "EVALUATE Sales"
pbi --json visual list --page overview
```
Run `pbi <command> --help` for full options.
---
## Supported Visual Types (32)
pbi-cli supports creating and binding data to 32 Power BI visual types:
**Charts:** bar, line, column, area, ribbon, waterfall, stacked bar, clustered bar, clustered column, scatter, funnel, combo, donut/pie, treemap
**Cards/KPIs:** card (legacy), cardVisual (modern), cardNew, multi-row card, KPI, gauge
**Tables:** table, matrix (pivot table)
**Slicers:** slicer, text slicer, list slicer, advanced slicer (tile/image)
**Maps:** Azure Map
**Decorative:** action button, image, shape, textbox, page navigator
Use friendly aliases: `pbi visual add --page p1 --type bar` instead of `--type barChart`.
---
## REPL Mode
For interactive work, the REPL keeps a persistent connection:
@ -208,7 +259,7 @@ pip install -e ".[dev]"
```bash
ruff check src/ tests/ # Lint
mypy src/ # Type check
pytest -m "not e2e" # Run tests
pytest -m "not e2e" # Run tests (488 tests)
```
---

View file

@ -1,7 +1,7 @@
<img src="https://raw.githubusercontent.com/MinaSaad1/pbi-cli/master/assets/header.svg" alt="pbi-cli" width="800"/>
**Give Claude Code the Power BI skills it needs.**
Install once, then just ask Claude to work with your semantic models.
Install once, then just ask Claude to work with your semantic models *and* reports.
<a href="https://pypi.org/project/pbi-cli-tool/"><img src="https://img.shields.io/pypi/pyversions/pbi-cli-tool?style=flat-square&color=3776ab&label=Python" alt="Python"></a>
<a href="https://github.com/MinaSaad1/pbi-cli/actions"><img src="https://img.shields.io/github/actions/workflow/status/MinaSaad1/pbi-cli/ci.yml?branch=master&style=flat-square&label=CI" alt="CI"></a>
@ -13,14 +13,18 @@ Install once, then just ask Claude to work with your semantic models.
## What is this?
pbi-cli gives **Claude Code** (and other AI agents) the ability to manage Power BI semantic models. It ships with 7 skills that Claude discovers automatically. You ask in plain English, Claude uses the right `pbi` commands.
pbi-cli gives **Claude Code** (and other AI agents) the ability to manage Power BI semantic models **and reports**. It ships with 12 skills that Claude discovers automatically. You ask in plain English, Claude uses the right `pbi` commands.
```
You Claude Code pbi-cli Power BI
"Add a YTD measure ---> Uses Power BI ---> CLI commands ---> Desktop
to the Sales table" skills
to the Sales table" skills (12)
```
**Two layers, one CLI:**
- **Semantic Model** -- Direct .NET interop to Power BI Desktop (measures, tables, DAX, security)
- **Report Layer** -- Reads/writes PBIR JSON files directly (visuals, pages, themes, filters)
---
## Get Started
@ -40,8 +44,6 @@ pbi connect # 2. Auto-detects Power BI Desktop and installs ski
That's it. Open Power BI Desktop with a `.pbix` file, run `pbi connect`, and everything is set up automatically. Open Claude Code and start asking.
You can also specify the port manually: `pbi connect -d localhost:54321`
> **Requires:** Windows with Python 3.10+ and Power BI Desktop running.
<details>
@ -60,12 +62,7 @@ Find the directory:
python -c "import site; print(site.getusersitepackages().replace('site-packages','Scripts'))"
```
Add the printed path to your system PATH:
```cmd
setx PATH "%PATH%;C:\Users\YourName\AppData\Roaming\Python\PythonXXX\Scripts"
```
Then **restart your terminal**. We recommend `pipx` instead to avoid this entirely.
Add the printed path to your system PATH, then restart your terminal. We recommend `pipx` to avoid this entirely.
</details>
@ -73,182 +70,76 @@ Then **restart your terminal**. We recommend `pipx` instead to avoid this entire
## Skills
After running `pbi connect`, Claude Code discovers **7 Power BI skills**. Each skill teaches Claude a different area of Power BI development. You don't need to memorize commands. Just describe what you want.
After running `pbi connect`, Claude Code discovers **12 Power BI skills**. Each skill teaches Claude a different area. You don't need to memorize commands.
```
You: "Set up RLS for regional managers"
|
v
Claude Code --> Picks the right skill
|
+-- Modeling
+-- DAX
+-- Deployment
+-- Security
+-- Documentation
+-- Diagnostics
+-- Partitions
```
### Semantic Model (require `pbi connect`)
### Modeling
| Skill | What you say | What Claude does |
|-------|-------------|-----------------|
| **DAX** | *"Top 10 products by revenue?"* | Writes and executes DAX queries |
| **Modeling** | *"Create a star schema"* | Creates tables, relationships, measures |
| **Deployment** | *"Save a snapshot"* | Exports/imports TMDL, diffs snapshots |
| **Security** | *"Set up RLS"* | Creates roles, filters, perspectives |
| **Docs** | *"Document this model"* | Generates data dictionaries |
| **Partitions** | *"Show the M query"* | Manages partitions, expressions |
| **Diagnostics** | *"Why is this slow?"* | Traces queries, benchmarks |
> *"Create a star schema with Sales, Products, and Calendar tables"*
### Report Layer (no connection needed)
Claude creates the tables, sets up relationships, marks the date table, and adds formatted measures. Covers tables, columns, measures, relationships, hierarchies, and calculation groups.
<details>
<summary>Example: what Claude runs behind the scenes</summary>
```bash
pbi table create Sales --mode Import
pbi table create Products --mode Import
pbi table create Calendar --mode Import
pbi relationship create --from-table Sales --from-column ProductKey --to-table Products --to-column ProductKey
pbi relationship create --from-table Sales --from-column DateKey --to-table Calendar --to-column DateKey
pbi table mark-date Calendar --date-column Date
pbi measure create "Total Revenue" -e "SUM(Sales[Revenue])" -t Sales --format-string "$#,##0"
```
</details>
### DAX
> *"What are the top 10 products by revenue this year?"*
Claude writes and executes DAX queries, validates syntax, and creates measures with time intelligence patterns like YTD, previous year, and rolling averages.
<details>
<summary>Example: what Claude runs behind the scenes</summary>
```bash
pbi dax execute "
EVALUATE
TOPN(
10,
ADDCOLUMNS(VALUES(Products[Name]), \"Revenue\", CALCULATE(SUM(Sales[Amount]))),
[Revenue], DESC
)
"
```
</details>
### Deployment
> *"Export the model to Git for version control"*
Claude exports your model as TMDL files for version control and imports them back. Handles transactions for safe multi-step changes.
<details>
<summary>Example: what Claude runs behind the scenes</summary>
```bash
pbi database export-tmdl ./model/
# ... you commit to git ...
pbi database import-tmdl ./model/
```
</details>
### Security
> *"Set up row-level security so regional managers only see their region"*
Claude creates RLS roles with descriptions, sets up perspectives for different user groups, and exports the model for version control.
<details>
<summary>Example: what Claude runs behind the scenes</summary>
```bash
pbi security-role create "Regional Manager" --description "Users see only their region's data"
pbi perspective create "Executive Dashboard"
pbi perspective create "Regional Detail"
pbi database export-tmdl ./model-backup/
```
</details>
### Documentation
> *"Document everything in this model"*
Claude catalogs every table, measure, column, and relationship. Generates data dictionaries, measure inventories, and can export the full model as TMDL for human-readable reference.
<details>
<summary>Example: what Claude runs behind the scenes</summary>
```bash
pbi --json model get
pbi --json model stats
pbi --json table list
pbi --json measure list
pbi --json relationship list
pbi database export-tmdl ./model-docs/
```
</details>
### Diagnostics
> *"Why is this DAX query so slow?"*
Claude traces query execution, clears caches for clean benchmarks, checks model health, and verifies the environment.
<details>
<summary>Example: what Claude runs behind the scenes</summary>
```bash
pbi dax clear-cache
pbi trace start
pbi dax execute "EVALUATE SUMMARIZECOLUMNS(...)" --timeout 300
pbi trace stop
pbi trace export ./trace.json
```
</details>
### Partitions & Expressions
> *"Set up partitions for incremental refresh on the Sales table"*
Claude manages table partitions, shared M/Power Query expressions, and calendar table configuration.
<details>
<summary>Example: what Claude runs behind the scenes</summary>
```bash
pbi partition list --table Sales
pbi partition create "Sales_2024" --table Sales --expression "..." --mode Import
pbi expression create "ServerURL" --expression '"https://api.example.com"'
pbi calendar mark Calendar --date-column Date
```
</details>
| Skill | What you say | What Claude does |
|-------|-------------|-----------------|
| **Report** | *"Create a new report"* | Scaffolds PBIR reports, validates, previews |
| **Visuals** | *"Add a bar chart"* | Adds, binds, bulk-manages 32 visual types |
| **Pages** | *"Add a new page"* | Manages pages, bookmarks, drillthrough |
| **Themes** | *"Apply brand colours"* | Themes, conditional formatting |
| **Filters** | *"Show top 10 only"* | TopN, date, categorical filters |
---
## All Commands
22 command groups covering the full Power BI Tabular Object Model. You rarely need these directly when using Claude Code, but they're available for scripting, CI/CD, or manual use.
27 command groups covering both the semantic model and the report layer.
| Category | Commands |
|----------|----------|
| **Queries** | `dax execute`, `dax validate`, `dax clear-cache` |
| **Model** | `table`, `column`, `measure`, `relationship`, `hierarchy`, `calc-group` |
| **Deploy** | `database export-tmdl`, `database import-tmdl`, `database export-tmsl`, `transaction` |
| **Deploy** | `database export-tmdl/import-tmdl/export-tmsl/diff-tmdl`, `transaction` |
| **Security** | `security-role`, `perspective` |
| **Connect** | `connect`, `disconnect`, `connections list`, `connections last` |
| **Connect** | `connect`, `disconnect`, `connections list/last` |
| **Data** | `partition`, `expression`, `calendar`, `advanced culture` |
| **Diagnostics** | `trace start`, `trace stop`, `trace fetch`, `trace export`, `model stats` |
| **Tools** | `setup`, `repl`, `skills install`, `skills list` |
| **Diagnostics** | `trace start/stop/fetch/export`, `model stats` |
| **Report** | `report create/info/validate/preview/reload`, `report add-page/delete-page/get-page` |
| **Visuals** | `visual add/get/list/update/delete/bind`, `visual bulk-bind/bulk-update/bulk-delete` |
| **Filters** | `filters list/add-categorical/add-topn/add-relative-date/remove/clear` |
| **Formatting** | `format get/clear/background-gradient/background-conditional/background-measure` |
| **Bookmarks** | `bookmarks list/get/add/delete/set-visibility` |
| **Tools** | `setup`, `repl`, `skills install/list/uninstall` |
Use `--json` for machine-readable output (for scripts and AI agents):
Use `--json` for machine-readable output:
```bash
pbi --json measure list
pbi --json dax execute "EVALUATE Sales"
pbi --json visual list --page overview
```
Run `pbi <command> --help` for full options.
---
## 32 Supported Visual Types
**Charts:** bar, line, column, area, ribbon, waterfall, stacked bar, clustered bar, clustered column, scatter, funnel, combo, donut/pie, treemap
**Cards/KPIs:** card, cardVisual (modern), cardNew, multi-row card, KPI, gauge
**Tables:** table, matrix &bull; **Slicers:** slicer, text, list, advanced &bull; **Maps:** Azure Map
**Decorative:** action button, image, shape, textbox, page navigator
---
## REPL Mode
For interactive work, the REPL keeps a persistent connection alive between commands:
For interactive work, the REPL keeps a persistent connection:
```
$ pbi repl
@ -261,44 +152,7 @@ pbi(localhost-54321)> dax execute "EVALUATE TOPN(5, Sales)"
pbi(localhost-54321)> exit
```
Tab completion, command history, and a dynamic prompt showing your active connection.
---
## How It Works
pbi-cli connects directly to Power BI Desktop's Analysis Services engine via pythonnet and the .NET Tabular Object Model (TOM). No external binaries or MCP servers needed. Everything runs in-process for sub-second command execution.
```
+------------------+ +---------------------+ +------------------+
| pbi-cli | | Bundled TOM DLLs | | Power BI |
| (Python CLI) | pythnet | (.NET in-process) | XMLA | Desktop |
| Click commands |-------->| TOM / ADOMD.NET |-------->| msmdsrv.exe |
+------------------+ +---------------------+ +------------------+
```
**Why a CLI?** When an AI agent uses an MCP server directly, the tool schemas consume ~4,000+ tokens per tool in the context window. A `pbi` command costs ~30 tokens. Same capabilities, 100x less context.
<details>
<summary><b>Configuration details</b></summary>
All config lives in `~/.pbi-cli/`:
```
~/.pbi-cli/
config.json # Default connection preference
connections.json # Named connections
repl_history # REPL command history
```
Bundled DLLs ship inside the Python package (`pbi_cli/dlls/`):
- Microsoft.AnalysisServices.Tabular.dll
- Microsoft.AnalysisServices.AdomdClient.dll
- Microsoft.AnalysisServices.Core.dll
- Microsoft.AnalysisServices.Tabular.Json.dll
- Microsoft.AnalysisServices.dll
</details>
Tab completion, command history, and a dynamic prompt.
---
@ -313,7 +167,7 @@ pip install -e ".[dev]"
```bash
ruff check src/ tests/ # Lint
mypy src/ # Type check
pytest -m "not e2e" # Run tests
pytest -m "not e2e" # Run tests (488 tests)
```
---

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pbi-cli-tool"
version = "3.9.0"
version = "3.10.0"
description = "CLI for Power BI semantic models and PBIR reports - direct .NET connection for token-efficient AI agent usage"
readme = "README.pypi.md"
license = {text = "MIT"}

View file

@ -1,3 +1,3 @@
"""pbi-cli: CLI for Power BI semantic models via direct .NET interop."""
__version__ = "3.9.0"
__version__ = "3.10.0"

View file

@ -19,13 +19,21 @@ _PBI_CLI_CLAUDE_MD_SNIPPET = (
"When working with Power BI, DAX, semantic models, or data modeling,\n"
"invoke the relevant pbi-cli skill before responding:\n"
"\n"
"**Semantic Model (requires `pbi connect`):**\n"
"- **power-bi-dax** -- DAX queries, measures, calculations\n"
"- **power-bi-modeling** -- tables, columns, measures, relationships\n"
"- **power-bi-diagnostics** -- troubleshooting, tracing, setup\n"
"- **power-bi-deployment** -- TMDL export/import, transactions\n"
"- **power-bi-deployment** -- TMDL export/import, transactions, diff\n"
"- **power-bi-docs** -- model documentation, data dictionary\n"
"- **power-bi-partitions** -- partitions, M expressions, data sources\n"
"- **power-bi-security** -- RLS roles, perspectives, access control\n"
"- **power-bi-diagnostics** -- troubleshooting, tracing, setup\n"
"\n"
"**Report Layer (no connection needed):**\n"
"- **power-bi-report** -- scaffold, validate, preview PBIR reports\n"
"- **power-bi-visuals** -- add, bind, update, bulk-manage visuals\n"
"- **power-bi-pages** -- pages, bookmarks, visibility, drillthrough\n"
"- **power-bi-themes** -- themes, conditional formatting, styling\n"
"- **power-bi-filters** -- page and visual filters (TopN, date, categorical)\n"
"\n"
"Critical: Multi-line DAX (VAR/RETURN) cannot be passed via `-e`.\n"
"Use `--file` or stdin piping instead. See power-bi-dax skill.\n"

View file

@ -40,3 +40,27 @@ class TomError(PbiCliError):
self.operation = operation
self.detail = detail
super().__init__(f"{operation}: {detail}")
class VisualTypeError(PbiCliError):
"""Raised when a visual type is not recognised."""
def __init__(self, visual_type: str) -> None:
self.visual_type = visual_type
super().__init__(
f"Unknown visual type '{visual_type}'. "
"Run 'pbi visual types' to see supported types."
)
class ReportNotFoundError(PbiCliError):
"""Raised when no PBIR report definition folder can be found."""
def __init__(
self,
message: str = (
"No PBIR report found. Run this command inside a .pbip project "
"or pass --path to the .Report folder."
),
) -> None:
super().__init__(message)

View file

@ -0,0 +1,492 @@
"""Pure-function backend for PBIR filter operations.
Every function takes a ``Path`` to the definition folder and returns a plain
Python dict suitable for ``format_result()``.
Filters are stored in ``filterConfig.filters[]`` inside either:
- ``pages/<page_name>/page.json`` for page-level filters
- ``pages/<page_name>/visuals/<visual_name>/visual.json`` for visual-level filters
"""
from __future__ import annotations
import json
import secrets
from pathlib import Path
from typing import Any
from pbi_cli.core.errors import PbiCliError
from pbi_cli.core.pbir_path import get_page_dir, get_visual_dir
# ---------------------------------------------------------------------------
# JSON helpers
# ---------------------------------------------------------------------------
def _read_json(path: Path) -> dict[str, Any]:
"""Read and parse a JSON file."""
return json.loads(path.read_text(encoding="utf-8"))
def _write_json(path: Path, data: dict[str, Any]) -> None:
"""Write JSON with consistent formatting."""
path.write_text(
json.dumps(data, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
def _generate_name() -> str:
"""Generate a 20-character hex identifier matching PBIR convention."""
return secrets.token_hex(10)
# ---------------------------------------------------------------------------
# Path resolution helpers
# ---------------------------------------------------------------------------
def _resolve_target_path(
definition_path: Path,
page_name: str,
visual_name: str | None,
) -> Path:
"""Return the JSON file path for the target (page or visual)."""
if visual_name is None:
return get_page_dir(definition_path, page_name) / "page.json"
return get_visual_dir(definition_path, page_name, visual_name) / "visual.json"
def _get_filters(data: dict[str, Any]) -> list[dict[str, Any]]:
"""Extract the filters list from a page or visual JSON dict."""
filter_config = data.get("filterConfig")
if not isinstance(filter_config, dict):
return []
filters = filter_config.get("filters")
if not isinstance(filters, list):
return []
return filters
def _set_filters(data: dict[str, Any], filters: list[dict[str, Any]]) -> dict[str, Any]:
"""Return a new dict with filterConfig.filters replaced (immutable update)."""
filter_config = dict(data.get("filterConfig") or {})
filter_config["filters"] = filters
return {**data, "filterConfig": filter_config}
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def filter_list(
definition_path: Path,
page_name: str,
visual_name: str | None = None,
) -> list[dict[str, Any]]:
"""List filters on a page or specific visual.
If visual_name is None, returns page-level filters from page.json.
If visual_name is given, returns visual-level filters from visual.json.
Returns the raw filter dicts from filterConfig.filters[].
"""
target = _resolve_target_path(definition_path, page_name, visual_name)
if not target.exists():
raise PbiCliError(f"File not found: {target}")
data = _read_json(target)
return _get_filters(data)
def filter_add_categorical(
definition_path: Path,
page_name: str,
table: str,
column: str,
values: list[str],
visual_name: str | None = None,
name: str | None = None,
) -> dict[str, Any]:
"""Add a categorical filter to a page or visual.
Builds the full filterConfig entry from table/column/values.
The source alias is always the first letter of the table name (lowercase).
Returns a status dict with name, type, and scope.
"""
target = _resolve_target_path(definition_path, page_name, visual_name)
if not target.exists():
raise PbiCliError(f"File not found: {target}")
filter_name = name if name is not None else _generate_name()
alias = table[0].lower()
scope = "visual" if visual_name is not None else "page"
where_values: list[list[dict[str, Any]]] = [
[{"Literal": {"Value": f"'{v}'"}}] for v in values
]
entry: dict[str, Any] = {
"name": filter_name,
"field": {
"Column": {
"Expression": {"SourceRef": {"Entity": table}},
"Property": column,
}
},
"type": "Categorical",
"filter": {
"Version": 2,
"From": [{"Name": alias, "Entity": table, "Type": 0}],
"Where": [
{
"Condition": {
"In": {
"Expressions": [
{
"Column": {
"Expression": {"SourceRef": {"Source": alias}},
"Property": column,
}
}
],
"Values": where_values,
}
}
}
],
},
}
if scope == "page":
entry["howCreated"] = "User"
data = _read_json(target)
filters = list(_get_filters(data))
filters.append(entry)
updated = _set_filters(data, filters)
_write_json(target, updated)
return {"status": "added", "name": filter_name, "type": "Categorical", "scope": scope}
def filter_remove(
definition_path: Path,
page_name: str,
filter_name: str,
visual_name: str | None = None,
) -> dict[str, Any]:
"""Remove a filter by name from a page or visual.
Raises PbiCliError if filter_name is not found.
Returns a status dict with the removed filter name.
"""
target = _resolve_target_path(definition_path, page_name, visual_name)
if not target.exists():
raise PbiCliError(f"File not found: {target}")
data = _read_json(target)
filters = _get_filters(data)
remaining = [f for f in filters if f.get("name") != filter_name]
if len(remaining) == len(filters):
raise PbiCliError(
f"Filter '{filter_name}' not found on "
f"{'visual ' + visual_name if visual_name else 'page'} '{page_name}'."
)
updated = _set_filters(data, remaining)
_write_json(target, updated)
return {"status": "removed", "name": filter_name}
def filter_add_topn(
definition_path: Path,
page_name: str,
table: str,
column: str,
n: int,
order_by_table: str,
order_by_column: str,
direction: str = "Top",
visual_name: str | None = None,
name: str | None = None,
) -> dict[str, Any]:
"""Add a TopN filter to a page or visual.
*direction* is ``"Top"`` (highest N by *order_by_column*) or
``"Bottom"`` (lowest N). Direction maps to Power BI query Direction
values: Top = 2 (Descending), Bottom = 1 (Ascending).
Returns a status dict with name, type, scope, n, and direction.
"""
direction_upper = direction.strip().capitalize()
if direction_upper not in ("Top", "Bottom"):
raise PbiCliError(f"direction must be 'Top' or 'Bottom', got '{direction}'.")
pbi_direction = 2 if direction_upper == "Top" else 1
target = _resolve_target_path(definition_path, page_name, visual_name)
if not target.exists():
raise PbiCliError(f"File not found: {target}")
filter_name = name if name is not None else _generate_name()
cat_alias = table[0].lower()
ord_alias = order_by_table[0].lower()
# Avoid alias collision when both tables start with the same letter
if ord_alias == cat_alias and order_by_table != table:
ord_alias = ord_alias + "2"
scope = "visual" if visual_name is not None else "page"
# Inner subquery From: include both tables when they differ
inner_from: list[dict[str, Any]] = [
{"Name": cat_alias, "Entity": table, "Type": 0},
]
if order_by_table != table:
inner_from.append({"Name": ord_alias, "Entity": order_by_table, "Type": 0})
entry: dict[str, Any] = {
"name": filter_name,
"field": {
"Column": {
"Expression": {"SourceRef": {"Entity": table}},
"Property": column,
}
},
"type": "TopN",
"filter": {
"Version": 2,
"From": [
{
"Name": "subquery",
"Expression": {
"Subquery": {
"Query": {
"Version": 2,
"From": inner_from,
"Select": [
{
"Column": {
"Expression": {
"SourceRef": {"Source": cat_alias}
},
"Property": column,
},
"Name": "field",
}
],
"OrderBy": [
{
"Direction": pbi_direction,
"Expression": {
"Aggregation": {
"Expression": {
"Column": {
"Expression": {
"SourceRef": {
"Source": ord_alias
if order_by_table != table
else cat_alias
}
},
"Property": order_by_column,
}
},
"Function": 0,
}
},
}
],
"Top": n,
}
}
},
"Type": 2,
},
{"Name": cat_alias, "Entity": table, "Type": 0},
],
"Where": [
{
"Condition": {
"In": {
"Expressions": [
{
"Column": {
"Expression": {
"SourceRef": {"Source": cat_alias}
},
"Property": column,
}
}
],
"Table": {"SourceRef": {"Source": "subquery"}},
}
}
}
],
},
}
if scope == "page":
entry["howCreated"] = "User"
data = _read_json(target)
filters = list(_get_filters(data))
filters.append(entry)
updated = _set_filters(data, filters)
_write_json(target, updated)
return {
"status": "added",
"name": filter_name,
"type": "TopN",
"scope": scope,
"n": n,
"direction": direction_upper,
}
# TimeUnit integer codes used by Power BI for RelativeDate filters.
_RELATIVE_DATE_TIME_UNITS: dict[str, int] = {
"days": 0,
"weeks": 1,
"months": 2,
"years": 3,
}
def filter_add_relative_date(
definition_path: Path,
page_name: str,
table: str,
column: str,
amount: int,
time_unit: str,
visual_name: str | None = None,
name: str | None = None,
) -> dict[str, Any]:
"""Add a RelativeDate filter (e.g. "last 3 months") to a page or visual.
*amount* is a positive integer representing the period count.
*time_unit* is one of ``"days"``, ``"weeks"``, ``"months"``, ``"years"``.
The filter matches rows where *column* falls in the last *amount* *time_unit*
relative to today (inclusive of the current period boundary).
Returns a status dict with name, type, scope, amount, and time_unit.
"""
time_unit_lower = time_unit.strip().lower()
if time_unit_lower not in _RELATIVE_DATE_TIME_UNITS:
valid = ", ".join(_RELATIVE_DATE_TIME_UNITS)
raise PbiCliError(
f"time_unit must be one of {valid}, got '{time_unit}'."
)
time_unit_code = _RELATIVE_DATE_TIME_UNITS[time_unit_lower]
days_code = _RELATIVE_DATE_TIME_UNITS["days"]
target = _resolve_target_path(definition_path, page_name, visual_name)
if not target.exists():
raise PbiCliError(f"File not found: {target}")
filter_name = name if name is not None else _generate_name()
alias = table[0].lower()
scope = "visual" if visual_name is not None else "page"
# LowerBound: DateSpan(DateAdd(DateAdd(Now(), +1, days), -amount, time_unit), days)
lower_bound: dict[str, Any] = {
"DateSpan": {
"Expression": {
"DateAdd": {
"Expression": {
"DateAdd": {
"Expression": {"Now": {}},
"Amount": 1,
"TimeUnit": days_code,
}
},
"Amount": -amount,
"TimeUnit": time_unit_code,
}
},
"TimeUnit": days_code,
}
}
# UpperBound: DateSpan(Now(), days)
upper_bound: dict[str, Any] = {
"DateSpan": {
"Expression": {"Now": {}},
"TimeUnit": days_code,
}
}
entry: dict[str, Any] = {
"name": filter_name,
"field": {
"Column": {
"Expression": {"SourceRef": {"Entity": table}},
"Property": column,
}
},
"type": "RelativeDate",
"filter": {
"Version": 2,
"From": [{"Name": alias, "Entity": table, "Type": 0}],
"Where": [
{
"Condition": {
"Between": {
"Expression": {
"Column": {
"Expression": {"SourceRef": {"Source": alias}},
"Property": column,
}
},
"LowerBound": lower_bound,
"UpperBound": upper_bound,
}
}
}
],
},
}
if scope == "page":
entry["howCreated"] = "User"
data = _read_json(target)
filters = list(_get_filters(data))
filters.append(entry)
updated = _set_filters(data, filters)
_write_json(target, updated)
return {
"status": "added",
"name": filter_name,
"type": "RelativeDate",
"scope": scope,
"amount": amount,
"time_unit": time_unit_lower,
}
def filter_clear(
definition_path: Path,
page_name: str,
visual_name: str | None = None,
) -> dict[str, Any]:
"""Remove all filters from a page or visual.
Returns a status dict with the count of removed filters and scope.
"""
target = _resolve_target_path(definition_path, page_name, visual_name)
if not target.exists():
raise PbiCliError(f"File not found: {target}")
scope = "visual" if visual_name is not None else "page"
data = _read_json(target)
filters = _get_filters(data)
removed = len(filters)
updated = _set_filters(data, [])
_write_json(target, updated)
return {"status": "cleared", "removed": removed, "scope": scope}

View file

@ -515,13 +515,12 @@ def theme_set(
if not found:
resource_packages.append({
"name": "RegisteredResources",
"resourcePackage": {
"items": [{
"name": theme_path.name,
"type": 202,
"path": f"BaseThemes/{theme_path.name}",
}],
},
"type": "RegisteredResources",
"items": [{
"name": theme_path.name,
"type": 202,
"path": f"BaseThemes/{theme_path.name}",
}],
})
report_data["resourcePackages"] = resource_packages

View file

@ -145,17 +145,29 @@ def _diff_table_entities(
"partitions_added": [],
"partitions_removed": [],
"partitions_changed": [],
"hierarchies_added": [],
"hierarchies_removed": [],
"hierarchies_changed": [],
"other_added": [],
"other_removed": [],
"other_changed": [],
}
# Map TMDL keywords to their plural result-dict prefix
keyword_plurals: dict[str, str] = {
"measure": "measures",
"column": "columns",
"partition": "partitions",
"hierarchy": "hierarchies",
}
all_keys = set(base_entities) | set(head_entities)
for key in sorted(all_keys):
keyword, _, name = key.partition("/")
added_key = f"{keyword}s_added" if f"{keyword}s_added" in result else "other_added"
removed_key = f"{keyword}s_removed" if f"{keyword}s_removed" in result else "other_removed"
changed_key = f"{keyword}s_changed" if f"{keyword}s_changed" in result else "other_changed"
plural = keyword_plurals.get(keyword, "other")
added_key = f"{plural}_added"
removed_key = f"{plural}_removed"
changed_key = f"{plural}_changed"
if key not in base_entities:
result[added_key].append(name)

View file

@ -541,7 +541,7 @@ def visual_bind(
definition_path: Path,
page_name: str,
visual_name: str,
bindings: list[dict[str, str]],
bindings: list[dict[str, Any]],
) -> dict[str, Any]:
"""Bind semantic model fields to visual data roles.

View file

@ -52,6 +52,7 @@ def cli(ctx: click.Context, json_output: bool, connection: str | None) -> None:
def _register_commands() -> None:
"""Lazily import and register all command groups."""
from pbi_cli.commands.advanced import advanced
from pbi_cli.commands.bookmarks import bookmarks
from pbi_cli.commands.calc_group import calc_group
from pbi_cli.commands.calendar import calendar
from pbi_cli.commands.column import column
@ -59,6 +60,8 @@ def _register_commands() -> None:
from pbi_cli.commands.database import database
from pbi_cli.commands.dax import dax
from pbi_cli.commands.expression import expression
from pbi_cli.commands.filters import filters
from pbi_cli.commands.format_cmd import format_cmd
from pbi_cli.commands.hierarchy import hierarchy
from pbi_cli.commands.measure import measure
from pbi_cli.commands.model import model
@ -66,12 +69,14 @@ def _register_commands() -> None:
from pbi_cli.commands.perspective import perspective
from pbi_cli.commands.relationship import relationship
from pbi_cli.commands.repl_cmd import repl
from pbi_cli.commands.report import report
from pbi_cli.commands.security import security_role
from pbi_cli.commands.setup_cmd import setup
from pbi_cli.commands.skills_cmd import skills
from pbi_cli.commands.table import table
from pbi_cli.commands.trace import trace
from pbi_cli.commands.transaction import transaction
from pbi_cli.commands.visual import visual
cli.add_command(setup)
cli.add_command(connect)
@ -96,6 +101,11 @@ def _register_commands() -> None:
cli.add_command(advanced)
cli.add_command(repl)
cli.add_command(skills)
cli.add_command(report)
cli.add_command(visual)
cli.add_command(filters)
cli.add_command(format_cmd)
cli.add_command(bookmarks)
_register_commands()

View file

@ -50,6 +50,31 @@ pbi database import-tmdl ./model-tmdl/
pbi database export-tmsl
```
## TMDL Diff (Compare Snapshots)
Compare two TMDL export folders to see what changed between snapshots.
Useful for CI/CD pipelines ("what did this PR change in the model?").
```bash
# Compare two exports
pbi database diff-tmdl ./model-before/ ./model-after/
# JSON output for CI/CD scripting
pbi --json database diff-tmdl ./baseline/ ./current/
```
Returns a structured summary:
- **tables**: added, removed, and changed tables with per-table entity diffs
(measures, columns, partitions, hierarchies added/removed/changed)
- **relationships**: added, removed, and changed relationships
- **model**: changed model-level properties (e.g. culture, default power bi dataset version)
- **summary**: total counts of all changes
LineageTag-only changes (GUID regeneration without real edits) are automatically
filtered out to avoid false positives.
No connection to Power BI Desktop is needed -- works on exported folders.
## Database Operations
```bash

View file

@ -0,0 +1,137 @@
---
name: Power BI Filters
description: >
Add, remove, and manage page-level and visual-level filters on Power BI PBIR
reports using pbi-cli. Invoke this skill whenever the user mentions "filter",
"TopN filter", "top 10", "bottom 5", "relative date filter", "last 30 days",
"categorical filter", "include values", "exclude values", "clear filters",
"slicer filter", "page filter", "visual filter", or wants to restrict which
data appears on a page or in a specific visual.
tools: pbi-cli
---
# Power BI Filters Skill
Add and manage filters on PBIR report pages and visuals. Filters are stored
in the `filterConfig` section of `page.json` (page-level) or `visual.json`
(visual-level). No Power BI Desktop connection is needed.
## Listing Filters
```bash
# List all filters on a page
pbi filters list --page page_abc123
# List filters on a specific visual
pbi filters list --page page_abc123 --visual visual_def456
```
Returns each filter's name, type, field, and scope (page or visual).
## Categorical Filters
Include or exclude specific values from a column:
```bash
# Include only East and West regions
pbi filters add-categorical --page page1 \
--table Sales --column Region \
--values "East" "West"
```
The filter appears in the page's `filterConfig.filters` array. Power BI
evaluates it as an IN-list against the specified column.
## TopN Filters
Show only the top (or bottom) N items ranked by a measure:
```bash
# Top 10 products by revenue
pbi filters add-topn --page page1 \
--table Product --column Name \
--n 10 \
--order-by-table Sales --order-by-column Revenue
# Bottom 5 by quantity (ascending)
pbi filters add-topn --page page1 \
--table Product --column Name \
--n 5 \
--order-by-table Sales --order-by-column Quantity \
--direction Bottom
```
The `--table` and `--column` define which dimension to filter (the rows you
want to keep). The `--order-by-table` and `--order-by-column` define the
measure used for ranking. These can be different tables -- for example,
filtering Product names by Sales revenue.
Direction defaults to `Top` (descending -- highest N). Use `--direction Bottom`
for ascending (lowest N).
## Relative Date Filters
Filter by a rolling window relative to today:
```bash
# Last 30 days
pbi filters add-relative-date --page page1 \
--table Calendar --column Date \
--period days --count 30 --direction last
# Next 7 days
pbi filters add-relative-date --page page1 \
--table Calendar --column Date \
--period days --count 7 --direction next
```
Period options: `days`, `weeks`, `months`, `quarters`, `years`.
Direction: `last` (past) or `next` (future).
## Visual-Level Filters
Add a filter to a specific visual instead of the whole page by including
`--visual`:
```bash
pbi filters add-categorical --page page1 --visual vis_abc \
--table Sales --column Channel \
--values "Online"
```
## Removing Filters
```bash
# Remove a specific filter by name
pbi filters remove --page page1 --name filter_abc123
# Clear ALL filters from a page
pbi filters clear --page page1
```
Filter names are auto-generated unique IDs. Use `pbi filters list` to find
the name of the filter you want to remove.
## Workflow: Set Up Dashboard Filters
```bash
# 1. Add a date filter to the overview page
pbi filters add-relative-date --page overview \
--table Calendar --column Date \
--period months --count 12 --direction last
# 2. Add a TopN filter to show only top customers
pbi filters add-topn --page overview \
--table Customer --column Name \
--n 20 \
--order-by-table Sales --order-by-column Revenue
# 3. Verify
pbi filters list --page overview
```
## JSON Output
```bash
pbi --json filters list --page page1
```

View file

@ -0,0 +1,151 @@
---
name: Power BI Pages
description: >
Manage Power BI report pages and bookmarks -- add, remove, configure, and lay
out pages in PBIR reports using pbi-cli. Invoke this skill whenever the user
mentions "add page", "new page", "delete page", "page layout", "page size",
"page background", "hide page", "show page", "drillthrough", "page order",
"page visibility", "page settings", "page navigation", "bookmark", "create
bookmark", "save bookmark", "delete bookmark", or wants to manage bookmarks
that capture page-level state. Also invoke when the user asks about drillthrough
configuration or pageBinding.
tools: pbi-cli
---
# Power BI Pages Skill
Manage pages in PBIR reports. Pages are folders inside `definition/pages/`
containing a `page.json` file and a `visuals/` directory. No Power BI Desktop
connection is needed.
## Listing and Inspecting Pages
```bash
# List all pages with display names, order, and visibility
pbi report list-pages
# Get full details of a specific page
pbi report get-page page_abc123
```
`get-page` returns:
- `name`, `display_name`, `ordinal` (sort order)
- `width`, `height` (canvas size in pixels)
- `display_option` (e.g. `"FitToPage"`)
- `visual_count` -- how many visuals on the page
- `is_hidden` -- whether the page is hidden in the navigation pane
- `page_type` -- `"Default"` or `"Drillthrough"`
- `filter_config` -- page-level filter configuration (if any)
- `visual_interactions` -- custom visual interaction rules (if any)
- `page_binding` -- drillthrough parameter definition (if drillthrough page)
## Adding Pages
```bash
# Add with display name (folder name auto-generated)
pbi report add-page --display-name "Executive Overview"
# Custom folder name and canvas size
pbi report add-page --display-name "Details" --name detail_page \
--width 1920 --height 1080
```
Default canvas size is 1280x720 (standard 16:9). Common alternatives:
- 1920x1080 -- Full HD
- 1280x960 -- 4:3
- Custom dimensions for mobile or dashboard layouts
## Deleting Pages
```bash
# Delete a page and all its visuals
pbi report delete-page page_abc123
```
This removes the entire page folder including all visual subdirectories.
## Page Background
```bash
# Set a solid background colour
pbi report set-background page_abc123 --color "#F5F5F5"
```
## Page Visibility
Control whether a page appears in the report navigation pane:
```bash
# Hide a page (useful for drillthrough or tooltip pages)
pbi report set-visibility page_abc123 --hidden
# Show a hidden page
pbi report set-visibility page_abc123 --visible
```
## Bookmarks
Bookmarks capture page-level state (filters, visibility, scroll position).
They live in `definition/bookmarks/`:
```bash
# List all bookmarks in the report
pbi bookmarks list
# Get details of a specific bookmark
pbi bookmarks get "My Bookmark"
# Add a new bookmark
pbi bookmarks add "Executive View"
# Delete a bookmark
pbi bookmarks delete "Old Bookmark"
# Toggle bookmark visibility
pbi bookmarks set-visibility "Draft View" --hidden
```
## Drillthrough Pages
Drillthrough pages have a `pageBinding` field in `page.json` that defines the
drillthrough parameter. When you call `get-page` on a drillthrough page, the
`page_binding` field returns the full binding definition including parameter
name, bound filter, and field expression. Regular pages return `null`.
To create a drillthrough page, add a page and then configure it as drillthrough
in Power BI Desktop (PBIR drillthrough configuration is not yet supported via
CLI -- the CLI can read and report on drillthrough configuration).
## Workflow: Set Up Report Pages
```bash
# 1. Add pages in order
pbi report add-page --display-name "Overview" --name overview
pbi report add-page --display-name "Sales Detail" --name sales_detail
pbi report add-page --display-name "Regional Drillthrough" --name region_drill
# 2. Hide the drillthrough page from navigation
pbi report set-visibility region_drill --hidden
# 3. Set backgrounds
pbi report set-background overview --color "#FAFAFA"
# 4. Verify the setup
pbi report list-pages
```
## Path Resolution
Page commands inherit the report path from the parent `pbi report` group:
1. Explicit: `pbi report --path ./MyReport.Report list-pages`
2. Auto-detect: walks up from CWD looking for `*.Report/definition/`
3. From `.pbip`: finds sibling `.Report` folder from `.pbip` file
## JSON Output
```bash
pbi --json report list-pages
pbi --json report get-page page_abc123
pbi --json bookmarks list
```

View file

@ -0,0 +1,169 @@
---
name: Power BI Report
description: >
Scaffold, validate, preview, and manage Power BI PBIR report projects using
pbi-cli. Invoke this skill whenever the user mentions "create report", "new
report", "PBIR", "scaffold", "validate report", "report structure", "preview
report", "report info", "reload Desktop", "convert report", ".pbip project",
"report project", or wants to understand the PBIR folder format, set up a new
report from scratch, or work with the report as a whole. For specific tasks,
see also: power-bi-visuals (charts, binding), power-bi-pages (page management),
power-bi-themes (themes, formatting), power-bi-filters (page/visual filters).
tools: pbi-cli
---
# Power BI Report Skill
Manage Power BI PBIR report projects at the top level -- scaffolding, validation,
preview, and Desktop integration. No connection to Power BI Desktop is needed
for most operations.
## PBIR Format
PBIR (Enhanced Report Format) stores reports as a folder of JSON files:
```
MyReport.Report/
definition.pbir # dataset reference
definition/
version.json # PBIR version
report.json # report settings, theme
pages/
pages.json # page order
page_abc123/
page.json # page settings
visuals/
visual_def456/
visual.json # visual type, position, bindings
```
Each file has a public JSON schema from Microsoft for validation.
PBIR is GA as of January 2026 and the default format in Desktop since March 2026.
## Creating a Report
```bash
# Scaffold a new report project
pbi report create ./MyProject --name "Sales Report"
# With dataset reference
pbi report create ./MyProject --name "Sales" --dataset-path "../Sales.Dataset"
```
This creates the full folder structure with `definition.pbir`, `report.json`,
`version.json`, and an empty `pages/` directory.
## Report Info and Validation
```bash
# Show report metadata summary (pages, theme, dataset)
pbi report info
pbi report info --path ./MyReport.Report
# Validate report structure and JSON files
pbi report validate
```
Validation checks:
- Required files exist (`definition.pbir`, `report.json`, `version.json`)
- All JSON files parse without errors
- Schema URLs are present and consistent
- Page references in `pages.json` match actual page folders
## Preview
Start a live HTML preview of the report layout:
```bash
pbi report preview
```
Opens a browser showing all pages with visual placeholders, types, positions,
and data bindings. The preview auto-refreshes when files change.
Requires the `preview` optional dependency: `pip install pbi-cli-tool[preview]`
## Desktop Integration
```bash
# Trigger Power BI Desktop to reload the current report
pbi report reload
```
Power BI Desktop's Developer Mode auto-detects TMDL changes but not PBIR
changes. This command sends a keyboard shortcut to the Desktop window to
trigger a reload. Requires the `reload` optional dependency: `pip install pbi-cli-tool[reload]`
## Convert
```bash
# Convert a .Report folder into a distributable .pbip project
pbi report convert ./MyReport.Report --output ./distributable/
```
## Path Resolution
All report commands auto-detect the `.Report` folder:
1. Explicit: `pbi report --path ./MyReport.Report info`
2. Auto-detect: walks up from CWD looking for `*.Report/definition/`
3. From `.pbip`: finds sibling `.Report` folder from `.pbip` file
## Workflow: Build a Complete Report
This workflow uses commands from multiple skills:
```bash
# 1. Scaffold report (this skill)
pbi report create . --name "SalesDashboard" --dataset-path "../SalesModel.Dataset"
# 2. Add pages (power-bi-pages skill)
pbi report add-page --display-name "Overview" --name overview
pbi report add-page --display-name "Details" --name details
# 3. Add visuals (power-bi-visuals skill)
pbi visual add --page overview --type card --name revenue_card
pbi visual add --page overview --type bar --name sales_by_region
# 4. Bind data (power-bi-visuals skill)
pbi visual bind revenue_card --page overview --field "Sales[Total Revenue]"
pbi visual bind sales_by_region --page overview \
--category "Geo[Region]" --value "Sales[Amount]"
# 5. Apply theme (power-bi-themes skill)
pbi report set-theme --file brand-colors.json
# 6. Validate (this skill)
pbi report validate
```
## Combining Model and Report
pbi-cli covers both the semantic model layer and the report layer:
```bash
# Model layer (requires pbi connect)
pbi connect
pbi measure create Sales "Total Revenue" "SUM(Sales[Amount])"
# Report layer (no connection needed)
pbi report create . --name "Sales"
pbi visual add --page overview --type card --name rev_card
pbi visual bind rev_card --page overview --field "Sales[Total Revenue]"
```
## Related Skills
| Skill | When to use |
|-------|-------------|
| **power-bi-visuals** | Add, bind, update, delete visuals |
| **power-bi-pages** | Add, remove, configure pages and bookmarks |
| **power-bi-themes** | Themes, conditional formatting |
| **power-bi-filters** | Page and visual filters |
## JSON Output
```bash
pbi --json report info
pbi --json report validate
```

View file

@ -0,0 +1,137 @@
---
name: Power BI Themes
description: >
Apply, inspect, and compare Power BI report themes and conditional formatting
rules using pbi-cli. Invoke this skill whenever the user mentions "theme",
"colours", "colors", "branding", "dark mode", "corporate theme", "styling",
"conditional formatting", "colour scale", "gradient", "data bars",
"background colour", "formatting rules", "visual formatting", or wants to
change the overall look-and-feel of a report or apply data-driven formatting
to specific visuals.
tools: pbi-cli
---
# Power BI Themes Skill
Manage report-wide themes and per-visual conditional formatting. No Power BI
Desktop connection is needed.
## Applying a Theme
Power BI themes are JSON files that define colours, fonts, and visual defaults
for the entire report. Apply one with:
```bash
pbi report set-theme --file corporate-theme.json
```
This copies the theme file into the report's `StaticResources/RegisteredResources/`
folder and updates `report.json` to reference it. The theme takes effect when
the report is opened in Power BI Desktop.
## Inspecting the Current Theme
```bash
pbi report get-theme
```
Returns:
- `base_theme` -- the built-in theme name (e.g. `"CY24SU06"`)
- `custom_theme` -- custom theme name if one is applied (or `null`)
- `theme_data` -- full JSON of the custom theme file (if it exists)
## Comparing Themes
Before applying a new theme, preview what would change:
```bash
pbi report diff-theme --file proposed-theme.json
```
Returns:
- `current` / `proposed` -- display names
- `added` -- keys in proposed but not current
- `removed` -- keys in current but not proposed
- `changed` -- keys present in both but with different values
This helps catch unintended colour changes before committing.
## Theme JSON Structure
A Power BI theme JSON file typically contains:
```json
{
"name": "Corporate Brand",
"dataColors": ["#0078D4", "#00BCF2", "#FFB900", "#D83B01", "#8661C5", "#00B294"],
"background": "#FFFFFF",
"foreground": "#252423",
"tableAccent": "#0078D4",
"visualStyles": { ... }
}
```
Key sections:
- `dataColors` -- palette for data series (6-12 colours recommended)
- `background` / `foreground` -- page and text defaults
- `tableAccent` -- header colour for tables and matrices
- `visualStyles` -- per-visual-type overrides (font sizes, padding, etc.)
See [Microsoft theme documentation](https://learn.microsoft.com/power-bi/create-reports/desktop-report-themes) for the full schema.
## Conditional Formatting
Apply data-driven formatting to individual visuals:
```bash
# Gradient background (colour scale from min to max)
pbi format background-gradient visual_abc --page page1 \
--table Sales --column Revenue \
--min-color "#FFFFFF" --max-color "#0078D4"
# Rules-based background (specific value triggers a colour)
pbi format background-conditional visual_abc --page page1 \
--table Sales --column Status --value "Critical" --color "#FF0000"
# Measure-driven background (a DAX measure returns the colour)
pbi format background-measure visual_abc --page page1 \
--table Sales --measure "Status Color"
# Inspect current formatting rules
pbi format get visual_abc --page page1
# Clear all formatting rules on a visual
pbi format clear visual_abc --page page1
```
## Workflow: Brand a Report
```bash
# 1. Create the theme file
cat > brand-theme.json << 'EOF'
{
"name": "Acme Corp",
"dataColors": ["#1B365D", "#5B8DB8", "#E87722", "#00A3E0", "#6D2077", "#43B02A"],
"background": "#F8F8F8",
"foreground": "#1B365D",
"tableAccent": "#1B365D"
}
EOF
# 2. Preview the diff against the current theme
pbi report diff-theme --file brand-theme.json
# 3. Apply it
pbi report set-theme --file brand-theme.json
# 4. Verify
pbi report get-theme
```
## JSON Output
```bash
pbi --json report get-theme
pbi --json report diff-theme --file proposed.json
pbi --json format get vis1 --page p1
```

View file

@ -0,0 +1,223 @@
---
name: Power BI Visuals
description: >
Add, configure, bind data to, and bulk-manage visuals on Power BI PBIR report
pages using pbi-cli. Invoke this skill whenever the user mentions "add a chart",
"bar chart", "line chart", "card", "KPI", "gauge", "scatter", "table visual",
"matrix", "slicer", "combo chart", "bind data", "visual type", "visual layout",
"resize visuals", "bulk update visuals", "bulk delete", "visual calculations",
or wants to place, move, bind, or remove any visual on a report page. Also invoke
when the user asks what visual types are supported or how to connect a visual to
their data model.
tools: pbi-cli
---
# Power BI Visuals Skill
Create and manage visuals on PBIR report pages. No Power BI Desktop connection
is needed -- these commands operate directly on JSON files.
## Adding Visuals
```bash
# Add by alias (pbi-cli resolves to the PBIR type)
pbi visual add --page page_abc123 --type bar
pbi visual add --page page_abc123 --type card --name "Revenue Card"
# Custom position and size (pixels)
pbi visual add --page page_abc123 --type scatter \
--x 50 --y 400 --width 600 --height 350
# Named visual for easy reference
pbi visual add --page page_abc123 --type combo --name sales_combo
```
Each visual is created as a folder with a `visual.json` file inside the page's
`visuals/` directory. The template includes the correct schema URL and queryState
roles for the chosen type.
## Binding Data
Visuals start empty. Use `visual bind` with `Table[Column]` notation to connect
them to your semantic model. The bind options vary by visual type -- see the
type table below.
```bash
# Bar chart: category axis + value
pbi visual bind mybar --page p1 \
--category "Geography[Region]" --value "Sales[Revenue]"
# Card: single field
pbi visual bind mycard --page p1 --field "Sales[Total Revenue]"
# Matrix: rows + values + optional column
pbi visual bind mymatrix --page p1 \
--row "Product[Category]" --value "Sales[Amount]" --value "Sales[Quantity]"
# Scatter: X, Y, detail, optional size and legend
pbi visual bind myscatter --page p1 \
--x "Sales[Quantity]" --y "Sales[Revenue]" --detail "Product[Name]"
# Combo chart: category + column series + line series
pbi visual bind mycombo --page p1 \
--category "Calendar[Month]" --column "Sales[Revenue]" --line "Sales[Margin]"
# KPI: indicator + goal + trend axis
pbi visual bind mykpi --page p1 \
--indicator "Sales[Revenue]" --goal "Sales[Target]" --trend "Calendar[Date]"
# Gauge: value + max/target
pbi visual bind mygauge --page p1 \
--value "Sales[Revenue]" --max "Sales[Target]"
```
Binding uses ROLE_ALIASES to translate friendly names like `--value` into the PBIR
role name (e.g. `Y`, `Values`, `Data`). Measure vs Column is inferred from the role:
value/indicator/goal/max roles create Measure references, category/row/detail roles
create Column references. Override with `--measure` flag if needed.
## Inspecting and Updating
```bash
# List all visuals on a page
pbi visual list --page page_abc123
# Get full details of one visual
pbi visual get visual_def456 --page page_abc123
# Move, resize, or toggle visibility
pbi visual update vis1 --page p1 --width 600 --height 400
pbi visual update vis1 --page p1 --x 100 --y 200
pbi visual update vis1 --page p1 --hidden
pbi visual update vis1 --page p1 --visible
# Delete a visual
pbi visual delete visual_def456 --page page_abc123
```
## Container Properties
Set border, background, or title on the visual container itself:
```bash
pbi visual set-container vis1 --page p1 --background "#F0F0F0"
pbi visual set-container vis1 --page p1 --border-color "#CCCCCC" --border-width 2
pbi visual set-container vis1 --page p1 --title "Sales by Region"
```
## Visual Calculations
Add DAX calculations that run inside the visual scope:
```bash
pbi visual calc-add vis1 --page p1 --role Values \
--name "RunningTotal" --expression "RUNNINGSUM([Revenue])"
pbi visual calc-list vis1 --page p1
pbi visual calc-delete vis1 --page p1 --name "RunningTotal"
```
## Bulk Operations
Operate on many visuals at once by filtering with `--type` or `--name-pattern`:
```bash
# Find visuals matching criteria
pbi visual where --page overview --type barChart
pbi visual where --page overview --type kpi --y-min 300
# Bind the same field to ALL bar charts on a page
pbi visual bulk-bind --page overview --type barChart \
--category "Date[Month]" --value "Sales[Revenue]"
# Resize all KPI cards
pbi visual bulk-update --page overview --type kpi --width 250 --height 120
# Hide all visuals matching a pattern
pbi visual bulk-update --page overview --name-pattern "Temp_*" --hidden
# Delete all placeholders
pbi visual bulk-delete --page overview --name-pattern "Placeholder_*"
```
Filter options for `where`, `bulk-bind`, `bulk-update`, `bulk-delete`:
- `--type` -- PBIR visual type or alias (e.g. `barChart`, `bar`)
- `--name-pattern` -- fnmatch glob on visual name (e.g. `Chart_*`)
- `--x-min`, `--x-max`, `--y-min`, `--y-max` -- position bounds (pixels)
All bulk commands require at least `--type` or `--name-pattern` to prevent
accidental mass operations.
## Supported Visual Types (32)
### Charts
| Alias | PBIR Type | Bind Options |
|--------------------|------------------------------|-----------------------------------------------|
| bar | barChart | --category, --value, --legend |
| line | lineChart | --category, --value, --legend |
| column | columnChart | --category, --value, --legend |
| area | areaChart | --category, --value, --legend |
| ribbon | ribbonChart | --category, --value, --legend |
| waterfall | waterfallChart | --category, --value, --breakdown |
| stacked_bar | stackedBarChart | --category, --value, --legend |
| clustered_bar | clusteredBarChart | --category, --value, --legend |
| clustered_column | clusteredColumnChart | --category, --value, --legend |
| scatter | scatterChart | --x, --y, --detail, --size, --legend |
| funnel | funnelChart | --category, --value |
| combo | lineStackedColumnComboChart | --category, --column, --line, --legend |
| donut / pie | donutChart | --category, --value, --legend |
| treemap | treemap | --category, --value |
### Cards and KPIs
| Alias | PBIR Type | Bind Options |
|--------------------|------------------------------|-----------------------------------------------|
| card | card | --field |
| card_visual | cardVisual | --field (modern card) |
| card_new | cardNew | --field |
| multi_row_card | multiRowCard | --field |
| kpi | kpi | --indicator, --goal, --trend |
| gauge | gauge | --value, --max / --target |
### Tables
| Alias | PBIR Type | Bind Options |
|--------------------|------------------------------|-----------------------------------------------|
| table | tableEx | --value |
| matrix | pivotTable | --row, --value, --column |
### Slicers
| Alias | PBIR Type | Bind Options |
|--------------------|------------------------------|-----------------------------------------------|
| slicer | slicer | --field |
| text_slicer | textSlicer | --field |
| list_slicer | listSlicer | --field |
| advanced_slicer | advancedSlicerVisual | --field (tile/image slicer) |
### Maps
| Alias | PBIR Type | Bind Options |
|--------------------|------------------------------|-----------------------------------------------|
| azure_map / map | azureMap | --category, --size |
### Decorative and Navigation
| Alias | PBIR Type | Bind Options |
|--------------------|------------------------------|-----------------------------------------------|
| action_button | actionButton | (no data binding) |
| image | image | (no data binding) |
| shape | shape | (no data binding) |
| textbox | textbox | (no data binding) |
| page_navigator | pageNavigator | (no data binding) |
## JSON Output
All commands support `--json` for agent consumption:
```bash
pbi --json visual list --page overview
pbi --json visual get vis1 --page overview
pbi --json visual where --page overview --type barChart
```

View file

@ -844,7 +844,7 @@ class TestThemeSet:
None,
)
assert reg is not None
items = reg.get("resourcePackage", {}).get("items", [])
items = reg.get("items", [])
assert any(i["name"] == "Ocean.json" for i in items)
def test_theme_set_idempotent_for_same_theme(
@ -857,7 +857,7 @@ class TestThemeSet:
report_data = _read(sample_report / "report.json")
packages: list[dict[str, Any]] = report_data.get("resourcePackages", [])
reg = next(p for p in packages if p.get("name") == "RegisteredResources")
items = reg.get("resourcePackage", {}).get("items", [])
items = reg.get("items", [])
names = [i["name"] for i in items]
# No duplicate entries for the same file
assert names.count("Stable.json") == 1

View file

@ -0,0 +1,109 @@
"""Skill triggering evaluation -- verify prompts match expected skills.
This is NOT a pytest test. Run directly:
python tests/test_skill_triggering.py
Uses keyword-based scoring to simulate which skill description best matches
each user prompt, without requiring an LLM call.
"""
from __future__ import annotations
import importlib.resources
import re
import yaml
def _load_skills() -> dict[str, str]:
"""Load all skill names and descriptions from bundled skills."""
skills_pkg = importlib.resources.files("pbi_cli.skills")
skills: dict[str, str] = {}
for item in skills_pkg.iterdir():
if item.is_dir() and (item / "SKILL.md").is_file():
content = (item / "SKILL.md").read_text(encoding="utf-8")
match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
if match:
fm = yaml.safe_load(match.group(1))
skills[item.name] = fm.get("description", "").lower()
return skills
def _score_prompt(prompt: str, description: str) -> int:
"""Score how well a prompt matches a skill description using word overlap."""
prompt_words = set(re.findall(r"[a-z]+", prompt.lower()))
desc_words = set(re.findall(r"[a-z]+", description))
# Weight longer matching words higher (domain terms matter more)
score = 0
for word in prompt_words & desc_words:
if len(word) >= 5:
score += 3
elif len(word) >= 3:
score += 1
return score
def _find_best_skill(prompt: str, skills: dict[str, str]) -> str:
"""Find the skill with the highest keyword overlap score."""
scores = {name: _score_prompt(prompt, desc) for name, desc in skills.items()}
return max(scores, key=lambda k: scores[k])
# Test cases: (prompt, expected_skill)
TEST_CASES: list[tuple[str, str]] = [
# power-bi-visuals
("Add a bar chart to the overview page showing sales by region", "power-bi-visuals"),
("I need to bind Sales[Revenue] to the value field on my KPI visual", "power-bi-visuals"),
("What visual types does pbi-cli support? I need a scatter plot", "power-bi-visuals"),
("Resize all the card visuals on the dashboard page to 200x120", "power-bi-visuals"),
# power-bi-pages
("Add a new page called Regional Detail to my report", "power-bi-pages"),
("Hide the drillthrough page from the navigation bar", "power-bi-pages"),
("Create a bookmark for the current executive view", "power-bi-pages"),
# power-bi-themes
("Apply our corporate brand colours to the entire report", "power-bi-themes"),
(
"I want conditional formatting on the revenue column green for high red for low",
"power-bi-themes",
),
("Compare this new theme JSON against what is currently applied", "power-bi-themes"),
# power-bi-filters
("Filter the overview page to show only the top 10 products by revenue", "power-bi-filters"),
("Add a date filter for the last 30 days on the Sales page", "power-bi-filters"),
("What filters are currently on my dashboard page", "power-bi-filters"),
# power-bi-report
("Create a new PBIR report project for our sales dashboard", "power-bi-report"),
("Validate the report structure to make sure everything is correct", "power-bi-report"),
("Start the preview server so I can see the layout", "power-bi-report"),
# Should NOT trigger report skills
("Create a measure called Total Revenue equals SUM of Sales Amount", "power-bi-modeling"),
("Export the semantic model to TMDL for version control", "power-bi-deployment"),
("Set up row-level security for regional managers", "power-bi-security"),
]
def main() -> None:
skills = _load_skills()
passed = 0
failed = 0
print(f"Testing {len(TEST_CASES)} prompts against {len(skills)} skills\n")
print(f"{'#':<3} {'Result':<6} {'Expected':<22} {'Got':<22} Prompt")
print("-" * 100)
for i, (prompt, expected) in enumerate(TEST_CASES, 1):
got = _find_best_skill(prompt, skills)
ok = got == expected
status = "PASS" if ok else "FAIL"
if ok:
passed += 1
else:
failed += 1
short_prompt = prompt[:45] + "..." if len(prompt) > 45 else prompt
print(f"{i:<3} {status:<6} {expected:<22} {got:<22} {short_prompt}")
print(f"\n{passed}/{len(TEST_CASES)} passed, {failed} failed")
if __name__ == "__main__":
main()