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