pypbip 0.1.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.
pypbip/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ from .model_bim import ModelBim, SemanticModel, Table, DaxExpression, Column, Measure
2
+ from .report import Report, Section, VisualContainer, ResourcePackage
3
+ from .diagram import DiagramLayout, Diagram, DiagramNode
4
+
5
+ __all__ = [
6
+ "ModelBim",
7
+ "SemanticModel",
8
+ "Table",
9
+ "DaxExpression",
10
+ "Column",
11
+ "Measure",
12
+ "Report",
13
+ "Section",
14
+ "VisualContainer",
15
+ "ResourcePackage",
16
+ "DiagramLayout",
17
+ "Diagram",
18
+ "DiagramNode",
19
+ ]
20
+
21
+
@@ -0,0 +1,9 @@
1
+ from .diagram_layout import DiagramLayout
2
+ from .diagram import Diagram
3
+ from .diagram_node import DiagramNode
4
+
5
+ __all__ = [
6
+ "DiagramLayout",
7
+ "Diagram",
8
+ "DiagramNode",
9
+ ]
@@ -0,0 +1,68 @@
1
+ from typing import Dict, List
2
+
3
+ from .diagram_node import DiagramNode
4
+ from ..json_util import dump_json
5
+
6
+
7
+ class Diagram:
8
+ def __init__(
9
+ self,
10
+ name: str,
11
+ ordinal: int,
12
+ nodes: List[DiagramNode],
13
+ scroll_position: Dict[str, float],
14
+ zoom_value: float,
15
+ pin_key_fields_to_top: bool,
16
+ show_extra_header_info: bool,
17
+ hide_key_fields_when_collapsed: bool,
18
+ tables_locked: bool,
19
+ ):
20
+ self.name = name
21
+ self.ordinal = ordinal
22
+ self.nodes = nodes
23
+ self.scroll_position = scroll_position
24
+ self.zoom_value = zoom_value
25
+ self.pin_key_fields_to_top = pin_key_fields_to_top
26
+ self.show_extra_header_info = show_extra_header_info
27
+ self.hide_key_fields_when_collapsed = hide_key_fields_when_collapsed
28
+ self.tables_locked = tables_locked
29
+ self.node_list = [n.name for n in self.nodes]
30
+ self.summary = {
31
+ "nodes": self.node_list,
32
+ }
33
+
34
+ @classmethod
35
+ def from_dict(cls, data: dict) -> "Diagram":
36
+ return cls(
37
+ name=data.get("name"),
38
+ ordinal=data.get("ordinal"),
39
+ nodes=[DiagramNode.from_dict(n) for n in data.get("nodes")],
40
+ scroll_position=data.get("scrollPosition"),
41
+ zoom_value=data.get("zoomValue"),
42
+ pin_key_fields_to_top=data.get("pinKeyFieldsToTop"),
43
+ show_extra_header_info=data.get("showExtraHeaderInfo"),
44
+ hide_key_fields_when_collapsed=data.get("hideKeyFieldsWhenCollapsed"),
45
+ tables_locked=data.get("tablesLocked"),
46
+ )
47
+
48
+ def to_dict(self) -> dict:
49
+ return {
50
+ "ordinal": self.ordinal,
51
+ "scrollPosition": self.scroll_position,
52
+ "nodes": [n.to_dict() for n in self.nodes],
53
+ "name": self.name,
54
+ "zoomValue": self.zoom_value,
55
+ "pinKeyFieldsToTop": self.pin_key_fields_to_top,
56
+ "showExtraHeaderInfo": self.show_extra_header_info,
57
+ "hideKeyFieldsWhenCollapsed": self.hide_key_fields_when_collapsed,
58
+ "tablesLocked": self.tables_locked,
59
+ }
60
+
61
+ def to_json(self, indent: int = 2) -> str:
62
+ return dump_json(self.to_dict(), indent=indent)
63
+
64
+
65
+ if __name__ == "__main__":
66
+ diagram_dict = {"name": "ExampleDiagram", "ordinal": 0, "nodes": []}
67
+ diagram = Diagram.from_dict(diagram_dict)
68
+ print(diagram.to_json())
@@ -0,0 +1,115 @@
1
+ import contextlib
2
+ from ..json_util import read_json_file
3
+ from typing import List, Dict
4
+ from .diagram import Diagram
5
+ from .diagram_node import DiagramNode
6
+ from ..report import Section
7
+ from ..model_bim import ModelBim, Table
8
+
9
+
10
+ class DiagramLayout:
11
+ def __init__(
12
+ self,
13
+ version: str,
14
+ diagrams: List[Diagram],
15
+ selectedDiagram: str,
16
+ defaultDiagram: str,
17
+ ):
18
+ self.version = version
19
+ self.diagrams = diagrams
20
+ self.selectedDiagram = selectedDiagram
21
+ self.defaultDiagram = defaultDiagram
22
+
23
+ @classmethod
24
+ def from_dict(cls, data: dict) -> "DiagramLayout":
25
+ return cls(
26
+ version=data.get("version"),
27
+ diagrams=[Diagram.from_dict(d) for d in data.get("diagrams", [])],
28
+ selectedDiagram=data.get("selectedDiagram"),
29
+ defaultDiagram=data.get("defaultDiagram"),
30
+ )
31
+
32
+ def to_dict(self) -> dict:
33
+ return {
34
+ "version": self.version,
35
+ "diagrams": [d.to_dict() for d in self.diagrams],
36
+ "selectedDiagram": self.selectedDiagram,
37
+ "defaultDiagram": self.defaultDiagram,
38
+ }
39
+
40
+ @classmethod
41
+ def from_file(cls, file_path: str) -> "DiagramLayout":
42
+ data = read_json_file(file_path)
43
+ return cls.from_dict(data)
44
+
45
+ def create_nodes(self, tables: List[str], mapping: Dict[str, Table]) -> List[DiagramNode]:
46
+ nodes = []
47
+ width = 300
48
+ height = 300
49
+ for idx, table in enumerate(tables):
50
+ with contextlib.suppress(Exception):
51
+ node_dict = {
52
+ "location": {
53
+ "x": 0 + idx * (width + 30),
54
+ "y": 50 + 0 * height,
55
+ },
56
+ "nodeIndex": table,
57
+ "nodeLineageTag": mapping[table].lineageTag,
58
+ "size": {
59
+ "height": height,
60
+ "width": width
61
+ },
62
+ "zIndex": 0
63
+ }
64
+ node = DiagramNode.from_dict(node_dict)
65
+ nodes.append(node)
66
+ return nodes
67
+
68
+ def create_diagram(self, nodes: List[DiagramNode], name: str) -> Diagram:
69
+ diagram_dict = {
70
+ "ordinal": 99,
71
+ "scrollPosition": {
72
+ "x": 0,
73
+ "y": 0
74
+ },
75
+ "nodes": [node.to_dict() for node in nodes],
76
+ "name": f"@ {name}",
77
+ "zoomValue": 100,
78
+ "pinKeyFieldsToTop": True,
79
+ "showExtraHeaderInfo": False,
80
+ "hideKeyFieldsWhenCollapsed": False,
81
+ "tablesLocked": False
82
+ }
83
+ return Diagram.from_dict(diagram_dict)
84
+
85
+ def delete_existing_diagrams_with_section_names(self, section_names):
86
+ self.diagrams = [
87
+ diagram
88
+ for diagram in self.diagrams
89
+ if not diagram.name.startswith("@")
90
+ ]
91
+
92
+ def add_sections_diagrams(self, sections: List[Section], dataset: ModelBim):
93
+ section_names = []
94
+ sections_diagrams = []
95
+ for section in sections:
96
+ tables_from_fields = []
97
+ used_measures = [f.split(".")[-1] for f in section.used_fields if f.split(".")[-1] in dataset.model.measures.keys()]
98
+ for measure in used_measures:
99
+ field_used_tables = dataset.model.measures[measure].used_tables
100
+ tables_from_fields.extend(field_used_tables)
101
+ used_tables = section.used_tables + tables_from_fields
102
+ used_tables = list(set(used_tables))
103
+ nodes = self.create_nodes(used_tables, dataset.model.tables)
104
+ diagram = self.create_diagram(nodes, section.displayName)
105
+ sections_diagrams.append(diagram)
106
+ section_names.append(section.displayName)
107
+
108
+ self.delete_existing_diagrams_with_section_names(section_names)
109
+ self.diagrams.extend(sections_diagrams)
110
+
111
+
112
+ if __name__ == "__main__":
113
+ diagramLayout_path = "BaseDataSet.Dataset/diagramLayout.json"
114
+ dl = DiagramLayout.from_file(diagramLayout_path)
115
+ print(dl.to_dict())
@@ -0,0 +1,51 @@
1
+ from typing import Optional, Dict
2
+
3
+
4
+ class DiagramNode:
5
+ def __init__(
6
+ self,
7
+ name: str,
8
+ location: Dict[str, float],
9
+ node_lineage_tag: str,
10
+ size: Dict[str, float],
11
+ z_index: int,
12
+ expanded_height: Optional[float] = None,
13
+ ):
14
+ self.name = name
15
+ self.location = location
16
+ self.node_lineage_tag = node_lineage_tag
17
+ self.size = size
18
+ self.z_index = z_index
19
+ self.expanded_height = expanded_height
20
+
21
+ @classmethod
22
+ def from_dict(cls, data: dict) -> "DiagramNode":
23
+ return cls(
24
+ name=data.get("nodeIndex"),
25
+ location=data.get("location"),
26
+ node_lineage_tag=data.get("nodeLineageTag"),
27
+ size=data.get("size"),
28
+ z_index=data.get("zIndex"),
29
+ expanded_height=data.get("expandedHeight"),
30
+ )
31
+
32
+ def to_dict(self) -> dict:
33
+ base_dict = {
34
+ "location": self.location,
35
+ "nodeIndex": self.name,
36
+ "nodeLineageTag": self.node_lineage_tag,
37
+ "size": self.size,
38
+ "zIndex": self.z_index,
39
+ }
40
+ if self.expanded_height is not None:
41
+ base_dict["expandedHeight"] = self.expanded_height
42
+
43
+ for k in list(base_dict.keys()):
44
+ if base_dict[k] in (None, "", {}):
45
+ del base_dict[k]
46
+ return base_dict
47
+
48
+
49
+ if __name__ == "__main__":
50
+ node = DiagramNode.from_dict({"nodeIndex": "ExampleNode", "location": {"x": 0, "y": 0}, "nodeLineageTag": "example-tag", "size": {"height": 100, "width": 200}, "zIndex": 0})
51
+ print(node.to_json())
pypbip/json_util.py ADDED
@@ -0,0 +1,24 @@
1
+ import json
2
+
3
+
4
+ def write_json_file(filepath, content):
5
+ with open(filepath, "w", encoding="utf-8", newline="\n") as output_file:
6
+ json.dump(content, output_file, indent=2, ensure_ascii=False)
7
+
8
+
9
+ def read_json_file(file_path) -> dict:
10
+ with open(file_path, "r", encoding="utf-8") as f:
11
+ return json.load(f)
12
+
13
+
14
+ def dump_json(obj, indent=2) -> str:
15
+ return json.dumps(obj, indent=indent, ensure_ascii=False)
16
+
17
+
18
+ def dump_json_inline(obj) -> str:
19
+ return json.dumps(obj)
20
+
21
+
22
+ def parse_json(s: str):
23
+ return json.loads(s)
24
+
@@ -0,0 +1,17 @@
1
+ from .model_bim import ModelBim
2
+ from .semantic_model import SemanticModel
3
+ from .table import Table
4
+ from .dax_expression import DaxExpression
5
+ from .column import Column
6
+ from .measures import Measure
7
+
8
+ __all__ = [
9
+ "ModelBim",
10
+ "SemanticModel",
11
+ "Table",
12
+ "DaxExpression",
13
+ "Column",
14
+ "Measure",
15
+ ]
16
+
17
+
@@ -0,0 +1,104 @@
1
+ from ..json_util import read_json_file, dump_json
2
+ from .dax_expression import DaxExpression
3
+
4
+
5
+ class Column:
6
+ def __init__(
7
+ self,
8
+ sortByColumn,
9
+ expression,
10
+ isDataTypeInferred,
11
+ extendedProperties,
12
+ dataType,
13
+ description,
14
+ summarizeBy,
15
+ type,
16
+ isHidden,
17
+ name,
18
+ formatString,
19
+ changedProperties,
20
+ isNameInferred,
21
+ relatedColumnDetails,
22
+ sourceColumn,
23
+ lineageTag,
24
+ displayFolder,
25
+ annotations,
26
+ ):
27
+ self.sortByColumn = sortByColumn
28
+ self.expression = DaxExpression(expression)
29
+ self.isDataTypeInferred = isDataTypeInferred
30
+ self.extendedProperties = extendedProperties
31
+ self.dataType = dataType
32
+ self.description = description
33
+ self.summarizeBy = summarizeBy
34
+ self.type = type
35
+ self.isHidden = isHidden
36
+ self.name = name
37
+ self.formatString = formatString
38
+ self.changedProperties = changedProperties
39
+ self.isNameInferred = isNameInferred
40
+ self.relatedColumnDetails = relatedColumnDetails
41
+ self.sourceColumn = sourceColumn
42
+ self.lineageTag = lineageTag
43
+ self.displayFolder = displayFolder
44
+ self.annotations = annotations
45
+
46
+ @classmethod
47
+ def from_file(cls, file_path: str) -> "Column":
48
+ data: dict = read_json_file(file_path)
49
+ return Column.from_dict(data)
50
+
51
+ @classmethod
52
+ def from_dict(cls, data: dict) -> "Column":
53
+ return cls(
54
+ sortByColumn=data.get("sortByColumn"),
55
+ expression=data.get("expression"),
56
+ isDataTypeInferred=data.get("isDataTypeInferred"),
57
+ extendedProperties=data.get("extendedProperties"),
58
+ dataType=data.get("dataType"),
59
+ description=data.get("description"),
60
+ summarizeBy=data.get("summarizeBy"),
61
+ type=data.get("type"),
62
+ isHidden=data.get("isHidden"),
63
+ name=data.get("name"),
64
+ formatString=data.get("formatString"),
65
+ changedProperties=data.get("changedProperties"),
66
+ isNameInferred=data.get("isNameInferred"),
67
+ relatedColumnDetails=data.get("relatedColumnDetails"),
68
+ sourceColumn=data.get("sourceColumn"),
69
+ lineageTag=data.get("lineageTag"),
70
+ displayFolder=data.get("displayFolder"),
71
+ annotations=data.get("annotations"),
72
+ )
73
+
74
+ def to_dict(self) -> dict:
75
+ base_dict = {
76
+ "name": self.name,
77
+ "annotations": self.annotations,
78
+ "changedProperties": self.changedProperties,
79
+ "dataType": self.dataType,
80
+ "description": self.description,
81
+ "displayFolder": self.displayFolder,
82
+ "expression": self.expression.to_dict(),
83
+ "extendedProperties": self.extendedProperties,
84
+ "formatString": self.formatString,
85
+ "isDataTypeInferred": self.isDataTypeInferred,
86
+ "isHidden": self.isHidden,
87
+ "isNameInferred": self.isNameInferred,
88
+ "lineageTag": self.lineageTag,
89
+ "relatedColumnDetails": self.relatedColumnDetails,
90
+ "sortByColumn": self.sortByColumn,
91
+ "sourceColumn": self.sourceColumn,
92
+ "summarizeBy": self.summarizeBy,
93
+ "type": self.type,
94
+ }
95
+ for k in list(base_dict.keys()):
96
+ if base_dict[k] in (None, "", []):
97
+ del base_dict[k]
98
+ return base_dict
99
+
100
+ def to_json(self, indent: int = 2) -> str:
101
+ return dump_json(self.to_dict(), indent=indent)
102
+
103
+ def clean_expression(self):
104
+ self.expression.clean_expression()
@@ -0,0 +1,122 @@
1
+ from typing import List, Tuple
2
+ from re import Match
3
+ import re
4
+
5
+
6
+ class DaxExpression:
7
+ def __init__(
8
+ self,
9
+ expression: str | List[str],
10
+ ):
11
+ self.expression: List[str] = expression if isinstance(expression, list) else [expression]
12
+
13
+ def to_dict(self) -> str | List[str]:
14
+ return self.expression[0] if len(self.expression) == 1 else self.expression
15
+
16
+ def as_str(self) -> str:
17
+ return "\n".join([x for x in self.expression if x])
18
+
19
+ def __remove_block_comments(self) -> None:
20
+ self.expression = re.sub(r'/\*.*?\*/', '', self.as_str(), flags=re.DOTALL).split("\n")
21
+
22
+ def __remove_hifen_comments(self) -> None:
23
+ self.expression = re.sub(r'--.*$', '', self.as_str(), flags=re.MULTILINE).split("\n")
24
+
25
+ def __remove_slash_comments(self) -> None:
26
+ pattern = r"""
27
+ ("(?:\\.|[^"\\])*" # string entre aspas duplas (pode ter escapes)
28
+ |'(?:\\.|[^'\\])*' # ou string entre aspas simples
29
+ ) # <-- grupo 1: strings
30
+ | # ou
31
+ (//.*?$) # <-- grupo 2: comentarios de linha
32
+ """
33
+ self.expression = re.sub(pattern, lambda m: m.group(1) or '', self.as_str(), flags=re.MULTILINE | re.VERBOSE).split("\n")
34
+
35
+ def remove_comments(self) -> None:
36
+ self.__remove_block_comments()
37
+ self.__remove_hifen_comments()
38
+ self.__remove_slash_comments()
39
+
40
+ def __remove_slash_r(self) -> None:
41
+ self.expression = [line.rstrip('\r') for line in self.expression]
42
+
43
+ def __replace_tab(self) -> None:
44
+ self.expression = [line.replace("\t", " ") for line in self.expression]
45
+
46
+ def clean_expression(self) -> None:
47
+ if self.expression == [None]:
48
+ return
49
+ self.__remove_slash_r()
50
+ self.__replace_tab()
51
+
52
+ @staticmethod
53
+ def get_inner_calculate_boundates(measure: str, match: Match[str]) -> Tuple[int, int]:
54
+ inner_calculate_start_index = match.start(1)
55
+ inner_calculate_open_parenthesis = measure.find("(", match.end(1))
56
+ return inner_calculate_start_index, inner_calculate_open_parenthesis
57
+
58
+ @staticmethod
59
+ def check_if_nested_calculates_exist(measure: str) -> Tuple[bool, int, int]:
60
+ upper_measure = measure.upper()
61
+ pattern = re.compile(r"\bCALCULATE\s*\(\s*(CALCULATE)\s*\(", re.IGNORECASE)
62
+ match = pattern.search(upper_measure)
63
+ inner_calculate_start_index = None
64
+ inner_calculate_open_parenthesis = None
65
+ exists = False
66
+ if match:
67
+ exists = True
68
+ inner_calculate_start_index, inner_calculate_open_parenthesis = DaxExpression.get_inner_calculate_boundates(upper_measure, match)
69
+ return exists, inner_calculate_start_index, inner_calculate_open_parenthesis
70
+
71
+ @staticmethod
72
+ def check_if_inner_calculate_is_first_argument(measure: str, inner_calculate_close_parenthesis: int) -> bool:
73
+ after_close_parenthesis_content = measure[inner_calculate_close_parenthesis + 1:]
74
+ pattern = re.compile(r"^[ \t]*,", re.MULTILINE)
75
+ match = pattern.match(after_close_parenthesis_content)
76
+ return bool(match)
77
+
78
+ @staticmethod
79
+ def find_closing_parenthesis(text: str, open_index: int) -> int:
80
+ level = 0
81
+ for i in range(open_index - 1, len(text)):
82
+ if text[i] == "(":
83
+ level += 1
84
+ elif text[i] == ")":
85
+ level -= 1
86
+ if level == 0:
87
+ return i
88
+ raise RuntimeError("Measure with parenthesis erros")
89
+
90
+ @staticmethod
91
+ def remove_inner_calculate(measure: str) -> str:
92
+ one_line_measure = " ".join([m.strip() for m in measure.split("\n")])
93
+
94
+ has_nested_calculate_exist, inner_calculate_start_index, inner_calculate_open_parenthesis = DaxExpression.check_if_nested_calculates_exist(one_line_measure)
95
+ if not has_nested_calculate_exist:
96
+ return measure
97
+
98
+ inner_calculate_close_parenthesis = DaxExpression.find_closing_parenthesis(one_line_measure, inner_calculate_open_parenthesis)
99
+ inner_calculate_boundaries = {
100
+ "inner_calculate_start_index": inner_calculate_start_index,
101
+ "inner_calculate_open_parenthesis": inner_calculate_open_parenthesis,
102
+ "inner_calculate_close_parenthesis": inner_calculate_close_parenthesis,
103
+ }
104
+
105
+ is_inner_calculate_the_first_argument = DaxExpression.check_if_inner_calculate_is_first_argument(one_line_measure, inner_calculate_boundaries["inner_calculate_close_parenthesis"])
106
+ if not is_inner_calculate_the_first_argument:
107
+ return measure
108
+
109
+ inner_calculate_start_index = inner_calculate_boundaries["inner_calculate_start_index"]
110
+ inner_calculate_open_parenthesis = inner_calculate_boundaries["inner_calculate_open_parenthesis"]
111
+ inner_calculate_close_parenthesis = inner_calculate_boundaries["inner_calculate_close_parenthesis"]
112
+
113
+ content_before_inner_calculate = one_line_measure[:inner_calculate_start_index]
114
+ inner_calculate_arguments = one_line_measure[inner_calculate_open_parenthesis + 1:inner_calculate_close_parenthesis]
115
+ content_after_inner_calculate = one_line_measure[inner_calculate_close_parenthesis + 1:]
116
+ improved_measure = f"{content_before_inner_calculate}{inner_calculate_arguments}{content_after_inner_calculate}"
117
+ return DaxExpression.remove_inner_calculate(improved_measure)
118
+
119
+ def simplify_expression(self):
120
+ self.expression = DaxExpression.remove_inner_calculate(self.as_str()).split("\n")
121
+
122
+
@@ -0,0 +1,123 @@
1
+ import re
2
+ from ..json_util import read_json_file, dump_json
3
+ from .dax_expression import DaxExpression
4
+ from typing import Callable, Optional, List
5
+
6
+
7
+ class Measure:
8
+ def __init__(
9
+ self,
10
+ name: str,
11
+ expression: str | List[str],
12
+ lineage_tag: str,
13
+ annotations: Optional[List[dict]] = None,
14
+ description: str | List[str] | None = None,
15
+ display_folder: Optional[str] = None,
16
+ format_string: Optional[str] = None,
17
+ ):
18
+ self.name = name
19
+ self.expression = DaxExpression(expression)
20
+ self.lineage_tag = lineage_tag
21
+ self.annotations = annotations
22
+ self.description = DaxExpression(description) if description else DaxExpression(expression)
23
+ self.display_folder = display_folder
24
+ self.format_string = format_string
25
+ self.used_tables: List[str] = self.get_used_tables()
26
+
27
+ def get_used_tables(self) -> list:
28
+ description = self.description.as_str()
29
+ if not description:
30
+ return []
31
+
32
+ tables = re.findall(r"[\'\"]?([A-Za-z0-9_]+)[\'\"]?\s*\[", description)
33
+
34
+ functions = re.findall(r"(?i)\b([A-Z_][A-Z0-9_]*)\s*\(", description)
35
+ for function in list(dict.fromkeys(functions)):
36
+ description = description.replace(function, "")
37
+
38
+ columns_or_measures = re.findall(r"(\[.*?\])", description)
39
+ for c_o_m in list(dict.fromkeys(columns_or_measures)):
40
+ description = description.replace(c_o_m, "")
41
+
42
+ matches = re.findall(r"\(([^()]*)\)", description)
43
+ cleaned = [re.sub(r"['\"\s]", "", m) for m in matches if m.strip()]
44
+ tables += cleaned
45
+ return list(set(tables))
46
+
47
+ @classmethod
48
+ def from_file(cls, file_path: str) -> "Measure":
49
+ data: dict = read_json_file(file_path)
50
+ return Measure.from_dict(data)
51
+
52
+ @classmethod
53
+ def from_dict(cls, data: dict) -> "Measure":
54
+ return cls(
55
+ name=data.get("name"),
56
+ annotations=data.get("annotations"),
57
+ description=data.get("description"),
58
+ display_folder=data.get("displayFolder"),
59
+ expression=data.get("expression"),
60
+ format_string=data.get("formatString"),
61
+ lineage_tag=data.get("lineageTag"),
62
+ )
63
+
64
+ def to_dict(self) -> dict:
65
+ base_dict = {
66
+ "name": self.name,
67
+ "annotations": self.annotations,
68
+ "description": self.description.to_dict(),
69
+ "displayFolder": self.display_folder,
70
+ "expression": self.expression.to_dict(),
71
+ "formatString": self.format_string,
72
+ "lineageTag": self.lineage_tag,
73
+ }
74
+ for k in list(base_dict.keys()):
75
+ if base_dict[k] in (None, "", []):
76
+ del base_dict[k]
77
+ return base_dict
78
+
79
+ def to_json(self, indent: int = 2) -> str:
80
+ return dump_json(self.to_dict(), indent=indent)
81
+
82
+ def clean_expression(self):
83
+ self.expression.clean_expression()
84
+
85
+ def _sanitize(self) -> DaxExpression:
86
+ sanitized = DaxExpression(self.expression.expression)
87
+ sanitized.clean_expression()
88
+ sanitized.remove_comments()
89
+ return sanitized
90
+
91
+ def _apply_expanded(
92
+ self,
93
+ expanded_expression: str,
94
+ dax_formatter: Callable[[str], str] | None = None,
95
+ ) -> None:
96
+ desc = DaxExpression(expanded_expression)
97
+ desc.simplify_expression()
98
+ if dax_formatter:
99
+ formatted = dax_formatter(desc.as_str().replace("\n", " ")).split("\n")
100
+ desc = DaxExpression(formatted)
101
+ self.description = desc
102
+
103
+ def _is_original(self) -> bool:
104
+ expr = "".join(self.expression.expression).replace(" ", "").upper()
105
+ desc = "".join(self.description.expression).replace(" ", "").upper()
106
+ return expr == desc
107
+
108
+ def _augment_with_original(self) -> None:
109
+ augmented = self.expression.expression + [""] + self.description.expression
110
+ self.description = DaxExpression(augmented)
111
+
112
+ def expand_description(
113
+ self,
114
+ expand_expression: Callable[[str], str],
115
+ dax_formatter: Callable[[str], str] | None = None,
116
+ ) -> None:
117
+ sanitized = self._sanitize()
118
+ expanded = expand_expression(sanitized.as_str())
119
+ self._apply_expanded(expanded, dax_formatter)
120
+ if self._is_original():
121
+ self.description = DaxExpression(self.expression.expression)
122
+ return
123
+ self._augment_with_original()