#!/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