Initial commit
This commit is contained in:
80
skills/fabric-cli/scripts/README.md
Normal file
80
skills/fabric-cli/scripts/README.md
Normal 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`
|
||||
240
skills/fabric-cli/scripts/create_direct_lake_model.py
Normal file
240
skills/fabric-cli/scripts/create_direct_lake_model.py
Normal 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()
|
||||
436
skills/fabric-cli/scripts/download_workspace.py
Executable file
436
skills/fabric-cli/scripts/download_workspace.py
Executable 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
|
||||
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
|
||||
399
skills/fabric-cli/scripts/export_semantic_model_as_pbip.py
Executable file
399
skills/fabric-cli/scripts/export_semantic_model_as_pbip.py
Executable 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
|
||||
Reference in New Issue
Block a user