├── .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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------