Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:18:03 +08:00
commit 31ff8e1c29
18 changed files with 5925 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
# Fabric CLI Utility Scripts
Python scripts extending `fab` CLI with common operations. All scripts use the same path syntax as fab commands.
## Path Syntax
All scripts use Fabric path format: `Workspace.Workspace/Item.ItemType`
```bash
# Examples
"Sales.Workspace/Model.SemanticModel"
"Production.Workspace/LH.Lakehouse"
"Dev.Workspace/Report.Report"
```
## Scripts
### create_direct_lake_model.py
Create a Direct Lake semantic model from lakehouse tables. This is the recommended approach for querying lakehouse data via DAX.
```bash
python3 create_direct_lake_model.py "src.Workspace/LH.Lakehouse" "dest.Workspace/Model.SemanticModel" -t schema.table
python3 create_direct_lake_model.py "Sales.Workspace/SalesLH.Lakehouse" "Sales.Workspace/Sales Model.SemanticModel" -t gold.orders
```
Arguments:
- `source` - Source lakehouse: Workspace.Workspace/Lakehouse.Lakehouse
- `dest` - Destination model: Workspace.Workspace/Model.SemanticModel
- `-t, --table` - Table in schema.table format (required)
### execute_dax.py
Execute DAX queries against semantic models.
```bash
python3 execute_dax.py "ws.Workspace/Model.SemanticModel" -q "EVALUATE VALUES('Date'[Year])"
python3 execute_dax.py "Sales.Workspace/Sales Model.SemanticModel" -q "EVALUATE TOPN(10, 'Orders')" --format csv
python3 execute_dax.py "ws.Workspace/Model.SemanticModel" -q "EVALUATE ROW(\"Total\", SUM('Sales'[Amount]))" -o results.json
```
Options:
- `-q, --query` - DAX query (required)
- `-o, --output` - Output file
- `--format` - Output format: table (default), csv, json
- `--include-nulls` - Include null values
### export_semantic_model_as_pbip.py
Export semantic model as PBIP (Power BI Project) format.
```bash
python3 export_semantic_model_as_pbip.py "ws.Workspace/Model.SemanticModel" -o ./output
python3 export_semantic_model_as_pbip.py "Sales.Workspace/Sales Model.SemanticModel" -o /tmp/exports
```
Creates complete PBIP structure with TMDL definition and blank report.
### download_workspace.py
Download complete workspace with all items and lakehouse files.
```bash
python3 download_workspace.py "Sales.Workspace"
python3 download_workspace.py "Production.Workspace" ./backup
python3 download_workspace.py "Dev.Workspace" --no-lakehouse-files
```
Options:
- `output_dir` - Output directory (default: ./workspace_downloads/<name>)
- `--no-lakehouse-files` - Skip lakehouse file downloads
## Requirements
- Python 3.10+
- `fab` CLI installed and authenticated
- For lakehouse file downloads: `azure-storage-file-datalake`, `azure-identity`

View File

@@ -0,0 +1,240 @@
#!/usr/bin/env python3
"""
Create a Direct Lake semantic model from lakehouse tables.
Usage:
python3 create_direct_lake_model.py "src.Workspace/LH.Lakehouse" "dest.Workspace/Model.SemanticModel" -t schema.table
Requirements:
- fab CLI installed and authenticated
"""
import argparse
import json
import subprocess
import sys
import uuid
import tempfile
from pathlib import Path
def run_fab(args: list[str]) -> str:
"""Run fab command and return output."""
result = subprocess.run(["fab"] + args, capture_output=True, text=True)
if result.returncode != 0:
print(f"fab error: {result.stderr}", file=sys.stderr)
return result.stdout.strip()
def get_lakehouse_sql_endpoint(workspace: str, lakehouse: str) -> dict:
"""Get lakehouse SQL endpoint info."""
path = f"{workspace}/{lakehouse}"
output = run_fab(["get", path, "-q", "properties.sqlEndpointProperties"])
return json.loads(output)
def get_table_schema(workspace: str, lakehouse: str, schema: str, table: str) -> list:
"""Get table schema from lakehouse (parses text output)."""
path = f"{workspace}/{lakehouse}/Tables/{schema}/{table}"
output = run_fab(["table", "schema", path])
# Parse text table format:
# name type
# ------------------------------------------
# col_name col_type
columns = []
in_data = False
for line in output.split("\n"):
line = line.strip()
if line.startswith("---"):
in_data = True
continue
if in_data and line:
parts = line.split()
if len(parts) >= 2:
columns.append({"name": parts[0], "type": parts[1]})
return columns
def tmdl_data_type(sql_type: str) -> str:
"""Convert SQL type to TMDL data type."""
sql_type = sql_type.lower()
if 'int' in sql_type:
return 'int64'
elif 'float' in sql_type or 'double' in sql_type or 'decimal' in sql_type:
return 'double'
elif 'bool' in sql_type or 'bit' in sql_type:
return 'boolean'
elif 'date' in sql_type or 'time' in sql_type:
return 'dateTime'
else:
return 'string'
def create_model_tmdl(model_name: str, table_name: str) -> str:
"""Create model.tmdl content."""
return f"""model '{model_name}'
\tculture: en-US
\tdefaultPowerBIDataSourceVersion: powerBI_V3
ref table '{table_name}'
"""
def create_expressions_tmdl(connection_string: str, endpoint_id: str) -> str:
"""Create expressions.tmdl content."""
return f"""expression DatabaseQuery =
\t\tlet
\t\t\tdatabase = Sql.Database("{connection_string}", "{endpoint_id}")
\t\tin
\t\t\tdatabase
\tlineageTag: {uuid.uuid4()}
"""
def create_table_tmdl(table_name: str, schema_name: str, columns: list) -> str:
"""Create table.tmdl content."""
lines = [
f"table '{table_name}'",
f"\tlineageTag: {uuid.uuid4()}",
f"\tsourceLineageTag: [{schema_name}].[{table_name}]",
""
]
# Add columns
for col in columns:
col_name = col['name']
data_type = tmdl_data_type(col['type'])
lines.extend([
f"\tcolumn '{col_name}'",
f"\t\tdataType: {data_type}",
f"\t\tlineageTag: {uuid.uuid4()}",
f"\t\tsourceLineageTag: {col_name}",
f"\t\tsummarizeBy: none",
f"\t\tsourceColumn: {col_name}",
"",
f"\t\tannotation SummarizationSetBy = Automatic",
""
])
# Add partition
lines.extend([
f"\tpartition '{table_name}' = entity",
f"\t\tmode: directLake",
f"\t\tsource",
f"\t\t\tentityName: {table_name}",
f"\t\t\tschemaName: {schema_name}",
f"\t\t\texpressionSource: DatabaseQuery",
""
])
return "\n".join(lines)
def create_database_tmdl() -> str:
"""Create database.tmdl content."""
return f"""database '{uuid.uuid4()}'
"""
def create_pbism() -> str:
"""Create definition.pbism content."""
return json.dumps({
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/semanticModel/definitionProperties/1.0.0/schema.json",
"version": "4.0",
"settings": {}
}, indent=2)
def create_platform(model_name: str) -> str:
"""Create .platform content."""
return json.dumps({
"$schema": "https://developer.microsoft.com/json-schemas/fabric/gitIntegration/platformProperties/2.0.0/schema.json",
"metadata": {
"type": "SemanticModel",
"displayName": model_name
},
"config": {
"version": "2.0",
"logicalId": str(uuid.uuid4())
}
}, indent=2)
def main():
parser = argparse.ArgumentParser(description="Create Direct Lake semantic model")
parser.add_argument("source", help="Source: Workspace.Workspace/Lakehouse.Lakehouse")
parser.add_argument("dest", help="Destination: Workspace.Workspace/Model.SemanticModel")
parser.add_argument("-t", "--table", required=True, help="Table: schema.table_name")
args = parser.parse_args()
# Parse source
src_parts = args.source.split("/")
src_workspace = src_parts[0]
src_lakehouse = src_parts[1].replace(".Lakehouse", "") + ".Lakehouse"
# Parse destination
dest_parts = args.dest.split("/")
dest_workspace = dest_parts[0]
model_name = dest_parts[1].replace(".SemanticModel", "")
# Parse table
table_parts = args.table.split(".")
schema_name = table_parts[0]
table_name = table_parts[1]
print(f"Source: {src_workspace}/{src_lakehouse}")
print(f"Table: {schema_name}.{table_name}")
print(f"Dest: {dest_workspace}/{model_name}.SemanticModel")
# Get SQL endpoint
print("\nGetting SQL endpoint...")
endpoint = get_lakehouse_sql_endpoint(src_workspace, src_lakehouse)
print(f" Connection: {endpoint['connectionString']}")
print(f" ID: {endpoint['id']}")
# Get table schema
print(f"\nGetting table schema for {schema_name}.{table_name}...")
columns = get_table_schema(src_workspace, src_lakehouse, schema_name, table_name)
print(f" Found {len(columns)} columns")
# Create temp directory with TMDL
with tempfile.TemporaryDirectory() as tmpdir:
model_dir = Path(tmpdir) / f"{model_name}.SemanticModel"
def_dir = model_dir / "definition"
tables_dir = def_dir / "tables"
model_dir.mkdir()
def_dir.mkdir()
tables_dir.mkdir()
# Write files
print("\nCreating TMDL files...")
(model_dir / ".platform").write_text(create_platform(model_name))
(model_dir / "definition.pbism").write_text(create_pbism())
(def_dir / "model.tmdl").write_text(create_model_tmdl(model_name, table_name))
(def_dir / "database.tmdl").write_text(create_database_tmdl())
(def_dir / "expressions.tmdl").write_text(
create_expressions_tmdl(endpoint['connectionString'], endpoint['id'])
)
(tables_dir / f"{table_name}.tmdl").write_text(
create_table_tmdl(table_name, schema_name, columns)
)
print(f" Created: {model_dir}")
for f in model_dir.rglob("*"):
if f.is_file():
print(f" {f.relative_to(model_dir)}")
# Import to Fabric
print(f"\nImporting to {dest_workspace}...")
dest_path = f"{dest_workspace}/{model_name}.SemanticModel"
result = run_fab(["import", dest_path, "-i", str(model_dir), "-f"])
print(result)
print("\nDone!")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,436 @@
#!/usr/bin/env python3
"""
Download complete Fabric workspace including items and lakehouse files.
Uses the same path syntax as fab CLI commands.
Usage:
python3 download_workspace.py "Workspace.Workspace" [output_dir]
python3 download_workspace.py "Sales.Workspace" ./backup
python3 download_workspace.py "Production.Workspace" --no-lakehouse-files
Requirements:
- fab CLI installed and authenticated
- azure-storage-file-datalake (for lakehouse files)
- azure-identity
"""
import subprocess
import json
import sys
import argparse
from pathlib import Path
from collections import defaultdict
try:
from azure.storage.filedatalake import DataLakeServiceClient
from azure.identity import DefaultAzureCredential
AZURE_AVAILABLE = True
except ImportError:
AZURE_AVAILABLE = False
#region Helper Functions
def run_fab_command(args: list) -> str:
"""
Execute fab CLI command and return output.
Args:
args: Command arguments as list
Returns:
Command stdout
Raises:
subprocess.CalledProcessError if command fails
"""
result = subprocess.run(
["fab"] + args,
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
def parse_workspace_path(path: str) -> str:
"""
Parse and normalize workspace path.
Args:
path: Workspace path (with or without .Workspace)
Returns:
Normalized path with .Workspace extension
"""
if ".Workspace" not in path:
return f"{path}.Workspace"
return path
def get_workspace_items(workspace_path: str) -> list:
"""
Get all items in workspace.
Args:
workspace_path: Full workspace path (e.g., "Sales.Workspace")
Returns:
List of items with metadata
"""
output = run_fab_command(["ls", workspace_path, "-l"])
lines = output.strip().split('\n')
if len(lines) < 2:
return []
items = []
for line in lines[2:]:
parts = line.split()
if len(parts) >= 2:
item_id = parts[-1]
display_name = ' '.join(parts[:-1])
if '.' in display_name:
name, item_type = display_name.rsplit('.', 1)
else:
name = display_name
item_type = "Unknown"
items.append({
"displayName": name,
"type": item_type,
"id": item_id
})
return items
def export_item(workspace_path: str, item_name: str, item_type: str, output_path: Path) -> bool:
"""
Export item using fab export.
Args:
workspace_path: Workspace path
item_name: Item display name
item_type: Item type
output_path: Output directory
Returns:
True if successful
"""
item_path = f"{workspace_path}/{item_name}.{item_type}"
try:
subprocess.run(
["fab", "export", item_path, "-o", str(output_path), "-f"],
capture_output=True,
text=True,
check=True,
timeout=300
)
return True
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
print(f" Failed to export {item_name}: {e}")
return False
#endregion
#region Lakehouse Operations
def download_lakehouse_files(workspace_id: str, lakehouse_id: str, lakehouse_name: str, output_dir: Path):
"""
Download all files from lakehouse using OneLake Storage API.
Args:
workspace_id: Workspace GUID
lakehouse_id: Lakehouse GUID
lakehouse_name: Lakehouse display name
output_dir: Output directory for files
"""
if not AZURE_AVAILABLE:
print(f" Skipping lakehouse files (azure-storage-file-datalake not installed)")
return
print(f"\n Downloading lakehouse files from {lakehouse_name}...")
try:
account_url = "https://onelake.dfs.fabric.microsoft.com"
credential = DefaultAzureCredential()
service_client = DataLakeServiceClient(account_url=account_url, credential=credential)
fs_client = service_client.get_file_system_client(file_system=workspace_id)
base_path = f"{lakehouse_id}/Files"
try:
paths = fs_client.get_paths(path=base_path, recursive=True)
file_count = 0
dir_count = 0
for path in paths:
relative_path = path.name[len(base_path)+1:] if len(path.name) > len(base_path) else path.name
if path.is_directory:
local_dir = output_dir / relative_path
local_dir.mkdir(parents=True, exist_ok=True)
dir_count += 1
else:
local_file = output_dir / relative_path
local_file.parent.mkdir(parents=True, exist_ok=True)
file_client = fs_client.get_file_client(path.name)
with open(local_file, 'wb') as f:
download = file_client.download_file()
f.write(download.readall())
file_count += 1
print(f" {relative_path}")
print(f" Downloaded {file_count} files, {dir_count} directories")
except Exception as e:
if "404" in str(e) or "PathNotFound" in str(e):
print(f" No files found in lakehouse")
else:
raise
except Exception as e:
print(f" Error downloading lakehouse files: {e}")
def list_lakehouse_tables(workspace_path: str, lakehouse_name: str) -> list:
"""
List tables in lakehouse.
Args:
workspace_path: Workspace path
lakehouse_name: Lakehouse name
Returns:
List of table names
"""
try:
tables_path = f"{workspace_path}/{lakehouse_name}.Lakehouse/Tables"
output = run_fab_command(["ls", tables_path])
lines = output.strip().split('\n')
tables = [line.strip() for line in lines if line.strip() and not line.startswith('---')]
return tables
except subprocess.CalledProcessError:
return []
def export_table_schema(workspace_path: str, lakehouse_name: str, table_name: str, output_file: Path) -> bool:
"""
Export table schema.
Args:
workspace_path: Workspace path
lakehouse_name: Lakehouse name
table_name: Table name
output_file: Output JSON file
Returns:
True if successful
"""
try:
table_path = f"{workspace_path}/{lakehouse_name}.Lakehouse/Tables/{table_name}"
schema_output = run_fab_command(["table", "schema", table_path])
with open(output_file, 'w') as f:
f.write(schema_output)
return True
except subprocess.CalledProcessError as e:
print(f" Failed to export schema for {table_name}: {e}")
return False
#endregion
#region Main Download
def download_workspace(workspace_path: str, output_dir: Path, download_lakehouse_files_flag: bool = True):
"""
Download complete workspace contents.
Args:
workspace_path: Workspace path (e.g., "Sales.Workspace")
output_dir: Output directory
download_lakehouse_files_flag: Whether to download lakehouse files
"""
print(f"Downloading workspace: {workspace_path}")
print(f"Output directory: {output_dir}")
print()
output_dir.mkdir(parents=True, exist_ok=True)
# Get workspace ID
print("Getting workspace ID...")
workspace_id = run_fab_command(["get", workspace_path, "-q", "id"])
print(f"Workspace ID: {workspace_id}")
print()
# Get all items
print("Discovering workspace items...")
items = get_workspace_items(workspace_path)
if not items:
print("No items found")
return
# Group by type
items_by_type = defaultdict(list)
for item in items:
items_by_type[item["type"]].append(item)
print(f"Found {len(items)} items across {len(items_by_type)} types:")
for item_type, type_items in sorted(items_by_type.items()):
print(f" {item_type}: {len(type_items)}")
print()
# Track statistics
total_success = 0
total_failed = 0
lakehouses = []
# Download items by type
for item_type, type_items in sorted(items_by_type.items()):
print(f"Downloading {item_type} items ({len(type_items)})...")
type_dir = output_dir / item_type
type_dir.mkdir(parents=True, exist_ok=True)
for item in type_items:
item_name = item["displayName"]
item_id = item["id"]
print(f" {item_name}...")
if item_type == "Lakehouse" and download_lakehouse_files_flag:
lakehouses.append({
"name": item_name,
"id": item_id,
"output_dir": type_dir / f"{item_name}.Lakehouse"
})
success = export_item(workspace_path, item_name, item_type, type_dir)
if success:
total_success += 1
print(f" Done: {item_name}")
else:
total_failed += 1
print()
# Download lakehouse files
if lakehouses and download_lakehouse_files_flag:
print(f"Downloading lakehouse files ({len(lakehouses)} lakehouses)...")
for lh in lakehouses:
lh_files_dir = lh["output_dir"] / "Files"
lh_files_dir.mkdir(parents=True, exist_ok=True)
download_lakehouse_files(
workspace_id=workspace_id,
lakehouse_id=lh["id"],
lakehouse_name=lh["name"],
output_dir=lh_files_dir
)
# Export table schemas
print(f"\n Exporting table schemas from {lh['name']}...")
tables = list_lakehouse_tables(workspace_path, lh["name"])
if tables:
tables_dir = lh["output_dir"] / "Tables"
tables_dir.mkdir(parents=True, exist_ok=True)
for table in tables:
schema_file = tables_dir / f"{table}_schema.json"
if export_table_schema(workspace_path, lh["name"], table, schema_file):
print(f" {table}")
print(f" Exported {len(tables)} table schemas")
else:
print(f" No tables found")
print()
# Summary
print("=" * 60)
print("Download Summary")
print("=" * 60)
print(f"Successfully downloaded: {total_success}")
print(f"Failed: {total_failed}")
print(f"Output directory: {output_dir.absolute()}")
#endregion
#region Main
def main():
parser = argparse.ArgumentParser(
description="Download complete Fabric workspace",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python3 download_workspace.py "Sales.Workspace"
python3 download_workspace.py "Production.Workspace" ./backup
python3 download_workspace.py "dev.Workspace" --no-lakehouse-files
"""
)
parser.add_argument("workspace", help="Workspace path: Name.Workspace or just Name")
parser.add_argument("output_dir", nargs="?", default=None,
help="Output directory (default: ./workspace_downloads/<name>)")
parser.add_argument("--no-lakehouse-files", action="store_true",
help="Skip downloading lakehouse files")
args = parser.parse_args()
workspace_path = parse_workspace_path(args.workspace)
# Extract name for default output dir
workspace_name = workspace_path.replace(".Workspace", "")
if args.output_dir:
output_dir = Path(args.output_dir)
else:
output_dir = Path("./workspace_downloads") / workspace_name
try:
download_workspace(
workspace_path=workspace_path,
output_dir=output_dir,
download_lakehouse_files_flag=not args.no_lakehouse_files
)
except KeyboardInterrupt:
print("\n\nDownload interrupted by user")
sys.exit(1)
except Exception as e:
print(f"\nError: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()
#endregion

View File

@@ -0,0 +1,284 @@
#!/usr/bin/env python3
"""
Execute DAX queries against Fabric semantic models.
Uses the same path syntax as fab CLI commands.
Usage:
python3 execute_dax.py "Workspace.Workspace/Model.SemanticModel" -q "EVALUATE VALUES(Date[Year])"
python3 execute_dax.py "Sales.Workspace/Sales Model.SemanticModel" -q "EVALUATE TOPN(5, 'Orders')"
Requirements:
- fab CLI installed and authenticated
"""
import argparse
import json
import subprocess
import sys
import re
#region Helper Functions
def run_fab_command(args: list[str]) -> str:
"""
Run fab CLI command and return output.
Args:
args: List of command arguments
Returns:
Command stdout as string
Raises:
SystemExit if command fails or fab not found
"""
try:
result = subprocess.run(
["fab"] + args,
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"Error running fab command: {e.stderr}", file=sys.stderr)
sys.exit(1)
except FileNotFoundError:
print("Error: fab CLI not found. Install from: https://microsoft.github.io/fabric-cli/", file=sys.stderr)
sys.exit(1)
def parse_path(path: str) -> tuple[str, str]:
"""
Parse Fabric path into workspace and item components.
Args:
path: Full path like "Workspace.Workspace/Model.SemanticModel"
Returns:
Tuple of (workspace_path, item_path)
Raises:
ValueError if path format is invalid
"""
if "/" not in path:
raise ValueError(f"Invalid path format: {path}. Expected: Workspace.Workspace/Item.Type")
parts = path.split("/", 1)
workspace = parts[0]
item = parts[1]
if ".Workspace" not in workspace:
workspace = f"{workspace}.Workspace"
if ".SemanticModel" not in item:
item = f"{item}.SemanticModel"
return workspace, item
def get_id(path: str) -> str:
"""
Get ID for a Fabric path.
Args:
path: Fabric path
Returns:
Item ID as string
"""
output = run_fab_command(["get", path, "-q", "id"])
return output.strip('"')
#endregion
#region DAX Execution
def execute_dax_query(workspace_id: str, dataset_id: str, query: str, include_nulls: bool = False) -> dict:
"""
Execute DAX query against semantic model using Fabric CLI.
Uses Power BI API via fab CLI: fab api -A powerbi
Args:
workspace_id: Workspace GUID
dataset_id: Semantic model GUID
query: DAX query string
include_nulls: Whether to include null values in results
Returns:
Query results as dict
"""
payload = {
"queries": [{"query": query}],
"serializerSettings": {"includeNulls": include_nulls}
}
endpoint = f"groups/{workspace_id}/datasets/{dataset_id}/executeQueries"
output = run_fab_command([
"api",
"-A", "powerbi",
"-X", "post",
endpoint,
"-i", json.dumps(payload)
])
return json.loads(output)
#endregion
#region Output Formatting
def format_results_as_table(results: dict) -> str:
"""Format query results as ASCII table."""
output_lines = []
if "text" in results:
data = results["text"]
else:
data = results
for result_set in data.get("results", []):
tables = result_set.get("tables", [])
for table in tables:
rows = table.get("rows", [])
if not rows:
output_lines.append("(No rows returned)")
continue
columns = list(rows[0].keys())
widths = {col: len(col) for col in columns}
for row in rows:
for col in columns:
value_str = str(row.get(col, ""))
widths[col] = max(widths[col], len(value_str))
header = " | ".join(col.ljust(widths[col]) for col in columns)
output_lines.append(header)
output_lines.append("-" * len(header))
for row in rows:
row_str = " | ".join(str(row.get(col, "")).ljust(widths[col]) for col in columns)
output_lines.append(row_str)
output_lines.append("")
output_lines.append(f"({len(rows)} row(s) returned)")
return "\n".join(output_lines)
def format_results_as_csv(results: dict) -> str:
"""Format query results as CSV."""
import csv
import io
output = io.StringIO()
if "text" in results:
data = results["text"]
else:
data = results
for result_set in data.get("results", []):
tables = result_set.get("tables", [])
for table in tables:
rows = table.get("rows", [])
if rows:
columns = list(rows[0].keys())
writer = csv.DictWriter(output, fieldnames=columns)
writer.writeheader()
writer.writerows(rows)
return output.getvalue()
#endregion
#region Main
def main():
parser = argparse.ArgumentParser(
description="Execute DAX query against Fabric semantic model",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python3 execute_dax.py "Sales.Workspace/Sales Model.SemanticModel" -q "EVALUATE VALUES('Date'[Year])"
python3 execute_dax.py "Production.Workspace/Sales.SemanticModel" -q "EVALUATE TOPN(10, 'Sales')" --format csv
python3 execute_dax.py "ws.Workspace/Model.SemanticModel" -q "EVALUATE ROW(\\"Total\\", SUM('Sales'[Amount]))"
DAX Requirements:
- EVALUATE is mandatory - all queries must start with EVALUATE
- Use single quotes for table names: 'Sales', 'Date'
- Qualify columns: 'Sales'[Amount], not just [Amount]
"""
)
parser.add_argument("path", help="Model path: Workspace.Workspace/Model.SemanticModel")
parser.add_argument("-q", "--query", required=True, help="DAX query to execute")
parser.add_argument("-o", "--output", help="Output file path")
parser.add_argument("--format", choices=["json", "csv", "table"], default="table",
help="Output format (default: table)")
parser.add_argument("--include-nulls", action="store_true",
help="Include null values in results")
args = parser.parse_args()
# Parse path
try:
workspace, model = parse_path(args.path)
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
# Get IDs
print(f"Resolving: {workspace}...", file=sys.stderr)
workspace_id = get_id(workspace)
full_path = f"{workspace}/{model}"
print(f"Resolving: {full_path}...", file=sys.stderr)
model_id = get_id(full_path)
# Execute query
print(f"Executing DAX query...", file=sys.stderr)
results = execute_dax_query(workspace_id, model_id, args.query, args.include_nulls)
# Format results
if args.format == "json":
formatted_output = json.dumps(results, indent=2)
elif args.format == "csv":
formatted_output = format_results_as_csv(results)
else:
formatted_output = format_results_as_table(results)
# Output results
if args.output:
with open(args.output, 'w') as f:
f.write(formatted_output)
print(f"\nResults saved to: {args.output}", file=sys.stderr)
else:
print(formatted_output)
if __name__ == "__main__":
main()
#endregion

View File

@@ -0,0 +1,399 @@
#!/usr/bin/env python3
"""
Export Fabric semantic model as PBIP (Power BI Project) format.
Uses the same path syntax as fab CLI commands.
Usage:
python3 export_semantic_model_as_pbip.py "Workspace.Workspace/Model.SemanticModel" -o ./output
python3 export_semantic_model_as_pbip.py "Sales.Workspace/Sales Model.SemanticModel" -o /tmp/exports
Requirements:
- fab CLI installed and authenticated
"""
import argparse
import base64
import json
import re
import subprocess
import sys
import uuid
from pathlib import Path
#region Helper Functions
def run_fab_command(args: list[str]) -> str:
"""
Run fab CLI command and return output.
Args:
args: List of command arguments
Returns:
Command stdout as string
Raises:
SystemExit if command fails or fab not found
"""
try:
result = subprocess.run(
["fab"] + args,
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"Error running fab command: {e.stderr}", file=sys.stderr)
sys.exit(1)
except FileNotFoundError:
print("Error: fab CLI not found. Install from: https://microsoft.github.io/fabric-cli/", file=sys.stderr)
sys.exit(1)
def parse_path(path: str) -> tuple[str, str, str]:
"""
Parse Fabric path into workspace, item, and display name.
Args:
path: Full path like "Workspace.Workspace/Model.SemanticModel"
Returns:
Tuple of (workspace_path, item_path, display_name)
Raises:
ValueError if path format is invalid
"""
if "/" not in path:
raise ValueError(f"Invalid path format: {path}. Expected: Workspace.Workspace/Item.Type")
parts = path.split("/", 1)
workspace = parts[0]
item = parts[1]
if ".Workspace" not in workspace:
workspace = f"{workspace}.Workspace"
# Extract display name before adding extension
display_name = re.sub(r'\.SemanticModel$', '', item, flags=re.IGNORECASE)
if ".SemanticModel" not in item:
item = f"{item}.SemanticModel"
return workspace, item, display_name
def sanitize_name(name: str) -> str:
"""
Sanitize name for filesystem usage.
Args:
name: Display name
Returns:
Filesystem-safe name
"""
name = re.sub(r'\.SemanticModel$', '', name, flags=re.IGNORECASE)
safe_name = re.sub(r'[<>:"/\\|?*]', '_', name)
safe_name = re.sub(r'\s+', ' ', safe_name)
return safe_name.strip()
#endregion
#region Model Definition
def get_model_definition(full_path: str) -> dict:
"""
Get model definition from Fabric.
Args:
full_path: Full path like "Workspace.Workspace/Model.SemanticModel"
Returns:
Definition dict
"""
print(f"Fetching model definition...")
output = run_fab_command(["get", full_path, "-q", "definition"])
try:
return json.loads(output)
except json.JSONDecodeError:
print("Error: Failed to parse model definition JSON", file=sys.stderr)
sys.exit(1)
def parse_tmdl_definition(definition: dict) -> dict[str, str]:
"""
Parse TMDL definition parts from base64-encoded payload.
Args:
definition: Definition dict with parts array
Returns:
Dict mapping path to decoded content
"""
parts = {}
for part in definition.get("parts", []):
path = part.get("path", "")
payload = part.get("payload", "")
try:
content = base64.b64decode(payload).decode("utf-8")
parts[path] = content
except Exception as e:
print(f"Warning: Failed to decode part {path}: {e}", file=sys.stderr)
return parts
#endregion
#region PBIP Structure Creation
def create_pbip_structure(definition: dict, output_path: Path, model_name: str):
"""
Create PBIP folder structure with model definition.
Args:
definition: Model definition dict
output_path: Output directory
model_name: Model display name
"""
safe_name = sanitize_name(model_name)
container_path = output_path / safe_name
container_path.mkdir(parents=True, exist_ok=True)
print(f"Creating PBIP structure in: {container_path}")
# Create .pbip metadata file
pbip_metadata = {
"$schema": "https://developer.microsoft.com/json-schemas/fabric/pbip/pbipProperties/1.0.0/schema.json",
"version": "1.0",
"artifacts": [
{
"report": {
"path": f"{safe_name}.Report"
}
}
],
"settings": {
"enableAutoRecovery": True
}
}
pbip_file = container_path / f"{safe_name}.pbip"
with open(pbip_file, "w", encoding="utf-8") as f:
json_str = json.dumps(pbip_metadata, indent=2)
json_str = json_str.replace(': True', ': true').replace(': False', ': false')
f.write(json_str)
create_report_folder(container_path, safe_name)
create_model_folder(container_path, safe_name, definition)
print(f"PBIP created: {container_path}")
print(f"Open in Power BI Desktop: {pbip_file}")
def create_report_folder(container_path: Path, safe_name: str):
"""Create minimal Report folder structure."""
report_folder = container_path / f"{safe_name}.Report"
report_folder.mkdir(parents=True, exist_ok=True)
# .platform file
platform_content = {
"$schema": "https://developer.microsoft.com/json-schemas/fabric/gitIntegration/platformProperties/2.0.0/schema.json",
"metadata": {
"type": "Report",
"displayName": safe_name
},
"config": {
"version": "2.0",
"logicalId": str(uuid.uuid4())
}
}
with open(report_folder / '.platform', 'w', encoding='utf-8') as f:
json.dump(platform_content, f, indent=2)
# definition.pbir
pbir_content = {
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definitionProperties/1.0.0/schema.json",
"version": "4.0",
"datasetReference": {
"byPath": {
"path": f"../{safe_name}.SemanticModel"
}
}
}
with open(report_folder / 'definition.pbir', 'w', encoding='utf-8') as f:
json.dump(pbir_content, f, indent=2)
# definition folder
definition_folder = report_folder / 'definition'
definition_folder.mkdir()
# report.json
report_json = {
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/report/2.1.0/schema.json",
"themeCollection": {
"baseTheme": {
"name": "CY24SU10",
"reportVersionAtImport": "5.59",
"type": "SharedResources"
}
},
"settings": {
"useStylableVisualContainerHeader": True,
"defaultDrillFilterOtherVisuals": True
}
}
with open(definition_folder / 'report.json', 'w', encoding='utf-8') as f:
json_str = json.dumps(report_json, indent=2)
json_str = json_str.replace(': True', ': true').replace(': False', ': false')
f.write(json_str)
# version.json
with open(definition_folder / 'version.json', 'w', encoding='utf-8') as f:
json.dump({
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/versionMetadata/1.0.0/schema.json",
"version": "2.0.0"
}, f, indent=2)
# blank page
pages_folder = definition_folder / 'pages'
pages_folder.mkdir()
page_id = str(uuid.uuid4()).replace('-', '')[:16]
page_folder = pages_folder / page_id
page_folder.mkdir()
with open(page_folder / 'page.json', 'w', encoding='utf-8') as f:
json.dump({
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/page/2.0.0/schema.json",
"name": page_id,
"displayName": "Page 1",
"width": 1920,
"height": 1080
}, f, indent=2)
(page_folder / 'visuals').mkdir()
with open(pages_folder / 'pages.json', 'w', encoding='utf-8') as f:
json.dump({
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/pagesMetadata/1.0.0/schema.json",
"pageOrder": [page_id],
"activePageName": page_id
}, f, indent=2)
def create_model_folder(container_path: Path, safe_name: str, definition: dict):
"""Create .SemanticModel folder with TMDL definition."""
model_folder = container_path / f"{safe_name}.SemanticModel"
model_folder.mkdir(parents=True, exist_ok=True)
# .platform file
with open(model_folder / '.platform', 'w', encoding='utf-8') as f:
json.dump({
"$schema": "https://developer.microsoft.com/json-schemas/fabric/gitIntegration/platformProperties/2.0.0/schema.json",
"metadata": {
"type": "SemanticModel",
"displayName": safe_name
},
"config": {
"version": "2.0",
"logicalId": str(uuid.uuid4())
}
}, f, indent=2)
# definition.pbism
with open(model_folder / 'definition.pbism', 'w', encoding='utf-8') as f:
json.dump({
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/semanticModel/definitionProperties/1.0.0/schema.json",
"version": "4.0",
"settings": {}
}, f, indent=2)
# .pbi folder
pbi_folder = model_folder / ".pbi"
pbi_folder.mkdir(parents=True, exist_ok=True)
with open(pbi_folder / "editorSettings.json", "w", encoding="utf-8") as f:
json_str = json.dumps({
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/semanticModel/editorSettings/1.0.0/schema.json",
"autodetectRelationships": True,
"parallelQueryLoading": True
}, indent=2)
json_str = json_str.replace(': True', ': true').replace(': False', ': false')
f.write(json_str)
# Write TMDL parts
tmdl_parts = parse_tmdl_definition(definition)
for part_path, content in tmdl_parts.items():
if part_path == '.platform':
continue
file_path = model_folder / part_path
file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
print(f" Wrote {len(tmdl_parts)} TMDL parts")
#endregion
#region Main
def main():
parser = argparse.ArgumentParser(
description="Export Fabric semantic model as PBIP format",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python3 export_semantic_model_as_pbip.py "Production.Workspace/Sales.SemanticModel" -o /tmp/exports
python3 export_semantic_model_as_pbip.py "Sales.Workspace/Sales Model.SemanticModel" -o ./models
"""
)
parser.add_argument("path", help="Model path: Workspace.Workspace/Model.SemanticModel")
parser.add_argument("-o", "--output", required=True, help="Output directory")
args = parser.parse_args()
# Parse path
try:
workspace, item, display_name = parse_path(args.path)
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
full_path = f"{workspace}/{item}"
output_path = Path(args.output)
# Get and export definition
definition = get_model_definition(full_path)
create_pbip_structure(definition, output_path, display_name)
if __name__ == "__main__":
main()
#endregion