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.
- {semantic_link_labs-0.9.10.dist-info → semantic_link_labs-0.10.0.dist-info}/METADATA +28 -21
- {semantic_link_labs-0.9.10.dist-info → semantic_link_labs-0.10.0.dist-info}/RECORD +38 -31
- {semantic_link_labs-0.9.10.dist-info → semantic_link_labs-0.10.0.dist-info}/WHEEL +1 -1
- sempy_labs/__init__.py +26 -1
- sempy_labs/_delta_analyzer.py +9 -8
- sempy_labs/_dictionary_diffs.py +221 -0
- sempy_labs/_environments.py +19 -1
- sempy_labs/_generate_semantic_model.py +1 -1
- sempy_labs/_helper_functions.py +358 -134
- sempy_labs/_kusto.py +25 -23
- sempy_labs/_list_functions.py +13 -35
- sempy_labs/_model_bpa_rules.py +13 -3
- sempy_labs/_notebooks.py +44 -11
- sempy_labs/_semantic_models.py +93 -1
- sempy_labs/_sql.py +4 -3
- sempy_labs/_tags.py +194 -0
- sempy_labs/_user_delegation_key.py +42 -0
- sempy_labs/_variable_libraries.py +89 -0
- sempy_labs/_vpax.py +388 -0
- sempy_labs/admin/__init__.py +8 -0
- sempy_labs/admin/_tags.py +126 -0
- sempy_labs/directlake/_generate_shared_expression.py +5 -1
- sempy_labs/directlake/_update_directlake_model_lakehouse_connection.py +55 -5
- sempy_labs/dotnet_lib/dotnet.runtime.config.json +10 -0
- sempy_labs/lakehouse/__init__.py +14 -0
- sempy_labs/lakehouse/_blobs.py +100 -85
- sempy_labs/lakehouse/_get_lakehouse_tables.py +1 -13
- sempy_labs/lakehouse/_helper.py +211 -0
- sempy_labs/lakehouse/_lakehouse.py +1 -1
- sempy_labs/lakehouse/_livy_sessions.py +137 -0
- sempy_labs/report/__init__.py +2 -0
- sempy_labs/report/_download_report.py +1 -1
- sempy_labs/report/_generate_report.py +5 -1
- sempy_labs/report/_report_helper.py +27 -128
- sempy_labs/report/_reportwrapper.py +1903 -1165
- sempy_labs/tom/_model.py +83 -21
- sempy_labs/report/_bpareporttemplate/.pbi/localSettings.json +0 -9
- sempy_labs/report/_bpareporttemplate/.platform +0 -11
- {semantic_link_labs-0.9.10.dist-info → semantic_link_labs-0.10.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
46
|
-
A
|
|
61
|
+
None
|
|
62
|
+
A connection to the report is established and the report definition is retrieved.
|
|
47
63
|
"""
|
|
48
64
|
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
The
|
|
67
|
-
|
|
68
|
-
The
|
|
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
|
-
|
|
75
|
-
|
|
186
|
+
dict | List[Tuple[str, dict]]
|
|
187
|
+
The json content of the specified report definition file.
|
|
76
188
|
"""
|
|
77
189
|
|
|
78
|
-
|
|
190
|
+
parts = self._report_definition.get("parts")
|
|
79
191
|
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
98
|
-
|
|
271
|
+
self._report_definition["parts"].append(
|
|
272
|
+
{"path": file_path, "payload": decoded_payload}
|
|
273
|
+
)
|
|
99
274
|
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
374
|
+
self.update(file_path=path, payload=new_payload)
|
|
375
|
+
|
|
376
|
+
def list_paths(self) -> pd.DataFrame:
|
|
132
377
|
"""
|
|
133
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
391
|
+
def __all_pages(self):
|
|
146
392
|
|
|
147
|
-
|
|
393
|
+
self._ensure_pbir()
|
|
148
394
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
511
|
+
str
|
|
169
512
|
The page name.
|
|
170
513
|
"""
|
|
171
514
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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:
|
|
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 :
|
|
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
|
-
|
|
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
|
-
|
|
540
|
+
def __add_to_registered_resources(self, name: str, path: str, type: str):
|
|
194
541
|
|
|
195
|
-
|
|
196
|
-
"""
|
|
197
|
-
Obtains the theme file of the report.
|
|
542
|
+
type = type.capitalize()
|
|
198
543
|
|
|
199
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
211
|
-
|
|
572
|
+
# Add the new item to the existing RegisteredResources
|
|
573
|
+
rp["items"].append(new_item)
|
|
212
574
|
|
|
213
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
df["Custom Visual Name"] =
|
|
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
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
339
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
payload =
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
423
|
-
|
|
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
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
payload =
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
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
|
-
|
|
603
|
-
file_path = r["path"]
|
|
604
|
-
payload = r["payload"]
|
|
1007
|
+
dfV = self.list_visuals()
|
|
605
1008
|
|
|
606
|
-
|
|
1009
|
+
dfs = []
|
|
1010
|
+
for p in self.__all_pages():
|
|
1011
|
+
file_path = p.get("path")
|
|
607
1012
|
page_prefix = file_path[0:-9]
|
|
608
|
-
|
|
609
|
-
page_name =
|
|
610
|
-
height =
|
|
611
|
-
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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(
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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(
|
|
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":
|
|
658
|
-
"Display Option":
|
|
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
|
-
|
|
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
|
-
|
|
681
|
-
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
|
|
781
|
-
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
1206
|
+
show_all_data = find_show_all_with_jsonpath(payload)
|
|
819
1207
|
|
|
820
|
-
|
|
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.
|
|
823
|
-
).find(
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
-
|
|
861
|
-
|
|
1248
|
+
# Sparkline
|
|
1249
|
+
has_sparkline = contains_key(payload, ["SparklineData"])
|
|
862
1250
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
if
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
1063
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1250
|
-
path = r["path"]
|
|
1251
|
-
payload = r["payload"]
|
|
1641
|
+
dfs = []
|
|
1252
1642
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1643
|
+
for b in bookmarks:
|
|
1644
|
+
path = b.get("path")
|
|
1645
|
+
payload = b.get("payload")
|
|
1255
1646
|
|
|
1256
|
-
bookmark_name =
|
|
1257
|
-
bookmark_display =
|
|
1258
|
-
rpt_page_id =
|
|
1259
|
-
page_id, page_display
|
|
1260
|
-
|
|
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
|
|
1654
|
+
for rptPg in payload.get("explorationState", {}).get("sections", {}):
|
|
1264
1655
|
for visual_name in (
|
|
1265
|
-
|
|
1656
|
+
payload.get("explorationState", {})
|
|
1266
1657
|
.get("sections", {})
|
|
1267
1658
|
.get(rptPg, {})
|
|
1268
1659
|
.get("visualContainers", {})
|
|
1269
1660
|
):
|
|
1270
1661
|
if (
|
|
1271
|
-
|
|
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
|
-
|
|
1295
|
-
|
|
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
|
-
|
|
1719
|
+
report_file = self.get(file_path=self._report_extensions_path)
|
|
1326
1720
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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
|
-
|
|
1348
|
-
|
|
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
|
|
1746
|
+
def get_theme(self, theme_type: str = "baseTheme") -> dict:
|
|
1354
1747
|
"""
|
|
1355
|
-
|
|
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
|
-
|
|
1360
|
-
|
|
1757
|
+
dict
|
|
1758
|
+
The theme.json file
|
|
1361
1759
|
"""
|
|
1362
1760
|
|
|
1363
|
-
|
|
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
|
-
|
|
1372
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1436
|
-
theme_version = "5.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
1459
|
-
|
|
1460
|
-
|
|
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
|
-
|
|
1837
|
+
custom_theme = {
|
|
1838
|
+
"name": theme_name_full,
|
|
1839
|
+
"reportVersionAtImport": theme_version,
|
|
1840
|
+
"type": "RegisteredResources",
|
|
1841
|
+
}
|
|
1463
1842
|
|
|
1464
|
-
|
|
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
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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
|
-
|
|
1538
|
-
|
|
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
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
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
|
|
1588
|
-
self
|
|
1944
|
+
(file_path, page_id, page_display_name) = (
|
|
1945
|
+
self.__resolve_page_name_and_display_name_file_path(page_name)
|
|
1589
1946
|
)
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1993
|
+
Hides all tooltip pages and drillthrough pages in a report.
|
|
1607
1994
|
"""
|
|
1608
1995
|
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
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
|
|
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
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
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
|
-
|
|
2032
|
+
dfCV = self.list_custom_visuals()
|
|
2033
|
+
df = dfCV[dfCV["Used in Report"] == False]
|
|
1642
2034
|
|
|
1643
|
-
|
|
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
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
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
|
|
2075
|
+
if rlm.empty:
|
|
1665
2076
|
print(
|
|
1666
|
-
f"{icons.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
2098
|
+
table_name = r["Table Name"]
|
|
2099
|
+
measure_name = r["Measure Name"]
|
|
2100
|
+
expr = r["Expression"]
|
|
1696
2101
|
# mDataType = r["Data Type"]
|
|
1697
|
-
|
|
2102
|
+
format_string = r["Format String"]
|
|
1698
2103
|
# Add measures to the model
|
|
1699
|
-
if
|
|
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=
|
|
1702
|
-
measure_name=
|
|
1703
|
-
expression=
|
|
1704
|
-
format_string=
|
|
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[
|
|
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
|
|
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
|
-
|
|
1721
|
-
entity for entity in
|
|
2127
|
+
file["entities"] = [
|
|
2128
|
+
entity for entity in file["entities"] if entity["measures"]
|
|
1722
2129
|
]
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2141
|
+
Shows a list of annotations in the report.
|
|
1741
2142
|
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
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
|
-
|
|
1757
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1770
|
-
|
|
1771
|
-
)
|
|
2200
|
+
if dfs:
|
|
2201
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
1772
2202
|
|
|
1773
|
-
|
|
2203
|
+
return df
|
|
2204
|
+
|
|
2205
|
+
def _add_image(self, image_path: str, resource_name: Optional[str] = None) -> str:
|
|
1774
2206
|
"""
|
|
1775
|
-
|
|
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
|
-
|
|
1779
|
-
dfP_filt = dfP[
|
|
1780
|
-
(dfP["Type"] == "Tooltip") | (dfP["Drillthrough Target Page"] == True)
|
|
1781
|
-
]
|
|
2223
|
+
id = generate_number_guid()
|
|
1782
2224
|
|
|
1783
|
-
if
|
|
1784
|
-
|
|
1785
|
-
|
|
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
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
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
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
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
|
-
|
|
1828
|
-
|
|
2275
|
+
if isinstance(page, str):
|
|
2276
|
+
page = [page]
|
|
1829
2277
|
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
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
|
-
|
|
1842
|
-
|
|
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
|
-
|
|
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
|
|
2301
|
+
def _set_wallpaper_color(
|
|
1847
2302
|
self,
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
The
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
if
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
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}
|
|
2330
|
+
f"{icons.red_dot} Theme color percentage must be between -0.6 and 0.6."
|
|
1888
2331
|
)
|
|
1889
2332
|
|
|
1890
|
-
|
|
1891
|
-
file = _decode_b64(payload)
|
|
1892
|
-
json_file = json.loads(file)
|
|
2333
|
+
page_list = self.__resolve_page_list(page)
|
|
1893
2334
|
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
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
|
-
|
|
2350
|
+
color_dict = ({"solid": {"color": {"expr": color_expr}}},)
|
|
2351
|
+
transparency_dict = {"expr": {"Literal": {"Value": f"{transparency}D"}}}
|
|
1900
2352
|
|
|
1901
|
-
|
|
1902
|
-
|
|
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
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
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
|
|
2370
|
+
def _set_wallpaper_image(
|
|
1914
2371
|
self,
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
The
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
The
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
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
|
|
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
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
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
|
-
|
|
1959
|
-
|
|
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
|
-
|
|
2465
|
+
def _add_page(self, payload: dict | bytes, generate_id: bool = True):
|
|
2466
|
+
"""
|
|
2467
|
+
Add a new page to the report.
|
|
1962
2468
|
|
|
1963
|
-
|
|
1964
|
-
|
|
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
|
-
|
|
1967
|
-
|
|
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
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
The
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
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
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
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
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
)
|
|
2006
|
-
|
|
2007
|
-
|
|
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
|
-
|
|
2018
|
-
|
|
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
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
)
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
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
|
-
|
|
2053
|
-
|
|
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"
|
|
2586
|
+
f"{icons.red_dot} The Percent values must be between -0.6 and 0.6."
|
|
2058
2587
|
)
|
|
2059
2588
|
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
for
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
if path
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
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
|
-
|
|
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
|
|
2622
|
+
def _rename_fields(self, mapping: dict):
|
|
2087
2623
|
"""
|
|
2088
|
-
|
|
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
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
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
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
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
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
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
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
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
|
-
|
|
2114
|
-
|
|
2115
|
-
setting_name="defaultFilterActionIsDataFilter",
|
|
2116
|
-
setting_value=value,
|
|
2117
|
-
)
|
|
2722
|
+
if not table:
|
|
2723
|
+
continue # skip if can't resolve table
|
|
2118
2724
|
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
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
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
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
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
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
|
-
|
|
2153
|
-
"""
|
|
2154
|
-
Allow users to change filter types.
|
|
2155
|
-
"""
|
|
2836
|
+
image_name = image_path.split("RegisteredResources/")[1]
|
|
2156
2837
|
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
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
|
-
|
|
2164
|
-
"""
|
|
2165
|
-
|
|
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
|
-
|
|
2169
|
-
setting_type="settings",
|
|
2170
|
-
setting_name="disableFilterPaneSearch",
|
|
2171
|
-
setting_value=value,
|
|
2172
|
-
)
|
|
2850
|
+
def save_changes(self):
|
|
2173
2851
|
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
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
|
-
|
|
2180
|
-
|
|
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
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
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
|
-
|
|
2191
|
-
setting_type="slowDataSourceSettings",
|
|
2192
|
-
setting_name="isCrossHighlightingDisabled",
|
|
2193
|
-
setting_value=value,
|
|
2194
|
-
)
|
|
2896
|
+
def close(self):
|
|
2195
2897
|
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
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
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
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
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2934
|
+
Returns
|
|
2935
|
+
-------
|
|
2936
|
+
typing.Iterator[ReportWrapper]
|
|
2937
|
+
A connection to the report's metadata.
|
|
2938
|
+
"""
|
|
2210
2939
|
|
|
2211
|
-
|
|
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()
|