From f4fac32c8020401516108ecd4db9b7e7d210f5cb Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 30 May 2025 10:49:10 +0300 Subject: [PATCH 1/7] connec_report api --- src/sempy_labs/report/_reportwrapper.py | 856 ++++++++++++++++++++++++ 1 file changed, 856 insertions(+) diff --git a/src/sempy_labs/report/_reportwrapper.py b/src/sempy_labs/report/_reportwrapper.py index eed973ec..d4012146 100644 --- a/src/sempy_labs/report/_reportwrapper.py +++ b/src/sempy_labs/report/_reportwrapper.py @@ -164,6 +164,548 @@ def _ensure_pbir(self): f"{icons.red_dot} This ReportWrapper function requires the report to be in the PBIR format." "See here for details: https://powerbi.microsoft.com/blog/power-bi-enhanced-report-format-pbir-in-power-bi-desktop-developer-mode-preview/" ) + + class Report: + def __init__(self, wrapper): + self.ObjectType = "Report" + self.Name = wrapper._report_name + self._wrapper = wrapper + self._pages = None + self._custom_visuals = None + self._filters = None + self._wrapper._ensure_pbir() + + @property + def Pages(self): + if self._pages is None: + self._pages = ReportWrapper.PageCollection(self._wrapper) + return self._pages + + @property + def CustomVisuals(self): + if self._custom_visuals is None: + self._custom_visuals = ReportWrapper.CustomVisualCollection( + self._wrapper + ) + return self._custom_visuals + + @property + def Filters(self): + if self._filters is None: + self._filters = ReportFilterCollection(self._wrapper) + return self._filters + + class Page: + def __init__(self, file_path, page_data, wrapper): + self._wrapper = wrapper + self._visuals = None + self._filters = None + self._data = page_data + self.ObjectType = "Page" + self.FilePath = file_path + self.Name = page_data.get("name") + self.DisplayOption = page_data.get("displayOption") + self.FilterCount = len( + get_jsonpath_value( + data=page_data, path="$.filterConfig.filters", default=[] + ) + ) + self.URL = wrapper._get_url(page_name=self.Name) + + def _get_property( + self, json_path: str, default=None, remove_quotes: bool = False + ): + return get_jsonpath_value( + self._data, json_path, default=default, remove_quotes=remove_quotes + ) + + def _set_property(self, json_path, value, add_quotes: bool = False): + if add_quotes: + value = f"'{value}'" + self._data = set_json_value( + payload=self._data, + json_path=json_path, + json_value=value, + ) + self._wrapper.set_json( + file_path=self.FilePath, + json_path=json_path, + json_value=value, + ) + + def _remove_property(self, json_path): + self._wrapper.remove( + file_path=self.FilePath, json_path=json_path, verbose=False + ) + remove_json_value( + path=self.FilePath, + payload=self._data, + json_path=json_path, + verbose=False, + ) + + @property + def DisplayName(self): + path = "$.displayName" + return self._get_property(path) + + @DisplayName.setter + def DisplayName(self, value: str): + path = "$.displayName" + self._set_property(path, value) + + @property + def Height(self): + path = "$.height" + return self._get_property(path) + + @Height.setter + def Height(self, value: int): + if not isinstance(value, int): + raise TypeError( + f"Height must be an integer, got {type(value).__name__}" + ) + path = "$.height" + self._set_property(path, value) + + @property + def Width(self): + path = "$.width" + return self._get_property(path) + + @Width.setter + def Width(self, value: int): + if not isinstance(value, int): + raise TypeError(f"Width must be an integer, got {type(value).__name__}") + path = "$.width" + self._set_property(path, value) + + @property + def IsActive(self): + return ( + self._wrapper.get( + file_path=ReportWrapper._pages_file_path, + json_path="$.activePageName", + ) + == self.Name + ) + + @IsActive.setter + def IsActive(self, value: bool): + if not isinstance(value, bool): + raise TypeError(f"IsActive must be a bool, got {type(value).__name__}") + if not value: + raise ValueError("IsActive can only be set to True.") + self._wrapper.set_json( + file_path=ReportWrapper._pages_file_path, + json_path="$.activePageName", + json_value=self.Name, + ) + + @property + def IsHidden(self): + return self._get_property(path="$.visibility") == "HiddenInViewMode" + + @IsHidden.setter + def IsHidden(self, value: bool): + if not isinstance(value, bool): + raise TypeError(f"IsHidden must be a bool, got {type(value).__name__}") + if value: + self._set_property( + json_path="$.visibility", json_value="HiddenInViewMode" + ) + else: + self._remove_property("$.visibility") + + @property + def Visuals(self): + if self._visuals is None: + self._visuals = ReportWrapper.VisualCollection( + self._wrapper, self.FilePath + ) + return self._visuals + + @property + def Filters(self): + if self._filters is None: + self._filters = PageFilterCollection(self._wrapper, self.FilePath) + return self._filters + + class PageCollection: + def __init__(self, wrapper): + self._wrapper = wrapper + self._pages = None + + def _load_pages(self): + parts = self._wrapper.get(file_path="definition/pages/*/page.json") + self._pages = [ + ReportWrapper.Page(file_path, data, self._wrapper) + for file_path, data in parts + ] + + def __iter__(self): + if self._pages is None: + self._load_pages() + return iter(self._pages) + + def __getitem__(self, key): + if self._pages is None: + self._load_pages() + + if isinstance(key, int): + return self._pages[key] + + # Support lookup by Name or DisplayName + for page in self._pages: + if ( + getattr(page, "Name", None) == key + or getattr(page, "DisplayName", None) == key + ): + return page + + raise KeyError(f"No page found with Name or DisplayName = '{key}'") + + @property + def Count(self): + if self._pages is None: + self._load_pages() + return len(self._pages) + + class Visual: + def __init__(self, file_path, visual_data, wrapper): + self._wrapper = wrapper + self._data = visual_data + self._filters = None + self._page = None + self.FilePath = file_path + self._page_file_path = self.FilePath.split("/visuals/")[0] + "/page.json" + self.ObjectType = "Visual" + self.Name = get_jsonpath_value(visual_data, "$.name") + self.Z = get_jsonpath_value(visual_data, "$.position.z") + self.TabOrder = get_jsonpath_value(visual_data, "$.position.tabOrder") + self.Type = get_jsonpath_value(visual_data, "$.visual.visualType", "Group") + self.DisplayType = helper.vis_type_mapping.get(self.Type, self.Type) + report_file = self._wrapper.get(file_path=self._wrapper._report_file_path) + custom_visuals = report_file.get("publicCustomVisuals", []) + self.IsCustomVisual = self.Type in custom_visuals + self.DataLimit = get_jsonpath_value( + data=visual_data, + path='$.filterConfig.filters[?(@.type == "VisualTopN")].filter.Where[*].Condition.VisualTopN.ItemCount', + default=0, + ) + self.FilterCount = len( + get_jsonpath_value( + data=visual_data, path="$.filterConfig.filters", default=[] + ) + ) + self.SlicerType = get_jsonpath_value( + data=visual_data, + path="$.visual.objects.data[*].properties.mode.expr.Literal.Value", + default="N/A", + remove_quotes=True, + ) + self.HowCreated = get_jsonpath_value( + data=visual_data, path="$.visual.howCreated", default="Unknown" + ) + self.HasSparkline = ( + get_jsonpath_value(data=visual_data, path="$..SparklineData") + is not None + ) + data_keys = [ + "Aggregation", + "Column", + "Measure", + "HierarchyLevel", + "NativeVisualCalculation", + ] + + self.IsDataVisual = any( + get_jsonpath_value(data=visual_data, path=f"$..{key}") + for key in data_keys + ) + + @property + def Page(self): + return ReportWrapper.Page( + file_path=self._page_file_path, + page_data=self._wrapper.get(file_path=self._page_file_path), + wrapper=self._wrapper, + ) + + def _get_property( + self, + json_path: str, + default=None, + remove_quotes: bool = False, + fix_true: bool = False, + ): + return get_jsonpath_value( + self._data, + json_path, + default=default, + remove_quotes=remove_quotes, + fix_true=fix_true, + ) + + def _set_property(self, json_path, value, add_quotes: bool = False): + if add_quotes: + value = f"'{value}'" + self._data = set_json_value( + payload=self._data, + json_path=json_path, + json_value=value, + ) + self._wrapper.set_json( + file_path=self.FilePath, + json_path=json_path, + json_value=value, + ) + + def _remove_property(self, json_path): + self._wrapper.remove( + file_path=self.FilePath, json_path=json_path, verbose=False + ) + remove_json_value( + path=self.FilePath, + payload=self._data, + json_path=json_path, + verbose=False, + ) + + # Title + @property + def Title(self): + base_path = "$.visual.visualContainerObjects.title[*].properties" + return TitleObject(self, base_path) + + @property + def SubTitle(self): + base_path = "$.visual.visualContainerObjects.subTitle[*].properties" + return TitleObject(self, base_path) + + @property + def X(self): + path = ReportWrapper._visual_x_path + return self._get_property(path) + + @X.setter + def X(self, value: float | int): + if not isinstance(value, float | int): + raise TypeError(f"X must be an integer, got {type(value).__name__}") + path = ReportWrapper._visual_x_path + self._set_property(path, value) + + @property + def Y(self): + path = ReportWrapper._visual_y_path + return self._get_property(path) + + @Y.setter + def Y(self, value: float | int): + if not isinstance(value, float | int): + raise TypeError(f"Y must be an integer, got {type(value).__name__}") + path = ReportWrapper._visual_y_path + self._set_property(path, value) + + @property + def Height(self): + return self._get_property("$.position.height") + + @Height.setter + def Height(self, value: float | int): + self._set_property("$.position.height", value) + + @property + def Width(self): + return self._get_property("$.position.width") + + @Width.setter + def Width(self, value: float | int): + self._set_property("$.position.width", value) + + @property + def IsHidden(self): + return self._get_property("$.isHidden", default=False) + + @IsHidden.setter + def IsHidden(self, value: bool): + if not isinstance(value, bool): + raise TypeError(f"IsHidden must be a bool, got {type(value).__name__}") + if value: + self._set_property("$.isHidden", True) + else: + self._remove_property("$.isHidden") + + @property + def ShowItemsWithNoData(self): + return self._get_property("$..showAll", default=False) + + @ShowItemsWithNoData.setter + def ShowItemsWithNoData(self, value: bool): + if not isinstance(value, bool): + raise TypeError( + f"ShowItemsWithNoData must be a bool, got {type(value).__name__}" + ) + if value: + self._set_property( + file_path=self.FilePath, json_path="$..showAll", json_value=True + ) + else: + self._remove_property(json_path="$..showAll") + + @property + def LockAspectRatio(self): + return self._get_property( + "$.visual.visualContainerObjects.lockAspect[*].properties.show.expr.Literal.Value", + default=False, + fix_true=True, + ) + + @LockAspectRatio.setter + def LockAspectRatio(self, value: bool): + if not isinstance(value, bool): + raise TypeError( + f"LockAspectRatio must be a bool, got {type(value).__name__}" + ) + if value: + self._set_property( + file_path=self.FilePath, + json_path="$.visual.visualContainerObjects.lockAspect[*].properties.show.expr.Literal.Value", + json_value="true", + ) + else: + self._remove_property( + file_path=self.FilePath, + json_path="$.visual.visualContainerObjects.lockAspect", + ) + + @property + def MaintainLayerOrder(self): + return self._get_property( + "$.visual.visualContainerObjects.general[*].properties.keepLayerOrder.expr.Literal.Value", + default=False, + fix_true=True, + ) + + @MaintainLayerOrder.setter + def MaintainLayerOrder(self, value: bool): + if not isinstance(value, bool): + raise TypeError( + f"MaintainLayerOrder must be a bool, got {type(value).__name__}" + ) + if value: + self._set_property( + "$.visual.visualContainerObjects.general[*].properties.keepLayerOrder.expr.Literal.Value", + "true", + ) + else: + self._remove_property( + "$.visual.visualContainerObjects.general[*].properties.keepLayerOrder", + ) + + @property + def AltText(self): + return self._get_property( + "$.visual.visualContainerObjects.general[*].properties.altText.expr.Literal.Value" + ) + + @AltText.setter + def AltText(self, value: str): + if value: + self._set_property( + "$.visual.visualContainerObjects.general[*].properties.altText.expr.Literal.Value", + value, + ) + else: + self._remove_property( + "$.visual.visualContainerObjects.general[*].properties.altText" + ) + + @property + def Filters(self): + if self._filters is None: + self._filters = VisualFilterCollection(self._wrapper, self.FilePath) + return self._filters + + class VisualCollection: + def __init__(self, wrapper, page_file_path=None): + self._wrapper = wrapper + self._page_file_path = page_file_path + self._visuals = None + + def _load_visuals(self): + all_visuals = [] + parts = self._wrapper.get(file_path="definition/pages/*/visual.json") + for file_path, data in parts: + if self._page_file_path: + page_dir = "/".join(self._page_file_path.split("/")[:3]) + if not file_path.startswith(page_dir): + continue + all_visuals.append(ReportWrapper.Visual(file_path, data, self._wrapper)) + self._visuals = all_visuals + + def __iter__(self): + if self._visuals is None: + self._load_visuals() + return iter(self._visuals) + + def __getitem__(self, key): + if self._visuals is None: + self._load_visuals() + + # If key is an int, return by index + if isinstance(key, int): + return self._visuals[key] + + # Otherwise, search for a visual by name + for visual in self._visuals: + if visual.Name == key: + return visual + + raise KeyError(f"Visual '{key}' not found") + + @property + def Count(self): + if self._visuals is None: + self._load_visuals() + return len(self._visuals) + + class CustomVisual: + def __init__(self, name): + self.ObjectType = "Custom Visual" + self.Name = name + self.DisplayName = helper.vis_type_mapping.get(name, name) + + class CustomVisualCollection: + def __init__(self, wrapper): + self._wrapper = wrapper + self._custom_visuals = None + + def _load_custom_visuals(self): + raw = ( + self._wrapper.get( + file_path=self._wrapper._report_file_path, + json_path="$.publicCustomVisuals", + ) + or [] + ) + self._custom_visuals = [ReportWrapper.CustomVisual(name) for name in raw] + + def __iter__(self): + if self._custom_visuals is None: + self._load_custom_visuals() + return iter(self._custom_visuals) + + def __getitem__(self, key): + if self._custom_visuals is None: + self._load_custom_visuals() + return self._custom_visuals[key] + + @property + def Count(self): + if self._custom_visuals is None: + self._load_custom_visuals() + return len(self._custom_visuals) # Basic functions def get( @@ -2947,3 +3489,317 @@ def connect_report( yield rw finally: rw.close() + + +# Supplementary classes +class TitleObject: + def __init__(self, visual, base_path: str): + self.visual = visual + self.base_path = base_path + + # Text + @property + def Text(self): + path = f"{self.base_path}.text.expr.Literal.Value" + return self.visual._get_property(path, remove_quotes=True) + + @Text.setter + def Text(self, value: str): + path = f"{self.base_path}.text.expr.Literal.Value" + self.visual._set_property(path, value, add_quotes=True) + + # Show + @property + def Show(self): + path = f"{self.base_path}.show.expr.Literal.Value" + return self.visual._get_property(path, fix_true=True) + + @Show.setter + def Show(self, value: bool): + if not isinstance(value, bool): + raise ValueError( + f"{icons.red_dot} The value for 'Show' must be a boolean. Provided: {type(value)}" + ) + path = f"{self.base_path}.show.expr.Literal.Value" + if value: + self.visual._set_property(path, "true") + else: + self.visual._remove_property(path) + + # Font + @property + def Font(self): + path = f"{self.base_path}.fontFamily.expr.Literal.Value" + value = self.visual._get_property(path) + if value: + value = value.strip("'") + return value + + @Font.setter + def Font(self, value: str): + path = f"{self.base_path}.fontFamily.expr.Literal.Value" + self.visual._set_property(path, f"'''{value}'''") + + # Font Size + @property + def FontSize(self): + path = f"{self.base_path}.fontSize.expr.Literal.Value" + value = self.visual._get_property(path) + if value: + value = value.rstrip("D") + return value + + @FontSize.setter + def FontSize(self, value: int): + if not isinstance(value, int): + raise ValueError( + f"{icons.red_dot} The value for 'FontSize' must be an integer. Provided: {type(value)}" + ) + path = f"{self.base_path}.fontSize.expr.Literal.Value" + self.visual._set_property(path, f"{value}D") + + # Bold + @property + def Bold(self): + path = f"{self.base_path}.bold.expr.Literal.Value" + return self.visual._get_property(path, fix_true=True) + + @Bold.setter + def Bold(self, value: bool): + if not isinstance(value, bool): + raise ValueError( + f"{icons.red_dot} The value for 'Bold' must be a bool. Provided: {type(value)}" + ) + path = f"{self.base_path}.bold.expr.Literal.Value" + if value: + self.visual._set_property(path, "true") + else: + self.visual._remove_property(path) + + # Italic + @property + def Italic(self): + path = f"{self.base_path}.italic.expr.Literal.Value" + return self.visual._get_property(path, fix_true=True) + + @Italic.setter + def Italic(self, value: bool): + if not isinstance(value, bool): + raise ValueError( + f"{icons.red_dot} The value for 'Italic' must be a bool. Provided: {type(value)}" + ) + path = f"{self.base_path}.italic.expr.Literal.Value" + if value: + self.visual._set_property(path, "true") + else: + self.visual._remove_property(path) + + # Underline + @property + def Underline(self): + path = f"{self.base_path}.underline.expr.Literal.Value" + return self.visual._get_property(path, fix_true=True) + + @Underline.setter + def Underline(self, value: bool): + if not isinstance(value, bool): + raise ValueError( + f"{icons.red_dot} The value for 'Underline' must be a bool. Provided: {type(value)}" + ) + path = f"{self.base_path}.underline.expr.Literal.Value" + if value: + self.visual._set_property(path, "true") + else: + self.visual._remove_property(path) + + # Font Color + + # Alignment + @property + def Alignment(self): + path = f"{self.base_path}.alignment.expr.Literal.Value" + value = self.visual._get_property(path, remove_quotes=True) + if value: + value = value.capitalize() + return value + + @Alignment.setter + def Alignment(self, value: Literal["Left", "Center", "Right"]): + value = value.capitalize() + if value not in ["Left", "Center", "Right"]: + raise ValueError( + f"{icons.red_dot} The value for 'Alignment' must be one of 'Left', 'Center', or 'Right'. Provided: {value}" + ) + path = f"{self.base_path}.alignment.expr.Literal.Value" + self.visual._set_property(path, f"{value.lower()}", add_quotes=True) + + # Text Wrap + @property + def TextWrap(self): + path = f"{self.base_path}.titleWrap.expr.Literal.Value" + return self.visual._get_property(path, fix_true=True) + + @TextWrap.setter + def TextWrap(self, value: bool): + if not isinstance(value, bool): + raise ValueError( + f"{icons.red_dot} The value for 'TextWrap' must be a bool. Provided: {type(value)}" + ) + path = f"{self.base_path}.underline.expr.Literal.Value" + if not value: + self.visual._set_property(path, "false") + else: + self.visual._remove_property(path) + + +class Filter: + def __init__(self, file_path, filter_data, wrapper): + self._wrapper = wrapper + self._data = filter_data + self.FilePath = file_path + self.ObjectType = "Filter" + self.Name = filter_data.get("name") + self.Ordinal = filter_data.get("ordinal") + self.HowCreated = filter_data.get("howCreated", "Unknown") + self.Locked = filter_data.get("isLockedInViewMode", False) + self.Hidden = filter_data.get("isHiddenInViewMode", False) + self.Type = filter_data.get("type", "Basic") + self.IsUsed = True if "Where" in filter_data.get("filter", {}) else False + + # Initialize fields + self.TableName = None + self.ObjectName = None + self.ObjectType = None + + field = filter_data.get("field", {}) + + # Case 1: Simple column + if "Column" in field: + col = field["Column"] + self.TableName = ( + col.get("Expression", {}).get("SourceRef", {}).get("Entity") + ) + self.ObjectName = col.get("Property") + self.ObjectType = "Column" + + # Case 2: Aggregation over column (still a column) + elif "Aggregation" in field: + col = field.get("Aggregation", {}).get("Expression", {}).get("Column", {}) + self.TableName = ( + col.get("Expression", {}).get("SourceRef", {}).get("Entity") + ) + self.ObjectName = col.get("Property") + self.ObjectType = "Column" + + # Case 3: Measure + elif "Measure" in field: + measure = field["Measure"] + self.TableName = ( + measure.get("Expression", {}).get("SourceRef", {}).get("Entity") + ) + self.ObjectName = measure.get("Property") + self.ObjectType = "Measure" + + def to_dict(self): + return { + **self._data, + "TableName": self.TableName, + "ObjectName": self.ObjectName, + "ObjectType": self.ObjectType, + } + + +class ReportFilterCollection: + def __init__(self, wrapper): + self._wrapper = wrapper + self._filters = None + + def _load_filters(self): + data = self._wrapper.get(file_path=ReportWrapper._report_file_path) + raw_filters = get_jsonpath_value( + data=data, path="$.filterConfig.filters", default=[] + ) + self._filters = [ + Filter(ReportWrapper._report_file_path, f, self._wrapper) + for f in raw_filters + ] + + def __iter__(self): + if self._filters is None: + self._load_filters() + return iter(self._filters) + + def __getitem__(self, index): + if self._filters is None: + self._load_filters() + return self._filters[index] + + @property + def Count(self): + if self._filters is None: + self._load_filters() + return len(self._filters) + + +class PageFilterCollection: + def __init__(self, wrapper, page_file_path=None): + self._wrapper = wrapper + self._page_filters = None + self._page_file_path = page_file_path + + def _load_filters(self): + data = self._wrapper.get(file_path=self._page_file_path) + filter_list = get_jsonpath_value( + data, path="$.filterConfig.filters", default=[] + ) + self._page_filters = [ + Filter(self._page_file_path, f, self._wrapper) for f in filter_list + ] + + def __iter__(self): + if self._page_filters is None: + self._load_filters() + return iter(self._page_filters) + + def __getitem__(self, index): + if self._page_filters is None: + self._load_filters() + return self._page_filters[index] + + @property + def Count(self): + if self._page_filters is None: + self._load_filters() + return len(self._page_filters) + + +class VisualFilterCollection: + def __init__(self, wrapper, visual_file_path=None): + self._wrapper = wrapper + self._visual_filters = None + self._visual_file_path = visual_file_path + + def _load_filters(self): + data = self._wrapper.get(file_path=self._visual_file_path) + filter_list = get_jsonpath_value( + data, path="$.filterConfig.filters", default=[] + ) + self._visual_filters = [ + Filter(self._visual_file_path, f, self._wrapper) for f in filter_list + ] + + def __iter__(self): + if self._visual_filters is None: + self._load_filters() + return iter(self._visual_filters) + + def __getitem__(self, index): + if self._visual_filters is None: + self._load_filters() + return self._visual_filters[index] + + @property + def Count(self): + if self._visual_filters is None: + self._load_filters() + return len(self._visual_filters) From ab932efb9a596bd038f42549dcef607b19ff38b8 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 4 Jun 2025 09:44:34 +0300 Subject: [PATCH 2/7] fix --- src/sempy_labs/report/_reportwrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sempy_labs/report/_reportwrapper.py b/src/sempy_labs/report/_reportwrapper.py index d4012146..2ae9ae25 100644 --- a/src/sempy_labs/report/_reportwrapper.py +++ b/src/sempy_labs/report/_reportwrapper.py @@ -153,7 +153,7 @@ def __init__( self._current_report_definition = copy.deepcopy(self._report_definition) - # self.report = self.Report(self) + self.report = self.Report(self) helper.populate_custom_visual_display_names() From 02fb9ac878d04458bb497da657f5f0606f66c924 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 4 Jun 2025 15:15:28 +0300 Subject: [PATCH 3/7] added json, set_json properties to page class --- src/sempy_labs/report/_reportwrapper.py | 123 ++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 7 deletions(-) diff --git a/src/sempy_labs/report/_reportwrapper.py b/src/sempy_labs/report/_reportwrapper.py index 2ae9ae25..54716318 100644 --- a/src/sempy_labs/report/_reportwrapper.py +++ b/src/sempy_labs/report/_reportwrapper.py @@ -35,6 +35,7 @@ from urllib.parse import urlparse import os import fnmatch +from sempy_labs.tom import connect_semantic_model class ReportWrapper: @@ -164,7 +165,7 @@ def _ensure_pbir(self): f"{icons.red_dot} This ReportWrapper function requires the report to be in the PBIR format." "See here for details: https://powerbi.microsoft.com/blog/power-bi-enhanced-report-format-pbir-in-power-bi-desktop-developer-mode-preview/" ) - + class Report: def __init__(self, wrapper): self.ObjectType = "Report" @@ -244,6 +245,20 @@ def _remove_property(self, json_path): verbose=False, ) + @property + def json(self): + return self._data + + @json.setter + def json(self, value): + if isinstance(value, dict): + self._data = value + else: + raise ValueError("json must be a dictionary") + + def set_json(self, json_path: str, json_value: str | dict | List): + ReportWrapper.set_json(self=self._wrapper, file_path=self.FilePath, json_path=json_path, json_value=json_value) + @property def DisplayName(self): path = "$.displayName" @@ -707,6 +722,106 @@ def Count(self): self._load_custom_visuals() return len(self._custom_visuals) + # Report Level Measures + class ReportLevelMeasure: + def __init__(self, name, table_name, expr, format_string): + self.ObjectType = "Report Level Measure" + self.MeasureName = name + self.TableName = table_name + self.Expression = expr + self.FormatString = format_string + + class ReportLevelMeasureCollection: + def __init__(self, wrapper): + self._wrapper = wrapper + self._report_level_measures = None + + def _load_report_level_measures(self): + payload = self._wrapper.get( + file_path=self._wrapper._report_file_path, + json_path="$.entities", + ) or [] + + measures = [] + for entity in payload: + table_name = entity.get("name") + for m in entity.get("measures", []): + measure_name = m.get("name") + expr = m.get("expression") + format_string = m.get("formatString") + measure = ReportWrapper.ReportLevelMeasure( + name=measure_name, + table_name=table_name, + expr=expr, + format_string=format_string, + ) + measures.append(measure) + + self._report_level_measures = measures + + def __iter__(self): + if self._report_level_measures is None: + self._load_report_level_measures() + return iter(self._report_level_measures) + + def __getitem__(self, key): + if self._report_level_measures is None: + self._load_report_level_measures() + return self._report_level_measures[key] + + @property + def Count(self): + if self._report_level_measures is None: + self._load_report_level_measures() + return len(self._report_level_measures) + + # Bookmarks + class Bookmark: + def __init__(self, file_path, data, wrapper): + self.ObjectType = "Bookmark" + self._wrapper = wrapper + self._data = data + self.FilePath = file_path + self.Name = get_jsonpath_value(data, "$.name") + + @property + def DisplayName(self): + path = "$.displayName" + return self._get_property(path) + + @DisplayName.setter + def DisplayName(self, value: str): + path = "$.displayName" + self._set_property(path, value) + + class BookmarkCollection: + def __init__(self, wrapper): + self._wrapper = wrapper + self._bookmarks = None + + def _load_bookmarks(self): + all_bookmarks = [] + parts = self._wrapper.get(file_path="definition/bookmarks/*/bookmark.json") + for file_path, data in parts: + all_bookmarks.append(ReportWrapper.Visual(file_path, data, self._wrapper)) + self._bookmarks = all_bookmarks + + def __iter__(self): + if self._bookmarks is None: + self._load_bookmarks() + return iter(self._bookmarks) + + def __getitem__(self, key): + if self._bookmarks is None: + self._load_bookmarks() + return self._bookmarks[key] + + @property + def Count(self): + if self._bookmarks is None: + self._load_bookmarks() + return len(self._bookmarks) + # Basic functions def get( self, @@ -1118,8 +1233,6 @@ def __add_to_registered_resources(self, name: str, path: str, type: str): def _add_extended(self, dataframe): - from sempy_labs.tom import connect_semantic_model - dataset_id, dataset_name, dataset_workspace_id, dataset_workspace_name = ( resolve_dataset_from_report( report=self._report_id, workspace=self._workspace_id @@ -2018,8 +2131,6 @@ def list_semantic_model_objects(self, extended: bool = False) -> pd.DataFrame: """ self._ensure_pbir() - from sempy_labs.tom import connect_semantic_model - columns = { "Table Name": "str", "Object Name": "str", @@ -2611,8 +2722,6 @@ def migrate_report_level_measures(self, measures: Optional[str | List[str]] = No """ self._ensure_pbir() - from sempy_labs.tom import connect_semantic_model - rlm = self.list_report_level_measures() if rlm.empty: print( From 2334c4acd8640d5047d31a37513306c4eb487841 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 5 Jun 2025 09:11:56 +0300 Subject: [PATCH 4/7] update for rlm --- src/sempy_labs/report/_reportwrapper.py | 55 +++++++++++++++---------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/src/sempy_labs/report/_reportwrapper.py b/src/sempy_labs/report/_reportwrapper.py index 54716318..e9ed10df 100644 --- a/src/sempy_labs/report/_reportwrapper.py +++ b/src/sempy_labs/report/_reportwrapper.py @@ -724,12 +724,13 @@ def Count(self): # Report Level Measures class ReportLevelMeasure: - def __init__(self, name, table_name, expr, format_string): + def __init__(self, name, table_name, expr, format_string, data_category): self.ObjectType = "Report Level Measure" self.MeasureName = name self.TableName = table_name self.Expression = expr self.FormatString = format_string + self.DataCategory = data_category class ReportLevelMeasureCollection: def __init__(self, wrapper): @@ -737,10 +738,13 @@ def __init__(self, wrapper): self._report_level_measures = None def _load_report_level_measures(self): - payload = self._wrapper.get( - file_path=self._wrapper._report_file_path, - json_path="$.entities", - ) or [] + if self._wrapper._report_file_path not in self._wrapper.list_paths()['Paths'].values: + payload = [] + else: + payload = self._wrapper.get( + file_path=self._wrapper._report_file_path, + json_path="$.entities", + ) or [] measures = [] for entity in payload: @@ -749,11 +753,13 @@ def _load_report_level_measures(self): measure_name = m.get("name") expr = m.get("expression") format_string = m.get("formatString") + data_category = m.get("dataCategory") measure = ReportWrapper.ReportLevelMeasure( name=measure_name, table_name=table_name, expr=expr, format_string=format_string, + data_category=data_category, ) measures.append(measure) @@ -2365,31 +2371,36 @@ def list_report_level_measures(self) -> pd.DataFrame: "Expression": "str", "Data Type": "str", "Format String": "str", + "Data Category": "str", } df = _create_dataframe(columns=columns) + # If no report extensions path, return empty DataFrame + if self._report_extensions_path not in self.list_paths()["Path"].values: + return df + report_file = self.get(file_path=self._report_extensions_path) dfs = [] - if report_file: - payload = report_file.get("payload") - for e in payload.get("entities", []): - table_name = e.get("name") - for m in e.get("measures", []): - measure_name = m.get("name") - expr = m.get("expression") - data_type = m.get("dataType") - format_string = m.get("formatString") + for e in report_file.get("entities", []): + table_name = e.get("name") + for m in e.get("measures", []): + measure_name = m.get("name") + expr = m.get("expression") + data_type = m.get("dataType") + format_string = m.get("formatString") + data_category = m.get("dataCategory") - new_data = { - "Measure Name": measure_name, - "Table Name": table_name, - "Expression": expr, - "Data Type": data_type, - "Format String": format_string, - } - dfs.append(pd.DataFrame(new_data, index=[0])) + new_data = { + "Measure Name": measure_name, + "Table Name": table_name, + "Expression": expr, + "Data Type": data_type, + "Format String": format_string, + "Data Category": data_category, + } + dfs.append(pd.DataFrame(new_data, index=[0])) if dfs: df = pd.concat(dfs, ignore_index=True) From a8dabe94ecf3ffeb046b34219e6995ca3d2774e3 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 5 Jun 2025 13:00:26 +0300 Subject: [PATCH 5/7] fixed add_image --- docs/source/conf.py | 2 +- pyproject.toml | 2 +- src/sempy_labs/report/_reportwrapper.py | 206 +++++++++++++++++------- 3 files changed, 151 insertions(+), 59 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 05dbb6eb..cf578a3e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,7 +13,7 @@ project = 'semantic-link-labs' copyright = '2024, Microsoft and community' author = 'Microsoft and community' -release = '0.10.0' +release = '0.10.1' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/pyproject.toml b/pyproject.toml index 5473ace0..9be8d327 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name="semantic-link-labs" authors = [ { name = "Microsoft Corporation" }, ] -version="0.10.0" +version="0.10.1" description="Semantic Link Labs for Microsoft Fabric" readme="README.md" requires-python=">=3.10,<3.12" diff --git a/src/sempy_labs/report/_reportwrapper.py b/src/sempy_labs/report/_reportwrapper.py index e9ed10df..be666824 100644 --- a/src/sempy_labs/report/_reportwrapper.py +++ b/src/sempy_labs/report/_reportwrapper.py @@ -246,18 +246,23 @@ def _remove_property(self, json_path): ) @property - def json(self): + def Metadata(self): return self._data - @json.setter - def json(self, value): + @Metadata.setter + def Metadata(self, value): if isinstance(value, dict): self._data = value else: - raise ValueError("json must be a dictionary") + raise ValueError("Metadata must be a dictionary") def set_json(self, json_path: str, json_value: str | dict | List): - ReportWrapper.set_json(self=self._wrapper, file_path=self.FilePath, json_path=json_path, json_value=json_value) + ReportWrapper.set_json( + self=self._wrapper, + file_path=self.FilePath, + json_path=json_path, + json_value=json_value, + ) @property def DisplayName(self): @@ -392,6 +397,7 @@ def __init__(self, file_path, visual_data, wrapper): self._data = visual_data self._filters = None self._page = None + self._fields = None self.FilePath = file_path self._page_file_path = self.FilePath.split("/visuals/")[0] + "/page.json" self.ObjectType = "Visual" @@ -439,14 +445,6 @@ def __init__(self, file_path, visual_data, wrapper): for key in data_keys ) - @property - def Page(self): - return ReportWrapper.Page( - file_path=self._page_file_path, - page_data=self._wrapper.get(file_path=self._page_file_path), - wrapper=self._wrapper, - ) - def _get_property( self, json_path: str, @@ -487,6 +485,33 @@ def _remove_property(self, json_path): verbose=False, ) + @property + def Metadata(self): + return self._data + + @Metadata.setter + def Metadata(self, value): + if isinstance(value, dict): + self._data = value + else: + raise ValueError("Metadata must be a dictionary") + + def set_json(self, json_path: str, json_value: str | dict | List): + ReportWrapper.set_json( + self=self._wrapper, + file_path=self.FilePath, + json_path=json_path, + json_value=json_value, + ) + + @property + def Page(self): + return ReportWrapper.Page( + file_path=self._page_file_path, + page_data=self._wrapper.get(file_path=self._page_file_path), + wrapper=self._wrapper, + ) + # Title @property def Title(self): @@ -724,13 +749,16 @@ def Count(self): # Report Level Measures class ReportLevelMeasure: - def __init__(self, name, table_name, expr, format_string, data_category): + def __init__(self, name, table_name, expr, format_string, data_category, hidden, description, display_folder): self.ObjectType = "Report Level Measure" self.MeasureName = name self.TableName = table_name self.Expression = expr self.FormatString = format_string self.DataCategory = data_category + self.Hidden = hidden + self.Description = description + self.DisplayFolder = display_folder class ReportLevelMeasureCollection: def __init__(self, wrapper): @@ -738,13 +766,19 @@ def __init__(self, wrapper): self._report_level_measures = None def _load_report_level_measures(self): - if self._wrapper._report_file_path not in self._wrapper.list_paths()['Paths'].values: + if ( + self._wrapper._report_file_path + not in self._wrapper.list_paths()["Paths"].values + ): payload = [] else: - payload = self._wrapper.get( - file_path=self._wrapper._report_file_path, - json_path="$.entities", - ) or [] + payload = ( + self._wrapper.get( + file_path=self._wrapper._report_file_path, + json_path="$.entities", + ) + or [] + ) measures = [] for entity in payload: @@ -752,14 +786,20 @@ def _load_report_level_measures(self): for m in entity.get("measures", []): measure_name = m.get("name") expr = m.get("expression") - format_string = m.get("formatString") - data_category = m.get("dataCategory") + format_string = m.get("formatString", "") + data_category = m.get("dataCategory", "") + hidden = m.get("hidden", False) + description = m.get("description", "") + display_folder = m.get("displayFolder", "") measure = ReportWrapper.ReportLevelMeasure( name=measure_name, table_name=table_name, expr=expr, format_string=format_string, data_category=data_category, + hidden=hidden, + description=description, + display_folder=display_folder, ) measures.append(measure) @@ -809,7 +849,9 @@ def _load_bookmarks(self): all_bookmarks = [] parts = self._wrapper.get(file_path="definition/bookmarks/*/bookmark.json") for file_path, data in parts: - all_bookmarks.append(ReportWrapper.Visual(file_path, data, self._wrapper)) + all_bookmarks.append( + ReportWrapper.Visual(file_path, data, self._wrapper) + ) self._bookmarks = all_bookmarks def __iter__(self): @@ -828,6 +870,19 @@ def Count(self): self._load_bookmarks() return len(self._bookmarks) + @property + def all_pages(self): + + for page in self.report.Pages: + yield page + + @property + def all_visuals(self): + + for page in self.report.Pages: + for visual in page.Visuals: + yield visual + # Basic functions def get( self, @@ -1230,7 +1285,7 @@ def __add_to_registered_resources(self, name: str, path: str, type: str): print( f"{icons.info} The '{item_name}' {type.lower()} already exists in the report definition." ) - raise ValueError() + return # Add the new item to the existing RegisteredResources rp["items"].append(new_item) @@ -2864,7 +2919,7 @@ def _list_annotations(self) -> pd.DataFrame: return df - def _add_image(self, image_path: str, resource_name: Optional[str] = None) -> str: + def add_image(self, image_path: str, resource_name: Optional[str] = None) -> str: """ Add an image to the report definition. The image will be added to the StaticResources/RegisteredResources folder in the report definition. If the image_name already exists as a file in the report definition it will be updated. @@ -2885,6 +2940,9 @@ def _add_image(self, image_path: str, resource_name: Optional[str] = None) -> st id = generate_number_guid() if image_path.startswith("http://") or image_path.startswith("https://"): + if "github.com" in image_path and "/blob/" in image_path: + image_path = image_path.replace("github.com", "raw.githubusercontent.com") + image_path = image_path.replace("/blob/", "/") response = requests.get(image_path) response.raise_for_status() image_bytes = response.content @@ -3786,39 +3844,8 @@ def __init__(self, file_path, filter_data, wrapper): self.Type = filter_data.get("type", "Basic") self.IsUsed = True if "Where" in filter_data.get("filter", {}) else False - # Initialize fields - self.TableName = None - self.ObjectName = None - self.ObjectType = None - - field = filter_data.get("field", {}) - - # Case 1: Simple column - if "Column" in field: - col = field["Column"] - self.TableName = ( - col.get("Expression", {}).get("SourceRef", {}).get("Entity") - ) - self.ObjectName = col.get("Property") - self.ObjectType = "Column" - - # Case 2: Aggregation over column (still a column) - elif "Aggregation" in field: - col = field.get("Aggregation", {}).get("Expression", {}).get("Column", {}) - self.TableName = ( - col.get("Expression", {}).get("SourceRef", {}).get("Entity") - ) - self.ObjectName = col.get("Property") - self.ObjectType = "Column" - - # Case 3: Measure - elif "Measure" in field: - measure = field["Measure"] - self.TableName = ( - measure.get("Expression", {}).get("SourceRef", {}).get("Entity") - ) - self.ObjectName = measure.get("Property") - self.ObjectType = "Measure" + field_data = filter_data.get("field", {}) + self.Field = Field.from_dict(field_data) def to_dict(self): return { @@ -3828,6 +3855,17 @@ def to_dict(self): "ObjectType": self.ObjectType, } + @property + def Metadata(self): + return self._data + + @Metadata.setter + def Metadata(self, value): + if isinstance(value, dict): + self._data = value + else: + raise ValueError("Metadata must be a dictionary") + class ReportFilterCollection: def __init__(self, wrapper): @@ -3923,3 +3961,57 @@ def Count(self): if self._visual_filters is None: self._load_filters() return len(self._visual_filters) + + +class Field: + def __init__(self, table_name=None, object_name=None, object_type=None): + self.TableName = table_name + self.ObjectName = object_name + self.ObjectType = object_type + + @classmethod + def from_dict(cls, field: dict): + # Case 1: Simple column + if "Column" in field: + col = field["Column"] + table_name = col.get("Expression", {}).get("SourceRef", {}).get("Entity") + object_name = col.get("Property") + object_type = "Column" + return cls(table_name, object_name, object_type) + + # Case 2: Measure + elif "Measure" in field: + measure = field["Measure"] + table_name = measure.get("Expression", {}).get("SourceRef", {}).get("Entity") + object_name = measure.get("Property") + object_type = "Measure" + return cls(table_name, object_name, object_type) + + # Case 3: Aggregation over column + elif "Aggregation" in field: + col = field.get("Aggregation", {}).get("Expression", {}).get("Column", {}) + table_name = col.get("Expression", {}).get("SourceRef", {}).get("Entity") + object_name = col.get("Property") + object_type = "Column" + return cls(table_name, object_name, object_type) + + elif "SparklineData" in field: + m = field.get('SparklineData').get('Measure') + if "Measure" in m: + measure = m["Measure"] + table_name = measure.get("Expression", {}).get("SourceRef", {}).get("Entity") + object_name = measure.get("Property") + object_type = "Measure" + return cls(table_name, object_name, object_type) + elif "Aggregation" in m: + col = m.get("Aggregation", {}).get("Expression", {}).get("Column", {}) + table_name = col.get("Expression", {}).get("SourceRef", {}).get("Entity") + object_name = col.get("Property") + object_type = "Column" + return cls(table_name, object_name, object_type) + + # If no recognized key found, return empty Field + return cls() + + def __repr__(self): + return f"Field(TableName={self.TableName}, ObjectName={self.ObjectName}, ObjectType={self.ObjectType})" From 0fd98944075d5d13e1d166ab9e19f8501c82c61e Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 5 Jun 2025 13:45:28 +0300 Subject: [PATCH 6/7] cleaned up urlparse --- src/sempy_labs/_helper_functions.py | 32 ++++++++++++++++++------- src/sempy_labs/report/_reportwrapper.py | 32 ++++++++++++++++++------- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/sempy_labs/_helper_functions.py b/src/sempy_labs/_helper_functions.py index 6a88dd30..0f2506a7 100644 --- a/src/sempy_labs/_helper_functions.py +++ b/src/sempy_labs/_helper_functions.py @@ -12,29 +12,29 @@ from uuid import UUID import sempy_labs._icons as icons from azure.core.credentials import TokenCredential, AccessToken -import urllib.parse import numpy as np from IPython.display import display, HTML import requests import sempy_labs._authentication as auth from jsonpath_ng.ext import parse from jsonpath_ng.jsonpath import Fields, Index +from urllib.parse import urlparse, urlunparse, urlencode, quote def _build_url(url: str, params: dict) -> str: """ Build the url with a list of parameters """ - url_parts = list(urllib.parse.urlparse(url)) - url_parts[4] = urllib.parse.urlencode(params) - url = urllib.parse.urlunparse(url_parts) + url_parts = list(urlparse(url)) + url_parts[4] = urlencode(params) + url = urlunparse(url_parts) return url def _encode_user(user: str) -> str: - return urllib.parse.quote(user, safe="@") + return quote(user, safe="@") def create_abfss_path( @@ -94,7 +94,7 @@ def _get_default_file_path() -> str: def _split_abfss_path(path: str) -> Tuple[UUID, UUID, str]: - parsed_url = urllib.parse.urlparse(path) + parsed_url = urlparse(path) workspace_id = parsed_url.netloc.split("@")[0] item_id = parsed_url.path.lstrip("/").split("/")[0] @@ -1092,7 +1092,7 @@ def resolve_workspace_capacity( from sempy_labs._capacities import list_capacities (workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace) - filter_condition = urllib.parse.quote(workspace_id) + filter_condition = quote(workspace_id) dfW = fabric.list_workspaces(filter=f"id eq '{filter_condition}'") capacity_id = dfW["Capacity Id"].iloc[0] dfC = list_capacities() @@ -1126,7 +1126,7 @@ def get_capacity_id(workspace: Optional[str | UUID] = None) -> UUID: capacity_id = _get_fabric_context_setting(name="trident.capacity.id") else: (workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace) - filter_condition = urllib.parse.quote(workspace_id) + filter_condition = quote(workspace_id) dfW = fabric.list_workspaces(filter=f"id eq '{filter_condition}'") if len(dfW) == 0: raise ValueError(f"{icons.red_dot} The '{workspace_name}' does not exist'.") @@ -2435,3 +2435,19 @@ def remove_json_value(path: str, payload: dict, json_path: str, verbose: bool = print(f"{icons.green_dot} Removed index [{index}] from '{path}'.") return payload + + +def fix_github_url(url: str) -> str: + parsed = urlparse(url) + + # Check for valid scheme and correct host + if parsed.scheme in ("http", "https") and parsed.netloc == "github.com": + path_parts = parsed.path.split("/") + if len(path_parts) > 4 and path_parts[3] == "blob": + # Convert to raw.githubusercontent.com + new_netloc = "raw.githubusercontent.com" + new_path = "/".join(path_parts[:3] + path_parts[4:]) # remove 'blob' + new_url = urlunparse(("https", new_netloc, new_path, "", "", "")) + return new_url + + return url # return original if not a valid GitHub blob URL diff --git a/src/sempy_labs/report/_reportwrapper.py b/src/sempy_labs/report/_reportwrapper.py index be666824..bf81121c 100644 --- a/src/sempy_labs/report/_reportwrapper.py +++ b/src/sempy_labs/report/_reportwrapper.py @@ -17,6 +17,7 @@ get_jsonpath_value, set_json_value, remove_json_value, + fix_github_url, ) from sempy_labs._dictionary_diffs import ( diff_parts, @@ -749,7 +750,17 @@ def Count(self): # Report Level Measures class ReportLevelMeasure: - def __init__(self, name, table_name, expr, format_string, data_category, hidden, description, display_folder): + def __init__( + self, + name, + table_name, + expr, + format_string, + data_category, + hidden, + description, + display_folder, + ): self.ObjectType = "Report Level Measure" self.MeasureName = name self.TableName = table_name @@ -2532,6 +2543,7 @@ def set_theme(self, theme_file_path: str): f"{icons.red_dot} The '{theme_file_path}' theme file path must be a .json file." ) elif theme_file_path.startswith("https://"): + theme_file_path = fix_github_url(theme_file_path) response = requests.get(theme_file_path) theme_file = response.json() elif theme_file_path.startswith("/lakehouse") or theme_file_path.startswith( @@ -2940,9 +2952,7 @@ def add_image(self, image_path: str, resource_name: Optional[str] = None) -> str id = generate_number_guid() if image_path.startswith("http://") or image_path.startswith("https://"): - if "github.com" in image_path and "/blob/" in image_path: - image_path = image_path.replace("github.com", "raw.githubusercontent.com") - image_path = image_path.replace("/blob/", "/") + image_path = fix_github_url(image_path) response = requests.get(image_path) response.raise_for_status() image_bytes = response.content @@ -3982,7 +3992,9 @@ def from_dict(cls, field: dict): # Case 2: Measure elif "Measure" in field: measure = field["Measure"] - table_name = measure.get("Expression", {}).get("SourceRef", {}).get("Entity") + table_name = ( + measure.get("Expression", {}).get("SourceRef", {}).get("Entity") + ) object_name = measure.get("Property") object_type = "Measure" return cls(table_name, object_name, object_type) @@ -3996,16 +4008,20 @@ def from_dict(cls, field: dict): return cls(table_name, object_name, object_type) elif "SparklineData" in field: - m = field.get('SparklineData').get('Measure') + m = field.get("SparklineData").get("Measure") if "Measure" in m: measure = m["Measure"] - table_name = measure.get("Expression", {}).get("SourceRef", {}).get("Entity") + table_name = ( + measure.get("Expression", {}).get("SourceRef", {}).get("Entity") + ) object_name = measure.get("Property") object_type = "Measure" return cls(table_name, object_name, object_type) elif "Aggregation" in m: col = m.get("Aggregation", {}).get("Expression", {}).get("Column", {}) - table_name = col.get("Expression", {}).get("SourceRef", {}).get("Entity") + table_name = ( + col.get("Expression", {}).get("SourceRef", {}).get("Entity") + ) object_name = col.get("Property") object_type = "Column" return cls(table_name, object_name, object_type) From 66964a4c098200a75cedb9eaf8b3eca4377fda96 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 10 Jun 2025 08:54:49 +0300 Subject: [PATCH 7/7] rename fields --- src/sempy_labs/report/_reportwrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sempy_labs/report/_reportwrapper.py b/src/sempy_labs/report/_reportwrapper.py index bf81121c..69344f9c 100644 --- a/src/sempy_labs/report/_reportwrapper.py +++ b/src/sempy_labs/report/_reportwrapper.py @@ -3349,7 +3349,7 @@ def _update_to_theme_colors(self, mapping: dict[str, tuple[int, float]]): self.update(file_path=file_path, payload=payload) - def _rename_fields(self, mapping: dict): + def rename_fields(self, mapping: dict): """ Renames fields in the report definition based on the provided rename mapping.