pbi-enterprise-cli 0.1.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pbi_cli/__init__.py +3 -0
- pbi_cli/_audit.py +57 -0
- pbi_cli/_snapshot.py +95 -0
- pbi_cli/backends/__init__.py +1 -0
- pbi_cli/backends/mock_backend.py +323 -0
- pbi_cli/backends/pbir_backend.py +813 -0
- pbi_cli/backends/protocol.py +52 -0
- pbi_cli/backends/tom_backend.py +650 -0
- pbi_cli/backends/xmla_backend.py +627 -0
- pbi_cli/cli.py +332 -0
- pbi_cli/commands/__init__.py +1 -0
- pbi_cli/commands/_doctor.py +84 -0
- pbi_cli/commands/_shared.py +88 -0
- pbi_cli/commands/calendar_cmd.py +186 -0
- pbi_cli/commands/connections.py +153 -0
- pbi_cli/commands/custom_visual.py +325 -0
- pbi_cli/commands/database.py +76 -0
- pbi_cli/commands/dax.py +174 -0
- pbi_cli/commands/deploy.py +193 -0
- pbi_cli/commands/docs.py +57 -0
- pbi_cli/commands/filter_cmd.py +235 -0
- pbi_cli/commands/govern.py +124 -0
- pbi_cli/commands/layout.py +104 -0
- pbi_cli/commands/measure.py +185 -0
- pbi_cli/commands/model.py +499 -0
- pbi_cli/commands/partition.py +89 -0
- pbi_cli/commands/repl.py +209 -0
- pbi_cli/commands/report.py +561 -0
- pbi_cli/commands/security.py +90 -0
- pbi_cli/commands/server_cmd.py +30 -0
- pbi_cli/commands/skills_cmd.py +168 -0
- pbi_cli/commands/source.py +581 -0
- pbi_cli/commands/theme.py +60 -0
- pbi_cli/commands/trace.py +142 -0
- pbi_cli/commands/visual.py +507 -0
- pbi_cli/commands/watch.py +145 -0
- pbi_cli/docs_gen/__init__.py +1 -0
- pbi_cli/docs_gen/confluence.py +24 -0
- pbi_cli/docs_gen/markdown.py +36 -0
- pbi_cli/governance/__init__.py +1 -0
- pbi_cli/governance/engine.py +70 -0
- pbi_cli/governance/rules/__init__.py +85 -0
- pbi_cli/governance/rules/measure_brackets.py +27 -0
- pbi_cli/governance/rules/measure_description.py +41 -0
- pbi_cli/governance/rules/measure_format.py +38 -0
- pbi_cli/governance/rules/measure_naming.py +93 -0
- pbi_cli/governance/rules/table_pascal_case.py +44 -0
- pbi_cli/intelligence/__init__.py +1 -0
- pbi_cli/intelligence/layout_engine.py +192 -0
- pbi_cli/intelligence/measure_generator.py +40 -0
- pbi_cli/intelligence/theme_generator.py +193 -0
- pbi_cli/intelligence/visual_builder.py +429 -0
- pbi_cli/intelligence/visual_recommender.py +42 -0
- pbi_cli/server/__init__.py +1 -0
- pbi_cli/server/api.py +185 -0
- pbi_enterprise_cli-0.1.0.dev0.dist-info/METADATA +103 -0
- pbi_enterprise_cli-0.1.0.dev0.dist-info/RECORD +61 -0
- pbi_enterprise_cli-0.1.0.dev0.dist-info/WHEEL +5 -0
- pbi_enterprise_cli-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- pbi_enterprise_cli-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
- pbi_enterprise_cli-0.1.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""pbi connections — named connection profiles (~/.pbi-cli/connections.json)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from pbi_cli.commands._shared import output_json_or_table
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
_CONFIG_PATH = Path.home() / ".pbi-cli" / "connections.json"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load() -> dict[str, Any]:
|
|
20
|
+
if _CONFIG_PATH.exists():
|
|
21
|
+
return json.loads(_CONFIG_PATH.read_text(encoding="utf-8"))
|
|
22
|
+
return {"connections": [], "last": None}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _save(data: dict[str, Any]) -> None:
|
|
26
|
+
_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
_CONFIG_PATH.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@click.group()
|
|
31
|
+
def connections() -> None:
|
|
32
|
+
"""Manage named Power BI connection profiles (~/.pbi-cli/connections.json)."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@connections.command("list")
|
|
36
|
+
@click.pass_context
|
|
37
|
+
def connections_list(ctx: click.Context) -> None:
|
|
38
|
+
"""List all saved named connections."""
|
|
39
|
+
data = _load()
|
|
40
|
+
conns = data.get("connections", [])
|
|
41
|
+
if not conns:
|
|
42
|
+
console.print("[yellow]No saved connections.[/yellow]")
|
|
43
|
+
console.print("Use 'pbi connections add' to save one.")
|
|
44
|
+
return
|
|
45
|
+
# Mask secrets
|
|
46
|
+
display = []
|
|
47
|
+
for c in conns:
|
|
48
|
+
row = dict(c)
|
|
49
|
+
if row.get("client_secret"):
|
|
50
|
+
row["client_secret"] = "***"
|
|
51
|
+
display.append(row)
|
|
52
|
+
output_json_or_table(display, ctx, title="Saved Connections")
|
|
53
|
+
if data.get("last"):
|
|
54
|
+
console.print(f"\n[dim]Last used:[/dim] [cyan]{data['last']}[/cyan]")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@connections.command("last")
|
|
58
|
+
def connections_last() -> None:
|
|
59
|
+
"""Show the most recently used connection name."""
|
|
60
|
+
data = _load()
|
|
61
|
+
last = data.get("last")
|
|
62
|
+
if last:
|
|
63
|
+
console.print(f"[cyan]Last connection:[/cyan] {last}")
|
|
64
|
+
else:
|
|
65
|
+
console.print("[yellow]No connection has been used yet.[/yellow]")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@connections.command("add")
|
|
69
|
+
@click.option("--name", required=True, help="Short alias for this connection.")
|
|
70
|
+
@click.option(
|
|
71
|
+
"--type",
|
|
72
|
+
"conn_type",
|
|
73
|
+
required=True,
|
|
74
|
+
type=click.Choice(["desktop", "xmla"]),
|
|
75
|
+
help="Connection type.",
|
|
76
|
+
)
|
|
77
|
+
@click.option("--endpoint", default=None, help="XMLA endpoint URL.")
|
|
78
|
+
@click.option("--catalog", default=None, help="Dataset / semantic model name.")
|
|
79
|
+
@click.option(
|
|
80
|
+
"--auth",
|
|
81
|
+
default="device_flow",
|
|
82
|
+
type=click.Choice(["device_flow", "service_principal", "token"]),
|
|
83
|
+
help="Auth mode (XMLA only).",
|
|
84
|
+
)
|
|
85
|
+
@click.option("--client-id", default=None, help="AAD client ID (service_principal).")
|
|
86
|
+
@click.option("--client-secret", default=None, help="AAD client secret (service_principal).")
|
|
87
|
+
@click.option("--tenant-id", default=None, help="AAD tenant ID (service_principal).")
|
|
88
|
+
@click.option("--port", default=None, type=int, help="Desktop local server port.")
|
|
89
|
+
def connections_add(
|
|
90
|
+
name: str,
|
|
91
|
+
conn_type: str,
|
|
92
|
+
endpoint: str | None,
|
|
93
|
+
catalog: str | None,
|
|
94
|
+
auth: str,
|
|
95
|
+
client_id: str | None,
|
|
96
|
+
client_secret: str | None,
|
|
97
|
+
tenant_id: str | None,
|
|
98
|
+
port: int | None,
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Save a named connection profile."""
|
|
101
|
+
data = _load()
|
|
102
|
+
# Remove existing with same name
|
|
103
|
+
data["connections"] = [c for c in data.get("connections", []) if c["name"] != name]
|
|
104
|
+
record: dict[str, Any] = {"name": name, "type": conn_type}
|
|
105
|
+
if conn_type == "xmla":
|
|
106
|
+
if not endpoint:
|
|
107
|
+
raise click.UsageError("--endpoint is required for xmla connections.")
|
|
108
|
+
record.update({"endpoint": endpoint, "catalog": catalog or "", "auth": auth})
|
|
109
|
+
if auth == "service_principal":
|
|
110
|
+
record.update(
|
|
111
|
+
{"client_id": client_id, "client_secret": client_secret, "tenant_id": tenant_id}
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
if port:
|
|
115
|
+
record["port"] = port
|
|
116
|
+
data.setdefault("connections", []).append(record)
|
|
117
|
+
_save(data)
|
|
118
|
+
console.print(f"[green]Connection saved:[/green] '{name}' ({conn_type})")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@connections.command("remove")
|
|
122
|
+
@click.argument("name")
|
|
123
|
+
def connections_remove(name: str) -> None:
|
|
124
|
+
"""Remove a saved connection profile by name."""
|
|
125
|
+
data = _load()
|
|
126
|
+
before = len(data.get("connections", []))
|
|
127
|
+
data["connections"] = [c for c in data.get("connections", []) if c["name"] != name]
|
|
128
|
+
if len(data["connections"]) == before:
|
|
129
|
+
console.print(f"[yellow]Connection '{name}' not found.[/yellow]")
|
|
130
|
+
return
|
|
131
|
+
if data.get("last") == name:
|
|
132
|
+
data["last"] = None
|
|
133
|
+
_save(data)
|
|
134
|
+
console.print(f"[red]Removed[/red] connection '{name}'.")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@connections.command("use")
|
|
138
|
+
@click.argument("name")
|
|
139
|
+
def connections_use(name: str) -> None:
|
|
140
|
+
"""Set a connection as the active default and print its connect command."""
|
|
141
|
+
data = _load()
|
|
142
|
+
conn = next((c for c in data.get("connections", []) if c["name"] == name), None)
|
|
143
|
+
if not conn:
|
|
144
|
+
console.print(f"[red]Connection '{name}' not found.[/red]")
|
|
145
|
+
raise SystemExit(1)
|
|
146
|
+
data["last"] = name
|
|
147
|
+
_save(data)
|
|
148
|
+
console.print(f"[green]Active connection:[/green] {name}")
|
|
149
|
+
if conn["type"] == "xmla":
|
|
150
|
+
console.print(f" pbi --backend xmla model info # uses {conn.get('endpoint', '')}")
|
|
151
|
+
else:
|
|
152
|
+
port_flag = f" --port {conn['port']}" if conn.get("port") else ""
|
|
153
|
+
console.print(f" pbi{port_flag} model info")
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""pbi custom-visual — TypeScript custom visual SDK scaffolding, build, package, import."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group("custom-visual")
|
|
17
|
+
def custom_visual() -> None:
|
|
18
|
+
"""Scaffold, build, package, and import Power BI custom visuals (TypeScript SDK)."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@custom_visual.command("scaffold")
|
|
22
|
+
@click.option("--name", required=True, help="Visual name (PascalCase, e.g. MyBarChart).")
|
|
23
|
+
@click.option(
|
|
24
|
+
"--display", default=None, help="Display name shown in Power BI (defaults to --name)."
|
|
25
|
+
)
|
|
26
|
+
@click.option("--guid", default=None, help="Visual GUID (auto-generated if omitted).")
|
|
27
|
+
@click.option(
|
|
28
|
+
"--output", default=".", type=click.Path(), help="Parent directory for the new project."
|
|
29
|
+
)
|
|
30
|
+
@click.option("--author", default="", help="Author name for package.json.")
|
|
31
|
+
def custom_visual_scaffold(
|
|
32
|
+
name: str, display: str | None, guid: str | None, output: str, author: str
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Create a new custom visual TypeScript project from the pbi-cli template.
|
|
35
|
+
|
|
36
|
+
\b
|
|
37
|
+
Produces the standard pbiviz project structure:
|
|
38
|
+
<name>/
|
|
39
|
+
package.json
|
|
40
|
+
pbiviz.json
|
|
41
|
+
tsconfig.json
|
|
42
|
+
src/
|
|
43
|
+
visual.ts
|
|
44
|
+
settings.ts
|
|
45
|
+
style/
|
|
46
|
+
visual.less
|
|
47
|
+
assets/
|
|
48
|
+
icon.png (placeholder)
|
|
49
|
+
capabilities.json
|
|
50
|
+
|
|
51
|
+
\b
|
|
52
|
+
Example:
|
|
53
|
+
pbi custom-visual scaffold --name MyBarChart --author "Mudassir"
|
|
54
|
+
"""
|
|
55
|
+
import uuid
|
|
56
|
+
|
|
57
|
+
display_name = display or name
|
|
58
|
+
visual_guid = guid or str(uuid.uuid4())
|
|
59
|
+
project_dir = Path(output) / name
|
|
60
|
+
if project_dir.exists():
|
|
61
|
+
console.print(f"[yellow]Directory already exists:[/yellow] {project_dir}")
|
|
62
|
+
raise SystemExit(1)
|
|
63
|
+
|
|
64
|
+
project_dir.mkdir(parents=True)
|
|
65
|
+
(project_dir / "src").mkdir()
|
|
66
|
+
(project_dir / "style").mkdir()
|
|
67
|
+
(project_dir / "assets").mkdir()
|
|
68
|
+
|
|
69
|
+
# pbiviz.json
|
|
70
|
+
(project_dir / "pbiviz.json").write_text(
|
|
71
|
+
json.dumps(
|
|
72
|
+
{
|
|
73
|
+
"visual": {
|
|
74
|
+
"name": name,
|
|
75
|
+
"displayName": display_name,
|
|
76
|
+
"guid": visual_guid,
|
|
77
|
+
"visualClassName": name,
|
|
78
|
+
"version": "1.0.0",
|
|
79
|
+
"description": f"{display_name} Power BI custom visual",
|
|
80
|
+
"supportUrl": "",
|
|
81
|
+
"gitHubUrl": "",
|
|
82
|
+
},
|
|
83
|
+
"apiVersion": "5.3.0",
|
|
84
|
+
"author": {"name": author, "email": ""},
|
|
85
|
+
"assets": {"icon": "assets/icon.png"},
|
|
86
|
+
"stringResources": [],
|
|
87
|
+
"capabilities": "capabilities.json",
|
|
88
|
+
"stringResourcesPath": "",
|
|
89
|
+
"externalJS": [],
|
|
90
|
+
"style": "style/visual.less",
|
|
91
|
+
"sources": ["src/visual.ts"],
|
|
92
|
+
},
|
|
93
|
+
indent=2,
|
|
94
|
+
),
|
|
95
|
+
encoding="utf-8",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# package.json
|
|
99
|
+
(project_dir / "package.json").write_text(
|
|
100
|
+
json.dumps(
|
|
101
|
+
{
|
|
102
|
+
"name": name.lower(),
|
|
103
|
+
"version": "1.0.0",
|
|
104
|
+
"description": f"{display_name} custom visual",
|
|
105
|
+
"scripts": {
|
|
106
|
+
"build": "tsc --noEmit",
|
|
107
|
+
"package": "pbiviz package",
|
|
108
|
+
"start": "pbiviz start",
|
|
109
|
+
},
|
|
110
|
+
"devDependencies": {
|
|
111
|
+
"powerbi-visuals-tools": "^5.2.0",
|
|
112
|
+
"typescript": "^5.0.0",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
indent=2,
|
|
116
|
+
),
|
|
117
|
+
encoding="utf-8",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# tsconfig.json
|
|
121
|
+
(project_dir / "tsconfig.json").write_text(
|
|
122
|
+
json.dumps(
|
|
123
|
+
{
|
|
124
|
+
"compilerOptions": {
|
|
125
|
+
"target": "ES6",
|
|
126
|
+
"module": "commonjs",
|
|
127
|
+
"lib": ["es2015", "dom"],
|
|
128
|
+
"strict": True,
|
|
129
|
+
"outDir": ".tmp/build",
|
|
130
|
+
"declaration": True,
|
|
131
|
+
"sourceMap": True,
|
|
132
|
+
},
|
|
133
|
+
"include": ["src/**/*.ts"],
|
|
134
|
+
"exclude": ["node_modules"],
|
|
135
|
+
},
|
|
136
|
+
indent=2,
|
|
137
|
+
),
|
|
138
|
+
encoding="utf-8",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# capabilities.json
|
|
142
|
+
(project_dir / "capabilities.json").write_text(
|
|
143
|
+
json.dumps(
|
|
144
|
+
{
|
|
145
|
+
"dataRoles": [{"name": "Values", "kind": "Measure", "displayName": "Values"}],
|
|
146
|
+
"dataViewMappings": [{"single": {"role": "Values"}}],
|
|
147
|
+
"objects": {},
|
|
148
|
+
"supportsHighlight": True,
|
|
149
|
+
"sorting": {"default": {}},
|
|
150
|
+
},
|
|
151
|
+
indent=2,
|
|
152
|
+
),
|
|
153
|
+
encoding="utf-8",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# src/visual.ts
|
|
157
|
+
(project_dir / "src" / "visual.ts").write_text(
|
|
158
|
+
f"""\
|
|
159
|
+
"use strict";
|
|
160
|
+
|
|
161
|
+
import powerbi from "powerbi-visuals-api";
|
|
162
|
+
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
|
|
163
|
+
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
|
|
164
|
+
import IVisual = powerbi.extensibility.visual.IVisual;
|
|
165
|
+
|
|
166
|
+
export class {name} implements IVisual {{
|
|
167
|
+
private target: HTMLElement;
|
|
168
|
+
|
|
169
|
+
constructor(options: VisualConstructorOptions) {{
|
|
170
|
+
this.target = options.element;
|
|
171
|
+
}}
|
|
172
|
+
|
|
173
|
+
public update(options: VisualUpdateOptions): void {{
|
|
174
|
+
const dataView = options.dataViews?.[0];
|
|
175
|
+
if (!dataView?.single?.value) return;
|
|
176
|
+
this.target.innerHTML = `<p style="font-size:24px">${{dataView.single.value}}</p>`;
|
|
177
|
+
}}
|
|
178
|
+
}}
|
|
179
|
+
""",
|
|
180
|
+
encoding="utf-8",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# style/visual.less
|
|
184
|
+
(project_dir / "style" / "visual.less").write_text(
|
|
185
|
+
f"/** {display_name} styles */\n.visual-container {{\n font-family: 'Segoe UI', sans-serif;\n}}\n", # noqa: E501
|
|
186
|
+
encoding="utf-8",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# assets/icon.png placeholder (empty file)
|
|
190
|
+
(project_dir / "assets" / "icon.png").write_bytes(b"")
|
|
191
|
+
|
|
192
|
+
console.print(f"[green]Scaffolded:[/green] {project_dir}")
|
|
193
|
+
console.print(f" GUID: {visual_guid}")
|
|
194
|
+
console.print(" Version: 1.0.0")
|
|
195
|
+
console.print("\n[cyan]Next steps:[/cyan]")
|
|
196
|
+
console.print(f" cd {project_dir}")
|
|
197
|
+
console.print(" npm install")
|
|
198
|
+
console.print(" pbi custom-visual build --path .")
|
|
199
|
+
console.print(" pbi custom-visual package --path .")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@custom_visual.command("build")
|
|
203
|
+
@click.option("--path", default=".", type=click.Path(exists=True), help="Visual project directory.")
|
|
204
|
+
@click.option("--watch", is_flag=True, help="Watch for changes and rebuild automatically.")
|
|
205
|
+
def custom_visual_build(path: str, watch: bool) -> None:
|
|
206
|
+
"""Run tsc --noEmit to type-check the visual TypeScript source.
|
|
207
|
+
|
|
208
|
+
Requires Node.js and the devDependencies installed (npm install).
|
|
209
|
+
|
|
210
|
+
\b
|
|
211
|
+
Example:
|
|
212
|
+
pbi custom-visual build --path ./MyBarChart
|
|
213
|
+
"""
|
|
214
|
+
cmd = ["npx", "tsc", "--noEmit"]
|
|
215
|
+
if watch:
|
|
216
|
+
cmd.append("--watch")
|
|
217
|
+
console.print(f"[cyan]Type-checking:[/cyan] {path}")
|
|
218
|
+
try:
|
|
219
|
+
result = subprocess.run(cmd, cwd=path)
|
|
220
|
+
if result.returncode == 0:
|
|
221
|
+
console.print("[green]Type check passed.[/green]")
|
|
222
|
+
else:
|
|
223
|
+
console.print("[red]Type errors found.[/red] Fix the issues above and re-run.")
|
|
224
|
+
raise SystemExit(result.returncode)
|
|
225
|
+
except FileNotFoundError:
|
|
226
|
+
console.print("[red]npx / Node.js not found.[/red] Install Node.js from https://nodejs.org")
|
|
227
|
+
raise SystemExit(1)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@custom_visual.command("package")
|
|
231
|
+
@click.option("--path", default=".", type=click.Path(exists=True), help="Visual project directory.")
|
|
232
|
+
@click.option(
|
|
233
|
+
"--output", default=None, type=click.Path(), help="Output .pbiviz path (default: dist/)."
|
|
234
|
+
)
|
|
235
|
+
def custom_visual_package(path: str, output: str | None) -> None:
|
|
236
|
+
"""Package the visual into a .pbiviz file ready for Power BI import.
|
|
237
|
+
|
|
238
|
+
Requires powerbi-visuals-tools (pbiviz) installed: npm install -g powerbi-visuals-tools
|
|
239
|
+
|
|
240
|
+
\b
|
|
241
|
+
Example:
|
|
242
|
+
pbi custom-visual package --path ./MyBarChart
|
|
243
|
+
"""
|
|
244
|
+
project = Path(path)
|
|
245
|
+
pbiviz_json = project / "pbiviz.json"
|
|
246
|
+
if not pbiviz_json.exists():
|
|
247
|
+
console.print("[red]pbiviz.json not found.[/red] Run 'pbi custom-visual scaffold' first.")
|
|
248
|
+
raise SystemExit(1)
|
|
249
|
+
|
|
250
|
+
meta = json.loads(pbiviz_json.read_text(encoding="utf-8"))
|
|
251
|
+
name = meta["visual"]["name"]
|
|
252
|
+
version = meta["visual"]["version"]
|
|
253
|
+
|
|
254
|
+
console.print(f"[cyan]Packaging:[/cyan] {name} v{version}")
|
|
255
|
+
try:
|
|
256
|
+
result = subprocess.run(["pbiviz", "package"], cwd=path)
|
|
257
|
+
if result.returncode == 0:
|
|
258
|
+
dist_file = project / "dist" / f"{name}.pbiviz"
|
|
259
|
+
if output and dist_file.exists():
|
|
260
|
+
shutil.copy(dist_file, output)
|
|
261
|
+
console.print(f"[green]Packaged →[/green] {output}")
|
|
262
|
+
else:
|
|
263
|
+
console.print(f"[green]Packaged →[/green] {dist_file}")
|
|
264
|
+
else:
|
|
265
|
+
console.print("[red]Packaging failed.[/red]")
|
|
266
|
+
raise SystemExit(result.returncode)
|
|
267
|
+
except FileNotFoundError:
|
|
268
|
+
console.print("[red]pbiviz not found.[/red] Run: npm install -g powerbi-visuals-tools")
|
|
269
|
+
raise SystemExit(1)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@custom_visual.command("import")
|
|
273
|
+
@click.option("--pbip", required=True, help="Path to the .pbip report project.")
|
|
274
|
+
@click.option(
|
|
275
|
+
"--pbiviz",
|
|
276
|
+
required=True,
|
|
277
|
+
type=click.Path(exists=True),
|
|
278
|
+
help="Path to the .pbiviz package file.",
|
|
279
|
+
)
|
|
280
|
+
@click.pass_context
|
|
281
|
+
def custom_visual_import(ctx: click.Context, pbip: str, pbiviz: str) -> None:
|
|
282
|
+
"""Import a .pbiviz package into a .pbip report project.
|
|
283
|
+
|
|
284
|
+
Registers the custom visual in the report so it can be used on any page.
|
|
285
|
+
|
|
286
|
+
\b
|
|
287
|
+
Example:
|
|
288
|
+
pbi custom-visual import --pbip ./MyReport --pbiviz ./MyBarChart/dist/MyBarChart.pbiviz
|
|
289
|
+
"""
|
|
290
|
+
from pbi_cli.commands._shared import dry_run_echo
|
|
291
|
+
|
|
292
|
+
if dry_run_echo(ctx, f"import custom visual '{pbiviz}' into '{pbip}'"):
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
pbiviz_path = Path(pbiviz)
|
|
296
|
+
if not pbiviz_path.suffix == ".pbiviz":
|
|
297
|
+
console.print("[red]File must have .pbiviz extension.[/red]")
|
|
298
|
+
raise SystemExit(1)
|
|
299
|
+
|
|
300
|
+
# Extract the pbiviz (it's a zip) to find the visual GUID
|
|
301
|
+
import zipfile
|
|
302
|
+
|
|
303
|
+
with zipfile.ZipFile(pbiviz_path) as z:
|
|
304
|
+
names = z.namelist()
|
|
305
|
+
pbiviz_json_name = next((n for n in names if n.endswith("pbiviz.json")), None)
|
|
306
|
+
if not pbiviz_json_name:
|
|
307
|
+
console.print("[red]Invalid .pbiviz file — pbiviz.json not found inside archive.[/red]")
|
|
308
|
+
raise SystemExit(1)
|
|
309
|
+
meta = json.loads(z.read(pbiviz_json_name).decode("utf-8"))
|
|
310
|
+
|
|
311
|
+
visual_name = meta.get("visual", {}).get("name", pbiviz_path.stem)
|
|
312
|
+
visual_guid = meta.get("visual", {}).get("guid", "unknown")
|
|
313
|
+
version = meta.get("visual", {}).get("version", "1.0.0")
|
|
314
|
+
|
|
315
|
+
# Copy pbiviz into report's custom visuals folder
|
|
316
|
+
report_path = Path(pbip)
|
|
317
|
+
cv_dir = report_path / "definition" / "customVisuals"
|
|
318
|
+
cv_dir.mkdir(parents=True, exist_ok=True)
|
|
319
|
+
dest = cv_dir / pbiviz_path.name
|
|
320
|
+
shutil.copy(pbiviz_path, dest)
|
|
321
|
+
|
|
322
|
+
console.print(f"[green]Custom visual imported:[/green] {visual_name} v{version}")
|
|
323
|
+
console.print(f" GUID: {visual_guid}")
|
|
324
|
+
console.print(f" Stored: {dest}")
|
|
325
|
+
console.print("[dim]Reload the report in Power BI Desktop to use the visual.[/dim]")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""pbi database — TMDL export/import commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from pbi_cli.commands._shared import dry_run_echo, get_backend
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
def database() -> None:
|
|
15
|
+
"""Export and import TMDL snapshots."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@database.command("export-tmdl")
|
|
19
|
+
@click.argument("path")
|
|
20
|
+
@click.pass_context
|
|
21
|
+
def export_tmdl(ctx: click.Context, path: str) -> None:
|
|
22
|
+
"""Export the current model as TMDL files to a directory."""
|
|
23
|
+
if dry_run_echo(ctx, f"export TMDL to {path}"):
|
|
24
|
+
return
|
|
25
|
+
backend = get_backend(ctx)
|
|
26
|
+
backend.tmdl_export(path)
|
|
27
|
+
console.print(f"[green]TMDL exported to:[/green] {path}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@database.command("import-tmdl")
|
|
31
|
+
@click.argument("path")
|
|
32
|
+
@click.pass_context
|
|
33
|
+
def import_tmdl(ctx: click.Context, path: str) -> None:
|
|
34
|
+
"""Import TMDL files from a directory into the connected model."""
|
|
35
|
+
if dry_run_echo(ctx, f"import TMDL from {path}"):
|
|
36
|
+
return
|
|
37
|
+
backend = get_backend(ctx)
|
|
38
|
+
backend.tmdl_import(path)
|
|
39
|
+
console.print(f"[green]TMDL imported from:[/green] {path}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@database.command("diff-tmdl")
|
|
43
|
+
@click.argument("snapshot_path", type=click.Path(exists=True))
|
|
44
|
+
@click.option("--output", default=None, type=click.Path(), help="Save diff report to a JSON file.")
|
|
45
|
+
@click.pass_context
|
|
46
|
+
def diff_tmdl(ctx: click.Context, snapshot_path: str, output: str | None) -> None:
|
|
47
|
+
"""Compare the live model against a TMDL snapshot directory.
|
|
48
|
+
|
|
49
|
+
Reports added, removed, and changed objects (tables, measures, columns,
|
|
50
|
+
relationships) relative to the snapshot.
|
|
51
|
+
|
|
52
|
+
\b
|
|
53
|
+
Example:
|
|
54
|
+
pbi database diff-tmdl ./snapshots/2024-01-01/
|
|
55
|
+
"""
|
|
56
|
+
import json as _json
|
|
57
|
+
from pathlib import Path
|
|
58
|
+
|
|
59
|
+
from pbi_cli.commands._shared import output_json_or_table
|
|
60
|
+
|
|
61
|
+
backend = get_backend(ctx)
|
|
62
|
+
diff = backend.model_diff(snapshot_path)
|
|
63
|
+
|
|
64
|
+
if output:
|
|
65
|
+
Path(output).write_text(_json.dumps(diff, indent=2, default=str), encoding="utf-8")
|
|
66
|
+
console.print(f"[green]Diff saved to:[/green] {output}")
|
|
67
|
+
else:
|
|
68
|
+
if not diff.get("has_changes"):
|
|
69
|
+
console.print("[green]No changes detected[/green] — model matches snapshot.")
|
|
70
|
+
else:
|
|
71
|
+
console.print(
|
|
72
|
+
f"[yellow]Changes detected:[/yellow] {len(diff.get('added', []))} added, "
|
|
73
|
+
f"{len(diff.get('removed', []))} removed, "
|
|
74
|
+
f"{len(diff.get('changed', []))} modified"
|
|
75
|
+
)
|
|
76
|
+
output_json_or_table(diff, ctx, title="TMDL Diff")
|