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