├── .github
├── ISSUE_TEMPLATE
│ ├── ask-a-question.md
│ └── bug_report.md
└── workflows
│ └── unit_test_pipeline.yml
├── .gitignore
├── .readthedocs.yml
├── Images
├── 1.svg
├── 2.svg
├── 3.svg
├── 4.svg
├── 5.svg
├── 6.svg
└── Logo.svg
├── LICENSE
├── README.md
├── TM1py
├── Exceptions
│ ├── Exceptions.py
│ └── __init__.py
├── Objects
│ ├── Annotation.py
│ ├── Application.py
│ ├── Axis.py
│ ├── Chore.py
│ ├── ChoreFrequency.py
│ ├── ChoreStartTime.py
│ ├── ChoreTask.py
│ ├── Cube.py
│ ├── Dimension.py
│ ├── Element.py
│ ├── ElementAttribute.py
│ ├── Git.py
│ ├── GitCommit.py
│ ├── GitPlan.py
│ ├── GitProject.py
│ ├── GitRemote.py
│ ├── Hierarchy.py
│ ├── MDXView.py
│ ├── NativeView.py
│ ├── Process.py
│ ├── ProcessDebugBreakpoint.py
│ ├── Rules.py
│ ├── Sandbox.py
│ ├── Server.py
│ ├── Subset.py
│ ├── TM1Object.py
│ ├── User.py
│ ├── View.py
│ └── __init__.py
├── Services
│ ├── AnnotationService.py
│ ├── ApplicationService.py
│ ├── AuditLogService.py
│ ├── CellService.py
│ ├── ChoreService.py
│ ├── ConfigurationService.py
│ ├── CubeService.py
│ ├── DimensionService.py
│ ├── ElementService.py
│ ├── FileService.py
│ ├── GitService.py
│ ├── HierarchyService.py
│ ├── JobService.py
│ ├── LoggerService.py
│ ├── ManageService.py
│ ├── MessageLogService.py
│ ├── MonitoringService.py
│ ├── ObjectService.py
│ ├── PowerBiService.py
│ ├── ProcessService.py
│ ├── RestService.py
│ ├── SandboxService.py
│ ├── SecurityService.py
│ ├── ServerService.py
│ ├── SessionService.py
│ ├── SubsetService.py
│ ├── TM1Service.py
│ ├── ThreadService.py
│ ├── TransactionLogService.py
│ ├── UserService.py
│ ├── ViewService.py
│ └── __init__.py
├── Utils
│ ├── MDXUtils.py
│ ├── Utils.py
│ └── __init__.py
└── __init__.py
├── Tests
├── AnnotationService_test.py
├── ApplicationService_test.py
├── CaseAndSpaceInsensitiveDict_test.py
├── CaseAndSpaceInsensitiveSet_test.py
├── CaseAndSpaceInsensitiveTuplesDict_test.py
├── CellService_test.py
├── ChoreService_test.py
├── ChoreStartTime_test.py
├── Chore_test.py
├── CubeService_test.py
├── Cube_test.py
├── DimensionService_test.py
├── ElementAttribute_test.py
├── ElementService_test.py
├── Element_test.py
├── FileService_test.py
├── HierarchyService_test.py
├── Hierarchy_test.py
├── MDXView_test.py
├── ManageService_test.py
├── MonitoringService_test.py
├── NativeView_test.py
├── PowerBiService_test.py
├── ProcessService_test.py
├── Process_test.py
├── RestService_test.py
├── SandboxService_test.py
├── Sandbox_test.py
├── SecurityService_test.py
├── ServerService_test.py
├── SubsetService_test.py
├── Subset_test.py
├── TM1Project_test.py
├── Utils.py
├── Utils_test.py
├── ViewService_test.py
├── __init__.py
├── _gh_README.md
├── _readme.md
├── config.ini.template
└── resources
│ ├── Bedrock.Cube.Clone.json
│ ├── Bedrock.Dim.Clone.json
│ ├── Bedrock.Server.Wait.json
│ ├── document.xlsx
│ ├── file.csv
│ ├── generate_config.py
│ └── raw_cellset.json
├── docs
├── Makefile
├── api.rst
├── conf.py
├── index.rst
├── make.bat
└── requirements.txt
├── pyproject.toml
└── setup.py
/.github/ISSUE_TEMPLATE/ask-a-question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Ask a question
3 | about: Use this template for usage questions
4 | title: ''
5 | labels: question
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe what did you try to do with TM1py**
11 | Describe the script you ran and what it is supposed to do.
12 |
13 | **Describe what's not working the way you expect**
14 | Didn't get the expected result? Describe:
15 | 1. The result you expected.
16 | 2. A clear and concise description of what you are trying to do.
17 |
18 | **Version**
19 | - TM1py [e.g. 1.3.1]
20 | - TM1 Server Version: [e.g. 11.4]
21 |
22 | **Additional context**
23 | If you encounter an error, please add the error message and the stack trace
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Report a defect
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | describe a setup or post a script that reproduces the bug.
15 |
16 | **Expected behavior**
17 | A clear and concise description of what you expected to happen.
18 |
19 | **Version**
20 | TM1py [e.g. 1.3.1]
21 | TM1 Server Version: [e.g. 11.4]
22 |
23 | **Additional context**
24 | Add any other context about the problem here.
25 |
--------------------------------------------------------------------------------
/.github/workflows/unit_test_pipeline.yml:
--------------------------------------------------------------------------------
1 | name: TM1Py Integration Tests
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | code_reference:
7 | description: 'PR number (e.g., #612) or branch name (e.g., master or bugfix_123 ) to test'
8 | required: true
9 | default: ''
10 | environments:
11 | description: 'JSON array of environments to test (e.g., ["tm1-11-onprem", "tm1-11-cloud"])'
12 | required: true
13 | default: '["tm1-11-onprem","tm1-11-cloud","tm1-12-mcsp"]'
14 |
15 | jobs:
16 | test:
17 | runs-on: ubuntu-latest
18 | strategy:
19 | matrix:
20 | environment: ${{ fromJson(inputs.environments) }}
21 | environment: ${{ matrix.environment }}
22 | steps:
23 | - name: Determine ref and PR number
24 | id: determine-ref
25 | shell: bash
26 | run: |
27 | code_ref='${{ inputs.code_reference }}'
28 | if [[ "$code_ref" == \#* ]]; then
29 | # Remove the '#' character
30 | pr_number="${code_ref#\#}"
31 | echo "Detected PR number: $pr_number"
32 | echo "ref=refs/pull/$pr_number/merge" >> $GITHUB_OUTPUT
33 | echo "pr_number=$pr_number" >> $GITHUB_OUTPUT
34 | else
35 | echo "Detected branch name: $code_ref"
36 | echo "ref=$code_ref" >> $GITHUB_OUTPUT
37 | fi
38 |
39 | - name: Checkout code
40 | uses: actions/checkout@v3
41 | with:
42 | repository: ${{ github.repository }}
43 | ref: ${{ steps.determine-ref.outputs.ref }}
44 |
45 | - name: Set up Python
46 | uses: actions/setup-python@v4
47 | with:
48 | python-version: '3.x'
49 |
50 | - name: Install dependencies
51 | run: |
52 | pip install -e .[pandas,dev]
53 |
54 | - name: Retrieve TM1 Connection Details
55 | run: echo "Retrieving TM1 connection details"
56 | env:
57 | TM1_CONNECTION: ${{ vars.TM1_CONNECTION }}
58 | TM1_CONNECTION_SECRET: ${{ secrets.TM1_CONNECTION_SECRET }}
59 |
60 | - name: Generate config.ini
61 | run: |
62 | python Tests/resources/generate_config.py
63 | env:
64 | TM1_CONNECTION: ${{ vars.TM1_CONNECTION }}
65 | TM1_CONNECTION_SECRET: ${{ secrets.TM1_CONNECTION_SECRET }}
66 |
67 | - name: Run tests
68 | run: pytest Tests/
69 |
70 | - name: Upload test results
71 | if: always()
72 | uses: actions/upload-artifact@v3
73 | with:
74 | name: test-results-${{ matrix.environment }}
75 | path: Tests/test-reports/
76 |
77 | - name: Post comment to PR
78 | if: ${{ always() && steps.determine-ref.outputs.pr_number }}
79 | uses: actions/github-script@v6
80 | with:
81 | script: |
82 | github.rest.issues.createComment({
83 | owner: context.repo.owner,
84 | repo: context.repo.repo,
85 | issue_number: ${{ steps.determine-ref.outputs.pr_number }},
86 | body: 'Tests completed for environment: ${{ matrix.environment }}. Check artifacts for details.'
87 | })
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 | Tests/config.ini
54 |
55 | # Translations
56 | *.mo
57 | *.pot
58 |
59 | # Django stuff:
60 | *.log
61 | local_settings.py
62 | db.sqlite3
63 | db.sqlite3-journal
64 |
65 | # Flask stuff:
66 | instance/
67 | .webassets-cache
68 |
69 | # Scrapy stuff:
70 | .scrapy
71 |
72 | # Sphinx documentation
73 | docs/_build/
74 |
75 | # PyBuilder
76 | .pybuilder/
77 | target/
78 |
79 | # Jupyter Notebook
80 | .ipynb_checkpoints
81 |
82 | # IPython
83 | profile_default/
84 | ipython_config.py
85 |
86 | # pyenv
87 | # For a library or package, you might want to ignore these files since the code is
88 | # intended to run in multiple environments; otherwise, check them in:
89 | # .python-version
90 |
91 | # pipenv
92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
95 | # install all needed dependencies.
96 | #Pipfile.lock
97 |
98 | # poetry
99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
100 | # This is especially recommended for binary packages to ensure reproducibility, and is more
101 | # commonly ignored for libraries.
102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
103 | #poetry.lock
104 |
105 | # pdm
106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
107 | #pdm.lock
108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
109 | # in version control.
110 | # https://pdm.fming.dev/#use-with-ide
111 | .pdm.toml
112 |
113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
114 | __pypackages__/
115 |
116 | # Celery stuff
117 | celerybeat-schedule
118 | celerybeat.pid
119 |
120 | # SageMath parsed files
121 | *.sage.py
122 |
123 | # Environments
124 | .env
125 | .venv
126 | env/
127 | venv/
128 | ENV/
129 | env.bak/
130 | venv.bak/
131 |
132 | # Spyder project settings
133 | .spyderproject
134 | .spyproject
135 |
136 | # Rope project settings
137 | .ropeproject
138 |
139 | # mkdocs documentation
140 | /site
141 |
142 | # mypy
143 | .mypy_cache/
144 | .dmypy.json
145 | dmypy.json
146 |
147 | # Pyre type checker
148 | .pyre/
149 |
150 | # pytype static type analyzer
151 | .pytype/
152 |
153 | # Cython debug symbols
154 | cython_debug/
155 |
156 | # PyCharm
157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
159 | # and can be added to the global gitignore or merged into this file. For a more nuclear
160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
161 | .idea/
162 |
163 | # VSCode
164 | .vscode/
165 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | # ..readthedocs.yml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 |
6 | # Required
7 | version: 2
8 |
9 | sphinx:
10 | configuration: docs/conf.py
11 |
12 | formats: all
13 |
14 | build:
15 | os: ubuntu-22.04
16 | tools:
17 | python: "3.11"
18 | python:
19 | install:
20 | - requirements: docs/requirements.txt
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Cubewise CODE
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | TM1py is the python package for IBM Planning Analytics (TM1).
6 |
7 | ``` python
8 | with TM1Service(address='localhost', port=8001, user='admin', password='apple', ssl=True) as tm1:
9 | subset = Subset(dimension_name='Month', subset_name='Q1', elements=['Jan', 'Feb', 'Mar'])
10 | tm1.subsets.create(subset, private=True)
11 | ```
12 |
13 | Features
14 | =======================
15 |
16 | TM1py offers handy features to interact with TM1 from Python, such as
17 |
18 | - Functions to read data from cubes through cube views or MDX queries (e.g. `tm1.cells.execute_mdx`)
19 | - Functions to write data to cubes (e.g. `tm1.cells.write`)
20 | - Functions to update dimensions and hierarchies (e.g. `tm1.hierarchies.get`)
21 | - Functions to update metadata, clear or write to cubes directly from pandas dataframes (e.g. `tm1.elements.get_elements_dataframe`)
22 | - Async functions to easily parallelize your read or write operations (e.g. `tm1.cells.write_async`)
23 | - Functions to execute TI process or loose statements of TI (e.g. `tm1.processes.execute_with_return`)
24 | - CRUD features for all TM1 objects (cubes, dimensions, subsets, etc.)
25 |
26 | Requirements
27 | =======================
28 |
29 | - python (3.7 or higher)
30 | - requests
31 | - requests_negotiate_sspi
32 | - TM1 11, TM1 12
33 | - keyring
34 |
35 |
36 | Optional Requirements
37 | =======================
38 |
39 | - pandas
40 |
41 | Install
42 | =======================
43 |
44 | > without pandas
45 |
46 | pip install tm1py
47 |
48 | > with pandas
49 |
50 | pip install "tm1py[pandas]"
51 |
52 | > keyring
53 |
54 | pip install keyring
55 |
56 | Usage
57 | =======================
58 |
59 | > TM1 11 on-premise
60 |
61 | ``` python
62 | from TM1py.Services import TM1Service
63 |
64 | with TM1Service(address='localhost', port=8001, user='admin', password='apple', ssl=True) as tm1:
65 | print(tm1.server.get_product_version())
66 | ```
67 |
68 | > TM1 11 on IBM cloud
69 |
70 | ``` python
71 | with TM1Service(
72 | base_url='https://mycompany.planning-analytics.ibmcloud.com/tm1/api/tm1/',
73 | user="non_interactive_user",
74 | namespace="LDAP",
75 | password="U3lSn5QLwoQZY2",
76 | ssl=True,
77 | verify=True,
78 | async_requests_mode=True) as tm1:
79 | print(tm1.server.get_product_version())
80 | ```
81 |
82 |
83 | > TM1 12 PAaaS
84 |
85 | ``` python
86 | from TM1py import TM1Service
87 |
88 | params = {
89 | "base_url": "https://us-east-1.planninganalytics.saas.ibm.com/api//v0/tm1//",
90 | "user": "apikey",
91 | "password": "",
92 | "async_requests_mode": True,
93 | "ssl": True,
94 | "verify": True
95 | }
96 |
97 | with TM1Service(**params) as tm1:
98 | print(tm1.server.get_product_version())
99 | ```
100 |
101 | > TM1 12 on-premise & Cloud Pak For Data
102 |
103 | ``` python
104 | with TM1Service(
105 | address="tm1-ibm-operands-services.apps.cluster.your-cluster.company.com",
106 | instance="your instance name",
107 | database="your database name",
108 | application_client_id="client id",
109 | application_client_secret="client secret",
110 | user="admin",
111 | ssl=True) as tm1:
112 |
113 | print(tm1.server.get_product_version())
114 | ```
115 |
116 | > TM1 12 on-premise with access token
117 |
118 | ``` python
119 | params = {
120 | "base_url": "https://pa12.dev.net/api//v0/tm1/",
121 | "user": "8643fd6....8a6b",
122 | "access_token":"",
123 | "async_requests_mode": True,
124 | "ssl": True,
125 | "verify": True
126 | }
127 |
128 | with TM1Service(**params) as tm1:
129 | print(tm1.server.get_product_version())
130 | ```
131 |
132 |
133 | Documentation
134 | =======================
135 |
136 | https://tm1py.readthedocs.io/en/master/
137 |
138 |
139 | Issues
140 | =======================
141 |
142 | If you find issues, sign up in GitHub and open an Issue in this repository
143 |
144 |
145 | Contribution
146 | =======================
147 |
148 | TM1py is an open source project. It thrives on contribution from the TM1 community.
149 | If you find a bug or feel like you can contribute please fork the repository, update the code and then create a pull request so we can merge in the changes.
150 |
--------------------------------------------------------------------------------
/TM1py/Exceptions/Exceptions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # TM1py Exceptions are defined here
4 | from typing import Mapping, List
5 |
6 |
7 | class TM1pyTimeout(Exception):
8 | def __init__(self, method: str, url: str, timeout: float):
9 | self.method = method
10 | self.url = url
11 | self.timeout = timeout
12 |
13 | def __str__(self):
14 | return f"Timeout after {self.timeout} seconds for '{self.method}' request with url :'{self.url}'"
15 |
16 |
17 | class TM1pyVersionException(Exception):
18 | def __init__(self, function: str, required_version, feature: str = None):
19 | self.function = function
20 | self.required_version = required_version
21 | self.feature = feature
22 |
23 | def __str__(self):
24 | require_string = f"requires TM1 server version >= '{self.required_version}'"
25 | if self.feature:
26 | return f"'{self.feature}' feature of function '{self.function}' {require_string}"
27 | else:
28 | return f"Function '{self.function}' {require_string}"
29 |
30 |
31 | class TM1pyVersionDeprecationException(Exception):
32 | def __init__(self, function: str, deprecated_in_version):
33 | self.function = function
34 | self.deprecated_in_version = deprecated_in_version
35 |
36 | def __str__(self):
37 | return f"Function '{self.function}' has been deprecated in TM1 server version >= '{self.deprecated_in_version}'"
38 |
39 |
40 | class TM1pyNotAdminException(Exception):
41 | def __init__(self, function: str):
42 | self.function = function
43 |
44 | def __str__(self):
45 | return f"Function '{self.function}' requires admin permissions"
46 |
47 | class TM1pyNotDataAdminException(Exception):
48 | def __init__(self, function: str):
49 | self.function = function
50 |
51 | def __str__(self):
52 | return f"Function '{self.function}' requires DataAdmin permissions"
53 |
54 | class TM1pyNotSecurityAdminException(Exception):
55 | def __init__(self, function: str):
56 | self.function = function
57 |
58 | def __str__(self):
59 | return f"Function '{self.function}' requires SecurityAdmin permissions"
60 |
61 | class TM1pyNotOpsAdminException(Exception):
62 | def __init__(self, function: str):
63 | self.function = function
64 |
65 | def __str__(self):
66 | return f"Function '{self.function}' requires OperationsAdmin permissions"
67 |
68 | class TM1pyException(Exception):
69 | """ The default exception for TM1py
70 |
71 | """
72 |
73 | def __init__(self, message):
74 | self.message = message
75 |
76 | def __str__(self):
77 | return self.message
78 |
79 |
80 | class TM1pyRestException(TM1pyException):
81 | """ Exception for failing REST operations
82 |
83 | """
84 |
85 | def __init__(self, response: str, status_code: int, reason: str, headers: Mapping):
86 | super(TM1pyRestException, self).__init__(response)
87 | self._status_code = status_code
88 | self._reason = reason
89 | self._headers = headers
90 |
91 | @property
92 | def status_code(self):
93 | return self._status_code
94 |
95 | @property
96 | def reason(self):
97 | return self._reason
98 |
99 | @property
100 | def response(self):
101 | return self.message
102 |
103 | @property
104 | def headers(self):
105 | return self._headers
106 |
107 | def __str__(self):
108 | return "Text: '{}' - Status Code: {} - Reason: '{}' - Headers: {}".format(
109 | self.message,
110 | self._status_code,
111 | self._reason,
112 | self._headers)
113 |
114 |
115 | class TM1pyWriteFailureException(TM1pyException):
116 |
117 | def __init__(self, statuses: List[str], error_log_files: List[str]):
118 | self.statuses = statuses
119 | self.error_log_files = error_log_files
120 |
121 | message = f"All {len(self.statuses)} write operations failed. Details: {self.error_log_files}"
122 | super(TM1pyWriteFailureException, self).__init__(message)
123 |
124 |
125 | class TM1pyWritePartialFailureException(TM1pyException):
126 |
127 | def __init__(self, statuses: List[str], error_log_files: List[str], attempts: int):
128 | self.statuses = statuses
129 | self.error_log_files = error_log_files
130 | self.attempts = attempts
131 |
132 | message = f"{len(self.statuses)} out of {self.attempts} write operations failed partially. " \
133 | f"Details: {self.error_log_files}"
134 | super(TM1pyWritePartialFailureException, self).__init__(message)
135 |
--------------------------------------------------------------------------------
/TM1py/Exceptions/__init__.py:
--------------------------------------------------------------------------------
1 | from TM1py.Exceptions.Exceptions import TM1pyRestException, TM1pyException, TM1pyTimeout, TM1pyVersionException, \
2 | TM1pyNotAdminException, TM1pyWriteFailureException, TM1pyWritePartialFailureException
3 |
--------------------------------------------------------------------------------
/TM1py/Objects/Annotation.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import collections
4 | import json
5 | from typing import Iterable, Dict, List
6 |
7 | from TM1py.Objects.TM1Object import TM1Object
8 | from TM1py.Utils import format_url
9 |
10 |
11 | class Annotation(TM1Object):
12 | """ Abtraction of TM1 Annotation
13 |
14 | :Notes:
15 | - Class complete, functional and tested.
16 | - doesn't cover Attachments though
17 | """
18 |
19 | def __init__(self, comment_value: str, object_name: str, dimensional_context: Iterable[str],
20 | comment_type: str = 'ANNOTATION', annotation_id: str = None,
21 | text: str = '', creator: str = None, created: str = None, last_updated_by: str = None,
22 | last_updated: str = None):
23 | self._id = annotation_id
24 | self._text = text
25 | self._creator = creator
26 | self._created = created
27 | self._last_updated_by = last_updated_by
28 | self._last_updated = last_updated
29 | self._dimensional_context = list(dimensional_context)
30 | self._comment_type = comment_type
31 | self._comment_value = comment_value
32 | self._object_name = object_name
33 |
34 | @classmethod
35 | def from_json(cls, annotation_as_json: str) -> 'Annotation':
36 | """ Alternative constructor
37 |
38 | :param annotation_as_json: String, JSON
39 | :return: instance of TM1py.Process
40 | """
41 | annotation_as_dict = json.loads(annotation_as_json)
42 | annotation_id = annotation_as_dict['ID']
43 | text = annotation_as_dict['Text']
44 | creator = annotation_as_dict['Creator']
45 | created = annotation_as_dict['Created']
46 | last_updated_by = annotation_as_dict['LastUpdatedBy']
47 | last_updated = annotation_as_dict['LastUpdated']
48 | dimensional_context = [item['Name'] for item in annotation_as_dict['DimensionalContext']]
49 | comment_type = annotation_as_dict['commentType']
50 | comment_value = annotation_as_dict['commentValue']
51 | object_name = annotation_as_dict['objectName']
52 | return cls(comment_value=comment_value, object_name=object_name, dimensional_context=dimensional_context,
53 | comment_type=comment_type, annotation_id=annotation_id, text=text, creator=creator, created=created,
54 | last_updated_by=last_updated_by, last_updated=last_updated)
55 |
56 | @property
57 | def body(self) -> str:
58 | return json.dumps(self._construct_body())
59 |
60 | @property
61 | def body_as_dict(self) -> Dict:
62 | return self._construct_body()
63 |
64 | @property
65 | def comment_value(self) -> str:
66 | return self._comment_value
67 |
68 | @property
69 | def text(self) -> str:
70 | return self._text
71 |
72 | @property
73 | def dimensional_context(self) -> List[str]:
74 | return self._dimensional_context
75 |
76 | @property
77 | def created(self) -> str:
78 | return self._created
79 |
80 | @property
81 | def object_name(self) -> str:
82 | return self._object_name
83 |
84 | @property
85 | def last_updated(self) -> str:
86 | return self._last_updated
87 |
88 | @property
89 | def last_updated_by(self) -> str:
90 | return self._last_updated_by
91 |
92 | @comment_value.setter
93 | def comment_value(self, value: str):
94 | self._comment_value = value
95 |
96 | @property
97 | def id(self) -> str:
98 | return self._id
99 |
100 | def move(self, dimension_order: Iterable[str], dimension: str, target_element: str, source_element: str = None):
101 | """ Move annotation on given dimension from source_element to target_element
102 |
103 | :param dimension_order: List, order of the dimensions in the cube
104 | :param dimension: dimension name
105 | :param target_element: target element name
106 | :param source_element: source element name
107 | :return:
108 | """
109 | for i, dimension_name in enumerate(dimension_order):
110 | if dimension_name.lower() == dimension.lower():
111 | if not source_element or self._dimensional_context[i] == source_element:
112 | self._dimensional_context[i] = target_element
113 |
114 | def _construct_body(self) -> Dict:
115 | """ construct the ODATA conform JSON represenation for the Annotation entity.
116 |
117 | :return: string, the valid JSON
118 | """
119 | dimensional_context = [{'Name': element} for element in self._dimensional_context]
120 | body = collections.OrderedDict()
121 | body['ID'] = self._id
122 | body['Text'] = self._text
123 | body['Creator'] = self._creator
124 | body['Created'] = self._created
125 | body['LastUpdatedBy'] = self._last_updated_by
126 | body['LastUpdated'] = self._last_updated
127 | body['DimensionalContext'] = dimensional_context
128 | comment_locations = ','.join(self._dimensional_context)
129 | body['commentLocation'] = comment_locations[1:]
130 | body['commentType'] = self._comment_type
131 | body['commentValue'] = self._comment_value
132 | body['objectName'] = self._object_name
133 | return body
134 |
135 | def construct_body_for_post(self, cube_dimensions) -> Dict:
136 | body = collections.OrderedDict()
137 | body["Text"] = self.text
138 | body["ApplicationContext"] = [{
139 | "Facet@odata.bind": "ApplicationContextFacets('}Cubes')",
140 | "Value": self.object_name}]
141 | body["DimensionalContext@odata.bind"] = []
142 |
143 | for dimension, element in zip(cube_dimensions, self.dimensional_context):
144 | coordinates = format_url("Dimensions('{}')/Hierarchies('{}')/Members('{}')", dimension, dimension, element)
145 | body["DimensionalContext@odata.bind"].append(coordinates)
146 |
147 | body['objectName'] = self.object_name
148 | body['commentValue'] = self.comment_value
149 | body['commentType'] = 'ANNOTATION'
150 | body['commentLocation'] = ','.join(self.dimensional_context)
151 |
152 | return body
153 |
--------------------------------------------------------------------------------
/TM1py/Objects/Axis.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import collections
4 | import json
5 | from typing import Dict, Union
6 |
7 | from TM1py.Objects.Subset import Subset, AnonymousSubset
8 | from TM1py.Objects.TM1Object import TM1Object
9 | from TM1py.Utils import format_url
10 |
11 |
12 | class ViewAxisSelection(TM1Object):
13 | """ Describes what is selected in a dimension on an axis. Can be a Registered Subset or an Anonymous Subset
14 |
15 | """
16 |
17 | def __init__(self, dimension_name: str, subset: Union[Subset, AnonymousSubset]):
18 | """
19 | :Parameters:
20 | `dimension_name` : String
21 | `subset` : Subset or AnonymousSubset
22 | """
23 | self._subset = subset
24 | self._dimension_name = dimension_name
25 | self._hierarchy_name = dimension_name
26 |
27 | @property
28 | def subset(self) -> Union[Subset, AnonymousSubset]:
29 | return self._subset
30 |
31 | @property
32 | def dimension_name(self) -> str:
33 | return self._dimension_name
34 |
35 | @property
36 | def hierarchy_name(self) -> str:
37 | return self._hierarchy_name
38 |
39 | @property
40 | def body(self) -> str:
41 | return json.dumps(self._construct_body(), ensure_ascii=False)
42 |
43 | @property
44 | def body_as_dict(self) -> Dict:
45 | return self._construct_body()
46 |
47 | def _construct_body(self) -> Dict:
48 | """ construct the ODATA conform JSON represenation for the ViewAxisSelection entity.
49 |
50 | :return: dictionary
51 | """
52 | body_as_dict = collections.OrderedDict()
53 | if isinstance(self._subset, AnonymousSubset):
54 | body_as_dict['Subset'] = json.loads(self._subset.body)
55 | elif isinstance(self._subset, Subset):
56 | subset_path = format_url(
57 | "Dimensions('{}')/Hierarchies('{}')/Subsets('{}')",
58 | self._dimension_name, self._hierarchy_name, self._subset.name)
59 | body_as_dict['Subset@odata.bind'] = subset_path
60 | return body_as_dict
61 |
62 |
63 | class ViewTitleSelection:
64 | """ Describes what is selected in a dimension on the view title.
65 | Can be a Registered Subset or an Anonymous Subset
66 |
67 | """
68 |
69 | def __init__(self, dimension_name: str, subset: Union[AnonymousSubset, Subset], selected: str):
70 | self._dimension_name = dimension_name
71 | self._hierarchy_name = dimension_name
72 | self._subset = subset
73 | self._selected = selected
74 |
75 | @property
76 | def subset(self) -> Union[Subset, AnonymousSubset]:
77 | return self._subset
78 |
79 | @property
80 | def dimension_name(self) -> str:
81 | return self._dimension_name
82 |
83 | @property
84 | def hierarchy_name(self) -> str:
85 | return self._hierarchy_name
86 |
87 | @property
88 | def selected(self) -> str:
89 | return self._selected
90 |
91 | @property
92 | def body(self) -> str:
93 | return json.dumps(self._construct_body(), ensure_ascii=False)
94 |
95 | def _construct_body(self) -> Dict:
96 | """ construct the ODATA conform JSON represenation for the ViewTitleSelection entity.
97 |
98 | :return: string, the valid JSON
99 | """
100 | body_as_dict = collections.OrderedDict()
101 | if isinstance(self._subset, AnonymousSubset):
102 | body_as_dict['Subset'] = json.loads(self._subset.body)
103 | elif isinstance(self._subset, Subset):
104 | subset_path = format_url(
105 | "Dimensions('{}')/Hierarchies('{}')/Subsets('{}')",
106 | self._dimension_name, self._hierarchy_name, self._subset.name)
107 | body_as_dict['Subset@odata.bind'] = subset_path
108 | element_path = format_url(
109 | "Dimensions('{}')/Hierarchies('{}')/Elements('{}')",
110 | self._dimension_name, self._hierarchy_name, self._selected)
111 | body_as_dict['Selected@odata.bind'] = element_path
112 | return body_as_dict
113 |
--------------------------------------------------------------------------------
/TM1py/Objects/Chore.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import collections
4 | import json
5 | from typing import Dict, List, Iterable
6 |
7 | from TM1py.Objects.ChoreFrequency import ChoreFrequency
8 | from TM1py.Objects.ChoreStartTime import ChoreStartTime
9 | from TM1py.Objects.ChoreTask import ChoreTask
10 | from TM1py.Objects.TM1Object import TM1Object
11 |
12 |
13 | class Chore(TM1Object):
14 | """ Abstraction of TM1 Chore
15 |
16 | """
17 | SINGLE_COMMIT = 'SingleCommit'
18 | MULTIPLE_COMMIT = 'MultipleCommit'
19 |
20 | def __init__(self, name: str, start_time: ChoreStartTime, dst_sensitivity: bool, active: bool,
21 | execution_mode: str, frequency: ChoreFrequency, tasks: Iterable[ChoreTask]):
22 | self._name = name
23 | self._start_time = start_time
24 | self._dst_sensitivity = dst_sensitivity
25 | self._active = active
26 | self._execution_mode = execution_mode
27 | self._frequency = frequency
28 | self._tasks = list(tasks)
29 |
30 | @classmethod
31 | def from_json(cls, chore_as_json: str) -> 'Chore':
32 | """ Alternative constructor
33 |
34 | :param chore_as_json: string, JSON. Response of /Chores('x')/Tasks?$expand=*
35 | :return: Chore, an instance of this class
36 | """
37 | chore_as_dict = json.loads(chore_as_json)
38 | return cls.from_dict(chore_as_dict)
39 |
40 | @classmethod
41 | def from_dict(cls, chore_as_dict: Dict) -> 'Chore':
42 | """ Alternative constructor
43 |
44 | :param chore_as_dict: Chore as dict
45 | :return: Chore, an instance of this class
46 | """
47 | return cls(name=chore_as_dict['Name'],
48 | start_time=ChoreStartTime.from_string(chore_as_dict['StartTime']),
49 | dst_sensitivity=chore_as_dict['DSTSensitive'],
50 | active=chore_as_dict['Active'],
51 | execution_mode=chore_as_dict['ExecutionMode'],
52 | frequency=ChoreFrequency.from_string(chore_as_dict['Frequency']),
53 | tasks=[ChoreTask.from_dict(task, step)
54 | for step, task in
55 | enumerate(chore_as_dict['Tasks'])])
56 |
57 | @property
58 | def name(self) -> str:
59 | return self._name
60 |
61 | @name.setter
62 | def name(self, name: str):
63 | self._name = name
64 |
65 | @property
66 | def start_time(self) -> ChoreStartTime:
67 | return self._start_time
68 |
69 | @start_time.setter
70 | def start_time(self, start_time: ChoreStartTime):
71 | self._start_time = start_time
72 |
73 | @property
74 | def dst_sensitivity(self) -> bool:
75 | return self._dst_sensitivity
76 |
77 | @dst_sensitivity.setter
78 | def dst_sensitivity(self, dst_sensitivity: bool):
79 | self._dst_sensitivity = dst_sensitivity
80 |
81 | @property
82 | def active(self) -> bool:
83 | return self._active
84 |
85 | @property
86 | def execution_mode(self) -> str:
87 | return self._execution_mode
88 |
89 | @execution_mode.setter
90 | def execution_mode(self, execution_mode):
91 | self._execution_mode = execution_mode
92 |
93 | @property
94 | def frequency(self) -> ChoreFrequency:
95 | return self._frequency
96 |
97 | @frequency.setter
98 | def frequency(self, frequency: ChoreFrequency):
99 | self._frequency = frequency
100 |
101 | @property
102 | def tasks(self) -> List[ChoreTask]:
103 | return self._tasks
104 |
105 | @tasks.setter
106 | def tasks(self, tasks: List[ChoreTask]):
107 | self._tasks = tasks
108 |
109 | @property
110 | def body(self) -> str:
111 | return self.construct_body()
112 |
113 | @property
114 | def body_as_dict(self) -> Dict:
115 | return json.loads(self.body)
116 |
117 | @property
118 | def execution_path(self) -> Dict:
119 | """
120 | 1 chore together with its executed processes
121 | Use case: building out a tree of chores and their processes (and again the processes that are called by the latter (if any)).
122 | :return: dictionary containing chore name as the key and a list of process names as the value
123 | """
124 | return {self.name: [task.process_name for task in self.tasks]}
125 |
126 | def add_task(self, task: ChoreTask):
127 | self._tasks.append(task)
128 |
129 | def insert_task(self, new_task: ChoreTask):
130 | task_list = self.tasks
131 | for task in task_list[new_task._step:]:
132 | task._step = task._step + 1
133 | task_list.insert(new_task._step, new_task)
134 | self.tasks = task_list
135 |
136 | def activate(self):
137 | self._active = True
138 |
139 | def deactivate(self):
140 | self._active = False
141 |
142 | def reschedule(self, days: int = 0, hours: int = 0, minutes: int = 0, seconds: int = 0):
143 | self._start_time.add(days=days, hours=hours, minutes=minutes, seconds=seconds)
144 |
145 | def construct_body(self) -> str:
146 | """
147 | construct self.body (json) from the class attributes
148 | :return: String, TM1 JSON representation of a chore
149 | """
150 | body_as_dict = collections.OrderedDict()
151 | body_as_dict['Name'] = self._name
152 | body_as_dict['StartTime'] = self._start_time.start_time_string
153 | body_as_dict['DSTSensitive'] = self._dst_sensitivity
154 | body_as_dict['Active'] = self._active
155 | body_as_dict['ExecutionMode'] = self._execution_mode
156 | body_as_dict['Frequency'] = self._frequency.frequency_string
157 | body_as_dict['Tasks'] = [task.body_as_dict for task in self._tasks]
158 | return json.dumps(body_as_dict, ensure_ascii=False)
159 |
--------------------------------------------------------------------------------
/TM1py/Objects/ChoreFrequency.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typing import Union
3 |
4 | from TM1py.Objects.TM1Object import TM1Object
5 |
6 |
7 | class ChoreFrequency(TM1Object):
8 | """ Utility class to handle time representation fore Chore Frequency
9 |
10 | """
11 |
12 | def __init__(self, days: Union[str, int], hours: Union[str, int], minutes: Union[str, int],
13 | seconds: Union[str, int]):
14 | self._days = str(days).zfill(2)
15 | self._hours = str(hours).zfill(2)
16 | self._minutes = str(minutes).zfill(2)
17 | self._seconds = str(seconds).zfill(2)
18 |
19 | @property
20 | def days(self) -> str:
21 | return self._days
22 |
23 | @property
24 | def hours(self) -> str:
25 | return self._hours
26 |
27 | @property
28 | def minutes(self) -> str:
29 | return self._minutes
30 |
31 | @property
32 | def seconds(self) -> str:
33 | return self._seconds
34 |
35 | @days.setter
36 | def days(self, value: Union[str, int]):
37 | self._days = str(value).zfill(2)
38 |
39 | @hours.setter
40 | def hours(self, value: Union[str, int]):
41 | self._hours = str(value).zfill(2)
42 |
43 | @minutes.setter
44 | def minutes(self, value: Union[str, int]):
45 | self._minutes = str(value).zfill(2)
46 |
47 | @seconds.setter
48 | def seconds(self, value: Union[str, int]):
49 | self._seconds = str(value).zfill(2)
50 |
51 | @classmethod
52 | def from_string(cls, frequency_string: str) -> 'ChoreFrequency':
53 | pos_dt = frequency_string.find('DT', 1)
54 | pos_h = frequency_string.find('H', pos_dt)
55 | pos_m = frequency_string.find('M', pos_h)
56 | pos_s = len(frequency_string) - 1
57 | return cls(days=frequency_string[1:pos_dt],
58 | hours=frequency_string[pos_dt + 2:pos_h],
59 | minutes=frequency_string[pos_h + 1:pos_m],
60 | seconds=frequency_string[pos_m + 1:pos_s])
61 |
62 | @property
63 | def frequency_string(self) -> str:
64 | return "P{}DT{}H{}M{}S".format(self._days, self._hours, self._minutes, self._seconds)
65 |
66 | def __str__(self) -> str:
67 | return self.frequency_string
68 |
--------------------------------------------------------------------------------
/TM1py/Objects/ChoreStartTime.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import datetime
4 |
5 |
6 | class ChoreStartTime:
7 | """ Utility class to handle time representation for Chore Start Time
8 |
9 | """
10 |
11 | def __init__(self, year: int, month: int, day: int, hour: int, minute: int, second: int, tz: str = None):
12 | """
13 |
14 | :param year: year
15 | :param month: month
16 | :param day: day
17 | :param hour: hour or None
18 | :param minute: minute or None
19 | :param second: second or None
20 | """
21 | self._datetime = datetime.datetime.combine(datetime.date(year, month, day), datetime.time(hour, minute, second))
22 | self.tz = tz
23 |
24 | @classmethod
25 | def from_string(cls, start_time_string: str) -> 'ChoreStartTime':
26 | # extract optional tz info (e.g., +01:00) from string end
27 | if '+' in start_time_string:
28 | # case "2020-11-05T08:00:01+01:00",
29 | tz = "+" + start_time_string.split('+')[1]
30 | elif start_time_string.count('-') == 3:
31 | # case: "2020-11-05T08:00:01-01:00",
32 | tz = "-" + start_time_string.split('-')[-1]
33 | else:
34 | tz = None
35 |
36 | # f to handle strange timestamp 2016-09-25T20:25Z instead of common 2016-09-25T20:25:00Z
37 | # second is defaulted to 0 if not specified in the chore schedule
38 | f = lambda x: int(x) if x else 0
39 | return cls(year=f(start_time_string[0:4]),
40 | month=f(start_time_string[5:7]),
41 | day=f(start_time_string[8:10]),
42 | hour=f(start_time_string[11:13]),
43 | minute=f(start_time_string[14:16]),
44 | second=f(0 if start_time_string[16] != ":" else start_time_string[17:19]),
45 | tz=tz)
46 |
47 | @property
48 | def start_time_string(self) -> str:
49 | # produce timestamp 2016-09-25T20:25:00Z instead of common 2016-09-25T20:25Z where no seconds are specified
50 | start_time = self._datetime.strftime("%Y-%m-%dT%H:%M:%S")
51 |
52 | if self.tz:
53 | start_time += self.tz
54 | else:
55 | start_time += "Z"
56 |
57 | return start_time
58 |
59 | @property
60 | def datetime(self) -> datetime:
61 | return self._datetime
62 |
63 | def __str__(self):
64 | return self.start_time_string
65 |
66 | def set_time(self, year: int = None, month: int = None, day: int = None, hour: int = None, minute: int = None,
67 | second: int = None):
68 |
69 | _year = year if year is not None else self._datetime.year
70 | _month = month if month is not None else self._datetime.month
71 | _day = day if day is not None else self._datetime.day
72 | _hour = hour if hour is not None else self._datetime.hour
73 | _minute = minute if minute is not None else self._datetime.minute
74 | _second = second if second is not None else self._datetime.second
75 |
76 | self._datetime = self._datetime.replace(year=_year, month=_month, day=_day, hour=_hour, minute=_minute,
77 | second=_second)
78 |
79 | def add(self, days: int = 0, hours: int = 0, minutes: int = 0, seconds: int = 0):
80 | self._datetime = self._datetime + datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
81 |
82 | def subtract(self, days: int = 0, hours: int = 0, minutes: int = 0, seconds: int = 0):
83 | self._datetime = self._datetime - datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
--------------------------------------------------------------------------------
/TM1py/Objects/ChoreTask.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import collections
4 | import json
5 | from typing import Dict, List
6 |
7 | from TM1py.Objects.TM1Object import TM1Object
8 | from TM1py.Utils import format_url
9 |
10 |
11 | class ChoreTask(TM1Object):
12 | """ Abstraction of a Chore Task
13 |
14 | A Chore task always conistst of
15 | - The step integer ID: it's order in the execution plan.
16 | 1 to n, where n is the last Process in the Chore
17 | - The name of the process to execute
18 | - The parameters for the process
19 |
20 | """
21 |
22 | def __init__(self, step: int, process_name: str, parameters: List[Dict[str, str]]):
23 | """
24 |
25 | :param step: step in the execution order of the Chores' processes. 1 to n, where n the number of processes
26 | :param process_name: name of the process
27 | :param parameters: list of dictionaries with 'Name' and 'Value' property:
28 | [{
29 | 'Name': '..',
30 | 'Value': '..'
31 | },
32 | ...
33 | ]
34 | """
35 | self._step = step
36 | self._process_name = process_name
37 | self._parameters = parameters
38 |
39 | @classmethod
40 | def from_dict(cls, chore_task_as_dict: Dict, step: int = None):
41 | if 'Process' in chore_task_as_dict:
42 | process_name = chore_task_as_dict['Process']['Name']
43 | else:
44 | # Extract "ProcessName" from "Processes('ProcessName')"
45 | process_name = chore_task_as_dict['Process@odata.bind'][11:-2]
46 |
47 | return cls(step=step if step is not None else int(chore_task_as_dict['Step']),
48 | process_name=process_name,
49 | parameters=[{'Name': p['Name'], 'Value': p['Value']} for p in chore_task_as_dict['Parameters']])
50 |
51 | @property
52 | def body_as_dict(self) -> Dict:
53 | body_as_dict = collections.OrderedDict()
54 | body_as_dict['Process@odata.bind'] = format_url("Processes('{}')", self._process_name)
55 | body_as_dict['Parameters'] = self._parameters
56 | return body_as_dict
57 |
58 | @property
59 | def step(self) -> int:
60 | return self._step
61 |
62 | @property
63 | def process_name(self) -> str:
64 | return self._process_name
65 |
66 | @property
67 | def parameters(self) -> List[Dict[str, str]]:
68 | return self._parameters
69 |
70 | @property
71 | def body(self) -> str:
72 | return json.dumps(self.body_as_dict, ensure_ascii=False)
73 |
74 | def __eq__(self, other: 'ChoreTask') -> bool:
75 | return self.process_name == other.process_name and self.parameters == other.parameters
76 |
77 | def __ne__(self, other: 'ChoreTask') -> bool:
78 | return self.process_name != other.process_name or self._parameters != other.parameters
79 |
--------------------------------------------------------------------------------
/TM1py/Objects/Cube.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import collections
4 | import json
5 | from typing import Iterable, List, Dict, Optional, Union
6 |
7 | from TM1py.Objects.Rules import Rules
8 | from TM1py.Objects.TM1Object import TM1Object
9 | from TM1py.Utils import format_url
10 |
11 |
12 | class Cube(TM1Object):
13 | """ Abstraction of a TM1 Cube
14 |
15 | """
16 |
17 | def __init__(self, name: str, dimensions: Iterable[str], rules: Optional[Union[str, Rules]] = None):
18 | """
19 |
20 | :param name: name of the Cube
21 | :param dimensions: list of (existing) dimension names
22 | :param rules: instance of TM1py.Objects.Rules
23 | """
24 | self._name = name
25 | self.dimensions = list(dimensions)
26 | self.rules = rules
27 |
28 | @property
29 | def name(self) -> str:
30 | return self._name
31 |
32 | @property
33 | def dimensions(self) -> List[str]:
34 | return self._dimensions
35 |
36 | @dimensions.setter
37 | def dimensions(self, value: List[str]):
38 | self._dimensions = value
39 |
40 | @property
41 | def has_rules(self) -> bool:
42 | if self._rules:
43 | return True
44 | return False
45 |
46 | @property
47 | def rules(self) -> Rules:
48 | return self._rules
49 |
50 | @rules.setter
51 | def rules(self, value: Union[str, Rules]):
52 | if value is None:
53 | self._rules = None
54 | elif isinstance(value, str):
55 | self._rules = Rules(rules=value)
56 | elif isinstance(value, Rules):
57 | self._rules = value
58 | else:
59 | raise ValueError('value must None or of type str or Rules')
60 |
61 | @property
62 | def skipcheck(self) -> bool:
63 | if self.has_rules:
64 | return self.rules.skipcheck
65 | return False
66 |
67 | @property
68 | def undefvals(self) -> bool:
69 | if self.has_rules:
70 | return self.rules.undefvals
71 | return False
72 |
73 | @property
74 | def feedstrings(self) -> bool:
75 | if self.has_rules:
76 | return self.rules.feedstrings
77 | return False
78 |
79 | @classmethod
80 | def from_json(cls, cube_as_json: str) -> 'Cube':
81 | """ Alternative constructor
82 |
83 | :param cube_as_json: user as JSON string
84 | :return: cube, an instance of this class
85 | """
86 | cube_as_dict = json.loads(cube_as_json)
87 | return cls.from_dict(cube_as_dict)
88 |
89 | @classmethod
90 | def from_dict(cls, cube_as_dict: Dict) -> 'Cube':
91 | """ Alternative constructor
92 |
93 | :param cube_as_dict: user as dict
94 | :return: user, an instance of this class
95 | """
96 | return cls(
97 | name=cube_as_dict['Name'],
98 | dimensions=[dimension['Name'] for dimension in cube_as_dict['Dimensions']],
99 | rules=Rules(cube_as_dict['Rules']) if cube_as_dict['Rules'] else None)
100 |
101 | @property
102 | def body(self) -> str:
103 | return self._construct_body()
104 |
105 | def _construct_body(self) -> str:
106 | """
107 | construct body (json) from the class attributes
108 | :return: String, TM1 JSON representation of a cube
109 | """
110 | body_as_dict = collections.OrderedDict()
111 | body_as_dict['Name'] = self.name
112 | body_as_dict['Dimensions@odata.bind'] = [format_url("Dimensions('{}')", dimension)
113 | for dimension
114 | in self.dimensions]
115 | if self.has_rules:
116 | body_as_dict['Rules'] = str(self.rules)
117 | return json.dumps(body_as_dict, ensure_ascii=False)
118 |
--------------------------------------------------------------------------------
/TM1py/Objects/Dimension.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import collections
4 | import json
5 | from typing import Optional, Iterable, Dict, List
6 |
7 | from TM1py.Objects.Hierarchy import Hierarchy
8 | from TM1py.Objects.TM1Object import TM1Object
9 | from TM1py.Utils.Utils import case_and_space_insensitive_equals
10 |
11 |
12 | class Dimension(TM1Object):
13 | """ Abstraction of TM1 Dimension
14 |
15 | A Dimension is a container for hierarchies.
16 | """
17 |
18 | def __init__(self, name: str, hierarchies: Optional[Iterable[Hierarchy]] = None):
19 | """ Abstraction of TM1 Dimension
20 |
21 |
22 | :param name: Name of the dimension
23 | :param hierarchies: List of TM1py.Objects.Hierarchy instances
24 | """
25 | self._name = name
26 | self._hierarchies = list(hierarchies) if hierarchies else []
27 | self._attributes = {'Caption': name}
28 |
29 | @classmethod
30 | def from_json(cls, dimension_as_json: str) -> 'Dimension':
31 | dimension_as_dict = json.loads(dimension_as_json)
32 | return cls.from_dict(dimension_as_dict)
33 |
34 | @classmethod
35 | def from_dict(cls, dimension_as_dict: Dict) -> 'Dimension':
36 | return cls(name=dimension_as_dict['Name'],
37 | hierarchies=[Hierarchy.from_dict(hierarchy, dimension_name=dimension_as_dict['Name'])
38 | for hierarchy
39 | in dimension_as_dict['Hierarchies']])
40 |
41 | @property
42 | def name(self) -> str:
43 | return self._name
44 |
45 | @property
46 | def unique_name(self) -> str:
47 | return '[' + self._name + ']'
48 |
49 | @property
50 | def hierarchies(self) -> List[Hierarchy]:
51 | return self._hierarchies
52 |
53 | @property
54 | def hierarchy_names(self) -> List[str]:
55 | return [hierarchy.name for hierarchy in self._hierarchies]
56 |
57 | @property
58 | def default_hierarchy(self) -> Hierarchy:
59 | return self._hierarchies[0]
60 |
61 | @name.setter
62 | def name(self, value: str):
63 | for hierarchy in self.hierarchies:
64 | hierarchy._dimension_name = value
65 | if hierarchy.name == self._name:
66 | hierarchy.name = value
67 | self._name = value
68 |
69 | @property
70 | def body(self) -> str:
71 | return json.dumps(self._construct_body())
72 |
73 | @property
74 | def body_as_dict(self) -> Dict:
75 | return self._construct_body()
76 |
77 | def __iter__(self):
78 | return iter(self._hierarchies)
79 |
80 | def __len__(self):
81 | return len(self.hierarchies)
82 |
83 | def __contains__(self, item):
84 | return self.contains_hierarchy(item)
85 |
86 | def __getitem__(self, item):
87 | return self.get_hierarchy(item)
88 |
89 | def contains_hierarchy(self, hierarchy_name: str) -> bool:
90 | for hierarchy in self._hierarchies:
91 | if case_and_space_insensitive_equals(hierarchy.name, hierarchy_name):
92 | return True
93 | return False
94 |
95 | def get_hierarchy(self, hierarchy_name: str) -> Hierarchy:
96 | for hierarchy in self._hierarchies:
97 | if case_and_space_insensitive_equals(hierarchy.name, hierarchy_name):
98 | return hierarchy
99 | raise ValueError("Hierarchy: {} not found in dimension: {}".format(hierarchy_name, self.name))
100 |
101 | def add_hierarchy(self, hierarchy: Hierarchy):
102 | if self.contains_hierarchy(hierarchy.name):
103 | raise ValueError("Hierarchy: {} already exists in dimension: {}".format(hierarchy.name, self.name))
104 | self._hierarchies.append(hierarchy)
105 |
106 | def remove_hierarchy(self, hierarchy_name: str):
107 | if case_and_space_insensitive_equals(hierarchy_name, "leaves"):
108 | raise ValueError("'Leaves' hierarchy must not be removed from dimension")
109 |
110 | for num, hierarchy in enumerate(self._hierarchies):
111 | if case_and_space_insensitive_equals(hierarchy.name, hierarchy_name):
112 | del self._hierarchies[num]
113 | return
114 |
115 | def _construct_body(self, include_leaves_hierarchy=False) -> Dict:
116 | body_as_dict = collections.OrderedDict()
117 | body_as_dict["Name"] = self._name
118 | body_as_dict["UniqueName"] = self.unique_name
119 | body_as_dict["Attributes"] = self._attributes
120 | body_as_dict["Hierarchies"] = [
121 | hierarchy.body_as_dict
122 | for hierarchy
123 | in self.hierarchies if
124 | not case_and_space_insensitive_equals(hierarchy.name, "Leaves") or include_leaves_hierarchy]
125 | return body_as_dict
126 |
--------------------------------------------------------------------------------
/TM1py/Objects/Element.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import collections
4 | import json
5 | from enum import Enum
6 | from typing import Union, Dict, List
7 |
8 | from TM1py.Objects.TM1Object import TM1Object
9 | from TM1py.Utils import case_and_space_insensitive_equals
10 |
11 |
12 | class Element(TM1Object):
13 | """ Abstraction of TM1 Element
14 |
15 | """
16 | ELEMENT_ATTRIBUTES_PREFIX = "}ElementAttributes_"
17 |
18 | class Types(Enum):
19 | NUMERIC = 1
20 | STRING = 2
21 | CONSOLIDATED = 3
22 |
23 | def __str__(self):
24 | return self.name.capitalize()
25 |
26 | @classmethod
27 | def _missing_(cls, value: str):
28 | for member in cls:
29 | if member.name.lower() == value.replace(" ", "").lower():
30 | return member
31 | # default
32 | raise ValueError(f"Invalid element type: '{value}'")
33 |
34 | def __init__(self, name, element_type: Union[Types, str], attributes: List[str] = None, unique_name: str = None,
35 | index: int = None):
36 | self._name = name
37 | self._unique_name = unique_name
38 | self._index = index
39 | self._element_type = None
40 | self.element_type = element_type
41 | self._attributes = attributes
42 |
43 | @staticmethod
44 | def from_dict(element_as_dict: Dict) -> 'Element':
45 | return Element(name=element_as_dict['Name'],
46 | unique_name=element_as_dict.get('UniqueName', None),
47 | index=element_as_dict.get('Index', None),
48 | element_type=element_as_dict['Type'],
49 | attributes=element_as_dict.get('Attributes', None))
50 |
51 | @property
52 | def name(self) -> str:
53 | return self._name
54 |
55 | @name.setter
56 | def name(self, value: str):
57 | self._name = value
58 |
59 | @property
60 | def unique_name(self) -> str:
61 | return self._unique_name
62 |
63 | @property
64 | def index(self) -> int:
65 | return self._index
66 |
67 | @property
68 | def element_attributes(self) -> List[str]:
69 | return self._attributes
70 |
71 | @property
72 | def element_type(self) -> Types:
73 | return self._element_type
74 |
75 | @element_type.setter
76 | def element_type(self, value: Union[Types, str]):
77 | self._element_type = Element.Types(value)
78 |
79 | @property
80 | def body(self) -> str:
81 | return json.dumps(self._construct_body())
82 |
83 | @property
84 | def body_as_dict(self) -> Dict:
85 | return self._construct_body()
86 |
87 | def _construct_body(self) -> Dict:
88 | body_as_dict = collections.OrderedDict()
89 | body_as_dict['Name'] = self._name
90 | body_as_dict['Type'] = str(self._element_type)
91 | return body_as_dict
92 |
93 | def __eq__(self, other: 'Element'):
94 | return all([
95 | isinstance(other, Element),
96 | case_and_space_insensitive_equals(self.name, other.name),
97 | self.element_type == other.element_type])
98 |
99 | def __hash__(self):
100 | return super().__hash__()
101 |
--------------------------------------------------------------------------------
/TM1py/Objects/ElementAttribute.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import json
4 | from enum import Enum
5 | from typing import Dict, Union
6 |
7 | from TM1py.Objects.TM1Object import TM1Object
8 | from TM1py.Utils import case_and_space_insensitive_equals
9 |
10 |
11 | class ElementAttribute(TM1Object):
12 | """ Abstraction of TM1 Element Attributes
13 |
14 | """
15 |
16 | class Types(Enum):
17 | NUMERIC = 1
18 | STRING = 2
19 | ALIAS = 3
20 |
21 | def __str__(self):
22 | return self.name.capitalize()
23 |
24 | @classmethod
25 | def _missing_(cls, value: str):
26 | for member in cls:
27 | if member.name.lower() == value.replace(" ", "").lower():
28 | return member
29 | # default
30 | raise ValueError(f"Invalid attribute type: '{value}'")
31 |
32 | def __init__(self, name: str, attribute_type: Union[Types, str]):
33 | self.name = name
34 | self.attribute_type = attribute_type
35 |
36 | @property
37 | def name(self) -> str:
38 | return self._name
39 |
40 | @name.setter
41 | def name(self, value: str):
42 | self._name = value
43 |
44 | @property
45 | def attribute_type(self) -> str:
46 | return str(self._attribute_type)
47 |
48 | @attribute_type.setter
49 | def attribute_type(self, value: Union[Types, str]):
50 | self._attribute_type = ElementAttribute.Types(value)
51 |
52 | @property
53 | def body_as_dict(self) -> Dict:
54 | return {"Name": self._name, "Type": self.attribute_type}
55 |
56 | @property
57 | def body(self) -> str:
58 | return json.dumps(self.body_as_dict, ensure_ascii=False)
59 |
60 | @classmethod
61 | def from_json(cls, element_attribute_as_json: str) -> 'ElementAttribute':
62 | return cls.from_dict(json.loads(element_attribute_as_json))
63 |
64 | @classmethod
65 | def from_dict(cls, element_attribute_as_dict: Dict) -> 'ElementAttribute':
66 | return cls(name=element_attribute_as_dict['Name'],
67 | attribute_type=element_attribute_as_dict['Type'])
68 |
69 | def __eq__(self, other: Union[str, 'ElementAttribute']):
70 | if isinstance(other, str):
71 | return case_and_space_insensitive_equals(self.name, other)
72 | elif isinstance(other, ElementAttribute):
73 | return case_and_space_insensitive_equals(self.name, other.name)
74 | else:
75 | raise ValueError("Argument: 'other' must be of type str or ElementAttribute")
76 |
77 | def __hash__(self):
78 | return super().__hash__()
79 |
--------------------------------------------------------------------------------
/TM1py/Objects/Git.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typing import Dict
3 |
4 | from TM1py.Objects.GitCommit import GitCommit
5 | from TM1py.Objects.GitRemote import GitRemote
6 |
7 |
8 | class Git:
9 | """ Abstraction of Git object
10 | """
11 |
12 | def __init__(self, url: str, deployment: str, force: bool, deployed_commit: GitCommit, remote: GitRemote,
13 | config: dict = None):
14 | """ Initialize GIT object
15 | :param url: file or http(s) path to GIT repository
16 | :param deployment: name of selected deployment group
17 | :param force: whether or not Git context was forced
18 | :param deployed_commit: GitCommit object of the currently deployed commit
19 | :param remote: GitRemote object of the current remote
20 | :param config: Dictionary containing git configuration parameters
21 |
22 | """
23 | self._url = url
24 | self._deployment = deployment
25 | self._force = force
26 | self._deployed_commit = deployed_commit
27 | self._remote = remote
28 | self._config = config
29 |
30 | @property
31 | def url(self) -> str:
32 | return self._url
33 |
34 | @property
35 | def force(self) -> bool:
36 | return self._force
37 |
38 | @property
39 | def config(self) -> dict:
40 | return self._config
41 |
42 | @property
43 | def deployment(self) -> str:
44 | return self._deployment
45 |
46 | @property
47 | def deployed_commit(self) -> GitCommit:
48 | return self._deployed_commit
49 |
50 | @property
51 | def remote(self) -> GitRemote:
52 | return self._remote
53 |
54 | @classmethod
55 | def from_dict(cls, json_response: Dict) -> 'Git':
56 | deployed_commit = GitCommit(
57 | commit_id=json_response["DeployedCommit"].get("ID"),
58 | summary=json_response["DeployedCommit"].get("Summary"),
59 | author=json_response["DeployedCommit"].get("Author")
60 | )
61 |
62 | remote = GitRemote(
63 | connected=json_response["Remote"].get("Connected"),
64 | branches=json_response["Remote"].get("Branches"),
65 | tags=json_response["Remote"].get("Tags"),
66 | )
67 |
68 | git = Git(
69 | url=json_response["URL"],
70 | deployment=json_response["Deployment"],
71 | force=json_response["Deployment"],
72 | deployed_commit=deployed_commit,
73 | remote=remote)
74 |
75 | return git
76 |
--------------------------------------------------------------------------------
/TM1py/Objects/GitCommit.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 |
4 | class GitCommit:
5 | """ Abstraction of Git Commit
6 | """
7 |
8 | def __init__(self, commit_id: str, summary: str, author: str):
9 | """ Initialize GitCommit object
10 | :param commit_id: id of the commit
11 | :param summary: commit message
12 | :param author: the author of the commit
13 | """
14 | self._commit_id = commit_id
15 | self._summary = summary
16 | self._author = author
17 |
18 | @property
19 | def commit_id(self) -> str:
20 | return self._commit_id
21 |
22 | @property
23 | def summary(self) -> str:
24 | return self._summary
25 |
26 | @property
27 | def author(self) -> str:
28 | return self._author
29 |
--------------------------------------------------------------------------------
/TM1py/Objects/GitPlan.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typing import List
3 |
4 | from TM1py.Objects.GitCommit import GitCommit
5 |
6 |
7 | class GitPlan:
8 | """ Base GitPlan abstraction
9 | """
10 |
11 | def __init__(self, plan_id: str, branch: str, force: bool):
12 | """ Initialize GitPlan object
13 | :param plan_id: id of the Plan
14 | :param branch: current branch
15 | :param force: force git context reset
16 | """
17 | self._plan_id = plan_id
18 | self._branch = branch
19 | self._force = force
20 |
21 | @property
22 | def plan_id(self) -> str:
23 | return self._plan_id
24 |
25 | @property
26 | def branch(self) -> str:
27 | return self._branch
28 |
29 | @property
30 | def force(self) -> bool:
31 | return self._force
32 |
33 |
34 | class GitPushPlan(GitPlan):
35 | """ GitPushPlan abstraction based on GitPlan
36 | """
37 |
38 | def __init__(self, plan_id: str, branch: str, force: bool, new_branch: str, new_commit: GitCommit,
39 | parent_commit: GitCommit, source_files: List[str]):
40 | """ Initialize GitPushPlan object
41 | :param plan_id: id of the PushPlan
42 | :param branch: current branch to base the pushplan on
43 | :param force: force git context reset
44 | :param new_branch: the new branch that will be pushed to
45 | :param new_commit: GitCommit of the new commit
46 | :param parent_commit: The current commit in the branch
47 | :param source_files: list of included files in the push
48 | """
49 | self._new_branch = new_branch
50 | self._new_commit = new_commit
51 | self._parent_commit = parent_commit
52 | self._source_files = source_files
53 |
54 | super().__init__(plan_id=plan_id, branch=branch, force=force)
55 |
56 | @property
57 | def new_branch(self) -> str:
58 | return self._new_branch
59 |
60 | @property
61 | def new_commit(self) -> GitCommit:
62 | return self._new_commit
63 |
64 | @property
65 | def parent_commit(self) -> GitCommit:
66 | return self._parent_commit
67 |
68 | @property
69 | def source_files(self) -> List[str]:
70 | return self._source_files
71 |
72 |
73 | class GitPullPlan(GitPlan):
74 | """ GitPushPlan abstraction based on GitPlan
75 | """
76 |
77 | def __init__(self, plan_id: str, branch: str, force: bool, commit: GitCommit, operations: List[str]):
78 | """ Initialize GitPushPlan object
79 | :param plan_id: id of the PullPlan
80 | :param branch: current branch to base the pullplan on
81 | :param force: force git context reset
82 | :param commit: GitCommit of the commit to pull
83 | :param operations: list of changes made upon pulling
84 | """
85 | self._commit = commit
86 | self._operations = operations
87 |
88 | super().__init__(plan_id=plan_id, branch=branch, force=force)
89 |
90 | @property
91 | def commit(self) -> GitCommit:
92 | return self._commit
93 |
94 | @property
95 | def operations(self) -> List[str]:
96 | return self._operations
97 |
--------------------------------------------------------------------------------
/TM1py/Objects/GitRemote.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typing import List
3 |
4 |
5 | class GitRemote:
6 | """ Abstraction of GitRemote
7 | """
8 |
9 | def __init__(self, connected: bool, branches: List[str], tags: List[str]):
10 | """ Initialize GitRemote object
11 | :param connected: is Git connected to remote
12 | :param branches: list of remote branches
13 | :param tags: list of remote tags
14 | """
15 | self._connected = connected
16 | self._branches = branches
17 | self._tags = tags
18 |
19 | @property
20 | def connected(self) -> bool:
21 | return self._connected
22 |
23 | @property
24 | def branches(self) -> List[str]:
25 | return self._branches
26 |
27 | @property
28 | def tags(self) -> List[str]:
29 | return self._tags
30 |
--------------------------------------------------------------------------------
/TM1py/Objects/MDXView.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import collections
4 | import json
5 | import re
6 | from typing import Optional, Dict
7 |
8 | from TM1py.Objects.View import View
9 | from TM1py.Utils import case_and_space_insensitive_equals
10 |
11 |
12 | class MDXView(View):
13 | """ Abstraction on TM1 MDX view
14 |
15 | IMPORTANT. MDXViews can't be seen through the old TM1 clients (Archict, Perspectives). They do exist though!
16 | """
17 |
18 | def __init__(self, cube_name: str, view_name: str, MDX: str):
19 | View.__init__(self, cube_name, view_name)
20 | self._mdx = MDX
21 |
22 | @property
23 | def mdx(self):
24 | return self._mdx
25 |
26 | @mdx.setter
27 | def mdx(self, value: str):
28 | self._mdx = value
29 |
30 | @property
31 | def MDX(self) -> str:
32 | return self._mdx
33 |
34 | @MDX.setter
35 | def MDX(self, value: str):
36 | self._mdx = value
37 |
38 | @property
39 | def body(self) -> str:
40 | return self.construct_body()
41 |
42 | def substitute_title(self, dimension: str, hierarchy: str, element: str):
43 | """ dimension and hierarchy name are space sensitive!
44 |
45 | :param dimension:
46 | :param hierarchy:
47 | :param element:
48 | :return:
49 | """
50 | pattern = re.compile(r"\[" + dimension + r"\].\[" + hierarchy + r"\].\[(.*?)\]", re.IGNORECASE)
51 | findings = re.findall(pattern, self._mdx)
52 |
53 | if findings:
54 | self._mdx = re.sub(
55 | pattern=pattern,
56 | repl=f"[{dimension}].[{hierarchy}].[{element}]",
57 | string=self._mdx)
58 | return
59 |
60 | if hierarchy is None or case_and_space_insensitive_equals(dimension, hierarchy):
61 | pattern = re.compile(r"\[" + dimension + r"\].\[(.*?)\]", re.IGNORECASE)
62 | findings = re.findall(pattern, self._mdx)
63 | if findings:
64 | self._mdx = re.sub(
65 | pattern=pattern,
66 | repl=f"[{dimension}].[{element}]",
67 | string=self._mdx)
68 | return
69 |
70 | raise ValueError(f"No selection in title with dimension: '{dimension}' and hierarchy: '{hierarchy}'")
71 |
72 | @classmethod
73 | def from_json(cls, view_as_json: str, cube_name: Optional[str] = None) -> 'MDXView':
74 | view_as_dict = json.loads(view_as_json)
75 | return cls.from_dict(view_as_dict, cube_name)
76 |
77 | @classmethod
78 | def from_dict(cls, view_as_dict: Dict, cube_name: str = None) -> 'MDXView':
79 | return cls(cube_name=view_as_dict['Cube']['Name'] if not cube_name else cube_name,
80 | view_name=view_as_dict['Name'],
81 | MDX=view_as_dict['MDX'])
82 |
83 | def construct_body(self) -> str:
84 | mdx_view_as_dict = collections.OrderedDict()
85 | mdx_view_as_dict['@odata.type'] = 'ibm.tm1.api.v1.MDXView'
86 | mdx_view_as_dict['Name'] = self._name
87 | mdx_view_as_dict['MDX'] = self._mdx
88 | return json.dumps(mdx_view_as_dict, ensure_ascii=False)
89 |
--------------------------------------------------------------------------------
/TM1py/Objects/Rules.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import json
3 | from typing import List, Dict
4 |
5 | from TM1py.Objects.TM1Object import TM1Object
6 |
7 |
8 | class Rules(TM1Object):
9 | """
10 | Abstraction of Rules on a cube.
11 |
12 | rules_analytics
13 | A collection of rulestatements, where each statement is stored in uppercase without linebreaks.
14 | comments are not included.
15 |
16 | """
17 | KEYWORDS = ['SKIPCHECK', 'FEEDSTRINGS', 'UNDEFVALS', 'FEEDERS']
18 |
19 | def __init__(self, rules: str):
20 | self._text = rules
21 | self._rules_analytics = []
22 | self.init_analytics()
23 |
24 | # self._rules_analytics_upper serves for analysis on cube rules
25 | def init_analytics(self):
26 | text_without_comments = '\n'.join(
27 | [rule
28 | for rule in self._text.split('\n')
29 | if len(rule.strip()) > 0 and rule.strip()[0] != '#'])
30 | for statement in text_without_comments.split(';'):
31 | if len(statement.strip()) > 0:
32 | self._rules_analytics.append(statement.replace('\n', '').upper())
33 |
34 | @property
35 | def text(self) -> str:
36 | return self._text
37 |
38 | @property
39 | def rules_analytics(self) -> List[str]:
40 | return self._rules_analytics
41 |
42 | @property
43 | def rule_statements(self) -> List[str]:
44 | if self.has_feeders:
45 | return self.rules_analytics[:self._rules_analytics.index('FEEDERS')]
46 | return self.rules_analytics
47 |
48 | @property
49 | def feeder_statements(self) -> List[str]:
50 | if self.has_feeders:
51 | return self.rules_analytics[self._rules_analytics.index('FEEDERS') + 1:]
52 | return []
53 |
54 | @property
55 | def skipcheck(self) -> bool:
56 | for rule in self._rules_analytics[0:5]:
57 | if rule == 'SKIPCHECK':
58 | return True
59 | return False
60 |
61 | @property
62 | def undefvals(self) -> bool:
63 | for rule in self._rules_analytics[0:5]:
64 | if rule == 'UNDEFVALS':
65 | return True
66 | return False
67 |
68 | @property
69 | def feedstrings(self) -> bool:
70 | for rule in self._rules_analytics[0:5]:
71 | if rule == 'FEEDSTRINGS':
72 | return True
73 | return False
74 |
75 | @property
76 | def has_feeders(self) -> bool:
77 | if 'FEEDERS' in self._rules_analytics:
78 | # has feeders declaration
79 | feeders = self.rules_analytics[self._rules_analytics.index('FEEDERS'):]
80 | # has at least one actual feeder statements
81 | return len(feeders) > 1
82 | return False
83 |
84 | @property
85 | def body(self) -> str:
86 | return json.dumps(self.body_as_dict)
87 |
88 | @property
89 | def body_as_dict(self) -> Dict:
90 | return {'Rules': self.text}
91 |
92 | def __len__(self):
93 | return len(self.rules_analytics)
94 |
95 | # iterate through actual rule statments without linebreaks. Ignore comments.
96 | def __iter__(self):
97 | return iter(self.rules_analytics)
98 |
99 | def __str__(self):
100 | return self.text
101 |
--------------------------------------------------------------------------------
/TM1py/Objects/Sandbox.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import collections
4 | import json
5 | from typing import Dict
6 |
7 | from TM1py.Objects.TM1Object import TM1Object
8 |
9 |
10 | class Sandbox(TM1Object):
11 | """ Abstraction of a TM1 Sandbox
12 |
13 | """
14 |
15 | def __init__(
16 | self,
17 | name: str,
18 | include_in_sandbox_dimension: bool = True,
19 | loaded: bool = False,
20 | active: bool = False,
21 | queued: bool = False
22 | ):
23 | """
24 |
25 | :param name: name of the Sandbox
26 | :param include_in_sandbox_dimension:
27 | :params loaded, active, queued: leave default as false when creating sanbox
28 | """
29 | self.name = name
30 | self.include_in_sandbox_dimension = include_in_sandbox_dimension
31 | self.loaded = loaded
32 | self.active = active
33 | self.queued = queued
34 |
35 | @property
36 | def name(self) -> str:
37 | return self._name
38 |
39 | @name.setter
40 | def name(self, value: str):
41 | self._name = value
42 |
43 | @property
44 | def include_in_sandbox_dimension(self) -> bool:
45 | return self._include_in_sandbox_dimension
46 |
47 | @include_in_sandbox_dimension.setter
48 | def include_in_sandbox_dimension(self, value: bool):
49 | self._include_in_sandbox_dimension = value
50 |
51 | @classmethod
52 | def from_json(cls, sandbox_as_json: str) -> "Sandbox":
53 | """ Alternative constructor
54 |
55 | :param sandbox_as_json: user as JSON string
56 | :return: sandbox, an instance of this class
57 | """
58 | sandbox_as_dict = json.loads(sandbox_as_json)
59 | return cls.from_dict(sandbox_as_dict)
60 |
61 | @classmethod
62 | def from_dict(cls, sandbox_as_dict: Dict) -> "Sandbox":
63 | """ Alternative constructor
64 |
65 | :param sandbox_as_dict: user as dict
66 | :return: an instance of this class
67 | """
68 | return cls(
69 | name=sandbox_as_dict["Name"],
70 | include_in_sandbox_dimension=sandbox_as_dict["IncludeInSandboxDimension"],
71 | loaded=sandbox_as_dict["IsLoaded"],
72 | active=sandbox_as_dict["IsActive"],
73 | queued=sandbox_as_dict["IsQueued"]
74 | )
75 |
76 | @property
77 | def body(self) -> str:
78 | return self._construct_body()
79 |
80 | def _construct_body(self) -> str:
81 | """
82 | construct body (json) from the class attributes
83 | :return: String, TM1 JSON representation of a sandbox
84 | """
85 | body_as_dict = collections.OrderedDict()
86 | body_as_dict["Name"] = self.name
87 | body_as_dict["IncludeInSandboxDimension"] = self._include_in_sandbox_dimension
88 | return json.dumps(body_as_dict, ensure_ascii=False)
89 |
--------------------------------------------------------------------------------
/TM1py/Objects/Server.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typing import Dict
3 |
4 |
5 | class Server:
6 | """ Abstraction of the TM1 Server
7 |
8 | :Notes:
9 | contains the information you get from http://localhost:5895/Servers
10 | no methods so far
11 | """
12 | def __init__(self, server_as_dict: Dict):
13 | self.name = server_as_dict['Name']
14 | self.ip_address = server_as_dict['IPAddress']
15 | self.ip_v6_address = server_as_dict['IPv6Address']
16 | self.port_number = server_as_dict['PortNumber']
17 | self.client_message_port_number = server_as_dict['ClientMessagePortNumber']
18 | self.http_port_number = server_as_dict['HTTPPortNumber']
19 | self.using_ssl = server_as_dict['UsingSSL']
20 | self.accepting_clients = server_as_dict['AcceptingClients']
21 | self.self_registered = server_as_dict['SelfRegistered']
22 | self.host = server_as_dict['Host']
23 | self.is_local = server_as_dict['IsLocal']
24 | self.ssl_certificate_id = server_as_dict['SSLCertificateID']
25 | self.ssl_certificate_authority = server_as_dict['SSLCertificateAuthority']
26 | self.ssl_certificate_revocation_list = server_as_dict['SSLCertificateRevocationList']
27 | self.client_export_ssl_server_keyid = server_as_dict['ClientExportSSLSvrKeyID']
28 | self.client_export_ssl_server_cert = server_as_dict['ClientExportSSLSvrCert']
29 | self.last_updated = server_as_dict['LastUpdated']
--------------------------------------------------------------------------------
/TM1py/Objects/TM1Object.py:
--------------------------------------------------------------------------------
1 | from abc import abstractmethod
2 |
3 |
4 | class TM1Object:
5 | """ Parent Class for all TM1 Objects e.g. Cube, Process, Dimension.
6 |
7 | """
8 | SANDBOX_DIMENSION = "Sandboxes"
9 |
10 | @property
11 | @abstractmethod
12 | def body(self) -> str:
13 | pass
14 |
15 | def __hash__(self):
16 | return hash(self.body)
17 |
18 | def __str__(self):
19 | return self.body
20 |
21 | def __repr__(self):
22 | return "{}:{}".format(self.__class__.__name__, self.body)
23 |
24 | def __eq__(self, other):
25 | return self.body == other.body
26 |
27 | def __ne__(self, other):
28 | return self.body != other.body
29 |
--------------------------------------------------------------------------------
/TM1py/Objects/User.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import collections
4 | import json
5 | from enum import Enum
6 | from typing import Iterable, Optional, List, Dict, Union
7 |
8 | from TM1py.Objects.TM1Object import TM1Object
9 | from TM1py.Utils.Utils import CaseAndSpaceInsensitiveSet, format_url
10 |
11 |
12 | class UserType(Enum):
13 | User = 0
14 | SecurityAdmin = 1
15 | DataAdmin = 2
16 | Admin = 3
17 | OperationsAdmin = 4
18 |
19 | def __str__(self):
20 | return self.name
21 |
22 | @classmethod
23 | def _missing_(cls, value: str):
24 | for member in cls:
25 | if member.name.lower() == value.replace(" ", "").lower():
26 | return member
27 | # default
28 | raise ValueError("Invalid element type=" + value)
29 |
30 |
31 | class User(TM1Object):
32 | """ Abstraction of a TM1 User
33 |
34 | """
35 |
36 | def __init__(self, name: str, groups: Iterable[str], friendly_name: Optional[str] = None,
37 | password: Optional[str] = None, user_type: Union[UserType, str] = None, enabled: bool = None):
38 | self._name = name
39 | self._groups = CaseAndSpaceInsensitiveSet(*groups)
40 | self._friendly_name = friendly_name
41 | self._password = password
42 | self._enabled = enabled
43 | self._user_type = user_type
44 | # determine user_type
45 | if user_type is None:
46 | if str(UserType.Admin) in self._groups:
47 | self.user_type = UserType.Admin
48 | elif str(UserType.SecurityAdmin) in self._groups:
49 | self.user_type = UserType.SecurityAdmin
50 | elif str(UserType.DataAdmin) in self._groups:
51 | self.user_type = UserType.DataAdmin
52 | elif str(UserType.OperationsAdmin) in self._groups:
53 | self.user_type = UserType.OperationsAdmin
54 | else:
55 | self.user_type = UserType.User
56 | else:
57 | self.user_type = user_type
58 |
59 | @property
60 | def name(self) -> str:
61 | return self._name
62 |
63 | @property
64 | def user_type(self) -> UserType:
65 | return self._user_type
66 |
67 | @property
68 | def friendly_name(self) -> str:
69 | return self._friendly_name
70 |
71 | @property
72 | def password(self) -> str:
73 | if self._password:
74 | return self._password
75 |
76 | @property
77 | def is_admin(self) -> bool:
78 | return "ADMIN" in CaseAndSpaceInsensitiveSet(*self.groups)
79 |
80 | @property
81 | def is_data_admin(self) -> bool:
82 | return any(g in CaseAndSpaceInsensitiveSet(
83 | *self.groups) for g in ["Admin", "DataAdmin"])
84 |
85 | @property
86 | def is_security_admin(self) -> bool:
87 | return any(g in CaseAndSpaceInsensitiveSet(
88 | *self.groups) for g in ["Admin", "SecurityAdmin"])
89 |
90 | @property
91 | def is_ops_admin(self) -> bool:
92 | return any(g in CaseAndSpaceInsensitiveSet(
93 | *self.groups) for g in ["Admin", "OperationsAdmin"])
94 |
95 | @property
96 | def groups(self) -> List[str]:
97 | return [group for group in self._groups]
98 |
99 | @property
100 | def enabled(self) -> bool:
101 | return self._enabled
102 |
103 | @name.setter
104 | def name(self, value: str):
105 | self._name = value
106 |
107 | @friendly_name.setter
108 | def friendly_name(self, value: str):
109 | self._friendly_name = value
110 |
111 | @password.setter
112 | def password(self, value: str):
113 | self._password = value
114 |
115 | @enabled.setter
116 | def enabled(self, value: Union[bool, None]):
117 | self._enabled = value
118 |
119 | @user_type.setter
120 | def user_type(self, value: Union[str, UserType]):
121 | if not isinstance(value, str) and not isinstance(value, UserType):
122 | raise ValueError("argument 'user_type' must be of type str or UserType")
123 |
124 | self._user_type = UserType(value)
125 | # update groups as well, since TM1 doesn't react to change in user_type property
126 | if self._user_type is not UserType.User:
127 | self.add_group(str(self._user_type))
128 |
129 | def add_group(self, group_name: str):
130 | self._groups.add(group_name)
131 |
132 | def remove_group(self, group_name: str):
133 | self._groups.discard(group_name)
134 |
135 | @classmethod
136 | def from_json(cls, user_as_json: str):
137 | """ Alternative constructor
138 |
139 | :param user_as_json: user as JSON string
140 | :return: user, an instance of this class
141 | """
142 | user_as_dict = json.loads(user_as_json)
143 | return cls.from_dict(user_as_dict)
144 |
145 | @classmethod
146 | def from_dict(cls, user_as_dict: Dict) -> 'User':
147 | """ Alternative constructor
148 |
149 | :param user_as_dict: user as dict
150 | :return: user, an instance of this class
151 | """
152 | return cls(name=user_as_dict['Name'],
153 | friendly_name=user_as_dict['FriendlyName'],
154 | enabled=user_as_dict.get('Enabled', None),
155 | user_type=user_as_dict["Type"],
156 | groups=[group["Name"] for group in user_as_dict['Groups']])
157 |
158 | @property
159 | def body(self) -> str:
160 | return self.construct_body()
161 |
162 | def construct_body(self) -> str:
163 | """
164 | construct body (json) from the class attributes
165 | :return: String, TM1 JSON representation of a user
166 | """
167 | body_as_dict = collections.OrderedDict()
168 | body_as_dict['Name'] = self.name
169 | body_as_dict['FriendlyName'] = self.friendly_name or self.name
170 | body_as_dict['Enabled'] = self._enabled
171 | body_as_dict['Type'] = str(self._user_type)
172 | if self.password:
173 | body_as_dict['Password'] = self._password
174 | body_as_dict['Groups@odata.bind'] = [format_url("Groups('{}')", group)
175 | for group
176 | in self.groups]
177 | return json.dumps(body_as_dict, ensure_ascii=False)
178 |
--------------------------------------------------------------------------------
/TM1py/Objects/View.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from abc import abstractmethod
3 |
4 | from TM1py.Objects.TM1Object import TM1Object
5 |
6 |
7 | class View(TM1Object):
8 | """ Abstraction of TM1 View
9 | serves as a parentclass for TM1py.Objects.MDXView and TM1py.Objects.NativeView
10 |
11 | """
12 |
13 | def __init__(self, cube: str, name: str):
14 | self._cube = cube
15 | self._name = name
16 |
17 | @abstractmethod
18 | def body(self) -> str:
19 | pass
20 |
21 | @property
22 | def cube(self) -> str:
23 | return self._cube
24 |
25 | @property
26 | def name(self) -> str:
27 | return self._name
28 |
29 | @cube.setter
30 | def cube(self, value: str):
31 | self._cube = value
32 |
33 | @name.setter
34 | def name(self, value: str):
35 | self._name = value
36 |
37 | @property
38 | def mdx(self):
39 | raise NotImplementedError
40 |
--------------------------------------------------------------------------------
/TM1py/Objects/__init__.py:
--------------------------------------------------------------------------------
1 | from TM1py.Objects.Annotation import Annotation
2 | from TM1py.Objects.Application import Application
3 | from TM1py.Objects.Axis import ViewAxisSelection, ViewTitleSelection
4 | from TM1py.Objects.Chore import Chore
5 | from TM1py.Objects.ChoreFrequency import ChoreFrequency
6 | from TM1py.Objects.ChoreStartTime import ChoreStartTime
7 | from TM1py.Objects.ChoreTask import ChoreTask
8 | from TM1py.Objects.Cube import Cube
9 | from TM1py.Objects.Dimension import Dimension
10 | from TM1py.Objects.Element import Element
11 | from TM1py.Objects.ElementAttribute import ElementAttribute
12 | from TM1py.Objects.GitProject import TM1Project, TM1ProjectTask
13 | from TM1py.Objects.Hierarchy import Hierarchy
14 | from TM1py.Objects.MDXView import MDXView
15 | from TM1py.Objects.NativeView import NativeView
16 | from TM1py.Objects.Process import Process
17 | from TM1py.Objects.ProcessDebugBreakpoint import ProcessDebugBreakpoint, BreakPointType, HitMode
18 | from TM1py.Objects.Rules import Rules
19 | from TM1py.Objects.Server import Server
20 | from TM1py.Objects.Subset import Subset, AnonymousSubset
21 | from TM1py.Objects.User import User
22 | from TM1py.Objects.View import View
23 | from TM1py.Objects.Sandbox import Sandbox
24 |
--------------------------------------------------------------------------------
/TM1py/Services/AnnotationService.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import json
4 | from typing import List, Iterable
5 |
6 | from requests import Response
7 |
8 | from TM1py.Objects.Annotation import Annotation
9 | from TM1py.Services.ObjectService import ObjectService
10 | from TM1py.Services.RestService import RestService
11 | from TM1py.Utils import format_url, CaseAndSpaceInsensitiveDict
12 |
13 |
14 | class AnnotationService(ObjectService):
15 | """ Service to handle Object Updates for TM1 CellAnnotations
16 |
17 | """
18 |
19 | def __init__(self, rest: RestService):
20 | super().__init__(rest)
21 |
22 | def get_all(self, cube_name: str, **kwargs) -> List[Annotation]:
23 | """ get all annotations from given cube as a List.
24 |
25 | :param cube_name:
26 | """
27 | url = format_url("/Cubes('{}')/Annotations?$expand=DimensionalContext($select=Name)", cube_name)
28 | response = self._rest.GET(url, **kwargs)
29 |
30 | annotations_as_dict = response.json()['value']
31 | annotations = [Annotation.from_json(json.dumps(element)) for element in annotations_as_dict]
32 | return annotations
33 |
34 | def create(self, annotation: Annotation, **kwargs) -> Response:
35 | """ create an Annotation
36 |
37 | :param annotation: instance of TM1py.Annotation
38 | """
39 | url = "/Annotations"
40 |
41 | from TM1py import CubeService
42 | cube_dimensions = CubeService(self._rest).get_dimension_names(
43 | cube_name=annotation.object_name,
44 | skip_sandbox_dimension=True)
45 |
46 | response = self._rest.POST(url, json.dumps(annotation.construct_body_for_post(cube_dimensions)), **kwargs)
47 | return response
48 |
49 | def create_many(self, annotations: Iterable[Annotation], **kwargs) -> Response:
50 | """ create an Annotation
51 |
52 | :param annotations: instances of TM1py.Annotation
53 | """
54 | payload = list()
55 | cube_dimensions = CaseAndSpaceInsensitiveDict()
56 |
57 | for annotation in annotations:
58 | dimension_names = cube_dimensions.get(annotation.object_name, None)
59 | if not dimension_names:
60 | from TM1py import CubeService
61 | dimension_names = CubeService(self._rest).get_dimension_names(
62 | cube_name=annotation.object_name,
63 | skip_sandbox_dimension=True)
64 |
65 | cube_dimensions[annotation.object_name] = dimension_names
66 | payload.append(annotation.construct_body_for_post(dimension_names))
67 |
68 | response = self._rest.POST("/Annotations", json.dumps(payload), **kwargs)
69 |
70 | return response
71 |
72 | def get(self, annotation_id: str, **kwargs) -> Annotation:
73 | """ get an annotation from any cube through its unique id
74 |
75 | :param annotation_id: String, the id of the annotation
76 | """
77 | request = format_url("/Annotations('{}')?$expand=DimensionalContext($select=Name)", annotation_id)
78 | response = self._rest.GET(url=request, **kwargs)
79 | return Annotation.from_json(response.text)
80 |
81 | def update(self, annotation: Annotation, **kwargs) -> Response:
82 | """ update Annotation.
83 | updateable attributes: commentValue
84 |
85 | :param annotation: instance of TM1py.Annotation
86 | """
87 | url = format_url("/Annotations('{}')", annotation.id)
88 | return self._rest.PATCH(url=url, data=annotation.body, **kwargs)
89 |
90 | def delete(self, annotation_id: str, **kwargs) -> Response:
91 | """ delete Annotation
92 |
93 | :param annotation_id: string, the id of the annotation
94 | """
95 | url = format_url("/Annotations('{}')", annotation_id)
96 | return self._rest.DELETE(url=url, **kwargs)
97 |
--------------------------------------------------------------------------------
/TM1py/Services/AuditLogService.py:
--------------------------------------------------------------------------------
1 | from warnings import warn
2 |
3 | from datetime import datetime
4 | from typing import Dict
5 |
6 |
7 | from TM1py.Services.ObjectService import ObjectService
8 | from TM1py.Services.RestService import RestService
9 | from TM1py.Utils import verify_version, deprecated_in_version, odata_track_changes_header, require_data_admin, \
10 | format_url, \
11 | require_version, require_ops_admin, utc_localize_time
12 | from TM1py.Services.ConfigurationService import ConfigurationService
13 |
14 |
15 | class AuditLogService(ObjectService):
16 |
17 | def __init__(self, rest: RestService):
18 | super().__init__(rest)
19 | if verify_version(required_version="12.0.0", version=rest.version):
20 | # warn only due to use in Monitoring Service
21 | warn("Audit Logs are not available in this version of TM1, removed as of 12.0.0", DeprecationWarning,
22 | 2)
23 | self.last_delta_request = None
24 | self.configuration = ConfigurationService(rest)
25 |
26 |
27 | @deprecated_in_version(version="12.0.0")
28 | @odata_track_changes_header
29 | def initialize_delta_requests(self, filter=None, **kwargs):
30 | url = "/TailAuditLog()"
31 | if filter:
32 | url += "?$filter={}".format(filter)
33 | response = self._rest.GET(url=url, **kwargs)
34 | # Read the next delta-request-url from the response
35 | self.last_delta_request = response.text[response.text.rfind(
36 | "AuditLogEntries/!delta('"):-2]
37 |
38 | @deprecated_in_version(version="12.0.0")
39 | @odata_track_changes_header
40 | def execute_delta_request(self, **kwargs) -> Dict:
41 | response = self._rest.GET(
42 | url="/" + self.last_delta_request, **kwargs)
43 | self.last_delta_request = response.text[response.text.rfind(
44 | "AuditLogEntries/!delta('"):-2]
45 | return response.json()['value']
46 |
47 | @require_data_admin
48 | @deprecated_in_version(version="12.0.0")
49 | @require_version(version="11.6")
50 | def get_entries(self, user: str = None, object_type: str = None, object_name: str = None,
51 | since: datetime = None, until: datetime = None, top: int = None, **kwargs) -> Dict:
52 | """
53 | :param user: UserName
54 | :param object_type: ObjectType
55 | :param object_name: ObjectName
56 | :param since: of type datetime. If it doesn't have tz information, UTC is assumed.
57 | :param until: of type datetime. If it doesn't have tz information, UTC is assumed.
58 | :param top: int
59 | :return:
60 | """
61 |
62 | url = '/AuditLogEntries?$expand=AuditDetails'
63 | # filter on user, object_type, object_name and time
64 | if any([user, object_type, object_name, since, until]):
65 | log_filters = []
66 | if user:
67 | log_filters.append(format_url("UserName eq '{}'", user))
68 | if object_type:
69 | log_filters.append(format_url(
70 | "ObjectType eq '{}'", object_type))
71 | if object_name:
72 | log_filters.append(format_url(
73 | "ObjectName eq '{}'", object_name))
74 | if since:
75 | # If since doesn't have tz information, UTC is assumed
76 | if not since.tzinfo:
77 | since = utc_localize_time(since)
78 | log_filters.append(format_url(
79 | "TimeStamp ge {}", since.strftime("%Y-%m-%dT%H:%M:%SZ")))
80 | if until:
81 | # If until doesn't have tz information, UTC is assumed
82 | if not until.tzinfo:
83 | until = utc_localize_time(until)
84 | log_filters.append(format_url(
85 | "TimeStamp le {}", until.strftime("%Y-%m-%dT%H:%M:%SZ")))
86 | url += "&$filter={}".format(" and ".join(log_filters))
87 | # top limit
88 | if top:
89 | url += '&$top={}'.format(top)
90 | response = self._rest.GET(url, **kwargs)
91 | return response.json()['value']
92 |
93 | @require_ops_admin
94 | def activate(self):
95 | config = {'Administration': {'AuditLog': {'Enable': True}}}
96 | self.configuration.update_static(config)
97 |
--------------------------------------------------------------------------------
/TM1py/Services/ConfigurationService.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from requests import Response
4 |
5 | from typing import Dict
6 |
7 | from TM1py.Services.ObjectService import ObjectService
8 | from TM1py.Services.RestService import RestService
9 | from TM1py.Utils import deprecated_in_version, require_ops_admin
10 |
11 |
12 | class ConfigurationService(ObjectService):
13 |
14 | def __init__(self, rest: RestService):
15 | super().__init__(rest)
16 |
17 | def get_all(self, **kwargs) -> Dict:
18 | url = '/Configuration'
19 | config = self._rest.GET(url, **kwargs).json()
20 | del config["@odata.context"]
21 | return config
22 |
23 | def get_server_name(self, **kwargs) -> str:
24 | """ Ask TM1 Server for its name
25 |
26 | :Returns:
27 | String, the server name
28 | """
29 | url = '/Configuration/ServerName/$value'
30 | return self._rest.GET(url, **kwargs).text
31 |
32 | def get_product_version(self, **kwargs) -> str:
33 | """ Ask TM1 Server for its version
34 |
35 | :Returns:
36 | String, the version
37 | """
38 | url = '/Configuration/ProductVersion/$value'
39 | return self._rest.GET(url, **kwargs).text
40 |
41 | @deprecated_in_version(version="12.0.0")
42 | def get_admin_host(self, **kwargs) -> str:
43 | url = '/Configuration/AdminHost/$value'
44 | return self._rest.GET(url, **kwargs).text
45 |
46 | @deprecated_in_version(version="12.0.0")
47 | def get_data_directory(self, **kwargs) -> str:
48 | url = '/Configuration/DataBaseDirectory/$value'
49 | return self._rest.GET(url, **kwargs).text
50 |
51 | @require_ops_admin
52 | def get_static(self, **kwargs) -> Dict:
53 | """ Read TM1 config settings as dictionary from TM1 Server
54 |
55 | :return: config as dictionary
56 | """
57 | url = '/StaticConfiguration'
58 | config = self._rest.GET(url, **kwargs).json()
59 | del config["@odata.context"]
60 | return config
61 |
62 | @require_ops_admin
63 | def get_active(self, **kwargs) -> Dict:
64 | """ Read effective(!) TM1 config settings as dictionary from TM1 Server
65 |
66 | :return: config as dictionary
67 | """
68 | url = '/ActiveConfiguration'
69 | config = self._rest.GET(url, **kwargs).json()
70 | del config["@odata.context"]
71 | return config
72 |
73 | @require_ops_admin
74 | def update_static(self, configuration: Dict) -> Response:
75 | """ Update the .cfg file and triggers TM1 to re-read the file.
76 |
77 | :param configuration:
78 | :return: Response
79 | """
80 | url = '/StaticConfiguration'
81 | return self._rest.PATCH(url, json.dumps(configuration))
--------------------------------------------------------------------------------
/TM1py/Services/JobService.py:
--------------------------------------------------------------------------------
1 | try:
2 | import pandas as pd
3 | _has_pandas = True
4 | except ImportError:
5 | _has_pandas = False
6 |
7 | from TM1py.Services.ObjectService import ObjectService
8 | from TM1py.Services.RestService import RestService
9 | from TM1py.Utils.Utils import format_url, require_pandas, require_version
10 |
11 |
12 | class JobService(ObjectService):
13 | """ Service to handle TM1 Job objects introduced in v12
14 |
15 | """
16 |
17 | def __init__(self, rest: RestService):
18 | super().__init__(rest)
19 |
20 | @require_version(version="12.0.0")
21 | def get_all(self, **kwargs):
22 | """ Return a dict of the currently running jobs from the TM1 Server
23 |
24 | :return:
25 | dict: the response
26 | """
27 | url = '/Jobs'
28 | response = self._rest.GET(url, **kwargs)
29 | return response.json()['value']
30 |
31 | @require_version(version="12.0.0")
32 | def cancel(self, job_id, **kwargs):
33 | """ Cancels a running Job
34 |
35 | :param job_id:
36 | :return:
37 | """
38 | url = format_url("/Jobs('{}')/tm1.Cancel", str(job_id))
39 | response = self._rest.POST(url, **kwargs)
40 | return response
41 |
42 | @require_version(version="12.0.0")
43 | def cancel_all(self, **kwargs):
44 | jobs = self.get()
45 | canceled_jobs = list()
46 | for job in jobs:
47 | self.cancel(job["ID"])
48 | canceled_jobs.append(job)
49 | return canceled_jobs
50 |
51 | @require_pandas
52 | @require_version(version="12.0.0")
53 | def get_as_dataframe(self):
54 | """ Gets jobs and returns them as a dataframe
55 |
56 | """
57 | jobs = self.get()
58 | df = pd.DataFrame.from_records(jobs)
59 | return df
60 |
61 |
62 |
--------------------------------------------------------------------------------
/TM1py/Services/LoggerService.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import Dict, List
3 |
4 | from TM1py.Services.ObjectService import ObjectService
5 | from TM1py.Services.RestService import RestService
6 | from TM1py.Utils import format_url
7 | from TM1py.Utils.Utils import CaseAndSpaceInsensitiveDict, require_ops_admin
8 |
9 |
10 | class LoggerService(ObjectService):
11 | """ Service to query and update loggers
12 |
13 | """
14 |
15 | def __init__(self, rest: RestService):
16 | super().__init__(rest)
17 |
18 | @require_ops_admin
19 | def get_all(self, **kwargs) -> Dict:
20 | url = f"/Loggers"
21 | loggers = self._rest.GET(url, **kwargs).json()
22 | return loggers['value']
23 |
24 | @require_ops_admin
25 | def get_all_names(self, **kwargs) -> List[str]:
26 | loggers = self.get_all(**kwargs)
27 | return [logger['Name'] for logger in loggers]
28 |
29 | @require_ops_admin
30 | def get(self, logger: str, **kwargs) -> Dict:
31 | """ Get level for specified logger
32 |
33 | :param logger: string name of logger
34 | :return: Dict of logger and level
35 | """
36 | url = format_url("/Loggers('{}')", logger)
37 | logger = self._rest.GET(url, **kwargs).json()
38 | del logger["@odata.context"]
39 | return logger
40 |
41 | @require_ops_admin
42 | def search(self, wildcard: str = '', level: str = '', **kwargs) -> Dict:
43 | """ Searches logger names by wildcard or by level. Combining wildcard and level will filter via AND and not OR
44 |
45 | :param wildcard: string to match in logger name
46 | :param level: string e.g. FATAL, ERROR, WARNING, INFO, DEBUG, UNKOWN, OFF
47 | :return: Dict of matching loggers and levels
48 | """
49 | url = f"/Loggers"
50 |
51 | logger_filters = []
52 |
53 | if level:
54 | level_dict = CaseAndSpaceInsensitiveDict(
55 | {'FATAL': 0, 'ERROR': 1, 'WARNING': 2, 'INFO': 3, 'DEBUG': 4, 'UNKNOWN': 5, 'OFF': 6}
56 | )
57 | level_index = level_dict.get(level)
58 | if level_index:
59 | logger_filters.append("Level eq {}".format(level_index))
60 |
61 | if wildcard:
62 | logger_filters.append("contains(tolower(Name), tolower('{}'))".format(wildcard))
63 |
64 | url += "?$filter={}".format(" and ".join(logger_filters))
65 |
66 | loggers = self._rest.GET(url, **kwargs).json()
67 | return loggers['value']
68 |
69 | @require_ops_admin
70 | def exists(self, logger: str, **kwargs) -> bool:
71 | """ Test if logger exists
72 | :param logger: string name of logger
73 | :return: bool
74 | """
75 | url = format_url("/Loggers('{}')", logger)
76 | return self._exists(url, **kwargs)
77 |
78 | @require_ops_admin
79 | def set_level(self, logger: str, level: str, **kwargs):
80 | """ Set logger level
81 | :param logger: string name of logger
82 | :param level: string e.g. FATAL, ERROR, WARNING, INFO, DEBUG, UNKOWN, OFF
83 | :return: response
84 | """
85 | url = format_url("/Loggers('{}')", logger)
86 |
87 | if not self.exists(logger=logger, **kwargs):
88 | raise ValueError('{} is not a valid logger'.format(logger))
89 |
90 | level_dict = CaseAndSpaceInsensitiveDict(
91 | {'FATAL': 0, 'ERROR': 1, 'WARNING': 2, 'INFO': 3, 'DEBUG': 4, 'UNKNOWN': 5, 'OFF': 6}
92 | )
93 | level_index = level_dict.get(level)
94 | if level_index:
95 | logger = {'Level': level_index}
96 | else:
97 | raise ValueError('{} is not a valid level'.format(level))
98 |
99 | return self._rest.PATCH(url, json.dumps(logger))
100 |
--------------------------------------------------------------------------------
/TM1py/Services/MonitoringService.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from typing import List
3 | from warnings import warn
4 | from requests import Response
5 |
6 | from TM1py.Objects.User import User
7 | from TM1py.Services.ObjectService import ObjectService
8 | from TM1py.Services.RestService import RestService
9 | from TM1py.Utils import require_admin
10 | from TM1py.Services.ThreadService import ThreadService
11 | from TM1py.Services.SessionService import SessionService
12 | from TM1py.Services.UserService import UserService
13 |
14 |
15 | class MonitoringService(ObjectService):
16 | """ Service to Query and Cancel Threads in TM1
17 |
18 | """
19 |
20 | def __init__(self, rest: RestService):
21 | super().__init__(rest)
22 | warn("Monitoring Service will be moved to a new location in a future version", DeprecationWarning, 2)
23 | self.users = UserService(rest)
24 | self.threads = ThreadService(rest)
25 | self.session = SessionService(rest)
26 |
27 | def get_threads(self, **kwargs) -> List:
28 | """ Return a dict of the currently running threads from the TM1 Server
29 |
30 | :return:
31 | dict: the response
32 | """
33 | return self.threads.get_all(**kwargs)
34 |
35 | def get_active_threads(self, **kwargs):
36 | """Return a list of non-idle threads from the TM1 Server
37 |
38 | :return:
39 | list: TM1 threads as dict
40 | """
41 | return self.threads.get_active(**kwargs)
42 |
43 | def cancel_thread(self, thread_id: int, **kwargs) -> Response:
44 | """ Kill a running thread
45 |
46 | :param thread_id:
47 | :return:
48 | """
49 | return self.threads.cancel(thread_id, **kwargs)
50 |
51 | def cancel_all_running_threads(self, **kwargs) -> list:
52 | return self.threads.cancel_all_running(**kwargs)
53 |
54 | def get_active_users(self, **kwargs) -> List[User]:
55 | """ Get the activate users in TM1
56 |
57 | :return: List of TM1py.User instances
58 | """
59 | return self.users.get_active(**kwargs)
60 |
61 | def user_is_active(self, user_name: str, **kwargs) -> bool:
62 | """ Check if user is currently active in TM1
63 |
64 | :param user_name:
65 | :return: Boolean
66 | """
67 | return self.users.is_active(user_name, **kwargs)
68 |
69 | def disconnect_user(self, user_name: str, **kwargs) -> Response:
70 | """ Disconnect User
71 |
72 | :param user_name:
73 | :return:
74 | """
75 | return self.users.disconnect(user_name, **kwargs)
76 |
77 | def get_active_session_threads(self, exclude_idle: bool = True, **kwargs):
78 | return self.session.get_threads_for_current(exclude_idle, **kwargs)
79 |
80 | def get_sessions(self, include_user: bool = True, include_threads: bool = True, **kwargs) -> List:
81 | return self.session.get_all(include_user, include_threads, **kwargs)
82 |
83 | @require_admin
84 | def disconnect_all_users(self, **kwargs) -> list:
85 | return self.users.disconnect_all(**kwargs)
86 |
87 | def close_session(self, session_id, **kwargs) -> Response:
88 | return self.session.close(session_id, **kwargs)
89 |
90 | @require_admin
91 | def close_all_sessions(self, **kwargs) -> list:
92 | return self.session.close_all(**kwargs)
93 |
94 | def get_current_user(self, **kwargs):
95 | return self.users.get_current(**kwargs)
96 |
--------------------------------------------------------------------------------
/TM1py/Services/ObjectService.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import hashlib
3 | import random
4 | import threading
5 |
6 | from TM1py.Exceptions import TM1pyRestException
7 | from TM1py.Services import RestService
8 | from TM1py.Utils import format_url, verify_version
9 |
10 |
11 | class ObjectService:
12 | """ Parent class for all Object Services
13 |
14 | """
15 |
16 | ELEMENT_ATTRIBUTES_PREFIX = "}ElementAttributes_"
17 | SANDBOX_DIMENSION = "Sandboxes"
18 |
19 | BINARY_HTTP_HEADER_PRE_V12 = {'Content-Type': 'application/octet-stream; odata.streaming=true'}
20 | BINARY_HTTP_HEADER = {'Content-Type': 'application/json;charset=UTF-8'}
21 |
22 | def __init__(self, rest_service: RestService):
23 | """ Constructor, Create an instance of ObjectService
24 |
25 | :param rest_service:
26 | """
27 | self._rest = rest_service
28 | if verify_version("12", self.version):
29 | self.binary_http_header = self.BINARY_HTTP_HEADER
30 | else:
31 | self.binary_http_header = self.BINARY_HTTP_HEADER_PRE_V12
32 |
33 | def suggest_unique_object_name(self, random_seed: float = None) -> str:
34 | """
35 | Generate hash based on tm1-session-id, local-thread-id and random id to guarantee unique name
36 | avoids name conflicts in multithreading operations
37 | """
38 | if not random_seed:
39 | random_seed = random.random()
40 | unique_string = f"{self._rest.session_id}{threading.get_ident()}{random_seed}"
41 | unique_hash = "tm1py." + hashlib.sha256(unique_string.encode('utf-8')).hexdigest()[:12]
42 | return unique_hash
43 |
44 | def determine_actual_object_name(self, object_class: str, object_name: str, **kwargs) -> str:
45 | url = format_url(
46 | "/{}?$filter=tolower(replace(Name, ' ', '')) eq '{}'",
47 | object_class,
48 | object_name.replace(" ", "").lower())
49 | response = self._rest.GET(url, **kwargs)
50 |
51 | if len(response.json()["value"]) == 0:
52 | raise ValueError("Object '{}' of type '{}' doesn't exist".format(object_name, object_class))
53 |
54 | return response.json()["value"][0]["Name"]
55 |
56 | def _exists(self, url: str, **kwargs) -> bool:
57 | """ Check if resource exists in the TM1 Server
58 |
59 | :param url:
60 | :return:
61 | """
62 | try:
63 | self._rest.GET(url, **kwargs)
64 | return True
65 | except TM1pyRestException as e:
66 | if e.status_code == 404:
67 | return False
68 | raise e
69 |
70 | @property
71 | def version(self) -> str:
72 | return self._rest.version
73 |
74 | @property
75 | def is_admin(self) -> bool:
76 | return self._rest.is_admin
77 |
78 | @property
79 | def is_data_admin(self) -> bool:
80 | return self._rest.is_data_admin
81 |
82 | @property
83 | def is_security_admin(self) -> bool:
84 | return self._rest.is_security_admin
85 |
86 | @property
87 | def is_ops_admin(self) -> bool:
88 | return self._rest.is_ops_admin
89 |
--------------------------------------------------------------------------------
/TM1py/Services/PowerBiService.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Iterable
2 |
3 | from TM1py.Services import CellService
4 | from TM1py.Services import ElementService
5 | from TM1py.Utils import require_pandas
6 |
7 | try:
8 | import pandas as pd
9 |
10 | _has_pandas = True
11 | except ImportError:
12 | _has_pandas = False
13 |
14 |
15 | class PowerBiService:
16 | def __init__(self, tm1_rest):
17 | """
18 |
19 | :param tm1_rest: instance of RestService
20 | """
21 | self._tm1_rest = tm1_rest
22 | self.cells = CellService(tm1_rest)
23 | self.elements = ElementService(tm1_rest)
24 |
25 | @require_pandas
26 | def execute_mdx(self, mdx, **kwargs) -> 'pd.DataFrame':
27 | return self.cells.execute_mdx_dataframe_shaped(mdx, **kwargs)
28 |
29 | @require_pandas
30 | def execute_view(self, cube_name: str, view_name: str, private: bool, use_iterative_json=False, use_blob=False,
31 | **kwargs) -> 'pd.DataFrame':
32 | return self.cells.execute_view_dataframe_shaped(
33 | cube_name,
34 | view_name,
35 | private,
36 | use_iterative_json=use_iterative_json,
37 | use_blob=use_blob,
38 | **kwargs)
39 |
40 | @require_pandas
41 | def get_member_properties(self, dimension_name: str = None, hierarchy_name: str = None,
42 | member_selection: Iterable = None,
43 | skip_consolidations: bool = True, attributes: Iterable = None,
44 | skip_parents: bool = False, level_names=None,
45 | parent_attribute: str = None, skip_weights=True, use_blob=False,
46 | **kwargs) -> 'pd.DataFrame':
47 | """
48 |
49 | :param dimension_name: Name of the dimension
50 | :param hierarchy_name: Name of the hierarchy in the dimension
51 | :param member_selection: Selection of members. Iterable or valid MDX string
52 | :param skip_consolidations: Boolean flag to skip consolidations
53 | :param attributes: Selection of attributes. Iterable. If None retrieve all.
54 | :param level_names: List of labels for parent columns. If None use level names from TM1.
55 | :param skip_parents: Boolean Flag to skip parent columns.
56 | :param parent_attribute: Attribute to be displayed in parent columns. If None, parent name is used.
57 | :param skip_weights: include weight columns
58 | :param use_blob: Better performance on large sets and lower memory footprint in any case. Requires admin permissions
59 |
60 | :return: pandas DataFrame
61 | """
62 | if not skip_weights and skip_parents:
63 | raise ValueError("skip_weights must not be False if skip_parents is True")
64 |
65 | return self.elements.get_elements_dataframe(
66 | dimension_name=dimension_name, hierarchy_name=hierarchy_name, elements=member_selection,
67 | skip_consolidations=skip_consolidations, attributes=attributes, skip_parents=skip_parents,
68 | level_names=level_names, parent_attribute=parent_attribute, skip_weights=skip_weights,
69 | use_blob=use_blob, **kwargs)
70 |
--------------------------------------------------------------------------------
/TM1py/Services/SandboxService.py:
--------------------------------------------------------------------------------
1 | from typing import List, Iterable
2 | from requests import Response
3 | import json
4 |
5 | from TM1py.Exceptions.Exceptions import TM1pyRestException
6 | from TM1py.Services.ObjectService import ObjectService
7 | from TM1py.Services.RestService import RestService
8 | from TM1py.Utils import format_url
9 | from TM1py.Objects.Sandbox import Sandbox
10 |
11 |
12 | class SandboxService(ObjectService):
13 | """ Service to handle sandboxes in TM1
14 |
15 | """
16 |
17 | def __init__(self, rest: RestService):
18 | super().__init__(rest)
19 |
20 | def get(self, sandbox_name: str, **kwargs) -> Sandbox:
21 | """ get a sandbox from TM1 Server
22 |
23 | :param sandbox_name: str
24 | :return: instance of TM1py.Sandbox
25 | """
26 | url = format_url("/Sandboxes('{}')", sandbox_name)
27 | response = self._rest.GET(url=url, **kwargs)
28 | sandbox = Sandbox.from_json(response.text)
29 | return sandbox
30 |
31 | def get_all(self, **kwargs) -> List[Sandbox]:
32 | """ get all sandboxes from TM1 Server
33 |
34 | :return: List of TM1py.Sandbox instances
35 | """
36 | url = "/Sandboxes?$select=Name,IncludeInSandboxDimension,IsLoaded,IsActive,IsQueued"
37 | response = self._rest.GET(url, **kwargs)
38 | sandboxes = [
39 | Sandbox.from_dict(sandbox_as_dict=sandbox)
40 | for sandbox in response.json()["value"]
41 | ]
42 | return sandboxes
43 |
44 | def get_all_names(self, **kwargs) -> List[str]:
45 | """ get all sandbox names
46 |
47 | :param kwargs:
48 | :return:
49 | """
50 | url = "/Sandboxes?$select=Name"
51 | response = self._rest.GET(url, **kwargs)
52 | return [entry["Name"] for entry in response.json()["value"]]
53 |
54 | def create(self, sandbox: Sandbox, **kwargs) -> Response:
55 | """ create a new sandbox in TM1 Server
56 |
57 | :param sandbox: Sandbox
58 | :return: response
59 | """
60 | url = "/Sandboxes"
61 | return self._rest.POST(url=url, data=sandbox.body, **kwargs)
62 |
63 | def update(self, sandbox: Sandbox, **kwargs) -> Response:
64 | """ update a sandbox in TM1
65 |
66 | :param sandbox:
67 | :return: response
68 | """
69 | url = format_url("/Sandboxes('{}')", sandbox.name)
70 | return self._rest.PATCH(url=url, data=sandbox.body, **kwargs)
71 |
72 | def delete(self, sandbox_name: str, **kwargs) -> Response:
73 | """ delete a sandbox in TM1
74 |
75 | :param sandbox_name:
76 | :return: response
77 | """
78 | url = format_url("/Sandboxes('{}')", sandbox_name)
79 | return self._rest.DELETE(url, **kwargs)
80 |
81 | def publish(self, sandbox_name: str, **kwargs) -> Response:
82 | """ publish existing sandbox to base
83 |
84 | :param sandbox_name: str
85 | :return: response
86 | """
87 | url = format_url("/Sandboxes('{}')/tm1.Publish", sandbox_name)
88 | return self._rest.POST(url=url, **kwargs)
89 |
90 | def reset(self, sandbox_name: str, **kwargs) -> Response:
91 | """ reset all changes in specified sandbox
92 |
93 | :param sandbox_name: str
94 | :return: response
95 | """
96 | url = format_url("/Sandboxes('{}')/tm1.DiscardChanges", sandbox_name)
97 | return self._rest.POST(url=url, **kwargs)
98 |
99 | def merge(
100 | self,
101 | source_sandbox_name: str,
102 | target_sandbox_name: str,
103 | clean_after: bool = False,
104 | **kwargs
105 | ) -> Response:
106 | """ merge one sandbox into another
107 |
108 | :param source_sandbox_name: str
109 | :param target_sandbox_name: str
110 | :param clean_after: bool: Reset source sandbox after merging
111 | :return: response
112 | """
113 | url = format_url("/Sandboxes('{}')/tm1.Merge", source_sandbox_name)
114 | payload = dict()
115 | payload["Target@odata.bind"] = format_url(
116 | "Sandboxes('{}')", target_sandbox_name
117 | )
118 | payload["CleanAfter"] = clean_after
119 | return self._rest.POST(url=url, data=json.dumps(payload), **kwargs)
120 |
121 | def exists(self, sandbox_name: str, **kwargs) -> bool:
122 | """ check if the sandbox exists in TM1
123 |
124 | :param sandbox_name: String
125 | :return: bool
126 | """
127 | url = format_url("/Sandboxes('{}')", sandbox_name)
128 | return self._exists(url, **kwargs)
129 |
130 | def load(self, sandbox_name: str, **kwargs) -> Response:
131 | """ load sandbox into memory
132 |
133 | :param sandbox_name: str
134 | :return: response
135 | """
136 | url = format_url("/Sandboxes('{}')/tm1.Load", sandbox_name)
137 | return self._rest.POST(url=url, **kwargs)
138 |
139 | def unload(self, sandbox_name: str, **kwargs) -> Response:
140 | """ unload sandbox from memory
141 |
142 | :param sandbox_name: str
143 | :return: response
144 | """
145 | url = format_url("/Sandboxes('{}')/tm1.Unload", sandbox_name)
146 | return self._rest.POST(url=url, **kwargs)
147 |
--------------------------------------------------------------------------------
/TM1py/Services/SessionService.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | from requests import Response
3 |
4 | from TM1py.Services.ObjectService import ObjectService
5 | from TM1py.Services.RestService import RestService
6 | from TM1py.Services.UserService import UserService
7 | from TM1py.Utils import format_url, case_and_space_insensitive_equals, require_admin
8 |
9 |
10 | class SessionService(ObjectService):
11 | """ Service to Query and Cancel Threads in TM1
12 |
13 | """
14 |
15 | def __init__(self, rest: RestService):
16 | super().__init__(rest)
17 | self.users = UserService(rest)
18 |
19 | def get_all(self, include_user: bool = True, include_threads: bool = True, **kwargs) -> List:
20 | url = "/Sessions"
21 | if include_user or include_threads:
22 | expands = list()
23 | if include_user:
24 | expands.append("User")
25 | if include_threads:
26 | expands.append("Threads")
27 | url += "?$expand=" + ",".join(expands)
28 |
29 | response = self._rest.GET(url, **kwargs)
30 | return response.json()["value"]
31 |
32 | def get_current(self, **kwargs):
33 | url = "/ActiveSession"
34 |
35 | response = self._rest.GET(url, **kwargs)
36 | return response.json()['value']
37 |
38 | def get_threads_for_current(self, exclude_idle: bool = True, **kwargs):
39 | url = "/ActiveSession/Threads?$filter=Function ne 'GET /ActiveSession/Threads'"
40 | if exclude_idle:
41 | url += " and State ne 'Idle'"
42 |
43 | response = self._rest.GET(url, **kwargs)
44 | return response.json()['value']
45 |
46 | def close(self, session_id, **kwargs) -> Response:
47 | url = format_url(f"/Sessions('{session_id}')/tm1.Close")
48 | return self._rest.POST(url, **kwargs)
49 |
50 | @require_admin
51 | def close_all(self, **kwargs) -> list:
52 | current_user = self.users.get_current(**kwargs)
53 | sessions = self.get_all(**kwargs)
54 | closed_sessions = list()
55 | for session in sessions:
56 | if "User" not in session:
57 | continue
58 | if session["User"] is None:
59 | continue
60 | if "Name" not in session["User"]:
61 | continue
62 | if case_and_space_insensitive_equals(current_user.name, session["User"]["Name"]):
63 | continue
64 | self.close(session['ID'], **kwargs)
65 | closed_sessions.append(session)
66 | return closed_sessions
67 |
--------------------------------------------------------------------------------
/TM1py/Services/ThreadService.py:
--------------------------------------------------------------------------------
1 | from requests import Response
2 | from warnings import warn
3 |
4 | from typing import List
5 |
6 | from TM1py.Services.ObjectService import ObjectService
7 | from TM1py.Services.RestService import RestService
8 | from TM1py.Utils.Utils import format_url, verify_version, deprecated_in_version
9 |
10 |
11 | class ThreadService(ObjectService):
12 | """ Service to work with Threads in TM1
13 | Deprecated as of TM1 Server v12
14 |
15 | """
16 |
17 | def __init__(self, rest: RestService):
18 | super().__init__(rest)
19 | if verify_version(required_version="12.0.0", version=self.version):
20 | # warn only due to use in Monitoring Service
21 | warn("Threads not available in this version of TM1, removed as of 12.0.0", DeprecationWarning, 2)
22 |
23 | @deprecated_in_version(version="12.0.0")
24 | def get_all(self, **kwargs) -> List:
25 | """ Return a list of the currently running threads from the TM1 Server
26 |
27 | :return:
28 | dict: the response
29 | """
30 | url = '/Threads'
31 | response = self._rest.GET(url, **kwargs)
32 | return response.json()['value']
33 |
34 | @deprecated_in_version(version="12.0.0")
35 | def get_active(self, **kwargs):
36 | """Return a list of non-idle threads from the TM1 Server
37 |
38 | :return:
39 | list: TM1 threads as dict
40 | """
41 | url = "/Threads?$filter=Function ne 'GET /Threads' and State ne 'Idle'"
42 | response = self._rest.GET(url, **kwargs)
43 | return response.json()['value']
44 |
45 | @deprecated_in_version(version="12.0.0")
46 | def cancel(self, thread_id: int, **kwargs) -> Response:
47 | """ Kill a running thread
48 |
49 | :param thread_id:
50 | :return:
51 | """
52 | url = format_url("/Threads('{}')/tm1.CancelOperation", str(thread_id))
53 | response = self._rest.POST(url, **kwargs)
54 | return response
55 |
56 | @deprecated_in_version(version="12.0.0")
57 | def cancel_all_running(self, **kwargs) -> list:
58 | running_threads = self.get_all(**kwargs)
59 | canceled_threads = list()
60 | for thread in running_threads:
61 | if thread["State"] == "Idle":
62 | continue
63 | if thread["Type"] == "System":
64 | continue
65 | if thread["Name"] == "Pseudo":
66 | continue
67 | if thread["Function"] == "GET /Threads":
68 | continue
69 | if thread["Function"] == "GET /api/v1/Threads":
70 | continue
71 | self.cancel(thread["ID"], **kwargs)
72 | canceled_threads.append(thread)
73 | return canceled_threads
74 |
--------------------------------------------------------------------------------
/TM1py/Services/TransactionLogService.py:
--------------------------------------------------------------------------------
1 | from warnings import warn
2 |
3 | from datetime import datetime
4 | from typing import Dict
5 |
6 | from TM1py.Services.ObjectService import ObjectService
7 | from TM1py.Services.RestService import RestService
8 | from TM1py.Utils import verify_version, deprecated_in_version, odata_track_changes_header, require_data_admin, format_url, utc_localize_time
9 |
10 |
11 | class TransactionLogService(ObjectService):
12 |
13 | def __init__(self, rest: RestService):
14 | super().__init__(rest)
15 | if verify_version(required_version="12.0.0", version=rest.version):
16 | # warn only due to use in Monitoring Service
17 | warn("Transaction Logs are not available in this version of TM1, removed as of 12.0.0", DeprecationWarning,
18 | 2)
19 | self.last_delta_request = None
20 |
21 | @deprecated_in_version(version="12.0.0")
22 | @odata_track_changes_header
23 | def initialize_delta_requests(self, filter=None, **kwargs):
24 | url = "/TailTransactionLog()"
25 | if filter:
26 | url += "?$filter={}".format(filter)
27 | response = self._rest.GET(url=url, **kwargs)
28 | # Read the next delta-request-url from the response
29 | self.last_delta_request = response.text[response.text.rfind(
30 | "TransactionLogEntries/!delta('"):-2]
31 |
32 | @deprecated_in_version(version="12.0.0")
33 | @odata_track_changes_header
34 | def execute_delta_request(self, **kwargs) -> Dict:
35 | response = self._rest.GET(
36 | url="/" + self.last_delta_request, **kwargs)
37 | self.last_delta_request = response.text[response.text.rfind(
38 | "TransactionLogEntries/!delta('"):-2]
39 | return response.json()['value']
40 |
41 | @deprecated_in_version(version="12.0.0")
42 | @require_data_admin
43 | def get_entries(self, reverse: bool = True, user: str = None, cube: str = None,
44 | since: datetime = None, until: datetime = None, top: int = None,
45 | element_tuple_filter: Dict[str, str] = None,
46 | element_position_filter: Dict[int, Dict[str, str]] = None, **kwargs) -> Dict:
47 | """
48 | :param reverse: Boolean
49 | :param user: UserName
50 | :param cube: CubeName
51 | :param since: of type datetime. If it doesn't have tz information, UTC is assumed.
52 | :param until: of type datetime. If it doesn't have tz information, UTC is assumed.
53 | :param top: int
54 | :param element_tuple_filter: of type dict. Element name as key and comparison operator as value
55 | :param element_position_filter: not yet implemented
56 | tuple={'Actual':'eq','2020': 'ge'}
57 | :return:
58 | """
59 | if element_position_filter:
60 | raise NotImplementedError("Feature expected in upcoming releases of TM1, TM1py")
61 |
62 | reverse = 'desc' if reverse else 'asc'
63 | url = '/TransactionLogEntries?$orderby=TimeStamp {} '.format(reverse)
64 |
65 | # filter on user, cube, time and elements
66 | if any([user, cube, since, until, element_tuple_filter, element_position_filter]):
67 | log_filters = []
68 | if user:
69 | log_filters.append(format_url("User eq '{}'", user))
70 | if cube:
71 | log_filters.append(format_url("Cube eq '{}'", cube))
72 | if element_tuple_filter:
73 | log_filters.append(format_url(
74 | "Tuple/any(e: {})".format(" or ".join([f"e {v} '{k}'" for k, v in element_tuple_filter.items()]))))
75 | if since:
76 | # If since doesn't have tz information, UTC is assumed
77 | if not since.tzinfo:
78 | since = utc_localize_time(since)
79 | log_filters.append(format_url(
80 | "TimeStamp ge {}", since.strftime("%Y-%m-%dT%H:%M:%SZ")))
81 | if until:
82 | # If until doesn't have tz information, UTC is assumed
83 | if not until.tzinfo:
84 | until = utc_localize_time(until)
85 | log_filters.append(format_url(
86 | "TimeStamp le {}", until.strftime("%Y-%m-%dT%H:%M:%SZ")))
87 | url += "&$filter={}".format(" and ".join(log_filters))
88 | # top limit
89 | if top:
90 | url += '&$top={}'.format(top)
91 | response = self._rest.GET(url, **kwargs)
92 | return response.json()['value']
93 |
--------------------------------------------------------------------------------
/TM1py/Services/UserService.py:
--------------------------------------------------------------------------------
1 | from requests import Response
2 | from typing import List
3 |
4 | from TM1py.Objects.User import User
5 | from TM1py.Services.ObjectService import ObjectService
6 | from TM1py.Services.RestService import RestService
7 | from TM1py.Utils import format_url, case_and_space_insensitive_equals, require_admin, deprecated_in_version
8 |
9 |
10 | class UserService(ObjectService):
11 |
12 | def __init__(self, rest: RestService):
13 | super().__init__(rest)
14 |
15 | def get_all(self, **kwargs) ->List[User]:
16 | """ Get all users
17 |
18 | :return: List of TM1py.User instances
19 | """
20 | url = '/Users?$expand=Groups'
21 | response = self._rest.GET(url, **kwargs)
22 | users = [User.from_dict(user) for user in response.json()['value']]
23 | return users
24 |
25 | def get_active(self, **kwargs) -> List[User]:
26 | """ Get the activate users in TM1
27 |
28 | :return: List of TM1py.User instances
29 | """
30 | url = '/Users?$filter=IsActive eq true&$expand=Groups'
31 | response = self._rest.GET(url, **kwargs)
32 | users = [User.from_dict(user) for user in response.json()['value']]
33 | return users
34 |
35 | def is_active(self, user_name: str, **kwargs) -> bool:
36 | """ Check if user is currently active in TM1
37 |
38 | :param user_name:
39 | :return: Boolean
40 | """
41 | url = format_url("/Users('{}')/IsActive", user_name)
42 | response = self._rest.GET(url, **kwargs)
43 | return bool(response.json()['value'])
44 |
45 | def disconnect(self, user_name: str, **kwargs) -> Response:
46 | """ Disconnect User
47 |
48 | :param user_name:
49 | :return:
50 | """
51 | url = format_url("/Users('{}')/tm1.Disconnect", user_name)
52 | response = self._rest.POST(url, **kwargs)
53 | return response
54 |
55 | @require_admin
56 | def disconnect_all(self, **kwargs) -> list:
57 | current_user = self.get_current(**kwargs)
58 | active_users = self.get_active(**kwargs)
59 | disconnected_users = list()
60 | for active_user in active_users:
61 | if not case_and_space_insensitive_equals(current_user.name, active_user.name):
62 | self.disconnect(active_user.name, **kwargs)
63 | disconnected_users += [active_user.name]
64 | return disconnected_users
65 |
66 | def get_current(self, **kwargs):
67 | from TM1py import SecurityService
68 | security_service = SecurityService(self._rest)
69 | return security_service.get_current_user(**kwargs)
70 |
--------------------------------------------------------------------------------
/TM1py/Services/__init__.py:
--------------------------------------------------------------------------------
1 | from TM1py.Services.AnnotationService import AnnotationService
2 | from TM1py.Services.ApplicationService import ApplicationService
3 | from TM1py.Services.CellService import CellService
4 | from TM1py.Services.ChoreService import ChoreService
5 | from TM1py.Services.CubeService import CubeService
6 | from TM1py.Services.DimensionService import DimensionService
7 | from TM1py.Services.ElementService import ElementService
8 | from TM1py.Services.GitService import GitService
9 | from TM1py.Services.HierarchyService import HierarchyService
10 | from TM1py.Services.ProcessService import ProcessService
11 | from TM1py.Services.RestService import RestService
12 | from TM1py.Services.SandboxService import SandboxService
13 | from TM1py.Services.SecurityService import SecurityService
14 | from TM1py.Services.SubsetService import SubsetService
15 | from TM1py.Services.ViewService import ViewService
16 | from TM1py.Services.GitService import GitService
17 | from TM1py.Services.TM1Service import TM1Service
18 | from TM1py.Services.ManageService import ManageService
19 | from TM1py.Services.JobService import JobService
20 | from TM1py.Services.UserService import UserService
21 | from TM1py.Services.ThreadService import ThreadService
22 | from TM1py.Services.SessionService import SessionService
23 | from TM1py.Services.TransactionLogService import TransactionLogService
24 | from TM1py.Services.MessageLogService import MessageLogService
25 | from TM1py.Services.ConfigurationService import ConfigurationService
26 | from TM1py.Services.AuditLogService import AuditLogService
27 | from TM1py.Services.LoggerService import LoggerService
28 |
29 | from TM1py.Services.ServerService import ServerService
30 | from TM1py.Services.MonitoringService import MonitoringService
31 | from TM1py.Services.PowerBiService import PowerBiService
32 |
--------------------------------------------------------------------------------
/TM1py/Utils/__init__.py:
--------------------------------------------------------------------------------
1 | from TM1py.Utils.MDXUtils import *
2 | from TM1py.Utils.Utils import *
3 |
--------------------------------------------------------------------------------
/TM1py/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | A python module for TM1.
3 |
4 | https://github.com/cubewise-code/TM1py
5 |
6 | TM1py wraps the TM1 REST API into concise Python classes and Services that simplify TM1 interactions from python.
7 |
8 | Usage:
9 | >>> with RestService(address='', port=8001, user='admin', password='apple', ssl=False) as tm1_rest:
10 | >>> subset_service = SubsetService(tm1_rest)
11 | >>> subset = Subset(dimension_name='Month', subset_name='Q1', elements=['Jan', 'Feb', 'Mar'])
12 | >>> subset_service.create(subset, private=True)
13 |
14 | """
15 |
16 | # __init__ can hoist attributes from submodules into higher namespaces for convenience
17 |
18 | from TM1py.Objects.Annotation import Annotation
19 | from TM1py.Objects.Application import Application
20 | from TM1py.Objects.Axis import ViewAxisSelection, ViewTitleSelection
21 | from TM1py.Objects.Chore import Chore
22 | from TM1py.Objects.ChoreFrequency import ChoreFrequency
23 | from TM1py.Objects.ChoreStartTime import ChoreStartTime
24 | from TM1py.Objects.ChoreTask import ChoreTask
25 | from TM1py.Objects.Cube import Cube
26 | from TM1py.Objects.Dimension import Dimension
27 | from TM1py.Objects.Element import Element
28 | from TM1py.Objects.ElementAttribute import ElementAttribute
29 | from TM1py.Objects.Git import Git
30 | from TM1py.Objects.GitCommit import GitCommit
31 | from TM1py.Objects.GitPlan import GitPlan
32 | from TM1py.Objects.GitRemote import GitRemote
33 | from TM1py.Objects.Hierarchy import Hierarchy
34 | from TM1py.Objects.MDXView import MDXView
35 | from TM1py.Objects.NativeView import NativeView
36 | from TM1py.Objects.Process import Process
37 | from TM1py.Objects.Rules import Rules
38 | from TM1py.Objects.Sandbox import Sandbox
39 | from TM1py.Objects.Server import Server
40 | from TM1py.Objects.Subset import Subset, AnonymousSubset
41 | from TM1py.Objects.User import User
42 | from TM1py.Objects.View import View
43 | from TM1py.Services.ObjectService import ObjectService
44 | from TM1py.Services.RestService import RestService
45 | from TM1py.Services.AnnotationService import AnnotationService
46 | from TM1py.Services.ApplicationService import ApplicationService
47 | from TM1py.Services.CellService import CellService
48 | from TM1py.Services.ChoreService import ChoreService
49 | from TM1py.Services.CubeService import CubeService
50 | from TM1py.Services.DimensionService import DimensionService
51 | from TM1py.Services.ElementService import ElementService
52 | from TM1py.Services.FileService import FileService
53 | from TM1py.Services.GitService import GitService
54 | from TM1py.Services.HierarchyService import HierarchyService
55 | from TM1py.Services.ProcessService import ProcessService
56 | from TM1py.Services.SandboxService import SandboxService
57 | from TM1py.Services.SecurityService import SecurityService
58 | from TM1py.Services.SubsetService import SubsetService
59 | from TM1py.Services.TM1Service import TM1Service
60 | from TM1py.Services.ViewService import ViewService
61 | from TM1py.Services.ManageService import ManageService
62 | from TM1py.Services.JobService import JobService
63 | from TM1py.Services.UserService import UserService
64 | from TM1py.Services.ThreadService import ThreadService
65 | from TM1py.Services.SessionService import SessionService
66 | from TM1py.Services.TransactionLogService import TransactionLogService
67 | from TM1py.Services.MessageLogService import MessageLogService
68 | from TM1py.Services.ConfigurationService import ConfigurationService
69 | from TM1py.Services.AuditLogService import AuditLogService
70 |
71 | from TM1py.Services.ServerService import ServerService
72 | from TM1py.Services.PowerBiService import PowerBiService
73 | from TM1py.Services.MonitoringService import MonitoringService
74 |
75 | from TM1py.Utils import Utils
76 | from TM1py.Services.JobService import JobService
77 |
78 | __version__ = "2.1"
79 |
--------------------------------------------------------------------------------
/Tests/AnnotationService_test.py:
--------------------------------------------------------------------------------
1 | import configparser
2 | import random
3 | import string
4 | import unittest
5 | from pathlib import Path
6 | from uuid import uuid1
7 |
8 | from TM1py.Objects import Annotation, Cube, Dimension, Element, Hierarchy
9 | from TM1py.Services import TM1Service
10 |
11 |
12 | class TestAnnotationService(unittest.TestCase):
13 | tm1: TM1Service
14 |
15 | @classmethod
16 | def setUpClass(cls):
17 | """
18 | Establishes a connection to TM1
19 | """
20 |
21 | # Connection to TM1
22 | cls.config = configparser.ConfigParser()
23 | cls.config.read(Path(__file__).parent.joinpath('config.ini'))
24 | cls.tm1 = TM1Service(**cls.config['tm1srv01'])
25 |
26 | @classmethod
27 | def tearDownClass(cls):
28 | """
29 | Close the connection once all tests have run.
30 | """
31 | cls.tm1.logout()
32 |
33 | def setUp(self):
34 | """
35 | Run before each test to create a cube with test annotations
36 | """
37 | # Build Dimensions
38 | test_uuid = str(uuid1()).replace('-', "_")
39 | self.dimension_names = (
40 | "TM1py_tests_annotations_dimension1_" + test_uuid,
41 | "TM1py_tests_annotations_dimension2_" + test_uuid,
42 | "TM1py_tests_annotations_dimension3_" + test_uuid,
43 | )
44 |
45 | for dimension_name in self.dimension_names:
46 | elements = [Element('Element {}'.format(str(j)), 'Numeric') for j in range(1, 1001)]
47 | hierarchy = Hierarchy(dimension_name=dimension_name,
48 | name=dimension_name,
49 | elements=elements)
50 | dimension = Dimension(dimension_name, [hierarchy])
51 | self.tm1.dimensions.update_or_create(dimension)
52 |
53 | # Build Cube
54 | self.cube_name = "TM1py_tests_annotations_" + test_uuid
55 | cube = Cube(self.cube_name, self.dimension_names)
56 | self.tm1.cubes.update_or_create(cube)
57 |
58 | random_intersection = self.tm1.cubes.get_random_intersection(self.cube_name, False)
59 | random_text = "".join([random.choice(string.printable) for _ in range(100)])
60 |
61 | annotation = Annotation(comment_value=random_text,
62 | object_name=self.cube_name,
63 | dimensional_context=random_intersection)
64 |
65 | self.annotation_id = self.tm1.cubes.annotations.create(annotation).json().get("ID")
66 |
67 | def tearDown(self):
68 | """
69 | Run at the end of each test to remove unique test cube and dimensions.
70 | Created annotations will be deleted implicitly by removing the cube.
71 | """
72 | self.tm1.cubes.delete(cube_name=self.cube_name)
73 |
74 | for dimension_name in self.dimension_names:
75 | self.tm1.dimensions.delete(dimension_name=dimension_name)
76 |
77 | def test_get_all(self):
78 | """
79 | Check that get_all returns a list
80 | Check that the list of annotations returned contains the test annotation
81 | """
82 | annotations = self.tm1.cubes.annotations.get_all(self.cube_name)
83 | self.assertIsInstance(annotations, list)
84 |
85 | annotation_ids = [a.id for a in annotations]
86 | self.assertIn(self.annotation_id, annotation_ids)
87 |
88 | def test_create(self):
89 | """
90 | Check that an annotation can be created on the server
91 | Check that created annotation has the correct comment_value
92 | """
93 | annotation_count = len(self.tm1.cubes.annotations.get_all(self.cube_name))
94 | random_intersection = self.tm1.cubes.get_random_intersection(self.cube_name, False)
95 | random_text = "".join([random.choice(string.printable) for _ in range(100)])
96 |
97 | annotation = Annotation(
98 | comment_value=random_text,
99 | object_name=self.cube_name,
100 | dimensional_context=random_intersection)
101 |
102 | annotation_id = self.tm1.cubes.annotations.create(annotation).json().get("ID")
103 | all_annotations = self.tm1.cubes.annotations.get_all(self.cube_name)
104 | self.assertGreater(len(all_annotations), annotation_count)
105 |
106 | new_annotation = self.tm1.cubes.annotations.get(annotation_id)
107 | self.assertEqual(new_annotation.comment_value, random_text)
108 |
109 | def test_create_many(self):
110 | """
111 | Check that an annotation can be created on the server
112 | Check that created annotation has the correct comment_value
113 | """
114 | pre_annotation_count = len(self.tm1.cubes.annotations.get_all(self.cube_name))
115 |
116 | annotations = list()
117 | for _ in range(5):
118 | random_intersection = self.tm1.cubes.get_random_intersection(self.cube_name, False)
119 | random_text = "".join([random.choice(string.printable) for _ in range(100)])
120 |
121 | annotations.append(Annotation(
122 | comment_value=random_text,
123 | object_name=self.cube_name,
124 | dimensional_context=random_intersection))
125 |
126 | self.tm1.cubes.annotations.create_many(annotations)
127 | all_annotations = self.tm1.cubes.annotations.get_all(self.cube_name)
128 | self.assertEqual(len(all_annotations), pre_annotation_count + 5)
129 |
130 | def test_get(self):
131 | """
132 | Check that get returns the test annotation from its id
133 | """
134 | annotation = self.tm1.cubes.annotations.get(self.annotation_id)
135 | self.assertEqual(annotation.id, self.annotation_id)
136 |
137 | def test_update(self):
138 | """
139 | Check that the test annotation's comment_value can be changed
140 | Check that the last_updated date has increased
141 | Check that the created date remains the same
142 | """
143 | annotation = self.tm1.cubes.annotations.get(self.annotation_id)
144 | new_random_text = "".join([random.choice(string.printable) for _ in range(100)])
145 | annotation.comment_value = new_random_text
146 |
147 | self.tm1.cubes.annotations.update(annotation)
148 | annotation_updated = self.tm1.cubes.annotations.get(self.annotation_id)
149 |
150 | self.assertEqual(annotation_updated.comment_value, new_random_text)
151 | self.assertNotEqual(annotation_updated.last_updated, annotation.last_updated)
152 | self.assertEqual(annotation_updated.created, annotation.created)
153 |
154 | def test_delete(self):
155 | """
156 | Check that the test annotation can be deleted
157 | """
158 |
159 | annotation_id = self.annotation_id
160 | annotation_count = len(self.tm1.cubes.annotations.get_all(self.cube_name))
161 | self.tm1.annotations.delete(annotation_id)
162 | self.assertLess(len(self.tm1.cubes.annotations.get_all(self.cube_name)), annotation_count)
163 |
164 |
165 | if __name__ == '__main__':
166 | unittest.main()
167 |
--------------------------------------------------------------------------------
/Tests/CaseAndSpaceInsensitiveDict_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from TM1py.Utils.Utils import CaseAndSpaceInsensitiveDict
3 |
4 |
5 | class TestCaseAndSpaceInsensitiveDict(unittest.TestCase):
6 |
7 | def setUp(self):
8 | self.map = CaseAndSpaceInsensitiveDict()
9 | self.map["key1"] = "value1"
10 | self.map["key2"] = "value2"
11 | self.map["key3"] = "value3"
12 |
13 | def tearDown(self):
14 | del self.map
15 |
16 | def test_set_item(self):
17 | # Test setting new items with case and space variations
18 | self.map["key4"] = "value4"
19 | self.assertEqual(self.map["KEY4"], "value4")
20 | self.assertEqual(self.map["key4"], "value4")
21 | self.assertEqual(self.map["K e Y 4"], "value4")
22 |
23 | def test_get_item(self):
24 | # Test getting items with case and space variations
25 | self.assertEqual(self.map["KEY1"], "value1")
26 | self.assertEqual(self.map["key2"], "value2")
27 | self.assertEqual(self.map["K e Y 3"], "value3")
28 |
29 | def test_get_with_default(self):
30 | # Test getting an item with a default value for non-existing key
31 | self.assertEqual(self.map.get("nonexistent", "default"), "default")
32 | self.assertEqual(self.map.get("KEY1", "default"), "value1")
33 |
34 | def test_delete_item(self):
35 | # Delete items with case and space insensitivity
36 | del self.map["KEY1"]
37 | del self.map["key2"]
38 | del self.map["K e Y 3"]
39 |
40 | # Confirm deletion
41 | self.assertNotIn("key1", self.map)
42 | self.assertNotIn("key2", self.map)
43 | self.assertNotIn("key3", self.map)
44 |
45 | def test_iter(self):
46 | # Ensure iteration maintains insertion order
47 | for key1, key2 in zip(self.map, ("key1", "key2", "key3")):
48 | self.assertEqual(key1, key2)
49 |
50 | def test_len(self):
51 | # Verify length
52 | self.assertEqual(len(self.map), 3)
53 | self.map["key4"] = "value4"
54 | self.assertEqual(len(self.map), 4)
55 |
56 | def test_copy(self):
57 | # Verify copy creates a new instance with the same data
58 | copy_map = self.map.copy()
59 | self.assertIsNot(copy_map, self.map)
60 | self.assertEqual(copy_map, self.map)
61 |
62 | def test_equality(self):
63 | # Exact match
64 | other_map = CaseAndSpaceInsensitiveDict()
65 | other_map["key1"] = "value1"
66 | other_map["key2"] = "value2"
67 | other_map["key3"] = "value3"
68 | self.assertEqual(self.map, other_map)
69 |
70 | def test_equality_case_and_space_insensitive(self):
71 | # Case and space insensitive match
72 | other_map = CaseAndSpaceInsensitiveDict()
73 | other_map["key1"] = "value1"
74 | other_map["KEY2"] = "value2"
75 | other_map["K e Y 3"] = "value3"
76 | self.assertEqual(self.map, other_map)
77 |
78 | def test_inequality(self):
79 | # Mismatched values or additional keys should cause inequality
80 | other_map = CaseAndSpaceInsensitiveDict({"key 1": "wrong", "key 2": "value2", "key3": "value3"})
81 | self.assertNotEqual(self.map, other_map)
82 |
83 | other_map = CaseAndSpaceInsensitiveDict({"key1": "value1", "key 2": "wrong", "key3": "value3"})
84 | self.assertNotEqual(self.map, other_map)
85 |
86 | other_map = CaseAndSpaceInsensitiveDict({"key1": "value1", "key2": "value2", "key4": "value4"})
87 | self.assertNotEqual(self.map, other_map)
88 |
89 | def test_update(self):
90 | # Test updating existing and new keys
91 | update_map = {"KEY1": "new_value1", "new_key": "new_value"}
92 | self.map.update(update_map)
93 |
94 | self.assertEqual(self.map["key1"], "new_value1")
95 | self.assertEqual(self.map["new_key"], "new_value")
96 | self.assertEqual(len(self.map), 4)
97 |
98 | def test_setdefault(self):
99 | # Existing key should return its value
100 | self.assertEqual(self.map.setdefault("key1", "default"), "value1")
101 |
102 | # New key should set and return the default value
103 | self.assertEqual(self.map.setdefault("new_key", "default"), "default")
104 | self.assertEqual(self.map["new_key"], "default")
105 |
106 | def test_keys(self):
107 | # Check keys() method for case and insertion order preservation
108 | expected_keys = ["key1", "key2", "key3"]
109 | self.assertEqual(list(self.map.keys()), expected_keys)
110 |
111 | def test_values(self):
112 | # Check values() method for correct values in insertion order
113 | expected_values = ["value1", "value2", "value3"]
114 | self.assertEqual(list(self.map.values()), expected_values)
115 |
116 | def test_items(self):
117 | # Check items() method for correct key-value pairs in insertion order
118 | expected_items = [("key1", "value1"), ("key2", "value2"), ("key3", "value3")]
119 | self.assertEqual(list(self.map.items()), expected_items)
120 |
121 | def test_adjusted_keys(self):
122 | # Test adjusted_keys() to ensure all keys are lowercased and spaceless
123 | expected_adjusted_keys = ["key1", "key2", "key3"]
124 | self.assertEqual(list(self.map.adjusted_keys()), expected_adjusted_keys)
125 |
126 | def test_adjusted_items(self):
127 | # Test adjusted_items() for lowercased, spaceless keys
128 | expected_adjusted_items = {
129 | "key1": "value1",
130 | "key2": "value2",
131 | "key3": "value3"
132 | }
133 | adjusted_items = dict(self.map.adjusted_items())
134 | self.assertEqual(adjusted_items, expected_adjusted_items)
135 |
136 | def test_contains(self):
137 | # Test in operator with case and space insensitivity
138 | self.assertIn("key1", self.map)
139 | self.assertIn("KEY2", self.map)
140 | self.assertIn("k e y 3", self.map)
141 | self.assertNotIn("nonexistent_key", self.map)
142 |
143 | def test_keyerror_on_nonexistent_key(self):
144 | # Confirm that accessing a nonexistent key raises KeyError
145 | with self.assertRaises(KeyError):
146 | _ = self.map["nonexistent_key"]
147 |
148 |
149 | if __name__ == '__main__':
150 | unittest.main()
151 |
--------------------------------------------------------------------------------
/Tests/CaseAndSpaceInsensitiveSet_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from TM1py.Utils.Utils import CaseAndSpaceInsensitiveSet
3 |
4 |
5 | class TestCaseAndSpaceInsensitiveSet(unittest.TestCase):
6 |
7 | def setUp(self):
8 | self.original_values = ("Value1", "Value 2", "V A L U E 3")
9 | self.set = CaseAndSpaceInsensitiveSet()
10 | for value in self.original_values:
11 | self.set.add(value)
12 |
13 | def tearDown(self):
14 | del self.set
15 |
16 | def test_contains(self):
17 | # Case and space insensitivity
18 | self.assertIn("Value1", self.set)
19 | self.assertIn("VALUE1", self.set)
20 | self.assertIn("V ALUE 1", self.set)
21 | self.assertIn("Value 2", self.set)
22 | self.assertIn("V A L UE 2", self.set)
23 | self.assertIn("VALUE3", self.set)
24 | self.assertIn("V A LUE 3", self.set)
25 |
26 | # Non-existent values
27 | self.assertNotIn("Value", self.set)
28 | self.assertNotIn("VALUE4", self.set)
29 | self.assertNotIn("VA LUE 4", self.set)
30 |
31 | def test_del(self):
32 | # Remove elements with del
33 | del self.set["VALUE1"]
34 | del self.set["value 2"]
35 | del self.set["V a L u E 3"]
36 | # Verify the set is empty
37 | self.assertFalse(self.set)
38 |
39 | def test_discard(self):
40 | # Discard elements and check if set is empty
41 | self.set.discard("Value1")
42 | self.set.discard("Value 2")
43 | self.set.discard("Value3")
44 | self.assertFalse(self.set)
45 |
46 | def test_discard_case_and_space_insensitivity(self):
47 | # Discard with case and space insensitivity
48 | self.set.discard("VAL UE 1")
49 | self.set.discard("Value 2")
50 | self.set.discard("VA LUE3")
51 | self.assertFalse(self.set)
52 |
53 | def test_len(self):
54 | # Check length after addition and removal
55 | self.assertEqual(len(self.set), 3)
56 | self.set.add("Value4")
57 | self.assertEqual(len(self.set), 4)
58 | self.set.discard("VALUE4")
59 | self.assertEqual(len(self.set), 3)
60 |
61 | def test_iter(self):
62 | # Ensure set iteration matches the original values
63 | self.assertEqual(len(self.set), len(self.original_values))
64 | for value in self.set:
65 | self.assertIn(value, self.original_values)
66 |
67 | def test_add(self):
68 | # Test adding a new element with various case/space combinations
69 | self.set.add("Value4")
70 | self.assertIn("Value4", self.set)
71 | self.assertIn("VALUE4", self.set)
72 | self.assertIn(" VALUE4", self.set)
73 | self.assertIn("VALUE4 ", self.set)
74 | self.assertIn("V ALUE 4", self.set)
75 | self.assertIn("Va L UE4", self.set)
76 | self.assertIn(" VAlue4", self.set)
77 |
78 | def test_copy(self):
79 | # Verify copy creates a new instance with identical elements
80 | copy_set = self.set.copy()
81 | self.assertIsNot(copy_set, self.set)
82 | self.assertEqual(copy_set, self.set)
83 |
84 | def test_eq(self):
85 | # Equality with exact and case-insensitive values
86 | new_set = CaseAndSpaceInsensitiveSet(self.original_values)
87 | self.assertEqual(self.set, new_set)
88 |
89 | # Case and space-insensitive match
90 | new_set = CaseAndSpaceInsensitiveSet(value.upper() for value in self.original_values)
91 | self.assertEqual(self.set, new_set)
92 |
93 | def test_eq_against_builtin_set(self):
94 | # Equality check against Python's built-in set
95 | new_set = set(self.original_values)
96 | self.assertEqual(self.set, new_set)
97 |
98 | def test_ne(self):
99 | # Inequality with completely different values
100 | new_set = CaseAndSpaceInsensitiveSet(["wrong1", "wrong2", "wrong3"])
101 | self.assertNotEqual(self.set, new_set)
102 |
103 | def test_clear(self):
104 | # Clear the set and verify it's empty
105 | self.set.clear()
106 | self.assertEqual(len(self.set), 0)
107 | self.assertFalse(self.set)
108 |
109 | def test_pop(self):
110 | # Pop elements and confirm they exist in the original values
111 | popped_value = self.set.pop()
112 | self.assertIn(popped_value, self.original_values)
113 | self.assertEqual(len(self.set), 2)
114 |
115 | def test_update(self):
116 | # Update set with additional elements
117 | self.set.update(["NewValue", "Value1"]) # Value1 already exists
118 | self.assertIn("NewValue", self.set)
119 | self.assertEqual(len(self.set), 4)
120 |
121 | def test_union(self):
122 | # Union with another set
123 | new_set = CaseAndSpaceInsensitiveSet(["ExtraValue", "VALUE1"])
124 | result_set = self.set.union(new_set)
125 | self.assertIn("ExtraValue", result_set)
126 | self.assertEqual(len(result_set), 4)
127 |
128 | def test_intersection(self):
129 | # Intersection with a set containing common elements
130 | new_set = CaseAndSpaceInsensitiveSet(["VALUE1", "UnknownValue"])
131 | result_set = self.set.intersection(new_set)
132 | self.assertIn("Value1", result_set)
133 | self.assertEqual(len(result_set), 1)
134 |
135 | def test_difference(self):
136 | # Difference with another set
137 | new_set = CaseAndSpaceInsensitiveSet(["Value1", "ExtraValue"])
138 | result_set = self.set.difference(new_set)
139 | self.assertNotIn("Value1", result_set)
140 | self.assertIn("Value 2", result_set)
141 | self.assertEqual(len(result_set), 2)
142 |
143 | def test_subset_and_superset_operations(self):
144 | # Test subset, superset, and proper subset/superset relationships
145 | new_set = CaseAndSpaceInsensitiveSet(["Value1"])
146 | self.assertTrue(new_set < self.set) # Proper subset
147 | self.assertTrue(new_set <= self.set) # Subset
148 | self.assertTrue(self.set > new_set) # Proper superset
149 | self.assertTrue(self.set >= new_set) # Superset
150 | self.assertFalse(new_set > self.set) # Not a superset
151 | self.assertFalse(new_set == self.set) # Not equal
152 |
153 | def test_disjoint(self):
154 | # Check for disjoint sets
155 | disjoint_set = CaseAndSpaceInsensitiveSet(["OtherValue"])
156 | self.assertTrue(self.set.isdisjoint(disjoint_set))
157 |
158 | overlapping_set = CaseAndSpaceInsensitiveSet(["VALUE1", "NewValue"])
159 | self.assertFalse(self.set.isdisjoint(overlapping_set))
160 |
161 |
162 | if __name__ == '__main__':
163 | unittest.main()
164 |
--------------------------------------------------------------------------------
/Tests/ChoreStartTime_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from TM1py import ChoreStartTime
4 |
5 |
6 | class TestChoreStartTime(unittest.TestCase):
7 |
8 | def test_start_time_string_happy_case(self):
9 | chore_start_time = ChoreStartTime(year=2020, month=11, day=26, hour=10, minute=11, second=2)
10 | self.assertEqual(chore_start_time.start_time_string, "2020-11-26T10:11:02Z")
11 |
12 | def test_start_time_string_no_seconds(self):
13 | chore_start_time = ChoreStartTime(year=2020, month=11, day=26, hour=10, minute=11, second=0)
14 | self.assertEqual(chore_start_time.start_time_string, "2020-11-26T10:11Z")
15 |
16 | def test_start_time_string_no_minutes_no_seconds(self):
17 | chore_start_time = ChoreStartTime(year=2020, month=11, day=26, hour=10, minute=0, second=0)
18 | self.assertEqual(chore_start_time.start_time_string, "2020-11-26T10:00Z")
19 |
20 | def test_start_time_string_with_positive_tz(self):
21 | chore_start_time = ChoreStartTime(year=2020, month=11, day=26, hour=10, minute=1, second=1, tz="+02:00")
22 | self.assertEqual(chore_start_time.start_time_string, "2020-11-26T10:01:01+02:00")
23 |
24 | def test_start_time_string_with_negative_tz(self):
25 | chore_start_time = ChoreStartTime(year=2020, month=11, day=26, hour=10, minute=1, second=1, tz="-01:00")
26 | self.assertEqual(chore_start_time.start_time_string, "2020-11-26T10:01:01-01:00")
27 |
28 | def test_start_time_string_without_tz(self):
29 | chore_start_time = ChoreStartTime(year=2020, month=11, day=26, hour=10, minute=1, second=1)
30 | self.assertEqual(chore_start_time.start_time_string, "2020-11-26T10:01:01Z")
31 |
--------------------------------------------------------------------------------
/Tests/Chore_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from TM1py import Chore, ChoreStartTime, ChoreFrequency, ChoreTask
4 | from TM1py.Objects import Chore
5 |
6 | class TestChore(unittest.TestCase):
7 | def setUp(self):
8 | self.chore = Chore(
9 | name="chore",
10 | start_time=ChoreStartTime(2023, 8, 4, 10, 0, 0),
11 | dst_sensitivity=False,
12 | active=True,
13 | execution_mode="SingleCommit",
14 | frequency=ChoreFrequency(1, 0, 0, 0),
15 | tasks=[
16 | ChoreTask(
17 | step=0,
18 | process_name="}bedrock.server.wait",
19 | parameters=[
20 | {'Name': 'pLogOutput', 'Value': 0},
21 | {'Name': 'pStrictErrorHandling', 'Value': 1},
22 | {'Name': 'pWaitSec', 'Value': 4}]),
23 | ChoreTask(
24 | step=1,
25 | process_name="}bedrock.server.wait",
26 | parameters=[
27 | {'Name': 'pLogOutput', 'Value': 0},
28 | {'Name': 'pStrictErrorHandling', 'Value': 1},
29 | {'Name': 'pWaitSec', 'Value': 5}]),
30 | ])
31 |
32 | def test_from_dict_and_construct_body(self):
33 | text = self.chore.body
34 |
35 | chore = Chore.from_json(text)
36 |
37 | self.assertEqual(self.chore, chore)
38 |
39 | def test_insert_task_as_step_0(self):
40 | self.chore.insert_task(
41 | ChoreTask(
42 | step=0,
43 | process_name="}bedrock.cube.clone",
44 | parameters=[
45 | {'Name': 'pLogOutput', 'Value': 0},
46 | {'Name': 'pStrictErrorHandling', 'Value': 1},
47 | {'Name': 'pWaitSec', 'Value': 5}]))
48 | expected_chore = Chore(
49 | name="chore",
50 | start_time=ChoreStartTime(2023, 8, 4, 10, 0, 0),
51 | dst_sensitivity=False,
52 | active=True,
53 | execution_mode="SingleCommit",
54 | frequency=ChoreFrequency(1, 0, 0, 0),
55 | tasks=[
56 | ChoreTask(
57 | step=0,
58 | process_name="}bedrock.cube.clone",
59 | parameters=[
60 | {'Name': 'pLogOutput', 'Value': 0},
61 | {'Name': 'pStrictErrorHandling', 'Value': 1},
62 | {'Name': 'pWaitSec', 'Value': 5}]),
63 | ChoreTask(
64 | step=1,
65 | process_name="}bedrock.server.wait",
66 | parameters=[
67 | {'Name': 'pLogOutput', 'Value': 0},
68 | {'Name': 'pStrictErrorHandling', 'Value': 1},
69 | {'Name': 'pWaitSec', 'Value': 4}]),
70 |
71 | ChoreTask(
72 | step=2,
73 | process_name="}bedrock.server.wait",
74 | parameters=[
75 | {'Name': 'pLogOutput', 'Value': 0},
76 | {'Name': 'pStrictErrorHandling', 'Value': 1},
77 | {'Name': 'pWaitSec', 'Value': 5}]),
78 | ])
79 | self.assertEqual(self.chore, expected_chore)
80 |
81 | def test_insert_task_as_step_1(self):
82 | self.chore.insert_task(
83 | ChoreTask(
84 | step=1,
85 | process_name="}bedrock.cube.clone",
86 | parameters=[
87 | {'Name': 'pLogOutput', 'Value': 0},
88 | {'Name': 'pStrictErrorHandling', 'Value': 1},
89 | {'Name': 'pWaitSec', 'Value': 5}]))
90 | expected_chore = Chore(
91 | name="chore",
92 | start_time=ChoreStartTime(2023, 8, 4, 10, 0, 0),
93 | dst_sensitivity=False,
94 | active=True,
95 | execution_mode="SingleCommit",
96 | frequency=ChoreFrequency(1, 0, 0, 0),
97 | tasks=[
98 | ChoreTask(
99 | step=0,
100 | process_name="}bedrock.server.wait",
101 | parameters=[
102 | {'Name': 'pLogOutput', 'Value': 0},
103 | {'Name': 'pStrictErrorHandling', 'Value': 1},
104 | {'Name': 'pWaitSec', 'Value': 4}]),
105 | ChoreTask(
106 | step=1,
107 | process_name="}bedrock.cube.clone",
108 | parameters=[
109 | {'Name': 'pLogOutput', 'Value': 0},
110 | {'Name': 'pStrictErrorHandling', 'Value': 1},
111 | {'Name': 'pWaitSec', 'Value': 5}]),
112 | ChoreTask(
113 | step=2,
114 | process_name="}bedrock.server.wait",
115 | parameters=[
116 | {'Name': 'pLogOutput', 'Value': 0},
117 | {'Name': 'pStrictErrorHandling', 'Value': 1},
118 | {'Name': 'pWaitSec', 'Value': 5}]),
119 | ])
120 | self.assertEqual(self.chore, expected_chore)
121 |
122 |
123 | if __name__ == '__main__':
124 | unittest.main()
125 |
126 |
--------------------------------------------------------------------------------
/Tests/Cube_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from TM1py import Cube, Rules
4 |
5 |
6 | class TestCube(unittest.TestCase):
7 | rules = """
8 | ['d1':'e1'] = N: 1;
9 | ['d1':'e2'] = N: 2;
10 | ['d1':'e3'] = N: 3;
11 | """
12 | cube = Cube(
13 | name="c1",
14 | dimensions=["d1", "d2"],
15 | rules=rules)
16 |
17 | def test_update_rule_with_str(self):
18 | self.cube.rules = "['d1':'e1'] = N: 1;"
19 |
20 | self.assertEqual(
21 | self.cube.rules,
22 | Rules("['d1':'e1'] = N: 1;"))
23 |
24 | def test_update_rule_with_rules_obj(self):
25 | self.cube.rules = Rules("['d1':'e1'] = N: 2;")
26 |
27 | self.assertEqual(
28 | self.cube.rules,
29 | Rules("['d1':'e1'] = N: 2;"))
30 |
31 |
32 | if __name__ == '__main__':
33 | unittest.main()
34 |
--------------------------------------------------------------------------------
/Tests/ElementAttribute_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from TM1py import ElementAttribute
4 |
5 |
6 | class TestElementAttribute(unittest.TestCase):
7 |
8 | def test_eq_happy_case(self):
9 | element_attribute1 = ElementAttribute(name="Attribute 1", attribute_type="String")
10 | element_attribute2 = ElementAttribute(name="Attribute 1", attribute_type="String")
11 |
12 | self.assertEqual(element_attribute1, element_attribute2)
13 |
14 | def test_ne_name(self):
15 | element_attribute1 = ElementAttribute(name="Attribute 1", attribute_type="String")
16 | element_attribute2 = ElementAttribute(name="Attribute 2", attribute_type="String")
17 |
18 | self.assertNotEqual(element_attribute1, element_attribute2)
19 |
20 | def test_ne_type(self):
21 | element_attribute1 = ElementAttribute(name="Attribute 1", attribute_type="String")
22 | element_attribute2 = ElementAttribute(name="Attribute 1", attribute_type="Numeric")
23 |
24 | self.assertNotEqual(element_attribute1, element_attribute2)
25 |
26 | def test_eq_case_space_difference(self):
27 | element_attribute1 = ElementAttribute(name="Attribute 1", attribute_type="String")
28 | element_attribute2 = ElementAttribute(name="ATTRIBUTE1", attribute_type="String")
29 |
30 | self.assertEqual(element_attribute1, element_attribute2)
31 |
32 | def test_hash_happy_case(self):
33 | element_attribute1 = ElementAttribute(name="Attribute 1", attribute_type="String")
34 | element_attribute2 = ElementAttribute(name="Attribute 1", attribute_type="String")
35 |
36 | self.assertEqual(hash(element_attribute1), hash(element_attribute2))
37 |
38 | def test_construct_body(self):
39 | element = ElementAttribute(name="Attribute 1", attribute_type="Numeric")
40 |
41 | self.assertEqual(
42 | element.body_as_dict,
43 | {'Name': 'Attribute 1', 'Type': 'Numeric'})
44 |
45 |
46 | if __name__ == '__main__':
47 | unittest.main()
48 |
--------------------------------------------------------------------------------
/Tests/Element_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from TM1py import Element
4 |
5 |
6 | class TestElement(unittest.TestCase):
7 |
8 | def test_eq_happy_case(self):
9 | element1 = Element(name="Element 1", element_type="Numeric")
10 | element2 = Element(name="Element 1", element_type="NUMERIC")
11 |
12 | self.assertEqual(element1, element2)
13 |
14 | def test_ne_happy_case(self):
15 | element1 = Element(name="Element 1", element_type="Numeric")
16 | element2 = Element(name="Element 2", element_type="NUMERIC")
17 |
18 | self.assertNotEqual(element1, element2)
19 |
20 | def test_eq_case_space_difference(self):
21 | element1 = Element(name="Element 1", element_type="Numeric")
22 | element2 = Element(name="ELEMENT1", element_type="NUMERIC")
23 |
24 | self.assertEqual(element1, element2)
25 |
26 | def test_hash_happy_case(self):
27 | element1 = Element(name="Element 1", element_type="Numeric")
28 | element2 = Element(name="Element 1", element_type="Numeric")
29 |
30 | self.assertEqual(hash(element1), hash(element2))
31 |
32 | def test_construct_body(self):
33 | element = Element("e1", "Numeric")
34 |
35 | self.assertEqual(
36 | element._construct_body(),
37 | {'Name': 'e1', 'Type': 'Numeric'})
38 |
39 |
40 | if __name__ == '__main__':
41 | unittest.main()
42 |
--------------------------------------------------------------------------------
/Tests/MDXView_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from TM1py import MDXView
4 |
5 |
6 | class TestMDXView(unittest.TestCase):
7 | cube_name = "c1"
8 | view_name = "v1"
9 | mdx = """
10 | SELECT
11 | {[d1].[e1], [d1].[e2], [d1].[e3]} ON ROWS,
12 | {Tm1SubsetAll([d2])} ON COLUMNS
13 | FROM [c1]
14 | WHERE ([d3].[d3].[e1], [d4].[e1], [d5].[h1].[e1])
15 | """
16 |
17 | def setUp(self) -> None:
18 | self.view = MDXView(
19 | cube_name=self.cube_name,
20 | view_name=self.view_name,
21 | MDX=self.mdx)
22 |
23 | def test_substitute_title(self):
24 | self.view.substitute_title(dimension="d3", hierarchy="d3", element="e2")
25 | expected_mdx = """
26 | SELECT
27 | {[d1].[e1], [d1].[e2], [d1].[e3]} ON ROWS,
28 | {Tm1SubsetAll([d2])} ON COLUMNS
29 | FROM [c1]
30 | WHERE ([d3].[d3].[e2], [d4].[e1], [d5].[h1].[e1])
31 | """
32 | self.assertEqual(expected_mdx, self.view.mdx)
33 |
34 | def test_substitute_title_different_case(self):
35 | self.view.substitute_title(dimension="D3", hierarchy="D3", element="e2")
36 | expected_mdx = """
37 | SELECT
38 | {[d1].[e1], [d1].[e2], [d1].[e3]} ON ROWS,
39 | {Tm1SubsetAll([d2])} ON COLUMNS
40 | FROM [c1]
41 | WHERE ([D3].[D3].[e2], [d4].[e1], [d5].[h1].[e1])
42 | """
43 | self.assertEqual(expected_mdx, self.view.mdx)
44 |
45 | def test_substitute_title_without_hierarchy(self):
46 | self.view.substitute_title(dimension="d4", hierarchy="d4", element="e2")
47 | expected_mdx = """
48 | SELECT
49 | {[d1].[e1], [d1].[e2], [d1].[e3]} ON ROWS,
50 | {Tm1SubsetAll([d2])} ON COLUMNS
51 | FROM [c1]
52 | WHERE ([d3].[d3].[e1], [d4].[e2], [d5].[h1].[e1])
53 | """
54 | self.assertEqual(expected_mdx, self.view.mdx)
55 |
56 | def test_substitute_title_with_hierarchy(self):
57 | self.view.substitute_title(dimension="d5", hierarchy="h1", element="e2")
58 | expected_mdx = """
59 | SELECT
60 | {[d1].[e1], [d1].[e2], [d1].[e3]} ON ROWS,
61 | {Tm1SubsetAll([d2])} ON COLUMNS
62 | FROM [c1]
63 | WHERE ([d3].[d3].[e1], [d4].[e1], [d5].[h1].[e2])
64 | """
65 | self.assertEqual(expected_mdx, self.view.mdx)
66 |
67 | def test_substitute_title_value_error(self):
68 | with self.assertRaises(ValueError) as error:
69 | self.view.substitute_title(dimension="d6", hierarchy="d6", element="e2")
70 | print(error)
71 |
--------------------------------------------------------------------------------
/Tests/ManageService_test.py:
--------------------------------------------------------------------------------
1 | import configparser
2 | import pytest
3 | import random
4 | import string
5 | import unittest
6 | from pathlib import Path
7 | import time
8 |
9 | from TM1py.Services import ManageService, TM1Service
10 | from .Utils import skip_if_version_lower_than
11 | from .Utils import verify_version
12 |
13 |
14 | class TestManagerService(unittest.TestCase):
15 | manager: ManageService
16 | instance = "TM1py_tests_instance"
17 | database = "TM1py_tests_database"
18 | application = "TM1py_tests_application"
19 | backup_set = f"{database}_backup"
20 | starting_replicas = 0
21 | cpu_requests = "1000m"
22 | cpu_limits = "2000m"
23 | memory_requests = "1G"
24 | memory_limits = "2G"
25 | storage_size = "20Gi"
26 | version = "12.3.4"
27 | application_client = None
28 | application_secret = None
29 |
30 | @classmethod
31 | def setUpClass(cls):
32 | """
33 | Establishes a connection to the manager endpoint and creates a testing environment
34 | """
35 | # Manager Connection
36 | cls.config = configparser.ConfigParser()
37 | cls.config.read(Path(__file__).parent.joinpath('config.ini'))
38 | connection_details = cls.config['tm1srv01']
39 |
40 | #Connect to TM1 to get server versions.
41 | cls.tm1 = TM1Service(**cls.config['tm1srv01'])
42 |
43 | if verify_version(required_version="12.0.0", version=cls.tm1.version):
44 |
45 | cls.manager = ManageService(domain=connection_details["domain"],
46 | root_client=connection_details["root_client"],
47 | root_secret=connection_details["root_secret"])
48 |
49 | # Cleanup and Create New Instance
50 | if cls.manager.instance_exists(instance_name=cls.instance):
51 | cls.manager.delete_instance(instance_name=cls.instance)
52 | cls.manager.create_instance(instance_name=cls.instance)
53 |
54 | # Cleanup and Create New Database
55 | if cls.manager.database_exists(instance_name=cls.instance, database_name=cls.database):
56 | cls.manager.delete_database(instance_name=cls.instance, database_name=cls.database)
57 | cls.manager.create_database(instance_name=cls.instance,
58 | database_name=cls.database,
59 | product_version=cls.version,
60 | number_replicas=cls.starting_replicas,
61 | cpu_requests=cls.cpu_requests,
62 | cpu_limits=cls.cpu_limits,
63 | memory_limits=cls.memory_limits,
64 | memory_requests=cls.memory_requests,
65 | storage_size=cls.storage_size)
66 | else:
67 | raise unittest.SkipTest(f"Skipping all Manager Service tests, version minimum not met, "
68 | f"12.0.0 > {cls.tm1.version}")
69 |
70 | @classmethod
71 | def tearDownClass(cls):
72 | cls.manager.delete_database(instance_name=cls.instance,
73 | database_name=cls.database)
74 | cls.manager.delete_instance(instance_name=cls.instance)
75 |
76 | def test_get_instance(self):
77 | instance = self.manager.get_instance(instance_name=self.instance)
78 | self.assertEqual(self.instance, instance.get('Name'))
79 |
80 | def test_get_database(self):
81 | database = self.manager.get_database(instance_name=self.instance,
82 | database_name=self.database)
83 | self.assertEqual(self.database, database.get('Name'))
84 |
85 | @pytest.mark.skip(reason="Too slow for regular tests. Only run before releases")
86 | def test_scale_database(self):
87 | self.manager.scale_database(instance_name=self.instance,
88 | database_name=self.database,
89 | replicas=(self.starting_replicas + 1))
90 |
91 | replicas = self.manager.get_database(instance_name=self.instance,
92 | database_name=self.database).get('Replicas')
93 |
94 | self.assertEqual(replicas, (self.starting_replicas + 1))
95 |
96 | time.sleep(30)
97 |
98 | self.manager.scale_database(instance_name=self.instance,
99 | database_name=self.database,
100 | replicas=self.starting_replicas)
101 |
102 | replicas = self.manager.get_database(instance_name=self.instance,
103 | database_name=self.database).get('Replicas')
104 |
105 | self.assertEqual(replicas, self.starting_replicas)
106 |
107 | def test_create_and_get_application(self):
108 |
109 | # Create Application and Store Credentials
110 | self.clientID, self.clientSecret = self.manager.create_application(instance_name=self.instance,
111 | application_name=self.application)
112 |
113 | self.assertIsNotNone(self.clientID)
114 | self.assertIsNotNone(self.clientSecret)
115 |
116 | application = self.manager.get_application(instance_name=self.instance, application_name=self.application)
117 |
118 | self.assertEqual(self.application, application.get('Name'))
119 |
120 | @pytest.mark.skip(reason="Too slow for regular tests. Only run before releases")
121 | def test_create_backup_set(self):
122 |
123 | response = self.manager.create_database_backup(instance_name=self.instance,
124 | database_name=self.database,
125 | backup_set_name=self.backup_set)
126 |
127 | self.assertTrue(response.ok)
128 |
129 |
130 | if __name__ == '__main__':
131 | unittest.main(failfast=True)
132 |
--------------------------------------------------------------------------------
/Tests/MonitoringService_test.py:
--------------------------------------------------------------------------------
1 | import configparser
2 | import unittest
3 | import warnings
4 | from pathlib import Path
5 |
6 | from TM1py.Services import TM1Service
7 | from TM1py.Utils import case_and_space_insensitive_equals
8 | from .Utils import skip_if_version_higher_or_equal_than
9 |
10 |
11 | class TestMonitoringService(unittest.TestCase):
12 | tm1: TM1Service
13 |
14 | @classmethod
15 | def setUpClass(cls):
16 | """
17 | Establishes a connection to TM1 and creates TM! objects to use across all tests
18 | """
19 |
20 | # Connection to TM1
21 | cls.config = configparser.ConfigParser()
22 | cls.config.read(Path(__file__).parent.joinpath('config.ini'))
23 | cls.tm1 = TM1Service(**cls.config['tm1srv01'])
24 | with warnings.catch_warnings():
25 | warnings.simplefilter("ignore", DeprecationWarning)
26 | cls.tm1.monitoring
27 |
28 | @skip_if_version_higher_or_equal_than(version="12.0.0")
29 | def test_get_threads(self):
30 | threads = self.tm1.monitoring.get_threads()
31 | self.assertTrue(any(thread["Function"] == "GET /api/v1/Threads" for thread in threads)
32 | or
33 | any(thread["Function"] == "GET /Threads" for thread in threads))
34 |
35 | def test_get_active_users(self):
36 | current_user = self.tm1.security.get_current_user()
37 | active_users = self.tm1.monitoring.get_active_users()
38 | self.assertTrue(any(case_and_space_insensitive_equals(user.name, current_user.name) for user in active_users))
39 |
40 | def test_user_is_active(self):
41 | current_user = self.tm1.security.get_current_user()
42 | self.assertTrue(self.tm1.monitoring.user_is_active(current_user.name))
43 |
44 | def test_close_all_sessions(self):
45 | self.tm1.monitoring.close_all_sessions()
46 |
47 | def test_disconnect_all_users(self):
48 | self.tm1.monitoring.disconnect_all_users()
49 |
50 | @skip_if_version_higher_or_equal_than(version="12.0.0")
51 | def test_cancel_all_running_threads(self):
52 | self.tm1.monitoring.cancel_all_running_threads()
53 |
54 | def test_get_sessions(self):
55 | sessions = self.tm1.monitoring.get_sessions()
56 | self.assertTrue(len(sessions) > 0)
57 | self.assertIn('ID', sessions[0])
58 | self.assertIn('Context', sessions[0])
59 | self.assertIn('Active', sessions[0])
60 | self.assertIn('User', sessions[0])
61 | self.assertIn('Threads', sessions[0])
62 |
63 | def test_get_sessions_exclude_user(self):
64 | sessions = self.tm1.monitoring.get_sessions(include_user=False)
65 | self.assertTrue(len(sessions) > 0)
66 | self.assertIn('ID', sessions[0])
67 | self.assertIn('Context', sessions[0])
68 | self.assertIn('Active', sessions[0])
69 | self.assertNotIn('User', sessions[0])
70 | self.assertIn('Threads', sessions[0])
71 |
72 | def test_get_sessions_exclude_threads(self):
73 | sessions = self.tm1.monitoring.get_sessions(include_threads=False)
74 | self.assertTrue(len(sessions) > 0)
75 | self.assertIn('ID', sessions[0])
76 | self.assertIn('Context', sessions[0])
77 | self.assertIn('Active', sessions[0])
78 | self.assertIn('User', sessions[0])
79 | self.assertNotIn('Threads', sessions[0])
80 |
81 | def test_get_sessions_exclude_threads_and_user(self):
82 | sessions = self.tm1.monitoring.get_sessions(include_threads=False, include_user=False)
83 | self.assertTrue(len(sessions) > 0)
84 | self.assertIn('ID', sessions[0])
85 | self.assertIn('Context', sessions[0])
86 | self.assertIn('Active', sessions[0])
87 | self.assertNotIn('User', sessions[0])
88 | self.assertNotIn('Threads', sessions[0])
89 |
90 | @classmethod
91 | def tearDownClass(cls):
92 | cls.tm1.logout()
93 |
94 |
95 | if __name__ == '__main__':
96 | unittest.main()
97 |
--------------------------------------------------------------------------------
/Tests/Process_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from TM1py.Objects import BreakPointType, HitMode
4 |
5 |
6 | class TestBreakPointType(unittest.TestCase):
7 |
8 | def test_BreakPointType_init(self):
9 | break_point_type = BreakPointType("ProcessDebugContextDataBreakpoint")
10 | self.assertEqual(
11 | BreakPointType.PROCESS_DEBUG_CONTEXT_DATA_BREAK_POINT,
12 | break_point_type
13 | )
14 |
15 | def test_BreakPointType_init_case(self):
16 | break_point_type = BreakPointType("ProcessDebugContextDataBREAKPOINT")
17 | self.assertEqual(
18 | BreakPointType.PROCESS_DEBUG_CONTEXT_DATA_BREAK_POINT,
19 | break_point_type
20 | )
21 |
22 | def test_BreakPointType_str(self):
23 | break_point_type = BreakPointType.PROCESS_DEBUG_CONTEXT_DATA_BREAK_POINT
24 | self.assertEqual(
25 | "ProcessDebugContextDataBreakpoint",
26 | str(break_point_type)
27 | )
28 |
29 |
30 | class TestHitMode(unittest.TestCase):
31 |
32 | def test_HitMode_init(self):
33 | hit_mode = HitMode("BreakAlways")
34 | self.assertEqual(
35 | HitMode.BREAK_ALWAYS,
36 | hit_mode
37 | )
38 |
39 | def test_BreakPointType_init_case(self):
40 | hit_mode = HitMode("BreakAlways")
41 | self.assertEqual(
42 | HitMode.BREAK_ALWAYS,
43 | hit_mode
44 | )
45 |
46 | def test_BreakPointType_str(self):
47 | hit_mode = HitMode.BREAK_ALWAYS
48 | self.assertEqual(
49 | "BreakAlways",
50 | str(hit_mode)
51 | )
52 |
53 |
54 |
--------------------------------------------------------------------------------
/Tests/RestService_test.py:
--------------------------------------------------------------------------------
1 | import configparser
2 | import unittest
3 | from pathlib import Path
4 |
5 | from TM1py import TM1Service
6 | from TM1py.Services.RestService import RestService
7 |
8 |
9 | class TestRestService(unittest.TestCase):
10 | tm1: TM1Service
11 |
12 | @classmethod
13 | def setUpClass(cls):
14 | """
15 | Establishes a connection to TM1 and creates TM! objects to use across all tests
16 | """
17 |
18 | # Connection to TM1
19 | cls.config = configparser.ConfigParser()
20 | cls.config.read(Path(__file__).parent.joinpath('config.ini'))
21 | cls.tm1 = TM1Service(**cls.config['tm1srv01'])
22 |
23 | def test_wait_time_generator_with_float_timeout(self):
24 | self.assertEqual(
25 | [0.1, 0.3, 0.6, 1, 1, 1, 1, 1, 1, 1, 1, 1],
26 | list(self.tm1._tm1_rest.wait_time_generator(10.0)))
27 | self.assertEqual(sum(self.tm1._tm1_rest.wait_time_generator(10)), 10)
28 |
29 | def test_wait_time_generator_with_timeout(self):
30 | self.assertEqual(
31 | [0.1, 0.3, 0.6, 1, 1, 1, 1, 1, 1, 1, 1, 1],
32 | list(self.tm1._tm1_rest.wait_time_generator(10)))
33 | self.assertEqual(sum(self.tm1._tm1_rest.wait_time_generator(10)), 10)
34 |
35 | def test_wait_time_generator_without_timeout(self):
36 | generator = self.tm1._tm1_rest.wait_time_generator(None)
37 | self.assertEqual(0.1, next(generator))
38 | self.assertEqual(0.3, next(generator))
39 | self.assertEqual(0.6, next(generator))
40 | self.assertEqual(1, next(generator))
41 | self.assertEqual(1, next(generator))
42 |
43 | def test_build_response_from_async_response_ok(self):
44 | response_content = b'HTTP/1.1 200 OK\r\nContent-Length: 32\r\nConnection: keep-alive\r\nContent-Encoding: ' \
45 | b'gzip\r\nCache-Control: no-cache\r\nContent-Type: text/plain; charset=utf-8\r\n' \
46 | b'OData-Version: 4.0\r\n\r\n\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x0b34\xd43\xd730000' \
47 | b'\xd23\x04\x00\xf4\x1c\xa0j\x0c\x00\x00\x00'
48 | response = RestService.build_response_from_binary_response(response_content)
49 | self.assertEqual(response.status_code, 200)
50 | self.assertEqual(response.headers.get("Content-Length"), "32")
51 | self.assertEqual(response.headers.get("Connection"), "keep-alive")
52 | self.assertEqual(response.headers.get("Content-Encoding"), "gzip")
53 | self.assertEqual(response.headers.get("Cache-Control"), "no-cache")
54 | self.assertEqual(response.headers.get("Content-Type"), "text/plain; charset=utf-8")
55 | self.assertEqual(response.headers.get("OData-Version"), "4.0")
56 | self.assertEqual(response.text, "11.7.00002.1")
57 |
58 | def test_build_response_from_async_response_not_found(self):
59 | response_content = b'HTTP/1.1 404 Not Found\r\nContent-Length: 105\r\nConnection: keep-alive\r\n' \
60 | b'Content-Encoding: gzip\r\nCache-Control: no-cache\r\n' \
61 | b'Content-Type: application/json; charset=utf-8\r\nOData-Version: 4.0\r\n' \
62 | b'\r\n\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x0b\x15\xc71\n\x800\x0c\x05\xd0\xab|\xb2t\x11' \
63 | b'\x07\x17\xc5Sx\x05M\xa3\x14\xdaD\xda:\x94\xe2\xdd\xc5\xb7\xbdN\x92\xb3eZ;\xb1y\xa1\x95' \
64 | b'\xa6y\xa1\x81\x92\x94\xb2_\xff\x9d\x7fRj\x0e\xbc+\xd4*\x0e\xc1i\x8fz\x04\x05[\x8c\xc25' \
65 | b'\x98\xc2N\xd4v\x0b\xdc\x96\x8d\xa5\x147\xd2\xfb~Od\xf8E^\x00\x00\x00'
66 | response = RestService.build_response_from_binary_response(response_content)
67 | self.assertEqual(response.status_code, 404)
68 | self.assertEqual(response.headers.get("Content-Length"), "105")
69 | self.assertEqual(response.headers.get("Connection"), "keep-alive")
70 | self.assertEqual(response.headers.get("Content-Encoding"), "gzip")
71 | self.assertEqual(response.headers.get("Cache-Control"), "no-cache")
72 | self.assertEqual(response.headers.get("Content-Type"), "application/json; charset=utf-8")
73 | self.assertEqual(response.headers.get("OData-Version"), "4.0")
74 | self.assertEqual(
75 | response.text,
76 | "{\"error\":{\"code\":\"278\",\"message\":\"\'dummy\' "
77 | "can not be found in collection of type \'Process\'.\"}}")
78 |
79 | @classmethod
80 | def tearDownClass(cls):
81 | cls.tm1.logout()
82 |
--------------------------------------------------------------------------------
/Tests/Sandbox_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from TM1py.Objects import Sandbox
4 |
5 |
6 | class TestSandboxMethods(unittest.TestCase):
7 |
8 | def test_body_include_in_sandbox_dimension_true(self):
9 | sandbox = Sandbox("sandbox", True)
10 |
11 | self.assertEqual(
12 | '{"Name": "sandbox", "IncludeInSandboxDimension": true}',
13 | sandbox.body)
14 |
15 | def test_body_include_in_sandbox_dimension_false(self):
16 | sandbox = Sandbox("sandbox", False)
17 |
18 | self.assertEqual(
19 | '{"Name": "sandbox", "IncludeInSandboxDimension": false}',
20 | sandbox.body)
21 |
22 | def test_from_json_include_in_sandbox_dimension_true(self):
23 | sandbox = Sandbox.from_dict({
24 | "Name": "sandbox",
25 | "IncludeInSandboxDimension": True,
26 | "IsLoaded": True,
27 | "IsActive": False,
28 | "IsQueued": False})
29 |
30 | self.assertEqual(sandbox.name, "sandbox")
31 | self.assertTrue(sandbox._include_in_sandbox_dimension)
32 |
33 | def test_from_json_include_in_sandbox_dimension_false(self):
34 | sandbox = Sandbox.from_dict({
35 | "Name": "sandbox",
36 | "IncludeInSandboxDimension": False,
37 | "IsLoaded": True,
38 | "IsActive": False,
39 | "IsQueued": False})
40 |
41 | self.assertEqual(sandbox.name, "sandbox")
42 | self.assertFalse(sandbox._include_in_sandbox_dimension)
43 |
44 | def test_change_name(self):
45 | sandbox = Sandbox("sandbox", True)
46 | sandbox.name = "new sandbox"
47 |
48 | self.assertEqual(
49 | '{"Name": "new sandbox", "IncludeInSandboxDimension": true}',
50 | sandbox.body)
51 |
52 | def test_change_include_in_sandbox_dimension_false(self):
53 | sandbox = Sandbox("sandbox", True)
54 | sandbox.include_in_sandbox_dimension = False
55 |
56 | self.assertEqual(
57 | '{"Name": "sandbox", "IncludeInSandboxDimension": false}',
58 | sandbox.body)
59 |
--------------------------------------------------------------------------------
/Tests/Subset_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from TM1py.Objects import AnonymousSubset, Subset
4 |
5 |
6 | class TestSubset(unittest.TestCase):
7 | prefix = "TM1py_Tests_Subset_"
8 |
9 | dimension_name = prefix + "dimension"
10 | hierarchy_name = prefix + "hierarchy"
11 | subset_name_static = prefix + "static_subset"
12 | subset_name_dynamic = prefix + "dynamic_subset"
13 | subset_name_minimal = prefix + "minimal_subset"
14 | subset_name_complete = prefix + "complete_subset"
15 | subset_name_alias = prefix + "alias_subset"
16 | subset_name_anon = prefix + "anon_subset"
17 | element_name = prefix + "element"
18 |
19 | @classmethod
20 | def setUpClass(cls):
21 | """
22 | Create any class scoped fixtures here.
23 | """
24 |
25 | cls.subset_dict = {
26 | "Name": "dict_subset",
27 | "UniqueName": f"[{cls.dimension_name}]",
28 | "Hierarchy": {
29 | "Name": f"{cls.hierarchy_name}"
30 | },
31 | "Alias": "dict_subset" + "_alias",
32 | "Elements": [
33 | {
34 | "Name": "x"
35 | },
36 | {
37 | "Name": "y"
38 | },
39 | {
40 | "Name": "z"
41 | }
42 | ],
43 | "Expression": ""
44 | }
45 |
46 | cls.subset_json = '''
47 | {
48 | "Name": "json_subset",
49 | "UniqueName" : "json_subset",
50 | "Hierarchy": {
51 | "Name": "json_subset"
52 | },
53 | "Alias": "json_subset_alias",
54 | "Elements" : [
55 | {
56 | "Name" : "xoy"
57 | },
58 | {
59 | "Name": "o"
60 | },
61 | {
62 | "Name": "xxx"
63 | }
64 | ],
65 | "Expression" : ""
66 | }
67 | '''
68 |
69 | def setUp(self):
70 | """
71 | Instantiate subsets that will be available to all tests.
72 | """
73 |
74 | self.static_subset = Subset(
75 | dimension_name=self.dimension_name,
76 | subset_name=self.subset_name_static,
77 | elements=['USD', 'EUR', 'NZD', 'Dum\'my'])
78 |
79 | self.dynamic_subset = Subset(
80 | dimension_name=self.dimension_name,
81 | subset_name=self.subset_name_dynamic,
82 | expression='{ HIERARCHIZE( {TM1SUBSETALL( [' + self.dimension_name + '] )} ) }')
83 |
84 | # subset constructed from only the mandatory arguments
85 | self.minimal_subset = Subset(
86 | dimension_name=self.dimension_name,
87 | subset_name=self.subset_name_minimal
88 | )
89 |
90 | # a static subset constructed with optional arguments
91 | self.complete_subset = Subset(
92 | dimension_name=self.dimension_name,
93 | subset_name=self.subset_name_complete,
94 | hierarchy_name=self.hierarchy_name,
95 | alias=self.subset_name_alias,
96 | elements=["a", "b", "c"]
97 | )
98 |
99 | # an instance of the AnonymoustSubset subclass
100 | self.anon_subset = AnonymousSubset(
101 | dimension_name=self.dimension_name,
102 | hierarchy_name=self.hierarchy_name,
103 | elements=["x", "y", "z"]
104 | )
105 |
106 | def tearDown(self):
107 | """
108 | Remove any artifacts created.
109 | """
110 | # nothing required here, all objects will be reset by setUp
111 | pass
112 |
113 | def test_is_dynamic(self):
114 | self.assertTrue(self.dynamic_subset.is_dynamic)
115 | self.assertFalse(self.static_subset.is_dynamic)
116 | self.assertFalse(self.minimal_subset.is_dynamic)
117 | self.assertFalse(self.complete_subset.is_dynamic)
118 | self.assertFalse(self.anon_subset.is_dynamic)
119 |
120 | def test_is_static(self):
121 | self.assertFalse(self.dynamic_subset.is_static)
122 | self.assertTrue(self.static_subset.is_static)
123 | self.assertTrue(self.minimal_subset.is_static)
124 | self.assertTrue(self.complete_subset.is_static)
125 | self.assertTrue(self.anon_subset.is_static)
126 |
127 | def test_from_json(self):
128 | s = Subset.from_json(self.subset_json)
129 | self.assertEqual(s.name, "json_subset")
130 | self.assertEqual(s.elements, ["xoy", "o", "xxx"])
131 |
132 | def test_from_dict(self):
133 | s = Subset.from_dict(self.subset_dict)
134 | self.assertEqual(s.name, "dict_subset")
135 | self.assertEqual(s.elements, ["x", "y", "z"])
136 |
137 | def test_add_elements(self):
138 | self.static_subset.add_elements(["AUD", "CHF"])
139 | self.assertIn("AUD", self.static_subset.elements)
140 | self.assertIn("CHF", self.static_subset.elements)
141 |
142 | def test_anonymous_subset(self):
143 | self.assertEqual(self.anon_subset.name, "")
144 |
145 | def test_property_setters(self):
146 | self.minimal_subset.elements = ["1", "2", "3"]
147 | self.assertEqual(self.minimal_subset.elements, ["1", "2", "3"])
148 |
149 | def test_property_getters(self):
150 | self.assertEqual(self.complete_subset.name, self.subset_name_complete)
151 | self.assertEqual(self.dynamic_subset.name, self.subset_name_dynamic)
152 | self.assertEqual(self.static_subset.dimension_name,
153 | self.dimension_name)
154 | self.assertEqual(self.minimal_subset.elements, [])
155 | self.assertIn("a", self.complete_subset.elements)
156 |
157 | def test_subset_equality(self):
158 | self.assertNotEqual(self.complete_subset, self.minimal_subset)
159 | self.assertNotEqual(self.complete_subset, self.anon_subset)
160 | self.assertNotEqual(self.complete_subset, self.static_subset)
161 | self.assertNotEqual(self.complete_subset, self.dynamic_subset)
162 | static_subset_copy = self.static_subset
163 | self.assertEqual(static_subset_copy, self.static_subset)
164 |
165 | @classmethod
166 | def tearDownClass(cls):
167 | """
168 | Tear down anything as required
169 | """
170 | # nothing to do here
171 | pass
172 |
173 |
174 | if __name__ == '__main__':
175 | unittest.main()
176 |
--------------------------------------------------------------------------------
/Tests/TM1Project_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from TM1py.Objects.GitProject import TM1Project, TM1ProjectTask, TM1ProjectDeployment
4 |
5 |
6 | class TestGitProject(unittest.TestCase):
7 |
8 | def test_add_task_process(self):
9 | project = TM1Project(name='TM1py Tests')
10 |
11 | project.add_task(
12 | TM1ProjectTask(
13 | 'TaskA',
14 | process='bedrock.server.savedataall',
15 | parameters=[
16 | {
17 | "param_name": "sPar1",
18 | "param_val": "1"
19 | }
20 | ],
21 | dependencies=[
22 | "Tasks('TaskB')"
23 | ]
24 | )
25 | )
26 |
27 | project.add_task(
28 | TM1ProjectTask(
29 | 'TaskB',
30 | chore="Chores('bedrock.server.savedataall')",
31 | dependencies=[
32 | "Tasks('TaskA')"
33 | ]
34 | )
35 | )
36 |
37 | expected_body = {
38 | "Version": 1.0,
39 | "Name": "TM1py Tests",
40 | "Tasks": {
41 | "TaskA": {
42 | "Process": "Processes('bedrock.server.savedataall')",
43 | "Parameters": [
44 | {
45 | "param_name": "sPar1",
46 | "param_val": "1"
47 | }
48 | ],
49 | "Dependencies": [
50 | "Tasks('TaskB')"
51 | ]
52 | },
53 | "TaskB": {
54 | "Chore": "Chores('bedrock.server.savedataall')",
55 | "Dependencies": [
56 | "Tasks('TaskA')"
57 | ]
58 | }
59 | }
60 | }
61 |
62 | self.assertEqual(expected_body, project.body_as_dict)
63 |
64 | def test_remove_task_process(self):
65 | project = TM1Project(name="TM1py Tests")
66 |
67 | project.add_task(TM1ProjectTask("TaskA", process="bedrock.server.savedataall"))
68 | project.add_task(TM1ProjectTask("TaskB", process="bedrock.server.wait"))
69 | project.remove_task(task_name="TaskB")
70 |
71 | expected_body = {
72 | "Version": 1.0,
73 | "Name": "TM1py Tests",
74 | "Tasks": {"TaskA": {"Process": "Processes('bedrock.server.savedataall')"}}}
75 |
76 | self.assertEqual(expected_body, project.body_as_dict)
77 |
78 | def test_add_ignore(self):
79 | project = TM1Project(name="TM1py Tests")
80 | project.add_ignore(object_class="Dimensions", object_name="Dim*")
81 |
82 | expected_body = {"Version": 1.0, "Name": "TM1py Tests", "Ignore": ["Dimensions('Dim*')"]}
83 | self.assertEqual(
84 | expected_body,
85 | project.body_as_dict)
86 |
87 | def test_remove_ignore(self):
88 | project = TM1Project(name="TM1py Tests")
89 | project.add_ignore(object_class="Dimensions", object_name="Dim*")
90 | project.add_ignore(object_class="Cubes", object_name="Cubetoberemoved")
91 | project.remove_ignore(ignore_entry="Cubes('Cubetoberemoved')")
92 |
93 | expected_body = {"Version": 1.0, "Name": "TM1py Tests", "Ignore": ["Dimensions('Dim*')"]}
94 | self.assertEqual(
95 | expected_body,
96 | project.body_as_dict)
97 |
98 | def test_add_ignore_exception(self):
99 | project = TM1Project(name="TM1py Tests")
100 | project.add_ignore(object_class="Dimensions", object_name="Dim*")
101 | project.add_ignore_exceptions(object_class="Dimensions", object_names=["DimA", "DimB"])
102 | project.add_ignore_exceptions(object_class="Dimensions", object_names=["DimC", "DimD"])
103 | project.remove_ignore(ignore_entry="!Dimensions('DimA')")
104 | project.remove_ignore(ignore_entry="!Dimensions('DimB')")
105 |
106 | expected_body = {
107 | "Version": 1.0,
108 | "Name": "TM1py Tests",
109 | "Ignore": [
110 | "Dimensions('Dim*')",
111 | "!Dimensions('DimC')",
112 | "!Dimensions('DimD')"]
113 | }
114 | self.assertEqual(
115 | expected_body,
116 | project.body_as_dict)
117 |
118 | def test_add_deployment(self):
119 | project = TM1Project(name="TM1py Tests")
120 | project.add_deployment(TM1ProjectDeployment(
121 | deployment_name="Dev",
122 | settings={"ServerName": "dev"},
123 | tasks={"TaskA": TM1ProjectTask(task_name="TaskA", process="bedrock.server.savedataall")},
124 | pre_push=["TaskA", "TaskB"]
125 | ))
126 |
127 | expected_body = {
128 | "Version": 1.0,
129 | "Name": "TM1py Tests",
130 | "Deployment": {
131 | "Dev": {
132 | "Settings": {"ServerName": "dev"},
133 | "Tasks": {"TaskA": {"Process": "Processes('bedrock.server.savedataall')"}},
134 | "PrePush": ["TaskA", "TaskB"]
135 | }
136 | }
137 | }
138 |
139 | self.assertEqual(
140 | expected_body,
141 | project.body_as_dict)
142 |
143 | def test_remove_deployment(self):
144 | project = TM1Project(name="TM1py Tests")
145 | project.add_deployment(TM1ProjectDeployment(
146 | deployment_name="Dev",
147 | settings={"ServerName": "dev"},
148 | tasks={"TaskA": TM1ProjectTask(task_name="TaskA", process="bedrock.server.savedataall")},
149 | pre_push=["TaskA", "TaskB"]
150 | ))
151 | project.add_deployment(TM1ProjectDeployment(
152 | deployment_name="Prod",
153 | settings={"ServerName": "prod"},
154 | tasks={"TaskA": TM1ProjectTask(task_name="TaskA", process="bedrock.server.savedataall")},
155 | pre_push=["TaskA", "TaskB"]
156 | ))
157 | project.remove_deployment("Dev")
158 |
159 | expected_body = {
160 | "Version": 1.0,
161 | "Name": "TM1py Tests",
162 | "Deployment": {
163 | "Prod": {
164 | "Settings": {"ServerName": "prod"},
165 | "Tasks": {"TaskA": {"Process": "Processes('bedrock.server.savedataall')"}},
166 | "PrePush": ["TaskA", "TaskB"]
167 | }
168 | }
169 | }
170 |
171 | self.assertEqual(
172 | expected_body,
173 | project.body_as_dict)
174 |
175 |
176 | if __name__ == '__main__':
177 | unittest.main()
178 |
--------------------------------------------------------------------------------
/Tests/Utils.py:
--------------------------------------------------------------------------------
1 | import functools
2 |
3 | from TM1py.Services.RestService import AuthenticationMode
4 | from TM1py.Utils.Utils import verify_version
5 |
6 |
7 | def skip_if_no_pandas(func):
8 | """
9 | Checks whether pandas is installed and skips the test if not
10 | """
11 |
12 | @functools.wraps(func)
13 | def wrapper(self, *args, **kwargs):
14 | try:
15 | import pandas
16 |
17 | return func(self, *args, **kwargs)
18 | except ImportError:
19 | return self.skipTest(f"Test '{func.__name__}' requires pandas")
20 |
21 | return wrapper
22 |
23 |
24 | def skip_if_version_lower_than(version):
25 | """
26 | Checks whether TM1 version is lower than a certain version and skips the test
27 | if this is the case. This function is useful if a test needs a minimum required version.
28 | """
29 |
30 | def wrap(func):
31 | @functools.wraps(func)
32 | def wrapper(self, *args, **kwargs):
33 | if not verify_version(required_version=version, version=self.tm1.version):
34 | return self.skipTest(
35 | f"Function '{func.__name__,}' requires TM1 server version >= '{version}'"
36 | )
37 | else:
38 | return func(self, *args, **kwargs)
39 |
40 | return wrapper
41 |
42 | return wrap
43 |
44 |
45 | def skip_if_version_higher_or_equal_than(version):
46 | """
47 | Checks whether TM1 version is higher or equal than a certain version and skips the test
48 | if this is the case. This function is useful if a test should not run for higher versions.
49 | """
50 |
51 | def wrap(func):
52 | @functools.wraps(func)
53 | def wrapper(self, *args, **kwargs):
54 | if verify_version(required_version=version, version=self.tm1.version):
55 | return self.skipTest(
56 | f"Function '{func.__name__,}' requires TM1 server version < '{version}'"
57 | )
58 | else:
59 | return func(self, *args, **kwargs)
60 |
61 | return wrapper
62 |
63 | return wrap
64 |
65 |
66 | def skip_if_auth_not_basic(version):
67 | """
68 | Checks whether TM1 authentication is basic (user+password). Skip test if authentication is not basic
69 | """
70 |
71 | def wrap(func):
72 | @functools.wraps(func)
73 | def wrapper(self, *args, **kwargs):
74 | if self.tm1.conn._auth_mode != AuthenticationMode.BASIC:
75 | return self.skipTest(
76 | f"Function '{func.__name__,}' requires IntegratedSecurityMode1 (Basic)"
77 | )
78 | else:
79 | return func(self, *args, **kwargs)
80 |
81 | return wrapper
82 |
83 | return wrap
84 |
85 |
86 | def skip_if_paoc(version):
87 | """
88 | Checks whether TM1 is deployed as Planning Analytics on Cloud (PAoC)
89 | """
90 |
91 | def wrap(func):
92 | @functools.wraps(func)
93 | def wrapper(self, *args, **kwargs):
94 | if "planning-analytics.ibmcloud.com/tm1/api" in self.tm1.conn._base_url:
95 | return self.skipTest(
96 | f"Function '{func.__name__,}' requires on prem TM1 instead of PAoC"
97 | )
98 | else:
99 | return func(self, *args, **kwargs)
100 |
101 | return wrapper
102 |
103 | return wrap
104 |
--------------------------------------------------------------------------------
/Tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cubewise-code/tm1py/a8818d4d23ea542270fd03f3040f6356e8855b3f/Tests/__init__.py
--------------------------------------------------------------------------------
/Tests/_readme.md:
--------------------------------------------------------------------------------
1 | # Setup TM1py Tests
2 |
3 | - `pip install "tm1py[dev]"`
4 |
5 | - Use a TM1 development environment
6 |
7 | - In `Tests` folder: Copy the file `config.ini.template` with the new name `config.ini`
8 |
9 | - Specify your instance coordinates in the config.ini file as `tm1srv01`
10 |
11 | - `EnableSandboxDimension` config parameter must be set to `F` for the target TM1 instance
12 |
13 | - `EnableTIDebugging` config parameter must be set to `T` for the target TM1 instance
14 |
15 | # Run TM1py Tests
16 |
17 | ## Run tests via commandline
18 |
19 | ### To run all tests
20 |
21 | On Windows:
22 |
23 | `pytest .\Tests\`
24 |
25 | On Linux and macOS:
26 |
27 | `pytest ./Tests/`
28 |
29 | ### To run a specific test file from the `Tests` folder
30 |
31 | On Windows:
32 |
33 | `pytest .\Tests\ChoreService_test.py`
34 |
35 | On Linux and macOS:
36 |
37 | `pytest ./Tests/ChoreService_test.py`
38 |
39 | ## Run tests via PyCharm
40 |
41 | ### To run all tests
42 |
43 | rightclick `Tests` folder -> run 'pytest in Tests'
44 |
45 | ### To run a specific test file from the `Tests` folder:
46 | rightclick file (e.g., `ChoreService_test.py`) -> run 'pytest in ChoreService_test.py'
47 |
--------------------------------------------------------------------------------
/Tests/config.ini.template:
--------------------------------------------------------------------------------
1 | [tm1srv01]
2 | address=
3 | api_key=
4 | iam_url=https://iam.cloud.ibm.com/identity/token
5 | tenant=
6 | database=US Database 2
7 |
8 | [tm1srv02]
9 | address=
10 | instance=
11 | database=
12 | application_client_id=
13 | application_client_secret=
14 | user=
15 | ssl=
16 |
17 | [tm1srv03]
18 | address=localhost
19 | port=12354
20 | ssl=True
21 | user=admin
22 | password=apple
23 | async_requests_mode=True
--------------------------------------------------------------------------------
/Tests/resources/Bedrock.Server.Wait.json:
--------------------------------------------------------------------------------
1 | {"Parameters": [{"Prompt": "Seconds", "Type": "String", "Value": "", "Name": "pWaitSec"}, {"Prompt": "", "Type": "Numeric", "Value": 0, "Name": "pDebug"}], "DataProcedure": "#****Begin: Generated Statements***\r\n#****End: Generated Statements****\r\n\r\n#****Begin: Generated Statements***\r\n#****End: Generated Statements****", "Variables": [], "HasSecurityAccess": false, "UIData": "CubeAction=1511\fDataAction=1503\fCubeLogChanges=0\f", "Name": "Bedrock.Server.Wait", "PrologProcedure": "\r\n#****Begin: Generated Statements***\r\n#****End: Generated Statements****\r\n\r\n\r\n#####################################################################################\r\n##~~Copyright bedrocktm1.org 2011 www.bedrocktm1.org/how-to-licence.php Ver 3.0.0~~##\r\n#####################################################################################\r\n\r\n\r\n### Constants ###\r\n\r\ncProcess = 'Bedrock.Server.Wait';\r\ncTimeStamp = TimSt( Now, '\\Y\\m\\d\\h\\i\\s' );\r\nsRandomInt = NumberToString( INT( RAND( ) * 1000 ));\r\ncDebugFile = GetProcessErrorFileDirectory | cProcess | '.' | cTimeStamp | '.' | sRandomInt ;\r\n\r\n### Initialise Debug ###\r\n\r\nIf( pDebug >= 1 );\r\n\r\n # Set debug file name\r\n sDebugFile = cDebugFile | 'Prolog.debug';\r\n\r\n # Log start time\r\n AsciiOutput( sDebugFile, 'Process Started: ' | TimSt( Now, '\\d-\\m-\\Y \\h:\\i:\\s' ) );\r\n AsciiOutput( sDebugFile, '' );\r\n # Log parameters\r\n AsciiOutput( sDebugFile, 'Parameters: pWaitSec: ' | pWaitSec );\r\n\r\nEndIf;\r\n\r\n### LOOP TIME ##\r\nnStartNow = NOW();\r\nnWaitTime = nStartNow + ( StringToNumber( pWaitSec ) / 86400 );\r\n\r\n\r\nnTime = NOW();\r\nWHILE( nTime <= nWaitTime );\r\n nTime = NOW();\r\nEND;\r\n\r\n", "MetadataProcedure": "#****Begin: Generated Statements***\r\n#****End: Generated Statements****\r\n\r\n#****Begin: Generated Statements***\r\n#****End: Generated Statements****", "DataSource": {"Type": "None"}, "EpilogProcedure": "\r\n#****Begin: Generated Statements***\r\n#****End: Generated Statements****\r\n\r\n#####################################################################################\r\n##~~Copyright bedrocktm1.org 2011 www.bedrocktm1.org/how-to-licence.php Ver 3.0.0~~##\r\n#####################################################################################\r\n\r\n\r\n### Initialise Debug ###\r\n\r\nIf( pDebug >= 1 );\r\n\r\n # Set debug file name\r\n sDebugFile = cDebugFile | 'Epilog.debug';\r\n\r\n # Log finish time\r\n AsciiOutput( sDebugFile, 'Process Finished: ' | TimSt( Now, '\\d-\\m-\\Y \\h:\\i:\\s' ) );\r\n\r\nEndIf;\r\n\r\n\r\n### End Prolog ###", "VariablesUIData": []}
--------------------------------------------------------------------------------
/Tests/resources/document.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cubewise-code/tm1py/a8818d4d23ea542270fd03f3040f6356e8855b3f/Tests/resources/document.xlsx
--------------------------------------------------------------------------------
/Tests/resources/file.csv:
--------------------------------------------------------------------------------
1 | d1,d2,value
2 | e6,e2,888
--------------------------------------------------------------------------------
/Tests/resources/generate_config.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 | tm1_connection = os.environ.get('TM1_CONNECTION')
5 | tm1_connection_secret = os.environ.get('TM1_CONNECTION_SECRET')
6 |
7 | config_content = '[tm1srv01]\n'
8 |
9 | if tm1_connection:
10 | conn_data = json.loads(tm1_connection)
11 | for key, value in conn_data.items():
12 | config_content += f"{key}={value}\n"
13 |
14 | if tm1_connection_secret:
15 | secret_data = json.loads(tm1_connection_secret)
16 | for key, value in secret_data.items():
17 | config_content += f"{key}={value}\n"
18 |
19 | with open('Tests/config.ini', 'w') as f:
20 | f.write(config_content)
--------------------------------------------------------------------------------
/Tests/resources/raw_cellset.json:
--------------------------------------------------------------------------------
1 | {
2 | "@odata.context": "Irrelevant",
3 | "ID": "5l1vFa8FAIB7AAAg",
4 | "Cube": {
5 | "Name": "BikeShares",
6 | "Dimensions": [{
7 | "Name": "Version"
8 | },
9 | {
10 | "Name": "Date"
11 | },
12 | {
13 | "Name": "City"
14 | },
15 | {
16 | "Name": "BikeSharesMeasure"
17 | }]
18 | },
19 | "Axes": [{
20 | "Ordinal": 0,
21 | "Cardinality": 2,
22 | "Tuples": [{
23 | "Ordinal": 0,
24 | "Members": [{
25 | "UniqueName": "[City].[City].[NYC]",
26 | "Element": {
27 | "UniqueName": "[City].[City].[NYC]"
28 | }
29 | }]
30 | },
31 | {
32 | "Ordinal": 1,
33 | "Members": [{
34 | "UniqueName": "[City].[City].[Chicago]",
35 | "Element": {
36 | "UniqueName": "[City].[City].[Chicago]"
37 | }
38 | }]
39 | }]
40 | },
41 | {
42 | "Ordinal": 1,
43 | "Cardinality": 2,
44 | "Tuples": [{
45 | "Ordinal": 0,
46 | "Members": [{
47 | "UniqueName": "[Date].[Date].[2017-11-26]",
48 | "Element": {
49 | "UniqueName": "[Date].[Date].[2017-11-26]"
50 | }
51 | }]
52 | },
53 | {
54 | "Ordinal": 1,
55 | "Members": [{
56 | "UniqueName": "[Date].[Date].[2017-11-27]",
57 | "Element": {
58 | "UniqueName": "[Date].[Date].[2017-11-27]"
59 | }
60 | }]
61 | }]
62 | },
63 | {
64 | "Ordinal": 2,
65 | "Cardinality": 1,
66 | "Tuples": [{
67 | "Ordinal": 0,
68 | "Members": [{
69 | "UniqueName": "[Version].[Version].[Actual]",
70 | "Element": {
71 | "UniqueName": "[Version].[Version].[Actual]"
72 | }
73 | },
74 | {
75 | "UniqueName": "[BikeSharesMeasure].[BikeSharesMeasure].[Count]",
76 | "Element": {
77 | "UniqueName": "[BikeSharesMeasure].[BikeSharesMeasure].[Count]"
78 | }
79 | }]
80 | }]
81 | }],
82 | "Cells": [{
83 | "Value": 27181
84 | },
85 | {
86 | "Value": 3606
87 | },
88 | {
89 | "Value": 46733
90 | },
91 | {
92 | "Value": 9146
93 | }]
94 | }
--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
1 | .. _api:
2 |
3 | Developer Interface
4 | ===================
5 |
6 | .. module:: schedule
7 |
8 | This part of the documentation covers all the classes of TM1py.
9 |
10 | TM1 Services
11 | -------
12 |
13 | .. autoclass:: TM1py.AnnotationService
14 | :members:
15 | :undoc-members:
16 |
17 | .. autoclass:: TM1py.ApplicationService
18 | :members:
19 | :undoc-members:
20 |
21 | .. autoclass:: TM1py.CellService
22 | :members:
23 | :undoc-members:
24 |
25 | .. autoclass:: TM1py.ChoreService
26 | :members:
27 | :undoc-members:
28 |
29 | .. autoclass:: TM1py.CubeService
30 | :members:
31 | :undoc-members:
32 |
33 | .. autoclass:: TM1py.DimensionService
34 | :members:
35 | :undoc-members:
36 |
37 | .. autoclass:: TM1py.ElementService
38 | :members:
39 | :undoc-members:
40 |
41 | .. autoclass:: TM1py.FileService
42 | :members:
43 | :undoc-members:
44 |
45 | .. autoclass:: TM1py.GitService
46 | :members:
47 | :undoc-members:
48 |
49 | .. autoclass:: TM1py.HierarchyService
50 | :members:
51 | :undoc-members:
52 |
53 | .. autoclass:: TM1py.MonitoringService
54 | :members:
55 | :undoc-members:
56 |
57 | .. autoclass:: TM1py.PowerBiService
58 | :members:
59 | :undoc-members:
60 |
61 | .. autoclass:: TM1py.ProcessService
62 | :members:
63 | :undoc-members:
64 |
65 | .. autoclass:: TM1py.RestService
66 | :members:
67 | :undoc-members:
68 |
69 | .. autoclass:: TM1py.SandboxService
70 | :members:
71 | :undoc-members:
72 |
73 | .. autoclass:: TM1py.SecurityService
74 | :members:
75 | :undoc-members:
76 |
77 | .. autoclass:: TM1py.ServerService
78 | :members:
79 | :undoc-members:
80 |
81 | .. autoclass:: TM1py.SubsetService
82 | :members:
83 | :undoc-members:
84 |
85 | .. autoclass:: TM1py.TM1Service
86 | :members:
87 | :undoc-members:
88 |
89 | .. autoclass:: TM1py.ViewService
90 | :members:
91 | :undoc-members:
92 |
93 | TM1 Objects
94 | -------
95 |
96 | .. autoclass:: TM1py.Annotation
97 | :members:
98 | :undoc-members:
99 |
100 |
101 | .. autoclass:: TM1py.Application
102 | :members:
103 | :undoc-members:
104 |
105 | .. autoclass:: TM1py.Chore
106 | :members:
107 | :undoc-members:
108 |
109 | .. autoclass:: TM1py.ChoreFrequency
110 | :members:
111 | :undoc-members:
112 |
113 | .. autoclass:: TM1py.ChoreStartTime
114 | :members:
115 | :undoc-members:
116 |
117 | .. autoclass:: TM1py.ChoreTask
118 | :members:
119 | :undoc-members:
120 |
121 | .. autoclass:: TM1py.Cube
122 | :members:
123 | :undoc-members:
124 |
125 | .. autoclass:: TM1py.Dimension
126 | :members:
127 | :undoc-members:
128 |
129 | .. autoclass:: TM1py.Element
130 | :members:
131 | :undoc-members:
132 |
133 | .. autoclass:: TM1py.ElementAttribute
134 | :members:
135 | :undoc-members:
136 |
137 | .. autoclass:: TM1py.Git
138 | :members:
139 | :undoc-members:
140 |
141 | .. autoclass:: TM1py.GitCommit
142 | :members:
143 | :undoc-members:
144 |
145 | .. autoclass:: TM1py.GitPlan
146 | :members:
147 | :undoc-members:
148 |
149 | .. autoclass:: TM1py.GitRemote
150 | :members:
151 | :undoc-members:
152 |
153 | .. autoclass:: TM1py.Hierarchy
154 | :members:
155 | :undoc-members:
156 |
157 | .. autoclass:: TM1py.MDXView
158 | :members:
159 | :undoc-members:
160 |
161 | .. autoclass:: TM1py.NativeView
162 | :members:
163 | :undoc-members:
164 |
165 | .. autoclass:: TM1py.Process
166 | :members:
167 | :undoc-members:
168 |
169 | .. autoclass:: TM1py.Rules
170 | :members:
171 | :undoc-members:
172 |
173 | .. autoclass:: TM1py.Sandbox
174 | :members:
175 | :undoc-members:
176 |
177 | .. autoclass:: TM1py.Server
178 | :members:
179 | :undoc-members:
180 |
181 | .. autoclass:: TM1py.Subset
182 | :members:
183 | :undoc-members:
184 |
185 | .. autoclass:: TM1py.User
186 | :members:
187 | :undoc-members:
188 |
189 | .. autoclass:: TM1py.View
190 | :members:
191 | :undoc-members:
192 |
193 | .. autoclass:: TM1py.ViewAxisSelection
194 | :members:
195 | :undoc-members:
196 |
197 | .. autoclass:: TM1py.ViewTitleSelection
198 | :members:
199 | :undoc-members:
200 |
201 | Exceptions
202 | ----------
203 |
204 | .. autoclass:: TM1py.Exceptions.Exceptions.TM1pyTimeout
205 | :members:
206 | :undoc-members:
207 |
208 | .. autoclass:: TM1py.Exceptions.Exceptions.TM1pyVersionException
209 | :members:
210 | :undoc-members:
211 |
212 | .. autoclass:: TM1py.Exceptions.Exceptions.TM1pyNotAdminException
213 | :members:
214 | :undoc-members:
215 |
216 | .. autoclass:: TM1py.Exceptions.Exceptions.TM1pyRestException
217 | :members:
218 | :undoc-members:
219 |
220 | .. autoclass:: TM1py.Exceptions.Exceptions.TM1pyException
221 | :members:
222 | :undoc-members:
223 |
224 | .. autoclass:: TM1py.Exceptions.Exceptions.TM1pyWriteFailureException
225 | :members:
226 | :undoc-members:
227 |
228 | .. autoclass:: TM1py.Exceptions.Exceptions.TM1pyWritePartialFailureException
229 | :members:
230 | :undoc-members:
231 |
232 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 |
2 |
3 | .. image:: https://s3-ap-southeast-2.amazonaws.com/downloads.cubewise.com/web_assets/CubewiseLogos/TM1py-logo.png
4 |
5 | By wrapping the IBM Planning Analytics (TM1) REST API in a concise Python framework, TM1py facilitates Python developments for TM1.
6 |
7 | Interacting with TM1 programmatically has never been easier.
8 |
9 |
10 | .. code-block:: python
11 |
12 | with TM1Service(address='localhost', port=8001, user='admin', password='apple', ssl=True) as tm1:
13 | subset = Subset(dimension_name='Month', subset_name='Q1', elements=['Jan', 'Feb', 'Mar'])
14 | tm1.subsets.create(subset, private=True)
15 |
16 | Features
17 | =======================
18 |
19 | TM1py offers handy features to interact with TM1 from Python, such as
20 |
21 | - Read data from cubes through cube views and MDX Queries
22 | - Write data into cubes
23 | - Execute processes and chores
24 | - Execute loose statements of TI
25 | - CRUD features for TM1 objects (cubes, dimensions, subsets, etc.)
26 | - Query and kill threads
27 | - Query MessageLog, TransactionLog and AuditLog
28 | - Generate MDX Queries from existing cube views
29 |
30 | Requirements
31 | =======================
32 |
33 | - python (3.7 or higher)
34 | - requests
35 | - requests_negotiate_sspi
36 | - TM1 11
37 |
38 |
39 | Optional Requirements
40 | =======================
41 |
42 | - pandas
43 |
44 | Install
45 | =======================
46 |
47 | without pandas
48 |
49 | .. code-block:: python
50 |
51 | pip install tm1py
52 |
53 | with pandas
54 |
55 | .. code-block:: python
56 |
57 | pip install "tm1py[pandas]"
58 |
59 |
60 | Usage
61 | =======================
62 |
63 | on-premise
64 |
65 | .. code-block:: python
66 |
67 | from TM1py.Services import TM1Service
68 |
69 | with TM1Service(address='localhost', port=8001, user='admin', password='apple', ssl=True) as tm1:
70 | for chore in tm1.chores.get_all():
71 | chore.reschedule(hours=-1)
72 | tm1.chores.update(chore)
73 |
74 | IBM cloud
75 |
76 | .. code-block:: python
77 |
78 | with TM1Service(
79 | base_url='https://mycompany.planning-analytics.ibmcloud.com/tm1/api/tm1/',
80 | user="non_interactive_user",
81 | namespace="LDAP",
82 | password="U3lSn5QLwoQZY2",
83 | ssl=True,
84 | verify=True,
85 | async_requests_mode=True) as tm1:
86 | for chore in tm1.chores.get_all():
87 | chore.reschedule(hours=-1)
88 | tm1.chores.update(chore)
89 |
90 |
91 |
92 | API Documentation
93 | -----------------
94 |
95 | If you are looking for information on a specific function, class, or method,
96 | this part of the documentation is for you.
97 |
98 | .. toctree::
99 | :maxdepth: 2
100 |
101 | api
102 |
103 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | TM1py>=1.10.1
2 | pandas>=1.2.2
3 | mdxpy>=1.3
4 | python-dateutil>=2.8.1
5 | requests>=2.25.1
6 | urllib3>=1.26.3
7 | pytz>=2021.1
8 | setuptools>=49.2.1
9 | networkx
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools", "wheel"]
3 | build-backend = "setuptools.build_meta"
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | SCHEDULE_VERSION = '2.1'
4 | SCHEDULE_DOWNLOAD_URL = (
5 | 'https://github.com/Cubewise-code/TM1py/tarball/' + SCHEDULE_VERSION
6 | )
7 |
8 | with open('README.md', 'r') as f:
9 | long_description = f.read()
10 |
11 | setup(
12 | name='TM1py',
13 | packages=['TM1py', 'TM1py/Exceptions', 'TM1py/Objects', 'TM1py/Services', 'TM1py/Utils'],
14 | version=SCHEDULE_VERSION,
15 | description='A python module for TM1.',
16 | long_description=long_description,
17 | long_description_content_type='text/markdown',
18 | license='MIT',
19 | author='Marius Wirtz',
20 | author_email='MWirtz@cubewise.com',
21 | url='https://github.com/cubewise-code/tm1py',
22 | download_url=SCHEDULE_DOWNLOAD_URL,
23 | keywords=[
24 | 'TM1', 'IBM Cognos TM1', 'Planning Analytics', 'PA', 'Cognos'
25 | ],
26 | classifiers=[
27 | 'Intended Audience :: Developers',
28 | 'License :: OSI Approved :: MIT License',
29 | 'Programming Language :: Python',
30 | 'Programming Language :: Python :: 3.6',
31 | 'Programming Language :: Python :: 3.7',
32 | 'Programming Language :: Python :: 3.8',
33 | 'Programming Language :: Python :: 3.9',
34 | 'Programming Language :: Python :: 3.10',
35 | 'Programming Language :: Python :: 3.11',
36 | 'Natural Language :: English',
37 | ],
38 | install_requires=[
39 | 'ijson',
40 | 'requests',
41 | 'pytz',
42 | 'requests_negotiate_sspi;platform_system=="Windows"',
43 | 'mdxpy>=1.3.1',
44 | 'networkx'],
45 | extras_require={
46 | "pandas": ["pandas"],
47 | "dev": [
48 | "pytest",
49 | "pytest-xdist",
50 | "python-dateutil"
51 | ]
52 | },
53 | python_requires='>=3.6',
54 | )
55 |
--------------------------------------------------------------------------------