semantic-link-labs 0.9.10__py3-none-any.whl → 0.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

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