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

@@ -301,3 +301,165 @@ def send_mail(
301
301
  if attachments:
302
302
  printout += f" with {len(attachments)} attachment(s)"
303
303
  print(f"{printout}.")
304
+
305
+
306
+ @log
307
+ def create_user(
308
+ display_name: str,
309
+ user_principal_name: str,
310
+ mail_nickname: str,
311
+ password: str,
312
+ account_enabled: bool = True,
313
+ force_change_password_next_sign_in: bool = True,
314
+ ):
315
+ """
316
+ Creates a new user.
317
+
318
+ This is a wrapper function for the following API: `Create User <https://learn.microsoft.com/graph/api/user-post-users>`_.
319
+
320
+ Service Principal Authentication is required (see `here <https://github.com/microsoft/semantic-link-labs/blob/main/notebooks/Service%20Principal.ipynb>`_ for examples).
321
+
322
+ Parameters
323
+ ----------
324
+ display_name : str
325
+ The name to display in the address book for the user.
326
+ user_principal_name : str
327
+ The user principal name (someuser@contoso.com).
328
+ mail_nickname : str
329
+ The mail alias for the user.
330
+ password : str
331
+ The initial password for the user.
332
+ account_enabled : bool, default=True
333
+ Whether the account is enabled. Default is True.
334
+ force_change_password_next_sign_in : bool, default=True
335
+ Whether the user must change their password on next sign-in. Default is True.
336
+ """
337
+
338
+ payload = {
339
+ "accountEnabled": account_enabled,
340
+ "displayName": display_name,
341
+ "mailNickname": mail_nickname,
342
+ "userPrincipalName": user_principal_name,
343
+ "passwordProfile": {
344
+ "forceChangePasswordNextSignIn": force_change_password_next_sign_in,
345
+ "password": password,
346
+ },
347
+ }
348
+
349
+ _base_api(
350
+ request="users",
351
+ client="graph",
352
+ status_codes=201,
353
+ payload=payload,
354
+ method="post",
355
+ )
356
+
357
+ print(f"{icons.green_dot} The '{display_name}' user has been created successfully.")
358
+
359
+
360
+ @log
361
+ def delete_user(user: str | UUID):
362
+ """
363
+ Deletes a user.
364
+
365
+ This is a wrapper function for the following API: `Delete User <https://learn.microsoft.com/graph/api/user-delete>`_.
366
+
367
+ Service Principal Authentication is required (see `here <https://github.com/microsoft/semantic-link-labs/blob/main/notebooks/Service%20Principal.ipynb>`_ for examples).
368
+
369
+ Parameters
370
+ ----------
371
+ user : str | uuid.UUID
372
+ The user name or ID.
373
+ """
374
+
375
+ user_id = resolve_user_id(user)
376
+
377
+ _base_api(
378
+ request=f"users/{user_id}",
379
+ client="graph",
380
+ status_codes=204,
381
+ method="delete",
382
+ )
383
+
384
+ print(f"{icons.green_dot} The '{user}' user has been deleted successfully.")
385
+
386
+
387
+ @log
388
+ def update_user(
389
+ user: str | UUID,
390
+ display_name: Optional[str] = None,
391
+ user_principal_name: Optional[str] = None,
392
+ given_name: Optional[str] = None,
393
+ surname: Optional[str] = None,
394
+ job_title: Optional[str] = None,
395
+ mail_nickname: Optional[str] = None,
396
+ my_site: Optional[str] = None,
397
+ office_location: Optional[str] = None,
398
+ account_enabled: Optional[bool] = None,
399
+ ):
400
+ """
401
+ Updates a user's properties.
402
+
403
+ This is a wrapper function for the following API: `Update user <https://learn.microsoft.com/graph/api/user-update>`_.
404
+
405
+ Service Principal Authentication is required (see `here <https://github.com/microsoft/semantic-link-labs/blob/main/notebooks/Service%20Principal.ipynb>`_ for examples).
406
+
407
+ Parameters
408
+ ----------
409
+ user : str | uuid.UUID
410
+ The user name or ID.
411
+ display_name : str, default=None
412
+ The name displayed in the address book for the user.
413
+ user_principal_name : str, default=None
414
+ The user principal name (UPN) of the user.
415
+ given_name : str, default=None
416
+ The given name (first name) of the user.
417
+ surname : str, default=None
418
+ The user's surname (family name or last name).
419
+ job_title : str, default=None
420
+ The user's job title.
421
+ mail_nickname : str, default=None
422
+ The mail alias for the user. This property must be specified when a user is created.
423
+ my_site : str, default=None
424
+ The URL for the user's personal site.
425
+ office_location : str, default=None
426
+ The office location in the user's place of business.
427
+ account_enabled : bool, default=None
428
+ Whether the account is enabled. If None, the property will not be updated.
429
+ """
430
+
431
+ user_id = resolve_user_id(user)
432
+
433
+ payload = {}
434
+ if display_name is not None:
435
+ payload["displayName"] = display_name
436
+ if mail_nickname is not None:
437
+ payload["mailNickname"] = mail_nickname
438
+ if user_principal_name is not None:
439
+ payload["userPrincipalName"] = user_principal_name
440
+ if given_name is not None:
441
+ payload["givenName"] = given_name
442
+ if job_title is not None:
443
+ payload["jobTitle"] = job_title
444
+ if my_site is not None:
445
+ payload["mySite"] = my_site
446
+ if office_location is not None:
447
+ payload["officeLocation"] = office_location
448
+ if surname is not None:
449
+ payload["surname"] = surname
450
+ if account_enabled is not None and isinstance(account_enabled, bool):
451
+ payload["accountEnabled"] = account_enabled
452
+
453
+ if not payload:
454
+ print(f"{icons.info} No properties to update.")
455
+ return
456
+
457
+ _base_api(
458
+ request=f"users/{user_id}",
459
+ client="graph",
460
+ status_codes=204,
461
+ payload=payload,
462
+ method="patch",
463
+ )
464
+
465
+ print(f"{icons.green_dot} The '{user}' user has been updated successfully.")
@@ -396,17 +396,22 @@ def list_shortcuts(
396
396
  # Cache and use it to getitem type and name
397
397
  source_item_type = None
398
398
  source_item_name = None
399
- dfI = source_items_df[
400
- source_items_df["Workspace Id"] == source_workspace_id
401
- ]
402
- if dfI.empty:
403
- dfI = fabric.list_items(workspace=source_workspace_id)
404
- source_items_df = pd.concat([source_items_df, dfI], ignore_index=True)
405
-
406
- dfI_filt = dfI[dfI["Id"] == source_item_id]
407
- if not dfI_filt.empty:
408
- source_item_type = dfI_filt["Type"].iloc[0]
409
- source_item_name = dfI_filt["Display Name"].iloc[0]
399
+ try:
400
+ dfI = source_items_df[
401
+ source_items_df["Workspace Id"] == source_workspace_id
402
+ ]
403
+ if dfI.empty:
404
+ dfI = fabric.list_items(workspace=source_workspace_id)
405
+ source_items_df = pd.concat(
406
+ [source_items_df, dfI], ignore_index=True
407
+ )
408
+
409
+ dfI_filt = dfI[dfI["Id"] == source_item_id]
410
+ if not dfI_filt.empty:
411
+ source_item_type = dfI_filt["Type"].iloc[0]
412
+ source_item_name = dfI_filt["Display Name"].iloc[0]
413
+ except Exception:
414
+ pass
410
415
 
411
416
  rows.append(
412
417
  {
@@ -12,8 +12,8 @@ from uuid import UUID
12
12
 
13
13
  @log
14
14
  def report_rebind(
15
- report: str | List[str],
16
- dataset: str,
15
+ report: str | UUID | List[str | UUID],
16
+ dataset: str | UUID,
17
17
  report_workspace: Optional[str | UUID] = None,
18
18
  dataset_workspace: Optional[str | UUID] = None,
19
19
  ):
@@ -77,47 +77,50 @@ def report_rebind(
77
77
 
78
78
  @log
79
79
  def report_rebind_all(
80
- dataset: str,
81
- new_dataset: str,
82
- dataset_workspace: Optional[str] = None,
83
- new_dataset_workpace: Optional[str] = None,
84
- report_workspace: Optional[str | List[str]] = None,
80
+ dataset: str | UUID,
81
+ new_dataset: str | UUID,
82
+ dataset_workspace: Optional[str | UUID] = None,
83
+ new_dataset_workspace: Optional[str | UUID] = None,
84
+ report_workspace: Optional[str | UUID | List[str | UUID]] = None,
85
85
  ):
86
86
  """
87
- Rebinds all reports across all workspaces which are bound to a specific semantic model to a new semantic model.
87
+ Rebinds all reports across the provided report workspaces which are bound to a specific semantic model to a new semantic model.
88
88
 
89
89
  Service Principal Authentication is supported (see `here <https://github.com/microsoft/semantic-link-labs/blob/main/notebooks/Service%20Principal.ipynb>`_ for examples).
90
90
 
91
91
  Parameters
92
92
  ----------
93
- dataset : str
93
+ dataset : str | uuid.UUID
94
94
  Name of the semantic model currently binded to the reports.
95
- new_dataset : str
95
+ new_dataset : str | uuid.UUID
96
96
  Name of the semantic model to rebind to the reports.
97
- dataset_workspace : str, default=None
97
+ dataset_workspace : str | uuid.UUID, default=None
98
98
  The name of the Fabric workspace in which the original semantic model resides.
99
99
  Defaults to None which resolves to the workspace of the attached lakehouse
100
100
  or if no lakehouse attached, resolves to the workspace of the notebook.
101
- new_dataset_workspace : str, default=None
101
+ new_dataset_workspace : str | uuid.UUID, default=None
102
102
  The name of the Fabric workspace in which the new semantic model resides.
103
103
  Defaults to None which resolves to the workspace of the attached lakehouse
104
104
  or if no lakehouse attached, resolves to the workspace of the notebook.
105
- report_workspace : str | List[str], default=None
106
- The name(s) of the Fabric workspace(s) in which the report(s) reside(s).
105
+ report_workspace : str | uuid.UUID | List[str | uuid.UUID], default=None
106
+ The name(s) or IDs of the Fabric workspace(s) in which the report(s) reside(s).
107
107
  Defaults to None which finds all reports in all workspaces which use the semantic model and rebinds them to
108
108
  the new semantic model.
109
109
  """
110
110
 
111
111
  from sempy_labs._list_functions import list_reports_using_semantic_model
112
112
 
113
- if dataset == new_dataset:
114
- raise ValueError(
115
- f"{icons.red_dot} The 'dataset' and 'new_dataset' parameters are both set to '{dataset}'. These parameters must be set to different values."
116
- )
117
-
118
113
  (dataset_name, dataset_id) = resolve_dataset_name_and_id(
119
114
  dataset=dataset, workspace=dataset_workspace
120
115
  )
116
+ (new_dataset_name, new_dataset_id) = resolve_dataset_name_and_id(
117
+ dataset=new_dataset, workspace=new_dataset_workspace
118
+ )
119
+
120
+ if dataset_id == new_dataset_id:
121
+ raise ValueError(
122
+ f"{icons.red_dot} The 'dataset' and 'new_dataset' parameters are both set to '{dataset}'. These parameters must be set to different values."
123
+ )
121
124
  (dataset_workspace_name, dataset_workspace_id) = resolve_workspace_name_and_id(
122
125
  workspace=dataset_workspace
123
126
  )
@@ -129,7 +132,7 @@ def report_rebind_all(
129
132
  dataset=dataset, workspace=dataset_workspace
130
133
  )
131
134
 
132
- if len(dfR) == 0:
135
+ if dfR.empty:
133
136
  print(
134
137
  f"{icons.info} The '{dataset_name}' semantic model within the '{dataset_workspace_name}' workspace has no dependent reports."
135
138
  )
@@ -138,9 +141,18 @@ def report_rebind_all(
138
141
  if report_workspace is None:
139
142
  dfR_filt = dfR.copy()
140
143
  else:
141
- dfR_filt = dfR[dfR["Report Workspace Name"].isin(report_workspace)]
144
+ dfR_filt = dfR[
145
+ (dfR["Report Workspace Name"].isin(report_workspace))
146
+ | (dfR["Report Workspace ID"].isin(report_workspace))
147
+ ]
148
+
149
+ if dfR_filt.empty:
150
+ print(
151
+ f"{icons.info} No reports found for the '{dataset_name}' semantic model within the '{dataset_workspace_name}' workspace."
152
+ )
153
+ return
142
154
 
143
- for i, r in dfR_filt.iterrows():
155
+ for _, r in dfR_filt.iterrows():
144
156
  rpt_name = r["Report Name"]
145
157
  rpt_wksp = r["Report Workspace Name"]
146
158
 
@@ -148,5 +160,5 @@ def report_rebind_all(
148
160
  report=rpt_name,
149
161
  dataset=new_dataset,
150
162
  report_workspace=rpt_wksp,
151
- dataset_workspace=new_dataset_workpace,
163
+ dataset_workspace=new_dataset_workspace,
152
164
  )
@@ -935,9 +935,6 @@ class ReportWrapper:
935
935
  """
936
936
  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.
937
937
 
938
- Parameters
939
- ----------
940
-
941
938
  Returns
942
939
  -------
943
940
  pandas.DataFrame
@@ -980,6 +977,56 @@ class ReportWrapper:
980
977
 
981
978
  return df
982
979
 
980
+ def list_visual_calculations(self) -> pd.DataFrame:
981
+ """
982
+ Shows a list of all `visual calculations <https://learn.microsoft.com/power-bi/transform-model/desktop-visual-calculations-overview>`_.
983
+
984
+ Returns
985
+ -------
986
+ pandas.DataFrame
987
+ A pandas dataframe containing a list of all visual calculations within the report.
988
+ """
989
+
990
+ self._ensure_pbir()
991
+
992
+ columns = {
993
+ "Page Display Name": "str",
994
+ "Visual Name": "str",
995
+ "Name": "str",
996
+ "Language": "str",
997
+ "Expression": "str",
998
+ }
999
+
1000
+ df = _create_dataframe(columns=columns)
1001
+ visual_mapping = self._visual_page_mapping()
1002
+
1003
+ rows = []
1004
+ for v in self.__all_visuals():
1005
+ path = v.get("path")
1006
+ payload = v.get("payload")
1007
+ page_name = visual_mapping.get(path)[0]
1008
+ page_display_name = visual_mapping.get(path)[1]
1009
+ visual_name = payload.get("name")
1010
+ matches = parse("$..field.NativeVisualCalculation").find(payload)
1011
+ if matches:
1012
+ for match in matches:
1013
+ m = match.value
1014
+ rows.append(
1015
+ {
1016
+ "Page Display Name": page_display_name,
1017
+ "Page Name": page_name,
1018
+ "Visual Name": visual_name,
1019
+ "Name": m.get("Name"),
1020
+ "Language": m.get("Language"),
1021
+ "Expression": m.get("Expression"),
1022
+ }
1023
+ )
1024
+
1025
+ if rows:
1026
+ df = pd.DataFrame(rows, columns=list(columns.keys()))
1027
+
1028
+ return df
1029
+
983
1030
  def list_pages(self) -> pd.DataFrame:
984
1031
  """
985
1032
  Shows a list of all pages in the report.
@@ -1651,7 +1698,7 @@ class ReportWrapper:
1651
1698
  bookmarks = [
1652
1699
  o
1653
1700
  for o in self._report_definition.get("parts")
1654
- if o.get("path").endswith("/bookmark.json")
1701
+ if o.get("path").endswith(".bookmark.json")
1655
1702
  ]
1656
1703
 
1657
1704
  rows = []
@@ -2069,8 +2116,8 @@ class ReportWrapper:
2069
2116
  df = dfCV[dfCV["Used in Report"] == False]
2070
2117
 
2071
2118
  if not df.empty:
2072
- cv_remove = df["Custom Visual Name"].values()
2073
- cv_remove_display = df["Custom Visual Display Name"].values()
2119
+ cv_remove = df["Custom Visual Name"].values
2120
+ cv_remove_display = df["Custom Visual Display Name"].values
2074
2121
  else:
2075
2122
  print(
2076
2123
  f"{icons.red_dot} There are no unnecessary custom visuals in the '{self._report_name}' report within the '{self._workspace_name}' workspace."
sempy_labs/tom/_model.py CHANGED
@@ -6,7 +6,7 @@ import os
6
6
  import json
7
7
  from datetime import datetime
8
8
  from decimal import Decimal
9
- from .._helper_functions import (
9
+ from sempy_labs._helper_functions import (
10
10
  format_dax_object_name,
11
11
  generate_guid,
12
12
  _make_list_unique,
@@ -17,10 +17,11 @@ from .._helper_functions import (
17
17
  resolve_item_id,
18
18
  resolve_lakehouse_id,
19
19
  _validate_weight,
20
+ _get_url_prefix,
20
21
  )
21
- from .._list_functions import list_relationships
22
- from .._refresh_semantic_model import refresh_semantic_model
23
- from ..directlake._dl_helper import check_fallback_reason
22
+ from sempy_labs._list_functions import list_relationships
23
+ from sempy_labs._refresh_semantic_model import refresh_semantic_model
24
+ from sempy_labs.directlake._dl_helper import check_fallback_reason
24
25
  from contextlib import contextmanager
25
26
  from typing import List, Iterator, Optional, Union, TYPE_CHECKING, Literal
26
27
  from sempy._utils._log import log
@@ -28,7 +29,9 @@ import sempy_labs._icons as icons
28
29
  import ast
29
30
  from uuid import UUID
30
31
  import sempy_labs._authentication as auth
31
- from ..lakehouse._lakehouse import lakehouse_attached
32
+ from sempy_labs.lakehouse._lakehouse import lakehouse_attached
33
+ import requests
34
+ from sempy.fabric.exceptions import FabricHTTPException
32
35
 
33
36
 
34
37
  if TYPE_CHECKING:
@@ -4601,8 +4604,12 @@ class TOMWrapper:
4601
4604
  pandas.DataFrame
4602
4605
  A pandas dataframe showing the updated measure(s) and their new description(s).
4603
4606
  """
4607
+ import notebookutils
4608
+
4604
4609
  icons.sll_tags.append("GenerateMeasureDescriptions")
4605
4610
 
4611
+ prefix = _get_url_prefix()
4612
+
4606
4613
  df = pd.DataFrame(
4607
4614
  columns=["Table Name", "Measure Name", "Expression", "Description"]
4608
4615
  )
@@ -4655,11 +4662,17 @@ class TOMWrapper:
4655
4662
  "modelItems"
4656
4663
  ].append(new_item)
4657
4664
 
4658
- response = _base_api(
4659
- request="/explore/v202304/nl2nl/completions",
4660
- method="post",
4661
- payload=payload,
4665
+ token = notebookutils.credentials.getToken("pbi")
4666
+ headers = {"Authorization": f"Bearer {token}"}
4667
+ response = requests.post(
4668
+ f"{prefix}/explore/v202304/nl2nl/completions",
4669
+ headers=headers,
4670
+ json=payload,
4662
4671
  )
4672
+ if response.status_code != 200:
4673
+ raise FabricHTTPException(
4674
+ f"Failed to retrieve descriptions: {response.text}"
4675
+ )
4663
4676
 
4664
4677
  for item in response.json().get("modelItems", []):
4665
4678
  ms_name = item["urn"]