"""Lightweight Salesforce API helper for Tableau Next authoring scripts. Reads SF_TOKEN and SF_INSTANCE from environment variables. Adapted from tabnext-tools-main/backend/lib/salesforce_api.py. """ import json import os import re import sys import time from typing import Any, Dict, Optional, Tuple import requests MAJOR_VERSION = "v67.0" MINOR_VERSION = "10" def get_credentials() -> Tuple[str, str]: """Return (access_token, instance_url) from environment variables.""" token = os.environ.get("SF_TOKEN") instance = os.environ.get("SF_INSTANCE") if not token or not instance: print( "Error: SF_TOKEN and SF_INSTANCE environment variables are required.\n" "Set them with:\n" " export SF_ORG=myorg\n" ' export SF_TOKEN=$(sf org auth show-access-token --target-org $SF_ORG --json | jq -r \'.result.accessToken\')\n' ' export SF_INSTANCE=$(sf org display --target-org $SF_ORG --json | jq -r \'.result.instanceUrl\')', file=sys.stderr, ) sys.exit(1) return token, instance.rstrip("/") # -- Endpoint builders -------------------------------------------------------- def sdm_list_endpoint() -> str: return f"/services/data/{MAJOR_VERSION}/ssot/semantic/models" def sdm_detail_endpoint(sdm_name: str) -> str: return f"/services/data/{MAJOR_VERSION}/ssot/semantic/models/{sdm_name}" def visualization_endpoint(viz_name: Optional[str] = None) -> str: base = f"/services/data/{MAJOR_VERSION}/tableau/visualizations" if viz_name: return f"{base}/{viz_name}?minorVersion={MINOR_VERSION}" return f"{base}?minorVersion={MINOR_VERSION}" def dashboard_endpoint(dashboard_name: Optional[str] = None) -> str: base = f"/services/data/{MAJOR_VERSION}/tableau/dashboards" if dashboard_name: return f"{base}/{dashboard_name}?minorVersion={MINOR_VERSION}" return f"{base}?minorVersion={MINOR_VERSION}" def sdm_create_endpoint() -> str: """Endpoint for creating an SDM (POST). Same path as the list endpoint. POST /ssot/semantic/models -> 201. No minorVersion. """ return f"/services/data/{MAJOR_VERSION}/ssot/semantic/models" def sdm_data_objects_endpoint(sdm_name: str) -> str: """Endpoint for adding a data object to an SDM (POST). POST .../models/{sdm}/data-objects -> 200. No minorVersion. """ return f"/services/data/{MAJOR_VERSION}/ssot/semantic/models/{sdm_name}/data-objects" def sdm_dimensions_endpoint(sdm_name: str, object_name: str) -> str: """Endpoint for adding a base dimension to a data object (POST). POST .../data-objects/{obj}/dimensions -> 201. No minorVersion. """ return ( f"/services/data/{MAJOR_VERSION}/ssot/semantic/models/{sdm_name}" f"/data-objects/{object_name}/dimensions" ) def sdm_measurements_endpoint(sdm_name: str, object_name: str) -> str: """Endpoint for adding a base measure to a data object (POST). POST .../data-objects/{obj}/measurements -> 201. No minorVersion. """ return ( f"/services/data/{MAJOR_VERSION}/ssot/semantic/models/{sdm_name}" f"/data-objects/{object_name}/measurements" ) def sdm_relationships_endpoint(sdm_name: str) -> str: """Endpoint for adding a model-level relationship (join) to an SDM (POST). POST .../models/{sdm}/relationships -> 201. No minorVersion. """ return f"/services/data/{MAJOR_VERSION}/ssot/semantic/models/{sdm_name}/relationships" def semantic_query_endpoint() -> str: """Endpoint for the semantic-query gateway (POST, camelCase body). POST /semantic-engine/gateway -> 201, status SUCCESS. Used to verify a cross-object query spans an authored relationship. """ return f"/services/data/{MAJOR_VERSION}/semantic-engine/gateway" def query_sql_endpoint(dataspace: Optional[str] = None) -> str: """Endpoint for the Data 360 SQL query gateway (POST, camelCase body). POST /ssot/query-sql -> 200. No minorVersion. Body is a single, un-wrapped object: ``{"sql": "