Spaces:
Running
fix: empty table detection + test suite robustness
Browse filesEmpty tables (Wells Fargo, Best Buy):
- Extend empty-table check to ALL tables, not just fact tables
- is_fact_table heuristic (>=2 FKs) fails when DDL has no FOREIGN KEY
constraints β fact_table_names was empty so the check never triggered
- Now checks all non-date tables; 0-row dim table triggers retry
- Final check also covers dim tables: raises RuntimeError if still empty
- Add Step 3b generation summary (per-table row counts before write)
- Log schema name in populate log_end so fetch_run_diagnostics can find it
- Improved insert_rows logging when filtered_columns is empty
Test suite fixes:
- Liveboard Name selector: try textarea/input/generic aria-label variants
- Custom tab Context selector: 1.5s wait after vertical change + try
placeholder and aria-label selectors with visibility check
- Tag/share checks: guard against empty API response body before .json()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- legitdata_bridge.py +38 -7
- tests/e2e_quality.py +162 -74
|
@@ -197,7 +197,14 @@ class KeyPairSnowflakeWriter:
|
|
| 197 |
filtered_indices.append(i)
|
| 198 |
|
| 199 |
if not filtered_columns:
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
return 0
|
| 202 |
|
| 203 |
col_list = ', '.join(filtered_columns)
|
|
@@ -435,6 +442,11 @@ class DemoLegitGenerator:
|
|
| 435 |
# Register PK values for FK references
|
| 436 |
self._gen._register_pk_values(table, rows)
|
| 437 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
print("\n=== Step 4: Writing to Database ===")
|
| 439 |
results = self._write_to_database(generated_data, truncate_first)
|
| 440 |
|
|
@@ -640,11 +652,25 @@ def populate_demo_data(
|
|
| 640 |
if _slog:
|
| 641 |
_slog.log_verbose("populate", "first attempt failed", error=first_attempt_error)
|
| 642 |
|
| 643 |
-
# Check all tables for 0 rows even if no exception
|
|
|
|
|
|
|
|
|
|
| 644 |
fact_table_names = {t.name for t in schema.fact_tables}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 645 |
empty_facts = [t for t in fact_table_names if results.get(t, 0) == 0]
|
| 646 |
-
|
| 647 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
log(f"β οΈ {first_attempt_error}")
|
| 649 |
|
| 650 |
# Attempt 2 if anything failed
|
|
@@ -667,9 +693,12 @@ def populate_demo_data(
|
|
| 667 |
log(f" {status} {table_name}: {count:,} rows")
|
| 668 |
|
| 669 |
empty_facts_final = [t for t in fact_table_names if results.get(t, 0) == 0]
|
| 670 |
-
|
|
|
|
|
|
|
| 671 |
raise RuntimeError(
|
| 672 |
-
f"
|
|
|
|
| 673 |
f"First attempt error: {first_attempt_error}"
|
| 674 |
)
|
| 675 |
|
|
@@ -688,7 +717,9 @@ Data contextually generated for:
|
|
| 688 |
"""
|
| 689 |
log(message)
|
| 690 |
if _slog and _t_populate is not None:
|
| 691 |
-
_slog.log_end("populate", _t_populate,
|
|
|
|
|
|
|
| 692 |
return True, message, results
|
| 693 |
|
| 694 |
except Exception as e:
|
|
|
|
| 197 |
filtered_indices.append(i)
|
| 198 |
|
| 199 |
if not filtered_columns:
|
| 200 |
+
# Log the first row so we can see what the generator produced
|
| 201 |
+
if rows:
|
| 202 |
+
sample = dict(zip(columns, rows[0]))
|
| 203 |
+
print(f"ERROR: No columns with non-None values to insert for {table_name}")
|
| 204 |
+
print(f" columns expected: {columns[:8]}")
|
| 205 |
+
print(f" first row sample: {str(sample)[:300]}")
|
| 206 |
+
else:
|
| 207 |
+
print(f"Warning: No rows to insert for {table_name}")
|
| 208 |
return 0
|
| 209 |
|
| 210 |
col_list = ', '.join(filtered_columns)
|
|
|
|
| 442 |
# Register PK values for FK references
|
| 443 |
self._gen._register_pk_values(table, rows)
|
| 444 |
|
| 445 |
+
print("\n=== Step 3b: Generation Summary ===")
|
| 446 |
+
for tname, trows in generated_data.items():
|
| 447 |
+
status = "β
" if trows else "β EMPTY"
|
| 448 |
+
print(f" {status} {tname}: {len(trows)} rows in memory")
|
| 449 |
+
|
| 450 |
print("\n=== Step 4: Writing to Database ===")
|
| 451 |
results = self._write_to_database(generated_data, truncate_first)
|
| 452 |
|
|
|
|
| 652 |
if _slog:
|
| 653 |
_slog.log_verbose("populate", "first attempt failed", error=first_attempt_error)
|
| 654 |
|
| 655 |
+
# Check all tables for 0 rows even if no exception.
|
| 656 |
+
# NOTE: is_fact_table uses a FK-count heuristic; if DDL has no explicit FOREIGN KEY
|
| 657 |
+
# constraints (common in Snowflake schemas), ALL tables look like dim tables and
|
| 658 |
+
# fact_table_names is empty β so we must also check dim tables.
|
| 659 |
fact_table_names = {t.name for t in schema.fact_tables}
|
| 660 |
+
all_table_names = {t.name for t in schema.tables}
|
| 661 |
+
date_table_names = {t.name for t in schema.tables
|
| 662 |
+
if t.name.upper() in ('DATES', 'DATE', 'DATE_DIM', 'DIM_DATE', 'CALENDAR')}
|
| 663 |
+
|
| 664 |
empty_facts = [t for t in fact_table_names if results.get(t, 0) == 0]
|
| 665 |
+
# Also catch dim tables (excluding date tables, which may legitimately be smaller)
|
| 666 |
+
empty_dims = [t for t in (all_table_names - fact_table_names - date_table_names)
|
| 667 |
+
if results.get(t, 0) == 0]
|
| 668 |
+
|
| 669 |
+
if not first_attempt_error and (empty_facts or empty_dims):
|
| 670 |
+
first_attempt_error = (
|
| 671 |
+
f"Empty table(s) after first attempt β "
|
| 672 |
+
f"facts: {empty_facts or 'none'}, dims: {empty_dims or 'none'}"
|
| 673 |
+
)
|
| 674 |
log(f"β οΈ {first_attempt_error}")
|
| 675 |
|
| 676 |
# Attempt 2 if anything failed
|
|
|
|
| 693 |
log(f" {status} {table_name}: {count:,} rows")
|
| 694 |
|
| 695 |
empty_facts_final = [t for t in fact_table_names if results.get(t, 0) == 0]
|
| 696 |
+
empty_dims_final = [t for t in (all_table_names - fact_table_names - date_table_names)
|
| 697 |
+
if results.get(t, 0) == 0]
|
| 698 |
+
if empty_facts_final or empty_dims_final:
|
| 699 |
raise RuntimeError(
|
| 700 |
+
f"Empty table(s) still present after 2 attempts β "
|
| 701 |
+
f"facts: {empty_facts_final or 'none'}, dims: {empty_dims_final or 'none'}. "
|
| 702 |
f"First attempt error: {first_attempt_error}"
|
| 703 |
)
|
| 704 |
|
|
|
|
| 717 |
"""
|
| 718 |
log(message)
|
| 719 |
if _slog and _t_populate is not None:
|
| 720 |
+
_slog.log_end("populate", _t_populate,
|
| 721 |
+
tables=len(results), total_rows=total_rows,
|
| 722 |
+
schema=schema_name)
|
| 723 |
return True, message, results
|
| 724 |
|
| 725 |
except Exception as e:
|
|
@@ -95,9 +95,14 @@ def _parse_json(text: str) -> dict:
|
|
| 95 |
# ---------------------------------------------------------------------------
|
| 96 |
# Test case generation
|
| 97 |
# ---------------------------------------------------------------------------
|
| 98 |
-
def generate_ai_test_case(config: dict) -> dict:
|
| 99 |
"""AI picks vertical, line, function, and a matching company."""
|
| 100 |
prompt = config["ai_generated"]["generation_prompt"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
data = _parse_json(_llm(prompt))
|
| 102 |
return {
|
| 103 |
"name": f"AI: {data['company']} β {data['vertical']} / {data['line']} / {data['function']}",
|
|
@@ -192,10 +197,9 @@ def build_test_suite(config: dict) -> list:
|
|
| 192 |
ai_count = config["ai_generated"].get("count", 2)
|
| 193 |
for i in range(ai_count):
|
| 194 |
try:
|
| 195 |
-
tc = generate_ai_test_case(config)
|
| 196 |
-
# Deduplicate: if this company was already picked, regenerate once
|
| 197 |
if tc["company"] in used_companies:
|
| 198 |
-
tc = generate_ai_test_case(config)
|
| 199 |
used_companies.append(tc["company"])
|
| 200 |
suite.append(tc)
|
| 201 |
except Exception as e:
|
|
@@ -204,7 +208,9 @@ def build_test_suite(config: dict) -> list:
|
|
| 204 |
|
| 205 |
for i in range(2):
|
| 206 |
try:
|
| 207 |
-
|
|
|
|
|
|
|
| 208 |
except Exception as e:
|
| 209 |
print(f" β οΈ Custom test case {i+1} failed ({e}), skipping")
|
| 210 |
|
|
@@ -280,6 +286,34 @@ def submit_job(page: Page, test_case: dict):
|
|
| 280 |
page.get_by_role('tab', name='App', exact=True).click(timeout=10000)
|
| 281 |
page.wait_for_timeout(1000)
|
| 282 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
# Select TS environment first (required for deploy)
|
| 284 |
select_gradio_dropdown(page, "TS Environment", test_case.get("ts_environment", "secloud - primary"))
|
| 285 |
|
|
@@ -287,8 +321,23 @@ def submit_job(page: Page, test_case: dict):
|
|
| 287 |
select_gradio_dropdown(page, "Vertical", test_case["vertical"])
|
| 288 |
|
| 289 |
if test_case["vertical"] == "* CUSTOM *":
|
| 290 |
-
# Custom mode:
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
ctx_el.click(click_count=3, timeout=5000)
|
| 293 |
ctx_el.fill(test_case.get("context", ""))
|
| 294 |
page.wait_for_timeout(300)
|
|
@@ -302,17 +351,6 @@ def submit_job(page: Page, test_case: dict):
|
|
| 302 |
url_el.fill(test_case["company_url"])
|
| 303 |
page.wait_for_timeout(300)
|
| 304 |
|
| 305 |
-
# Set liveboard name so test runs are identifiable in ThoughtSpot
|
| 306 |
-
lb_name = f"QA β {test_case['company']} {test_case['function']}"
|
| 307 |
-
try:
|
| 308 |
-
lb_el = page.locator('input[aria-label="Liveboard Name"]').first
|
| 309 |
-
lb_el.click(click_count=3, timeout=3000)
|
| 310 |
-
lb_el.fill(lb_name)
|
| 311 |
-
page.wait_for_timeout(300)
|
| 312 |
-
except Exception:
|
| 313 |
-
lb_name += " (name not set)"
|
| 314 |
-
print(f" β οΈ Liveboard Name field not found β continuing without it")
|
| 315 |
-
|
| 316 |
# Click GO
|
| 317 |
page.click('button:has-text("β GO")', timeout=10000)
|
| 318 |
print(f" β
Form submitted: {test_case['vertical']} / {test_case['line']} / {test_case['function']} β {test_case['company_url']} | lb: {lb_name}")
|
|
@@ -439,18 +477,51 @@ def get_snowflake_sample(db: str, schema: str) -> str:
|
|
| 439 |
cursor = conn.cursor()
|
| 440 |
cursor.execute(f'SHOW TABLES IN SCHEMA "{db}"."{schema}"')
|
| 441 |
tables = [row[1] for row in cursor.fetchall()]
|
| 442 |
-
|
| 443 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
try:
|
| 445 |
cursor.execute(f'SELECT * FROM "{db}"."{schema}"."{table}" LIMIT 30')
|
| 446 |
cols = [d[0] for d in cursor.description]
|
| 447 |
rows = cursor.fetchall()
|
| 448 |
-
parts.append(
|
|
|
|
|
|
|
|
|
|
| 449 |
parts.append(f"Columns: {', '.join(cols)}")
|
| 450 |
for row in rows[:15]:
|
| 451 |
parts.append(" " + str(dict(zip(cols, row))))
|
| 452 |
except Exception as e:
|
| 453 |
parts.append(f"\nTable: {table} β error: {e}")
|
|
|
|
| 454 |
cursor.close()
|
| 455 |
conn.close()
|
| 456 |
return "\n".join(parts) if parts else "No tables found"
|
|
@@ -483,18 +554,23 @@ Analytics function: {function}
|
|
| 483 |
The goal is a compelling demo with realistic data, outliers that drive a narrative,
|
| 484 |
and a schema that supports the key KPIs for this use case.
|
| 485 |
|
| 486 |
-
MODEL TML:
|
| 487 |
-
{model_tml[:
|
| 488 |
|
| 489 |
-
SNOWFLAKE
|
| 490 |
-
{sample_data[:
|
| 491 |
|
| 492 |
-
Grade 0β100
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
|
| 499 |
Return ONLY valid JSON:
|
| 500 |
{{"score": 0, "reasoning": "...", "strengths": ["..."], "weaknesses": ["..."]}}"""
|
|
@@ -750,22 +826,25 @@ def verify_group1_settings(result: dict) -> dict:
|
|
| 750 |
if expected_tag and ts_base and model_guid:
|
| 751 |
try:
|
| 752 |
session = ts_authenticate(ts_base)
|
| 753 |
-
# v1 API: metadata/list returns tags per object
|
| 754 |
resp = session.get(
|
| 755 |
f"{ts_base}/tspublic/v1/metadata/list",
|
| 756 |
params={"type": "LOGICAL_TABLE", "batchsize": 1,
|
| 757 |
"offset": 0, "pattern": model_guid},
|
| 758 |
)
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 769 |
except Exception as e:
|
| 770 |
checks["tag_name"] = {"pass": None, "note": f"tag check failed: {e}"}
|
| 771 |
|
|
@@ -779,15 +858,19 @@ def verify_group1_settings(result: dict) -> dict:
|
|
| 779 |
json={"metadata": [{"type": "LOGICAL_TABLE", "identifier": model_guid}]},
|
| 780 |
timeout=15,
|
| 781 |
)
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 791 |
except Exception as e:
|
| 792 |
checks["share_with"] = {"pass": None, "note": f"share check failed: {e}"}
|
| 793 |
|
|
@@ -911,29 +994,32 @@ def fetch_run_diagnostics(start_time: float) -> dict:
|
|
| 911 |
return {"found": False, "reason": f"Diagnostics query failed: {e}"}
|
| 912 |
|
| 913 |
|
| 914 |
-
def check_snowflake_schema(company: str, start_time: float) -> dict:
|
| 915 |
"""
|
| 916 |
-
|
| 917 |
-
|
|
|
|
| 918 |
"""
|
| 919 |
try:
|
| 920 |
from snowflake_auth import get_snowflake_connection
|
| 921 |
from datetime import datetime
|
| 922 |
|
| 923 |
-
prefix = company[:3].upper()
|
| 924 |
-
date_str = datetime.fromtimestamp(start_time).strftime("%m%d")
|
| 925 |
-
|
| 926 |
conn = get_snowflake_connection()
|
| 927 |
cursor = conn.cursor()
|
| 928 |
-
cursor.execute("SHOW SCHEMAS IN DATABASE DEMOBUILD")
|
| 929 |
-
all_schemas = [row[1] for row in cursor.fetchall()]
|
| 930 |
-
|
| 931 |
-
matching = [s for s in all_schemas if s.startswith(prefix) and date_str in s]
|
| 932 |
-
if not matching:
|
| 933 |
-
cursor.close(); conn.close()
|
| 934 |
-
return {"found": False, "prefix": prefix, "date": date_str}
|
| 935 |
|
| 936 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 937 |
cursor.execute(f'SHOW TABLES IN SCHEMA DEMOBUILD."{schema}"')
|
| 938 |
tables = cursor.fetchall()
|
| 939 |
|
|
@@ -1063,21 +1149,23 @@ def run_single_test(page: Page, test_case: dict, config: dict) -> dict:
|
|
| 1063 |
|
| 1064 |
result["duration_seconds"] = round(time.time() - start)
|
| 1065 |
|
| 1066 |
-
# Always
|
| 1067 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1068 |
result["snowflake_check"] = sf
|
| 1069 |
if sf.get("found"):
|
| 1070 |
print(f" π¦ Snowflake: {sf['schema']} ({sf['total_rows']} rows, {len(sf['tables'])} tables)")
|
| 1071 |
for t in sf["tables"]:
|
| 1072 |
print(f" {t['table']}: {t['rows']} rows")
|
| 1073 |
else:
|
| 1074 |
-
print(f" π¦ Snowflake:
|
| 1075 |
|
| 1076 |
-
#
|
| 1077 |
if result["timed_out"] or result["error"]:
|
| 1078 |
-
print(" π Fetching failure diagnostics...")
|
| 1079 |
-
diag = fetch_run_diagnostics(start)
|
| 1080 |
-
result["diagnostics"] = diag
|
| 1081 |
print_diagnostics(diag, {}) # sf already printed above
|
| 1082 |
|
| 1083 |
# Stage scoring
|
|
@@ -1160,7 +1248,7 @@ def run_quality_suite(max_tests: int = None):
|
|
| 1160 |
print("β
Logged in\n")
|
| 1161 |
|
| 1162 |
for i, test_case in enumerate(suite, 1):
|
| 1163 |
-
label = {"fixed": "π", "random": "π²", "ai_generated": "π€"}[test_case["type"]]
|
| 1164 |
print(f"{'β'*62}")
|
| 1165 |
print(f"[{i}/{len(suite)}] {label} {test_case['name']}")
|
| 1166 |
|
|
@@ -1207,7 +1295,7 @@ def run_quality_suite(max_tests: int = None):
|
|
| 1207 |
print(f"\n{'='*62}")
|
| 1208 |
print(f" COMPLETE β Avg: {avg}/100 Grade: {grade}")
|
| 1209 |
for r in results:
|
| 1210 |
-
label = {"fixed": "π", "random": "π²", "ai_generated": "π€"}[r["type"]]
|
| 1211 |
ag = r["ai_grading"]
|
| 1212 |
ds = f"{ag['data_score']}/100" if ag.get("data_score") is not None else "n/a"
|
| 1213 |
ls = f"{ag['liveboard_score']}/100" if ag.get("liveboard_score") is not None else "n/a"
|
|
|
|
| 95 |
# ---------------------------------------------------------------------------
|
| 96 |
# Test case generation
|
| 97 |
# ---------------------------------------------------------------------------
|
| 98 |
+
def generate_ai_test_case(config: dict, exclude_list: list = None) -> dict:
|
| 99 |
"""AI picks vertical, line, function, and a matching company."""
|
| 100 |
prompt = config["ai_generated"]["generation_prompt"]
|
| 101 |
+
if exclude_list:
|
| 102 |
+
prompt = prompt.replace(
|
| 103 |
+
"Do NOT pick: Nike, Wells Fargo.",
|
| 104 |
+
f"Do NOT pick: {', '.join(exclude_list)}."
|
| 105 |
+
)
|
| 106 |
data = _parse_json(_llm(prompt))
|
| 107 |
return {
|
| 108 |
"name": f"AI: {data['company']} β {data['vertical']} / {data['line']} / {data['function']}",
|
|
|
|
| 197 |
ai_count = config["ai_generated"].get("count", 2)
|
| 198 |
for i in range(ai_count):
|
| 199 |
try:
|
| 200 |
+
tc = generate_ai_test_case(config, exclude_list=used_companies)
|
|
|
|
| 201 |
if tc["company"] in used_companies:
|
| 202 |
+
tc = generate_ai_test_case(config, exclude_list=used_companies)
|
| 203 |
used_companies.append(tc["company"])
|
| 204 |
suite.append(tc)
|
| 205 |
except Exception as e:
|
|
|
|
| 208 |
|
| 209 |
for i in range(2):
|
| 210 |
try:
|
| 211 |
+
tc = generate_custom_test_case(config, used_companies)
|
| 212 |
+
used_companies.append(tc["company"]) # prevent second custom from picking same
|
| 213 |
+
suite.append(tc)
|
| 214 |
except Exception as e:
|
| 215 |
print(f" β οΈ Custom test case {i+1} failed ({e}), skipping")
|
| 216 |
|
|
|
|
| 286 |
page.get_by_role('tab', name='App', exact=True).click(timeout=10000)
|
| 287 |
page.wait_for_timeout(1000)
|
| 288 |
|
| 289 |
+
# Set liveboard name FIRST β right panel is independent of the form tabs
|
| 290 |
+
lb_name = f"QA β {test_case['company']} {test_case.get('function', 'Demo')}"
|
| 291 |
+
try:
|
| 292 |
+
# Gradio Textbox with label="Liveboard Name" β try multiple selectors
|
| 293 |
+
lb_el = None
|
| 294 |
+
for sel in [
|
| 295 |
+
'textarea[aria-label="Liveboard Name"]',
|
| 296 |
+
'input[aria-label="Liveboard Name"]',
|
| 297 |
+
'[aria-label="Liveboard Name"]',
|
| 298 |
+
]:
|
| 299 |
+
try:
|
| 300 |
+
el = page.locator(sel).first
|
| 301 |
+
if el.is_visible(timeout=2000):
|
| 302 |
+
lb_el = el
|
| 303 |
+
break
|
| 304 |
+
except Exception:
|
| 305 |
+
pass
|
| 306 |
+
if lb_el:
|
| 307 |
+
lb_el.scroll_into_view_if_needed(timeout=3000)
|
| 308 |
+
lb_el.click(click_count=3, timeout=3000)
|
| 309 |
+
lb_el.fill(lb_name)
|
| 310 |
+
page.wait_for_timeout(300)
|
| 311 |
+
else:
|
| 312 |
+
raise Exception("No matching element found")
|
| 313 |
+
except Exception:
|
| 314 |
+
lb_name += " (name not set)"
|
| 315 |
+
print(f" β οΈ Liveboard Name field not found β continuing without it")
|
| 316 |
+
|
| 317 |
# Select TS environment first (required for deploy)
|
| 318 |
select_gradio_dropdown(page, "TS Environment", test_case.get("ts_environment", "secloud - primary"))
|
| 319 |
|
|
|
|
| 321 |
select_gradio_dropdown(page, "Vertical", test_case["vertical"])
|
| 322 |
|
| 323 |
if test_case["vertical"] == "* CUSTOM *":
|
| 324 |
+
# Custom mode: wait for UI to settle after vertical dropdown change, then fill Context
|
| 325 |
+
page.wait_for_timeout(1500)
|
| 326 |
+
ctx_el = None
|
| 327 |
+
for sel in [
|
| 328 |
+
'textarea[placeholder="Any extra context for the demo..."]',
|
| 329 |
+
'textarea[aria-label="Context"]',
|
| 330 |
+
'input[aria-label="Context"]',
|
| 331 |
+
]:
|
| 332 |
+
try:
|
| 333 |
+
el = page.locator(sel).first
|
| 334 |
+
if el.is_visible(timeout=3000):
|
| 335 |
+
ctx_el = el
|
| 336 |
+
break
|
| 337 |
+
except Exception:
|
| 338 |
+
pass
|
| 339 |
+
if ctx_el is None:
|
| 340 |
+
raise Exception('Context textarea not found β tried placeholder and aria-label="Context"')
|
| 341 |
ctx_el.click(click_count=3, timeout=5000)
|
| 342 |
ctx_el.fill(test_case.get("context", ""))
|
| 343 |
page.wait_for_timeout(300)
|
|
|
|
| 351 |
url_el.fill(test_case["company_url"])
|
| 352 |
page.wait_for_timeout(300)
|
| 353 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
# Click GO
|
| 355 |
page.click('button:has-text("β GO")', timeout=10000)
|
| 356 |
print(f" β
Form submitted: {test_case['vertical']} / {test_case['line']} / {test_case['function']} β {test_case['company_url']} | lb: {lb_name}")
|
|
|
|
| 477 |
cursor = conn.cursor()
|
| 478 |
cursor.execute(f'SHOW TABLES IN SCHEMA "{db}"."{schema}"')
|
| 479 |
tables = [row[1] for row in cursor.fetchall()]
|
| 480 |
+
|
| 481 |
+
# Count rows in every table first β so we can prioritize the fact table
|
| 482 |
+
row_counts = {}
|
| 483 |
+
for table in tables:
|
| 484 |
+
try:
|
| 485 |
+
cursor.execute(f'SELECT COUNT(*) FROM "{db}"."{schema}"."{table}"')
|
| 486 |
+
row_counts[table] = cursor.fetchone()[0]
|
| 487 |
+
except Exception:
|
| 488 |
+
row_counts[table] = 0
|
| 489 |
+
|
| 490 |
+
# Sort descending β fact table (most rows) sampled first
|
| 491 |
+
tables_sorted = sorted(tables, key=lambda t: row_counts.get(t, 0), reverse=True)
|
| 492 |
+
|
| 493 |
+
# Build row-count summary header so grader knows what's populated
|
| 494 |
+
header = ["Table row counts:"]
|
| 495 |
+
for t in tables_sorted:
|
| 496 |
+
header.append(f" {t}: {row_counts.get(t, 0)} rows")
|
| 497 |
+
empty_tables = [t for t in tables_sorted if row_counts.get(t, 0) == 0]
|
| 498 |
+
if empty_tables:
|
| 499 |
+
header.append(
|
| 500 |
+
f"\nβ οΈ WARNING: {len(empty_tables)} table(s) have 0 rows: "
|
| 501 |
+
f"{', '.join(empty_tables)}"
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
parts = ["\n".join(header)]
|
| 505 |
+
|
| 506 |
+
# Sample from tables that actually have data (up to 6); fall back to first 3 if all empty
|
| 507 |
+
tables_with_data = [t for t in tables_sorted if row_counts.get(t, 0) > 0]
|
| 508 |
+
to_sample = tables_with_data[:6] if tables_with_data else tables_sorted[:3]
|
| 509 |
+
|
| 510 |
+
for table in to_sample:
|
| 511 |
try:
|
| 512 |
cursor.execute(f'SELECT * FROM "{db}"."{schema}"."{table}" LIMIT 30')
|
| 513 |
cols = [d[0] for d in cursor.description]
|
| 514 |
rows = cursor.fetchall()
|
| 515 |
+
parts.append(
|
| 516 |
+
f"\nTable: {table} "
|
| 517 |
+
f"({row_counts.get(table, 0)} total rows, {len(rows)} sampled)"
|
| 518 |
+
)
|
| 519 |
parts.append(f"Columns: {', '.join(cols)}")
|
| 520 |
for row in rows[:15]:
|
| 521 |
parts.append(" " + str(dict(zip(cols, row))))
|
| 522 |
except Exception as e:
|
| 523 |
parts.append(f"\nTable: {table} β error: {e}")
|
| 524 |
+
|
| 525 |
cursor.close()
|
| 526 |
conn.close()
|
| 527 |
return "\n".join(parts) if parts else "No tables found"
|
|
|
|
| 554 |
The goal is a compelling demo with realistic data, outliers that drive a narrative,
|
| 555 |
and a schema that supports the key KPIs for this use case.
|
| 556 |
|
| 557 |
+
MODEL TML (full schema, column definitions, and relationships):
|
| 558 |
+
{model_tml[:7000]}
|
| 559 |
|
| 560 |
+
SNOWFLAKE DATA β actual row counts and sample rows:
|
| 561 |
+
{sample_data[:4000]}
|
| 562 |
|
| 563 |
+
Grade 0β100 using the actual data above. Do NOT hedge with phrases like "constrained by
|
| 564 |
+
partial TML" or "missing sample data" β the full TML and real row counts are provided.
|
| 565 |
+
Score based on what you can observe.
|
| 566 |
+
|
| 567 |
+
1. REALISM (20 pts): Values look like real {company} data at realistic scale and ranges.
|
| 568 |
+
2. STORY POTENTIAL (30 pts): Outliers, trends, or anomalies exist that anchor a demo narrative.
|
| 569 |
+
3. TIME COVERAGE (20 pts): 12β24 months of history with meaningful trends over time.
|
| 570 |
+
4. SCHEMA FITNESS (15 pts): Star schema design supports the key KPIs for {line} {function}.
|
| 571 |
+
5. COMPLETENESS (15 pts): Tables are populated. Dimensions have 20+ distinct members.
|
| 572 |
+
RULE: If the row counts above show any key table at 0 rows, score COMPLETENESS = 0 for
|
| 573 |
+
that criteria. If the fact table is 0 rows, also deduct heavily from STORY POTENTIAL.
|
| 574 |
|
| 575 |
Return ONLY valid JSON:
|
| 576 |
{{"score": 0, "reasoning": "...", "strengths": ["..."], "weaknesses": ["..."]}}"""
|
|
|
|
| 826 |
if expected_tag and ts_base and model_guid:
|
| 827 |
try:
|
| 828 |
session = ts_authenticate(ts_base)
|
|
|
|
| 829 |
resp = session.get(
|
| 830 |
f"{ts_base}/tspublic/v1/metadata/list",
|
| 831 |
params={"type": "LOGICAL_TABLE", "batchsize": 1,
|
| 832 |
"offset": 0, "pattern": model_guid},
|
| 833 |
)
|
| 834 |
+
body = resp.text.strip()
|
| 835 |
+
if not body:
|
| 836 |
+
checks["tag_name"] = {"pass": None, "note": "tag check skipped: empty API response"}
|
| 837 |
+
else:
|
| 838 |
+
headers_data = resp.json().get("headers", [])
|
| 839 |
+
obj_tags = []
|
| 840 |
+
for h in headers_data:
|
| 841 |
+
if h.get("id") == model_guid:
|
| 842 |
+
obj_tags = [t.get("name", "") for t in (h.get("tags") or [])]
|
| 843 |
+
break
|
| 844 |
+
checks["tag_name"] = {
|
| 845 |
+
"expected": expected_tag, "actual": obj_tags,
|
| 846 |
+
"pass": expected_tag in obj_tags,
|
| 847 |
+
}
|
| 848 |
except Exception as e:
|
| 849 |
checks["tag_name"] = {"pass": None, "note": f"tag check failed: {e}"}
|
| 850 |
|
|
|
|
| 858 |
json={"metadata": [{"type": "LOGICAL_TABLE", "identifier": model_guid}]},
|
| 859 |
timeout=15,
|
| 860 |
)
|
| 861 |
+
body = resp.text.strip()
|
| 862 |
+
if not body:
|
| 863 |
+
checks["share_with"] = {"pass": None, "note": "share check skipped: empty API response"}
|
| 864 |
+
else:
|
| 865 |
+
perms = resp.json()
|
| 866 |
+
principals = []
|
| 867 |
+
for item in (perms if isinstance(perms, list) else []):
|
| 868 |
+
for p in (item.get("permissions") or []):
|
| 869 |
+
principals.append(p.get("principal", {}).get("name", ""))
|
| 870 |
+
checks["share_with"] = {
|
| 871 |
+
"expected": expected_share, "actual": principals,
|
| 872 |
+
"pass": any(expected_share.lower() in p.lower() for p in principals),
|
| 873 |
+
}
|
| 874 |
except Exception as e:
|
| 875 |
checks["share_with"] = {"pass": None, "note": f"share check failed: {e}"}
|
| 876 |
|
|
|
|
| 994 |
return {"found": False, "reason": f"Diagnostics query failed: {e}"}
|
| 995 |
|
| 996 |
|
| 997 |
+
def check_snowflake_schema(company: str, start_time: float, schema_override: str = None) -> dict:
|
| 998 |
"""
|
| 999 |
+
Get row counts for the Snowflake schema created during this test run.
|
| 1000 |
+
Uses schema_override (from session_logs) if available β no guessing needed.
|
| 1001 |
+
Falls back to prefix+date matching only if session_logs didn't record the schema.
|
| 1002 |
"""
|
| 1003 |
try:
|
| 1004 |
from snowflake_auth import get_snowflake_connection
|
| 1005 |
from datetime import datetime
|
| 1006 |
|
|
|
|
|
|
|
|
|
|
| 1007 |
conn = get_snowflake_connection()
|
| 1008 |
cursor = conn.cursor()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1009 |
|
| 1010 |
+
if schema_override:
|
| 1011 |
+
schema = schema_override
|
| 1012 |
+
else:
|
| 1013 |
+
# Fall back: guess from company prefix + date
|
| 1014 |
+
prefix = company[:3].upper()
|
| 1015 |
+
date_str = datetime.fromtimestamp(start_time).strftime("%m%d")
|
| 1016 |
+
cursor.execute("SHOW SCHEMAS IN DATABASE DEMOBUILD")
|
| 1017 |
+
all_schemas = [row[1] for row in cursor.fetchall()]
|
| 1018 |
+
matching = [s for s in all_schemas if s.startswith(prefix) and date_str in s]
|
| 1019 |
+
if not matching:
|
| 1020 |
+
cursor.close(); conn.close()
|
| 1021 |
+
return {"found": False, "prefix": prefix, "date": date_str}
|
| 1022 |
+
schema = sorted(matching)[-1] # most recent
|
| 1023 |
cursor.execute(f'SHOW TABLES IN SCHEMA DEMOBUILD."{schema}"')
|
| 1024 |
tables = cursor.fetchall()
|
| 1025 |
|
|
|
|
| 1149 |
|
| 1150 |
result["duration_seconds"] = round(time.time() - start)
|
| 1151 |
|
| 1152 |
+
# Always fetch session_logs β gives us the real schema name + any errors
|
| 1153 |
+
diag = fetch_run_diagnostics(start)
|
| 1154 |
+
result["diagnostics"] = diag
|
| 1155 |
+
|
| 1156 |
+
# Use the real schema name from session_logs; fall back to prefix-guessing only if missing
|
| 1157 |
+
known_schema = diag.get("snowflake_schema") if diag.get("found") else None
|
| 1158 |
+
sf = check_snowflake_schema(result["company"], start, schema_override=known_schema)
|
| 1159 |
result["snowflake_check"] = sf
|
| 1160 |
if sf.get("found"):
|
| 1161 |
print(f" π¦ Snowflake: {sf['schema']} ({sf['total_rows']} rows, {len(sf['tables'])} tables)")
|
| 1162 |
for t in sf["tables"]:
|
| 1163 |
print(f" {t['table']}: {t['rows']} rows")
|
| 1164 |
else:
|
| 1165 |
+
print(f" π¦ Snowflake: schema not found (logged as: {known_schema or 'not in logs'})")
|
| 1166 |
|
| 1167 |
+
# Print diagnostics on timeout or error
|
| 1168 |
if result["timed_out"] or result["error"]:
|
|
|
|
|
|
|
|
|
|
| 1169 |
print_diagnostics(diag, {}) # sf already printed above
|
| 1170 |
|
| 1171 |
# Stage scoring
|
|
|
|
| 1248 |
print("β
Logged in\n")
|
| 1249 |
|
| 1250 |
for i, test_case in enumerate(suite, 1):
|
| 1251 |
+
label = {"fixed": "π", "random": "π²", "ai_generated": "π€", "custom": "βοΈ"}[test_case["type"]]
|
| 1252 |
print(f"{'β'*62}")
|
| 1253 |
print(f"[{i}/{len(suite)}] {label} {test_case['name']}")
|
| 1254 |
|
|
|
|
| 1295 |
print(f"\n{'='*62}")
|
| 1296 |
print(f" COMPLETE β Avg: {avg}/100 Grade: {grade}")
|
| 1297 |
for r in results:
|
| 1298 |
+
label = {"fixed": "π", "random": "π²", "ai_generated": "π€", "custom": "βοΈ"}[r["type"]]
|
| 1299 |
ag = r["ai_grading"]
|
| 1300 |
ds = f"{ag['data_score']}/100" if ag.get("data_score") is not None else "n/a"
|
| 1301 |
ls = f"{ag['liveboard_score']}/100" if ag.get("liveboard_score") is not None else "n/a"
|