class HierarchyService(ObjectService): """ Service to handle Object Updates for TM1 Hierarchies """ # Tuple with TM1 Versions where Edges need to be created through TI, due to bug: # https://www.ibm.com/developerworks/community/forums/html/topic?id=75f2b99e-6961-4c71-9364-1d5e1e083eff EDGES_WORKAROUND_VERSIONS = ('11.0.002', '11.0.003', '11.1.000') def __init__(self, rest): super().__init__(rest) self.subsets = SubsetService(rest) self.elements = ElementService(rest) def create(self, hierarchy): """ Create a hierarchy in an existing dimension :param hierarchy: :return: """ request = '/api/v1/Dimensions(\'{}\')/Hierarchies'.format( hierarchy.dimension_name) response = self._rest.POST(request, hierarchy.body) return response def get(self, dimension_name, hierarchy_name): """ get hierarchy :param dimension_name: name of the dimension :param hierarchy_name: name of the hierarchy :return: """ request = "/api/v1/Dimensions('{}')/Hierarchies('{}')?$expand=" \ "Edges,Elements,ElementAttributes,Subsets,DefaultMember" \ .format(dimension_name, hierarchy_name) response = self._rest.GET(request, '') return Hierarchy.from_dict(response.json()) def get_all_names(self, dimension_name): """ get all names of existing Hierarchies in a dimension :param dimension_name: :return: """ request = "/api/v1/Dimensions('{}')/Hierarchies?$select=Name".format( dimension_name) response = self._rest.GET(request, '') return [hierarchy["Name"] for hierarchy in response.json()["value"]] def update(self, hierarchy): """ update a hierarchy. It's a two step process: 1. Update Hierarchy 2. Update Element-Attributes Function caters for Bug with Edge Creation: https://www.ibm.com/developerworks/community/forums/html/topic?id=75f2b99e-6961-4c71-9364-1d5e1e083eff :param hierarchy: instance of TM1py.Hierarchy :return: list of responses """ # functions returns multiple responses responses = list() # 1. Update Hierarchy request = '/api/v1/Dimensions(\'{}\')/Hierarchies(\'{}\')'.format( hierarchy.dimension_name, hierarchy.name) # Workaround EDGES: Handle Issue, that Edges cant be created in one batch with the Hierarchy in certain versions hierarchy_body = hierarchy.body_as_dict if self.version[0:8] in self.EDGES_WORKAROUND_VERSIONS: del hierarchy_body["Edges"] responses.append(self._rest.PATCH(request, json.dumps(hierarchy_body))) # 2. Update Attributes responses.append(self._update_element_attributes(hierarchy=hierarchy)) # Workaround EDGES if self.version[0:8] in self.EDGES_WORKAROUND_VERSIONS: from TM1py.Services import ProcessService process_service = ProcessService(self._rest) ti_function = "HierarchyElementComponentAdd('{}', '{}', '{}', '{}', {});" ti_statements = [ ti_function.format(hierarchy.dimension_name, hierarchy.name, edge[0], edge[1], hierarchy.edges[(edge[0], edge[1])]) for edge in hierarchy.edges ] responses.append( process_service.execute_ti_code(lines_prolog=ti_statements)) return responses def exists(self, dimension_name, hierarchy_name): """ :param dimension_name: :param hierarchy_name: :return: """ request = '/api/v1/Dimensions(\'{}\')/Hierarchies(\'{}\')'.format( dimension_name, hierarchy_name) return self._exists(request) def delete(self, dimension_name, hierarchy_name): request = '/api/v1/Dimensions(\'{}\')/Hierarchies(\'{}\')'.format( dimension_name, hierarchy_name) return self._rest.DELETE(request) def get_hierarchy_summary(self, dimension_name, hierarchy_name): hierarchy_properties = ("Elements", "Edges", "ElementAttributes", "Members", "Levels") request = "/api/v1/Dimensions(\'{}\')/Hierarchies(\'{}\')?$expand=Edges/$count,Elements/$count," \ "ElementAttributes/$count,Members/$count,Levels/$count&$select=Cardinality" \ .format(dimension_name, hierarchy_name) hierary_summary_raw = self._rest.GET(request).json() return { hierarchy_property: hierary_summary_raw[hierarchy_property + "@odata.count"] for hierarchy_property in hierarchy_properties } def _update_element_attributes(self, hierarchy): """ Update the elementattributes of a hierarchy :param hierarchy: Instance of TM1py.Hierarchy :return: """ # get existing attributes first. element_attributes = self.elements.get_element_attributes( dimension_name=hierarchy.dimension_name, hierarchy_name=hierarchy.name) element_attribute_names = [ea.name for ea in element_attributes] # write ElementAttributes that don't already exist ! for element_attribute in hierarchy.element_attributes: if element_attribute not in element_attribute_names: self.elements.create_element_attribute( dimension_name=hierarchy.dimension_name, hierarchy_name=hierarchy.name, element_attribute=element_attribute) # delete attributes that are determined to be removed for element_attribute in element_attribute_names: if element_attribute not in hierarchy.element_attributes: self.elements.delete_element_attribute( dimension_name=hierarchy.dimension_name, hierarchy_name=hierarchy.name, element_attribute=element_attribute) def get_default_member(self, dimension_name, hierarchy_name=None): """ Get the defined default_member for a Hierarchy. Will return the element with index 1, if default member is not specified explicitly in }HierarchyProperty Cube :param dimension_name: :param hierarchy_name: :return: String, name of Member """ request = "/api/v1/Dimensions('{dimension}')/Hierarchies('{hierarchy}')/DefaultMember/Name/$value".format( dimension=dimension_name, hierarchy=hierarchy_name if hierarchy_name else dimension_name) response = self._rest.GET(request=request) return response.text def update_default_member(self, dimension_name, hierarchy_name=None, member_name=""): """ Update the default member of a hierarchy. Currently implemented through TI, since TM1 API does not supports default member updates yet. :param dimension_name: :param hierarchy_name: :param member_name: :return: """ from TM1py import ProcessService, CellService if hierarchy_name and not case_and_space_insensitive_equals( dimension_name, hierarchy_name): dimension = "{}:{}".format(dimension_name, hierarchy_name) else: dimension = dimension_name cells = {(dimension, 'hierarchy0', 'defaultMember'): member_name} CellService(self._rest).write_values( cube_name="}HierarchyProperties", cellset_as_dict=cells, dimensions=('}Dimensions', '}Hierarchies', '}HierarchyProperties')) return ProcessService(self._rest).execute_ti_code( lines_prolog="RefreshMdxHierarchy('{}');".format(dimension_name)) def remove_all_edges(self, dimension_name, hierarchy_name=None): if not hierarchy_name: hierarchy_name = dimension_name request = "/api/v1/Dimensions('{}')/Hierarchies('{}')".format( dimension_name, hierarchy_name) body = {"Edges": []} return self._rest.PATCH(request=request, data=json.dumps(body))
class HierarchyService(ObjectService): """ Service to handle Object Updates for TM1 Hierarchies """ # Tuple with TM1 Versions where Edges need to be created through TI, due to bug: # https://www.ibm.com/developerworks/community/forums/html/topic?id=75f2b99e-6961-4c71-9364-1d5e1e083eff EDGES_WORKAROUND_VERSIONS = ('11.0.002', '11.0.003', '11.1.000') def __init__(self, rest): super().__init__(rest) self.subsets = SubsetService(rest) self.elements = ElementService(rest) def create(self, hierarchy): """ Create a hierarchy in an existing dimension :param hierarchy: :return: """ request = '/api/v1/Dimensions(\'{}\')/Hierarchies'.format(hierarchy.dimension_name) response = self._rest.POST(request, hierarchy.body) return response def get(self, dimension_name, hierarchy_name): """ get hierarchy :param dimension_name: name of the dimension :param hierarchy_name: name of the hierarchy :return: """ request = "/api/v1/Dimensions('{}')/Hierarchies('{}')?$expand=" \ "Edges,Elements,ElementAttributes,Subsets,DefaultMember"\ .format(dimension_name, hierarchy_name) response = self._rest.GET(request, '') return Hierarchy.from_dict(response.json()) def update(self, hierarchy): """ update a hierarchy. It's a two step process: 1. Update Hierarchy 2. Update Element-Attributes Function caters for Bug with Edge Creation: https://www.ibm.com/developerworks/community/forums/html/topic?id=75f2b99e-6961-4c71-9364-1d5e1e083eff :param hierarchy: instance of TM1py.Hierarchy :return: list of responses """ # functions returns multiple responses responses = list() # 1. Update Hierarchy request = '/api/v1/Dimensions(\'{}\')/Hierarchies(\'{}\')'.format(hierarchy.dimension_name, hierarchy.name) # Workaround EDGES: Handle Issue, that Edges cant be created in one batch with the Hierarchy in certain versions hierarchy_body = hierarchy.body_as_dict if self.version[0:8] in self.EDGES_WORKAROUND_VERSIONS: del hierarchy_body["Edges"] responses.append(self._rest.PATCH(request, json.dumps(hierarchy_body))) # 2. Update Attributes responses.append(self._update_element_attributes(hierarchy=hierarchy)) # Workaround EDGES if self.version[0:8] in self.EDGES_WORKAROUND_VERSIONS: from TM1py.Services import ProcessService process_service = ProcessService(self._rest) ti_function = "HierarchyElementComponentAdd('{}', '{}', '{}', '{}', {});" ti_statements = [ti_function.format(hierarchy.dimension_name, hierarchy.name, edge[0], edge[1], hierarchy.edges[(edge[0], edge[1])]) for edge in hierarchy.edges] responses.append(process_service.execute_ti_code(lines_prolog=ti_statements)) return responses def exists(self, dimension_name, hierarchy_name): """ :param dimension_name: :param hierarchy_name: :return: """ request = '/api/v1/Dimensions(\'{}\')/Hierarchies(\'{}\')'.format(dimension_name, hierarchy_name) return self._exists(request) def delete(self, dimension_name, hierarchy_name): request = '/api/v1/Dimensions(\'{}\')/Hierarchies(\'{}\')'.format(dimension_name, hierarchy_name) return self._rest.DELETE(request) def get_hierarchy_summary(self, dimension_name, hierarchy_name): hierarchy_properties = ("Elements", "Edges", "ElementAttributes", "Members", "Levels") request = "/api/v1/Dimensions(\'{}\')/Hierarchies(\'{}\')?$expand=Edges/$count,Elements/$count," \ "ElementAttributes/$count,Members/$count,Levels/$count&$select=Cardinality"\ .format(dimension_name, hierarchy_name) hierary_summary_raw = self._rest.GET(request).json() return {hierarchy_property: hierary_summary_raw[hierarchy_property + "@odata.count"] for hierarchy_property in hierarchy_properties} def _update_element_attributes(self, hierarchy): """ Update the elementattributes of a hierarchy :param hierarchy: Instance of TM1py.Hierarchy :return: """ # get existing attributes first. element_attributes = self.elements.get_element_attributes(dimension_name=hierarchy.dimension_name, hierarchy_name=hierarchy.name) element_attribute_names = [ea.name for ea in element_attributes] # write ElementAttributes that don't already exist ! for element_attribute in hierarchy.element_attributes: if element_attribute not in element_attribute_names: self.elements.create_element_attribute(dimension_name=hierarchy.dimension_name, hierarchy_name=hierarchy.name, element_attribute=element_attribute) # delete attributes that are determined to be removed for element_attribute in element_attribute_names: if element_attribute not in hierarchy.element_attributes: self.elements.delete_element_attribute(dimension_name=hierarchy.dimension_name, hierarchy_name=hierarchy.name, element_attribute=element_attribute)
class HierarchyService(ObjectService): """ Service to handle Object Updates for TM1 Hierarchies """ # Tuple with TM1 Versions where Edges need to be created through TI, due to bug: # https://www.ibm.com/developerworks/community/forums/html/topic?id=75f2b99e-6961-4c71-9364-1d5e1e083eff EDGES_WORKAROUND_VERSIONS = ('11.0.002', '11.0.003', '11.1.000') def __init__(self, rest: RestService): super().__init__(rest) self.subsets = SubsetService(rest) self.elements = ElementService(rest) def create(self, hierarchy: Hierarchy, **kwargs): """ Create a hierarchy in an existing dimension :param hierarchy: :return: """ url = format_url("/api/v1/Dimensions('{}')/Hierarchies", hierarchy.dimension_name) response = self._rest.POST(url, hierarchy.body, **kwargs) return response def get(self, dimension_name: str, hierarchy_name: str, **kwargs): """ get hierarchy :param dimension_name: name of the dimension :param hierarchy_name: name of the hierarchy :return: """ url = format_url( "/api/v1/Dimensions('{}')/Hierarchies('{}')?$expand=Edges,Elements,ElementAttributes,Subsets,DefaultMember", dimension_name, hierarchy_name) response = self._rest.GET(url, **kwargs) return Hierarchy.from_dict(response.json()) def get_all_names(self, dimension_name: str, **kwargs): """ get all names of existing Hierarchies in a dimension :param dimension_name: :return: """ url = format_url("/api/v1/Dimensions('{}')/Hierarchies?$select=Name", dimension_name) response = self._rest.GET(url, **kwargs) return [hierarchy["Name"] for hierarchy in response.json()["value"]] def update(self, hierarchy: Hierarchy, **kwargs) -> List[Response]: """ update a hierarchy. It's a two step process: 1. Update Hierarchy 2. Update Element-Attributes Function caters for Bug with Edge Creation: https://www.ibm.com/developerworks/community/forums/html/topic?id=75f2b99e-6961-4c71-9364-1d5e1e083eff :param hierarchy: instance of TM1py.Hierarchy :return: list of responses """ # functions returns multiple responses responses = list() # 1. Update Hierarchy url = format_url("/api/v1/Dimensions('{}')/Hierarchies('{}')", hierarchy.dimension_name, hierarchy.name) # Workaround EDGES: Handle Issue, that Edges cant be created in one batch with the Hierarchy in certain versions hierarchy_body = hierarchy.body_as_dict if self.version[0:8] in self.EDGES_WORKAROUND_VERSIONS: del hierarchy_body["Edges"] responses.append( self._rest.PATCH(url, json.dumps(hierarchy_body), **kwargs)) # 2. Update Attributes responses.append( self.update_element_attributes(hierarchy=hierarchy, **kwargs)) # Workaround EDGES if self.version[0:8] in self.EDGES_WORKAROUND_VERSIONS: from TM1py.Services import ProcessService process_service = ProcessService(self._rest) ti_function = "HierarchyElementComponentAdd('{}', '{}', '{}', '{}', {});" ti_statements = [ ti_function.format(hierarchy.dimension_name, hierarchy.name, edge[0], edge[1], hierarchy.edges[(edge[0], edge[1])]) for edge in hierarchy.edges ] responses.append( process_service.execute_ti_code(lines_prolog=ti_statements, **kwargs)) return responses def exists(self, dimension_name: str, hierarchy_name: str, **kwargs) -> bool: """ :param dimension_name: :param hierarchy_name: :return: """ url = format_url("/api/v1/Dimensions('{}')/Hierarchies('{}')", dimension_name, hierarchy_name) return self._exists(url, **kwargs) def delete(self, dimension_name: str, hierarchy_name: str, **kwargs) -> Response: url = format_url("/api/v1/Dimensions('{}')/Hierarchies('{}')", dimension_name, hierarchy_name) return self._rest.DELETE(url, **kwargs) def get_hierarchy_summary(self, dimension_name: str, hierarchy_name: str, **kwargs) -> Dict[str, int]: hierarchy_properties = ("Elements", "Edges", "ElementAttributes", "Members", "Levels") url = format_url( "/api/v1/Dimensions('{}')/Hierarchies('{}')?$expand=Edges/$count,Elements/$count," "ElementAttributes/$count,Members/$count,Levels/$count&$select=Cardinality", dimension_name, hierarchy_name) hierary_summary_raw = self._rest.GET(url, **kwargs).json() return { hierarchy_property: hierary_summary_raw[hierarchy_property + "@odata.count"] for hierarchy_property in hierarchy_properties } def update_element_attributes(self, hierarchy: Hierarchy, **kwargs): """ Update the elementattributes of a hierarchy :param hierarchy: Instance of TM1py.Hierarchy :return: """ # get existing attributes first element_attributes = self.elements.get_element_attributes( dimension_name=hierarchy.dimension_name, hierarchy_name=hierarchy.name, **kwargs) element_attribute_names = [ea.name for ea in element_attributes] # write ElementAttributes that don't already exist for element_attribute in hierarchy.element_attributes: if element_attribute not in element_attribute_names: self.elements.create_element_attribute( dimension_name=hierarchy.dimension_name, hierarchy_name=hierarchy.name, element_attribute=element_attribute, **kwargs) # delete attributes that are determined to be removed for element_attribute in element_attribute_names: if element_attribute not in hierarchy.element_attributes: self.elements.delete_element_attribute( dimension_name=hierarchy.dimension_name, hierarchy_name=hierarchy.name, element_attribute=element_attribute, **kwargs) def get_default_member(self, dimension_name: str, hierarchy_name: str = None, **kwargs) -> Optional[str]: """ Get the defined default_member for a Hierarchy. Will return the element with index 1, if default member is not specified explicitly in }HierarchyProperty Cube :param dimension_name: :param hierarchy_name: :return: String, name of Member """ url = format_url( "/api/v1/Dimensions('{dimension}')/Hierarchies('{hierarchy}')/DefaultMember", dimension=dimension_name, hierarchy=hierarchy_name if hierarchy_name else dimension_name) response = self._rest.GET(url=url, **kwargs) if not response.text: return None return response.json()["Name"] def update_default_member(self, dimension_name: str, hierarchy_name: str = None, member_name: str = "", **kwargs) -> Response: """ Update the default member of a hierarchy. Currently implemented through TI, since TM1 API does not supports default member updates yet. :param dimension_name: :param hierarchy_name: :param member_name: :return: """ from TM1py import ProcessService, CellService if hierarchy_name and not case_and_space_insensitive_equals( dimension_name, hierarchy_name): dimension = "{}:{}".format(dimension_name, hierarchy_name) else: dimension = dimension_name cells = {(dimension, 'hierarchy0', 'defaultMember'): member_name} CellService(self._rest).write_values( cube_name="}HierarchyProperties", cellset_as_dict=cells, dimensions=('}Dimensions', '}Hierarchies', '}HierarchyProperties'), **kwargs) return ProcessService(self._rest).execute_ti_code( lines_prolog=format_url("RefreshMdxHierarchy('{}');", dimension_name), **kwargs) def remove_all_edges(self, dimension_name: str, hierarchy_name: str = None, **kwargs) -> Response: if not hierarchy_name: hierarchy_name = dimension_name url = format_url("/api/v1/Dimensions('{}')/Hierarchies('{}')", dimension_name, hierarchy_name) body = {"Edges": []} return self._rest.PATCH(url=url, data=json.dumps(body), **kwargs) def remove_edges_under_consolidation(self, dimension_name: str, hierarchy_name: str, consolidation_element: str, **kwargs) -> List[Response]: """ :param dimension_name: Name of the dimension :param hierarchy_name: Name of the hierarchy :param consolidation_element: Name of the Consolidated element :return: response """ hierarchy = self.get(dimension_name, hierarchy_name) from TM1py.Services import ElementService element_service = ElementService(self._rest) elements_under_consolidations = element_service.get_members_under_consolidation( dimension_name, hierarchy_name, consolidation_element) elements_under_consolidations.append(consolidation_element) remove_edges = [] for (parent, component) in hierarchy.edges: if parent in elements_under_consolidations and component in elements_under_consolidations: remove_edges.append((parent, component)) hierarchy.remove_edges(remove_edges) return self.update(hierarchy, **kwargs) def add_edges(self, dimension_name: str, hierarchy_name: str = None, edges: Dict[Tuple[str, str], int] = None, **kwargs) -> Response: """ Add Edges to hierarchy. Fails if any edge already exists. :param dimension_name: :param hierarchy_name: :param edges: :return: """ if not hierarchy_name: hierarchy_name = dimension_name url = format_url("/api/v1/Dimensions('{}')/Hierarchies('{}')/Edges", dimension_name, hierarchy_name) body = [{ "ParentName": parent, "ComponentName": component, "Weight": float(weight) } for (parent, component), weight in edges.items()] return self._rest.POST(url=url, data=json.dumps(body), **kwargs) def is_balanced(self, dimension_name: str, hierarchy_name: str, **kwargs): """ Check if hierarchy is balanced :param dimension_name: :param hierarchy_name: :return: """ url = format_url( "/api/v1/Dimensions('{}')/Hierarchies('{}')/Structure/$value", dimension_name, hierarchy_name) structure = int(self._rest.GET(url, **kwargs).text) # 0 = balanced, 2 = unbalanced if structure == 0: return True elif structure == 2: return False else: raise RuntimeError( f"Unexpected return value from TM1 API request: {str(structure)}" )
class HierarchyService(ObjectService): """ Service to handle Object Updates for TM1 Hierarchies """ def __init__(self, rest): super().__init__(rest) self.elements = ElementService(rest) def create(self, hierarchy): """ Create a hierarchy in an existing dimension :param hierarchy: :return: """ raise NotImplementedError('not supported') def get(self, dimension_name, hierarchy_name): """ get hierarchy :param dimension_name: name of the dimension :param hierarchy_name: name of the hierarchy :return: """ request = '/api/v1/Dimensions(\'{}\')/Hierarchies(\'{}\')'.format( dimension_name, hierarchy_name) response = self._rest.GET(request, '') return response def update(self, hierarchy): """ update a hierarchy. Is a two step process. 1. Update Hierarchy 2. Update Element-Attributes :param hierarchy: instance of TM1py.Hierarchy :return: """ # Update Hierarchy request = '/api/v1/Dimensions(\'{}\')/Hierarchies(\'{}\')'.format( hierarchy.dimension_name, hierarchy.name) response = self._rest.PATCH(request, hierarchy.body) # Update Attributes self._update_element_attributes(hierarchy=hierarchy) return response def exists(self, dimension_name, hierarchy_name): """ :param dimension_name: :param hierarchy_name: :return: """ request = '/api/v1/Dimensions(\'{}\')/Hierarchies(\'{}\')'.format( dimension_name, hierarchy_name) return super(HierarchyService, self).exists(request) def delete(self, dimension_name, hierarchy_name): request = '/api/v1/Dimensions(\'{}\')/Hierarchies(\'{}\')'.format( dimension_name, hierarchy_name) return self._rest.DELETE(request) def _update_element_attributes(self, hierarchy): """ Update the element sattributes of a hierarchy :param hierarchy: Instance of TM1py.Hierarchy :return: """ # get existing attributes first. element_attributes = self.elements.get_element_attributes( dimension_name=hierarchy.dimension_name, hierarchy_name=hierarchy.name) element_attribute_names = [ea.name for ea in element_attributes] # write ElementAttributes that don't already exist ! for element_attribute in hierarchy.element_attributes: if element_attribute not in element_attribute_names: self.elements.create_element_attribute( dimension_name=hierarchy.dimension_name, hierarchy_name=hierarchy.name, element_attribute=element_attribute) # delete attributes that are determined to be removed for element_attribute in element_attribute_names: if element_attribute not in hierarchy.element_attributes: self.elements.delete_element_attribute( dimension_name=hierarchy.dimension_name, hierarchy_name=hierarchy.name, element_attribute=element_attribute)