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

@@ -1,28 +1,40 @@
1
+ from typing import Optional, Tuple, List, Literal
2
+ from contextlib import contextmanager
3
+ from sempy._utils._log import log
4
+ from uuid import UUID
1
5
  from sempy_labs._helper_functions import (
2
- resolve_report_id,
3
- format_dax_object_name,
4
- resolve_dataset_from_report,
5
- _conv_b64,
6
- _extract_json,
7
- _add_part,
8
- _decode_b64,
9
6
  resolve_workspace_name_and_id,
10
- _update_dataframe_datatypes,
7
+ resolve_item_name_and_id,
11
8
  _base_api,
12
9
  _create_dataframe,
10
+ _update_dataframe_datatypes,
11
+ format_dax_object_name,
12
+ resolve_dataset_from_report,
13
+ generate_number_guid,
14
+ decode_payload,
15
+ is_base64,
16
+ generate_hex,
17
+ get_jsonpath_value,
18
+ set_json_value,
19
+ remove_json_value,
20
+ )
21
+ from sempy_labs._dictionary_diffs import (
22
+ diff_parts,
13
23
  )
14
- from typing import Optional, List
15
- import pandas as pd
16
24
  import json
17
- import base64
18
- from uuid import UUID
19
- from sempy._utils._log import log
20
25
  import sempy_labs._icons as icons
26
+ import copy
27
+ import pandas as pd
28
+ from jsonpath_ng.ext import parse
21
29
  import sempy_labs.report._report_helper as helper
22
30
  from sempy_labs._model_dependencies import get_measure_dependencies
23
- from jsonpath_ng.ext import parse
24
- import warnings
25
31
  import requests
32
+ import re
33
+ import base64
34
+ from pathlib import Path
35
+ from urllib.parse import urlparse
36
+ import os
37
+ import fnmatch
26
38
 
27
39
 
28
40
  class ReportWrapper:
@@ -33,141 +45,459 @@ class ReportWrapper:
33
45
 
34
46
  Parameters
35
47
  ----------
36
- report : str
37
- The name of the report.
48
+ report : str | uuid.UUID
49
+ The name or ID of the report.
38
50
  workspace : str | uuid.UUID
39
51
  The name or ID of the workspace in which the report resides.
40
52
  Defaults to None which resolves to the workspace of the attached lakehouse
41
53
  or if no lakehouse attached, resolves to the workspace of the notebook.
54
+ readonly: bool, default=True
55
+ Whether the connection is read-only or read/write. Setting this to False enables read/write which saves the changes made back to the server.
56
+ show_diffs: bool, default=True
57
+ Whether to show the differences between the current report definition in the service and the new report definition.
42
58
 
43
59
  Returns
44
60
  -------
45
- pandas.DataFrame
46
- A pandas dataframe containing the report metadata definition files.
61
+ None
62
+ A connection to the report is established and the report definition is retrieved.
47
63
  """
48
64
 
49
- _report: str
50
- _workspace: str
65
+ _report_name: str
66
+ _report_id: str
67
+ _workspace_name: str
68
+ _workspace_id: str
69
+ _readonly: bool
70
+ _report_file_path = "definition/report.json"
71
+ _pages_file_path = "definition/pages/pages.json"
72
+ _report_extensions_path = "definition/reportExtensions.json"
73
+
74
+ # Visuals
75
+ _title_path = (
76
+ "$.visual.visualContainerObjects.title[*].properties.text.expr.Literal.Value"
77
+ )
78
+ _subtitle_path = (
79
+ "$.visual.visualContainerObjects.subTitle[*].properties.text.expr.Literal.Value"
80
+ )
81
+ _visual_x_path = "$.position.x"
82
+ _visual_y_path = "$.position.y"
51
83
 
52
84
  @log
53
85
  def __init__(
54
86
  self,
55
- report: str,
87
+ report: str | UUID,
56
88
  workspace: Optional[str | UUID] = None,
89
+ readonly: bool = True,
90
+ show_diffs: bool = True,
57
91
  ):
58
- """
59
- Connects to a Power BI report and retrieves its definition.
92
+ (self._workspace_name, self._workspace_id) = resolve_workspace_name_and_id(
93
+ workspace
94
+ )
95
+ (self._report_name, self._report_id) = resolve_item_name_and_id(
96
+ item=report, type="Report", workspace=self._workspace_id
97
+ )
98
+ self._readonly = readonly
99
+ self._show_diffs = show_diffs
100
+
101
+ result = _base_api(
102
+ request=f"/v1/workspaces/{self._workspace_id}/items/{self._report_id}/getDefinition",
103
+ method="post",
104
+ status_codes=None,
105
+ lro_return_json=True,
106
+ )
107
+
108
+ # def is_zip_file(data: bytes) -> bool:
109
+ # return data.startswith(b"PK\x03\x04")
110
+
111
+ # Check that the report is in the PBIR format
112
+ parts = result.get("definition", {}).get("parts", [])
113
+ if self._report_file_path not in [p.get("path") for p in parts]:
114
+ self.format = "PBIR-Legacy"
115
+ else:
116
+ self.format = "PBIR"
117
+ self._report_definition = {"parts": []}
118
+ for part in parts:
119
+ path = part.get("path")
120
+ payload = part.get("payload")
121
+
122
+ # decoded_bytes = base64.b64decode(payload)
123
+ # decoded_payload = json.loads(_decode_b64(payload))
124
+ # try:
125
+ # decoded_payload = json.loads(base64.b64decode(payload).decode("utf-8"))
126
+ # except Exception:
127
+ # decoded_payload = base64.b64decode(payload)
128
+ decoded_payload = decode_payload(payload)
129
+
130
+ # if is_zip_file(decoded_bytes):
131
+ # merged_payload = {}
132
+ # with zipfile.ZipFile(BytesIO(decoded_bytes)) as zip_file:
133
+ # for filename in zip_file.namelist():
134
+ # if filename.endswith(".json"):
135
+ # with zip_file.open(filename) as f:
136
+ # content = f.read()
137
+ # part_data = json.loads(content.decode("utf-8"))
138
+
139
+ # if isinstance(part_data, dict):
140
+ # merged_payload.update(part_data)
141
+ # else:
142
+ # # For non-dict top-level json (rare), store under filename
143
+ # merged_payload[filename] = part_data
144
+
145
+ # self._report_definition["parts"].append(
146
+ # {"path": path, "payload": merged_payload}
147
+ # )
148
+ # else:
149
+ # decoded_payload = json.loads(decoded_bytes.decode("utf-8"))
150
+ self._report_definition["parts"].append(
151
+ {"path": path, "payload": decoded_payload}
152
+ )
153
+
154
+ self._current_report_definition = copy.deepcopy(self._report_definition)
60
155
 
61
- The ReportWrapper and all functions which depend on it require the report to be in the `PBIR <https://powerbi.microsoft.com/blog/power-bi-enhanced-report-format-pbir-in-power-bi-desktop-developer-mode-preview>`_ format.
156
+ # self.report = self.Report(self)
157
+
158
+ helper.populate_custom_visual_display_names()
159
+
160
+ def _ensure_pbir(self):
161
+
162
+ if self.format != "PBIR":
163
+ raise NotImplementedError(
164
+ f"{icons.red_dot} This ReportWrapper function requires the report to be in the PBIR format."
165
+ "See here for details: https://powerbi.microsoft.com/blog/power-bi-enhanced-report-format-pbir-in-power-bi-desktop-developer-mode-preview/"
166
+ )
167
+
168
+ # Basic functions
169
+ def get(
170
+ self,
171
+ file_path: str,
172
+ json_path: Optional[str] = None,
173
+ ) -> dict | List[Tuple[str, dict]]:
174
+ """
175
+ Get the json content of the specified report definition file.
62
176
 
63
177
  Parameters
64
178
  ----------
65
- report : str
66
- The name of the report.
67
- workspace : str | UUID
68
- The name or ID of the workspace in which the report resides.
69
- Defaults to None which resolves to the workspace of the attached lakehouse
70
- or if no lakehouse attached, resolves to the workspace of the notebook.
179
+ file_path : str
180
+ The path of the report definition file. For example: "definition/pages/pages.json". You may also use wildcards. For example: "definition/pages/*/page.json".
181
+ json_path : str, default=None
182
+ The json path to the specific part of the file to be retrieved. If None, the entire file content is returned.
71
183
 
72
184
  Returns
73
185
  -------
74
- pandas.DataFrame
75
- A pandas dataframe containing the report metadata definition files.
186
+ dict | List[Tuple[str, dict]]
187
+ The json content of the specified report definition file.
76
188
  """
77
189
 
78
- from sempy_labs.report import get_report_definition
190
+ parts = self._report_definition.get("parts")
79
191
 
80
- warnings.simplefilter(action="ignore", category=FutureWarning)
192
+ # Find matching parts
193
+ if "*" in file_path:
194
+ matching_parts = [
195
+ (part.get("path"), part.get("payload"))
196
+ for part in parts
197
+ if fnmatch.fnmatch(part.get("path"), file_path)
198
+ ]
81
199
 
82
- self._report = report
83
- (self._workspace_name, self._workspace_id) = resolve_workspace_name_and_id(
84
- workspace
85
- )
86
- self._report_id = resolve_report_id(report, self._workspace_id)
87
- self.rdef = get_report_definition(
88
- report=self._report, workspace=self._workspace_id
200
+ if not matching_parts:
201
+ raise ValueError(
202
+ f"{icons.red_dot} No files match the wildcard path '{file_path}'."
203
+ )
204
+
205
+ results = []
206
+ for path, payload in matching_parts:
207
+ if not json_path:
208
+ results.append((path, payload))
209
+ elif not isinstance(payload, dict):
210
+ raise ValueError(
211
+ f"{icons.red_dot} The payload of the file '{path}' is not a dictionary."
212
+ )
213
+ else:
214
+ jsonpath_expr = parse(json_path)
215
+ matches = jsonpath_expr.find(payload)
216
+ if matches:
217
+ results.append((path, matches[0].value))
218
+ # else:
219
+ # raise ValueError(
220
+ # f"{icons.red_dot} No match found for '{json_path}' in '{path}'."
221
+ # )
222
+ if not results:
223
+ raise ValueError(
224
+ f"{icons.red_dot} No match found for '{json_path}' in any of the files matching the wildcard path '{file_path}'."
225
+ )
226
+ return results
227
+
228
+ # Exact path match
229
+ for part in parts:
230
+ if part.get("path") == file_path:
231
+ payload = part.get("payload")
232
+ if not json_path:
233
+ return payload
234
+ elif not isinstance(payload, dict):
235
+ raise ValueError(
236
+ f"{icons.red_dot} The payload of the file '{file_path}' is not a dictionary."
237
+ )
238
+ else:
239
+ jsonpath_expr = parse(json_path)
240
+ matches = jsonpath_expr.find(payload)
241
+ if matches:
242
+ return matches[0].value
243
+ else:
244
+ raise ValueError(
245
+ f"{icons.red_dot} No match found for '{json_path}'."
246
+ )
247
+
248
+ raise ValueError(
249
+ f"{icons.red_dot} File '{file_path}' not found in report definition."
89
250
  )
90
251
 
91
- if len(self.rdef[self.rdef["path"] == "definition/report.json"]) == 0:
252
+ def add(self, file_path: str, payload: dict | bytes):
253
+ """
254
+ Add a new file to the report definition.
255
+
256
+ Parameters
257
+ ----------
258
+ file_path : str
259
+ The path of the file to be added. For example: "definition/pages/pages.json".
260
+ payload : dict | bytes
261
+ The json content of the file to be added. This can be a dictionary or a base64 encoded string.
262
+ """
263
+
264
+ decoded_payload = decode_payload(payload)
265
+
266
+ if file_path in self.list_paths().get("Path").values:
92
267
  raise ValueError(
93
- f"{icons.red_dot} The ReportWrapper function requires the report to be in the PBIR format."
94
- "See here for details: https://powerbi.microsoft.com/blog/power-bi-enhanced-report-format-pbir-in-power-bi-desktop-developer-mode-preview/"
268
+ f"{icons.red_dot} Cannot add the '{file_path}' file as this file path already exists in the report definition."
95
269
  )
96
270
 
97
- # Helper functions
98
- def _add_extended(self, dataframe):
271
+ self._report_definition["parts"].append(
272
+ {"path": file_path, "payload": decoded_payload}
273
+ )
99
274
 
100
- from sempy_labs.tom import connect_semantic_model
275
+ def remove(self, file_path: str, json_path: Optional[str] = None, verbose=True):
276
+ """
277
+ Removes a file from the report definition.
101
278
 
102
- dataset_id, dataset_name, dataset_workspace_id, dataset_workspace_name = (
103
- resolve_dataset_from_report(
104
- report=self._report, workspace=self._workspace_id
279
+ Parameters
280
+ ----------
281
+ file_path : str
282
+ The path of the file to be removed. For example: "definition/pages/fjdis323484/page.json".
283
+ json_path : str, default=None
284
+ The json path to the specific part of the file to be removed. If None, the entire file is removed. Wildcards are supported (i.e. "definition/pages/*/page.json").
285
+ verbose : bool, default=True
286
+ If True, prints messages about the removal process. If False, suppresses these messages.
287
+ """
288
+
289
+ parts = self._report_definition.get("parts")
290
+ matching_parts = []
291
+
292
+ if "*" in file_path:
293
+ matching_parts = [
294
+ part for part in parts if fnmatch.fnmatch(part.get("path"), file_path)
295
+ ]
296
+ else:
297
+ matching_parts = [part for part in parts if part.get("path") == file_path]
298
+
299
+ if not matching_parts:
300
+ raise ValueError(
301
+ f"{icons.red_dot} No file(s) found for path '{file_path}'."
105
302
  )
106
- )
107
303
 
108
- report_level_measures = list(
109
- self.list_report_level_measures()["Measure Name"].values
110
- )
111
- with connect_semantic_model(
112
- dataset=dataset_id, readonly=True, workspace=dataset_workspace_id
113
- ) as tom:
114
- measure_names = {m.Name for m in tom.all_measures()}
115
- measure_names.update(report_level_measures)
116
- column_names = {
117
- format_dax_object_name(c.Parent.Name, c.Name) for c in tom.all_columns()
118
- }
119
- hierarchy_names = {
120
- format_dax_object_name(h.Parent.Name, h.Name)
121
- for h in tom.all_hierarchies()
122
- }
304
+ for part in matching_parts:
305
+ path = part.get("path")
306
+ payload = part.get("payload")
123
307
 
124
- # Vectorized checks
125
- def is_valid(row):
126
- obj_type = row["Object Type"]
127
- obj_name = row["Object Name"]
128
- if obj_type == "Measure":
129
- return obj_name in measure_names
130
- elif obj_type == "Column":
131
- return (
132
- format_dax_object_name(row["Table Name"], obj_name) in column_names
308
+ if not json_path:
309
+ self._report_definition["parts"].remove(part)
310
+ print(
311
+ f"{icons.green_dot} The file '{path}' has been removed from the report definition."
133
312
  )
134
- elif obj_type == "Hierarchy":
135
- return (
136
- format_dax_object_name(row["Table Name"], obj_name)
137
- in hierarchy_names
313
+ else:
314
+ remove_json_value(
315
+ path=path, payload=payload, json_path=json_path, verbose=verbose
138
316
  )
139
- return False
140
317
 
141
- dataframe["Valid Semantic Model Object"] = dataframe.apply(is_valid, axis=1)
142
- return dataframe
318
+ def update(self, file_path: str, payload: dict | bytes):
319
+ """
320
+ Updates the payload of a file in the report definition.
143
321
 
144
- def _update_single_file(self, file_name: str, new_payload):
322
+ Parameters
323
+ ----------
324
+ file_path : str
325
+ The path of the file to be updated. For example: "definition/pages/pages.json".
326
+ payload : dict | bytes
327
+ The new json content of the file to be updated. This can be a dictionary or a base64 encoded string.
145
328
  """
146
- Updates a single file within the PBIR structure
329
+
330
+ decoded_payload = decode_payload(payload)
331
+
332
+ for part in self._report_definition.get("parts"):
333
+ if part.get("path") == file_path:
334
+ part["payload"] = decoded_payload
335
+ # if not self._readonly:
336
+ # print(
337
+ # f"The file '{file_path}' has been updated in the report definition."
338
+ # )
339
+ return
340
+
341
+ raise ValueError(
342
+ f"The '{file_path}' file was not found in the report definition."
343
+ )
344
+
345
+ def set_json(self, file_path: str, json_path: str, json_value: str | dict | List):
147
346
  """
347
+ Sets the JSON value of a file in the report definition. If the json_path does not exist, it will be created.
148
348
 
149
- request_body = {"definition": {"parts": []}}
150
- for _, r in self.rdef.iterrows():
151
- path = r["path"]
152
- payload = r["payload"]
153
- if path == file_name:
154
- _add_part(request_body, path=path, payload=new_payload)
155
- else:
156
- _add_part(request_body, path=path, payload=payload)
349
+ Parameters
350
+ ----------
351
+ file_path : str
352
+ The file path of the JSON file to be updated. For example: "definition/pages/ReportSection1/visuals/a1d8f99b81dcc2d59035/visual.json". Also supports wildcards.
353
+ json_path : str
354
+ The JSON path to the value to be updated or created. This must be a valid JSONPath expression.
355
+ Examples:
356
+ "$.objects.outspace"
357
+ "$.hi.def[*].vv"
358
+ json_value : str | dict | List
359
+ The new value to be set at the specified JSON path. This can be a string, dictionary, or list.
360
+ """
361
+
362
+ files = self.get(file_path=file_path)
363
+
364
+ if isinstance(files, dict):
365
+ files = [(file_path, files)]
366
+
367
+ for file in files:
368
+ path = file[0]
369
+ payload = file[1]
370
+ new_payload = set_json_value(
371
+ payload=payload, json_path=json_path, json_value=json_value
372
+ )
157
373
 
158
- self.update_report(request_body)
374
+ self.update(file_path=path, payload=new_payload)
159
375
 
160
- def update_report(self, request_body: dict):
376
+ def list_paths(self) -> pd.DataFrame:
377
+ """
378
+ List all file paths in the report definition.
161
379
 
162
- _base_api(
163
- request=f"/v1/workspaces/{self._workspace_id}/reports/{self._report_id}/updateDefinition",
164
- method="post",
165
- payload=request_body,
166
- lro_return_status_code=True,
167
- status_codes=None,
380
+ Returns
381
+ -------
382
+ pandas.DataFrame
383
+ A pandas dataframe containing a list of all paths in the report definition.
384
+ """
385
+
386
+ existing_paths = [
387
+ part.get("path") for part in self._report_definition.get("parts")
388
+ ]
389
+ return pd.DataFrame(existing_paths, columns=["Path"])
390
+
391
+ def __all_pages(self):
392
+
393
+ self._ensure_pbir()
394
+
395
+ return [
396
+ o
397
+ for o in self._report_definition.get("parts")
398
+ if o.get("path").endswith("/page.json")
399
+ ]
400
+
401
+ def __all_visuals(self):
402
+
403
+ self._ensure_pbir()
404
+
405
+ return [
406
+ o
407
+ for o in self._report_definition.get("parts")
408
+ if o.get("path").endswith("/visual.json")
409
+ ]
410
+
411
+ # Helper functions
412
+ def __resolve_page_list(self, page: Optional[str | List[str]] = None) -> List[str]:
413
+
414
+ if isinstance(page, str):
415
+ page = [page]
416
+
417
+ # Resolve page list
418
+ return (
419
+ [self.resolve_page_name(p) for p in page]
420
+ if page
421
+ else [
422
+ p["payload"]["name"]
423
+ for p in self.__all_pages()
424
+ if "payload" in p and "name" in p["payload"]
425
+ ]
168
426
  )
169
427
 
170
- def resolve_page_name(self, page_display_name: str) -> UUID:
428
+ def _get_url(self, page_name: Optional[str] = None) -> str:
429
+ """
430
+ Gets the URL of the report. If specified, gets the URL of the specified page.
431
+
432
+ Parameters
433
+ ----------
434
+ page_name : str, default=None
435
+ The name of the page. If None, gets the URL of the report.
436
+ If specified, gets the URL of the specified page.
437
+
438
+ Returns
439
+ -------
440
+ str
441
+ The URL of the report or the specified page.
442
+ """
443
+
444
+ url = f"https://app.powerbi.com/groups/{self._workspace_id}/reports/{self._report_id}"
445
+
446
+ if page_name:
447
+ url += f"/{page_name}"
448
+
449
+ return url
450
+
451
+ def __resolve_page_name_and_display_name_file_path(
452
+ self, page: str
453
+ ) -> Tuple[str, str, str]:
454
+
455
+ self._ensure_pbir()
456
+ page_map = {
457
+ p["path"]: [p["payload"]["name"], p["payload"]["displayName"]]
458
+ for p in self._report_definition.get("parts", [])
459
+ if p.get("path", "").endswith("/page.json") and "payload" in p
460
+ }
461
+
462
+ # Build lookup: page_id → (path, display_name)
463
+ id_lookup = {v[0]: (k, v[1]) for k, v in page_map.items()}
464
+
465
+ # Build lookup: display_name → (path, page_id)
466
+ name_lookup = {v[1]: (k, v[0]) for k, v in page_map.items()}
467
+
468
+ if page in id_lookup:
469
+ path, display_name = id_lookup[page]
470
+ return path, page, display_name
471
+ elif page in name_lookup:
472
+ path, page_id = name_lookup[page]
473
+ return path, page_id, page
474
+ else:
475
+ raise ValueError(
476
+ f"{icons.red_dot} Invalid page display name. The '{page}' page does not exist in the '{self._report_name}' report within the '{self._workspace_name}' workspace."
477
+ )
478
+
479
+ def _resolve_page_name_and_display_name(self, page: str) -> Tuple[str, str]:
480
+ """
481
+ Obtains the page name, page display name for a given page in a report.
482
+
483
+ Parameters
484
+ ----------
485
+ page : str
486
+ The page name or display name.
487
+
488
+ Returns
489
+ -------
490
+ Tuple[str, str]
491
+ The page name and display name.
492
+ """
493
+
494
+ (_, page_id, page_name) = self.__resolve_page_name_and_display_name_file_path(
495
+ page
496
+ )
497
+
498
+ return (page_id, page_name)
499
+
500
+ def resolve_page_name(self, page_display_name: str) -> str:
171
501
  """
172
502
  Obtains the page name, page display name, and the file path for a given page in a report.
173
503
 
@@ -178,21 +508,22 @@ class ReportWrapper:
178
508
 
179
509
  Returns
180
510
  -------
181
- UUID
511
+ str
182
512
  The page name.
183
513
  """
184
514
 
185
- x, y, z = helper.resolve_page_name(self, page_display_name)
186
-
187
- return x
515
+ (path, page_id, page_name) = (
516
+ self.__resolve_page_name_and_display_name_file_path(page_display_name)
517
+ )
518
+ return page_id
188
519
 
189
- def resolve_page_display_name(self, page_name: UUID) -> str:
520
+ def resolve_page_display_name(self, page_name: str) -> str:
190
521
  """
191
522
  Obtains the page dispaly name.
192
523
 
193
524
  Parameters
194
525
  ----------
195
- page_name : UUID
526
+ page_name : str
196
527
  The name of the page of the report.
197
528
 
198
529
  Returns
@@ -201,59 +532,120 @@ class ReportWrapper:
201
532
  The page display name.
202
533
  """
203
534
 
204
- x, y, z = helper.resolve_page_name(self, page_name=page_name)
535
+ (path, page_id, page_name) = (
536
+ self.__resolve_page_name_and_display_name_file_path(page_name)
537
+ )
538
+ return page_name
205
539
 
206
- return y
540
+ def __add_to_registered_resources(self, name: str, path: str, type: str):
207
541
 
208
- def get_theme(self, theme_type: str = "baseTheme") -> dict:
209
- """
210
- Obtains the theme file of the report.
542
+ type = type.capitalize()
211
543
 
212
- Parameters
213
- ----------
214
- theme_type : str, default="baseTheme"
215
- The theme type. Options: "baseTheme", "customTheme".
544
+ report_file = self.get(file_path=self._report_file_path)
545
+ rp_names = [rp.get("name") for rp in report_file.get("resourcePackages")]
216
546
 
217
- Returns
218
- -------
219
- dict
220
- The theme.json file
221
- """
547
+ new_item = {"name": name, "path": path, "type": type}
548
+ if "RegisteredResources" not in rp_names:
549
+ res = {
550
+ "name": "RegisteredResources",
551
+ "type": "RegisteredResources",
552
+ "items": [new_item],
553
+ }
554
+ report_file.get("resourcePackages").append(res)
555
+ else:
556
+ for rp in report_file.get("resourcePackages"):
557
+ if rp.get("name") == "RegisteredResources":
558
+ for item in rp.get("items"):
559
+ item_name = item.get("name")
560
+ item_type = item.get("type")
561
+ item_path = item.get("path")
562
+ if (
563
+ item_name == name
564
+ and item_type == type
565
+ and item_path == path
566
+ ):
567
+ print(
568
+ f"{icons.info} The '{item_name}' {type.lower()} already exists in the report definition."
569
+ )
570
+ raise ValueError()
222
571
 
223
- theme_types = ["baseTheme", "customTheme"]
224
- theme_type = theme_type.lower()
572
+ # Add the new item to the existing RegisteredResources
573
+ rp["items"].append(new_item)
225
574
 
226
- if "custom" in theme_type:
227
- theme_type = "customTheme"
228
- elif "base" in theme_type:
229
- theme_type = "baseTheme"
230
- if theme_type not in theme_types:
231
- raise ValueError(
232
- f"{icons.red_dot} Invalid theme type. Valid options: {theme_types}."
233
- )
575
+ self.update(file_path=self._report_file_path, payload=report_file)
234
576
 
235
- rptdef = self.rdef[self.rdef["path"] == "definition/report.json"]
236
- rptJson = _extract_json(rptdef)
237
- theme_collection = rptJson.get("themeCollection", {})
238
- if theme_type not in theme_collection:
239
- raise ValueError(
240
- f"{icons.red_dot} The {self._report} report within the '{self._workspace_name} workspace has no custom theme."
241
- )
242
- ct = theme_collection.get(theme_type)
243
- theme_name = ct["name"]
244
- theme_location = ct["type"]
245
- theme_file_path = f"StaticResources/{theme_location}/{theme_name}"
246
- if theme_type == "baseTheme":
247
- theme_file_path = (
248
- f"StaticResources/{theme_location}/BaseThemes/{theme_name}"
577
+ def _add_extended(self, dataframe):
578
+
579
+ from sempy_labs.tom import connect_semantic_model
580
+
581
+ dataset_id, dataset_name, dataset_workspace_id, dataset_workspace_name = (
582
+ resolve_dataset_from_report(
583
+ report=self._report_id, workspace=self._workspace_id
249
584
  )
250
- if not theme_file_path.endswith(".json"):
251
- theme_file_path = f"{theme_file_path}.json"
585
+ )
586
+
587
+ report_level_measures = list(
588
+ self.list_report_level_measures()["Measure Name"].values
589
+ )
590
+ with connect_semantic_model(
591
+ dataset=dataset_id, readonly=True, workspace=dataset_workspace_id
592
+ ) as tom:
593
+ measure_names = {m.Name for m in tom.all_measures()}
594
+ measure_names.update(report_level_measures)
595
+ column_names = {
596
+ format_dax_object_name(c.Parent.Name, c.Name) for c in tom.all_columns()
597
+ }
598
+ hierarchy_names = {
599
+ format_dax_object_name(h.Parent.Name, h.Name)
600
+ for h in tom.all_hierarchies()
601
+ }
602
+
603
+ # Vectorized checks
604
+ def is_valid(row):
605
+ obj_type = row["Object Type"]
606
+ obj_name = row["Object Name"]
607
+ if obj_type == "Measure":
608
+ return obj_name in measure_names
609
+ elif obj_type == "Column":
610
+ return (
611
+ format_dax_object_name(row["Table Name"], obj_name) in column_names
612
+ )
613
+ elif obj_type == "Hierarchy":
614
+ return (
615
+ format_dax_object_name(row["Table Name"], obj_name)
616
+ in hierarchy_names
617
+ )
618
+ return False
252
619
 
253
- theme_df = self.rdef[self.rdef["path"] == theme_file_path]
254
- theme_json = _extract_json(theme_df)
620
+ dataframe["Valid Semantic Model Object"] = dataframe.apply(is_valid, axis=1)
621
+ return dataframe
622
+
623
+ def _visual_page_mapping(self) -> dict:
624
+ self._ensure_pbir()
625
+
626
+ page_mapping = {}
627
+ visual_mapping = {}
628
+
629
+ for p in self.__all_pages():
630
+ path = p.get("path")
631
+ payload = p.get("payload")
632
+ pattern_page = r"/pages/(.*?)/page.json"
633
+ page_name = re.search(pattern_page, path).group(1)
634
+ page_id = payload.get("name")
635
+ page_display = payload.get("displayName")
636
+ page_mapping[page_name] = (page_id, page_display)
637
+
638
+ for v in self.__all_visuals():
639
+ path = v.get("path")
640
+ payload = v.get("payload")
641
+ pattern_page = r"/pages/(.*?)/visuals/"
642
+ page_name = re.search(pattern_page, path).group(1)
643
+ visual_mapping[path] = (
644
+ page_mapping.get(page_name)[0],
645
+ page_mapping.get(page_name)[1],
646
+ )
255
647
 
256
- return theme_json
648
+ return visual_mapping
257
649
 
258
650
  # List functions
259
651
  def list_custom_visuals(self) -> pd.DataFrame:
@@ -265,8 +657,7 @@ class ReportWrapper:
265
657
  pandas.DataFrame
266
658
  A pandas dataframe containing a list of all the custom visuals used in the report.
267
659
  """
268
-
269
- helper.populate_custom_visual_display_names()
660
+ self._ensure_pbir()
270
661
 
271
662
  columns = {
272
663
  "Custom Visual Name": "str",
@@ -275,17 +666,27 @@ class ReportWrapper:
275
666
  }
276
667
 
277
668
  df = _create_dataframe(columns=columns)
278
- rd = self.rdef
279
- rd_filt = rd[rd["path"] == "definition/report.json"]
280
- rptJson = _extract_json(rd_filt)
281
- df["Custom Visual Name"] = rptJson.get("publicCustomVisuals")
669
+
670
+ report_file = self.get(file_path=self._report_file_path)
671
+
672
+ df["Custom Visual Name"] = report_file.get("publicCustomVisuals")
282
673
  df["Custom Visual Display Name"] = df["Custom Visual Name"].apply(
283
674
  lambda x: helper.vis_type_mapping.get(x, x)
284
675
  )
285
676
 
286
- df["Used in Report"] = df["Custom Visual Name"].isin(
287
- self.list_visuals()["Type"]
288
- )
677
+ visual_types = set()
678
+ for v in self.__all_visuals():
679
+ payload = v.get("payload", {})
680
+ visual = payload.get("visual", {})
681
+ visual_type = visual.get("visualType")
682
+ if visual_type:
683
+ visual_types.add(visual_type)
684
+
685
+ for _, r in df.iterrows():
686
+ if r["Custom Visual Name"] in visual_types:
687
+ df.at[_, "Used in Report"] = True
688
+ else:
689
+ df.at[_, "Used in Report"] = False
289
690
 
290
691
  _update_dataframe_datatypes(dataframe=df, column_map=columns)
291
692
 
@@ -307,7 +708,9 @@ class ReportWrapper:
307
708
  A pandas dataframe containing a list of all the report filters used in the report.
308
709
  """
309
710
 
310
- rd_filt = self.rdef[self.rdef["path"] == "definition/report.json"]
711
+ self._ensure_pbir()
712
+
713
+ report_file = self.get(file_path=self._report_file_path)
311
714
 
312
715
  columns = {
313
716
  "Filter Name": "str",
@@ -322,35 +725,36 @@ class ReportWrapper:
322
725
  }
323
726
  df = _create_dataframe(columns=columns)
324
727
 
325
- if len(rd_filt) == 1:
326
- rpt_json = _extract_json(rd_filt)
327
- if "filterConfig" in rpt_json:
328
- for flt in rpt_json.get("filterConfig", {}).get("filters", {}):
329
- filter_name = flt.get("name")
330
- how_created = flt.get("howCreated")
331
- locked = flt.get("isLockedInViewMode", False)
332
- hidden = flt.get("isHiddenInViewMode", False)
333
- filter_type = flt.get("type", "Basic")
334
- filter_used = True if "Where" in flt.get("filter", {}) else False
728
+ dfs = []
335
729
 
336
- entity_property_pairs = helper.find_entity_property_pairs(flt)
730
+ if "filterConfig" in report_file:
731
+ for flt in report_file.get("filterConfig", {}).get("filters", {}):
732
+ filter_name = flt.get("name")
733
+ how_created = flt.get("howCreated")
734
+ locked = flt.get("isLockedInViewMode", False)
735
+ hidden = flt.get("isHiddenInViewMode", False)
736
+ filter_type = flt.get("type", "Basic")
737
+ filter_used = True if "Where" in flt.get("filter", {}) else False
337
738
 
338
- for object_name, properties in entity_property_pairs.items():
339
- new_data = {
340
- "Filter Name": filter_name,
341
- "Type": filter_type,
342
- "Table Name": properties[0],
343
- "Object Name": object_name,
344
- "Object Type": properties[1],
345
- "Hidden": hidden,
346
- "Locked": locked,
347
- "How Created": how_created,
348
- "Used": filter_used,
349
- }
739
+ entity_property_pairs = helper.find_entity_property_pairs(flt)
350
740
 
351
- df = pd.concat(
352
- [df, pd.DataFrame(new_data, index=[0])], ignore_index=True
353
- )
741
+ for object_name, properties in entity_property_pairs.items():
742
+ new_data = {
743
+ "Filter Name": filter_name,
744
+ "Type": filter_type,
745
+ "Table Name": properties[0],
746
+ "Object Name": object_name,
747
+ "Object Type": properties[1],
748
+ "Hidden": hidden,
749
+ "Locked": locked,
750
+ "How Created": how_created,
751
+ "Used": filter_used,
752
+ }
753
+
754
+ dfs.append(pd.DataFrame(new_data, index=[0]))
755
+
756
+ if dfs:
757
+ df = pd.concat(dfs, ignore_index=True)
354
758
 
355
759
  _update_dataframe_datatypes(dataframe=df, column_map=columns)
356
760
 
@@ -374,6 +778,7 @@ class ReportWrapper:
374
778
  pandas.DataFrame
375
779
  A pandas dataframe containing a list of all the page filters used in the report.
376
780
  """
781
+ self._ensure_pbir()
377
782
 
378
783
  columns = {
379
784
  "Page Name": "str",
@@ -390,51 +795,43 @@ class ReportWrapper:
390
795
  }
391
796
  df = _create_dataframe(columns=columns)
392
797
 
393
- for _, r in self.rdef.iterrows():
394
- path = r["path"]
395
- payload = r["payload"]
396
- if path.endswith("/page.json"):
397
- obj_file = base64.b64decode(payload).decode("utf-8")
398
- obj_json = json.loads(obj_file)
399
- page_id = obj_json.get("name")
400
- page_display = obj_json.get("displayName")
401
-
402
- if "filterConfig" in obj_json:
403
- for flt in obj_json.get("filterConfig", {}).get("filters", {}):
404
- filter_name = flt.get("name")
405
- how_created = flt.get("howCreated")
406
- locked = flt.get("isLockedInViewMode", False)
407
- hidden = flt.get("isHiddenInViewMode", False)
408
- filter_type = flt.get("type", "Basic")
409
- filter_used = (
410
- True if "Where" in flt.get("filter", {}) else False
411
- )
798
+ dfs = []
799
+ for p in self.__all_pages():
800
+ payload = p.get("payload")
801
+ page_id = payload.get("name")
802
+ page_display = payload.get("displayName")
803
+
804
+ if "filterConfig" in payload:
805
+ for flt in payload.get("filterConfig", {}).get("filters", {}):
806
+ filter_name = flt.get("name")
807
+ how_created = flt.get("howCreated")
808
+ locked = flt.get("isLockedInViewMode", False)
809
+ hidden = flt.get("isHiddenInViewMode", False)
810
+ filter_type = flt.get("type", "Basic")
811
+ filter_used = True if "Where" in flt.get("filter", {}) else False
812
+
813
+ entity_property_pairs = helper.find_entity_property_pairs(flt)
814
+
815
+ for object_name, properties in entity_property_pairs.items():
816
+ new_data = {
817
+ "Page Name": page_id,
818
+ "Page Display Name": page_display,
819
+ "Filter Name": filter_name,
820
+ "Type": filter_type,
821
+ "Table Name": properties[0],
822
+ "Object Name": object_name,
823
+ "Object Type": properties[1],
824
+ "Hidden": hidden,
825
+ "Locked": locked,
826
+ "How Created": how_created,
827
+ "Used": filter_used,
828
+ "Page URL": self._get_url(page_name=page_id),
829
+ }
412
830
 
413
- entity_property_pairs = helper.find_entity_property_pairs(flt)
414
-
415
- for object_name, properties in entity_property_pairs.items():
416
- new_data = {
417
- "Page Name": page_id,
418
- "Page Display Name": page_display,
419
- "Filter Name": filter_name,
420
- "Type": filter_type,
421
- "Table Name": properties[0],
422
- "Object Name": object_name,
423
- "Object Type": properties[1],
424
- "Hidden": hidden,
425
- "Locked": locked,
426
- "How Created": how_created,
427
- "Used": filter_used,
428
- }
429
-
430
- df = pd.concat(
431
- [df, pd.DataFrame(new_data, index=[0])],
432
- ignore_index=True,
433
- )
831
+ dfs.append(pd.DataFrame(new_data, index=[0]))
434
832
 
435
- df["Page URL"] = df["Page Name"].apply(
436
- lambda page_name: f"{helper.get_web_url(report=self._report, workspace=self._workspace_id)}/{page_name}"
437
- )
833
+ if dfs:
834
+ df = pd.concat(dfs, ignore_index=True)
438
835
 
439
836
  _update_dataframe_datatypes(dataframe=df, column_map=columns)
440
837
 
@@ -458,6 +855,7 @@ class ReportWrapper:
458
855
  pandas.DataFrame
459
856
  A pandas dataframe containing a list of all the visual filters used in the report.
460
857
  """
858
+ self._ensure_pbir()
461
859
 
462
860
  columns = {
463
861
  "Page Name": "str",
@@ -475,51 +873,47 @@ class ReportWrapper:
475
873
  }
476
874
  df = _create_dataframe(columns=columns)
477
875
 
478
- page_mapping, visual_mapping = helper.visual_page_mapping(self)
479
-
480
- for _, r in self.rdef.iterrows():
481
- path = r["path"]
482
- payload = r["payload"]
483
- if path.endswith("/visual.json"):
484
- obj_file = base64.b64decode(payload).decode("utf-8")
485
- obj_json = json.loads(obj_file)
486
- page_id = visual_mapping.get(path)[0]
487
- page_display = visual_mapping.get(path)[1]
488
- visual_name = obj_json.get("name")
489
-
490
- if "filterConfig" in obj_json:
491
- for flt in obj_json.get("filterConfig", {}).get("filters", {}):
492
- filter_name = flt.get("name")
493
- how_created = flt.get("howCreated")
494
- locked = flt.get("isLockedInViewMode", False)
495
- hidden = flt.get("isHiddenInViewMode", False)
496
- filter_type = flt.get("type", "Basic")
497
- filter_used = (
498
- True if "Where" in flt.get("filter", {}) else False
499
- )
876
+ visual_mapping = self._visual_page_mapping()
500
877
 
501
- entity_property_pairs = helper.find_entity_property_pairs(flt)
502
-
503
- for object_name, properties in entity_property_pairs.items():
504
- new_data = {
505
- "Page Name": page_id,
506
- "Page Display Name": page_display,
507
- "Visual Name": visual_name,
508
- "Filter Name": filter_name,
509
- "Type": filter_type,
510
- "Table Name": properties[0],
511
- "Object Name": object_name,
512
- "Object Type": properties[1],
513
- "Hidden": hidden,
514
- "Locked": locked,
515
- "How Created": how_created,
516
- "Used": filter_used,
517
- }
518
-
519
- df = pd.concat(
520
- [df, pd.DataFrame(new_data, index=[0])],
521
- ignore_index=True,
522
- )
878
+ dfs = []
879
+ for v in self.__all_visuals():
880
+ path = v.get("path")
881
+ payload = v.get("payload")
882
+ page_id = visual_mapping.get(path)[0]
883
+ page_display = visual_mapping.get(path)[1]
884
+ visual_name = payload.get("name")
885
+
886
+ if "filterConfig" in payload:
887
+ for flt in payload.get("filterConfig", {}).get("filters", {}):
888
+ filter_name = flt.get("name")
889
+ how_created = flt.get("howCreated")
890
+ locked = flt.get("isLockedInViewMode", False)
891
+ hidden = flt.get("isHiddenInViewMode", False)
892
+ filter_type = flt.get("type", "Basic")
893
+ filter_used = True if "Where" in flt.get("filter", {}) else False
894
+
895
+ entity_property_pairs = helper.find_entity_property_pairs(flt)
896
+
897
+ for object_name, properties in entity_property_pairs.items():
898
+ new_data = {
899
+ "Page Name": page_id,
900
+ "Page Display Name": page_display,
901
+ "Visual Name": visual_name,
902
+ "Filter Name": filter_name,
903
+ "Type": filter_type,
904
+ "Table Name": properties[0],
905
+ "Object Name": object_name,
906
+ "Object Type": properties[1],
907
+ "Hidden": hidden,
908
+ "Locked": locked,
909
+ "How Created": how_created,
910
+ "Used": filter_used,
911
+ }
912
+
913
+ dfs.append(pd.DataFrame(new_data, index=[0]))
914
+
915
+ if dfs:
916
+ df = pd.concat(dfs, ignore_index=True)
523
917
 
524
918
  _update_dataframe_datatypes(dataframe=df, column_map=columns)
525
919
 
@@ -540,6 +934,7 @@ class ReportWrapper:
540
934
  pandas.DataFrame
541
935
  A pandas dataframe containing a list of all modified visual interactions used in the report.
542
936
  """
937
+ self._ensure_pbir()
543
938
 
544
939
  columns = {
545
940
  "Page Name": "str",
@@ -550,30 +945,28 @@ class ReportWrapper:
550
945
  }
551
946
  df = _create_dataframe(columns=columns)
552
947
 
553
- for _, r in self.rdef.iterrows():
554
- file_path = r["path"]
555
- payload = r["payload"]
556
- if file_path.endswith("/page.json"):
557
- obj_file = base64.b64decode(payload).decode("utf-8")
558
- obj_json = json.loads(obj_file)
559
- page_name = obj_json.get("name")
560
- page_display = obj_json.get("displayName")
948
+ dfs = []
949
+ for p in self.__all_pages():
950
+ payload = p.get("payload")
951
+ page_name = payload.get("name")
952
+ page_display = payload.get("displayName")
561
953
 
562
- for vizInt in obj_json.get("visualInteractions", []):
563
- sourceVisual = vizInt.get("source")
564
- targetVisual = vizInt.get("target")
565
- vizIntType = vizInt.get("type")
954
+ for vizInt in payload.get("visualInteractions", []):
955
+ sourceVisual = vizInt.get("source")
956
+ targetVisual = vizInt.get("target")
957
+ vizIntType = vizInt.get("type")
566
958
 
567
- new_data = {
568
- "Page Name": page_name,
569
- "Page Display Name": page_display,
570
- "Source Visual Name": sourceVisual,
571
- "Target Visual Name": targetVisual,
572
- "Type": vizIntType,
573
- }
574
- df = pd.concat(
575
- [df, pd.DataFrame(new_data, index=[0])], ignore_index=True
576
- )
959
+ new_data = {
960
+ "Page Name": page_name,
961
+ "Page Display Name": page_display,
962
+ "Source Visual Name": sourceVisual,
963
+ "Target Visual Name": targetVisual,
964
+ "Type": vizIntType,
965
+ }
966
+ dfs.append(pd.DataFrame(new_data, index=[0]))
967
+
968
+ if dfs:
969
+ df = pd.concat(dfs, ignore_index=True)
577
970
 
578
971
  return df
579
972
 
@@ -586,6 +979,7 @@ class ReportWrapper:
586
979
  pandas.DataFrame
587
980
  A pandas dataframe containing a list of all pages in the report.
588
981
  """
982
+ self._ensure_pbir()
589
983
 
590
984
  columns = {
591
985
  "File Path": "str",
@@ -607,46 +1001,42 @@ class ReportWrapper:
607
1001
  }
608
1002
  df = _create_dataframe(columns=columns)
609
1003
 
610
- dfV = self.list_visuals()
611
-
612
- page_rows = self.rdef[self.rdef["path"].str.endswith("/page.json")]
613
- pages_row = self.rdef[self.rdef["path"] == "definition/pages/pages.json"]
1004
+ page = self.get(file_path=self._pages_file_path)
1005
+ active_page = page.get("activePageName")
614
1006
 
615
- for _, r in page_rows.iterrows():
616
- file_path = r["path"]
617
- payload = r["payload"]
1007
+ dfV = self.list_visuals()
618
1008
 
619
- pageFile = base64.b64decode(payload).decode("utf-8")
1009
+ dfs = []
1010
+ for p in self.__all_pages():
1011
+ file_path = p.get("path")
620
1012
  page_prefix = file_path[0:-9]
621
- pageJson = json.loads(pageFile)
622
- page_name = pageJson.get("name")
623
- height = pageJson.get("height")
624
- width = pageJson.get("width")
1013
+ payload = p.get("payload")
1014
+ page_name = payload.get("name")
1015
+ height = payload.get("height")
1016
+ width = payload.get("width")
625
1017
 
626
1018
  # Alignment
627
- matches = parse(
628
- "$.objects.displayArea[0].properties.verticalAlignment.expr.Literal.Value"
629
- ).find(pageJson)
630
- alignment_value = (
631
- matches[0].value[1:-1] if matches and matches[0].value else "Top"
1019
+ alignment_value = get_jsonpath_value(
1020
+ data=payload,
1021
+ path="$.objects.displayArea[*].properties.verticalAlignment.expr.Literal.Value",
1022
+ default="Top",
1023
+ remove_quotes=True,
632
1024
  )
633
1025
 
634
1026
  # Drillthrough
635
- matches = parse("$.filterConfig.filters[*].howCreated").find(pageJson)
1027
+ matches = parse("$.filterConfig.filters[*].howCreated").find(payload)
636
1028
  how_created_values = [match.value for match in matches]
637
1029
  drill_through = any(value == "Drillthrough" for value in how_created_values)
638
- # matches = parse("$.filterConfig.filters[*]").find(pageJson)
639
- # drill_through = any(
640
- # filt.get("howCreated") == "Drillthrough"
641
- # for filt in (match.value for match in matches)
642
- # )
643
1030
 
644
1031
  visual_count = len(
645
- self.rdef[
646
- self.rdef["path"].str.endswith("/visual.json")
647
- & (self.rdef["path"].str.startswith(page_prefix))
1032
+ [
1033
+ v
1034
+ for v in self._report_definition.get("parts")
1035
+ if v.get("path").endswith("/visual.json")
1036
+ and v.get("path").startswith(page_prefix)
648
1037
  ]
649
1038
  )
1039
+
650
1040
  data_visual_count = len(
651
1041
  dfV[(dfV["Page Name"] == page_name) & (dfV["Data Visual"])]
652
1042
  )
@@ -655,24 +1045,25 @@ class ReportWrapper:
655
1045
  )
656
1046
 
657
1047
  # Page Filter Count
658
- matches = parse("$.filterConfig.filters").find(pageJson)
659
- page_filter_count = (
660
- len(matches[0].value) if matches and matches[0].value else 0
1048
+ page_filter_count = len(
1049
+ get_jsonpath_value(
1050
+ data=payload, path="$.filterConfig.filters", default=[]
1051
+ )
661
1052
  )
662
1053
 
663
1054
  # Hidden
664
- matches = parse("$.visibility").find(pageJson)
1055
+ matches = parse("$.visibility").find(payload)
665
1056
  is_hidden = any(match.value == "HiddenInViewMode" for match in matches)
666
1057
 
667
1058
  new_data = {
668
1059
  "File Path": file_path,
669
1060
  "Page Name": page_name,
670
- "Page Display Name": pageJson.get("displayName"),
671
- "Display Option": pageJson.get("displayOption"),
1061
+ "Page Display Name": payload.get("displayName"),
1062
+ "Display Option": payload.get("displayOption"),
672
1063
  "Height": height,
673
1064
  "Width": width,
674
1065
  "Hidden": is_hidden,
675
- "Active": False,
1066
+ "Active": True if page_name == active_page else False,
676
1067
  "Type": helper.page_type_mapping.get((width, height), "Custom"),
677
1068
  "Alignment": alignment_value,
678
1069
  "Drillthrough Target Page": drill_through,
@@ -680,24 +1071,16 @@ class ReportWrapper:
680
1071
  "Data Visual Count": data_visual_count,
681
1072
  "Visible Visual Count": visible_visual_count,
682
1073
  "Page Filter Count": page_filter_count,
1074
+ "Page URL": self._get_url(page_name=page_name),
683
1075
  }
684
- df = pd.concat([df, pd.DataFrame(new_data, index=[0])], ignore_index=True)
1076
+ dfs.append(pd.DataFrame(new_data, index=[0]))
685
1077
 
686
- page_payload = pages_row["payload"].iloc[0]
687
- pageFile = base64.b64decode(page_payload).decode("utf-8")
688
- pageJson = json.loads(pageFile)
689
- activePage = pageJson["activePageName"]
690
-
691
- df.loc[df["Page Name"] == activePage, "Active"] = True
692
-
693
- df["Page URL"] = df["Page Name"].apply(
694
- lambda page_name: f"{helper.get_web_url(report=self._report, workspace=self._workspace_id)}/{page_name}"
695
- )
1078
+ if dfs:
1079
+ df = pd.concat(dfs, ignore_index=True)
696
1080
 
697
1081
  _update_dataframe_datatypes(dataframe=df, column_map=columns)
698
1082
 
699
1083
  return df
700
- # return df.style.format({"Page URL": _make_clickable})
701
1084
 
702
1085
  def list_visuals(self) -> pd.DataFrame:
703
1086
  """
@@ -708,6 +1091,7 @@ class ReportWrapper:
708
1091
  pandas.DataFrame
709
1092
  A pandas dataframe containing a list of all visuals in the report.
710
1093
  """
1094
+ self._ensure_pbir()
711
1095
 
712
1096
  columns = {
713
1097
  "File Path": "str",
@@ -739,12 +1123,9 @@ class ReportWrapper:
739
1123
  }
740
1124
  df = _create_dataframe(columns=columns)
741
1125
 
742
- rd_filt = self.rdef[self.rdef["path"] == "definition/report.json"]
743
- payload = rd_filt["payload"].iloc[0]
744
- rptJson = _extract_json(rd_filt)
745
- custom_visuals = rptJson.get("publicCustomVisuals", [])
746
- page_mapping, visual_mapping = helper.visual_page_mapping(self)
747
- helper.populate_custom_visual_display_names()
1126
+ report_file = self.get(file_path=self._report_file_path)
1127
+ custom_visuals = report_file.get("publicCustomVisuals", [])
1128
+ visual_mapping = self._visual_page_mapping()
748
1129
  agg_type_map = helper._get_agg_type_mapping()
749
1130
 
750
1131
  def contains_key(data, keys_to_check):
@@ -761,150 +1142,144 @@ class ReportWrapper:
761
1142
 
762
1143
  return any(key in all_keys for key in keys_to_check)
763
1144
 
764
- for _, r in self.rdef.iterrows():
765
- file_path = r["path"]
766
- payload = r["payload"]
767
- if file_path.endswith("/visual.json"):
768
- visual_file = base64.b64decode(payload).decode("utf-8")
769
- visual_json = json.loads(visual_file)
770
- page_id = visual_mapping.get(file_path)[0]
771
- page_display = visual_mapping.get(file_path)[1]
772
- pos = visual_json.get("position")
773
-
774
- # Visual Type
775
- matches = parse("$.visual.visualType").find(visual_json)
776
- visual_type = matches[0].value if matches else "Group"
777
-
778
- visual_type_display = helper.vis_type_mapping.get(
779
- visual_type, visual_type
780
- )
781
- cst_value, rst_value, slicer_type = False, False, "N/A"
1145
+ dfs = []
782
1146
 
783
- # Visual Filter Count
784
- matches = parse("$.filterConfig.filters[*]").find(visual_json)
785
- visual_filter_count = len(matches)
1147
+ for v in self.__all_visuals():
1148
+ path = v.get("path")
1149
+ payload = v.get("payload")
1150
+ page_id = visual_mapping.get(path)[0]
1151
+ page_display = visual_mapping.get(path)[1]
1152
+ pos = payload.get("position")
786
1153
 
787
- # Data Limit
788
- matches = parse(
789
- '$.filterConfig.filters[?(@.type == "VisualTopN")].filter.Where[*].Condition.VisualTopN.ItemCount'
790
- ).find(visual_json)
791
- data_limit = matches[0].value if matches else 0
1154
+ # Visual Type
1155
+ matches = parse("$.visual.visualType").find(payload)
1156
+ visual_type = matches[0].value if matches else "Group"
792
1157
 
793
- # Title
794
- matches = parse(
795
- "$.visual.visualContainerObjects.title[0].properties.text.expr"
796
- ).find(visual_json)
797
- # title = matches[0].value[1:-1] if matches else ""
798
- title = (
799
- helper._get_expression(matches[0].value, agg_type_map)
800
- if matches
801
- else ""
802
- )
1158
+ visual_type_display = helper.vis_type_mapping.get(visual_type, visual_type)
1159
+ cst_value, rst_value, slicer_type = False, False, "N/A"
803
1160
 
804
- # SubTitle
805
- matches = parse(
806
- "$.visual.visualContainerObjects.subTitle[0].properties.text.expr"
807
- ).find(visual_json)
808
- # sub_title = matches[0].value[1:-1] if matches else ""
809
- sub_title = (
810
- helper._get_expression(matches[0].value, agg_type_map)
811
- if matches
812
- else ""
813
- )
1161
+ # Visual Filter Count
1162
+ matches = parse("$.filterConfig.filters[*]").find(payload)
1163
+ visual_filter_count = len(matches)
814
1164
 
815
- # Alt Text
816
- matches = parse(
817
- "$.visual.visualContainerObjects.general[0].properties.altText.expr"
818
- ).find(visual_json)
819
- # alt_text = matches[0].value[1:-1] if matches else ""
820
- alt_text = (
821
- helper._get_expression(matches[0].value, agg_type_map)
822
- if matches
823
- else ""
824
- )
1165
+ # Data Limit
1166
+ matches = parse(
1167
+ '$.filterConfig.filters[?(@.type == "VisualTopN")].filter.Where[*].Condition.VisualTopN.ItemCount'
1168
+ ).find(payload)
1169
+ data_limit = matches[0].value if matches else 0
1170
+
1171
+ # Title
1172
+ matches = parse(
1173
+ "$.visual.visualContainerObjects.title[0].properties.text.expr"
1174
+ ).find(payload)
1175
+ title = (
1176
+ helper._get_expression(matches[0].value, agg_type_map)
1177
+ if matches
1178
+ else ""
1179
+ )
1180
+
1181
+ # SubTitle
1182
+ matches = parse(
1183
+ "$.visual.visualContainerObjects.subTitle[0].properties.text.expr"
1184
+ ).find(payload)
1185
+ sub_title = (
1186
+ helper._get_expression(matches[0].value, agg_type_map)
1187
+ if matches
1188
+ else ""
1189
+ )
1190
+
1191
+ # Alt Text
1192
+ matches = parse(
1193
+ "$.visual.visualContainerObjects.general[0].properties.altText.expr"
1194
+ ).find(payload)
1195
+ alt_text = (
1196
+ helper._get_expression(matches[0].value, agg_type_map)
1197
+ if matches
1198
+ else ""
1199
+ )
825
1200
 
826
- # Show items with no data
827
- def find_show_all_with_jsonpath(obj):
828
- matches = parse("$..showAll").find(obj)
829
- return any(match.value is True for match in matches)
1201
+ # Show items with no data
1202
+ def find_show_all_with_jsonpath(obj):
1203
+ matches = parse("$..showAll").find(obj)
1204
+ return any(match.value is True for match in matches)
830
1205
 
831
- show_all_data = find_show_all_with_jsonpath(visual_json)
1206
+ show_all_data = find_show_all_with_jsonpath(payload)
832
1207
 
833
- # Divider
1208
+ # Divider
1209
+ matches = parse(
1210
+ "$.visual.visualContainerObjects.divider[0].properties.show.expr.Literal.Value"
1211
+ ).find(payload)
1212
+ divider = matches[0] if matches else ""
1213
+
1214
+ # Row/Column Subtotals
1215
+ if visual_type == "pivotTable":
1216
+ cst_matches = parse(
1217
+ "$.visual.objects.subTotals[0].properties.columnSubtotals.expr.Literal.Value"
1218
+ ).find(payload)
1219
+ rst_matches = parse(
1220
+ "$.visual.objects.subTotals[0].properties.rowSubtotals.expr.Literal.Value"
1221
+ ).find(payload)
1222
+
1223
+ if cst_matches:
1224
+ cst_value = False if cst_matches[0].value == "false" else True
1225
+
1226
+ if rst_matches:
1227
+ rst_value = False if rst_matches[0].value == "false" else True
1228
+
1229
+ # Slicer Type
1230
+ if visual_type == "slicer":
834
1231
  matches = parse(
835
- "$.visual.visualContainerObjects.divider[0].properties.show.expr.Literal.Value"
836
- ).find(visual_json)
837
- divider = matches[0] if matches else ""
838
-
839
- # Row/Column Subtotals
840
- if visual_type == "pivotTable":
841
- cst_matches = parse(
842
- "$.visual.objects.subTotals[0].properties.columnSubtotals.expr.Literal.Value"
843
- ).find(visual_json)
844
- rst_matches = parse(
845
- "$.visual.objects.subTotals[0].properties.rowSubtotals.expr.Literal.Value"
846
- ).find(visual_json)
847
-
848
- if cst_matches:
849
- cst_value = False if cst_matches[0].value == "false" else True
850
-
851
- if rst_matches:
852
- rst_value = False if rst_matches[0].value == "false" else True
853
-
854
- # Slicer Type
855
- if visual_type == "slicer":
856
- matches = parse(
857
- "$.visual.objects.data[0].properties.mode.expr.Literal.Value"
858
- ).find(visual_json)
859
- slicer_type = matches[0].value[1:-1] if matches else "N/A"
860
-
861
- # Data Visual
862
- is_data_visual = contains_key(
863
- visual_json,
864
- [
865
- "Aggregation",
866
- "Column",
867
- "Measure",
868
- "HierarchyLevel",
869
- "NativeVisualCalculation",
870
- ],
871
- )
1232
+ "$.visual.objects.data[0].properties.mode.expr.Literal.Value"
1233
+ ).find(payload)
1234
+ slicer_type = matches[0].value[1:-1] if matches else "N/A"
1235
+
1236
+ # Data Visual
1237
+ is_data_visual = contains_key(
1238
+ payload,
1239
+ [
1240
+ "Aggregation",
1241
+ "Column",
1242
+ "Measure",
1243
+ "HierarchyLevel",
1244
+ "NativeVisualCalculation",
1245
+ ],
1246
+ )
872
1247
 
873
- # Sparkline
874
- has_sparkline = contains_key(visual_json, ["SparklineData"])
1248
+ # Sparkline
1249
+ has_sparkline = contains_key(payload, ["SparklineData"])
875
1250
 
876
- new_data = {
877
- "File Path": file_path,
878
- "Page Name": page_id,
879
- "Page Display Name": page_display,
880
- "Visual Name": visual_json.get("name"),
881
- "X": pos.get("x"),
882
- "Y": pos.get("y"),
883
- "Z": pos.get("z"),
884
- "Width": pos.get("width"),
885
- "Height": pos.get("height"),
886
- "Tab Order": pos.get("tabOrder"),
887
- "Hidden": visual_json.get("isHidden", False),
888
- "Type": visual_type,
889
- "Display Type": visual_type_display,
890
- "Title": title,
891
- "SubTitle": sub_title,
892
- "Custom Visual": visual_type in custom_visuals,
893
- "Alt Text": alt_text,
894
- "Show Items With No Data": show_all_data,
895
- "Divider": divider,
896
- "Row SubTotals": rst_value,
897
- "Column SubTotals": cst_value,
898
- "Slicer Type": slicer_type,
899
- "Data Visual": is_data_visual,
900
- "Has Sparkline": has_sparkline,
901
- "Visual Filter Count": visual_filter_count,
902
- "Data Limit": data_limit,
903
- }
1251
+ new_data = {
1252
+ "File Path": path,
1253
+ "Page Name": page_id,
1254
+ "Page Display Name": page_display,
1255
+ "Visual Name": payload.get("name"),
1256
+ "X": pos.get("x"),
1257
+ "Y": pos.get("y"),
1258
+ "Z": pos.get("z"),
1259
+ "Width": pos.get("width"),
1260
+ "Height": pos.get("height"),
1261
+ "Tab Order": pos.get("tabOrder"),
1262
+ "Hidden": payload.get("isHidden", False),
1263
+ "Type": visual_type,
1264
+ "Display Type": visual_type_display,
1265
+ "Title": title,
1266
+ "SubTitle": sub_title,
1267
+ "Custom Visual": visual_type in custom_visuals,
1268
+ "Alt Text": alt_text,
1269
+ "Show Items With No Data": show_all_data,
1270
+ "Divider": divider,
1271
+ "Row SubTotals": rst_value,
1272
+ "Column SubTotals": cst_value,
1273
+ "Slicer Type": slicer_type,
1274
+ "Data Visual": is_data_visual,
1275
+ "Has Sparkline": has_sparkline,
1276
+ "Visual Filter Count": visual_filter_count,
1277
+ "Data Limit": data_limit,
1278
+ }
1279
+ dfs.append(pd.DataFrame(new_data, index=[0]))
904
1280
 
905
- df = pd.concat(
906
- [df, pd.DataFrame(new_data, index=[0])], ignore_index=True
907
- )
1281
+ if dfs:
1282
+ df = pd.concat(dfs, ignore_index=True)
908
1283
 
909
1284
  grouped_df = (
910
1285
  self.list_visual_objects()
@@ -941,8 +1316,9 @@ class ReportWrapper:
941
1316
  pandas.DataFrame
942
1317
  A pandas dataframe containing a list of all semantic model objects used in each visual in the report.
943
1318
  """
1319
+ self._ensure_pbir()
944
1320
 
945
- page_mapping, visual_mapping = helper.visual_page_mapping(self)
1321
+ visual_mapping = self._visual_page_mapping()
946
1322
 
947
1323
  columns = {
948
1324
  "Page Name": "str",
@@ -1022,59 +1398,58 @@ class ReportWrapper:
1022
1398
 
1023
1399
  return result
1024
1400
 
1025
- for _, r in self.rdef.iterrows():
1026
- file_path = r["path"]
1027
- payload = r["payload"]
1028
- if file_path.endswith("/visual.json"):
1029
- visual_file = base64.b64decode(payload).decode("utf-8")
1030
- visual_json = json.loads(visual_file)
1031
- page_id = visual_mapping.get(file_path)[0]
1032
- page_display = visual_mapping.get(file_path)[1]
1033
-
1034
- entity_property_pairs = find_entity_property_pairs(visual_json)
1035
- query_state = (
1036
- visual_json.get("visual", {}).get("query", {}).get("queryState", {})
1037
- )
1401
+ dfs = []
1402
+ for v in self.__all_visuals():
1403
+ path = v.get("path")
1404
+ payload = v.get("payload")
1405
+ page_id = visual_mapping.get(path)[0]
1406
+ page_display = visual_mapping.get(path)[1]
1038
1407
 
1039
- format_mapping = {}
1040
- obj_display_mapping = {}
1041
- for a, p in query_state.items():
1042
- for proj in p.get("projections", []):
1043
- query_ref = proj.get("queryRef")
1044
- fmt = proj.get("format")
1045
- obj_display_name = proj.get("displayName")
1046
- if fmt is not None:
1047
- format_mapping[query_ref] = fmt
1048
- obj_display_mapping[query_ref] = obj_display_name
1408
+ entity_property_pairs = find_entity_property_pairs(payload)
1409
+ query_state = (
1410
+ payload.get("visual", {}).get("query", {}).get("queryState", {})
1411
+ )
1049
1412
 
1050
- for object_name, properties in entity_property_pairs.items():
1051
- table_name = properties[0]
1052
- obj_full = f"{table_name}.{object_name}"
1053
- is_agg = properties[2]
1054
- format_value = format_mapping.get(obj_full)
1055
- obj_display = obj_display_mapping.get(obj_full)
1056
-
1057
- if is_agg:
1058
- for k, v in format_mapping.items():
1059
- if obj_full in k:
1060
- format_value = v
1061
- new_data = {
1062
- "Page Name": page_id,
1063
- "Page Display Name": page_display,
1064
- "Visual Name": visual_json.get("name"),
1065
- "Table Name": table_name,
1066
- "Object Name": object_name,
1067
- "Object Type": properties[1],
1068
- "Implicit Measure": is_agg,
1069
- "Sparkline": properties[4],
1070
- "Visual Calc": properties[3],
1071
- "Format": format_value,
1072
- "Object Display Name": obj_display,
1073
- }
1413
+ format_mapping = {}
1414
+ obj_display_mapping = {}
1415
+ for a, p in query_state.items():
1416
+ for proj in p.get("projections", []):
1417
+ query_ref = proj.get("queryRef")
1418
+ fmt = proj.get("format")
1419
+ obj_display_name = proj.get("displayName")
1420
+ if fmt is not None:
1421
+ format_mapping[query_ref] = fmt
1422
+ obj_display_mapping[query_ref] = obj_display_name
1423
+
1424
+ for object_name, properties in entity_property_pairs.items():
1425
+ table_name = properties[0]
1426
+ obj_full = f"{table_name}.{object_name}"
1427
+ is_agg = properties[2]
1428
+ format_value = format_mapping.get(obj_full)
1429
+ obj_display = obj_display_mapping.get(obj_full)
1430
+
1431
+ if is_agg:
1432
+ for k, v in format_mapping.items():
1433
+ if obj_full in k:
1434
+ format_value = v
1435
+ new_data = {
1436
+ "Page Name": page_id,
1437
+ "Page Display Name": page_display,
1438
+ "Visual Name": payload.get("name"),
1439
+ "Table Name": table_name,
1440
+ "Object Name": object_name,
1441
+ "Object Type": properties[1],
1442
+ "Implicit Measure": is_agg,
1443
+ "Sparkline": properties[4],
1444
+ "Visual Calc": properties[3],
1445
+ "Format": format_value,
1446
+ "Object Display Name": obj_display,
1447
+ }
1074
1448
 
1075
- df = pd.concat(
1076
- [df, pd.DataFrame(new_data, index=[0])], ignore_index=True
1077
- )
1449
+ dfs.append(pd.DataFrame(new_data, index=[0]))
1450
+
1451
+ if dfs:
1452
+ df = pd.concat(dfs, ignore_index=True)
1078
1453
 
1079
1454
  if extended:
1080
1455
  df = self._add_extended(dataframe=df)
@@ -1099,6 +1474,7 @@ class ReportWrapper:
1099
1474
  pandas.DataFrame
1100
1475
  A pandas dataframe showing the semantic model objects used in the report.
1101
1476
  """
1477
+ self._ensure_pbir()
1102
1478
 
1103
1479
  from sempy_labs.tom import connect_semantic_model
1104
1480
 
@@ -1118,7 +1494,7 @@ class ReportWrapper:
1118
1494
 
1119
1495
  rf_subset = rf[["Table Name", "Object Name", "Object Type"]].copy()
1120
1496
  rf_subset["Report Source"] = "Report Filter"
1121
- rf_subset["Report Source Object"] = self._report
1497
+ rf_subset["Report Source Object"] = self._report_name
1122
1498
 
1123
1499
  pf_subset = pf[
1124
1500
  ["Table Name", "Object Name", "Object Type", "Page Display Name"]
@@ -1162,9 +1538,9 @@ class ReportWrapper:
1162
1538
  )
1163
1539
 
1164
1540
  if extended:
1165
- dataset_id, dataset_name, dataset_workspace_id, dataset_workspace_name = (
1541
+ (dataset_id, dataset_name, dataset_workspace_id, dataset_workspace_name) = (
1166
1542
  resolve_dataset_from_report(
1167
- report=self._report, workspace=self._workspace_id
1543
+ report=self._report_id, workspace=self._workspace_id
1168
1544
  )
1169
1545
  )
1170
1546
 
@@ -1208,7 +1584,7 @@ class ReportWrapper:
1208
1584
  )
1209
1585
  dataset_id, dataset_name, dataset_workspace_id, dataset_workspace_name = (
1210
1586
  resolve_dataset_from_report(
1211
- report=self._report, workspace=self._workspace_id
1587
+ report=self._report_id, workspace=self._workspace_id
1212
1588
  )
1213
1589
  )
1214
1590
  dep = get_measure_dependencies(
@@ -1243,8 +1619,7 @@ class ReportWrapper:
1243
1619
  pandas.DataFrame
1244
1620
  A pandas dataframe containing a list of all bookmarks in the report.
1245
1621
  """
1246
-
1247
- rd = self.rdef
1622
+ self._ensure_pbir()
1248
1623
 
1249
1624
  columns = {
1250
1625
  "File Path": "str",
@@ -1257,31 +1632,34 @@ class ReportWrapper:
1257
1632
  }
1258
1633
  df = _create_dataframe(columns=columns)
1259
1634
 
1260
- bookmark_rows = rd[rd["path"].str.endswith(".bookmark.json")]
1635
+ bookmarks = [
1636
+ o
1637
+ for o in self._report_definition.get("parts")
1638
+ if o.get("path").endswith("/bookmark.json")
1639
+ ]
1261
1640
 
1262
- for _, r in bookmark_rows.iterrows():
1263
- path = r["path"]
1264
- payload = r["payload"]
1641
+ dfs = []
1265
1642
 
1266
- obj_file = base64.b64decode(payload).decode("utf-8")
1267
- obj_json = json.loads(obj_file)
1643
+ for b in bookmarks:
1644
+ path = b.get("path")
1645
+ payload = b.get("payload")
1268
1646
 
1269
- bookmark_name = obj_json.get("name")
1270
- bookmark_display = obj_json.get("displayName")
1271
- rpt_page_id = obj_json.get("explorationState", {}).get("activeSection")
1272
- page_id, page_display, file_path = helper.resolve_page_name(
1273
- self, page_name=rpt_page_id
1647
+ bookmark_name = payload.get("name")
1648
+ bookmark_display = payload.get("displayName")
1649
+ rpt_page_id = payload.get("explorationState", {}).get("activeSection")
1650
+ (page_id, page_display) = self._resolve_page_name_and_display_name(
1651
+ rpt_page_id
1274
1652
  )
1275
1653
 
1276
- for rptPg in obj_json.get("explorationState", {}).get("sections", {}):
1654
+ for rptPg in payload.get("explorationState", {}).get("sections", {}):
1277
1655
  for visual_name in (
1278
- obj_json.get("explorationState", {})
1656
+ payload.get("explorationState", {})
1279
1657
  .get("sections", {})
1280
1658
  .get(rptPg, {})
1281
1659
  .get("visualContainers", {})
1282
1660
  ):
1283
1661
  if (
1284
- obj_json.get("explorationState", {})
1662
+ payload.get("explorationState", {})
1285
1663
  .get("sections", {})
1286
1664
  .get(rptPg, {})
1287
1665
  .get("visualContainers", {})
@@ -1304,9 +1682,10 @@ class ReportWrapper:
1304
1682
  "Visual Name": visual_name,
1305
1683
  "Visual Hidden": visual_hidden,
1306
1684
  }
1307
- df = pd.concat(
1308
- [df, pd.DataFrame(new_data, index=[0])], ignore_index=True
1309
- )
1685
+ dfs.append(pd.DataFrame(new_data, index=[0]))
1686
+
1687
+ if dfs:
1688
+ df = pd.concat(dfs, ignore_index=True)
1310
1689
 
1311
1690
  _update_dataframe_datatypes(dataframe=df, column_map=columns)
1312
1691
 
@@ -1325,114 +1704,99 @@ class ReportWrapper:
1325
1704
  A pandas dataframe containing a list of all report-level measures in the report.
1326
1705
  """
1327
1706
 
1707
+ self._ensure_pbir()
1708
+
1328
1709
  columns = {
1329
1710
  "Measure Name": "str",
1330
1711
  "Table Name": "str",
1331
1712
  "Expression": "str",
1332
1713
  "Data Type": "str",
1333
1714
  "Format String": "str",
1715
+ "Data Category": "str",
1334
1716
  }
1335
1717
 
1336
1718
  df = _create_dataframe(columns=columns)
1337
1719
 
1338
- rd_filt = self.rdef[self.rdef["path"] == "definition/reportExtensions.json"]
1720
+ # If no report extensions path, return empty DataFrame
1721
+ if self._report_extensions_path not in self.list_paths()["Path"].values:
1722
+ return df
1339
1723
 
1340
- if len(rd_filt) == 1:
1341
- payload = rd_filt["payload"].iloc[0]
1342
- obj_file = base64.b64decode(payload).decode("utf-8")
1343
- obj_json = json.loads(obj_file)
1724
+ report_file = self.get(file_path=self._report_extensions_path)
1344
1725
 
1345
- for e in obj_json.get("entities", []):
1346
- table_name = e.get("name")
1347
- for m in e.get("measures", []):
1348
- measure_name = m.get("name")
1349
- expr = m.get("expression")
1350
- data_type = m.get("dataType")
1351
- format_string = m.get("formatString")
1726
+ dfs = []
1727
+ for e in report_file.get("entities", []):
1728
+ table_name = e.get("name")
1729
+ for m in e.get("measures", []):
1730
+ measure_name = m.get("name")
1731
+ expr = m.get("expression")
1732
+ data_type = m.get("dataType")
1733
+ format_string = m.get("formatString")
1734
+ data_category = m.get("dataCategory")
1352
1735
 
1353
- new_data = {
1354
- "Measure Name": measure_name,
1355
- "Table Name": table_name,
1356
- "Expression": expr,
1357
- "Data Type": data_type,
1358
- "Format String": format_string,
1359
- }
1360
- df = pd.concat(
1361
- [df, pd.DataFrame(new_data, index=[0])], ignore_index=True
1362
- )
1736
+ new_data = {
1737
+ "Measure Name": measure_name,
1738
+ "Table Name": table_name,
1739
+ "Expression": expr,
1740
+ "Data Type": data_type,
1741
+ "Format String": format_string,
1742
+ "Data Category": data_category,
1743
+ }
1744
+ dfs.append(pd.DataFrame(new_data, index=[0]))
1745
+
1746
+ if dfs:
1747
+ df = pd.concat(dfs, ignore_index=True)
1363
1748
 
1364
1749
  return df
1365
1750
 
1366
- def _list_annotations(self) -> pd.DataFrame:
1751
+ def get_theme(self, theme_type: str = "baseTheme") -> dict:
1367
1752
  """
1368
- Shows a list of annotations in the report.
1753
+ Obtains the theme file of the report.
1754
+
1755
+ Parameters
1756
+ ----------
1757
+ theme_type : str, default="baseTheme"
1758
+ The theme type. Options: "baseTheme", "customTheme".
1369
1759
 
1370
1760
  Returns
1371
1761
  -------
1372
- pandas.DataFrame
1373
- A pandas dataframe showing a list of report, page and visual annotations in the report.
1762
+ dict
1763
+ The theme.json file
1374
1764
  """
1375
1765
 
1376
- columns = {
1377
- "Type": "str",
1378
- "Object Name": "str",
1379
- "Annotation Name": "str",
1380
- "Annotation Value": "str",
1381
- }
1382
- df = _create_dataframe(columns=columns)
1766
+ self._ensure_pbir()
1383
1767
 
1384
- page_mapping, visual_mapping = helper.visual_page_mapping(self)
1385
- for _, r in self.rdef.iterrows():
1386
- payload = r["payload"]
1387
- path = r["path"]
1388
- if path == "definition/report.json":
1389
- file = _decode_b64(payload)
1390
- json_file = json.loads(file)
1391
- if "annotations" in json_file:
1392
- for ann in json_file["annotations"]:
1393
- new_data = {
1394
- "Type": "Report",
1395
- "Object Name": self._report,
1396
- "Annotation Name": ann.get("name"),
1397
- "Annotation Value": ann.get("value"),
1398
- }
1399
- df = pd.concat(
1400
- [df, pd.DataFrame(new_data, index=[0])], ignore_index=True
1401
- )
1402
- elif path.endswith("/page.json"):
1403
- file = _decode_b64(payload)
1404
- json_file = json.loads(file)
1405
- if "annotations" in json_file:
1406
- for ann in json_file["annotations"]:
1407
- new_data = {
1408
- "Type": "Page",
1409
- "Object Name": json_file.get("displayName"),
1410
- "Annotation Name": ann.get("name"),
1411
- "Annotation Value": ann.get("value"),
1412
- }
1413
- df = pd.concat(
1414
- [df, pd.DataFrame(new_data, index=[0])], ignore_index=True
1415
- )
1416
- elif path.endswith("/visual.json"):
1417
- file = _decode_b64(payload)
1418
- json_file = json.loads(file)
1419
- page_display = visual_mapping.get(path)[1]
1420
- visual_name = json_file.get("name")
1421
- if "annotations" in json_file:
1422
- for ann in json_file["annotations"]:
1423
- new_data = {
1424
- "Type": "Visual",
1425
- "Object Name": f"'{page_display}'[{visual_name}]",
1426
- "Annotation Name": ann.get("name"),
1427
- "Annotation Value": ann.get("value"),
1428
- }
1429
- df = pd.concat(
1430
- [df, pd.DataFrame(new_data, index=[0])], ignore_index=True
1431
- )
1768
+ theme_types = ["baseTheme", "customTheme"]
1769
+ theme_type = theme_type.lower()
1432
1770
 
1433
- return df
1771
+ if "custom" in theme_type:
1772
+ theme_type = "customTheme"
1773
+ elif "base" in theme_type:
1774
+ theme_type = "baseTheme"
1775
+ if theme_type not in theme_types:
1776
+ raise ValueError(
1777
+ f"{icons.red_dot} Invalid theme type. Valid options: {theme_types}."
1778
+ )
1779
+
1780
+ report_file = self.get(file_path=self._report_file_path)
1781
+ theme_collection = report_file.get("themeCollection", {})
1782
+ if theme_type not in theme_collection:
1783
+ raise ValueError(
1784
+ f"{icons.red_dot} The {self._report} report within the '{self._workspace_name} workspace has no custom theme."
1785
+ )
1786
+ ct = theme_collection.get(theme_type)
1787
+ theme_name = ct["name"]
1788
+ theme_location = ct["type"]
1789
+ theme_file_path = f"StaticResources/{theme_location}/{theme_name}"
1790
+ if theme_type == "baseTheme":
1791
+ theme_file_path = (
1792
+ f"StaticResources/{theme_location}/BaseThemes/{theme_name}"
1793
+ )
1794
+ if not theme_file_path.endswith(".json"):
1795
+ theme_file_path = f"{theme_file_path}.json"
1796
+
1797
+ return self.get(file_path=theme_file_path)
1434
1798
 
1435
- # Automation functions
1799
+ # Action functions
1436
1800
  def set_theme(self, theme_file_path: str):
1437
1801
  """
1438
1802
  Sets a custom theme for a report based on a theme .json file.
@@ -1445,98 +1809,85 @@ class ReportWrapper:
1445
1809
  Example for web url: file_path = 'https://raw.githubusercontent.com/PowerBiDevCamp/FabricUserApiDemo/main/FabricUserApiDemo/DefinitionTemplates/Shared/Reports/StaticResources/SharedResources/BaseThemes/CY23SU08.json'
1446
1810
  """
1447
1811
 
1448
- report_path = "definition/report.json"
1449
- theme_version = "5.5.4"
1450
- request_body = {"definition": {"parts": []}}
1812
+ self._ensure_pbir()
1813
+ theme_version = "5.6.4"
1451
1814
 
1815
+ # Open file
1452
1816
  if not theme_file_path.endswith(".json"):
1453
1817
  raise ValueError(
1454
1818
  f"{icons.red_dot} The '{theme_file_path}' theme file path must be a .json file."
1455
1819
  )
1456
1820
  elif theme_file_path.startswith("https://"):
1457
1821
  response = requests.get(theme_file_path)
1458
- json_file = response.json()
1459
- elif theme_file_path.startswith("/lakehouse"):
1822
+ theme_file = response.json()
1823
+ elif theme_file_path.startswith("/lakehouse") or theme_file_path.startswith(
1824
+ "/synfs/"
1825
+ ):
1460
1826
  with open(theme_file_path, "r", encoding="utf-8-sig") as file:
1461
- json_file = json.load(file)
1827
+ theme_file = json.load(file)
1462
1828
  else:
1463
1829
  ValueError(
1464
1830
  f"{icons.red_dot} Incorrect theme file path value '{theme_file_path}'."
1465
1831
  )
1466
1832
 
1467
- theme_name = json_file["name"]
1833
+ theme_name = theme_file.get("name")
1468
1834
  theme_name_full = f"{theme_name}.json"
1469
- rd = self.rdef
1470
1835
 
1471
- # Add theme.json file to request_body
1472
- file_payload = _conv_b64(json_file)
1473
- filePath = f"StaticResources/RegisteredResources/{theme_name_full}"
1836
+ # Add theme.json file
1837
+ self.add(
1838
+ file_path=f"StaticResources/RegisteredResources/{theme_name_full}",
1839
+ payload=theme_file,
1840
+ )
1841
+
1842
+ custom_theme = {
1843
+ "name": theme_name_full,
1844
+ "reportVersionAtImport": theme_version,
1845
+ "type": "RegisteredResources",
1846
+ }
1474
1847
 
1475
- _add_part(request_body, filePath, file_payload)
1848
+ self.set_json(
1849
+ file_path=self._report_file_path,
1850
+ json_path="$.themeCollection.customTheme",
1851
+ json_value=custom_theme,
1852
+ )
1476
1853
 
1477
- new_theme = {
1854
+ # Update
1855
+ report_file = self.get(
1856
+ file_path=self._report_file_path, json_path="$.resourcePackages"
1857
+ )
1858
+ new_item = {
1478
1859
  "name": theme_name_full,
1479
1860
  "path": theme_name_full,
1480
1861
  "type": "CustomTheme",
1481
1862
  }
1863
+ # Find or create RegisteredResources
1864
+ registered = next(
1865
+ (res for res in report_file if res["name"] == "RegisteredResources"), None
1866
+ )
1482
1867
 
1483
- for _, r in rd.iterrows():
1484
- path = r["path"]
1485
- payload = r["payload"]
1486
- if path == filePath:
1487
- pass
1488
- elif path != report_path:
1489
- _add_part(request_body, path, payload)
1490
- # Update the report.json file
1491
- else:
1492
- rptFile = base64.b64decode(payload).decode("utf-8")
1493
- rptJson = json.loads(rptFile)
1494
- resource_type = "RegisteredResources"
1495
-
1496
- # Add to theme collection
1497
- if "customTheme" not in rptJson["themeCollection"]:
1498
- rptJson["themeCollection"]["customTheme"] = {
1499
- "name": theme_name_full,
1500
- "reportVersionAtImport": theme_version,
1501
- "type": resource_type,
1502
- }
1503
- else:
1504
- rptJson["themeCollection"]["customTheme"]["name"] = theme_name_full
1505
- rptJson["themeCollection"]["customTheme"]["type"] = resource_type
1506
-
1507
- for package in rptJson["resourcePackages"]:
1508
- package["items"] = [
1509
- item
1510
- for item in package["items"]
1511
- if item["type"] != "CustomTheme"
1512
- ]
1513
-
1514
- if not any(
1515
- package["name"] == resource_type
1516
- for package in rptJson["resourcePackages"]
1517
- ):
1518
- new_registered_resources = {
1519
- "name": resource_type,
1520
- "type": resource_type,
1521
- "items": [new_theme],
1522
- }
1523
- rptJson["resourcePackages"].append(new_registered_resources)
1524
- else:
1525
- names = [
1526
- rp["name"] for rp in rptJson["resourcePackages"][1]["items"]
1527
- ]
1528
-
1529
- if theme_name_full not in names:
1530
- rptJson["resourcePackages"][1]["items"].append(new_theme)
1531
-
1532
- file_payload = _conv_b64(rptJson)
1533
- _add_part(request_body, path, file_payload)
1534
-
1535
- self.update_report(request_body=request_body)
1536
- print(
1537
- f"{icons.green_dot} The '{theme_name}' theme has been set as the theme for the '{self._report}' report within the '{self._workspace_name}' workspace."
1868
+ if not registered:
1869
+ registered = {
1870
+ "name": "RegisteredResources",
1871
+ "type": "RegisteredResources",
1872
+ "items": [new_item],
1873
+ }
1874
+ report_file.append(registered)
1875
+ else:
1876
+ # Check for duplicate by 'name'
1877
+ if all(item["name"] != new_item["name"] for item in registered["items"]):
1878
+ registered["items"].append(new_item)
1879
+
1880
+ self.set_json(
1881
+ file_path=self._report_file_path,
1882
+ json_path="$.resourcePackages",
1883
+ json_value=report_file,
1538
1884
  )
1539
1885
 
1886
+ if not self._readonly:
1887
+ print(
1888
+ f"{icons.green_dot} The '{theme_name}' theme has been set as the theme for the '{self._report_name}' report within the '{self._workspace_name}' workspace."
1889
+ )
1890
+
1540
1891
  def set_active_page(self, page_name: str):
1541
1892
  """
1542
1893
  Sets the active page (first page displayed when opening a report) for a report.
@@ -1546,25 +1897,22 @@ class ReportWrapper:
1546
1897
  page_name : str
1547
1898
  The page name or page display name of the report.
1548
1899
  """
1900
+ self._ensure_pbir()
1549
1901
 
1550
- pages_file = "definition/pages/pages.json"
1551
- page_id, page_display_name, file_path = helper.resolve_page_name(
1552
- self, page_name=page_name
1902
+ (page_id, page_display_name) = self._resolve_page_name_and_display_name(
1903
+ page_name
1553
1904
  )
1554
-
1555
- pagePath = self.rdef[self.rdef["path"] == pages_file]
1556
- payload = pagePath["payload"].iloc[0]
1557
- page_file = _decode_b64(payload)
1558
- json_file = json.loads(page_file)
1559
- json_file["activePageName"] = page_id
1560
- file_payload = _conv_b64(json_file)
1561
-
1562
- self._update_single_file(file_name=pages_file, new_payload=file_payload)
1563
-
1564
- print(
1565
- f"{icons.green_dot} The '{page_display_name}' page has been set as the active page in the '{self._report}' report within the '{self._workspace_name}' workspace."
1905
+ self.set_json(
1906
+ file_path=self._pages_file_path,
1907
+ json_path="$.activePageName",
1908
+ json_value=page_id,
1566
1909
  )
1567
1910
 
1911
+ if not self._readonly:
1912
+ print(
1913
+ f"{icons.green_dot} The '{page_display_name}' page has been set as the active page in the '{self._report_name}' report within the '{self._workspace_name}' workspace."
1914
+ )
1915
+
1568
1916
  def set_page_type(self, page_name: str, page_type: str):
1569
1917
  """
1570
1918
  Changes the page type of a report page.
@@ -1576,43 +1924,110 @@ class ReportWrapper:
1576
1924
  page_type : str
1577
1925
  The page type. Valid page types: 'Tooltip', 'Letter', '4:3', '16:9'.
1578
1926
  """
1927
+ self._ensure_pbir()
1928
+
1929
+ if page_type not in helper.page_types:
1930
+ raise ValueError(
1931
+ f"{icons.red_dot} Invalid page type. Valid options: {helper.page_types}."
1932
+ )
1933
+
1934
+ letter_key = next(
1935
+ (
1936
+ key
1937
+ for key, value in helper.page_type_mapping.items()
1938
+ if value == page_type
1939
+ ),
1940
+ None,
1941
+ )
1942
+ if letter_key:
1943
+ width, height = letter_key
1944
+ else:
1945
+ raise ValueError(
1946
+ f"{icons.red_dot} Invalid page_type parameter. Valid options: ['Tooltip', 'Letter', '4:3', '16:9']."
1947
+ )
1948
+
1949
+ (file_path, page_id, page_display_name) = (
1950
+ self.__resolve_page_name_and_display_name_file_path(page_name)
1951
+ )
1952
+
1953
+ self.set_json(file_path=file_path, json_path="$.width", json_value=width)
1954
+ self.set_json(file_path=file_path, json_path="$.height", json_value=height)
1955
+
1956
+ if not self._readonly:
1957
+ print(
1958
+ f"{icons.green_dot} The '{page_display_name}' page has been updated to the '{page_type}' page type."
1959
+ )
1960
+
1961
+ # def set_page_vertical_alignment(self, page: str, vertical_alignment: Literal["Top", "Middle"] = "Top"):
1962
+
1963
+ def set_page_visibility(self, page_name: str, hidden: bool):
1964
+ """
1965
+ Sets whether a report page is visible or hidden.
1966
+
1967
+ Parameters
1968
+ ----------
1969
+ page_name : str
1970
+ The page name or page display name of the report.
1971
+ hidden : bool
1972
+ If set to True, hides the report page.
1973
+ If set to False, makes the report page visible.
1974
+ """
1975
+ self._ensure_pbir()
1976
+ (file_path, page_id, page_display_name) = (
1977
+ self.__resolve_page_name_and_display_name_file_path(page_name)
1978
+ )
1979
+
1980
+ if hidden:
1981
+ self.set_json(
1982
+ file_path=file_path,
1983
+ json_path="$.visibility",
1984
+ json_value="HiddenInViewMode",
1985
+ )
1986
+ else:
1987
+ self.remove(file_path=file_path, json_path="$.visibility", verbose=False)
1988
+
1989
+ visibility = "visible" if hidden is False else "hidden"
1990
+
1991
+ if not self._readonly:
1992
+ print(
1993
+ f"{icons.green_dot} The '{page_display_name}' page has been set to '{visibility}' in the '{self._report_name}' report within the '{self._workspace_name}' workspace."
1994
+ )
1995
+
1996
+ def hide_tooltip_drillthrough_pages(self):
1997
+ """
1998
+ Hides all tooltip pages and drillthrough pages in a report.
1999
+ """
2000
+
2001
+ dfP = self.list_pages()
2002
+ dfP_filt = dfP[
2003
+ (dfP["Type"] == "Tooltip") | (dfP["Drillthrough Target Page"] == True)
2004
+ ]
2005
+
2006
+ if dfP_filt.empty:
2007
+ print(
2008
+ f"{icons.green_dot} There are no Tooltip or Drillthrough pages in the '{self._report_name}' report within the '{self._workspace_name}' workspace."
2009
+ )
2010
+ return
2011
+
2012
+ for _, r in dfP_filt.iterrows():
2013
+ page_name = r["Page Name"]
2014
+ self.set_page_visibility(page_name=page_name, hidden=True)
1579
2015
 
1580
- if page_type not in helper.page_types:
1581
- raise ValueError(
1582
- f"{icons.red_dot} Invalid page type. Valid options: {helper.page_types}."
1583
- )
2016
+ def disable_show_items_with_no_data(self):
2017
+ """
2018
+ 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.
2019
+ """
1584
2020
 
1585
- letter_key = next(
1586
- (
1587
- key
1588
- for key, value in helper.page_type_mapping.items()
1589
- if value == page_type
1590
- ),
1591
- None,
2021
+ self.remove(
2022
+ file_path="definition/pages/*/visual.json",
2023
+ json_path="$..showAll",
2024
+ verbose=False,
1592
2025
  )
1593
- if letter_key:
1594
- width, height = letter_key
1595
- else:
1596
- raise ValueError(
1597
- f"{icons.red_dot} Invalid page_type parameter. Valid options: ['Tooltip', 'Letter', '4:3', '16:9']."
1598
- )
1599
2026
 
1600
- page_id, page_display_name, file_path = helper.resolve_page_name(
1601
- self, page_name=page_name
1602
- )
1603
- rd_filt = self.rdef[self.rdef["path"] == file_path]
1604
- payload = rd_filt["payload"].iloc[0]
1605
- page_file = _decode_b64(payload)
1606
- json_file = json.loads(page_file)
1607
- json_file["width"] = width
1608
- json_file["height"] = height
1609
- file_payload = _conv_b64(json_file)
1610
-
1611
- self._update_single_file(file_name=file_path, new_payload=file_payload)
1612
-
1613
- print(
1614
- f"{icons.green_dot} The '{page_display_name}' page has been updated to the '{page_type}' page type."
1615
- )
2027
+ if not self._readonly:
2028
+ print(
2029
+ f"{icons.green_dot} Show items with data has been disabled for all visuals in the '{self._report_name}' report within the '{self._workspace_name}' workspace."
2030
+ )
1616
2031
 
1617
2032
  def remove_unnecessary_custom_visuals(self):
1618
2033
  """
@@ -1620,46 +2035,33 @@ class ReportWrapper:
1620
2035
  """
1621
2036
 
1622
2037
  dfCV = self.list_custom_visuals()
1623
- dfV = self.list_visuals()
1624
- rd = self.rdef
1625
- cv_remove = []
1626
- cv_remove_display = []
1627
- request_body = {"definition": {"parts": []}}
1628
-
1629
- for _, r in dfCV.iterrows():
1630
- cv = r["Custom Visual Name"]
1631
- cv_display = r["Custom Visual Display Name"]
1632
- dfV_filt = dfV[dfV["Type"] == cv]
1633
- if len(dfV_filt) == 0:
1634
- cv_remove.append(cv) # Add to the list for removal
1635
- cv_remove_display.append(cv_display)
1636
- if len(cv_remove) == 0:
2038
+ df = dfCV[dfCV["Used in Report"] == False]
2039
+
2040
+ if not df.empty:
2041
+ cv_remove = df["Custom Visual Name"].values()
2042
+ cv_remove_display = df["Custom Visual Display Name"].values()
2043
+ else:
1637
2044
  print(
1638
- f"{icons.green_dot} There are no unnecessary custom visuals in the '{self._report}' report within the '{self._workspace_name}' workspace."
2045
+ f"{icons.red_dot} There are no unnecessary custom visuals in the '{self._report_name}' report within the '{self._workspace_name}' workspace."
1639
2046
  )
1640
2047
  return
1641
2048
 
1642
- for _, r in rd.iterrows():
1643
- file_path = r["path"]
1644
- payload = r["payload"]
1645
- if file_path == "definition/report.json":
1646
- rpt_file = base64.b64decode(payload).decode("utf-8")
1647
- rpt_json = json.loads(rpt_file)
1648
- rpt_json["publicCustomVisuals"] = [
1649
- item
1650
- for item in rpt_json["publicCustomVisuals"]
1651
- if item not in cv_remove
1652
- ]
1653
-
1654
- payload = _conv_b64(rpt_json)
1655
-
1656
- _add_part(request_body, file_path, payload)
1657
-
1658
- self.update_report(request_body=request_body)
1659
- print(
1660
- f"{icons.green_dot} The {cv_remove_display} custom visuals have been removed from the '{self._report}' report within the '{self._workspace_name}' workspace."
2049
+ json_path = "$.publicCustomVisuals"
2050
+ custom_visuals = self.get(file_path=self._report_file_path, json_path=json_path)
2051
+ updated_custom_visuals = [
2052
+ item for item in custom_visuals if item not in cv_remove
2053
+ ]
2054
+ self.set_json(
2055
+ file_path=self._report_path,
2056
+ json_path=json_path,
2057
+ json_value=updated_custom_visuals,
1661
2058
  )
1662
2059
 
2060
+ if not self._readonly:
2061
+ print(
2062
+ f"{icons.green_dot} The {cv_remove_display} custom visuals have been removed from the '{self._report_name}' report within the '{self._workspace_name}' workspace."
2063
+ )
2064
+
1663
2065
  def migrate_report_level_measures(self, measures: Optional[str | List[str]] = None):
1664
2066
  """
1665
2067
  Moves all report-level measures from the report to the semantic model on which the report is based.
@@ -1670,555 +2072,883 @@ class ReportWrapper:
1670
2072
  A measure or list of measures to move to the semantic model.
1671
2073
  Defaults to None which resolves to moving all report-level measures to the semantic model.
1672
2074
  """
2075
+ self._ensure_pbir()
1673
2076
 
1674
2077
  from sempy_labs.tom import connect_semantic_model
1675
2078
 
1676
2079
  rlm = self.list_report_level_measures()
1677
- if len(rlm) == 0:
2080
+ if rlm.empty:
1678
2081
  print(
1679
- f"{icons.green_dot} The '{self._report}' report within the '{self._workspace_name}' workspace has no report-level measures."
2082
+ f"{icons.info} The '{self._report_name}' report within the '{self._workspace_name}' workspace has no report-level measures."
1680
2083
  )
1681
2084
  return
1682
2085
 
1683
2086
  dataset_id, dataset_name, dataset_workspace_id, dataset_workspace_name = (
1684
2087
  resolve_dataset_from_report(
1685
- report=self._report, workspace=self._workspace_id
2088
+ report=self._report_id, workspace=self._workspace_id
1686
2089
  )
1687
2090
  )
1688
2091
 
1689
2092
  if isinstance(measures, str):
1690
2093
  measures = [measures]
1691
2094
 
1692
- request_body = {"definition": {"parts": []}}
1693
- rpt_file = "definition/reportExtensions.json"
1694
-
1695
- rd = self.rdef
1696
- rd_filt = rd[rd["path"] == rpt_file]
1697
- payload = rd_filt["payload"].iloc[0]
1698
- extFile = base64.b64decode(payload).decode("utf-8")
1699
- extJson = json.loads(extFile)
2095
+ file = self.get(file_path=self._report_extensions_path)
1700
2096
 
1701
2097
  mCount = 0
1702
2098
  with connect_semantic_model(
1703
2099
  dataset=dataset_id, readonly=False, workspace=dataset_workspace_id
1704
2100
  ) as tom:
2101
+ existing_measures = [m.Name for m in tom.all_measures()]
1705
2102
  for _, r in rlm.iterrows():
1706
- tableName = r["Table Name"]
1707
- mName = r["Measure Name"]
1708
- mExpr = r["Expression"]
2103
+ table_name = r["Table Name"]
2104
+ measure_name = r["Measure Name"]
2105
+ expr = r["Expression"]
1709
2106
  # mDataType = r["Data Type"]
1710
- mformatString = r["Format String"]
2107
+ format_string = r["Format String"]
1711
2108
  # Add measures to the model
1712
- if mName in measures or measures is None:
2109
+ if (
2110
+ measure_name in measures or measures is None
2111
+ ) and measure_name not in existing_measures:
1713
2112
  tom.add_measure(
1714
- table_name=tableName,
1715
- measure_name=mName,
1716
- expression=mExpr,
1717
- format_string=mformatString,
2113
+ table_name=table_name,
2114
+ measure_name=measure_name,
2115
+ expression=expr,
2116
+ format_string=format_string,
1718
2117
  )
1719
2118
  tom.set_annotation(
1720
- object=tom.model.Tables[tableName].Measures[mName],
2119
+ object=tom.model.Tables[table_name].Measures[measure_name],
1721
2120
  name="semanticlinklabs",
1722
2121
  value="reportlevelmeasure",
1723
2122
  )
1724
2123
  mCount += 1
1725
2124
  # Remove measures from the json
1726
2125
  if measures is not None and len(measures) < mCount:
1727
- for e in extJson["entities"]:
2126
+ for e in file["entities"]:
1728
2127
  e["measures"] = [
1729
2128
  measure
1730
2129
  for measure in e["measures"]
1731
2130
  if measure["name"] not in measures
1732
2131
  ]
1733
- extJson["entities"] = [
1734
- entity for entity in extJson["entities"] if entity["measures"]
2132
+ file["entities"] = [
2133
+ entity for entity in file["entities"] if entity["measures"]
1735
2134
  ]
1736
- file_payload = _conv_b64(extJson)
1737
- _add_part(request_body, rpt_file, file_payload)
1738
-
1739
- # Add unchanged payloads
1740
- for _, r in rd.iterrows():
1741
- path = r["path"]
1742
- payload = r["payload"]
1743
- if path != rpt_file:
1744
- _add_part(request_body, path, payload)
1745
-
1746
- self.update_report(request_body=request_body)
1747
- print(
1748
- f"{icons.green_dot} The report-level measures have been migrated to the '{dataset_name}' semantic model within the '{dataset_workspace_name}' workspace."
1749
- )
2135
+ self.update(file_path=self._report_extensions_path, payload=file)
2136
+ # what about if measures is None?
1750
2137
 
1751
- def set_page_visibility(self, page_name: str, hidden: bool):
2138
+ if not self._readonly:
2139
+ print(
2140
+ f"{icons.green_dot} The report-level measures have been migrated to the '{dataset_name}' semantic model within the '{dataset_workspace_name}' workspace."
2141
+ )
2142
+
2143
+ # In progress...
2144
+ def _list_annotations(self) -> pd.DataFrame:
1752
2145
  """
1753
- Sets whether a report page is visible or hidden.
2146
+ Shows a list of annotations in the report.
1754
2147
 
1755
- Parameters
1756
- ----------
1757
- page_name : str
1758
- The page name or page display name of the report.
1759
- hidden : bool
1760
- If set to True, hides the report page.
1761
- If set to False, makes the report page visible.
2148
+ Returns
2149
+ -------
2150
+ pandas.DataFrame
2151
+ A pandas dataframe showing a list of report, page and visual annotations in the report.
1762
2152
  """
1763
2153
 
1764
- page_id, page_display_name, file_path = helper.resolve_page_name(
1765
- self, page_name=page_name
1766
- )
1767
- visibility = "visible" if hidden is False else "hidden"
2154
+ columns = {
2155
+ "Type": "str",
2156
+ "Object Name": "str",
2157
+ "Annotation Name": "str",
2158
+ "Annotation Value": "str",
2159
+ }
2160
+ df = _create_dataframe(columns=columns)
1768
2161
 
1769
- rd_filt = self.rdef[self.rdef["path"] == file_path]
1770
- payload = rd_filt["payload"].iloc[0]
1771
- obj_file = _decode_b64(payload)
1772
- obj_json = json.loads(obj_file)
1773
- if hidden:
1774
- obj_json["visibility"] = "HiddenInViewMode"
1775
- else:
1776
- if "visibility" in obj_json:
1777
- del obj_json["visibility"]
1778
- new_payload = _conv_b64(obj_json)
2162
+ visual_mapping = self._visual_page_mapping()
2163
+ report_file = self.get(file_path="definition/report.json")
2164
+
2165
+ dfs = []
2166
+ if "annotations" in report_file:
2167
+ for ann in report_file["annotations"]:
2168
+ new_data = {
2169
+ "Type": "Report",
2170
+ "Object Name": self._report_name,
2171
+ "Annotation Name": ann.get("name"),
2172
+ "Annotation Value": ann.get("value"),
2173
+ }
2174
+ dfs.append(pd.DataFrame(new_data, index=[0]))
2175
+
2176
+ for p in self.__all_pages():
2177
+ path = p.get("path")
2178
+ payload = p.get("payload")
2179
+ page_name = payload.get("displayName")
2180
+ if "annotations" in payload:
2181
+ for ann in payload["annotations"]:
2182
+ new_data = {
2183
+ "Type": "Page",
2184
+ "Object Name": page_name,
2185
+ "Annotation Name": ann.get("name"),
2186
+ "Annotation Value": ann.get("value"),
2187
+ }
2188
+ dfs.append(pd.DataFrame(new_data, index=[0]))
2189
+
2190
+ for v in self.__all_visuals():
2191
+ path = v.get("path")
2192
+ payload = v.get("payload")
2193
+ page_display = visual_mapping.get(path)[1]
2194
+ visual_name = payload.get("name")
2195
+ if "annotations" in payload:
2196
+ for ann in payload["annotations"]:
2197
+ new_data = {
2198
+ "Type": "Visual",
2199
+ "Object Name": f"'{page_display}'[{visual_name}]",
2200
+ "Annotation Name": ann.get("name"),
2201
+ "Annotation Value": ann.get("value"),
2202
+ }
2203
+ dfs.append(pd.DataFrame(new_data, index=[0]))
1779
2204
 
1780
- self._update_single_file(file_name=file_path, new_payload=new_payload)
2205
+ if dfs:
2206
+ df = pd.concat(dfs, ignore_index=True)
1781
2207
 
1782
- print(
1783
- f"{icons.green_dot} The '{page_display_name}' page has been set to {visibility}."
1784
- )
2208
+ return df
1785
2209
 
1786
- def hide_tooltip_drillthrough_pages(self):
2210
+ def _add_image(self, image_path: str, resource_name: Optional[str] = None) -> str:
1787
2211
  """
1788
- Hides all tooltip pages and drillthrough pages in a report.
2212
+ Add an image to the report definition. The image will be added to the StaticResources/RegisteredResources folder in the report definition. If the image_name already exists as a file in the report definition it will be updated.
2213
+
2214
+ Parameters
2215
+ ----------
2216
+ image_path : str
2217
+ The path of the image file to be added. For example: "./builtin/MyImage.png".
2218
+ resource_name : str, default=None
2219
+ The name of the image file to be added. For example: "MyImage.png". If not specified, the name will be derived from the image path and a unique ID will be appended to it.
2220
+
2221
+ Returns
2222
+ -------
2223
+ str
2224
+ The name of the image file added to the report definition.
1789
2225
  """
2226
+ self._ensure_pbir()
1790
2227
 
1791
- dfP = self.list_pages()
1792
- dfP_filt = dfP[
1793
- (dfP["Type"] == "Tooltip") | (dfP["Drillthrough Target Page"] == True)
1794
- ]
2228
+ id = generate_number_guid()
1795
2229
 
1796
- if len(dfP_filt) == 0:
1797
- print(
1798
- f"{icons.green_dot} There are no Tooltip or Drillthrough pages in the '{self._report}' report within the '{self._workspace_name}' workspace."
2230
+ if image_path.startswith("http://") or image_path.startswith("https://"):
2231
+ response = requests.get(image_path)
2232
+ response.raise_for_status()
2233
+ image_bytes = response.content
2234
+ # Extract the suffix (extension) from the URL path
2235
+ suffix = Path(urlparse(image_path).path).suffix
2236
+ else:
2237
+ with open(image_path, "rb") as image_file:
2238
+ image_bytes = image_file.read()
2239
+ suffix = Path(image_path).suffix
2240
+
2241
+ payload = base64.b64encode(image_bytes).decode("utf-8")
2242
+ if resource_name is None:
2243
+ resource_name = os.path.splitext(os.path.basename(image_path))[0]
2244
+ file_name = f"{resource_name}{id}{suffix}"
2245
+ else:
2246
+ file_name = resource_name
2247
+ file_path = f"StaticResources/RegisteredResources/{file_name}"
2248
+
2249
+ # Add StaticResources/RegisteredResources file. If the file already exists, update it.
2250
+ try:
2251
+ self.get(file_path=file_path)
2252
+ self.update(file_path=file_path, payload=payload)
2253
+ except Exception:
2254
+ self.add(
2255
+ file_path=file_path,
2256
+ payload=payload,
1799
2257
  )
1800
- return
1801
2258
 
1802
- for _, r in dfP_filt.iterrows():
1803
- page_name = r["Page Name"]
1804
- self.set_page_visibility(page_name=page_name, hidden=True)
2259
+ # Add to report.json file
2260
+ self.__add_to_registered_resources(
2261
+ name=file_name,
2262
+ path=file_name,
2263
+ type="Image",
2264
+ )
1805
2265
 
1806
- def disable_show_items_with_no_data(self):
1807
- """
1808
- 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.
1809
- """
2266
+ return file_name
1810
2267
 
1811
- request_body = {"definition": {"parts": []}}
1812
-
1813
- def delete_key_in_json(obj, key_to_delete):
1814
- if isinstance(obj, dict):
1815
- if key_to_delete in obj:
1816
- del obj[key_to_delete]
1817
- for key, value in obj.items():
1818
- delete_key_in_json(value, key_to_delete)
1819
- elif isinstance(obj, list):
1820
- for item in obj:
1821
- delete_key_in_json(item, key_to_delete)
1822
-
1823
- rd = self.rdef
1824
- for _, r in rd.iterrows():
1825
- file_path = r["path"]
1826
- payload = r["payload"]
1827
- if file_path.endswith("/visual.json"):
1828
- objFile = base64.b64decode(payload).decode("utf-8")
1829
- objJson = json.loads(objFile)
1830
- delete_key_in_json(objJson, "showAll")
1831
- _add_part(request_body, file_path, _conv_b64(objJson))
1832
- else:
1833
- _add_part(request_body, file_path, payload)
2268
+ def _remove_wallpaper(self, page: Optional[str | List[str]] = None):
2269
+ """
2270
+ Remove the wallpaper image from a page.
1834
2271
 
1835
- self.update_report(request_body=request_body)
1836
- print(
1837
- f"{icons.green_dot} Show items with data has been disabled for all visuals in the '{self._report}' report within the '{self._workspace_name}' workspace."
1838
- )
2272
+ Parameters
2273
+ ----------
2274
+ page : str | List[str], default=None
2275
+ The name or display name of the page(s) from which the wallpaper image will be removed.
2276
+ If None, removes from all pages.
2277
+ """
2278
+ self._ensure_pbir()
1839
2279
 
1840
- # Set Annotations
1841
- def __set_annotation(self, json_file: dict, name: str, value: str) -> dict:
2280
+ if isinstance(page, str):
2281
+ page = [page]
1842
2282
 
1843
- if "annotations" in json_file:
1844
- if any(
1845
- annotation["name"] == name for annotation in json_file["annotations"]
1846
- ):
1847
- for annotation in json_file["annotations"]:
1848
- if annotation["name"] == name:
1849
- annotation["value"] = value
1850
- break
1851
- else:
1852
- json_file["annotations"].append({"name": name, "value": value})
2283
+ page_list = []
2284
+ if page:
2285
+ for p in page:
2286
+ page_id = self.resolve_page_name(p)
2287
+ page_list.append(page_id)
1853
2288
  else:
1854
- json_file["annotations"] = []
1855
- json_file["annotations"].append({"name": name, "value": value})
2289
+ page_list = [
2290
+ p.get("payload", {}).get("name")
2291
+ for p in self.__all_pages()
2292
+ if p.get("payload") and "name" in p["payload"]
2293
+ ]
1856
2294
 
1857
- return json_file
2295
+ for p in self.__all_pages():
2296
+ path = p.get("path")
2297
+ payload = p.get("payload")
2298
+ page_name = payload.get("name")
2299
+ page_display_name = payload.get("displayName")
2300
+ if page_name in page_list:
2301
+ self.remove(file_path=path, json_path="$.objects.outspace")
2302
+ print(
2303
+ f"{icons.green_dot} The wallpaper has been removed from the '{page_display_name}' page."
2304
+ )
1858
2305
 
1859
- def _set_annotation(
2306
+ def _set_wallpaper_color(
1860
2307
  self,
1861
- annotation_name: str,
1862
- annotation_value: str,
1863
- page_name: Optional[str] = None,
1864
- visual_name: Optional[str] = None,
2308
+ color_value: str,
2309
+ page: Optional[str | List[str]] = None,
2310
+ transparency: int = 0,
2311
+ theme_color_percent: float = 0.0,
1865
2312
  ):
1866
2313
  """
1867
- Sets an annotation on the report/page/visual. If the annotation already exists, the annotation value is updated.
1868
- In order to set a report annotation, leave page_name=None, visual_name=None.
1869
- In order to set a page annotation, leave visual_annotation=None.
1870
- In order to set a visual annotation, set all parameters.
2314
+ Set the wallpaper color of a page (or pages).
1871
2315
 
1872
2316
  Parameters
1873
2317
  ----------
1874
- annotation_name : str
1875
- Name of the annotation.
1876
- annotation_value : str
1877
- Value of the annotation.
1878
- page_name : str, default=None
1879
- The page name or page display name.
1880
- Set this annotation when setting an annotation on a page or visual.
1881
- visual_name : str, default=None
1882
- The visual name.
1883
- Set this property when setting an annotation on a visual.
1884
- """
1885
-
1886
- if page_name is None and visual_name is None:
1887
- file_path = "definition/report.json"
1888
- elif page_name is not None and visual_name is None:
1889
- page_id, page_display, file_path = helper.resolve_page_name(
1890
- self, page_name=page_name
1891
- )
1892
- elif page_name is not None and visual_name is not None:
1893
- page_name, page_display_name, visual_name, file_path = (
1894
- helper.resolve_visual_name(
1895
- self, page_name=page_name, visual_name=visual_name
1896
- )
1897
- )
1898
- else:
2318
+ color_value : str
2319
+ The color value to be set. This can be a hex color code (e.g., "#FF5733") or an integer based on the theme color.
2320
+ page : str | List[str], default=None
2321
+ The name or display name of the page(s) to which the wallpaper color will be applied.
2322
+ If None, applies to all pages.
2323
+ transparency : int, default=0
2324
+ The transparency level of the wallpaper color. Valid values are between 0 and 100.
2325
+ theme_color_percent : float, default=0.0
2326
+ The percentage of the theme color to be applied. Valid values are between -0.6 and 0.6.
2327
+ """
2328
+ self._ensure_pbir()
2329
+
2330
+ if transparency < 0 or transparency > 100:
2331
+ raise ValueError(f"{icons.red_dot} Transparency must be between 0 and 100.")
2332
+
2333
+ if theme_color_percent < -0.6 or theme_color_percent > 0.6:
1899
2334
  raise ValueError(
1900
- f"{icons.red_dot} Invalid parameters. If specifying a visual_name you must specify the page_name."
2335
+ f"{icons.red_dot} Theme color percentage must be between -0.6 and 0.6."
1901
2336
  )
1902
2337
 
1903
- payload = self.rdef[self.rdef["path"] == file_path]["payload"].iloc[0]
1904
- file = _decode_b64(payload)
1905
- json_file = json.loads(file)
1906
-
1907
- new_file = self.__set_annotation(
1908
- json_file, name=annotation_name, value=annotation_value
1909
- )
1910
- new_payload = _conv_b64(new_file)
2338
+ page_list = self.__resolve_page_list(page)
1911
2339
 
1912
- self._update_single_file(file_name=file_path, new_payload=new_payload)
2340
+ # Define the color dictionary based on color_value type
2341
+ if isinstance(color_value, int):
2342
+ color_expr = {
2343
+ "ThemeDataColor": {
2344
+ "ColorId": color_value,
2345
+ "Percent": theme_color_percent,
2346
+ }
2347
+ }
2348
+ elif isinstance(color_value, str) and color_value.startswith("#"):
2349
+ color_expr = {"Literal": {"Value": f"'{color_value}'"}}
2350
+ else:
2351
+ raise NotImplementedError(
2352
+ f"{icons.red_dot} The color value '{color_value}' is not supported. Please provide a hex color code or an integer based on the color theme."
2353
+ )
1913
2354
 
1914
- # Remove Annotations
1915
- def __remove_annotation(self, json_file: dict, name: str) -> dict:
2355
+ color_dict = ({"solid": {"color": {"expr": color_expr}}},)
2356
+ transparency_dict = {"expr": {"Literal": {"Value": f"{transparency}D"}}}
1916
2357
 
1917
- if "annotations" in json_file:
1918
- json_file["annotations"] = [
1919
- annotation
1920
- for annotation in json_file["annotations"]
1921
- if annotation["name"] != name
1922
- ]
2358
+ for p in self.__all_pages():
2359
+ path = p.get("path")
2360
+ payload = p.get("payload", {})
2361
+ page_name = payload.get("name")
1923
2362
 
1924
- return json_file
2363
+ if page_name in page_list:
2364
+ self.set_json(
2365
+ file_path=path,
2366
+ json_path="$.objects.outspace[*].properties.color",
2367
+ json_value=color_dict,
2368
+ )
2369
+ self.set_json(
2370
+ file_path=path,
2371
+ json_path="$.objects.outspace[*].properties.transparency",
2372
+ json_value=transparency_dict,
2373
+ )
1925
2374
 
1926
- def _remove_annotation(
2375
+ def _set_wallpaper_image(
1927
2376
  self,
1928
- annotation_name: str,
1929
- page_name: Optional[str] = None,
1930
- visual_name: Optional[str] = None,
2377
+ image_path: str,
2378
+ page: Optional[str | List[str]] = None,
2379
+ transparency: int = 0,
2380
+ image_fit: Literal["Normal", "Fit", "Fill"] = "Normal",
1931
2381
  ):
1932
2382
  """
1933
- Removes an annotation on the report/page/visual.
1934
- In order to remove a report annotation, leave page_name=None, visual_name=None.
1935
- In order to remove a page annotation, leave visual_annotation=None.
1936
- In order to remove a visual annotation, set all parameters.
2383
+ Add an image as the wallpaper of a page.
1937
2384
 
1938
2385
  Parameters
1939
2386
  ----------
1940
- annotation_name : str
1941
- Name of the annotation.
1942
- page_name : str, default=None
1943
- The page name or page display name.
1944
- Set this annotation when setting an annotation on a page or visual.
1945
- visual_name : str, default=None
1946
- The visual name.
1947
- Set this property when setting an annotation on a visual.
1948
- """
1949
-
1950
- if page_name is None and visual_name is None:
1951
- file_path = "definition/report.json"
1952
- elif page_name is not None and visual_name is None:
1953
- page_id, page_display, file_path = helper.resolve_page_name(
1954
- self, page_name=page_name
1955
- )
1956
- elif page_name is not None and visual_name is not None:
1957
- page_name, page_display_name, visual_name, file_path = (
1958
- helper.resolve_visual_name(
1959
- self, page_name=page_name, visual_name=visual_name
1960
- )
1961
- )
1962
- else:
2387
+ image_path : str
2388
+ The path of the image file to be added. For example: "./builtin/MyImage.png".
2389
+ page : str | List[str], default=None
2390
+ The name or display name of the page(s) to which the wallpaper image will be applied.
2391
+ If None, applies to all pages.
2392
+ transparency : int, default=0
2393
+ The transparency level of the wallpaper image. Valid values are between 0 and 100.
2394
+ image_fit : str, default="Normal"
2395
+ The fit type of the wallpaper image. Valid options: "Normal", "Fit", "Fill".
2396
+ """
2397
+ self._ensure_pbir()
2398
+
2399
+ image_fits = ["Normal", "Fit", "Fill"]
2400
+ image_fit = image_fit.capitalize()
2401
+ if image_fit not in image_fits:
1963
2402
  raise ValueError(
1964
- f"{icons.red_dot} Invalid parameters. If specifying a visual_name you must specify the page_name."
2403
+ f"{icons.red_dot} Invalid image fit. Valid options: {image_fits}."
1965
2404
  )
2405
+ if transparency < 0 or transparency > 100:
2406
+ raise ValueError(f"{icons.red_dot} Transparency must be between 0 and 100.")
2407
+
2408
+ page_list = self.__resolve_page_list(page)
2409
+
2410
+ image_name = os.path.splitext(os.path.basename(image_path))[0]
2411
+ image_file_path = self._add_image(image_path=image_path, image_name=image_name)
2412
+
2413
+ image_dict = {
2414
+ "image": {
2415
+ "name": {"expr": {"Literal": {"Value": f"'{image_file_path}'"}}},
2416
+ "url": {
2417
+ "expr": {
2418
+ "ResourcePackageItem": {
2419
+ "PackageName": "RegisteredResources",
2420
+ "PackageType": 1,
2421
+ "ItemName": image_file_path,
2422
+ }
2423
+ }
2424
+ },
2425
+ "scaling": {"expr": {"Literal": {"Value": f"'{image_fit}'"}}},
2426
+ }
2427
+ }
2428
+ transparency_dict = {"expr": {"Literal": {"Value": f"{transparency}D"}}}
2429
+
2430
+ for p in self.__all_pages():
2431
+ path = p.get("path")
2432
+ payload = p.get("payload")
2433
+ page_name = payload.get("name")
2434
+ if page_name in page_list:
2435
+ self.set_json(
2436
+ file_path=path,
2437
+ json_path="$.objects.outspace[*].properties.image",
2438
+ json_value=image_dict,
2439
+ )
2440
+ self.set_json(
2441
+ file_path=path,
2442
+ json_path="$.objects.outspace[*].properties.transparency",
2443
+ json_value=transparency_dict,
2444
+ )
2445
+
2446
+ def _add_blank_page(
2447
+ self,
2448
+ name: str,
2449
+ width: int = 1280,
2450
+ height: int = 720,
2451
+ display_option: str = "FitToPage",
2452
+ ):
2453
+ self._ensure_pbir()
2454
+
2455
+ page_id = generate_hex()
2456
+ payload = {
2457
+ "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/page/1.4.0/schema.json",
2458
+ "name": page_id,
2459
+ "displayName": name,
2460
+ "displayOption": display_option,
2461
+ "height": height,
2462
+ "width": width,
2463
+ }
2464
+ self.add(file_path=f"definition/pages/{page_id}/page.json", payload=payload)
1966
2465
 
1967
- payload = self.rdef[self.rdef["path"] == file_path]["payload"].iloc[0]
1968
- file = _decode_b64(payload)
1969
- json_file = json.loads(file)
2466
+ # Add the page to the pages.json file
2467
+ pages_file = self.get(file_path=self._pages_file_path)
2468
+ pages_file["pageOrder"].append(page_id)
2469
+
2470
+ def _add_page(self, payload: dict | bytes, generate_id: bool = True):
2471
+ """
2472
+ Add a new page to the report.
1970
2473
 
1971
- new_file = self.__remove_annotation(json_file, name=annotation_name)
1972
- new_payload = _conv_b64(new_file)
2474
+ Parameters
2475
+ ----------
2476
+ payload : dict | bytes
2477
+ The json content of the page to be added. This can be a dictionary or a base64 encoded string.
2478
+ generate_id : bool, default=True
2479
+ Whether to generate a new page ID. If False, the page ID will be taken from the payload.
2480
+ """
2481
+ self._ensure_pbir()
1973
2482
 
1974
- self._update_single_file(file_name=file_path, new_payload=new_payload)
2483
+ page_file = decode_payload(payload)
2484
+ page_file_copy = copy.deepcopy(page_file)
1975
2485
 
1976
- # Get Annotation Value
1977
- def __get_annotation_value(self, json_file: dict, name: str) -> str:
2486
+ if generate_id:
2487
+ # Generate a new page ID and update the page file accordingly
2488
+ page_id = generate_hex()
2489
+ page_file_copy["name"] = page_id
2490
+ else:
2491
+ page_id = page_file_copy.get("name")
1978
2492
 
1979
- if "annotations" in json_file:
1980
- for ann in json_file["annotations"]:
1981
- if ann.get("name") == name:
1982
- return ann.get("value")
2493
+ self.add(
2494
+ file_path=f"definition/pages/{page_id}/page.json", payload=page_file_copy
2495
+ )
1983
2496
 
1984
- def _get_annotation_value(
1985
- self,
1986
- annotation_name: str,
1987
- page_name: Optional[str] = None,
1988
- visual_name: Optional[str] = None,
1989
- ) -> str:
2497
+ def _add_visual(self, page: str, payload: dict | bytes, generate_id: bool = True):
1990
2498
  """
1991
- Retrieves the annotation value of an annotation on the report/page/visual.
1992
- In order to retrieve a report annotation value, leave page_name=None, visual_name=None.
1993
- In order to retrieve a page annotation value, leave visual_annotation=None.
1994
- In order to retrieve a visual annotation value, set all parameters.
2499
+ Add a new visual to a page in the report.
1995
2500
 
1996
2501
  Parameters
1997
2502
  ----------
1998
- annotation_name : str
1999
- Name of the annotation.
2000
- page_name : str, default=None
2001
- The page name or page display name.
2002
- Set this annotation when setting an annotation on a page or visual.
2003
- visual_name : str, default=None
2004
- The visual name.
2005
- Set this property when setting an annotation on a visual.
2503
+ page : str
2504
+ The name or display name of the page to which the visual will be added.
2505
+ payload : dict | bytes
2506
+ The json content of the visual to be added. This can be a dictionary or a base64 encoded string.
2507
+ generate_id : bool, default=True
2508
+ Whether to generate a new visual ID. If False, the visual ID will be taken from the payload.
2509
+ """
2510
+ self._ensure_pbir()
2511
+
2512
+ visual_file = decode_payload(payload)
2513
+ visual_file_copy = copy.deepcopy(visual_file)
2514
+
2515
+ if generate_id:
2516
+ # Generate a new visual ID and update the visual file accordingly
2517
+ visual_id = generate_hex()
2518
+ visual_file_copy["name"] = visual_id
2519
+ else:
2520
+ visual_id = visual_file_copy.get("name")
2521
+ (page_file_path, page_id, page_name) = (
2522
+ self.__resolve_page_name_and_display_name_file_path(page)
2523
+ )
2524
+ visual_file_path = helper.generate_visual_file_path(page_file_path, visual_id)
2006
2525
 
2007
- Returns
2008
- -------
2009
- str
2010
- The annotation value.
2526
+ self.add(file_path=visual_file_path, payload=visual_file_copy)
2527
+
2528
+ def _add_new_visual(
2529
+ self,
2530
+ page: str,
2531
+ type: str,
2532
+ x: int,
2533
+ y: int,
2534
+ height: int = 720,
2535
+ width: int = 1280,
2536
+ ):
2537
+ self._ensure_pbir()
2538
+
2539
+ type = helper.resolve_visual_type(type)
2540
+ visual_id = generate_hex()
2541
+ (page_file_path, page_id, page_name) = (
2542
+ self.__resolve_page_name_and_display_name_file_path(page)
2543
+ )
2544
+ visual_file_path = helper.generate_visual_file_path(page_file_path, visual_id)
2545
+
2546
+ payload = {
2547
+ "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.0.0/schema.json",
2548
+ "name": visual_id,
2549
+ "position": {
2550
+ "x": x,
2551
+ "y": y,
2552
+ "z": 0,
2553
+ "height": height,
2554
+ "width": width,
2555
+ "tabOrder": 0,
2556
+ },
2557
+ "visual": {"visualType": type, "drillFilterOtherVisuals": True},
2558
+ }
2559
+
2560
+ self.add(file_path=visual_file_path, payload=payload)
2561
+
2562
+ def _update_to_theme_colors(self, mapping: dict[str, tuple[int, float]]):
2011
2563
  """
2564
+ Updates the report definition to use theme colors instead of hex colors.
2012
2565
 
2013
- if page_name is None and visual_name is None:
2014
- file_path = "definition/report.json"
2015
- elif page_name is not None and visual_name is None:
2016
- page_id, page_display, file_path = helper.resolve_page_name(
2017
- self, page_name=page_name
2018
- )
2019
- elif page_name is not None and visual_name is not None:
2020
- page_name, page_display_name, visual_name, file_path = (
2021
- helper.resolve_visual_name(
2022
- self, page_name=page_name, visual_name=visual_name
2023
- )
2024
- )
2025
- else:
2026
- raise ValueError(
2027
- f"{icons.red_dot} Invalid parameters. If specifying a visual_name you must specify the page_name."
2028
- )
2566
+ Parameters
2567
+ ----------
2568
+ mapping : dict[str, tuple[int, float]
2569
+ A dictionary mapping color names to their corresponding theme color IDs.
2570
+ Example: {"#FF0000": (1, 0), "#00FF00": (2, 0)}
2571
+ The first value in the tuple is the theme color ID and the second value is the percentage (a value between -0.6 and 0.6).
2572
+ """
2573
+ self._ensure_pbir()
2029
2574
 
2030
- payload = self.rdef[self.rdef["path"] == file_path]["payload"].iloc[0]
2031
- file = _decode_b64(payload)
2032
- json_file = json.loads(file)
2033
-
2034
- return self.__get_annotation_value(json_file, name=annotation_name)
2035
-
2036
- def __adjust_settings(
2037
- self, setting_type: str, setting_name: str, setting_value: bool
2038
- ): # Meta function
2039
-
2040
- valid_setting_types = ["settings", "slowDataSourceSettings"]
2041
- valid_settings = [
2042
- "isPersistentUserStateDisabled",
2043
- "hideVisualContainerHeader",
2044
- "defaultFilterActionIsDataFilter",
2045
- "useStylableVisualContainerHeader",
2046
- "useDefaultAggregateDisplayName",
2047
- "useEnhancedTooltips",
2048
- "allowChangeFilterTypes",
2049
- "disableFilterPaneSearch",
2050
- "useCrossReportDrillthrough",
2051
- ]
2052
- valid_slow_settings = [
2053
- "isCrossHighlightingDisabled",
2054
- "isSlicerSelectionsButtonEnabled",
2055
- ]
2575
+ # Ensure theme color mapping is in the correct format (with Percent value)
2576
+ mapping = {k: (v, 0) if isinstance(v, int) else v for k, v in mapping.items()}
2056
2577
 
2057
- if setting_type not in valid_setting_types:
2058
- raise ValueError(
2059
- f"Invalid setting_type. Valid options: {valid_setting_types}."
2060
- )
2061
- if setting_type == "settings" and setting_name not in valid_settings:
2062
- raise ValueError(
2063
- f"The '{setting_name}' is not a valid setting. Valid options: {valid_settings}."
2578
+ out_of_range = {
2579
+ color: value
2580
+ for color, value in mapping.items()
2581
+ if len(value) > 1 and not (-0.6 <= value[1] <= 0.6)
2582
+ }
2583
+
2584
+ if out_of_range:
2585
+ print(
2586
+ f"{icons.red_dot} The following mapping entries have Percent values out of range [-0.6, 0.6]:"
2064
2587
  )
2065
- if (
2066
- setting_type == "slowDataSourceSettings"
2067
- and setting_name not in valid_slow_settings
2068
- ):
2588
+ for color, val in out_of_range.items():
2589
+ print(f" {color}: Percent = {val[1]}")
2069
2590
  raise ValueError(
2070
- f"The '{setting_name}' is not a valid setting. Valid options: {valid_slow_settings}."
2591
+ f"{icons.red_dot} The Percent values must be between -0.6 and 0.6."
2071
2592
  )
2072
2593
 
2073
- request_body = {"definition": {"parts": []}}
2074
-
2075
- rd = self.rdef
2076
- for _, r in rd.iterrows():
2077
- path = r["path"]
2078
- payload = r["payload"]
2079
- if path == "definition/report.json":
2080
- obj_file = base64.b64decode(payload).decode("utf-8")
2081
- obj_json = json.loads(obj_file)
2082
- if setting_value is False:
2083
- if setting_name in obj_json.get(setting_type, {}):
2084
- del obj_json[setting_type][setting_name]
2085
- else:
2086
- if setting_name not in obj_json.get(setting_type, {}):
2087
- obj_json[setting_type][setting_name] = True
2088
-
2089
- _add_part(request_body, path, _conv_b64(obj_json))
2090
- else:
2091
- _add_part(request_body, path, payload)
2594
+ json_path = "$..color.expr.Literal.Value"
2595
+ jsonpath_expr = parse(json_path)
2596
+
2597
+ for part in [
2598
+ part
2599
+ for part in self._report_definition.get("parts")
2600
+ if part.get("path").endswith(".json")
2601
+ ]:
2602
+ file_path = part.get("path")
2603
+ payload = part.get("payload")
2604
+ matches = jsonpath_expr.find(payload)
2605
+ if matches:
2606
+ for match in matches:
2607
+ color_string = match.value.strip("'")
2608
+ if color_string in mapping:
2609
+ color_data = mapping[color_string]
2610
+ if isinstance(color_data, int):
2611
+ color_data = [color_data, 0]
2612
+
2613
+ # Get reference to parent of 'Value' (i.e. 'Literal')
2614
+ # literal_dict = match.context.value
2615
+ # Get reference to parent of 'Literal' (i.e. 'expr')
2616
+ expr_dict = match.context.context.value
2617
+
2618
+ # Replace the 'expr' with new structure
2619
+ expr_dict.clear()
2620
+ expr_dict["ThemeDataColor"] = {
2621
+ "ColorId": color_data[0],
2622
+ "Percent": color_data[1],
2623
+ }
2092
2624
 
2093
- upd = self.update_report(request_body=request_body)
2094
- if upd == 200:
2095
- print(f"{icons.green_dot}")
2096
- else:
2097
- print(f"{icons.red_dot}")
2625
+ self.update(file_path=file_path, payload=payload)
2098
2626
 
2099
- def __persist_filters(self, value: Optional[bool] = False):
2627
+ def _rename_fields(self, mapping: dict):
2100
2628
  """
2101
- Don't allow end user to save filters on this file in the Power BI service.
2629
+ Renames fields in the report definition based on the provided rename mapping.
2630
+
2631
+ Parameters
2632
+ ----------
2633
+ mapping : dict
2634
+ A dictionary containing the mapping of old field names to new field names.
2635
+ Example:
2636
+
2637
+ {
2638
+ "columns": {
2639
+ ("TableName", "OldColumnName1"): "NewColumnName1",
2640
+ ("TableName", "OldColumnName2"): "NewColumnName2",
2641
+ },
2642
+ "measures": {
2643
+ ("TableName", "OldMeasureName1"): "NewMeasureName1",
2644
+ ("TableName", "OldMeasureName2"): "NewMeasureName2",
2645
+ }
2646
+ }
2102
2647
  """
2648
+ self._ensure_pbir()
2103
2649
 
2104
- self.adjust_settings(
2105
- setting_type="settings",
2106
- setting_name="isPersistentUserStateDisabled",
2107
- setting_value=value,
2108
- )
2650
+ selector_mapping = {
2651
+ key: {
2652
+ ".".join(k): v # join tuple with '.' to form the string
2653
+ for k, v in value.items()
2654
+ }
2655
+ for key, value in mapping.items()
2656
+ }
2109
2657
 
2110
- def __hide_visual_header(self, value: Optional[bool] = False):
2111
- """
2112
- Hide the visual header in reading view.
2113
- """
2658
+ for part in [
2659
+ part
2660
+ for part in self._report_definition.get("parts")
2661
+ if part.get("path").endswith(".json")
2662
+ ]:
2663
+ file_path = part.get("path")
2664
+ payload = part.get("payload")
2665
+
2666
+ # Paths for columns, measures, and expressions
2667
+ col_expr_path = parse("$..Column")
2668
+ meas_expr_path = parse("$..Measure")
2669
+ entity_ref_path = parse("$..Expression.SourceRef.Entity")
2670
+ query_ref_path = parse("$..queryRef")
2671
+ native_query_ref_path = parse("$..nativeQueryRef")
2672
+ filter_expr_path = parse("$..filterConfig.filters[*].filter.From")
2673
+ source_ref_path = parse("$..Expression.SourceRef.Source")
2674
+ metadata_ref_path = parse("$..selector.metadata")
2675
+
2676
+ # Populate table alias map
2677
+ alias_map = {}
2678
+ for match in filter_expr_path.find(payload):
2679
+ alias_list = match.value
2680
+ for alias in alias_list:
2681
+ alias_name = alias.get("Name")
2682
+ alias_entity = alias.get("Entity")
2683
+ alias_map[alias_name] = alias_entity
2684
+
2685
+ # Rename selector.metadata objects
2686
+ for match in metadata_ref_path.find(payload):
2687
+ obj = match.value
2688
+
2689
+ # Check both measures and columns
2690
+ for category in ["measures", "columns"]:
2691
+ if obj in selector_mapping.get(category, {}):
2692
+ value = selector_mapping[category][obj]
2693
+
2694
+ # Find original tuple key from mapping for this category
2695
+ for tup_key in mapping.get(category, {}).keys():
2696
+ if ".".join(tup_key) == obj:
2697
+ key = tup_key[
2698
+ 0
2699
+ ] # first element of tuple, like table name
2700
+ new_value = f"{key}.{value}"
2701
+
2702
+ # Update the dictionary node holding "metadata"
2703
+ if isinstance(match.context.value, dict):
2704
+ match.context.value["metadata"] = new_value
2705
+ else:
2706
+ print(
2707
+ f"Warning: Cannot assign metadata, context is {type(match.context.value)}"
2708
+ )
2709
+ break
2710
+
2711
+ # Once found in one category, no need to check the other
2712
+ break
2114
2713
 
2115
- self.adjust_settings(
2116
- setting_type="settings",
2117
- setting_name="hideVisualContainerHeader",
2118
- setting_value=value,
2119
- )
2714
+ # Rename Column Properties
2715
+ for match in col_expr_path.find(payload):
2716
+ col_obj = match.value
2717
+ parent = match.context.value
2120
2718
 
2121
- def __default_cross_filtering(self, value: Optional[bool] = False):
2122
- """
2123
- Change the default visual interaction from cross highlighting to cross filtering.
2124
- """
2719
+ # Extract table name from SourceRef
2720
+ source_matches = entity_ref_path.find(parent)
2721
+ if source_matches:
2722
+ table = source_matches[0].value
2723
+ else:
2724
+ alias = source_ref_path.find(parent)
2725
+ table = alias_map.get(alias[0].value)
2125
2726
 
2126
- self.adjust_settings(
2127
- setting_type="settings",
2128
- setting_name="defaultFilterActionIsDataFilter",
2129
- setting_value=value,
2130
- )
2727
+ if not table:
2728
+ continue # skip if can't resolve table
2131
2729
 
2132
- def __modern_visual_header(self, value: Optional[bool] = True):
2133
- """
2134
- Use the modern visual header with updated styling options.
2135
- """
2730
+ old_name = col_obj.get("Property")
2731
+ if (table, old_name) in mapping.get("columns", {}):
2732
+ col_obj["Property"] = mapping["columns"][(table, old_name)]
2136
2733
 
2137
- self.adjust_settings(
2138
- setting_type="settings",
2139
- setting_name="useStylableVisualContainerHeader",
2140
- setting_value=value,
2141
- )
2734
+ # Rename Measure Properties
2735
+ for match in meas_expr_path.find(payload):
2736
+ meas_obj = match.value
2737
+ parent = match.context.value
2142
2738
 
2143
- def __show_default_summarization_type(self, value: Optional[bool] = True):
2144
- """
2145
- For aggregated fields, always show the default summarization type.
2739
+ source_matches = entity_ref_path.find(parent)
2740
+ if source_matches:
2741
+ table = source_matches[0].value
2742
+ else:
2743
+ alias = source_ref_path.find(parent)
2744
+ table = alias_map.get(alias[0].value)
2745
+
2746
+ if not table:
2747
+ continue # skip if can't resolve table
2748
+
2749
+ old_name = meas_obj.get("Property")
2750
+ if (table, old_name) in mapping.get("measures", {}):
2751
+ meas_obj["Property"] = mapping["measures"][(table, old_name)]
2752
+
2753
+ # Update queryRef and nativeQueryRef
2754
+ def update_refs(path_expr):
2755
+ for match in path_expr.find(payload):
2756
+ ref_key = match.path.fields[0]
2757
+ ref_value = match.value
2758
+ parent = match.context.value
2759
+
2760
+ for (tbl, old_name), new_name in mapping.get("columns", {}).items():
2761
+ pattern = rf"\b{re.escape(tbl)}\.{re.escape(old_name)}\b"
2762
+ if re.search(pattern, ref_value):
2763
+ if ref_key == "queryRef":
2764
+ ref_value = re.sub(
2765
+ pattern, f"{tbl}.{new_name}", ref_value
2766
+ )
2767
+ elif ref_key == "nativeQueryRef":
2768
+ agg_match = re.match(
2769
+ rf"(?i)([a-z]+)\s*\(\s*{re.escape(tbl)}\.{re.escape(old_name)}\s*\)",
2770
+ ref_value,
2771
+ )
2772
+ if agg_match:
2773
+ func = agg_match.group(1).capitalize()
2774
+ ref_value = f"{func} of {new_name}"
2775
+ else:
2776
+ ref_value = ref_value.replace(old_name, new_name)
2777
+ parent[ref_key] = ref_value
2778
+
2779
+ for (tbl, old_name), new_name in mapping.get(
2780
+ "measures", {}
2781
+ ).items():
2782
+ pattern = rf"\b{re.escape(tbl)}\.{re.escape(old_name)}\b"
2783
+ if re.search(pattern, ref_value):
2784
+ if ref_key == "queryRef":
2785
+ ref_value = re.sub(
2786
+ pattern, f"{tbl}.{new_name}", ref_value
2787
+ )
2788
+ elif ref_key == "nativeQueryRef":
2789
+ agg_match = re.match(
2790
+ rf"(?i)([a-z]+)\s*\(\s*{re.escape(tbl)}\.{re.escape(old_name)}\s*\)",
2791
+ ref_value,
2792
+ )
2793
+ if agg_match:
2794
+ func = agg_match.group(1).capitalize()
2795
+ ref_value = f"{func} of {new_name}"
2796
+ else:
2797
+ ref_value = ref_value.replace(old_name, new_name)
2798
+ parent[ref_key] = ref_value
2799
+
2800
+ update_refs(query_ref_path)
2801
+ update_refs(native_query_ref_path)
2802
+
2803
+ self.update(file_path=file_path, payload=payload)
2804
+
2805
+ def _list_color_codes(self) -> List[str]:
2806
+ """
2807
+ Shows a list of all the hex color codes used in the report.
2808
+
2809
+ Returns
2810
+ -------
2811
+ list[str]
2812
+ A list of hex color codes used in the report.
2146
2813
  """
2814
+ self._ensure_pbir()
2147
2815
 
2148
- self.adjust_settings(
2149
- setting_type="settings",
2150
- setting_name="useDefaultAggregateDisplayName",
2151
- setting_value=value,
2152
- )
2816
+ file = self.get("*.json", json_path="$..color.expr.Literal.Value")
2153
2817
 
2154
- def __modern_visual_tooltips(self, value: Optional[bool] = True):
2818
+ return [x[1].strip("'") for x in file]
2819
+
2820
+ def __update_visual_image(self, file_path: str, image_path: str):
2155
2821
  """
2156
- Use modern visual tooltips with drill actions and updated styling.
2822
+ Update the image of a visual in the report definition. Only supported for 'image' visual types.
2823
+
2824
+ Parameters
2825
+ ----------
2826
+ file_path : str
2827
+ The file path of the visual to be updated. For example: "definition/pages/ReportSection1/visuals/a1d8f99b81dcc2d59035/visual.json".
2828
+ image_path : str
2829
+ The name of the image file to be added. For example: "MyImage".
2157
2830
  """
2158
2831
 
2159
- self.adjust_settings(
2160
- setting_type="settings",
2161
- setting_name="useEnhancedTooltips",
2162
- setting_value=value,
2163
- )
2832
+ if image_path not in self.list_paths().get("Path").values:
2833
+ raise ValueError(
2834
+ f"Image path '{image_path}' not found in the report definition."
2835
+ )
2836
+ if not image_path.startswith("StaticResources/RegisteredResources/"):
2837
+ raise ValueError(
2838
+ f"Image path must start with 'StaticResources/RegisteredResources/'. Provided: {image_path}"
2839
+ )
2164
2840
 
2165
- def __user_can_change_filter_types(self, value: Optional[bool] = True):
2166
- """
2167
- Allow users to change filter types.
2168
- """
2841
+ image_name = image_path.split("RegisteredResources/")[1]
2169
2842
 
2170
- self.adjust_settings(
2171
- setting_type="settings",
2172
- setting_name="allowChangeFilterTypes",
2173
- setting_value=value,
2174
- )
2843
+ if not file_path.endswith("/visual.json"):
2844
+ raise ValueError(
2845
+ f"File path must end with '/visual.json'. Provided: {file_path}"
2846
+ )
2175
2847
 
2176
- def __disable_search_filter_pane(self, value: Optional[bool] = False):
2177
- """
2178
- Enable search for the filter pane.
2179
- """
2848
+ file = self.get(file_path=file_path)
2849
+ if file.get("visual").get("visualType") != "image":
2850
+ raise ValueError("This function is only valid for image visuals.")
2851
+ file.get("visual").get("objects").get("general")[0].get("properties").get(
2852
+ "imageUrl"
2853
+ ).get("expr").get("ResourcePackageItem")["ItemName"] == image_name
2180
2854
 
2181
- self.adjust_settings(
2182
- setting_type="settings",
2183
- setting_name="disableFilterPaneSearch",
2184
- setting_value=value,
2185
- )
2855
+ def save_changes(self):
2186
2856
 
2187
- def __enable_cross_report_drillthrough(self, value: Optional[bool] = False):
2188
- """
2189
- Allow visuals in this report to use drillthrough targets from other reports.
2190
- """
2857
+ if self._readonly:
2858
+ print(
2859
+ f"{icons.warning} The connection is read-only. Set 'readonly' to False to save changes."
2860
+ )
2861
+ else:
2862
+ # Convert the report definition to base64
2863
+ if self._current_report_definition == self._report_definition:
2864
+ print(f"{icons.info} No changes were made to the report definition.")
2865
+ return
2866
+ new_report_definition = copy.deepcopy(self._report_definition)
2867
+
2868
+ for part in new_report_definition.get("parts"):
2869
+ part["payloadType"] = "InlineBase64"
2870
+ path = part.get("path")
2871
+ payload = part.get("payload")
2872
+ if isinstance(payload, dict):
2873
+ converted_json = json.dumps(part["payload"])
2874
+ part["payload"] = base64.b64encode(
2875
+ converted_json.encode("utf-8")
2876
+ ).decode("utf-8")
2877
+ elif isinstance(payload, bytes):
2878
+ part["payload"] = base64.b64encode(part["payload"]).decode("utf-8")
2879
+ elif is_base64(payload):
2880
+ part["payload"] = payload
2881
+ else:
2882
+ raise NotImplementedError(
2883
+ f"{icons.red_dot} Unsupported payload type: {type(payload)} for the '{path}' file."
2884
+ )
2191
2885
 
2192
- self.adjust_settings(
2193
- setting_type="settings",
2194
- setting_name="useCrossReportDrillthrough",
2195
- setting_value=value,
2196
- )
2886
+ # Generate payload for the updateDefinition API
2887
+ new_payload = {"definition": {"parts": new_report_definition.get("parts")}}
2197
2888
 
2198
- def __disable_default_cross_highlighting(self, value: Optional[bool] = False):
2199
- """
2200
- Disable cross highlighting/filtering by default.
2201
- """
2889
+ # Update item definition
2890
+ _base_api(
2891
+ request=f"/v1/workspaces/{self._workspace_id}/reports/{self._report_id}/updateDefinition",
2892
+ method="post",
2893
+ payload=new_payload,
2894
+ lro_return_status_code=True,
2895
+ status_codes=None,
2896
+ )
2897
+ print(
2898
+ f"{icons.green_dot} The report definition has been updated successfully."
2899
+ )
2202
2900
 
2203
- self.adjust_settings(
2204
- setting_type="slowDataSourceSettings",
2205
- setting_name="isCrossHighlightingDisabled",
2206
- setting_value=value,
2207
- )
2901
+ def close(self):
2208
2902
 
2209
- def __add_slicer_apply_button(self, value: Optional[bool] = False):
2210
- """
2211
- Add an Apply button to each individual slicer (not recommended).
2212
- """
2903
+ if self._show_diffs and (
2904
+ self._current_report_definition != self._report_definition
2905
+ ):
2906
+ diff_parts(
2907
+ self._current_report_definition.get("parts"),
2908
+ self._report_definition.get("parts"),
2909
+ )
2910
+ # Save the changes to the service if the connection is read/write
2911
+ if not self._readonly:
2912
+ self.save_changes()
2913
+
2914
+
2915
+ @log
2916
+ @contextmanager
2917
+ def connect_report(
2918
+ report: str | UUID,
2919
+ workspace: Optional[str | UUID] = None,
2920
+ readonly: bool = True,
2921
+ show_diffs: bool = True,
2922
+ ):
2923
+ """
2924
+ Connects to the report.
2213
2925
 
2214
- self.adjust_settings(
2215
- setting_type="slowDataSourceSettings",
2216
- setting_name="isSlicerSelectionsButtonEnabled",
2217
- setting_value=value,
2218
- )
2926
+ Parameters
2927
+ ----------
2928
+ report : str | uuid.UUID
2929
+ Name or ID of the report.
2930
+ workspace : str | uuid.UUID, default=None
2931
+ The workspace name or ID.
2932
+ Defaults to None which resolves to the workspace of the attached lakehouse
2933
+ or if no lakehouse attached, resolves to the workspace of the notebook.
2934
+ readonly: bool, default=True
2935
+ Whether the connection is read-only or read/write. Setting this to False enables read/write which saves the changes made back to the server.
2936
+ show_diffs: bool, default=True
2937
+ Whether to show the differences between the current report definition in the service and the new report definition.
2219
2938
 
2220
- # def close(self):
2221
- # if not self._readonly and self._report is not None:
2222
- # print("saving...")
2939
+ Returns
2940
+ -------
2941
+ typing.Iterator[ReportWrapper]
2942
+ A connection to the report's metadata.
2943
+ """
2223
2944
 
2224
- # self._report = None
2945
+ rw = ReportWrapper(
2946
+ report=report,
2947
+ workspace=workspace,
2948
+ readonly=readonly,
2949
+ show_diffs=show_diffs,
2950
+ )
2951
+ try:
2952
+ yield rw
2953
+ finally:
2954
+ rw.close()