afv-library/skills/tableau-next-semantic-model-generate/scripts/discover_sdm.py

238 lines
8.3 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Discover Semantic Data Models and their fields via the Salesforce REST API.
Usage:
python scripts/discover_sdm.py --list
python scripts/discover_sdm.py --sdm Sales_Intelligence_Model
python scripts/discover_sdm.py --sdm Sales_Intelligence_Model --json
"""
import argparse
import json
import sys
from typing import Any, Dict, List
from _shared.sf_api import get_credentials, sdm_list_endpoint, sdm_detail_endpoint, sf_get
def list_sdms(token: str, instance: str, as_json: bool) -> None:
data = sf_get(token, instance, sdm_list_endpoint())
if data is None:
sys.exit(1)
models = data.get("semantic_models") or data.get("items") or []
if as_json:
print(json.dumps(models, indent=2))
return
if not models:
print("No Semantic Data Models found.")
return
print(f"{'API Name':<40} {'Label':<40} {'Dataspace'}")
print("-" * 100)
for m in models:
api = m.get("apiName", "")
label = m.get("label", "")
ds = m.get("dataspace", "")
print(f"{api:<40} {label:<40} {ds}")
def show_sdm(token: str, instance: str, sdm_name: str, as_json: bool) -> None:
data = sf_get(token, instance, sdm_detail_endpoint(sdm_name))
if data is None:
print(f"Error: SDM '{sdm_name}' not found or API error.", file=sys.stderr)
sys.exit(1)
if as_json:
print(json.dumps(_structured_output(data), indent=2))
return
print(f"SDM: {data.get('apiName', sdm_name)}")
print(f"Label: {data.get('label', '')}")
print()
for obj in data.get("semanticDataObjects", []):
obj_name = obj.get("apiName", "")
print(f"Object: {obj_name}")
dims = obj.get("semanticDimensions", [])
if dims:
print(" Dimensions:")
for d in dims:
api = d.get("apiName", "")
dtype = d.get("dataType", "")
print(f" {api:<35} ({dtype:<12}) objectName={obj_name}")
measures = obj.get("semanticMeasurements", [])
if measures:
print(" Measures:")
for m in measures:
api = m.get("apiName", "")
agg = m.get("aggregationType", "Sum")
print(f" {api:<35} ({agg:<12}) objectName={obj_name}")
print()
calc_dims = data.get("semanticCalculatedDimensions", [])
if calc_dims:
print("Calculated Dimensions:")
for d in calc_dims:
api = d.get("apiName", "")
dtype = d.get("dataType", "")
print(f" {api:<37} ({dtype:<12}) objectName=null")
print()
calc_measures = data.get("semanticCalculatedMeasurements", [])
if calc_measures:
print("Calculated Measures:")
for m in calc_measures:
api = m.get("apiName", "")
agg = m.get("aggregationType", "Sum")
note = " <-- NOT Sum!" if agg != "Sum" else ""
print(f" {api:<37} ({agg:<12}) objectName=null{note}")
print()
metrics = data.get("semanticMetrics", [])
if metrics:
print("Metrics:")
for m in metrics:
api = m.get("apiName", "")
label = m.get("label", "")
agg = m.get("aggregationType", "")
print(f" {api:<37} ({agg:<12}) label={label}")
print()
relationships = data.get("semanticRelationships", [])
if relationships:
print("Relationships:")
for rel in relationships:
api = rel.get("apiName", "")
card = rel.get("cardinality", "")
left = rel.get("leftSemanticDefinitionApiName", "")
right = rel.get("rightSemanticDefinitionApiName", "")
queryable = rel.get("isQueryable", "")
print(f" {api:<37} ({card:<12}) {left} <-> {right} [{queryable}]")
for crit in rel.get("criteria", []):
lf = crit.get("leftSemanticFieldApiName", "")
rf = crit.get("rightSemanticFieldApiName", "")
op = crit.get("joinOperator", "")
print(f" {lf} {op} {rf}")
print()
def _structured_output(data: Dict[str, Any]) -> Dict[str, Any]:
"""Build a machine-readable summary of an SDM.
Args:
data: Raw SDM detail response from API
Returns:
Structured dict with apiName, label, objects, calculatedDimensions,
calculatedMeasures, and metrics
"""
result: Dict[str, Any] = {
"apiName": data.get("apiName", ""),
"label": data.get("label", ""),
"objects": [],
"calculatedDimensions": [],
"calculatedMeasures": [],
"metrics": [],
"relationships": [],
}
for obj in data.get("semanticDataObjects", []):
obj_entry: dict = {
"objectName": obj.get("apiName", ""),
"dimensions": [],
"measures": [],
}
for d in obj.get("semanticDimensions", []):
obj_entry["dimensions"].append({
"fieldName": d.get("apiName", ""),
"dataType": d.get("dataType", ""),
"objectName": obj.get("apiName", ""),
"role": "Dimension",
"displayCategory": "Discrete",
"function": None,
})
for m in obj.get("semanticMeasurements", []):
obj_entry["measures"].append({
"fieldName": m.get("apiName", ""),
"aggregationType": m.get("aggregationType", "Sum"),
"objectName": obj.get("apiName", ""),
"role": "Measure",
"displayCategory": "Continuous",
"function": m.get("aggregationType", "Sum"),
})
result["objects"].append(obj_entry)
for d in data.get("semanticCalculatedDimensions", []):
result["calculatedDimensions"].append({
"fieldName": d.get("apiName", ""),
"dataType": d.get("dataType", ""),
"objectName": None,
"role": "Dimension",
"displayCategory": "Discrete",
"function": None,
})
for m in data.get("semanticCalculatedMeasurements", []):
result["calculatedMeasures"].append({
"fieldName": m.get("apiName", ""),
"aggregationType": m.get("aggregationType", "Sum"),
"objectName": None,
"role": "Measure",
"displayCategory": "Continuous",
"function": m.get("aggregationType", "Sum"),
})
for m in data.get("semanticMetrics", []):
result["metrics"].append({
"apiName": m.get("apiName", ""),
"label": m.get("label", ""),
"aggregationType": m.get("aggregationType", ""),
})
for rel in data.get("semanticRelationships", []):
result["relationships"].append({
"apiName": rel.get("apiName", ""),
"label": rel.get("label", ""),
"joinType": rel.get("joinType", ""),
"cardinality": rel.get("cardinality", ""),
"isQueryable": rel.get("isQueryable", ""),
"leftObject": rel.get("leftSemanticDefinitionApiName", ""),
"rightObject": rel.get("rightSemanticDefinitionApiName", ""),
"criteria": [
{
"leftField": c.get("leftSemanticFieldApiName", ""),
"rightField": c.get("rightSemanticFieldApiName", ""),
"leftFieldType": c.get("leftFieldType", ""),
"rightFieldType": c.get("rightFieldType", ""),
"joinOperator": c.get("joinOperator", ""),
}
for c in rel.get("criteria", [])
],
})
return result
def main() -> None:
parser = argparse.ArgumentParser(description="Discover Tableau Next Semantic Data Models")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--list", action="store_true", help="List all available SDMs")
group.add_argument("--sdm", type=str, help="Show fields for a specific SDM (by API name)")
parser.add_argument("--json", action="store_true", help="Output as JSON (machine-readable)")
args = parser.parse_args()
token, instance = get_credentials()
if args.list:
list_sdms(token, instance, args.json)
elif args.sdm:
show_sdm(token, instance, args.sdm, args.json)
if __name__ == "__main__":
main()