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 +21 -0
- pypbip/diagram/__init__.py +9 -0
- pypbip/diagram/diagram.py +68 -0
- pypbip/diagram/diagram_layout.py +115 -0
- pypbip/diagram/diagram_node.py +51 -0
- pypbip/json_util.py +24 -0
- pypbip/model_bim/__init__.py +17 -0
- pypbip/model_bim/column.py +104 -0
- pypbip/model_bim/dax_expression.py +122 -0
- pypbip/model_bim/measures.py +123 -0
- pypbip/model_bim/model_bim.py +55 -0
- pypbip/model_bim/semantic_model.py +287 -0
- pypbip/model_bim/table.py +150 -0
- pypbip/model_naming_util.py +20 -0
- pypbip/report/__init__.py +11 -0
- pypbip/report/report.py +122 -0
- pypbip/report/resource_package.py +30 -0
- pypbip/report/section.py +108 -0
- pypbip/report/visual_container.py +95 -0
- pypbip-0.1.0.dist-info/METADATA +60 -0
- pypbip-0.1.0.dist-info/RECORD +24 -0
- pypbip-0.1.0.dist-info/WHEEL +5 -0
- pypbip-0.1.0.dist-info/licenses/LICENSE +21 -0
- pypbip-0.1.0.dist-info/top_level.txt +1 -0
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,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()
|