clickzetta-semantic-model-generator 1.0.1__py3-none-any.whl → 1.0.3__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.
Files changed (22) hide show
  1. {clickzetta_semantic_model_generator-1.0.1.dist-info → clickzetta_semantic_model_generator-1.0.3.dist-info}/METADATA +5 -5
  2. {clickzetta_semantic_model_generator-1.0.1.dist-info → clickzetta_semantic_model_generator-1.0.3.dist-info}/RECORD +22 -19
  3. semantic_model_generator/clickzetta_utils/clickzetta_connector.py +91 -33
  4. semantic_model_generator/clickzetta_utils/env_vars.py +7 -2
  5. semantic_model_generator/data_processing/cte_utils.py +1 -1
  6. semantic_model_generator/generate_model.py +588 -224
  7. semantic_model_generator/llm/dashscope_client.py +4 -2
  8. semantic_model_generator/llm/enrichment.py +144 -57
  9. semantic_model_generator/llm/progress_tracker.py +16 -15
  10. semantic_model_generator/relationships/__init__.py +15 -0
  11. semantic_model_generator/relationships/discovery.py +202 -0
  12. semantic_model_generator/tests/clickzetta_connector_test.py +3 -7
  13. semantic_model_generator/tests/cte_utils_test.py +1 -1
  14. semantic_model_generator/tests/generate_model_classification_test.py +12 -2
  15. semantic_model_generator/tests/llm_enrichment_test.py +152 -46
  16. semantic_model_generator/tests/relationship_discovery_test.py +114 -0
  17. semantic_model_generator/tests/relationships_filters_test.py +166 -30
  18. semantic_model_generator/tests/utils_test.py +1 -1
  19. semantic_model_generator/validate/keywords.py +453 -53
  20. semantic_model_generator/validate/schema.py +4 -2
  21. {clickzetta_semantic_model_generator-1.0.1.dist-info → clickzetta_semantic_model_generator-1.0.3.dist-info}/LICENSE +0 -0
  22. {clickzetta_semantic_model_generator-1.0.1.dist-info → clickzetta_semantic_model_generator-1.0.3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,202 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from dataclasses import dataclass
5
+ from typing import Any, Iterable, List, Optional, Sequence, Tuple
6
+
7
+ import pandas as pd
8
+ from loguru import logger
9
+
10
+ from semantic_model_generator.clickzetta_utils.clickzetta_connector import (
11
+ _TABLE_NAME_COL,
12
+ get_table_representation,
13
+ get_valid_schemas_tables_columns_df,
14
+ )
15
+ from semantic_model_generator.data_processing import data_types
16
+ from semantic_model_generator.data_processing.data_types import FQNParts, Table
17
+ from semantic_model_generator.generate_model import (
18
+ _DEFAULT_N_SAMPLE_VALUES_PER_COL,
19
+ _infer_relationships,
20
+ )
21
+ from semantic_model_generator.protos import semantic_model_pb2
22
+
23
+ try: # pragma: no cover - optional dependency for type checking
24
+ from clickzetta.zettapark.session import Session
25
+ except Exception: # pragma: no cover
26
+ Session = Any # type: ignore
27
+
28
+ DEFAULT_MAX_WORKERS = 4
29
+
30
+
31
+ @dataclass
32
+ class RelationshipSummary:
33
+ total_tables: int
34
+ total_columns: int
35
+ total_relationships_found: int
36
+ processing_time_ms: int
37
+
38
+
39
+ @dataclass
40
+ class RelationshipDiscoveryResult:
41
+ relationships: List[semantic_model_pb2.Relationship]
42
+ tables: List[Table]
43
+ summary: RelationshipSummary
44
+
45
+
46
+ def _normalize_table_names(table_names: Optional[Iterable[str]]) -> Optional[List[str]]:
47
+ if table_names is None:
48
+ return None
49
+ return [name.upper() for name in table_names]
50
+
51
+
52
+ def _build_tables_from_dataframe(
53
+ session: Session,
54
+ workspace: str,
55
+ schema: str,
56
+ columns_df: pd.DataFrame,
57
+ sample_values_per_column: int,
58
+ max_workers: int = DEFAULT_MAX_WORKERS,
59
+ ) -> List[Tuple[FQNParts, Table]]:
60
+ if columns_df.empty:
61
+ return []
62
+
63
+ if _TABLE_NAME_COL not in columns_df.columns:
64
+ raise KeyError(
65
+ f"Expected '{_TABLE_NAME_COL}' column in metadata dataframe. "
66
+ "Ensure information_schema query returned table names."
67
+ )
68
+
69
+ table_order = (
70
+ columns_df[_TABLE_NAME_COL].astype(str).str.upper().drop_duplicates().tolist()
71
+ )
72
+
73
+ tables: List[Tuple[FQNParts, Table]] = []
74
+ for idx, table_name in enumerate(table_order):
75
+ table_columns_df = columns_df[columns_df[_TABLE_NAME_COL] == table_name]
76
+ if table_columns_df.empty:
77
+ continue
78
+
79
+ max_workers_for_table = min(max_workers, len(table_columns_df.index) or 1)
80
+ table_proto = get_table_representation(
81
+ session=session,
82
+ workspace=workspace,
83
+ schema_name=schema,
84
+ table_name=table_name,
85
+ table_index=idx,
86
+ ndv_per_column=sample_values_per_column,
87
+ columns_df=table_columns_df,
88
+ max_workers=max_workers_for_table,
89
+ )
90
+ tables.append(
91
+ (
92
+ FQNParts(database=workspace, schema_name=schema, table=table_name),
93
+ table_proto,
94
+ )
95
+ )
96
+
97
+ return tables
98
+
99
+
100
+ def _discover_relationships(
101
+ raw_tables: List[Tuple[FQNParts, Table]],
102
+ strict_join_inference: bool,
103
+ session: Optional[Session],
104
+ ) -> List[semantic_model_pb2.Relationship]:
105
+ if not raw_tables:
106
+ return []
107
+
108
+ relationships = _infer_relationships(
109
+ raw_tables,
110
+ session=session if strict_join_inference else None,
111
+ strict_join_inference=strict_join_inference,
112
+ )
113
+ return relationships
114
+
115
+
116
+ def discover_relationships_from_tables(
117
+ tables: Sequence[Tuple[FQNParts, Table]],
118
+ *,
119
+ strict_join_inference: bool = False,
120
+ session: Optional[Session] = None,
121
+ ) -> RelationshipDiscoveryResult:
122
+ """
123
+ Run relationship inference using pre-constructed table metadata.
124
+ """
125
+ start = time.perf_counter()
126
+ relationships = _discover_relationships(
127
+ list(tables),
128
+ strict_join_inference=strict_join_inference,
129
+ session=session,
130
+ )
131
+ end = time.perf_counter()
132
+
133
+ all_columns = sum(len(table.columns) for _, table in tables)
134
+ summary = RelationshipSummary(
135
+ total_tables=len(tables),
136
+ total_columns=all_columns,
137
+ total_relationships_found=len(relationships),
138
+ processing_time_ms=int((end - start) * 1000),
139
+ )
140
+
141
+ return RelationshipDiscoveryResult(
142
+ relationships=relationships,
143
+ tables=[table for _, table in tables],
144
+ summary=summary,
145
+ )
146
+
147
+
148
+ def discover_relationships_from_schema(
149
+ session: Session,
150
+ workspace: str,
151
+ schema: str,
152
+ *,
153
+ table_names: Optional[Sequence[str]] = None,
154
+ sample_values_per_column: int = _DEFAULT_N_SAMPLE_VALUES_PER_COL,
155
+ strict_join_inference: bool = False,
156
+ max_workers: int = DEFAULT_MAX_WORKERS,
157
+ ) -> RelationshipDiscoveryResult:
158
+ """
159
+ Discover table relationships for all tables in a ClickZetta schema.
160
+ """
161
+ normalized_tables = _normalize_table_names(table_names)
162
+
163
+ metadata_df = get_valid_schemas_tables_columns_df(
164
+ session=session,
165
+ workspace=workspace,
166
+ table_schema=schema,
167
+ table_names=normalized_tables,
168
+ )
169
+ metadata_df.columns = [str(col).upper() for col in metadata_df.columns]
170
+
171
+ if metadata_df.empty:
172
+ logger.warning(
173
+ "No column metadata found for workspace=%s schema=%s tables=%s",
174
+ workspace,
175
+ schema,
176
+ table_names,
177
+ )
178
+ return RelationshipDiscoveryResult(
179
+ relationships=[],
180
+ tables=[],
181
+ summary=RelationshipSummary(
182
+ total_tables=0,
183
+ total_columns=0,
184
+ total_relationships_found=0,
185
+ processing_time_ms=0,
186
+ ),
187
+ )
188
+
189
+ raw_tables = _build_tables_from_dataframe(
190
+ session=session,
191
+ workspace=workspace,
192
+ schema=schema,
193
+ columns_df=metadata_df,
194
+ sample_values_per_column=sample_values_per_column,
195
+ max_workers=max_workers,
196
+ )
197
+
198
+ return discover_relationships_from_tables(
199
+ raw_tables,
200
+ strict_join_inference=strict_join_inference,
201
+ session=session,
202
+ )
@@ -3,15 +3,13 @@ from unittest import mock
3
3
 
4
4
  import pandas as pd
5
5
 
6
- from semantic_model_generator.clickzetta_utils import env_vars
7
6
  from semantic_model_generator.clickzetta_utils import clickzetta_connector as connector
7
+ from semantic_model_generator.clickzetta_utils import env_vars
8
8
 
9
9
 
10
10
  def test_fetch_stages_includes_user_volume(monkeypatch):
11
11
  data = pd.DataFrame({"name": ["shared_stage"]})
12
- with mock.patch.object(
13
- connector, "_execute_query_to_pandas", return_value=data
14
- ):
12
+ with mock.patch.object(connector, "_execute_query_to_pandas", return_value=data):
15
13
  stages = connector.fetch_stages_in_schema(
16
14
  connection=mock.MagicMock(), schema_name="WORKSPACE.SCHEMA"
17
15
  )
@@ -29,9 +27,7 @@ def test_fetch_yaml_names_in_user_volume(monkeypatch):
29
27
  ]
30
28
  }
31
29
  )
32
- with mock.patch.object(
33
- connector, "_execute_query_to_pandas", return_value=data
34
- ):
30
+ with mock.patch.object(connector, "_execute_query_to_pandas", return_value=data):
35
31
  files = connector.fetch_yaml_names_in_stage(
36
32
  connection=mock.MagicMock(),
37
33
  stage="volume:user://~/semantic_models/",
@@ -4,10 +4,10 @@ import pytest
4
4
  import sqlglot
5
5
 
6
6
  from semantic_model_generator.data_processing.cte_utils import (
7
+ ClickzettaDialect,
7
8
  _enrich_column_in_expr_with_aggregation,
8
9
  _get_col_expr,
9
10
  _validate_col,
10
- ClickzettaDialect,
11
11
  context_to_column_format,
12
12
  expand_all_logical_tables_as_ctes,
13
13
  generate_select,
@@ -31,8 +31,18 @@ def test_string_date_promoted_to_time_dimension() -> None:
31
31
  id_=0,
32
32
  name="ORDERS",
33
33
  columns=[
34
- Column(id_=0, column_name="order_date", column_type="STRING", values=["2024-01-01", "2024-02-01"]),
35
- Column(id_=1, column_name="order_status", column_type="STRING", values=["OPEN", "CLOSED"]),
34
+ Column(
35
+ id_=0,
36
+ column_name="order_date",
37
+ column_type="STRING",
38
+ values=["2024-01-01", "2024-02-01"],
39
+ ),
40
+ Column(
41
+ id_=1,
42
+ column_name="order_status",
43
+ column_type="STRING",
44
+ values=["OPEN", "CLOSED"],
45
+ ),
36
46
  ],
37
47
  )
38
48
 
@@ -1,6 +1,5 @@
1
1
  import json
2
2
 
3
- from semantic_model_generator import generate_model
4
3
  from semantic_model_generator.data_processing.data_types import Column, FQNParts, Table
5
4
  from semantic_model_generator.llm.dashscope_client import DashscopeResponse
6
5
  from semantic_model_generator.llm.enrichment import enrich_semantic_model
@@ -16,9 +15,16 @@ class _FakeDashscopeClient:
16
15
  self._index = 0
17
16
 
18
17
  def chat_completion(self, messages): # type: ignore[no-untyped-def]
19
- payload = self._payloads[self._index] if self._index < len(self._payloads) else self._payloads[-1]
18
+ payload = (
19
+ self._payloads[self._index]
20
+ if self._index < len(self._payloads)
21
+ else self._payloads[-1]
22
+ )
20
23
  self._index += 1
21
- return DashscopeResponse(content=json.dumps(payload, ensure_ascii=False), request_id=f"test_{self._index}")
24
+ return DashscopeResponse(
25
+ content=json.dumps(payload, ensure_ascii=False),
26
+ request_id=f"test_{self._index}",
27
+ )
22
28
 
23
29
 
24
30
  def test_enrich_semantic_model_populates_descriptions_and_synonyms() -> None:
@@ -26,15 +32,27 @@ def test_enrich_semantic_model_populates_descriptions_and_synonyms() -> None:
26
32
  id_=0,
27
33
  name="orders",
28
34
  columns=[
29
- Column(id_=0, column_name="order_status", column_type="STRING", values=["OPEN", "CLOSED"]),
30
- Column(id_=1, column_name="total_amount", column_type="NUMBER", values=["12.5", "18.3"]),
35
+ Column(
36
+ id_=0,
37
+ column_name="order_status",
38
+ column_type="STRING",
39
+ values=["OPEN", "CLOSED"],
40
+ ),
41
+ Column(
42
+ id_=1,
43
+ column_name="total_amount",
44
+ column_type="NUMBER",
45
+ values=["12.5", "18.3"],
46
+ ),
31
47
  ],
32
48
  )
33
49
 
34
50
  table_proto = semantic_model_pb2.Table(
35
51
  name="ORDERS",
36
52
  description=" ",
37
- base_table=semantic_model_pb2.FullyQualifiedTable(database="SALES", schema="PUBLIC", table="ORDERS"),
53
+ base_table=semantic_model_pb2.FullyQualifiedTable(
54
+ database="SALES", schema="PUBLIC", table="ORDERS"
55
+ ),
38
56
  dimensions=[
39
57
  semantic_model_pb2.Dimension(
40
58
  name="order_status",
@@ -98,16 +116,18 @@ def test_enrich_semantic_model_populates_descriptions_and_synonyms() -> None:
98
116
  }
99
117
  ],
100
118
  "filters": [
101
- {
102
- "name": "order_status_include_values",
103
- "description": "Limit the result set to a sample of order statuses.",
104
- "synonyms": ["Order status filter"],
105
- }
119
+ {
120
+ "name": "order_status_include_values",
121
+ "description": "Limit the result set to a sample of order statuses.",
122
+ "synonyms": ["Order status filter"],
123
+ }
106
124
  ],
107
125
  "model_description": "Semantic model for customer orders and related metrics.",
108
126
  }
109
127
 
110
- client = _FakeDashscopeClient([fake_response, {"model_metrics": []}, {"verified_queries": []}])
128
+ client = _FakeDashscopeClient(
129
+ [fake_response, {"model_metrics": []}, {"verified_queries": []}]
130
+ )
111
131
  enrich_semantic_model(
112
132
  model,
113
133
  [(FQNParts(database="SALES", schema_name="PUBLIC", table="ORDERS"), raw_table)],
@@ -116,7 +136,10 @@ def test_enrich_semantic_model_populates_descriptions_and_synonyms() -> None:
116
136
  )
117
137
 
118
138
  table = model.tables[0]
119
- assert table.description == "Orders fact table that records order status and total amount."
139
+ assert (
140
+ table.description
141
+ == "Orders fact table that records order status and total amount."
142
+ )
120
143
 
121
144
  dimension = next(dim for dim in table.dimensions if dim.expr == "order_status")
122
145
  assert dimension.description == "Current execution status for each order."
@@ -126,8 +149,12 @@ def test_enrich_semantic_model_populates_descriptions_and_synonyms() -> None:
126
149
  assert fact.description == "Order total including taxes."
127
150
  assert "Order total" in list(fact.synonyms)
128
151
 
129
- filter_obj = next(flt for flt in table.filters if flt.name == "order_status_include_values")
130
- assert filter_obj.description == "Limit the result set to a sample of order statuses."
152
+ filter_obj = next(
153
+ flt for flt in table.filters if flt.name == "order_status_include_values"
154
+ )
155
+ assert (
156
+ filter_obj.description == "Limit the result set to a sample of order statuses."
157
+ )
131
158
  assert "Order status filter" in list(filter_obj.synonyms)
132
159
 
133
160
  assert len(table.metrics) == 1
@@ -135,10 +162,15 @@ def test_enrich_semantic_model_populates_descriptions_and_synonyms() -> None:
135
162
  assert metric.name.startswith("gmv")
136
163
  assert metric.expr == "SUM(total_amount)"
137
164
  assert "GMV" in list(metric.synonyms)
138
- assert metric.description == "Based on total_amount and used as gross merchandise value."
165
+ assert (
166
+ metric.description
167
+ == "Based on total_amount and used as gross merchandise value."
168
+ )
139
169
 
140
170
  assert model.custom_instructions == ""
141
- assert model.description == "Semantic model for customer orders and related metrics."
171
+ assert (
172
+ model.description == "Semantic model for customer orders and related metrics."
173
+ )
142
174
 
143
175
 
144
176
  class _FakeSession:
@@ -160,8 +192,15 @@ def test_enrich_semantic_model_generates_model_metrics_and_verified_queries() ->
160
192
  id_=0,
161
193
  name="orders",
162
194
  columns=[
163
- Column(id_=0, column_name="order_id", column_type="NUMBER", values=["1", "2"]),
164
- Column(id_=1, column_name="total_amount", column_type="NUMBER", values=["10", "20"]),
195
+ Column(
196
+ id_=0, column_name="order_id", column_type="NUMBER", values=["1", "2"]
197
+ ),
198
+ Column(
199
+ id_=1,
200
+ column_name="total_amount",
201
+ column_type="NUMBER",
202
+ values=["10", "20"],
203
+ ),
165
204
  ],
166
205
  )
167
206
 
@@ -169,15 +208,21 @@ def test_enrich_semantic_model_generates_model_metrics_and_verified_queries() ->
169
208
  id_=1,
170
209
  name="payments",
171
210
  columns=[
172
- Column(id_=0, column_name="payment_id", column_type="NUMBER", values=["1", "2"]),
173
- Column(id_=1, column_name="amount", column_type="NUMBER", values=["5", "15"]),
211
+ Column(
212
+ id_=0, column_name="payment_id", column_type="NUMBER", values=["1", "2"]
213
+ ),
214
+ Column(
215
+ id_=1, column_name="amount", column_type="NUMBER", values=["5", "15"]
216
+ ),
174
217
  ],
175
218
  )
176
219
 
177
220
  orders_proto = semantic_model_pb2.Table(
178
221
  name="ORDERS",
179
222
  description=" ",
180
- base_table=semantic_model_pb2.FullyQualifiedTable(database="SALES", schema="PUBLIC", table="ORDERS"),
223
+ base_table=semantic_model_pb2.FullyQualifiedTable(
224
+ database="SALES", schema="PUBLIC", table="ORDERS"
225
+ ),
181
226
  facts=[
182
227
  semantic_model_pb2.Fact(
183
228
  name="total_amount",
@@ -191,7 +236,9 @@ def test_enrich_semantic_model_generates_model_metrics_and_verified_queries() ->
191
236
  payments_proto = semantic_model_pb2.Table(
192
237
  name="PAYMENTS",
193
238
  description=" ",
194
- base_table=semantic_model_pb2.FullyQualifiedTable(database="SALES", schema="PUBLIC", table="PAYMENTS"),
239
+ base_table=semantic_model_pb2.FullyQualifiedTable(
240
+ database="SALES", schema="PUBLIC", table="PAYMENTS"
241
+ ),
195
242
  facts=[
196
243
  semantic_model_pb2.Fact(
197
244
  name="amount",
@@ -202,7 +249,9 @@ def test_enrich_semantic_model_generates_model_metrics_and_verified_queries() ->
202
249
  ],
203
250
  )
204
251
 
205
- model = semantic_model_pb2.SemanticModel(name="Orders Model", tables=[orders_proto, payments_proto])
252
+ model = semantic_model_pb2.SemanticModel(
253
+ name="Orders Model", tables=[orders_proto, payments_proto]
254
+ )
206
255
 
207
256
  table_payload = {
208
257
  "table_description": "Orders fact table with totals.",
@@ -253,22 +302,32 @@ def test_enrich_semantic_model_generates_model_metrics_and_verified_queries() ->
253
302
  }
254
303
 
255
304
  # Model description response for when _summarize_model_description is called
256
- model_description_payload = "This is an orders model for tracking sales and payments."
257
-
258
- client = _FakeDashscopeClient([
259
- table_payload,
260
- table_payload_payments,
261
- model_description_payload,
262
- model_metrics_payload,
263
- verified_queries_payload,
264
- ])
305
+ model_description_payload = (
306
+ "This is an orders model for tracking sales and payments."
307
+ )
308
+
309
+ client = _FakeDashscopeClient(
310
+ [
311
+ table_payload,
312
+ table_payload_payments,
313
+ model_description_payload,
314
+ model_metrics_payload,
315
+ verified_queries_payload,
316
+ ]
317
+ )
265
318
  session = _FakeSession()
266
319
 
267
320
  enrich_semantic_model(
268
321
  model,
269
322
  [
270
- (FQNParts(database="SALES", schema_name="PUBLIC", table="ORDERS"), raw_orders),
271
- (FQNParts(database="SALES", schema_name="PUBLIC", table="PAYMENTS"), raw_payments),
323
+ (
324
+ FQNParts(database="SALES", schema_name="PUBLIC", table="ORDERS"),
325
+ raw_orders,
326
+ ),
327
+ (
328
+ FQNParts(database="SALES", schema_name="PUBLIC", table="PAYMENTS"),
329
+ raw_payments,
330
+ ),
272
331
  ],
273
332
  client,
274
333
  placeholder=" ",
@@ -297,15 +356,24 @@ def test_model_metrics_generated_with_single_fact_table() -> None:
297
356
  id_=0,
298
357
  name="orders",
299
358
  columns=[
300
- Column(id_=0, column_name="order_id", column_type="NUMBER", values=["1", "2"]),
301
- Column(id_=1, column_name="total_amount", column_type="NUMBER", values=["10", "20"]),
359
+ Column(
360
+ id_=0, column_name="order_id", column_type="NUMBER", values=["1", "2"]
361
+ ),
362
+ Column(
363
+ id_=1,
364
+ column_name="total_amount",
365
+ column_type="NUMBER",
366
+ values=["10", "20"],
367
+ ),
302
368
  ],
303
369
  )
304
370
 
305
371
  orders_proto = semantic_model_pb2.Table(
306
372
  name="ORDERS",
307
373
  description=" ",
308
- base_table=semantic_model_pb2.FullyQualifiedTable(database="SALES", schema="PUBLIC", table="ORDERS"),
374
+ base_table=semantic_model_pb2.FullyQualifiedTable(
375
+ database="SALES", schema="PUBLIC", table="ORDERS"
376
+ ),
309
377
  facts=[
310
378
  semantic_model_pb2.Fact(
311
379
  name="total_amount",
@@ -341,12 +409,24 @@ def test_model_metrics_generated_with_single_fact_table() -> None:
341
409
  # Model description response for when _summarize_model_description is called
342
410
  model_description_payload = "This is an orders model for tracking order metrics."
343
411
 
344
- client = _FakeDashscopeClient([table_payload, model_description_payload, model_metrics_payload, verified_queries_payload])
412
+ client = _FakeDashscopeClient(
413
+ [
414
+ table_payload,
415
+ model_description_payload,
416
+ model_metrics_payload,
417
+ verified_queries_payload,
418
+ ]
419
+ )
345
420
  session = _FakeSession()
346
421
 
347
422
  enrich_semantic_model(
348
423
  model,
349
- [(FQNParts(database="SALES", schema_name="PUBLIC", table="ORDERS"), raw_orders)],
424
+ [
425
+ (
426
+ FQNParts(database="SALES", schema_name="PUBLIC", table="ORDERS"),
427
+ raw_orders,
428
+ )
429
+ ],
350
430
  client,
351
431
  placeholder=" ",
352
432
  session=session,
@@ -365,15 +445,27 @@ def test_model_metrics_skipped_with_no_facts() -> None:
365
445
  id_=0,
366
446
  name="customers",
367
447
  columns=[
368
- Column(id_=0, column_name="customer_id", column_type="NUMBER", values=["1", "2"]),
369
- Column(id_=1, column_name="customer_name", column_type="STRING", values=["Alice", "Bob"]),
448
+ Column(
449
+ id_=0,
450
+ column_name="customer_id",
451
+ column_type="NUMBER",
452
+ values=["1", "2"],
453
+ ),
454
+ Column(
455
+ id_=1,
456
+ column_name="customer_name",
457
+ column_type="STRING",
458
+ values=["Alice", "Bob"],
459
+ ),
370
460
  ],
371
461
  )
372
462
 
373
463
  customers_proto = semantic_model_pb2.Table(
374
464
  name="CUSTOMERS",
375
465
  description=" ",
376
- base_table=semantic_model_pb2.FullyQualifiedTable(database="SALES", schema="PUBLIC", table="CUSTOMERS"),
466
+ base_table=semantic_model_pb2.FullyQualifiedTable(
467
+ database="SALES", schema="PUBLIC", table="CUSTOMERS"
468
+ ),
377
469
  dimensions=[
378
470
  semantic_model_pb2.Dimension(
379
471
  name="customer_name",
@@ -384,7 +476,9 @@ def test_model_metrics_skipped_with_no_facts() -> None:
384
476
  ],
385
477
  )
386
478
 
387
- model = semantic_model_pb2.SemanticModel(name="Customer Model", tables=[customers_proto])
479
+ model = semantic_model_pb2.SemanticModel(
480
+ name="Customer Model", tables=[customers_proto]
481
+ )
388
482
 
389
483
  table_payload = {
390
484
  "table_description": "Customer dimension table.",
@@ -408,12 +502,24 @@ def test_model_metrics_skipped_with_no_facts() -> None:
408
502
  # Model description response for when _summarize_model_description is called
409
503
  model_description_payload = "This is a customer dimension model."
410
504
 
411
- client = _FakeDashscopeClient([table_payload, model_description_payload, model_metrics_payload, verified_queries_payload])
505
+ client = _FakeDashscopeClient(
506
+ [
507
+ table_payload,
508
+ model_description_payload,
509
+ model_metrics_payload,
510
+ verified_queries_payload,
511
+ ]
512
+ )
412
513
  session = _FakeSession()
413
514
 
414
515
  enrich_semantic_model(
415
516
  model,
416
- [(FQNParts(database="SALES", schema_name="PUBLIC", table="CUSTOMERS"), raw_customers)],
517
+ [
518
+ (
519
+ FQNParts(database="SALES", schema_name="PUBLIC", table="CUSTOMERS"),
520
+ raw_customers,
521
+ )
522
+ ],
417
523
  client,
418
524
  placeholder=" ",
419
525
  session=session,