semantic-link-labs 0.8.3__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 (109) hide show
  1. {semantic_link_labs-0.8.3.dist-info → semantic_link_labs-0.8.5.dist-info}/METADATA +38 -8
  2. {semantic_link_labs-0.8.3.dist-info → semantic_link_labs-0.8.5.dist-info}/RECORD +109 -104
  3. {semantic_link_labs-0.8.3.dist-info → semantic_link_labs-0.8.5.dist-info}/WHEEL +1 -1
  4. sempy_labs/__init__.py +53 -1
  5. sempy_labs/_bpa_translation/_model/_translations_am-ET.po +24 -5
  6. sempy_labs/_bpa_translation/_model/_translations_ar-AE.po +28 -4
  7. sempy_labs/_bpa_translation/_model/_translations_bg-BG.po +34 -4
  8. sempy_labs/_bpa_translation/_model/_translations_ca-ES.po +33 -4
  9. sempy_labs/_bpa_translation/_model/_translations_cs-CZ.po +31 -4
  10. sempy_labs/_bpa_translation/_model/_translations_da-DK.po +31 -4
  11. sempy_labs/_bpa_translation/_model/_translations_de-DE.po +34 -4
  12. sempy_labs/_bpa_translation/_model/_translations_el-GR.po +36 -4
  13. sempy_labs/_bpa_translation/_model/_translations_es-ES.po +90 -58
  14. sempy_labs/_bpa_translation/_model/_translations_fa-IR.po +31 -5
  15. sempy_labs/_bpa_translation/_model/_translations_fi-FI.po +31 -4
  16. sempy_labs/_bpa_translation/_model/_translations_fr-FR.po +34 -4
  17. sempy_labs/_bpa_translation/_model/_translations_ga-IE.po +34 -4
  18. sempy_labs/_bpa_translation/_model/_translations_he-IL.po +28 -4
  19. sempy_labs/_bpa_translation/_model/_translations_hi-IN.po +32 -4
  20. sempy_labs/_bpa_translation/_model/_translations_hu-HU.po +32 -4
  21. sempy_labs/_bpa_translation/_model/_translations_id-ID.po +32 -4
  22. sempy_labs/_bpa_translation/_model/_translations_is-IS.po +31 -4
  23. sempy_labs/_bpa_translation/_model/_translations_it-IT.po +34 -4
  24. sempy_labs/_bpa_translation/_model/_translations_ja-JP.po +24 -4
  25. sempy_labs/_bpa_translation/_model/_translations_ko-KR.po +72 -56
  26. sempy_labs/_bpa_translation/_model/_translations_mt-MT.po +34 -4
  27. sempy_labs/_bpa_translation/_model/_translations_nl-NL.po +34 -4
  28. sempy_labs/_bpa_translation/_model/_translations_pl-PL.po +95 -71
  29. sempy_labs/_bpa_translation/_model/_translations_pt-BR.po +32 -4
  30. sempy_labs/_bpa_translation/_model/_translations_pt-PT.po +32 -4
  31. sempy_labs/_bpa_translation/_model/_translations_ro-RO.po +33 -4
  32. sempy_labs/_bpa_translation/_model/_translations_ru-RU.po +34 -4
  33. sempy_labs/_bpa_translation/_model/_translations_sk-SK.po +31 -4
  34. sempy_labs/_bpa_translation/_model/_translations_sl-SL.po +32 -4
  35. sempy_labs/_bpa_translation/_model/_translations_sv-SE.po +32 -4
  36. sempy_labs/_bpa_translation/_model/_translations_ta-IN.po +32 -4
  37. sempy_labs/_bpa_translation/_model/_translations_te-IN.po +31 -4
  38. sempy_labs/_bpa_translation/_model/_translations_th-TH.po +31 -4
  39. sempy_labs/_bpa_translation/_model/_translations_tr-TR.po +32 -4
  40. sempy_labs/_bpa_translation/_model/_translations_uk-UA.po +100 -72
  41. sempy_labs/_bpa_translation/_model/_translations_zh-CN.po +23 -5
  42. sempy_labs/_bpa_translation/_model/_translations_zu-ZA.po +32 -4
  43. sempy_labs/_capacities.py +49 -14
  44. sempy_labs/_capacity_migration.py +1 -7
  45. sempy_labs/_data_pipelines.py +6 -0
  46. sempy_labs/_dataflows.py +118 -1
  47. sempy_labs/_dax.py +189 -3
  48. sempy_labs/_deployment_pipelines.py +13 -7
  49. sempy_labs/_environments.py +6 -0
  50. sempy_labs/_eventhouses.py +6 -0
  51. sempy_labs/_eventstreams.py +6 -0
  52. sempy_labs/_external_data_shares.py +6 -4
  53. sempy_labs/_generate_semantic_model.py +26 -3
  54. sempy_labs/_git.py +14 -14
  55. sempy_labs/_helper_functions.py +197 -1
  56. sempy_labs/_icons.py +55 -22
  57. sempy_labs/_kql_databases.py +6 -0
  58. sempy_labs/_kql_querysets.py +6 -0
  59. sempy_labs/_list_functions.py +1 -1
  60. sempy_labs/_managed_private_endpoints.py +166 -0
  61. sempy_labs/_mirrored_databases.py +428 -0
  62. sempy_labs/_mirrored_warehouses.py +2 -0
  63. sempy_labs/_ml_experiments.py +6 -0
  64. sempy_labs/_ml_models.py +7 -1
  65. sempy_labs/_model_bpa.py +215 -181
  66. sempy_labs/_model_bpa_bulk.py +46 -42
  67. sempy_labs/_model_bpa_rules.py +8 -3
  68. sempy_labs/_model_dependencies.py +41 -87
  69. sempy_labs/_notebooks.py +107 -12
  70. sempy_labs/_query_scale_out.py +8 -6
  71. sempy_labs/_refresh_semantic_model.py +299 -49
  72. sempy_labs/_spark.py +12 -5
  73. sempy_labs/_translations.py +2 -0
  74. sempy_labs/_vertipaq.py +89 -86
  75. sempy_labs/_warehouses.py +79 -0
  76. sempy_labs/_workloads.py +128 -0
  77. sempy_labs/_workspace_identity.py +4 -4
  78. sempy_labs/_workspaces.py +14 -1
  79. sempy_labs/admin/__init__.py +2 -0
  80. sempy_labs/admin/_basic_functions.py +131 -43
  81. sempy_labs/admin/_domains.py +18 -18
  82. sempy_labs/directlake/__init__.py +2 -0
  83. sempy_labs/directlake/_directlake_schema_sync.py +2 -1
  84. sempy_labs/directlake/_dl_helper.py +4 -1
  85. sempy_labs/directlake/_get_shared_expression.py +7 -1
  86. sempy_labs/directlake/_guardrails.py +2 -1
  87. sempy_labs/directlake/_show_unsupported_directlake_objects.py +1 -7
  88. sempy_labs/directlake/_update_directlake_model_lakehouse_connection.py +78 -0
  89. sempy_labs/directlake/_update_directlake_partition_entity.py +13 -32
  90. sempy_labs/directlake/_warm_cache.py +10 -9
  91. sempy_labs/lakehouse/_get_lakehouse_tables.py +6 -2
  92. sempy_labs/lakehouse/_shortcuts.py +4 -0
  93. sempy_labs/migration/_create_pqt_file.py +5 -2
  94. sempy_labs/migration/_migrate_calctables_to_lakehouse.py +3 -2
  95. sempy_labs/migration/_migrate_calctables_to_semantic_model.py +2 -0
  96. sempy_labs/migration/_migrate_model_objects_to_semantic_model.py +2 -8
  97. sempy_labs/migration/_migrate_tables_columns_to_semantic_model.py +17 -0
  98. sempy_labs/migration/_migration_validation.py +2 -0
  99. sempy_labs/migration/_refresh_calc_tables.py +1 -0
  100. sempy_labs/report/__init__.py +6 -1
  101. sempy_labs/report/_download_report.py +75 -0
  102. sempy_labs/report/_generate_report.py +6 -0
  103. sempy_labs/report/_paginated.py +74 -0
  104. sempy_labs/report/_report_functions.py +6 -0
  105. sempy_labs/report/_report_rebind.py +2 -0
  106. sempy_labs/report/_reportwrapper.py +4 -2
  107. sempy_labs/tom/_model.py +135 -68
  108. {semantic_link_labs-0.8.3.dist-info → semantic_link_labs-0.8.5.dist-info}/LICENSE +0 -0
  109. {semantic_link_labs-0.8.3.dist-info → semantic_link_labs-0.8.5.dist-info}/top_level.txt +0 -0
sempy_labs/_model_bpa.py CHANGED
@@ -56,7 +56,7 @@ def run_model_bpa(
56
56
  extended : bool, default=False
57
57
  If True, runs the set_vertipaq_annotations function to collect Vertipaq Analyzer statistics to be used in the analysis of the semantic model.
58
58
  language : str, default=None
59
- Specifying a language code (i.e. 'it-IT' for Italian) will auto-translate the Category, Rule Name and Description into the specified language.
59
+ Specifying a language name or code (i.e. 'it-IT' for Italian) will auto-translate the Category, Rule Name and Description into the specified language.
60
60
  Defaults to None which resolves to English.
61
61
 
62
62
  Returns
@@ -113,6 +113,7 @@ def run_model_bpa(
113
113
  )
114
114
 
115
115
  if extended:
116
+ icons.sll_tags.append("ModelBPAExtended")
116
117
  with connect_semantic_model(
117
118
  dataset=dataset, workspace=workspace, readonly=False
118
119
  ) as tom:
@@ -122,210 +123,239 @@ def run_model_bpa(
122
123
  dataset=dataset, workspace=workspace, readonly=True
123
124
  ) as tom:
124
125
 
125
- dep = get_model_calc_dependencies(dataset=dataset, workspace=workspace)
126
-
127
- def translate_using_po(rule_file):
128
- current_dir = os.path.dirname(os.path.abspath(__file__))
129
- translation_file = (
130
- 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."
131
130
  )
132
- for c in ["Category", "Description", "Rule Name"]:
133
- po = polib.pofile(translation_file)
134
- for entry in po:
135
- if entry.tcomment == c.lower().replace(" ", "_"):
136
- rule_file.loc[rule_file["Rule Name"] == entry.msgid, c] = (
137
- entry.msgstr
138
- )
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)
139
144
 
140
- 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
+ )
141
157
 
142
- # Translations
143
- if language is not None and rules is None and language in language_list:
144
- rules = model_bpa_rules(dependencies=dep)
145
- translate_using_po(rules)
146
- translated = True
147
- if rules is None:
148
- rules = model_bpa_rules(dependencies=dep)
149
- if language is not None and not translated:
158
+ translated = False
150
159
 
151
- 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:
152
168
 
153
- from synapse.ml.services import Translate
154
- from pyspark.sql import SparkSession
169
+ def translate_using_spark(rule_file):
155
170
 
156
- rules_temp = rule_file.copy()
157
- rules_temp = rules_temp.drop(["Expression", "URL", "Severity"], axis=1)
171
+ from synapse.ml.services import Translate
172
+ from pyspark.sql import SparkSession
158
173
 
159
- schema = StructType(
160
- [
161
- StructField("Category", StringType(), True),
162
- StructField("Scope", StringType(), True),
163
- StructField("Rule Name", StringType(), True),
164
- StructField("Description", StringType(), True),
165
- ]
166
- )
174
+ rules_temp = rule_file.copy()
175
+ rules_temp = rules_temp.drop(
176
+ ["Expression", "URL", "Severity"], axis=1
177
+ )
167
178
 
168
- spark = SparkSession.builder.getOrCreate()
169
- dfRules = spark.createDataFrame(rules_temp, schema)
170
-
171
- columns = ["Category", "Rule Name", "Description"]
172
- for clm in columns:
173
- translate = (
174
- Translate()
175
- .setTextCol(clm)
176
- .setToLanguage(language)
177
- .setOutputCol("translation")
178
- .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
+ ]
179
186
  )
180
187
 
181
- if clm == "Rule Name":
182
- transDF = (
183
- translate.transform(dfRules)
184
- .withColumn(
185
- "translation", flatten(col("translation.translations"))
186
- )
187
- .withColumn("translation", col("translation.text"))
188
- .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)
189
199
  )
190
- else:
191
- transDF = (
192
- translate.transform(dfRules)
193
- .withColumn(
194
- "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")
195
220
  )
196
- .withColumn("translation", col("translation.text"))
197
- .select("Rule Name", clm, "translation")
198
- )
199
221
 
200
- df_panda = transDF.toPandas()
201
- rule_file = pd.merge(
202
- rule_file,
203
- df_panda[["Rule Name", "translation"]],
204
- on="Rule Name",
205
- how="left",
206
- )
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
+ )
207
229
 
208
- rule_file = rule_file.rename(
209
- columns={"translation": f"{clm}Translated"}
210
- )
211
- rule_file[f"{clm}Translated"] = rule_file[f"{clm}Translated"].apply(
212
- lambda x: x[0] if x is not None else None
213
- )
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)
214
236
 
215
- for clm in columns:
216
- rule_file = rule_file.drop([clm], axis=1)
217
- 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})
218
240
 
219
- return rule_file
241
+ return rule_file
220
242
 
221
- rules = translate_using_spark(rules)
243
+ rules = translate_using_spark(rules)
222
244
 
223
- rules.loc[rules["Severity"] == "Warning", "Severity"] = icons.warning
224
- rules.loc[rules["Severity"] == "Error", "Severity"] = icons.error
225
- 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
226
248
 
227
- pd.set_option("display.max_colwidth", 1000)
249
+ pd.set_option("display.max_colwidth", 1000)
228
250
 
229
- violations = pd.DataFrame(columns=["Object Name", "Scope", "Rule Name"])
251
+ violations = pd.DataFrame(columns=["Object Name", "Scope", "Rule Name"])
230
252
 
231
- scope_to_dataframe = {
232
- "Relationship": (
233
- tom.model.Relationships,
234
- lambda obj: create_relationship_name(
235
- obj.FromTable.Name,
236
- obj.FromColumn.Name,
237
- obj.ToTable.Name,
238
- 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
+ ),
239
262
  ),
240
- ),
241
- "Column": (
242
- tom.all_columns(),
243
- lambda obj: format_dax_object_name(obj.Parent.Name, obj.Name),
244
- ),
245
- "Measure": (tom.all_measures(), lambda obj: obj.Name),
246
- "Hierarchy": (
247
- tom.all_hierarchies(),
248
- lambda obj: format_dax_object_name(obj.Parent.Name, obj.Name),
249
- ),
250
- "Table": (tom.model.Tables, lambda obj: obj.Name),
251
- "Role": (tom.model.Roles, lambda obj: obj.Name),
252
- "Model": (tom.model, lambda obj: obj.Model.Name),
253
- "Calculation Item": (
254
- tom.all_calculation_items(),
255
- lambda obj: format_dax_object_name(obj.Parent.Table.Name, obj.Name),
256
- ),
257
- "Row Level Security": (
258
- tom.all_rls(),
259
- lambda obj: format_dax_object_name(obj.Parent.Name, obj.Name),
260
- ),
261
- "Partition": (
262
- tom.all_partitions(),
263
- lambda obj: format_dax_object_name(obj.Parent.Name, obj.Name),
264
- ),
265
- }
266
-
267
- for i, r in rules.iterrows():
268
- ruleName = r["Rule Name"]
269
- expr = r["Expression"]
270
- scopes = r["Scope"]
271
-
272
- if isinstance(scopes, str):
273
- scopes = [scopes]
274
-
275
- for scope in scopes:
276
- func = scope_to_dataframe[scope][0]
277
- nm = scope_to_dataframe[scope][1]
278
-
279
- if scope == "Model":
280
- x = []
281
- if expr(func, tom):
282
- x = ["Model"]
283
- elif scope == "Measure":
284
- x = [nm(obj) for obj in tom.all_measures() if expr(obj, tom)]
285
- elif scope == "Column":
286
- x = [nm(obj) for obj in tom.all_columns() if expr(obj, tom)]
287
- elif scope == "Partition":
288
- x = [nm(obj) for obj in tom.all_partitions() if expr(obj, tom)]
289
- elif scope == "Hierarchy":
290
- x = [nm(obj) for obj in tom.all_hierarchies() if expr(obj, tom)]
291
- elif scope == "Table":
292
- x = [nm(obj) for obj in tom.model.Tables if expr(obj, tom)]
293
- elif scope == "Relationship":
294
- x = [nm(obj) for obj in tom.model.Relationships if expr(obj, tom)]
295
- elif scope == "Role":
296
- x = [nm(obj) for obj in tom.model.Roles if expr(obj, tom)]
297
- elif scope == "Row Level Security":
298
- x = [nm(obj) for obj in tom.all_rls() if expr(obj, tom)]
299
- elif scope == "Calculation Item":
300
- x = [
301
- nm(obj) for obj in tom.all_calculation_items() if expr(obj, tom)
302
- ]
303
-
304
- if len(x) > 0:
305
- new_data = {"Object Name": x, "Scope": scope, "Rule Name": ruleName}
306
- violations = pd.concat(
307
- [violations, pd.DataFrame(new_data)], ignore_index=True
308
- )
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
+ )
309
339
 
310
- prepDF = pd.merge(
311
- violations,
312
- rules[["Rule Name", "Category", "Severity", "Description", "URL"]],
313
- left_on="Rule Name",
314
- right_on="Rule Name",
315
- how="left",
316
- )
317
- prepDF.rename(columns={"Scope": "Object Type"}, inplace=True)
318
- finalDF = prepDF[
319
- [
320
- "Category",
321
- "Rule Name",
322
- "Severity",
323
- "Object Type",
324
- "Object Name",
325
- "Description",
326
- "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
+ ]
327
358
  ]
328
- ]
329
359
 
330
360
  if export:
331
361
  if not lakehouse_attached():
@@ -387,10 +417,14 @@ def run_model_bpa(
387
417
  dfExport.insert(5, colName, dfExport.pop(colName))
388
418
 
389
419
  dfExport.columns = dfExport.columns.str.replace(" ", "_")
420
+ schema = {
421
+ key.replace(" ", "_"): value for key, value in icons.bpa_schema.items()
422
+ }
390
423
  save_as_delta_table(
391
424
  dataframe=dfExport,
392
425
  delta_table_name=delta_table_name,
393
426
  write_mode="append",
427
+ schema=schema,
394
428
  merge_schema=True,
395
429
  )
396
430
 
@@ -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():
@@ -59,24 +64,6 @@ def run_model_bpa_bulk(
59
64
 
60
65
  skip_models.extend(["ModelBPA", "Fabric Capacity Metrics"])
61
66
 
62
- cols = [
63
- "Capacity Name",
64
- "Capacity Id",
65
- "Workspace Name",
66
- "Workspace Id",
67
- "Dataset Name",
68
- "Dataset Id",
69
- "Configured By",
70
- "Rule Name",
71
- "Category",
72
- "Severity",
73
- "Object Type",
74
- "Object Name",
75
- "Description",
76
- "URL",
77
- "RunId",
78
- "Timestamp",
79
- ]
80
67
  now = datetime.datetime.now()
81
68
  output_table = "modelbparesults"
82
69
  lakehouse_workspace = fabric.resolve_workspace_name()
@@ -86,7 +73,6 @@ def run_model_bpa_bulk(
86
73
  )
87
74
  lakeT = get_lakehouse_tables(lakehouse=lakehouse, workspace=lakehouse_workspace)
88
75
  lakeT_filt = lakeT[lakeT["Table Name"] == output_table]
89
- # query = f"SELECT MAX(RunId) FROM {lakehouse}.{output_table}"
90
76
  if len(lakeT_filt) == 0:
91
77
  runId = 1
92
78
  else:
@@ -102,13 +88,22 @@ def run_model_bpa_bulk(
102
88
  else:
103
89
  dfW_filt = dfW[dfW["Name"].isin(workspace)]
104
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
+
105
96
  for i, r in dfW_filt.iterrows():
106
97
  wksp = r["Name"]
107
98
  wksp_id = r["Id"]
108
99
  capacity_id, capacity_name = resolve_workspace_capacity(workspace=wksp)
109
- df = pd.DataFrame(columns=cols)
100
+ df = pd.DataFrame(columns=list(icons.bpa_schema.keys()))
110
101
  dfD = fabric.list_datasets(workspace=wksp, mode="rest")
111
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
+
112
107
  # Exclude default semantic models
113
108
  if len(dfD) > 0:
114
109
  dfI = fabric.list_items(workspace=wksp)
@@ -137,8 +132,8 @@ def run_model_bpa_bulk(
137
132
  rules=rules,
138
133
  extended=extended,
139
134
  )
140
- bpa_df["Capacity Id"] = capacity_id
141
135
  bpa_df["Capacity Name"] = capacity_name
136
+ bpa_df["Capacity Id"] = capacity_id
142
137
  bpa_df["Workspace Name"] = wksp
143
138
  bpa_df["Workspace Id"] = wksp_id
144
139
  bpa_df["Dataset Name"] = dataset_name
@@ -146,7 +141,7 @@ def run_model_bpa_bulk(
146
141
  bpa_df["Configured By"] = config_by
147
142
  bpa_df["Timestamp"] = now
148
143
  bpa_df["RunId"] = runId
149
- bpa_df = bpa_df[cols]
144
+ bpa_df = bpa_df[list(icons.bpa_schema.keys())]
150
145
 
151
146
  bpa_df["RunId"] = bpa_df["RunId"].astype("int")
152
147
 
@@ -160,21 +155,33 @@ def run_model_bpa_bulk(
160
155
  )
161
156
  print(e)
162
157
 
163
- 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)
164
164
 
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
- )
169
- save_as_delta_table(
170
- dataframe=df,
171
- delta_table_name=output_table,
172
- write_mode="append",
173
- merge_schema=True,
174
- )
175
- print(
176
- f"{icons.green_dot} Saved BPA results to the '{output_table}' delta table."
177
- )
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
+ )
169
+
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
+ )
178
185
 
179
186
  print(f"{icons.green_dot} Bulk BPA scan complete.")
180
187
 
@@ -203,9 +210,6 @@ def create_model_bpa_semantic_model(
203
210
  The workspace in which the lakehouse resides.
204
211
  Defaults to None which resolves to the workspace of the attached lakehouse
205
212
  or if no lakehouse attached, resolves to the workspace of the notebook.
206
-
207
- Returns
208
- -------
209
213
  """
210
214
 
211
215
  from sempy_labs._helper_functions import resolve_lakehouse_name
@@ -244,7 +248,7 @@ def create_model_bpa_semantic_model(
244
248
  tom.model
245
249
 
246
250
  dyn_connect()
247
-
251
+ icons.sll_tags.append("ModelBPABulk")
248
252
  table_exists = False
249
253
  with connect_semantic_model(
250
254
  dataset=dataset, readonly=False, workspace=lakehouse_workspace