databricks-explore 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,653 @@
1
+ """Demo mode: a fake Databricks workspace with invented data, so every real route works offline.
2
+
3
+ dbx.py swaps `WorkspaceClient` for `FakeWorkspaceClient` when EXPLORER_DEMO=1 (`databricks-explore --demo`).
4
+ Nothing here talks to a network. Everything is invented and deterministic.
5
+
6
+ Magic words in SQL (matched anywhere in the statement, case-insensitive):
7
+ demo_slow stays RUNNING for DEMO_SLOW_SECONDS (to test Cancel)
8
+ demo_error fails with a table-not-found message
9
+ demo_wide a 60-column result
10
+ demo_big 50,000 rows, so the row cap and the truncated badge show up
11
+ Otherwise the result follows the first demo table named in the SQL (orders when none is named).
12
+ The system tables system.query.history and system.access.table_lineage / column_lineage are answered by demo_history.py.
13
+ The information_schema tag tables (catalog_tags ... column_tags) answer with the few tags in TAGS.
14
+ """
15
+ import json
16
+ import random
17
+ import re
18
+ import time
19
+ import zlib
20
+ from datetime import date, datetime, timedelta, timezone
21
+ from types import SimpleNamespace
22
+
23
+ import sqlglot
24
+ from . import demo_history
25
+ from databricks.sdk.service import catalog as uc
26
+ from databricks.sdk.service import iam
27
+ from databricks.sdk.service import sql as sqlsvc
28
+
29
+ PROFILES = [{"name": "demo", "host": "https://demo.invalid", "auth_type": "demo"}]
30
+ WAREHOUSES = [("demo-wh-1", "Demo Serverless", sqlsvc.State.RUNNING),
31
+ ("demo-wh-2", "Demo Pro (stopped)", sqlsvc.State.STOPPED)]
32
+
33
+ NORMAL_DELAY = 0.5 # seconds an ordinary statement stays RUNNING, so the UI shows a timer
34
+ SLOW_DELAY = 45.0 # seconds a `demo_slow` statement stays RUNNING
35
+ CHUNK_ROWS = 1000 # rows per result chunk, so the chunked reader is exercised
36
+ BIG_ROWS = 50_000
37
+
38
+
39
+ class DemoError(Exception):
40
+ """A statement the fake warehouse rejects (surfaces as a FAILED statement)."""
41
+
42
+
43
+ # ---------------------------------------------------------------- catalog
44
+
45
+ CATALOGS = {"demo": "Invented demo data", "samples": "Invented sample datasets"}
46
+
47
+ # name, type text, comment
48
+ ORDERS = [("order_id", "bigint", "Primary key"), ("customer_id", "bigint", "Customer who placed the order"),
49
+ ("order_ts", "timestamp", None), ("order_date", "date", None), ("status", "string", None),
50
+ ("amount", "decimal(12,2)", "Order total in dollars"), ("discount_pct", "double", None),
51
+ ("is_gift", "boolean", None), ("notes", "string", "Free text, can be long"),
52
+ ("tags", "array<string>", None), ("shipping", "struct<city:string,zip:string>", None)]
53
+ CUSTOMERS = [("customer_id", "bigint", "Primary key"), ("name", "string", None), ("email", "string", None),
54
+ ("country", "string", None), ("signup_date", "date", None), ("lifetime_value", "decimal(24,6)", None)]
55
+ ITEMS = [("order_id", "bigint", None), ("line_no", "int", None), ("sku", "string", None),
56
+ ("qty", "int", None), ("unit_price", "decimal(10,2)", None)]
57
+ REVENUE = [("order_date", "date", None), ("revenue", "decimal(14,2)", None), ("orders", "bigint", None)]
58
+ EMPLOYEES = [("emp_id", "bigint", "Primary key"), ("name", "string", None), ("dept", "string", None),
59
+ ("salary", "decimal(10,2)", None), ("hired_at", "timestamp", None)]
60
+ WIDE = [("id", "bigint", None), ("event_ts", "timestamp", None)] + [
61
+ (f"metric_{i:02d}_{['latency_ms', 'throughput_per_sec', 'error_rate', 'queue_depth'][i % 4]}",
62
+ ["double", "bigint", "decimal(18,6)", "int"][i % 4], None) for i in range(1, 59)]
63
+ TRIPS = [("trip_id", "bigint", None), ("pickup_ts", "timestamp", None), ("dropoff_ts", "timestamp", None),
64
+ ("distance_mi", "double", None), ("fare", "decimal(8,2)", None), ("zone", "string", None)]
65
+
66
+ PRODUCTS = [("sku", "string", "Stock keeping unit"), ("name", "string", None), ("category", "string", None),
67
+ ("list_price", "decimal(10,2)", None)]
68
+ RETURNS = [("return_id", "bigint", "Primary key"), ("order_id", "bigint", None), ("line_no", "int", None),
69
+ ("reason", "string", None), ("returned_at", "timestamp", None)]
70
+ SHIPMENTS = [("shipment_id", "bigint", "Primary key"), ("order_id", "bigint", None), ("carrier", "string", None),
71
+ ("shipped_at", "timestamp", None)]
72
+ DEPARTMENTS = [("dept", "string", "Department name"), ("manager_id", "bigint", "Employee who runs it"),
73
+ ("budget", "decimal(12,2)", None)]
74
+
75
+ # full name -> (table type, columns, comment, row count)
76
+ TABLES = {
77
+ "demo.sales.products": ("MANAGED", PRODUCTS, "Product catalog", 300),
78
+ "demo.sales.returns": ("MANAGED", RETURNS, "Returned order lines", 400),
79
+ "demo.sales.shipments": ("MANAGED", SHIPMENTS, "Shipments per order", 2500),
80
+ "demo.hr.departments": ("MANAGED", DEPARTMENTS, "Departments", 5),
81
+ "demo.sales.orders": ("MANAGED", ORDERS, "Customer orders", 3000),
82
+ "demo.sales.customers": ("MANAGED", CUSTOMERS, "Customer master", 800),
83
+ "demo.sales.order_items": ("MANAGED", ITEMS, "Order lines", 4000),
84
+ "demo.sales.v_daily_revenue": ("VIEW", REVENUE, "Daily revenue rollup", 240),
85
+ "demo.hr.employees": ("MANAGED", EMPLOYEES, "Staff", 120),
86
+ "demo.metrics.wide_metrics": ("MANAGED", WIDE, "60-column telemetry table", 800),
87
+ "samples.nyctaxi.trips": ("EXTERNAL", TRIPS, "Sample trips", 2500),
88
+ "samples.tpch.lineitem": ("MANAGED", ITEMS, "Sample order lines", 3000),
89
+ }
90
+ # child table, parent table, child column, parent column
91
+ FOREIGN_KEYS = [("demo.sales.orders", "demo.sales.customers", "customer_id", "customer_id"),
92
+ ("demo.sales.order_items", "demo.sales.orders", "order_id", "order_id")]
93
+ VIEW_DEPS = {"demo.sales.v_daily_revenue": ["demo.sales.orders"]}
94
+ # upstream table, downstream table, producing job id
95
+ LINEAGE = [("demo.sales.orders", "demo.sales.v_daily_revenue", "1001"),
96
+ ("samples.nyctaxi.trips", "demo.metrics.wide_metrics", "1002")]
97
+ LINEAGE_SEEN = "2026-11-10T08:00:00.000Z"
98
+ # Unity Catalog tags: scope, catalog, schema, table, column, tag name, tag value (an empty value is a bare label)
99
+ TAGS = [("catalog", "demo", None, None, None, "environment", "sandbox"),
100
+ ("schema", "demo", "sales", None, None, "domain", "sales"),
101
+ ("table", "demo", "sales", "orders", None, "domain", "sales"),
102
+ ("table", "demo", "sales", "customers", None, "domain", "sales"),
103
+ ("table", "demo", "sales", "orders", None, "certified", ""),
104
+ ("table", "demo", "hr", "employees", None, "sensitivity", "confidential"),
105
+ ("column", "demo", "sales", "customers", "email", "pii", "email"),
106
+ ("column", "demo", "sales", "customers", "name", "pii", "name"),
107
+ ("column", "demo", "hr", "employees", "salary", "sensitivity", "confidential")]
108
+
109
+
110
+ def _type_name(type_text):
111
+ base = re.split(r"[(<]", type_text)[0].upper()
112
+ return {"BIGINT": "LONG"}.get(base, base)
113
+
114
+
115
+ def _table_info(full_name):
116
+ if full_name not in TABLES:
117
+ raise LookupError(f"[TABLE_OR_VIEW_NOT_FOUND] {full_name}")
118
+ kind, cols, comment, _ = TABLES[full_name]
119
+ c, s, n = full_name.split(".")
120
+ constraints = [uc.TableConstraint(primary_key_constraint=uc.PrimaryKeyConstraint(
121
+ name=f"pk_{n}", child_columns=[cols[0][0]]))] if kind == "MANAGED" and n != "wide_metrics" else []
122
+ constraints += [uc.TableConstraint(foreign_key_constraint=uc.ForeignKeyConstraint(
123
+ name=f"fk_{cn}_{pn}", child_columns=[cc], parent_table=pt, parent_columns=[pc]))
124
+ for ct, pt, cc, pc in FOREIGN_KEYS if ct == full_name for cn, pn in [(n, pt.split(".")[2])]]
125
+ deps = VIEW_DEPS.get(full_name)
126
+ return uc.TableInfo(
127
+ name=n, catalog_name=c, schema_name=s, full_name=full_name, comment=comment,
128
+ table_type=uc.TableType.VIEW if kind == "VIEW" else uc.TableType[kind],
129
+ columns=[uc.ColumnInfo(name=cn, type_text=tt, type_name=uc.ColumnTypeName[_type_name(tt)],
130
+ position=i, comment=cm, nullable=i > 0) for i, (cn, tt, cm) in enumerate(cols)],
131
+ table_constraints=constraints or None,
132
+ view_dependencies=uc.DependencyList(dependencies=[
133
+ uc.Dependency(table=uc.TableDependency(table_full_name=d)) for d in deps]) if deps else None)
134
+
135
+
136
+ # ---------------------------------------------------------------- data
137
+
138
+ STATUSES = ["shipped", "pending", "cancelled", "returned", "processing"]
139
+ CITIES = ["Zurich", "Sao Paulo", "Reykjavik", "Portland", "Osaka", "Nairobi", "Hamburg"]
140
+ COUNTRIES = ["US", "DE", "BR", "JP", "KE", "IS"]
141
+ DEPTS = ["Finance", "Support", "Logistics", "Engineering", "Marketing"]
142
+ NOTES = ["", "Leave at door", "Caller said: \"ring first\", then hung up.\nSecond line of the note.",
143
+ "Gift wrap, include a card that reads: happy birthday from everyone at the front desk. "
144
+ "Please sign for it on delivery between 9 and 5 and do not leave it in the lobby. " * 2,
145
+ "tab\tseparated\tnote", "O'Brien's order; 100% rush", "plain note"]
146
+ BIG_DECIMAL = "99999999999999999999.123456789012"
147
+
148
+
149
+ def _fmt_ts(dt):
150
+ return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{dt.microsecond // 1000:03d}Z"
151
+
152
+
153
+ def _cell(name, type_text, i, r, first):
154
+ """One invented cell as the warehouse would return it: a string, or None for NULL."""
155
+ base = re.split(r"[(<]", type_text)[0].lower()
156
+ if not first and r.random() < 0.05:
157
+ return None
158
+ if base in ("bigint", "int"):
159
+ if first:
160
+ return str(100000 + i)
161
+ return str(r.randint(1, 800) if name.endswith("_id") else r.randint(-5, 5000))
162
+ if base == "decimal":
163
+ if name == "lifetime_value" and i == 7:
164
+ return BIG_DECIMAL
165
+ scale = int(type_text.split(",")[1].rstrip(")"))
166
+ return f"{r.lognormvariate(4.2, 1.1):.{scale}f}"
167
+ if base == "double":
168
+ return str(round(r.random() * 0.3, 4)) if "pct" in name else f"{r.gauss(100, 30):.6f}"
169
+ if base == "boolean":
170
+ return r.choice(["true", "false"])
171
+ if base == "date":
172
+ return (date(2026, 1, 1) + timedelta(days=r.randint(0, 300))).isoformat()
173
+ if base == "timestamp":
174
+ dt = datetime(2026, 1, 1, tzinfo=timezone.utc) + timedelta(seconds=r.randint(0, 300 * 86400))
175
+ if r.random() < 0.4:
176
+ dt = dt.replace(microsecond=r.randint(0, 999) * 1000)
177
+ return _fmt_ts(dt)
178
+ if base == "array":
179
+ return json.dumps(r.sample(["a", "b", "vip", "promo"], r.randint(0, 3)))
180
+ if base == "struct":
181
+ return json.dumps({"city": r.choice(CITIES), "zip": str(r.randint(10000, 99999))})
182
+ if name == "status":
183
+ return r.choice(STATUSES)
184
+ if name == "notes":
185
+ return r.choice(NOTES)
186
+ if name == "email":
187
+ return f"user{i + 1}@example.invalid"
188
+ if name == "name":
189
+ return f"Customer {i + 1} {r.choice(['Holdings', 'Trading Co', 'and Sons', 'International Partners'])}"
190
+ if name == "country":
191
+ return r.choice(COUNTRIES)
192
+ if name == "dept":
193
+ return r.choice(DEPTS)
194
+ if name == "zone":
195
+ return r.choice(CITIES)
196
+ return f"{name}_{r.randint(1, 50)}"
197
+
198
+
199
+ def table_rows(full_name, n):
200
+ """n invented rows of a demo table as (columns, rows). Deterministic per table."""
201
+ cols = TABLES[full_name][1]
202
+ r = random.Random(zlib.crc32(full_name.encode()))
203
+ rows = [[_cell(cn, tt, i, r, k == 0) for k, (cn, tt, _) in enumerate(cols)] for i in range(n)]
204
+ return [{"name": cn, "type": _type_name(tt), "type_text": tt} for cn, tt, _ in cols], rows
205
+
206
+
207
+ def _daily_revenue(n):
208
+ r = random.Random(1)
209
+ cols = [{"name": "order_date", "type": "DATE"}, {"name": "status", "type": "STRING"},
210
+ {"name": "revenue", "type": "DECIMAL"}, {"name": "orders", "type": "LONG"}]
211
+ rows = [[(date(2026, 7, 1) + timedelta(days=d)).isoformat(), st, f"{r.uniform(2000, 90000):.2f}",
212
+ str(r.randint(20, 400))] for d in range(120) for st in ("shipped", "pending", "returned")]
213
+ return cols, rows[:n]
214
+
215
+
216
+ def _literal_select(sql):
217
+ """A FROM-less SELECT of literals: one row with the aliases the query chose. Falls back to a canned row."""
218
+ try:
219
+ node = sqlglot.parse_one(sql, read="databricks")
220
+ cols, row = [], []
221
+ for e in node.expressions:
222
+ lit = e.this if isinstance(e, sqlglot.exp.Alias) else e
223
+ if isinstance(lit, sqlglot.exp.Literal):
224
+ kind = "STRING" if lit.is_string else ("INT" if lit.is_int else "DOUBLE")
225
+ cols.append({"name": e.alias_or_name or f"col{len(cols) + 1}", "type": kind})
226
+ row.append(lit.name)
227
+ else:
228
+ raise ValueError("not a literal")
229
+ return cols, [row]
230
+ except Exception:
231
+ return ([{"name": "one", "type": "INT"}, {"name": "pi", "type": "DOUBLE"}, {"name": "s", "type": "STRING"}],
232
+ [["1", "3.14159", "hello"]])
233
+
234
+
235
+ def _project(sql, cols, rows):
236
+ """Honors a plain column list (`SELECT a, b AS c FROM t`) over a table's columns. Anything else passes through."""
237
+ try:
238
+ node = sqlglot.parse_one(sql, read="databricks")
239
+ if not isinstance(node, sqlglot.exp.Select) or node.args.get("with_"):
240
+ return cols, rows
241
+ names = [c["name"] for c in cols]
242
+ picks = [] # (source index, output name)
243
+ for e in node.expressions:
244
+ inner = e.this if isinstance(e, sqlglot.exp.Alias) else e
245
+ if isinstance(inner, sqlglot.exp.Star):
246
+ picks += list(enumerate(names))
247
+ elif isinstance(inner, sqlglot.exp.Column) and inner.name in names:
248
+ picks.append((names.index(inner.name), e.alias_or_name))
249
+ else:
250
+ return cols, rows
251
+ return ([{**cols[i], "name": n} for i, n in picks],
252
+ [[r[i] for i, _ in picks] for r in rows])
253
+ except Exception:
254
+ return cols, rows
255
+
256
+
257
+ def result_for(statement, params=None):
258
+ """(columns, rows) for any SQL. Raises DemoError for the `demo_error` magic word."""
259
+ low = " ".join(statement.split()).lower()
260
+ if "demo_error" in low:
261
+ raise DemoError("[TABLE_OR_VIEW_NOT_FOUND] The table or view `demo_error` cannot be found. "
262
+ "Verify the spelling and correctness of the schema and catalog. SQLSTATE: 42P01; line 1, pos 14")
263
+ limits = [int(x) for x in re.findall(r"\blimit\s+(\d+)", low)]
264
+ cap = min(limits) if limits else None
265
+ wrapped = re.match(r"^select \* from \((.*)\) limit \d+$", low) # the row-cap wrapper the Explorer adds
266
+ wrapped_sql = None
267
+ if wrapped:
268
+ low = wrapped.group(1)
269
+ inner = re.match(r"^\s*select\s+\*\s+from\s+\((.*)\)\s+limit\s+\d+\s*$", statement, re.I | re.S)
270
+ wrapped_sql = inner.group(1) if inner else low
271
+ if re.search(r"\bsystem\.(query\.history|access\.(table|column)_lineage)\b", low):
272
+ return demo_history.system_query(low, params, cap or 5000)
273
+ if re.search(r"\binformation_schema\.(catalog|schema|table|column)_tags\b", low):
274
+ cols = [{"name": k, "type": "STRING"} for k in
275
+ ("scope", "catalog_name", "schema_name", "table_name", "column_name", "tag_name", "tag_value")]
276
+ return cols, [list(t) for t in TAGS][: cap or len(TAGS)]
277
+ if "information_schema.tables" in low:
278
+ q = str((params or {}).get("q", "")).lower()
279
+ hits = [(f.split(".")[0], f.split(".")[1], f.split(".")[2], "VIEW" if v[0] == "VIEW" else "BASE TABLE")
280
+ for f, v in sorted(TABLES.items()) if q in f.split(".")[2]]
281
+ cols = [{"name": k, "type": "STRING"} for k in ("table_catalog", "table_schema", "table_name", "table_type")]
282
+ return cols, [list(h) for h in hits][: cap or 300]
283
+ if re.match(r"^(create|insert|drop|alter|use|set|optimize|vacuum|grant|merge|update|delete|truncate)\b", low):
284
+ return [], []
285
+ if re.match(r"^select\s+[\d'\"(]", low) and " from " not in low:
286
+ return _literal_select(statement if not wrapped else wrapped_sql)
287
+ if "demo_big" in low:
288
+ cols, rows = table_rows("demo.sales.orders", min(cap or BIG_ROWS, BIG_ROWS))
289
+ return cols, rows
290
+ if "demo_wide" in low:
291
+ return table_rows("demo.metrics.wide_metrics", min(cap or 800, 800))
292
+ if re.search(r"group\s+by|count\(", low):
293
+ cols, rows = _daily_revenue(cap or 360)
294
+ return cols, rows
295
+ table = next((t for t in TABLES if t in low), None) or next(
296
+ (t for t in TABLES if re.search(rf"\b{t.split('.')[2]}\b", low)), "demo.sales.orders")
297
+ cols, rows = table_rows(table, min(cap or TABLES[table][3], TABLES[table][3]))
298
+ return _project(wrapped_sql or statement, cols, rows)
299
+
300
+
301
+ # ---------------------------------------------------------------- fake SDK surface
302
+
303
+ class _Statements:
304
+ """Stands in for WorkspaceClient().statement_execution, using the real SDK response types."""
305
+
306
+ def __init__(self):
307
+ self.stmts = {}
308
+
309
+ def _delay(self, statement):
310
+ return SLOW_DELAY if "demo_slow" in statement.lower() else NORMAL_DELAY
311
+
312
+ def _state(self, st):
313
+ if st["canceled"]:
314
+ return sqlsvc.StatementState.CANCELED
315
+ if time.monotonic() - st["t0"] < st["delay"]:
316
+ return sqlsvc.StatementState.RUNNING
317
+ return sqlsvc.StatementState.FAILED if st["error"] else sqlsvc.StatementState.SUCCEEDED
318
+
319
+ def _manifest(self, st):
320
+ cols = [sqlsvc.ColumnInfo(name=c["name"], position=i, type_name=sqlsvc.ColumnInfoTypeName[c["type"]],
321
+ type_text=c.get("type_text") or c["type"].lower()) for i, c in enumerate(st["cols"])]
322
+ return sqlsvc.ResultManifest(schema=sqlsvc.ResultSchema(column_count=len(cols), columns=cols),
323
+ total_row_count=len(st["rows"]))
324
+
325
+ def _chunk(self, sid, i):
326
+ rows = self.stmts[sid]["rows"]
327
+ lo, hi = i * CHUNK_ROWS, min((i + 1) * CHUNK_ROWS, len(rows))
328
+ return sqlsvc.ResultData(chunk_index=i, data_array=rows[lo:hi], row_count=hi - lo, row_offset=lo,
329
+ next_chunk_index=i + 1 if hi < len(rows) else None)
330
+
331
+ def _response(self, sid):
332
+ st = self.stmts[sid]
333
+ state = self._state(st)
334
+ if state == sqlsvc.StatementState.FAILED:
335
+ return sqlsvc.StatementResponse(statement_id=sid, status=sqlsvc.StatementStatus(
336
+ state=state, error=sqlsvc.ServiceError(message=st["error"])))
337
+ if state != sqlsvc.StatementState.SUCCEEDED:
338
+ return sqlsvc.StatementResponse(statement_id=sid, status=sqlsvc.StatementStatus(state=state))
339
+ return sqlsvc.StatementResponse(statement_id=sid, status=sqlsvc.StatementStatus(state=state),
340
+ manifest=self._manifest(st), result=self._chunk(sid, 0))
341
+
342
+ def execute_statement(self, statement, warehouse_id, wait_timeout=None, parameters=None, **_):
343
+ params = {p.name: p.value for p in (parameters or [])}
344
+ try:
345
+ cols, rows, error = (*result_for(statement, params), None)
346
+ except DemoError as e:
347
+ cols, rows, error = [], [], str(e)
348
+ sid = f"demo-stmt-{len(self.stmts) + 1}"
349
+ self.stmts[sid] = {"cols": cols, "rows": rows, "error": error, "canceled": False,
350
+ "t0": time.monotonic(), "delay": self._delay(statement)}
351
+ wait = float(re.sub(r"\D", "", wait_timeout or "0") or 0)
352
+ if wait: # the real service holds the call open for up to wait_timeout
353
+ time.sleep(min(wait, self.stmts[sid]["delay"]))
354
+ return self._response(sid)
355
+
356
+ def get_statement(self, statement_id):
357
+ return self._response(statement_id)
358
+
359
+ def get_statement_result_chunk_n(self, statement_id, chunk_index):
360
+ return self._chunk(statement_id, chunk_index)
361
+
362
+ def cancel_execution(self, statement_id):
363
+ self.stmts[statement_id]["canceled"] = True
364
+
365
+
366
+ class _Catalogs:
367
+ def list(self):
368
+ return [uc.CatalogInfo(name=n, comment=c, catalog_type=uc.CatalogType.MANAGED_CATALOG)
369
+ for n, c in CATALOGS.items()]
370
+
371
+
372
+ class _Schemas:
373
+ def list(self, catalog_name):
374
+ names = sorted({f.split(".")[1] for f in TABLES if f.startswith(catalog_name + ".")})
375
+ return [uc.SchemaInfo(name=n, catalog_name=catalog_name, comment=f"Invented {n} schema") for n in names]
376
+
377
+
378
+ class _Tables:
379
+ def list(self, catalog_name, schema_name):
380
+ return [_table_info(f) for f in sorted(TABLES) if f.startswith(f"{catalog_name}.{schema_name}.")]
381
+
382
+ def get(self, full_name):
383
+ return _table_info(full_name)
384
+
385
+
386
+ class _Warehouses:
387
+ def list(self):
388
+ return [sqlsvc.EndpointInfo(id=i, name=n, state=s) for i, n, s in WAREHOUSES]
389
+
390
+
391
+ class _CurrentUser:
392
+ def me(self):
393
+ return iam.User(user_name="demo.user@example.invalid")
394
+
395
+
396
+ class _ApiClient:
397
+ def do(self, method, path, body=None, **_):
398
+ """Only the table-lineage endpoint exists."""
399
+ name = (body or {}).get("table_name")
400
+ ups, downs = [], []
401
+ for up, down, job in LINEAGE:
402
+ for target, other, bucket in ((down, up, ups), (up, down, downs)):
403
+ if name == target:
404
+ c, s, n = other.split(".")
405
+ bucket.append({"tableInfo": {"catalog_name": c, "schema_name": s, "name": n,
406
+ "table_type": TABLES[other][0], "lineage_timestamp": LINEAGE_SEEN},
407
+ "jobInfos": [{"job_id": job, "workspace_id": "1111111111",
408
+ "lineage_timestamp": LINEAGE_SEEN}]})
409
+ return {"upstreams": ups, "downstreams": downs}
410
+
411
+
412
+ class FakeWorkspaceClient:
413
+ """Stands in for databricks.sdk.WorkspaceClient. The profile name is ignored."""
414
+
415
+ def __init__(self, profile=None):
416
+ self.config = SimpleNamespace(host=PROFILES[0]["host"])
417
+ self.current_user = _CurrentUser()
418
+ self.warehouses = _Warehouses()
419
+ self.catalogs = _Catalogs()
420
+ self.schemas = _Schemas()
421
+ self.tables = _Tables()
422
+ self.api_client = _ApiClient()
423
+ self.statement_execution = _Statements()
424
+
425
+ def get_workspace_id(self):
426
+ return 1111111111
427
+
428
+
429
+ # ---------------------------------------------------------------- fake Genie One (the Ask tab)
430
+ # Stands in for genie.GenieClient when EXPLORER_DEMO=1. Canned answers, picked by words in the question:
431
+ # revenue revenue by status, from a view
432
+ # customer, country customers per country
433
+ # demo_error, fail the turn ends `failed`
434
+ # demo_slow stays in progress for SLOW_DELAY seconds (to test Cancel)
435
+ # Anything else counts orders. A follow-up in the same conversation gets one extra progress step.
436
+ from . import genie # noqa: E402 (kept with the Genie code so this block can be appended on its own)
437
+
438
+ GENIE_STEP_SECONDS = 1.2
439
+ GENIE_HOST = "https://demo.invalid"
440
+
441
+
442
+ def _genie_table_link(full_name):
443
+ return f"[{full_name}]({GENIE_HOST}/explore/data/{full_name.replace('.', '/')}?isDbOne=true&utm_source=one-chat)"
444
+
445
+
446
+ def _genie_plan(question):
447
+ """The canned plan for a question: progress steps, SQL, result columns and rows, answer text, or failed=True."""
448
+ q = question.lower()
449
+ if "demo_error" in q or "fail" in q:
450
+ return {"failed": True, "steps": ["Thinking: I'll look for the table you mean.",
451
+ "Thinking: running the query on the warehouse."]}
452
+ if "revenue" in q:
453
+ sql = "SELECT status, SUM(revenue) AS revenue FROM demo.sales.v_daily_revenue\nGROUP BY status ORDER BY revenue DESC"
454
+ cols = [("status", "string"), ("revenue", "decimal(14,2)")]
455
+ rows = [["shipped", "3120450.75"], ["pending", "1984210.10"], ["returned", "402318.40"]]
456
+ answer = (f"**Shipped** orders bring in the most revenue, about **$3.12M**. The numbers come from "
457
+ f"{_genie_table_link('demo.sales.v_daily_revenue')}, a view over {_genie_table_link('demo.sales.orders')}.\n\n"
458
+ "- Pending: $1.98M\n- Returned: $0.40M\n\nThe view groups revenue by day and status.")
459
+ tables = ["demo.sales.v_daily_revenue"]
460
+ elif "customer" in q or "country" in q:
461
+ sql = "SELECT country, COUNT(*) AS customers FROM demo.sales.customers\nGROUP BY country ORDER BY customers DESC"
462
+ cols = [("country", "string"), ("customers", "bigint")]
463
+ rows = [["US", "412"], ["DE", "233"], ["JP", "187"], ["BR", "96"]]
464
+ answer = (f"Most customers are in the **US** (412), then Germany (233). Source: "
465
+ f"{_genie_table_link('demo.sales.customers')}.\n\n"
466
+ "| country | customers |\n| --- | --- |\n| US | 412 |\n| DE | 233 |\n| JP | 187 |\n| BR | 96 |")
467
+ tables = ["demo.sales.customers"]
468
+ else:
469
+ sql = "SELECT COUNT(*) AS order_count FROM demo.sales.orders"
470
+ cols = [("order_count", "bigint")]
471
+ rows = [[str(TABLES["demo.sales.orders"][3])]]
472
+ answer = (f"The {_genie_table_link('demo.sales.orders')} table has **{rows[0][0]} rows**. "
473
+ "It is described as \"One row per order\".")
474
+ tables = ["demo.sales.orders"]
475
+ steps = ["Thinking: I'll find the table that matches your question.",
476
+ f"Thinking: I found {', '.join(tables)}. I'll write a query for it.",
477
+ "Thinking: the query ran, so I can answer now."]
478
+ return {"failed": False, "steps": steps, "sql": sql, "cols": cols, "rows": rows, "answer": answer}
479
+
480
+
481
+ class FakeGenie:
482
+ """Same four calls as genie.GenieClient, answered from canned plans, with progress that advances over time."""
483
+
484
+ def __init__(self):
485
+ self.warehouse_id = None
486
+ self.convs = {} # conversation id -> turns asked
487
+ self.turns = {} # response id -> {conv, plan, t0, delay, canceled}
488
+
489
+ def ask(self, question, conversation_id=None):
490
+ """Start a turn. A conversation id that was never issued here starts a new conversation."""
491
+ cid = conversation_id if conversation_id in self.convs else f"demo-conv-{len(self.convs) + 1}"
492
+ self.convs[cid] = self.convs.get(cid, 0) + 1
493
+ rid = f"demo-resp-{len(self.turns) + 1}"
494
+ plan = _genie_plan(question)
495
+ if self.convs[cid] > 1:
496
+ plan["steps"] = [f"Thinking: continuing the conversation (question {self.convs[cid]})."] + plan["steps"]
497
+ delay = SLOW_DELAY if "demo_slow" in question.lower() else GENIE_STEP_SECONDS * len(plan["steps"])
498
+ self.turns[rid] = {"conv": cid, "plan": plan, "t0": time.monotonic(), "delay": delay, "canceled": False}
499
+ return {"conversation_id": cid, "response_id": rid, "status": "in_progress"}
500
+
501
+ def _turn(self, conversation_id, response_id):
502
+ t = self.turns.get(response_id)
503
+ if not t or t["conv"] != conversation_id:
504
+ raise genie.GenieError("Unknown conversation or response.", 502)
505
+ return t
506
+
507
+ def poll(self, conversation_id, response_id):
508
+ """One poll: more progress steps each time, then completed (or failed) once the delay has passed. Like the real server, a finished poll carries no steps."""
509
+ t = self._turn(conversation_id, response_id)
510
+ plan, elapsed = t["plan"], time.monotonic() - t["t0"]
511
+ sc = {"conversation_id": conversation_id, "response_id": response_id,
512
+ "deep_link": f"{GENIE_HOST}/one/chat/threads/{conversation_id}"}
513
+ if t["canceled"]:
514
+ return genie.shape_poll({**sc, "status": "incomplete", "progress_steps": plan["steps"][:1]})
515
+ if elapsed < t["delay"]:
516
+ shown = min(len(plan["steps"]), int(elapsed / max(GENIE_STEP_SECONDS, 1e-6)) + 1)
517
+ return genie.shape_poll({**sc, "status": "in_progress", "progress_steps": plan["steps"][:shown]})
518
+ if plan["failed"]:
519
+ return genie.shape_poll({**sc, "status": "failed",
520
+ "message": "The query could not run: the table Genie picked does not exist."})
521
+ return genie.shape_poll({**sc, "status": "completed", "final_answer": plan["answer"],
522
+ "query_items": [{"item_id": f"{response_id}-item-1", "sql": plan["sql"]}]})
523
+
524
+ def query_result(self, conversation_id, response_id, item_id):
525
+ """The canned rows of the turn's one SQL item."""
526
+ t = self._turn(conversation_id, response_id)
527
+ if item_id != f"{response_id}-item-1" or t["plan"]["failed"]:
528
+ raise genie.GenieError("Unknown query item.", 502)
529
+ plan = t["plan"]
530
+ return genie.shape_result({"item_id": item_id, "statement_state": "SUCCEEDED", "ready": True,
531
+ "columns": [{"name": n, "type_text": tt} for n, tt in plan["cols"]],
532
+ "rows": plan["rows"], "total_row_count": len(plan["rows"]), "truncated": False})
533
+
534
+ def cancel(self, conversation_id):
535
+ """Mark every turn of the conversation canceled."""
536
+ for t in self.turns.values():
537
+ if t["conv"] == conversation_id:
538
+ t["canceled"] = True
539
+ return {"conversation_id": conversation_id, "cancel_requested": True}
540
+
541
+ # ---------------------------------------------------------------- ontology import fakes (domains, pages, a metric view, certification)
542
+ # Genie Ontology domains and pages over REST, one metric view whose YAML the tables API returns, and the certification tag.
543
+
544
+ METRIC_VIEW = "demo.sales.sales_metrics"
545
+ # Like the real tables API, the YAML it returns has lost display_name, comment and synonyms. Those come from the
546
+ # warehouse: DESCRIBE TABLE EXTENDED <view> AS JSON (METRIC_VIEW_META below).
547
+ METRIC_VIEW_YAML = """version: 1.1
548
+ source: demo.sales.orders
549
+ dimensions:
550
+ - name: order_month
551
+ expr: date_trunc('month', order_date)
552
+ - name: status
553
+ expr: status
554
+ measures:
555
+ - name: total_revenue
556
+ expr: SUM(amount)
557
+ - name: order_count
558
+ expr: COUNT(1)
559
+ """
560
+ METRIC_VIEW_COLUMNS = [("order_month", "date", "Calendar month in which the order was placed"),
561
+ ("status", "string", "Fulfillment status of the order"),
562
+ ("total_revenue", "decimal(14,2)", "Sum of order amounts in dollars"),
563
+ ("order_count", "bigint", "Number of orders")]
564
+ # column -> (metric_view.type, display_name or None, synonyms)
565
+ METRIC_VIEW_META = {"order_month": ("dimension", "Order month", ["month", "period"]),
566
+ "status": ("dimension", None, []),
567
+ "total_revenue": ("measure", "Total revenue", ["sales", "turnover"]),
568
+ "order_count": ("measure", "Order count", [])}
569
+ TABLES[METRIC_VIEW] = ("VIEW", METRIC_VIEW_COLUMNS, "Sales metric view", 240)
570
+
571
+
572
+ def _metric_view_describe():
573
+ """The one JSON string DESCRIBE TABLE EXTENDED ... AS JSON returns for the metric view."""
574
+ columns = []
575
+ for name, type_text, comment in METRIC_VIEW_COLUMNS:
576
+ _, label, synonyms = METRIC_VIEW_META[name]
577
+ meta = ({"display_name": label} if label else {}) | ({"synonyms": synonyms} if synonyms else {})
578
+ columns.append({"name": name, "type": {"name": type_text.split("(")[0]}, "nullable": True, "comment": comment,
579
+ **({"metadata": meta} if meta else {})})
580
+ return json.dumps({"table_name": "sales_metrics", "catalog_name": "demo", "schema_name": "sales",
581
+ "columns": columns, "comment": "Sales metric view", "view_text": METRIC_VIEW_YAML})
582
+
583
+ ONTOLOGY_DOMAINS = [
584
+ {"domain_id": "d-sales", "tag_key": "sales", "display_name": "Sales", "description": "Orders and customers"},
585
+ {"domain_id": "d-people", "tag_key": "people", "description": "Staff and departments"}]
586
+ ONTOLOGY_PAGES = [
587
+ {"name": "knowledge/pages/customer", "domain": "domains/d-sales", "display_name": "Customer",
588
+ "description": "A person or company that has placed at least one order.",
589
+ "body": "## Notes\n\nA customer is identified by `customer_id`.\nGuests who never create an account are not customers.",
590
+ "synonyms": ["client", "buyer"], "state": "PUBLISHED"},
591
+ {"name": "knowledge/pages/active-customer", "domain": "domains/d-sales", "display_name": "Active customer",
592
+ "description": "A customer with an order in the last 90 days.", "body": "", "synonyms": ["recent buyer"],
593
+ "state": "DRAFT"},
594
+ {"name": "knowledge/pages/headcount", "domain": "domains/d-people", "display_name": "Headcount",
595
+ "description": "Employees on the payroll at the end of a month.", "body": "", "synonyms": [], "state": "PUBLISHED"}]
596
+ ONTOLOGY_PAGE_SIZE = 2 # so the importer has to follow next_page_token
597
+ # scope, catalog, schema, table, column, value of the system.certification_status tag
598
+ CERTIFICATION = [("table", "demo", "sales", "orders", None, "certified"),
599
+ ("table", "demo", "sales", "customers", None, "certified"),
600
+ ("table", "demo", "sales", "v_daily_revenue", None, "deprecated"),
601
+ ("table", "demo", "sales", "sales_metrics", None, "certified"),
602
+ ("column", "demo", "sales", "customers", "email", "certified"),
603
+ ("column", "demo", "sales", "orders", "discount_pct", "deprecated")]
604
+ TAGS.extend((s, c, sc, t, col, "system.certification_status", v) for s, c, sc, t, col, v in CERTIFICATION)
605
+
606
+ _table_info_before_ontology = _table_info
607
+ _api_do_before_ontology = _ApiClient.do
608
+ _result_for_before_ontology = result_for
609
+
610
+
611
+ def _table_info(full_name): # noqa: F811 - wraps the function above; the fake tables API looks it up by name
612
+ """The table's info, with the metric view's YAML as its view_definition and each column's metric_view.type metadata."""
613
+ info = _table_info_before_ontology(full_name)
614
+ if full_name == METRIC_VIEW:
615
+ info.view_definition = METRIC_VIEW_YAML
616
+ for col in info.columns:
617
+ col.type_json = json.dumps({"name": col.name, "nullable": True, "metadata": {
618
+ "metric_view.type": METRIC_VIEW_META[col.name][0]}})
619
+ return info
620
+
621
+
622
+ def _ontology_page(items, query, key):
623
+ start = int((query or {}).get("page_token") or 0)
624
+ out = {key: items[start:start + ONTOLOGY_PAGE_SIZE]}
625
+ if start + ONTOLOGY_PAGE_SIZE < len(items):
626
+ out["next_page_token"] = str(start + ONTOLOGY_PAGE_SIZE)
627
+ return out
628
+
629
+
630
+ def _api_do_with_ontology(self, method, path, body=None, query=None, **kw):
631
+ if path == "/api/2.0/domains":
632
+ return {"domains": ONTOLOGY_DOMAINS}
633
+ if path == "/api/2.0/knowledge/pages":
634
+ return _ontology_page(ONTOLOGY_PAGES, query, "pages")
635
+ return _api_do_before_ontology(self, method, path, body, **kw)
636
+
637
+
638
+ _ApiClient.do = _api_do_with_ontology
639
+
640
+
641
+ def result_for(statement, params=None): # noqa: F811 - wraps the function above; the fake warehouse looks it up by name
642
+ """(columns, rows) as before, plus the metric view list and the certification tag rows."""
643
+ low = " ".join(statement.split()).lower()
644
+ if low == "describe table extended `demo`.`sales`.`sales_metrics` as json":
645
+ return [{"name": "json_metadata", "type": "STRING"}], [[_metric_view_describe()]]
646
+ if "table_type = 'metric_view'" in low:
647
+ cols = [{"name": k, "type": "STRING"} for k in ("table_catalog", "table_schema", "table_name")]
648
+ return cols, [METRIC_VIEW.split(".")]
649
+ if "where tag_name = 'system.certification_status'" in low:
650
+ cols = [{"name": k, "type": "STRING"} for k in
651
+ ("scope", "catalog_name", "schema_name", "table_name", "column_name", "tag_value")]
652
+ return cols, [list(r) for r in CERTIFICATION]
653
+ return _result_for_before_ontology(statement, params)