From 9800ed535263bdf574082ae9ecaa9f4356164d96 Mon Sep 17 00:00:00 2001 From: Joe Chow HK Date: Sat, 26 Jul 2025 18:44:58 +0800 Subject: [PATCH 1/4] Add support for metadata in MDXView initialization and enhance alias retrieval --- TM1py/Objects/MDXView.py | 28 +++++++++++++++++++++++++++- TM1py/Services/ViewService.py | 2 +- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/TM1py/Objects/MDXView.py b/TM1py/Objects/MDXView.py index 7b60f99d..635f99d6 100644 --- a/TM1py/Objects/MDXView.py +++ b/TM1py/Objects/MDXView.py @@ -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]: + """ 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[^\]]+)\]\.\[(?P[^\]]+)\]') + 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): diff --git a/TM1py/Services/ViewService.py b/TM1py/Services/ViewService.py index 7647cb9b..fd8af07c 100644 --- a/TM1py/Services/ViewService.py +++ b/TM1py/Services/ViewService.py @@ -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) From b03cb57779b1d94a19c011a2c293d5108c0bd3b9 Mon Sep 17 00:00:00 2001 From: Joe Chow HK Date: Sat, 26 Jul 2025 18:47:11 +0800 Subject: [PATCH 2/4] Add support for Meta parameter in MDXView.from_dict method --- TM1py/Objects/MDXView.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TM1py/Objects/MDXView.py b/TM1py/Objects/MDXView.py index 635f99d6..c88257ad 100644 --- a/TM1py/Objects/MDXView.py +++ b/TM1py/Objects/MDXView.py @@ -104,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() From 1e4a8ce7f907c979fad39eb57a08e0f8a46d2037 Mon Sep 17 00:00:00 2001 From: Joe Chow HK Date: Sat, 26 Jul 2025 19:13:43 +0800 Subject: [PATCH 3/4] Add tests for retrieving metadata from MDXView response and enhance from_json method --- Tests/MDXView_test.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Tests/MDXView_test.py b/Tests/MDXView_test.py index 0aa43c42..778c3ada 100644 --- a/Tests/MDXView_test.py +++ b/Tests/MDXView_test.py @@ -1,6 +1,9 @@ import unittest from TM1py import MDXView +from unittest.mock import Mock, patch +from TM1py import ViewService +import json class TestMDXView(unittest.TestCase): @@ -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( @@ -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"}) From 9b5f2141b5c7289fd4db2acb49802652fed01937 Mon Sep 17 00:00:00 2001 From: Joe Chow HK Date: Sun, 31 Aug 2025 16:45:39 +0800 Subject: [PATCH 4/4] Fix parameter name from 'Meta' to 'meta' in MDXView initialization for consistency --- TM1py/Objects/MDXView.py | 4 ++-- TM1py/Services/ViewService.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TM1py/Objects/MDXView.py b/TM1py/Objects/MDXView.py index c88257ad..988fc890 100644 --- a/TM1py/Objects/MDXView.py +++ b/TM1py/Objects/MDXView.py @@ -15,10 +15,10 @@ 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, Meta: dict = {}): + 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', {}) + self._aliases = meta.get('Aliases', {}) @property def mdx(self): diff --git a/TM1py/Services/ViewService.py b/TM1py/Services/ViewService.py index fd8af07c..910a52fd 100644 --- a/TM1py/Services/ViewService.py +++ b/TM1py/Services/ViewService.py @@ -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"], Meta=view_as_dict.get("Meta", {})) + 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)