semantic-link-labs 0.9.11__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.11.dist-info → semantic_link_labs-0.10.0.dist-info}/METADATA +3 -2
- {semantic_link_labs-0.9.11.dist-info → semantic_link_labs-0.10.0.dist-info}/RECORD +16 -14
- {semantic_link_labs-0.9.11.dist-info → semantic_link_labs-0.10.0.dist-info}/WHEEL +1 -1
- sempy_labs/__init__.py +4 -0
- sempy_labs/_dictionary_diffs.py +221 -0
- sempy_labs/_helper_functions.py +165 -0
- sempy_labs/_sql.py +1 -1
- sempy_labs/_user_delegation_key.py +42 -0
- sempy_labs/_vpax.py +2 -0
- sempy_labs/lakehouse/__init__.py +0 -2
- sempy_labs/lakehouse/_blobs.py +0 -37
- sempy_labs/report/__init__.py +2 -0
- sempy_labs/report/_report_helper.py +27 -128
- sempy_labs/report/_reportwrapper.py +1918 -1193
- {semantic_link_labs-0.9.11.dist-info → semantic_link_labs-0.10.0.dist-info}/licenses/LICENSE +0 -0
- {semantic_link_labs-0.9.11.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,141 +45,459 @@ class ReportWrapper:
|
|
|
33
45
|
|
|
34
46
|
Parameters
|
|
35
47
|
----------
|
|
36
|
-
report : str
|
|
37
|
-
The name of the report.
|
|
48
|
+
report : str | uuid.UUID
|
|
49
|
+
The name or ID of the report.
|
|
38
50
|
workspace : str | uuid.UUID
|
|
39
51
|
The name or ID of the workspace in which the report resides.
|
|
40
52
|
Defaults to None which resolves to the workspace of the attached lakehouse
|
|
41
53
|
or if no lakehouse attached, resolves to the workspace of the notebook.
|
|
54
|
+
readonly: bool, default=True
|
|
55
|
+
Whether the connection is read-only or read/write. Setting this to False enables read/write which saves the changes made back to the server.
|
|
56
|
+
show_diffs: bool, default=True
|
|
57
|
+
Whether to show the differences between the current report definition in the service and the new report definition.
|
|
42
58
|
|
|
43
59
|
Returns
|
|
44
60
|
-------
|
|
45
|
-
|
|
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
|
+
)
|
|
60
153
|
|
|
61
|
-
|
|
154
|
+
self._current_report_definition = copy.deepcopy(self._report_definition)
|
|
155
|
+
|
|
156
|
+
# self.report = self.Report(self)
|
|
157
|
+
|
|
158
|
+
helper.populate_custom_visual_display_names()
|
|
159
|
+
|
|
160
|
+
def _ensure_pbir(self):
|
|
161
|
+
|
|
162
|
+
if self.format != "PBIR":
|
|
163
|
+
raise NotImplementedError(
|
|
164
|
+
f"{icons.red_dot} This ReportWrapper function requires the report to be in the PBIR format."
|
|
165
|
+
"See here for details: https://powerbi.microsoft.com/blog/power-bi-enhanced-report-format-pbir-in-power-bi-desktop-developer-mode-preview/"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Basic functions
|
|
169
|
+
def get(
|
|
170
|
+
self,
|
|
171
|
+
file_path: str,
|
|
172
|
+
json_path: Optional[str] = None,
|
|
173
|
+
) -> dict | List[Tuple[str, dict]]:
|
|
174
|
+
"""
|
|
175
|
+
Get the json content of the specified report definition file.
|
|
62
176
|
|
|
63
177
|
Parameters
|
|
64
178
|
----------
|
|
65
|
-
|
|
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
|
)
|
|
106
|
-
)
|
|
107
303
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
with connect_semantic_model(
|
|
112
|
-
dataset=dataset_id, readonly=True, workspace=dataset_workspace_id
|
|
113
|
-
) as tom:
|
|
114
|
-
measure_names = {m.Name for m in tom.all_measures()}
|
|
115
|
-
measure_names.update(report_level_measures)
|
|
116
|
-
column_names = {
|
|
117
|
-
format_dax_object_name(c.Parent.Name, c.Name) for c in tom.all_columns()
|
|
118
|
-
}
|
|
119
|
-
hierarchy_names = {
|
|
120
|
-
format_dax_object_name(h.Parent.Name, h.Name)
|
|
121
|
-
for h in tom.all_hierarchies()
|
|
122
|
-
}
|
|
304
|
+
for part in matching_parts:
|
|
305
|
+
path = part.get("path")
|
|
306
|
+
payload = part.get("payload")
|
|
123
307
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if obj_type == "Measure":
|
|
129
|
-
return obj_name in measure_names
|
|
130
|
-
elif obj_type == "Column":
|
|
131
|
-
return (
|
|
132
|
-
format_dax_object_name(row["Table Name"], obj_name) in column_names
|
|
308
|
+
if not json_path:
|
|
309
|
+
self._report_definition["parts"].remove(part)
|
|
310
|
+
print(
|
|
311
|
+
f"{icons.green_dot} The file '{path}' has been removed from the report definition."
|
|
133
312
|
)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
in hierarchy_names
|
|
313
|
+
else:
|
|
314
|
+
remove_json_value(
|
|
315
|
+
path=path, payload=payload, json_path=json_path, verbose=verbose
|
|
138
316
|
)
|
|
139
|
-
return False
|
|
140
317
|
|
|
141
|
-
|
|
142
|
-
|
|
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."
|
|
343
|
+
)
|
|
143
344
|
|
|
144
|
-
def
|
|
345
|
+
def set_json(self, file_path: str, json_path: str, json_value: str | dict | List):
|
|
145
346
|
"""
|
|
146
|
-
|
|
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
|
+
)
|
|
373
|
+
|
|
374
|
+
self.update(file_path=path, payload=new_payload)
|
|
375
|
+
|
|
376
|
+
def list_paths(self) -> pd.DataFrame:
|
|
147
377
|
"""
|
|
378
|
+
List all file paths in the report definition.
|
|
148
379
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
_add_part(request_body, path=path, payload=new_payload)
|
|
155
|
-
else:
|
|
156
|
-
_add_part(request_body, path=path, payload=payload)
|
|
380
|
+
Returns
|
|
381
|
+
-------
|
|
382
|
+
pandas.DataFrame
|
|
383
|
+
A pandas dataframe containing a list of all paths in the report definition.
|
|
384
|
+
"""
|
|
157
385
|
|
|
158
|
-
|
|
386
|
+
existing_paths = [
|
|
387
|
+
part.get("path") for part in self._report_definition.get("parts")
|
|
388
|
+
]
|
|
389
|
+
return pd.DataFrame(existing_paths, columns=["Path"])
|
|
159
390
|
|
|
160
|
-
def
|
|
391
|
+
def __all_pages(self):
|
|
161
392
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
393
|
+
self._ensure_pbir()
|
|
394
|
+
|
|
395
|
+
return [
|
|
396
|
+
o
|
|
397
|
+
for o in self._report_definition.get("parts")
|
|
398
|
+
if o.get("path").endswith("/page.json")
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
def __all_visuals(self):
|
|
402
|
+
|
|
403
|
+
self._ensure_pbir()
|
|
404
|
+
|
|
405
|
+
return [
|
|
406
|
+
o
|
|
407
|
+
for o in self._report_definition.get("parts")
|
|
408
|
+
if o.get("path").endswith("/visual.json")
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
# Helper functions
|
|
412
|
+
def __resolve_page_list(self, page: Optional[str | List[str]] = None) -> List[str]:
|
|
413
|
+
|
|
414
|
+
if isinstance(page, str):
|
|
415
|
+
page = [page]
|
|
416
|
+
|
|
417
|
+
# Resolve page list
|
|
418
|
+
return (
|
|
419
|
+
[self.resolve_page_name(p) for p in page]
|
|
420
|
+
if page
|
|
421
|
+
else [
|
|
422
|
+
p["payload"]["name"]
|
|
423
|
+
for p in self.__all_pages()
|
|
424
|
+
if "payload" in p and "name" in p["payload"]
|
|
425
|
+
]
|
|
168
426
|
)
|
|
169
427
|
|
|
170
|
-
def
|
|
428
|
+
def _get_url(self, page_name: Optional[str] = None) -> str:
|
|
429
|
+
"""
|
|
430
|
+
Gets the URL of the report. If specified, gets the URL of the specified page.
|
|
431
|
+
|
|
432
|
+
Parameters
|
|
433
|
+
----------
|
|
434
|
+
page_name : str, default=None
|
|
435
|
+
The name of the page. If None, gets the URL of the report.
|
|
436
|
+
If specified, gets the URL of the specified page.
|
|
437
|
+
|
|
438
|
+
Returns
|
|
439
|
+
-------
|
|
440
|
+
str
|
|
441
|
+
The URL of the report or the specified page.
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
url = f"https://app.powerbi.com/groups/{self._workspace_id}/reports/{self._report_id}"
|
|
445
|
+
|
|
446
|
+
if page_name:
|
|
447
|
+
url += f"/{page_name}"
|
|
448
|
+
|
|
449
|
+
return url
|
|
450
|
+
|
|
451
|
+
def __resolve_page_name_and_display_name_file_path(
|
|
452
|
+
self, page: str
|
|
453
|
+
) -> Tuple[str, str, str]:
|
|
454
|
+
|
|
455
|
+
self._ensure_pbir()
|
|
456
|
+
page_map = {
|
|
457
|
+
p["path"]: [p["payload"]["name"], p["payload"]["displayName"]]
|
|
458
|
+
for p in self._report_definition.get("parts", [])
|
|
459
|
+
if p.get("path", "").endswith("/page.json") and "payload" in p
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
# Build lookup: page_id → (path, display_name)
|
|
463
|
+
id_lookup = {v[0]: (k, v[1]) for k, v in page_map.items()}
|
|
464
|
+
|
|
465
|
+
# Build lookup: display_name → (path, page_id)
|
|
466
|
+
name_lookup = {v[1]: (k, v[0]) for k, v in page_map.items()}
|
|
467
|
+
|
|
468
|
+
if page in id_lookup:
|
|
469
|
+
path, display_name = id_lookup[page]
|
|
470
|
+
return path, page, display_name
|
|
471
|
+
elif page in name_lookup:
|
|
472
|
+
path, page_id = name_lookup[page]
|
|
473
|
+
return path, page_id, page
|
|
474
|
+
else:
|
|
475
|
+
raise ValueError(
|
|
476
|
+
f"{icons.red_dot} Invalid page display name. The '{page}' page does not exist in the '{self._report_name}' report within the '{self._workspace_name}' workspace."
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
def _resolve_page_name_and_display_name(self, page: str) -> Tuple[str, str]:
|
|
480
|
+
"""
|
|
481
|
+
Obtains the page name, page display name for a given page in a report.
|
|
482
|
+
|
|
483
|
+
Parameters
|
|
484
|
+
----------
|
|
485
|
+
page : str
|
|
486
|
+
The page name or display name.
|
|
487
|
+
|
|
488
|
+
Returns
|
|
489
|
+
-------
|
|
490
|
+
Tuple[str, str]
|
|
491
|
+
The page name and display name.
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
(_, page_id, page_name) = self.__resolve_page_name_and_display_name_file_path(
|
|
495
|
+
page
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
return (page_id, page_name)
|
|
499
|
+
|
|
500
|
+
def resolve_page_name(self, page_display_name: str) -> str:
|
|
171
501
|
"""
|
|
172
502
|
Obtains the page name, page display name, and the file path for a given page in a report.
|
|
173
503
|
|
|
@@ -178,21 +508,22 @@ class ReportWrapper:
|
|
|
178
508
|
|
|
179
509
|
Returns
|
|
180
510
|
-------
|
|
181
|
-
|
|
511
|
+
str
|
|
182
512
|
The page name.
|
|
183
513
|
"""
|
|
184
514
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
515
|
+
(path, page_id, page_name) = (
|
|
516
|
+
self.__resolve_page_name_and_display_name_file_path(page_display_name)
|
|
517
|
+
)
|
|
518
|
+
return page_id
|
|
188
519
|
|
|
189
|
-
def resolve_page_display_name(self, page_name:
|
|
520
|
+
def resolve_page_display_name(self, page_name: str) -> str:
|
|
190
521
|
"""
|
|
191
522
|
Obtains the page dispaly name.
|
|
192
523
|
|
|
193
524
|
Parameters
|
|
194
525
|
----------
|
|
195
|
-
page_name :
|
|
526
|
+
page_name : str
|
|
196
527
|
The name of the page of the report.
|
|
197
528
|
|
|
198
529
|
Returns
|
|
@@ -201,59 +532,120 @@ class ReportWrapper:
|
|
|
201
532
|
The page display name.
|
|
202
533
|
"""
|
|
203
534
|
|
|
204
|
-
|
|
535
|
+
(path, page_id, page_name) = (
|
|
536
|
+
self.__resolve_page_name_and_display_name_file_path(page_name)
|
|
537
|
+
)
|
|
538
|
+
return page_name
|
|
205
539
|
|
|
206
|
-
|
|
540
|
+
def __add_to_registered_resources(self, name: str, path: str, type: str):
|
|
207
541
|
|
|
208
|
-
|
|
209
|
-
"""
|
|
210
|
-
Obtains the theme file of the report.
|
|
542
|
+
type = type.capitalize()
|
|
211
543
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
theme_type : str, default="baseTheme"
|
|
215
|
-
The theme type. Options: "baseTheme", "customTheme".
|
|
544
|
+
report_file = self.get(file_path=self._report_file_path)
|
|
545
|
+
rp_names = [rp.get("name") for rp in report_file.get("resourcePackages")]
|
|
216
546
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
547
|
+
new_item = {"name": name, "path": path, "type": type}
|
|
548
|
+
if "RegisteredResources" not in rp_names:
|
|
549
|
+
res = {
|
|
550
|
+
"name": "RegisteredResources",
|
|
551
|
+
"type": "RegisteredResources",
|
|
552
|
+
"items": [new_item],
|
|
553
|
+
}
|
|
554
|
+
report_file.get("resourcePackages").append(res)
|
|
555
|
+
else:
|
|
556
|
+
for rp in report_file.get("resourcePackages"):
|
|
557
|
+
if rp.get("name") == "RegisteredResources":
|
|
558
|
+
for item in rp.get("items"):
|
|
559
|
+
item_name = item.get("name")
|
|
560
|
+
item_type = item.get("type")
|
|
561
|
+
item_path = item.get("path")
|
|
562
|
+
if (
|
|
563
|
+
item_name == name
|
|
564
|
+
and item_type == type
|
|
565
|
+
and item_path == path
|
|
566
|
+
):
|
|
567
|
+
print(
|
|
568
|
+
f"{icons.info} The '{item_name}' {type.lower()} already exists in the report definition."
|
|
569
|
+
)
|
|
570
|
+
raise ValueError()
|
|
222
571
|
|
|
223
|
-
|
|
224
|
-
|
|
572
|
+
# Add the new item to the existing RegisteredResources
|
|
573
|
+
rp["items"].append(new_item)
|
|
225
574
|
|
|
226
|
-
|
|
227
|
-
theme_type = "customTheme"
|
|
228
|
-
elif "base" in theme_type:
|
|
229
|
-
theme_type = "baseTheme"
|
|
230
|
-
if theme_type not in theme_types:
|
|
231
|
-
raise ValueError(
|
|
232
|
-
f"{icons.red_dot} Invalid theme type. Valid options: {theme_types}."
|
|
233
|
-
)
|
|
575
|
+
self.update(file_path=self._report_file_path, payload=report_file)
|
|
234
576
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
ct = theme_collection.get(theme_type)
|
|
243
|
-
theme_name = ct["name"]
|
|
244
|
-
theme_location = ct["type"]
|
|
245
|
-
theme_file_path = f"StaticResources/{theme_location}/{theme_name}"
|
|
246
|
-
if theme_type == "baseTheme":
|
|
247
|
-
theme_file_path = (
|
|
248
|
-
f"StaticResources/{theme_location}/BaseThemes/{theme_name}"
|
|
577
|
+
def _add_extended(self, dataframe):
|
|
578
|
+
|
|
579
|
+
from sempy_labs.tom import connect_semantic_model
|
|
580
|
+
|
|
581
|
+
dataset_id, dataset_name, dataset_workspace_id, dataset_workspace_name = (
|
|
582
|
+
resolve_dataset_from_report(
|
|
583
|
+
report=self._report_id, workspace=self._workspace_id
|
|
249
584
|
)
|
|
250
|
-
|
|
251
|
-
theme_file_path = f"{theme_file_path}.json"
|
|
585
|
+
)
|
|
252
586
|
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
622
|
+
|
|
623
|
+
def _visual_page_mapping(self) -> dict:
|
|
624
|
+
self._ensure_pbir()
|
|
625
|
+
|
|
626
|
+
page_mapping = {}
|
|
627
|
+
visual_mapping = {}
|
|
628
|
+
|
|
629
|
+
for p in self.__all_pages():
|
|
630
|
+
path = p.get("path")
|
|
631
|
+
payload = p.get("payload")
|
|
632
|
+
pattern_page = r"/pages/(.*?)/page.json"
|
|
633
|
+
page_name = re.search(pattern_page, path).group(1)
|
|
634
|
+
page_id = payload.get("name")
|
|
635
|
+
page_display = payload.get("displayName")
|
|
636
|
+
page_mapping[page_name] = (page_id, page_display)
|
|
637
|
+
|
|
638
|
+
for v in self.__all_visuals():
|
|
639
|
+
path = v.get("path")
|
|
640
|
+
payload = v.get("payload")
|
|
641
|
+
pattern_page = r"/pages/(.*?)/visuals/"
|
|
642
|
+
page_name = re.search(pattern_page, path).group(1)
|
|
643
|
+
visual_mapping[path] = (
|
|
644
|
+
page_mapping.get(page_name)[0],
|
|
645
|
+
page_mapping.get(page_name)[1],
|
|
646
|
+
)
|
|
255
647
|
|
|
256
|
-
return
|
|
648
|
+
return visual_mapping
|
|
257
649
|
|
|
258
650
|
# List functions
|
|
259
651
|
def list_custom_visuals(self) -> pd.DataFrame:
|
|
@@ -265,8 +657,7 @@ class ReportWrapper:
|
|
|
265
657
|
pandas.DataFrame
|
|
266
658
|
A pandas dataframe containing a list of all the custom visuals used in the report.
|
|
267
659
|
"""
|
|
268
|
-
|
|
269
|
-
helper.populate_custom_visual_display_names()
|
|
660
|
+
self._ensure_pbir()
|
|
270
661
|
|
|
271
662
|
columns = {
|
|
272
663
|
"Custom Visual Name": "str",
|
|
@@ -275,17 +666,27 @@ class ReportWrapper:
|
|
|
275
666
|
}
|
|
276
667
|
|
|
277
668
|
df = _create_dataframe(columns=columns)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
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")
|
|
282
673
|
df["Custom Visual Display Name"] = df["Custom Visual Name"].apply(
|
|
283
674
|
lambda x: helper.vis_type_mapping.get(x, x)
|
|
284
675
|
)
|
|
285
676
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
677
|
+
visual_types = set()
|
|
678
|
+
for v in self.__all_visuals():
|
|
679
|
+
payload = v.get("payload", {})
|
|
680
|
+
visual = payload.get("visual", {})
|
|
681
|
+
visual_type = visual.get("visualType")
|
|
682
|
+
if visual_type:
|
|
683
|
+
visual_types.add(visual_type)
|
|
684
|
+
|
|
685
|
+
for _, r in df.iterrows():
|
|
686
|
+
if r["Custom Visual Name"] in visual_types:
|
|
687
|
+
df.at[_, "Used in Report"] = True
|
|
688
|
+
else:
|
|
689
|
+
df.at[_, "Used in Report"] = False
|
|
289
690
|
|
|
290
691
|
_update_dataframe_datatypes(dataframe=df, column_map=columns)
|
|
291
692
|
|
|
@@ -307,7 +708,9 @@ class ReportWrapper:
|
|
|
307
708
|
A pandas dataframe containing a list of all the report filters used in the report.
|
|
308
709
|
"""
|
|
309
710
|
|
|
310
|
-
|
|
711
|
+
self._ensure_pbir()
|
|
712
|
+
|
|
713
|
+
report_file = self.get(file_path=self._report_file_path)
|
|
311
714
|
|
|
312
715
|
columns = {
|
|
313
716
|
"Filter Name": "str",
|
|
@@ -322,35 +725,36 @@ class ReportWrapper:
|
|
|
322
725
|
}
|
|
323
726
|
df = _create_dataframe(columns=columns)
|
|
324
727
|
|
|
325
|
-
|
|
326
|
-
rpt_json = _extract_json(rd_filt)
|
|
327
|
-
if "filterConfig" in rpt_json:
|
|
328
|
-
for flt in rpt_json.get("filterConfig", {}).get("filters", {}):
|
|
329
|
-
filter_name = flt.get("name")
|
|
330
|
-
how_created = flt.get("howCreated")
|
|
331
|
-
locked = flt.get("isLockedInViewMode", False)
|
|
332
|
-
hidden = flt.get("isHiddenInViewMode", False)
|
|
333
|
-
filter_type = flt.get("type", "Basic")
|
|
334
|
-
filter_used = True if "Where" in flt.get("filter", {}) else False
|
|
728
|
+
dfs = []
|
|
335
729
|
|
|
336
|
-
|
|
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
|
-
new_data = {
|
|
340
|
-
"Filter Name": filter_name,
|
|
341
|
-
"Type": filter_type,
|
|
342
|
-
"Table Name": properties[0],
|
|
343
|
-
"Object Name": object_name,
|
|
344
|
-
"Object Type": properties[1],
|
|
345
|
-
"Hidden": hidden,
|
|
346
|
-
"Locked": locked,
|
|
347
|
-
"How Created": how_created,
|
|
348
|
-
"Used": filter_used,
|
|
349
|
-
}
|
|
739
|
+
entity_property_pairs = helper.find_entity_property_pairs(flt)
|
|
350
740
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
741
|
+
for object_name, properties in entity_property_pairs.items():
|
|
742
|
+
new_data = {
|
|
743
|
+
"Filter Name": filter_name,
|
|
744
|
+
"Type": filter_type,
|
|
745
|
+
"Table Name": properties[0],
|
|
746
|
+
"Object Name": object_name,
|
|
747
|
+
"Object Type": properties[1],
|
|
748
|
+
"Hidden": hidden,
|
|
749
|
+
"Locked": locked,
|
|
750
|
+
"How Created": how_created,
|
|
751
|
+
"Used": filter_used,
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
dfs.append(pd.DataFrame(new_data, index=[0]))
|
|
755
|
+
|
|
756
|
+
if dfs:
|
|
757
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
354
758
|
|
|
355
759
|
_update_dataframe_datatypes(dataframe=df, column_map=columns)
|
|
356
760
|
|
|
@@ -374,6 +778,7 @@ class ReportWrapper:
|
|
|
374
778
|
pandas.DataFrame
|
|
375
779
|
A pandas dataframe containing a list of all the page filters used in the report.
|
|
376
780
|
"""
|
|
781
|
+
self._ensure_pbir()
|
|
377
782
|
|
|
378
783
|
columns = {
|
|
379
784
|
"Page Name": "str",
|
|
@@ -390,51 +795,43 @@ class ReportWrapper:
|
|
|
390
795
|
}
|
|
391
796
|
df = _create_dataframe(columns=columns)
|
|
392
797
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
payload =
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
798
|
+
dfs = []
|
|
799
|
+
for p in self.__all_pages():
|
|
800
|
+
payload = p.get("payload")
|
|
801
|
+
page_id = payload.get("name")
|
|
802
|
+
page_display = payload.get("displayName")
|
|
803
|
+
|
|
804
|
+
if "filterConfig" in payload:
|
|
805
|
+
for flt in payload.get("filterConfig", {}).get("filters", {}):
|
|
806
|
+
filter_name = flt.get("name")
|
|
807
|
+
how_created = flt.get("howCreated")
|
|
808
|
+
locked = flt.get("isLockedInViewMode", False)
|
|
809
|
+
hidden = flt.get("isHiddenInViewMode", False)
|
|
810
|
+
filter_type = flt.get("type", "Basic")
|
|
811
|
+
filter_used = True if "Where" in flt.get("filter", {}) else False
|
|
812
|
+
|
|
813
|
+
entity_property_pairs = helper.find_entity_property_pairs(flt)
|
|
814
|
+
|
|
815
|
+
for object_name, properties in entity_property_pairs.items():
|
|
816
|
+
new_data = {
|
|
817
|
+
"Page Name": page_id,
|
|
818
|
+
"Page Display Name": page_display,
|
|
819
|
+
"Filter Name": filter_name,
|
|
820
|
+
"Type": filter_type,
|
|
821
|
+
"Table Name": properties[0],
|
|
822
|
+
"Object Name": object_name,
|
|
823
|
+
"Object Type": properties[1],
|
|
824
|
+
"Hidden": hidden,
|
|
825
|
+
"Locked": locked,
|
|
826
|
+
"How Created": how_created,
|
|
827
|
+
"Used": filter_used,
|
|
828
|
+
"Page URL": self._get_url(page_name=page_id),
|
|
829
|
+
}
|
|
412
830
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
for object_name, properties in entity_property_pairs.items():
|
|
416
|
-
new_data = {
|
|
417
|
-
"Page Name": page_id,
|
|
418
|
-
"Page Display Name": page_display,
|
|
419
|
-
"Filter Name": filter_name,
|
|
420
|
-
"Type": filter_type,
|
|
421
|
-
"Table Name": properties[0],
|
|
422
|
-
"Object Name": object_name,
|
|
423
|
-
"Object Type": properties[1],
|
|
424
|
-
"Hidden": hidden,
|
|
425
|
-
"Locked": locked,
|
|
426
|
-
"How Created": how_created,
|
|
427
|
-
"Used": filter_used,
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
df = pd.concat(
|
|
431
|
-
[df, pd.DataFrame(new_data, index=[0])],
|
|
432
|
-
ignore_index=True,
|
|
433
|
-
)
|
|
831
|
+
dfs.append(pd.DataFrame(new_data, index=[0]))
|
|
434
832
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
)
|
|
833
|
+
if dfs:
|
|
834
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
438
835
|
|
|
439
836
|
_update_dataframe_datatypes(dataframe=df, column_map=columns)
|
|
440
837
|
|
|
@@ -458,6 +855,7 @@ class ReportWrapper:
|
|
|
458
855
|
pandas.DataFrame
|
|
459
856
|
A pandas dataframe containing a list of all the visual filters used in the report.
|
|
460
857
|
"""
|
|
858
|
+
self._ensure_pbir()
|
|
461
859
|
|
|
462
860
|
columns = {
|
|
463
861
|
"Page Name": "str",
|
|
@@ -475,51 +873,47 @@ class ReportWrapper:
|
|
|
475
873
|
}
|
|
476
874
|
df = _create_dataframe(columns=columns)
|
|
477
875
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
for _, r in self.rdef.iterrows():
|
|
481
|
-
path = r["path"]
|
|
482
|
-
payload = r["payload"]
|
|
483
|
-
if path.endswith("/visual.json"):
|
|
484
|
-
obj_file = base64.b64decode(payload).decode("utf-8")
|
|
485
|
-
obj_json = json.loads(obj_file)
|
|
486
|
-
page_id = visual_mapping.get(path)[0]
|
|
487
|
-
page_display = visual_mapping.get(path)[1]
|
|
488
|
-
visual_name = obj_json.get("name")
|
|
489
|
-
|
|
490
|
-
if "filterConfig" in obj_json:
|
|
491
|
-
for flt in obj_json.get("filterConfig", {}).get("filters", {}):
|
|
492
|
-
filter_name = flt.get("name")
|
|
493
|
-
how_created = flt.get("howCreated")
|
|
494
|
-
locked = flt.get("isLockedInViewMode", False)
|
|
495
|
-
hidden = flt.get("isHiddenInViewMode", False)
|
|
496
|
-
filter_type = flt.get("type", "Basic")
|
|
497
|
-
filter_used = (
|
|
498
|
-
True if "Where" in flt.get("filter", {}) else False
|
|
499
|
-
)
|
|
876
|
+
visual_mapping = self._visual_page_mapping()
|
|
500
877
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
878
|
+
dfs = []
|
|
879
|
+
for v in self.__all_visuals():
|
|
880
|
+
path = v.get("path")
|
|
881
|
+
payload = v.get("payload")
|
|
882
|
+
page_id = visual_mapping.get(path)[0]
|
|
883
|
+
page_display = visual_mapping.get(path)[1]
|
|
884
|
+
visual_name = payload.get("name")
|
|
885
|
+
|
|
886
|
+
if "filterConfig" in payload:
|
|
887
|
+
for flt in payload.get("filterConfig", {}).get("filters", {}):
|
|
888
|
+
filter_name = flt.get("name")
|
|
889
|
+
how_created = flt.get("howCreated")
|
|
890
|
+
locked = flt.get("isLockedInViewMode", False)
|
|
891
|
+
hidden = flt.get("isHiddenInViewMode", False)
|
|
892
|
+
filter_type = flt.get("type", "Basic")
|
|
893
|
+
filter_used = True if "Where" in flt.get("filter", {}) else False
|
|
894
|
+
|
|
895
|
+
entity_property_pairs = helper.find_entity_property_pairs(flt)
|
|
896
|
+
|
|
897
|
+
for object_name, properties in entity_property_pairs.items():
|
|
898
|
+
new_data = {
|
|
899
|
+
"Page Name": page_id,
|
|
900
|
+
"Page Display Name": page_display,
|
|
901
|
+
"Visual Name": visual_name,
|
|
902
|
+
"Filter Name": filter_name,
|
|
903
|
+
"Type": filter_type,
|
|
904
|
+
"Table Name": properties[0],
|
|
905
|
+
"Object Name": object_name,
|
|
906
|
+
"Object Type": properties[1],
|
|
907
|
+
"Hidden": hidden,
|
|
908
|
+
"Locked": locked,
|
|
909
|
+
"How Created": how_created,
|
|
910
|
+
"Used": filter_used,
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
dfs.append(pd.DataFrame(new_data, index=[0]))
|
|
914
|
+
|
|
915
|
+
if dfs:
|
|
916
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
523
917
|
|
|
524
918
|
_update_dataframe_datatypes(dataframe=df, column_map=columns)
|
|
525
919
|
|
|
@@ -540,6 +934,7 @@ class ReportWrapper:
|
|
|
540
934
|
pandas.DataFrame
|
|
541
935
|
A pandas dataframe containing a list of all modified visual interactions used in the report.
|
|
542
936
|
"""
|
|
937
|
+
self._ensure_pbir()
|
|
543
938
|
|
|
544
939
|
columns = {
|
|
545
940
|
"Page Name": "str",
|
|
@@ -550,30 +945,28 @@ class ReportWrapper:
|
|
|
550
945
|
}
|
|
551
946
|
df = _create_dataframe(columns=columns)
|
|
552
947
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
payload =
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
obj_json = json.loads(obj_file)
|
|
559
|
-
page_name = obj_json.get("name")
|
|
560
|
-
page_display = obj_json.get("displayName")
|
|
948
|
+
dfs = []
|
|
949
|
+
for p in self.__all_pages():
|
|
950
|
+
payload = p.get("payload")
|
|
951
|
+
page_name = payload.get("name")
|
|
952
|
+
page_display = payload.get("displayName")
|
|
561
953
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
954
|
+
for vizInt in payload.get("visualInteractions", []):
|
|
955
|
+
sourceVisual = vizInt.get("source")
|
|
956
|
+
targetVisual = vizInt.get("target")
|
|
957
|
+
vizIntType = vizInt.get("type")
|
|
566
958
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
959
|
+
new_data = {
|
|
960
|
+
"Page Name": page_name,
|
|
961
|
+
"Page Display Name": page_display,
|
|
962
|
+
"Source Visual Name": sourceVisual,
|
|
963
|
+
"Target Visual Name": targetVisual,
|
|
964
|
+
"Type": vizIntType,
|
|
965
|
+
}
|
|
966
|
+
dfs.append(pd.DataFrame(new_data, index=[0]))
|
|
967
|
+
|
|
968
|
+
if dfs:
|
|
969
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
577
970
|
|
|
578
971
|
return df
|
|
579
972
|
|
|
@@ -586,6 +979,7 @@ class ReportWrapper:
|
|
|
586
979
|
pandas.DataFrame
|
|
587
980
|
A pandas dataframe containing a list of all pages in the report.
|
|
588
981
|
"""
|
|
982
|
+
self._ensure_pbir()
|
|
589
983
|
|
|
590
984
|
columns = {
|
|
591
985
|
"File Path": "str",
|
|
@@ -607,46 +1001,42 @@ class ReportWrapper:
|
|
|
607
1001
|
}
|
|
608
1002
|
df = _create_dataframe(columns=columns)
|
|
609
1003
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
page_rows = self.rdef[self.rdef["path"].str.endswith("/page.json")]
|
|
613
|
-
pages_row = self.rdef[self.rdef["path"] == "definition/pages/pages.json"]
|
|
1004
|
+
page = self.get(file_path=self._pages_file_path)
|
|
1005
|
+
active_page = page.get("activePageName")
|
|
614
1006
|
|
|
615
|
-
|
|
616
|
-
file_path = r["path"]
|
|
617
|
-
payload = r["payload"]
|
|
1007
|
+
dfV = self.list_visuals()
|
|
618
1008
|
|
|
619
|
-
|
|
1009
|
+
dfs = []
|
|
1010
|
+
for p in self.__all_pages():
|
|
1011
|
+
file_path = p.get("path")
|
|
620
1012
|
page_prefix = file_path[0:-9]
|
|
621
|
-
|
|
622
|
-
page_name =
|
|
623
|
-
height =
|
|
624
|
-
width =
|
|
1013
|
+
payload = p.get("payload")
|
|
1014
|
+
page_name = payload.get("name")
|
|
1015
|
+
height = payload.get("height")
|
|
1016
|
+
width = payload.get("width")
|
|
625
1017
|
|
|
626
1018
|
# Alignment
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
1019
|
+
alignment_value = get_jsonpath_value(
|
|
1020
|
+
data=payload,
|
|
1021
|
+
path="$.objects.displayArea[*].properties.verticalAlignment.expr.Literal.Value",
|
|
1022
|
+
default="Top",
|
|
1023
|
+
remove_quotes=True,
|
|
632
1024
|
)
|
|
633
1025
|
|
|
634
1026
|
# Drillthrough
|
|
635
|
-
matches = parse("$.filterConfig.filters[*].howCreated").find(
|
|
1027
|
+
matches = parse("$.filterConfig.filters[*].howCreated").find(payload)
|
|
636
1028
|
how_created_values = [match.value for match in matches]
|
|
637
1029
|
drill_through = any(value == "Drillthrough" for value in how_created_values)
|
|
638
|
-
# matches = parse("$.filterConfig.filters[*]").find(pageJson)
|
|
639
|
-
# drill_through = any(
|
|
640
|
-
# filt.get("howCreated") == "Drillthrough"
|
|
641
|
-
# for filt in (match.value for match in matches)
|
|
642
|
-
# )
|
|
643
1030
|
|
|
644
1031
|
visual_count = len(
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
1032
|
+
[
|
|
1033
|
+
v
|
|
1034
|
+
for v in self._report_definition.get("parts")
|
|
1035
|
+
if v.get("path").endswith("/visual.json")
|
|
1036
|
+
and v.get("path").startswith(page_prefix)
|
|
648
1037
|
]
|
|
649
1038
|
)
|
|
1039
|
+
|
|
650
1040
|
data_visual_count = len(
|
|
651
1041
|
dfV[(dfV["Page Name"] == page_name) & (dfV["Data Visual"])]
|
|
652
1042
|
)
|
|
@@ -655,24 +1045,25 @@ class ReportWrapper:
|
|
|
655
1045
|
)
|
|
656
1046
|
|
|
657
1047
|
# Page Filter Count
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
1048
|
+
page_filter_count = len(
|
|
1049
|
+
get_jsonpath_value(
|
|
1050
|
+
data=payload, path="$.filterConfig.filters", default=[]
|
|
1051
|
+
)
|
|
661
1052
|
)
|
|
662
1053
|
|
|
663
1054
|
# Hidden
|
|
664
|
-
matches = parse("$.visibility").find(
|
|
1055
|
+
matches = parse("$.visibility").find(payload)
|
|
665
1056
|
is_hidden = any(match.value == "HiddenInViewMode" for match in matches)
|
|
666
1057
|
|
|
667
1058
|
new_data = {
|
|
668
1059
|
"File Path": file_path,
|
|
669
1060
|
"Page Name": page_name,
|
|
670
|
-
"Page Display Name":
|
|
671
|
-
"Display Option":
|
|
1061
|
+
"Page Display Name": payload.get("displayName"),
|
|
1062
|
+
"Display Option": payload.get("displayOption"),
|
|
672
1063
|
"Height": height,
|
|
673
1064
|
"Width": width,
|
|
674
1065
|
"Hidden": is_hidden,
|
|
675
|
-
"Active": False,
|
|
1066
|
+
"Active": True if page_name == active_page else False,
|
|
676
1067
|
"Type": helper.page_type_mapping.get((width, height), "Custom"),
|
|
677
1068
|
"Alignment": alignment_value,
|
|
678
1069
|
"Drillthrough Target Page": drill_through,
|
|
@@ -680,24 +1071,16 @@ class ReportWrapper:
|
|
|
680
1071
|
"Data Visual Count": data_visual_count,
|
|
681
1072
|
"Visible Visual Count": visible_visual_count,
|
|
682
1073
|
"Page Filter Count": page_filter_count,
|
|
1074
|
+
"Page URL": self._get_url(page_name=page_name),
|
|
683
1075
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
page_payload = pages_row["payload"].iloc[0]
|
|
687
|
-
pageFile = base64.b64decode(page_payload).decode("utf-8")
|
|
688
|
-
pageJson = json.loads(pageFile)
|
|
689
|
-
activePage = pageJson["activePageName"]
|
|
690
|
-
|
|
691
|
-
df.loc[df["Page Name"] == activePage, "Active"] = True
|
|
1076
|
+
dfs.append(pd.DataFrame(new_data, index=[0]))
|
|
692
1077
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
)
|
|
1078
|
+
if dfs:
|
|
1079
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
696
1080
|
|
|
697
1081
|
_update_dataframe_datatypes(dataframe=df, column_map=columns)
|
|
698
1082
|
|
|
699
1083
|
return df
|
|
700
|
-
# return df.style.format({"Page URL": _make_clickable})
|
|
701
1084
|
|
|
702
1085
|
def list_visuals(self) -> pd.DataFrame:
|
|
703
1086
|
"""
|
|
@@ -708,6 +1091,7 @@ class ReportWrapper:
|
|
|
708
1091
|
pandas.DataFrame
|
|
709
1092
|
A pandas dataframe containing a list of all visuals in the report.
|
|
710
1093
|
"""
|
|
1094
|
+
self._ensure_pbir()
|
|
711
1095
|
|
|
712
1096
|
columns = {
|
|
713
1097
|
"File Path": "str",
|
|
@@ -739,12 +1123,9 @@ class ReportWrapper:
|
|
|
739
1123
|
}
|
|
740
1124
|
df = _create_dataframe(columns=columns)
|
|
741
1125
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
custom_visuals = rptJson.get("publicCustomVisuals", [])
|
|
746
|
-
page_mapping, visual_mapping = helper.visual_page_mapping(self)
|
|
747
|
-
helper.populate_custom_visual_display_names()
|
|
1126
|
+
report_file = self.get(file_path=self._report_file_path)
|
|
1127
|
+
custom_visuals = report_file.get("publicCustomVisuals", [])
|
|
1128
|
+
visual_mapping = self._visual_page_mapping()
|
|
748
1129
|
agg_type_map = helper._get_agg_type_mapping()
|
|
749
1130
|
|
|
750
1131
|
def contains_key(data, keys_to_check):
|
|
@@ -761,150 +1142,144 @@ class ReportWrapper:
|
|
|
761
1142
|
|
|
762
1143
|
return any(key in all_keys for key in keys_to_check)
|
|
763
1144
|
|
|
764
|
-
|
|
765
|
-
file_path = r["path"]
|
|
766
|
-
payload = r["payload"]
|
|
767
|
-
if file_path.endswith("/visual.json"):
|
|
768
|
-
visual_file = base64.b64decode(payload).decode("utf-8")
|
|
769
|
-
visual_json = json.loads(visual_file)
|
|
770
|
-
page_id = visual_mapping.get(file_path)[0]
|
|
771
|
-
page_display = visual_mapping.get(file_path)[1]
|
|
772
|
-
pos = visual_json.get("position")
|
|
773
|
-
|
|
774
|
-
# Visual Type
|
|
775
|
-
matches = parse("$.visual.visualType").find(visual_json)
|
|
776
|
-
visual_type = matches[0].value if matches else "Group"
|
|
777
|
-
|
|
778
|
-
visual_type_display = helper.vis_type_mapping.get(
|
|
779
|
-
visual_type, visual_type
|
|
780
|
-
)
|
|
781
|
-
cst_value, rst_value, slicer_type = False, False, "N/A"
|
|
1145
|
+
dfs = []
|
|
782
1146
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1147
|
+
for v in self.__all_visuals():
|
|
1148
|
+
path = v.get("path")
|
|
1149
|
+
payload = v.get("payload")
|
|
1150
|
+
page_id = visual_mapping.get(path)[0]
|
|
1151
|
+
page_display = visual_mapping.get(path)[1]
|
|
1152
|
+
pos = payload.get("position")
|
|
786
1153
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
).find(visual_json)
|
|
791
|
-
data_limit = matches[0].value if matches else 0
|
|
1154
|
+
# Visual Type
|
|
1155
|
+
matches = parse("$.visual.visualType").find(payload)
|
|
1156
|
+
visual_type = matches[0].value if matches else "Group"
|
|
792
1157
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
"$.visual.visualContainerObjects.title[0].properties.text.expr"
|
|
796
|
-
).find(visual_json)
|
|
797
|
-
# title = matches[0].value[1:-1] if matches else ""
|
|
798
|
-
title = (
|
|
799
|
-
helper._get_expression(matches[0].value, agg_type_map)
|
|
800
|
-
if matches
|
|
801
|
-
else ""
|
|
802
|
-
)
|
|
1158
|
+
visual_type_display = helper.vis_type_mapping.get(visual_type, visual_type)
|
|
1159
|
+
cst_value, rst_value, slicer_type = False, False, "N/A"
|
|
803
1160
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
).find(visual_json)
|
|
808
|
-
# sub_title = matches[0].value[1:-1] if matches else ""
|
|
809
|
-
sub_title = (
|
|
810
|
-
helper._get_expression(matches[0].value, agg_type_map)
|
|
811
|
-
if matches
|
|
812
|
-
else ""
|
|
813
|
-
)
|
|
1161
|
+
# Visual Filter Count
|
|
1162
|
+
matches = parse("$.filterConfig.filters[*]").find(payload)
|
|
1163
|
+
visual_filter_count = len(matches)
|
|
814
1164
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1165
|
+
# Data Limit
|
|
1166
|
+
matches = parse(
|
|
1167
|
+
'$.filterConfig.filters[?(@.type == "VisualTopN")].filter.Where[*].Condition.VisualTopN.ItemCount'
|
|
1168
|
+
).find(payload)
|
|
1169
|
+
data_limit = matches[0].value if matches else 0
|
|
1170
|
+
|
|
1171
|
+
# Title
|
|
1172
|
+
matches = parse(
|
|
1173
|
+
"$.visual.visualContainerObjects.title[0].properties.text.expr"
|
|
1174
|
+
).find(payload)
|
|
1175
|
+
title = (
|
|
1176
|
+
helper._get_expression(matches[0].value, agg_type_map)
|
|
1177
|
+
if matches
|
|
1178
|
+
else ""
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
# SubTitle
|
|
1182
|
+
matches = parse(
|
|
1183
|
+
"$.visual.visualContainerObjects.subTitle[0].properties.text.expr"
|
|
1184
|
+
).find(payload)
|
|
1185
|
+
sub_title = (
|
|
1186
|
+
helper._get_expression(matches[0].value, agg_type_map)
|
|
1187
|
+
if matches
|
|
1188
|
+
else ""
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
# Alt Text
|
|
1192
|
+
matches = parse(
|
|
1193
|
+
"$.visual.visualContainerObjects.general[0].properties.altText.expr"
|
|
1194
|
+
).find(payload)
|
|
1195
|
+
alt_text = (
|
|
1196
|
+
helper._get_expression(matches[0].value, agg_type_map)
|
|
1197
|
+
if matches
|
|
1198
|
+
else ""
|
|
1199
|
+
)
|
|
825
1200
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1201
|
+
# Show items with no data
|
|
1202
|
+
def find_show_all_with_jsonpath(obj):
|
|
1203
|
+
matches = parse("$..showAll").find(obj)
|
|
1204
|
+
return any(match.value is True for match in matches)
|
|
830
1205
|
|
|
831
|
-
|
|
1206
|
+
show_all_data = find_show_all_with_jsonpath(payload)
|
|
832
1207
|
|
|
833
|
-
|
|
1208
|
+
# Divider
|
|
1209
|
+
matches = parse(
|
|
1210
|
+
"$.visual.visualContainerObjects.divider[0].properties.show.expr.Literal.Value"
|
|
1211
|
+
).find(payload)
|
|
1212
|
+
divider = matches[0] if matches else ""
|
|
1213
|
+
|
|
1214
|
+
# Row/Column Subtotals
|
|
1215
|
+
if visual_type == "pivotTable":
|
|
1216
|
+
cst_matches = parse(
|
|
1217
|
+
"$.visual.objects.subTotals[0].properties.columnSubtotals.expr.Literal.Value"
|
|
1218
|
+
).find(payload)
|
|
1219
|
+
rst_matches = parse(
|
|
1220
|
+
"$.visual.objects.subTotals[0].properties.rowSubtotals.expr.Literal.Value"
|
|
1221
|
+
).find(payload)
|
|
1222
|
+
|
|
1223
|
+
if cst_matches:
|
|
1224
|
+
cst_value = False if cst_matches[0].value == "false" else True
|
|
1225
|
+
|
|
1226
|
+
if rst_matches:
|
|
1227
|
+
rst_value = False if rst_matches[0].value == "false" else True
|
|
1228
|
+
|
|
1229
|
+
# Slicer Type
|
|
1230
|
+
if visual_type == "slicer":
|
|
834
1231
|
matches = parse(
|
|
835
|
-
"$.visual.
|
|
836
|
-
).find(
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
if rst_matches:
|
|
852
|
-
rst_value = False if rst_matches[0].value == "false" else True
|
|
853
|
-
|
|
854
|
-
# Slicer Type
|
|
855
|
-
if visual_type == "slicer":
|
|
856
|
-
matches = parse(
|
|
857
|
-
"$.visual.objects.data[0].properties.mode.expr.Literal.Value"
|
|
858
|
-
).find(visual_json)
|
|
859
|
-
slicer_type = matches[0].value[1:-1] if matches else "N/A"
|
|
860
|
-
|
|
861
|
-
# Data Visual
|
|
862
|
-
is_data_visual = contains_key(
|
|
863
|
-
visual_json,
|
|
864
|
-
[
|
|
865
|
-
"Aggregation",
|
|
866
|
-
"Column",
|
|
867
|
-
"Measure",
|
|
868
|
-
"HierarchyLevel",
|
|
869
|
-
"NativeVisualCalculation",
|
|
870
|
-
],
|
|
871
|
-
)
|
|
1232
|
+
"$.visual.objects.data[0].properties.mode.expr.Literal.Value"
|
|
1233
|
+
).find(payload)
|
|
1234
|
+
slicer_type = matches[0].value[1:-1] if matches else "N/A"
|
|
1235
|
+
|
|
1236
|
+
# Data Visual
|
|
1237
|
+
is_data_visual = contains_key(
|
|
1238
|
+
payload,
|
|
1239
|
+
[
|
|
1240
|
+
"Aggregation",
|
|
1241
|
+
"Column",
|
|
1242
|
+
"Measure",
|
|
1243
|
+
"HierarchyLevel",
|
|
1244
|
+
"NativeVisualCalculation",
|
|
1245
|
+
],
|
|
1246
|
+
)
|
|
872
1247
|
|
|
873
|
-
|
|
874
|
-
|
|
1248
|
+
# Sparkline
|
|
1249
|
+
has_sparkline = contains_key(payload, ["SparklineData"])
|
|
875
1250
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1251
|
+
new_data = {
|
|
1252
|
+
"File Path": path,
|
|
1253
|
+
"Page Name": page_id,
|
|
1254
|
+
"Page Display Name": page_display,
|
|
1255
|
+
"Visual Name": payload.get("name"),
|
|
1256
|
+
"X": pos.get("x"),
|
|
1257
|
+
"Y": pos.get("y"),
|
|
1258
|
+
"Z": pos.get("z"),
|
|
1259
|
+
"Width": pos.get("width"),
|
|
1260
|
+
"Height": pos.get("height"),
|
|
1261
|
+
"Tab Order": pos.get("tabOrder"),
|
|
1262
|
+
"Hidden": payload.get("isHidden", False),
|
|
1263
|
+
"Type": visual_type,
|
|
1264
|
+
"Display Type": visual_type_display,
|
|
1265
|
+
"Title": title,
|
|
1266
|
+
"SubTitle": sub_title,
|
|
1267
|
+
"Custom Visual": visual_type in custom_visuals,
|
|
1268
|
+
"Alt Text": alt_text,
|
|
1269
|
+
"Show Items With No Data": show_all_data,
|
|
1270
|
+
"Divider": divider,
|
|
1271
|
+
"Row SubTotals": rst_value,
|
|
1272
|
+
"Column SubTotals": cst_value,
|
|
1273
|
+
"Slicer Type": slicer_type,
|
|
1274
|
+
"Data Visual": is_data_visual,
|
|
1275
|
+
"Has Sparkline": has_sparkline,
|
|
1276
|
+
"Visual Filter Count": visual_filter_count,
|
|
1277
|
+
"Data Limit": data_limit,
|
|
1278
|
+
}
|
|
1279
|
+
dfs.append(pd.DataFrame(new_data, index=[0]))
|
|
904
1280
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
)
|
|
1281
|
+
if dfs:
|
|
1282
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
908
1283
|
|
|
909
1284
|
grouped_df = (
|
|
910
1285
|
self.list_visual_objects()
|
|
@@ -941,8 +1316,9 @@ class ReportWrapper:
|
|
|
941
1316
|
pandas.DataFrame
|
|
942
1317
|
A pandas dataframe containing a list of all semantic model objects used in each visual in the report.
|
|
943
1318
|
"""
|
|
1319
|
+
self._ensure_pbir()
|
|
944
1320
|
|
|
945
|
-
|
|
1321
|
+
visual_mapping = self._visual_page_mapping()
|
|
946
1322
|
|
|
947
1323
|
columns = {
|
|
948
1324
|
"Page Name": "str",
|
|
@@ -1022,59 +1398,58 @@ class ReportWrapper:
|
|
|
1022
1398
|
|
|
1023
1399
|
return result
|
|
1024
1400
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
visual_json.get("visual", {}).get("query", {}).get("queryState", {})
|
|
1037
|
-
)
|
|
1401
|
+
dfs = []
|
|
1402
|
+
for v in self.__all_visuals():
|
|
1403
|
+
path = v.get("path")
|
|
1404
|
+
payload = v.get("payload")
|
|
1405
|
+
page_id = visual_mapping.get(path)[0]
|
|
1406
|
+
page_display = visual_mapping.get(path)[1]
|
|
1407
|
+
|
|
1408
|
+
entity_property_pairs = find_entity_property_pairs(payload)
|
|
1409
|
+
query_state = (
|
|
1410
|
+
payload.get("visual", {}).get("query", {}).get("queryState", {})
|
|
1411
|
+
)
|
|
1038
1412
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
+
}
|
|
1049
1448
|
|
|
1050
|
-
|
|
1051
|
-
table_name = properties[0]
|
|
1052
|
-
obj_full = f"{table_name}.{object_name}"
|
|
1053
|
-
is_agg = properties[2]
|
|
1054
|
-
format_value = format_mapping.get(obj_full)
|
|
1055
|
-
obj_display = obj_display_mapping.get(obj_full)
|
|
1056
|
-
|
|
1057
|
-
if is_agg:
|
|
1058
|
-
for k, v in format_mapping.items():
|
|
1059
|
-
if obj_full in k:
|
|
1060
|
-
format_value = v
|
|
1061
|
-
new_data = {
|
|
1062
|
-
"Page Name": page_id,
|
|
1063
|
-
"Page Display Name": page_display,
|
|
1064
|
-
"Visual Name": visual_json.get("name"),
|
|
1065
|
-
"Table Name": table_name,
|
|
1066
|
-
"Object Name": object_name,
|
|
1067
|
-
"Object Type": properties[1],
|
|
1068
|
-
"Implicit Measure": is_agg,
|
|
1069
|
-
"Sparkline": properties[4],
|
|
1070
|
-
"Visual Calc": properties[3],
|
|
1071
|
-
"Format": format_value,
|
|
1072
|
-
"Object Display Name": obj_display,
|
|
1073
|
-
}
|
|
1449
|
+
dfs.append(pd.DataFrame(new_data, index=[0]))
|
|
1074
1450
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
)
|
|
1451
|
+
if dfs:
|
|
1452
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
1078
1453
|
|
|
1079
1454
|
if extended:
|
|
1080
1455
|
df = self._add_extended(dataframe=df)
|
|
@@ -1099,6 +1474,7 @@ class ReportWrapper:
|
|
|
1099
1474
|
pandas.DataFrame
|
|
1100
1475
|
A pandas dataframe showing the semantic model objects used in the report.
|
|
1101
1476
|
"""
|
|
1477
|
+
self._ensure_pbir()
|
|
1102
1478
|
|
|
1103
1479
|
from sempy_labs.tom import connect_semantic_model
|
|
1104
1480
|
|
|
@@ -1118,7 +1494,7 @@ class ReportWrapper:
|
|
|
1118
1494
|
|
|
1119
1495
|
rf_subset = rf[["Table Name", "Object Name", "Object Type"]].copy()
|
|
1120
1496
|
rf_subset["Report Source"] = "Report Filter"
|
|
1121
|
-
rf_subset["Report Source Object"] = self.
|
|
1497
|
+
rf_subset["Report Source Object"] = self._report_name
|
|
1122
1498
|
|
|
1123
1499
|
pf_subset = pf[
|
|
1124
1500
|
["Table Name", "Object Name", "Object Type", "Page Display Name"]
|
|
@@ -1162,9 +1538,9 @@ class ReportWrapper:
|
|
|
1162
1538
|
)
|
|
1163
1539
|
|
|
1164
1540
|
if extended:
|
|
1165
|
-
dataset_id, dataset_name, dataset_workspace_id, dataset_workspace_name = (
|
|
1541
|
+
(dataset_id, dataset_name, dataset_workspace_id, dataset_workspace_name) = (
|
|
1166
1542
|
resolve_dataset_from_report(
|
|
1167
|
-
report=self.
|
|
1543
|
+
report=self._report_id, workspace=self._workspace_id
|
|
1168
1544
|
)
|
|
1169
1545
|
)
|
|
1170
1546
|
|
|
@@ -1208,7 +1584,7 @@ class ReportWrapper:
|
|
|
1208
1584
|
)
|
|
1209
1585
|
dataset_id, dataset_name, dataset_workspace_id, dataset_workspace_name = (
|
|
1210
1586
|
resolve_dataset_from_report(
|
|
1211
|
-
report=self.
|
|
1587
|
+
report=self._report_id, workspace=self._workspace_id
|
|
1212
1588
|
)
|
|
1213
1589
|
)
|
|
1214
1590
|
dep = get_measure_dependencies(
|
|
@@ -1243,8 +1619,7 @@ class ReportWrapper:
|
|
|
1243
1619
|
pandas.DataFrame
|
|
1244
1620
|
A pandas dataframe containing a list of all bookmarks in the report.
|
|
1245
1621
|
"""
|
|
1246
|
-
|
|
1247
|
-
rd = self.rdef
|
|
1622
|
+
self._ensure_pbir()
|
|
1248
1623
|
|
|
1249
1624
|
columns = {
|
|
1250
1625
|
"File Path": "str",
|
|
@@ -1257,31 +1632,34 @@ class ReportWrapper:
|
|
|
1257
1632
|
}
|
|
1258
1633
|
df = _create_dataframe(columns=columns)
|
|
1259
1634
|
|
|
1260
|
-
|
|
1635
|
+
bookmarks = [
|
|
1636
|
+
o
|
|
1637
|
+
for o in self._report_definition.get("parts")
|
|
1638
|
+
if o.get("path").endswith("/bookmark.json")
|
|
1639
|
+
]
|
|
1261
1640
|
|
|
1262
|
-
|
|
1263
|
-
path = r["path"]
|
|
1264
|
-
payload = r["payload"]
|
|
1641
|
+
dfs = []
|
|
1265
1642
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1643
|
+
for b in bookmarks:
|
|
1644
|
+
path = b.get("path")
|
|
1645
|
+
payload = b.get("payload")
|
|
1268
1646
|
|
|
1269
|
-
bookmark_name =
|
|
1270
|
-
bookmark_display =
|
|
1271
|
-
rpt_page_id =
|
|
1272
|
-
page_id, page_display
|
|
1273
|
-
|
|
1647
|
+
bookmark_name = payload.get("name")
|
|
1648
|
+
bookmark_display = payload.get("displayName")
|
|
1649
|
+
rpt_page_id = payload.get("explorationState", {}).get("activeSection")
|
|
1650
|
+
(page_id, page_display) = self._resolve_page_name_and_display_name(
|
|
1651
|
+
rpt_page_id
|
|
1274
1652
|
)
|
|
1275
1653
|
|
|
1276
|
-
for rptPg in
|
|
1654
|
+
for rptPg in payload.get("explorationState", {}).get("sections", {}):
|
|
1277
1655
|
for visual_name in (
|
|
1278
|
-
|
|
1656
|
+
payload.get("explorationState", {})
|
|
1279
1657
|
.get("sections", {})
|
|
1280
1658
|
.get(rptPg, {})
|
|
1281
1659
|
.get("visualContainers", {})
|
|
1282
1660
|
):
|
|
1283
1661
|
if (
|
|
1284
|
-
|
|
1662
|
+
payload.get("explorationState", {})
|
|
1285
1663
|
.get("sections", {})
|
|
1286
1664
|
.get(rptPg, {})
|
|
1287
1665
|
.get("visualContainers", {})
|
|
@@ -1304,9 +1682,10 @@ class ReportWrapper:
|
|
|
1304
1682
|
"Visual Name": visual_name,
|
|
1305
1683
|
"Visual Hidden": visual_hidden,
|
|
1306
1684
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1685
|
+
dfs.append(pd.DataFrame(new_data, index=[0]))
|
|
1686
|
+
|
|
1687
|
+
if dfs:
|
|
1688
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
1310
1689
|
|
|
1311
1690
|
_update_dataframe_datatypes(dataframe=df, column_map=columns)
|
|
1312
1691
|
|
|
@@ -1325,6 +1704,8 @@ class ReportWrapper:
|
|
|
1325
1704
|
A pandas dataframe containing a list of all report-level measures in the report.
|
|
1326
1705
|
"""
|
|
1327
1706
|
|
|
1707
|
+
self._ensure_pbir()
|
|
1708
|
+
|
|
1328
1709
|
columns = {
|
|
1329
1710
|
"Measure Name": "str",
|
|
1330
1711
|
"Table Name": "str",
|
|
@@ -1335,14 +1716,12 @@ class ReportWrapper:
|
|
|
1335
1716
|
|
|
1336
1717
|
df = _create_dataframe(columns=columns)
|
|
1337
1718
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
if len(rd_filt) == 1:
|
|
1341
|
-
payload = rd_filt["payload"].iloc[0]
|
|
1342
|
-
obj_file = base64.b64decode(payload).decode("utf-8")
|
|
1343
|
-
obj_json = json.loads(obj_file)
|
|
1719
|
+
report_file = self.get(file_path=self._report_extensions_path)
|
|
1344
1720
|
|
|
1345
|
-
|
|
1721
|
+
dfs = []
|
|
1722
|
+
if report_file:
|
|
1723
|
+
payload = report_file.get("payload")
|
|
1724
|
+
for e in payload.get("entities", []):
|
|
1346
1725
|
table_name = e.get("name")
|
|
1347
1726
|
for m in e.get("measures", []):
|
|
1348
1727
|
measure_name = m.get("name")
|
|
@@ -1357,82 +1736,62 @@ class ReportWrapper:
|
|
|
1357
1736
|
"Data Type": data_type,
|
|
1358
1737
|
"Format String": format_string,
|
|
1359
1738
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1739
|
+
dfs.append(pd.DataFrame(new_data, index=[0]))
|
|
1740
|
+
|
|
1741
|
+
if dfs:
|
|
1742
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
1363
1743
|
|
|
1364
1744
|
return df
|
|
1365
1745
|
|
|
1366
|
-
def
|
|
1746
|
+
def get_theme(self, theme_type: str = "baseTheme") -> dict:
|
|
1367
1747
|
"""
|
|
1368
|
-
|
|
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".
|
|
1369
1754
|
|
|
1370
1755
|
Returns
|
|
1371
1756
|
-------
|
|
1372
|
-
|
|
1373
|
-
|
|
1757
|
+
dict
|
|
1758
|
+
The theme.json file
|
|
1374
1759
|
"""
|
|
1375
1760
|
|
|
1376
|
-
|
|
1377
|
-
"Type": "str",
|
|
1378
|
-
"Object Name": "str",
|
|
1379
|
-
"Annotation Name": "str",
|
|
1380
|
-
"Annotation Value": "str",
|
|
1381
|
-
}
|
|
1382
|
-
df = _create_dataframe(columns=columns)
|
|
1761
|
+
self._ensure_pbir()
|
|
1383
1762
|
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
payload = r["payload"]
|
|
1387
|
-
path = r["path"]
|
|
1388
|
-
if path == "definition/report.json":
|
|
1389
|
-
file = _decode_b64(payload)
|
|
1390
|
-
json_file = json.loads(file)
|
|
1391
|
-
if "annotations" in json_file:
|
|
1392
|
-
for ann in json_file["annotations"]:
|
|
1393
|
-
new_data = {
|
|
1394
|
-
"Type": "Report",
|
|
1395
|
-
"Object Name": self._report,
|
|
1396
|
-
"Annotation Name": ann.get("name"),
|
|
1397
|
-
"Annotation Value": ann.get("value"),
|
|
1398
|
-
}
|
|
1399
|
-
df = pd.concat(
|
|
1400
|
-
[df, pd.DataFrame(new_data, index=[0])], ignore_index=True
|
|
1401
|
-
)
|
|
1402
|
-
elif path.endswith("/page.json"):
|
|
1403
|
-
file = _decode_b64(payload)
|
|
1404
|
-
json_file = json.loads(file)
|
|
1405
|
-
if "annotations" in json_file:
|
|
1406
|
-
for ann in json_file["annotations"]:
|
|
1407
|
-
new_data = {
|
|
1408
|
-
"Type": "Page",
|
|
1409
|
-
"Object Name": json_file.get("displayName"),
|
|
1410
|
-
"Annotation Name": ann.get("name"),
|
|
1411
|
-
"Annotation Value": ann.get("value"),
|
|
1412
|
-
}
|
|
1413
|
-
df = pd.concat(
|
|
1414
|
-
[df, pd.DataFrame(new_data, index=[0])], ignore_index=True
|
|
1415
|
-
)
|
|
1416
|
-
elif path.endswith("/visual.json"):
|
|
1417
|
-
file = _decode_b64(payload)
|
|
1418
|
-
json_file = json.loads(file)
|
|
1419
|
-
page_display = visual_mapping.get(path)[1]
|
|
1420
|
-
visual_name = json_file.get("name")
|
|
1421
|
-
if "annotations" in json_file:
|
|
1422
|
-
for ann in json_file["annotations"]:
|
|
1423
|
-
new_data = {
|
|
1424
|
-
"Type": "Visual",
|
|
1425
|
-
"Object Name": f"'{page_display}'[{visual_name}]",
|
|
1426
|
-
"Annotation Name": ann.get("name"),
|
|
1427
|
-
"Annotation Value": ann.get("value"),
|
|
1428
|
-
}
|
|
1429
|
-
df = pd.concat(
|
|
1430
|
-
[df, pd.DataFrame(new_data, index=[0])], ignore_index=True
|
|
1431
|
-
)
|
|
1763
|
+
theme_types = ["baseTheme", "customTheme"]
|
|
1764
|
+
theme_type = theme_type.lower()
|
|
1432
1765
|
|
|
1433
|
-
|
|
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
|
+
)
|
|
1774
|
+
|
|
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)
|
|
1434
1793
|
|
|
1435
|
-
#
|
|
1794
|
+
# Action functions
|
|
1436
1795
|
def set_theme(self, theme_file_path: str):
|
|
1437
1796
|
"""
|
|
1438
1797
|
Sets a custom theme for a report based on a theme .json file.
|
|
@@ -1445,98 +1804,85 @@ class ReportWrapper:
|
|
|
1445
1804
|
Example for web url: file_path = 'https://raw.githubusercontent.com/PowerBiDevCamp/FabricUserApiDemo/main/FabricUserApiDemo/DefinitionTemplates/Shared/Reports/StaticResources/SharedResources/BaseThemes/CY23SU08.json'
|
|
1446
1805
|
"""
|
|
1447
1806
|
|
|
1448
|
-
|
|
1449
|
-
theme_version = "5.
|
|
1450
|
-
request_body = {"definition": {"parts": []}}
|
|
1807
|
+
self._ensure_pbir()
|
|
1808
|
+
theme_version = "5.6.4"
|
|
1451
1809
|
|
|
1810
|
+
# Open file
|
|
1452
1811
|
if not theme_file_path.endswith(".json"):
|
|
1453
1812
|
raise ValueError(
|
|
1454
1813
|
f"{icons.red_dot} The '{theme_file_path}' theme file path must be a .json file."
|
|
1455
1814
|
)
|
|
1456
1815
|
elif theme_file_path.startswith("https://"):
|
|
1457
1816
|
response = requests.get(theme_file_path)
|
|
1458
|
-
|
|
1459
|
-
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
|
+
):
|
|
1460
1821
|
with open(theme_file_path, "r", encoding="utf-8-sig") as file:
|
|
1461
|
-
|
|
1822
|
+
theme_file = json.load(file)
|
|
1462
1823
|
else:
|
|
1463
1824
|
ValueError(
|
|
1464
1825
|
f"{icons.red_dot} Incorrect theme file path value '{theme_file_path}'."
|
|
1465
1826
|
)
|
|
1466
1827
|
|
|
1467
|
-
theme_name =
|
|
1828
|
+
theme_name = theme_file.get("name")
|
|
1468
1829
|
theme_name_full = f"{theme_name}.json"
|
|
1469
|
-
rd = self.rdef
|
|
1470
1830
|
|
|
1471
|
-
# Add theme.json file
|
|
1472
|
-
|
|
1473
|
-
|
|
1831
|
+
# Add theme.json file
|
|
1832
|
+
self.add(
|
|
1833
|
+
file_path=f"StaticResources/RegisteredResources/{theme_name_full}",
|
|
1834
|
+
payload=theme_file,
|
|
1835
|
+
)
|
|
1836
|
+
|
|
1837
|
+
custom_theme = {
|
|
1838
|
+
"name": theme_name_full,
|
|
1839
|
+
"reportVersionAtImport": theme_version,
|
|
1840
|
+
"type": "RegisteredResources",
|
|
1841
|
+
}
|
|
1474
1842
|
|
|
1475
|
-
|
|
1843
|
+
self.set_json(
|
|
1844
|
+
file_path=self._report_file_path,
|
|
1845
|
+
json_path="$.themeCollection.customTheme",
|
|
1846
|
+
json_value=custom_theme,
|
|
1847
|
+
)
|
|
1476
1848
|
|
|
1477
|
-
|
|
1849
|
+
# Update
|
|
1850
|
+
report_file = self.get(
|
|
1851
|
+
file_path=self._report_file_path, json_path="$.resourcePackages"
|
|
1852
|
+
)
|
|
1853
|
+
new_item = {
|
|
1478
1854
|
"name": theme_name_full,
|
|
1479
1855
|
"path": theme_name_full,
|
|
1480
1856
|
"type": "CustomTheme",
|
|
1481
1857
|
}
|
|
1858
|
+
# Find or create RegisteredResources
|
|
1859
|
+
registered = next(
|
|
1860
|
+
(res for res in report_file if res["name"] == "RegisteredResources"), None
|
|
1861
|
+
)
|
|
1482
1862
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
"name": theme_name_full,
|
|
1500
|
-
"reportVersionAtImport": theme_version,
|
|
1501
|
-
"type": resource_type,
|
|
1502
|
-
}
|
|
1503
|
-
else:
|
|
1504
|
-
rptJson["themeCollection"]["customTheme"]["name"] = theme_name_full
|
|
1505
|
-
rptJson["themeCollection"]["customTheme"]["type"] = resource_type
|
|
1506
|
-
|
|
1507
|
-
for package in rptJson["resourcePackages"]:
|
|
1508
|
-
package["items"] = [
|
|
1509
|
-
item
|
|
1510
|
-
for item in package["items"]
|
|
1511
|
-
if item["type"] != "CustomTheme"
|
|
1512
|
-
]
|
|
1513
|
-
|
|
1514
|
-
if not any(
|
|
1515
|
-
package["name"] == resource_type
|
|
1516
|
-
for package in rptJson["resourcePackages"]
|
|
1517
|
-
):
|
|
1518
|
-
new_registered_resources = {
|
|
1519
|
-
"name": resource_type,
|
|
1520
|
-
"type": resource_type,
|
|
1521
|
-
"items": [new_theme],
|
|
1522
|
-
}
|
|
1523
|
-
rptJson["resourcePackages"].append(new_registered_resources)
|
|
1524
|
-
else:
|
|
1525
|
-
names = [
|
|
1526
|
-
rp["name"] for rp in rptJson["resourcePackages"][1]["items"]
|
|
1527
|
-
]
|
|
1528
|
-
|
|
1529
|
-
if theme_name_full not in names:
|
|
1530
|
-
rptJson["resourcePackages"][1]["items"].append(new_theme)
|
|
1531
|
-
|
|
1532
|
-
file_payload = _conv_b64(rptJson)
|
|
1533
|
-
_add_part(request_body, path, file_payload)
|
|
1534
|
-
|
|
1535
|
-
self.update_report(request_body=request_body)
|
|
1536
|
-
print(
|
|
1537
|
-
f"{icons.green_dot} The '{theme_name}' theme has been set as the theme for the '{self._report}' report within the '{self._workspace_name}' workspace."
|
|
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,
|
|
1538
1879
|
)
|
|
1539
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
|
+
|
|
1540
1886
|
def set_active_page(self, page_name: str):
|
|
1541
1887
|
"""
|
|
1542
1888
|
Sets the active page (first page displayed when opening a report) for a report.
|
|
@@ -1546,25 +1892,22 @@ class ReportWrapper:
|
|
|
1546
1892
|
page_name : str
|
|
1547
1893
|
The page name or page display name of the report.
|
|
1548
1894
|
"""
|
|
1895
|
+
self._ensure_pbir()
|
|
1549
1896
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
self, page_name=page_name
|
|
1897
|
+
(page_id, page_display_name) = self._resolve_page_name_and_display_name(
|
|
1898
|
+
page_name
|
|
1553
1899
|
)
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
json_file = json.loads(page_file)
|
|
1559
|
-
json_file["activePageName"] = page_id
|
|
1560
|
-
file_payload = _conv_b64(json_file)
|
|
1561
|
-
|
|
1562
|
-
self._update_single_file(file_name=pages_file, new_payload=file_payload)
|
|
1563
|
-
|
|
1564
|
-
print(
|
|
1565
|
-
f"{icons.green_dot} The '{page_display_name}' page has been set as the active page in the '{self._report}' report within the '{self._workspace_name}' workspace."
|
|
1900
|
+
self.set_json(
|
|
1901
|
+
file_path=self._pages_file_path,
|
|
1902
|
+
json_path="$.activePageName",
|
|
1903
|
+
json_value=page_id,
|
|
1566
1904
|
)
|
|
1567
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
|
+
|
|
1568
1911
|
def set_page_type(self, page_name: str, page_type: str):
|
|
1569
1912
|
"""
|
|
1570
1913
|
Changes the page type of a report page.
|
|
@@ -1576,43 +1919,110 @@ class ReportWrapper:
|
|
|
1576
1919
|
page_type : str
|
|
1577
1920
|
The page type. Valid page types: 'Tooltip', 'Letter', '4:3', '16:9'.
|
|
1578
1921
|
"""
|
|
1922
|
+
self._ensure_pbir()
|
|
1923
|
+
|
|
1924
|
+
if page_type not in helper.page_types:
|
|
1925
|
+
raise ValueError(
|
|
1926
|
+
f"{icons.red_dot} Invalid page type. Valid options: {helper.page_types}."
|
|
1927
|
+
)
|
|
1928
|
+
|
|
1929
|
+
letter_key = next(
|
|
1930
|
+
(
|
|
1931
|
+
key
|
|
1932
|
+
for key, value in helper.page_type_mapping.items()
|
|
1933
|
+
if value == page_type
|
|
1934
|
+
),
|
|
1935
|
+
None,
|
|
1936
|
+
)
|
|
1937
|
+
if letter_key:
|
|
1938
|
+
width, height = letter_key
|
|
1939
|
+
else:
|
|
1940
|
+
raise ValueError(
|
|
1941
|
+
f"{icons.red_dot} Invalid page_type parameter. Valid options: ['Tooltip', 'Letter', '4:3', '16:9']."
|
|
1942
|
+
)
|
|
1943
|
+
|
|
1944
|
+
(file_path, page_id, page_display_name) = (
|
|
1945
|
+
self.__resolve_page_name_and_display_name_file_path(page_name)
|
|
1946
|
+
)
|
|
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)
|
|
1973
|
+
)
|
|
1974
|
+
|
|
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):
|
|
1992
|
+
"""
|
|
1993
|
+
Hides all tooltip pages and drillthrough pages in a report.
|
|
1994
|
+
"""
|
|
1995
|
+
|
|
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:
|
|
2002
|
+
print(
|
|
2003
|
+
f"{icons.green_dot} There are no Tooltip or Drillthrough pages in the '{self._report_name}' report within the '{self._workspace_name}' workspace."
|
|
2004
|
+
)
|
|
2005
|
+
return
|
|
2006
|
+
|
|
2007
|
+
for _, r in dfP_filt.iterrows():
|
|
2008
|
+
page_name = r["Page Name"]
|
|
2009
|
+
self.set_page_visibility(page_name=page_name, hidden=True)
|
|
1579
2010
|
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
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
|
+
"""
|
|
1584
2015
|
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
if value == page_type
|
|
1590
|
-
),
|
|
1591
|
-
None,
|
|
2016
|
+
self.remove(
|
|
2017
|
+
file_path="definition/pages/*/visual.json",
|
|
2018
|
+
json_path="$..showAll",
|
|
2019
|
+
verbose=False,
|
|
1592
2020
|
)
|
|
1593
|
-
if letter_key:
|
|
1594
|
-
width, height = letter_key
|
|
1595
|
-
else:
|
|
1596
|
-
raise ValueError(
|
|
1597
|
-
f"{icons.red_dot} Invalid page_type parameter. Valid options: ['Tooltip', 'Letter', '4:3', '16:9']."
|
|
1598
|
-
)
|
|
1599
2021
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
payload = rd_filt["payload"].iloc[0]
|
|
1605
|
-
page_file = _decode_b64(payload)
|
|
1606
|
-
json_file = json.loads(page_file)
|
|
1607
|
-
json_file["width"] = width
|
|
1608
|
-
json_file["height"] = height
|
|
1609
|
-
file_payload = _conv_b64(json_file)
|
|
1610
|
-
|
|
1611
|
-
self._update_single_file(file_name=file_path, new_payload=file_payload)
|
|
1612
|
-
|
|
1613
|
-
print(
|
|
1614
|
-
f"{icons.green_dot} The '{page_display_name}' page has been updated to the '{page_type}' page type."
|
|
1615
|
-
)
|
|
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
|
+
)
|
|
1616
2026
|
|
|
1617
2027
|
def remove_unnecessary_custom_visuals(self):
|
|
1618
2028
|
"""
|
|
@@ -1620,46 +2030,33 @@ class ReportWrapper:
|
|
|
1620
2030
|
"""
|
|
1621
2031
|
|
|
1622
2032
|
dfCV = self.list_custom_visuals()
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
for _, r in dfCV.iterrows():
|
|
1630
|
-
cv = r["Custom Visual Name"]
|
|
1631
|
-
cv_display = r["Custom Visual Display Name"]
|
|
1632
|
-
dfV_filt = dfV[dfV["Type"] == cv]
|
|
1633
|
-
if len(dfV_filt) == 0:
|
|
1634
|
-
cv_remove.append(cv) # Add to the list for removal
|
|
1635
|
-
cv_remove_display.append(cv_display)
|
|
1636
|
-
if len(cv_remove) == 0:
|
|
2033
|
+
df = dfCV[dfCV["Used in Report"] == False]
|
|
2034
|
+
|
|
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:
|
|
1637
2039
|
print(
|
|
1638
|
-
f"{icons.
|
|
2040
|
+
f"{icons.red_dot} There are no unnecessary custom visuals in the '{self._report_name}' report within the '{self._workspace_name}' workspace."
|
|
1639
2041
|
)
|
|
1640
2042
|
return
|
|
1641
2043
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
if
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
if item not in cv_remove
|
|
1652
|
-
]
|
|
1653
|
-
|
|
1654
|
-
payload = _conv_b64(rpt_json)
|
|
1655
|
-
|
|
1656
|
-
_add_part(request_body, file_path, payload)
|
|
1657
|
-
|
|
1658
|
-
self.update_report(request_body=request_body)
|
|
1659
|
-
print(
|
|
1660
|
-
f"{icons.green_dot} The {cv_remove_display} custom visuals have been removed from the '{self._report}' report within the '{self._workspace_name}' workspace."
|
|
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,
|
|
1661
2053
|
)
|
|
1662
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
|
+
|
|
1663
2060
|
def migrate_report_level_measures(self, measures: Optional[str | List[str]] = None):
|
|
1664
2061
|
"""
|
|
1665
2062
|
Moves all report-level measures from the report to the semantic model on which the report is based.
|
|
@@ -1670,555 +2067,883 @@ class ReportWrapper:
|
|
|
1670
2067
|
A measure or list of measures to move to the semantic model.
|
|
1671
2068
|
Defaults to None which resolves to moving all report-level measures to the semantic model.
|
|
1672
2069
|
"""
|
|
2070
|
+
self._ensure_pbir()
|
|
1673
2071
|
|
|
1674
2072
|
from sempy_labs.tom import connect_semantic_model
|
|
1675
2073
|
|
|
1676
2074
|
rlm = self.list_report_level_measures()
|
|
1677
|
-
if
|
|
2075
|
+
if rlm.empty:
|
|
1678
2076
|
print(
|
|
1679
|
-
f"{icons.
|
|
2077
|
+
f"{icons.info} The '{self._report_name}' report within the '{self._workspace_name}' workspace has no report-level measures."
|
|
1680
2078
|
)
|
|
1681
2079
|
return
|
|
1682
2080
|
|
|
1683
2081
|
dataset_id, dataset_name, dataset_workspace_id, dataset_workspace_name = (
|
|
1684
2082
|
resolve_dataset_from_report(
|
|
1685
|
-
report=self.
|
|
2083
|
+
report=self._report_id, workspace=self._workspace_id
|
|
1686
2084
|
)
|
|
1687
2085
|
)
|
|
1688
2086
|
|
|
1689
2087
|
if isinstance(measures, str):
|
|
1690
2088
|
measures = [measures]
|
|
1691
2089
|
|
|
1692
|
-
|
|
1693
|
-
rpt_file = "definition/reportExtensions.json"
|
|
1694
|
-
|
|
1695
|
-
rd = self.rdef
|
|
1696
|
-
rd_filt = rd[rd["path"] == rpt_file]
|
|
1697
|
-
payload = rd_filt["payload"].iloc[0]
|
|
1698
|
-
extFile = base64.b64decode(payload).decode("utf-8")
|
|
1699
|
-
extJson = json.loads(extFile)
|
|
2090
|
+
file = self.get(file_path=self._report_extensions_path)
|
|
1700
2091
|
|
|
1701
2092
|
mCount = 0
|
|
1702
2093
|
with connect_semantic_model(
|
|
1703
2094
|
dataset=dataset_id, readonly=False, workspace=dataset_workspace_id
|
|
1704
2095
|
) as tom:
|
|
2096
|
+
existing_measures = [m.Name for m in tom.all_measures()]
|
|
1705
2097
|
for _, r in rlm.iterrows():
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
2098
|
+
table_name = r["Table Name"]
|
|
2099
|
+
measure_name = r["Measure Name"]
|
|
2100
|
+
expr = r["Expression"]
|
|
1709
2101
|
# mDataType = r["Data Type"]
|
|
1710
|
-
|
|
2102
|
+
format_string = r["Format String"]
|
|
1711
2103
|
# Add measures to the model
|
|
1712
|
-
if
|
|
2104
|
+
if (
|
|
2105
|
+
measure_name in measures or measures is None
|
|
2106
|
+
) and measure_name not in existing_measures:
|
|
1713
2107
|
tom.add_measure(
|
|
1714
|
-
table_name=
|
|
1715
|
-
measure_name=
|
|
1716
|
-
expression=
|
|
1717
|
-
format_string=
|
|
2108
|
+
table_name=table_name,
|
|
2109
|
+
measure_name=measure_name,
|
|
2110
|
+
expression=expr,
|
|
2111
|
+
format_string=format_string,
|
|
1718
2112
|
)
|
|
1719
2113
|
tom.set_annotation(
|
|
1720
|
-
object=tom.model.Tables[
|
|
2114
|
+
object=tom.model.Tables[table_name].Measures[measure_name],
|
|
1721
2115
|
name="semanticlinklabs",
|
|
1722
2116
|
value="reportlevelmeasure",
|
|
1723
2117
|
)
|
|
1724
2118
|
mCount += 1
|
|
1725
2119
|
# Remove measures from the json
|
|
1726
2120
|
if measures is not None and len(measures) < mCount:
|
|
1727
|
-
for e in
|
|
2121
|
+
for e in file["entities"]:
|
|
1728
2122
|
e["measures"] = [
|
|
1729
2123
|
measure
|
|
1730
2124
|
for measure in e["measures"]
|
|
1731
2125
|
if measure["name"] not in measures
|
|
1732
2126
|
]
|
|
1733
|
-
|
|
1734
|
-
entity for entity in
|
|
2127
|
+
file["entities"] = [
|
|
2128
|
+
entity for entity in file["entities"] if entity["measures"]
|
|
1735
2129
|
]
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
# Add unchanged payloads
|
|
1740
|
-
for _, r in rd.iterrows():
|
|
1741
|
-
path = r["path"]
|
|
1742
|
-
payload = r["payload"]
|
|
1743
|
-
if path != rpt_file:
|
|
1744
|
-
_add_part(request_body, path, payload)
|
|
1745
|
-
|
|
1746
|
-
self.update_report(request_body=request_body)
|
|
1747
|
-
print(
|
|
1748
|
-
f"{icons.green_dot} The report-level measures have been migrated to the '{dataset_name}' semantic model within the '{dataset_workspace_name}' workspace."
|
|
1749
|
-
)
|
|
2130
|
+
self.update(file_path=self._report_extensions_path, payload=file)
|
|
2131
|
+
# what about if measures is None?
|
|
1750
2132
|
|
|
1751
|
-
|
|
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:
|
|
1752
2140
|
"""
|
|
1753
|
-
|
|
2141
|
+
Shows a list of annotations in the report.
|
|
1754
2142
|
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
hidden : bool
|
|
1760
|
-
If set to True, hides the report page.
|
|
1761
|
-
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.
|
|
1762
2147
|
"""
|
|
1763
2148
|
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
2149
|
+
columns = {
|
|
2150
|
+
"Type": "str",
|
|
2151
|
+
"Object Name": "str",
|
|
2152
|
+
"Annotation Name": "str",
|
|
2153
|
+
"Annotation Value": "str",
|
|
2154
|
+
}
|
|
2155
|
+
df = _create_dataframe(columns=columns)
|
|
1768
2156
|
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
if
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2157
|
+
visual_mapping = self._visual_page_mapping()
|
|
2158
|
+
report_file = self.get(file_path="definition/report.json")
|
|
2159
|
+
|
|
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]))
|
|
1779
2199
|
|
|
1780
|
-
|
|
2200
|
+
if dfs:
|
|
2201
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
1781
2202
|
|
|
1782
|
-
|
|
1783
|
-
f"{icons.green_dot} The '{page_display_name}' page has been set to {visibility}."
|
|
1784
|
-
)
|
|
2203
|
+
return df
|
|
1785
2204
|
|
|
1786
|
-
def
|
|
2205
|
+
def _add_image(self, image_path: str, resource_name: Optional[str] = None) -> str:
|
|
1787
2206
|
"""
|
|
1788
|
-
|
|
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.
|
|
1789
2220
|
"""
|
|
2221
|
+
self._ensure_pbir()
|
|
1790
2222
|
|
|
1791
|
-
|
|
1792
|
-
dfP_filt = dfP[
|
|
1793
|
-
(dfP["Type"] == "Tooltip") | (dfP["Drillthrough Target Page"] == True)
|
|
1794
|
-
]
|
|
2223
|
+
id = generate_number_guid()
|
|
1795
2224
|
|
|
1796
|
-
if
|
|
1797
|
-
|
|
1798
|
-
|
|
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,
|
|
1799
2252
|
)
|
|
1800
|
-
return
|
|
1801
2253
|
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
2254
|
+
# Add to report.json file
|
|
2255
|
+
self.__add_to_registered_resources(
|
|
2256
|
+
name=file_name,
|
|
2257
|
+
path=file_name,
|
|
2258
|
+
type="Image",
|
|
2259
|
+
)
|
|
1805
2260
|
|
|
1806
|
-
|
|
1807
|
-
"""
|
|
1808
|
-
Disables the `show items with no data <https://learn.microsoft.com/power-bi/create-reports/desktop-show-items-no-data>`_ property in all visuals within the report.
|
|
1809
|
-
"""
|
|
2261
|
+
return file_name
|
|
1810
2262
|
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
if isinstance(obj, dict):
|
|
1815
|
-
if key_to_delete in obj:
|
|
1816
|
-
del obj[key_to_delete]
|
|
1817
|
-
for key, value in obj.items():
|
|
1818
|
-
delete_key_in_json(value, key_to_delete)
|
|
1819
|
-
elif isinstance(obj, list):
|
|
1820
|
-
for item in obj:
|
|
1821
|
-
delete_key_in_json(item, key_to_delete)
|
|
1822
|
-
|
|
1823
|
-
rd = self.rdef
|
|
1824
|
-
for _, r in rd.iterrows():
|
|
1825
|
-
file_path = r["path"]
|
|
1826
|
-
payload = r["payload"]
|
|
1827
|
-
if file_path.endswith("/visual.json"):
|
|
1828
|
-
objFile = base64.b64decode(payload).decode("utf-8")
|
|
1829
|
-
objJson = json.loads(objFile)
|
|
1830
|
-
delete_key_in_json(objJson, "showAll")
|
|
1831
|
-
_add_part(request_body, file_path, _conv_b64(objJson))
|
|
1832
|
-
else:
|
|
1833
|
-
_add_part(request_body, file_path, payload)
|
|
2263
|
+
def _remove_wallpaper(self, page: Optional[str | List[str]] = None):
|
|
2264
|
+
"""
|
|
2265
|
+
Remove the wallpaper image from a page.
|
|
1834
2266
|
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
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()
|
|
1839
2274
|
|
|
1840
|
-
|
|
1841
|
-
|
|
2275
|
+
if isinstance(page, str):
|
|
2276
|
+
page = [page]
|
|
1842
2277
|
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
if annotation["name"] == name:
|
|
1849
|
-
annotation["value"] = value
|
|
1850
|
-
break
|
|
1851
|
-
else:
|
|
1852
|
-
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)
|
|
1853
2283
|
else:
|
|
1854
|
-
|
|
1855
|
-
|
|
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
|
+
]
|
|
1856
2289
|
|
|
1857
|
-
|
|
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
|
+
)
|
|
1858
2300
|
|
|
1859
|
-
def
|
|
2301
|
+
def _set_wallpaper_color(
|
|
1860
2302
|
self,
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
2303
|
+
color_value: str,
|
|
2304
|
+
page: Optional[str | List[str]] = None,
|
|
2305
|
+
transparency: int = 0,
|
|
2306
|
+
theme_color_percent: float = 0.0,
|
|
1865
2307
|
):
|
|
1866
2308
|
"""
|
|
1867
|
-
|
|
1868
|
-
In order to set a report annotation, leave page_name=None, visual_name=None.
|
|
1869
|
-
In order to set a page annotation, leave visual_annotation=None.
|
|
1870
|
-
In order to set a visual annotation, set all parameters.
|
|
2309
|
+
Set the wallpaper color of a page (or pages).
|
|
1871
2310
|
|
|
1872
2311
|
Parameters
|
|
1873
2312
|
----------
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
The
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
if
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
self, page_name=page_name
|
|
1891
|
-
)
|
|
1892
|
-
elif page_name is not None and visual_name is not None:
|
|
1893
|
-
page_name, page_display_name, visual_name, file_path = (
|
|
1894
|
-
helper.resolve_visual_name(
|
|
1895
|
-
self, page_name=page_name, visual_name=visual_name
|
|
1896
|
-
)
|
|
1897
|
-
)
|
|
1898
|
-
else:
|
|
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:
|
|
1899
2329
|
raise ValueError(
|
|
1900
|
-
f"{icons.red_dot}
|
|
2330
|
+
f"{icons.red_dot} Theme color percentage must be between -0.6 and 0.6."
|
|
1901
2331
|
)
|
|
1902
2332
|
|
|
1903
|
-
|
|
1904
|
-
file = _decode_b64(payload)
|
|
1905
|
-
json_file = json.loads(file)
|
|
1906
|
-
|
|
1907
|
-
new_file = self.__set_annotation(
|
|
1908
|
-
json_file, name=annotation_name, value=annotation_value
|
|
1909
|
-
)
|
|
1910
|
-
new_payload = _conv_b64(new_file)
|
|
2333
|
+
page_list = self.__resolve_page_list(page)
|
|
1911
2334
|
|
|
1912
|
-
|
|
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
|
+
)
|
|
1913
2349
|
|
|
1914
|
-
|
|
1915
|
-
|
|
2350
|
+
color_dict = ({"solid": {"color": {"expr": color_expr}}},)
|
|
2351
|
+
transparency_dict = {"expr": {"Literal": {"Value": f"{transparency}D"}}}
|
|
1916
2352
|
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
if annotation["name"] != name
|
|
1922
|
-
]
|
|
2353
|
+
for p in self.__all_pages():
|
|
2354
|
+
path = p.get("path")
|
|
2355
|
+
payload = p.get("payload", {})
|
|
2356
|
+
page_name = payload.get("name")
|
|
1923
2357
|
|
|
1924
|
-
|
|
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
|
+
)
|
|
1925
2369
|
|
|
1926
|
-
def
|
|
2370
|
+
def _set_wallpaper_image(
|
|
1927
2371
|
self,
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2372
|
+
image_path: str,
|
|
2373
|
+
page: Optional[str | List[str]] = None,
|
|
2374
|
+
transparency: int = 0,
|
|
2375
|
+
image_fit: Literal["Normal", "Fit", "Fill"] = "Normal",
|
|
1931
2376
|
):
|
|
1932
2377
|
"""
|
|
1933
|
-
|
|
1934
|
-
In order to remove a report annotation, leave page_name=None, visual_name=None.
|
|
1935
|
-
In order to remove a page annotation, leave visual_annotation=None.
|
|
1936
|
-
In order to remove a visual annotation, set all parameters.
|
|
2378
|
+
Add an image as the wallpaper of a page.
|
|
1937
2379
|
|
|
1938
2380
|
Parameters
|
|
1939
2381
|
----------
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
The
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
The
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
)
|
|
1956
|
-
elif page_name is not None and visual_name is not None:
|
|
1957
|
-
page_name, page_display_name, visual_name, file_path = (
|
|
1958
|
-
helper.resolve_visual_name(
|
|
1959
|
-
self, page_name=page_name, visual_name=visual_name
|
|
1960
|
-
)
|
|
1961
|
-
)
|
|
1962
|
-
else:
|
|
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:
|
|
1963
2397
|
raise ValueError(
|
|
1964
|
-
f"{icons.red_dot} Invalid
|
|
2398
|
+
f"{icons.red_dot} Invalid image fit. Valid options: {image_fits}."
|
|
1965
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
|
+
)
|
|
2440
|
+
|
|
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)
|
|
1966
2460
|
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
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)
|
|
2464
|
+
|
|
2465
|
+
def _add_page(self, payload: dict | bytes, generate_id: bool = True):
|
|
2466
|
+
"""
|
|
2467
|
+
Add a new page to the report.
|
|
1970
2468
|
|
|
1971
|
-
|
|
1972
|
-
|
|
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()
|
|
1973
2477
|
|
|
1974
|
-
|
|
2478
|
+
page_file = decode_payload(payload)
|
|
2479
|
+
page_file_copy = copy.deepcopy(page_file)
|
|
1975
2480
|
|
|
1976
|
-
|
|
1977
|
-
|
|
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")
|
|
1978
2487
|
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
return ann.get("value")
|
|
2488
|
+
self.add(
|
|
2489
|
+
file_path=f"definition/pages/{page_id}/page.json", payload=page_file_copy
|
|
2490
|
+
)
|
|
1983
2491
|
|
|
1984
|
-
def
|
|
1985
|
-
self,
|
|
1986
|
-
annotation_name: str,
|
|
1987
|
-
page_name: Optional[str] = None,
|
|
1988
|
-
visual_name: Optional[str] = None,
|
|
1989
|
-
) -> str:
|
|
2492
|
+
def _add_visual(self, page: str, payload: dict | bytes, generate_id: bool = True):
|
|
1990
2493
|
"""
|
|
1991
|
-
|
|
1992
|
-
In order to retrieve a report annotation value, leave page_name=None, visual_name=None.
|
|
1993
|
-
In order to retrieve a page annotation value, leave visual_annotation=None.
|
|
1994
|
-
In order to retrieve a visual annotation value, set all parameters.
|
|
2494
|
+
Add a new visual to a page in the report.
|
|
1995
2495
|
|
|
1996
2496
|
Parameters
|
|
1997
2497
|
----------
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
The
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
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)
|
|
2006
2520
|
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
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]]):
|
|
2011
2558
|
"""
|
|
2559
|
+
Updates the report definition to use theme colors instead of hex colors.
|
|
2012
2560
|
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
)
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
helper.resolve_visual_name(
|
|
2022
|
-
self, page_name=page_name, visual_name=visual_name
|
|
2023
|
-
)
|
|
2024
|
-
)
|
|
2025
|
-
else:
|
|
2026
|
-
raise ValueError(
|
|
2027
|
-
f"{icons.red_dot} Invalid parameters. If specifying a visual_name you must specify the page_name."
|
|
2028
|
-
)
|
|
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()
|
|
2029
2569
|
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
json_file = json.loads(file)
|
|
2033
|
-
|
|
2034
|
-
return self.__get_annotation_value(json_file, name=annotation_name)
|
|
2035
|
-
|
|
2036
|
-
def __adjust_settings(
|
|
2037
|
-
self, setting_type: str, setting_name: str, setting_value: bool
|
|
2038
|
-
): # Meta function
|
|
2039
|
-
|
|
2040
|
-
valid_setting_types = ["settings", "slowDataSourceSettings"]
|
|
2041
|
-
valid_settings = [
|
|
2042
|
-
"isPersistentUserStateDisabled",
|
|
2043
|
-
"hideVisualContainerHeader",
|
|
2044
|
-
"defaultFilterActionIsDataFilter",
|
|
2045
|
-
"useStylableVisualContainerHeader",
|
|
2046
|
-
"useDefaultAggregateDisplayName",
|
|
2047
|
-
"useEnhancedTooltips",
|
|
2048
|
-
"allowChangeFilterTypes",
|
|
2049
|
-
"disableFilterPaneSearch",
|
|
2050
|
-
"useCrossReportDrillthrough",
|
|
2051
|
-
]
|
|
2052
|
-
valid_slow_settings = [
|
|
2053
|
-
"isCrossHighlightingDisabled",
|
|
2054
|
-
"isSlicerSelectionsButtonEnabled",
|
|
2055
|
-
]
|
|
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()}
|
|
2056
2572
|
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
)
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
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]:"
|
|
2064
2582
|
)
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
and setting_name not in valid_slow_settings
|
|
2068
|
-
):
|
|
2583
|
+
for color, val in out_of_range.items():
|
|
2584
|
+
print(f" {color}: Percent = {val[1]}")
|
|
2069
2585
|
raise ValueError(
|
|
2070
|
-
f"
|
|
2586
|
+
f"{icons.red_dot} The Percent values must be between -0.6 and 0.6."
|
|
2071
2587
|
)
|
|
2072
2588
|
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
for
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
if path
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
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
|
+
}
|
|
2092
2619
|
|
|
2093
|
-
|
|
2094
|
-
if upd == 200:
|
|
2095
|
-
print(f"{icons.green_dot}")
|
|
2096
|
-
else:
|
|
2097
|
-
print(f"{icons.red_dot}")
|
|
2620
|
+
self.update(file_path=file_path, payload=payload)
|
|
2098
2621
|
|
|
2099
|
-
def
|
|
2622
|
+
def _rename_fields(self, mapping: dict):
|
|
2100
2623
|
"""
|
|
2101
|
-
|
|
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
|
+
}
|
|
2102
2642
|
"""
|
|
2643
|
+
self._ensure_pbir()
|
|
2103
2644
|
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
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
|
+
}
|
|
2109
2652
|
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
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
|
|
2114
2708
|
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
)
|
|
2709
|
+
# Rename Column Properties
|
|
2710
|
+
for match in col_expr_path.find(payload):
|
|
2711
|
+
col_obj = match.value
|
|
2712
|
+
parent = match.context.value
|
|
2120
2713
|
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
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)
|
|
2125
2721
|
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
setting_name="defaultFilterActionIsDataFilter",
|
|
2129
|
-
setting_value=value,
|
|
2130
|
-
)
|
|
2722
|
+
if not table:
|
|
2723
|
+
continue # skip if can't resolve table
|
|
2131
2724
|
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
"""
|
|
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)]
|
|
2136
2728
|
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
)
|
|
2729
|
+
# Rename Measure Properties
|
|
2730
|
+
for match in meas_expr_path.find(payload):
|
|
2731
|
+
meas_obj = match.value
|
|
2732
|
+
parent = match.context.value
|
|
2142
2733
|
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
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.
|
|
2146
2808
|
"""
|
|
2809
|
+
self._ensure_pbir()
|
|
2147
2810
|
|
|
2148
|
-
self.
|
|
2149
|
-
setting_type="settings",
|
|
2150
|
-
setting_name="useDefaultAggregateDisplayName",
|
|
2151
|
-
setting_value=value,
|
|
2152
|
-
)
|
|
2811
|
+
file = self.get("*.json", json_path="$..color.expr.Literal.Value")
|
|
2153
2812
|
|
|
2154
|
-
|
|
2813
|
+
return [x[1].strip("'") for x in file]
|
|
2814
|
+
|
|
2815
|
+
def __update_visual_image(self, file_path: str, image_path: str):
|
|
2155
2816
|
"""
|
|
2156
|
-
|
|
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".
|
|
2157
2825
|
"""
|
|
2158
2826
|
|
|
2159
|
-
self.
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
)
|
|
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
|
+
)
|
|
2164
2835
|
|
|
2165
|
-
|
|
2166
|
-
"""
|
|
2167
|
-
Allow users to change filter types.
|
|
2168
|
-
"""
|
|
2836
|
+
image_name = image_path.split("RegisteredResources/")[1]
|
|
2169
2837
|
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
)
|
|
2838
|
+
if not file_path.endswith("/visual.json"):
|
|
2839
|
+
raise ValueError(
|
|
2840
|
+
f"File path must end with '/visual.json'. Provided: {file_path}"
|
|
2841
|
+
)
|
|
2175
2842
|
|
|
2176
|
-
|
|
2177
|
-
"""
|
|
2178
|
-
|
|
2179
|
-
"""
|
|
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
|
|
2180
2849
|
|
|
2181
|
-
|
|
2182
|
-
setting_type="settings",
|
|
2183
|
-
setting_name="disableFilterPaneSearch",
|
|
2184
|
-
setting_value=value,
|
|
2185
|
-
)
|
|
2850
|
+
def save_changes(self):
|
|
2186
2851
|
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
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
|
+
)
|
|
2191
2880
|
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
setting_name="useCrossReportDrillthrough",
|
|
2195
|
-
setting_value=value,
|
|
2196
|
-
)
|
|
2881
|
+
# Generate payload for the updateDefinition API
|
|
2882
|
+
new_payload = {"definition": {"parts": new_report_definition.get("parts")}}
|
|
2197
2883
|
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
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
|
+
)
|
|
2202
2895
|
|
|
2203
|
-
|
|
2204
|
-
setting_type="slowDataSourceSettings",
|
|
2205
|
-
setting_name="isCrossHighlightingDisabled",
|
|
2206
|
-
setting_value=value,
|
|
2207
|
-
)
|
|
2896
|
+
def close(self):
|
|
2208
2897
|
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
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.
|
|
2213
2920
|
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
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.
|
|
2219
2933
|
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2934
|
+
Returns
|
|
2935
|
+
-------
|
|
2936
|
+
typing.Iterator[ReportWrapper]
|
|
2937
|
+
A connection to the report's metadata.
|
|
2938
|
+
"""
|
|
2223
2939
|
|
|
2224
|
-
|
|
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()
|