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