semantic-link-labs 0.9.10__py3-none-any.whl → 0.9.11__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.

Files changed (36) hide show
  1. {semantic_link_labs-0.9.10.dist-info → semantic_link_labs-0.9.11.dist-info}/METADATA +27 -21
  2. {semantic_link_labs-0.9.10.dist-info → semantic_link_labs-0.9.11.dist-info}/RECORD +34 -29
  3. {semantic_link_labs-0.9.10.dist-info → semantic_link_labs-0.9.11.dist-info}/WHEEL +1 -1
  4. sempy_labs/__init__.py +22 -1
  5. sempy_labs/_delta_analyzer.py +9 -8
  6. sempy_labs/_environments.py +19 -1
  7. sempy_labs/_generate_semantic_model.py +1 -1
  8. sempy_labs/_helper_functions.py +193 -134
  9. sempy_labs/_kusto.py +25 -23
  10. sempy_labs/_list_functions.py +13 -35
  11. sempy_labs/_model_bpa_rules.py +13 -3
  12. sempy_labs/_notebooks.py +44 -11
  13. sempy_labs/_semantic_models.py +93 -1
  14. sempy_labs/_sql.py +3 -2
  15. sempy_labs/_tags.py +194 -0
  16. sempy_labs/_variable_libraries.py +89 -0
  17. sempy_labs/_vpax.py +386 -0
  18. sempy_labs/admin/__init__.py +8 -0
  19. sempy_labs/admin/_tags.py +126 -0
  20. sempy_labs/directlake/_generate_shared_expression.py +5 -1
  21. sempy_labs/directlake/_update_directlake_model_lakehouse_connection.py +55 -5
  22. sempy_labs/dotnet_lib/dotnet.runtime.config.json +10 -0
  23. sempy_labs/lakehouse/__init__.py +16 -0
  24. sempy_labs/lakehouse/_blobs.py +115 -63
  25. sempy_labs/lakehouse/_get_lakehouse_tables.py +1 -13
  26. sempy_labs/lakehouse/_helper.py +211 -0
  27. sempy_labs/lakehouse/_lakehouse.py +1 -1
  28. sempy_labs/lakehouse/_livy_sessions.py +137 -0
  29. sempy_labs/report/_download_report.py +1 -1
  30. sempy_labs/report/_generate_report.py +5 -1
  31. sempy_labs/report/_reportwrapper.py +31 -18
  32. sempy_labs/tom/_model.py +83 -21
  33. sempy_labs/report/_bpareporttemplate/.pbi/localSettings.json +0 -9
  34. sempy_labs/report/_bpareporttemplate/.platform +0 -11
  35. {semantic_link_labs-0.9.10.dist-info → semantic_link_labs-0.9.11.dist-info}/licenses/LICENSE +0 -0
  36. {semantic_link_labs-0.9.10.dist-info → semantic_link_labs-0.9.11.dist-info}/top_level.txt +0 -0
sempy_labs/_vpax.py ADDED
@@ -0,0 +1,386 @@
1
+ import sempy
2
+ import re
3
+ from urllib.parse import urlparse
4
+ import sempy.fabric as fabric
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Optional
8
+ from uuid import UUID
9
+ from sempy_labs._helper_functions import (
10
+ resolve_workspace_name_and_id,
11
+ resolve_dataset_name_and_id,
12
+ resolve_lakehouse_name_and_id,
13
+ _mount,
14
+ _get_column_aggregate,
15
+ resolve_item_type,
16
+ file_exists,
17
+ create_abfss_path_from_path,
18
+ )
19
+ import sempy_labs._icons as icons
20
+ import zipfile
21
+ import requests
22
+
23
+
24
+ VPA_VERSION = "1.10.0"
25
+ NUGET_BASE_URL = "https://www.nuget.org/api/v2/package"
26
+ ASSEMBLIES = [
27
+ "Dax.Metadata",
28
+ "Dax.Model.Extractor",
29
+ "Dax.ViewVpaExport",
30
+ "Dax.Vpax",
31
+ ]
32
+
33
+ _vpa_initialized = False
34
+ current_dir = Path(__file__).parent
35
+ nuget_dir = current_dir / "nuget_dlls"
36
+
37
+
38
+ def find_lib_folder(pkg_folder: Path) -> Path:
39
+ lib_base = pkg_folder / "lib"
40
+ if not lib_base.exists():
41
+ raise FileNotFoundError(f"No 'lib' directory in package {pkg_folder}")
42
+
43
+ # Prefer netstandard2.0 if available
44
+ candidates = sorted(lib_base.iterdir())
45
+ for preferred in ["netstandard2.0", "net6.0", "net5.0", "netcoreapp3.1", "net472"]:
46
+ if (lib_base / preferred).exists():
47
+ return lib_base / preferred
48
+
49
+ # Fallback: first available folder
50
+ for candidate in candidates:
51
+ if candidate.is_dir():
52
+ return candidate
53
+
54
+ raise FileNotFoundError(f"No usable framework folder found in {lib_base}")
55
+
56
+
57
+ def download_and_extract_package(
58
+ package_name: str, version: str, target_dir: Path
59
+ ) -> Path:
60
+ nupkg_url = f"{NUGET_BASE_URL}/{package_name}/{version}"
61
+ nupkg_path = target_dir / f"{package_name}.{version}.nupkg"
62
+
63
+ if not nupkg_path.exists():
64
+ r = requests.get(nupkg_url)
65
+ r.raise_for_status()
66
+ target_dir.mkdir(parents=True, exist_ok=True)
67
+ with open(nupkg_path, "wb") as f:
68
+ f.write(r.content)
69
+
70
+ extract_path = target_dir / f"{package_name}_{version}"
71
+ if not extract_path.exists():
72
+ with zipfile.ZipFile(nupkg_path, "r") as zip_ref:
73
+ zip_ref.extractall(extract_path)
74
+ return extract_path
75
+
76
+
77
+ def download_and_load_nuget_package(
78
+ package_name, version, target_dir: Path = None, load_assembly=True
79
+ ):
80
+
81
+ from System.Reflection import Assembly
82
+
83
+ if target_dir is None:
84
+ target_dir = nuget_dir
85
+
86
+ # Download and extract
87
+ pkg_folder = download_and_extract_package(package_name, version, target_dir)
88
+ lib_folder = find_lib_folder(pkg_folder)
89
+
90
+ dll_path = lib_folder / f"{package_name}.dll"
91
+ if not dll_path.exists():
92
+ raise FileNotFoundError(f"{dll_path} not found")
93
+
94
+ sys.path.append(str(lib_folder))
95
+ if load_assembly:
96
+ Assembly.LoadFile(str(dll_path))
97
+
98
+
99
+ def init_vertipaq_analyzer():
100
+ global _vpa_initialized
101
+ if _vpa_initialized:
102
+ return
103
+
104
+ from clr_loader import get_coreclr
105
+ from pythonnet import set_runtime
106
+
107
+ # Load the runtime and set it BEFORE importing clr
108
+ runtime_config_path = current_dir / "dotnet_lib" / "dotnet.runtime.config.json"
109
+ rt = get_coreclr(runtime_config=str(runtime_config_path))
110
+ set_runtime(rt)
111
+
112
+ sempy.fabric._client._utils._init_analysis_services()
113
+
114
+ from System.Reflection import Assembly
115
+
116
+ for name in ASSEMBLIES:
117
+ download_and_load_nuget_package(
118
+ name, VPA_VERSION, nuget_dir, load_assembly=False
119
+ )
120
+
121
+ download_and_load_nuget_package("Newtonsoft.Json", "13.0.1")
122
+ download_and_load_nuget_package("System.IO.Packaging", "7.0.0")
123
+
124
+ # For some reason I have to load these after and not inside the download_and_load_nuget_package function
125
+ dll_paths = [
126
+ f"{nuget_dir}/Dax.Model.Extractor_1.10.0/lib/net6.0/Dax.Model.Extractor.dll",
127
+ f"{nuget_dir}/Dax.Metadata_1.10.0/lib/netstandard2.0/Dax.Metadata.dll",
128
+ f"{nuget_dir}/Dax.ViewVpaExport_1.10.0/lib/netstandard2.0/Dax.ViewVpaExport.dll",
129
+ f"{nuget_dir}/Dax.Vpax_1.10.0/lib/net6.0/Dax.Vpax.dll",
130
+ ]
131
+ for dll_path in dll_paths:
132
+ Assembly.LoadFile(dll_path)
133
+
134
+ _vpa_initialized = True
135
+
136
+
137
+ def create_vpax(
138
+ dataset: str | UUID,
139
+ workspace: Optional[str | UUID] = None,
140
+ lakehouse: Optional[str | UUID] = None,
141
+ lakehouse_workspace: Optional[str | UUID] = None,
142
+ file_path: Optional[str] = None,
143
+ read_stats_from_data: bool = False,
144
+ read_direct_query_stats: bool = False,
145
+ direct_lake_stats_mode: str = "ResidentOnly",
146
+ overwrite: bool = False,
147
+ ):
148
+ """
149
+ Creates a .vpax file for a semantic model and saves it to a lakehouse. This is based on `SQL BI's VertiPaq Analyzer <https://www.sqlbi.com/tools/vertipaq-analyzer/>`_.
150
+
151
+ Parameters
152
+ ----------
153
+ dataset : str | uuid.UUID
154
+ Name or ID of the semantic model.
155
+ workspace : str | uuid.UUID, default=None
156
+ The workspace name or ID.
157
+ Defaults to None which resolves to the workspace of the attached lakehouse
158
+ or if no lakehouse attached, resolves to the workspace of the notebook.
159
+ lakehouse : str | uuid.UUID, default=None
160
+ The lakehouse name or ID.
161
+ Defaults to None which resolves to the attached lakehouse.
162
+ lakehouse_workspace : str | uuid.UUID, default=None
163
+ The workspace name or ID of the lakehouse.
164
+ Defaults to None which resolves to the workspace of the attached lakehouse.
165
+ file_path : str, default=None
166
+ The path where the .vpax file will be saved in the lakehouse.
167
+ Defaults to None which resolves to the dataset name.
168
+ read_stats_from_data : bool, default=False
169
+ Whether to read statistics from the data.
170
+ read_direct_query_stats : bool, default=False
171
+ Whether to analyze DirectQuery tables.
172
+ direct_lake_stats_mode : str, default='ResidentOnly'
173
+ The Direct Lake extraction mode. Options are 'ResidentOnly' or 'Full'. This parameter is ignored if read_stats_from_data is False. This parameter is only relevant for tables which use Direct Lake mode.
174
+ If set to 'ResidentOnly', column statistics are obtained only for the columns which are in memory.
175
+ If set to 'Full', column statistics are obtained for all columns - pending the proper identification of the Direct Lake source.
176
+ overwrite : bool, default=False
177
+ Whether to overwrite the .vpax file if it already exists in the lakehouse.
178
+ """
179
+
180
+ init_vertipaq_analyzer()
181
+
182
+ import notebookutils
183
+ from Dax.Metadata import DirectLakeExtractionMode
184
+ from Dax.Model.Extractor import TomExtractor
185
+ from Dax.Vpax.Tools import VpaxTools
186
+ from Dax.ViewVpaExport import Model
187
+ from System.IO import MemoryStream, FileMode, FileStream, FileAccess, FileShare
188
+
189
+ direct_lake_stats_mode = direct_lake_stats_mode.capitalize()
190
+
191
+ (workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace)
192
+ (dataset_name, dataset_id) = resolve_dataset_name_and_id(dataset, workspace_id)
193
+ (lakehouse_workspace_name, lakehouse_workspace_id) = resolve_workspace_name_and_id(
194
+ lakehouse_workspace
195
+ )
196
+ (lakehouse_name, lakehouse_id) = resolve_lakehouse_name_and_id(
197
+ lakehouse=lakehouse, workspace=lakehouse_workspace_id
198
+ )
199
+
200
+ local_path = _mount(lakehouse=lakehouse_id, workspace=lakehouse_workspace_id)
201
+ if file_path is None:
202
+ file_path = dataset_name
203
+
204
+ if file_path.endswith(".vpax"):
205
+ file_path = file_path[:-5]
206
+ save_location = f"Files/{file_path}.vpax"
207
+ path = f"{local_path}/{save_location}"
208
+
209
+ # Check if the .vpax file already exists in the lakehouse
210
+ if not overwrite:
211
+ new_path = create_abfss_path_from_path(
212
+ lakehouse_id, lakehouse_workspace_id, save_location
213
+ )
214
+ if file_exists(new_path):
215
+ print(
216
+ f"{icons.warning} The {save_location} file already exists in the '{lakehouse_name}' lakehouse. Set overwrite=True to overwrite the file."
217
+ )
218
+ return
219
+
220
+ vpax_stream = MemoryStream()
221
+ extractor_app_name = "VPAX Notebook"
222
+ extractor_app_version = "1.0"
223
+ column_batch_size = 50
224
+ token = notebookutils.credentials.getToken("pbi")
225
+ connection_string = f"data source=powerbi://api.powerbi.com/v1.0/myorg/{workspace_name};initial catalog={dataset_name};User ID=;Password={token};Persist Security Info=True;Impersonation Level=Impersonate"
226
+
227
+ print(f"{icons.in_progress} Extracting .vpax metadata...")
228
+
229
+ # Get stats for the model; for direct lake only get is_resident
230
+ dax_model = TomExtractor.GetDaxModel(
231
+ connection_string,
232
+ extractor_app_name,
233
+ extractor_app_version,
234
+ read_stats_from_data,
235
+ 0,
236
+ read_direct_query_stats,
237
+ DirectLakeExtractionMode.ResidentOnly,
238
+ column_batch_size,
239
+ )
240
+ vpa_model = Model(dax_model)
241
+ tom_database = TomExtractor.GetDatabase(connection_string)
242
+
243
+ # Calculate Direct Lake stats for columns which are IsResident=False
244
+ from sempy_labs.tom import connect_semantic_model
245
+
246
+ with connect_semantic_model(dataset=dataset, workspace=workspace) as tom:
247
+ is_direct_lake = tom.is_direct_lake()
248
+ if read_stats_from_data and is_direct_lake and direct_lake_stats_mode == "Full":
249
+
250
+ df_not_resident = fabric.evaluate_dax(
251
+ dataset=dataset,
252
+ workspace=workspace,
253
+ dax_string=""" SELECT [DIMENSION_NAME] AS [TableName], [ATTRIBUTE_NAME] AS [ColumnName] FROM $SYSTEM.DISCOVER_STORAGE_TABLE_COLUMNS WHERE NOT [ISROWNUMBER] AND NOT [DICTIONARY_ISRESIDENT]""",
254
+ )
255
+
256
+ import Microsoft.AnalysisServices.Tabular as TOM
257
+
258
+ print(f"{icons.in_progress} Calculating Direct Lake statistics...")
259
+
260
+ # For SQL endpoints (do once)
261
+ dfI = fabric.list_items(workspace=workspace)
262
+ # Get list of tables in Direct Lake mode which have columns that are not resident
263
+ tbls = [
264
+ t
265
+ for t in tom.model.Tables
266
+ if t.Name in df_not_resident["TableName"].values
267
+ and any(p.Mode == TOM.ModeType.DirectLake for p in t.Partitions)
268
+ ]
269
+ for t in tbls:
270
+ column_cardinalities = {}
271
+ table_name = t.Name
272
+ partition = next(p for p in t.Partitions)
273
+ entity_name = partition.Source.EntityName
274
+ schema_name = partition.Source.SchemaName
275
+ if len(schema_name) == 0 or schema_name == "dbo":
276
+ schema_name = None
277
+ expr_name = partition.Source.ExpressionSource.Name
278
+ expr = tom.model.Expressions[expr_name].Expression
279
+ item_id = None
280
+ if "Sql.Database(" in expr:
281
+ matches = re.findall(r'"([^"]+)"', expr)
282
+ sql_endpoint_id = matches[1]
283
+ dfI_filt = dfI[dfI["Id"] == sql_endpoint_id]
284
+ item_name = (
285
+ dfI_filt["Display Name"].iloc[0] if not dfI_filt.empty else None
286
+ )
287
+ dfI_filt2 = dfI[
288
+ (dfI["Display Name"] == item_name)
289
+ & (dfI["Type"].isin(["Lakehouse", "Warehouse"]))
290
+ ]
291
+ item_id = dfI_filt2["Id"].iloc[0]
292
+ item_type = dfI_filt2["Type"].iloc[0]
293
+ item_workspace_id = workspace_id
294
+ elif "AzureStorage.DataLake(" in expr:
295
+ match = re.search(r'AzureStorage\.DataLake\("([^"]+)"', expr)
296
+ if match:
297
+ url = match.group(1)
298
+ path_parts = urlparse(url).path.strip("/").split("/")
299
+ if len(path_parts) >= 2:
300
+ item_workspace_id, item_id = (
301
+ path_parts[0],
302
+ path_parts[1],
303
+ )
304
+ item_type = resolve_item_type(
305
+ item_id=item_id, workspace=workspace_id
306
+ )
307
+ else:
308
+ raise NotImplementedError(
309
+ f"Direct Lake source '{expr}' is not supported. Please report this issue on GitHub (https://github.com/microsoft/semantic-link-labs/issues)."
310
+ )
311
+
312
+ if not item_id:
313
+ print(
314
+ f"{icons.info} Cannot determine the Direct Lake source of the '{table_name}' table."
315
+ )
316
+ elif item_type == "Warehouse":
317
+ print(
318
+ f"{icons.info} The '{table_name}' table references a warehouse. Warehouses are not yet supported for this method."
319
+ )
320
+ else:
321
+ df_not_resident_cols = df_not_resident[
322
+ df_not_resident["TableName"] == table_name
323
+ ]
324
+ col_dict = {
325
+ c.Name: c.SourceColumn
326
+ for c in t.Columns
327
+ if c.Type != TOM.ColumnType.RowNumber
328
+ and c.Name in df_not_resident_cols["ColumnName"].values
329
+ }
330
+ col_agg = _get_column_aggregate(
331
+ lakehouse=item_id,
332
+ workspace=item_workspace_id,
333
+ table_name=entity_name,
334
+ schema_name=schema_name,
335
+ column_name=list(col_dict.values()),
336
+ function="distinct",
337
+ )
338
+ column_cardinalities = {
339
+ column_name: col_agg[source_column]
340
+ for column_name, source_column in col_dict.items()
341
+ if source_column in col_agg
342
+ }
343
+
344
+ # Update the dax_model file with column cardinalities
345
+ tbl = next(
346
+ table
347
+ for table in dax_model.Tables
348
+ if str(table.TableName) == table_name
349
+ )
350
+ # print(
351
+ # f"{icons.in_progress} Calculating column cardinalities for the '{table_name}' table..."
352
+ # )
353
+ cols = [
354
+ col
355
+ for col in tbl.Columns
356
+ if str(col.ColumnType) != "RowNumber"
357
+ and str(col.ColumnName) in column_cardinalities
358
+ ]
359
+ for col in cols:
360
+ # print(str(col.ColumnName), col.ColumnCardinality)
361
+ col.ColumnCardinality = column_cardinalities.get(
362
+ str(col.ColumnName)
363
+ )
364
+
365
+ VpaxTools.ExportVpax(vpax_stream, dax_model, vpa_model, tom_database)
366
+
367
+ print(f"{icons.in_progress} Exporting .vpax file...")
368
+
369
+ mode = FileMode.Create
370
+ file_stream = FileStream(path, mode, FileAccess.Write, FileShare.Read)
371
+ vpax_stream.CopyTo(file_stream)
372
+ file_stream.Close()
373
+
374
+ print(
375
+ f"{icons.green_dot} The {file_path}.vpax file has been saved in the '{lakehouse_name}' lakehouse within the '{lakehouse_workspace_name}' workspace."
376
+ )
377
+
378
+
379
+ def _dax_distinctcount(table_name, columns):
380
+
381
+ dax = "EVALUATE\nROW("
382
+ for c in columns:
383
+ full_name = f"'{table_name}'[{c}]"
384
+ dax += f"""\n"{c}", DISTINCTCOUNT({full_name}),"""
385
+
386
+ return f"{dax.rstrip(',')}\n)"
@@ -84,6 +84,11 @@ from sempy_labs.admin._git import (
84
84
  from sempy_labs.admin._dataflows import (
85
85
  export_dataflow,
86
86
  )
87
+ from sempy_labs.admin._tags import (
88
+ list_tags,
89
+ create_tags,
90
+ delete_tag,
91
+ )
87
92
 
88
93
  __all__ = [
89
94
  "list_items",
@@ -139,4 +144,7 @@ __all__ = [
139
144
  "list_report_subscriptions",
140
145
  "get_refreshables",
141
146
  "export_dataflow",
147
+ "list_tags",
148
+ "create_tags",
149
+ "delete_tag",
142
150
  ]
@@ -0,0 +1,126 @@
1
+ from sempy_labs._helper_functions import (
2
+ _base_api,
3
+ _is_valid_uuid,
4
+ )
5
+ from uuid import UUID
6
+ from sempy_labs._tags import list_tags
7
+ import sempy_labs._icons as icons
8
+ from typing import List
9
+
10
+
11
+ def resolve_tag_id(tag: str | UUID):
12
+
13
+ if _is_valid_uuid(tag):
14
+ tag_id = tag
15
+ else:
16
+ df = list_tags()
17
+ df[df["Tag Name"] == tag]
18
+ if df.empty:
19
+ raise ValueError(f"{icons.red_dot} The '{tag}' tag does not exist.")
20
+ tag_id = df.iloc[0]["Tag Id"]
21
+
22
+ return tag_id
23
+
24
+
25
+ def create_tags(tags: str | List[str]):
26
+ """
27
+ Creates a new tag or tags.
28
+
29
+ This is a wrapper function for the following API: `Tags - Bulk Create Tags <https://learn.microsoft.com/rest/api/fabric/admin/tags/bulk-create-tags>`_.
30
+
31
+ Service Principal Authentication is supported (see `here <https://github.com/microsoft/semantic-link-labs/blob/main/notebooks/Service%20Principal.ipynb>`_ for examples).
32
+
33
+ Parameters
34
+ ----------
35
+ tags : str | List[str]
36
+ The name of the tag or tags to create.
37
+ """
38
+
39
+ if isinstance(tags, str):
40
+ tags = [tags]
41
+
42
+ # Check the length of the tags
43
+ for tag in tags:
44
+ if len(tag) > 40:
45
+ raise ValueError(
46
+ f"{icons.red_dot} The '{tag}' tag name is too long. It must be 40 characters or less."
47
+ )
48
+
49
+ # Check if the tags already exist
50
+ df = list_tags()
51
+ existing_names = df["Tag Name"].tolist()
52
+ existing_ids = df["Tag Id"].tolist()
53
+
54
+ available_tags = [
55
+ tag for tag in tags if tag not in existing_names and tag not in existing_ids
56
+ ]
57
+ unavailable_tags = [
58
+ tag for tag in tags if tag in existing_names or tag in existing_ids
59
+ ]
60
+
61
+ print(f"{icons.warning} The following tags already exist: {unavailable_tags}")
62
+ if not available_tags:
63
+ print(f"{icons.info} No new tags to create.")
64
+ return
65
+
66
+ payload = [{"displayName": name} for name in available_tags]
67
+
68
+ for tag in tags:
69
+ _base_api(
70
+ request="/v1/admin/bulkCreateTags",
71
+ client="fabric_sp",
72
+ method="post",
73
+ payload=payload,
74
+ status_codes=201,
75
+ )
76
+
77
+ print(f"{icons.green_dot} The '{available_tags}' tag(s) have been created.")
78
+
79
+
80
+ def delete_tag(tag: str | UUID):
81
+ """
82
+ Deletes a tag.
83
+
84
+ This is a wrapper function for the following API: `Tags - Delete Tag <https://learn.microsoft.com/rest/api/fabric/admin/tags/delete-tag>`_.
85
+
86
+ Service Principal Authentication is supported (see `here <https://github.com/microsoft/semantic-link-labs/blob/main/notebooks/Service%20Principal.ipynb>`_ for examples).
87
+
88
+ Parameters
89
+ ----------
90
+ tag : str | uuid.UUID
91
+ The name or ID of the tag to delete.
92
+ """
93
+
94
+ tag_id = resolve_tag_id(tag)
95
+
96
+ _base_api(request=f"/v1/admin/tags/{tag_id}", client="fabric_sp", method="delete")
97
+
98
+ print(f"{icons.green_dot} The '{tag}' tag has been deleted.")
99
+
100
+
101
+ def update_tag(name: str, tag: str | UUID):
102
+ """
103
+ Updates the name of a tag.
104
+
105
+ This is a wrapper function for the following API: `Tags - Update Tag <https://learn.microsoft.com/rest/api/fabric/admin/tags/update-tag>`_.
106
+
107
+ Service Principal Authentication is supported (see `here <https://github.com/microsoft/semantic-link-labs/blob/main/notebooks/Service%20Principal.ipynb>`_ for examples).
108
+
109
+ Parameters
110
+ ----------
111
+ name : str
112
+ The new name of the tag.
113
+ tag : str | uuid.UUID
114
+ The name or ID of the tag to update.
115
+ """
116
+
117
+ tag_id = resolve_tag_id(tag)
118
+
119
+ _base_api(
120
+ request=f"/v1/admin/tags/{tag_id}",
121
+ client="fabric_sp",
122
+ method="patch",
123
+ payload={"displayName": name},
124
+ )
125
+
126
+ print(f"{icons.green_dot} The '{tag}' tag has been renamed to '{name}'.")
@@ -3,6 +3,7 @@ from sempy_labs._helper_functions import (
3
3
  _base_api,
4
4
  resolve_lakehouse_name_and_id,
5
5
  resolve_item_name_and_id,
6
+ _get_fabric_context_setting,
6
7
  )
7
8
  from typing import Optional
8
9
  import sempy_labs._icons as icons
@@ -85,4 +86,7 @@ def generate_shared_expression(
85
86
  return f"{start_expr}{mid_expr}{end_expr}"
86
87
  else:
87
88
  # Build DL/OL expression
88
- return f"""let\n\tSource = AzureStorage.DataLake("onelake.dfs.fabric.microsoft.com/{workspace_id}/{item_id}")\nin\n\tSource"""
89
+ env = _get_fabric_context_setting("spark.trident.pbienv").lower()
90
+ env = "" if env == "prod" else f"{env}-"
91
+
92
+ return f"""let\n\tSource = AzureStorage.DataLake("https://{env}onelake.dfs.fabric.microsoft.com/{workspace_id}/{item_id}")\nin\n\tSource"""
@@ -7,7 +7,7 @@ from sempy_labs._helper_functions import (
7
7
  )
8
8
  from sempy._utils._log import log
9
9
  from sempy_labs.tom import connect_semantic_model
10
- from typing import Optional
10
+ from typing import Optional, List
11
11
  import sempy_labs._icons as icons
12
12
  from uuid import UUID
13
13
  import re
@@ -19,7 +19,9 @@ def _extract_expression_list(expression):
19
19
  """
20
20
 
21
21
  pattern_sql = r'Sql\.Database\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)'
22
- pattern_no_sql = r'AzureDataLakeStorage\s*\{\s*"server".*?:\s*onelake\.dfs\.fabric\.microsoft\.com"\s*,\s*"path"\s*:\s*"/([\da-fA-F-]+)\s*/\s*([\da-fA-F-]+)\s*/"\s*\}'
22
+ pattern_no_sql = (
23
+ r'AzureStorage\.DataLake\(".*?/([0-9a-fA-F\-]{36})/([0-9a-fA-F\-]{36})"'
24
+ )
23
25
 
24
26
  match_sql = re.search(pattern_sql, expression)
25
27
  match_no_sql = re.search(pattern_no_sql, expression)
@@ -102,6 +104,7 @@ def update_direct_lake_model_connection(
102
104
  source_type: str = "Lakehouse",
103
105
  source_workspace: Optional[str | UUID] = None,
104
106
  use_sql_endpoint: bool = True,
107
+ tables: Optional[str | List[str]] = None,
105
108
  ):
106
109
  """
107
110
  Remaps a Direct Lake semantic model's SQL Endpoint connection to a new lakehouse/warehouse.
@@ -126,12 +129,19 @@ def update_direct_lake_model_connection(
126
129
  use_sql_endpoint : bool, default=True
127
130
  If True, the SQL Endpoint will be used for the connection.
128
131
  If False, Direct Lake over OneLake will be used.
132
+ tables : str | List[str], default=None
133
+ The name(s) of the table(s) to update in the Direct Lake semantic model.
134
+ If None, all tables will be updated (if there is only one expression).
135
+ If multiple tables are specified, they must be provided as a list.
129
136
  """
130
137
  if use_sql_endpoint:
131
138
  icons.sll_tags.append("UpdateDLConnection_SQL")
132
139
  else:
133
140
  icons.sll_tags.append("UpdateDLConnection_DLOL")
134
141
 
142
+ if isinstance(tables, str):
143
+ tables = [tables]
144
+
135
145
  (workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace)
136
146
  (dataset_name, dataset_id) = resolve_dataset_name_and_id(dataset, workspace_id)
137
147
 
@@ -174,7 +184,12 @@ def update_direct_lake_model_connection(
174
184
  )
175
185
 
176
186
  # Update the single connection expression
177
- if len(expressions) == 1:
187
+ if len(expressions) > 1 and not tables:
188
+ print(
189
+ f"{icons.info} Multiple expressions found in the model. Please specify the tables to update using the 'tables parameter."
190
+ )
191
+ return
192
+ elif len(expressions) == 1 and not tables:
178
193
  expr = expressions[0]
179
194
  tom.model.Expressions[expr].Expression = shared_expression
180
195
 
@@ -182,6 +197,41 @@ def update_direct_lake_model_connection(
182
197
  f"{icons.green_dot} The expression in the '{dataset_name}' semantic model within the '{workspace_name}' workspace has been updated to point to the '{source}' {source_type.lower()} in the '{source_workspace}' workspace."
183
198
  )
184
199
  else:
185
- print(
186
- f"{icons.info} Multiple expressions found in the model. Please use the update_direct_lake_partition_entity function to update specific tables."
200
+ import sempy
201
+
202
+ sempy.fabric._client._utils._init_analysis_services()
203
+ import Microsoft.AnalysisServices.Tabular as TOM
204
+
205
+ expr_list = _extract_expression_list(shared_expression)
206
+
207
+ expr_name = next(
208
+ (name for name, exp in expression_dict.items() if exp == expr_list),
209
+ None,
187
210
  )
211
+
212
+ # If the expression does not already exist, create it
213
+ def generate_unique_name(existing_names):
214
+ i = 1
215
+ while True:
216
+ candidate = f"DatabaseQuery{i}"
217
+ if candidate not in existing_names:
218
+ return candidate
219
+ i += 1
220
+
221
+ if not expr_name:
222
+ expr_name = generate_unique_name(expressions)
223
+ tom.add_expression(name=expr_name, expression=shared_expression)
224
+
225
+ all_tables = [t.Name for t in tom.model.Tables]
226
+ for t_name in tables:
227
+ if t_name not in all_tables:
228
+ raise ValueError(
229
+ f"{icons.red_dot} The table '{t_name}' does not exist in the '{dataset_name}' semantic model within the '{workspace_name}' workspace."
230
+ )
231
+ p = next(p for p in tom.model.Tables[t_name].Partitions)
232
+ if p.Mode != TOM.ModeType.DirectLake:
233
+ raise ValueError(
234
+ f"{icons.red_dot} The table '{t_name}' in the '{dataset_name}' semantic model within the '{workspace_name}' workspace is not in Direct Lake mode. This function is only applicable to Direct Lake tables."
235
+ )
236
+
237
+ p.Source.ExpressionSource = tom.model.Expressions[expr_name]
@@ -0,0 +1,10 @@
1
+ {
2
+ "runtimeOptions": {
3
+ "tfm": "net6.0",
4
+ "framework": {
5
+ "name": "Microsoft.NETCore.App",
6
+ "version": "6.0.0"
7
+ },
8
+ "rollForward": "Major"
9
+ }
10
+ }
@@ -20,6 +20,16 @@ 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
+ )
25
+ from sempy_labs.lakehouse._livy_sessions import (
26
+ list_livy_sessions,
27
+ )
28
+ from sempy_labs.lakehouse._helper import (
29
+ is_v_ordered,
30
+ delete_lakehouse,
31
+ update_lakehouse,
32
+ load_table,
23
33
  )
24
34
 
25
35
  __all__ = [
@@ -36,4 +46,10 @@ __all__ = [
36
46
  "list_shortcuts",
37
47
  "recover_lakehouse_object",
38
48
  "list_blobs",
49
+ "list_livy_sessions",
50
+ "is_v_ordered",
51
+ "delete_lakehouse",
52
+ "update_lakehouse",
53
+ "load_table",
54
+ "get_user_delegation_key",
39
55
  ]