semantic-link-labs 0.7.4__py3-none-any.whl → 0.8.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.

Potentially problematic release.


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

Files changed (32) hide show
  1. {semantic_link_labs-0.7.4.dist-info → semantic_link_labs-0.8.0.dist-info}/METADATA +7 -3
  2. {semantic_link_labs-0.7.4.dist-info → semantic_link_labs-0.8.0.dist-info}/RECORD +32 -23
  3. {semantic_link_labs-0.7.4.dist-info → semantic_link_labs-0.8.0.dist-info}/WHEEL +1 -1
  4. sempy_labs/__init__.py +57 -18
  5. sempy_labs/_capacities.py +39 -3
  6. sempy_labs/_capacity_migration.py +624 -0
  7. sempy_labs/_clear_cache.py +8 -8
  8. sempy_labs/_connections.py +15 -13
  9. sempy_labs/_git.py +20 -21
  10. sempy_labs/_helper_functions.py +33 -30
  11. sempy_labs/_icons.py +19 -0
  12. sempy_labs/_list_functions.py +210 -0
  13. sempy_labs/_model_bpa.py +1 -1
  14. sempy_labs/_query_scale_out.py +4 -3
  15. sempy_labs/_spark.py +31 -36
  16. sempy_labs/_sql.py +60 -15
  17. sempy_labs/_vertipaq.py +9 -7
  18. sempy_labs/admin/__init__.py +53 -0
  19. sempy_labs/admin/_basic_functions.py +806 -0
  20. sempy_labs/admin/_domains.py +411 -0
  21. sempy_labs/directlake/_generate_shared_expression.py +11 -14
  22. sempy_labs/directlake/_update_directlake_model_lakehouse_connection.py +14 -24
  23. sempy_labs/report/__init__.py +9 -6
  24. sempy_labs/report/_report_bpa.py +359 -0
  25. sempy_labs/report/_report_bpa_rules.py +113 -0
  26. sempy_labs/report/_report_helper.py +254 -0
  27. sempy_labs/report/_report_list_functions.py +95 -0
  28. sempy_labs/report/_report_rebind.py +0 -4
  29. sempy_labs/report/_reportwrapper.py +2039 -0
  30. sempy_labs/tom/_model.py +78 -4
  31. {semantic_link_labs-0.7.4.dist-info → semantic_link_labs-0.8.0.dist-info}/LICENSE +0 -0
  32. {semantic_link_labs-0.7.4.dist-info → semantic_link_labs-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2039 @@
1
+ import sempy.fabric as fabric
2
+ from sempy_labs._helper_functions import (
3
+ resolve_report_id,
4
+ format_dax_object_name,
5
+ resolve_dataset_from_report,
6
+ _conv_b64,
7
+ _extract_json,
8
+ _add_part,
9
+ lro,
10
+ )
11
+ from typing import Optional, List
12
+ import pandas as pd
13
+ import re
14
+ import json
15
+ import base64
16
+ from uuid import UUID
17
+ from sempy._utils._log import log
18
+ import sempy_labs._icons as icons
19
+ import sempy_labs.report._report_helper as helper
20
+ from sempy_labs._model_dependencies import get_measure_dependencies
21
+ from jsonpath_ng.ext import parse
22
+ import warnings
23
+
24
+
25
+ class ReportWrapper:
26
+
27
+ _report: str
28
+ _workspace: str
29
+
30
+ @log
31
+ def __init__(
32
+ self,
33
+ report: str,
34
+ workspace: Optional[str] = None,
35
+ ):
36
+
37
+ from sempy_labs.report import get_report_definition
38
+
39
+ warnings.simplefilter(action="ignore", category=FutureWarning)
40
+
41
+ self._report = report
42
+ self._workspace = workspace
43
+ self._workspace_id = fabric.resolve_workspace_id(workspace)
44
+ self._report_id = resolve_report_id(report, workspace)
45
+ self.rdef = get_report_definition(
46
+ report=self._report, workspace=self._workspace
47
+ )
48
+
49
+ if len(self.rdef[self.rdef["path"] == "definition/report.json"]) == 0:
50
+ raise ValueError(
51
+ f"{icons.red_dot} The ReportWrapper function requires the report to be in the PBIR format."
52
+ "See here for details: https://powerbi.microsoft.com/blog/power-bi-enhanced-report-format-pbir-in-power-bi-desktop-developer-mode-preview/"
53
+ )
54
+
55
+ # Helper functions
56
+ def _add_extended(self, dataframe):
57
+
58
+ from sempy_labs.tom import connect_semantic_model
59
+
60
+ dataset_id, dataset_name, dataset_workspace_id, dataset_workspace = (
61
+ resolve_dataset_from_report(report=self._report, workspace=self._workspace)
62
+ )
63
+
64
+ with connect_semantic_model(
65
+ dataset=dataset_name, readonly=True, workspace=dataset_workspace
66
+ ) as tom:
67
+ for index, row in dataframe.iterrows():
68
+ obj_type = row["Object Type"]
69
+ if obj_type == "Measure":
70
+ dataframe.at[index, "Valid Semantic Model Object"] = any(
71
+ o.Name == row["Object Name"] for o in tom.all_measures()
72
+ )
73
+ elif obj_type == "Column":
74
+ dataframe.at[index, "Valid Semantic Model Object"] = any(
75
+ format_dax_object_name(c.Parent.Name, c.Name)
76
+ == format_dax_object_name(row["Table Name"], row["Object Name"])
77
+ for c in tom.all_columns()
78
+ )
79
+ elif obj_type == "Hierarchy":
80
+ dataframe.at[index, "Valid Semantic Model Object"] = any(
81
+ format_dax_object_name(h.Parent.Name, h.Name)
82
+ == format_dax_object_name(row["Table Name"], row["Object Name"])
83
+ for h in tom.all_hierarchies()
84
+ )
85
+ return dataframe
86
+
87
+ def update_report(self, request_body: dict):
88
+
89
+ client = fabric.FabricRestClient()
90
+ response = client.post(
91
+ f"/v1/workspaces/{self._workspace_id}/reports/{self._report_id}/updateDefinition",
92
+ json=request_body,
93
+ )
94
+
95
+ lro(client, response, return_status_code=True)
96
+
97
+ def resolve_page_name(self, page_display_name: str) -> UUID:
98
+ """
99
+ Obtains the page name, page display name, and the file path for a given page in a report.
100
+
101
+ Parameters
102
+ ----------
103
+ page_display_name : str
104
+ The display name of the page of the report.
105
+
106
+ Returns
107
+ -------
108
+ UUID
109
+ The page name.
110
+ """
111
+
112
+ x, y, z = helper.resolve_page_name(self, page_display_name)
113
+
114
+ return x
115
+
116
+ def resolve_page_display_name(self, page_name: UUID) -> str:
117
+ """
118
+ Obtains the page dispaly name.
119
+
120
+ Parameters
121
+ ----------
122
+ page_name : UUID
123
+ The name of the page of the report.
124
+
125
+ Returns
126
+ -------
127
+ str
128
+ The page display name.
129
+ """
130
+
131
+ x, y, z = helper.resolve_page_name(self, page_name=page_name)
132
+
133
+ return y
134
+
135
+ def get_theme(self, theme_type: str = "baseTheme") -> dict:
136
+ """
137
+ Obtains
138
+
139
+ Parameters
140
+ ----------
141
+ dataset : str
142
+ Name of the semantic model.
143
+ workspace : str, default=None
144
+ The Fabric workspace name.
145
+ Defaults to None which resolves to the workspace of the attached lakehouse
146
+ or if no lakehouse attached, resolves to the workspace of the notebook.
147
+
148
+ Returns
149
+ -------
150
+ pandas.DataFrame
151
+ A pandas dataframe showing a list of all columns in the semantic model which are not used in any related Power BI reports (including dependencies).
152
+ """
153
+
154
+ theme_types = ["baseTheme", "customTheme"]
155
+ theme_type = theme_type.lower()
156
+
157
+ if "custom" in theme_type:
158
+ theme_type = "customTheme"
159
+ elif "base" in theme_type:
160
+ theme_type = "baseTheme"
161
+ if theme_type not in theme_types:
162
+ raise ValueError(
163
+ f"{icons.red_dot} Invalid theme type. Valid options: {theme_types}."
164
+ )
165
+
166
+ rptdef = self.rdef[self.rdef["path"] == "definition/report.json"]
167
+ rptJson = _extract_json(rptdef)
168
+ theme_collection = rptJson.get("themeCollection", {})
169
+ if theme_type not in theme_collection:
170
+ raise ValueError(
171
+ f"{icons.red_dot} The {self._report} report within the '{self._workspace} workspace has no custom theme."
172
+ )
173
+ ct = theme_collection.get(theme_type)
174
+ theme_name = ct["name"]
175
+ theme_location = ct["type"]
176
+ theme_file_path = f"StaticResources/{theme_location}/{theme_name}"
177
+ if theme_type == "baseTheme":
178
+ theme_file_path = (
179
+ f"StaticResources/{theme_location}/BaseThemes/{theme_name}"
180
+ )
181
+ if not theme_file_path.endswith(".json"):
182
+ theme_file_path = f"{theme_file_path}.json"
183
+
184
+ theme_df = self.rdef[self.rdef["path"] == theme_file_path]
185
+ theme_json = _extract_json(theme_df)
186
+
187
+ return theme_json
188
+
189
+ # List functions
190
+ def list_custom_visuals(self) -> pd.DataFrame:
191
+ """
192
+ Shows a list of all custom visuals used in the report.
193
+
194
+ Returns
195
+ -------
196
+ pandas.DataFrame
197
+ A pandas dataframe containing a list of all the custom visuals used in the report.
198
+ """
199
+
200
+ helper.populate_custom_visual_display_names()
201
+
202
+ df = pd.DataFrame(columns=["Custom Visual Name", "Custom Visual Display Name"])
203
+ rd = self.rdef
204
+ rd_filt = rd[rd["path"] == "definition/report.json"]
205
+ rptJson = _extract_json(rd_filt)
206
+ df["Custom Visual Name"] = rptJson.get("publicCustomVisuals")
207
+ df["Custom Visual Display Name"] = df["Custom Visual Name"].apply(
208
+ lambda x: helper.vis_type_mapping.get(x, x)
209
+ )
210
+
211
+ df["Used in Report"] = df["Custom Visual Name"].isin(
212
+ self.list_visuals()["Type"]
213
+ )
214
+
215
+ bool_cols = ["Used in Report"]
216
+ df[bool_cols] = df[bool_cols].astype(bool)
217
+
218
+ return df
219
+
220
+ def list_report_filters(self, extended: bool = False) -> pd.DataFrame:
221
+ """
222
+ Shows a list of all report filters used in the report.
223
+
224
+ Parameters
225
+ ----------
226
+ extended : bool, default=False
227
+ If True, adds an extra column called 'Valid Semantic Model Object' which identifies whether the semantic model object used
228
+ in the report exists in the semantic model which feeds data to the report.
229
+
230
+ Returns
231
+ -------
232
+ pandas.DataFrame
233
+ A pandas dataframe containing a list of all the report filters used in the report.
234
+ """
235
+
236
+ rd_filt = self.rdef[self.rdef["path"] == "definition/report.json"]
237
+
238
+ df = pd.DataFrame(
239
+ columns=[
240
+ "Filter Name",
241
+ "Type",
242
+ "Table Name",
243
+ "Object Name",
244
+ "Object Type",
245
+ "Hidden",
246
+ "Locked",
247
+ "How Created",
248
+ "Used",
249
+ ]
250
+ )
251
+
252
+ if len(rd_filt) == 1:
253
+ rpt_json = _extract_json(rd_filt)
254
+ if "filterConfig" in rpt_json:
255
+ for flt in rpt_json.get("filterConfig", {}).get("filters", {}):
256
+ filter_name = flt.get("name")
257
+ how_created = flt.get("howCreated")
258
+ locked = flt.get("isLockedInViewMode", False)
259
+ hidden = flt.get("isHiddenInViewMode", False)
260
+ filter_type = flt.get("type", "Basic")
261
+ filter_used = True if "Where" in flt.get("filter", {}) else False
262
+
263
+ entity_property_pairs = helper.find_entity_property_pairs(flt)
264
+
265
+ for object_name, properties in entity_property_pairs.items():
266
+ new_data = {
267
+ "Filter Name": filter_name,
268
+ "Type": filter_type,
269
+ "Table Name": properties[0],
270
+ "Object Name": object_name,
271
+ "Object Type": properties[1],
272
+ "Hidden": hidden,
273
+ "Locked": locked,
274
+ "How Created": how_created,
275
+ "Used": filter_used,
276
+ }
277
+
278
+ df = pd.concat(
279
+ [df, pd.DataFrame(new_data, index=[0])], ignore_index=True
280
+ )
281
+
282
+ bool_cols = ["Hidden", "Locked", "Used"]
283
+ df[bool_cols] = df[bool_cols].astype(bool)
284
+
285
+ if extended:
286
+ df = self._add_extended(dataframe=df)
287
+
288
+ return df
289
+
290
+ def list_page_filters(self, extended: bool = False) -> pd.DataFrame:
291
+ """
292
+ Shows a list of all page filters used in the report.
293
+
294
+ Parameters
295
+ ----------
296
+ extended : bool, default=False
297
+ If True, adds an extra column called 'Valid Semantic Model Object' which identifies whether the semantic model object used
298
+ in the report exists in the semantic model which feeds data to the report.
299
+
300
+ Returns
301
+ -------
302
+ pandas.DataFrame
303
+ A pandas dataframe containing a list of all the page filters used in the report.
304
+ """
305
+
306
+ rd = self.rdef
307
+ df = pd.DataFrame(
308
+ columns=[
309
+ "Page Name",
310
+ "Page Display Name",
311
+ "Filter Name",
312
+ "Type",
313
+ "Table Name",
314
+ "Object Name",
315
+ "Object Type",
316
+ "Hidden",
317
+ "Locked",
318
+ "How Created",
319
+ "Used",
320
+ ]
321
+ )
322
+
323
+ for _, r in rd.iterrows():
324
+ path = r["path"]
325
+ payload = r["payload"]
326
+ if path.endswith("/page.json"):
327
+ obj_file = base64.b64decode(payload).decode("utf-8")
328
+ obj_json = json.loads(obj_file)
329
+ page_id = obj_json.get("name")
330
+ page_display = obj_json.get("displayName")
331
+
332
+ if "filterConfig" in obj_json:
333
+ for flt in obj_json.get("filterConfig", {}).get("filters", {}):
334
+ filter_name = flt.get("name")
335
+ how_created = flt.get("howCreated")
336
+ locked = flt.get("isLockedInViewMode", False)
337
+ hidden = flt.get("isHiddenInViewMode", False)
338
+ filter_type = flt.get("type", "Basic")
339
+ filter_used = (
340
+ True if "Where" in flt.get("filter", {}) else False
341
+ )
342
+
343
+ entity_property_pairs = helper.find_entity_property_pairs(flt)
344
+
345
+ for object_name, properties in entity_property_pairs.items():
346
+ new_data = {
347
+ "Page Name": page_id,
348
+ "Page Display Name": page_display,
349
+ "Filter Name": filter_name,
350
+ "Type": filter_type,
351
+ "Table Name": properties[0],
352
+ "Object Name": object_name,
353
+ "Object Type": properties[1],
354
+ "Hidden": hidden,
355
+ "Locked": locked,
356
+ "How Created": how_created,
357
+ "Used": filter_used,
358
+ }
359
+
360
+ df = pd.concat(
361
+ [df, pd.DataFrame(new_data, index=[0])],
362
+ ignore_index=True,
363
+ )
364
+
365
+ df["Page URL"] = (
366
+ f"{helper.get_web_url(report=self._report, workspace=self._workspace)}/{df['Page Name']}"
367
+ )
368
+
369
+ bool_cols = ["Hidden", "Locked", "Used"]
370
+ df[bool_cols] = df[bool_cols].astype(bool)
371
+
372
+ if extended:
373
+ df = self._add_extended(dataframe=df)
374
+
375
+ return df
376
+
377
+ def list_visual_filters(self, extended: bool = False) -> pd.DataFrame:
378
+ """
379
+ Shows a list of all visual filters used in the report.
380
+
381
+ Parameters
382
+ ----------
383
+ extended : bool, default=False
384
+ If True, adds an extra column called 'Valid Semantic Model Object' which identifies whether the semantic model object used
385
+ in the report exists in the semantic model which feeds data to the report.
386
+
387
+ Returns
388
+ -------
389
+ pandas.DataFrame
390
+ A pandas dataframe containing a list of all the visual filters used in the report.
391
+ """
392
+
393
+ rd = self.rdef
394
+ df = pd.DataFrame(
395
+ columns=[
396
+ "Page Name",
397
+ "Page Display Name",
398
+ "Visual Name",
399
+ "Filter Name",
400
+ "Type",
401
+ "Table Name",
402
+ "Object Name",
403
+ "Object Type",
404
+ "Hidden",
405
+ "Locked",
406
+ "How Created",
407
+ "Used",
408
+ ]
409
+ )
410
+ page_mapping, visual_mapping = helper.visual_page_mapping(self)
411
+
412
+ for _, r in rd.iterrows():
413
+ path = r["path"]
414
+ payload = r["payload"]
415
+ if path.endswith("/visual.json"):
416
+ obj_file = base64.b64decode(payload).decode("utf-8")
417
+ obj_json = json.loads(obj_file)
418
+ page_id = visual_mapping.get(path)[0]
419
+ page_display = visual_mapping.get(path)[1]
420
+ visual_name = obj_json.get("name")
421
+
422
+ if "filterConfig" in obj_json:
423
+ for flt in obj_json.get("filterConfig", {}).get("filters", {}):
424
+ filter_name = flt.get("name")
425
+ how_created = flt.get("howCreated")
426
+ locked = flt.get("isLockedInViewMode", False)
427
+ hidden = flt.get("isHiddenInViewMode", False)
428
+ filter_type = flt.get("type", "Basic")
429
+ filter_used = (
430
+ True if "Where" in flt.get("filter", {}) else False
431
+ )
432
+
433
+ entity_property_pairs = helper.find_entity_property_pairs(flt)
434
+
435
+ for object_name, properties in entity_property_pairs.items():
436
+ new_data = {
437
+ "Page Name": page_id,
438
+ "Page Display Name": page_display,
439
+ "Visual Name": visual_name,
440
+ "Filter Name": filter_name,
441
+ "Type": filter_type,
442
+ "Table Name": properties[0],
443
+ "Object Name": object_name,
444
+ "Object Type": properties[1],
445
+ "Hidden": hidden,
446
+ "Locked": locked,
447
+ "How Created": how_created,
448
+ "Used": filter_used,
449
+ }
450
+
451
+ df = pd.concat(
452
+ [df, pd.DataFrame(new_data, index=[0])],
453
+ ignore_index=True,
454
+ )
455
+
456
+ bool_cols = ["Hidden", "Locked", "Used"]
457
+ df[bool_cols] = df[bool_cols].astype(bool)
458
+
459
+ if extended:
460
+ df = self._add_extended(dataframe=df)
461
+
462
+ return df
463
+
464
+ def list_visual_interactions(self) -> pd.DataFrame:
465
+ """
466
+ Shows a list of all modified `visual interactions <https://learn.microsoft.com/power-bi/create-reports/service-reports-visual-interactions?tabs=powerbi-desktop>`_ used in the report.
467
+
468
+ Parameters
469
+ ----------
470
+
471
+ Returns
472
+ -------
473
+ pandas.DataFrame
474
+ A pandas dataframe containing a list of all modified visual interactions used in the report.
475
+ """
476
+
477
+ rd = self.rdef
478
+ df = pd.DataFrame(
479
+ columns=[
480
+ "Page Name",
481
+ "Page Display Name",
482
+ "Source Visual Name",
483
+ "Target Visual Name",
484
+ "Type",
485
+ ]
486
+ )
487
+
488
+ for _, r in rd.iterrows():
489
+ file_path = r["path"]
490
+ payload = r["payload"]
491
+ if file_path.endswith("/page.json"):
492
+ obj_file = base64.b64decode(payload).decode("utf-8")
493
+ obj_json = json.loads(obj_file)
494
+ page_name = obj_json.get("name")
495
+ page_display = obj_json.get("displayName")
496
+
497
+ for vizInt in obj_json.get("visualInteractions", []):
498
+ sourceVisual = vizInt.get("source")
499
+ targetVisual = vizInt.get("target")
500
+ vizIntType = vizInt.get("type")
501
+
502
+ new_data = {
503
+ "Page Name": page_name,
504
+ "Page Display Name": page_display,
505
+ "Source Visual Name": sourceVisual,
506
+ "Target Visual Name": targetVisual,
507
+ "Type": vizIntType,
508
+ }
509
+ df = pd.concat(
510
+ [df, pd.DataFrame(new_data, index=[0])], ignore_index=True
511
+ )
512
+
513
+ return df
514
+
515
+ def list_pages(self) -> pd.DataFrame:
516
+ """
517
+ Shows a list of all pages in the report.
518
+
519
+ Returns
520
+ -------
521
+ pandas.DataFrame
522
+ A pandas dataframe containing a list of all pages in the report.
523
+ """
524
+
525
+ rd = self.rdef
526
+ df = pd.DataFrame(
527
+ columns=[
528
+ "File Path",
529
+ "Page Name",
530
+ "Page Display Name",
531
+ "Hidden",
532
+ "Active",
533
+ "Width",
534
+ "Height",
535
+ "Display Option",
536
+ "Type",
537
+ "Alignment",
538
+ "Drillthrough Target Page",
539
+ "Visual Count",
540
+ "Data Visual Count",
541
+ "Visible Visual Count",
542
+ "Page Filter Count",
543
+ ]
544
+ )
545
+
546
+ dfV = self.list_visuals()
547
+
548
+ page_rows = rd[rd["path"].str.endswith("/page.json")]
549
+ pages_row = rd[rd["path"] == "definition/pages/pages.json"]
550
+
551
+ for _, r in page_rows.iterrows():
552
+ file_path = r["path"]
553
+ payload = r["payload"]
554
+
555
+ pageFile = base64.b64decode(payload).decode("utf-8")
556
+ page_prefix = file_path[0:-9]
557
+ pageJson = json.loads(pageFile)
558
+ page_name = pageJson.get("name")
559
+ height = pageJson.get("height")
560
+ width = pageJson.get("width")
561
+
562
+ # Alignment
563
+ matches = parse(
564
+ "$.objects.displayArea[0].properties.verticalAlignment.expr.Literal.Value"
565
+ ).find(pageJson)
566
+ alignment_value = (
567
+ matches[0].value[1:-1] if matches and matches[0].value else "Top"
568
+ )
569
+
570
+ # Drillthrough
571
+ matches = parse("$.filterConfig.filters[*].howCreated").find(pageJson)
572
+ how_created_values = [match.value for match in matches]
573
+ drill_through = any(value == "Drillthrough" for value in how_created_values)
574
+ # matches = parse("$.filterConfig.filters[*]").find(pageJson)
575
+ # drill_through = any(
576
+ # filt.get("howCreated") == "Drillthrough"
577
+ # for filt in (match.value for match in matches)
578
+ # )
579
+
580
+ visual_count = len(
581
+ rd[
582
+ rd["path"].str.endswith("/visual.json")
583
+ & (rd["path"].str.startswith(page_prefix))
584
+ ]
585
+ )
586
+ data_visual_count = len(
587
+ dfV[(dfV["Page Name"] == page_name) & (dfV["Data Visual"])]
588
+ )
589
+ visible_visual_count = len(
590
+ dfV[(dfV["Page Name"] == page_name) & (dfV["Hidden"] == False)]
591
+ )
592
+
593
+ # Page Filter Count
594
+ matches = parse("$.filterConfig.filters").find(pageJson)
595
+ page_filter_count = (
596
+ len(matches[0].value) if matches and matches[0].value else 0
597
+ )
598
+
599
+ # Hidden
600
+ matches = parse("$.visibility").find(pageJson)
601
+ is_hidden = any(match.value == "HiddenInViewMode" for match in matches)
602
+
603
+ new_data = {
604
+ "File Path": file_path,
605
+ "Page Name": page_name,
606
+ "Page Display Name": pageJson.get("displayName"),
607
+ "Display Option": pageJson.get("displayOption"),
608
+ "Height": height,
609
+ "Width": width,
610
+ "Hidden": is_hidden,
611
+ "Active": False,
612
+ "Type": helper.page_type_mapping.get((width, height), "Custom"),
613
+ "Alignment": alignment_value,
614
+ "Drillthrough Target Page": drill_through,
615
+ "Visual Count": visual_count,
616
+ "Data Visual Count": data_visual_count,
617
+ "Visible Visual Count": visible_visual_count,
618
+ "Page Filter Count": page_filter_count,
619
+ }
620
+ df = pd.concat([df, pd.DataFrame(new_data, index=[0])], ignore_index=True)
621
+
622
+ page_payload = pages_row["payload"].iloc[0]
623
+ pageFile = base64.b64decode(page_payload).decode("utf-8")
624
+ pageJson = json.loads(pageFile)
625
+ activePage = pageJson["activePageName"]
626
+
627
+ df.loc[df["Page Name"] == activePage, "Active"] = True
628
+
629
+ int_cols = [
630
+ "Width",
631
+ "Height",
632
+ "Page Filter Count",
633
+ "Visual Count",
634
+ "Visible Visual Count",
635
+ "Data Visual Count",
636
+ ]
637
+ df[int_cols] = df[int_cols].astype(int)
638
+
639
+ bool_cols = ["Hidden", "Active", "Drillthrough Target Page"]
640
+ df[bool_cols] = df[bool_cols].astype(bool)
641
+
642
+ df["Page URL"] = (
643
+ f"{helper.get_web_url(report=self._report, workspace=self._workspace)}/{df['Page Name']}"
644
+ )
645
+
646
+ return df
647
+
648
+ def list_visuals(self) -> pd.DataFrame:
649
+ """
650
+ Shows a list of all visuals in the report.
651
+
652
+ Returns
653
+ -------
654
+ pandas.DataFrame
655
+ A pandas dataframe containing a list of all visuals in the report.
656
+ """
657
+
658
+ rd = self.rdef
659
+ df = pd.DataFrame(
660
+ columns=[
661
+ "File Path",
662
+ "Page Name",
663
+ "Page Display Name",
664
+ "Visual Name",
665
+ "Type",
666
+ "Display Type",
667
+ "X",
668
+ "Y",
669
+ "Z",
670
+ "Width",
671
+ "Height",
672
+ "Tab Order",
673
+ "Hidden",
674
+ "Title",
675
+ "SubTitle",
676
+ "Custom Visual",
677
+ "Alt Text",
678
+ "Show Items With No Data",
679
+ "Divider",
680
+ "Slicer Type",
681
+ "Row SubTotals",
682
+ "Column SubTotals",
683
+ "Data Visual",
684
+ "Has Sparkline",
685
+ "Visual Filter Count",
686
+ "Data Limit",
687
+ ]
688
+ )
689
+
690
+ rd_filt = rd[rd["path"] == "definition/report.json"]
691
+ payload = rd_filt["payload"].iloc[0]
692
+ rptJson = _extract_json(rd_filt)
693
+ custom_visuals = rptJson.get("publicCustomVisuals", [])
694
+ page_mapping, visual_mapping = helper.visual_page_mapping(self)
695
+ helper.populate_custom_visual_display_names()
696
+
697
+ def contains_key(data, keys_to_check):
698
+ matches = parse("$..*").find(data)
699
+
700
+ all_keys = set()
701
+ for match in matches:
702
+ if isinstance(match.value, dict):
703
+ all_keys.update(match.value.keys())
704
+ elif isinstance(match.value, list):
705
+ for item in match.value:
706
+ if isinstance(item, dict):
707
+ all_keys.update(item.keys())
708
+
709
+ return any(key in all_keys for key in keys_to_check)
710
+
711
+ for _, r in rd.iterrows():
712
+ file_path = r["path"]
713
+ payload = r["payload"]
714
+ if file_path.endswith("/visual.json"):
715
+ visual_file = base64.b64decode(payload).decode("utf-8")
716
+ visual_json = json.loads(visual_file)
717
+ page_id = visual_mapping.get(file_path)[0]
718
+ page_display = visual_mapping.get(file_path)[1]
719
+ pos = visual_json.get("position")
720
+
721
+ # Visual Type
722
+ matches = parse("$.visual.visualType").find(visual_json)
723
+ visual_type = matches[0].value if matches else "Group"
724
+
725
+ visual_type_display = helper.vis_type_mapping.get(
726
+ visual_type, visual_type
727
+ )
728
+ cst_value, rst_value, slicer_type = False, False, "N/A"
729
+
730
+ # Visual Filter Count
731
+ matches = parse("$.filterConfig.filters[*]").find(visual_json)
732
+ visual_filter_count = len(matches)
733
+
734
+ # Data Limit
735
+ matches = parse(
736
+ '$.filterConfig.filters[?(@.type == "VisualTopN")].filter.Where[*].Condition.VisualTopN.ItemCount'
737
+ ).find(visual_json)
738
+ data_limit = matches[0].value if matches else 0
739
+
740
+ # Title
741
+ matches = parse(
742
+ "$.visual.visualContainerObjects.title[0].properties.text.expr.Literal.Value"
743
+ ).find(visual_json)
744
+ title = matches[0].value[1:-1] if matches else ""
745
+
746
+ # SubTitle
747
+ matches = parse(
748
+ "$.visual.visualContainerObjects.subTitle[0].properties.text.expr.Literal.Value"
749
+ ).find(visual_json)
750
+ sub_title = matches[0].value[1:-1] if matches else ""
751
+
752
+ # Alt Text
753
+ matches = parse(
754
+ "$.visual.visualContainerObjects.general[0].properties.altText.expr.Literal.Value"
755
+ ).find(visual_json)
756
+ alt_text = matches[0].value[1:-1] if matches else ""
757
+
758
+ # Show items with no data
759
+ def find_show_all_with_jsonpath(obj):
760
+ matches = parse("$..showAll").find(obj)
761
+ return any(match.value is True for match in matches)
762
+
763
+ show_all_data = find_show_all_with_jsonpath(visual_json)
764
+
765
+ # Divider
766
+ matches = parse(
767
+ "$.visual.visualContainerObjects.divider[0].properties.show.expr.Literal.Value"
768
+ ).find(visual_json)
769
+ divider = matches[0] if matches else ""
770
+
771
+ # Row/Column Subtotals
772
+ if visual_type == "pivotTable":
773
+ cst_matches = parse(
774
+ "$.visual.objects.subTotals[0].properties.columnSubtotals.expr.Literal.Value"
775
+ ).find(visual_json)
776
+ rst_matches = parse(
777
+ "$.visual.objects.subTotals[0].properties.rowSubtotals.expr.Literal.Value"
778
+ ).find(visual_json)
779
+
780
+ if cst_matches:
781
+ cst_value = False if cst_matches[0].value == "false" else True
782
+
783
+ if rst_matches:
784
+ rst_value = False if rst_matches[0].value == "false" else True
785
+
786
+ # Slicer Type
787
+ if visual_type == "slicer":
788
+ matches = parse(
789
+ "$.visual.objects.data[0].properties.mode.expr.Literal.Value"
790
+ ).find(visual_json)
791
+ slicer_type = matches[0].value[1:-1] if matches else "N/A"
792
+
793
+ # Data Visual
794
+ is_data_visual = contains_key(
795
+ visual_json,
796
+ [
797
+ "Aggregation",
798
+ "Column",
799
+ "Measure",
800
+ "HierarchyLevel",
801
+ "NativeVisualCalculation",
802
+ ],
803
+ )
804
+
805
+ # Sparkline
806
+ has_sparkline = contains_key(visual_json, ["SparklineData"])
807
+
808
+ new_data = {
809
+ "File Path": file_path,
810
+ "Page Name": page_id,
811
+ "Page Display Name": page_display,
812
+ "Visual Name": visual_json.get("name"),
813
+ "X": pos.get("x"),
814
+ "Y": pos.get("y"),
815
+ "Z": pos.get("z"),
816
+ "Width": pos.get("width"),
817
+ "Height": pos.get("height"),
818
+ "Tab Order": pos.get("tabOrder"),
819
+ "Hidden": visual_json.get("isHidden", False),
820
+ "Type": visual_type,
821
+ "Display Type": visual_type_display,
822
+ "Title": title,
823
+ "SubTitle": sub_title,
824
+ "Custom Visual": visual_type in custom_visuals,
825
+ "Alt Text": alt_text,
826
+ "Show Items With No Data": show_all_data,
827
+ "Divider": divider,
828
+ "Row SubTotals": rst_value,
829
+ "Column SubTotals": cst_value,
830
+ "Slicer Type": slicer_type,
831
+ "Data Visual": is_data_visual,
832
+ "Has Sparkline": has_sparkline,
833
+ "Visual Filter Count": visual_filter_count,
834
+ "Data Limit": data_limit,
835
+ }
836
+
837
+ df = pd.concat(
838
+ [df, pd.DataFrame(new_data, index=[0])], ignore_index=True
839
+ )
840
+
841
+ bool_cols = [
842
+ "Hidden",
843
+ "Show Items With No Data",
844
+ "Custom Visual",
845
+ "Data Visual",
846
+ "Has Sparkline",
847
+ "Row SubTotals",
848
+ "Column SubTotals",
849
+ ]
850
+
851
+ grouped_df = (
852
+ self.list_visual_objects()
853
+ .groupby(["Page Name", "Visual Name"])
854
+ .size()
855
+ .reset_index(name="Visual Object Count")
856
+ )
857
+
858
+ df = pd.merge(
859
+ df,
860
+ grouped_df,
861
+ left_on=["Page Name", "Visual Name"],
862
+ right_on=["Page Name", "Visual Name"],
863
+ how="left",
864
+ )
865
+ df["Visual Object Count"] = df["Visual Object Count"].fillna(0).astype(int)
866
+
867
+ float_cols = ["X", "Y", "Width", "Height"]
868
+ int_cols = ["Z", "Visual Filter Count", "Data Limit", "Visual Object Count"]
869
+ df[bool_cols] = df[bool_cols].astype(bool)
870
+ df[int_cols] = df[int_cols].astype(int)
871
+ df[float_cols] = df[float_cols].astype(float)
872
+
873
+ return df
874
+
875
+ def list_visual_objects(self, extended: bool = False) -> pd.DataFrame:
876
+ """
877
+ Shows a list of all semantic model objects used in each visual in the report.
878
+
879
+ Parameters
880
+ ----------
881
+ extended : bool, default=False
882
+ If True, adds an extra column called 'Valid Semantic Model Object' which identifies whether the semantic model object used
883
+ in the report exists in the semantic model which feeds data to the report.
884
+
885
+ Returns
886
+ -------
887
+ pandas.DataFrame
888
+ A pandas dataframe containing a list of all semantic model objects used in each visual in the report.
889
+ """
890
+
891
+ rd = self.rdef
892
+ page_mapping, visual_mapping = helper.visual_page_mapping(self)
893
+ df = pd.DataFrame(
894
+ columns=[
895
+ "Page Name",
896
+ "Page Display Name",
897
+ "Visual Name",
898
+ "Table Name",
899
+ "Object Name",
900
+ "Object Type",
901
+ "Implicit Measure",
902
+ "Sparkline",
903
+ "Visual Calc",
904
+ "Format",
905
+ ]
906
+ )
907
+
908
+ def contains_key(data, keys_to_check):
909
+ if isinstance(data, dict):
910
+ for key, value in data.items():
911
+ if key in keys_to_check:
912
+ return True
913
+ if contains_key(value, keys_to_check):
914
+ return True
915
+ elif isinstance(data, list):
916
+ for item in data:
917
+ if contains_key(item, keys_to_check):
918
+ return True
919
+ return False
920
+
921
+ def find_entity_property_pairs(data, result=None, keys_path=None):
922
+ if result is None:
923
+ result = {}
924
+ if keys_path is None:
925
+ keys_path = []
926
+
927
+ if isinstance(data, dict):
928
+ if (
929
+ "Entity" in data.get("Expression", {}).get("SourceRef", {})
930
+ and "Property" in data
931
+ ):
932
+ entity = (
933
+ data.get("Expression", {})
934
+ .get("SourceRef", {})
935
+ .get("Entity", {})
936
+ )
937
+ property_value = data.get("Property", {})
938
+ object_type = keys_path[-1].replace("HierarchyLevel", "Hierarchy")
939
+ is_agg = keys_path[-3] == "Aggregation"
940
+ is_viz_calc = keys_path[-3] == "NativeVisualCalculation"
941
+ is_sparkline = keys_path[-3] == "SparklineData"
942
+ result[property_value] = (
943
+ entity,
944
+ object_type,
945
+ is_agg,
946
+ is_viz_calc,
947
+ is_sparkline,
948
+ )
949
+ keys_path.pop()
950
+
951
+ # Recursively search the rest of the dictionary
952
+ for key, value in data.items():
953
+ keys_path.append(key)
954
+ find_entity_property_pairs(value, result, keys_path)
955
+
956
+ elif isinstance(data, list):
957
+ for item in data:
958
+ find_entity_property_pairs(item, result, keys_path)
959
+
960
+ return result
961
+
962
+ for _, r in rd.iterrows():
963
+ file_path = r["path"]
964
+ payload = r["payload"]
965
+ if file_path.endswith("/visual.json"):
966
+ visual_file = base64.b64decode(payload).decode("utf-8")
967
+ visual_json = json.loads(visual_file)
968
+ page_id = visual_mapping.get(file_path)[0]
969
+ page_display = visual_mapping.get(file_path)[1]
970
+
971
+ entity_property_pairs = find_entity_property_pairs(visual_json)
972
+ query_state = (
973
+ visual_json.get("visual", {})
974
+ .get("query", {})
975
+ .get("queryState", {})
976
+ .get("Values", {})
977
+ )
978
+ format_mapping = {}
979
+ for p in query_state.get("projections", []):
980
+ query_ref = p.get("queryRef")
981
+ fmt = p.get("format")
982
+ if fmt is not None:
983
+ format_mapping[query_ref] = fmt
984
+
985
+ for object_name, properties in entity_property_pairs.items():
986
+ table_name = properties[0]
987
+ obj_full = f"{table_name}.{object_name}"
988
+ is_agg = properties[2]
989
+ format_value = format_mapping.get(obj_full)
990
+
991
+ if is_agg:
992
+ for k, v in format_mapping.items():
993
+ if obj_full in k:
994
+ format_value = v
995
+ new_data = {
996
+ "Page Name": page_id,
997
+ "Page Display Name": page_display,
998
+ "Visual Name": visual_json.get("name"),
999
+ "Table Name": table_name,
1000
+ "Object Name": object_name,
1001
+ "Object Type": properties[1],
1002
+ "Implicit Measure": is_agg,
1003
+ "Sparkline": properties[4],
1004
+ "Visual Calc": properties[3],
1005
+ "Format": format_value,
1006
+ }
1007
+
1008
+ df = pd.concat(
1009
+ [df, pd.DataFrame(new_data, index=[0])], ignore_index=True
1010
+ )
1011
+
1012
+ if extended:
1013
+ df = self._add_extended(dataframe=df)
1014
+
1015
+ bool_cols = ["Implicit Measure", "Sparkline", "Visual Calc"]
1016
+ df[bool_cols] = df[bool_cols].astype(bool)
1017
+
1018
+ return df
1019
+
1020
+ def list_semantic_model_objects(self, extended: bool = False) -> pd.DataFrame:
1021
+ """
1022
+ Shows a list of all semantic model objects (measures, columns, hierarchies) that are used in the report and where the objects
1023
+ were used (i.e. visual, report filter, page filter, visual filter).
1024
+
1025
+ Parameters
1026
+ ----------
1027
+ extended : bool, default=False
1028
+ If True, adds an extra column called 'Valid Semantic Model Object' which identifies whether the semantic model object used
1029
+ in the report exists in the semantic model which feeds data to the report.
1030
+
1031
+ Returns
1032
+ -------
1033
+ pandas.DataFrame
1034
+ A pandas dataframe showing the semantic model objects used in the report.
1035
+ """
1036
+
1037
+ from sempy_labs.tom import connect_semantic_model
1038
+
1039
+ df = pd.DataFrame(
1040
+ columns=[
1041
+ "Table Name",
1042
+ "Object Name",
1043
+ "Object Type",
1044
+ "Report Source",
1045
+ "Report Source Object",
1046
+ ]
1047
+ )
1048
+
1049
+ rf = self.list_report_filters()
1050
+ pf = self.list_page_filters()
1051
+ vf = self.list_visual_filters()
1052
+ vo = self.list_visual_objects()
1053
+
1054
+ rf_subset = rf[["Table Name", "Object Name", "Object Type"]].copy()
1055
+ rf_subset["Report Source"] = "Report Filter"
1056
+ rf_subset["Report Source Object"] = self._report
1057
+
1058
+ pf_subset = pf[
1059
+ ["Table Name", "Object Name", "Object Type", "Page Display Name"]
1060
+ ].copy()
1061
+ pf_subset["Report Source"] = "Page Filter"
1062
+ pf_subset["Report Source Object"] = pf_subset["Page Display Name"]
1063
+ pf_subset.drop(columns=["Page Display Name"], inplace=True)
1064
+
1065
+ vf_subset = vf[
1066
+ [
1067
+ "Table Name",
1068
+ "Object Name",
1069
+ "Object Type",
1070
+ "Page Display Name",
1071
+ "Visual Name",
1072
+ ]
1073
+ ].copy()
1074
+ vf_subset["Report Source"] = "Visual Filter"
1075
+ vf_subset["Report Source Object"] = format_dax_object_name(
1076
+ vf_subset["Page Display Name"], vf_subset["Visual Name"]
1077
+ )
1078
+ vf_subset.drop(columns=["Page Display Name", "Visual Name"], inplace=True)
1079
+
1080
+ vo_subset = vo[
1081
+ [
1082
+ "Table Name",
1083
+ "Object Name",
1084
+ "Object Type",
1085
+ "Page Display Name",
1086
+ "Visual Name",
1087
+ ]
1088
+ ].copy()
1089
+ vo_subset["Report Source"] = "Visual"
1090
+ vo_subset["Report Source Object"] = format_dax_object_name(
1091
+ vo_subset["Page Display Name"], vo_subset["Visual Name"]
1092
+ )
1093
+ vo_subset.drop(columns=["Page Display Name", "Visual Name"], inplace=True)
1094
+
1095
+ df = pd.concat(
1096
+ [df, rf_subset, pf_subset, vf_subset, vo_subset], ignore_index=True
1097
+ )
1098
+
1099
+ if extended:
1100
+ dataset_id, dataset_name, dataset_workspace_id, dataset_workspace = (
1101
+ resolve_dataset_from_report(
1102
+ report=self._report, workspace=self._workspace
1103
+ )
1104
+ )
1105
+
1106
+ def check_validity(tom, row):
1107
+ object_validators = {
1108
+ "Measure": lambda: any(
1109
+ o.Name == row["Object Name"] for o in tom.all_measures()
1110
+ ),
1111
+ "Column": lambda: any(
1112
+ format_dax_object_name(c.Parent.Name, c.Name)
1113
+ == format_dax_object_name(row["Table Name"], row["Object Name"])
1114
+ for c in tom.all_columns()
1115
+ ),
1116
+ "Hierarchy": lambda: any(
1117
+ format_dax_object_name(h.Parent.Name, h.Name)
1118
+ == format_dax_object_name(row["Table Name"], row["Object Name"])
1119
+ for h in tom.all_hierarchies()
1120
+ ),
1121
+ }
1122
+ return object_validators.get(row["Object Type"], lambda: False)()
1123
+
1124
+ with connect_semantic_model(
1125
+ dataset=dataset_name, readonly=True, workspace=dataset_workspace
1126
+ ) as tom:
1127
+ df["Valid Semantic Model Object"] = df.apply(
1128
+ lambda row: check_validity(tom, row), axis=1
1129
+ )
1130
+
1131
+ return df
1132
+
1133
+ def _list_all_semantic_model_objects(self):
1134
+
1135
+ # Includes dependencies
1136
+
1137
+ df = (
1138
+ self.list_semantic_model_objects()[
1139
+ ["Table Name", "Object Name", "Object Type"]
1140
+ ]
1141
+ .drop_duplicates()
1142
+ .reset_index(drop=True)
1143
+ )
1144
+ dataset_id, dataset_name, dataset_workspace_id, dataset_workspace = (
1145
+ resolve_dataset_from_report(report=self._report, workspace=self._workspace)
1146
+ )
1147
+ dep = get_measure_dependencies(
1148
+ dataset=dataset_name, workspace=dataset_workspace
1149
+ )
1150
+ rpt_measures = df[df["Object Type"] == "Measure"]["Object Name"].values
1151
+ new_rows = dep[dep["Object Name"].isin(rpt_measures)][
1152
+ ["Referenced Table", "Referenced Object", "Referenced Object Type"]
1153
+ ]
1154
+ new_rows.columns = ["Table Name", "Object Name", "Object Type"]
1155
+ result_df = (
1156
+ pd.concat([df, new_rows], ignore_index=True)
1157
+ .drop_duplicates()
1158
+ .reset_index(drop=True)
1159
+ )
1160
+
1161
+ result_df["Dataset Name"] = dataset_name
1162
+ result_df["Dataset Workspace Name"] = dataset_workspace
1163
+ colName = "Dataset Name"
1164
+ result_df.insert(0, colName, result_df.pop(colName))
1165
+ colName = "Dataset Workspace Name"
1166
+ result_df.insert(1, colName, result_df.pop(colName))
1167
+
1168
+ return result_df
1169
+
1170
+ def list_bookmarks(self) -> pd.DataFrame:
1171
+ """
1172
+ Shows a list of all bookmarks in the report.
1173
+
1174
+ Returns
1175
+ -------
1176
+ pandas.DataFrame
1177
+ A pandas dataframe containing a list of all bookmarks in the report.
1178
+ """
1179
+
1180
+ rd = self.rdef
1181
+ df = pd.DataFrame(
1182
+ columns=[
1183
+ "File Path",
1184
+ "Bookmark Name",
1185
+ "Bookmark Display Name",
1186
+ "Page Name",
1187
+ "Page Display Name",
1188
+ "Visual Name",
1189
+ "Visual Hidden",
1190
+ ]
1191
+ )
1192
+
1193
+ bookmark_rows = rd[rd["path"].str.endswith(".bookmark.json")]
1194
+
1195
+ for _, r in bookmark_rows.iterrows():
1196
+ path = r["path"]
1197
+ payload = r["payload"]
1198
+
1199
+ obj_file = base64.b64decode(payload).decode("utf-8")
1200
+ obj_json = json.loads(obj_file)
1201
+
1202
+ bookmark_name = obj_json.get("name")
1203
+ bookmark_display = obj_json.get("displayName")
1204
+ rpt_page_id = obj_json.get("explorationState", {}).get("activeSection")
1205
+ page_id, page_display, file_path = helper.resolve_page_name(
1206
+ self, page_name=rpt_page_id
1207
+ )
1208
+
1209
+ for rptPg in obj_json.get("explorationState", {}).get("sections", {}):
1210
+ for visual_name in (
1211
+ obj_json.get("explorationState", {})
1212
+ .get("sections", {})
1213
+ .get(rptPg, {})
1214
+ .get("visualContainers", {})
1215
+ ):
1216
+ if (
1217
+ obj_json.get("explorationState", {})
1218
+ .get("sections", {})
1219
+ .get(rptPg, {})
1220
+ .get("visualContainers", {})
1221
+ .get(visual_name, {})
1222
+ .get("singleVisual", {})
1223
+ .get("display", {})
1224
+ .get("mode", {})
1225
+ == "hidden"
1226
+ ):
1227
+ visual_hidden = True
1228
+ else:
1229
+ visual_hidden = False
1230
+
1231
+ new_data = {
1232
+ "File Path": path,
1233
+ "Bookmark Name": bookmark_name,
1234
+ "Bookmark Display Name": bookmark_display,
1235
+ "Page Name": page_id,
1236
+ "Page Display Name": page_display,
1237
+ "Visual Name": visual_name,
1238
+ "Visual Hidden": visual_hidden,
1239
+ }
1240
+ df = pd.concat(
1241
+ [df, pd.DataFrame(new_data, index=[0])], ignore_index=True
1242
+ )
1243
+
1244
+ bool_cols = ["Visual Hidden"]
1245
+ df[bool_cols] = df[bool_cols].astype(bool)
1246
+
1247
+ return df
1248
+
1249
+ def list_report_level_measures(self) -> pd.DataFrame:
1250
+ """
1251
+ Shows a list of all `report-level measures <https://learn.microsoft.com/power-bi/transform-model/desktop-measures#report-level-measures>`_ in the report.
1252
+
1253
+ Parameters
1254
+ ----------
1255
+
1256
+ Returns
1257
+ -------
1258
+ pandas.DataFrame
1259
+ A pandas dataframe containing a list of all report-level measures in the report.
1260
+ """
1261
+
1262
+ df = pd.DataFrame(
1263
+ columns=[
1264
+ "Measure Name",
1265
+ "Table Name",
1266
+ "Expression",
1267
+ "Data Type",
1268
+ "Format String",
1269
+ ]
1270
+ )
1271
+ rd = self.rdef
1272
+ rd_filt = rd[rd["path"] == "definition/reportExtensions.json"]
1273
+
1274
+ if len(rd_filt) == 1:
1275
+ payload = rd_filt["payload"].iloc[0]
1276
+ obj_file = base64.b64decode(payload).decode("utf-8")
1277
+ obj_json = json.loads(obj_file)
1278
+
1279
+ for e in obj_json.get("entities", []):
1280
+ table_name = e.get("name")
1281
+ for m in e.get("measures", []):
1282
+ measure_name = m.get("name")
1283
+ expr = m.get("expression")
1284
+ data_type = m.get("dataType")
1285
+ format_string = m.get("formatString")
1286
+
1287
+ new_data = {
1288
+ "Measure Name": measure_name,
1289
+ "Table Name": table_name,
1290
+ "Expression": expr,
1291
+ "Data Type": data_type,
1292
+ "Format String": format_string,
1293
+ }
1294
+ df = pd.concat(
1295
+ [df, pd.DataFrame(new_data, index=[0])], ignore_index=True
1296
+ )
1297
+
1298
+ return df
1299
+
1300
+ # Automation functions
1301
+ def set_theme(self, theme_file_path: str):
1302
+ """
1303
+ Sets a custom theme for a report based on a theme .json file.
1304
+
1305
+ Parameters
1306
+ ----------
1307
+ theme_file_path : str
1308
+ The file path of the theme.json file. This can either be from a Fabric lakehouse or from the web.
1309
+ Examples:
1310
+ file_path = '/lakehouse/default/Files/CY23SU09.json'
1311
+ file_path = 'https://raw.githubusercontent.com/PowerBiDevCamp/FabricUserApiDemo/main/FabricUserApiDemo/DefinitionTemplates/Shared/Reports/StaticResources/SharedResources/BaseThemes/CY23SU08.json'
1312
+ """
1313
+
1314
+ import requests
1315
+
1316
+ report_path = "definition/report.json"
1317
+ theme_version = "5.5.4"
1318
+ request_body = {"definition": {"parts": []}}
1319
+
1320
+ if not theme_file_path.endswith(".json"):
1321
+ raise ValueError(
1322
+ f"{icons.red_dot} The '{theme_file_path}' theme file path must be a .json file."
1323
+ )
1324
+ elif theme_file_path.startswith("https://"):
1325
+ response = requests.get(theme_file_path)
1326
+ json_file = response.json()
1327
+ elif theme_file_path.startswith("/lakehouse"):
1328
+ with open(theme_file_path, "r", encoding="utf-8-sig") as file:
1329
+ json_file = json.load(file)
1330
+ else:
1331
+ ValueError(
1332
+ f"{icons.red_dot} Incorrect theme file path value '{theme_file_path}'."
1333
+ )
1334
+
1335
+ theme_name = json_file["name"]
1336
+ theme_name_full = f"{theme_name}.json"
1337
+
1338
+ # Add theme.json file to request_body
1339
+ file_payload = _conv_b64(json_file)
1340
+ filePath = f"StaticResources/RegisteredResources/{theme_name_full}"
1341
+
1342
+ _add_part(request_body, filePath, file_payload)
1343
+
1344
+ new_theme = {
1345
+ "name": theme_name_full,
1346
+ "path": theme_name_full,
1347
+ "type": "CustomTheme",
1348
+ }
1349
+
1350
+ rd = self.rdef
1351
+ for _, r in rd.iterrows():
1352
+ path = r["path"]
1353
+ payload = r["payload"]
1354
+ if path != report_path:
1355
+ _add_part(request_body, path, payload)
1356
+ # Update the report.json file
1357
+ else:
1358
+ rptFile = base64.b64decode(payload).decode("utf-8")
1359
+ rptJson = json.loads(rptFile)
1360
+ resource_type = "RegisteredResources"
1361
+
1362
+ # Add to theme collection
1363
+ if "customTheme" not in rptJson["themeCollection"]:
1364
+ rptJson["themeCollection"]["customTheme"] = {
1365
+ "name": theme_name_full,
1366
+ "reportVersionAtImport": theme_version,
1367
+ "type": resource_type,
1368
+ }
1369
+ else:
1370
+ rptJson["themeCollection"]["customTheme"]["name"] = theme_name_full
1371
+ rptJson["themeCollection"]["customTheme"]["type"] = resource_type
1372
+
1373
+ for package in rptJson["resourcePackages"]:
1374
+ package["items"] = [
1375
+ item
1376
+ for item in package["items"]
1377
+ if item["type"] != "CustomTheme"
1378
+ ]
1379
+
1380
+ if not any(
1381
+ package["name"] == resource_type
1382
+ for package in rptJson["resourcePackages"]
1383
+ ):
1384
+ new_registered_resources = {
1385
+ "name": resource_type,
1386
+ "type": resource_type,
1387
+ "items": [new_theme],
1388
+ }
1389
+ rptJson["resourcePackages"].append(new_registered_resources)
1390
+ else:
1391
+ names = [
1392
+ rp["name"] for rp in rptJson["resourcePackages"][1]["items"]
1393
+ ]
1394
+
1395
+ if theme_name_full not in names:
1396
+ rptJson["resourcePackages"][1]["items"].append(new_theme)
1397
+
1398
+ file_payload = _conv_b64(rptJson)
1399
+ _add_part(request_body, path, file_payload)
1400
+
1401
+ self.update_report(request_body=request_body)
1402
+ print(
1403
+ f"{icons.green_dot} The '{theme_name}' theme has been set as the theme for the '{self._report}' report within the '{self._workspace}' workspace."
1404
+ )
1405
+
1406
+ def set_active_page(self, page_name: str):
1407
+ """
1408
+ Sets the active page (first page displayed when opening a report) for a report.
1409
+
1410
+ Parameters
1411
+ ----------
1412
+ page_name : str
1413
+ The page name or page display name of the report.
1414
+ """
1415
+
1416
+ pages_file = "definition/pages/pages.json"
1417
+ request_body = {"definition": {"parts": []}}
1418
+
1419
+ rd = self.rdef
1420
+ page_id, page_display, file_path = helper.resolve_page_name(
1421
+ self, page_name=page_name
1422
+ )
1423
+ for _, r in rd.iterrows():
1424
+ path = r["path"]
1425
+ file_payload = r["payload"]
1426
+ if path != pages_file:
1427
+ _add_part(request_body, path, file_payload)
1428
+
1429
+ pagePath = rd[rd["path"] == pages_file]
1430
+ payload = pagePath["payload"].iloc[0]
1431
+ pageFile = base64.b64decode(payload).decode("utf-8")
1432
+ pageJson = json.loads(pageFile)
1433
+ pageJson["activePageName"] = page_id
1434
+ file_payload = _conv_b64(pageJson)
1435
+
1436
+ _add_part(request_body, pages_file, file_payload)
1437
+
1438
+ self.update_report(request_body=request_body)
1439
+ print(
1440
+ f"{icons.green_dot} The '{page_name}' page has been set as the active page in the '{self._report}' report within the '{self._workspace}' workspace."
1441
+ )
1442
+
1443
+ def set_page_type(self, page_name: str, page_type: str):
1444
+
1445
+ if page_type not in helper.page_types:
1446
+ raise ValueError(
1447
+ f"{icons.red_dot} Invalid page type. Valid options: {helper.page_types}."
1448
+ )
1449
+
1450
+ request_body = {"definition": {"parts": []}}
1451
+
1452
+ letter_key = next(
1453
+ (
1454
+ key
1455
+ for key, value in helper.page_type_mapping.items()
1456
+ if value == page_type
1457
+ ),
1458
+ None,
1459
+ )
1460
+ if letter_key:
1461
+ width, height = letter_key
1462
+ else:
1463
+ raise ValueError(
1464
+ "Invalid page_type parameter. Valid options: ['Tooltip', 'Letter', '4:3', '16:9']."
1465
+ )
1466
+
1467
+ rd = self.rdef
1468
+ page_id, page_display, file_path = helper.resolve_page_name(
1469
+ self, page_name=page_name
1470
+ )
1471
+ rd_filt = rd[rd["path"] == file_path]
1472
+ payload = rd_filt["payload"].iloc[0]
1473
+ pageFile = base64.b64decode(payload).decode("utf-8")
1474
+ pageJson = json.loads(pageFile)
1475
+ pageJson["width"] = width
1476
+ pageJson["height"] = height
1477
+
1478
+ file_payload = _conv_b64(pageJson)
1479
+ _add_part(request_body, file_path, file_payload)
1480
+
1481
+ for _, r in rd.iterrows():
1482
+ if r["path"] != file_path:
1483
+ _add_part(request_body, r["path"], r["payload"])
1484
+
1485
+ self.update_report(request_body=request_body)
1486
+ print(
1487
+ f"The '{page_display}' page has been updated to the '{page_type}' page type."
1488
+ )
1489
+
1490
+ def remove_unnecessary_custom_visuals(self):
1491
+ """
1492
+ Removes any custom visuals within the report that are not used in the report.
1493
+ """
1494
+
1495
+ dfCV = self.list_custom_visuals()
1496
+ dfV = self.list_visuals()
1497
+ rd = self.rdef
1498
+ cv_remove = []
1499
+ cv_remove_display = []
1500
+ request_body = {"definition": {"parts": []}}
1501
+
1502
+ for _, r in dfCV.iterrows():
1503
+ cv = r["Custom Visual Name"]
1504
+ cv_display = r["Custom Visual Display Name"]
1505
+ dfV_filt = dfV[dfV["Type"] == cv]
1506
+ if len(dfV_filt) == 0:
1507
+ cv_remove.append(cv) # Add to the list for removal
1508
+ cv_remove_display.append(cv_display)
1509
+ if len(cv_remove) == 0:
1510
+ print(
1511
+ f"{icons.green_dot} There are no unnecessary custom visuals in the '{self._report}' report within the '{self._workspace}' workspace."
1512
+ )
1513
+ return
1514
+
1515
+ for _, r in rd.iterrows():
1516
+ file_path = r["path"]
1517
+ payload = r["payload"]
1518
+ if file_path == "definition/report.json":
1519
+ rpt_file = base64.b64decode(payload).decode("utf-8")
1520
+ rpt_json = json.loads(rpt_file)
1521
+ rpt_json["publicCustomVisuals"] = [
1522
+ item
1523
+ for item in rpt_json["publicCustomVisuals"]
1524
+ if item not in cv_remove
1525
+ ]
1526
+
1527
+ payload = _conv_b64(rpt_json)
1528
+
1529
+ _add_part(request_body, file_path, payload)
1530
+
1531
+ self.update_report(request_body=request_body)
1532
+ print(
1533
+ f"{icons.green_dot} The {cv_remove_display} custom visuals have been removed from the '{self._report}' report within the '{self._workspace}' workspace."
1534
+ )
1535
+
1536
+ def migrate_report_level_measures(self, measures: Optional[str | List[str]] = None):
1537
+ """
1538
+ Moves all report-level measures from the report to the semantic model on which the report is based.
1539
+
1540
+ Parameters
1541
+ ----------
1542
+ measures : str | List[str], default=None
1543
+ A measure or list of measures to move to the semantic model.
1544
+ Defaults to None which resolves to moving all report-level measures to the semantic model.
1545
+ """
1546
+
1547
+ from sempy_labs.tom import connect_semantic_model
1548
+
1549
+ rlm = self.list_report_level_measures()
1550
+ if len(rlm) == 0:
1551
+ print(
1552
+ f"{icons.green_dot} The '{self._report}' report within the '{self._workspace}' workspace has no report-level measures."
1553
+ )
1554
+ return
1555
+
1556
+ dataset_id, dataset_name, dataset_workspace_id, dataset_workspace = (
1557
+ resolve_dataset_from_report(report=self._report, workspace=self._workspace)
1558
+ )
1559
+
1560
+ if isinstance(measures, str):
1561
+ measures = [measures]
1562
+
1563
+ request_body = {"definition": {"parts": []}}
1564
+ rpt_file = "definition/reportExtensions.json"
1565
+
1566
+ rd = self.rdef
1567
+ rd_filt = rd[rd["path"] == rpt_file]
1568
+ payload = rd_filt["payload"].iloc[0]
1569
+ extFile = base64.b64decode(payload).decode("utf-8")
1570
+ extJson = json.loads(extFile)
1571
+
1572
+ mCount = 0
1573
+ with connect_semantic_model(
1574
+ dataset=dataset_name, readonly=False, workspace=dataset_workspace
1575
+ ) as tom:
1576
+ for _, r in rlm.iterrows():
1577
+ tableName = r["Table Name"]
1578
+ mName = r["Measure Name"]
1579
+ mExpr = r["Expression"]
1580
+ # mDataType = r["Data Type"]
1581
+ mformatString = r["Format String"]
1582
+ # Add measures to the model
1583
+ if mName in measures or measures is None:
1584
+ tom.add_measure(
1585
+ table_name=tableName,
1586
+ measure_name=mName,
1587
+ expression=mExpr,
1588
+ format_string=mformatString,
1589
+ )
1590
+ tom.set_annotation(
1591
+ object=tom.model.Tables[tableName].Measures[mName],
1592
+ name="semanticlinklabs",
1593
+ value="reportlevelmeasure",
1594
+ )
1595
+ mCount += 1
1596
+ # Remove measures from the json
1597
+ if measures is not None and len(measures) < mCount:
1598
+ for e in extJson["entities"]:
1599
+ e["measures"] = [
1600
+ measure
1601
+ for measure in e["measures"]
1602
+ if measure["name"] not in measures
1603
+ ]
1604
+ extJson["entities"] = [
1605
+ entity for entity in extJson["entities"] if entity["measures"]
1606
+ ]
1607
+ file_payload = _conv_b64(extJson)
1608
+ _add_part(request_body, rpt_file, file_payload)
1609
+
1610
+ # Add unchanged payloads
1611
+ for _, r in rd.iterrows():
1612
+ path = r["path"]
1613
+ payload = r["payload"]
1614
+ if path != rpt_file:
1615
+ _add_part(request_body, path, payload)
1616
+
1617
+ self.update_report(request_body=request_body)
1618
+ print(
1619
+ f"{icons.green_dot} The report-level measures have been migrated to the '{dataset_name}' semantic model within the '{dataset_workspace}' workspace."
1620
+ )
1621
+
1622
+ def set_page_visibility(self, page_name: str, hidden: bool):
1623
+ """
1624
+ Sets whether a report page is visible or hidden.
1625
+
1626
+ Parameters
1627
+ ----------
1628
+ page_name : str
1629
+ The page name or page display name of the report.
1630
+ hidden : bool
1631
+ If set to True, hides the report page.
1632
+ If set to False, makes the report page visible.
1633
+ """
1634
+
1635
+ rd = self.rdef
1636
+ page_id, page_display, file_path = helper.resolve_page_name(
1637
+ self, page_name=page_name
1638
+ )
1639
+ visibility = "visible" if hidden is False else "hidden"
1640
+
1641
+ request_body = {"definition": {"parts": []}}
1642
+
1643
+ for _, r in rd.iterrows():
1644
+ path = r["path"]
1645
+ payload = r["payload"]
1646
+ if path == file_path:
1647
+ obj_file = base64.b64decode(payload).decode("utf-8")
1648
+ obj_json = json.loads(obj_file)
1649
+ if hidden:
1650
+ obj_json["visibility"] = "HiddenInViewMode"
1651
+ else:
1652
+ if "visibility" in obj_json:
1653
+ del obj_json["visibility"]
1654
+ _add_part(request_body, path, _conv_b64(obj_json))
1655
+ else:
1656
+ _add_part(request_body, path, payload)
1657
+
1658
+ self.update_report(request_body=request_body)
1659
+ print(f"{icons.green_dot} The '{page_name}' page has been set to {visibility}.")
1660
+
1661
+ def hide_tooltip_drillthrough_pages(self):
1662
+ """
1663
+ Hides all tooltip pages and drillthrough pages in a report.
1664
+ """
1665
+
1666
+ dfP = self.list_pages()
1667
+ dfP_filt = dfP[
1668
+ (dfP["Type"] == "Tooltip") | (dfP["Drillthrough Target Page"] == True)
1669
+ ]
1670
+
1671
+ if len(dfP_filt) == 0:
1672
+ print(
1673
+ f"{icons.green_dot} There are no Tooltip or Drillthrough pages in the '{self._report}' report within the '{self._workspace}' workspace."
1674
+ )
1675
+ return
1676
+
1677
+ for _, r in dfP_filt.iterrows():
1678
+ page_name = r["Page Name"]
1679
+ self.set_page_visibility(page_name=page_name, hidden=True)
1680
+
1681
+ def disable_show_items_with_no_data(self):
1682
+ """
1683
+ Disables the `show items with no data <https://learn.microsoft.com/power-bi/create-reports/desktop-show-items-no-data>`_ property in all visuals within the report.
1684
+ """
1685
+
1686
+ request_body = {"definition": {"parts": []}}
1687
+
1688
+ def delete_key_in_json(obj, key_to_delete):
1689
+ if isinstance(obj, dict):
1690
+ if key_to_delete in obj:
1691
+ del obj[key_to_delete]
1692
+ for key, value in obj.items():
1693
+ delete_key_in_json(value, key_to_delete)
1694
+ elif isinstance(obj, list):
1695
+ for item in obj:
1696
+ delete_key_in_json(item, key_to_delete)
1697
+
1698
+ rd = self.rdef
1699
+ for _, r in rd.iterrows():
1700
+ file_path = r["path"]
1701
+ payload = r["payload"]
1702
+ if file_path.endswith("/visual.json"):
1703
+ objFile = base64.b64decode(payload).decode("utf-8")
1704
+ objJson = json.loads(objFile)
1705
+ delete_key_in_json(objJson, "showAll")
1706
+ _add_part(request_body, file_path, _conv_b64(objJson))
1707
+ else:
1708
+ _add_part(request_body, file_path, payload)
1709
+
1710
+ self.update_report(request_body=request_body)
1711
+ print(
1712
+ f"{icons.green_dot} Show items with data has been disabled for all visuals in the '{self._report}' report within the '{self._workspace}' workspace."
1713
+ )
1714
+
1715
+ def __get_annotation_value(self, object_name: str, object_type: str, name: str):
1716
+
1717
+ object_types = ["Visual", "Page", "Report"]
1718
+ object_type = object_type.capitalize()
1719
+ if object_type not in object_types:
1720
+ raise ValueError(
1721
+ f"{icons.red_dot} Invalid object type. Valid options: {object_types}."
1722
+ )
1723
+
1724
+ rd = self.rdef
1725
+
1726
+ if object_type == "Report":
1727
+ rd_filt = rd[rd["path"] == "definition/report.json"]
1728
+ obj_json = _extract_json(rd_filt)
1729
+ elif object_type == "Page":
1730
+ page_id, page_display, page_file = helper.resolve_page_name(
1731
+ self, page_name=object_name
1732
+ )
1733
+ rd_filt = rd[rd["path"] == page_file]
1734
+ payload = rd_filt["payload"].iloc[0]
1735
+ obj_file = base64.b64decode(payload).decode("utf-8")
1736
+ obj_json = json.loads(obj_file)
1737
+ elif object_type == "Visual":
1738
+ pattern = r"'([^']+)'\[([^]]+)\]"
1739
+ match = re.search(pattern, object_name)
1740
+ if match:
1741
+ p_name = match.group(1)
1742
+ v_name = match.group(2)
1743
+ else:
1744
+ raise ValueError(
1745
+ "Invalid page/visual name within the 'object_name' parameter. Valid format: 'Page 1'[f8dvo24PdJ39fp6]"
1746
+ )
1747
+ valid_page_name, valid_display_name, visual_name, file_path = (
1748
+ helper.resolve_visual_name(self, page_name=p_name, visual_name=v_name)
1749
+ )
1750
+ rd_filt = rd[rd["path"] == file_path]
1751
+ payload = rd_filt["payload"].iloc[0]
1752
+ obj_file = base64.b64decode(payload).decode("utf-8")
1753
+ obj_json = json.loads(obj_file)
1754
+
1755
+ value = obj_json.get("annotations", {}).get(name, "")
1756
+
1757
+ return value
1758
+
1759
+ def __remove_annotation(self, object_name: str, object_type: str, name: str):
1760
+
1761
+ object_types = ["Visual", "Page", "Report"]
1762
+ object_type = object_type.capitalize()
1763
+ if object_type not in object_types:
1764
+ raise ValueError(
1765
+ f"{icons.red_dot} Invalid object type. Valid options: {object_types}."
1766
+ )
1767
+
1768
+ def __set_annotation(
1769
+ self, object_name: str, object_type: str, name: str, value: str
1770
+ ):
1771
+
1772
+ object_types = ["Visual", "Page", "Report"]
1773
+ object_type = object_type.capitalize()
1774
+ if object_type not in object_types:
1775
+ raise ValueError(
1776
+ f"{icons.red_dot} Invalid object type. Valid options: {object_types}."
1777
+ )
1778
+
1779
+ request_body = {"definition": {"parts": []}}
1780
+ new_annotation = {"name": name, "value": value}
1781
+
1782
+ # Creates the annotation if it does not exist. Updates the annotation value if the annotation already exists
1783
+ def update_annotation(payload):
1784
+ objFile = base64.b64decode(payload).decode("utf-8")
1785
+ objJson = json.loads(objFile)
1786
+ if "annotations" not in objJson:
1787
+ objJson["annotations"] = [new_annotation]
1788
+ else:
1789
+ names = []
1790
+ for ann in objJson["annotations"]:
1791
+ names.append(ann["name"])
1792
+ if name not in names:
1793
+ objJson["annotations"].append(new_annotation)
1794
+ else:
1795
+ for ann in objJson["annotations"]:
1796
+ if ann["name"] == name:
1797
+ ann["value"] = value
1798
+ return objJson
1799
+
1800
+ # Validate page and visual names
1801
+ if object_type == "Page":
1802
+ page_id, page_display, file_path = helper.resolve_page_name(
1803
+ self, page_name=object_name
1804
+ )
1805
+ elif object_type == "Visual":
1806
+ pattern = r"'(.*?)'|\[(.*?)\]"
1807
+ matches = re.findall(pattern, object_name)
1808
+ page_name = matches[0][0]
1809
+ visual_id = matches[1][1]
1810
+ page_id, page_display, file_path = helper.resolve_page_name(
1811
+ self, page_name=page_name
1812
+ )
1813
+
1814
+ rd = self.rdef
1815
+ for _, r in rd.iterrows():
1816
+ path = r["path"]
1817
+ payload = r["payload"]
1818
+ if object_type == "Report" and path == "definition/report.json":
1819
+ a = update_annotation(payload=payload)
1820
+ _add_part(request_body, path, _conv_b64(a))
1821
+ elif (
1822
+ object_type == "Page"
1823
+ and path == f"definition/pages/{page_id}/page.json"
1824
+ ):
1825
+ a = update_annotation(payload=payload)
1826
+ _add_part(request_body, path, _conv_b64(a))
1827
+ elif (
1828
+ object_type == "Visual"
1829
+ and path
1830
+ == f"definition/pages/{page_id}/visuals/{visual_id}/visual.json"
1831
+ ):
1832
+ a = update_annotation(payload=payload)
1833
+ _add_part(request_body, path, _conv_b64(a))
1834
+ else:
1835
+ _add_part(request_body, path, payload)
1836
+
1837
+ self.update_report(request_body=request_body)
1838
+ if object_type == "Report":
1839
+ print(
1840
+ f"{icons.green_dot} The '{name}' annotation has been set on the report with the '{value}' value."
1841
+ )
1842
+ elif object_type == "Page":
1843
+ print(
1844
+ f"{icons.green_dot} The '{name}' annotation has been set on the '{object_name}' page with the '{value}' value."
1845
+ )
1846
+ elif object_type == "Visual":
1847
+ print(
1848
+ f"{icons.green_dot} The '{name}' annotation has been set on the '{visual_id}' visual on the '{page_display}' page with the '{value}' value."
1849
+ )
1850
+
1851
+ def __adjust_settings(
1852
+ self, setting_type: str, setting_name: str, setting_value: bool
1853
+ ): # Meta function
1854
+
1855
+ valid_setting_types = ["settings", "slowDataSourceSettings"]
1856
+ valid_settings = [
1857
+ "isPersistentUserStateDisabled",
1858
+ "hideVisualContainerHeader",
1859
+ "defaultFilterActionIsDataFilter",
1860
+ "useStylableVisualContainerHeader",
1861
+ "useDefaultAggregateDisplayName",
1862
+ "useEnhancedTooltips",
1863
+ "allowChangeFilterTypes",
1864
+ "disableFilterPaneSearch",
1865
+ "useCrossReportDrillthrough",
1866
+ ]
1867
+ valid_slow_settings = [
1868
+ "isCrossHighlightingDisabled",
1869
+ "isSlicerSelectionsButtonEnabled",
1870
+ ]
1871
+
1872
+ if setting_type not in valid_setting_types:
1873
+ raise ValueError(
1874
+ f"Invalid setting_type. Valid options: {valid_setting_types}."
1875
+ )
1876
+ if setting_type == "settings" and setting_name not in valid_settings:
1877
+ raise ValueError(
1878
+ f"The '{setting_name}' is not a valid setting. Valid options: {valid_settings}."
1879
+ )
1880
+ if (
1881
+ setting_type == "slowDataSourceSettings"
1882
+ and setting_name not in valid_slow_settings
1883
+ ):
1884
+ raise ValueError(
1885
+ f"The '{setting_name}' is not a valid setting. Valid options: {valid_slow_settings}."
1886
+ )
1887
+
1888
+ request_body = {"definition": {"parts": []}}
1889
+
1890
+ rd = self.rdef
1891
+ for _, r in rd.iterrows():
1892
+ path = r["path"]
1893
+ payload = r["payload"]
1894
+ if path == "definition/report.json":
1895
+ obj_file = base64.b64decode(payload).decode("utf-8")
1896
+ obj_json = json.loads(obj_file)
1897
+ if setting_value is False:
1898
+ if setting_name in obj_json.get(setting_type, {}):
1899
+ del obj_json[setting_type][setting_name]
1900
+ else:
1901
+ if setting_name not in obj_json.get(setting_type, {}):
1902
+ obj_json[setting_type][setting_name] = True
1903
+
1904
+ _add_part(request_body, path, _conv_b64(obj_json))
1905
+ else:
1906
+ _add_part(request_body, path, payload)
1907
+
1908
+ upd = self.update_report(request_body=request_body)
1909
+ if upd == 200:
1910
+ print(f"{icons.green_dot}")
1911
+ else:
1912
+ print(f"{icons.red_dot}")
1913
+
1914
+ def __persist_filters(self, value: Optional[bool] = False):
1915
+ """
1916
+ Don't allow end user to save filters on this file in the Power BI service.
1917
+ """
1918
+
1919
+ self.adjust_settings(
1920
+ setting_type="settings",
1921
+ setting_name="isPersistentUserStateDisabled",
1922
+ setting_value=value,
1923
+ )
1924
+
1925
+ def __hide_visual_header(self, value: Optional[bool] = False):
1926
+ """
1927
+ Hide the visual header in reading view.
1928
+ """
1929
+
1930
+ self.adjust_settings(
1931
+ setting_type="settings",
1932
+ setting_name="hideVisualContainerHeader",
1933
+ setting_value=value,
1934
+ )
1935
+
1936
+ def __default_cross_filtering(self, value: Optional[bool] = False):
1937
+ """
1938
+ Change the default visual interaction from cross highlighting to cross filtering.
1939
+ """
1940
+
1941
+ self.adjust_settings(
1942
+ setting_type="settings",
1943
+ setting_name="defaultFilterActionIsDataFilter",
1944
+ setting_value=value,
1945
+ )
1946
+
1947
+ def __modern_visual_header(self, value: Optional[bool] = True):
1948
+ """
1949
+ Use the modern visual header with updated styling options.
1950
+ """
1951
+
1952
+ self.adjust_settings(
1953
+ setting_type="settings",
1954
+ setting_name="useStylableVisualContainerHeader",
1955
+ setting_value=value,
1956
+ )
1957
+
1958
+ def __show_default_summarization_type(self, value: Optional[bool] = True):
1959
+ """
1960
+ For aggregated fields, always show the default summarization type.
1961
+ """
1962
+
1963
+ self.adjust_settings(
1964
+ setting_type="settings",
1965
+ setting_name="useDefaultAggregateDisplayName",
1966
+ setting_value=value,
1967
+ )
1968
+
1969
+ def __modern_visual_tooltips(self, value: Optional[bool] = True):
1970
+ """
1971
+ Use modern visual tooltips with drill actions and updated styling.
1972
+ """
1973
+
1974
+ self.adjust_settings(
1975
+ setting_type="settings",
1976
+ setting_name="useEnhancedTooltips",
1977
+ setting_value=value,
1978
+ )
1979
+
1980
+ def __user_can_change_filter_types(self, value: Optional[bool] = True):
1981
+ """
1982
+ Allow users to change filter types.
1983
+ """
1984
+
1985
+ self.adjust_settings(
1986
+ setting_type="settings",
1987
+ setting_name="allowChangeFilterTypes",
1988
+ setting_value=value,
1989
+ )
1990
+
1991
+ def __disable_search_filter_pane(self, value: Optional[bool] = False):
1992
+ """
1993
+ Enable search for the filter pane.
1994
+ """
1995
+
1996
+ self.adjust_settings(
1997
+ setting_type="settings",
1998
+ setting_name="disableFilterPaneSearch",
1999
+ setting_value=value,
2000
+ )
2001
+
2002
+ def __enable_cross_report_drillthrough(self, value: Optional[bool] = False):
2003
+ """
2004
+ Allow visuals in this report to use drillthrough targets from other reports.
2005
+ """
2006
+
2007
+ self.adjust_settings(
2008
+ setting_type="settings",
2009
+ setting_name="useCrossReportDrillthrough",
2010
+ setting_value=value,
2011
+ )
2012
+
2013
+ def __disable_default_cross_highlighting(self, value: Optional[bool] = False):
2014
+ """
2015
+ Disable cross highlighting/filtering by default.
2016
+ """
2017
+
2018
+ self.adjust_settings(
2019
+ setting_type="slowDataSourceSettings",
2020
+ setting_name="isCrossHighlightingDisabled",
2021
+ setting_value=value,
2022
+ )
2023
+
2024
+ def __add_slicer_apply_button(self, value: Optional[bool] = False):
2025
+ """
2026
+ Add an Apply button to each individual slicer (not recommended).
2027
+ """
2028
+
2029
+ self.adjust_settings(
2030
+ setting_type="slowDataSourceSettings",
2031
+ setting_name="isSlicerSelectionsButtonEnabled",
2032
+ setting_value=value,
2033
+ )
2034
+
2035
+ # def close(self):
2036
+ # if not self._readonly and self._report is not None:
2037
+ # print("saving...")
2038
+
2039
+ # self._report = None