semantic-link-labs 0.8.4__py3-none-any.whl → 0.8.5__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.

Potentially problematic release.


This version of semantic-link-labs might be problematic. Click here for more details.

Files changed (49) hide show
  1. {semantic_link_labs-0.8.4.dist-info → semantic_link_labs-0.8.5.dist-info}/METADATA +8 -3
  2. {semantic_link_labs-0.8.4.dist-info → semantic_link_labs-0.8.5.dist-info}/RECORD +49 -47
  3. {semantic_link_labs-0.8.4.dist-info → semantic_link_labs-0.8.5.dist-info}/WHEEL +1 -1
  4. sempy_labs/__init__.py +29 -1
  5. sempy_labs/_data_pipelines.py +3 -3
  6. sempy_labs/_dataflows.py +116 -3
  7. sempy_labs/_dax.py +189 -3
  8. sempy_labs/_deployment_pipelines.py +3 -3
  9. sempy_labs/_environments.py +3 -3
  10. sempy_labs/_eventhouses.py +3 -3
  11. sempy_labs/_eventstreams.py +3 -3
  12. sempy_labs/_external_data_shares.py +1 -1
  13. sempy_labs/_generate_semantic_model.py +3 -3
  14. sempy_labs/_git.py +7 -7
  15. sempy_labs/_helper_functions.py +25 -1
  16. sempy_labs/_kql_databases.py +3 -3
  17. sempy_labs/_kql_querysets.py +3 -3
  18. sempy_labs/_mirrored_databases.py +428 -0
  19. sempy_labs/_mirrored_warehouses.py +1 -1
  20. sempy_labs/_ml_experiments.py +3 -3
  21. sempy_labs/_ml_models.py +4 -4
  22. sempy_labs/_model_bpa.py +209 -180
  23. sempy_labs/_model_bpa_bulk.py +41 -23
  24. sempy_labs/_model_dependencies.py +41 -87
  25. sempy_labs/_notebooks.py +2 -2
  26. sempy_labs/_query_scale_out.py +4 -4
  27. sempy_labs/_refresh_semantic_model.py +2 -2
  28. sempy_labs/_spark.py +6 -6
  29. sempy_labs/_vertipaq.py +31 -19
  30. sempy_labs/_warehouses.py +3 -3
  31. sempy_labs/_workspace_identity.py +2 -2
  32. sempy_labs/_workspaces.py +7 -7
  33. sempy_labs/admin/__init__.py +2 -0
  34. sempy_labs/admin/_basic_functions.py +54 -8
  35. sempy_labs/admin/_domains.py +1 -1
  36. sempy_labs/directlake/_update_directlake_partition_entity.py +1 -1
  37. sempy_labs/directlake/_warm_cache.py +10 -9
  38. sempy_labs/lakehouse/_get_lakehouse_tables.py +1 -1
  39. sempy_labs/lakehouse/_shortcuts.py +2 -2
  40. sempy_labs/migration/_create_pqt_file.py +5 -2
  41. sempy_labs/report/__init__.py +2 -0
  42. sempy_labs/report/_download_report.py +75 -0
  43. sempy_labs/report/_generate_report.py +3 -3
  44. sempy_labs/report/_report_functions.py +3 -3
  45. sempy_labs/report/_report_rebind.py +1 -1
  46. sempy_labs/report/_reportwrapper.py +4 -2
  47. sempy_labs/tom/_model.py +71 -35
  48. {semantic_link_labs-0.8.4.dist-info → semantic_link_labs-0.8.5.dist-info}/LICENSE +0 -0
  49. {semantic_link_labs-0.8.4.dist-info → semantic_link_labs-0.8.5.dist-info}/top_level.txt +0 -0
sempy_labs/_model_bpa.py CHANGED
@@ -123,210 +123,239 @@ def run_model_bpa(
123
123
  dataset=dataset, workspace=workspace, readonly=True
124
124
  ) as tom:
125
125
 
126
- dep = get_model_calc_dependencies(dataset=dataset, workspace=workspace)
127
-
128
- def translate_using_po(rule_file):
129
- current_dir = os.path.dirname(os.path.abspath(__file__))
130
- translation_file = (
131
- f"{current_dir}/_bpa_translation/_model/_translations_{language}.po"
126
+ # Do not run BPA for models with no tables
127
+ if tom.model.Tables.Count == 0:
128
+ print(
129
+ f"{icons.warning} The '{dataset}' semantic model within the '{workspace}' workspace has no tables and therefore there are no valid BPA results."
132
130
  )
133
- for c in ["Category", "Description", "Rule Name"]:
134
- po = polib.pofile(translation_file)
135
- for entry in po:
136
- if entry.tcomment == c.lower().replace(" ", "_"):
137
- rule_file.loc[rule_file["Rule Name"] == entry.msgid, c] = (
138
- entry.msgstr
139
- )
131
+ finalDF = pd.DataFrame(
132
+ columns=[
133
+ "Category",
134
+ "Rule Name",
135
+ "Severity",
136
+ "Object Type",
137
+ "Object Name",
138
+ "Description",
139
+ "URL",
140
+ ]
141
+ )
142
+ else:
143
+ dep = get_model_calc_dependencies(dataset=dataset, workspace=workspace)
140
144
 
141
- translated = False
145
+ def translate_using_po(rule_file):
146
+ current_dir = os.path.dirname(os.path.abspath(__file__))
147
+ translation_file = (
148
+ f"{current_dir}/_bpa_translation/_model/_translations_{language}.po"
149
+ )
150
+ for c in ["Category", "Description", "Rule Name"]:
151
+ po = polib.pofile(translation_file)
152
+ for entry in po:
153
+ if entry.tcomment == c.lower().replace(" ", "_"):
154
+ rule_file.loc[rule_file["Rule Name"] == entry.msgid, c] = (
155
+ entry.msgstr
156
+ )
142
157
 
143
- # Translations
144
- if language is not None and rules is None and language in language_list:
145
- rules = model_bpa_rules(dependencies=dep)
146
- translate_using_po(rules)
147
- translated = True
148
- if rules is None:
149
- rules = model_bpa_rules(dependencies=dep)
150
- if language is not None and not translated:
158
+ translated = False
151
159
 
152
- def translate_using_spark(rule_file):
160
+ # Translations
161
+ if language is not None and rules is None and language in language_list:
162
+ rules = model_bpa_rules(dependencies=dep)
163
+ translate_using_po(rules)
164
+ translated = True
165
+ if rules is None:
166
+ rules = model_bpa_rules(dependencies=dep)
167
+ if language is not None and not translated:
153
168
 
154
- from synapse.ml.services import Translate
155
- from pyspark.sql import SparkSession
169
+ def translate_using_spark(rule_file):
156
170
 
157
- rules_temp = rule_file.copy()
158
- rules_temp = rules_temp.drop(["Expression", "URL", "Severity"], axis=1)
171
+ from synapse.ml.services import Translate
172
+ from pyspark.sql import SparkSession
159
173
 
160
- schema = StructType(
161
- [
162
- StructField("Category", StringType(), True),
163
- StructField("Scope", StringType(), True),
164
- StructField("Rule Name", StringType(), True),
165
- StructField("Description", StringType(), True),
166
- ]
167
- )
174
+ rules_temp = rule_file.copy()
175
+ rules_temp = rules_temp.drop(
176
+ ["Expression", "URL", "Severity"], axis=1
177
+ )
168
178
 
169
- spark = SparkSession.builder.getOrCreate()
170
- dfRules = spark.createDataFrame(rules_temp, schema)
171
-
172
- columns = ["Category", "Rule Name", "Description"]
173
- for clm in columns:
174
- translate = (
175
- Translate()
176
- .setTextCol(clm)
177
- .setToLanguage(language)
178
- .setOutputCol("translation")
179
- .setConcurrency(5)
179
+ schema = StructType(
180
+ [
181
+ StructField("Category", StringType(), True),
182
+ StructField("Scope", StringType(), True),
183
+ StructField("Rule Name", StringType(), True),
184
+ StructField("Description", StringType(), True),
185
+ ]
180
186
  )
181
187
 
182
- if clm == "Rule Name":
183
- transDF = (
184
- translate.transform(dfRules)
185
- .withColumn(
186
- "translation", flatten(col("translation.translations"))
187
- )
188
- .withColumn("translation", col("translation.text"))
189
- .select(clm, "translation")
188
+ spark = SparkSession.builder.getOrCreate()
189
+ dfRules = spark.createDataFrame(rules_temp, schema)
190
+
191
+ columns = ["Category", "Rule Name", "Description"]
192
+ for clm in columns:
193
+ translate = (
194
+ Translate()
195
+ .setTextCol(clm)
196
+ .setToLanguage(language)
197
+ .setOutputCol("translation")
198
+ .setConcurrency(5)
190
199
  )
191
- else:
192
- transDF = (
193
- translate.transform(dfRules)
194
- .withColumn(
195
- "translation", flatten(col("translation.translations"))
200
+
201
+ if clm == "Rule Name":
202
+ transDF = (
203
+ translate.transform(dfRules)
204
+ .withColumn(
205
+ "translation",
206
+ flatten(col("translation.translations")),
207
+ )
208
+ .withColumn("translation", col("translation.text"))
209
+ .select(clm, "translation")
210
+ )
211
+ else:
212
+ transDF = (
213
+ translate.transform(dfRules)
214
+ .withColumn(
215
+ "translation",
216
+ flatten(col("translation.translations")),
217
+ )
218
+ .withColumn("translation", col("translation.text"))
219
+ .select("Rule Name", clm, "translation")
196
220
  )
197
- .withColumn("translation", col("translation.text"))
198
- .select("Rule Name", clm, "translation")
199
- )
200
221
 
201
- df_panda = transDF.toPandas()
202
- rule_file = pd.merge(
203
- rule_file,
204
- df_panda[["Rule Name", "translation"]],
205
- on="Rule Name",
206
- how="left",
207
- )
222
+ df_panda = transDF.toPandas()
223
+ rule_file = pd.merge(
224
+ rule_file,
225
+ df_panda[["Rule Name", "translation"]],
226
+ on="Rule Name",
227
+ how="left",
228
+ )
208
229
 
209
- rule_file = rule_file.rename(
210
- columns={"translation": f"{clm}Translated"}
211
- )
212
- rule_file[f"{clm}Translated"] = rule_file[f"{clm}Translated"].apply(
213
- lambda x: x[0] if x is not None else None
214
- )
230
+ rule_file = rule_file.rename(
231
+ columns={"translation": f"{clm}Translated"}
232
+ )
233
+ rule_file[f"{clm}Translated"] = rule_file[
234
+ f"{clm}Translated"
235
+ ].apply(lambda x: x[0] if x is not None else None)
215
236
 
216
- for clm in columns:
217
- rule_file = rule_file.drop([clm], axis=1)
218
- rule_file = rule_file.rename(columns={f"{clm}Translated": clm})
237
+ for clm in columns:
238
+ rule_file = rule_file.drop([clm], axis=1)
239
+ rule_file = rule_file.rename(columns={f"{clm}Translated": clm})
219
240
 
220
- return rule_file
241
+ return rule_file
221
242
 
222
- rules = translate_using_spark(rules)
243
+ rules = translate_using_spark(rules)
223
244
 
224
- rules.loc[rules["Severity"] == "Warning", "Severity"] = icons.warning
225
- rules.loc[rules["Severity"] == "Error", "Severity"] = icons.error
226
- rules.loc[rules["Severity"] == "Info", "Severity"] = icons.info
245
+ rules.loc[rules["Severity"] == "Warning", "Severity"] = icons.warning
246
+ rules.loc[rules["Severity"] == "Error", "Severity"] = icons.error
247
+ rules.loc[rules["Severity"] == "Info", "Severity"] = icons.info
227
248
 
228
- pd.set_option("display.max_colwidth", 1000)
249
+ pd.set_option("display.max_colwidth", 1000)
229
250
 
230
- violations = pd.DataFrame(columns=["Object Name", "Scope", "Rule Name"])
251
+ violations = pd.DataFrame(columns=["Object Name", "Scope", "Rule Name"])
231
252
 
232
- scope_to_dataframe = {
233
- "Relationship": (
234
- tom.model.Relationships,
235
- lambda obj: create_relationship_name(
236
- obj.FromTable.Name,
237
- obj.FromColumn.Name,
238
- obj.ToTable.Name,
239
- obj.ToColumn.Name,
253
+ scope_to_dataframe = {
254
+ "Relationship": (
255
+ tom.model.Relationships,
256
+ lambda obj: create_relationship_name(
257
+ obj.FromTable.Name,
258
+ obj.FromColumn.Name,
259
+ obj.ToTable.Name,
260
+ obj.ToColumn.Name,
261
+ ),
240
262
  ),
241
- ),
242
- "Column": (
243
- tom.all_columns(),
244
- lambda obj: format_dax_object_name(obj.Parent.Name, obj.Name),
245
- ),
246
- "Measure": (tom.all_measures(), lambda obj: obj.Name),
247
- "Hierarchy": (
248
- tom.all_hierarchies(),
249
- lambda obj: format_dax_object_name(obj.Parent.Name, obj.Name),
250
- ),
251
- "Table": (tom.model.Tables, lambda obj: obj.Name),
252
- "Role": (tom.model.Roles, lambda obj: obj.Name),
253
- "Model": (tom.model, lambda obj: obj.Model.Name),
254
- "Calculation Item": (
255
- tom.all_calculation_items(),
256
- lambda obj: format_dax_object_name(obj.Parent.Table.Name, obj.Name),
257
- ),
258
- "Row Level Security": (
259
- tom.all_rls(),
260
- lambda obj: format_dax_object_name(obj.Parent.Name, obj.Name),
261
- ),
262
- "Partition": (
263
- tom.all_partitions(),
264
- lambda obj: format_dax_object_name(obj.Parent.Name, obj.Name),
265
- ),
266
- }
267
-
268
- for i, r in rules.iterrows():
269
- ruleName = r["Rule Name"]
270
- expr = r["Expression"]
271
- scopes = r["Scope"]
272
-
273
- if isinstance(scopes, str):
274
- scopes = [scopes]
275
-
276
- for scope in scopes:
277
- func = scope_to_dataframe[scope][0]
278
- nm = scope_to_dataframe[scope][1]
279
-
280
- if scope == "Model":
281
- x = []
282
- if expr(func, tom):
283
- x = ["Model"]
284
- elif scope == "Measure":
285
- x = [nm(obj) for obj in tom.all_measures() if expr(obj, tom)]
286
- elif scope == "Column":
287
- x = [nm(obj) for obj in tom.all_columns() if expr(obj, tom)]
288
- elif scope == "Partition":
289
- x = [nm(obj) for obj in tom.all_partitions() if expr(obj, tom)]
290
- elif scope == "Hierarchy":
291
- x = [nm(obj) for obj in tom.all_hierarchies() if expr(obj, tom)]
292
- elif scope == "Table":
293
- x = [nm(obj) for obj in tom.model.Tables if expr(obj, tom)]
294
- elif scope == "Relationship":
295
- x = [nm(obj) for obj in tom.model.Relationships if expr(obj, tom)]
296
- elif scope == "Role":
297
- x = [nm(obj) for obj in tom.model.Roles if expr(obj, tom)]
298
- elif scope == "Row Level Security":
299
- x = [nm(obj) for obj in tom.all_rls() if expr(obj, tom)]
300
- elif scope == "Calculation Item":
301
- x = [
302
- nm(obj) for obj in tom.all_calculation_items() if expr(obj, tom)
303
- ]
304
-
305
- if len(x) > 0:
306
- new_data = {"Object Name": x, "Scope": scope, "Rule Name": ruleName}
307
- violations = pd.concat(
308
- [violations, pd.DataFrame(new_data)], ignore_index=True
309
- )
263
+ "Column": (
264
+ tom.all_columns(),
265
+ lambda obj: format_dax_object_name(obj.Parent.Name, obj.Name),
266
+ ),
267
+ "Measure": (tom.all_measures(), lambda obj: obj.Name),
268
+ "Hierarchy": (
269
+ tom.all_hierarchies(),
270
+ lambda obj: format_dax_object_name(obj.Parent.Name, obj.Name),
271
+ ),
272
+ "Table": (tom.model.Tables, lambda obj: obj.Name),
273
+ "Role": (tom.model.Roles, lambda obj: obj.Name),
274
+ "Model": (tom.model, lambda obj: obj.Model.Name),
275
+ "Calculation Item": (
276
+ tom.all_calculation_items(),
277
+ lambda obj: format_dax_object_name(obj.Parent.Table.Name, obj.Name),
278
+ ),
279
+ "Row Level Security": (
280
+ tom.all_rls(),
281
+ lambda obj: format_dax_object_name(obj.Parent.Name, obj.Name),
282
+ ),
283
+ "Partition": (
284
+ tom.all_partitions(),
285
+ lambda obj: format_dax_object_name(obj.Parent.Name, obj.Name),
286
+ ),
287
+ }
288
+
289
+ for i, r in rules.iterrows():
290
+ ruleName = r["Rule Name"]
291
+ expr = r["Expression"]
292
+ scopes = r["Scope"]
293
+
294
+ if isinstance(scopes, str):
295
+ scopes = [scopes]
296
+
297
+ for scope in scopes:
298
+ func = scope_to_dataframe[scope][0]
299
+ nm = scope_to_dataframe[scope][1]
300
+
301
+ if scope == "Model":
302
+ x = []
303
+ if expr(func, tom):
304
+ x = ["Model"]
305
+ elif scope == "Measure":
306
+ x = [nm(obj) for obj in tom.all_measures() if expr(obj, tom)]
307
+ elif scope == "Column":
308
+ x = [nm(obj) for obj in tom.all_columns() if expr(obj, tom)]
309
+ elif scope == "Partition":
310
+ x = [nm(obj) for obj in tom.all_partitions() if expr(obj, tom)]
311
+ elif scope == "Hierarchy":
312
+ x = [nm(obj) for obj in tom.all_hierarchies() if expr(obj, tom)]
313
+ elif scope == "Table":
314
+ x = [nm(obj) for obj in tom.model.Tables if expr(obj, tom)]
315
+ elif scope == "Relationship":
316
+ x = [
317
+ nm(obj) for obj in tom.model.Relationships if expr(obj, tom)
318
+ ]
319
+ elif scope == "Role":
320
+ x = [nm(obj) for obj in tom.model.Roles if expr(obj, tom)]
321
+ elif scope == "Row Level Security":
322
+ x = [nm(obj) for obj in tom.all_rls() if expr(obj, tom)]
323
+ elif scope == "Calculation Item":
324
+ x = [
325
+ nm(obj)
326
+ for obj in tom.all_calculation_items()
327
+ if expr(obj, tom)
328
+ ]
329
+
330
+ if len(x) > 0:
331
+ new_data = {
332
+ "Object Name": x,
333
+ "Scope": scope,
334
+ "Rule Name": ruleName,
335
+ }
336
+ violations = pd.concat(
337
+ [violations, pd.DataFrame(new_data)], ignore_index=True
338
+ )
310
339
 
311
- prepDF = pd.merge(
312
- violations,
313
- rules[["Rule Name", "Category", "Severity", "Description", "URL"]],
314
- left_on="Rule Name",
315
- right_on="Rule Name",
316
- how="left",
317
- )
318
- prepDF.rename(columns={"Scope": "Object Type"}, inplace=True)
319
- finalDF = prepDF[
320
- [
321
- "Category",
322
- "Rule Name",
323
- "Severity",
324
- "Object Type",
325
- "Object Name",
326
- "Description",
327
- "URL",
340
+ prepDF = pd.merge(
341
+ violations,
342
+ rules[["Rule Name", "Category", "Severity", "Description", "URL"]],
343
+ left_on="Rule Name",
344
+ right_on="Rule Name",
345
+ how="left",
346
+ )
347
+ prepDF.rename(columns={"Scope": "Object Type"}, inplace=True)
348
+ finalDF = prepDF[
349
+ [
350
+ "Category",
351
+ "Rule Name",
352
+ "Severity",
353
+ "Object Type",
354
+ "Object Name",
355
+ "Description",
356
+ "URL",
357
+ ]
328
358
  ]
329
- ]
330
359
 
331
360
  if export:
332
361
  if not lakehouse_attached():
@@ -25,6 +25,7 @@ def run_model_bpa_bulk(
25
25
  language: Optional[str] = None,
26
26
  workspace: Optional[str | List[str]] = None,
27
27
  skip_models: Optional[str | List[str]] = ["ModelBPA", "Fabric Capacity Metrics"],
28
+ skip_models_in_workspace: Optional[dict] = None,
28
29
  ):
29
30
  """
30
31
  Runs the semantic model Best Practice Analyzer across all semantic models in a workspace (or all accessible workspaces).
@@ -33,8 +34,6 @@ def run_model_bpa_bulk(
33
34
 
34
35
  Parameters
35
36
  ----------
36
- dataset : str
37
- Name of the semantic model.
38
37
  rules : pandas.DataFrame, default=None
39
38
  A pandas dataframe containing rules to be evaluated. Based on the format of the dataframe produced by the model_bpa_rules function.
40
39
  extended : bool, default=False
@@ -47,6 +46,12 @@ def run_model_bpa_bulk(
47
46
  Defaults to None which scans all accessible workspaces.
48
47
  skip_models : str | List[str], default=['ModelBPA', 'Fabric Capacity Metrics']
49
48
  The semantic models to always skip when running this analysis.
49
+ skip_models_in_workspace : dict, default=None
50
+ A dictionary showing specific semantic models within specific workspaces to skip. See the example below:
51
+ {
52
+ "Workspace A": ["Dataset1", "Dataset2"],
53
+ "Workspace B": ["Dataset5", "Dataset 8"],
54
+ }
50
55
  """
51
56
 
52
57
  if not lakehouse_attached():
@@ -68,7 +73,6 @@ def run_model_bpa_bulk(
68
73
  )
69
74
  lakeT = get_lakehouse_tables(lakehouse=lakehouse, workspace=lakehouse_workspace)
70
75
  lakeT_filt = lakeT[lakeT["Table Name"] == output_table]
71
- # query = f"SELECT MAX(RunId) FROM {lakehouse}.{output_table}"
72
76
  if len(lakeT_filt) == 0:
73
77
  runId = 1
74
78
  else:
@@ -84,6 +88,11 @@ def run_model_bpa_bulk(
84
88
  else:
85
89
  dfW_filt = dfW[dfW["Name"].isin(workspace)]
86
90
 
91
+ if len(dfW_filt) == 0:
92
+ raise ValueError(
93
+ f"{icons.red_dot} There are no valid workspaces to assess. This is likely due to not having proper permissions to the workspace(s) entered in the 'workspace' parameter."
94
+ )
95
+
87
96
  for i, r in dfW_filt.iterrows():
88
97
  wksp = r["Name"]
89
98
  wksp_id = r["Id"]
@@ -91,6 +100,10 @@ def run_model_bpa_bulk(
91
100
  df = pd.DataFrame(columns=list(icons.bpa_schema.keys()))
92
101
  dfD = fabric.list_datasets(workspace=wksp, mode="rest")
93
102
 
103
+ # Skip models in workspace
104
+ skip_models_wkspc = skip_models_in_workspace.get(wksp)
105
+ dfD = dfD[~dfD["Dataset Name"].isin(skip_models_wkspc)]
106
+
94
107
  # Exclude default semantic models
95
108
  if len(dfD) > 0:
96
109
  dfI = fabric.list_items(workspace=wksp)
@@ -142,28 +155,33 @@ def run_model_bpa_bulk(
142
155
  )
143
156
  print(e)
144
157
 
145
- df["Severity"].replace(icons.severity_mapping)
158
+ if len(df) == 0:
159
+ print(
160
+ f"{icons.yellow_dot} No BPA results to save for the '{wksp}' workspace."
161
+ )
162
+ else:
163
+ df["Severity"].replace(icons.severity_mapping)
146
164
 
147
- # Append save results individually for each workspace (so as not to create a giant dataframe)
148
- print(
149
- f"{icons.in_progress} Saving the Model BPA results of the '{wksp}' workspace to the '{output_table}' within the '{lakehouse}' lakehouse within the '{lakehouse_workspace}' workspace..."
150
- )
165
+ # Append save results individually for each workspace (so as not to create a giant dataframe)
166
+ print(
167
+ f"{icons.in_progress} Saving the Model BPA results of the '{wksp}' workspace to the '{output_table}' within the '{lakehouse}' lakehouse within the '{lakehouse_workspace}' workspace..."
168
+ )
151
169
 
152
- schema = {
153
- key.replace(" ", "_"): value
154
- for key, value in icons.bpa_schema.items()
155
- }
156
-
157
- save_as_delta_table(
158
- dataframe=df,
159
- delta_table_name=output_table,
160
- write_mode="append",
161
- schema=schema,
162
- merge_schema=True,
163
- )
164
- print(
165
- f"{icons.green_dot} Saved BPA results to the '{output_table}' delta table."
166
- )
170
+ schema = {
171
+ key.replace(" ", "_"): value
172
+ for key, value in icons.bpa_schema.items()
173
+ }
174
+
175
+ save_as_delta_table(
176
+ dataframe=df,
177
+ delta_table_name=output_table,
178
+ write_mode="append",
179
+ schema=schema,
180
+ merge_schema=True,
181
+ )
182
+ print(
183
+ f"{icons.green_dot} Saved BPA results to the '{output_table}' delta table."
184
+ )
167
185
 
168
186
  print(f"{icons.green_dot} Bulk BPA scan complete.")
169
187