285 lines
7.4 KiB
Python
285 lines
7.4 KiB
Python
#!/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
|