semantic-link-labs 0.10.0__py3-none-any.whl → 0.11.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.10.0.dist-info → semantic_link_labs-0.11.0.dist-info}/METADATA +9 -6
- {semantic_link_labs-0.10.0.dist-info → semantic_link_labs-0.11.0.dist-info}/RECORD +95 -87
- sempy_labs/__init__.py +11 -1
- sempy_labs/_a_lib_info.py +2 -0
- sempy_labs/_capacities.py +2 -0
- sempy_labs/_connections.py +11 -0
- sempy_labs/_dashboards.py +9 -4
- sempy_labs/_data_pipelines.py +5 -0
- sempy_labs/_dataflows.py +284 -17
- sempy_labs/_daxformatter.py +80 -0
- sempy_labs/_delta_analyzer_history.py +4 -1
- sempy_labs/_deployment_pipelines.py +4 -0
- sempy_labs/_documentation.py +3 -0
- sempy_labs/_environments.py +10 -1
- sempy_labs/_eventhouses.py +12 -5
- sempy_labs/_eventstreams.py +11 -3
- sempy_labs/_external_data_shares.py +8 -2
- sempy_labs/_gateways.py +26 -5
- sempy_labs/_git.py +11 -0
- sempy_labs/_graphQL.py +10 -3
- sempy_labs/_helper_functions.py +62 -10
- sempy_labs/_job_scheduler.py +54 -7
- sempy_labs/_kql_databases.py +11 -2
- sempy_labs/_kql_querysets.py +11 -3
- sempy_labs/_list_functions.py +17 -45
- sempy_labs/_managed_private_endpoints.py +11 -2
- sempy_labs/_mirrored_databases.py +17 -3
- sempy_labs/_mirrored_warehouses.py +9 -3
- sempy_labs/_ml_experiments.py +11 -3
- sempy_labs/_ml_models.py +11 -3
- sempy_labs/_model_bpa_rules.py +2 -0
- sempy_labs/_mounted_data_factories.py +12 -8
- sempy_labs/_notebooks.py +6 -3
- sempy_labs/_refresh_semantic_model.py +1 -0
- sempy_labs/_semantic_models.py +107 -0
- sempy_labs/_spark.py +7 -0
- sempy_labs/_sql_endpoints.py +208 -0
- sempy_labs/_sqldatabase.py +13 -4
- sempy_labs/_tags.py +5 -1
- sempy_labs/_user_delegation_key.py +2 -0
- sempy_labs/_variable_libraries.py +3 -1
- sempy_labs/_warehouses.py +13 -3
- sempy_labs/_workloads.py +3 -0
- sempy_labs/_workspace_identity.py +3 -0
- sempy_labs/_workspaces.py +14 -1
- sempy_labs/admin/__init__.py +2 -0
- sempy_labs/admin/_activities.py +6 -5
- sempy_labs/admin/_apps.py +31 -31
- sempy_labs/admin/_artifacts.py +8 -3
- sempy_labs/admin/_basic_functions.py +5 -0
- sempy_labs/admin/_capacities.py +39 -28
- sempy_labs/admin/_datasets.py +51 -51
- sempy_labs/admin/_domains.py +17 -1
- sempy_labs/admin/_external_data_share.py +8 -2
- sempy_labs/admin/_git.py +14 -9
- sempy_labs/admin/_items.py +15 -2
- sempy_labs/admin/_reports.py +64 -65
- sempy_labs/admin/_shared.py +7 -1
- sempy_labs/admin/_tags.py +5 -0
- sempy_labs/admin/_tenant.py +5 -2
- sempy_labs/admin/_users.py +9 -3
- sempy_labs/admin/_workspaces.py +88 -0
- sempy_labs/directlake/_dl_helper.py +2 -0
- sempy_labs/directlake/_generate_shared_expression.py +2 -0
- sempy_labs/directlake/_get_directlake_lakehouse.py +2 -4
- sempy_labs/directlake/_get_shared_expression.py +2 -0
- sempy_labs/directlake/_guardrails.py +2 -0
- sempy_labs/directlake/_update_directlake_model_lakehouse_connection.py +5 -3
- sempy_labs/directlake/_warm_cache.py +1 -0
- sempy_labs/graph/_groups.py +22 -7
- sempy_labs/graph/_teams.py +7 -2
- sempy_labs/graph/_users.py +1 -0
- sempy_labs/lakehouse/_blobs.py +1 -0
- sempy_labs/lakehouse/_get_lakehouse_tables.py +88 -27
- sempy_labs/lakehouse/_helper.py +2 -0
- sempy_labs/lakehouse/_lakehouse.py +38 -5
- sempy_labs/lakehouse/_livy_sessions.py +2 -1
- sempy_labs/lakehouse/_shortcuts.py +7 -1
- sempy_labs/migration/_direct_lake_to_import.py +2 -0
- sempy_labs/mirrored_azure_databricks_catalog/__init__.py +15 -0
- sempy_labs/mirrored_azure_databricks_catalog/_discover.py +213 -0
- sempy_labs/mirrored_azure_databricks_catalog/_refresh_catalog_metadata.py +45 -0
- sempy_labs/report/_download_report.py +2 -1
- sempy_labs/report/_generate_report.py +2 -0
- sempy_labs/report/_paginated.py +2 -0
- sempy_labs/report/_report_bpa.py +110 -122
- sempy_labs/report/_report_bpa_rules.py +2 -0
- sempy_labs/report/_report_functions.py +7 -0
- sempy_labs/report/_reportwrapper.py +86 -48
- sempy_labs/theme/__init__.py +12 -0
- sempy_labs/theme/_org_themes.py +96 -0
- sempy_labs/tom/_model.py +702 -35
- {semantic_link_labs-0.10.0.dist-info → semantic_link_labs-0.11.0.dist-info}/WHEEL +0 -0
- {semantic_link_labs-0.10.0.dist-info → semantic_link_labs-0.11.0.dist-info}/licenses/LICENSE +0 -0
- {semantic_link_labs-0.10.0.dist-info → semantic_link_labs-0.11.0.dist-info}/top_level.txt +0 -0
sempy_labs/report/_report_bpa.py
CHANGED
|
@@ -2,7 +2,7 @@ from typing import Optional
|
|
|
2
2
|
import pandas as pd
|
|
3
3
|
import datetime
|
|
4
4
|
from sempy._utils._log import log
|
|
5
|
-
from sempy_labs.report import
|
|
5
|
+
from sempy_labs.report import connect_report, report_bpa_rules
|
|
6
6
|
from sempy_labs._helper_functions import (
|
|
7
7
|
format_dax_object_name,
|
|
8
8
|
save_as_delta_table,
|
|
@@ -14,7 +14,6 @@ from sempy_labs._helper_functions import (
|
|
|
14
14
|
from sempy_labs.lakehouse import get_lakehouse_tables, lakehouse_attached
|
|
15
15
|
import sempy_labs._icons as icons
|
|
16
16
|
from IPython.display import display, HTML
|
|
17
|
-
import sempy_labs.report._report_helper as helper
|
|
18
17
|
from uuid import UUID
|
|
19
18
|
|
|
20
19
|
|
|
@@ -52,124 +51,114 @@ def run_report_bpa(
|
|
|
52
51
|
A pandas dataframe in HTML format showing report objects which violated the best practice analyzer rules.
|
|
53
52
|
"""
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
with connect_report(
|
|
55
|
+
report=report, workspace=workspace, readonly=True, show_diffs=False
|
|
56
|
+
) as rpt:
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# Convert back to dataframe
|
|
65
|
-
# if isinstance(dfPF, pd.io.formats.style.Styler):
|
|
66
|
-
# dfPF = dfPF.data
|
|
67
|
-
# if isinstance(dfP, pd.io.formats.style.Styler):
|
|
68
|
-
# dfP = dfP.data
|
|
69
|
-
|
|
70
|
-
dfPF["Filter Object"] = (
|
|
71
|
-
dfPF["Page Display Name"]
|
|
72
|
-
+ " : "
|
|
73
|
-
+ format_dax_object_name(dfPF["Table Name"], dfPF["Object Name"])
|
|
74
|
-
)
|
|
75
|
-
dfVF = rpt.list_visual_filters()
|
|
76
|
-
dfVF["Filter Object"] = (
|
|
77
|
-
format_dax_object_name(dfVF["Page Display Name"], dfVF["Visual Name"])
|
|
78
|
-
+ " : "
|
|
79
|
-
+ format_dax_object_name(dfVF["Table Name"], dfVF["Object Name"])
|
|
80
|
-
)
|
|
81
|
-
dfRLM = rpt.list_report_level_measures()
|
|
82
|
-
dfV = rpt.list_visuals()
|
|
83
|
-
dfV["Visual Full Name"] = format_dax_object_name(
|
|
84
|
-
dfV["Page Display Name"], dfV["Visual Name"]
|
|
85
|
-
)
|
|
86
|
-
dfSMO = rpt.list_semantic_model_objects(extended=True)
|
|
87
|
-
|
|
88
|
-
# Translations
|
|
89
|
-
if rules is None:
|
|
90
|
-
rules = report_bpa_rules()
|
|
91
|
-
|
|
92
|
-
scope_to_dataframe = {
|
|
93
|
-
"Custom Visual": (dfCV, ["Custom Visual Display Name"]),
|
|
94
|
-
"Page": (dfP, ["Page Display Name"]),
|
|
95
|
-
"Visual": (dfV, ["Visual Full Name"]),
|
|
96
|
-
"Report Filter": (dfRF, ["Filter Object"]),
|
|
97
|
-
"Page Filter": (dfPF, ["Filter Object"]),
|
|
98
|
-
"Visual Filter": (dfVF, ["Filter Object"]),
|
|
99
|
-
"Report Level Measure": (dfRLM, ["Measure Name"]),
|
|
100
|
-
"Semantic Model": (dfSMO, ["Report Source Object"]),
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
def execute_rule(row):
|
|
104
|
-
scopes = row["Scope"]
|
|
105
|
-
|
|
106
|
-
# support both str and list as scope type
|
|
107
|
-
if isinstance(scopes, str):
|
|
108
|
-
scopes = [scopes]
|
|
109
|
-
|
|
110
|
-
# collect output dataframes
|
|
111
|
-
df_outputs = []
|
|
112
|
-
|
|
113
|
-
for scope in scopes:
|
|
114
|
-
# common fields for each scope
|
|
115
|
-
(df, violation_cols_or_func) = scope_to_dataframe[scope]
|
|
116
|
-
|
|
117
|
-
# execute rule and subset df
|
|
118
|
-
df_violations = df[row["Expression"](df)]
|
|
119
|
-
|
|
120
|
-
# subset the right output columns (e.g. Table Name & Column Name)
|
|
121
|
-
if isinstance(violation_cols_or_func, list):
|
|
122
|
-
violation_func = lambda violations: violations[violation_cols_or_func]
|
|
123
|
-
else:
|
|
124
|
-
violation_func = violation_cols_or_func
|
|
125
|
-
|
|
126
|
-
# build output data frame
|
|
127
|
-
df_output = violation_func(df_violations).copy()
|
|
128
|
-
|
|
129
|
-
df_output.columns = ["Object Name"]
|
|
130
|
-
df_output["Rule Name"] = row["Rule Name"]
|
|
131
|
-
df_output["Category"] = row["Category"]
|
|
132
|
-
|
|
133
|
-
# Handle 'Object Type' based on scope
|
|
134
|
-
if scope == "Semantic Model":
|
|
135
|
-
# Ensure 'Report Source Object' is unique in dfSMO
|
|
136
|
-
dfSMO_unique = dfSMO.drop_duplicates(subset="Report Source Object")
|
|
137
|
-
# Map 'Object Name' to the 'Report Source' column in dfSMO
|
|
138
|
-
df_output["Object Type"] = df_output["Object Name"].map(
|
|
139
|
-
dfSMO_unique.set_index("Report Source Object")["Report Source"]
|
|
140
|
-
)
|
|
141
|
-
else:
|
|
142
|
-
df_output["Object Type"] = scope
|
|
143
|
-
|
|
144
|
-
df_output["Severity"] = row["Severity"]
|
|
145
|
-
df_output["Description"] = row["Description"]
|
|
146
|
-
df_output["URL"] = row["URL"]
|
|
147
|
-
df_output["Report URL"] = helper.get_web_url(
|
|
148
|
-
report=report, workspace=workspace
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
page_mapping_dict = dfP.set_index("Page Display Name")["Page URL"].to_dict()
|
|
152
|
-
|
|
153
|
-
if scope == "Page":
|
|
154
|
-
df_output["Report URL"] = df_output["Object Name"].map(
|
|
155
|
-
page_mapping_dict
|
|
156
|
-
)
|
|
157
|
-
elif scope == "Page Filter":
|
|
158
|
-
df_output["Page Name"] = df_output["Object Name"].str.extract(
|
|
159
|
-
r"(.*?) : '"
|
|
160
|
-
)
|
|
161
|
-
df_output["Report URL"] = df_output["Page Name"].map(page_mapping_dict)
|
|
162
|
-
df_output.drop(columns=["Page Name"], inplace=True)
|
|
163
|
-
elif scope in ["Visual", "Visual Filter"]:
|
|
164
|
-
df_output["Page Name"] = df_output["Object Name"].str.extract(
|
|
165
|
-
r"'(.*?)'\[.*?\]"
|
|
166
|
-
)
|
|
167
|
-
df_output["Report URL"] = df_output["Page Name"].map(page_mapping_dict)
|
|
168
|
-
df_output.drop(columns=["Page Name"], inplace=True)
|
|
58
|
+
dfCV = rpt.list_custom_visuals()
|
|
59
|
+
dfP = rpt.list_pages()
|
|
60
|
+
dfRF = rpt.list_report_filters()
|
|
61
|
+
dfRF["Filter Object"] = format_dax_object_name(
|
|
62
|
+
dfRF["Table Name"], dfRF["Object Name"]
|
|
63
|
+
)
|
|
64
|
+
dfPF = rpt.list_page_filters()
|
|
169
65
|
|
|
170
|
-
|
|
66
|
+
dfPF["Filter Object"] = (
|
|
67
|
+
dfPF["Page Display Name"]
|
|
68
|
+
+ " : "
|
|
69
|
+
+ format_dax_object_name(dfPF["Table Name"], dfPF["Object Name"])
|
|
70
|
+
)
|
|
71
|
+
dfVF = rpt.list_visual_filters()
|
|
72
|
+
dfVF["Filter Object"] = (
|
|
73
|
+
format_dax_object_name(dfVF["Page Display Name"], dfVF["Visual Name"])
|
|
74
|
+
+ " : "
|
|
75
|
+
+ format_dax_object_name(dfVF["Table Name"], dfVF["Object Name"])
|
|
76
|
+
)
|
|
77
|
+
dfRLM = rpt.list_report_level_measures()
|
|
78
|
+
dfV = rpt.list_visuals()
|
|
79
|
+
dfV["Visual Full Name"] = format_dax_object_name(
|
|
80
|
+
dfV["Page Display Name"], dfV["Visual Name"]
|
|
81
|
+
)
|
|
82
|
+
dfSMO = rpt.list_semantic_model_objects(extended=True)
|
|
83
|
+
|
|
84
|
+
# Translations
|
|
85
|
+
if rules is None:
|
|
86
|
+
rules = report_bpa_rules()
|
|
87
|
+
|
|
88
|
+
scope_to_dataframe = {
|
|
89
|
+
"Custom Visual": (dfCV, ["Custom Visual Display Name"]),
|
|
90
|
+
"Page": (dfP, ["Page Display Name"]),
|
|
91
|
+
"Visual": (dfV, ["Visual Full Name"]),
|
|
92
|
+
"Report Filter": (dfRF, ["Filter Object"]),
|
|
93
|
+
"Page Filter": (dfPF, ["Filter Object"]),
|
|
94
|
+
"Visual Filter": (dfVF, ["Filter Object"]),
|
|
95
|
+
"Report Level Measure": (dfRLM, ["Measure Name"]),
|
|
96
|
+
"Semantic Model": (dfSMO, ["Report Source Object"]),
|
|
97
|
+
}
|
|
171
98
|
|
|
172
|
-
|
|
99
|
+
def execute_rule(row):
|
|
100
|
+
scopes = row["Scope"]
|
|
101
|
+
|
|
102
|
+
# support both str and list as scope type
|
|
103
|
+
if isinstance(scopes, str):
|
|
104
|
+
scopes = [scopes]
|
|
105
|
+
|
|
106
|
+
# collect output dataframes
|
|
107
|
+
df_outputs = []
|
|
108
|
+
|
|
109
|
+
for scope in scopes:
|
|
110
|
+
# common fields for each scope
|
|
111
|
+
(df, violation_cols_or_func) = scope_to_dataframe[scope]
|
|
112
|
+
|
|
113
|
+
# execute rule and subset df
|
|
114
|
+
df_violations = df[row["Expression"](df)]
|
|
115
|
+
|
|
116
|
+
# subset the right output columns (e.g. Table Name & Column Name)
|
|
117
|
+
if isinstance(violation_cols_or_func, list):
|
|
118
|
+
violation_func = lambda violations: violations[
|
|
119
|
+
violation_cols_or_func
|
|
120
|
+
]
|
|
121
|
+
else:
|
|
122
|
+
violation_func = violation_cols_or_func
|
|
123
|
+
|
|
124
|
+
# build output data frame
|
|
125
|
+
df_output = violation_func(df_violations).copy()
|
|
126
|
+
|
|
127
|
+
df_output.columns = ["Object Name"]
|
|
128
|
+
df_output["Rule Name"] = row["Rule Name"]
|
|
129
|
+
df_output["Category"] = row["Category"]
|
|
130
|
+
|
|
131
|
+
# Handle 'Object Type' based on scope
|
|
132
|
+
if scope == "Semantic Model":
|
|
133
|
+
# Ensure 'Report Source Object' is unique in dfSMO
|
|
134
|
+
dfSMO_unique = dfSMO.drop_duplicates(subset="Report Source Object")
|
|
135
|
+
# Map 'Object Name' to the 'Report Source' column in dfSMO
|
|
136
|
+
df_output["Object Type"] = df_output["Object Name"].map(
|
|
137
|
+
dfSMO_unique.set_index("Report Source Object")["Report Source"]
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
df_output["Object Type"] = scope
|
|
141
|
+
|
|
142
|
+
df_output["Severity"] = row["Severity"]
|
|
143
|
+
df_output["Description"] = row["Description"]
|
|
144
|
+
if scope in ["Page", "Page Filter"]:
|
|
145
|
+
if not df_output.empty:
|
|
146
|
+
for idx, r in df_output.iterrows():
|
|
147
|
+
df_output.loc[idx, "URL"] = rpt._get_url(
|
|
148
|
+
page_name=r["Object Name"]
|
|
149
|
+
)
|
|
150
|
+
elif scope in ["Visual", "Visual Filter"]:
|
|
151
|
+
if not df_output.empty:
|
|
152
|
+
for idx, r in df_output.iterrows():
|
|
153
|
+
df_output.loc[idx, "URL"] = rpt._get_url(
|
|
154
|
+
page_name=r["Object Name"].split("[")[0][1:-1],
|
|
155
|
+
visual_name=r["Object Name"].split("[")[1].rstrip("]"),
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
df_output["URL"] = rpt._get_url()
|
|
159
|
+
df_outputs.append(df_output)
|
|
160
|
+
|
|
161
|
+
return df_outputs
|
|
173
162
|
|
|
174
163
|
# flatten list of lists
|
|
175
164
|
flatten_dfs = [
|
|
@@ -186,7 +175,7 @@ def run_report_bpa(
|
|
|
186
175
|
"Severity",
|
|
187
176
|
"Description",
|
|
188
177
|
"URL",
|
|
189
|
-
"Report URL",
|
|
178
|
+
# "Report URL",
|
|
190
179
|
]
|
|
191
180
|
]
|
|
192
181
|
|
|
@@ -271,7 +260,7 @@ def run_report_bpa(
|
|
|
271
260
|
"Severity",
|
|
272
261
|
"Description",
|
|
273
262
|
"URL",
|
|
274
|
-
"Report URL",
|
|
263
|
+
# "Report URL",
|
|
275
264
|
]
|
|
276
265
|
]
|
|
277
266
|
.sort_values(["Category", "Rule Name", "Object Type", "Object Name"])
|
|
@@ -347,13 +336,12 @@ def run_report_bpa(
|
|
|
347
336
|
else:
|
|
348
337
|
content_html += f'<td>{row["Rule Name"]}</td>'
|
|
349
338
|
content_html += f'<td>{row["Object Type"]}</td>'
|
|
350
|
-
if pd.notnull(row["
|
|
339
|
+
if pd.notnull(row["URL"]):
|
|
351
340
|
content_html += (
|
|
352
|
-
f'<td><a href="{row["
|
|
341
|
+
f'<td><a href="{row["URL"]}">{row["Object Name"]}</a></td>'
|
|
353
342
|
)
|
|
354
343
|
else:
|
|
355
344
|
content_html += f'<td>{row["Object Name"]}</td>'
|
|
356
|
-
# content_html += f'<td>{row["Object Name"]}</td>'
|
|
357
345
|
content_html += f'<td style="text-align: center;">{row["Severity"]}</td>'
|
|
358
346
|
content_html += "</tr>"
|
|
359
347
|
content_html += "</table>"
|
|
@@ -26,6 +26,7 @@ import sempy_labs._icons as icons
|
|
|
26
26
|
from uuid import UUID
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
@log
|
|
29
30
|
def get_report_json(
|
|
30
31
|
report: str,
|
|
31
32
|
workspace: Optional[str | UUID] = None,
|
|
@@ -90,6 +91,7 @@ def get_report_json(
|
|
|
90
91
|
return report_json
|
|
91
92
|
|
|
92
93
|
|
|
94
|
+
@log
|
|
93
95
|
def report_dependency_tree(workspace: Optional[str | UUID] = None):
|
|
94
96
|
"""
|
|
95
97
|
Prints a dependency between reports and semantic models.
|
|
@@ -142,6 +144,7 @@ def report_dependency_tree(workspace: Optional[str | UUID] = None):
|
|
|
142
144
|
print(f"{pre}{node.custom_property}'{node.name}'")
|
|
143
145
|
|
|
144
146
|
|
|
147
|
+
@log
|
|
145
148
|
def clone_report(
|
|
146
149
|
report: str,
|
|
147
150
|
cloned_report: str,
|
|
@@ -221,6 +224,7 @@ def clone_report(
|
|
|
221
224
|
)
|
|
222
225
|
|
|
223
226
|
|
|
227
|
+
@log
|
|
224
228
|
def launch_report(report: str, workspace: Optional[str | UUID] = None):
|
|
225
229
|
"""
|
|
226
230
|
Shows a Power BI report within a Fabric notebook.
|
|
@@ -249,6 +253,7 @@ def launch_report(report: str, workspace: Optional[str | UUID] = None):
|
|
|
249
253
|
return report
|
|
250
254
|
|
|
251
255
|
|
|
256
|
+
@log
|
|
252
257
|
def list_report_pages(report: str, workspace: Optional[str | UUID] = None):
|
|
253
258
|
"""
|
|
254
259
|
Shows the properties of all pages within a Power BI report.
|
|
@@ -316,6 +321,7 @@ def list_report_pages(report: str, workspace: Optional[str | UUID] = None):
|
|
|
316
321
|
return df
|
|
317
322
|
|
|
318
323
|
|
|
324
|
+
@log
|
|
319
325
|
def list_report_visuals(report: str, workspace: Optional[str | UUID] = None):
|
|
320
326
|
"""
|
|
321
327
|
Shows the properties of all visuals within a Power BI report.
|
|
@@ -369,6 +375,7 @@ def list_report_visuals(report: str, workspace: Optional[str | UUID] = None):
|
|
|
369
375
|
return df
|
|
370
376
|
|
|
371
377
|
|
|
378
|
+
@log
|
|
372
379
|
def list_report_bookmarks(report: str, workspace: Optional[str | UUID] = None):
|
|
373
380
|
"""
|
|
374
381
|
Shows the properties of all bookmarks within a Power BI report.
|
|
@@ -17,6 +17,7 @@ from sempy_labs._helper_functions import (
|
|
|
17
17
|
get_jsonpath_value,
|
|
18
18
|
set_json_value,
|
|
19
19
|
remove_json_value,
|
|
20
|
+
get_tenant_id,
|
|
20
21
|
)
|
|
21
22
|
from sempy_labs._dictionary_diffs import (
|
|
22
23
|
diff_parts,
|
|
@@ -425,7 +426,9 @@ class ReportWrapper:
|
|
|
425
426
|
]
|
|
426
427
|
)
|
|
427
428
|
|
|
428
|
-
def _get_url(
|
|
429
|
+
def _get_url(
|
|
430
|
+
self, page_name: Optional[str] = None, visual_name: Optional[str] = None
|
|
431
|
+
) -> str:
|
|
429
432
|
"""
|
|
430
433
|
Gets the URL of the report. If specified, gets the URL of the specified page.
|
|
431
434
|
|
|
@@ -444,8 +447,16 @@ class ReportWrapper:
|
|
|
444
447
|
url = f"https://app.powerbi.com/groups/{self._workspace_id}/reports/{self._report_id}"
|
|
445
448
|
|
|
446
449
|
if page_name:
|
|
450
|
+
if page_name in [page["payload"]["name"] for page in self.__all_pages()]:
|
|
451
|
+
pass
|
|
452
|
+
else:
|
|
453
|
+
page_name = self.resolve_page_name(page_name)
|
|
447
454
|
url += f"/{page_name}"
|
|
448
455
|
|
|
456
|
+
if visual_name:
|
|
457
|
+
tenant_id = get_tenant_id()
|
|
458
|
+
url += f"?ctid={tenant_id}&pbi_source=shareVisual&visual={visual_name}"
|
|
459
|
+
|
|
449
460
|
return url
|
|
450
461
|
|
|
451
462
|
def __resolve_page_name_and_display_name_file_path(
|
|
@@ -1120,6 +1131,7 @@ class ReportWrapper:
|
|
|
1120
1131
|
"Has Sparkline": "bool",
|
|
1121
1132
|
"Visual Filter Count": "int",
|
|
1122
1133
|
"Data Limit": "int",
|
|
1134
|
+
"URL": "str",
|
|
1123
1135
|
}
|
|
1124
1136
|
df = _create_dataframe(columns=columns)
|
|
1125
1137
|
|
|
@@ -1247,12 +1259,13 @@ class ReportWrapper:
|
|
|
1247
1259
|
|
|
1248
1260
|
# Sparkline
|
|
1249
1261
|
has_sparkline = contains_key(payload, ["SparklineData"])
|
|
1262
|
+
visual_name = payload.get("name")
|
|
1250
1263
|
|
|
1251
1264
|
new_data = {
|
|
1252
1265
|
"File Path": path,
|
|
1253
1266
|
"Page Name": page_id,
|
|
1254
1267
|
"Page Display Name": page_display,
|
|
1255
|
-
"Visual Name":
|
|
1268
|
+
"Visual Name": visual_name,
|
|
1256
1269
|
"X": pos.get("x"),
|
|
1257
1270
|
"Y": pos.get("y"),
|
|
1258
1271
|
"Z": pos.get("z"),
|
|
@@ -1275,6 +1288,7 @@ class ReportWrapper:
|
|
|
1275
1288
|
"Has Sparkline": has_sparkline,
|
|
1276
1289
|
"Visual Filter Count": visual_filter_count,
|
|
1277
1290
|
"Data Limit": data_limit,
|
|
1291
|
+
"URL": self._get_url(page_name=page_id, visual_name=visual_name),
|
|
1278
1292
|
}
|
|
1279
1293
|
dfs.append(pd.DataFrame(new_data, index=[0]))
|
|
1280
1294
|
|
|
@@ -1361,6 +1375,7 @@ class ReportWrapper:
|
|
|
1361
1375
|
if isinstance(expression, dict)
|
|
1362
1376
|
else {}
|
|
1363
1377
|
)
|
|
1378
|
+
|
|
1364
1379
|
if (
|
|
1365
1380
|
isinstance(source_ref, dict)
|
|
1366
1381
|
and "Entity" in source_ref
|
|
@@ -1374,9 +1389,15 @@ class ReportWrapper:
|
|
|
1374
1389
|
if keys_path
|
|
1375
1390
|
else "Unknown"
|
|
1376
1391
|
)
|
|
1377
|
-
is_agg = keys_path[-3] == "Aggregation"
|
|
1378
|
-
is_viz_calc =
|
|
1379
|
-
|
|
1392
|
+
is_agg = len(keys_path) > 2 and keys_path[-3] == "Aggregation"
|
|
1393
|
+
is_viz_calc = (
|
|
1394
|
+
len(keys_path) > 2
|
|
1395
|
+
and keys_path[-3] == "NativeVisualCalculation"
|
|
1396
|
+
)
|
|
1397
|
+
is_sparkline = (
|
|
1398
|
+
len(keys_path) > 2 and keys_path[-3] == "SparklineData"
|
|
1399
|
+
)
|
|
1400
|
+
|
|
1380
1401
|
result[property_value] = (
|
|
1381
1402
|
entity,
|
|
1382
1403
|
object_type,
|
|
@@ -1384,13 +1405,10 @@ class ReportWrapper:
|
|
|
1384
1405
|
is_viz_calc,
|
|
1385
1406
|
is_sparkline,
|
|
1386
1407
|
)
|
|
1387
|
-
if keys_path:
|
|
1388
|
-
keys_path.pop()
|
|
1389
1408
|
|
|
1390
1409
|
# Recursively search the rest of the dictionary
|
|
1391
1410
|
for key, value in data.items():
|
|
1392
|
-
keys_path
|
|
1393
|
-
find_entity_property_pairs(value, result, keys_path)
|
|
1411
|
+
find_entity_property_pairs(value, result, keys_path + [key])
|
|
1394
1412
|
|
|
1395
1413
|
elif isinstance(data, list):
|
|
1396
1414
|
for item in data:
|
|
@@ -1712,31 +1730,36 @@ class ReportWrapper:
|
|
|
1712
1730
|
"Expression": "str",
|
|
1713
1731
|
"Data Type": "str",
|
|
1714
1732
|
"Format String": "str",
|
|
1733
|
+
"Data Category": "str",
|
|
1715
1734
|
}
|
|
1716
1735
|
|
|
1717
1736
|
df = _create_dataframe(columns=columns)
|
|
1718
1737
|
|
|
1738
|
+
# If no report extensions path, return empty DataFrame
|
|
1739
|
+
if self._report_extensions_path not in self.list_paths()["Path"].values:
|
|
1740
|
+
return df
|
|
1741
|
+
|
|
1719
1742
|
report_file = self.get(file_path=self._report_extensions_path)
|
|
1720
1743
|
|
|
1721
1744
|
dfs = []
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
for
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
format_string = m.get("formatString")
|
|
1745
|
+
for e in report_file.get("entities", []):
|
|
1746
|
+
table_name = e.get("name")
|
|
1747
|
+
for m in e.get("measures", []):
|
|
1748
|
+
measure_name = m.get("name")
|
|
1749
|
+
expr = m.get("expression")
|
|
1750
|
+
data_type = m.get("dataType")
|
|
1751
|
+
format_string = m.get("formatString")
|
|
1752
|
+
data_category = m.get("dataCategory")
|
|
1731
1753
|
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1754
|
+
new_data = {
|
|
1755
|
+
"Measure Name": measure_name,
|
|
1756
|
+
"Table Name": table_name,
|
|
1757
|
+
"Expression": expr,
|
|
1758
|
+
"Data Type": data_type,
|
|
1759
|
+
"Format String": format_string,
|
|
1760
|
+
"Data Category": data_category,
|
|
1761
|
+
}
|
|
1762
|
+
dfs.append(pd.DataFrame(new_data, index=[0]))
|
|
1740
1763
|
|
|
1741
1764
|
if dfs:
|
|
1742
1765
|
df = pd.concat(dfs, ignore_index=True)
|
|
@@ -1792,46 +1815,61 @@ class ReportWrapper:
|
|
|
1792
1815
|
return self.get(file_path=theme_file_path)
|
|
1793
1816
|
|
|
1794
1817
|
# Action functions
|
|
1795
|
-
def set_theme(
|
|
1818
|
+
def set_theme(
|
|
1819
|
+
self, theme_file_path: Optional[str] = None, theme_json: Optional[dict] = None
|
|
1820
|
+
):
|
|
1796
1821
|
"""
|
|
1797
1822
|
Sets a custom theme for a report based on a theme .json file.
|
|
1798
1823
|
|
|
1799
1824
|
Parameters
|
|
1800
1825
|
----------
|
|
1801
|
-
theme_file_path : str
|
|
1826
|
+
theme_file_path : str, default=None
|
|
1802
1827
|
The file path of the theme.json file. This can either be from a Fabric lakehouse or from the web.
|
|
1803
1828
|
Example for lakehouse: file_path = '/lakehouse/default/Files/CY23SU09.json'
|
|
1804
1829
|
Example for web url: file_path = 'https://raw.githubusercontent.com/PowerBiDevCamp/FabricUserApiDemo/main/FabricUserApiDemo/DefinitionTemplates/Shared/Reports/StaticResources/SharedResources/BaseThemes/CY23SU08.json'
|
|
1830
|
+
theme_json : dict, default=None
|
|
1831
|
+
The theme file in .json format. Must specify either the theme_file_path or the theme_json.
|
|
1805
1832
|
"""
|
|
1806
1833
|
|
|
1807
|
-
|
|
1808
|
-
theme_version = "5.6.4"
|
|
1809
|
-
|
|
1810
|
-
# Open file
|
|
1811
|
-
if not theme_file_path.endswith(".json"):
|
|
1834
|
+
if theme_file_path and theme_json:
|
|
1812
1835
|
raise ValueError(
|
|
1813
|
-
f"{icons.red_dot}
|
|
1836
|
+
f"{icons.red_dot} Please specify either the 'theme_file_path' or the 'theme_json' parameter, not both."
|
|
1814
1837
|
)
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
elif theme_file_path.startswith("/lakehouse") or theme_file_path.startswith(
|
|
1819
|
-
"/synfs/"
|
|
1820
|
-
):
|
|
1821
|
-
with open(theme_file_path, "r", encoding="utf-8-sig") as file:
|
|
1822
|
-
theme_file = json.load(file)
|
|
1823
|
-
else:
|
|
1824
|
-
ValueError(
|
|
1825
|
-
f"{icons.red_dot} Incorrect theme file path value '{theme_file_path}'."
|
|
1838
|
+
if not theme_file_path and not theme_json:
|
|
1839
|
+
raise ValueError(
|
|
1840
|
+
f"{icons.red_dot} Please specify either the 'theme_file_path' or the 'theme_json' parameter."
|
|
1826
1841
|
)
|
|
1827
1842
|
|
|
1828
|
-
|
|
1843
|
+
self._ensure_pbir()
|
|
1844
|
+
theme_version = "5.6.4"
|
|
1845
|
+
|
|
1846
|
+
# Extract theme_json from theme_file_path
|
|
1847
|
+
if theme_file_path:
|
|
1848
|
+
# Open file
|
|
1849
|
+
if not theme_file_path.endswith(".json"):
|
|
1850
|
+
raise ValueError(
|
|
1851
|
+
f"{icons.red_dot} The '{theme_file_path}' theme file path must be a .json file."
|
|
1852
|
+
)
|
|
1853
|
+
elif theme_file_path.startswith("https://"):
|
|
1854
|
+
response = requests.get(theme_file_path)
|
|
1855
|
+
theme_json = response.json()
|
|
1856
|
+
elif theme_file_path.startswith("/lakehouse") or theme_file_path.startswith(
|
|
1857
|
+
"/synfs/"
|
|
1858
|
+
):
|
|
1859
|
+
with open(theme_file_path, "r", encoding="utf-8-sig") as file:
|
|
1860
|
+
theme_json = json.load(file)
|
|
1861
|
+
else:
|
|
1862
|
+
ValueError(
|
|
1863
|
+
f"{icons.red_dot} Incorrect theme file path value '{theme_file_path}'."
|
|
1864
|
+
)
|
|
1865
|
+
|
|
1866
|
+
theme_name = theme_json.get("name")
|
|
1829
1867
|
theme_name_full = f"{theme_name}.json"
|
|
1830
1868
|
|
|
1831
1869
|
# Add theme.json file
|
|
1832
1870
|
self.add(
|
|
1833
1871
|
file_path=f"StaticResources/RegisteredResources/{theme_name_full}",
|
|
1834
|
-
payload=
|
|
1872
|
+
payload=theme_json,
|
|
1835
1873
|
)
|
|
1836
1874
|
|
|
1837
1875
|
custom_theme = {
|
|
@@ -2102,7 +2140,7 @@ class ReportWrapper:
|
|
|
2102
2140
|
format_string = r["Format String"]
|
|
2103
2141
|
# Add measures to the model
|
|
2104
2142
|
if (
|
|
2105
|
-
|
|
2143
|
+
measures is None or measure_name in measures
|
|
2106
2144
|
) and measure_name not in existing_measures:
|
|
2107
2145
|
tom.add_measure(
|
|
2108
2146
|
table_name=table_name,
|