#!/usr/bin/env python3 """Create semantic metrics on semantic models. Metrics reference calculated fields via measurementReference and require a time dimension. Usage: # Step 1: Create calculated field first python scripts/create_calc_field.py \\ --sdm Sales_Cloud12_backward \\ --type measurement \\ --name Total_Revenue_clc \\ --label "Total Revenue" \\ --expression "SUM([Amount])" \\ --aggregation Sum # Step 2: Create metric referencing the calculated field python scripts/create_metric.py \\ --sdm Sales_Cloud12_backward \\ --name Total_Revenue_mtc \\ --label "Total Revenue" \\ --calculated-field Total_Revenue_clc \\ --time-field Close_Date \\ --time-table Opportunity_TAB_Sales_Cloud # Dry-run (show payload without POSTing) python scripts/create_metric.py \\ --sdm Sales_Cloud12_backward \\ --name Account_Count_mtc \\ --label "Account Count" \\ --calculated-field Account_Count_clc \\ --time-field Close_Date \\ --time-table Opportunity_TAB_Sales_Cloud \\ --dry-run """ import argparse import json import sys from typing import Any, Dict, Optional from _shared.metric_templates import ( METRIC_TEMPLATE_REGISTRY, build_semantic_metric, parse_metric_filter, validate_metric, ) from _shared.sf_api import calculated_field_endpoint, get_credentials, sf_post from _shared.verify import report_verification, verify_metric_has_data def resolve_expression( template: Optional[str], template_args: Optional[str], expression: Optional[str] ) -> str: """Resolve expression from template or use provided expression. Args: template: Template name (sum, avg, win_rate, etc.) template_args: JSON string with template arguments expression: Direct expression string Returns: Resolved expression string """ if template: if template not in METRIC_TEMPLATE_REGISTRY: print( f"Error: Unknown template '{template}'. Valid: {', '.join(METRIC_TEMPLATE_REGISTRY.keys())}", file=sys.stderr ) sys.exit(1) if not template_args: print(f"Error: --template-args required when using --template", file=sys.stderr) sys.exit(1) try: args = json.loads(template_args) except json.JSONDecodeError as e: print(f"Error: Invalid JSON in --template-args: {e}", file=sys.stderr) sys.exit(1) template_func = METRIC_TEMPLATE_REGISTRY[template] try: return template_func(**args) except TypeError as e: print(f"Error: Template '{template}' arguments mismatch: {e}", file=sys.stderr) print( f"Expected arguments: {template_func.__code__.co_varnames[:template_func.__code__.co_argcount]}", file=sys.stderr ) sys.exit(1) if expression: return expression print("Error: Either --template or --expression must be provided", file=sys.stderr) sys.exit(1) def normalize_api_name(name: str) -> str: """Ensure API name ends with _mtc and doesn't have double underscores. Salesforce API names cannot contain double underscores (__). Args: name: API name Returns: Normalized API name (no double underscores, ends with _mtc) """ name = name.strip() # Replace double underscores with single underscore while "__" in name: name = name.replace("__", "_") if not name.endswith("_mtc"): return f"{name}_mtc" return name def main() -> int: parser = argparse.ArgumentParser( description="Create semantic metrics on semantic models", formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("--sdm", required=True, help="Semantic model API name") parser.add_argument("--name", required=True, help="API name (auto-appends _mtc if missing)") parser.add_argument("--label", required=True, help="Display label") # Calculated field reference (preferred method) parser.add_argument("--calculated-field", required=True, help="API name of calculated field to reference") # Time dimension (required for metrics) parser.add_argument("--time-field", required=True, help="Time dimension field API name (e.g., Close_Date)") parser.add_argument("--time-table", required=True, help="Time dimension table API name (e.g., Opportunity_TAB_Sales_Cloud)") # Legacy expression support (deprecated - metrics should reference calculated fields) expr_group = parser.add_mutually_exclusive_group(required=False) expr_group.add_argument("--expression", help="[DEPRECATED] Tableau formula expression - use --calculated-field instead") expr_group.add_argument("--template", choices=list(METRIC_TEMPLATE_REGISTRY.keys()), help="[DEPRECATED] Template name - use --calculated-field instead") parser.add_argument("--template-args", help="JSON dict of template arguments (required with --template)") parser.add_argument("--description", default="", help="Field description") # Additional dimensions for breakdown analysis parser.add_argument( "--additional-dimension", action="append", help="Additional dimension for breakdown analysis (format: fieldApiName:tableApiName). Can be repeated multiple times." ) # Identifying dimension (the field the TN metric UI dereferences on load) parser.add_argument( "--identifying-dimension", help=( "Field used as the metric's identifying dimension " "(format: fieldApiName:tableApiName). Defaults to the first " "--additional-dimension. The TN metric UI crashes without it. " "If not already an additional dimension, it is added to the list." ) ) # Metric filters. The field must be fully qualified (Table.Field); it is # auto-mirrored into additionalDimensions so the metric stays queryable, and # filterLogic ("1 AND 2 AND ...") is auto-generated. parser.add_argument( "--filter", action="append", dest="filters", help=( "Metric filter (format: '