Initial commit
This commit is contained in:
284
skills/fabric-cli/scripts/execute_dax.py
Normal file
284
skills/fabric-cli/scripts/execute_dax.py
Normal 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
|
||||
Reference in New Issue
Block a user