Skip to main content

  Exploring inherited SDMs in Tableau Next (Data Cloud 360) and want to export all fields and their descriptions into Excel to validate definitions with business stakeholders and build a data dictionary.    I confirmed via Workbench REST describe that `inlineHelpText` is null on the DMOs — so descriptions are in the Tableau Next SDM layer, not Data Cloud.    1. Is there a native UI export for SDM field metadata in Tableau Next?  2. If not, is the Tableau Metadata API (GraphQL) the right approach? Any sample queries would be appreciated.   

1 answer
  1. May 11, 11:50 PM

    @Mudit Mishra

     

    Hi, for this the only answer I know currently is to use the REST API (maybe there could be other approaches). 

     

    I will assume you know how to get an access and refresh token, but I will give some detail 

     

    You would need to create a External Client App (Setup, External Clien App) with the following scopes

    Hi, for this the only answer I know currently is to use the REST API (maybe there could be other approaches).

     

    Under Oauth, get your Client ID and Secret Value: 

    image.png

     

    and the following settings:  

    image.png

     

    After you have created and save your app, you would need to get a refresh token (well, I am using constantly this app, thus a refresh token works pretty well for me to skip authentication every time). To generate the token I use a python function like this:

    def run_oauth_flow(sf_login_url, client_id, client_secret):

    """Open browser for OAuth authorization, capture code via local server, exchange for tokens."""

    # Build auth URL

    CALLBACK_URL=f"http://localhost:8080/callback"

    auth_params = urllib.parse.urlencode({

    "response_type": "code",

    "client_id": client_id,

    "redirect_uri": CALLBACK_URL,

    "prompt": "login consent",

    "scope": "cdp_ingest_api cdp_query_api api sfap_api refresh_token",

    })

    auth_url = f"{sf_login_url}/services/oauth2/authorize?{auth_params}"

    # Start local callback server in background thread

    server = HTTPServer(("localhost", CALLBACK_PORT), _CallbackHandler)

    thread = threading.Thread(target=server.handle_request, daemon=True)

    thread.start()

    print("\n Opening your browser for Salesforce login...")

    print(f" If the browser doesn't open, visit this URL:\n")

    print(f" {auth_url}\n")

    webbrowser.open(auth_url)

    # Wait up to 120 seconds for the callback

    print(" Waiting for authorization (up to 2 minutes)...")

    for _ in range(120):

    time.sleep(1)

    if _CallbackHandler.captured_code:

    break

    server.server_close()

    if not _CallbackHandler.captured_code:

    print("\n ❌ Timed out waiting for authorization. Run the script again.")

    sys.exit(1)

    code = _CallbackHandler.captured_code

    print(" ✅ Authorization code received.")

    # Exchange code for tokens

    r = requests.post(

    f"{sf_login_url}/services/oauth2/token",

    data={

    "grant_type": "authorization_code",

    "code": code,

    "client_id": client_id,

    "client_secret": client_secret,

    "redirect_uri": CALLBACK_URL,

    }

    )

    if not r.ok:

    print(f"\n ❌ Token exchange failed: {r.status_code} {r.text[:300]}")

    sys.exit(1)

    data = r.json()

    sf_token = data["access_token"]

    refresh_token = data.get("refresh_token")

    sf_instance = data["instance_url"]

    if not refresh_token:

    print("\n ❌ No refresh_token returned. Make sure your Connected App has")

    print(" 'Perform requests at any time (refresh_token, offline_access)' scope enabled.")

    sys.exit(1)

    print("✅Refresh token captured")

    return sf_token, refresh_token, sf_instance

    which I use with something like:

    sf_token, refresh_token, sf_instance = run_oauth_flow("https://myorgname.my.salesforce.com", client_id, client_secret)

     

    After you get the refresh token, I created a file with the credentials (next_config.json) (login url ending in

    my.salesforce-setup.com, client id, client secrent, and refresh token)

    {

    "sf_login_url": "https://myorgname.my.salesforce-setup.com/",

    "client_id": "3MVGFPAT",

    "client_secret": "69CE3112C1E43",

    "refresh_token": "5AepzHaP9"

    }

     

    Finally, I use a python code like this:

    import json, os, requests

    import pandas as pd

    from pathlib import Path

    import json

    _PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))

    config_file = os.path.join(_PROJECT_ROOT, "next_config.json")

    CONFIG = json.loads(Path(config_file).read_text())

    def get_tokens(config):

    sf_resp = requests.post(

    f"{config['sf_login_url']}/services/oauth2/token",

    data={"grant_type": "refresh_token", "refresh_token": config["refresh_token"],

    "client_id": config["client_id"], "client_secret": config["client_secret"]}

    )

    sf_resp.raise_for_status()

    sf_token = sf_resp.json()["access_token"]

    sf_instance = sf_resp.json()["instance_url"]

    return sf_token, sf_instance

    sf_token, sf_instance = get_tokens(CONFIG)

    BASE_SEM = f"{sf_instance}/services/data/v65.0"

    SF_HDRS = {"Authorization": f"Bearer {sf_token}", "Content-Type": "application/json"}

    model_api_name = f"novatel_director_of_network_operations"

    r = requests.get(f"{BASE_SEM}/ssot/semantic/models/{model_api_name}",

    headers=SF_HDRS, params={"includeModelContent": True})

    r.raise_for_status()

    data = r.json()

    rows = []

    # Extract dimensions

    for obj in data.get("semanticDataObjects", []):

    # semanticDimensions

    for field in obj.get("semanticDimensions", []):

    rows.append({

    "category": "Dimension",

    "object": obj.get("label"),

    "field_name": field.get("label"),

    "description": field.get("description")

    })

    # semanticMeasurements

    for field in obj.get("semanticMeasurements", []):

    rows.append({

    "category": "Measurement",

    "object": obj.get("label"),

    "field_name": field.get("label"),

    "description": field.get("description")

    })

    # Calculated dimensions

    for field in data.get("semanticCalculatedDimensions", []):

    rows.append({

    "category": "Calculated Dimension",

    "object": "Global",

    "field_name": field.get("label"),

    "description": field.get("description")

    })

    # Calculated measurements

    for field in data.get("semanticCalculatedMeasurements", []):

    rows.append({

    "category": "Calculated Measurement",

    "object": "Global",

    "field_name": field.get("label"),

    "description": field.get("description")

    })

    # Metrics

    for field in data.get("semanticMetrics", []):

    rows.append({

    "category": "Metric",

    "object": "Global",

    "field_name": field.get("label"),

    "description": field.get("description")

    })

    # Create dataframe

    df = pd.DataFrame(rows)

    # Export to CSV

    df.to_csv("fields_descriptions.csv", index=False)

    print(df.head())

    print("CSV exported successfully!")

     

    Once last thing, you may get your semantic model_api_name from Tableau Next (In my case it was novatel_director_of_network_operations.

    image.png

     

     

    image.png

     

     

    The final csv files look like this:

    image.png

    If this post resolves the question, would you be so kind to "Accept this Answer"?. This will help other users find the same answer/resolution and help community keep track of answered questions. Thank you. 

     

    Regards, 

     

    Diego Martinez 

    Tableau Visionary and Tableau Ambassador 

0/9000