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,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