semantic-link-labs 0.4.2__py3-none-any.whl → 0.6.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.

Files changed (54) hide show
  1. {semantic_link_labs-0.4.2.dist-info → semantic_link_labs-0.6.0.dist-info}/METADATA +2 -2
  2. semantic_link_labs-0.6.0.dist-info/RECORD +54 -0
  3. {semantic_link_labs-0.4.2.dist-info → semantic_link_labs-0.6.0.dist-info}/WHEEL +1 -1
  4. sempy_labs/__init__.py +44 -14
  5. sempy_labs/_ai.py +31 -32
  6. sempy_labs/_clear_cache.py +5 -8
  7. sempy_labs/_connections.py +80 -72
  8. sempy_labs/_dax.py +7 -9
  9. sempy_labs/_generate_semantic_model.py +60 -54
  10. sempy_labs/_helper_functions.py +8 -10
  11. sempy_labs/_icons.py +15 -0
  12. sempy_labs/_list_functions.py +1139 -428
  13. sempy_labs/_model_auto_build.py +5 -6
  14. sempy_labs/_model_bpa.py +134 -1125
  15. sempy_labs/_model_bpa_rules.py +831 -0
  16. sempy_labs/_model_dependencies.py +21 -25
  17. sempy_labs/_one_lake_integration.py +10 -7
  18. sempy_labs/_query_scale_out.py +83 -93
  19. sempy_labs/_refresh_semantic_model.py +12 -16
  20. sempy_labs/_translations.py +214 -288
  21. sempy_labs/_vertipaq.py +51 -42
  22. sempy_labs/directlake/__init__.py +2 -0
  23. sempy_labs/directlake/_directlake_schema_compare.py +12 -11
  24. sempy_labs/directlake/_directlake_schema_sync.py +13 -23
  25. sempy_labs/directlake/_fallback.py +5 -7
  26. sempy_labs/directlake/_get_directlake_lakehouse.py +1 -1
  27. sempy_labs/directlake/_get_shared_expression.py +4 -8
  28. sempy_labs/directlake/_guardrails.py +6 -8
  29. sempy_labs/directlake/_list_directlake_model_calc_tables.py +18 -12
  30. sempy_labs/directlake/_show_unsupported_directlake_objects.py +4 -4
  31. sempy_labs/directlake/_update_directlake_model_lakehouse_connection.py +9 -8
  32. sempy_labs/directlake/_update_directlake_partition_entity.py +129 -12
  33. sempy_labs/directlake/_warm_cache.py +5 -5
  34. sempy_labs/lakehouse/_get_lakehouse_columns.py +2 -2
  35. sempy_labs/lakehouse/_get_lakehouse_tables.py +4 -4
  36. sempy_labs/lakehouse/_lakehouse.py +3 -4
  37. sempy_labs/lakehouse/_shortcuts.py +17 -13
  38. sempy_labs/migration/__init__.py +1 -1
  39. sempy_labs/migration/_create_pqt_file.py +21 -24
  40. sempy_labs/migration/_migrate_calctables_to_lakehouse.py +16 -13
  41. sempy_labs/migration/_migrate_calctables_to_semantic_model.py +17 -18
  42. sempy_labs/migration/_migrate_model_objects_to_semantic_model.py +45 -46
  43. sempy_labs/migration/_migrate_tables_columns_to_semantic_model.py +14 -14
  44. sempy_labs/migration/_migration_validation.py +6 -2
  45. sempy_labs/migration/_refresh_calc_tables.py +10 -5
  46. sempy_labs/report/__init__.py +2 -2
  47. sempy_labs/report/_generate_report.py +8 -7
  48. sempy_labs/report/_report_functions.py +47 -52
  49. sempy_labs/report/_report_rebind.py +38 -37
  50. sempy_labs/tom/__init__.py +1 -4
  51. sempy_labs/tom/_model.py +541 -180
  52. semantic_link_labs-0.4.2.dist-info/RECORD +0 -53
  53. {semantic_link_labs-0.4.2.dist-info → semantic_link_labs-0.6.0.dist-info}/LICENSE +0 -0
  54. {semantic_link_labs-0.4.2.dist-info → semantic_link_labs-0.6.0.dist-info}/top_level.txt +0 -0
sempy_labs/tom/_model.py CHANGED
@@ -21,7 +21,8 @@ class TOMWrapper:
21
21
  """
22
22
  Convenience wrapper around the TOM object model for a semantic model. Always use the connect_semantic_model function to make sure the TOM object is initialized correctly.
23
23
 
24
- `XMLA read/write endpoints <https://learn.microsoft.com/power-bi/enterprise/service-premium-connect-tools#to-enable-read-write-for-a-premium-capacity>`_ must be enabled if setting the readonly parameter to False.
24
+ `XMLA read/write endpoints <https://learn.microsoft.com/power-bi/enterprise/service-premium-connect-tools#to-enable-read-write-for-a-premium-capacity>`_ must
25
+ be enabled if setting the readonly parameter to False.
25
26
  """
26
27
 
27
28
  _dataset: str
@@ -93,7 +94,9 @@ class TOMWrapper:
93
94
  import Microsoft.AnalysisServices.Tabular as TOM
94
95
 
95
96
  for t in self.model.Tables:
96
- if any(p.SourceType == TOM.ColumnType.Calculated for p in t.Partitions):
97
+ if any(
98
+ p.SourceType == TOM.PartitionSourceType.Calculated for p in t.Partitions
99
+ ):
97
100
  yield t
98
101
 
99
102
  def all_calculation_groups(self):
@@ -500,7 +503,8 @@ class TOMWrapper:
500
503
  description: Optional[str] = None,
501
504
  ):
502
505
  """
503
- Adds a `calculation item <https://learn.microsoft.com/dotnet/api/microsoft.analysisservices.tabular.calculationitem?view=analysisservices-dotnet>`_ to a `calculation group <https://learn.microsoft.com/dotnet/api/microsoft.analysisservices.tabular.calculationgroup?view=analysisservices-dotnet>`_ within a semantic model.
506
+ Adds a `calculation item <https://learn.microsoft.com/dotnet/api/microsoft.analysisservices.tabular.calculationitem?view=analysisservices-dotnet>`_ to
507
+ a `calculation group <https://learn.microsoft.com/dotnet/api/microsoft.analysisservices.tabular.calculationgroup?view=analysisservices-dotnet>`_ within a semantic model.
504
508
 
505
509
  Parameters
506
510
  ----------
@@ -582,11 +586,15 @@ class TOMWrapper:
582
586
  tp.Table = self.model.Tables[table_name]
583
587
  tp.FilterExpression = filter_expression
584
588
 
585
- try:
589
+ if any(
590
+ t.Name == table_name and r.Name == role_name
591
+ for r in self.model.Roles
592
+ for t in r.TablePermissions
593
+ ):
586
594
  self.model.Roles[role_name].TablePermissions[
587
595
  table_name
588
596
  ].FilterExpression = filter_expression
589
- except:
597
+ else:
590
598
  self.model.Roles[role_name].TablePermissions.Add(tp)
591
599
 
592
600
  def set_ols(
@@ -613,17 +621,22 @@ class TOMWrapper:
613
621
  permission = permission.capitalize()
614
622
 
615
623
  if permission not in ["Read", "None", "Default"]:
616
- print(f"ERROR! Invalid 'permission' value.")
617
- return
624
+ raise ValueError(f"{icons.red_dot} Invalid 'permission' value.")
618
625
 
619
626
  cp = TOM.ColumnPermission()
620
627
  cp.Column = self.model.Tables[table_name].Columns[column_name]
621
628
  cp.MetadataPermission = System.Enum.Parse(TOM.MetadataPermission, permission)
622
- try:
629
+
630
+ if any(
631
+ c.Name == column_name and t.Name == table_name and r.Name == role_name
632
+ for r in self.model.Roles
633
+ for t in r.TablePermissions
634
+ for c in t.ColumnPermissions
635
+ ):
623
636
  self.model.Roles[role_name].TablePermissions[table_name].ColumnPermissions[
624
637
  column_name
625
638
  ].MetadataPermission = System.Enum.Parse(TOM.MetadataPermission, permission)
626
- except:
639
+ else:
627
640
  self.model.Roles[role_name].TablePermissions[
628
641
  table_name
629
642
  ].ColumnPermissions.Add(cp)
@@ -658,22 +671,22 @@ class TOMWrapper:
658
671
  import Microsoft.AnalysisServices.Tabular as TOM
659
672
 
660
673
  if isinstance(columns, str):
661
- print(
674
+ raise ValueError(
662
675
  f"{icons.red_dot} The 'levels' parameter must be a list. For example: ['Continent', 'Country', 'City']"
663
676
  )
664
- return
677
+
665
678
  if len(columns) == 1:
666
- print(f"{icons.red_dot} There must be at least 2 levels in order to create a hierarchy.")
667
- return
679
+ raise ValueError(
680
+ f"{icons.red_dot} There must be at least 2 levels in order to create a hierarchy."
681
+ )
668
682
 
669
683
  if levels is None:
670
684
  levels = columns
671
685
 
672
686
  if len(columns) != len(levels):
673
- print(
687
+ raise ValueError(
674
688
  f"{icons.red_dot} If specifying level names, you must specify a level for each column."
675
689
  )
676
- return
677
690
 
678
691
  obj = TOM.Hierarchy()
679
692
  obj.Name = hierarchy_name
@@ -864,10 +877,8 @@ class TOMWrapper:
864
877
  cul = TOM.Culture()
865
878
  cul.Name = language
866
879
 
867
- try:
880
+ if not any(c.Name == language for c in self.model.Cultures):
868
881
  self.model.Cultures.Add(cul)
869
- except:
870
- pass
871
882
 
872
883
  def add_perspective(self, perspective_name: str):
873
884
  """
@@ -1003,7 +1014,7 @@ class TOMWrapper:
1003
1014
  import System
1004
1015
 
1005
1016
  if base_column is not None and base_table is None:
1006
- print(
1017
+ raise ValueError(
1007
1018
  f"{icons.red_dot} If you specify the base table you must also specify the base column"
1008
1019
  )
1009
1020
 
@@ -1015,10 +1026,9 @@ class TOMWrapper:
1015
1026
 
1016
1027
  summarizationTypes = ["Sum", "GroupBy", "Count", "Min", "Max"]
1017
1028
  if summarization_type not in summarizationTypes:
1018
- print(
1029
+ raise ValueError(
1019
1030
  f"{icons.red_dot} The 'summarization_type' parameter must be one of the following valuse: {summarizationTypes}."
1020
1031
  )
1021
- return
1022
1032
 
1023
1033
  ao = TOM.AlternateOf()
1024
1034
  ao.Summarization = System.Enum.Parse(TOM.SummarizationType, summarization_type)
@@ -1096,9 +1106,9 @@ class TOMWrapper:
1096
1106
  ann.Name = name
1097
1107
  ann.Value = value
1098
1108
 
1099
- try:
1109
+ if any(a.Name == name for a in object.Annotations):
1100
1110
  object.Annotations[name].Value = value
1101
- except:
1111
+ else:
1102
1112
  object.Annotations.Add(ann)
1103
1113
 
1104
1114
  def get_annotation_value(self, object, name: str):
@@ -1117,8 +1127,12 @@ class TOMWrapper:
1117
1127
  str
1118
1128
  The annotation value.
1119
1129
  """
1130
+ if any(a.Name == name for a in object.Annotations):
1131
+ value = object.Annotations[name].Value
1132
+ else:
1133
+ value = None
1120
1134
 
1121
- return object.Annotations[name].Value
1135
+ return value
1122
1136
 
1123
1137
  def remove_annotation(self, object, name: str):
1124
1138
  """
@@ -1196,9 +1210,9 @@ class TOMWrapper:
1196
1210
  ep.Name = name
1197
1211
  ep.Value = value
1198
1212
 
1199
- try:
1213
+ if any(a.Name == name for a in object.Annotations):
1200
1214
  object.ExtendedProperties[name].Value = value
1201
- except:
1215
+ else:
1202
1216
  object.ExtendedProperties.Add(ep)
1203
1217
 
1204
1218
  def get_extended_property_value(self, object, name: str):
@@ -1217,8 +1231,12 @@ class TOMWrapper:
1217
1231
  str
1218
1232
  The extended property value.
1219
1233
  """
1234
+ if any(a.Name == name for a in object.ExtendedProperties):
1235
+ value = object.ExtendedProperties[name].Value
1236
+ else:
1237
+ value = None
1220
1238
 
1221
- return object.ExtendedProperties[name].Value
1239
+ return value
1222
1240
 
1223
1241
  def remove_extended_property(self, object, name: str):
1224
1242
  """
@@ -1277,10 +1295,9 @@ class TOMWrapper:
1277
1295
  objectType = object.ObjectType
1278
1296
 
1279
1297
  if objectType not in validObjects:
1280
- print(
1298
+ raise ValueError(
1281
1299
  f"{icons.red_dot} Only the following object types are valid for perspectives: {validObjects}."
1282
1300
  )
1283
- return
1284
1301
 
1285
1302
  object.Model.Perspectives[perspective_name]
1286
1303
 
@@ -1302,7 +1319,7 @@ class TOMWrapper:
1302
1319
  object.Parent.Name
1303
1320
  ].PerspectiveHierarchies[object.Name]
1304
1321
  return True
1305
- except:
1322
+ except Exception:
1306
1323
  return False
1307
1324
 
1308
1325
  def add_to_perspective(
@@ -1331,17 +1348,17 @@ class TOMWrapper:
1331
1348
  objectType = object.ObjectType
1332
1349
 
1333
1350
  if objectType not in validObjects:
1334
- print(
1351
+ raise ValueError(
1335
1352
  f"{icons.red_dot} Only the following object types are valid for perspectives: {validObjects}."
1336
1353
  )
1337
- return
1338
- try:
1354
+
1355
+ if any(p.Name == perspective_name for p in self.model.Perspectives):
1339
1356
  object.Model.Perspectives[perspective_name]
1340
- except:
1341
- print(f"{icons.red_dot} The '{perspective_name}' perspective does not exist.")
1342
- return
1357
+ else:
1358
+ raise ValueError(
1359
+ f"{icons.red_dot} The '{perspective_name}' perspective does not exist."
1360
+ )
1343
1361
 
1344
- # try:
1345
1362
  if objectType == TOM.ObjectType.Table:
1346
1363
  pt = TOM.PerspectiveTable()
1347
1364
  pt.Table = object
@@ -1364,8 +1381,6 @@ class TOMWrapper:
1364
1381
  object.Model.Perspectives[perspective_name].PerspectiveTables[
1365
1382
  object.Parent.Name
1366
1383
  ].PerspectiveHierarchies.Add(ph)
1367
- # except:
1368
- # pass
1369
1384
 
1370
1385
  def remove_from_perspective(
1371
1386
  self,
@@ -1393,17 +1408,15 @@ class TOMWrapper:
1393
1408
  objectType = object.ObjectType
1394
1409
 
1395
1410
  if objectType not in validObjects:
1396
- print(
1411
+ raise ValueError(
1397
1412
  f"{icons.red_dot} Only the following object types are valid for perspectives: {validObjects}."
1398
1413
  )
1399
- return
1400
- try:
1401
- object.Model.Perspectives[perspective_name]
1402
- except:
1403
- print(f"{icons.red_dot} The '{perspective_name}' perspective does not exist.")
1404
- return
1405
1414
 
1406
- # try:
1415
+ if not any(p.Name == perspective_name for p in self.model.Perspectives):
1416
+ raise ValueError(
1417
+ f"{icons.red_dot} The '{perspective_name}' perspective does not exist."
1418
+ )
1419
+
1407
1420
  if objectType == TOM.ObjectType.Table:
1408
1421
  pt = object.Model.Perspectives[perspective_name].PerspectiveTables[
1409
1422
  object.Name
@@ -1436,12 +1449,12 @@ class TOMWrapper:
1436
1449
  object.Model.Perspectives[perspective_name].PerspectiveTables[
1437
1450
  object.Parent.Name
1438
1451
  ].PerspectiveHierarchies.Remove(ph)
1439
- # except:
1440
- # pass
1441
1452
 
1442
1453
  def set_translation(
1443
1454
  self,
1444
- object: Union["TOM.Table", "TOM.Column", "TOM.Measure", "TOM.Hierarchy"],
1455
+ object: Union[
1456
+ "TOM.Table", "TOM.Column", "TOM.Measure", "TOM.Hierarchy", "TOM.Level"
1457
+ ],
1445
1458
  language: str,
1446
1459
  property: str,
1447
1460
  value: str,
@@ -1471,11 +1484,13 @@ class TOMWrapper:
1471
1484
  TOM.ObjectType.Column,
1472
1485
  TOM.ObjectType.Measure,
1473
1486
  TOM.ObjectType.Hierarchy,
1474
- ] # , 'Level'
1487
+ TOM.ObjectType.Level,
1488
+ ]
1475
1489
 
1476
1490
  if object.ObjectType not in validObjects:
1477
- print(f"{icons.red_dot} Translations can only be set to {validObjects}.")
1478
- return
1491
+ raise ValueError(
1492
+ f"{icons.red_dot} Translations can only be set to {validObjects}."
1493
+ )
1479
1494
 
1480
1495
  mapping = {
1481
1496
  "Name": TOM.TranslatedProperty.Caption,
@@ -1484,22 +1499,38 @@ class TOMWrapper:
1484
1499
  }
1485
1500
 
1486
1501
  prop = mapping.get(property)
1502
+ if prop is None:
1503
+ raise ValueError(
1504
+ f"{icons.red_dot} Invalid property value. Please choose from the following: ['Name', 'Description', Display Folder]."
1505
+ )
1487
1506
 
1488
- try:
1489
- object.Model.Cultures[language]
1490
- except:
1491
- print(
1507
+ if not any(c.Name == language for c in self.model.Cultures):
1508
+ raise ValueError(
1492
1509
  f"{icons.red_dot} The '{language}' translation language does not exist in the semantic model."
1493
1510
  )
1494
- return
1495
1511
 
1496
1512
  object.Model.Cultures[language].ObjectTranslations.SetTranslation(
1497
1513
  object, prop, value
1498
1514
  )
1499
1515
 
1516
+ if object.ObjectType in [TOM.ObjectType.Table, TOM.ObjectType.Measure]:
1517
+ print(
1518
+ f"{icons.green_dot} The {property} property for the '{object.Name}' {str(object.ObjectType).lower()} has been translated into '{language}' as '{value}'."
1519
+ )
1520
+ elif object.ObjectType in [
1521
+ TOM.ObjectType.Column,
1522
+ TOM.ObjectType.Hierarchy,
1523
+ TOM.ObjectType.Level,
1524
+ ]:
1525
+ print(
1526
+ f"{icons.green_dot} The {property} property for the '{object.Parent.Name}'[{object.Name}] {str(object.ObjectType).lower()} has been translated into '{language}' as '{value}'."
1527
+ )
1528
+
1500
1529
  def remove_translation(
1501
1530
  self,
1502
- object: Union["TOM.Table", "TOM.Column", "TOM.Measure", "TOM.Hierarchy"],
1531
+ object: Union[
1532
+ "TOM.Table", "TOM.Column", "TOM.Measure", "TOM.Hierarchy", "TOM.Level"
1533
+ ],
1503
1534
  language: str,
1504
1535
  ):
1505
1536
  """
@@ -1537,7 +1568,7 @@ class TOMWrapper:
1537
1568
  for lang in object.Model.Cultures:
1538
1569
  try:
1539
1570
  self.remove_translation(object=object, language=lang.Name)
1540
- except:
1571
+ except Exception:
1541
1572
  pass
1542
1573
  if objType in ["Table", "Column", "Measure", "Hierarchy"]:
1543
1574
  for persp in object.Model.Perspectives:
@@ -1545,7 +1576,7 @@ class TOMWrapper:
1545
1576
  self.remove_from_perspective(
1546
1577
  object=object, perspective_name=persp.Name
1547
1578
  )
1548
- except:
1579
+ except Exception:
1549
1580
  pass
1550
1581
 
1551
1582
  if objType == TOM.ObjectType.Column:
@@ -1653,12 +1684,12 @@ class TOMWrapper:
1653
1684
  objType = column.ObjectType
1654
1685
 
1655
1686
  if objType == TOM.ObjectType.Column:
1656
- for l in self.all_levels():
1687
+ for lev in self.all_levels():
1657
1688
  if (
1658
- l.Parent.Table.Name == column.Parent.Name
1659
- and l.Column.Name == column.Name
1689
+ lev.Parent.Table.Name == column.Parent.Name
1690
+ and lev.Column.Name == column.Name
1660
1691
  ):
1661
- yield l.Parent
1692
+ yield lev.Parent
1662
1693
 
1663
1694
  def used_in_sort_by(self, column: "TOM.Column"):
1664
1695
  """
@@ -1851,7 +1882,7 @@ class TOMWrapper:
1851
1882
  if m.Name in meas:
1852
1883
  yield m
1853
1884
 
1854
- def hybrid_tables(self):
1885
+ def all_hybrid_tables(self):
1855
1886
  """
1856
1887
  Outputs the `hybrid tables <https://learn.microsoft.com/power-bi/connect-data/service-dataset-modes-understand#hybrid-tables>`_ within a semantic model.
1857
1888
 
@@ -1870,7 +1901,7 @@ class TOMWrapper:
1870
1901
  if any(p.Mode == TOM.ModeType.DirectQuery for p in t.Partitions):
1871
1902
  yield t
1872
1903
 
1873
- def date_tables(self):
1904
+ def all_date_tables(self):
1874
1905
  """
1875
1906
  Outputs the tables which are marked as `date tables <https://learn.microsoft.com/power-bi/transform-model/desktop-date-tables>`_ within a semantic model.
1876
1907
 
@@ -1937,7 +1968,11 @@ class TOMWrapper:
1937
1968
  """
1938
1969
  import Microsoft.AnalysisServices.Tabular as TOM
1939
1970
 
1940
- return any(c.IsKey and c.DataType == TOM.DataType.DateTime for c in self.all_columns() if c.Parent.Name == table_name and c.Parent.DataCategory == 'Time')
1971
+ return any(
1972
+ c.IsKey and c.DataType == TOM.DataType.DateTime
1973
+ for c in self.all_columns()
1974
+ if c.Parent.Name == table_name and c.Parent.DataCategory == "Time"
1975
+ )
1941
1976
 
1942
1977
  def mark_as_date_table(self, table_name: str, column_name: str):
1943
1978
  """
@@ -1955,10 +1990,9 @@ class TOMWrapper:
1955
1990
  t = self.model.Tables[table_name]
1956
1991
  c = t.Columns[column_name]
1957
1992
  if c.DataType != TOM.DataType.DateTime:
1958
- print(
1993
+ raise ValueError(
1959
1994
  f"{icons.red_dot} The column specified in the 'column_name' parameter in this function must be of DateTime data type."
1960
1995
  )
1961
- return
1962
1996
 
1963
1997
  daxQuery = f"""
1964
1998
  define measure '{table_name}'[test] =
@@ -1978,10 +2012,9 @@ class TOMWrapper:
1978
2012
  )
1979
2013
  value = df["1"].iloc[0]
1980
2014
  if value != "1":
1981
- print(
2015
+ raise ValueError(
1982
2016
  f"{icons.red_dot} The '{column_name}' within the '{table_name}' table does not contain contiguous date values."
1983
2017
  )
1984
- return
1985
2018
 
1986
2019
  # Mark as a date table
1987
2020
  t.DataCategory = "Time"
@@ -2035,7 +2068,7 @@ class TOMWrapper:
2035
2068
  -------
2036
2069
  bool
2037
2070
  Indicates if the semantic model has a hybrid table.
2038
- """
2071
+ """
2039
2072
 
2040
2073
  return any(self.is_hybrid_table(table_name=t.Name) for t in self.model.Tables)
2041
2074
 
@@ -2176,15 +2209,19 @@ class TOMWrapper:
2176
2209
  # https://github.com/m-kovalsky/Tabular/blob/master/KPI%20Graphics.md
2177
2210
 
2178
2211
  if measure_name == target:
2179
- print(
2212
+ raise ValueError(
2180
2213
  f"{icons.red_dot} The 'target' parameter cannot be the same measure as the 'measure_name' parameter."
2181
2214
  )
2182
- return
2183
2215
 
2184
2216
  if status_graphic is None:
2185
2217
  status_graphic = "Three Circles Colored"
2186
2218
 
2187
- valid_status_types = ["Linear", "LinearReversed", "Centered", "CenteredReversed"]
2219
+ valid_status_types = [
2220
+ "Linear",
2221
+ "LinearReversed",
2222
+ "Centered",
2223
+ "CenteredReversed",
2224
+ ]
2188
2225
  status_type = status_type
2189
2226
  if status_type is None:
2190
2227
  status_type = "Linear"
@@ -2192,43 +2229,47 @@ class TOMWrapper:
2192
2229
  status_type = status_type.title().replace(" ", "")
2193
2230
 
2194
2231
  if status_type not in valid_status_types:
2195
- print(
2232
+ raise ValueError(
2196
2233
  f"{icons.red_dot} '{status_type}' is an invalid status_type. Please choose from these options: {valid_status_types}."
2197
2234
  )
2198
- return
2199
2235
 
2200
2236
  if status_type in ["Linear", "LinearReversed"]:
2201
2237
  if upper_bound is not None or lower_mid_bound is not None:
2202
- print(
2238
+ raise ValueError(
2203
2239
  f"{icons.red_dot} The 'upper_mid_bound' and 'lower_mid_bound' parameters are not used in the 'Linear' and 'LinearReversed' status types. Make sure these parameters are set to None."
2204
2240
  )
2205
- return
2241
+
2206
2242
  elif upper_bound <= lower_bound:
2207
- print(f"{icons.red_dot} The upper_bound must be greater than the lower_bound.")
2208
- return
2243
+ raise ValueError(
2244
+ f"{icons.red_dot} The upper_bound must be greater than the lower_bound."
2245
+ )
2209
2246
 
2210
2247
  if status_type in ["Centered", "CenteredReversed"]:
2211
2248
  if upper_mid_bound is None or lower_mid_bound is None:
2212
- print(
2249
+ raise ValueError(
2213
2250
  f"{icons.red_dot} The 'upper_mid_bound' and 'lower_mid_bound' parameters are necessary in the 'Centered' and 'CenteredReversed' status types."
2214
2251
  )
2215
- return
2216
2252
  elif upper_bound <= upper_mid_bound:
2217
- print(f"{icons.red_dot} The upper_bound must be greater than the upper_mid_bound.")
2253
+ raise ValueError(
2254
+ f"{icons.red_dot} The upper_bound must be greater than the upper_mid_bound."
2255
+ )
2218
2256
  elif upper_mid_bound <= lower_mid_bound:
2219
- print(f"{icons.red_dot} The upper_mid_bound must be greater than the lower_mid_bound.")
2257
+ raise ValueError(
2258
+ f"{icons.red_dot} The upper_mid_bound must be greater than the lower_mid_bound."
2259
+ )
2220
2260
  elif lower_mid_bound <= lower_bound:
2221
- print(f"{icons.red_dot} The lower_mid_bound must be greater than the lower_bound.")
2261
+ raise ValueError(
2262
+ f"{icons.red_dot} The lower_mid_bound must be greater than the lower_bound."
2263
+ )
2222
2264
 
2223
2265
  try:
2224
2266
  table_name = next(
2225
2267
  m.Parent.Name for m in self.all_measures() if m.Name == measure_name
2226
2268
  )
2227
- except:
2228
- print(
2269
+ except Exception:
2270
+ raise ValueError(
2229
2271
  f"{icons.red_dot} The '{measure_name}' measure does not exist in the '{self._dataset}' semantic model within the '{self._workspace}'."
2230
2272
  )
2231
- return
2232
2273
 
2233
2274
  graphics = [
2234
2275
  "Cylinder",
@@ -2251,10 +2292,9 @@ class TOMWrapper:
2251
2292
  ]
2252
2293
 
2253
2294
  if status_graphic not in graphics:
2254
- print(
2295
+ raise ValueError(
2255
2296
  f"{icons.red_dot} The '{status_graphic}' status graphic is not valid. Please choose from these options: {graphics}."
2256
2297
  )
2257
- return
2258
2298
 
2259
2299
  measure_target = True
2260
2300
 
@@ -2262,15 +2302,15 @@ class TOMWrapper:
2262
2302
  float(target)
2263
2303
  tgt = str(target)
2264
2304
  measure_target = False
2265
- except:
2305
+ except Exception:
2266
2306
  try:
2267
2307
  tgt = next(
2268
2308
  format_dax_object_name(m.Parent.Name, m.Name)
2269
2309
  for m in self.all_measures()
2270
2310
  if m.Name == target
2271
2311
  )
2272
- except:
2273
- print(
2312
+ except Exception:
2313
+ raise ValueError(
2274
2314
  f"{icons.red_dot} The '{target}' measure does not exist in the '{self._dataset}' semantic model within the '{self._workspace}'."
2275
2315
  )
2276
2316
 
@@ -2294,11 +2334,11 @@ class TOMWrapper:
2294
2334
  kpi.StatusExpression = expr
2295
2335
 
2296
2336
  ms = self.model.Tables[table_name].Measures[measure_name]
2297
- try:
2337
+ if ms.KPI is not None:
2298
2338
  ms.KPI.TargetExpression = tgt
2299
2339
  ms.KPI.StatusGraphic = status_graphic
2300
2340
  ms.KPI.StatusExpression = expr
2301
- except:
2341
+ else:
2302
2342
  ms.KPI = kpi
2303
2343
 
2304
2344
  def set_aggregations(self, table_name: str, agg_table_name: str):
@@ -2317,6 +2357,8 @@ class TOMWrapper:
2317
2357
 
2318
2358
  """
2319
2359
 
2360
+ import Microsoft.AnalysisServices.Tabular as TOM
2361
+
2320
2362
  for c in self.model.Tables[agg_table_name].Columns:
2321
2363
 
2322
2364
  dataType = c.DataType
@@ -2354,7 +2396,7 @@ class TOMWrapper:
2354
2396
  The IsAvailableInMdx property value.
2355
2397
  """
2356
2398
 
2357
- self.model.Tables[table_name].Columns[column_name].IsAvailableInMdx = value
2399
+ self.model.Tables[table_name].Columns[column_name].IsAvailableInMDX = value
2358
2400
 
2359
2401
  def set_summarize_by(
2360
2402
  self, table_name: str, column_name: str, value: Optional[str] = None
@@ -2373,6 +2415,7 @@ class TOMWrapper:
2373
2415
  Defaults to none which resolves to 'Default'.
2374
2416
  `Aggregate valid values <https://learn.microsoft.com/dotnet/api/microsoft.analysisservices.tabular.aggregatefunction?view=analysisservices-dotnet>`_
2375
2417
  """
2418
+ import Microsoft.AnalysisServices.Tabular as TOM
2376
2419
  import System
2377
2420
 
2378
2421
  values = [
@@ -2396,10 +2439,9 @@ class TOMWrapper:
2396
2439
  )
2397
2440
 
2398
2441
  if value not in values:
2399
- print(
2442
+ raise ValueError(
2400
2443
  f"{icons.red_dot} '{value}' is not a valid value for the SummarizeBy property. These are the valid values: {values}."
2401
2444
  )
2402
- return
2403
2445
 
2404
2446
  self.model.Tables[table_name].Columns[column_name].SummarizeBy = (
2405
2447
  System.Enum.Parse(TOM.AggregateFunction, value)
@@ -2415,6 +2457,7 @@ class TOMWrapper:
2415
2457
  The DirectLakeBehavior property value.
2416
2458
  `DirectLakeBehavior valid values <https://learn.microsoft.com/dotnet/api/microsoft.analysisservices.tabular.directlakebehavior?view=analysisservices-dotnet>`_
2417
2459
  """
2460
+ import Microsoft.AnalysisServices.Tabular as TOM
2418
2461
  import System
2419
2462
 
2420
2463
  direct_lake_behavior = direct_lake_behavior.capitalize()
@@ -2428,10 +2471,9 @@ class TOMWrapper:
2428
2471
  dlValues = ["Automatic", "DirectLakeOnly", "DirectQueryOnly"]
2429
2472
 
2430
2473
  if direct_lake_behavior not in dlValues:
2431
- print(
2474
+ raise ValueError(
2432
2475
  f"{icons.red_dot} The 'direct_lake_behavior' parameter must be one of these values: {dlValues}."
2433
2476
  )
2434
- return
2435
2477
 
2436
2478
  self.model.DirectLakeBehavior = System.Enum.Parse(
2437
2479
  TOM.DirectLakeBehavior, direct_lake_behavior
@@ -2529,13 +2571,14 @@ class TOMWrapper:
2529
2571
  import Microsoft.AnalysisServices.Tabular as TOM
2530
2572
 
2531
2573
  if isinstance(objects, str):
2532
- print(f"{icons.red_dot} The 'objects' parameter must be a list of columns/measures.")
2533
- return
2574
+ raise ValueError(
2575
+ f"{icons.red_dot} The 'objects' parameter must be a list of columns/measures."
2576
+ )
2577
+
2534
2578
  if len(objects) == 1:
2535
- print(
2579
+ raise ValueError(
2536
2580
  f"{icons.red_dot} There must be more than one object (column/measure) within the objects parameter."
2537
2581
  )
2538
- return
2539
2582
 
2540
2583
  expr = ""
2541
2584
  i = 0
@@ -2571,10 +2614,9 @@ class TOMWrapper:
2571
2614
  )
2572
2615
  success = True
2573
2616
  if not success:
2574
- print(
2617
+ raise ValueError(
2575
2618
  f"{icons.red_dot} The '{obj}' object was not found in the '{self._dataset}' semantic model."
2576
2619
  )
2577
- return
2578
2620
  else:
2579
2621
  i += 1
2580
2622
 
@@ -2742,7 +2784,7 @@ class TOMWrapper:
2742
2784
  try:
2743
2785
  runId = self.get_annotation_value(object=self.model, name="Vertipaq_Run")
2744
2786
  runId = str(int(runId) + 1)
2745
- except:
2787
+ except Exception:
2746
2788
  runId = "1"
2747
2789
  self.set_annotation(object=self.model, name="Vertipaq_Run", value=runId)
2748
2790
 
@@ -2771,7 +2813,7 @@ class TOMWrapper:
2771
2813
  object=object, name="Vertipaq_RecordCount"
2772
2814
  )
2773
2815
 
2774
- return int(result)
2816
+ return int(result) if result is not None else 0
2775
2817
 
2776
2818
  def records_per_segment(self, object: "TOM.Partition"):
2777
2819
  """
@@ -2796,7 +2838,7 @@ class TOMWrapper:
2796
2838
  object=object, name="Vertipaq_RecordsPerSegment"
2797
2839
  )
2798
2840
 
2799
- return float(result)
2841
+ return float(result) if result is not None else 0
2800
2842
 
2801
2843
  def used_size(self, object: Union["TOM.Hierarchy", "TOM.Relationship"]):
2802
2844
  """
@@ -2821,7 +2863,7 @@ class TOMWrapper:
2821
2863
  elif objType == TOM.ObjectType.Relationship:
2822
2864
  result = self.get_annotation_value(object=object, name="Vertipaq_UsedSize")
2823
2865
 
2824
- return int(result)
2866
+ return int(result) if result is not None else 0
2825
2867
 
2826
2868
  def data_size(self, column: "TOM.Column"):
2827
2869
  """
@@ -2844,7 +2886,7 @@ class TOMWrapper:
2844
2886
  if objType == TOM.ObjectType.Column:
2845
2887
  result = self.get_annotation_value(object=column, name="Vertipaq_DataSize")
2846
2888
 
2847
- return int(result)
2889
+ return int(result) if result is not None else 0
2848
2890
 
2849
2891
  def dictionary_size(self, column: "TOM.Column"):
2850
2892
  """
@@ -2869,7 +2911,7 @@ class TOMWrapper:
2869
2911
  object=column, name="Vertipaq_DictionarySize"
2870
2912
  )
2871
2913
 
2872
- return int(result)
2914
+ return int(result) if result is not None else 0
2873
2915
 
2874
2916
  def total_size(self, object: Union["TOM.Table", "TOM.Column"]):
2875
2917
  """
@@ -2894,7 +2936,7 @@ class TOMWrapper:
2894
2936
  elif objType == TOM.ObjectType.Table:
2895
2937
  result = self.get_annotation_value(object=object, name="Vertipaq_TotalSize")
2896
2938
 
2897
- return int(result)
2939
+ return int(result) if result is not None else 0
2898
2940
 
2899
2941
  def cardinality(self, column: "TOM.Column"):
2900
2942
  """
@@ -2919,7 +2961,7 @@ class TOMWrapper:
2919
2961
  object=column, name="Vertipaq_Cardinality"
2920
2962
  )
2921
2963
 
2922
- return int(result)
2964
+ return int(result) if result is not None else 0
2923
2965
 
2924
2966
  def depends_on(self, object, dependencies: pd.DataFrame):
2925
2967
  """
@@ -2947,7 +2989,7 @@ class TOMWrapper:
2947
2989
  objParentName = objName
2948
2990
 
2949
2991
  fil = dependencies[
2950
- (dependencies["Object Type"] == objType)
2992
+ (dependencies["Object Type"] == str(objType))
2951
2993
  & (dependencies["Table Name"] == objParentName)
2952
2994
  & (dependencies["Object Name"] == objName)
2953
2995
  ]
@@ -3004,7 +3046,7 @@ class TOMWrapper:
3004
3046
  objParentName = objName
3005
3047
 
3006
3048
  fil = dependencies[
3007
- (dependencies["Referenced Object Type"] == objType)
3049
+ (dependencies["Referenced Object Type"] == str(objType))
3008
3050
  & (dependencies["Referenced Table"] == objParentName)
3009
3051
  & (dependencies["Referenced Object"] == objName)
3010
3052
  ]
@@ -3051,7 +3093,7 @@ class TOMWrapper:
3051
3093
 
3052
3094
  for obj in self.depends_on(object=object, dependencies=dependencies):
3053
3095
  if obj.ObjectType == TOM.ObjectType.Measure:
3054
- if (obj.Parent.Name + obj.Name in object.Expression) or (
3096
+ if (f"{obj.Parent.Name}[{obj.Name}]" in object.Expression) or (
3055
3097
  format_dax_object_name(obj.Parent.Name, obj.Name)
3056
3098
  in object.Expression
3057
3099
  ):
@@ -3075,15 +3117,22 @@ class TOMWrapper:
3075
3117
  """
3076
3118
  import Microsoft.AnalysisServices.Tabular as TOM
3077
3119
 
3078
- def create_pattern(a, b):
3079
- return r"(?<!" + re.escape(a) + r"\[)(?<!" + re.escape(a) + r"'\[)" + re.escape(b)
3120
+ def create_pattern(tableList, b):
3121
+ patterns = [
3122
+ r"(?<!" + re.escape(table) + r"\[)(?<!" + re.escape(table) + r"'\[)"
3123
+ for table in tableList
3124
+ ]
3125
+ combined_pattern = "".join(patterns) + re.escape(b)
3126
+ return combined_pattern
3080
3127
 
3081
3128
  for obj in self.depends_on(object=object, dependencies=dependencies):
3082
3129
  if obj.ObjectType == TOM.ObjectType.Column:
3130
+ tableList = []
3131
+ for c in self.all_columns():
3132
+ if c.Name == obj.Name:
3133
+ tableList.append(c.Parent.Name)
3083
3134
  if (
3084
- re.search(
3085
- create_pattern(obj.Parent.Name, obj.Name), object.Expression
3086
- )
3135
+ re.search(create_pattern(tableList, obj.Name), object.Expression)
3087
3136
  is not None
3088
3137
  ):
3089
3138
  yield obj
@@ -3244,26 +3293,24 @@ class TOMWrapper:
3244
3293
  rolling_window_granularity = rolling_window_granularity.capitalize()
3245
3294
 
3246
3295
  if incremental_granularity not in incGran:
3247
- print(
3296
+ raise ValueError(
3248
3297
  f"{icons.red_dot} Invalid 'incremental_granularity' value. Please choose from the following options: {incGran}."
3249
3298
  )
3250
- return
3299
+
3251
3300
  if rolling_window_granularity not in incGran:
3252
- print(
3301
+ raise ValueError(
3253
3302
  f"{icons.red_dot} Invalid 'rolling_window_granularity' value. Please choose from the following options: {incGran}."
3254
3303
  )
3255
- return
3256
3304
 
3257
3305
  if rolling_window_periods < 1:
3258
- print(
3306
+ raise ValueError(
3259
3307
  f"{icons.red_dot} Invalid 'rolling_window_periods' value. Must be a value greater than 0."
3260
3308
  )
3261
- return
3309
+
3262
3310
  if incremental_periods < 1:
3263
- print(
3311
+ raise ValueError(
3264
3312
  f"{icons.red_dot} Invalid 'incremental_periods' value. Must be a value greater than 0."
3265
3313
  )
3266
- return
3267
3314
 
3268
3315
  t = self.model.Tables[table_name]
3269
3316
 
@@ -3271,10 +3318,9 @@ class TOMWrapper:
3271
3318
  dc = t.Columns[detect_data_changes_column]
3272
3319
 
3273
3320
  if dc.DataType != TOM.DataType.DateTime:
3274
- print(
3321
+ raise ValueError(
3275
3322
  f"{icons.red_dot} Invalid 'detect_data_changes_column' parameter. This column must be of DateTime data type."
3276
3323
  )
3277
- return
3278
3324
 
3279
3325
  rp = TOM.BasicRefreshPolicy()
3280
3326
  rp.IncrementalPeriods = incremental_periods
@@ -3354,26 +3400,24 @@ class TOMWrapper:
3354
3400
  rolling_window_granularity = rolling_window_granularity.capitalize()
3355
3401
 
3356
3402
  if incremental_granularity not in incGran:
3357
- print(
3403
+ raise ValueError(
3358
3404
  f"{icons.red_dot} Invalid 'incremental_granularity' value. Please choose from the following options: {incGran}."
3359
3405
  )
3360
- return
3406
+
3361
3407
  if rolling_window_granularity not in incGran:
3362
- print(
3408
+ raise ValueError(
3363
3409
  f"{icons.red_dot} Invalid 'rolling_window_granularity' value. Please choose from the following options: {incGran}."
3364
3410
  )
3365
- return
3366
3411
 
3367
3412
  if rolling_window_periods < 1:
3368
- print(
3413
+ raise ValueError(
3369
3414
  f"{icons.red_dot} Invalid 'rolling_window_periods' value. Must be a value greater than 0."
3370
3415
  )
3371
- return
3416
+
3372
3417
  if incremental_periods < 1:
3373
- print(
3418
+ raise ValueError(
3374
3419
  f"{icons.red_dot} Invalid 'incremental_periods' value. Must be a value greater than 0."
3375
3420
  )
3376
- return
3377
3421
 
3378
3422
  date_format = "%m/%d/%Y"
3379
3423
 
@@ -3388,10 +3432,9 @@ class TOMWrapper:
3388
3432
  end_day = date_obj_end.day
3389
3433
 
3390
3434
  if date_obj_end <= date_obj_start:
3391
- print(
3435
+ raise ValueError(
3392
3436
  f"{icons.red_dot} Invalid 'start_date' or 'end_date'. The 'end_date' must be after the 'start_date'."
3393
3437
  )
3394
- return
3395
3438
 
3396
3439
  t = self.model.Tables[table_name]
3397
3440
 
@@ -3400,20 +3443,18 @@ class TOMWrapper:
3400
3443
  dType = c.DataType
3401
3444
 
3402
3445
  if dType != TOM.DataType.DateTime:
3403
- print(
3446
+ raise ValueError(
3404
3447
  f"{icons.red_dot} The {fcName} column is of '{dType}' data type. The column chosen must be of DateTime data type."
3405
3448
  )
3406
- return
3407
3449
 
3408
3450
  if detect_data_changes_column is not None:
3409
3451
  dc = t.Columns[detect_data_changes_column]
3410
3452
  dcType = dc.DataType
3411
3453
 
3412
3454
  if dcType != TOM.DataType.DateTime:
3413
- print(
3455
+ raise ValueError(
3414
3456
  f"{icons.red_dot} Invalid 'detect_data_changes_column' parameter. This column must be of DateTime data type."
3415
3457
  )
3416
- return
3417
3458
 
3418
3459
  # Start changes:
3419
3460
 
@@ -3421,10 +3462,10 @@ class TOMWrapper:
3421
3462
  i = 0
3422
3463
  for p in t.Partitions:
3423
3464
  if p.SourceType != TOM.PartitionSourceType.M:
3424
- print(
3465
+ raise ValueError(
3425
3466
  f"{icons.red_dot} Invalid partition source type. Incremental refresh can only be set up if the table's partition is an M-partition."
3426
3467
  )
3427
- return
3468
+
3428
3469
  elif i == 0:
3429
3470
  text = p.Expression
3430
3471
  text = text.rstrip()
@@ -3440,8 +3481,7 @@ class TOMWrapper:
3440
3481
 
3441
3482
  print(text_before_last_match)
3442
3483
  else:
3443
- print(f"{icons.red_dot} Invalid M-partition expression.")
3444
- return
3484
+ raise ValueError(f"{icons.red_dot} Invalid M-partition expression.")
3445
3485
 
3446
3486
  endExpr = f'#"Filtered Rows IR" = Table.SelectRows({obj}, each [{column_name}] >= RangeStart and [{column_name}] <= RangeEnd)\n#"Filtered Rows IR"'
3447
3487
  finalExpr = text_before_last_match + endExpr
@@ -3537,15 +3577,13 @@ class TOMWrapper:
3537
3577
  ht = self.is_hybrid_table(table_name=table_name)
3538
3578
 
3539
3579
  if not ht:
3540
- print(
3580
+ raise ValueError(
3541
3581
  f"{icons.red_dot} The `data coverage definition <https://learn.microsoft.com/analysis-services/tom/table-partitions?view=asallproducts-allversions>`_ property is only applicable to `hybrid tables <https://learn.microsoft.com/power-bi/connect-data/service-dataset-modes-understand#hybrid-tables>`_. See the documentation: {doc}."
3542
3582
  )
3543
- return
3544
3583
  if p.Mode != TOM.ModeType.DirectQuery:
3545
- print(
3584
+ raise ValueError(
3546
3585
  f"{icons.red_dot} The `data coverage definition <https://learn.microsoft.com/analysis-services/tom/table-partitions?view=asallproducts-allversions>`_ property is only applicable to the DirectQuery partition of a `hybrid table <https://learn.microsoft.com/power-bi/connect-data/service-dataset-modes-understand#hybrid-tables>`_. See the documentation: {doc}."
3547
3586
  )
3548
- return
3549
3587
 
3550
3588
  dcd = TOM.DataCoverageDefinition()
3551
3589
  dcd.Expression = expression
@@ -3572,10 +3610,9 @@ class TOMWrapper:
3572
3610
  value = value.capitalize()
3573
3611
 
3574
3612
  if value not in values:
3575
- print(
3613
+ raise ValueError(
3576
3614
  f"{icons.red_dot} Invalid encoding hint value. Please choose from these options: {values}."
3577
3615
  )
3578
- return
3579
3616
 
3580
3617
  self.model.Tables[table_name].Columns[column_name].EncodingHint = (
3581
3618
  System.Enum.Parse(TOM.EncodingHintType, value)
@@ -3617,10 +3654,9 @@ class TOMWrapper:
3617
3654
  value = "Boolean"
3618
3655
 
3619
3656
  if value not in values:
3620
- print(
3657
+ raise ValueError(
3621
3658
  f"{icons.red_dot} Invalid data type. Please choose from these options: {values}."
3622
3659
  )
3623
- return
3624
3660
 
3625
3661
  self.model.Tables[table_name].Columns[column_name].DataType = System.Enum.Parse(
3626
3662
  TOM.DataType, value
@@ -3652,44 +3688,369 @@ class TOMWrapper:
3652
3688
  for t in time_intel:
3653
3689
  t = t.capitalize()
3654
3690
  if t not in [time_intel_options]:
3655
- print(
3691
+ raise ValueError(
3656
3692
  f"{icons.red_dot} The '{t}' time intelligence variation is not supported. Valid options: {time_intel_options}."
3657
3693
  )
3658
- return
3659
3694
 
3660
3695
  # Validate measure and extract table name
3661
- for m in self.all_measures():
3662
- if m.Name == measure_name:
3663
- table_name = m.Parent.Name
3696
+ matching_measures = [
3697
+ m.Parent.Name for m in self.all_measures() if m.Name == measure_name
3698
+ ]
3664
3699
 
3665
3700
  if table_name is None:
3666
- print(
3701
+ raise ValueError(
3667
3702
  f"{icons.red_dot} The '{measure_name}' is not a valid measure in the '{self._dataset}' semantic model within the '{self._workspace}' workspace."
3668
3703
  )
3669
- return
3670
3704
 
3705
+ table_name = matching_measures[0]
3671
3706
  # Validate date table
3672
3707
  if not self.is_date_table(date_table):
3673
- print(
3708
+ raise ValueError(
3674
3709
  f"{icons.red_dot} The '{date_table}' table is not a valid date table in the '{self._dataset}' wemantic model within the '{self._workspace}' workspace."
3675
3710
  )
3676
- return
3677
3711
 
3678
3712
  # Extract date key from date table
3679
- for c in self.all_columns():
3680
- if c.Parent.Name == date_table and c.IsKey:
3681
- date_key = c.Name
3713
+ matching_columns = [
3714
+ c.Name
3715
+ for c in self.all_columns()
3716
+ if c.Parent.Name == date_table and c.IsKey
3717
+ ]
3718
+
3719
+ if not matching_columns:
3720
+ raise ValueError(
3721
+ f"{icons.red_dot} The '{date_table}' table does not have a date key column in the '{self._dataset}' semantic model within the '{self._workspace}' workspace."
3722
+ )
3723
+
3724
+ date_key = matching_columns[0]
3682
3725
 
3683
3726
  # Create the new time intelligence measures
3684
3727
  for t in time_intel:
3685
- if t == "MTD":
3686
- expr = f"CALCULATE([{measure_name}],DATES{time_intel}('{date_table}'[{date_key}]))"
3687
- new_meas_name = f"{measure_name} {t}"
3688
- self.add_measure(
3689
- table_name=table_name,
3690
- measure_name=new_meas_name,
3691
- expression=expr,
3692
- )
3728
+ expr = f"CALCULATE([{measure_name}],DATES{t}('{date_table}'[{date_key}]))"
3729
+ new_meas_name = f"{measure_name} {t}"
3730
+ self.add_measure(
3731
+ table_name=table_name,
3732
+ measure_name=new_meas_name,
3733
+ expression=expr,
3734
+ )
3735
+
3736
+ def update_m_partition(
3737
+ self,
3738
+ table_name: str,
3739
+ partition_name: str,
3740
+ expression: Optional[str | None] = None,
3741
+ mode: Optional[str | None] = None,
3742
+ description: Optional[str | None] = None,
3743
+ ):
3744
+ """
3745
+ Updates an M partition for a table within a semantic model.
3746
+
3747
+ Parameters
3748
+ ----------
3749
+ table_name : str
3750
+ Name of the table.
3751
+ partition_name : str
3752
+ Name of the partition.
3753
+ expression : str, default=None
3754
+ The `M expression <https://learn.microsoft.com/dotnet/api/microsoft.analysisservices.tabular.mpartitionsource.expression?view=analysisservices-dotnet>`_ containing the logic for the partition.
3755
+ Defaults to None which keeps the existing setting.
3756
+ mode : str, default=None
3757
+ The query `mode <https://learn.microsoft.com/dotnet/api/microsoft.analysisservices.tabular.modetype?view=analysisservices-dotnet>`_ of the partition.
3758
+ Defaults to None which keeps the existing setting.
3759
+ description : str, default=None
3760
+ The description of the partition.
3761
+ Defaults to None which keeps the existing setting.
3762
+ """
3763
+
3764
+ import Microsoft.AnalysisServices.Tabular as TOM
3765
+ import System
3766
+
3767
+ p = self.model.Tables[table_name].Partitions[partition_name]
3768
+ if p.SourceType != TOM.PartitionSourceType.M:
3769
+ raise ValueError(
3770
+ f"{icons.red_dot} Invalid partition source type. This function is only for M partitions."
3771
+ )
3772
+ if expression is not None:
3773
+ p.Source.Expression = expression
3774
+ if mode is not None:
3775
+ p.Mode = System.Enum.Parse(TOM.ModeType, mode)
3776
+ if description is not None:
3777
+ p.Description = description
3778
+
3779
+ def update_measure(
3780
+ self,
3781
+ measure_name: str,
3782
+ expression: Optional[str | None] = None,
3783
+ format_string: Optional[str | None] = None,
3784
+ hidden: Optional[bool | None] = None,
3785
+ description: Optional[str | None] = None,
3786
+ display_folder: Optional[str | None] = None,
3787
+ ):
3788
+ """
3789
+ Updates a measure within a semantic model.
3790
+
3791
+ Parameters
3792
+ ----------
3793
+ measure_name : str
3794
+ Name of the measure.
3795
+ expression : str, default=None
3796
+ DAX expression of the measure.
3797
+ Defaults to None which keeps the existing setting.
3798
+ format_string : str, default=None
3799
+ Format string of the measure.
3800
+ Defaults to None which keeps the existing setting.
3801
+ hidden : bool, default=None
3802
+ Whether the measure will be hidden or visible.
3803
+ Defaults to None which keeps the existing setting.
3804
+ description : str, default=None
3805
+ A description of the measure.
3806
+ Defaults to None which keeps the existing setting.
3807
+ display_folder : str, default=None
3808
+ The display folder in which the measure will reside.
3809
+ Defaults to None which keeps the existing setting.
3810
+ """
3811
+
3812
+ table_name = next(
3813
+ m.Parent.Name for m in self.all_measures() if m.Name == measure_name
3814
+ )
3815
+ m = self.model.Tables[table_name].Measures[measure_name]
3816
+ if expression is not None:
3817
+ m.Expression = expression
3818
+ if format_string is not None:
3819
+ m.FormatString = format_string
3820
+ if hidden is not None:
3821
+ m.IsHidden = hidden
3822
+ if description is not None:
3823
+ m.Description = description
3824
+ if display_folder is not None:
3825
+ m.DisplayFolder = display_folder
3826
+
3827
+ def update_column(
3828
+ self,
3829
+ table_name: str,
3830
+ column_name: str,
3831
+ source_column: Optional[str | None] = None,
3832
+ data_type: Optional[str | None] = None,
3833
+ expression: Optional[str | None] = None,
3834
+ format_string: Optional[str | None] = None,
3835
+ hidden: Optional[bool | None] = None,
3836
+ description: Optional[str | None] = None,
3837
+ display_folder: Optional[str | None] = None,
3838
+ data_category: Optional[str | None] = None,
3839
+ key: Optional[bool | None] = None,
3840
+ summarize_by: Optional[str | None] = None,
3841
+ ):
3842
+ """
3843
+ Updates a column within a semantic model.
3844
+
3845
+ Parameters
3846
+ ----------
3847
+ table_name : str
3848
+ Name of the table in which the column exists.
3849
+ column_name : str
3850
+ Name of the column.
3851
+ source_column : str, default=None
3852
+ The source column for the column (for data columns only).
3853
+ Defaults to None which keeps the existing setting.
3854
+ data_type : str, default=None
3855
+ The data type of the column.
3856
+ Defaults to None which keeps the existing setting.
3857
+ expression : str, default=None
3858
+ The DAX expression of the column (for calculated columns only).
3859
+ Defaults to None which keeps the existing setting.
3860
+ format_string : str, default=None
3861
+ Format string of the column.
3862
+ Defaults to None which keeps the existing setting.
3863
+ hidden : bool, default=None
3864
+ Whether the column will be hidden or visible.
3865
+ Defaults to None which keeps the existing setting.
3866
+ description : str, default=None
3867
+ A description of the column.
3868
+ Defaults to None which keeps the existing setting.
3869
+ display_folder : str, default=None
3870
+ The display folder in which the column will reside.
3871
+ Defaults to None which keeps the existing setting.
3872
+ data_category : str, default=None
3873
+ The data category of the column.
3874
+ Defaults to None which keeps the existing setting.
3875
+ key : bool, default=False
3876
+ Marks the column as the primary key of the table.
3877
+ Defaults to None which keeps the existing setting.
3878
+ summarize_by : str, default=None
3879
+ Sets the value for the Summarize By property of the column.
3880
+ Defaults to None which keeps the existing setting.
3881
+ """
3882
+
3883
+ import Microsoft.AnalysisServices.Tabular as TOM
3884
+ import System
3885
+
3886
+ c = self.model.Tables[table_name].Measures[column_name]
3887
+ if c.Type == TOM.ColumnType.Data:
3888
+ if source_column is not None:
3889
+ c.SourceColumn = source_column
3890
+ if c.Type == TOM.ColumnType.Calculated:
3891
+ if expression is not None:
3892
+ c.Expression = expression
3893
+ if data_type is not None:
3894
+ c.DataType = System.Enum.Parse(TOM.DataType, data_type)
3895
+ if format_string is not None:
3896
+ c.FormatString = format_string
3897
+ if hidden is not None:
3898
+ c.IsHidden = hidden
3899
+ if description is not None:
3900
+ c.Description = description
3901
+ if display_folder is not None:
3902
+ c.DisplayFolder = display_folder
3903
+ if key is not None:
3904
+ c.IsKey = key
3905
+ if data_category is not None:
3906
+ c.DataCategory = data_category
3907
+ if summarize_by is not None:
3908
+ c.SummarizeBy = System.Enum.Parse(TOM.AggregateFunction, summarize_by)
3909
+
3910
+ def update_role(
3911
+ self,
3912
+ role_name: str,
3913
+ model_permission: Optional[str | None] = None,
3914
+ description: Optional[str | None] = None,
3915
+ ):
3916
+ """
3917
+ Updates a role within a semantic model.
3918
+
3919
+ Parameters
3920
+ ----------
3921
+ role_name : str
3922
+ Name of the role.
3923
+ model_permission : str, default=None
3924
+ The model permission for the role.
3925
+ Defaults to None which keeps the existing setting.
3926
+ description : str, default=None
3927
+ The description of the role.
3928
+ Defaults to None which keeps the existing setting.
3929
+ """
3930
+
3931
+ import Microsoft.AnalysisServices.Tabular as TOM
3932
+ import System
3933
+
3934
+ obj = self.model.Roles[role_name]
3935
+
3936
+ if model_permission is not None:
3937
+ obj.ModelPermission = System.Enum.Parse(
3938
+ TOM.ModelPermission, model_permission
3939
+ )
3940
+ if description is not None:
3941
+ obj.Description = description
3942
+
3943
+ def update_calculation_item(
3944
+ self,
3945
+ table_name: str,
3946
+ calculation_item_name: str,
3947
+ expression: Optional[str | None] = None,
3948
+ ordinal: Optional[int | None] = None,
3949
+ format_string_expression: Optional[str | None] = None,
3950
+ description: Optional[str | None] = None,
3951
+ ):
3952
+ """
3953
+ Updates a calculation item within a semantic model.
3954
+
3955
+ Parameters
3956
+ ----------
3957
+ table_name : str
3958
+ Name of the calculation group (table).
3959
+ calculation_item_name : str
3960
+ Name of the calculation item.
3961
+ expression : str, default=None
3962
+ The DAX expression of the calculation item.
3963
+ Defaults to None which keeps the existing setting.
3964
+ ordinal : int, default=None
3965
+ The ordinal of the calculation item.
3966
+ Defaults to None which keeps the existing setting.
3967
+ format_string_expression : str, default=None
3968
+ The format string expression for the calculation item.
3969
+ Defaults to None which keeps the existing setting.
3970
+ description : str, default=None
3971
+ The description of the role.
3972
+ Defaults to None which keeps the existing setting.
3973
+ """
3974
+
3975
+ obj = self.Tables[table_name].CalculationGroup.CalculationItems[
3976
+ calculation_item_name
3977
+ ]
3978
+
3979
+ if expression is not None:
3980
+ obj.Expression = expression
3981
+ if format_string_expression is not None:
3982
+ obj.FormatStringDefinition.Expression = format_string_expression
3983
+ if ordinal is not None:
3984
+ obj.Ordinal = ordinal
3985
+ if description is not None:
3986
+ obj.Description = description
3987
+
3988
+ def set_sort_by_column(
3989
+ self, table_name: str, column_name: str, sort_by_column: str
3990
+ ):
3991
+ """
3992
+ Sets the sort by column for a column in a semantic model.
3993
+
3994
+ Parameters
3995
+ ----------
3996
+ table_name : str
3997
+ Name of the table.
3998
+ column_name : str
3999
+ Name of the column.
4000
+ sort_by_column : str
4001
+ Name of the column to use for sorting. Must be of integer (Int64) data type.
4002
+ """
4003
+
4004
+ import Microsoft.AnalysisServices.Tabular as TOM
4005
+
4006
+ sbc = self.model.Tables[table_name].Columns[sort_by_column]
4007
+
4008
+ if sbc.DataType != TOM.DataType.Int64:
4009
+ raise ValueError(
4010
+ f"{icons.red_dot} Invalid sort by column data type. The sort by column must be of 'Int64' data type."
4011
+ )
4012
+
4013
+ self.model.Tables[table_name].Columns[column_name].SortByColumn = sbc
4014
+
4015
+ def remove_sort_by_column(self, table_name: str, column_name: str):
4016
+ """
4017
+ Removes the sort by column for a column in a semantic model.
4018
+
4019
+ Parameters
4020
+ ----------
4021
+ table_name : str
4022
+ Name of the table.
4023
+ column_name : str
4024
+ Name of the column.
4025
+ """
4026
+
4027
+ self.model.Tables[table_name].Columns[column_name].SortByColumn = None
4028
+
4029
+ def is_calculated_table(self, table_name: str):
4030
+ """
4031
+ Identifies if a table is a calculated table.
4032
+
4033
+ Parameters
4034
+ ----------
4035
+ table_name : str
4036
+ Name of the table.
4037
+
4038
+ Returns
4039
+ -------
4040
+ bool
4041
+ A boolean value indicating whether the table is a calculated table.
4042
+ """
4043
+
4044
+ import Microsoft.AnalysisServices.Tabular as TOM
4045
+
4046
+ isCalcTable = False
4047
+ t = self.model.Tables[table_name]
4048
+ if t.ObjectType == TOM.ObjectType.Table:
4049
+ if any(
4050
+ p.SourceType == TOM.PartitionSourceType.Calculated for p in t.Partitions
4051
+ ):
4052
+ isCalcTable = True
4053
+ return isCalcTable
3693
4054
 
3694
4055
  def close(self):
3695
4056
  if not self._readonly and self.model is not None: