semantic-link-labs 0.9.11__py3-none-any.whl → 0.10.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.9.11.dist-info → semantic_link_labs-0.10.1.dist-info}/METADATA +6 -3
- {semantic_link_labs-0.9.11.dist-info → semantic_link_labs-0.10.1.dist-info}/RECORD +27 -19
- {semantic_link_labs-0.9.11.dist-info → semantic_link_labs-0.10.1.dist-info}/WHEEL +1 -1
- sempy_labs/__init__.py +11 -1
- sempy_labs/_a_lib_info.py +2 -0
- sempy_labs/_daxformatter.py +78 -0
- sempy_labs/_dictionary_diffs.py +221 -0
- sempy_labs/_helper_functions.py +165 -0
- sempy_labs/_list_functions.py +0 -43
- sempy_labs/_notebooks.py +3 -3
- sempy_labs/_semantic_models.py +101 -0
- sempy_labs/_sql.py +1 -1
- sempy_labs/_sql_endpoints.py +185 -0
- sempy_labs/_user_delegation_key.py +42 -0
- sempy_labs/_vpax.py +2 -0
- sempy_labs/directlake/_update_directlake_model_lakehouse_connection.py +3 -3
- sempy_labs/lakehouse/__init__.py +0 -2
- sempy_labs/lakehouse/_blobs.py +0 -37
- sempy_labs/mirrored_azure_databricks_catalog/__init__.py +15 -0
- sempy_labs/mirrored_azure_databricks_catalog/_discover.py +209 -0
- sempy_labs/mirrored_azure_databricks_catalog/_refresh_catalog_metadata.py +43 -0
- sempy_labs/report/__init__.py +2 -0
- sempy_labs/report/_report_helper.py +27 -128
- sempy_labs/report/_reportwrapper.py +1935 -1205
- sempy_labs/tom/_model.py +193 -1
- {semantic_link_labs-0.9.11.dist-info → semantic_link_labs-0.10.1.dist-info}/licenses/LICENSE +0 -0
- {semantic_link_labs-0.9.11.dist-info → semantic_link_labs-0.10.1.dist-info}/top_level.txt +0 -0
sempy_labs/_helper_functions.py
CHANGED
|
@@ -17,6 +17,8 @@ import numpy as np
|
|
|
17
17
|
from IPython.display import display, HTML
|
|
18
18
|
import requests
|
|
19
19
|
import sempy_labs._authentication as auth
|
|
20
|
+
from jsonpath_ng.ext import parse
|
|
21
|
+
from jsonpath_ng.jsonpath import Fields, Index
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
def _build_url(url: str, params: dict) -> str:
|
|
@@ -2270,3 +2272,166 @@ def file_exists(file_path: str) -> bool:
|
|
|
2270
2272
|
import notebookutils
|
|
2271
2273
|
|
|
2272
2274
|
return len(notebookutils.fs.ls(file_path)) > 0
|
|
2275
|
+
|
|
2276
|
+
|
|
2277
|
+
def generate_number_guid():
|
|
2278
|
+
|
|
2279
|
+
guid = uuid.uuid4()
|
|
2280
|
+
return str(guid.int & ((1 << 64) - 1))
|
|
2281
|
+
|
|
2282
|
+
|
|
2283
|
+
def get_url_content(url: str):
|
|
2284
|
+
|
|
2285
|
+
if "github.com" in url and "/blob/" in url:
|
|
2286
|
+
url = url.replace("github.com", "raw.githubusercontent.com")
|
|
2287
|
+
url = url.replace("/blob/", "/")
|
|
2288
|
+
|
|
2289
|
+
response = requests.get(url)
|
|
2290
|
+
if response.ok:
|
|
2291
|
+
try:
|
|
2292
|
+
data = response.json() # Only works if the response is valid JSON
|
|
2293
|
+
except ValueError:
|
|
2294
|
+
data = response.text # Fallback: get raw text content
|
|
2295
|
+
return data
|
|
2296
|
+
else:
|
|
2297
|
+
print(f"Failed to fetch raw content: {response.status_code}")
|
|
2298
|
+
|
|
2299
|
+
|
|
2300
|
+
def generate_hex(length: int = 10) -> str:
|
|
2301
|
+
"""
|
|
2302
|
+
Generate a random hex string of the specified length. Used for generating IDs for report objects (page, visual, bookmark etc.).
|
|
2303
|
+
"""
|
|
2304
|
+
import secrets
|
|
2305
|
+
|
|
2306
|
+
return secrets.token_hex(length)
|
|
2307
|
+
|
|
2308
|
+
|
|
2309
|
+
def decode_payload(payload):
|
|
2310
|
+
|
|
2311
|
+
if is_base64(payload):
|
|
2312
|
+
try:
|
|
2313
|
+
decoded_payload = json.loads(base64.b64decode(payload).decode("utf-8"))
|
|
2314
|
+
except Exception:
|
|
2315
|
+
decoded_payload = base64.b64decode(payload)
|
|
2316
|
+
elif isinstance(payload, dict):
|
|
2317
|
+
decoded_payload = payload
|
|
2318
|
+
else:
|
|
2319
|
+
raise ValueError("Payload must be a dictionary or a base64 encoded value.")
|
|
2320
|
+
|
|
2321
|
+
return decoded_payload
|
|
2322
|
+
|
|
2323
|
+
|
|
2324
|
+
def is_base64(s):
|
|
2325
|
+
try:
|
|
2326
|
+
# Add padding if needed
|
|
2327
|
+
s_padded = s + "=" * (-len(s) % 4)
|
|
2328
|
+
decoded = base64.b64decode(s_padded, validate=True)
|
|
2329
|
+
# Optional: check if re-encoding gives the original (excluding padding)
|
|
2330
|
+
return base64.b64encode(decoded).decode().rstrip("=") == s.rstrip("=")
|
|
2331
|
+
except Exception:
|
|
2332
|
+
return False
|
|
2333
|
+
|
|
2334
|
+
|
|
2335
|
+
def get_jsonpath_value(
|
|
2336
|
+
data, path, default=None, remove_quotes=False, fix_true: bool = False
|
|
2337
|
+
):
|
|
2338
|
+
matches = parse(path).find(data)
|
|
2339
|
+
result = matches[0].value if matches else default
|
|
2340
|
+
if result and remove_quotes and isinstance(result, str):
|
|
2341
|
+
if result.startswith("'") and result.endswith("'"):
|
|
2342
|
+
result = result[1:-1]
|
|
2343
|
+
if fix_true and isinstance(result, str):
|
|
2344
|
+
if result.lower() == "true":
|
|
2345
|
+
result = True
|
|
2346
|
+
elif result.lower() == "false":
|
|
2347
|
+
result = False
|
|
2348
|
+
return result
|
|
2349
|
+
|
|
2350
|
+
|
|
2351
|
+
def set_json_value(payload: dict, json_path: str, json_value: str | dict | List):
|
|
2352
|
+
|
|
2353
|
+
jsonpath_expr = parse(json_path)
|
|
2354
|
+
matches = jsonpath_expr.find(payload)
|
|
2355
|
+
|
|
2356
|
+
if matches:
|
|
2357
|
+
# Update all matches
|
|
2358
|
+
for match in matches:
|
|
2359
|
+
parent = match.context.value
|
|
2360
|
+
path = match.path
|
|
2361
|
+
if isinstance(path, Fields):
|
|
2362
|
+
parent[path.fields[0]] = json_value
|
|
2363
|
+
elif isinstance(path, Index):
|
|
2364
|
+
parent[path.index] = json_value
|
|
2365
|
+
else:
|
|
2366
|
+
# Handle creation
|
|
2367
|
+
parts = json_path.lstrip("$").strip(".").split(".")
|
|
2368
|
+
current = payload
|
|
2369
|
+
|
|
2370
|
+
for i, part in enumerate(parts):
|
|
2371
|
+
is_last = i == len(parts) - 1
|
|
2372
|
+
|
|
2373
|
+
# Detect list syntax like "lockAspect[*]"
|
|
2374
|
+
list_match = re.match(r"(\w+)\[\*\]", part)
|
|
2375
|
+
if list_match:
|
|
2376
|
+
list_key = list_match.group(1)
|
|
2377
|
+
if list_key not in current or not isinstance(current[list_key], list):
|
|
2378
|
+
# Initialize with one dict element
|
|
2379
|
+
current[list_key] = [{}]
|
|
2380
|
+
|
|
2381
|
+
for item in current[list_key]:
|
|
2382
|
+
if is_last:
|
|
2383
|
+
# Last part, assign value
|
|
2384
|
+
item = json_value
|
|
2385
|
+
else:
|
|
2386
|
+
# Proceed to next level
|
|
2387
|
+
if not isinstance(item, dict):
|
|
2388
|
+
raise ValueError(
|
|
2389
|
+
f"Expected dict in list for key '{list_key}', got {type(item)}"
|
|
2390
|
+
)
|
|
2391
|
+
next_part = ".".join(parts[i + 1 :])
|
|
2392
|
+
set_json_value(item, "$." + next_part, json_value)
|
|
2393
|
+
return payload
|
|
2394
|
+
else:
|
|
2395
|
+
if part not in current or not isinstance(current[part], dict):
|
|
2396
|
+
current[part] = {} if not is_last else json_value
|
|
2397
|
+
elif is_last:
|
|
2398
|
+
current[part] = json_value
|
|
2399
|
+
current = current[part]
|
|
2400
|
+
|
|
2401
|
+
return payload
|
|
2402
|
+
|
|
2403
|
+
|
|
2404
|
+
def remove_json_value(path: str, payload: dict, json_path: str, verbose: bool = True):
|
|
2405
|
+
|
|
2406
|
+
if not isinstance(payload, dict):
|
|
2407
|
+
raise ValueError(
|
|
2408
|
+
f"{icons.red_dot} Cannot apply json_path to non-dictionary payload in '{path}'."
|
|
2409
|
+
)
|
|
2410
|
+
|
|
2411
|
+
jsonpath_expr = parse(json_path)
|
|
2412
|
+
matches = jsonpath_expr.find(payload)
|
|
2413
|
+
|
|
2414
|
+
if not matches and verbose:
|
|
2415
|
+
print(
|
|
2416
|
+
f"{icons.red_dot} No match found for '{json_path}' in '{path}'. Skipping."
|
|
2417
|
+
)
|
|
2418
|
+
return payload
|
|
2419
|
+
|
|
2420
|
+
for match in matches:
|
|
2421
|
+
parent = match.context.value
|
|
2422
|
+
path_expr = match.path
|
|
2423
|
+
|
|
2424
|
+
if isinstance(path_expr, Fields):
|
|
2425
|
+
key = path_expr.fields[0]
|
|
2426
|
+
if key in parent:
|
|
2427
|
+
del parent[key]
|
|
2428
|
+
if verbose:
|
|
2429
|
+
print(f"{icons.green_dot} Removed key '{key}' from '{path}'.")
|
|
2430
|
+
elif isinstance(path_expr, Index):
|
|
2431
|
+
index = path_expr.index
|
|
2432
|
+
if isinstance(parent, list) and 0 <= index < len(parent):
|
|
2433
|
+
parent.pop(index)
|
|
2434
|
+
if verbose:
|
|
2435
|
+
print(f"{icons.green_dot} Removed index [{index}] from '{path}'.")
|
|
2436
|
+
|
|
2437
|
+
return payload
|
sempy_labs/_list_functions.py
CHANGED
|
@@ -642,49 +642,6 @@ def list_lakehouses(workspace: Optional[str | UUID] = None) -> pd.DataFrame:
|
|
|
642
642
|
return df
|
|
643
643
|
|
|
644
644
|
|
|
645
|
-
def list_sql_endpoints(workspace: Optional[str | UUID] = None) -> pd.DataFrame:
|
|
646
|
-
"""
|
|
647
|
-
Shows the SQL endpoints within a workspace.
|
|
648
|
-
|
|
649
|
-
Parameters
|
|
650
|
-
----------
|
|
651
|
-
workspace : str | uuid.UUID, default=None
|
|
652
|
-
The Fabric workspace name or ID.
|
|
653
|
-
Defaults to None which resolves to the workspace of the attached lakehouse
|
|
654
|
-
or if no lakehouse attached, resolves to the workspace of the notebook.
|
|
655
|
-
|
|
656
|
-
Returns
|
|
657
|
-
-------
|
|
658
|
-
pandas.DataFrame
|
|
659
|
-
A pandas dataframe showing the SQL endpoints within a workspace.
|
|
660
|
-
"""
|
|
661
|
-
|
|
662
|
-
columns = {
|
|
663
|
-
"SQL Endpoint Id": "string",
|
|
664
|
-
"SQL Endpoint Name": "string",
|
|
665
|
-
"Description": "string",
|
|
666
|
-
}
|
|
667
|
-
df = _create_dataframe(columns=columns)
|
|
668
|
-
|
|
669
|
-
(workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace)
|
|
670
|
-
|
|
671
|
-
responses = _base_api(
|
|
672
|
-
request=f"/v1/workspaces/{workspace_id}/sqlEndpoints", uses_pagination=True
|
|
673
|
-
)
|
|
674
|
-
|
|
675
|
-
for r in responses:
|
|
676
|
-
for v in r.get("value", []):
|
|
677
|
-
|
|
678
|
-
new_data = {
|
|
679
|
-
"SQL Endpoint Id": v.get("id"),
|
|
680
|
-
"SQL Endpoint Name": v.get("displayName"),
|
|
681
|
-
"Description": v.get("description"),
|
|
682
|
-
}
|
|
683
|
-
df = pd.concat([df, pd.DataFrame(new_data, index=[0])], ignore_index=True)
|
|
684
|
-
|
|
685
|
-
return df
|
|
686
|
-
|
|
687
|
-
|
|
688
645
|
def list_datamarts(workspace: Optional[str | UUID] = None) -> pd.DataFrame:
|
|
689
646
|
"""
|
|
690
647
|
Shows the datamarts within a workspace.
|
sempy_labs/_notebooks.py
CHANGED
|
@@ -159,6 +159,7 @@ def import_notebook_from_web(
|
|
|
159
159
|
notebook_content=response.content,
|
|
160
160
|
workspace=workspace_id,
|
|
161
161
|
description=description,
|
|
162
|
+
format="ipynb",
|
|
162
163
|
)
|
|
163
164
|
elif len(dfI_filt) > 0 and overwrite:
|
|
164
165
|
print(f"{icons.info} Overwrite of notebooks is currently not supported.")
|
|
@@ -202,9 +203,8 @@ def create_notebook(
|
|
|
202
203
|
otherwise notebook_content should be GIT friendly format
|
|
203
204
|
"""
|
|
204
205
|
|
|
205
|
-
notebook_payload = base64.b64encode(notebook_content.
|
|
206
|
-
|
|
207
|
-
)
|
|
206
|
+
notebook_payload = base64.b64encode(notebook_content).decode("utf-8")
|
|
207
|
+
|
|
208
208
|
definition_payload = {
|
|
209
209
|
"parts": [
|
|
210
210
|
{
|
sempy_labs/_semantic_models.py
CHANGED
|
@@ -8,6 +8,8 @@ from sempy_labs._helper_functions import (
|
|
|
8
8
|
resolve_workspace_name_and_id,
|
|
9
9
|
resolve_dataset_name_and_id,
|
|
10
10
|
delete_item,
|
|
11
|
+
resolve_dataset_id,
|
|
12
|
+
resolve_workspace_id,
|
|
11
13
|
)
|
|
12
14
|
import sempy_labs._icons as icons
|
|
13
15
|
import re
|
|
@@ -227,3 +229,102 @@ def update_semantic_model_refresh_schedule(
|
|
|
227
229
|
print(
|
|
228
230
|
f"{icons.green_dot} Refresh schedule for the '{dataset_name}' within the '{workspace_name}' workspace has been updated."
|
|
229
231
|
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def list_semantic_model_datasources(
|
|
235
|
+
dataset: str | UUID,
|
|
236
|
+
workspace: Optional[str | UUID] = None,
|
|
237
|
+
expand_details: bool = True,
|
|
238
|
+
) -> pd.DataFrame:
|
|
239
|
+
"""
|
|
240
|
+
Lists the data sources for the specified semantic model.
|
|
241
|
+
|
|
242
|
+
This is a wrapper function for the following API: `Datasets - Get Datasources In Group <https://learn.microsoft.com/rest/api/power-bi/datasets/get-datasources-in-group>`_.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
dataset : str | uuid.UUID
|
|
247
|
+
Name or ID of the semantic model.
|
|
248
|
+
workspace : str | uuid.UUID, default=None
|
|
249
|
+
The workspace name or ID.
|
|
250
|
+
Defaults to None which resolves to the workspace of the attached lakehouse
|
|
251
|
+
or if no lakehouse attached, resolves to the workspace of the notebook.
|
|
252
|
+
expand_details : bool, default=True
|
|
253
|
+
If True, expands the connection details for each data source.
|
|
254
|
+
|
|
255
|
+
Returns
|
|
256
|
+
-------
|
|
257
|
+
pandas.DataFrame
|
|
258
|
+
DataFrame containing the data sources for the specified semantic model.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
workspace_id = resolve_workspace_id(workspace)
|
|
262
|
+
dataset_id = resolve_dataset_id(dataset, workspace_id)
|
|
263
|
+
|
|
264
|
+
if expand_details:
|
|
265
|
+
columns = {
|
|
266
|
+
"Datasource Type": "str",
|
|
267
|
+
"Connection Server": "str",
|
|
268
|
+
"Connection Database": "str",
|
|
269
|
+
"Connection Path": "str",
|
|
270
|
+
"Connection Account": "str",
|
|
271
|
+
"Connection Domain": "str",
|
|
272
|
+
"Connection Kind": "str",
|
|
273
|
+
"Connection Email Address": "str",
|
|
274
|
+
"Connection URL": "str",
|
|
275
|
+
"Connection Class Info": "str",
|
|
276
|
+
"Connection Login Server": "str",
|
|
277
|
+
"Datasource Id": "str",
|
|
278
|
+
"Gateway Id": "str",
|
|
279
|
+
}
|
|
280
|
+
else:
|
|
281
|
+
columns = {
|
|
282
|
+
"Datasource Type": "str",
|
|
283
|
+
"Connection Details": "str",
|
|
284
|
+
"Datasource Id": "str",
|
|
285
|
+
"Gateway Id": "str",
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
df = _create_dataframe(columns)
|
|
289
|
+
|
|
290
|
+
response = _base_api(
|
|
291
|
+
request=f"/v1.0/myorg/groups/{workspace_id}/datasets/{dataset_id}/datasources",
|
|
292
|
+
client="fabric_sp",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
dfs = []
|
|
296
|
+
for item in response.json().get("value", []):
|
|
297
|
+
ds_type = item.get("datasourceType")
|
|
298
|
+
conn_details = item.get("connectionDetails", {})
|
|
299
|
+
ds_id = item.get("datasourceId")
|
|
300
|
+
gateway_id = item.get("gatewayId")
|
|
301
|
+
if expand_details:
|
|
302
|
+
new_data = {
|
|
303
|
+
"Datasource Type": ds_type,
|
|
304
|
+
"Connection Server": conn_details.get("server"),
|
|
305
|
+
"Connection Database": conn_details.get("database"),
|
|
306
|
+
"Connection Path": conn_details.get("path"),
|
|
307
|
+
"Connection Account": conn_details.get("account"),
|
|
308
|
+
"Connection Domain": conn_details.get("domain"),
|
|
309
|
+
"Connection Kind": conn_details.get("kind"),
|
|
310
|
+
"Connection Email Address": conn_details.get("emailAddress"),
|
|
311
|
+
"Connection URL": conn_details.get("url"),
|
|
312
|
+
"Connection Class Info": conn_details.get("classInfo"),
|
|
313
|
+
"Connection Login Server": conn_details.get("loginServer"),
|
|
314
|
+
"Datasource Id": ds_id,
|
|
315
|
+
"Gateway Id": gateway_id,
|
|
316
|
+
}
|
|
317
|
+
dfs.append(pd.DataFrame(new_data, index=[0]))
|
|
318
|
+
else:
|
|
319
|
+
new_data = {
|
|
320
|
+
"Datasource Type": ds_type,
|
|
321
|
+
"Connection Details": conn_details,
|
|
322
|
+
"Datasource Id": ds_id,
|
|
323
|
+
"Gateway Id": gateway_id,
|
|
324
|
+
}
|
|
325
|
+
dfs.append(pd.DataFrame([new_data]))
|
|
326
|
+
|
|
327
|
+
if dfs:
|
|
328
|
+
df = pd.concat(dfs, ignore_index=True)
|
|
329
|
+
|
|
330
|
+
return df
|
sempy_labs/_sql.py
CHANGED
|
@@ -82,7 +82,7 @@ class ConnectBase:
|
|
|
82
82
|
)
|
|
83
83
|
|
|
84
84
|
# Set up the connection string
|
|
85
|
-
access_token = SynapseTokenProvider()()
|
|
85
|
+
access_token = SynapseTokenProvider()("sql")
|
|
86
86
|
tokenstruct = _bytes2mswin_bstr(access_token.encode())
|
|
87
87
|
if endpoint_type == "sqldatabase":
|
|
88
88
|
conn_str = f"DRIVER={{ODBC Driver 18 for SQL Server}};SERVER={tds_endpoint};DATABASE={resource_name}-{resource_id};Encrypt=Yes;"
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from typing import Optional, Literal
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from sempy_labs._helper_functions import (
|
|
5
|
+
_base_api,
|
|
6
|
+
_create_dataframe,
|
|
7
|
+
resolve_workspace_name_and_id,
|
|
8
|
+
resolve_item_name_and_id,
|
|
9
|
+
_update_dataframe_datatypes,
|
|
10
|
+
)
|
|
11
|
+
import sempy_labs._icons as icons
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def list_sql_endpoints(workspace: Optional[str | UUID] = None) -> pd.DataFrame:
|
|
15
|
+
"""
|
|
16
|
+
Shows the SQL endpoints within a workspace.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
workspace : str | uuid.UUID, default=None
|
|
21
|
+
The Fabric workspace name or ID.
|
|
22
|
+
Defaults to None which resolves to the workspace of the attached lakehouse
|
|
23
|
+
or if no lakehouse attached, resolves to the workspace of the notebook.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
pandas.DataFrame
|
|
28
|
+
A pandas dataframe showing the SQL endpoints within a workspace.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
columns = {
|
|
32
|
+
"SQL Endpoint Id": "string",
|
|
33
|
+
"SQL Endpoint Name": "string",
|
|
34
|
+
"Description": "string",
|
|
35
|
+
}
|
|
36
|
+
df = _create_dataframe(columns=columns)
|
|
37
|
+
|
|
38
|
+
(workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace)
|
|
39
|
+
|
|
40
|
+
responses = _base_api(
|
|
41
|
+
request=f"/v1/workspaces/{workspace_id}/sqlEndpoints", uses_pagination=True
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
for r in responses:
|
|
45
|
+
for v in r.get("value", []):
|
|
46
|
+
|
|
47
|
+
new_data = {
|
|
48
|
+
"SQL Endpoint Id": v.get("id"),
|
|
49
|
+
"SQL Endpoint Name": v.get("displayName"),
|
|
50
|
+
"Description": v.get("description"),
|
|
51
|
+
}
|
|
52
|
+
df = pd.concat([df, pd.DataFrame(new_data, index=[0])], ignore_index=True)
|
|
53
|
+
|
|
54
|
+
return df
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def refresh_sql_endpoint_metadata(
|
|
58
|
+
item: str | UUID,
|
|
59
|
+
type: Literal["Lakehouse", "MirroredDatabase"],
|
|
60
|
+
workspace: Optional[str | UUID] = None,
|
|
61
|
+
tables: dict[str, list[str]] = None,
|
|
62
|
+
) -> pd.DataFrame:
|
|
63
|
+
"""
|
|
64
|
+
Refreshes the metadata of a SQL endpoint.
|
|
65
|
+
|
|
66
|
+
This is a wrapper function for the following API: `Items - Refresh Sql Endpoint Metadata <https://learn.microsoft.com/rest/api/fabric/sqlendpoint/items/refresh-sql-endpoint-metadata>`_.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
item : str | uuid.UUID
|
|
71
|
+
The name or ID of the item (Lakehouse or MirroredDatabase).
|
|
72
|
+
type : Literal['Lakehouse', 'MirroredDatabase']
|
|
73
|
+
The type of the item. Must be 'Lakehouse' or 'MirroredDatabase'.
|
|
74
|
+
workspace : str | uuid.UUID, default=None
|
|
75
|
+
The Fabric workspace name or ID.
|
|
76
|
+
Defaults to None which resolves to the workspace of the attached lakehouse
|
|
77
|
+
or if no lakehouse attached, resolves to the workspace of the notebook.
|
|
78
|
+
tables : dict[str, list[str]], default=None
|
|
79
|
+
A dictionary where the keys are schema names and the values are lists of table names.
|
|
80
|
+
If empty, all table metadata will be refreshed.
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
{
|
|
84
|
+
"dbo": ["DimDate", "DimGeography"],
|
|
85
|
+
"sls": ["FactSales", "FactBudget"],
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
pandas.DataFrame
|
|
91
|
+
A pandas dataframe showing the status of the metadata refresh operation.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
(workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace)
|
|
95
|
+
|
|
96
|
+
(item_name, item_id) = resolve_item_name_and_id(
|
|
97
|
+
item=item, type=type, workspace=workspace
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if type == "Lakehouse":
|
|
101
|
+
response = _base_api(
|
|
102
|
+
request=f"/v1/workspaces/{workspace_id}/lakehouses/{item_id}",
|
|
103
|
+
client="fabric_sp",
|
|
104
|
+
)
|
|
105
|
+
sql_endpoint_id = (
|
|
106
|
+
response.json()
|
|
107
|
+
.get("properties", {})
|
|
108
|
+
.get("sqlEndpointProperties", {})
|
|
109
|
+
.get("id")
|
|
110
|
+
)
|
|
111
|
+
elif type == "MirroredDatabase":
|
|
112
|
+
response = _base_api(
|
|
113
|
+
request=f"/v1/workspaces/{workspace_id}/mirroredDatabases/{item_id}",
|
|
114
|
+
client="fabric_sp",
|
|
115
|
+
)
|
|
116
|
+
sql_endpoint_id = (
|
|
117
|
+
response.json()
|
|
118
|
+
.get("properties", {})
|
|
119
|
+
.get("sqlEndpointProperties", {})
|
|
120
|
+
.get("id")
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
raise ValueError("Invalid type. Must be 'Lakehouse' or 'MirroredDatabase'.")
|
|
124
|
+
|
|
125
|
+
payload = {}
|
|
126
|
+
if tables:
|
|
127
|
+
payload = {
|
|
128
|
+
"tableDefinitions": [
|
|
129
|
+
{"schema": schema, "tableNames": tables}
|
|
130
|
+
for schema, tables in tables.items()
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
result = _base_api(
|
|
135
|
+
request=f"v1/workspaces/{workspace_id}/sqlEndpoints/{sql_endpoint_id}/refreshMetadata?preview=true",
|
|
136
|
+
method="post",
|
|
137
|
+
status_codes=[200, 202],
|
|
138
|
+
lro_return_json=True,
|
|
139
|
+
payload=payload,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
columns = {
|
|
143
|
+
"Table Name": "string",
|
|
144
|
+
"Status": "string",
|
|
145
|
+
"Start Time": "datetime",
|
|
146
|
+
"End Time": "datetime",
|
|
147
|
+
"Last Successful Sync Time": "datetime",
|
|
148
|
+
"Error Code": "string",
|
|
149
|
+
"Error Message": "string",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
df = pd.json_normalize(result)
|
|
153
|
+
|
|
154
|
+
# Extract error code and message, set to None if no error
|
|
155
|
+
df['Error Code'] = df.get('error.errorCode', None)
|
|
156
|
+
df['Error Message'] = df.get('error.message', None)
|
|
157
|
+
|
|
158
|
+
# Friendly column renaming
|
|
159
|
+
df.rename(columns={
|
|
160
|
+
'tableName': 'Table Name',
|
|
161
|
+
'startDateTime': 'Start Time',
|
|
162
|
+
'endDateTime': 'End Time',
|
|
163
|
+
'status': 'Status',
|
|
164
|
+
'lastSuccessfulSyncDateTime': 'Last Successful Sync Time'
|
|
165
|
+
}, inplace=True)
|
|
166
|
+
|
|
167
|
+
# Drop the original 'error' column if present
|
|
168
|
+
df.drop(columns=[col for col in ['error'] if col in df.columns], inplace=True)
|
|
169
|
+
|
|
170
|
+
# Optional: Reorder columns
|
|
171
|
+
column_order = [
|
|
172
|
+
'Table Name', 'Status', 'Start Time', 'End Time',
|
|
173
|
+
'Last Successful Sync Time', 'Error Code', 'Error Message'
|
|
174
|
+
]
|
|
175
|
+
df = df[column_order]
|
|
176
|
+
|
|
177
|
+
_update_dataframe_datatypes(df, columns)
|
|
178
|
+
|
|
179
|
+
printout = f"{icons.green_dot} The metadata of the SQL endpoint for the '{item_name}' {type.lower()} within the '{workspace_name}' workspace has been refreshed"
|
|
180
|
+
if tables:
|
|
181
|
+
print(f"{printout} for the following tables: {tables}.")
|
|
182
|
+
else:
|
|
183
|
+
print(f"{printout} for all tables.")
|
|
184
|
+
|
|
185
|
+
return df
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from sempy_labs.lakehouse._blobs import _request_blob_api
|
|
2
|
+
from sempy_labs._helper_functions import (
|
|
3
|
+
_xml_to_dict,
|
|
4
|
+
)
|
|
5
|
+
from datetime import datetime, timedelta, timezone
|
|
6
|
+
import xml.etree.ElementTree as ET
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_user_delegation_key():
|
|
10
|
+
"""
|
|
11
|
+
Gets a key that can be used to sign a user delegation SAS (shared access signature). A user delegation SAS grants access to Azure Blob Storage resources by using Microsoft Entra credentials.
|
|
12
|
+
|
|
13
|
+
This is a wrapper function for the following API: `Get User Delegation Key <https://learn.microsoft.com/rest/api/storageservices/get-user-delegation-key>`_.
|
|
14
|
+
|
|
15
|
+
Returns
|
|
16
|
+
-------
|
|
17
|
+
str
|
|
18
|
+
The user delegation key value.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
utc_now = datetime.now(timezone.utc)
|
|
22
|
+
start_time = utc_now + timedelta(minutes=2)
|
|
23
|
+
expiry_time = start_time + timedelta(minutes=60)
|
|
24
|
+
start_str = start_time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
25
|
+
expiry_str = expiry_time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
26
|
+
|
|
27
|
+
payload = f"""<?xml version="1.0" encoding="utf-8"?>
|
|
28
|
+
<KeyInfo>
|
|
29
|
+
<Start>{start_str}</Start>
|
|
30
|
+
<Expiry>{expiry_str}</Expiry>
|
|
31
|
+
</KeyInfo>"""
|
|
32
|
+
|
|
33
|
+
response = _request_blob_api(
|
|
34
|
+
request="?restype=service&comp=userdelegationkey",
|
|
35
|
+
method="post",
|
|
36
|
+
payload=payload,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
root = ET.fromstring(response.content)
|
|
40
|
+
response_json = _xml_to_dict(root)
|
|
41
|
+
|
|
42
|
+
return response_json.get("UserDelegationKey", {}).get("Value", None)
|
sempy_labs/_vpax.py
CHANGED
|
@@ -16,6 +16,7 @@ from sempy_labs._helper_functions import (
|
|
|
16
16
|
file_exists,
|
|
17
17
|
create_abfss_path_from_path,
|
|
18
18
|
)
|
|
19
|
+
from sempy._utils._log import log
|
|
19
20
|
import sempy_labs._icons as icons
|
|
20
21
|
import zipfile
|
|
21
22
|
import requests
|
|
@@ -134,6 +135,7 @@ def init_vertipaq_analyzer():
|
|
|
134
135
|
_vpa_initialized = True
|
|
135
136
|
|
|
136
137
|
|
|
138
|
+
@log
|
|
137
139
|
def create_vpax(
|
|
138
140
|
dataset: str | UUID,
|
|
139
141
|
workspace: Optional[str | UUID] = None,
|
|
@@ -111,9 +111,9 @@ def update_direct_lake_model_connection(
|
|
|
111
111
|
|
|
112
112
|
Parameters
|
|
113
113
|
----------
|
|
114
|
-
dataset : str | UUID
|
|
114
|
+
dataset : str | uuid.UUID
|
|
115
115
|
Name or ID of the semantic model.
|
|
116
|
-
workspace : str | UUID, default=None
|
|
116
|
+
workspace : str | uuid.UUID, default=None
|
|
117
117
|
The Fabric workspace name or ID in which the semantic model exists.
|
|
118
118
|
Defaults to None which resolves to the workspace of the attached lakehouse
|
|
119
119
|
or if no lakehouse attached, resolves to the workspace of the notebook.
|
|
@@ -122,7 +122,7 @@ def update_direct_lake_model_connection(
|
|
|
122
122
|
Defaults to None which resolves to the lakehouse attached to the notebook.
|
|
123
123
|
source_type : str, default="Lakehouse"
|
|
124
124
|
The type of source for the Direct Lake semantic model. Valid options: "Lakehouse", "Warehouse".
|
|
125
|
-
source_workspace : str | UUID, default=None
|
|
125
|
+
source_workspace : str | uuid.UUID, default=None
|
|
126
126
|
The Fabric workspace name or ID used by the lakehouse/warehouse.
|
|
127
127
|
Defaults to None which resolves to the workspace of the attached lakehouse
|
|
128
128
|
or if no lakehouse attached, resolves to the workspace of the notebook.
|
sempy_labs/lakehouse/__init__.py
CHANGED
|
@@ -20,7 +20,6 @@ from sempy_labs.lakehouse._shortcuts import (
|
|
|
20
20
|
from sempy_labs.lakehouse._blobs import (
|
|
21
21
|
recover_lakehouse_object,
|
|
22
22
|
list_blobs,
|
|
23
|
-
get_user_delegation_key,
|
|
24
23
|
)
|
|
25
24
|
from sempy_labs.lakehouse._livy_sessions import (
|
|
26
25
|
list_livy_sessions,
|
|
@@ -51,5 +50,4 @@ __all__ = [
|
|
|
51
50
|
"delete_lakehouse",
|
|
52
51
|
"update_lakehouse",
|
|
53
52
|
"load_table",
|
|
54
|
-
"get_user_delegation_key",
|
|
55
53
|
]
|
sempy_labs/lakehouse/_blobs.py
CHANGED
|
@@ -244,40 +244,3 @@ def recover_lakehouse_object(
|
|
|
244
244
|
print(
|
|
245
245
|
f"{icons.red_dot} An error occurred while recovering the '{blob_name}' blob: {e}"
|
|
246
246
|
)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def get_user_delegation_key():
|
|
250
|
-
"""
|
|
251
|
-
Gets a key that can be used to sign a user delegation SAS (shared access signature). A user delegation SAS grants access to Azure Blob Storage resources by using Microsoft Entra credentials.
|
|
252
|
-
|
|
253
|
-
This is a wrapper function for the following API: `Get User Delegation Key <https://learn.microsoft.com/rest/api/storageservices/get-user-delegation-key>`_.
|
|
254
|
-
|
|
255
|
-
Returns
|
|
256
|
-
-------
|
|
257
|
-
str
|
|
258
|
-
The user delegation key value.
|
|
259
|
-
"""
|
|
260
|
-
|
|
261
|
-
from datetime import datetime, timedelta, timezone
|
|
262
|
-
|
|
263
|
-
utc_now = datetime.now(timezone.utc)
|
|
264
|
-
start_time = utc_now + timedelta(minutes=2)
|
|
265
|
-
expiry_time = start_time + timedelta(minutes=60)
|
|
266
|
-
start_str = start_time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
267
|
-
expiry_str = expiry_time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
268
|
-
|
|
269
|
-
payload = f"""<?xml version="1.0" encoding="utf-8"?>
|
|
270
|
-
<KeyInfo>
|
|
271
|
-
<Start>{start_str}</Start>
|
|
272
|
-
<Expiry>{expiry_str}</Expiry>
|
|
273
|
-
</KeyInfo>"""
|
|
274
|
-
|
|
275
|
-
response = _request_blob_api(
|
|
276
|
-
request="?restype=service&comp=userdelegationkey",
|
|
277
|
-
method="post",
|
|
278
|
-
payload=payload,
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
root = ET.fromstring(response.content)
|
|
282
|
-
response_json = _xml_to_dict(root)
|
|
283
|
-
return response_json.get("UserDelegationKey", {}).get("Value", None)
|