Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions TM1py/Objects/MDXView.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,39 @@ class MDXView(View):
IMPORTANT. MDXViews can't be seen through the old TM1 clients (Archict, Perspectives). They do exist though!
"""

def __init__(self, cube_name: str, view_name: str, MDX: str):
def __init__(self, cube_name: str, view_name: str, MDX: str, meta: dict = {}):
View.__init__(self, cube_name, view_name)
self._mdx = MDX
self._aliases = meta.get('Aliases', {})

@property
def mdx(self):
return self._mdx

@property
def aliases(self) -> dict[str, str]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call this alias_by_dimension and then we need a separate one for alias_by_hierarchy

""" Returns a dictionary with aliases for dimensions in the MDX view
self._aliases = {
'[Account].[Account]': 'Description',
'[Cost Center].[Cost Center]': 'Full Name',
}
return:
{
'Account': 'Description',
'Cost Center': 'Full Name'
}
"""
dimension_hierarchy_pattern = re.compile(r'\[(?P<dimension>[^\]]+)\]\.\[(?P<hierarchy>[^\]]+)\]')
alias_pool = {}
for dimension_hierarchy_tuple, alias in self._aliases.items():
pattern_matches = dimension_hierarchy_pattern.search(dimension_hierarchy_tuple)
if not pattern_matches:
continue
dimension = pattern_matches.group('dimension')
hierarchy = pattern_matches.group('hierarchy')
alias_pool[dimension] = alias

return alias_pool

@mdx.setter
def mdx(self, value: str):
Expand Down Expand Up @@ -78,7 +104,9 @@ def from_json(cls, view_as_json: str, cube_name: Optional[str] = None) -> 'MDXVi
def from_dict(cls, view_as_dict: Dict, cube_name: str = None) -> 'MDXView':
return cls(cube_name=view_as_dict['Cube']['Name'] if not cube_name else cube_name,
view_name=view_as_dict['Name'],
MDX=view_as_dict['MDX'])
MDX=view_as_dict['MDX'],
Meta=view_as_dict.get('Meta', {})
)

def construct_body(self) -> str:
mdx_view_as_dict = collections.OrderedDict()
Expand Down
2 changes: 1 addition & 1 deletion TM1py/Services/ViewService.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def get(self, cube_name: str, view_name: str, private: bool = False, **kwargs) -
response = self._rest.GET(url, **kwargs)
view_as_dict = response.json()
if "MDX" in view_as_dict:
return MDXView(cube_name=cube_name, view_name=view_name, MDX=view_as_dict["MDX"])
return MDXView(cube_name=cube_name, view_name=view_name, MDX=view_as_dict["MDX"], meta=view_as_dict.get("Meta", {}))
else:
return self.get_native_view(cube_name=cube_name, view_name=view_name, private=private)

Expand Down
22 changes: 22 additions & 0 deletions Tests/MDXView_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import unittest

from TM1py import MDXView
from unittest.mock import Mock, patch
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In TM1py, we don't use mocks for tests.
Instead, we define test cases that do real API interactions with TM1. It comes at a price, that running the test suite takes time and you need a real TM1 installation to run tests.

But the benefit of using real API interaction is that we can use the same test suite against different versions of TM1 (e.g. TM1 11 local, PAoC, PAaaS).

from TM1py import ViewService
import json


class TestMDXView(unittest.TestCase):
Expand All @@ -13,6 +16,8 @@ class TestMDXView(unittest.TestCase):
FROM [c1]
WHERE ([d3].[d3].[e1], [d4].[e1], [d5].[h1].[e1])
"""

rest_response = '''{"@odata.context":"../$metadata#Cubes(\'Cube\')/Views/ibm.tm1.api.v1.MDXView(Cube,LocalizedAttributes)/$entity","@odata.type":"#ibm.tm1.api.v1.MDXView","Name":"MDX View","Attributes":{"Caption":"MDX View"},"MDX":"SELECT {[Dim B].[Dim B].Members} PROPERTIES [Dim B].[Dim B].[Description B] ON COLUMNS , {[Dim A].[Dim A].Members} PROPERTIES [Dim A].[Dim A].[Description] ON ROWS FROM [Cube] ","Cube":{"Name":"Cube","Rules":null,"DrillthroughRules":null,"LastSchemaUpdate":"2025-07-26T10:53:18.870Z","LastDataUpdate":"2025-07-26T10:53:18.870Z","Attributes":{"Caption":"Cube"}},"LocalizedAttributes":[],"FormatString":"0.#########"\n,"Meta":{"Aliases":{"[Dim A].[Dim A]":"Description","[Dim B].[Dim B]":"Description B"},"ContextSets":{},"ExpandAboves":{}}\n}'''

def setUp(self) -> None:
self.view = MDXView(
Expand Down Expand Up @@ -68,3 +73,20 @@ def test_substitute_title_value_error(self):
with self.assertRaises(ValueError) as error:
self.view.substitute_title(dimension="d6", hierarchy="d6", element="e2")
print(error)

@patch('TM1py.Services.RestService.RestService')
def test_get_with_retrieving_meta_from_response(self, mock_rest):
mock_response_dict = json.loads(self.rest_response)
mock_response = Mock()
mock_response.json.return_value = mock_response_dict
mock_rest.GET.return_value = mock_response

service = ViewService(mock_rest)
view = service.get('Cube', 'MDX View', private=False)
self.assertIsInstance(view, MDXView)
self.assertDictEqual(view.aliases, {"Dim A": "Description", "Dim B": "Description B"})

def test_from_json_with_Meta_key(self):
view = MDXView.from_json(view_as_json=self.rest_response, cube_name='Cube')
self.assertIsInstance(view, MDXView)
self.assertDictEqual(view.aliases, {"Dim A": "Description", "Dim B": "Description B"})