mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-09 00:42:46 +08:00
Adds a skill for authoring Tableau Next semantic models (SDMs) on Data 360: build from scratch, add data objects, define joins, enrich with calculated fields and metrics, and make models AI-ready. Smoke-tested against a live Data 360 org: SDM discovery, AI-readiness flip, dimension creation, metric creation, and description backfill all exercised end-to-end.
155 lines
6.2 KiB
Python
155 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Update an existing semantic metric — the SAFE (full-payload PUT) way.
|
|
|
|
Metric update is a full-payload PUT: the body REPLACES the metric definition, so
|
|
a single-field change must re-send the COMPLETE metric or the server silently
|
|
drops ``insightsSettings`` / ``additionalDimensions`` / ``identifyingDimension``
|
|
(a partial body drops additionalDimensions to empty). This script does
|
|
resolve-and-merge: GET the metric's full definition, overlay only the
|
|
requested change, then PUT the complete body.
|
|
|
|
Use this to set the ``identifyingDimension`` (which the Tableau Next metric UI
|
|
dereferences on load — a metric without it can crash the UI) or the
|
|
time-comparison settings on an existing metric. Metric CREATION stays in
|
|
create_metric.py (untouched).
|
|
|
|
Usage:
|
|
# Set the identifying dimension (same-object)
|
|
python scripts/update_metric.py Workforce_SDM Headcount_mtc \\
|
|
--identifying-dimension "position_title:qb_hw_position"
|
|
|
|
# Cross-object identifying dimension (Field:Object on a joined object)
|
|
python scripts/update_metric.py Hotel_SDM ADR_mtc \\
|
|
--identifying-dimension "Date:Daily_Property_Performance"
|
|
|
|
# Set time comparisons
|
|
python scripts/update_metric.py Sales_SDM Total_Sales_mtc \\
|
|
--primary-comparison PriorPeriod --secondary-comparison PriorYear
|
|
|
|
# Dry-run: print the FULL PUT body without calling the org
|
|
python scripts/update_metric.py Workforce_SDM Headcount_mtc \\
|
|
--identifying-dimension "gender:qb_hw_employee" --dry-run
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
|
|
from _shared.sdm_ai_templates import build_metric_put_payload
|
|
from _shared.sdm_discovery import get_metric_definition
|
|
from _shared.sf_api import (
|
|
get_credentials,
|
|
metric_endpoint,
|
|
parse_too_large_error,
|
|
sf_put,
|
|
)
|
|
|
|
|
|
def explain_too_large_error(err: str):
|
|
"""Translate the server's 'data value too large (max length=N)' 400 for a metric.
|
|
|
|
Returns actionable guidance naming the over-long field + its cap, or None when
|
|
``err`` is not a length-cap error (caller prints the raw message).
|
|
"""
|
|
parsed = parse_too_large_error(err)
|
|
if parsed is None:
|
|
return None
|
|
field, max_len = parsed
|
|
if field is None:
|
|
return "The server rejected the update: a metric field value is too long."
|
|
limit = f" of {max_len} characters" if max_len is not None else ""
|
|
return (
|
|
f"The server rejected the update: the metric '{field}' field exceeds its "
|
|
f"maximum length{limit} (measured on the raw input). Shorten it."
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Update an existing metric via full-payload PUT (resolve-and-merge).",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
parser.add_argument("sdm", help="SDM apiName")
|
|
parser.add_argument("metric", help="Metric apiName (e.g. Headcount_mtc)")
|
|
parser.add_argument(
|
|
"--identifying-dimension",
|
|
help="Set insightsSettings.identifyingDimension. Format 'Field:Object' "
|
|
"(fieldApiName:tableApiName); cross-object is allowed (the object "
|
|
"can be a joined one). The field is mirrored into "
|
|
"additionalDimensions if absent (the UI requires membership).",
|
|
)
|
|
parser.add_argument("--primary-comparison",
|
|
help="Set primaryTimeComparison (top-level time-comparison field).")
|
|
parser.add_argument("--secondary-comparison",
|
|
help="Set secondaryTimeComparison.")
|
|
parser.add_argument("--description", help="Replace the metric description.")
|
|
parser.add_argument("--dry-run", action="store_true",
|
|
help="Print the full PUT body and exit without calling the org.")
|
|
args = parser.parse_args()
|
|
|
|
if not any([args.identifying_dimension, args.primary_comparison,
|
|
args.secondary_comparison, args.description]):
|
|
print(
|
|
"Error: nothing to change. Provide at least one of "
|
|
"--identifying-dimension, --primary-comparison, --secondary-comparison, "
|
|
"--description.",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
# RESOLVE: fetch the metric's FULL definition (the body the PUT must re-send
|
|
# in full). On --dry-run without creds this still needs the metric; we fetch
|
|
# it (the get_credentials call inside will exit if unset). To keep --dry-run
|
|
# usable without an org, a --from-file override is out of scope; dry-run still
|
|
# resolves the live metric so the printed body is real.
|
|
existing = get_metric_definition(args.sdm, args.metric)
|
|
if existing is None:
|
|
print(
|
|
f"Error: could not read metric '{args.metric}' on SDM '{args.sdm}'. "
|
|
f"A full-payload PUT requires the current definition to merge into.",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
# MERGE: overlay only the requested change onto the full definition.
|
|
try:
|
|
payload = build_metric_put_payload(
|
|
existing,
|
|
identifying_dimension=args.identifying_dimension,
|
|
primary_comparison=args.primary_comparison,
|
|
secondary_comparison=args.secondary_comparison,
|
|
description=args.description,
|
|
)
|
|
except ValueError as exc:
|
|
print(f"Error: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(json.dumps(payload, indent=2))
|
|
|
|
if args.dry_run:
|
|
print("\n[Dry-run mode - full PUT body shown above, not PUT]", file=sys.stderr)
|
|
return 0
|
|
|
|
# PUT the COMPLETE body.
|
|
token, instance = get_credentials()
|
|
resp, err = sf_put(token, instance, metric_endpoint(args.sdm, args.metric), payload)
|
|
if err:
|
|
# Catch the server's length-cap rejection and re-surface it clearly.
|
|
friendly = explain_too_large_error(err)
|
|
print(f"Error: {friendly or err}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"\n✓ Updated metric: {args.metric}", file=sys.stderr)
|
|
# Confirm actual state, not just the success code — discovery is the proof
|
|
# that identifyingDimension survived the PUT.
|
|
print(
|
|
f" Verify with: python scripts/discover_sdm.py {args.sdm} --metric {args.metric} "
|
|
f"(confirm insightsSettings.identifyingDimension is still present).",
|
|
file=sys.stderr,
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|