├── .gitignore ├── config.ini ├── requirements.txt ├── constants.py ├── cubecalc.py ├── README.md ├── Images ├── logo.svg └── logo_built_with_TM1Py.svg ├── setup_sample.py ├── utils.py ├── methods.py └── Tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv/** 2 | .idea/** 3 | CubeCalc.log 4 | /.venv/ 5 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [tm1srv01] 2 | base_url=https://localhost:12354 3 | user=admin 4 | password=YXBwbGU= 5 | decode_b64=True -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.21.0 2 | numpy-financial>=1.0.0 3 | pandas>=0.24.2 4 | git+https://github.com/cubewise-code/tm1py.git 5 | pytz>=2018.9 6 | click>=7.0 7 | python-dateutil~=2.8.0 8 | scipy>=1.2.1 -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from pathlib import Path 4 | from TM1py.Utils import CaseAndSpaceInsensitiveDict 5 | import methods 6 | 7 | METHODS = CaseAndSpaceInsensitiveDict({ 8 | "IRR": methods.irr, 9 | "NPV": methods.npv, 10 | "STDEV": methods.stdev, 11 | "STDEV_P": methods.stdev_p, 12 | "FV": methods.fv, 13 | "FV_SCHEDULE": methods.fv_schedule, 14 | "PV": methods.pv, 15 | "XNPV": methods.xnpv, 16 | "PMT": methods.pmt, 17 | "PPMT": methods.ppmt, 18 | "MIRR": methods.mirr, 19 | "XIRR": methods.xirr, 20 | "NPER": methods.nper, 21 | "RATE": methods.rate, 22 | "EFFECT": methods.effect, 23 | "NOMINAL": methods.nominal, 24 | "SLN": methods.sln, 25 | "MEAN": methods.mean, 26 | "SEM": methods.sem, 27 | "MEDIAN": methods.median, 28 | "MODE": methods.mode, 29 | "VAR": methods.var, 30 | "KURT": methods.kurt, 31 | "SKEW": methods.skew, 32 | "RNG": methods.rng, 33 | "MIN": methods.min_, 34 | "MAX": methods.max_, 35 | "SUM": methods.sum_, 36 | "COUNT": methods.count 37 | }) 38 | 39 | APP_NAME = "CubeCalc" 40 | # Determine current working directory for logging and result_file 41 | try: 42 | wd = sys._MEIPASS 43 | base_path = os.path.dirname(sys.executable) 44 | LOGFILE = os.path.join(base_path, APP_NAME + ".log") 45 | CONFIG = os.path.join(base_path, "config.ini") 46 | except AttributeError: 47 | LOGFILE = Path(__file__).parent.joinpath(APP_NAME + ".log") 48 | CONFIG = Path(__file__).parent.joinpath("config.ini") -------------------------------------------------------------------------------- /cubecalc.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import sys 4 | 5 | import click 6 | 7 | from constants import APP_NAME 8 | from utils import CubeCalc, exit_cubecalc, configure_logging 9 | 10 | 11 | @click.command( 12 | context_settings=dict( 13 | ignore_unknown_options=True, 14 | allow_extra_args=True)) 15 | @click.pass_context 16 | def main(click_arguments): 17 | """ Needs > 7 arguments arguments: 18 | method, 19 | tm1_source, 20 | tm1_target, 21 | cube_source, 22 | cube_target, 23 | view_source, 24 | view_target, 25 | dimension, 26 | subset 27 | 28 | """ 29 | parameters = {click_arguments.args[arg][2:]: click_arguments.args[arg + 1] 30 | for arg 31 | in range(0, len(click_arguments.args), 2)} 32 | method_name = parameters.pop('method') 33 | logging.info("{app_name} starts. Parameters: {parameters}.".format( 34 | app_name=APP_NAME, 35 | parameters=parameters)) 36 | # start timer 37 | start = datetime.datetime.now() 38 | # setup connections 39 | calculator = CubeCalc() 40 | # execute method 41 | success = calculator.execute(method=method_name, parameters=parameters) 42 | # exit 43 | exit_cubecalc(success=success, elapsed_time=datetime.datetime.now() - start) 44 | 45 | 46 | if __name__ == "__main__": 47 | try: 48 | configure_logging() 49 | main() 50 | except Exception as exception: 51 | sys.exit("Aborting {app_name}. Error: {error}".format(app_name=APP_NAME, error=str(exception))) 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/MariusWirtz/CubeCalc/blob/master/Images/logo.svg) 2 | 3 | Python command line tool to perform typical financial and statistical calculations in TM1: 4 | 5 | - COUNT 6 | - EFFECT 7 | - FV 8 | - FV_SCHEDULE 9 | - IRR 10 | - KURT 11 | - MAX 12 | - MEAN 13 | - MEDIAN 14 | - MIN 15 | - MIRR 16 | - MODE 17 | - NOMINAL 18 | - NPER 19 | - NPV 20 | - PMT 21 | - PPMT 22 | - PV 23 | - RATE 24 | - RNG 25 | - SEM 26 | - SKEW 27 | - SLN 28 | - STDEV 29 | - STDEV_P 30 | - SUM 31 | - VAR 32 | - XIRR 33 | - XNPV 34 | 35 | # Usage 36 | 37 | cubecalc offers two execution modes: 38 | 39 | > 1. Single Mode 40 | 41 | If no `dimension`, `hierarchy` and `subset` arguments are passed, cubecalc will execute the calculation for a single 42 | view. 43 | 44 | > 2. Batch Mode 45 | 46 | If `dimension`, `hierarchy` and `subset` arguments are passed, cubecalc will run the calculation for each element in the 47 | subset. The dynamic dimension (e.g., projects) **must** be placed in the titles! Effectively the source and target view 48 | is updated before every calculation. When no subset is passed, cubecalc will run the calculation for all leaf elements. 49 | When no hierarchy is passed cubecalc assumes the same named hierarchy. 50 | 51 | > Examples 52 | 53 | Execute the script like this: 54 | 55 | `C:\python\python.exe cubecalc.py ` 56 | 57 | and pass arguments like this: 58 | 59 | ``` 60 | --method "IRR" --tm1_source "tm1srv01" --tm1_target "tm1srv01" --cube_source "Py Project Planning" 61 | --cube_target "Py Project Summary" --view_source "Project1" --view_target "Project1 IRR" --dimension "Project" 62 | --hierarchy "Project --subset "All Projects" 63 | ``` 64 | 65 | ``` 66 | --method "NPV" --tm1_source "tm1srv01" --tm1_target "tm1srv01" --cube_source "Py Project Planning" 67 | --cube_target "Py Project Summary" --view_source "Project1" --view_target "Project1 NPV" --dimension "Project" 68 | --hierarchy "Project --subset "All Projects" --rate 0.1 69 | ``` 70 | 71 | ``` 72 | --method "STDEV" --tm1_source "tm1srv01" --tm1_target "tm1srv01" --cube_source "Py Project Planning" 73 | --cube_target "Py Project Summary" --view_source "Project1" --view_target "Project1 STDEV" --dimension "Project" 74 | --hierarchy "Project --subset "All Projects" 75 | ``` 76 | 77 | ``` 78 | --method "FV" --tm1_source "tm1srv01" --tm1_target "tm1srv01" --cube_source "Py Project Planning" 79 | --tm1_target "tm1srv01" --cube_target "Py Project Summary" --view_target "Project1 FV" --dimension "Project" 80 | --hierarchy "Project --subset "All Projects" --rate 0.1 --nper 3 --pmt 1 --pv -100 81 | ``` 82 | 83 | ``` 84 | --method "PMT" --tm1_source "tm1srv01" --tm1_target "tm1srv01" --cube_source "Py Project Planning" 85 | --tm1_target "tm1srv01" --cube_target "Py Project Summary" --view_target "Project1 PMT" --dimension "Project" 86 | --hierarchy "Project --subset "All Projects" --rate 0.1 --nper 3 --pv 1000 87 | ``` 88 | 89 | ``` 90 | --method "PV" --tm1_source "tm1srv01" --tm1_target "tm1srv01" --cube_source "Py Project Planning" 91 | --tm1_target "tm1srv01" --cube_target "Py Project Summary" --view_target "Project1 PV" --dimension "Project" 92 | --hierarchy "Project --subset "All Projects" --rate 0.1 --nper 3 --pmt 1 --fv -100 --when 0 93 | ``` 94 | 95 | ``` 96 | --method "MIRR" --tm1_source "tm1srv01" --tm1_target "tm1srv01" --cube_source "Py Project Planning" 97 | --cube_target "Py Project Summary" --view_source "Project1" --view_target "Project1 MIRR" --dimension "Project" 98 | --hierarchy "Project --subset "All Projects" --finance_rate 0.12 --reinvest_rate 0.1 99 | ``` 100 | 101 | All arguments have the same names as in the Excel functions (except: `type` is called `when` in CubeCalc since `type` is 102 | a reserved word in python) 103 | 104 | The `dimension`, `hierarchy` and `subset` argument are optional (see section on run modes) 105 | 106 | # Requirements 107 | 108 | - [TM1py](https://github.com/cubewise-code/TM1py) 109 | - [numpy-financial](https://github.com/numpy/numpy-financial) 110 | - [scipy](https://github.com/scipy/scipy) 111 | - [click](https://github.com/pallets/click/) 112 | 113 | # Getting Started Guide 114 | 115 | For more information about how to use CubeCalc, just follow 116 | the [Getting Started Guide](https://code.cubewise.com/tm1py-help-content/getting-started-with-cubecalc). 117 | 118 | # Installation 119 | 120 | Just download the repository 121 | 122 | # Configuration 123 | 124 | Adjust the `config.ini` to match your TM1 environment: 125 | 126 | ``` 127 | [tm1srv01] 128 | base_url=https://localhost:12354 129 | user=admin 130 | password=YXBwbGU= 131 | decode_b64=True 132 | ``` 133 | 134 | # Samples 135 | 136 | - Adjust the `config.ini` file to match your setup 137 | - Execute the `setup sample.py` file 138 | - Run `cubecalc.py` with appropriate arguments from the commandline or from TI 139 | 140 | # Contribution 141 | 142 | CubeCalc is an open source project. It thrives on contribution from the TM1 community. If you find a bug or want to add 143 | more functions to this repository, just: 144 | 145 | - Fork the repository 146 | - Add the new function to the methods.py file + Add some tests for your function in the Tests.py file 147 | - Create a MR and we will merge in the changes 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /Images/logo.svg: -------------------------------------------------------------------------------- 1 | Final logos -------------------------------------------------------------------------------- /setup_sample.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | 3 | from TM1py import TM1Service, Dimension, Hierarchy, Element, Cube, NativeView, AnonymousSubset 4 | 5 | CONFIG = "config.ini" 6 | 7 | config = configparser.ConfigParser() 8 | config.read(CONFIG) 9 | 10 | with TM1Service(**config['tm1srv01']) as tm1: 11 | # create dimensions 12 | 13 | dimension = Dimension( 14 | name="Py Project") 15 | hierarchy = Hierarchy( 16 | name="Py Project", 17 | dimension_name="Py Project", 18 | elements=[ 19 | Element("Project1", "Numeric"), 20 | Element("Project2", "Numeric"), 21 | Element("Project3", "Numeric")]) 22 | dimension.add_hierarchy(hierarchy) 23 | if not tm1.dimensions.exists(dimension.name): 24 | tm1.dimensions.create(dimension) 25 | 26 | dimension = Dimension( 27 | name="Py Quarter") 28 | hierarchy = Hierarchy( 29 | name="Py Quarter", 30 | dimension_name="Py Quarter", 31 | elements=[ 32 | Element("2018-Q1", "Numeric"), 33 | Element("2018-Q2", "Numeric"), 34 | Element("2018-Q3", "Numeric"), 35 | Element("2018-Q4", "Numeric"), 36 | Element("2019-Q1", "Numeric"), 37 | Element("2019-Q2", "Numeric"), 38 | Element("2019-Q3", "Numeric"), 39 | Element("2019-Q4", "Numeric"), 40 | Element("2020-Q1", "Numeric"), 41 | Element("2020-Q2", "Numeric"), 42 | Element("2020-Q3", "Numeric"), 43 | Element("2020-Q4", "Numeric")]) 44 | dimension.add_hierarchy(hierarchy) 45 | if not tm1.dimensions.exists(dimension.name): 46 | tm1.dimensions.create(dimension) 47 | 48 | dimension = Dimension( 49 | name="Py Project Planning Measure") 50 | hierarchy = Hierarchy( 51 | name="Py Project Planning Measure", 52 | dimension_name="Py Project Planning Measure", 53 | elements=[Element("Cashflow", "Numeric")]) 54 | dimension.add_hierarchy(hierarchy) 55 | tm1.dimensions.update_or_create(dimension) 56 | 57 | dimension = Dimension( 58 | name="Py Project Summary") 59 | hierarchy = Hierarchy( 60 | name="Py Project Summary", 61 | dimension_name="Py Project Summary", 62 | elements=[Element("IRR", "Numeric"), Element("NPV", "Numeric")]) 63 | dimension.add_hierarchy(hierarchy) 64 | tm1.dimensions.update_or_create(dimension) 65 | 66 | dimension = Dimension( 67 | name="Py Project Summary Measure") 68 | hierarchy = Hierarchy( 69 | name="Py Project Summary Measure", 70 | dimension_name="Py Project Summary Measure", 71 | elements=[Element("Value", "Numeric")]) 72 | dimension.add_hierarchy(hierarchy) 73 | tm1.dimensions.update_or_create(dimension) 74 | 75 | # create cube 1 76 | cube = Cube( 77 | name="Py Project Planning", dimensions=["Py Project", "Py Quarter", "Py Project Planning Measure"]) 78 | if tm1.cubes.exists(cube.name): 79 | tm1.cubes.delete(cube.name) 80 | tm1.cubes.create(cube) 81 | 82 | # create cube 2 83 | cube = Cube( 84 | name="Py Project Summary", dimensions=["Py Project", "Py Project Summary", "Py Project Summary Measure"]) 85 | if tm1.cubes.exists(cube.name): 86 | tm1.cubes.delete(cube.name) 87 | tm1.cubes.create(cube) 88 | 89 | # create views 90 | for project in ("Project1", "Project2", "Project3"): 91 | # Project Summary 92 | cube_name = "Py Project Summary" 93 | view = NativeView( 94 | cube_name=cube_name, 95 | view_name=project + " NPV", 96 | format_string="0.#########", 97 | suppress_empty_columns=False, 98 | suppress_empty_rows=False) 99 | view.add_row( 100 | dimension_name="Py Project Summary", 101 | subset=AnonymousSubset( 102 | dimension_name="Py Project Summary", 103 | elements=["NPV"])) 104 | view.add_title( 105 | dimension_name="Py Project", 106 | subset=AnonymousSubset( 107 | dimension_name="Py Project", 108 | elements=[project]), 109 | selection=project) 110 | view.add_column( 111 | dimension_name="Py Project Summary Measure", 112 | subset=AnonymousSubset( 113 | dimension_name="Py Project Summary Measure", 114 | elements=["Value"])) 115 | tm1.views.update_or_create(view) 116 | 117 | view = NativeView( 118 | cube_name=cube_name, 119 | view_name=project + " IRR", 120 | format_string="0.#########", 121 | suppress_empty_columns=False, 122 | suppress_empty_rows=False) 123 | view.add_row( 124 | dimension_name="Py Project Summary", 125 | subset=AnonymousSubset( 126 | dimension_name="Py Project Summary", 127 | elements=["IRR"])) 128 | view.add_title( 129 | dimension_name="Py Project", 130 | subset=AnonymousSubset( 131 | dimension_name="Py Project", 132 | elements=[project]), 133 | selection=project) 134 | view.add_column( 135 | dimension_name="Py Project Summary Measure", 136 | subset=AnonymousSubset( 137 | dimension_name="Py Project Summary Measure", 138 | elements=["Value"])) 139 | tm1.views.update_or_create(view) 140 | 141 | # Project Planning 142 | cube_name = "Py Project Planning" 143 | view = NativeView( 144 | cube_name=cube_name, 145 | view_name=project, 146 | format_string="0.#########", 147 | suppress_empty_columns=False, 148 | suppress_empty_rows=False) 149 | view.add_row( 150 | dimension_name="Py Quarter", 151 | subset=AnonymousSubset( 152 | dimension_name="Py Quarter", 153 | expression="{Tm1SubsetAll([Py Quarter])}")) 154 | view.add_title( 155 | dimension_name="Py Project", 156 | selection=project, 157 | subset=AnonymousSubset( 158 | dimension_name="Py Project", 159 | elements=[project])) 160 | view.add_column( 161 | dimension_name="Py Project Planning Measure", 162 | subset=AnonymousSubset( 163 | dimension_name="Py Project Planning Measure", 164 | elements=["Cashflow"]) 165 | ) 166 | tm1.views.update_or_create(view) 167 | 168 | # write to cube 1 169 | cellset = { 170 | ('Project1', '2018-Q1', 'Cashflow'): -100000, 171 | ('Project1', '2018-Q2', 'Cashflow'): 10000, 172 | ('Project1', '2018-Q3', 'Cashflow'): 10000, 173 | ('Project1', '2018-Q4', 'Cashflow'): 10000, 174 | ('Project1', '2019-Q1', 'Cashflow'): 10000, 175 | ('Project1', '2019-Q2', 'Cashflow'): 10000, 176 | ('Project1', '2019-Q3', 'Cashflow'): 10000, 177 | ('Project1', '2019-Q4', 'Cashflow'): 10000, 178 | ('Project1', '2020-Q1', 'Cashflow'): 10000, 179 | ('Project1', '2020-Q2', 'Cashflow'): 10000, 180 | ('Project1', '2020-Q3', 'Cashflow'): 10000, 181 | ('Project1', '2020-Q4', 'Cashflow'): 10000, 182 | ('Project2', '2018-Q1', 'Cashflow'): -100000, 183 | ('Project2', '2018-Q2', 'Cashflow'): 8000, 184 | ('Project2', '2018-Q3', 'Cashflow'): 8000, 185 | ('Project2', '2018-Q4', 'Cashflow'): 8000, 186 | ('Project2', '2019-Q1', 'Cashflow'): 8000, 187 | ('Project2', '2019-Q2', 'Cashflow'): 11000, 188 | ('Project2', '2019-Q3', 'Cashflow'): 11000, 189 | ('Project2', '2019-Q4', 'Cashflow'): 11000, 190 | ('Project2', '2020-Q1', 'Cashflow'): 12000, 191 | ('Project2', '2020-Q2', 'Cashflow'): 12000, 192 | ('Project2', '2020-Q3', 'Cashflow'): 13000, 193 | ('Project2', '2020-Q4', 'Cashflow'): 13000, 194 | ('Project3', '2018-Q1', 'Cashflow'): -90000, 195 | ('Project3', '2018-Q2', 'Cashflow'): 8000, 196 | ('Project3', '2018-Q3', 'Cashflow'): 8000, 197 | ('Project3', '2018-Q4', 'Cashflow'): 8000, 198 | ('Project3', '2019-Q1', 'Cashflow'): 8000, 199 | ('Project3', '2019-Q2', 'Cashflow'): 8000, 200 | ('Project3', '2019-Q3', 'Cashflow'): 8000, 201 | ('Project3', '2019-Q4', 'Cashflow'): 8000, 202 | ('Project3', '2020-Q1', 'Cashflow'): 8000, 203 | ('Project3', '2020-Q2', 'Cashflow'): 8000, 204 | ('Project3', '2020-Q3', 'Cashflow'): 8000, 205 | ('Project3', '2020-Q4', 'Cashflow'): 8000 206 | } 207 | tm1.cubes.cells.write_values( 208 | cube_name="Py Project Planning", 209 | cellset_as_dict=cellset 210 | ) 211 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import logging 3 | import os 4 | import re 5 | import sys 6 | from base64 import b64decode 7 | from typing import Dict 8 | 9 | from TM1py import TM1Service, AnonymousSubset, MDXView 10 | from TM1py.Utils import case_and_space_insensitive_equals 11 | 12 | from constants import LOGFILE, APP_NAME, CONFIG, METHODS 13 | 14 | def configure_logging(): 15 | logging.basicConfig( 16 | filename=LOGFILE, 17 | format="%(asctime)s - " + APP_NAME + " - %(levelname)s - %(message)s", 18 | level=logging.INFO, 19 | ) 20 | # also log to stdout 21 | logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) 22 | 23 | class CubeCalc: 24 | 25 | def __init__(self): 26 | self.tm1_services: Dict[str, TM1Service] = dict() 27 | self.setup() 28 | 29 | def setup(self): 30 | """ Fill Dictionary with TM1ServerName (as in config.ini) : Instantiated TM1Service 31 | 32 | :return: Dictionary server_names and TM1py.TM1Service instances pairs 33 | """ 34 | if not os.path.isfile(CONFIG): 35 | raise ValueError("{config} does not exist.".format(config=CONFIG)) 36 | config = configparser.ConfigParser() 37 | config.read(CONFIG) 38 | # build tm1_services dictionary 39 | for tm1_server_name, params in config.items(): 40 | # handle default values from configparser 41 | if tm1_server_name != config.default_section: 42 | try: 43 | self.tm1_services[tm1_server_name] = TM1Service(**params, session_context=APP_NAME) 44 | # Instance not running, Firewall or wrong connection parameters 45 | except Exception as e: 46 | logging.error("TM1 instance {} not accessible. Error: {}".format(tm1_server_name, str(e))) 47 | 48 | def logout(self): 49 | """ logout from all instances 50 | :return: 51 | """ 52 | for tm1 in self.tm1_services.values(): 53 | tm1.logout() 54 | 55 | def execute(self, method, parameters): 56 | """ 57 | 58 | :param method: 59 | :param parameters: 60 | :return: 61 | """ 62 | try: 63 | # single mode 64 | if "dimension" not in parameters: 65 | logging.info("Running in single mode") 66 | result = METHODS[method](**parameters, tm1_services=self.tm1_services) 67 | logging.info(f"Successfully calculated {method} with result: {result} from parameters: {parameters}") 68 | return True 69 | 70 | # iterative mode 71 | self.execute_iterative_mode(method, parameters) 72 | logging.info(f"Successfully calculated {method} in iterative mode with parameters: {parameters}") 73 | return True 74 | 75 | except Exception as ex: 76 | message = "Failed calculating {method} with parameters {parameters}. Error: {error}".format( 77 | method=method, 78 | parameters=parameters, 79 | error=str(ex)) 80 | logging.exception(message) 81 | return False 82 | finally: 83 | self.logout() 84 | 85 | def execute_iterative_mode(self, method, parameters): 86 | dimension = parameters.get("dimension") 87 | hierarchy = parameters.get("hierarchy", dimension) 88 | 89 | tm1_source_name = parameters['tm1_source'] 90 | tm1_target_name = parameters['tm1_target'] 91 | 92 | tm1_source: TM1Service = self.tm1_services[tm1_source_name] 93 | tm1_target: TM1Service = self.tm1_services[tm1_target_name] 94 | 95 | cube_source = parameters.get("cube_source") 96 | view_source = parameters.get("view_source") 97 | 98 | cube_target = parameters.get("cube_target") 99 | view_target = parameters.get("view_target") 100 | 101 | if "subset" in parameters: 102 | subset = parameters.pop("subset") 103 | element_names = tm1_source.subsets.get_element_names( 104 | dimension, 105 | hierarchy, 106 | subset, 107 | private=False) 108 | 109 | else: 110 | element_names = tm1_source.elements.get_leaf_element_names( 111 | dimension_name=dimension, 112 | hierarchy_name=hierarchy) 113 | 114 | # only pass tidy in run for last element 115 | if "tidy" in parameters: 116 | tidy = parameters.pop("tidy") 117 | else: 118 | tidy = False 119 | 120 | if not tidy: 121 | original_view_source = tm1_source.views.get( 122 | cube_name=cube_source, 123 | view_name=view_source, 124 | private=False) 125 | original_view_target = tm1_target.views.get( 126 | cube_name=cube_target, 127 | view_name=view_target, 128 | private=False) 129 | 130 | for element in element_names: 131 | self.alter_view(tm1_source=tm1_source_name, tm1_target=tm1_target_name, cube_source=cube_source, 132 | view_source=view_source, cube_target=cube_target, view_target=view_target, 133 | dimension=dimension, hierarchy=hierarchy, element=element) 134 | result = METHODS[method]( 135 | **parameters, 136 | tm1_services=self.tm1_services, 137 | tidy=tidy if element == element_names[-1] else False) 138 | logging.info(f"Successfully calculated {method} with result: {result} for title element '{element}'") 139 | 140 | # restore original source_view, target_view 141 | if not tidy: 142 | tm1_source.views.update_or_create(original_view_source, False) 143 | tm1_target.views.update_or_create(original_view_target, False) 144 | 145 | def substitute_mdx_view_title(self, view, dimension, hierarchy, element): 146 | pattern = re.compile(r"\[" + dimension + r"\].\[" + hierarchy + r"\].\[(.*?)\]", re.IGNORECASE) 147 | findings = re.findall(pattern, view.mdx) 148 | 149 | if findings: 150 | view.mdx = re.sub( 151 | pattern=pattern, 152 | repl=f"[{dimension}].[{hierarchy}].[{element}]", 153 | string=view.mdx) 154 | return 155 | 156 | if hierarchy is None or case_and_space_insensitive_equals(dimension, hierarchy): 157 | pattern = re.compile(r"\[" + dimension + r"\].\[(.*?)\]", re.IGNORECASE) 158 | findings = re.findall(pattern, view.mdx) 159 | if findings: 160 | view.mdx = re.sub( 161 | pattern=pattern, 162 | repl=f"[{dimension}].[{element}]", 163 | string=view.mdx) 164 | return 165 | 166 | raise ValueError(f"No selection in title with dimension: '{dimension}' and hierarchy: '{hierarchy}'") 167 | 168 | def substitute_native_view_title(self, view, dimension, element): 169 | for title in view.titles: 170 | if case_and_space_insensitive_equals(title.dimension_name, dimension): 171 | title._subset = AnonymousSubset(dimension, dimension, elements=[element]) 172 | title._selected = element 173 | return 174 | 175 | raise ValueError(f"Dimension '{dimension}' not found in titles") 176 | 177 | def alter_view(self, tm1_source: str, tm1_target: str, cube_source: str, view_source: str, cube_target: str, 178 | view_target: str, dimension: str, hierarchy: str, element: str): 179 | 180 | for tm1_instance_name, cube_name, view_name in zip( 181 | [tm1_source, tm1_target], 182 | [cube_source, cube_target], 183 | [view_source, view_target]): 184 | tm1 = self.tm1_services[tm1_instance_name] 185 | view = tm1.views.get(cube_name, view_name, private=False) 186 | 187 | if isinstance(view, MDXView): 188 | dimension = tm1.dimensions.determine_actual_object_name("Dimension", dimension) 189 | hierarchy = tm1.hierarchies.determine_actual_object_name("Hierarchy", hierarchy) 190 | self.substitute_mdx_view_title(view, dimension, hierarchy, element) 191 | 192 | else: 193 | self.substitute_native_view_title(view, dimension, element) 194 | 195 | tm1.views.update(view, private=False) 196 | 197 | 198 | def exit_cubecalc(success, elapsed_time): 199 | message = "{app_name} {ends}. Duration: {elapsed_time}.".format( 200 | app_name=APP_NAME, 201 | ends="aborted" if not success else "ends", 202 | elapsed_time=str(elapsed_time)) 203 | if success: 204 | logging.info(message) 205 | else: 206 | logging.error(message) 207 | sys.exit(message) 208 | 209 | 210 | def set_current_directory(): 211 | abspath = os.path.abspath(__file__) 212 | directory = os.path.dirname(abspath) 213 | # set current directory 214 | os.chdir(directory) 215 | return directory 216 | 217 | 218 | def decrypt_password(encrypted_password): 219 | """ b64 decoding 220 | 221 | :param encrypted_password: encrypted password with b64 222 | :return: password in plain text 223 | """ 224 | return b64decode(encrypted_password).decode("UTF-8") 225 | -------------------------------------------------------------------------------- /methods.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import re 3 | import statistics 4 | from datetime import date, datetime 5 | 6 | import numpy_financial as npf 7 | import numpy as np 8 | from dateutil import parser 9 | from scipy import optimize, stats 10 | import calendar 11 | 12 | 13 | 14 | from datetime import date 15 | from datetime import datetime 16 | from dateutil import parser 17 | import calendar 18 | 19 | 20 | def generate_dates_from_rows(rows): 21 | """ 22 | Converts row elements into datetime.date objects. 23 | 24 | Supports: 25 | - Standard date strings (for example '2024-03-31') 26 | - Quarter formats (for example '2024-Q1' or '2024Q1') 27 | -> mapped to the last day of the quarter: 28 | Q1 -> 31 Mar, Q2 -> 30 Jun, Q3 -> 30 Sep, Q4 -> 31 Dec 29 | - YearMonth formats (for example '2024-01' or '202401') 30 | -> mapped to the last day of that month 31 | """ 32 | dates = [] 33 | 34 | for row in rows: 35 | # Support both raw strings and rows with the date in the last column 36 | if isinstance(row, (list, tuple)): 37 | raw_value = row[-1] 38 | else: 39 | raw_value = row 40 | 41 | element = str(raw_value).strip() 42 | 43 | if not element: 44 | raise ValueError(f"Empty date value in row: {row!r}") 45 | 46 | upper = element.upper() 47 | 48 | # --- Handle Quarter formats first: 'YYYY-Q1', 'YYYYQ1' --- 49 | if "Q" in upper: 50 | try: 51 | cleaned = upper.replace("-", "").replace(" ", "") 52 | # Expect something like '2025Q1' 53 | if len(cleaned) >= 6 and cleaned[4] == "Q": 54 | year = int(cleaned[:4]) 55 | q = int(cleaned[5:]) # supports 'Q1' (and weird 'Q01', but fine) 56 | if 1 <= q <= 4: 57 | # End of quarter: Q1 -> Mar, Q2 -> Jun, Q3 -> Sep, Q4 -> Dec 58 | month = q * 3 59 | last_day = calendar.monthrange(year, month)[1] 60 | dates.append(date(year, month, last_day)) 61 | continue 62 | except ValueError: 63 | # Fall through to other handlers 64 | pass 65 | 66 | # --- Handle YearMonth formats: 'YYYYMM' or 'YYYY-MM' --- 67 | # Only 6 digits -> year-month, not a full date 68 | digits = "".join(ch for ch in element if ch.isdigit()) 69 | if len(digits) == 6: 70 | try: 71 | year = int(digits[:4]) 72 | month = int(digits[4:6]) 73 | if 1 <= month <= 12: 74 | last_day = calendar.monthrange(year, month)[1] 75 | dates.append(date(year, month, last_day)) 76 | continue 77 | except ValueError: 78 | # Fall through to generic parsing 79 | pass 80 | 81 | # --- Fallback: full date parsing --- 82 | try: 83 | dt = parser.parse(element, fuzzy=False) 84 | dates.append(dt.date()) 85 | except (ValueError, TypeError) as exc: 86 | raise ValueError(f"Unrecognized date format: {element!r}") from exc 87 | 88 | return dates 89 | 90 | 91 | def tm1_io(func): 92 | """Higher Order Function to read values from source and write result to target view""" 93 | 94 | @functools.wraps(func) 95 | def wrapper(*args, **kwargs): 96 | # read values from view 97 | if ( 98 | "tm1_services" in kwargs 99 | and "tm1_source" in kwargs 100 | and "cube_source" in kwargs 101 | and "view_source" in kwargs 102 | ): 103 | tm1 = kwargs["tm1_services"][kwargs["tm1_source"]] 104 | if "values" not in kwargs: 105 | rows_and_values = tm1.cubes.cells.execute_view_rows_and_values( 106 | cube_name=kwargs["cube_source"], 107 | view_name=kwargs["view_source"], 108 | private=False, 109 | element_unique_names=False, 110 | ) 111 | kwargs["values"] = [ 112 | values_by_row[0] for values_by_row in rows_and_values.values() 113 | ] 114 | kwargs["dates"] = generate_dates_from_rows(rows_and_values.keys()) 115 | result = func(*args, **kwargs) 116 | # write result to source view 117 | if ( 118 | "tm1_services" in kwargs 119 | and "tm1_target" in kwargs 120 | and "cube_target" in kwargs 121 | and "view_target" in kwargs 122 | ): 123 | tm1 = kwargs["tm1_services"][kwargs["tm1_target"]] 124 | mdx = tm1.cubes.views.get( 125 | cube_name=kwargs["cube_target"], 126 | view_name=kwargs["view_target"], 127 | private=False, 128 | ).MDX 129 | tm1.cubes.cells.write_values_through_cellset(mdx=mdx, values=(result,)) 130 | return result 131 | 132 | return wrapper 133 | 134 | 135 | def tm1_tidy(func): 136 | """Higher Order Function to delete source view and target view (if param tidy is set to true)""" 137 | 138 | @functools.wraps(func) 139 | def wrapper(*args, **kwargs): 140 | try: 141 | return func(*args, **kwargs) 142 | finally: 143 | if "tm1_services" in kwargs and kwargs.get("tidy", False) in ( 144 | "True", 145 | "true", 146 | "TRUE", 147 | "1", 148 | 1, 149 | ): 150 | # delete source view 151 | if ( 152 | "tm1_source" in kwargs 153 | and "cube_source" in kwargs 154 | and "view_source" in kwargs 155 | ): 156 | tm1 = kwargs["tm1_services"][kwargs["tm1_source"]] 157 | tm1.cubes.views.delete( 158 | cube_name=kwargs["cube_source"], 159 | view_name=kwargs["view_source"], 160 | private=False, 161 | ) 162 | # delete target view 163 | if ( 164 | kwargs 165 | and "tm1_target" in kwargs 166 | and "cube_target" in kwargs 167 | and "view_target" in kwargs 168 | ): 169 | tm1 = kwargs["tm1_services"][kwargs["tm1_target"]] 170 | tm1.cubes.views.delete( 171 | cube_name=kwargs["cube_target"], 172 | view_name=kwargs["view_target"], 173 | private=False, 174 | ) 175 | 176 | return wrapper 177 | 178 | 179 | def _nroot(value, n): 180 | """ 181 | Returns the nth root of the given value. 182 | """ 183 | return value ** (1.0 / n) 184 | 185 | 186 | @tm1_tidy 187 | @tm1_io 188 | def irr(values, *args, **kwargs): 189 | return npf.irr(values=values) 190 | 191 | 192 | @tm1_tidy 193 | @tm1_io 194 | def npv(rate, values, *args, **kwargs): 195 | return npf.npv(rate=float(rate), values=list(values)) 196 | 197 | 198 | @tm1_tidy 199 | @tm1_io 200 | def stdev(values, *args, **kwargs): 201 | return np.std(values) 202 | 203 | 204 | @tm1_tidy 205 | @tm1_io 206 | def stdev_p(values, *args, **kwargs): 207 | return np.std(values, ddof=1) 208 | 209 | 210 | @tm1_tidy 211 | @tm1_io 212 | def fv(rate, nper, pmt, pv, when=0, *args, **kwargs): 213 | """Calculates the future value 214 | 215 | :param rate: Rate of interest as decimal (not per cent) per period 216 | :param nper: Number of compounding periods 217 | :param pmt: Payment 218 | :param pv: Present Value 219 | :param when: 0 or 1. When the payment is made (Default: the payment is made at the end of the period) 220 | :param args: 221 | :param kwargs: 222 | :return: 223 | """ 224 | return npf.fv( 225 | rate=float(rate), nper=float(nper), pmt=float(pmt), pv=float(pv), when=int(when) 226 | ) 227 | 228 | 229 | @tm1_tidy 230 | @tm1_io 231 | def fv_schedule(principal, values, *args, **kwargs): 232 | """The future value with the variable interest rate 233 | 234 | :param principal: Principal is the present value of a particular investment 235 | :param values: A series of interest rate 236 | :return: 237 | """ 238 | return functools.reduce(lambda x, y: x + (x * y), values, float(principal)) 239 | 240 | 241 | @tm1_tidy 242 | @tm1_io 243 | def pv(rate, nper, pmt, fv, when=0, *args, **kwargs): 244 | """Calculate the Present Value 245 | 246 | :param rate: It is the interest rate/period 247 | :param nper: Number of periods 248 | :param pmt: Payment/period 249 | :param fv: Future Value 250 | :param when: 0 or 1. When the payment is made (Default: the payment is made at the end of the period) 251 | :return: 252 | """ 253 | return npf.pv( 254 | rate=float(rate), nper=float(nper), pmt=float(pmt), fv=float(fv), when=int(when) 255 | ) 256 | 257 | 258 | @tm1_tidy 259 | @tm1_io 260 | def xnpv(rate, values, dates, *args, **kwargs): 261 | """Calculates the Net Present Value for a schedule of cash flows that is not necessarily periodic 262 | 263 | :param rate: Discount rate for a period 264 | :param values: Positive or negative cash flows 265 | :param dates: Specific dates 266 | :return: 267 | """ 268 | rate = float(rate) 269 | if len(values) != len(dates): 270 | raise ValueError("values and dates must be the same length") 271 | if sorted(dates) != dates: 272 | raise ValueError("dates must be in chronological order") 273 | first_date = dates[0] 274 | return sum( 275 | [ 276 | value / ((1 + rate) ** ((date - first_date).days / 365.0)) 277 | for (value, date) in zip(values, dates) 278 | ] 279 | ) 280 | 281 | 282 | @tm1_tidy 283 | @tm1_io 284 | def pmt(rate, nper, pv, fv=0, when=0, *args, **kwargs): 285 | """PMT denotes the periodical payment required to pay off for a particular period of time with a constant interest rate 286 | 287 | :param rate: Interest rate/period 288 | :param nper: Number of periods 289 | :param pv: Present Value 290 | :param fv: Future Value, if not assigned 0 is assumed 291 | :param when: 0 or 1. When the payment is made (Default: the payment is made at the end of the period) 292 | :return: 293 | """ 294 | return npf.pmt( 295 | rate=float(rate), nper=float(nper), pv=float(pv), fv=float(fv), when=int(when) 296 | ) 297 | 298 | 299 | @tm1_tidy 300 | @tm1_io 301 | def ppmt(rate, per, nper, pv, fv=0, when=0, *args, **kwargs): 302 | """calculates payment on principal with a constant interest rate and constant periodic payments 303 | 304 | :param rate: Interest rate/period 305 | :param per: The period for which the principal is to be calculated 306 | :param nper: Number of periods 307 | :param pv: Present Value 308 | :param fv: Future Value, if not assigned 0 is assumed 309 | :param when: 0 or 1. When the payment is made (Default: the payment is made at the end of the period) 310 | :return: 311 | """ 312 | return npf.ppmt( 313 | rate=float(rate), 314 | per=float(per), 315 | nper=float(nper), 316 | pv=float(pv), 317 | fv=float(fv), 318 | when=int(when), 319 | ) 320 | 321 | 322 | @tm1_tidy 323 | @tm1_io 324 | def mirr(values, finance_rate, reinvest_rate, *args, **kwargs): 325 | """MIRR is calculated by assuming NPV as zero 326 | 327 | :param values: Positive or negative cash flows 328 | :param finance_rate: Interest rate paid for the money used in cash flows 329 | :param reinvest_rate: Interest rate paid for reinvestment of cash flows 330 | :return: 331 | """ 332 | return npf.mirr( 333 | values=values, 334 | finance_rate=float(finance_rate), 335 | reinvest_rate=float(reinvest_rate), 336 | ) 337 | 338 | 339 | @tm1_tidy 340 | @tm1_io 341 | def xirr(values, dates, guess=0.1, *args, **kwargs): 342 | """Returns the internal rate of return for a schedule of cash flows that is not necessarily periodic. 343 | 344 | :param values: Positive or negative cash flows 345 | :param dates: Specific dates 346 | :param guess: An assumption of what you think IRR should be 347 | :return: 348 | """ 349 | return optimize.newton(lambda r: xnpv(r, values, dates), float(guess)) 350 | 351 | 352 | @tm1_tidy 353 | @tm1_io 354 | def nper(rate, pmt, pv, fv=0, when=0, *args, **kwargs): 355 | """Number of periods one requires to pay off the loan 356 | 357 | :param rate: Interest rate/period 358 | :param pmt: Amount paid per period 359 | :param pv: Present Value 360 | :param fv: Future Value, if not assigned 0 is assumed 361 | :param when: 0 or 1. When the payment is made (Default: the payment is made at the end of the period) 362 | :return: 363 | """ 364 | return npf.nper( 365 | rate=float(rate), pmt=float(pmt), pv=float(pv), fv=float(fv), when=int(when) 366 | ).item(0) 367 | 368 | 369 | @tm1_tidy 370 | @tm1_io 371 | def rate(nper, pmt, pv, fv=0, when=0, guess=0.1, maxiter=100, *args, **kwargs): 372 | """The interest rate needed to pay off the loan in full for a given period of time 373 | 374 | :param nper: Number of periods 375 | :param pmt: Amount paid per period 376 | :param pv: Present Value 377 | :param fv: Future Value, if not assigned 0 is assumed 378 | :param when: 0 or 1. When the payment is made (Default: the payment is made at the end of the period) 379 | :param guess: An assumption of what you think the rate should be 380 | :param maxiter: maximum number of iterations 381 | :return: 382 | """ 383 | return npf.rate( 384 | nper=float(nper), 385 | pmt=float(pmt), 386 | pv=float(pv), 387 | fv=float(fv), 388 | when=int(when), 389 | guess=float(guess), 390 | maxiter=int(maxiter), 391 | ) 392 | 393 | 394 | @tm1_tidy 395 | @tm1_io 396 | def effect(nominal_rate, npery, *args, **kwargs): 397 | """Returns the effective annual interest rate, given the nominal annual interest rate 398 | and the number of compounding periods per year. 399 | 400 | :param nominal_rate: Nominal Interest Rate 401 | :param npery: Number of compounding per year 402 | :return: 403 | """ 404 | nominal_rate, npery = float(nominal_rate), float(npery) 405 | return ((1 + (nominal_rate / npery)) ** npery) - 1 406 | 407 | 408 | @tm1_tidy 409 | @tm1_io 410 | def nominal(effect_rate, npery, *args, **kwargs): 411 | """Returns the nominal annual interest rate, given the effective rate and the number of compounding periods per year. 412 | 413 | :param effect_rate: Effective annual interest rate 414 | :param npery: Number of compounding per year 415 | :return: 416 | """ 417 | effect_rate, npery = float(effect_rate), float(npery) 418 | return (_nroot(effect_rate + 1, npery) - 1) * npery 419 | 420 | 421 | @tm1_tidy 422 | @tm1_io 423 | def sln(cost, salvage, life, *args, **kwargs): 424 | """Returns the straight-line depreciation of an asset for one period. 425 | 426 | :param cost: Cost of asset when bought (initial amount) 427 | :param salvage: Value of asset after depreciation 428 | :param life: Number of periods over which the asset is being depreciated 429 | :return: 430 | """ 431 | return (float(cost) - float(salvage)) / float(life) 432 | 433 | 434 | @tm1_tidy 435 | @tm1_io 436 | def mean(values, *args, **kwargs): 437 | return statistics.mean(values) 438 | 439 | 440 | @tm1_tidy 441 | @tm1_io 442 | def sem(values, *args, **kwargs): 443 | """ 444 | https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.sem.html 445 | :return: 446 | """ 447 | return stats.sem(values) 448 | 449 | 450 | @tm1_tidy 451 | @tm1_io 452 | def median(values, *args, **kwargs): 453 | return statistics.median(values) 454 | 455 | 456 | @tm1_tidy 457 | @tm1_io 458 | def mode(values, *args, **kwargs): 459 | """ 460 | https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.mode.html 461 | :param values: 462 | :return: 463 | """ 464 | return stats.mode(values)[0][0] 465 | 466 | 467 | @tm1_tidy 468 | @tm1_io 469 | def var(values, *args, **kwargs): 470 | return np.var(values) 471 | 472 | 473 | @tm1_tidy 474 | @tm1_io 475 | def var_p(values, *args, **kwargs): 476 | return np.var(values, ddof=1) 477 | 478 | 479 | @tm1_tidy 480 | @tm1_io 481 | def kurt(values, *args, **kwargs): 482 | """ 483 | https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.kurtosis.html 484 | :param values: 485 | :return: 486 | """ 487 | return stats.kurtosis(values) 488 | 489 | 490 | @tm1_tidy 491 | @tm1_io 492 | def skew(values, *args, **kwargs): 493 | """ 494 | https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.skew.html 495 | :param values: 496 | :return: 497 | """ 498 | return stats.skew(values) 499 | 500 | 501 | @tm1_tidy 502 | @tm1_io 503 | def rng(values, *args, **kwargs): 504 | return max(values) - min(values) 505 | 506 | 507 | @tm1_tidy 508 | @tm1_io 509 | def min_(values, *args, **kwargs): 510 | return min(values) 511 | 512 | 513 | @tm1_tidy 514 | @tm1_io 515 | def max_(values, *args, **kwargs): 516 | return max(values) 517 | 518 | 519 | @tm1_tidy 520 | @tm1_io 521 | def sum_(values, *args, **kwargs): 522 | return sum(values) 523 | 524 | 525 | @tm1_tidy 526 | @tm1_io 527 | def count(values, *args, **kwargs): 528 | return len(set(values)) 529 | -------------------------------------------------------------------------------- /Tests.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import os 3 | import unittest 4 | from datetime import date 5 | from dateutil.relativedelta import relativedelta 6 | 7 | from TM1py import ( 8 | TM1Service, 9 | Dimension, 10 | Hierarchy, 11 | Element, 12 | Cube, 13 | NativeView, 14 | AnonymousSubset, 15 | MDXView, 16 | ) 17 | 18 | from methods import ( 19 | irr, 20 | npv, 21 | stdev, 22 | stdev_p, 23 | fv, 24 | fv_schedule, 25 | pv, 26 | xnpv, 27 | pmt, 28 | ppmt, 29 | mirr, 30 | xirr, 31 | nper, 32 | rate, 33 | effect, 34 | nominal, 35 | sln, 36 | mean, 37 | sem, 38 | median, 39 | mode, 40 | var, 41 | rng, 42 | count, 43 | skew, 44 | var_p, 45 | kurt, 46 | generate_dates_from_rows, 47 | ) 48 | 49 | config = configparser.ConfigParser() 50 | config.read(os.path.join(os.path.abspath(os.path.dirname(__file__)), "config.ini")) 51 | 52 | MDX_TEMPLATE = """ 53 | SELECT 54 | {rows} ON ROWS, 55 | {columns} ON COLUMNS 56 | FROM {cube} 57 | WHERE {where} 58 | """ 59 | 60 | MDX_TEMPLATE_SHORT = """ 61 | SELECT 62 | {rows} ON ROWS, 63 | {columns} ON COLUMNS 64 | FROM {cube} 65 | """ 66 | 67 | PREFIX = "CubeCalc_Tests_" 68 | 69 | CUBE_NAME_SOURCE = PREFIX + "Cube_Source" 70 | CUBE_NAME_TARGET = PREFIX + "Cube_Target" 71 | DIMENSION_NAMES = [ 72 | PREFIX + "Dimension1", 73 | PREFIX + "Dimension2" 74 | ] 75 | VIEW_NAME_SOURCE = PREFIX + "View_Source" 76 | VIEW_NAME_TARGET = PREFIX + "View_Target" 77 | 78 | IRR_INPUT_VALUES = (-100000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000) 79 | IRR_EXPECTED_RESULT = 0.0162313281744622 80 | IRR_TOLERANCE = 0.00001 81 | 82 | NPV_INPUT_RATE = 0.02 83 | NPV_INPUT_VALUES = IRR_INPUT_VALUES 84 | NPV_EXPECTED_RESULT = -2089.72504573015 85 | NPV_TOLERANCE = 50 86 | 87 | FV_INPUT_RATE = 0.1 88 | FV_INPUT_NPER = 3 89 | FV_INPUT_PMT = 1 90 | FV_INPUT_PV = -100 91 | FV_INPUT_WHEN = 0 92 | FV_EXPECTED_RESULT = 129.79 93 | FV_TOLERANCE = 0.00001 94 | 95 | FV_SCHEDULE_PRINCIPLE = 100 96 | FV_SCHEDULE_SCHEDULE = (0.04, 0.06, 0.05) 97 | FV_SCHEDULE_EXPECTED_RESULT = 115.752 98 | 99 | PV_INPUT_RATE = 0.1 100 | PV_INPUT_NPER = 3 101 | PV_INPUT_PMT = 1 102 | PV_INPUT_FV = -100 103 | PV_INPUT_WHEN = 0 104 | PV_EXPECTED_RESULT = 72.6446280991735 105 | PV_TOLERANCE = 0.00001 106 | 107 | XNPV_INPUT_RATE = 0.05 108 | XNPV_INPUT_VALUES = [-1000, 300, 400, 400, 300] 109 | XNPV_INPUT_DATES = [date(2011, 12, 1), date(2012, 1, 1), date(2013, 2, 1), date(2014, 3, 1), date(2015, 4, 1)] 110 | XNPV_EXPECTED_RESULT = 289.901722604195 111 | XNPV_TOLERANCE = 0.00001 112 | 113 | PMT_INPUT_RATE = 0.1 114 | PMT_INPUT_NPER = 3 115 | PMT_INPUT_PV = 1000 116 | PMT_EXPECTED_RESULT = -402.1148036253 117 | PMT_TOLERANCE = 0.00001 118 | 119 | PPMT_INPUT_RATE = 0.1 120 | PPMT_INPUT_PER = 2 121 | PPMT_INPUT_NPER = 3 122 | PPMT_INPUT_PV = 1000 123 | PPMT_EXPECTED_RESULT = -332.32628398791 124 | PPMT_TOLERANCE = 0.00001 125 | 126 | MIRR_INPUT_VALUES = [-1000, 300, 400, 400, 300] 127 | MIRR_INPUT_FINANCE_RATE = 0.12 128 | MIRR_INPUT_REINVEST_RATE = 0.1 129 | MIRR_EXPECTED_RESULT = 0.12875502614825 130 | MIRR_TOLERANCE = 0.00001 131 | 132 | XIRR_INPUT_VALUES = [-1000, 300, 400, 400, 300] 133 | XIRR_INPUT_DATES = [date(2011, 12, 1), date(2012, 1, 1), date(2013, 2, 1), date(2014, 3, 1), date(2015, 4, 1)] 134 | XIRR_EXPECTED_RESULT = 0.23860325587217 135 | XIRR_TOLERANCE = 0.00001 136 | 137 | NPER_INPUT_RATE = 0.1 138 | NPER_INPUT_PMT = -200 139 | NPER_INPUT_PV = 1000 140 | NPER_EXPECTED_RESULT = 7.272540897341 141 | NPER_TOLERANCE = 0.00001 142 | 143 | RATE_INPUT_NPER = 6 144 | RATE_INPUT_PMT = -200 145 | RATE_INPUT_PV = 1000 146 | RATE_EXPECTED_RESULT = 0.054717925023 147 | RATE_TOLERANCE = 0.00001 148 | 149 | EFFECT_INPUT_NOMINAL_RATE = 0.12 150 | EFFECT_INPUT_NPERY = 12 151 | EFFECT_EXPECTED_RESULT = 0.12682503013196 152 | EFFECT_TOLERANCE = 0.00001 153 | 154 | NOMINAL_INPUT_EFFECT_RATE = 0.12 155 | NOMINAL_INPUT_NPERY = 12 156 | NOMINAL_EXPECTED_RESULT = 0.11386551521 157 | NOMINAL_TOLERANCE = 0.00001 158 | 159 | SLN_INPUT_COST = 5000 160 | SLN_INPUT_SALVAGE = 300 161 | SLN_INPUT_LIFE = 10 162 | SLN_EXPECTED_RESULT = 470 163 | SLN_TOLERANCE = 0.00001 164 | 165 | MEAN_VALUES = [1, 2, 3, 4, 5] 166 | MEAN_EXPECTED_RESULT = 3 167 | 168 | SEM_VALUES = [1, 2, 3, 4, 5] 169 | SEM_EXPECTED_RESULT = 0.707106781 170 | SEM_TOLERANCE = 0.00001 171 | 172 | MEDIAN_VALUES = [1, 2, 3, 4, 5] 173 | MEDIAN_EXPECTED_RESULT = 3 174 | 175 | MODE_VALUES = [1, 1, 2, 3, 4, 5] 176 | MODE_EXPECTED_RESULT = 1 177 | 178 | VAR_VALUES = [1, 2, 3, 4, 5] 179 | VAR_EXPECTED_RESULT = 2 180 | VAR_P_EXPECTED_RESULT = 2.5 181 | 182 | KURTOSIS_VALUES = [1, 2, 3, 4, 5] 183 | KURTOSIS_EXPECTED_RESULT = -1.3 184 | 185 | SKEWNESS_VALUES = [1, 2, 3, 4, 5] 186 | SKEWNESS_EXPECTED_RESULT = 0 187 | 188 | RNG_VALUES = [1, 2, 3, 4, 5] 189 | RNG_EXPECTED_RESULT = 4 190 | 191 | COUNT_VALUES = [1, 2, 3, 4, 5] 192 | COUNT_EXPECTED_RESULT = 5 193 | 194 | STDEV_INPUT_VALUES = (1, 2, 3, 4, 5, 6, 7, 8, 9) 195 | STDEV_EXPECTED_RESULT = 2.58198889747161 196 | STDEV_P_EXPECTED_RESULT = 2.73861278752583 197 | STDEV_TOLERANCE = 0.00001 198 | 199 | 200 | 201 | 202 | 203 | class TestUtils(unittest.TestCase): 204 | 205 | def test_generate_dates_from_rows_quarters(self): 206 | dates = generate_dates_from_rows(["2025-Q1", "2025-Q2", "2025-Q3", "2025-Q4", "2026-Q1"]) 207 | expected_dates = [ 208 | date(2025, 3, 31), 209 | date(2025, 6, 30), 210 | date(2025, 9, 30), 211 | date(2025, 12, 31), 212 | date(2026, 3, 31) 213 | ] 214 | self.assertEqual(expected_dates, dates) 215 | 216 | 217 | def test_generate_dates_from_rows_moths(self): 218 | dates = generate_dates_from_rows(["2025-01", "2025-02", "2025-03", "2025-04", "2026-05"]) 219 | expected_dates = [ 220 | date(2025, 1, 31), 221 | date(2025, 2, 28), 222 | date(2025, 3, 31), 223 | date(2025, 4, 30), 224 | date(2026, 5, 31), 225 | ] 226 | self.assertEqual(expected_dates, dates) 227 | 228 | def test_generate_dates_from_rows_dates(self): 229 | dates = generate_dates_from_rows( 230 | ["2025-01-31", "2025-02-12", "2025-03-8", "2025-04-15", "2026-05-24"] 231 | ) 232 | expected_dates = [ 233 | date(2025, 1, 31), 234 | date(2025, 2, 12), 235 | date(2025, 3, 8), 236 | date(2025, 4, 15), 237 | date(2026, 5, 24), 238 | ] 239 | self.assertEqual(expected_dates, dates) 240 | 241 | def test_generate_dates_from_rows_invalid(self): 242 | with self.assertRaises(ValueError): 243 | generate_dates_from_rows( 244 | ["2025-01-31", "2025-02-12", "2025-03-8", "2025-04-15", "2026-05-42"] 245 | ) 246 | 247 | 248 | class TestMethods(unittest.TestCase): 249 | 250 | def test_irr(self): 251 | result = irr(values=IRR_INPUT_VALUES) 252 | self.assertAlmostEqual(result, IRR_EXPECTED_RESULT, delta=IRR_TOLERANCE) 253 | 254 | def test_npv(self): 255 | result = npv(values=IRR_INPUT_VALUES, rate=NPV_INPUT_RATE) 256 | self.assertAlmostEqual(result, NPV_EXPECTED_RESULT, delta=NPV_TOLERANCE) 257 | 258 | def test_stdev(self): 259 | result = stdev(values=STDEV_INPUT_VALUES) 260 | self.assertAlmostEqual(result, STDEV_EXPECTED_RESULT, delta=STDEV_TOLERANCE) 261 | 262 | def test_stdev_p(self): 263 | result = stdev_p(values=STDEV_INPUT_VALUES) 264 | self.assertAlmostEqual(result, STDEV_P_EXPECTED_RESULT, delta=STDEV_TOLERANCE) 265 | 266 | def test_fv(self): 267 | result = fv(rate=FV_INPUT_RATE, nper=FV_INPUT_NPER, pmt=FV_INPUT_PMT, pv=FV_INPUT_PV, when=FV_INPUT_WHEN) 268 | self.assertAlmostEqual(result, FV_EXPECTED_RESULT, delta=FV_TOLERANCE) 269 | 270 | def test_fv_schedule(self): 271 | result = fv_schedule(principal=FV_SCHEDULE_PRINCIPLE, values=FV_SCHEDULE_SCHEDULE) 272 | self.assertEqual(result, FV_SCHEDULE_EXPECTED_RESULT) 273 | 274 | def test_pv(self): 275 | result = pv(rate=PV_INPUT_RATE, nper=PV_INPUT_NPER, pmt=PV_INPUT_PMT, fv=PV_INPUT_FV, when=PV_INPUT_WHEN) 276 | self.assertAlmostEqual(result, PV_EXPECTED_RESULT, delta=PV_TOLERANCE) 277 | 278 | def test_xnpv(self): 279 | result = xnpv(rate=XNPV_INPUT_RATE, values=XNPV_INPUT_VALUES, dates=XNPV_INPUT_DATES) 280 | self.assertAlmostEqual(result, XNPV_EXPECTED_RESULT, delta=XNPV_TOLERANCE) 281 | 282 | def test_pmt(self): 283 | result = pmt(rate=PMT_INPUT_RATE, nper=PMT_INPUT_NPER, pv=PMT_INPUT_PV) 284 | self.assertAlmostEqual(result, PMT_EXPECTED_RESULT, delta=PMT_TOLERANCE) 285 | 286 | def test_ppmt(self): 287 | result = ppmt(rate=PPMT_INPUT_RATE, per=PPMT_INPUT_PER, nper=PPMT_INPUT_NPER, pv=PPMT_INPUT_PV) 288 | self.assertAlmostEqual(result, PPMT_EXPECTED_RESULT, delta=PPMT_TOLERANCE) 289 | 290 | def test_mirr(self): 291 | result = mirr( 292 | values=MIRR_INPUT_VALUES, 293 | finance_rate=MIRR_INPUT_FINANCE_RATE, 294 | reinvest_rate=MIRR_INPUT_REINVEST_RATE) 295 | print(result) 296 | self.assertAlmostEqual(result, MIRR_EXPECTED_RESULT, delta=MIRR_TOLERANCE) 297 | 298 | def test_xirr(self): 299 | result = xirr( 300 | values=XIRR_INPUT_VALUES, 301 | dates=XIRR_INPUT_DATES) 302 | self.assertAlmostEqual(result, XIRR_EXPECTED_RESULT, delta=XIRR_TOLERANCE) 303 | 304 | def test_nper(self): 305 | result = nper( 306 | rate=NPER_INPUT_RATE, 307 | pmt=NPER_INPUT_PMT, 308 | pv=NPER_INPUT_PV) 309 | self.assertAlmostEqual(result, NPER_EXPECTED_RESULT, delta=NPER_TOLERANCE) 310 | 311 | def test_rate(self): 312 | result = rate( 313 | nper=RATE_INPUT_NPER, 314 | pmt=RATE_INPUT_PMT, 315 | pv=RATE_INPUT_PV) 316 | self.assertAlmostEqual(result, RATE_EXPECTED_RESULT, delta=RATE_TOLERANCE) 317 | 318 | def test_effect(self): 319 | result = effect(nominal_rate=EFFECT_INPUT_NOMINAL_RATE, npery=EFFECT_INPUT_NPERY) 320 | self.assertAlmostEqual(result, EFFECT_EXPECTED_RESULT, delta=EFFECT_TOLERANCE) 321 | 322 | def test_nominal(self): 323 | result = nominal(effect_rate=NOMINAL_INPUT_EFFECT_RATE, npery=NOMINAL_INPUT_NPERY) 324 | self.assertAlmostEqual(result, NOMINAL_EXPECTED_RESULT, delta=NOMINAL_TOLERANCE) 325 | 326 | def test_sln(self): 327 | result = sln( 328 | cost=SLN_INPUT_COST, 329 | salvage=SLN_INPUT_SALVAGE, 330 | life=SLN_INPUT_LIFE) 331 | self.assertAlmostEqual(result, SLN_EXPECTED_RESULT, delta=SLN_TOLERANCE) 332 | 333 | def test_mean(self): 334 | result = mean(values=[1, 2, 3, 4, 5]) 335 | self.assertEqual(result, 3) 336 | 337 | def test_sem(self): 338 | result = sem(SEM_VALUES) 339 | self.assertAlmostEqual(result, SEM_EXPECTED_RESULT, delta=SEM_TOLERANCE) 340 | 341 | def test_median(self): 342 | result = median(MEDIAN_VALUES) 343 | self.assertEqual(result, MEDIAN_EXPECTED_RESULT) 344 | 345 | def test_mode(self): 346 | result = mode(MODE_VALUES) 347 | self.assertEqual(result, MODE_EXPECTED_RESULT) 348 | 349 | def test_var(self): 350 | result = var(VAR_VALUES) 351 | self.assertEqual(result, VAR_EXPECTED_RESULT) 352 | 353 | def test_var_p(self): 354 | result = var_p(VAR_VALUES) 355 | self.assertEqual(result, VAR_P_EXPECTED_RESULT) 356 | 357 | def test_kurt(self): 358 | result = kurt(KURTOSIS_VALUES) 359 | self.assertEqual(result, KURTOSIS_EXPECTED_RESULT) 360 | 361 | def test_skew(self): 362 | result = skew(SKEWNESS_VALUES) 363 | self.assertEqual(result, SKEWNESS_EXPECTED_RESULT) 364 | 365 | def test_rng(self): 366 | result = rng(RNG_VALUES) 367 | self.assertEqual(result, RNG_EXPECTED_RESULT) 368 | 369 | def test_count(self): 370 | result = count(COUNT_VALUES) 371 | self.assertEqual(result, COUNT_EXPECTED_RESULT) 372 | 373 | 374 | class TestDecorators(unittest.TestCase): 375 | tm1 = TM1Service(**config["tm1srv01"]) 376 | 377 | @classmethod 378 | def setUpClass(cls): 379 | start_date = date.today().replace(day=1) 380 | 381 | cls.dimension1 = Dimension( 382 | name=DIMENSION_NAMES[0], 383 | hierarchies=[ 384 | Hierarchy( 385 | name=DIMENSION_NAMES[0], 386 | dimension_name=DIMENSION_NAMES[0], 387 | elements=[ 388 | Element((start_date + relativedelta(months=i)).strftime("%Y-%m"), "Numeric") 389 | for i 390 | in range(100)])]) 391 | cls.dimension2 = Dimension( 392 | name=DIMENSION_NAMES[1], 393 | hierarchies=[ 394 | Hierarchy( 395 | name=DIMENSION_NAMES[1], 396 | dimension_name=DIMENSION_NAMES[1], 397 | elements=[Element(name="Element_{}".format(i), element_type="Numeric") for i in range(1, 101)])]) 398 | cls.cube_source = Cube( 399 | name=CUBE_NAME_SOURCE, 400 | dimensions=DIMENSION_NAMES) 401 | cls.cube_target = Cube( 402 | name=CUBE_NAME_TARGET, 403 | dimensions=DIMENSION_NAMES) 404 | 405 | @classmethod 406 | def tearDownClass(cls): 407 | cls.tm1.logout() 408 | 409 | def setUp(self): 410 | if not self.tm1.dimensions.exists(dimension_name=self.dimension1.name): 411 | self.tm1.dimensions.update_or_create(dimension=self.dimension1) 412 | if not self.tm1.dimensions.exists(dimension_name=self.dimension2.name): 413 | self.tm1.dimensions.update_or_create(dimension=self.dimension2) 414 | if not self.tm1.cubes.exists(cube_name=self.cube_source.name): 415 | self.tm1.cubes.update_or_create(cube=self.cube_source) 416 | if not self.tm1.cubes.exists(cube_name=self.cube_target.name): 417 | self.tm1.cubes.update_or_create(cube=self.cube_target) 418 | 419 | def tearDown(self): 420 | self.tm1.cubes.delete(cube_name=self.cube_source.name) 421 | self.tm1.cubes.delete(cube_name=self.cube_target.name) 422 | self.tm1.dimensions.delete(dimension_name=self.dimension1.name) 423 | self.tm1.dimensions.delete(dimension_name=self.dimension2.name) 424 | 425 | def test_tm1io_input_nativeview_output_nativeview(self): 426 | # create input view 427 | view_input = NativeView( 428 | cube_name=CUBE_NAME_SOURCE, 429 | view_name=VIEW_NAME_SOURCE, 430 | suppress_empty_columns=False, 431 | suppress_empty_rows=False) 432 | view_input.add_row( 433 | dimension_name=DIMENSION_NAMES[0], 434 | subset=AnonymousSubset( 435 | dimension_name=DIMENSION_NAMES[0], 436 | expression="{ HEAD ( { [" + DIMENSION_NAMES[0] + "].Members}," + str(len(IRR_INPUT_VALUES)) + ") }")) 437 | view_input.add_column( 438 | dimension_name=DIMENSION_NAMES[1], 439 | subset=AnonymousSubset( 440 | dimension_name=DIMENSION_NAMES[1], 441 | expression="{[" + DIMENSION_NAMES[1] + "].[Element_1]}")) 442 | self.tm1.cubes.views.update_or_create( 443 | view=view_input, 444 | private=False) 445 | # create output view 446 | view_output = NativeView( 447 | cube_name=CUBE_NAME_TARGET, 448 | view_name=VIEW_NAME_TARGET, 449 | suppress_empty_columns=False, 450 | suppress_empty_rows=False) 451 | view_output.add_row( 452 | dimension_name=DIMENSION_NAMES[0], 453 | subset=AnonymousSubset( 454 | dimension_name=DIMENSION_NAMES[0], 455 | expression="{[" + DIMENSION_NAMES[0] + "].DefaultMember}")) 456 | view_output.add_column( 457 | dimension_name=DIMENSION_NAMES[1], 458 | subset=AnonymousSubset( 459 | dimension_name=DIMENSION_NAMES[1], 460 | expression="{[" + DIMENSION_NAMES[1] + "].[Element_1]}")) 461 | self.tm1.cubes.views.update_or_create( 462 | view=view_output, 463 | private=False) 464 | # write values into input view 465 | mdx = view_input.MDX 466 | self.tm1.cubes.cells.write_values_through_cellset(mdx, IRR_INPUT_VALUES) 467 | # execute method 468 | result = irr( 469 | tm1_services={"tm1srv01": self.tm1, "tm1srv02": self.tm1}, 470 | tm1_source="tm1srv01", 471 | tm1_target="tm1srv02", 472 | cube_source=CUBE_NAME_SOURCE, 473 | cube_target=CUBE_NAME_TARGET, 474 | view_source=VIEW_NAME_SOURCE, 475 | view_target=VIEW_NAME_TARGET) 476 | self.assertAlmostEqual(IRR_EXPECTED_RESULT, result, delta=IRR_TOLERANCE) 477 | # check output view 478 | cell_value = self.tm1.cubes.cells.execute_view_values( 479 | cube_name=CUBE_NAME_TARGET, 480 | view_name=VIEW_NAME_TARGET, 481 | private=False)[0] 482 | self.assertAlmostEqual(cell_value, IRR_EXPECTED_RESULT, delta=IRR_TOLERANCE) 483 | 484 | def test_tm1io_input_mdx_view_output_mdx_view(self): 485 | # create input view 486 | mdx_input = MDX_TEMPLATE_SHORT.format( 487 | rows="{ HEAD ( { [" + DIMENSION_NAMES[0] + "].Members}," + str(len(STDEV_INPUT_VALUES)) + ") }", 488 | columns="{[" + DIMENSION_NAMES[1] + "].[Element_1]}", 489 | cube=CUBE_NAME_SOURCE) 490 | view_input = MDXView( 491 | cube_name=CUBE_NAME_SOURCE, 492 | view_name=VIEW_NAME_SOURCE, 493 | MDX=mdx_input) 494 | self.tm1.cubes.views.update_or_create( 495 | view=view_input, 496 | private=False) 497 | # create output view 498 | mdx_output = MDX_TEMPLATE_SHORT.format( 499 | rows="{[" + DIMENSION_NAMES[0] + "].DefaultMember}", 500 | columns="{[" + DIMENSION_NAMES[1] + "].[Element_1]}", 501 | cube=CUBE_NAME_TARGET) 502 | view_output = MDXView( 503 | cube_name=CUBE_NAME_TARGET, 504 | view_name=VIEW_NAME_TARGET, 505 | MDX=mdx_output) 506 | self.tm1.cubes.views.update_or_create( 507 | view=view_output, 508 | private=False) 509 | # write values into input view 510 | mdx = view_input.MDX 511 | self.tm1.cubes.cells.write_values_through_cellset(mdx, STDEV_INPUT_VALUES) 512 | # execute method 513 | result = stdev( 514 | tm1_services={"tm1srv01": self.tm1, "tm1srv02": self.tm1}, 515 | tm1_source="tm1srv01", 516 | tm1_target="tm1srv02", 517 | cube_source=CUBE_NAME_SOURCE, 518 | cube_target=CUBE_NAME_TARGET, 519 | view_source=VIEW_NAME_SOURCE, 520 | view_target=VIEW_NAME_TARGET) 521 | self.assertAlmostEqual(STDEV_EXPECTED_RESULT, result, delta=STDEV_TOLERANCE) 522 | # check output view 523 | cell_value = self.tm1.cubes.cells.execute_view_values( 524 | cube_name=CUBE_NAME_TARGET, 525 | view_name=VIEW_NAME_TARGET, 526 | private=False)[0] 527 | self.assertAlmostEqual(cell_value, STDEV_EXPECTED_RESULT, delta=STDEV_TOLERANCE) 528 | 529 | def test_tm1io_input_view(self): 530 | # define input view and output view 531 | mdx_input = MDX_TEMPLATE_SHORT.format( 532 | rows="{ HEAD ( { [" + DIMENSION_NAMES[0] + "].Members}," + str(len(IRR_INPUT_VALUES)) + ") }", 533 | columns="{[" + DIMENSION_NAMES[1] + "].[Element_1]}", 534 | cube=CUBE_NAME_SOURCE) 535 | view_input = MDXView( 536 | cube_name=CUBE_NAME_SOURCE, 537 | view_name=VIEW_NAME_SOURCE, 538 | MDX=mdx_input) 539 | self.tm1.cubes.views.update_or_create( 540 | view=view_input, 541 | private=False) 542 | # write values into input view 543 | mdx = view_input.MDX 544 | self.tm1.cubes.cells.write_values_through_cellset(mdx, IRR_INPUT_VALUES) 545 | # execute method 546 | result = irr( 547 | tm1_services={"tm1srv01": self.tm1, "tm1srv02": self.tm1}, 548 | tm1_source="tm1srv01", 549 | cube_source=CUBE_NAME_SOURCE, 550 | view_source=VIEW_NAME_SOURCE) 551 | self.assertAlmostEqual(IRR_EXPECTED_RESULT, result, delta=IRR_TOLERANCE) 552 | 553 | def test_tm1io_input_values_output_view(self): 554 | # define output view 555 | mdx_output = MDX_TEMPLATE_SHORT.format( 556 | rows="{[" + DIMENSION_NAMES[0] + "].DefaultMember}", 557 | columns="{[" + DIMENSION_NAMES[1] + "].[Element_1]}", 558 | cube=CUBE_NAME_TARGET) 559 | view_output = MDXView( 560 | cube_name=CUBE_NAME_TARGET, 561 | view_name=VIEW_NAME_TARGET, 562 | MDX=mdx_output) 563 | self.tm1.cubes.views.update_or_create( 564 | view=view_output, 565 | private=False) 566 | # execute method 567 | stdev_p( 568 | tm1_services={"tm1srv01": self.tm1}, 569 | tm1_target="tm1srv01", 570 | cube_target=CUBE_NAME_TARGET, 571 | view_target=VIEW_NAME_TARGET, 572 | values=STDEV_INPUT_VALUES, 573 | tidy=False) 574 | # do check 575 | result = self.tm1.cubes.cells.execute_view_values( 576 | cube_name=CUBE_NAME_TARGET, 577 | view_name=VIEW_NAME_TARGET, 578 | private=False 579 | )[0] 580 | self.assertAlmostEqual(result, STDEV_P_EXPECTED_RESULT, delta=STDEV_TOLERANCE) 581 | 582 | def test_tm1tidy_true_input_view_output_view(self): 583 | # create source_view 584 | mdx_input = MDX_TEMPLATE_SHORT.format( 585 | rows="{ HEAD ( { [" + DIMENSION_NAMES[0] + "].Members}," + str(len(STDEV_INPUT_VALUES)) + ") }", 586 | columns="{[" + DIMENSION_NAMES[1] + "].[Element_1]}", 587 | cube=CUBE_NAME_SOURCE) 588 | view_input = MDXView( 589 | cube_name=CUBE_NAME_SOURCE, 590 | view_name=VIEW_NAME_SOURCE, 591 | MDX=mdx_input) 592 | self.tm1.cubes.views.update_or_create( 593 | view=view_input, 594 | private=False) 595 | # create target_view 596 | mdx_output = MDX_TEMPLATE_SHORT.format( 597 | rows="{[" + DIMENSION_NAMES[0] + "].DefaultMember}", 598 | columns="{[" + DIMENSION_NAMES[1] + "].[Element_1]}", 599 | cube=CUBE_NAME_TARGET) 600 | view_output = MDXView( 601 | cube_name=CUBE_NAME_TARGET, 602 | view_name=VIEW_NAME_TARGET, 603 | MDX=mdx_output) 604 | self.tm1.cubes.views.update_or_create( 605 | view=view_output, 606 | private=False) 607 | # write values into input view 608 | mdx = view_input.MDX 609 | self.tm1.cubes.cells.write_values_through_cellset(mdx, STDEV_INPUT_VALUES) 610 | # execute method 611 | stdev( 612 | tm1_services={"tm1srv01": self.tm1, "tm1srv02": self.tm1}, 613 | tm1_source="tm1srv01", 614 | tm1_target="tm1srv02", 615 | cube_source=CUBE_NAME_SOURCE, 616 | cube_target=CUBE_NAME_TARGET, 617 | view_source=VIEW_NAME_SOURCE, 618 | view_target=VIEW_NAME_TARGET, 619 | tidy=True) 620 | # check existence 621 | self.assertFalse(self.tm1.cubes.views.exists( 622 | cube_name=CUBE_NAME_SOURCE, 623 | view_name=VIEW_NAME_SOURCE, 624 | private=False)) 625 | self.assertFalse(self.tm1.cubes.views.exists( 626 | cube_name=CUBE_NAME_TARGET, 627 | view_name=VIEW_NAME_TARGET, 628 | private=False)) 629 | 630 | def test_tm1tidy_false_input_view_output_view(self): 631 | # define input view 632 | mdx_input = MDX_TEMPLATE_SHORT.format( 633 | rows="{ HEAD ( { [" + DIMENSION_NAMES[0] + "].Members}," + str(len(STDEV_INPUT_VALUES)) + ") }", 634 | columns="{[" + DIMENSION_NAMES[1] + "].[Element_1]}", 635 | cube=CUBE_NAME_SOURCE) 636 | view_input = MDXView( 637 | cube_name=CUBE_NAME_SOURCE, 638 | view_name=VIEW_NAME_SOURCE, 639 | MDX=mdx_input) 640 | self.tm1.cubes.views.update_or_create( 641 | view=view_input, 642 | private=False) 643 | # define output view 644 | mdx_output = MDX_TEMPLATE_SHORT.format( 645 | rows="{[" + DIMENSION_NAMES[0] + "].DefaultMember}", 646 | columns="{[" + DIMENSION_NAMES[1] + "].[Element_1]}", 647 | cube=CUBE_NAME_TARGET) 648 | view_output = MDXView( 649 | cube_name=CUBE_NAME_TARGET, 650 | view_name=VIEW_NAME_TARGET, 651 | MDX=mdx_output) 652 | self.tm1.cubes.views.update_or_create( 653 | view=view_output, 654 | private=False) 655 | # write values into input view 656 | mdx = view_input.MDX 657 | self.tm1.cubes.cells.write_values_through_cellset(mdx, STDEV_INPUT_VALUES) 658 | # execute method 659 | stdev( 660 | tm1_services={"tm1srv01": self.tm1}, 661 | tm1_source="tm1srv01", 662 | cube_source=CUBE_NAME_SOURCE, 663 | view_source=VIEW_NAME_SOURCE, 664 | tidy=False) 665 | # check existence 666 | self.assertTrue(self.tm1.cubes.views.exists( 667 | cube_name=CUBE_NAME_SOURCE, 668 | view_name=VIEW_NAME_SOURCE, 669 | private=False)) 670 | self.assertTrue(self.tm1.cubes.views.exists( 671 | cube_name=CUBE_NAME_TARGET, 672 | view_name=VIEW_NAME_TARGET, 673 | private=False)) 674 | 675 | def test_tm1tidy_true_input_view(self): 676 | # define input view and output view 677 | mdx_input = MDX_TEMPLATE_SHORT.format( 678 | rows="{ HEAD ( { [" + DIMENSION_NAMES[0] + "].Members}," + str(len(STDEV_INPUT_VALUES)) + ") }", 679 | columns="{[" + DIMENSION_NAMES[1] + "].[Element_1]}", 680 | cube=CUBE_NAME_SOURCE) 681 | view_input = MDXView( 682 | cube_name=CUBE_NAME_SOURCE, 683 | view_name=VIEW_NAME_SOURCE, 684 | MDX=mdx_input) 685 | self.tm1.cubes.views.update_or_create( 686 | view=view_input, 687 | private=False) 688 | # write values into input view 689 | mdx = view_input.MDX 690 | self.tm1.cubes.cells.write_values_through_cellset(mdx, STDEV_INPUT_VALUES) 691 | # execute method 692 | stdev( 693 | tm1_services={"tm1srv01": self.tm1}, 694 | tm1_source="tm1srv01", 695 | cube_source=CUBE_NAME_SOURCE, 696 | view_source=VIEW_NAME_SOURCE, 697 | tidy=True) 698 | # check existence 699 | self.assertFalse(self.tm1.cubes.views.exists( 700 | cube_name=CUBE_NAME_SOURCE, 701 | view_name=VIEW_NAME_SOURCE, 702 | private=False)) 703 | 704 | def test_tm1tidy_false_input_view(self): 705 | # define input view and output view 706 | mdx_input = MDX_TEMPLATE_SHORT.format( 707 | rows="{ HEAD ( { [" + DIMENSION_NAMES[0] + "].Members}," + str(len(STDEV_INPUT_VALUES)) + ") }", 708 | columns="{[" + DIMENSION_NAMES[1] + "].[Element_1]}", 709 | cube=CUBE_NAME_SOURCE) 710 | view_input = MDXView( 711 | cube_name=CUBE_NAME_SOURCE, 712 | view_name=VIEW_NAME_SOURCE, 713 | MDX=mdx_input) 714 | self.tm1.cubes.views.update_or_create( 715 | view=view_input, 716 | private=False) 717 | # write values into input view 718 | mdx = view_input.MDX 719 | self.tm1.cubes.cells.write_values_through_cellset(mdx, STDEV_INPUT_VALUES) 720 | # execute method 721 | stdev_p( 722 | tm1_services={"tm1srv01": self.tm1}, 723 | tm1_source="tm1srv01", 724 | cube_source=CUBE_NAME_SOURCE, 725 | view_source=VIEW_NAME_SOURCE, 726 | tidy=False) 727 | self.assertTrue(self.tm1.cubes.views.exists( 728 | cube_name=CUBE_NAME_SOURCE, 729 | view_name=VIEW_NAME_SOURCE, 730 | private=False)) 731 | 732 | def test_tm1tidy_true_input_values_output_view(self): 733 | # define output view 734 | mdx_output = MDX_TEMPLATE_SHORT.format( 735 | rows="{[" + DIMENSION_NAMES[0] + "].DefaultMember}", 736 | columns="{[" + DIMENSION_NAMES[1] + "].[Element_1]}", 737 | cube=CUBE_NAME_TARGET) 738 | view_output = MDXView( 739 | cube_name=CUBE_NAME_TARGET, 740 | view_name=VIEW_NAME_TARGET, 741 | MDX=mdx_output) 742 | self.tm1.cubes.views.update_or_create( 743 | view=view_output, 744 | private=False) 745 | # execute method 746 | stdev( 747 | tm1_services={"tm1srv01": self.tm1}, 748 | tm1_target="tm1srv01", 749 | cube_target=CUBE_NAME_TARGET, 750 | view_target=VIEW_NAME_TARGET, 751 | values=STDEV_INPUT_VALUES, 752 | tidy=True) 753 | # check view existence 754 | self.assertFalse(self.tm1.cubes.views.exists( 755 | cube_name=CUBE_NAME_TARGET, 756 | view_name=VIEW_NAME_TARGET, 757 | private=False)) 758 | 759 | def test_tm1tidy_false_input_values_output_view(self): 760 | # define output view 761 | mdx_output = MDX_TEMPLATE_SHORT.format( 762 | rows="{[" + DIMENSION_NAMES[0] + "].DefaultMember}", 763 | columns="{[" + DIMENSION_NAMES[1] + "].[Element_1]}", 764 | cube=CUBE_NAME_TARGET) 765 | view_output = MDXView( 766 | cube_name=CUBE_NAME_TARGET, 767 | view_name=VIEW_NAME_TARGET, 768 | MDX=mdx_output) 769 | self.tm1.cubes.views.update_or_create( 770 | view=view_output, 771 | private=False) 772 | # execute method 773 | irr( 774 | tm1_services={"tm1srv01": self.tm1}, 775 | tm1_target="tm1srv01", 776 | cube_target=CUBE_NAME_TARGET, 777 | view_target=VIEW_NAME_TARGET, 778 | values=IRR_INPUT_VALUES, 779 | tidy=False) 780 | # check view existence 781 | self.assertTrue(self.tm1.cubes.views.exists( 782 | cube_name=CUBE_NAME_TARGET, 783 | view_name=VIEW_NAME_TARGET, 784 | private=False)) -------------------------------------------------------------------------------- /Images/logo_built_with_TM1Py.svg: -------------------------------------------------------------------------------- 1 | Final logos --------------------------------------------------------------------------------