From fc20e287feb4cc1b7bb983c44e25a8ae936580f0 Mon Sep 17 00:00:00 2001 From: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> Date: Tue, 29 Nov 2022 14:40:35 +0200 Subject: [PATCH] feat: jans cli to jans-cli-tui (#3063) * refactor: remove jans-cli * fix: jans-cli no need to get properties each time * feat: jans-linux-setup tui installer * fix: jans-linux-setup clean old cli user data * fix: jans-linux-setup missing cli libraries * fix: jans-cli code clean OrderedDict * fix: jans-cli runtime plugins * fix: jans-cli don't decode if access_token_enc is empty * fix: jans-cli edit property (ref: #3058) * fix: jans-cli screen hangs on scim save (ref: #3061) * fix: jans-cli multiple device auth message on save creds (ref: #3059) * fix:jans-cli remove unnecessary logs * fix:jans-cli reviced all array type in properties (ref: #3062) * fix:jans-cli fix move backword in auth server column (ref: #\3073) * fix: jans-cli delete if jca_client_secret manually added to config.ini * fix: jans-cli typo * fix: jans-cli config-api move to asyncio * fix: jans-cli save admin-ui roles * feat: jans-cli display progressing text on status bar * feat: jans-cli progressing texts for user plugin * fix: jans-cli decode encrypted data if salt changed * fix: jans-cli check existence of data when logout (ref: #3050) * fix:jans-cli getting window too small error in edit user (ref: #3041) * fix:jans-cli remove un-used scrollbar (ref: #3070 #3041) * feat: jans-cli add User-inum to request headers * fix: jans-cli check existence of inum in user info * fix: jans-cli DocString (ref: #2572) * fix: jans-cli DocString (ref: #2572) * fix: jans-cli DocString and removing unused imports (ref: #2572) * fix:jans-cli Ask on delete of scripts conf. properties (ref: #3105) * fix: jans-cli Error on deleting Claims in scopes(ref: #3106) * fix: jans-cli Error on deleting without verification of deletion (ref: #3107) Co-authored-by: Mustafa Baser Co-authored-by: AbdelwahabAdam --- .github/CODEOWNERS | 2 +- .github/workflows/build-packages.yml | 52 +- .../workflows/central_code_quality_check.yml | 20 +- .github/workflows/release.yaml | 2 +- automation/github-labels/labels-schema.json | 6 +- .../scripts/utils.py | 2 +- {jans-cli => jans-cli-tui}/CHANGELOG.md | 0 jans-cli-tui/cli_tui/cli/config_cli.py | 44 +- jans-cli-tui/cli_tui/jans_cli_tui.py | 68 +- .../plugins/010_oxauth/edit_client_dialog.py | 61 +- .../plugins/010_oxauth/edit_scope_dialog.py | 40 +- .../cli_tui/plugins/010_oxauth/main.py | 300 +- .../plugins/010_oxauth/view_property.py | 231 +- .../plugins/010_oxauth/view_uma_dialog.py | 52 +- jans-cli-tui/cli_tui/plugins/020_fido/main.py | 48 +- jans-cli-tui/cli_tui/plugins/030_scim/main.py | 20 +- .../cli_tui/plugins/040_config_api/main.py | 425 +-- .../plugins/060_scripts/edit_script_dialog.py | 96 +- .../cli_tui/plugins/060_scripts/main.py | 52 +- .../plugins/070_users/edit_user_dialog.py | 70 +- .../cli_tui/plugins/070_users/main.py | 43 +- jans-cli-tui/cli_tui/plugins/999_jans/main.py | 25 +- jans-cli-tui/cli_tui/version.py | 2 +- jans-cli-tui/setup.py | 2 +- jans-cli/.gitignore | 134 - jans-cli/LICENSE | 201 -- jans-cli/Makefile | 13 - jans-cli/README.md | 173 -- jans-cli/cli/__init__.py | 0 jans-cli/cli/config_cli.py | 2401 ----------------- jans-cli/cli/pylib/__init__.py | 0 jans-cli/cli/pylib/tabulate/LICENSE | 20 - jans-cli/cli/pylib/tabulate/README | 1 - jans-cli/cli/pylib/tabulate/README.md | 747 ----- jans-cli/cli/pylib/tabulate/__init__.py | 0 jans-cli/cli/pylib/tabulate/tabulate.py | 1792 ------------ jans-cli/cli/scim_cli.py | 1 - jans-cli/cli/version.py | 6 - jans-cli/setup.py | 84 - jans-linux-setup/jans_setup/app_info.json | 6 +- jans-linux-setup/jans_setup/jans_setup.py | 8 +- .../setup_app/data/package_list.json | 20 +- .../setup_app/installers/jans_cli.py | 45 +- 43 files changed, 762 insertions(+), 6553 deletions(-) rename {jans-cli => jans-cli-tui}/CHANGELOG.md (100%) delete mode 100644 jans-cli/.gitignore delete mode 100644 jans-cli/LICENSE delete mode 100644 jans-cli/Makefile delete mode 100644 jans-cli/README.md delete mode 100644 jans-cli/cli/__init__.py delete mode 100755 jans-cli/cli/config_cli.py delete mode 100644 jans-cli/cli/pylib/__init__.py delete mode 100644 jans-cli/cli/pylib/tabulate/LICENSE delete mode 100644 jans-cli/cli/pylib/tabulate/README delete mode 100644 jans-cli/cli/pylib/tabulate/README.md delete mode 100644 jans-cli/cli/pylib/tabulate/__init__.py delete mode 100644 jans-cli/cli/pylib/tabulate/tabulate.py delete mode 100644 jans-cli/cli/scim_cli.py delete mode 100644 jans-cli/cli/version.py delete mode 100644 jans-cli/setup.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 59bf47437fe..b6fb98a0c05 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -21,7 +21,7 @@ /jans-fido2/ @yurem /jans-scim/ @jgomer2001 /jans-config-api/ @pujavs @yuriyz -/jans-cli/ @devrimyatar +/jans-cli-tui/ @devrimyatar /jans-linux-setup/ @devrimyatar @smansoft @yuriyz @yurem /jans-linux-setup/jans_setup/setup_app/version.py @moabu /jans-linux-setup/static/scripts/admin_ui_plugin.py @devrimyatar @duttarnab diff --git a/.github/workflows/build-packages.yml b/.github/workflows/build-packages.yml index 9affed0a13d..3069b0f1b33 100644 --- a/.github/workflows/build-packages.yml +++ b/.github/workflows/build-packages.yml @@ -77,7 +77,7 @@ jobs: cp -r /opt/dist jans-src/opt/ cp -r /opt/jans jans-src/opt/ touch jans-src/opt/jans/jans-setup/package - rm -rf install.py install jans-cli + rm -rf install.py install jans-cli-tui rm -rf jans-src/opt/jans/jans-setup/logs/setup.log rm -rf jans-src/opt/jans/jans-setup/logs/setup_error.log sed -i "s/%VERSION%/${{ steps.previoustag.outputs.version }}/g" run-build.sh @@ -134,10 +134,10 @@ jobs: make zipapp mv jans-linux-setup.pyz jans-linux-suse-X86-64-setup.pyz sha256sum jans-linux-suse-X86-64-setup.pyz > jans-linux-suse-X86-64-setup.pyz.sha256sum - cd ../jans-cli + cd ../jans-cli-tui make zipapp - mv config-cli.pyz jans-cli-linux-suse-X86-64.pyz - sha256sum jans-cli-linux-suse-X86-64.pyz > jans-cli-linux-suse-X86-64.pyz.sha256sum + mv config-cli.pyz jans-cli-tui-linux-suse-X86-64.pyz + sha256sum jans-cli-tui-linux-suse-X86-64.pyz > jans-cli-tui-linux-suse-X86-64.pyz.sha256sum # To be removed once we get SUSE build working - uses: addnab/docker-run-action@v3 name: Build with CentOS7 @@ -163,10 +163,10 @@ jobs: make zipapp mv jans-linux-setup.pyz jans-linux-X86-64-setup.pyz sha256sum jans-linux-X86-64-setup.pyz > jans-linux-X86-64-setup.pyz.sha256sum - cd ../jans-cli + cd ../jans-cli-tui make zipapp - mv config-cli.pyz jans-cli-linux-X86-64.pyz - sha256sum jans-cli-linux-X86-64.pyz > jans-cli-linux-X86-64.pyz.sha256sum + mv config-cli.pyz jans-cli-tui-linux-X86-64.pyz + sha256sum jans-cli-tui-linux-X86-64.pyz > jans-cli-tui-linux-X86-64.pyz.sha256sum - name: Set up Python 3.6 uses: actions/setup-python@v4 with: @@ -183,10 +183,10 @@ jobs: make zipapp mv jans-linux-setup.pyz jans-linux-ubuntu-X86-64-setup.pyz sha256sum jans-linux-ubuntu-X86-64-setup.pyz > jans-linux-ubuntu-X86-64-setup.pyz.sha256sum - cd ../jans-cli + cd ../jans-cli-tui make zipapp - mv config-cli.pyz jans-cli-linux-ubuntu-X86-64.pyz - sha256sum jans-cli-linux-ubuntu-X86-64.pyz > jans-cli-linux-ubuntu-X86-64.pyz.sha256sum + mv config-cli.pyz jans-cli-tui-linux-ubuntu-X86-64.pyz + sha256sum jans-cli-tui-linux-ubuntu-X86-64.pyz > jans-cli-tui-linux-ubuntu-X86-64.pyz.sha256sum - uses: actions/cache@v3 id: cache-installers with: @@ -197,12 +197,12 @@ jobs: ${{github.workspace}}/jans-linux-setup/jans-linux-suse-X86-64-setup.pyz.sha256sum ${{github.workspace}}/jans-linux-setup/jans-linux-ubuntu-X86-64-setup.pyz ${{github.workspace}}/jans-linux-setup/jans-linux-ubuntu-X86-64-setup.pyz.sha256sum - ${{github.workspace}}/jans-cli/jans-cli-linux-X86-64.pyz - ${{github.workspace}}/jans-cli/jans-cli-linux-X86-64.pyz.sha256sum - ${{github.workspace}}/jans-cli/jans-cli-linux-suse-X86-64.pyz - ${{github.workspace}}/jans-cli/jans-cli-linux-suse-X86-64.pyz.sha256sum - ${{github.workspace}}/jans-cli/jans-cli-linux-ubuntu-X86-64.pyz - ${{github.workspace}}/jans-cli/jans-cli-linux-ubuntu-X86-64.pyz.sha256sum + ${{github.workspace}}/jans-cli-tui/jans-cli-tui-linux-X86-64.pyz + ${{github.workspace}}/jans-cli-tui/jans-cli-tui-linux-X86-64.pyz.sha256sum + ${{github.workspace}}/jans-cli-tui/jans-cli-tui-linux-suse-X86-64.pyz + ${{github.workspace}}/jans-cli-tui/jans-cli-tui-linux-suse-X86-64.pyz.sha256sum + ${{github.workspace}}/jans-cli-tui/jans-cli-tui-linux-ubuntu-X86-64.pyz + ${{github.workspace}}/jans-cli-tui/jans-cli-tui-linux-ubuntu-X86-64.pyz.sha256sum key: ${{ github.sha }} upload_python_packages: @@ -224,12 +224,12 @@ jobs: ${{github.workspace}}/jans-linux-setup/jans-linux-suse-X86-64-setup.pyz.sha256sum ${{github.workspace}}/jans-linux-setup/jans-linux-ubuntu-X86-64-setup.pyz ${{github.workspace}}/jans-linux-setup/jans-linux-ubuntu-X86-64-setup.pyz.sha256sum - ${{github.workspace}}/jans-cli/jans-cli-linux-X86-64.pyz - ${{github.workspace}}/jans-cli/jans-cli-linux-X86-64.pyz.sha256sum - ${{github.workspace}}/jans-cli/jans-cli-linux-suse-X86-64.pyz - ${{github.workspace}}/jans-cli/jans-cli-linux-suse-X86-64.pyz.sha256sum - ${{github.workspace}}/jans-cli/jans-cli-linux-ubuntu-X86-64.pyz - ${{github.workspace}}/jans-cli/jans-cli-linux-ubuntu-X86-64.pyz.sha256sum + ${{github.workspace}}/jans-cli-tui/jans-cli-tui-linux-X86-64.pyz + ${{github.workspace}}/jans-cli-tui/jans-cli-tui-linux-X86-64.pyz.sha256sum + ${{github.workspace}}/jans-cli-tui/jans-cli-tui-linux-suse-X86-64.pyz + ${{github.workspace}}/jans-cli-tui/jans-cli-tui-linux-suse-X86-64.pyz.sha256sum + ${{github.workspace}}/jans-cli-tui/jans-cli-tui-linux-ubuntu-X86-64.pyz + ${{github.workspace}}/jans-cli-tui/jans-cli-tui-linux-ubuntu-X86-64.pyz.sha256sum key: ${{ github.sha }} - name: Get latest tag id: previoustag @@ -262,8 +262,8 @@ jobs: uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.MOAUTO_WORKFLOW_TOKEN }} - file: ${{github.workspace}}/jans-cli/jans-cli-linux-${{ matrix.name }}-X86-64.pyz - asset_name: jans-cli-linux-${{ matrix.name }}-X86-64.pyz + file: ${{github.workspace}}/jans-cli-tui/jans-cli-tui-linux-${{ matrix.name }}-X86-64.pyz + asset_name: jans-cli-tui-linux-${{ matrix.name }}-X86-64.pyz tag: ${{ steps.previoustag.outputs.tag }} - name: Upload checksum to release id: upload_shas_cli @@ -271,6 +271,6 @@ jobs: uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.MOAUTO_WORKFLOW_TOKEN }} - file: ${{github.workspace}}/jans-cli/jans-cli-linux-${{ matrix.name }}-X86-64.pyz.sha256sum - asset_name: jans-cli-linux-${{ matrix.name }}-X86-64.pyz.sha256sum + file: ${{github.workspace}}/jans-cli-tui/jans-cli-tui-linux-${{ matrix.name }}-X86-64.pyz.sha256sum + asset_name: jans-cli-tui-linux-${{ matrix.name }}-X86-64.pyz.sha256sum tag: ${{ steps.previoustag.outputs.tag }} diff --git a/.github/workflows/central_code_quality_check.yml b/.github/workflows/central_code_quality_check.yml index c2f13cac27a..8f0778ae02b 100644 --- a/.github/workflows/central_code_quality_check.yml +++ b/.github/workflows/central_code_quality_check.yml @@ -20,7 +20,7 @@ on: - 'jans-eleven/**' - 'agama/**' - 'jans-linux-setup/**' - - 'jans-cli/**' + - 'jans-cli-tui/**' - 'jans-pycloudlib/**' - '!**/CHANGELOG.md' - '!**.txt' @@ -42,7 +42,7 @@ on: - 'jans-eleven/**' - 'agama/**' - 'jans-linux-setup/**' - - 'jans-cli/**' + - 'jans-cli-tui/**' - 'jans-pycloudlib/**' - '!**/CHANGELOG.md' - '!**.txt' @@ -56,7 +56,7 @@ jobs: strategy: fail-fast: false matrix: - module: [jans-auth-server, agama, jans-config-api, jans-core, jans-linux-setup, jans-cli, jans-fido2, jans-notify, jans-orm, jans-scim, jans-eleven, jans-pycloudlib] + module: [jans-auth-server, agama, jans-config-api, jans-core, jans-linux-setup, jans-cli-tui, jans-fido2, jans-notify, jans-orm, jans-scim, jans-eleven, jans-pycloudlib] env: JVM_PROJECTS: | jans-auth-server @@ -70,7 +70,7 @@ jobs: agama NON_JVM_PROJECTS: | jans-linux-setup - jans-cli + jans-cli-tui jans-pycloudlib steps: @@ -112,16 +112,16 @@ jobs: echo GH sha: $GITHUB_SHA - name: Set up JDK 11 - # JanssenProject/jans-cli is too similar to JanssenProject/jans-client-api as the contains function is returning it belonging to the JVM_PROJECT - if: contains(env.CHANGED_DIR, matrix.module) && contains(env.JVM_PROJECTS, matrix.module) && matrix.module != 'jans-cli' + # JanssenProject/jans-cli-tui is too similar to JanssenProject/jans-client-api as the contains function is returning it belonging to the JVM_PROJECT + if: contains(env.CHANGED_DIR, matrix.module) && contains(env.JVM_PROJECTS, matrix.module) && matrix.module != 'jans-cli-tui' uses: actions/setup-java@v3 with: java-version: '11' distribution: 'adopt' - name: Cache SonarCloud packages for JVM based project - # JanssenProject/jans-cli is too similar to JanssenProject/jans-client-api as the contains function is returning it belonging to the JVM_PROJECT - if: contains(env.CHANGED_DIR, matrix.module) && contains(env.JVM_PROJECTS, matrix.module) && matrix.module != 'jans-cli' + # JanssenProject/jans-cli-tui is too similar to JanssenProject/jans-client-api as the contains function is returning it belonging to the JVM_PROJECT + if: contains(env.CHANGED_DIR, matrix.module) && contains(env.JVM_PROJECTS, matrix.module) && matrix.module != 'jans-cli-tui' uses: actions/cache@v3 with: path: ~/.sonar/cache @@ -129,8 +129,8 @@ jobs: restore-keys: ${{ runner.os }}-sonar - name: Build and analyze JVM based project - # JanssenProject/jans-cli is too similar to JanssenProject/jans-client-api as the contains function is returning it belonging to the JVM_PROJECT - if: contains(env.CHANGED_DIR, matrix.module) && contains(env.JVM_PROJECTS, matrix.module) && matrix.module != 'jans-cli' + # JanssenProject/jans-cli-tui is too similar to JanssenProject/jans-client-api as the contains function is returning it belonging to the JVM_PROJECT + if: contains(env.CHANGED_DIR, matrix.module) && contains(env.JVM_PROJECTS, matrix.module) && matrix.module != 'jans-cli-tui' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ee49e5de573..3a365461246 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -143,7 +143,7 @@ jobs: #max-parallel: 1 fail-fast: false matrix: - python-projects: ["jans-pycloudlib", "jans-cli", "jans-linux-setup"] + python-projects: ["jans-pycloudlib", "jans-cli-tui", "jans-linux-setup"] steps: - name: Checkout uses: actions/checkout@v3 diff --git a/automation/github-labels/labels-schema.json b/automation/github-labels/labels-schema.json index e23fb26a1b1..07b44b57a51 100644 --- a/automation/github-labels/labels-schema.json +++ b/automation/github-labels/labels-schema.json @@ -152,12 +152,12 @@ "title-prefixes": [] } }, - "comp-jans-cli": { + "comp-jans-cli-tui": { "color": "0052CC", - "description": "Touching folder /jans-cli", + "description": "Touching folder /jans-cli-tui", "auto-label": { "branch": "", - "paths": ["jans-cli"], + "paths": ["jans-cli-tui"], "title-prefixes": [] } }, diff --git a/docker-jans-persistence-loader/scripts/utils.py b/docker-jans-persistence-loader/scripts/utils.py index ef54bd882dc..236dd0a58bb 100644 --- a/docker-jans-persistence-loader/scripts/utils.py +++ b/docker-jans-persistence-loader/scripts/utils.py @@ -204,7 +204,7 @@ def merge_jans_cli_ctx(manager, ctx): # - move the configs and secrets creation to configurator # - remove them on future release - # jans-cli client + # jans-cli-tui client ctx["role_based_client_id"] = manager.config.get("role_based_client_id") if not ctx["role_based_client_id"]: ctx["role_based_client_id"] = f"2000.{uuid4()}" diff --git a/jans-cli/CHANGELOG.md b/jans-cli-tui/CHANGELOG.md similarity index 100% rename from jans-cli/CHANGELOG.md rename to jans-cli-tui/CHANGELOG.md diff --git a/jans-cli-tui/cli_tui/cli/config_cli.py b/jans-cli-tui/cli_tui/cli/config_cli.py index af8f3fdfb57..94adc996b2f 100755 --- a/jans-cli-tui/cli_tui/cli/config_cli.py +++ b/jans-cli-tui/cli_tui/cli/config_cli.py @@ -1,7 +1,13 @@ #!/usr/bin/env python3 -import sys import os +import sys + +cur_dir = os.path.dirname(os.path.realpath(__file__)) +pylib_dir = os.path.join(cur_dir, 'pylib') +if os.path.exists(pylib_dir): + sys.path.insert(0, pylib_dir) + import json import re import urllib3 @@ -37,11 +43,8 @@ config_dir = home_dir.joinpath('.config') config_dir.mkdir(parents=True, exist_ok=True) config_ini_fn = config_dir.joinpath('jans-cli.ini') -cur_dir = os.path.dirname(os.path.realpath(__file__)) sys.path.append(cur_dir) - - my_op_mode = 'scim' if 'scim' in os.path.basename(sys.argv[0]) else 'jca' plugins = [] @@ -204,13 +207,19 @@ def get_plugin_name_from_title(title): if config['DEFAULT'].get(secret_key_str): client_secret = config['DEFAULT'][secret_key_str] elif config['DEFAULT'].get(secret_enc_key_str): - client_secret_enc = config['DEFAULT'][secret_enc_key_str] - client_secret = unobscure(client_secret_enc) + try: + client_secret_enc = config['DEFAULT'][secret_enc_key_str] + client_secret = unobscure(client_secret_enc) + except Exception: + pass - if 'access_token' in config['DEFAULT']: + if 'access_token' in config['DEFAULT'] and config['DEFAULT']['access_token'].strip(): access_token = config['DEFAULT']['access_token'] - elif 'access_token_enc' in config['DEFAULT']: - access_token = unobscure(config['DEFAULT']['access_token_enc']) + elif 'access_token_enc' in config['DEFAULT'] and config['DEFAULT']['access_token_enc'].strip(): + try: + access_token = unobscure(config['DEFAULT']['access_token_enc']) + except Exception: + pass debug = config['DEFAULT'].get('debug') log_dir = config['DEFAULT'].get('log_dir', log_dir) @@ -358,6 +367,10 @@ def get_request_header(self, headers={}, access_token=None): if not access_token: access_token = self.access_token + user = self.get_user_info() + if 'inum' in user: + headers['User-inum'] = user['inum'] + ret_val = {'Authorization': 'Bearer {}'.format(access_token)} ret_val.update(headers) return ret_val @@ -459,8 +472,9 @@ def revoke_session(self): def check_access_token(self): - if not self.access_token : - print(self.colored_text("Access token was not found.", warning_color)) + if not self.access_token: + if not self.wrapped: + print(self.colored_text("Access token was not found.", warning_color)) return try: @@ -472,9 +486,9 @@ def check_access_token(self): } ) except Exception as e: - print(self.colored_text("Unable to validate access token: {}".format(e), error_color)) - self.access_token = None - + if not self.wrapped: + print(self.colored_text("Unable to validate access token: {}".format(e), error_color)) + self.access_token = None def validate_date_time(self, date_str): try: @@ -672,7 +686,7 @@ def get_jwt_access_token(self, device_verified=None): def get_access_token(self, scope): if self.use_test_client: self.get_scoped_access_token(scope) - elif not self.access_token: + elif not self.access_token and not self.wrapped: self.check_access_token() self.get_jwt_access_token() return True, '' diff --git a/jans-cli-tui/cli_tui/jans_cli_tui.py b/jans-cli-tui/cli_tui/jans_cli_tui.py index 92e2f613447..4bf7e372a68 100755 --- a/jans-cli-tui/cli_tui/jans_cli_tui.py +++ b/jans-cli-tui/cli_tui/jans_cli_tui.py @@ -17,6 +17,10 @@ cur_dir = os.path.dirname(os.path.realpath(__file__)) sys.path.append(cur_dir) +pylib_dir = os.path.join(cur_dir, 'cli', 'pylib') +if os.path.exists(pylib_dir): + sys.path.insert(0, pylib_dir) + import prompt_toolkit from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app @@ -47,8 +51,8 @@ CheckboxList, Checkbox, ) - -from typing import Any, Optional, OrderedDict, Sequence, Union +from collections import OrderedDict +from typing import Any, Optional, Sequence, Union from prompt_toolkit.key_binding.key_processor import KeyPressEvent from prompt_toolkit.layout.containers import ( AnyContainer, @@ -97,12 +101,13 @@ def __init__(self): self.disabled_plugins = [] self.status_bar_text = '' self.progress_char = ' ' - self.progress_active = False self.progress_iterator = cycle(['⣾', '⣷', '⣯', '⣟', '⡿', '⢿', '⣻', '⣽']) self.styles = dict(style.style_rules) self._plugins = [] self._load_plugins() self.cli_object_ok = False + self.pbar_text = "" + self.progressing_text = "" self.not_implemented = Frame( body=HSplit([Label(text=_("Not imlemented yet")), Button(text=_("MyButton"))], width=D()), @@ -112,7 +117,8 @@ def __init__(self): self.no_button = Button(text=_("No"), handler=accept_no) self.pbar_window = Window(char=lambda: self.progress_char, style='class:progress', width=1) self.status_bar = VSplit([ - Window(FormattedTextControl(self.update_status_bar), style='class:status', height=1), + Window(FormattedTextControl(lambda: self.pbar_text), style='class:status', height=1), + Window(FormattedTextControl(self.update_status_bar), style='class:status', width=1), self.pbar_window, ], height=1 ) @@ -151,7 +157,6 @@ def __init__(self): mouse_support=True, ## added ) self.main_nav_selection_changed(self.nav_bar.navbar_entries[0][0]) - self.create_background_task(self.progress_coroutine()) self.plugins_initialised = False self.create_background_task(self.check_jans_cli_ini()) @@ -160,12 +165,14 @@ def __init__(self): async def progress_coroutine(self) -> None: """asyncio corotune for progress bar """ - while True: - if self.progress_active: - self.progress_char = next(self.progress_iterator) - self.invalidate() + self.progress_active = True + while self.progress_active: + self.progress_char = next(self.progress_iterator) + self.pbar_text="Progressing" + self.invalidate() await asyncio.sleep(0.15) - + self.progress_char = ' ' + self.invalidate() def cli_requests(self, args: dict) -> Response: response = self.cli_object.process_command_by_id( @@ -177,17 +184,17 @@ def cli_requests(self, args: dict) -> Response: ) return response - def start_progressing(self): - self.progress_active = True + def start_progressing(self, message: Optional[str]="Progressing") -> None: + self.progressing_text = message + self.create_background_task(self.progress_coroutine()) - def stop_progressing(self): + def stop_progressing(self, message: Optional[str]="") -> None: + self.progressing_text = message self.progress_active = False - self.progress_char = ' ' - self.invalidate() def _load_plugins(self) -> None: # check if admin-ui plugin is available: - + plugin_dir = os.path.join(cur_dir, 'plugins') for plugin_file in sorted(Path(plugin_dir).glob('*/main.py')): if plugin_file.parent.joinpath('.enabled').exists(): @@ -215,7 +222,7 @@ def plugin_enabled(self, pid: str) -> bool: return False - def remove_plugin(self, pid: str) -> None: + def remove_plugin(self, pid: str) -> None: """Removes plugin object Args: pid (str): PID of plugin @@ -299,10 +306,11 @@ async def coroutine(): self.stop_progressing() - self.cli_object_ok = True if not self.plugins_initialised: self.init_plugins() + self.runtime_plugins() + asyncio.ensure_future(coroutine()) else: @@ -310,6 +318,12 @@ async def coroutine(): if not self.plugins_initialised: self.init_plugins() + self.runtime_plugins() + + + def runtime_plugins(self) -> None: + """Disables plugins when cli object is ready""" + if self.cli_object_ok: response = self.cli_requests({'operation_id': 'is-license-active'}) if response.status_code == 404: @@ -568,15 +582,15 @@ def getButton( return b def update_status_bar(self) -> None: - text = '' - if self.status_bar_text: - text = self.status_bar_text - self.status_bar_text = '' + cur_text = self.pbar_text + if self.progressing_text: + self.pbar_text = self.progressing_text + elif hasattr(self.layout.current_window, 'jans_help') and self.layout.current_window.jans_help: + self.pbar_text = self.layout.current_window.jans_help else: - if hasattr(self.layout.current_window, 'jans_help') and self.layout.current_window.jans_help: - text = self.layout.current_window.jans_help - - return text + self.pbar_text = '' + if cur_text != self.pbar_text: + self.invalidate() def get_plugin_by_id(self, pid: str) -> None: for plugin in self._plugins: @@ -646,6 +660,8 @@ def save_creds(self, dialog:Dialog) -> None: prop_val = child.children[1].content.buffer.text if prop_name == 'jca_client_secret': config_cli.config['DEFAULT']['jca_client_secret_enc'] = config_cli.obscure(prop_val) + if 'jca_client_secret' in config_cli.config['DEFAULT']: + del config_cli.config['DEFAULT']['jca_client_secret'] else: config_cli.config['DEFAULT'][prop_name] = prop_val config_cli.write_config() diff --git a/jans-cli-tui/cli_tui/plugins/010_oxauth/edit_client_dialog.py b/jans-cli-tui/cli_tui/plugins/010_oxauth/edit_client_dialog.py index df938590aa8..28db4271dda 100755 --- a/jans-cli-tui/cli_tui/plugins/010_oxauth/edit_client_dialog.py +++ b/jans-cli-tui/cli_tui/plugins/010_oxauth/edit_client_dialog.py @@ -1,35 +1,20 @@ -from typing import Any, OrderedDict -from urllib import response - +from collections import OrderedDict +from typing import Any from prompt_toolkit.layout.dimension import D from prompt_toolkit.layout.containers import ( HSplit, VSplit, DynamicContainer, - Window ) from prompt_toolkit.widgets import ( - Box, Button, Label, -) -from prompt_toolkit.widgets import ( - Button, - Frame, - Label, - RadioList, TextArea, - CheckboxList, - Checkbox, + Dialog ) from prompt_toolkit.lexers import PygmentsLexer, DynamicLexer - from prompt_toolkit.application.current import get_app from asyncio import Future, ensure_future - - -import cli_style -from cli import config_cli from utils.static import DialogResult from utils.multi_lang import _ from wui_components.jans_dialog_with_nav import JansDialogWithNav @@ -41,18 +26,10 @@ from wui_components.jans_vetrical_nav import JansVerticalNav from view_uma_dialog import ViewUMADialog import threading -from prompt_toolkit.widgets import ( - Button, - Dialog, - VerticalLine, -) -from prompt_toolkit.layout.containers import AnyContainer from prompt_toolkit.buffer import Buffer - from prompt_toolkit.formatted_text import AnyFormattedText -from prompt_toolkit.layout.dimension import AnyDimension -from typing import Optional, Sequence, Union -from typing import TypeVar, Callable +from typing import Optional, Sequence +from typing import Callable import json @@ -80,6 +57,7 @@ def __init__( data (list): selected line data button_functions (list, optional): Dialog main buttons with their handlers. Defaults to []. save_handler (method, optional): handler invoked when closing the dialog. Defaults to None. + delete_UMAresource (method, optional): handler invoked when deleting UMA-resources """ super().__init__(parent, title, buttons) self.save_handler = save_handler @@ -91,6 +69,8 @@ def __init__( self.create_window() def save(self) -> None: + """method to invoked when saving the dialog (Save button is pressed) + """ self.data = self.make_data_from_dialog() self.data['disabled'] = not self.data['disabled'] @@ -164,6 +144,9 @@ def save(self) -> None: self.future.set_result(DialogResult.ACCEPT) def cancel(self) -> None: + """method to invoked when canceling changes in the dialog (Cancel button is pressed) + """ + self.future.set_result(DialogResult.CANCEL) def create_window(self) -> None: @@ -851,6 +834,12 @@ def search_uma_resources( self, tbuffer: Buffer, ) -> None: + """This method handel the search for UMA resources + + Args: + tbuffer (Buffer): Buffer returned from the TextArea widget > GetTitleText + """ + if not len(tbuffer.text) > 2: self.myparent.show_message(_("Error!"), _("Search string should be at least three characters")) return @@ -960,10 +949,18 @@ def client_dialog_nav_selection_changed( self, selection: str ) -> None: + """This method for client navigation bar when value is changed + + Args: + selection (str): the New Value from the nav-bar + """ + self.left_nav = selection def view_uma_resources(self, **params: Any) -> None: - + """This method view the UMA resources in a dialog + """ + selected_line_data = params['data'] ##self.uma_result title = _("Edit user Data (Clients)") @@ -972,5 +969,11 @@ def view_uma_resources(self, **params: Any) -> None: self.myparent.show_jans_dialog(dialog) def __pt_container__(self)-> Dialog: + """The container for the dialog itself + + Returns: + Dialog: The Edit Client Dialog + """ + return self.dialog diff --git a/jans-cli-tui/cli_tui/plugins/010_oxauth/edit_scope_dialog.py b/jans-cli-tui/cli_tui/plugins/010_oxauth/edit_scope_dialog.py index 5d101f9aa6b..4abdab70f54 100755 --- a/jans-cli-tui/cli_tui/plugins/010_oxauth/edit_scope_dialog.py +++ b/jans-cli-tui/cli_tui/plugins/010_oxauth/edit_scope_dialog.py @@ -1,4 +1,4 @@ -from typing import Any, OrderedDict, Optional, Sequence, Union, TypeVar, Callable +from typing import Any, Optional, Sequence, Union, TypeVar, Callable from asyncio import ensure_future from prompt_toolkit.layout.dimension import D @@ -58,8 +58,9 @@ def __init__( """init for `EditScopeDialog`, inherits from two diffrent classes `JansGDialog` and `DialogUtils` DialogUtils (methods): Responsable for all `make data from dialog` and `check required fields` in the form for any Edit or Add New + JansGDialog (dialog): This is the main dialog Class Widget for all Jans-cli-tui dialogs except custom dialogs like dialogs with navbar - Args: + Args: parent (widget): This is the parent widget for the dialog, to access `Pageup` and `Pagedown` title (str): The Main dialog title data (list): selected line data @@ -80,6 +81,9 @@ def __init__( self.sope_type = self.data.get('scopeType') or 'oauth' def save(self) -> None: + """method to invoked when saving the dialog (Save button is pressed) + """ + self.myparent.logger.debug('SAVE SCOPE') data = {} @@ -105,6 +109,9 @@ def save(self) -> None: self.future.set_result(DialogResult.ACCEPT) def cancel(self) -> None: + """method to invoked when canceling changes in the dialog (Cancel button is pressed) + """ + self.future.set_result(DialogResult.CANCEL) def create_window(self) -> None: @@ -175,12 +182,27 @@ def scope_selection_changed( self, cb: RadioList, ) -> None: + """This method for scope type selection set + + Args: + cb (RadioList): the New Value from the nav-bar + """ + self.sope_type = cb.current_value def get_named_claims( self, claims_list:list ) -> list: + """This method for getting claim name + + Args: + claims_list (list): List for Claims + + Returns: + list: List with Names retlated to that claims + """ + try : responce = self.myparent.cli_object.process_command_by_id( operation_id='get-attributes', @@ -208,7 +230,6 @@ def get_named_claims( return calims_names - def delete_claim(self, **kwargs: Any) -> None: """This method for the deletion of claim @@ -218,7 +239,7 @@ def delete_claim(self, **kwargs: Any) -> None: """ - dialog = self.myparent.get_confirm_dialog(_("Are you sure want to delete claim dn:")+"\n {} ?".format(selected[0])) + dialog = self.myparent.get_confirm_dialog(_("Are you sure want to delete claim dn:")+"\n {} ?".format(kwargs['selected'][0])) async def coroutine(): focused_before = self.myparent.layout.current_window result = await self.myparent.show_dialog_as_float(dialog) @@ -411,6 +432,11 @@ def search_claims( self, textbuffer: Buffer, ) -> None: + """This method handel the search for claims and adding new claims + + Args: + tbuffer (Buffer): Buffer returned from the TextArea widget > GetTitleText + """ try : responce = self.myparent.cli_object.process_command_by_id( @@ -465,5 +491,11 @@ def add_selected_claims(dialog): self.myparent.show_jans_dialog(dialog) def __pt_container__(self) -> Dialog: + """The container for the dialog itself + + Returns: + Dialog: The Edit Scope Dialog + """ + return self.dialog diff --git a/jans-cli-tui/cli_tui/plugins/010_oxauth/main.py b/jans-cli-tui/cli_tui/plugins/010_oxauth/main.py index 7c460759bae..59f89370afb 100755 --- a/jans-cli-tui/cli_tui/plugins/010_oxauth/main.py +++ b/jans-cli-tui/cli_tui/plugins/010_oxauth/main.py @@ -1,19 +1,10 @@ -import os -import sys import time import json - -import threading import asyncio from functools import partial from typing import Any, Optional - - -import prompt_toolkit from prompt_toolkit.application.current import get_app from prompt_toolkit.eventloop import get_event_loop -from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous from prompt_toolkit.layout.containers import ( HSplit, VSplit, @@ -26,37 +17,22 @@ Box, Button, Label, - Frame, Dialog, - CheckboxList, TextArea ) from prompt_toolkit.lexers import PygmentsLexer, DynamicLexer - - from utils.static import DialogResult -from prompt_toolkit.layout import ScrollablePane -from asyncio import Future - -from cli import config_cli from utils.utils import DialogUtils from wui_components.jans_nav_bar import JansNavBar -from wui_components.jans_side_nav_bar import JansSideNavBar from wui_components.jans_vetrical_nav import JansVerticalNav -from wui_components.jans_dialog import JansDialog -from wui_components.jans_dialog_with_nav import JansDialogWithNav from wui_components.jans_drop_down import DropDownWidget -from wui_components.jans_data_picker import DateSelectWidget from wui_components.jans_cli_dialog import JansGDialog - from view_property import ViewProperty from edit_client_dialog import EditClientDialog from edit_scope_dialog import EditScopeDialog from prompt_toolkit.buffer import Buffer from prompt_toolkit.application import Application - from utils.multi_lang import _ -import cli_style class Plugin(DialogUtils): """This is a general class for plugins @@ -68,39 +44,31 @@ def __init__( """init for Plugin class "oxauth" Args: - app (_type_): _description_ + app (Generic): The main Application class """ self.app = app self.pid = 'oxauth' self.name = '[A]uth Server' self.search_text= None - + self.oauth_update_properties_start_index = 0 + self.app_configuration = {} self.oauth_containers = {} self.oauth_prepare_navbar() self.oauth_prepare_containers() self.oauth_nav_selection_changed(self.nav_bar.navbar_entries[0][0]) def init_plugin(self) -> None: + """The initialization for this plugin + """ self.app.create_background_task(self.get_appconfiguration()) self.schema = self.app.cli_object.get_schema_from_reference('', '#/components/schemas/AppConfiguration') - async def get_appconfiguration(self) -> None: - """Coroutine for getting application configuration. - """ - try: - response = self.app.cli_object.process_command_by_id( - operation_id='get-properties', - url_suffix='', - endpoint_args='', - data_fn=None, - data={} - ) - - except Exception as e: - self.app.show_message(_("Error getting Jans configuration"), str(e)) - return + 'Coroutine for getting application configuration.' + + cli_args = {'operation_id': 'get-properties'} + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) if response.status_code not in (200, 201): self.app.show_message(_("Error getting Jans configuration"), str(response.text)) @@ -169,20 +137,19 @@ def oauth_prepare_containers(self) -> None: self.oauth_containers['properties'] = HSplit([ VSplit([ - self.app.getButton(text=_("Get properties"), name='oauth:scopes:get', jans_help=_("Retreive first {} Scopes").format(self.app.entries_per_page), handler=self.oauth_get_properties), self.app.getTitledText( _("Search: "), name='oauth:properties:search', jans_help=_("Press enter to perform search"), accept_handler=self.search_properties, - style='class:outh_containers_scopes.text') ], + style='class:outh_containers_scopes.text') + ], padding=3, width=D(), ), DynamicContainer(lambda: self.oauth_data_container['properties']) ],style='class:outh_containers_scopes') - self.oauth_containers['logging'] = DynamicContainer(lambda: self.oauth_data_container['logging']) self.oauth_main_container = HSplit([ @@ -214,6 +181,8 @@ def oauth_nav_selection_changed( selection (str): the current selected tab """ if selection in self.oauth_containers: + if selection == 'properties': + self.oauth_update_properties(tofocus=False) self.oauth_main_area = self.oauth_containers[selection] else: self.oauth_main_area = self.app.not_implemented @@ -226,21 +195,16 @@ def oauth_update_clients( """update the current clients data to server Args: - pattern (str, optional): endpoint arguments for the client data. Defaults to ''. + start_index (Optional[int], optional): This is flag for the clients page. Defaults to 0. + pattern (Optional[str], optional):endpoint arguments for the client data. Defaults to ''. """ - def get_next( - start_index: int, - pattern: Optional[str]= '', - ) -> None: - self.oauth_update_clients(start_index, pattern='') - async def coroutine(): endpoint_args ='limit:{},startIndex:{}'.format(self.app.entries_per_page, start_index) if pattern: endpoint_args +=',pattern:'+pattern cli_args = {'operation_id': 'get-oauth-openid-clients', 'endpoint_args': endpoint_args} - self.app.start_progressing() + self.app.start_progressing(_("Retreiving clients from server...")) response = await get_event_loop().run_in_executor(self.app.executor, self.app.cli_requests, cli_args) self.app.stop_progressing() @@ -256,11 +220,6 @@ async def coroutine(): data =[] - file1 = open("hopa.log", "w") - file1.write(str(response.status_code)+'\n') - file1.write(str(response.json())) - file1.close() - for d in result.get('entries', []): data.append( [ @@ -288,17 +247,17 @@ async def coroutine(): ) buttons = [] if start_index > 0: - handler_partial = partial(get_next, start_index-self.app.entries_per_page, pattern) + handler_partial = partial(self.oauth_update_clients, start_index-self.app.entries_per_page, pattern) prev_button = Button(_("Prev"), handler=handler_partial) prev_button.window.jans_help = _("Retreives previous %d entries") % self.app.entries_per_page buttons.append(prev_button) if result['start'] + self.app.entries_per_page < result['totalEntriesCount']: - handler_partial = partial(get_next, start_index+self.app.entries_per_page, pattern) + handler_partial = partial(self.oauth_update_clients, start_index+self.app.entries_per_page, pattern) next_button = Button(_("Next"), handler=handler_partial) next_button.window.jans_help = _("Retreives next %d entries") % self.app.entries_per_page buttons.append(next_button) - self.app.layout.focus(clients) # clients.focuse..!? TODO >> DONE + self.app.layout.focus(clients) self.oauth_data_container['clients'] = HSplit([ clients, VSplit(buttons, padding=5, align=HorizontalAlign.CENTER) @@ -310,10 +269,8 @@ async def coroutine(): else: self.app.show_message(_("Oops"), _("No matching result"),tobefocused = self.oauth_containers['clients']) - self.app.start_progressing() asyncio.ensure_future(coroutine()) - def delete_client(self, **kwargs: Any) -> None: """This method for the deletion of the clients data @@ -352,7 +309,6 @@ async def coroutine(): asyncio.ensure_future(coroutine()) - def oauth_get_scopes( self, start_index: Optional[int]= 0, @@ -361,7 +317,8 @@ def oauth_get_scopes( """update the current Scopes data to server Args: - start_index (int, optional): add Button("Prev") to the layout. Defaults to 0. + start_index (int, optional): add Button("Prev") and Button("Next")to the layout (which page am I in). Defaults to 0. + pattern (Optional[str], optional):endpoint arguments for the Scopes data. Defaults to ''. """ async def coroutine(): @@ -371,16 +328,16 @@ async def coroutine(): endpoint_args +=',pattern:'+pattern cli_args = {'operation_id': 'get-oauth-scopes', 'endpoint_args':endpoint_args} - self.app.start_progressing() + self.app.start_progressing(_("Retreiving scopes from server...")) response = await get_event_loop().run_in_executor(self.app.executor, self.app.cli_requests, cli_args) - self.app.stop_progressing() + self.app.stop_progressing(_("Retreived")) + try: result = response.json() except Exception as e: self.app.show_message(_("Error getting response"), str(response)) return - data =[] for d in result.get('entries', []): @@ -392,7 +349,7 @@ async def coroutine(): d['inum'] ] ) - + if data: scopes = JansVerticalNav( @@ -436,103 +393,74 @@ async def coroutine(): asyncio.ensure_future(coroutine()) - def oauth_update_properties( self, - start_index: Optional[int]= 0, + start_index: Optional[int]= 0, pattern: Optional[str]= '', + tofocus:Optional[bool]=True, ) -> None: + """update the current clients data to server Args: + start_index (int, optional): add Button("Prev") and Button("Next")to the layout (which page am I in). Defaults to 0. pattern (str, optional): endpoint arguments for the client data. Defaults to ''. + tofocus (Optional[bool], optional): To focus the properties or not (used to not focus on navigation). Defaults to True. """ - def get_next( - start_index: int, - pattern: Optional[str]= '', - ) -> None: - self.app.logger.debug("start_index="+str(start_index)) - self.oauth_update_properties(start_index, pattern=pattern) - - # ------------------------------------------------------------------------------- # - # ------------------------------------------------------------------------------- # - # ------------------------------------------------------------------------------- # - - try : - rsponse = self.app.cli_object.process_command_by_id( - operation_id='get-properties', - url_suffix='', - endpoint_args='', - data_fn=None, - data={} - ) - except Exception as e: - self.app.stop_progressing() - self.app.show_message(_("Error getting properties"), str(e)) - return - - self.app.stop_progressing() - if rsponse.status_code not in (200, 201): - self.app.show_message(_("Error getting properties"), str(rsponse.text)) - return - - try: - result = rsponse.json() - except Exception: - self.app.show_message(_("Error getting properties"), str(rsponse.text)) - return + self.oauth_update_properties_start_index = start_index # ------------------------------------------------------------------------------- # # ----------------------------------- Search ------------------------------------ # # ------------------------------------------------------------------------------- # porp_schema = self.app.cli_object.get_schema_from_reference('', '#/components/schemas/AppConfiguration') data =[] + if pattern: - for k in result: + for k in self.app_configuration: if pattern.lower() in k.lower(): if k in porp_schema.get('properties', {}): data.append( [ k, - result[k], + self.app_configuration[k], ] ) else: - for d in result: + for d in self.app_configuration: if d in porp_schema.get('properties', {}): data.append( [ d, - result[d], + self.app_configuration[d], ] ) - # ------------------------------------------------------------------------------- # # --------------------------------- View Data ----------------------------------- # - # ------------------------------------------------------------------------------- # + # ------------------------------------------------------------------------------- # + if data: buttons = [] - if int(len(data)/ 20) >=1 : - if start_index< int(len(data)/ 20) : - handler_partial = partial(get_next, start_index+1, pattern) - next_button = Button(_("Next"), handler=handler_partial) - next_button.window.jans_help = _("Retreives next %d entries") % self.app.entries_per_page - buttons.append(next_button) + if len(data)/20 >=1: if start_index!=0: - handler_partial = partial(get_next, start_index-1, pattern) + handler_partial = partial(self.oauth_update_properties, start_index-1, pattern) prev_button = Button(_("Prev"), handler=handler_partial) - prev_button.window.jans_help = _("Retreives previous %d entries") % self.app.entries_per_page + prev_button.window.jans_help = _("Displays previous %d entries") % self.app.entries_per_page buttons.append(prev_button) + if start_index< int(len(data)/ 20) : + handler_partial = partial(self.oauth_update_properties, start_index+1, pattern) + next_button = Button(_("Next"), handler=handler_partial) + next_button.window.jans_help = _("Displays next %d entries") % self.app.entries_per_page + buttons.append(next_button) data_now = data[start_index*20:start_index*20+20] - clients = JansVerticalNav( + properties = JansVerticalNav( myparent=self.app, headers=['Property Name', 'Property Value'], preferred_size= [0,0], @@ -541,22 +469,25 @@ def get_next( on_display=self.properties_display_dialog, get_help=(self.get_help,'AppConfiguration'), # selection_changed=self.data_selection_changed, - selectes=0, + selectes=0, headerColor='class:outh-verticalnav-headcolor', entriesColor='class:outh-verticalnav-entriescolor', - all_data=list(result.values()) + all_data=list(self.app_configuration.values()) ) - self.app.layout.focus(clients) # clients.focuse..!? TODO >> DONE + self.oauth_data_container['properties'] = HSplit([ - clients, + properties, VSplit(buttons, padding=5, align=HorizontalAlign.CENTER) ]) - get_app().invalidate() - self.app.layout.focus(clients) ### it fix focuse on the last item deletion >> try on UMA-res >> edit_client_dialog >> oauth_update_uma_resources + self.app.invalidate() + if tofocus: + self.app.layout.focus(properties) else: self.app.show_message(_("Oops"), _("No matching result"),tobefocused = self.oauth_containers['properties']) def properties_display_dialog(self, **params: Any) -> None: + """Display the properties as Text + """ data_property, data_value = params['selected'][0], params['selected'][1] body = HSplit([ TextArea( @@ -574,27 +505,24 @@ def properties_display_dialog(self, **params: Any) -> None: self.app.show_jans_dialog(dialog) - def oauth_get_properties(self) -> None: - """Method to get the clients data from server - """ - self.oauth_data_container['properties'] = HSplit([Label(_("Please wait while getting properties"),style='class:outh-waitclientdata.label')], width=D(),style='class:outh-waitclientdata') - t = threading.Thread(target=self.oauth_update_properties, daemon=True) - self.app.start_progressing() - t.start() - def view_property(self, **params: Any) -> None: - #property, value =params['passed'] - + """This method view the properties in Dialog to edit + """ selected_line_data = params['passed'] ##self.uma_result - + title = _("Edit property") - dialog = ViewProperty(self.app, title=title, data=selected_line_data, get_properties= self.oauth_get_properties, search_properties=self.search_properties, search_text=self.search_text) - + dialog = ViewProperty(app=self.app, parent=self, title=title, data=selected_line_data) + self.app.show_jans_dialog(dialog) - def search_properties(self, tbuffer:Buffer,) -> None: + def search_properties(self, tbuffer:Buffer) -> None: + """This method handel the search for Properties + + Args: + tbuffer (Buffer): Buffer returned from the TextArea widget > GetTitleText + """ self.app.logger.debug("tbuffer="+str(tbuffer)) self.app.logger.debug("type tbuffer="+str(type(tbuffer))) self.search_text=tbuffer.text @@ -602,42 +530,16 @@ def search_properties(self, tbuffer:Buffer,) -> None: if not len(tbuffer.text) > 2: self.app.show_message(_("Error!"), _("Search string should be at least three characters"),tobefocused=self.oauth_containers['properties']) return - t = threading.Thread(target=self.oauth_update_properties, args=(0,tbuffer.text), daemon=True) - self.app.start_progressing() - t.start() - def oauth_update_keys(self) -> None: + self.oauth_update_properties(0, tbuffer.text) + def oauth_update_keys(self) -> None: """update the current Keys fromserver """ - try : - rsponse = self.app.cli_object.process_command_by_id( - operation_id='get-config-jwks', - url_suffix='', - endpoint_args='', - data_fn=None, - data={} - ) - except Exception as e: - self.app.stop_progressing() - self.app.show_message(_("Error getting keys"), str(e)) - return - - self.app.stop_progressing() - if rsponse.status_code not in (200, 201): - self.app.show_message(_("Error getting keys"), str(rsponse.text)) - return - - try: - result = rsponse.json() - except Exception: - self.app.show_message(_("Error getting keys"), str(rsponse.text)) - return - data =[] - for d in result.get('keys', []): + for d in self.jwks_keys.get('keys', []): try: gmt = time.gmtime(int(d['exp'])/1000) exps = time.strftime("%d %b %Y %H:%M:%S", gmt) @@ -661,7 +563,7 @@ def oauth_update_keys(self) -> None: selectes=0, headerColor='class:outh-verticalnav-headcolor', entriesColor='class:outh-verticalnav-entriescolor', - all_data=result['keys'] + all_data=self.jwks_keys['keys'] ) self.oauth_data_container['keys'] = HSplit([keys]) @@ -669,30 +571,36 @@ def oauth_update_keys(self) -> None: self.app.layout.focus(keys) else: - self.app.show_message(_("Oops"), _("Nothing to display"), tobefocused=self.oauth_containers['keys']) + self.app.show_message(_("Oops"), _("No JWKS keys were found"), tobefocused=self.app.center_frame) def oauth_get_keys(self) -> None: """Method to get the Keys from server """ - self.oauth_data_container['keys'] = HSplit([Label(_("Please wait while getting Keys"), style='class:outh-waitscopedata.label')], width=D(), style='class:outh-waitclientdata') - t = threading.Thread(target=self.oauth_update_keys, daemon=True) - self.app.start_progressing() - t.start() - + + async def coroutine(): + cli_args = {'operation_id': 'get-config-jwks'} + self.app.start_progressing("Retreiving JWKS keys...") + response = await get_event_loop().run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + self.jwks_keys = response.json() + self.oauth_update_keys() + + asyncio.ensure_future(coroutine()) + def edit_scope_dialog(self, **params: Any) -> None: + """This Method show the scopes dialog for edit + """ selected_line_data = params['data'] dialog = EditScopeDialog(self.app, title=_("Edit Scopes"), data=selected_line_data, save_handler=self.save_scope) self.app.show_jans_dialog(dialog) def edit_client_dialog(self, **params: Any) -> None: + """This Method show the scopes dialog for edit + """ selected_line_data = params['data'] title = _("Edit user Data (Clients)") - file1= open("hopa.log",'a') - file1.write("selected_line_data : "+str(selected_line_data)+'\n \n') - file1.close() - self.EditClientDialog = EditClientDialog(self.app, title=title, data=selected_line_data,save_handler=self.save_client,delete_UMAresource=self.delete_UMAresource) self.app.show_jans_dialog(self.EditClientDialog) @@ -722,7 +630,6 @@ def save_client(self, dialog: Dialog) -> None: self.app.show_message(_("Error!"), _("An error ocurred while saving client:\n") + str(response.text)) - def save_scope(self, dialog: Dialog) -> None: """This method to save the client data to server @@ -748,6 +655,11 @@ async def coroutine(): asyncio.ensure_future(coroutine()) def search_scope(self, tbuffer:Buffer,) -> None: + """This method handel the search for Scopes + + Args: + tbuffer (Buffer): Buffer returned from the TextArea widget > GetTitleText + """ if not len(tbuffer.text) > 2: self.app.show_message(_("Error!"), _("Search string should be at least three characters"),tobefocused=self.oauth_containers['scopes']) return @@ -755,6 +667,11 @@ def search_scope(self, tbuffer:Buffer,) -> None: self.oauth_get_scopes(pattern=tbuffer.text) def search_clients(self, tbuffer:Buffer,) -> None: + """This method handel the search for Clients + + Args: + tbuffer (Buffer): Buffer returned from the TextArea widget > GetTitleText + """ if not len(tbuffer.text) > 2: self.app.show_message(_("Error!"), _("Search string should be at least three characters"),tobefocused=self.oauth_containers['clients']) return @@ -762,18 +679,20 @@ def search_clients(self, tbuffer:Buffer,) -> None: self.oauth_update_clients(pattern=tbuffer.text) def add_scope(self) -> None: - """Method to display the dialog of clients + """Method to display the dialog of Scopes (Add New) """ dialog = EditScopeDialog(self.app, title=_("Add New Scope"), data={}, save_handler=self.save_scope) result = self.app.show_jans_dialog(dialog) def add_client(self) -> None: - """Method to display the dialog of clients + """Method to display the dialog of clients (Add New) """ dialog = EditClientDialog(self.app, title=_("Add Client"), data={}, save_handler=self.save_client) result = self.app.show_jans_dialog(dialog) def get_help(self, **kwargs: Any): + """This method get focused field Description to display on statusbar + """ self.app.logger.debug("get_help: "+str(kwargs['data'])) self.app.logger.debug("get_help: "+str(kwargs['scheme'])) @@ -790,17 +709,9 @@ def get_help(self, **kwargs: Any): self.app.status_bar_text= kwargs['data'][1] self.app.logger.debug("kwargs['data']: "+str(kwargs['data'])) - - # self.app.status_bar_text= kwargs['data'][1] - - def delete_scope(self, **kwargs: Any): """This method for the deletion of the clients data - Args: - selected (_type_): The selected Client - event (_type_): _description_ - Returns: str: The server response """ @@ -828,6 +739,11 @@ async def coroutine(): asyncio.ensure_future(coroutine()) def delete_UMAresource(self, **kwargs: Any): + """This method for the deletion of the UMAresource + + Returns: + str: The server response + """ dialog = self.app.get_confirm_dialog(_("Are you sure want to delete UMA resoucres with id:")+"\n {} ?".format(kwargs ['selected'][0])) async def coroutine(): focused_before = self.app.layout.current_window @@ -852,6 +768,8 @@ async def coroutine(): asyncio.ensure_future(coroutine()) def oauth_logging(self) -> None: + """This method for the Auth Login + """ self.oauth_data_container['logging'] = HSplit([ self.app.getTitledWidget( _('Log Level'), @@ -900,6 +818,8 @@ def oauth_logging(self) -> None: ], style='class:outh_containers_clients', width=D()) def save_logging(self) -> None: + """This method to Save the Auth Login to server + """ mod_data = self.make_data_from_dialog({'logging':self.oauth_data_container['logging']}) pathches = [] for key_ in mod_data: diff --git a/jans-cli-tui/cli_tui/plugins/010_oxauth/view_property.py b/jans-cli-tui/cli_tui/plugins/010_oxauth/view_property.py index 3f786edac9c..a9f35d83d0f 100644 --- a/jans-cli-tui/cli_tui/plugins/010_oxauth/view_property.py +++ b/jans-cli-tui/cli_tui/plugins/010_oxauth/view_property.py @@ -1,62 +1,20 @@ -import json -from asyncio import Future -from typing import OrderedDict - -from prompt_toolkit.widgets import Button, TextArea -from prompt_toolkit.application.current import get_app +import asyncio from prompt_toolkit.layout.dimension import D -from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.buffer import Buffer from prompt_toolkit.formatted_text import AnyFormattedText -from prompt_toolkit.layout.dimension import AnyDimension - -from prompt_toolkit.widgets import ( - Button, - Label, - TextArea, - -) -from asyncio import ensure_future - -from prompt_toolkit.widgets import ( - Button, - Dialog, - VerticalLine, -) from cli import config_cli from prompt_toolkit.layout.containers import ( - ConditionalContainer, - Float, HSplit, - VSplit, - VerticalAlign, DynamicContainer, - FloatContainer, - Window, - AnyContainer ) from prompt_toolkit.widgets import ( - Box, Button, - Frame, - Label, RadioList, - TextArea, + Dialog, ) - from utils.static import DialogResult -from wui_components.jans_dialog import JansDialog -from wui_components.jans_dialog_with_nav import JansDialogWithNav -from wui_components.jans_nav_bar import JansNavBar -from wui_components.jans_side_nav_bar import JansSideNavBar from utils.utils import DialogUtils - from wui_components.jans_cli_dialog import JansGDialog - -from wui_components.jans_drop_down import DropDownWidget - -from typing import Optional, Sequence, Union -from typing import TypeVar, Callable +from typing import Optional, Sequence from utils.multi_lang import _ import cli_style @@ -64,34 +22,47 @@ class ViewProperty(JansGDialog, DialogUtils): """The Main UMA-resources Dialog to view UMA Resource Details """ def __init__( - self, - parent, - data:tuple, - title: AnyFormattedText= "", - search_text: AnyFormattedText= "", - buttons: Optional[Sequence[Button]]= [], - get_properties: Callable= None, - search_properties: Callable= None, - )-> Dialog: - - super().__init__(parent, title, buttons) + self, + app, + parent, + data:tuple, + title: AnyFormattedText= "", + buttons: Optional[Sequence[Button]]= [] + )-> None: + """init for `ViewProperty`, inherits from two diffrent classes `JansGDialog` and `DialogUtils` + + JansGDialog (dialog): This is the main dialog Class Widget for all Jans-cli-tui dialogs except custom dialogs like dialogs with navbar + DialogUtils (methods): Responsable for all `make data from dialog` and `check required fields` in the form for any Edit or Add New + + Args: + app (Generic): The main Application class + parent (widget): This is the parent widget for the dialog + data (tuple): selected line data + title (AnyFormattedText, optional): The Main dialog title. Defaults to "". + button_functions (list, optional): Dialog main buttons with their handlers. Defaults to []. + """ + super().__init__(app, title, buttons) self.property, self.value = data[0],data[1] - self.myparent= parent - self.get_properties = get_properties - self.search_properties= search_properties - self.search_text=search_text + self.app = app + self.myparent = parent self.value_content = HSplit([],width=D()) self.tabs = {} self.selected_tab = 'tab0' - self.schema = self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/AppConfiguration') + self.schema = self.app.cli_object.get_schema_from_reference('', '#/components/schemas/AppConfiguration') self.prepare_properties() self.create_window() def cancel(self) -> None: + """method to invoked when canceling changes in the dialog (Cancel button is pressed) + """ + self.future.set_result(DialogResult.CANCEL) def save(self) -> None: + """method to invoked when saving the dialog (Save button is pressed) + """ + data_dict = {} list_data =[] @@ -100,10 +71,15 @@ def save(self) -> None: prop_type = self.get_item_data(wid) data = prop_type['value'] - elif (type(self.value)==list and type(self.value[0]) not in [dict,list]): + elif (type(self.value)==list and (type(self.value[0]) not in [dict,list])): + for wid in self.value_content.children: prop_type = self.get_item_data(wid) - data = prop_type['value'].split('\n') + + if self.get_type(prop_type['key']) != 'checkboxlist': + data = prop_type['value'].split('\n') + else: + data = prop_type['value'] elif type(self.value) == dict : for wid in self.value_content.children: @@ -121,58 +97,58 @@ def save(self) -> None: list_data.append(data_dict) data = list_data else : - self.myparent.logger.debug("self.value: "+str(self.value)) - self.myparent.logger.debug("type self.value: "+str(type(self.value))) + self.app.logger.debug("self.value: "+str(self.value)) + self.app.logger.debug("type self.value: "+str(type(self.value))) data = [] # ------------------------------------------------------------# # --------------------- Patch to server ----------------------# # ------------------------------------------------------------# if data : - response = self.myparent.cli_object.process_command_by_id( - operation_id='patch-properties' , - url_suffix='', - endpoint_args='', - data_fn='', - data=[ {'op':'replace', 'path': self.property, 'value': data } ] - ) - else: - return - # ------------------------------------------------------------# - # -- get_properties or serach again to see Momentary change --# - # ------------------------------------------------------------# - if response: - if self.search_text: - tbuff = Buffer(name='', ) - tbuff.text=self.search_text - self.search_properties(tbuff) - else: - self.get_properties() - self.future.set_result(DialogResult.ACCEPT) - return True - self.myparent.show_message(_("Error!"), _("An error ocurred while saving property:\n") + str(response.text)) + cli_args = {'operation_id': 'patch-properties', 'data': [ {'op':'replace', 'path': self.property, 'value': data } ]} + + async def coroutine(): + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + self.myparent.app_configuration = response + self.future.set_result(DialogResult.ACCEPT) + self.myparent.oauth_update_properties(start_index=self.myparent.oauth_update_properties_start_index) + asyncio.ensure_future(coroutine()) def get_type(self,prop): + """This Method get a property and get its type from schema to return the widget type to implement + + Args: + prop (str): The property name + + Returns: + str: the widget type to implement + """ try : - if self.schema.get('properties', {})[prop]['type'] == 'string': + proper = self.schema.get('properties', {})[prop] + + if proper['type'] == 'string': prop_type= 'TitledText' - elif self.schema.get('properties', {})[prop]['type'] == 'integer': + elif proper['type'] == 'integer': prop_type= 'int-TitledText' - elif self.schema.get('properties', {})[prop]['type'] == 'boolean': + elif proper['type'] == 'boolean': prop_type= 'TitledCheckBox' - elif self.schema.get('properties', {})[prop]['type'] == 'object': + elif proper['type'] == 'object': prop_type= 'dict' - elif self.schema.get('properties', {})[prop]['type'] == 'array': - if 'enum' in self.schema.get('properties', {})[prop]: + elif proper['type'] == 'array': + if 'enum' in proper or ('enum' in proper['items']): prop_type= 'checkboxlist' else: if type(self.value[0]) == dict: prop_type= 'list-dict' + elif type(self.value[0]) == list: + prop_type= 'list-list' else: prop_type= 'long-TitledText' except: @@ -180,20 +156,35 @@ def get_type(self,prop): return prop_type - def get_listValues(self,prop): + def get_listValues(self,prop,type=None): + """This method get list values for properties own Enum values + + Args: + prop (str): The property name + type (_type_, optional): If the Items in Property properties had a nasted Enum. Defaults to None. + + Returns: + list: List of the properties enum to choose from + """ try : - list_values= self.schema.get('properties', {})[prop]['enum'] + if type !='nasted': + list_values= self.schema.get('properties', {})[prop]['items']['enum'] + else: + list_values= self.schema.get('properties', {})[prop]['items']['items']['enum'] + except: list_values = [] return list_values def prepare_properties(self): + """This method build the main value_content to edit the properties + """ prop_type = self.get_type(self.property) if prop_type == 'TitledText': - self.value_content= HSplit([self.myparent.getTitledText( + self.value_content= HSplit([self.app.getTitledText( self.property, name=self.property, value=self.value, @@ -202,7 +193,7 @@ def prepare_properties(self): ],width=D()) elif prop_type == 'int-TitledText': - self.value_content= HSplit([self.myparent.getTitledText( + self.value_content= HSplit([self.app.getTitledText( self.property, name=self.property, value=self.value, @@ -212,7 +203,7 @@ def prepare_properties(self): ],width=D()) elif prop_type == 'long-TitledText': - self.value_content= HSplit([self.myparent.getTitledText( + self.value_content= HSplit([self.app.getTitledText( self.property, name=self.property, height=3, @@ -221,9 +212,18 @@ def prepare_properties(self): ), ],width=D()) + elif prop_type == 'list-list': + self.value_content= HSplit([ + self.app.getTitledCheckBoxList( + self.property, + name=self.property, + values=self.get_listValues(self.property,'nasted'), + style='class:outh-client-checkboxlist'), + ],width=D()) + elif prop_type == 'checkboxlist': self.value_content= HSplit([ - self.myparent.getTitledCheckBoxList( + self.app.getTitledCheckBoxList( self.property, name=self.property, values=self.get_listValues(self.property), @@ -241,7 +241,7 @@ def prepare_properties(self): tab_list=[] for item in tab: if type(tab[item]) == str: - tab_list.append(HSplit([self.myparent.getTitledText( + tab_list.append(HSplit([self.app.getTitledText( item , name=item, value=tab[item], @@ -250,7 +250,7 @@ def prepare_properties(self): ],width=D())) if type(tab[item]) == int : - tab_list.append(HSplit([self.myparent.getTitledText( + tab_list.append(HSplit([self.app.getTitledText( item , name=item, value=tab[item], @@ -260,7 +260,7 @@ def prepare_properties(self): ],width=D())) elif type(tab[item]) == list: - tab_list.append(HSplit([self.myparent.getTitledText( + tab_list.append(HSplit([self.app.getTitledText( item, name=item, height=3, @@ -271,7 +271,7 @@ def prepare_properties(self): elif type(tab[item]) == bool: tab_list.append(HSplit([ - self.myparent.getTitledCheckBox( + self.app.getTitledCheckBox( item, name=item, checked= tab[item], @@ -281,7 +281,7 @@ def prepare_properties(self): self.tabs['tab{}'.format(self.value.index(tab))] = HSplit(tab_list,width=D()) self.value_content=HSplit([ - self.myparent.getTitledRadioButton( + self.app.getTitledRadioButton( _("Tab Num"), name='tabNum', current_value=self.selected_tab, @@ -295,7 +295,7 @@ def prepare_properties(self): elif prop_type == 'TitledCheckBox': self.value_content= HSplit([ - self.myparent.getTitledCheckBox( + self.app.getTitledCheckBox( self.property, name=self.property, checked= self.value, @@ -306,7 +306,7 @@ def prepare_properties(self): dict_list=[] for item in self.value: if type(self.value[item]) == str: - dict_list.append(HSplit([self.myparent.getTitledText( + dict_list.append(HSplit([self.app.getTitledText( item , name=item, value=self.value[item], @@ -315,7 +315,7 @@ def prepare_properties(self): ],width=D())) elif type(self.value[item]) == int : - dict_list.append(HSplit([self.myparent.getTitledText( + dict_list.append(HSplit([self.app.getTitledText( item , name=item, value=self.value[item], @@ -325,7 +325,7 @@ def prepare_properties(self): ],width=D())) elif type(self.value[item]) == list: - dict_list.append(HSplit([self.myparent.getTitledText( + dict_list.append(HSplit([self.app.getTitledText( item, name=item, height=3, @@ -336,7 +336,7 @@ def prepare_properties(self): elif type(self.value[item]) == bool: dict_list.append(HSplit([ - self.myparent.getTitledCheckBox( + self.app.getTitledCheckBox( item, name=item, checked= self.value[item], @@ -344,7 +344,7 @@ def prepare_properties(self): ],width=D())) else : - dict_list.append(HSplit([self.myparent.getTitledText( + dict_list.append(HSplit([self.app.getTitledText( item, name=item, value="No Items Here", @@ -378,8 +378,19 @@ def tab_selection_changed( self, cb: RadioList, ) -> None: + """This method for properties that implemented in multi tab + + Args: + cb (RadioList): the New Value from the nasted tab + """ self.selected_tab = cb.current_value def __pt_container__(self)-> Dialog: + """The container for the dialog itself + + Returns: + Dialog: View Property + """ + return self.dialog diff --git a/jans-cli-tui/cli_tui/plugins/010_oxauth/view_uma_dialog.py b/jans-cli-tui/cli_tui/plugins/010_oxauth/view_uma_dialog.py index 99d801f76d6..d6c551df023 100644 --- a/jans-cli-tui/cli_tui/plugins/010_oxauth/view_uma_dialog.py +++ b/jans-cli-tui/cli_tui/plugins/010_oxauth/view_uma_dialog.py @@ -1,65 +1,29 @@ -import json -from asyncio import Future -from typing import OrderedDict - -from prompt_toolkit.widgets import Button, TextArea -from prompt_toolkit.application.current import get_app from prompt_toolkit.layout.dimension import D from prompt_toolkit.layout.containers import ( VSplit, DynamicContainer, ) -from prompt_toolkit.key_binding import KeyBindings - -from prompt_toolkit.widgets import ( - Button, - Label, - TextArea, - -) -from asyncio import ensure_future - -from prompt_toolkit.widgets import ( - Button, - Dialog, - VerticalLine, -) -from cli import config_cli from prompt_toolkit.layout.containers import ( - ConditionalContainer, - Float, HSplit, VSplit, - VerticalAlign, DynamicContainer, - FloatContainer, - Window, - AnyContainer ) from prompt_toolkit.widgets import ( Box, Button, - Frame, Label, - RadioList, - TextArea, + Dialog, ) - import cli_style from utils.multi_lang import _ from utils.utils import DialogUtils from utils.static import DialogResult -from wui_components.jans_dialog_with_nav import JansDialogWithNav from wui_components.jans_nav_bar import JansNavBar -from wui_components.jans_side_nav_bar import JansSideNavBar -from wui_components.jans_dialog import JansDialog from wui_components.jans_cli_dialog import JansGDialog -from wui_components.jans_drop_down import DropDownWidget from prompt_toolkit.formatted_text import AnyFormattedText -from prompt_toolkit.layout.dimension import AnyDimension -from typing import Optional, Sequence, Union -from typing import TypeVar, Callable +from typing import Optional, Sequence +from typing import Callable class ViewUMADialog(JansGDialog, DialogUtils): """The Main UMA-resources Dialog to view UMA Resource Details @@ -81,8 +45,8 @@ def __init__( parent (widget): This is the parent widget for the dialog title (str): The Main dialog title data (list): selected line data - button_functions (list, optional): Dialog main buttons with their handlers. Defaults to []. - save_handler (method, optional): handler invoked when closing the dialog. Defaults to None. + buttons (list, optional): Dialog main buttons with their handlers. Defaults to []. + deleted_uma (method, optional): handler invoked when Deleting UMA-res Defaults to None. """ super().__init__(parent, title, buttons) self.data = data @@ -212,5 +176,11 @@ def oauth_nav_selection_changed( self.oauth_main_area = self.UMA_containers[selection] def __pt_container__(self)-> Dialog: + """The container for the dialog itself + + Returns: + Dialog: View Property + """ + return self.dialog diff --git a/jans-cli-tui/cli_tui/plugins/020_fido/main.py b/jans-cli-tui/cli_tui/plugins/020_fido/main.py index a78504df3a6..7029fb94971 100755 --- a/jans-cli-tui/cli_tui/plugins/020_fido/main.py +++ b/jans-cli-tui/cli_tui/plugins/020_fido/main.py @@ -1,29 +1,18 @@ -import os -import sys import asyncio -import time - from collections import OrderedDict from functools import partial from typing import Any - -from prompt_toolkit.application.current import get_app from prompt_toolkit.layout.containers import HSplit, DynamicContainer, VSplit, Window from prompt_toolkit.layout.dimension import D -from prompt_toolkit.widgets import Button, Label, Frame, Box, Dialog +from prompt_toolkit.widgets import Button, Label, Box, Dialog from prompt_toolkit.application import Application - from wui_components.jans_nav_bar import JansNavBar from wui_components.jans_drop_down import DropDownWidget from wui_components.jans_vetrical_nav import JansVerticalNav from wui_components.jans_cli_dialog import JansGDialog - - from utils.multi_lang import _ from utils.utils import DialogUtils -import cli_style - class Plugin(DialogUtils): """This is a general class for plugins """ @@ -34,7 +23,7 @@ def __init__( """init for Plugin class "fido" Args: - app (_type_): _description_ + app (Generic): The main Application class """ self.app = app self.pid = 'fido' @@ -48,11 +37,14 @@ def process(self) -> None: pass def init_plugin(self) -> None: + """The initialization for this plugin + """ self.app.create_background_task(self.get_fido_configuration()) - def edit_requested_party(self, **kwargs: Any) -> None: + """This method for editing the requested party + """ title = _("Enter Request Party Properties") schema = self.app.cli_object.get_schema_from_reference('Fido2', '#/components/schemas/RequestedParty') cur_data = kwargs.get('passed', ['', '']) @@ -74,9 +66,28 @@ def add_request_party(dialog: Dialog) -> None: dialog = JansGDialog(self.app, title=title, body=body, buttons=buttons, width=self.app.dialog_width-20) self.app.show_jans_dialog(dialog) - def delete_requested_party(self, **kwargs: Any) -> None: - self.requested_parties_container.remove_item(kwargs['selected']) + """This method for deleting the requested party + """ + + dialog = self.app.get_confirm_dialog(_("Are you sure want to delete requested patry:")+"\n {} ?".format(kwargs['selected'][1])) + + async def coroutine(): + focused_before = self.app.layout.current_window + result = await self.app.show_dialog_as_float(dialog) + try: + self.app.layout.focus(focused_before) + except: + self.app.stop_progressing() + self.app.layout.focus(self.app.center_frame) + + if result.lower() == 'yes': + self.requested_parties_container.remove_item(kwargs['selected']) + self.app.stop_progressing() + + return result + + asyncio.ensure_future(coroutine()) def create_widgets(self): self.schema = self.app.cli_object.get_schema_from_reference('Fido2', '#/components/schemas/AppConfiguration') @@ -188,7 +199,6 @@ def create_widgets(self): self.nav_selection_changed(list(self.tabs)[0]) - async def get_fido_configuration(self) -> None: 'Coroutine for getting fido2 configuration.' try: @@ -253,8 +263,9 @@ def nav_selection_changed( else: self.main_area = self.app.not_implemented - def save_config(self) -> None: + """This method for saving the configuration + """ fido2_config = self.make_data_from_dialog(tabs={'configuration': self.tabs['configuration']}) fido2_static = self.make_data_from_dialog(tabs={'static': self.tabs['static']}) @@ -276,7 +287,6 @@ async def coroutine(): asyncio.ensure_future(coroutine()) - def set_center_frame(self) -> None: """center frame content """ diff --git a/jans-cli-tui/cli_tui/plugins/030_scim/main.py b/jans-cli-tui/cli_tui/plugins/030_scim/main.py index 650d8bcc096..0e01fd02636 100755 --- a/jans-cli-tui/cli_tui/plugins/030_scim/main.py +++ b/jans-cli-tui/cli_tui/plugins/030_scim/main.py @@ -1,21 +1,12 @@ -import os -import sys import asyncio - -from typing import Sequence - - from prompt_toolkit.application import Application from prompt_toolkit.layout.containers import HSplit, VSplit, Window from prompt_toolkit.layout.dimension import D -from prompt_toolkit.widgets import Button, Label, Frame -from prompt_toolkit.formatted_text import HTML +from prompt_toolkit.widgets import Button, Frame from wui_components.jans_drop_down import DropDownWidget - from utils.utils import DialogUtils from utils.multi_lang import _ - class Plugin(DialogUtils): """This is a general class for plugins """ @@ -26,7 +17,7 @@ def __init__( """init for Plugin class "scim" Args: - app (_type_): _description_ + app (Generic): The main Application class """ self.app = app self.pid = 'scim' @@ -37,7 +28,6 @@ def __init__( body=HSplit([Button(text=_("Get Scim Configuration"), handler=self.get_app_config)], width=D()), height=D()) - def process(self) -> None: pass @@ -46,7 +36,6 @@ def set_center_frame(self) -> None: """ self.app.center_container = self.container - def create_widgets(self) -> None: """SCIM Application configuration widgets are created in this fonction """ @@ -87,7 +76,6 @@ def create_widgets(self) -> None: self.app.center_container = self.container - def get_app_config(self) -> None: """Gets SCIM application configurations from server. """ @@ -104,7 +92,6 @@ async def coroutine(): asyncio.ensure_future(coroutine()) - def save_app_config(self) -> None: """Save button handler for saving SCIM application configurations. Once configuration data was obtained from form, patch operations are prepared and saved to server. @@ -119,9 +106,8 @@ def save_app_config(self) -> None: if data[key] and key not in self.app_config: patche_list.append({'op':'add', 'path': key, 'value': data[key]}) - if not patche_list: - self.app.show_message(_("Warning"), _("No changes was done on Scim appilication configuration. Nothing to save.")) + self.app.show_message(_("Warning"), _("No changes was done on Scim appilication configuration. Nothing to save."), tobefocused=self.app.center_container) return async def coroutine(): diff --git a/jans-cli-tui/cli_tui/plugins/040_config_api/main.py b/jans-cli-tui/cli_tui/plugins/040_config_api/main.py index c493a292745..238ae2a4c27 100755 --- a/jans-cli-tui/cli_tui/plugins/040_config_api/main.py +++ b/jans-cli-tui/cli_tui/plugins/040_config_api/main.py @@ -1,35 +1,22 @@ -import os -import sys from prompt_toolkit.application import Application -import threading -import prompt_toolkit - from prompt_toolkit.layout.containers import ( HSplit, VSplit, HorizontalAlign, DynamicContainer, - Window, ) from prompt_toolkit.application.current import get_app from prompt_toolkit.buffer import Buffer - from prompt_toolkit.layout.dimension import D -from prompt_toolkit.widgets import Button, Label, Frame from wui_components.jans_nav_bar import JansNavBar -from prompt_toolkit.layout.containers import HSplit, DynamicContainer, VSplit, Window -from prompt_toolkit.widgets import Button, Label, Frame, Box, Dialog +from prompt_toolkit.widgets import Button, Label, Box, Dialog from wui_components.jans_cli_dialog import JansGDialog from collections import OrderedDict from functools import partial -from typing import Any from wui_components.jans_vetrical_nav import JansVerticalNav from utils.multi_lang import _ from typing import Any, Optional -from utils.utils import DialogUtils -from utils.static import DialogResult import asyncio -from prompt_toolkit.widgets.base import RadioList class Plugin(): """This is a general class for plugins @@ -41,7 +28,7 @@ def __init__( """init for Plugin class "config_api" Args: - app (_type_): _description_ + app (Generic): The main Application class """ self.app = app self.pid = 'config_api' @@ -120,7 +107,7 @@ def create_widgets(self): handler=self.get_adminui_permissions), self.app.getTitledText( - _("Search: "), + _("Search"), name='oauth:scopes:search', jans_help=_("Press enter to perform search"), accept_handler=self.search_adminui_permissions, @@ -147,7 +134,7 @@ def create_widgets(self): handler=self.get_adminui_mapping), self.app.getTitledText( - _("Search: "), + _("Search"), name='oauth:scopes:search', jans_help=_("Press enter to perform search"), accept_handler=self.search_adminui_mapping, @@ -167,10 +154,6 @@ def create_widgets(self): self.nav_selection_changed(list(self.containers)[0]) - #--------------------------------------------------------------------------------# - #----------------------------------- accessroles --------------------------------# - #--------------------------------------------------------------------------------# - def get_adminui_roles(self) -> None: """Method to get the admin ui roles from server """ @@ -191,13 +174,9 @@ async def coroutine(): asyncio.ensure_future(coroutine()) - def adminui_update_roles(self, ) -> None: - """update the current clients data to server - - Args: - pattern (str, optional): endpoint arguments for the client data. Defaults to ''. + """update the current adminui-roles to server """ data =[] @@ -210,10 +189,6 @@ def adminui_update_roles(self, ] ) - # ------------------------------------------------------------------------------- # - # --------------------------------- View Data ----------------------------------- # - # ------------------------------------------------------------------------------- # - if data: clients = JansVerticalNav( myparent=self.app, @@ -239,9 +214,7 @@ def adminui_update_roles(self, self.app.show_message(_("Oops"), _("No matching result"), tobefocused=self.app.center_container) def add_adminui_roles(self) -> None: - """Method to display the dialog of clients - """ - """Method to display the dialog of clients + """Method to display the dialog of adminui-roles """ self.adminui_role = self.app.getTitledText( @@ -260,9 +233,6 @@ def save(dialog: Dialog) -> None: role = self.adminui_role.me.text desc = self.adminui_role_description.me.text - # ------------------------------------------------------------# - # --------------------- Patch to server ----------------------# - # ------------------------------------------------------------# if desc : response = self.app.cli_object.process_command_by_id( operation_id='add-adminui-role' , @@ -273,9 +243,7 @@ def save(dialog: Dialog) -> None: ) else: return - # ------------------------------------------------------------# - # -- get_properties or serach again to see Momentary change --# - # ------------------------------------------------------------# + if response: self.get_adminui_roles() # self.future.set_result(DialogResult.ACCEPT) @@ -289,55 +257,43 @@ def save(dialog: Dialog) -> None: dialog = JansGDialog(self.app, title=_('Add New Role'), body=body, buttons=buttons, width=self.app.dialog_width-20) self.app.show_jans_dialog(dialog) - def edit_adminui_roles(self, **params: Any) -> None: - """Method to display the dialog of clients + """Method to display the dialog of admin-ui roles for editing """ role_data = params.get('data', {}) title = role_data.get('role','') self.adminui_role_description = self.app.getTitledText( - _("Domains"), - name='domains', + _("Description"), + name='description', value=role_data.get('description',''), - height=3, style='class:dialog-titled-widget') self.adminui_role_deletable = self.app.getTitledCheckBox( "Deletable", name='deletable', - checked= False, + checked= role_data.get('deletable', False), jans_help= "Default to False", - style='class:outh-client-checkbox') + style='class:outh-client-checkbox') def save(dialog: Dialog) -> None: desc = self.adminui_role_description.me.text deletable = self.adminui_role_deletable.me.checked - # ------------------------------------------------------------# - # --------------------- Patch to server ----------------------# - # ------------------------------------------------------------# - if desc : - response = self.app.cli_object.process_command_by_id( - operation_id='edit-adminui-role' , - url_suffix='', - endpoint_args='', - data_fn='', - data={'role': '{}'.format(title), 'description': '{}'.format(desc), 'deletable':'{}'.format(deletable)}, - ) - else: - return - # ------------------------------------------------------------# - # -- get_properties or serach again to see Momentary change --# - # ------------------------------------------------------------# - if response: + async def coroutine(): + cli_args = { + 'operation_id': 'edit-adminui-role', + 'data': {'role': '{}'.format(title), 'description': '{}'.format(desc), 'deletable':'{}'.format(deletable)} + } + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() self.get_adminui_roles() - # self.future.set_result(DialogResult.ACCEPT) - return True - - self.app.show_message(_("Error!"), _("An error ocurred while saving role adminui:\n") + str(response.text)) + if response.status_code != 200: + self.app.show_message(_("Error!"), _("An error ocurred while saving role adminui:\n") + str(response.text), tobefocused=self.app.center_container) + asyncio.ensure_future(coroutine()) body = HSplit([self.adminui_role_description,self.adminui_role_deletable]) buttons = [Button(_("Cancel")), Button(_("OK"), handler=save)] @@ -345,6 +301,8 @@ def save(dialog: Dialog) -> None: self.app.show_jans_dialog(dialog) def delete_adminui_roles(self, **kwargs: Any) -> None: + """Method to delete admin-ui roles + """ dialog = self.app.get_confirm_dialog(_("Are you sure want to delete adminui_roles :")+"\n {} ?".format(kwargs['selected'][0])) @@ -368,66 +326,45 @@ async def coroutine(): ## Need to add editable asyncio.ensure_future(coroutine()) - #--------------------------------------------------------------------------------# - #------------------------------------- permissions ------------------------------# - #--------------------------------------------------------------------------------# - - def get_adminui_permissions(self) -> None: - """Method to get the adminui_permissions data from server - """ - self.config_data_container['permissions'] = HSplit([Label(_("Please wait while getting adminui_permissions"),style='class:outh-waitclientdata.label')], width=D(),style='class:outh-waitclientdata') - t = threading.Thread(target=self.adminui_update_permissions, daemon=True) - self.app.start_progressing() - t.start() - - def adminui_update_permissions( - self, - start_index: Optional[int]= 0, - pattern: Optional[str]= '', + def get_adminui_permissions(self, + start_index: Optional[int]=0, + pattern: Optional[str]= '' ) -> None: - """update the current adminui_permissions data to server + """Method to get the adminui_permissions data from server Args: - pattern (str, optional): endpoint arguments for the client data. Defaults to ''. + start_index (Optional[int], optional): This is flag for the adminui-roles pages. Defaults to 0. + pattern (str, optional): endpoint arguments for the client data. Defaults to ''. """ - def get_next( - start_index: int, - pattern: Optional[str]= '', - ) -> None: - self.adminui_update_permissions(start_index, pattern='') - - endpoint_args ='limit:{},startIndex:{}'.format(self.app.entries_per_page, start_index) - - try : - rsponse = self.app.cli_object.process_command_by_id( - operation_id='get-all-adminui-permissions', - url_suffix='', - endpoint_args='', - data_fn=None, - data={} - ) - - except Exception as e: + async def coroutine(): + cli_args = {'operation_id': 'get-all-adminui-permissions'} + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) self.app.stop_progressing() - self.app.show_message(_("Error getting adminui_permissions"), str(e)) - return + self.adminui_permissions_data = response.json() + self.adminui_update_permissions(start_index, pattern) - self.app.stop_progressing() + asyncio.ensure_future(coroutine()) + + def adminui_update_permissions(self, + start_index: Optional[int]=0, + pattern: Optional[str]= '' + ) -> None: + """update the current adminui_permissions data to server - if rsponse.status_code not in (200, 201): - self.app.show_message(_("Error getting adminui_permissions"), str(rsponse.text)) - return + Args:. + start_index (Optional[int], optional): This is flag for the adminui-roles pages. Defaults to 0. + pattern (str, optional): endpoint arguments for the client data. Defaults to ''. + """ - try: - result = rsponse.json() - except Exception: - self.app.show_message(_("Error getting adminui_permissions"), str(rsponse.text)) + if not hasattr(self, 'adminui_permissions_data'): + self.get_adminui_permissions(start_index, pattern) return data =[] if pattern: - for k in result: + for k in self.adminui_permissions_data: if pattern.lower() in k.get('permission').lower(): data.append( [ @@ -436,7 +373,7 @@ def get_next( ] ) else: - for d in result: + for d in self.adminui_permissions_data: data.append( [ d.get('permission'), @@ -446,21 +383,21 @@ def get_next( if data: buttons = [] - if int(len(data)/ 20) >=1 : + if int(len(data)/self.app.entries_per_page) >=1: + if start_index!=0: + handler_partial = partial(self.adminui_update_permissions, start_index-1, pattern) + prev_button = Button(_("Prev"), handler=handler_partial) + prev_button.window.jans_help = _("Displays previous %d entries") % self.app.entries_per_page + buttons.append(prev_button) if start_index< int(len(data)/ 20) : - handler_partial = partial(get_next, start_index+1, pattern) + handler_partial = partial(self.adminui_update_permissions, start_index+1, pattern) next_button = Button(_("Next"), handler=handler_partial) - next_button.window.jans_help = _("Retreives next %d entries") % self.app.entries_per_page + next_button.window.jans_help = _("Displays next %d entries") % self.app.entries_per_page buttons.append(next_button) - if start_index!=0: - handler_partial = partial(get_next, start_index-1, pattern) - prev_button = Button(_("Prev"), handler=handler_partial) - prev_button.window.jans_help = _("Retreives previous %d entries") % self.app.entries_per_page - buttons.append(prev_button) data_now = data[start_index*20:start_index*20+20] - + adminui_permissions = JansVerticalNav( myparent=self.app, headers=['permission', 'defaultPermissionInToken',], @@ -473,20 +410,21 @@ def get_next( selectes=0, headerColor='class:outh-verticalnav-headcolor', entriesColor='class:outh-verticalnav-entriescolor', - all_data=result + all_data=self.adminui_permissions_data ) - self.app.layout.focus(adminui_permissions) + + self.app.layout.focus(adminui_permissions) self.config_data_container['permissions'] = HSplit([ adminui_permissions, VSplit(buttons, padding=5, align=HorizontalAlign.CENTER) ]) - get_app().invalidate() - self.app.layout.focus(adminui_permissions) + self.app.invalidate() + self.app.layout.focus(adminui_permissions) else: - self.app.show_message(_("Oops"), _("No matching result"),tobefocused = self.config_data_container['permissions']) + self.app.show_message(_("Oops"), _("No matching result"), tobefocused=self.app.center_container) def add_adminui_permissions(self) -> None: - """Method to display the dialog of clients + """Method to display the dialog of adminui-roles """ self.adminui_permission = self.app.getTitledText( @@ -532,22 +470,24 @@ async def coroutine(): dialog = JansGDialog(self.app, title=_('Add New Role'), body=body, buttons=buttons, width=self.app.dialog_width-20) self.app.show_jans_dialog(dialog) - def search_adminui_permissions(self, tbuffer:Buffer,) -> None: + def search_adminui_permissions(self, tbuffer:Buffer) -> None: + """This method handel the search for adminui_permissions + + Args: + tbuffer (Buffer): Buffer returned from the TextArea widget > GetTitleText + """ if not len(tbuffer.text) > 2: - self.app.show_message(_("Error!"), _("Search string should be at least three characters"),tobefocused=self.containers['permissions']) + self.app.show_message(_("Error!"), _("Search string should be at least three characters"), tobefocused=self.app.center_container) return - t = threading.Thread(target=self.adminui_update_permissions, args=(0,tbuffer.text), daemon=True) - self.app.start_progressing() - t.start() + self.adminui_update_permissions(0, tbuffer.text) def edit_adminui_permissions(self, **params: Any) -> None: - """Method to display the dialog of clients + """Method to display the dialog of adminui_permissions for editing """ role_data = params.get('passed', []) permission = role_data[0] - defaultPermissionInToken = role_data[1] @@ -587,9 +527,12 @@ def save(dialog: Dialog) -> None: dialog = JansGDialog(self.app, title='admin ui permissions', body=body, buttons=buttons, width=self.app.dialog_width-20) self.app.show_jans_dialog(dialog) - def delete_adminui_permissions(self, **kwargs: Any) -> None: + """This method for the deletion of the adminui_permissions + Returns: + str: The server response + """ dialog = self.app.get_confirm_dialog(_("Are you sure want to delete adminui_permissions :")+"\n {} ?".format(kwargs['selected'][0])) @@ -617,76 +560,34 @@ async def coroutine(): asyncio.ensure_future(coroutine()) - #--------------------------------------------------------------------------------# - #------------------------------------- mapping ----------------------------------# - #--------------------------------------------------------------------------------# - - def get_adminui_mapping(self) -> None: + def get_adminui_mapping(self, pattern: Optional[str]= '') -> None: """Method to get the adminui_permissions data from server - """ - self.config_data_container['mapping'] = HSplit([Label(_("Please wait while getting adminui_permissions"),style='class:outh-waitclientdata.label')], width=D(),style='class:outh-waitclientdata') - t = threading.Thread(target=self.adminui_update_mapping, daemon=True) - self.app.start_progressing() - t.start() - - def adminui_update_mapping( - self, - start_index: Optional[int]= 0, - pattern: Optional[str]= '', - ) -> None: + """ + async def coroutine(): + cli_args = {'operation_id': 'get-all-adminui-role-permissions'} + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + self.adminui_role_permissions_data = response.json() + self.adminui_update_mapping(pattern) + + asyncio.ensure_future(coroutine()) + + def adminui_update_mapping(self, pattern: Optional[str]= '') -> None: """update the current adminui_permissions data to server Args: pattern (str, optional): endpoint arguments for the client data. Defaults to ''. """ - def get_next( - start_index: int, - pattern: Optional[str]= '', - ) -> None: - self.adminui_update_mapping(start_index, pattern='') - - endpoint_args ='limit:{},startIndex:{}'.format(self.app.entries_per_page, start_index) - if pattern: - endpoint_args +=',pattern:'+pattern - try : - rsponse = self.app.cli_object.process_command_by_id( - operation_id='get-all-adminui-role-permissions', - url_suffix='', - endpoint_args='', - data_fn=None, - data={} - ) - - except Exception as e: - self.app.stop_progressing() - self.app.show_message(_("Error getting adminui_permissions"), str(e)) - return - - self.app.stop_progressing() - - if rsponse.status_code not in (200, 201): - self.app.show_message(_("Error getting adminui_permissions"), str(rsponse.text)) - return - - try: - result = rsponse.json() - except Exception: - self.app.show_message(_("Error getting adminui_permissions"), str(rsponse.text)) + if not hasattr(self, 'adminui_role_permissions_data'): + self.get_adminui_mapping(pattern) return data =[] - # for d in result: - # data.append( - # [ - # d.get('role'), - # len(d.get('permissions')), - # ] - # ) - if pattern: - for k in result: + for k in self.adminui_role_permissions_data: if pattern.lower() in k.get('role').lower(): data.append( [ @@ -695,7 +596,7 @@ def get_next( ] ) else: - for d in result: + for d in self.adminui_role_permissions_data: data.append( [ d.get('role'), @@ -703,138 +604,42 @@ def get_next( ] ) - if data: - buttons = [] - if int(len(data)/ 20) >=1 : - - if start_index< int(len(data)/ 20) : - handler_partial = partial(get_next, start_index+1, pattern) - next_button = Button(_("Next"), handler=handler_partial) - next_button.window.jans_help = _("Retreives next %d entries") % self.app.entries_per_page - buttons.append(next_button) - - if start_index!=0: - handler_partial = partial(get_next, start_index-1, pattern) - prev_button = Button(_("Prev"), handler=handler_partial) - prev_button.window.jans_help = _("Retreives previous %d entries") % self.app.entries_per_page - buttons.append(prev_button) - data_now = data[start_index*20:start_index*20+20] - adminui_permissions = JansVerticalNav( myparent=self.app, headers=['role', 'permissions',], preferred_size= [0,0], - data=data_now, + data=data, on_enter=self.edit_adminui_mapping, on_display=self.app.data_display_dialog, # get_help=(self.get_help,'AdminRole'), selectes=0, headerColor='class:outh-verticalnav-headcolor', entriesColor='class:outh-verticalnav-entriescolor', - all_data=result + all_data=self.adminui_role_permissions_data ) self.app.layout.focus(adminui_permissions) # clients.focuse..!? TODO >> DONE - self.config_data_container['mapping'] = HSplit([ - adminui_permissions, - VSplit(buttons, padding=5, align=HorizontalAlign.CENTER) - ]) - get_app().invalidate() - self.app.layout.focus(adminui_permissions) ### it fix focuse on the last item deletion >> try on UMA-res >> edit_client_dialog >> oauth_update_uma_resources - + self.config_data_container['mapping'] = adminui_permissions + self.app.invalidate() + self.app.layout.focus(adminui_permissions) + else: - self.app.show_message(_("Oops"), _("No matching result"),tobefocused = self.config_data_container['mapping']) - - # def add_adminui_mapping(self) -> None: - # try : - # rsponse = self.app.cli_object.process_command_by_id( - # operation_id='get-all-adminui-roles', - # url_suffix='', - # endpoint_args='', - # data_fn=None, - # data={} - # ) - - # except Exception as e: - # self.app.stop_progressing() - # self.app.show_message(_("Error getting clients"), str(e)) - # return - - # values=[] - # for i in rsponse.json(): - # values.append((i['role'],i['role'])) - - # #------------------------------------------------------------------------# - # #- values = [(api-manager,api-manager),(api-admin,api-admin),(api-editor,api-editor),(api-viewer,api-viewer)]# - # #------------------------------------------------------------------------# - - # self.alt_tabs = {} - # self.alt_tabs['api-manager'] = Label(text=_("api-manager"),style='red') - # self.alt_tabs['api-admin'] = Label(text=_("api-admin"),style='red') - # self.alt_tabs['api-editor'] = Label(text=_("api-editor"),style='red') - # self.alt_tabs['api-viewer'] = Label(text=_("api-viewer"),style='red') - # self.alt_tabs['api-editor2'] = Label(text=_("api-editor2"),style='red') - # self.alt_tabs['api-hopa'] = Label(text=_("api-hopa"),style='red') - - # def role_selection_changed( - # cb: RadioList, - # ) -> None: - # self.role_type = cb.current_value - - # self.adminui_mapping= self.app.getTitledRadioButton( - # _("role"), - # name='role', - # values=values, - # on_selection_changed=role_selection_changed, - # style='class:outh-scope-radiobutton') - - - # def save(dialog: Dialog) -> None: - - # permission = self.adminui_permission.me.text - # defaultPermissionInToken = self.adminui_role_permissions.me.checked - - # self.app.logger.debug("defaultPermissionInToken: "+str(defaultPermissionInToken)) - # # ------------------------------------------------------------# - # # --------------------- Patch to server ----------------------# - # # ------------------------------------------------------------# - # if permission : - # response = self.app.cli_object.process_command_by_id( - # operation_id='add-adminui-permission', - # url_suffix='', - # endpoint_args='', - # data_fn='', - # data={'permission': '{}'.format(permission), 'defaultPermissionInToken': '{}'.format(defaultPermissionInToken)}, - # ) - # else: - # return - # # ------------------------------------------------------------# - # # -- get_properties or serach again to see Momentary change --# - # # ------------------------------------------------------------# - # if response: - # self.get_adminui_permissions() - # # self.future.set_result(DialogResult.ACCEPT) - # return True - - # self.app.show_message(_("Error!"), _("An error ocurred while Addin role adminui permission:\n") + str(response.text)) - - - # body = HSplit([self.adminui_mapping,DynamicContainer(lambda: self.alt_tabs[self.role_type])]) - # buttons = [Button(_("Cancel")), Button(_("OK"), handler=save)] - # dialog = JansGDialog(self.app, title=_('Add New Role'), body=body, buttons=buttons, width=self.app.dialog_width-20) - # self.app.show_jans_dialog(dialog) - + self.app.show_message(_("Oops"), _("No matching result"), tobefocused=self.app.center_container) + def search_adminui_mapping(self, tbuffer:Buffer,) -> None: + """This method handel the search for adminui_mapping + + Args: + tbuffer (Buffer): Buffer returned from the TextArea widget > GetTitleText + """ if not len(tbuffer.text) > 2: self.app.show_message(_("Error!"), _("Search string should be at least three characters"),tobefocused=self.containers['mapping']) return - t = threading.Thread(target=self.adminui_update_mapping, args=(0,tbuffer.text), daemon=True) - self.app.start_progressing() - t.start() + self.adminui_update_mapping(tbuffer.text) def edit_adminui_mapping(self, **params: Any) -> None: - """Method to display the dialog of clients + """Method to display the dialog of adminui_mapping """ role_data = params.get('data', []) permission = role_data.get('role') @@ -888,10 +693,6 @@ def save(dialog: Dialog) -> None: dialog = JansGDialog(self.app, title='admin ui permissions', body=body, buttons=buttons, width=self.app.dialog_width-20) self.app.show_jans_dialog(dialog) - #--------------------------------------------------------------------------------# - #--------------------------------------------------------------------------------# - #--------------------------------------------------------------------------------# - def nav_selection_changed( self, selection: str diff --git a/jans-cli-tui/cli_tui/plugins/060_scripts/edit_script_dialog.py b/jans-cli-tui/cli_tui/plugins/060_scripts/edit_script_dialog.py index 9263b71af24..e55b9a35e5b 100755 --- a/jans-cli-tui/cli_tui/plugins/060_scripts/edit_script_dialog.py +++ b/jans-cli-tui/cli_tui/plugins/060_scripts/edit_script_dialog.py @@ -1,19 +1,11 @@ -import re -import threading - -from typing import OrderedDict -from asyncio import ensure_future from functools import partial - from prompt_toolkit.layout.dimension import D from prompt_toolkit.layout.containers import ( HSplit, VSplit, - DynamicContainer, Window ) from prompt_toolkit.widgets import ( - Box, Button, Label, TextArea, @@ -21,38 +13,23 @@ Button, Dialog, ) -from prompt_toolkit.application.current import get_app - +import asyncio from prompt_toolkit.lexers import PygmentsLexer from pygments.lexers.python import PythonLexer from pygments.lexers.jvm import JavaLexer - -from cli import config_cli from utils.static import DialogResult from wui_components.jans_dialog_with_nav import JansDialogWithNav -from wui_components.jans_side_nav_bar import JansSideNavBar from wui_components.jans_cli_dialog import JansGDialog from wui_components.jans_drop_down import DropDownWidget -from wui_components.jans_data_picker import DateSelectWidget from utils.utils import DialogUtils from wui_components.jans_vetrical_nav import JansVerticalNav from wui_components.jans_spinner import Spinner -from prompt_toolkit.layout.containers import ( - AnyContainer, -) from prompt_toolkit.formatted_text import AnyFormattedText -from prompt_toolkit.layout.dimension import AnyDimension -from typing import Optional, Sequence, Union -from typing import TypeVar, Callable - +from typing import Optional, Sequence +from typing import Callable from typing import Any, Optional - - -from view_uma_dialog import ViewUMADialog - from utils.multi_lang import _ - class EditScriptDialog(JansGDialog, DialogUtils): """This Script editing dialog """ @@ -66,8 +43,9 @@ def __init__( )-> Dialog: """init for `EditScriptDialog`, inherits from two diffrent classes `JansGDialog` and `DialogUtils` + JansGDialog (dialog): This is the main dialog Class Widget for all Jans-cli-tui dialogs except custom dialogs like dialogs with navbar DialogUtils (methods): Responsable for all `make data from dialog` and `check required fields` in the form for any Edit or Add New - + Args: parent (widget): This is the parent widget for the dialog, to access `Pageup` and `Pagedown` title (str): The Main dialog title @@ -85,6 +63,8 @@ def __init__( self.script = self.data.get('script','') def save(self) -> None: + """method to invoked when saving the dialog (Save button is pressed) + """ data = {} @@ -133,6 +113,9 @@ def save(self) -> None: self.future.set_result(DialogResult.ACCEPT) def cancel(self) -> None: + """method to invoked when canceling changes in the dialog (Cancel button is pressed) + """ + self.future.set_result(DialogResult.CANCEL) def create_window(self) -> None: @@ -319,27 +302,35 @@ def create_window(self) -> None: width=self.myparent.dialog_width, ) - def get_help(self, **kwargs: Any): + """This method get focused field Description to display on statusbar + """ # schema = self.app.cli_object.get_schema_from_reference('#/components/schemas/{}'.format(str(kwargs['scheme']))) if kwargs['scheme'] == 'Properties': self.myparent.status_bar_text= kwargs['data'][0] - - - def script_lang_changed( self, value: str, ) -> None: + """Change the script lang + + Args: + value (str): lang to change to (python, java) + """ self.cur_lang = value def set_location_widget_state( self, state: bool, ) -> None: + """This method check the state of the location to save script + + Args: + state (bool): state is changed or not + """ self.location_widget.me.read_only = not state self.location_widget.me.focusable = state if not state: @@ -349,10 +340,17 @@ def script_location_changed( self, redio_button: RadioList, ) -> None: + """Location to save Script + + Args: + redio_button (RadioList): Where to save the scripts (Database, Filesystem) + """ state = redio_button.current_value == 'file' self.set_location_widget_state(state) def edit_property(self, **kwargs: Any) -> None: + """This method for editing the properties + """ if kwargs['jans_name'] == 'moduleProperties': key, val = kwargs.get('data', ('','')) @@ -391,12 +389,35 @@ def add_property(dialog: Dialog) -> None: self.myparent.show_jans_dialog(dialog) def delete_config_property(self, **kwargs: Any) -> None: - if kwargs['jans_name'] == 'configurationProperties': - self.config_properties_container.remove_item(kwargs['selected']) - else: - self.module_properties_container.remove_item(kwargs['selected']) + """This method for deleting the coniguration of properties + """ + dialog = self.myparent.get_confirm_dialog(_("Are you sure want to delete property with Key:")+"\n {} ?".format(kwargs['selected'][0])) + + async def coroutine(): + focused_before = self.myparent.layout.current_window + result = await self.myparent.show_dialog_as_float(dialog) + try: + self.myparent.layout.focus(focused_before) + except: + self.myparent.stop_progressing() + self.myparent.layout.focus(self.myparent.center_frame) + + if result.lower() == 'yes': + if kwargs['jans_name'] == 'configurationProperties': + self.config_properties_container.remove_item(kwargs['selected']) + else: + self.module_properties_container.remove_item(kwargs['selected']) + self.myparent.stop_progressing() + + return result + + asyncio.ensure_future(coroutine()) + + def edit_script_dialog(self) -> None: + """This method shows the script itself and let the user view or edit it + """ text_editor = TextArea( text=self.script, @@ -417,5 +438,10 @@ def modify_script(arg) -> None: self.myparent.show_jans_dialog(dialog) def __pt_container__(self)-> Dialog: + """The container for the dialog itself + + Returns: + Dialog: The Edit Script Dialog + """ return self.dialog diff --git a/jans-cli-tui/cli_tui/plugins/060_scripts/main.py b/jans-cli-tui/cli_tui/plugins/060_scripts/main.py index dcc44894f33..a3c8e19d789 100755 --- a/jans-cli-tui/cli_tui/plugins/060_scripts/main.py +++ b/jans-cli-tui/cli_tui/plugins/060_scripts/main.py @@ -1,13 +1,6 @@ -import os -import sys - from functools import partial import asyncio - -import prompt_toolkit from prompt_toolkit.application.current import get_app -from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous from prompt_toolkit.layout.containers import ( HSplit, VSplit, @@ -17,32 +10,16 @@ ) from prompt_toolkit.layout.dimension import D from prompt_toolkit.widgets import ( - Box, Button, - Label, - Frame, Dialog ) from typing import Any, Optional from prompt_toolkit.buffer import Buffer - from utils.static import DialogResult - -from cli import config_cli -from wui_components.jans_nav_bar import JansNavBar -from wui_components.jans_side_nav_bar import JansSideNavBar from wui_components.jans_vetrical_nav import JansVerticalNav -from wui_components.jans_dialog import JansDialog -from wui_components.jans_dialog_with_nav import JansDialogWithNav -from wui_components.jans_drop_down import DropDownWidget -from wui_components.jans_data_picker import DateSelectWidget - from edit_script_dialog import EditScriptDialog from prompt_toolkit.application import Application - from utils.multi_lang import _ -import cli_style - class Plugin(): """This is a general class for plugins @@ -51,10 +28,10 @@ def __init__( self, app: Application ) -> None: - """init for Plugin class "oxauth" + """init for Plugin class "scripts" Args: - app (_type_): _description_ + app (Generic): The main Application class """ self.app = app self.pid = 'scripts' @@ -87,12 +64,17 @@ def scripts_prepare_containers(self) -> None: DynamicContainer(lambda: self.scripts_list_container) ],style='class:outh_containers_scopes') - def get_scripts( self, start_index: Optional[int]= 1, pattern: Optional[str]= '', ) -> None: + """Get the current Scripts from server + + Args: + start_index (Optional[int], optional): This is flag for the Scripts pages. Defaults to 0. + pattern (Optional[str], optional):endpoint arguments for the Scripts. Defaults to ''. + """ endpoint_args ='limit:{},startIndex:{}'.format(self.app.entries_per_page, start_index) if pattern: @@ -116,7 +98,6 @@ async def coroutine(): asyncio.ensure_future(coroutine()) - def scripts_update_list( self, pattern: Optional[str]= '', @@ -124,7 +105,7 @@ def scripts_update_list( """Updates Scripts data from server Args: - start_index (int, optional): add Button("Prev") to the layout. Defaults to 0. + pattern (Optional[str], optional):endpoint arguments for the Scripts. Defaults to ''. """ data =[] @@ -175,17 +156,20 @@ def scripts_update_list( self.app.layout.focus(self.scripts_listbox) get_app().invalidate() - def get_help(self, **kwargs: Any): + """This method get focused field Description to display on statusbar + """ # schema = self.app.cli_object.get_schema_from_reference('#/components/schemas/{}'.format(str(kwargs['scheme']))) - if kwargs['scheme'] == 'Scripts': self.app.status_bar_text= kwargs['data'][2] - - def search_scripts(self, tbuffer:Buffer) -> None: + """This method handel the search for scripts + + Args: + tbuffer (Buffer): Buffer returned from the TextArea widget > GetTitleText + """ if not len(tbuffer.text) > 2: self.app.show_message(_("Error!"), _("Search string should be at least three characters"), tobefocused=self.scripts_main_area) return @@ -212,7 +196,7 @@ def save_script(self, dialog: Dialog) -> None: dialog (_type_): the main dialog to save data in Returns: - _type_: bool value to check the status code response + bool : value to check the status code response """ async def coroutine(): @@ -231,6 +215,8 @@ async def coroutine(): asyncio.ensure_future(coroutine()) def delete_script(self, **kwargs: Any) -> None: + """This method for the deletion of the Scripts + """ def do_delete_script(): diff --git a/jans-cli-tui/cli_tui/plugins/070_users/edit_user_dialog.py b/jans-cli-tui/cli_tui/plugins/070_users/edit_user_dialog.py index 90b31613c96..1c50760bac4 100644 --- a/jans-cli-tui/cli_tui/plugins/070_users/edit_user_dialog.py +++ b/jans-cli-tui/cli_tui/plugins/070_users/edit_user_dialog.py @@ -1,50 +1,30 @@ -import re -import threading - -from typing import OrderedDict, Optional, Sequence, Union, TypeVar, Callable +from typing import Optional, Sequence, Callable import asyncio from functools import partial - - from prompt_toolkit.layout.dimension import D from prompt_toolkit.layout.containers import ( HSplit, VSplit, DynamicContainer, Window, - AnyContainer ) from prompt_toolkit.widgets import ( - Box, Button, Label, - TextArea, - RadioList, CheckboxList, - Button, Dialog, ) from prompt_toolkit.eventloop import get_event_loop - -from cli import config_cli from utils.static import DialogResult from wui_components.jans_dialog_with_nav import JansDialogWithNav -from wui_components.jans_side_nav_bar import JansSideNavBar from wui_components.jans_cli_dialog import JansGDialog -from wui_components.jans_drop_down import DropDownWidget -from wui_components.jans_data_picker import DateSelectWidget from utils.utils import DialogUtils, common_data from wui_components.jans_vetrical_nav import JansVerticalNav from prompt_toolkit.formatted_text import AnyFormattedText - from typing import Any, Optional - - -from view_uma_dialog import ViewUMADialog - +from prompt_toolkit.layout import ScrollablePane from utils.multi_lang import _ - class EditUserDialog(JansGDialog, DialogUtils): """This user editing dialog """ @@ -58,13 +38,14 @@ def __init__( )-> Dialog: """init for `EditUserDialog`, inherits from two diffrent classes `JansGDialog` and `DialogUtils` + JansGDialog (dialog): This is the main dialog Class Widget for all Jans-cli-tui dialogs except custom dialogs like dialogs with navbar DialogUtils (methods): Responsable for all `make data from dialog` and `check required fields` in the form for any Edit or Add New - + Args: parent (widget): This is the parent widget for the dialog, to access `Pageup` and `Pagedown` title (str): The Main dialog title data (list): selected line data - button_functions (list, optional): Dialog main buttons with their handlers. Defaults to []. + buttons (list, optional): Dialog main buttons with their handlers. Defaults to []. save_handler (method, optional): handler invoked when closing the dialog. Defaults to None. """ super().__init__(parent, title, buttons) @@ -77,10 +58,21 @@ def __init__( self.create_window() def cancel(self) -> None: - self.future.set_result(DialogResult.CANCEL) + """method to invoked when canceling changes in the dialog (Cancel button is pressed) + """ + self.future.set_result(DialogResult.CANCEL) def get_claim_properties(self, claim): + """This method for getting claims properties + + Args: + claim (str): Claim + + Returns: + _type_: properties + """ + ret_val = {} for tmp in common_data.users.claims: if tmp['name'] == claim: @@ -171,7 +163,10 @@ def get_custom_attribute(attribute, multi=False): self.app.getTitledCheckBox(_(claim_prop['displayName']), name=ca['name'], checked=checked, style='class:script-checkbox', jans_help=self.app.get_help_from_schema(self.schema, ca['name'])) ) - self.edit_user_container = HSplit(self.edit_user_content, height=D(), width=D()) + self.edit_user_container = ScrollablePane(content=HSplit(self.edit_user_content, width=D()),show_scrollbar=False) + + + self.dialog = JansDialogWithNav( title=self.title, @@ -181,11 +176,12 @@ def get_custom_attribute(attribute, multi=False): width=self.app.dialog_width, ) - def get_admin_ui_roles(self) -> None: + """This method for getting admin ui roles + """ async def coroutine(): cli_args = {'operation_id': 'get-all-adminui-roles'} - self.app.start_progressing() + self.app.start_progressing(_("Retreiving admin UI roles from server...")) response = await get_event_loop().run_in_executor(self.app.executor, self.app.cli_requests, cli_args) self.app.stop_progressing() self.admin_ui_roles = response.json() @@ -193,6 +189,8 @@ async def coroutine(): asyncio.ensure_future(coroutine()) def add_admin_ui_role(self) -> None: + """This method for adding new admin ui roles + """ if not self.admin_ui_roles: self.get_admin_ui_roles() return @@ -216,13 +214,14 @@ def add_role(dialog) -> None: dialog = JansGDialog(self.app, title=_("Select Admin-UI"), body=body, buttons=buttons, width=self.app.dialog_width-20) self.app.show_jans_dialog(dialog) - def delete_admin_ui_role(self, **kwargs: Any) -> None: + """This method for deleting admin ui roles + """ self.admin_ui_roles_container.remove_item(kwargs['selected']) - - def add_claim(self) -> None: + """This method for adding new claim + """ cur_claims = [] for w in self.edit_user_content: if hasattr(w, 'me'): @@ -249,7 +248,8 @@ def add_claim(dialog) -> None: else: widget = self.app.getTitledText(_(display_name), name=claim_, value='', style='class:script-titledtext', jans_help=self.app.get_help_from_schema(self.schema, claim_)) self.edit_user_content.insert(-1, widget) - self.edit_user_container = HSplit(self.edit_user_content, height=D(), width=D()) + self.edit_user_container = ScrollablePane(content=HSplit(self.edit_user_content, width=D()),show_scrollbar=False) + body = HSplit([Label(_("Select claim to be added to current user.")), claims_checkbox]) buttons = [Button(_("Cancel")), Button(_("OK"), handler=add_claim)] @@ -257,5 +257,11 @@ def add_claim(dialog) -> None: self.app.show_jans_dialog(dialog) def __pt_container__(self)-> Dialog: + """The container for the dialog itself + + Returns: + Dialog: The Edit User Dialog + """ + return self.dialog diff --git a/jans-cli-tui/cli_tui/plugins/070_users/main.py b/jans-cli-tui/cli_tui/plugins/070_users/main.py index 796c1c16ff2..2eefaa6434b 100755 --- a/jans-cli-tui/cli_tui/plugins/070_users/main.py +++ b/jans-cli-tui/cli_tui/plugins/070_users/main.py @@ -1,22 +1,14 @@ -import os -import sys import asyncio - from functools import partial from types import SimpleNamespace -from typing import Sequence, Any, Optional - +from typing import Any, Optional from prompt_toolkit.buffer import Buffer from prompt_toolkit.application import Application -from prompt_toolkit.layout.containers import HSplit, VSplit, Window, DynamicContainer, HorizontalAlign +from prompt_toolkit.layout.containers import HSplit, VSplit, DynamicContainer, HorizontalAlign from prompt_toolkit.layout.dimension import D -from prompt_toolkit.widgets import Button, Label, Frame, Dialog -from prompt_toolkit.formatted_text import HTML -from wui_components.jans_drop_down import DropDownWidget -from wui_components.jans_cli_dialog import JansGDialog +from prompt_toolkit.widgets import Button, Dialog from wui_components.jans_vetrical_nav import JansVerticalNav from edit_user_dialog import EditUserDialog - from utils.utils import DialogUtils, common_data from utils.static import DialogResult from utils.multi_lang import _ @@ -33,7 +25,7 @@ def __init__( """init for Plugin class "users" Args: - app (_type_): _description_ + app (Generic): The main Application class """ self.app = app self.pid = 'users' @@ -41,8 +33,6 @@ def __init__( self.users = {} self.widgets_ready = False - - def process(self) -> None: pass @@ -61,7 +51,7 @@ def set_center_frame(self) -> None: self.app.center_container = HSplit([ VSplit([ self.app.getButton(text=_("Get Users"), name='oauth:scopes:get', jans_help=_("Retreive first {} users").format(self.app.entries_per_page), handler=self.get_users), - self.app.getTitledText(_("Search: "), name='oauth:scopes:search', jans_help=_("Press enter to perform search"), accept_handler=self.search_user, style='class:outh_containers_scopes.text'), + self.app.getTitledText(_("Search"), name='oauth:scopes:search', jans_help=_("Press enter to perform search"), accept_handler=self.search_user, style='class:outh_containers_scopes.text'), self.app.getButton(text=_("Add Users"), name='oauth:scopes:add', jans_help=_("To add a new user press this button"), handler=self.edit_user_dialog), ], padding=3, @@ -71,7 +61,6 @@ def set_center_frame(self) -> None: DynamicContainer(lambda: self.nav_buttons), ],style='class:outh_containers_scopes') - def update_user_list_container(self, pattern: Optional[str]='') -> None: """User management list """ @@ -114,7 +103,6 @@ def update_user_list_container(self, pattern: Optional[str]='') -> None: self.app.invalidate() - def get_users(self, start_index: int=1, pattern: Optional[str]='') -> None: """Gets Users from server. """ @@ -125,7 +113,7 @@ def get_users(self, start_index: int=1, pattern: Optional[str]='') -> None: cli_args = {'operation_id': 'get-user', 'endpoint_args': endpoint_args} async def coroutine(): - self.app.start_progressing() + self.app.start_progressing(_("Retreiving users from server...")) response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) self.app.stop_progressing() self.users = response.json() @@ -141,7 +129,6 @@ async def coroutine(): asyncio.ensure_future(coroutine()) - def edit_user_dialog(self, **kwargs: Any) -> None: """Method to display the edit user dialog """ @@ -155,15 +142,16 @@ def edit_user_dialog(self, **kwargs: Any) -> None: edit_user_dialog = EditUserDialog(self.app, title=title, data=data, save_handler=self.save_user) self.app.show_jans_dialog(edit_user_dialog) - def delete_user(self, **kwargs: Any) -> None: + """This method for the deletion of the User + """ def do_delete_user(): for user in self.users['entries']: if user.get('userId') == kwargs['selected'][1]: async def coroutine(): cli_args = {'operation_id': 'delete-user', 'url_suffix':'inum:{}'.format(user['inum'])} - self.app.start_progressing() + self.app.start_progressing(_("Deleting user {}").format(user['userId'])) response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) self.app.stop_progressing() if response: @@ -182,7 +170,6 @@ async def coroutine(): tobefocused=self.user_list_container ) - def save_user(self, dialog: Dialog) -> None: """This method to save user data to server @@ -244,7 +231,7 @@ def save_user(self, dialog: Dialog) -> None: async def coroutine(): operation_id = 'put-user' if dialog.data.get('baseDn') else 'post-user' cli_args = {'operation_id': operation_id, 'data': user_info} - self.app.start_progressing() + self.app.start_progressing(_("Saving user ...")) response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) self.app.stop_progressing() if response.status_code == 500: @@ -255,13 +242,14 @@ async def coroutine(): asyncio.ensure_future(coroutine()) - def get_claims(self) -> None: + """This method for getting claims + """ if hasattr(common_data.users, 'claims'): return async def coroutine(): cli_args = {'operation_id': 'get-attributes', 'endpoint_args':'limit:200,status:active'} - self.app.start_progressing() + self.app.start_progressing(_("Retreiving claims")) response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) self.app.stop_progressing() result = response.json() @@ -270,6 +258,11 @@ async def coroutine(): asyncio.ensure_future(coroutine()) def search_user(self, tbuffer:Buffer) -> None: + """This method handel the search for Users + + Args: + tbuffer (Buffer): Buffer returned from the TextArea widget > GetTitleText + """ if not len(tbuffer.text) > 2: self.app.show_message(_("Error!"), _("Search string should be at least three characters"), tobefocused=self.app.center_container) return diff --git a/jans-cli-tui/cli_tui/plugins/999_jans/main.py b/jans-cli-tui/cli_tui/plugins/999_jans/main.py index fd74ef8009c..5b877812fae 100755 --- a/jans-cli-tui/cli_tui/plugins/999_jans/main.py +++ b/jans-cli-tui/cli_tui/plugins/999_jans/main.py @@ -1,23 +1,11 @@ -import os -import sys import asyncio - -from typing import Sequence - - from prompt_toolkit.application import Application -from prompt_toolkit.layout.containers import HSplit, VSplit, Window, Float +from prompt_toolkit.layout.containers import HSplit from prompt_toolkit.layout.dimension import D -from prompt_toolkit.widgets import Button, Label, Frame -from prompt_toolkit.formatted_text import HTML -from prompt_toolkit.widgets import Shadow -from prompt_toolkit.layout.controls import FormattedTextControl - - +from prompt_toolkit.widgets import Button, Frame from utils.multi_lang import _ from cli import config_cli - class Plugin: """This is a general class for plugins """ @@ -28,7 +16,7 @@ def __init__( """init for Plugin class "Jans CLI Menu" Args: - app (_type_): _description_ + app (Generic): The main Application class """ self.app = app self.pid = 'jans-menu' @@ -45,7 +33,6 @@ def __init__( height=D() ) - def process(self) -> None: pass @@ -55,13 +42,11 @@ def set_center_frame(self) -> None: self.app.center_container = self.menu_container - def exit_cli(self) -> None: """Exits """ self.app.exit(result=False) - def logout_exit_cli(self) -> None: """Removes auth token and exits """ @@ -73,8 +58,8 @@ async def coroutine(): asyncio.ensure_future(coroutine()) - del config_cli.config['DEFAULT']['access_token_enc'] - del config_cli.config['DEFAULT']['user_data'] + config_cli.config['DEFAULT'].pop('access_token_enc', None) + config_cli.config['DEFAULT'].pop('user_data', None) config_cli.write_config() self.exit_cli() diff --git a/jans-cli-tui/cli_tui/version.py b/jans-cli-tui/cli_tui/version.py index 80381fb7ede..9fbd42fb085 100644 --- a/jans-cli-tui/cli_tui/version.py +++ b/jans-cli-tui/cli_tui/version.py @@ -3,4 +3,4 @@ https://www.apache.org/licenses/LICENSE-2.0 """ -__version__ = "1.0.1-dev" +__version__ = "1.0.5-dev" diff --git a/jans-cli-tui/setup.py b/jans-cli-tui/setup.py index a4a97efce48..75cbe389539 100644 --- a/jans-cli-tui/setup.py +++ b/jans-cli-tui/setup.py @@ -62,7 +62,7 @@ def find_version(*file_paths): "ruamel.yaml>=0.16.5", "PyJWT==2.3.0", "pygments", - "prompt_toolkit", + "prompt_toolkit==3.0.33", "requests", "urllib3", "pyDes", diff --git a/jans-cli/.gitignore b/jans-cli/.gitignore deleted file mode 100644 index 14c925f58d3..00000000000 --- a/jans-cli/.gitignore +++ /dev/null @@ -1,134 +0,0 @@ -swagger_yaml.json -config.ini - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ -.idea/ -.idea diff --git a/jans-cli/LICENSE b/jans-cli/LICENSE deleted file mode 100644 index 261eeb9e9f8..00000000000 --- a/jans-cli/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/jans-cli/Makefile b/jans-cli/Makefile deleted file mode 100644 index 5a916486caa..00000000000 --- a/jans-cli/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -.DEFAULT_GOAL := develop - -develop: - pip3 install -e . - -install: - pip3 install . - -uninstall: - pip3 uninstall jans-cli -y - -zipapp: - shiv --compressed -o config-cli.pyz -p '/usr/bin/env python3' -e cli.config_cli:main . --no-cache diff --git a/jans-cli/README.md b/jans-cli/README.md deleted file mode 100644 index fb917ef7a23..00000000000 --- a/jans-cli/README.md +++ /dev/null @@ -1,173 +0,0 @@ -# _Janssen Command Line Interface_ -`jans-cli` is a **Command Line Interface** for Janssen Configuration. It also has `menu-driven` interface that makes it easier to understand how to use [Janssen Server](https://github.com/JanssenProject/home) through the Interactive Mode. - -Table of Contents -================= - - * [Janssen Command Line Interface](#_janssen-command-line-interface_) - * [Installation](#_installation_) - * [Quick Start](#_quick-start_) - -# _Installation_ - -You can directly download the `jans-cli` package file as below: - -### For macOs: - -``` -wget https://github.com/JanssenProject/jans-cli/releases/latest/download/jans-cli-macos-amd64.pyz -``` - -### for linux: - -``` -wget https://github.com/JanssenProject/jans-cli/releases/latest/download/jans-cli-linux-amd64.pyz -``` - -## Build `jans-cli.pyz` manually - -If you would like to build `jans-cli` manually, you can go through the following steps noted here: - -## Prerequisites -1. wget -2. unzip -3. Python 3.6+. -4. Python `pip3` package. - -### Building - -1. Install dependencies - - * On Ubuntu 20 - ```sh - apt install -y wget unzip python3-pip python3-dev - ``` - * On CentOS Stream 8 - ```sh - yum install -y wget unzip python3-pip python3-devel make - pip3 install --upgrade pip - ``` - - Install Shiv - - ```sh - pip3 install shiv - ``` - -2. Download the repository: - - ```sh - wget https://github.com/JanssenProject/jans/archive/refs/heads/main.zip - ``` - -3. Unzip package, and change to directory - - ```sh - unzip main.zip - cd jans-main/jans-cli - ``` - -4. Build - - ```sh - make zipapp - ``` - - You can verify with the following command line if everything is done successfully. - - ``` - python3 config-cli.pyz -h - ``` - -5. Executing config-cli.pyz Remotely - Login your Jans Server. Execute the following command to find **client-id** and **client-secret**: - ```sh - cat /opt/jans/jans-setup/setup.properties.last | grep "role_based_client" - ``` - It will output like this: - ```sh - role_based_client_encoded_pw=+U3XiW2uM/rnidqZ2mv9sw\=\= - role_based_client_id=2000.09b47f56-1b9e-4443-bebd-bdf970406a15 - role_based_client_pw=T68kLUz4YXnR - ``` - **client-id** is the value of **role_based_client_id** and **client-secret** is the value of **role_based_client_pw** - Thus we can execute CLI as: - - ```sh - python3 config-cli.pyz --host demoexmple.gluu.org --client-id 2000.09b47f56-1b9e-4443-bebd-bdf970406a15 --client-secret T68kLUz4YXnR - ``` - - -### Standard Python package -1. Install venv module - ```sh - pip3 install virtualenv - ``` - -2. Create virtual environment and activate: - - ```sh - python3 -m virtualenv .venv - source .venv/bin/activate - ``` - -3. Download and install the package: - - ``` - wget https://github.com/JanssenProject/jans/archive/refs/heads/main.zip - unzip main.zip - cd jans-main/jans-cli - make install - ``` - - This command will install executable called `jans-cli` available in virtual environment `PATH`. - - -![](../docs/assets/image-build-jans-cli-pyz-manually-03042021.png) - - -## Virtual Machine Setup - -**jans-cli** is automatically installed if you choose `jans-config-api` during [Janssen Server](https://github.com/JanssenProject/home/blob/main/development.md#install-janssen-into-vm) Installation on Virtual Machine. - -![](../docs/assets/image-jans-config-api-03042021.png) - -After successfully installed Janssen Server, you will get two command-line arguments as below: - -![](../docs/assets/image-installed-03042021.png) - -# _Quick Start_ - -As you have seen, CLI supports both of the `config-cli` and `scim-cli`. For a quick start, let's run the following command. - -``` -/opt/jans/jans-cli/config-cli.py -``` -If you get an error, you can try in this way: - -``` -python3 /opt/jans/jans-cli/config-cli.py -``` - -Alternatively, you can make python3 to default version: -``` -sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10 -/opt/jans/jans-cli/config-cli.py -``` - -You will get a menu as below image: - -![main-menu.png](../docs/assets/image-im-main-03042021.png) - -From the following list, you can choose any options by selecting its number. For example, let's say number 2, -to get **Default Authentication Method**. - -That returns another two options as below: - -![option-2-option.png](../docs/assets/image-im-default-auth-02-03042021.png) - -Now by selecting 1 it returns our desired result as below image: - -![default-authentication-method.png](../docs/assets/image-im-cur-default-auth-03042021.png) - -So, That was a quick start to view how this _jans-cli_ Interactive Mode works. Please, follow this [link](https://github.com/JanssenProject/jans/blob/main/docs/admin/config-guide/jans-cli/index.md) to read the _jans-cli_ docs for a better understanding of the Janssen Command-Line. diff --git a/jans-cli/cli/__init__.py b/jans-cli/cli/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/jans-cli/cli/config_cli.py b/jans-cli/cli/config_cli.py deleted file mode 100755 index 33da70bd508..00000000000 --- a/jans-cli/cli/config_cli.py +++ /dev/null @@ -1,2401 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import os -import json -import re -import urllib3 -import configparser -import readline -import argparse -import inspect -import random -import datetime -import ruamel.yaml -import importlib -import code -import traceback -import ast -import base64 -import pprint -import copy - -from pathlib import Path -from types import SimpleNamespace -from urllib.parse import urlencode -from collections import OrderedDict - -home_dir = Path.home() -config_dir = home_dir.joinpath('.config') -config_dir.mkdir(parents=True, exist_ok=True) -config_ini_fn = config_dir.joinpath('jans-cli.ini') -cur_dir = os.path.dirname(os.path.realpath(__file__)) -log_dir = os.environ.get('cli_log_dir', cur_dir) -sys.path.append(cur_dir) - -from pylib.tabulate.tabulate import tabulate -try: - import jwt -except ModuleNotFoundError: - from pylib import jwt - -tabulate_endpoints = { - 'jca.get-config-scripts': ['scriptType', 'name', 'enabled', 'inum'], - 'jca.get-user': ['inum', 'userId', 'mail','sn', 'givenName', 'jansStatus'], - 'jca.get-all-attribute': ['inum', 'name', 'displayName', 'status', 'dataType', 'claimName'], - 'jca.get-oauth-openid-clients': ['inum', 'displayName', 'clientName', 'applicationType'], - 'jca.get-oauth-scopes': ['dn', 'id', 'scopeType'], - 'jca.get-oauth-uma-resources': ['dn', 'name', 'expirationDate'], - 'scim.get-users': ['id', 'userName', 'displayName', 'active'] -} - -tabular_dataset = {'scim.get-users': 'Resources'} -excluded_operations = {'scim': ['search-user'], 'jca':[]} - -my_op_mode = 'scim' if 'scim' in os.path.basename(sys.argv[0]) else 'jca' -sys.path.append(os.path.join(cur_dir, my_op_mode)) -swagger_client = importlib.import_module(my_op_mode + '.swagger_client') -swagger_client.models = importlib.import_module(my_op_mode + '.swagger_client.models') -swagger_client.api = importlib.import_module(my_op_mode + '.swagger_client.api') -swagger_client.rest = importlib.import_module(my_op_mode + '.swagger_client.rest') -plugins = [] - -warning_color = 214 -error_color = 196 -success_color = 10 -bold_color = 15 -grey_color = 242 - -clear = lambda: os.system('clear') - -urllib3.disable_warnings() -config = configparser.ConfigParser() - -host = os.environ.get('jans_host') -client_id = os.environ.get(my_op_mode + 'jca_client_id') -client_secret = os.environ.get(my_op_mode + 'jca_client_secret') -access_token = None -debug = os.environ.get('jans_client_debug') - -def encode_decode(s, decode=False): - cmd = '/opt/jans/bin/encode.py ' - if decode: - cmd += '-D ' - result = os.popen(cmd + s + ' 2>/dev/null').read() - return result.strip() - - -# dummy api class to reach private ApiClient methods -class MyApiClient(swagger_client.ApiClient): - pass - - -class DummyPool: - def close(self): - pass - - def join(self): - pass - - -myapi = MyApiClient() -myapi.pool = DummyPool() - -##################### arguments ##################### -op_list = [] - -for api_name in dir(swagger_client.api): - if api_name.endswith('Api') and inspect.isclass(getattr(swagger_client.api, api_name)): - op_list.append(api_name[:-3]) - -parser = argparse.ArgumentParser() -parser.add_argument("--host", help="Hostname of server") -parser.add_argument("--client-id", help="Jans Config Api Client ID") -parser.add_argument("--client-secret", "--client_secret", help="Jans Config Api Client ID secret") -parser.add_argument("--access-token", help="JWT access token or path to file containing JWT access token") -parser.add_argument("--plugins", help="Available plugins separated by comma") -parser.add_argument("-debug", help="Run in debug mode", action='store_true') -parser.add_argument("--operation-id", help="Operation ID to be done") -parser.add_argument("--url-suffix", help="Argument to be added api endpoint url. For example inum:2B29") -parser.add_argument("--info", choices=op_list, help="Help for operation") -parser.add_argument("--op-mode", choices=['get', 'post', 'put', 'patch', 'delete'], default='get', - help="Operation mode to be done") -parser.add_argument("--endpoint-args", - help="Arguments to pass endpoint separated by comma. For example limit:5,status:INACTIVE") -parser.add_argument("--schema", help="Get sample json schema") - -parser.add_argument("-CC", "--config-api-mtls-client-cert", help="Path to SSL Certificate file") -parser.add_argument("-CK", "--config-api-mtls-client-key", help="Path to SSL Key file") -parser.add_argument("--key-password", help="Password for SSL Key file") -parser.add_argument("-noverify", help="Ignore verifying the SSL certificate", action='store_true', default=True) - -parser.add_argument("-use-test-client", help="Use test client without device authorization", action='store_true') - - -parser.add_argument("--patch-add", help="Colon delimited key:value pair for add patch operation. For example loggingLevel:DEBUG") -parser.add_argument("--patch-replace", help="Colon delimited key:value pair for replace patch operation. For example loggingLevel:DEBUG") -parser.add_argument("--patch-remove", help="Key for remove patch operation. For example imgLocation") -parser.add_argument("--no-suggestion", help="Do not use prompt toolkit to display word completer", action='store_true') -parser.add_argument("-log-dir", help="Do not use prompt toolkit to display word completer", default=log_dir) - - -# parser.add_argument("-show-data-type", help="Show data type in schema query", action='store_true') -parser.add_argument("--data", help="Path to json data file") -args = parser.parse_args() - -if args.config_api_mtls_client_cert and args.config_api_mtls_client_key: - excluded_operations['jca'] += [ - 'get-user', 'post-user', 'put-user', 'get-user-by-inum', 'delete-user', 'patch-user-by-inum', - 'get-properties-fido2', 'put-properties-fido2', 'get-registration-entries-fido2', - ] - -if not args.no_suggestion: - from prompt_toolkit import prompt, HTML - from prompt_toolkit.completion import WordCompleter - - -################## end of arguments ################# - -test_client = args.use_test_client - - -if args.plugins: - for plugin in args.plugins.split(','): - plugins.append(plugin.strip()) - -def write_config(): - with open(config_ini_fn, 'w') as w: - config.write(w) - -if not(host and (client_id and client_secret or access_token)): - host = args.host - client_id = args.client_id - client_secret = args.client_secret - debug = args.debug - - access_token = args.access_token - if access_token and os.path.isfile(access_token): - with open(access_token) as f: - access_token = f.read() - - -if not(host and (client_id and client_secret or access_token)): - - if config_ini_fn.exists(): - config.read_string(config_ini_fn.read_text()) - host = config['DEFAULT']['jans_host'] - - if 'jca_test_client_id' in config['DEFAULT'] and test_client: - client_id = config['DEFAULT']['jca_test_client_id'] - secret_key_str = 'jca_test_client_secret' - else: - client_id = config['DEFAULT']['jca_client_id'] - secret_key_str = 'jca_client_secret' - - secret_enc_key_str = secret_key_str + '_enc' - if config['DEFAULT'].get(secret_key_str): - client_secret = config['DEFAULT'][secret_key_str] - elif config['DEFAULT'].get(secret_enc_key_str): - client_secret_enc = config['DEFAULT'][secret_enc_key_str] - client_secret = encode_decode(client_secret_enc, decode=True) - - debug = config['DEFAULT'].get('debug') - log_dir = config['DEFAULT'].get('log_dir', log_dir) - - else: - config['DEFAULT'] = {'jans_host': 'jans server hostname,e.g, jans.foo.net', - 'jca_client_id': 'your jans config api client id', - 'jca_client_secret': 'client secret for your jans config api client', - 'scim_client_id': 'your jans scim client id', - 'scim_client_secret': 'client secret for your jans scim client'} - - write_config() - - print( - "Pelase fill {} or set environmental variables jans_host, jans_client_id ,and jans_client_secret and re-run".format(config_ini_fn) - ) - sys.exit() - - -def get_bool(val): - if str(val).lower() in ('yes', 'true', '1', 'on'): - return True - return False - - -debug = get_bool(debug) - - -class Menu(object): - - def __init__(self, name, method='', info={}, path=''): - self.name = name - self.display_name = name - self.method = method - self.info = info - self.path = path - self.children = [] - self.parent = None - self.ignore = False - - def __iter__(self): - self.current_index = 0 - return self - - def __repr__(self): - return self.display_name - self.__print_child(self) - - def tree(self): - print(self.name) - self.__print_child(self) - - def __get_parent_number(self, child): - n = 0 - while True: - if not child.parent: - break - n += 1 - child = child.parent - - return n - - def __print_child(self, menu): - if menu.children: - for child in menu.children: - print(' ' * self.__get_parent_number(child) * 2, child) - self.__print_child(child) - - def add_child(self, node): - assert isinstance(node, Menu) - node.parent = self - self.children.append(node) - - def get_child(self, n): - if len(self.children) > n: - return self.children[n] - - def __next__(self): - if self.current_index < len(self.children): - retVal = self.children[self.current_index] - self.current_index += 1 - return retVal - - else: - raise StopIteration - - def __contains__(self, child): - for child_ in self.children: - if child_.name == child: - return True - return False - - -class JCA_CLI: - - def __init__(self, host, client_id, client_secret, access_token, test_client=False): - self.host = host - self.client_id = client_id - self.client_secret = client_secret - self.use_test_client = test_client - - self.swagger_configuration = swagger_client.Configuration() - self.swagger_configuration.host = 'https://{}'.format(self.host) - self.access_token = access_token or config['DEFAULT'].get('access_token') - - self.set_user() - self.plugins() - - if not self.access_token and config['DEFAULT'].get('access_token_enc'): - self.access_token = encode_decode(config['DEFAULT']['access_token_enc'], decode=True) - - if my_op_mode == 'scim': - self.swagger_configuration.host += '/jans-scim/restv1/v2' - - self.ssl_settings() - - self.swagger_configuration.debug = debug - if self.swagger_configuration.debug: - self.swagger_configuration.logger_file = os.path.join(log_dir, 'swagger.log') - - self.swagger_yaml_fn = os.path.join(cur_dir, my_op_mode + '.yaml') - - self.cfg_yml = self.get_yaml() - self.make_menu() - self.current_menu = self.menu - self.enums() - - def enums(self): - self.enum_dict = { - "CustomAttribute": { - "properties.name": { - "f": "get_attrib_list" - } - } - } - - def set_user(self): - self.auth_username = None - self.auth_password = None - self.askuser = get_bool(config['DEFAULT'].get('askuser')) - - if self.askuser: - if args.username: - self.auth_username = args.username - if args.password: - self.auth_password = args.password - elif args.j: - if os.path.isfile(args.j): - with open(args.j) as reader: - self.auth_password = reader.read() - else: - print(args.j, "does not exist. Exiting ...") - sys.exit() - if not (self.auth_username and self.auth_password): - print("I need username and password. Exiting ...") - sys.exit() - - def plugins(self): - for plugin_s in config['DEFAULT'].get(my_op_mode + '_plugins', '').split(','): - plugin = plugin_s.strip() - if plugin: - plugins.append(plugin) - - def ssl_settings(self): - if args.noverify: - self.swagger_configuration.verify_ssl = False - else: - self.swagger_configuration.verify_ssl = True - - if args.config_api_mtls_client_cert: - self.swagger_configuration.cert_file = args.config_api_mtls_client_cert - - if args.config_api_mtls_client_key: - self.swagger_configuration.key_file = args.config_api_mtls_client_key - - def drop_to_shell(self, mylocals): - locals_ = locals() - locals_.update(mylocals) - code.interact(local=locals_) - sys.exit() - - def get_yaml(self): - debug_json = 'swagger_yaml.json' - if os.path.exists(debug_json): - with open(debug_json) as f: - return json.load(f, object_pairs_hook=OrderedDict) - - with open(self.swagger_yaml_fn) as f: - self.cfg_yml = ruamel.yaml.load(f.read().replace('\t', ''), ruamel.yaml.RoundTripLoader) - if os.environ.get('dump_yaml'): - with open(debug_json, 'w') as w: - json.dump(self.cfg_yml, w, indent=2) - return self.cfg_yml - - def get_rest_client(self): - rest = swagger_client.rest.RESTClientObject(self.swagger_configuration) - if args.key_password: - rest.pool_manager.connection_pool_kw['key_password'] = args.key_password - return rest - - def check_connection(self): - rest = self.get_rest_client() - headers = urllib3.make_headers(basic_auth='{}:{}'.format(self.client_id, self.client_secret)) - url = 'https://{}/jans-auth/restv1/token'.format(self.host) - headers['Content-Type'] = 'application/x-www-form-urlencoded' - - response = rest.POST( - url, - headers=headers, - post_params={"grant_type": "client_credentials"} - ) - - if response.status != 200: - raise ValueError( - self.colored_text("Unable to connect jans-auth server: {}".format(response.reason), error_color)) - - - def check_access_token(self): - if not self.access_token : - return False - - try: - jwt.decode(self.access_token, - options={ - 'verify_signature': False, - 'verify_exp': True, - 'verify_aud': False - } - ) - return True - except Exception as e: - print(self.colored_text("Unable to validate access token: {}".format(e), error_color)) - self.access_token = None - - return False - - - def guess_param_mapping(self, param_s): - word_list = re.sub( r"([A-Z])", r" \1", param_s).split() - word_list = [w.lower() for w in word_list] - param_name = '_'.join(word_list) - return param_name - - - def make_menu(self): - - menu_groups = [] - - def get_sep_pos(s): - for i, c in enumerate(s): - if c in ('-', '–'): - return i - return -1 - - def get_group_obj(mname): - for grp in menu_groups: - if grp.mname == mname: - return grp - - - for tag in self.cfg_yml['tags']: - tname = tag['name'].strip() - if tname == 'developers': - continue - n = get_sep_pos(tname) - mname = tname[:n].strip() if n > -1 else tname - grp = get_group_obj(mname) - if not grp: - grp = SimpleNamespace() - grp.tag = None if n > -1 else tname - grp.mname = mname - grp.submenu = [] - menu_groups.append(grp) - - if n > -1: - sname = tname[n+1:].strip() - sub = SimpleNamespace() - sub.tag = tname - sub.mname = sname - grp.submenu.append(sub) - - - def get_methods_of_tag(tag): - methods = [] - if tag: - for path_name in self.cfg_yml['paths']: - path = self.cfg_yml['paths'][path_name] - path_parameters = [] - if 'parameters' in path: - for pparam in path['parameters']: - if pparam.get('in') == 'path': - path_parameters.append(dict(pparam)) - - for method_name in path: - method = path[method_name] - - - - if hasattr(method, 'get') and method.get('operationId') in excluded_operations[my_op_mode]: - continue - if 'tags' in method and tag in method['tags'] and 'operationId' in method: - if method.get('x-cli-plugin') and method['x-cli-plugin'] not in plugins: - continue - if path_parameters: - method['__path_parameters__'] = path_parameters - method['__method_name__'] = method_name - method['__path_name__'] = path_name - methods.append(method) - - return methods - - menu = Menu('Main Menu') - - for grp in menu_groups: - methods = get_methods_of_tag(grp.tag) - m = Menu(name=grp.mname) - m.display_name = m.name + ' ˅' - menu.add_child(m) - - for method in methods: - for tag in method['tags']: - menu_name = method.get('summary') or method.get('description') - sm = Menu( - name=menu_name.strip('.'), - method=method['__method_name__'], - info=method, - path=method['__path_name__'], - ) - m.add_child(sm) - - if grp.submenu: - m.display_name = m.name + ' ˅' - for sub in grp.submenu: - methods = get_methods_of_tag(sub.tag) - if not methods: - continue - smenu = Menu(name=sub.mname) - smenu.display_name = smenu.name + ' ˅' - m.add_child(smenu) - - for method in methods: - for tag in method['tags']: - - sub_menu_name = method.get('summary') or method.get('description') - ssm = Menu( - name=sub_menu_name.strip('.'), - method=method['__method_name__'], - info=method, - path=method['__path_name__'], - ) - smenu.add_child(ssm) - - self.menu = menu - - - def get_json_from_response(self, response): - js_data = {} - data = response.data - if data: - try: - js_data = json.loads(data.decode()) - except: - pass - return js_data - - def get_scoped_access_token(self, scope): - sys.stderr.write("Getting access token for scope {}\n".format(scope)) - rest = self.get_rest_client() - headers = urllib3.make_headers(basic_auth='{}:{}'.format(self.client_id, self.client_secret)) - url = 'https://{}/jans-auth/restv1/token'.format(self.host) - headers['Content-Type'] = 'application/x-www-form-urlencoded' - if self.askuser: - post_params = {"grant_type": "password", "scope": scope, "username": self.auth_username, - "password": self.auth_password} - else: - post_params = {"grant_type": "client_credentials", "scope": scope} - - response = rest.POST( - url, - headers=headers, - post_params=post_params - ) - - try: - data = json.loads(response.data) - if 'access_token' in data: - self.swagger_configuration.access_token = data['access_token'] - else: - sys.stderr.write("Error while getting access token") - sys.stderr.write(data) - sys.stderr.write('\n') - except Exception as e: - print("Error while getting access token") - sys.stderr.write(response.data) - sys.stderr.write(e) - sys.stderr.write('\n') - - - def get_jwt_access_token(self): - - rest = self.get_rest_client() - - """ - STEP 1: Get device verification code - This fucntion requests user code from jans-auth, print result and - waits untill verification done. - """ - - headers_basic_auth = urllib3.make_headers(basic_auth='{}:{}'.format(self.client_id, self.client_secret)) - headers_basic_auth['Content-Type'] = 'application/x-www-form-urlencoded' - response = rest.POST( - 'https://{}/jans-auth/restv1/device_authorization'.format(host), - headers=headers_basic_auth, - post_params={ - 'client_id': self.client_id, - 'scope': 'openid+profile+email+offline_access' - } - ) - - if response.status != 200: - raise ValueError( - self.colored_text("Unable to get device authorization user code: {}".format(response.reason), error_color)) - - result = self.get_json_from_response(response.urllib3_response) - - if 'verification_uri' in result and 'user_code' in result: - - print("Please visit verification url {} and enter user code {} in {} secods".format( - self.colored_text(result['verification_uri'], success_color), - self.colored_text(result['user_code'], bold_color), - result['expires_in'] - ) - ) - - input(self.colored_text("Please press «Enter» when ready", warning_color)) - - else: - raise ValueError(self.colored_text("Unable to get device authorization user code")) - - """ - STEP 2: Get access token for retreiving user info - After device code was verified, we use it to retreive refresh token - """ - response = rest.POST( - 'https://{}/jans-auth/restv1/token'.format(host), - headers=headers_basic_auth, - post_params=[ - ('client_id',self.client_id), - ('scope','openid+profile+email+offline_access'), - ('grant_type', 'urn:ietf:params:oauth:grant-type:device_code'), - ('grant_type', 'refresh_token'), - ('device_code',result['device_code']) - ] - ) - - if response.status != 200: - raise ValueError( - self.colored_text("Unable to get access token")) - - result = self.get_json_from_response(response.urllib3_response) - - - """ - STEP 3: Get user info - refresh token is used for retreiving user information to identify user roles - """ - headers_bearer = urllib3.make_headers() - headers_bearer['Content-Type'] = 'application/x-www-form-urlencoded' - headers_bearer['Authorization'] = 'Bearer {}'.format(result['access_token']) - response = rest.POST( - 'https://{}/jans-auth/restv1/userinfo'.format(host), - headers=headers_bearer, - post_params={ - 'access_token': result['access_token'], - }, - ) - - if response.status != 200: - raise ValueError( - self.colored_text("Unable to get access token")) - - result = response.urllib3_response.data.decode() - - """ - STEP 4: Get access token for config-api endpoints - Use client creditentials to retreive access token for client endpoints. - Since introception script will be executed, access token will have permissions with all scopes - """ - response = rest.POST( - 'https://{}/jans-auth/restv1/token'.format(host), - headers=headers_basic_auth, - post_params={ - 'grant_type': 'client_credentials', - 'scope': 'openid', - 'ujwt': result, - }, - ) - - if response.status != 200: - raise ValueError( - self.colored_text("Unable to get access token")) - - result = self.get_json_from_response(response.urllib3_response) - - self.access_token = result['access_token'] - access_token_enc = encode_decode(self.access_token) - config['DEFAULT']['access_token_enc'] = access_token_enc - write_config() - - - def get_access_token(self, scope): - if self.use_test_client: - self.get_scoped_access_token(scope) - else: - if not self.check_access_token(): - self.get_jwt_access_token() - - if not self.use_test_client: - self.swagger_configuration.access_token = self.access_token - - def print_exception(self, e): - error_printed = False - if hasattr(e, 'body'): - try: - jsdata = json.loads(e.body.decode()) - print(self.colored_text(e.body.decode(), error_color)) - error_printed = True - except: - pass - if not error_printed: - print(self.colored_text("Error retreiving data", warning_color)) - print('\u001b[38;5;196m') - if hasattr(e, 'reason'): - print(e.reason) - if hasattr(e, 'body'): - print(e.body) - if hasattr(e, 'args'): - print(', '.join(e.args)) - print('\u001b[0m') - - def colored_text(self, text, color=255): - return u"\u001b[38;5;{}m{}\u001b[0m".format(color, text) - - - def guess_bool(self, val): - if val == '_false': - return False - if val == '_true': - return True - - - def check_type(self, val, vtype): - if vtype == 'string' and val: - return str(val) - elif vtype == 'integer': - if isinstance(val, int): - return val - if val.isnumeric(): - return int(val) - elif vtype == 'object': - try: - retVal = json.loads(val) - if isinstance(retVal, dict): - return retVal - except: - pass - elif vtype == 'boolean': - guessed_val = self.guess_bool(val) - if not guessed_val is None: - return guessed_val - - error_text = "Please enter a(n) {} value".format(vtype) - if vtype == 'boolean': - error_text += ': _true, _false' - - raise TypeError(self.colored_text(error_text, warning_color)) - - def get_input(self, values=[], text='Selection', default=None, itype=None, - help_text=None, sitype=None, enforce='__true__', - example=None, spacing=0 - ): - if 'b' in values and 'q' in values and 'x' in values: - greyed_help_list = [ ('b', 'back'), ('q', 'quit'), ('x', 'logout and quit') ] - for k,v in (('w', 'write result'), ('y', 'yes'), ('n', 'no')): - if k in values: - greyed_help_list.insert(1, (k, v)) - grey_help_text = ', '.join(['{}: {}'.format(k,v) for k,v in greyed_help_list]) - print(self.colored_text(grey_help_text, grey_color)) - print() - type_text = '' - if itype: - if itype == 'array': - type_text = "Type: array of {} separated by _,".format(sitype) - if values: - type_text += ' Valid values: {}'.format(', '.join(values)) - elif itype == 'boolean': - type_text = "Type: " + itype - if default is None: - default = False - else: - type_text = "Type: " + itype - if values: - type_text += ', Valid values: {}'.format(self.colored_text(', '.join(values), bold_color)) - - if help_text: - help_text = help_text.strip('.') + '. ' + type_text - else: - help_text = type_text - - if help_text: - print(' ' * spacing, self.colored_text('«{}»'.format(help_text), 244), sep='') - - if example: - join_char = '_,' if itype == 'array' else ', ' - if isinstance(example, list): - example_str = join_char.join(example) - else: - example_str = str(example) - if join_char == '_,': - example_str = example_str.replace(' ', join_char) - - print(' ' * spacing, self.colored_text('Example: {}'.format(example_str), 244), sep='') - - if not default is None: - default_text = str(default).lower() if itype == 'boolean' else str(default) - text += ' [' + default_text + ']' - if itype == 'integer': - default = int(default) - - if not text.endswith('?'): - text += ':' - - if itype == 'boolean' and not values: - values = ['_true', '_false'] - - while True: - - if args.no_suggestion: - selection = input(' ' * spacing + self.colored_text(text, 20) + ' ') - else: - html_completer = WordCompleter(values) - selection = prompt(HTML(' ' * spacing + text + ' '), completer=html_completer) - - selection = selection.strip() - - if selection == '_b': - self.display_menu(self.current_menu) - break - - if selection.startswith('_file '): - fname = selection.split()[1] - if os.path.isfile(fname): - with open(fname) as f: - selection = f.read().strip() - else: - print(self.colored_text("File {} does not exist".format(fname), warning_color)) - continue - - if itype == 'boolean' and not selection: - return False - - if not selection and default: - return default - - if enforce and not selection: - continue - - if not enforce and not selection: - if itype == 'array': - return [] - return None - - if 'q' in values and selection == 'q': - print("Quiting...") - sys.exit() - - if 'x' in values and selection == 'x': - print("Logging out...") - if 'access_token_enc' in config['DEFAULT']: - config['DEFAULT'].pop('access_token_enc') - write_config() - print("Quiting...") - sys.exit() - break - - - if itype == 'object' and sitype: - try: - object_ = self.check_type(selection, itype) - except Exception as e: - print(' ' * spacing, e, sep='') - continue - - data_ok = True - for items in object_: - try: - self.check_type(object_[items], sitype) - except Exception as e: - print(' ' * spacing, e, sep='') - data_ok = False - if data_ok: - return object_ - else: - continue - - if itype == 'array' and default and not selection: - return default - - if itype == 'array' and sitype: - if selection == '_null': - selection = [] - data_ok = True - else: - selection = selection.split('_,') - for i, item in enumerate(selection): - data_ok = True - try: - selection[i] = self.check_type(item.strip(), sitype) - if selection[i] == '_null': - selection[i] = None - if values: - if not selection[i] in values: - data_ok = False - print(' ' * spacing, self.colored_text( - "Please enter array of {} separated by _,".format(', '.join(values)), - warning_color), sep='') - break - except TypeError as e: - print(' ' * spacing, e, sep='') - data_ok = False - if data_ok: - break - else: - if not itype is None: - try: - selection = self.check_type(selection, itype) - except TypeError as e: - if enforce: - print(' ' * spacing, e, sep='') - continue - - if values: - if selection in values: - break - elif itype == 'boolean': - if isinstance(selection, bool): - break - else: - continue - else: - print(' ' * spacing, - self.colored_text('Please enter one of {}'.format(', '.join(values)), warning_color), - sep='') - - if not values and not selection and not enforce: - break - - if not values and selection: - break - - if selection == '_null': - selection = None - elif selection == '_q': - selection = 'q' - - return selection - - def print_underlined(self, text): - print() - print(text) - print('-' * len(text.splitlines()[-1])) - - def print_colored_output(self, data): - try: - data_json = json.dumps(data, indent=2) - except: - data = ast.literal_eval(str(data)) - data_json = json.dumps(data, indent=2) - - print(self.colored_text(data_json, success_color)) - - def pretty_print(self, data): - pp = pprint.PrettyPrinter(indent=2) - pp_string = pp.pformat(data) - print(self.colored_text(pp_string, success_color)) - - def get_url_param(self, url): - if url.endswith('}'): - pname = re.findall('/\{(.*?)\}$', url)[0] - return pname - - def get_endpiont_url_param(self, endpoint): - param = {} - pname = self.get_url_param(endpoint.path) - if pname: - param = {'name': pname, 'description': pname, 'schema': {'type': 'string'}} - - return param - - def make_swagger_var(self, varname): - word_list = re.sub(r'([A-Z])', r' \1', varname).lower().split() - return '_'.join(word_list) - - def obtain_parameters(self, endpoint, single=False): - parameters = {} - end_point_param = {} - endpoint_parameters = [] - if 'parameters' in endpoint.info: - endpoint_parameters = endpoint.info['parameters'] - - - if '__path_parameters__' in endpoint.info: - end_point_param = endpoint.info['__path_parameters__'][0] - - if end_point_param and not end_point_param in endpoint_parameters: - endpoint_parameters.insert(0, end_point_param) - - n = 1 if single else len(endpoint_parameters) - - for param in endpoint_parameters[0:n]: - param_name = self.make_swagger_var(param['name']) - if not param_name in parameters: - text_ = param['name'] - help_text = param.get('description') or param.get('summary') - enforce = True if param['schema']['type'] == 'integer' or (end_point_param and end_point_param['name'] == param['name']) else False - - parameters[param_name] = self.get_input( - text=text_.strip('.'), - itype=param['schema']['type'], - default=param['schema'].get('default'), - enforce=enforce, - help_text=help_text, - example=param.get('example'), - values=param['schema'].get('enum', []) - ) - - return parameters - - def get_name_from_string(self, txt): - return re.sub(r'[^0-9a-zA-Z\s]+', '', txt) - - def get_api_class_name(self, name): - namle_list = self.get_name_from_string(name).split() - for i, w in enumerate(namle_list[:]): - if len(w) > 1: - w = w[0].upper() + w[1:] - else: - w = w.upper() - - namle_list[i] = w - - return ''.join(namle_list) + 'Api' - - def get_path_by_id(self, operation_id): - retVal = {} - for path in self.cfg_yml['paths']: - for method in self.cfg_yml['paths'][path]: - if 'operationId' in self.cfg_yml['paths'][path][method] and self.cfg_yml['paths'][path][method]['operationId'] == operation_id: - retVal = self.cfg_yml['paths'][path][method].copy() - retVal['__path__'] = path - retVal['__method__'] = method - retVal['__urlsuffix__'] = self.get_url_param(path) - - return retVal - - def get_tag_from_api_name(self, api_name, qmethod=None): - - for tag in self.cfg_yml['tags']: - api_class_name = self.get_api_class_name(tag['name']) - if api_class_name == api_name: - break - - paths = [] - - for path in self.cfg_yml['paths']: - - for method in self.cfg_yml['paths'][path]: - - if 'tags' in self.cfg_yml['paths'][path][method] and tag['name'] in self.cfg_yml['paths'][path][method]['tags'] and 'operationId' in self.cfg_yml['paths'][path][method]: - retVal = self.cfg_yml['paths'][path][method].copy() - retVal['__path__'] = path - retVal['__method__'] = method - retVal['__urlsuffix__'] = self.get_url_param(path) - if qmethod: - if method == qmethod: - paths.append(retVal) - else: - paths.append(retVal) - - return paths - - def get_scope_for_endpoint(self, endpoint): - scope = [] - for security in endpoint.info.get('security', []): - for stype in security: - scope += security[stype] - - return ' '.join(scope) - - def unmap_model(self, model, data_dict=None): - if data_dict is None: - data_dict = {} - - for key_ in model.attribute_map: - - val = getattr(model, key_) - if isinstance(val, datetime.date): - val = str(val) - - if isinstance(val, list): - sub_list = [] - for entry in val: - if hasattr(entry, 'swagger_types'): - sub_list_dict = {} - self.unmap_model(entry, sub_list_dict) - sub_list.append(sub_list_dict) - else: - sub_list.append(entry) - data_dict[model.attribute_map[key_]] = sub_list - elif hasattr(val, 'swagger_types'): - sub_data_dict = {} - self.unmap_model(val, sub_data_dict) - data_dict[model.attribute_map[key_]] = sub_data_dict - else: - data_dict[model.attribute_map[key_]] = val - - return data_dict - - def get_model_key_map(self, model, key): - key_underscore = key.replace('-', '_') - for key_ in model.attribute_map: - if model.attribute_map[key_] == key or model.attribute_map[key_] == key_underscore: - return key_ - - def tabular_data(self, data, ome): - tab_data = [] - headers = tabulate_endpoints[ome] - for i, entry in enumerate(data): - row_ = [i + 1] - for header in headers: - row_.append(str(entry.get(header, ''))) - tab_data.append(row_) - - print(tabulate(tab_data, headers, tablefmt="grid")) - - def process_get(self, endpoint, return_value=False, parameters=None): - clear() - if not return_value: - title = endpoint.name - if endpoint.name != endpoint.info['description'].strip('.'): - title += '\n' + endpoint.info['description'] - - self.print_underlined(title) - - if not parameters: - parameters = self.obtain_parameters(endpoint, single=return_value) - - for param in parameters.copy(): - if not parameters[param]: - del parameters[param] - - if parameters and not return_value: - print("Calling Api with parameters:", parameters) - - print("Please wait while retreiving data ...\n") - - api_caller = self.get_api_caller(endpoint) - - api_response = None - raise_error = False - try: - api_response = api_caller(**parameters) - except Exception as e: - if return_value: - raise_error = True - else: - self.print_exception(e) - - if raise_error: - raise ValueError('Not found') - - if return_value: - if api_response: - return api_response - return False - - selections = ['q', 'x', 'b'] - item_counters = [] - tabulated = False - - if api_response: - try: - if 'response' in api_response: - api_response = api_response['response'] - except: - pass - - selections.append('w') - api_response_unmapped = [] - if isinstance(api_response, list): - for model in api_response: - data_dict = self.unmap_model(model) - api_response_unmapped.append(data_dict) - elif isinstance(api_response, dict): - api_response_unmapped = api_response - else: - data_dict = self.unmap_model(api_response) - api_response_unmapped = data_dict - - op_mode_endpoint = my_op_mode + '.' + endpoint.info['operationId'] - - if op_mode_endpoint in tabulate_endpoints: - api_response_unmapped_ext = copy.deepcopy(api_response_unmapped) - if 'entries' in api_response_unmapped_ext: - api_response_unmapped_ext = api_response_unmapped_ext['entries'] - - if endpoint.info['operationId'] == 'get-user': - for entry in api_response_unmapped_ext: - if entry.get('customAttributes'): - for attrib in entry['customAttributes']: - if attrib['name'] == 'mail': - entry['mail'] = ', '.join(attrib['values']) - elif attrib['name'] in tabulate_endpoints[op_mode_endpoint]: - entry[attrib['name']] = attrib['values'][0] - - if endpoint.info['operationId'] == 'get-oauth-openid-clients': - for entry in api_response_unmapped_ext: - for custom_attrib in entry.get('customAttributes', []): - if custom_attrib.get('name') == 'displayName': - entry['displayName'] = custom_attrib.get('value') or custom_attrib.get('values',['?'])[0] - break - if isinstance(entry['clientName'], dict) and 'value' in entry['clientName']: - entry['clientName'] = entry['clientName']['value'] - - tab_data = api_response_unmapped_ext - if op_mode_endpoint in tabular_dataset: - tab_data = api_response_unmapped_ext[tabular_dataset[op_mode_endpoint]] - self.tabular_data(tab_data, op_mode_endpoint) - item_counters = [str(i + 1) for i in range(len(tab_data))] - tabulated = True - else: - self.print_colored_output(api_response_unmapped) - - selections += item_counters - while True: - selection = self.get_input(selections) - if selection == 'b': - self.display_menu(endpoint.parent) - break - elif selection == 'w': - fn = input('File name: ') - try: - with open(fn, 'w') as w: - json.dump(api_response_unmapped, w, indent=2) - print("Output was written to", fn) - except Exception as e: - print("An error ocurred while saving data") - self.print_exception(e) - elif selection in item_counters: - if my_op_mode == 'scim' and 'Resources' in api_response_unmapped: - items = api_response_unmapped['Resources'] - elif my_op_mode == 'jca' and 'entries' in api_response_unmapped: - items = api_response_unmapped['entries'] - self.pretty_print(items[int(selection) - 1]) - - def get_schema_from_reference(self, ref): - schema_path_list = ref.strip('/#').split('/') - schema = self.cfg_yml[schema_path_list[0]] - - schema_ = schema.copy() - - for p in schema_path_list[1:]: - schema_ = schema_[p] - - if 'allOf' in schema_: - all_schema = OrderedDict() - all_schema['required'] = [] - - all_schema['properties'] = OrderedDict() - for sch in schema_['allOf']: - if '$ref' in sch: - all_schema.update(self.get_schema_from_reference(sch['$ref'])) - elif 'properties' in sch: - for sprop in sch['properties']: - all_schema['properties'][sprop] = sch['properties'][sprop] - all_schema['required'] += sch.get('required', []) - - schema_ = all_schema - - for key_ in schema_.get('properties', []): - if '$ref' in schema_['properties'][key_]: - schema_['properties'][key_] = self.get_schema_from_reference(schema_['properties'][key_]['$ref']) - elif schema_['properties'][key_].get('type') == 'array' and '$ref' in schema_['properties'][key_]['items']: - ref_path = schema_['properties'][key_]['items'].pop('$ref') - ref_schema = self.get_schema_from_reference(ref_path) - schema_['properties'][key_]['properties'] = ref_schema['properties'] - schema_['properties'][key_]['title'] = ref_schema['title'] - schema_['properties'][key_]['description'] = ref_schema.get('description', '') - schema_['properties'][key_]['__schema_name__'] = ref_schema['__schema_name__'] - - if not 'title' in schema_: - schema_['title'] = p - - schema_['__schema_name__'] = p - - return schema_ - - def get_scheme_for_endpoint(self, endpoint): - schema_ = {} - for content_type in endpoint.info.get('requestBody', {}).get('content', {}): - if 'schema' in endpoint.info['requestBody']['content'][content_type]: - schema = endpoint.info['requestBody']['content'][content_type]['schema'] - break - else: - return schema_ - - schema_ = schema.copy() - - if schema_.get('type') == 'array': - schma_ref = schema_.get('items', {}).pop('$ref') - else: - schma_ref = schema_.pop('$ref') - - if schma_ref: - schema_ref_ = self.get_schema_from_reference(schma_ref) - schema_.update(schema_ref_) - - return schema_ - - def get_swagger_types(self, model, name): - for attribute in model.swagger_types: - if model.swagger_types[attribute] == name: - return attribute - - def get_attrib_list(self): - for parent in self.menu: - for children in parent: - if children.info.get('operationId') == 'get-attributes': - attributes = self.process_get(children, return_value=True, parameters={'limit': 1000} ) - attrib_names = [] - for a in attributes: - attrib_names.append(a.name) - attrib_names.sort() - return attrib_names - - def get_enum(self, schema): - if schema['__schema_name__'] in self.enum_dict: - enum_obj = schema - - for path in self.enum_dict[schema['__schema_name__']].copy(): - for p in path.split('.'): - enum_obj = enum_obj[p] - - if not 'enum' in self.enum_dict[schema['__schema_name__']][path]: - self.enum_dict[schema['__schema_name__']][path]['enum'] = getattr(self, self.enum_dict[schema['__schema_name__']][path]['f'])() - - enum_obj['enum'] = self.enum_dict[schema['__schema_name__']][path]['enum'] - - - def get_input_for_schema_(self, schema, model, spacing=0, initialised=False, getitem=None, required_only=False): - - self.get_enum(schema) - data = {} - for prop in schema['properties']: - item = schema['properties'][prop] - if getitem and prop != getitem['__name__'] or prop in ('dn', 'inum'): - continue - - if required_only and not prop in schema.get('required', []): - continue - - prop_ = self.get_model_key_map(model, prop) - if item['type'] == 'object' and 'properties' in item: - print() - print("Data for object {}. {}".format(prop, item.get('description', ''))) - - model_name_str = item.get('__schema_name__') or item.get('title') or item.get('description') - model_name = self.get_name_from_string(model_name_str) - - if initialised and getattr(model, prop_): - sub_model = getattr(model, prop_) - self.get_input_for_schema_(item, sub_model, spacing=3, initialised=initialised) - elif isinstance(model, type) and hasattr(swagger_client.models, model_name): - sub_model_class = getattr(swagger_client.models, model_name) - result = self.get_input_for_schema_(item, sub_model_class, spacing=3, initialised=initialised) - setattr(model, prop_, result) - elif hasattr(swagger_client.models, model.swagger_types[prop_]): - sub_model = getattr(swagger_client.models, model.swagger_types[prop_]) - result = self.get_input_for_schema_(item, sub_model, spacing=3, initialised=initialised) - setattr(model, prop_, result) - else: - sub_model = getattr(model, prop_) - self.get_input_for_schema_(item, sub_model, spacing=3, initialised=initialised) - # print(self.colored_text("Fix me: can't find model", error_color)) - - elif item['type'] == 'array' and '__schema_name__' in item: - model_name = item['__schema_name__'] - sub_model_class = getattr(swagger_client.models, model_name) - sub_model_list = [] - sub_model_list_help_text = '' - sub_model_list_title_text = item.get('title') - if sub_model_list_title_text: - sub_model_list_help_text = item.get('description') - else: - sub_model_list_title_text = item.get('description') - - cur_model_data = getattr(model, prop_) - - if cur_model_data and initialised: - for cur_data in cur_model_data: - print("\nUpdate {}".format(sub_model_list_title_text)) - cur_model_data = self.get_input_for_schema_(item, cur_data, spacing=spacing + 3) - sub_model_list.append(cur_model_data) - - sub_model_list_selection = self.get_input(text="Add {}?".format(sub_model_list_title_text), - values=['y', 'n'], help_text=sub_model_list_help_text) - - if sub_model_list_selection == 'y': - while True: - sub_model_list_data = self.get_input_for_schema_(item, sub_model_class, spacing=spacing + 3) - sub_model_list.append(sub_model_list_data) - sub_model_list_selection = self.get_input( - text="Add another {}?".format(sub_model_list_title_text), values=['y', 'n']) - if sub_model_list_selection == 'n': - break - - data[prop_] = sub_model_list - - else: - default = getattr(model, prop_) - if isinstance(default, property): - default = None - enforce = True if item['type'] == 'boolean' else False - - if prop in schema.get('required', []): - enforce = True - - if not default: - default = item.get('default') - - values_ = item.get('enum', []) - if not values_ and item['type'] == 'array' and 'enum' in item['items']: - values_ = item['items']['enum'] - if item['type'] == 'object' and not default: - default = {} - - if not values_: - values_ = [] - - val = self.get_input( - values=values_, - text=prop, - default=default, - itype=item['type'], - help_text=item.get('description'), - sitype=item.get('items', {}).get('type'), - enforce=enforce, - example=item.get('example'), - spacing=spacing - ) - data[prop_] = val - - if model.__class__.__name__ == 'type': - modelObject = model(**data) - for key_ in data: - if data[key_] and not getattr(modelObject, key_, None): - setattr(modelObject, key_, data[key_]) - return modelObject - else: - for key_ in data: - setattr(model, key_, data[key_]) - - return model - - def get_api_caller(self, endpoint): - security = self.get_scope_for_endpoint(endpoint) - if security.strip(): - self.get_access_token(security) - - client = getattr(swagger_client, self.get_api_class_name(endpoint.info['tags'][0])) - api_instance = self.get_api_instance(client) - api_caller = getattr(api_instance, endpoint.info['operationId'].replace('-', '_')) - - return api_caller - - def process_post(self, endpoint): - schema = self.get_scheme_for_endpoint(endpoint) - - if schema: - - title = schema.get('description') or schema['title'] - data_dict = {} - - model_class = getattr(swagger_client.models, schema['__schema_name__']) - - if my_op_mode == 'scim': - if endpoint.path == '/jans-scim/restv1/v2/Groups': - schema['properties']['schemas']['default'] = ['urn:ietf:params:scim:schemas:core:2.0:Group'] - elif endpoint.path == '/jans-scim/restv1/v2/Users': - schema['properties']['schemas']['default'] = ['urn:ietf:params:scim:schemas:core:2.0:User'] - if endpoint.info['operationId'] == 'create-user': - schema['required'] = ['userName', 'name', 'displayName', 'emails', 'password'] - - model = self.get_input_for_schema_(schema, model_class, required_only=True) - - optional_fields = [] - required_fields = schema.get('required', []) + ['dn', 'inum'] - for field in schema['properties']: - if not field in required_fields: - optional_fields.append(field) - - optional_fields.sort() - if optional_fields: - fill_optional = self.get_input(values=['y', 'n'], text='Populate optional fields?') - fields_numbers = [] - if fill_optional == 'y': - print("Optional Fields:") - for i, field in enumerate(optional_fields): - print(i + 1, field) - fields_numbers.append(str(i + 1)) - - while True: - optional_selection = self.get_input(values=['q', 'x', 'c'] + fields_numbers, - help_text="c: continue, #: populate field") - if optional_selection == 'c': - break - if optional_selection in fields_numbers: - item_name = optional_fields[int(optional_selection) - 1] - schema_item = schema['properties'][item_name].copy() - schema_item['__name__'] = item_name - self.get_input_for_schema_(schema, model, initialised=True, getitem=schema_item) - - print("Obtained Data:\n") - model_unmapped = self.unmap_model(model) - self.print_colored_output(model_unmapped) - - selection = self.get_input(values=['q', 'x', 'b', 'y', 'n'], text='Continue?') - - else: - selection = 'y' - model = None - - - path_vals = {} - if '__path_parameters__' in endpoint.info: - for pparam in endpoint.info['__path_parameters__']: - swagger_var = self.make_swagger_var(pparam['name']) - path_vals[swagger_var] = self.get_input( - values=pparam['schema'].get('enum', []), - text=pparam['name'], - itype=pparam['schema']['type'], - help_text= pparam.get('description'), - enforce='__true__', - ) - - - if selection == 'y': - api_caller = self.get_api_caller(endpoint) - print("Please wait while posting data ...\n") - - try: - if model: - if path_vals: - api_response = api_caller(**path_vals, body=model) - else: - api_response = api_caller(body=model) - - else: - api_response = api_caller(**path_vals) - - except Exception as e: - api_response = None - self.print_exception(e) - - if api_response: - try: - api_response_unmapped = self.unmap_model(api_response) - self.print_colored_output(api_response_unmapped) - except: - print(self.colored_text(str(api_response), success_color)) - - selection = self.get_input(values=['q', 'x', 'b']) - if selection in ('b', 'n'): - self.display_menu(endpoint.parent) - - def process_delete(self, endpoint): - url_param = self.get_endpiont_url_param(endpoint) - if url_param: - url_param_val = self.get_input(text=url_param['name'], help_text='Entry to be deleted') - else: - url_param_val = '' - selection = self.get_input(text="Are you sure want to delete {} ?".format(url_param_val), - values=['b', 'y', 'n', 'q', 'x']) - if selection in ('b', 'n'): - self.display_menu(endpoint.parent) - elif selection == 'y': - api_caller = self.get_api_caller(endpoint) - print("Please wait while deleting {} ...\n".format(url_param_val)) - api_response = '__result__' - - try: - api_response = api_caller(url_param_val) if url_param_val else api_caller() - except Exception as e: - self.print_exception(e) - - if api_response is None: - print(self.colored_text("\nEntry {} was deleted successfully\n".format(url_param_val), success_color)) - - selection = self.get_input(['b', 'q', 'x']) - if selection == 'b': - self.display_menu(endpoint.parent) - - def process_patch(self, endpoint): - if endpoint.info['operationId'] == 'patch-user-by-inum': - schema = self.cfg_yml['components']['schemas']['CustomAttribute'].copy() - schema['__schema_name__'] = 'CustomAttribute' - model = getattr(swagger_client.models, 'CustomAttribute') - elif 'PatchOperation' in self.cfg_yml['components']['schemas']: - schema = self.cfg_yml['components']['schemas']['PatchOperation'].copy() - model = getattr(swagger_client.models, 'PatchOperation') - for item in schema['properties']: - if not 'type' in schema['properties'][item]: - schema['properties'][item]['type'] = 'string' - schema['__schema_name__'] = 'PatchOperation' - else: - schema = self.cfg_yml['components']['schemas']['PatchRequest'].copy() - schema['__schema_name__'] = 'PatchRequest' - model = getattr(swagger_client.models, 'PatchRequest') - - parent_schema = {} - - schema_ref = endpoint.info.get('responses', {}).get('200', {}).get('content', {}).get('application/json', {}).get('schema', {}).get('$ref') - if schema_ref: - parent_schema = self.get_schema_from_reference(schema_ref) - - url_param_val = None - url_param = self.get_endpiont_url_param(endpoint) - if 'name' in url_param: - url_param_val = self.get_input(text=url_param['name'], help_text='Entry to be patched') - body = [] - - if endpoint.info['operationId'] == 'patch-user-by-inum': - patch_op = self.get_input(text="Patch operation", values=['add', 'remove', 'replace'], help_text='The operation to be performed') - - while True: - data = self.get_input_for_schema_(schema, model) - if endpoint.info['operationId'] != 'patch-user-by-inum': - guessed_val = self.guess_bool(data.value) - if not guessed_val is None: - data.value = guessed_val - if my_op_mode != 'scim' and not data.path.startswith('/'): - data.path = '/' + data.path - - if my_op_mode == 'scim': - data.path = data.path.replace('/', '.') - - if parent_schema and 'properties' in parent_schema: - for prop_ in parent_schema['properties']: - if data.path.lstrip('/') == prop_: - if parent_schema['properties'][prop_]['type'] == 'array': - data.value = data.value.split('_,') - body.append(data) - selection = self.get_input(text='Another patch operation?', values=['y', 'n']) - if selection == 'n': - break - - unmapped_body = [] - for item in body: - unmapped_body.append(self.unmap_model(item)) - - self.print_colored_output(unmapped_body) - - selection = self.get_input(values=['y', 'n'], text='Continue?') - - if selection == 'y': - - api_caller = self.get_api_caller(endpoint) - - print("Please wait patching...\n") - - if my_op_mode == 'scim': - body = {'schemas': ['urn:ietf:params:scim:api:messages:2.0:PatchOp'], 'Operations': body} - elif endpoint.info['operationId'] == 'patch-user-by-inum': - patch_data = {'jsonPatchString': json.dumps([{'op': patch_op, 'path': '/dn', 'value': 'inum={},ou=people,o=jans'.format(url_param_val)}]), 'customAttributes':unmapped_body} - body = patch_data - try: - if url_param_val: - param_mapping = self.guess_param_mapping(url_param['name']) - payload = {param_mapping: url_param_val, 'body': body} - api_response = api_caller(**payload) - else: - api_response = api_caller(body=body) - except Exception as e: - api_response = None - self.print_exception(e) - - if api_response: - api_response_unmapped = self.unmap_model(api_response) - self.print_colored_output(api_response_unmapped) - - selection = self.get_input(['b']) - if selection == 'b': - self.display_menu(endpoint.parent) - - def process_put(self, endpoint): - - schema = self.get_scheme_for_endpoint(endpoint) - - initialised = False - cur_model = None - go_back = False - key_name = None - parent_model = None - - if endpoint.info.get('x-cli-getdata') != '_file': - if 'x-cli-getdata' in endpoint.info and endpoint.info['x-cli-getdata'] != None: - for m in endpoint.parent: - if m.info['operationId'] == endpoint.info['x-cli-getdata']: - while True: - try: - cur_model = self.process_get(m, return_value=True) - break - except ValueError as e: - print(self.colored_text("Server returned no data", error_color)) - retry = self.get_input(values=['y', 'n'], text='Retry?') - if retry == 'n': - self.display_menu(endpoint.parent) - break - initialised = True - get_endpoint = m - break - - else: - if endpoint.info['operationId'] == 'put-properties-fido2': - for m in endpoint.parent: - if m.method == 'get': - break - cur_model = self.process_get(m, return_value=True) - initialised = True - get_endpoint = m - - else: - for m in endpoint.parent: - if m.method == 'get' and m.path.endswith('}'): - while True: - while True: - try: - key_name_desc = self.get_endpiont_url_param(m) - if key_name_desc and 'name' in key_name_desc: - key_name = key_name_desc['name'] - cur_model = self.process_get(m, return_value=True) - break - except ValueError as e: - print(self.colored_text("Server returned no data", error_color)) - retry = self.get_input(values=['y', 'n'], text='Retry?') - if retry == 'n': - self.display_menu(endpoint.parent) - break - - if not cur_model is False: - break - - initialised = True - get_endpoint = m - break - - if not cur_model: - for m in endpoint.parent: - if m.method == 'get' and not m.path.endswith('}'): - cur_model = self.process_get(m, return_value=True) - get_endpoint = m - - - if not cur_model: - cur_model = getattr(swagger_client.models, schema['__schema_name__']) - - end_point_param = self.get_endpiont_url_param(endpoint) - - - if cur_model: - - if endpoint.info.get('x-cli-getdata') == '_file': - - schema_desc = schema.get('description') or schema['__schema_name__'] - text = 'Enter filename to load data for «{}»: '.format(schema_desc) - data_fn = input(self.colored_text(text, 244)) - if data_fn == 'b': - go_back = True - elif data_fn == 'q': - sys.exit() - else: - data_org = self.get_json_from_file(data_fn) - - data = {} - for k in data_org: - if k in cur_model.attribute_map: - mapped_key = cur_model.attribute_map[k] - data[mapped_key] = data_org[k] - else: - data[k] = data_org[k] - - api_caller = self.get_api_caller(endpoint) - - print("Please wait while posting data ...\n") - - try: - api_response = api_caller(body=data) - except Exception as e: - api_response = None - self.print_exception(e) - - if api_response: - api_response_unmapped = self.unmap_model(api_response) - self.print_colored_output(api_response_unmapped) - - selection = self.get_input(values=['q', 'x', 'b']) - if selection == 'b': - self.display_menu(endpoint.parent) - - - else: - - end_point_param_val = None - if end_point_param: - end_point_param_val = getattr(cur_model, end_point_param['name'], None) or self.get_model_key_map(cur_model, end_point_param['name']) - - attr_name_list = [] - for attr_name in cur_model.attribute_map: - if attr_name != 'dn': - attr_name_list.append(cur_model.attribute_map[attr_name]) - - attr_name_list.sort() - item_numbers = [] - - def print_fields(): - print("Fields:") - for i, attr_name in enumerate(attr_name_list): - print(str(i + 1).rjust(2), attr_name) - item_numbers.append(str(i + 1)) - - print_fields() - changed_items = [] - selection_list = ['q', 'x', 'b', 'v', 's', 'l'] + item_numbers - help_text = 'q: quit, v: view, s: save, l: list fields #: update field' - - while True: - selection = self.get_input(values=selection_list, help_text=help_text) - if selection == 'v': - self.pretty_print(self.unmap_model(cur_model)) - elif selection == 'l': - print_fields() - elif selection in item_numbers: - item = attr_name_list[int(selection) - 1] - item_unmapped = self.get_model_key_map(cur_model, item) - if schema['properties'].get('keys', {}).get('properties'): - schema = schema['properties']['keys'] - - schema_item = schema['properties'][item] - schema_item['__name__'] = item - self.get_input_for_schema_(schema, cur_model, initialised=initialised, getitem=schema_item) - changed_items.append(item) - - if selection == 'b': - self.display_menu(endpoint.parent) - break - elif selection == 's': - print('Changes:') - for ci in changed_items: - model_key = self.get_model_key_map(cur_model, ci) - str_val = str(getattr(cur_model, model_key)) - print(self.colored_text(ci, bold_color) + ':', self.colored_text(str_val, success_color)) - - selection = self.get_input(values=['y', 'n'], text='Continue?') - - if selection == 'y': - schema_must = self.get_scheme_for_endpoint(endpoint) - if schema_must['__schema_name__'] != cur_model.__class__.__name__: - for e in endpoint.parent.children: - if e.method == 'get': - parent_model = self.process_get(e, return_value=True) - break - - - if parent_model and key_name and hasattr(parent_model, 'keys'): - for i, wkey in enumerate(parent_model.keys): - if getattr(wkey, key_name) == getattr(cur_model, key_name): - parent_model.keys[i] = cur_model - cur_model = parent_model - break - - print("Please wait while posting data ...\n") - api_caller = self.get_api_caller(endpoint) - put_pname = self.get_url_param(endpoint.path) - - try: - if put_pname: - args_ = {'body': cur_model, put_pname: end_point_param_val} - api_response = api_caller(**args_) - else: - api_response = api_caller(body=cur_model) - except Exception as e: - api_response = None - self.print_exception(e) - - if api_response: - api_response_unmapped = self.unmap_model(api_response) - self.print_colored_output(api_response_unmapped) - go_back = True - break - - if go_back: - selection = self.get_input(values=['q', 'x', 'b']) - if selection == 'b': - self.display_menu(endpoint.parent) - else: - self.get_input_for_schema_(schema, cur_model, initialised=initialised) - - def display_menu(self, menu): - clear() - self.current_menu = menu - - name_list = [menu.name] - par = menu - while True: - par = par.parent - if not par: - break - name_list.insert(0, par.name) - - if len(name_list) > 1: - del name_list[0] - - self.print_underlined(': '.join(name_list)) - - selection_values = ['q', 'x', 'b'] - - menu_numbering = {} - - c = 0 - for i, item in enumerate(menu): - if item.info.get('x-cli-ignore') or (item.parent.name == 'Main Menu' and not item.children): - continue - - print(c + 1, item) - selection_values.append(str(c + 1)) - menu_numbering[c + 1] = i - c += 1 - - selection = self.get_input(selection_values) - - if selection == 'b' and not menu.parent: - print("Quiting...") - sys.exit() - elif selection == 'b': - self.display_menu(menu.parent) - elif int(selection) in menu_numbering and menu.get_child(menu_numbering[int(selection)]).children: - self.display_menu(menu.get_child(menu_numbering[int(selection)])) - else: - m = menu.get_child(menu_numbering[int(selection)]) - getattr(self, 'process_' + m.method)(m) - - def parse_command_args(self, args): - args_dict = {} - - if args: - for arg in args.split(','): - neq = arg.find(':') - if neq > 1: - arg_name = arg[:neq].strip() - arg_val = arg[neq + 1:].strip() - if arg_name and arg_val: - args_dict[arg_name] = arg_val - - return args_dict - - def parse_args(self, args, path): - param_names = [] - if not 'parameters' in path: - return {} - for param in path['parameters']: - param_names.append(param['name']) - - args_dict = self.parse_command_args(args) - - for arg_name in args_dict: - if not arg_name in param_names: - self.exit_with_error("valid endpoint args are: {}".format(', '.join(param_names))) - - return args_dict - - def help_for(self, op_name): - paths = self.get_tag_from_api_name(op_name + 'Api') - - schema_path = None - - for path in paths: - if 'tags' in path: - print('Operation ID:', path['operationId']) - print(' Description:', path['description']) - if path.get('__urlsuffix__'): - print(' url-suffix:', path['__urlsuffix__']) - if 'parameters' in path: - param_names = [] - for param in path['parameters']: - desc = param.get('description', 'No description is provided for this parameter') - param_type = param.get('schema', {}).get('type') - if param_type: - desc += ' [{}]'.format(param_type) - param_names.append((param['name'], desc)) - if param_names: - print(' Parameters:') - for param in param_names: - print(' {}: {}'.format(param[0], param[1])) - - if 'requestBody' in path: - for apptype in path['requestBody'].get('content', {}): - if 'schema' in path['requestBody']['content'][apptype]: - if path['requestBody']['content'][apptype]['schema'].get('type') == 'array': - schema_path = path['requestBody']['content'][apptype]['schema']['items']['$ref'] - print(' Schema: Array of {}'.format(schema_path[1:])) - else: - spparent = path['requestBody']['content'][apptype]['schema'] - schema_path = spparent.get('$ref') - if schema_path: - print(' Schema: {}'.format(schema_path[1:])) - else: - print(' Data type: {}'.format(spparent.get('type'))) - - - if schema_path: - print() - print("To get sample schema type {0} --schema , for example {0} --schema {1}".format(sys.argv[0], - schema_path[ - 1:])) - - def render_json_entry(self, val): - if isinstance(val, str) and val.startswith('_file '): - file_path = val[6:].strip() - if os.path.exists(file_path): - with open(file_path) as f: - val = f.read() - else: - raise ValueError("File '{}' not found".format(file_path)) - return val - - def get_json_from_file(self, data_fn): - - if not os.path.exists(data_fn): - self.exit_with_error("Can't find file {}".format(data_fn)) - - try: - with open(data_fn) as f: - data = json.load(f) - except: - self.exit_with_error("Error parsing json file {}".format(data_fn)) - - if isinstance(data, list): - for entry in data: - if isinstance(entry, dict): - for k in entry: - entry[k] = self.render_json_entry(entry[k]) - - if isinstance(data, dict): - for k in data: - data[k] = self.render_json_entry(data[k]) - - return data - - def get_api_instance(self, client): - api_instance = client(swagger_client.ApiClient(self.swagger_configuration)) - if args.key_password: - api_instance.api_client.rest_client.pool_manager.connection_pool_kw['key_password'] = args.key_password - return api_instance - - def get_path_api_caller_for_path(self, path): - - dummy_enpoint = Menu(name='', info=path) - security = self.get_scope_for_endpoint(dummy_enpoint) - if security.strip(): - self.get_access_token(security) - class_name = self.get_api_class_name(path['tags'][0]) - client = getattr(swagger_client, class_name) - api_instance = self.get_api_instance(client) - api_caller = getattr(api_instance, path['operationId'].replace('-', '_')) - - return api_caller - - def process_command_get(self, path, suffix_param, endpoint_params, data_fn, data=None): - api_caller = self.get_path_api_caller_for_path(path) - api_response = None - encoded_param = urlencode(endpoint_params) - - if encoded_param: - sys.stderr.write("Calling with params {}\n".format(encoded_param)) - - try: - if path.get('__urlsuffix__'): - api_response = api_caller(suffix_param[path['__urlsuffix__']], **endpoint_params) - else: - api_response = api_caller(**endpoint_params) - except Exception as e: - if hasattr(e, 'reason'): - sys.stderr.write(e.reason) - if hasattr(e, 'body'): - sys.stderr.write(e.body) - sys.stderr.write('\n') - sys.exit() - - api_response_unmapped = [] - if isinstance(api_response, list): - for model in api_response: - data_dict = self.unmap_model(model) - api_response_unmapped.append(data_dict) - else: - data_dict = self.unmap_model(api_response) - api_response_unmapped = data_dict - - print(json.dumps(api_response_unmapped, indent=2)) - - def get_sub_model(self, field): - sub_model_name_str = field.get('title') or field.get('description') - sub_model_name = self.get_name_from_string(sub_model_name_str) - if hasattr(swagger_client.models, sub_model_name): - return getattr(swagger_client.models, sub_model_name) - - def exit_with_error(self, error_text): - error_text += '\n' - sys.stderr.write(self.colored_text(error_text, error_color)) - print() - sys.exit() - - def process_command_post(self, path, suffix_param, endpoint_params, data_fn, data): - api_caller = self.get_path_api_caller_for_path(path) - - endpoint = Menu(name='', info=path) - schema = self.get_scheme_for_endpoint(endpoint) - model_name = schema['__schema_name__'] - model = getattr(swagger_client.models, model_name) - - if not data: - - if data_fn.endswith('jwt'): - with open(data_fn) as reader: - data_org = jwt.decode(reader.read(), - options={"verify_signature": False, "verify_exp": False, "verify_aud": False}) - else: - try: - data_org = self.get_json_from_file(data_fn) - except ValueError as ve: - self.exit_with_error(str(ve)) - - data = {} - - for k in data_org: - if k in model.attribute_map: - mapped_key = model.attribute_map[k] - data[mapped_key] = data_org[k] - else: - data[k] = data_org[k] - - try: - body = myapi._ApiClient__deserialize_model(data, model) - except Exception as e: - self.exit_with_error(str(e)) - - try: - if suffix_param: - api_response = api_caller(body=body, **suffix_param) - else: - api_response = api_caller(body=body) - except Exception as e: - self.print_exception(e) - sys.exit() - - unmapped_response = self.unmap_model(api_response) - sys.stderr.write("Server Response:\n") - print(json.dumps(unmapped_response, indent=2)) - - def process_command_put(self, path, suffix_param, endpoint_params, data_fn, data=None): - self.process_command_post(path, suffix_param, endpoint_params, data_fn, data=None) - - def process_command_patch(self, path, suffix_param, endpoint_params, data_fn, data=None): - - if not data: - try: - data = self.get_json_from_file(data_fn) - except ValueError as ve: - self.exit_with_error(str(ve)) - - if not isinstance(data, list): - self.exit_with_error("{} must be array of /components/schemas/PatchRequest".format(data_fn)) - - op_modes = ('add', 'remove', 'replace', 'move', 'copy', 'test') - - for item in data: - if not item['op'] in op_modes: - print("op must be one of {}".format(', '.join(op_modes))) - sys.exit() - if not item['path'].startswith('/'): - item['path'] = '/' + item['path'] - - api_caller = self.get_path_api_caller_for_path(path) - - try: - if suffix_param: - api_response = api_caller(suffix_param[path['__urlsuffix__']], body=data) - else: - api_response = api_caller(body=data) - except Exception as e: - self.print_exception(e) - sys.exit() - - unmapped_response = self.unmap_model(api_response) - sys.stderr.write("Server Response:\n") - print(json.dumps(unmapped_response, indent=2)) - - def process_command_delete(self, path, suffix_param, endpoint_params, data_fn, data=None): - - api_caller = self.get_path_api_caller_for_path(path) - api_response = None - - try: - api_response = api_caller(suffix_param[path['__urlsuffix__']], **endpoint_params) - except Exception as e: - self.print_exception(e) - sys.exit() - - if api_response: - unmapped_response = self.unmap_model(api_response) - sys.stderr.write("Server Response:\n") - print(json.dumps(unmapped_response, indent=2)) - - def process_command_by_id(self, operation_id, url_suffix, endpoint_args, data_fn, data=None): - path = self.get_path_by_id(operation_id) - - if not path: - self.exit_with_error("No such Operation ID") - - suffix_param = self.parse_command_args(url_suffix) - endpoint_params = self.parse_command_args(endpoint_args) - - if path.get('__urlsuffix__') and not path['__urlsuffix__'] in suffix_param: - self.exit_with_error("This operation requires a value for url-suffix {}".format(path['__urlsuffix__'])) - - endpoint = Menu('', info=path) - schema = self.get_scheme_for_endpoint(endpoint) - - if not data: - op_path = self.get_path_by_id(operation_id) - if op_path['__method__'] == 'patch' and not data_fn: - pop, pdata = '', '' - if args.patch_add: - pop = 'add' - pdata = args.patch_add - elif args.patch_remove: - pop = 'remove' - pdata = args.patch_remove - elif args.patch_replace: - pop = 'replace' - pdata = args.patch_replace - - if pop: - if pop != 'remove' and pdata.count(':') != 1: - self.exit_with_error("Please provide --patch-data as colon delimited key:value pair") - - if pop != 'remove': - ppath, pval = pdata.split(':') - data = [{'op': pop, 'path': '/'+ ppath.lstrip('/'), 'value': pval}] - else: - data = [{'op': pop, 'path': '/'+ pdata.lstrip('/')}] - - if (schema and not data_fn) and not data: - self.exit_with_error("Please provide schema with --data argument") - - caller_function = getattr(self, 'process_command_' + path['__method__']) - caller_function(path, suffix_param, endpoint_params, data_fn, data=data) - - def make_schema_val(self, stype): - if stype == 'object': - return {} - elif stype == 'list': - return [] - elif stype == 'bool': - return random.choice((True, False)) - else: - return None - - def is_native_type(self, model, prop): - stype = getattr(model, prop) - if stype.startswith('list['): - stype = re.match(r'list\[(.*)\]', stype).group(1) - elif stype.startswith('dict('): - stype = re.match(r'dict\(([^,]*), (.*)\)', stype).group(2) - - if stype in swagger_client.ApiClient.NATIVE_TYPES_MAPPING: - return True - - def get_schema_from_model(self, model, schema): - - for key in model.attribute_map: - stype = model.swagger_types[key] - mapped_key = model.attribute_map[key] - if stype in swagger_client.ApiClient.NATIVE_TYPES_MAPPING: - schema[mapped_key] = self.make_schema_val(stype) - else: - if stype.startswith('list['): - sub_cls = re.match(r'list\[(.*)\]', stype).group(1) - sub_type = 'list' - elif stype.startswith('dict('): - sub_cls = re.match(r'dict\(([^,]*), (.*)\)', stype).group(2) - sub_type = 'dict' - else: - sub_cls = stype - sub_type = None - - if sub_cls in swagger_client.ApiClient.NATIVE_TYPES_MAPPING: - schema[mapped_key] = self.make_schema_val(sub_type) - else: - sub_dict = {} - sub_model = getattr(swagger_client.models, sub_cls) - sub_schema = self.get_schema_from_model(sub_model, sub_dict) - schema[mapped_key] = sub_dict - - def get_swagger_model_attr(self, model, attr): - stype = model.swagger_types[attr] - if stype in swagger_client.ApiClient.NATIVE_TYPES_MAPPING: - return getattr(model, attr) - - def fill_defaults(self, schema, schema_={}): - - for k in schema: - if isinstance(schema[k], dict): - sub_schema_ = None - if '$ref' in schema_['properties'][k]: - sub_schema_ = self.cfg_yml['components']['schemas'][schema_['properties'][k]['$ref'].split('/')[-1]] - elif schema_['properties'][k].get('items', {}).get('$ref'): - sub_schema_ = self.cfg_yml['components']['schemas'][ - schema_['properties'][k]['items']['$ref'].split('/')[-1]] - elif 'properties' in schema_['properties'][k]: - sub_schema_ = schema_['properties'][k] - if sub_schema_: - self.fill_defaults(schema[k], sub_schema_) - - else: - if 'enum' in schema_['properties'][k]: - val = random.choice(schema_['properties'][k]['enum']) - if schema_['properties'][k]['type'] == 'array': - val = [val] - schema[k] = val - elif 'default' in schema_['properties'][k]: - schema[k] = schema_['properties'][k]['default'] - elif 'example' in schema_['properties'][k]: - schema[k] = schema_['properties'][k]['example'] - elif k in schema_.get('required', []): - schema[k] = schema_['properties'][k]['type'] - - def get_sample_schema(self, ref): - schema_ = self.get_schema_from_reference('#' + args.schema) - m = getattr(swagger_client.models, schema_['__schema_name__']) - schema = {} - self.get_schema_from_model(m, schema) - self.fill_defaults(schema, schema_) - - print(json.dumps(schema, indent=2)) - - def runApp(self): - clear() - self.display_menu(self.menu) - - -def main(): - - cli_object = JCA_CLI(host, client_id, client_secret, access_token, test_client) - error_log_file = os.path.join(log_dir, 'error.log') - try: - if not access_token: - cli_object.check_connection() - if not (args.operation_id or args.info or args.schema): - # reset previous color - print('\033[0m', end='') - cli_object.runApp() - else: - print() - if args.info: - cli_object.help_for(args.info) - elif args.schema: - cli_object.get_sample_schema(args.schema) - elif args.operation_id: - cli_object.process_command_by_id(args.operation_id, args.url_suffix, args.endpoint_args, args.data) - print() - except Exception as e: - if os.environ.get('errstdout'): - print(traceback.print_exc()) - print(u"\u001b[38;5;{}mAn Unhandled error raised: {}\u001b[0m".format(error_color, e)) - with open(error_log_file, 'a') as w: - traceback.print_exc(file=w) - print("Error is logged to {}".format(error_log_file)) - - -if __name__ == "__main__": - main() diff --git a/jans-cli/cli/pylib/__init__.py b/jans-cli/cli/pylib/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/jans-cli/cli/pylib/tabulate/LICENSE b/jans-cli/cli/pylib/tabulate/LICENSE deleted file mode 100644 index 81241eca637..00000000000 --- a/jans-cli/cli/pylib/tabulate/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2011-2020 Sergey Astanin and contributors - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/jans-cli/cli/pylib/tabulate/README b/jans-cli/cli/pylib/tabulate/README deleted file mode 100644 index 42061c01a1c..00000000000 --- a/jans-cli/cli/pylib/tabulate/README +++ /dev/null @@ -1 +0,0 @@ -README.md \ No newline at end of file diff --git a/jans-cli/cli/pylib/tabulate/README.md b/jans-cli/cli/pylib/tabulate/README.md deleted file mode 100644 index ce06dadc834..00000000000 --- a/jans-cli/cli/pylib/tabulate/README.md +++ /dev/null @@ -1,747 +0,0 @@ -python-tabulate -=============== - -Pretty-print tabular data in Python, a library and a command-line -utility. - -The main use cases of the library are: - -- printing small tables without hassle: just one function call, - formatting is guided by the data itself -- authoring tabular data for lightweight plain-text markup: multiple - output formats suitable for further editing or transformation -- readable presentation of mixed textual and numeric data: smart - column alignment, configurable number formatting, alignment by a - decimal point - -Installation ------------- - -To install the Python library and the command line utility, run: - - pip install tabulate - -The command line utility will be installed as `tabulate` to `bin` on -Linux (e.g. `/usr/bin`); or as `tabulate.exe` to `Scripts` in your -Python installation on Windows (e.g. -`C:\Python27\Scripts\tabulate.exe`). - -You may consider installing the library only for the current user: - - pip install tabulate --user - -In this case the command line utility will be installed to -`~/.local/bin/tabulate` on Linux and to -`%APPDATA%\Python\Scripts\tabulate.exe` on Windows. - -To install just the library on Unix-like operating systems: - - TABULATE_INSTALL=lib-only pip install tabulate - -On Windows: - - set TABULATE_INSTALL=lib-only - pip install tabulate - -Build status ------------- - -[![Build status](https://circleci.com/gh/astanin/python-tabulate.svg?style=svg)](https://circleci.com/gh/astanin/python-tabulate/tree/master) [![Build status](https://ci.appveyor.com/api/projects/status/8745yksvvol7h3d7/branch/master?svg=true)](https://ci.appveyor.com/project/astanin/python-tabulate/branch/master) - -Library usage -------------- - -The module provides just one function, `tabulate`, which takes a list of -lists or another tabular data type as the first argument, and outputs a -nicely formatted plain-text table: - - >>> from tabulate import tabulate - - >>> table = [["Sun",696000,1989100000],["Earth",6371,5973.6], - ... ["Moon",1737,73.5],["Mars",3390,641.85]] - >>> print(tabulate(table)) - ----- ------ ------------- - Sun 696000 1.9891e+09 - Earth 6371 5973.6 - Moon 1737 73.5 - Mars 3390 641.85 - ----- ------ ------------- - -The following tabular data types are supported: - -- list of lists or another iterable of iterables -- list or another iterable of dicts (keys as columns) -- dict of iterables (keys as columns) -- two-dimensional NumPy array -- NumPy record arrays (names as columns) -- pandas.DataFrame - -Examples in this file use Python2. Tabulate supports Python3 too. - -### Headers - -The second optional argument named `headers` defines a list of column -headers to be used: - - >>> print(tabulate(table, headers=["Planet","R (km)", "mass (x 10^29 kg)"])) - Planet R (km) mass (x 10^29 kg) - -------- -------- ------------------- - Sun 696000 1.9891e+09 - Earth 6371 5973.6 - Moon 1737 73.5 - Mars 3390 641.85 - -If `headers="firstrow"`, then the first row of data is used: - - >>> print(tabulate([["Name","Age"],["Alice",24],["Bob",19]], - ... headers="firstrow")) - Name Age - ------ ----- - Alice 24 - Bob 19 - -If `headers="keys"`, then the keys of a dictionary/dataframe, or column -indices are used. It also works for NumPy record arrays and lists of -dictionaries or named tuples: - - >>> print(tabulate({"Name": ["Alice", "Bob"], - ... "Age": [24, 19]}, headers="keys")) - Age Name - ----- ------ - 24 Alice - 19 Bob - -### Row Indices - -By default, only pandas.DataFrame tables have an additional column -called row index. To add a similar column to any other type of table, -pass `showindex="always"` or `showindex=True` argument to `tabulate()`. -To suppress row indices for all types of data, pass `showindex="never"` -or `showindex=False`. To add a custom row index column, pass -`showindex=rowIDs`, where `rowIDs` is some iterable: - - >>> print(tabulate([["F",24],["M",19]], showindex="always")) - - - -- - 0 F 24 - 1 M 19 - - - -- - -### Table format - -There is more than one way to format a table in plain text. The third -optional argument named `tablefmt` defines how the table is formatted. - -Supported table formats are: - -- "plain" -- "simple" -- "github" -- "grid" -- "fancy\_grid" -- "pipe" -- "orgtbl" -- "jira" -- "presto" -- "pretty" -- "psql" -- "rst" -- "mediawiki" -- "moinmoin" -- "youtrack" -- "html" -- "unsafehtml" -- "latex" -- "latex\_raw" -- "latex\_booktabs" -- "textile" - -`plain` tables do not use any pseudo-graphics to draw lines: - - >>> table = [["spam",42],["eggs",451],["bacon",0]] - >>> headers = ["item", "qty"] - >>> print(tabulate(table, headers, tablefmt="plain")) - item qty - spam 42 - eggs 451 - bacon 0 - -`simple` is the default format (the default may change in future -versions). It corresponds to `simple_tables` in [Pandoc Markdown -extensions](http://johnmacfarlane.net/pandoc/README.html#tables): - - >>> print(tabulate(table, headers, tablefmt="simple")) - item qty - ------ ----- - spam 42 - eggs 451 - bacon 0 - -`github` follows the conventions of Github flavored Markdown. It -corresponds to the `pipe` format without alignment colons: - - >>> print(tabulate(table, headers, tablefmt="github")) - | item | qty | - |--------|-------| - | spam | 42 | - | eggs | 451 | - | bacon | 0 | - -`grid` is like tables formatted by Emacs' -[table.el](http://table.sourceforge.net/) package. It corresponds to -`grid_tables` in Pandoc Markdown extensions: - - >>> print(tabulate(table, headers, tablefmt="grid")) - +--------+-------+ - | item | qty | - +========+=======+ - | spam | 42 | - +--------+-------+ - | eggs | 451 | - +--------+-------+ - | bacon | 0 | - +--------+-------+ - -`fancy_grid` draws a grid using box-drawing characters: - - >>> print(tabulate(table, headers, tablefmt="fancy_grid")) - ╒════════╤═══════╕ - │ item │ qty │ - ╞════════╪═══════╡ - │ spam │ 42 │ - ├────────┼───────┤ - │ eggs │ 451 │ - ├────────┼───────┤ - │ bacon │ 0 │ - ╘════════╧═══════╛ - -`presto` is like tables formatted by Presto cli: - - >>> print(tabulate(table, headers, tablefmt="presto")) - item | qty - --------+------- - spam | 42 - eggs | 451 - bacon | 0 - -`pretty` attempts to be close to the format emitted by the PrettyTables -library: - - >>> print(tabulate(table, headers, tablefmt="pretty")) - +-------+-----+ - | item | qty | - +-------+-----+ - | spam | 42 | - | eggs | 451 | - | bacon | 0 | - +-------+-----+ - -`psql` is like tables formatted by Postgres' psql cli: - - >>> print(tabulate(table, headers, tablefmt="psql")) - +--------+-------+ - | item | qty | - |--------+-------| - | spam | 42 | - | eggs | 451 | - | bacon | 0 | - +--------+-------+ - -`pipe` follows the conventions of [PHP Markdown -Extra](http://michelf.ca/projects/php-markdown/extra/#table) extension. -It corresponds to `pipe_tables` in Pandoc. This format uses colons to -indicate column alignment: - - >>> print(tabulate(table, headers, tablefmt="pipe")) - | item | qty | - |:-------|------:| - | spam | 42 | - | eggs | 451 | - | bacon | 0 | - -`orgtbl` follows the conventions of Emacs -[org-mode](http://orgmode.org/manual/Tables.html), and is editable also -in the minor orgtbl-mode. Hence its name: - - >>> print(tabulate(table, headers, tablefmt="orgtbl")) - | item | qty | - |--------+-------| - | spam | 42 | - | eggs | 451 | - | bacon | 0 | - -`jira` follows the conventions of Atlassian Jira markup language: - - >>> print(tabulate(table, headers, tablefmt="jira")) - || item || qty || - | spam | 42 | - | eggs | 451 | - | bacon | 0 | - -`rst` formats data like a simple table of the -[reStructuredText](http://docutils.sourceforge.net/docs/user/rst/quickref.html#tables) -format: - - >>> print(tabulate(table, headers, tablefmt="rst")) - ====== ===== - item qty - ====== ===== - spam 42 - eggs 451 - bacon 0 - ====== ===== - -`mediawiki` format produces a table markup used in -[Wikipedia](http://www.mediawiki.org/wiki/Help:Tables) and on other -MediaWiki-based sites: - - >>> print(tabulate(table, headers, tablefmt="mediawiki")) - {| class="wikitable" style="text-align: left;" - |+ - |- - ! item !! align="right"| qty - |- - | spam || align="right"| 42 - |- - | eggs || align="right"| 451 - |- - | bacon || align="right"| 0 - |} - -`moinmoin` format produces a table markup used in -[MoinMoin](https://moinmo.in/) wikis: - - >>> print(tabulate(table, headers, tablefmt="moinmoin")) - || ''' item ''' || ''' quantity ''' || - || spam || 41.999 || - || eggs || 451 || - || bacon || || - -`youtrack` format produces a table markup used in Youtrack tickets: - - >>> print(tabulate(table, headers, tablefmt="youtrack")) - || item || quantity || - | spam | 41.999 | - | eggs | 451 | - | bacon | | - -`textile` format produces a table markup used in -[Textile](http://redcloth.org/hobix.com/textile/) format: - - >>> print(tabulate(table, headers, tablefmt="textile")) - |_. item |_. qty | - |<. spam |>. 42 | - |<. eggs |>. 451 | - |<. bacon |>. 0 | - -`html` produces standard HTML markup as an html.escape'd str -with a ._repr_html_ method so that Jupyter Lab and Notebook display the HTML -and a .str property so that the raw HTML remains accessible. -`unsafehtml` table format can be used if an unescaped HTML is required: - - >>> print(tabulate(table, headers, tablefmt="html")) - - - - - - - -
item qty
spam 42
eggs 451
bacon 0
- -`latex` format creates a `tabular` environment for LaTeX markup, -replacing special characters like `_` or `\` to their LaTeX -correspondents: - - >>> print(tabulate(table, headers, tablefmt="latex")) - \begin{tabular}{lr} - \hline - item & qty \\ - \hline - spam & 42 \\ - eggs & 451 \\ - bacon & 0 \\ - \hline - \end{tabular} - -`latex_raw` behaves like `latex` but does not escape LaTeX commands and -special characters. - -`latex_booktabs` creates a `tabular` environment for LaTeX markup using -spacing and style from the `booktabs` package. - -### Column alignment - -`tabulate` is smart about column alignment. It detects columns which -contain only numbers, and aligns them by a decimal point (or flushes -them to the right if they appear to be integers). Text columns are -flushed to the left. - -You can override the default alignment with `numalign` and `stralign` -named arguments. Possible column alignments are: `right`, `center`, -`left`, `decimal` (only for numbers), and `None` (to disable alignment). - -Aligning by a decimal point works best when you need to compare numbers -at a glance: - - >>> print(tabulate([[1.2345],[123.45],[12.345],[12345],[1234.5]])) - ---------- - 1.2345 - 123.45 - 12.345 - 12345 - 1234.5 - ---------- - -Compare this with a more common right alignment: - - >>> print(tabulate([[1.2345],[123.45],[12.345],[12345],[1234.5]], numalign="right")) - ------ - 1.2345 - 123.45 - 12.345 - 12345 - 1234.5 - ------ - -For `tabulate`, anything which can be parsed as a number is a number. -Even numbers represented as strings are aligned properly. This feature -comes in handy when reading a mixed table of text and numbers from a -file: - - >>> import csv ; from StringIO import StringIO - >>> table = list(csv.reader(StringIO("spam, 42\neggs, 451\n"))) - >>> table - [['spam', ' 42'], ['eggs', ' 451']] - >>> print(tabulate(table)) - ---- ---- - spam 42 - eggs 451 - ---- ---- - - -To disable this feature use `disable_numparse=True`. - - >>> print(tabulate.tabulate([["Ver1", "18.0"], ["Ver2","19.2"]], tablefmt="simple", disable_numparse=True)) - ---- ---- - Ver1 18.0 - Ver2 19.2 - ---- ---- - - -### Custom column alignment - -`tabulate` allows a custom column alignment to override the above. The -`colalign` argument can be a list or a tuple of `stralign` named -arguments. Possible column alignments are: `right`, `center`, `left`, -`decimal` (only for numbers), and `None` (to disable alignment). -Omitting an alignment uses the default. For example: - - >>> print(tabulate([["one", "two"], ["three", "four"]], colalign=("right",)) - ----- ---- - one two - three four - ----- ---- - -### Number formatting - -`tabulate` allows to define custom number formatting applied to all -columns of decimal numbers. Use `floatfmt` named argument: - - >>> print(tabulate([["pi",3.141593],["e",2.718282]], floatfmt=".4f")) - -- ------ - pi 3.1416 - e 2.7183 - -- ------ - -`floatfmt` argument can be a list or a tuple of format strings, one per -column, in which case every column may have different number formatting: - - >>> print(tabulate([[0.12345, 0.12345, 0.12345]], floatfmt=(".1f", ".3f"))) - --- ----- ------- - 0.1 0.123 0.12345 - --- ----- ------- - -### Text formatting - -By default, `tabulate` removes leading and trailing whitespace from text -columns. To disable whitespace removal, set the global module-level flag -`PRESERVE_WHITESPACE`: - - import tabulate - tabulate.PRESERVE_WHITESPACE = True - -### Wide (fullwidth CJK) symbols - -To properly align tables which contain wide characters (typically -fullwidth glyphs from Chinese, Japanese or Korean languages), the user -should install `wcwidth` library. To install it together with -`tabulate`: - - pip install tabulate[widechars] - -Wide character support is enabled automatically if `wcwidth` library is -already installed. To disable wide characters support without -uninstalling `wcwidth`, set the global module-level flag -`WIDE_CHARS_MODE`: - - import tabulate - tabulate.WIDE_CHARS_MODE = False - -### Multiline cells - -Most table formats support multiline cell text (text containing newline -characters). The newline characters are honored as line break -characters. - -Multiline cells are supported for data rows and for header rows. - -Further automatic line breaks are not inserted. Of course, some output -formats such as latex or html handle automatic formatting of the cell -content on their own, but for those that don't, the newline characters -in the input cell text are the only means to break a line in cell text. - -Note that some output formats (e.g. simple, or plain) do not represent -row delimiters, so that the representation of multiline cells in such -formats may be ambiguous to the reader. - -The following examples of formatted output use the following table with -a multiline cell, and headers with a multiline cell: - - >>> table = [["eggs",451],["more\nspam",42]] - >>> headers = ["item\nname", "qty"] - -`plain` tables: - - >>> print(tabulate(table, headers, tablefmt="plain")) - item qty - name - eggs 451 - more 42 - spam - -`simple` tables: - - >>> print(tabulate(table, headers, tablefmt="simple")) - item qty - name - ------ ----- - eggs 451 - more 42 - spam - -`grid` tables: - - >>> print(tabulate(table, headers, tablefmt="grid")) - +--------+-------+ - | item | qty | - | name | | - +========+=======+ - | eggs | 451 | - +--------+-------+ - | more | 42 | - | spam | | - +--------+-------+ - -`fancy_grid` tables: - - >>> print(tabulate(table, headers, tablefmt="fancy_grid")) - ╒════════╤═══════╕ - │ item │ qty │ - │ name │ │ - ╞════════╪═══════╡ - │ eggs │ 451 │ - ├────────┼───────┤ - │ more │ 42 │ - │ spam │ │ - ╘════════╧═══════╛ - -`pipe` tables: - - >>> print(tabulate(table, headers, tablefmt="pipe")) - | item | qty | - | name | | - |:-------|------:| - | eggs | 451 | - | more | 42 | - | spam | | - -`orgtbl` tables: - - >>> print(tabulate(table, headers, tablefmt="orgtbl")) - | item | qty | - | name | | - |--------+-------| - | eggs | 451 | - | more | 42 | - | spam | | - -`jira` tables: - - >>> print(tabulate(table, headers, tablefmt="jira")) - | item | qty | - | name | | - |:-------|------:| - | eggs | 451 | - | more | 42 | - | spam | | - -`presto` tables: - - >>> print(tabulate(table, headers, tablefmt="presto")) - item | qty - name | - --------+------- - eggs | 451 - more | 42 - spam | - -`pretty` tables: - - >>> print(tabulate(table, headers, tablefmt="pretty")) - +------+-----+ - | item | qty | - | name | | - +------+-----+ - | eggs | 451 | - | more | 42 | - | spam | | - +------+-----+ - -`psql` tables: - - >>> print(tabulate(table, headers, tablefmt="psql")) - +--------+-------+ - | item | qty | - | name | | - |--------+-------| - | eggs | 451 | - | more | 42 | - | spam | | - +--------+-------+ - -`rst` tables: - - >>> print(tabulate(table, headers, tablefmt="rst")) - ====== ===== - item qty - name - ====== ===== - eggs 451 - more 42 - spam - ====== ===== - -Multiline cells are not well supported for the other table formats. - -Usage of the command line utility ---------------------------------- - - Usage: tabulate [options] [FILE ...] - - FILE a filename of the file with tabular data; - if "-" or missing, read data from stdin. - - Options: - - -h, --help show this message - -1, --header use the first row of data as a table header - -o FILE, --output FILE print table to FILE (default: stdout) - -s REGEXP, --sep REGEXP use a custom column separator (default: whitespace) - -F FPFMT, --float FPFMT floating point number format (default: g) - -f FMT, --format FMT set output table format; supported formats: - plain, simple, github, grid, fancy_grid, pipe, - orgtbl, rst, mediawiki, html, latex, latex_raw, - latex_booktabs, tsv - (default: simple) - -Performance considerations --------------------------- - -Such features as decimal point alignment and trying to parse everything -as a number imply that `tabulate`: - -- has to "guess" how to print a particular tabular data type -- needs to keep the entire table in-memory -- has to "transpose" the table twice -- does much more work than it may appear - -It may not be suitable for serializing really big tables (but who's -going to do that, anyway?) or printing tables in performance sensitive -applications. `tabulate` is about two orders of magnitude slower than -simply joining lists of values with a tab, coma or other separator. - -In the same time `tabulate` is comparable to other table -pretty-printers. Given a 10x10 table (a list of lists) of mixed text and -numeric data, `tabulate` appears to be slower than `asciitable`, and -faster than `PrettyTable` and `texttable` The following mini-benchmark -was run in Python 3.8.1 in Windows 10 x64: - - =========================== ========== =========== - Table formatter time, μs rel. time - =========================== ========== =========== - csv to StringIO 12.4 1.0 - join with tabs and newlines 15.7 1.3 - asciitable (0.8.0) 208.3 16.7 - tabulate (0.8.7) 492.1 39.5 - PrettyTable (0.7.2) 945.5 76.0 - texttable (1.6.2) 1239.5 99.6 - =========================== ========== =========== - - -Version history ---------------- - -The full version history can be found at the [changelog](https://github.com/astanin/python-tabulate/blob/master/CHANGELOG). - -How to contribute ------------------ - -Contributions should include tests and an explanation for the changes -they propose. Documentation (examples, docstrings, README.md) should be -updated accordingly. - -This project uses [nose](https://nose.readthedocs.org/) testing -framework and [tox](https://tox.readthedocs.io/) to automate testing in -different environments. Add tests to one of the files in the `test/` -folder. - -To run tests on all supported Python versions, make sure all Python -interpreters, `nose` and `tox` are installed, then run `tox` in the root -of the project source tree. - -On Linux `tox` expects to find executables like `python2.6`, -`python2.7`, `python3.4` etc. On Windows it looks for -`C:\Python26\python.exe`, `C:\Python27\python.exe` and -`C:\Python34\python.exe` respectively. - -To test only some Python environements, use `-e` option. For example, to -test only against Python 2.7 and Python 3.6, run: - - tox -e py27,py36 - -in the root of the project source tree. - -To enable NumPy and Pandas tests, run: - - tox -e py27-extra,py36-extra - -(this may take a long time the first time, because NumPy and Pandas will -have to be installed in the new virtual environments) - -See `tox.ini` file to learn how to use `nosetests` directly to test -individual Python versions. - -Contributors ------------- - -Sergey Astanin, Pau Tallada Crespí, Erwin Marsi, Mik Kocikowski, Bill -Ryder, Zach Dwiel, Frederik Rietdijk, Philipp Bogensberger, Greg -(anonymous), Stefan Tatschner, Emiel van Miltenburg, Brandon Bennett, -Amjith Ramanujam, Jan Schulz, Simon Percivall, Javier Santacruz -López-Cepero, Sam Denton, Alexey Ziyangirov, acaird, Cesar Sanchez, -naught101, John Vandenberg, Zack Dever, Christian Clauss, Benjamin -Maier, Andy MacKinlay, Thomas Roten, Jue Wang, Joe King, Samuel Phan, -Nick Satterly, Daniel Robbins, Dmitry B, Lars Butler, Andreas Maier, -Dick Marinus, Sébastien Celles, Yago González, Andrew Gaul, Wim Glenn, -Jean Michel Rouly, Tim Gates, John Vandenberg, Sorin Sbarnea, -Wes Turner, Andrew Tija, Marco Gorelli, Sean McGinnis, danja100. diff --git a/jans-cli/cli/pylib/tabulate/__init__.py b/jans-cli/cli/pylib/tabulate/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/jans-cli/cli/pylib/tabulate/tabulate.py b/jans-cli/cli/pylib/tabulate/tabulate.py deleted file mode 100644 index 2bbb913c62d..00000000000 --- a/jans-cli/cli/pylib/tabulate/tabulate.py +++ /dev/null @@ -1,1792 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Pretty-print tabular data.""" - -from __future__ import print_function -from __future__ import unicode_literals -from collections import namedtuple -from platform import python_version_tuple -import re -import math -import sys - - -if sys.version_info.major >= 3 and sys.version_info.minor >= 3: - from collections.abc import Iterable -else: - from collections import Iterable - - -if python_version_tuple()[0] < "3": - from itertools import izip_longest - from functools import partial - - _none_type = type(None) - _bool_type = bool - _int_type = int - _long_type = long # noqa - _float_type = float - _text_type = unicode # noqa - _binary_type = str - - def _is_file(f): - return hasattr(f, "read") - - -else: - from itertools import zip_longest as izip_longest - from functools import reduce, partial - - _none_type = type(None) - _bool_type = bool - _int_type = int - _long_type = int - _float_type = float - _text_type = str - _binary_type = bytes - basestring = str - - import io - - def _is_file(f): - return isinstance(f, io.IOBase) - - -try: - import wcwidth # optional wide-character (CJK) support -except ImportError: - wcwidth = None - -try: - from html import escape as htmlescape -except ImportError: - from cgi import escape as htmlescape - - -__all__ = ["tabulate", "tabulate_formats", "simple_separated_format"] -__version__ = "0.8.8" - - -# minimum extra space in headers -MIN_PADDING = 2 - -# Whether or not to preserve leading/trailing whitespace in data. -PRESERVE_WHITESPACE = False - -_DEFAULT_FLOATFMT = "g" -_DEFAULT_MISSINGVAL = "" - - -# if True, enable wide-character (CJK) support -WIDE_CHARS_MODE = wcwidth is not None - - -Line = namedtuple("Line", ["begin", "hline", "sep", "end"]) - - -DataRow = namedtuple("DataRow", ["begin", "sep", "end"]) - - -# A table structure is suppposed to be: -# -# --- lineabove --------- -# headerrow -# --- linebelowheader --- -# datarow -# --- linebewteenrows --- -# ... (more datarows) ... -# --- linebewteenrows --- -# last datarow -# --- linebelow --------- -# -# TableFormat's line* elements can be -# -# - either None, if the element is not used, -# - or a Line tuple, -# - or a function: [col_widths], [col_alignments] -> string. -# -# TableFormat's *row elements can be -# -# - either None, if the element is not used, -# - or a DataRow tuple, -# - or a function: [cell_values], [col_widths], [col_alignments] -> string. -# -# padding (an integer) is the amount of white space around data values. -# -# with_header_hide: -# -# - either None, to display all table elements unconditionally, -# - or a list of elements not to be displayed if the table has column headers. -# -TableFormat = namedtuple( - "TableFormat", - [ - "lineabove", - "linebelowheader", - "linebetweenrows", - "linebelow", - "headerrow", - "datarow", - "padding", - "with_header_hide", - ], -) - - -def _pipe_segment_with_colons(align, colwidth): - """Return a segment of a horizontal line with optional colons which - indicate column's alignment (as in `pipe` output format).""" - w = colwidth - if align in ["right", "decimal"]: - return ("-" * (w - 1)) + ":" - elif align == "center": - return ":" + ("-" * (w - 2)) + ":" - elif align == "left": - return ":" + ("-" * (w - 1)) - else: - return "-" * w - - -def _pipe_line_with_colons(colwidths, colaligns): - """Return a horizontal line with optional colons to indicate column's - alignment (as in `pipe` output format).""" - if not colaligns: # e.g. printing an empty data frame (github issue #15) - colaligns = [""] * len(colwidths) - segments = [_pipe_segment_with_colons(a, w) for a, w in zip(colaligns, colwidths)] - return "|" + "|".join(segments) + "|" - - -def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns): - alignment = { - "left": "", - "right": 'align="right"| ', - "center": 'align="center"| ', - "decimal": 'align="right"| ', - } - # hard-coded padding _around_ align attribute and value together - # rather than padding parameter which affects only the value - values_with_attrs = [ - " " + alignment.get(a, "") + c + " " for c, a in zip(cell_values, colaligns) - ] - colsep = separator * 2 - return (separator + colsep.join(values_with_attrs)).rstrip() - - -def _textile_row_with_attrs(cell_values, colwidths, colaligns): - cell_values[0] += " " - alignment = {"left": "<.", "right": ">.", "center": "=.", "decimal": ">."} - values = (alignment.get(a, "") + v for a, v in zip(colaligns, cell_values)) - return "|" + "|".join(values) + "|" - - -def _html_begin_table_without_header(colwidths_ignore, colaligns_ignore): - # this table header will be suppressed if there is a header row - return "\n" - - -def _html_row_with_attrs(celltag, unsafe, cell_values, colwidths, colaligns): - alignment = { - "left": "", - "right": ' style="text-align: right;"', - "center": ' style="text-align: center;"', - "decimal": ' style="text-align: right;"', - } - if unsafe: - values_with_attrs = [ - "<{0}{1}>{2}".format(celltag, alignment.get(a, ""), c) - for c, a in zip(cell_values, colaligns) - ] - else: - values_with_attrs = [ - "<{0}{1}>{2}".format(celltag, alignment.get(a, ""), htmlescape(c)) - for c, a in zip(cell_values, colaligns) - ] - rowhtml = "{}".format("".join(values_with_attrs).rstrip()) - if celltag == "th": # it's a header row, create a new table header - rowhtml = "
\n\n{}\n\n".format(rowhtml) - return rowhtml - - -def _moin_row_with_attrs(celltag, cell_values, colwidths, colaligns, header=""): - alignment = { - "left": "", - "right": '', - "center": '', - "decimal": '', - } - values_with_attrs = [ - "{0}{1} {2} ".format(celltag, alignment.get(a, ""), header + c + header) - for c, a in zip(cell_values, colaligns) - ] - return "".join(values_with_attrs) + "||" - - -def _latex_line_begin_tabular(colwidths, colaligns, booktabs=False): - alignment = {"left": "l", "right": "r", "center": "c", "decimal": "r"} - tabular_columns_fmt = "".join([alignment.get(a, "l") for a in colaligns]) - return "\n".join( - [ - "\\begin{tabular}{" + tabular_columns_fmt + "}", - "\\toprule" if booktabs else "\\hline", - ] - ) - - -LATEX_ESCAPE_RULES = { - r"&": r"\&", - r"%": r"\%", - r"$": r"\$", - r"#": r"\#", - r"_": r"\_", - r"^": r"\^{}", - r"{": r"\{", - r"}": r"\}", - r"~": r"\textasciitilde{}", - "\\": r"\textbackslash{}", - r"<": r"\ensuremath{<}", - r">": r"\ensuremath{>}", -} - - -def _latex_row(cell_values, colwidths, colaligns, escrules=LATEX_ESCAPE_RULES): - def escape_char(c): - return escrules.get(c, c) - - escaped_values = ["".join(map(escape_char, cell)) for cell in cell_values] - rowfmt = DataRow("", "&", "\\\\") - return _build_simple_row(escaped_values, rowfmt) - - -def _rst_escape_first_column(rows, headers): - def escape_empty(val): - if isinstance(val, (_text_type, _binary_type)) and not val.strip(): - return ".." - else: - return val - - new_headers = list(headers) - new_rows = [] - if headers: - new_headers[0] = escape_empty(headers[0]) - for row in rows: - new_row = list(row) - if new_row: - new_row[0] = escape_empty(row[0]) - new_rows.append(new_row) - return new_rows, new_headers - - -_table_formats = { - "simple": TableFormat( - lineabove=Line("", "-", " ", ""), - linebelowheader=Line("", "-", " ", ""), - linebetweenrows=None, - linebelow=Line("", "-", " ", ""), - headerrow=DataRow("", " ", ""), - datarow=DataRow("", " ", ""), - padding=0, - with_header_hide=["lineabove", "linebelow"], - ), - "plain": TableFormat( - lineabove=None, - linebelowheader=None, - linebetweenrows=None, - linebelow=None, - headerrow=DataRow("", " ", ""), - datarow=DataRow("", " ", ""), - padding=0, - with_header_hide=None, - ), - "grid": TableFormat( - lineabove=Line("+", "-", "+", "+"), - linebelowheader=Line("+", "=", "+", "+"), - linebetweenrows=Line("+", "-", "+", "+"), - linebelow=Line("+", "-", "+", "+"), - headerrow=DataRow("|", "|", "|"), - datarow=DataRow("|", "|", "|"), - padding=1, - with_header_hide=None, - ), - "fancy_grid": TableFormat( - lineabove=Line("╒", "═", "╤", "╕"), - linebelowheader=Line("╞", "═", "╪", "╡"), - linebetweenrows=Line("├", "─", "┼", "┤"), - linebelow=Line("╘", "═", "╧", "╛"), - headerrow=DataRow("│", "│", "│"), - datarow=DataRow("│", "│", "│"), - padding=1, - with_header_hide=None, - ), - "github": TableFormat( - lineabove=Line("|", "-", "|", "|"), - linebelowheader=Line("|", "-", "|", "|"), - linebetweenrows=None, - linebelow=None, - headerrow=DataRow("|", "|", "|"), - datarow=DataRow("|", "|", "|"), - padding=1, - with_header_hide=["lineabove"], - ), - "pipe": TableFormat( - lineabove=_pipe_line_with_colons, - linebelowheader=_pipe_line_with_colons, - linebetweenrows=None, - linebelow=None, - headerrow=DataRow("|", "|", "|"), - datarow=DataRow("|", "|", "|"), - padding=1, - with_header_hide=["lineabove"], - ), - "orgtbl": TableFormat( - lineabove=None, - linebelowheader=Line("|", "-", "+", "|"), - linebetweenrows=None, - linebelow=None, - headerrow=DataRow("|", "|", "|"), - datarow=DataRow("|", "|", "|"), - padding=1, - with_header_hide=None, - ), - "jira": TableFormat( - lineabove=None, - linebelowheader=None, - linebetweenrows=None, - linebelow=None, - headerrow=DataRow("||", "||", "||"), - datarow=DataRow("|", "|", "|"), - padding=1, - with_header_hide=None, - ), - "presto": TableFormat( - lineabove=None, - linebelowheader=Line("", "-", "+", ""), - linebetweenrows=None, - linebelow=None, - headerrow=DataRow("", "|", ""), - datarow=DataRow("", "|", ""), - padding=1, - with_header_hide=None, - ), - "pretty": TableFormat( - lineabove=Line("+", "-", "+", "+"), - linebelowheader=Line("+", "-", "+", "+"), - linebetweenrows=None, - linebelow=Line("+", "-", "+", "+"), - headerrow=DataRow("|", "|", "|"), - datarow=DataRow("|", "|", "|"), - padding=1, - with_header_hide=None, - ), - "psql": TableFormat( - lineabove=Line("+", "-", "+", "+"), - linebelowheader=Line("|", "-", "+", "|"), - linebetweenrows=None, - linebelow=Line("+", "-", "+", "+"), - headerrow=DataRow("|", "|", "|"), - datarow=DataRow("|", "|", "|"), - padding=1, - with_header_hide=None, - ), - "rst": TableFormat( - lineabove=Line("", "=", " ", ""), - linebelowheader=Line("", "=", " ", ""), - linebetweenrows=None, - linebelow=Line("", "=", " ", ""), - headerrow=DataRow("", " ", ""), - datarow=DataRow("", " ", ""), - padding=0, - with_header_hide=None, - ), - "mediawiki": TableFormat( - lineabove=Line( - '{| class="wikitable" style="text-align: left;"', - "", - "", - "\n|+ \n|-", - ), - linebelowheader=Line("|-", "", "", ""), - linebetweenrows=Line("|-", "", "", ""), - linebelow=Line("|}", "", "", ""), - headerrow=partial(_mediawiki_row_with_attrs, "!"), - datarow=partial(_mediawiki_row_with_attrs, "|"), - padding=0, - with_header_hide=None, - ), - "moinmoin": TableFormat( - lineabove=None, - linebelowheader=None, - linebetweenrows=None, - linebelow=None, - headerrow=partial(_moin_row_with_attrs, "||", header="'''"), - datarow=partial(_moin_row_with_attrs, "||"), - padding=1, - with_header_hide=None, - ), - "youtrack": TableFormat( - lineabove=None, - linebelowheader=None, - linebetweenrows=None, - linebelow=None, - headerrow=DataRow("|| ", " || ", " || "), - datarow=DataRow("| ", " | ", " |"), - padding=1, - with_header_hide=None, - ), - "html": TableFormat( - lineabove=_html_begin_table_without_header, - linebelowheader="", - linebetweenrows=None, - linebelow=Line("\n
", "", "", ""), - headerrow=partial(_html_row_with_attrs, "th", False), - datarow=partial(_html_row_with_attrs, "td", False), - padding=0, - with_header_hide=["lineabove"], - ), - "unsafehtml": TableFormat( - lineabove=_html_begin_table_without_header, - linebelowheader="", - linebetweenrows=None, - linebelow=Line("\n", "", "", ""), - headerrow=partial(_html_row_with_attrs, "th", True), - datarow=partial(_html_row_with_attrs, "td", True), - padding=0, - with_header_hide=["lineabove"], - ), - "latex": TableFormat( - lineabove=_latex_line_begin_tabular, - linebelowheader=Line("\\hline", "", "", ""), - linebetweenrows=None, - linebelow=Line("\\hline\n\\end{tabular}", "", "", ""), - headerrow=_latex_row, - datarow=_latex_row, - padding=1, - with_header_hide=None, - ), - "latex_raw": TableFormat( - lineabove=_latex_line_begin_tabular, - linebelowheader=Line("\\hline", "", "", ""), - linebetweenrows=None, - linebelow=Line("\\hline\n\\end{tabular}", "", "", ""), - headerrow=partial(_latex_row, escrules={}), - datarow=partial(_latex_row, escrules={}), - padding=1, - with_header_hide=None, - ), - "latex_booktabs": TableFormat( - lineabove=partial(_latex_line_begin_tabular, booktabs=True), - linebelowheader=Line("\\midrule", "", "", ""), - linebetweenrows=None, - linebelow=Line("\\bottomrule\n\\end{tabular}", "", "", ""), - headerrow=_latex_row, - datarow=_latex_row, - padding=1, - with_header_hide=None, - ), - "tsv": TableFormat( - lineabove=None, - linebelowheader=None, - linebetweenrows=None, - linebelow=None, - headerrow=DataRow("", "\t", ""), - datarow=DataRow("", "\t", ""), - padding=0, - with_header_hide=None, - ), - "textile": TableFormat( - lineabove=None, - linebelowheader=None, - linebetweenrows=None, - linebelow=None, - headerrow=DataRow("|_. ", "|_.", "|"), - datarow=_textile_row_with_attrs, - padding=1, - with_header_hide=None, - ), -} - - -tabulate_formats = list(sorted(_table_formats.keys())) - -# The table formats for which multiline cells will be folded into subsequent -# table rows. The key is the original format specified at the API. The value is -# the format that will be used to represent the original format. -multiline_formats = { - "plain": "plain", - "simple": "simple", - "grid": "grid", - "fancy_grid": "fancy_grid", - "pipe": "pipe", - "orgtbl": "orgtbl", - "jira": "jira", - "presto": "presto", - "pretty": "pretty", - "psql": "psql", - "rst": "rst", -} - -# TODO: Add multiline support for the remaining table formats: -# - mediawiki: Replace \n with
-# - moinmoin: TBD -# - youtrack: TBD -# - html: Replace \n with
-# - latex*: Use "makecell" package: In header, replace X\nY with -# \thead{X\\Y} and in data row, replace X\nY with \makecell{X\\Y} -# - tsv: TBD -# - textile: Replace \n with
(must be well-formed XML) - -_multiline_codes = re.compile(r"\r|\n|\r\n") -_multiline_codes_bytes = re.compile(b"\r|\n|\r\n") -_invisible_codes = re.compile( - r"\x1b\[\d+[;\d]*m|\x1b\[\d*\;\d*\;\d*m" -) # ANSI color codes -_invisible_codes_bytes = re.compile( - b"\x1b\\[\\d+\\[;\\d]*m|\x1b\\[\\d*;\\d*;\\d*m" -) # ANSI color codes - - -def simple_separated_format(separator): - """Construct a simple TableFormat with columns separated by a separator. - - >>> tsv = simple_separated_format("\\t") ; \ - tabulate([["foo", 1], ["spam", 23]], tablefmt=tsv) == 'foo \\t 1\\nspam\\t23' - True - - """ - return TableFormat( - None, - None, - None, - None, - headerrow=DataRow("", separator, ""), - datarow=DataRow("", separator, ""), - padding=0, - with_header_hide=None, - ) - - -def _isconvertible(conv, string): - try: - conv(string) - return True - except (ValueError, TypeError): - return False - - -def _isnumber(string): - """ - >>> _isnumber("123.45") - True - >>> _isnumber("123") - True - >>> _isnumber("spam") - False - >>> _isnumber("123e45678") - False - >>> _isnumber("inf") - True - """ - if not _isconvertible(float, string): - return False - elif isinstance(string, (_text_type, _binary_type)) and ( - math.isinf(float(string)) or math.isnan(float(string)) - ): - return string.lower() in ["inf", "-inf", "nan"] - return True - - -def _isint(string, inttype=int): - """ - >>> _isint("123") - True - >>> _isint("123.45") - False - """ - return ( - type(string) is inttype - or (isinstance(string, _binary_type) or isinstance(string, _text_type)) - and _isconvertible(inttype, string) - ) - - -def _isbool(string): - """ - >>> _isbool(True) - True - >>> _isbool("False") - True - >>> _isbool(1) - False - """ - return type(string) is _bool_type or ( - isinstance(string, (_binary_type, _text_type)) and string in ("True", "False") - ) - - -def _type(string, has_invisible=True, numparse=True): - """The least generic type (type(None), int, float, str, unicode). - - >>> _type(None) is type(None) - True - >>> _type("foo") is type("") - True - >>> _type("1") is type(1) - True - >>> _type('\x1b[31m42\x1b[0m') is type(42) - True - >>> _type('\x1b[31m42\x1b[0m') is type(42) - True - - """ - - if has_invisible and ( - isinstance(string, _text_type) or isinstance(string, _binary_type) - ): - string = _strip_invisible(string) - - if string is None: - return _none_type - elif hasattr(string, "isoformat"): # datetime.datetime, date, and time - return _text_type - elif _isbool(string): - return _bool_type - elif _isint(string) and numparse: - return int - elif _isint(string, _long_type) and numparse: - return int - elif _isnumber(string) and numparse: - return float - elif isinstance(string, _binary_type): - return _binary_type - else: - return _text_type - - -def _afterpoint(string): - """Symbols after a decimal point, -1 if the string lacks the decimal point. - - >>> _afterpoint("123.45") - 2 - >>> _afterpoint("1001") - -1 - >>> _afterpoint("eggs") - -1 - >>> _afterpoint("123e45") - 2 - - """ - if _isnumber(string): - if _isint(string): - return -1 - else: - pos = string.rfind(".") - pos = string.lower().rfind("e") if pos < 0 else pos - if pos >= 0: - return len(string) - pos - 1 - else: - return -1 # no point - else: - return -1 # not a number - - -def _padleft(width, s): - """Flush right. - - >>> _padleft(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430' - True - - """ - fmt = "{0:>%ds}" % width - return fmt.format(s) - - -def _padright(width, s): - """Flush left. - - >>> _padright(6, '\u044f\u0439\u0446\u0430') == '\u044f\u0439\u0446\u0430 ' - True - - """ - fmt = "{0:<%ds}" % width - return fmt.format(s) - - -def _padboth(width, s): - """Center string. - - >>> _padboth(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430 ' - True - - """ - fmt = "{0:^%ds}" % width - return fmt.format(s) - - -def _padnone(ignore_width, s): - return s - - -def _strip_invisible(s): - "Remove invisible ANSI color codes." - if isinstance(s, _text_type): - return re.sub(_invisible_codes, "", s) - else: # a bytestring - return re.sub(_invisible_codes_bytes, "", s) - - -def _visible_width(s): - """Visible width of a printed string. ANSI color codes are removed. - - >>> _visible_width('\x1b[31mhello\x1b[0m'), _visible_width("world") - (5, 5) - - """ - # optional wide-character support - if wcwidth is not None and WIDE_CHARS_MODE: - len_fn = wcwidth.wcswidth - else: - len_fn = len - if isinstance(s, _text_type) or isinstance(s, _binary_type): - return len_fn(_strip_invisible(s)) - else: - return len_fn(_text_type(s)) - - -def _is_multiline(s): - if isinstance(s, _text_type): - return bool(re.search(_multiline_codes, s)) - else: # a bytestring - return bool(re.search(_multiline_codes_bytes, s)) - - -def _multiline_width(multiline_s, line_width_fn=len): - """Visible width of a potentially multiline content.""" - return max(map(line_width_fn, re.split("[\r\n]", multiline_s))) - - -def _choose_width_fn(has_invisible, enable_widechars, is_multiline): - """Return a function to calculate visible cell width.""" - if has_invisible: - line_width_fn = _visible_width - elif enable_widechars: # optional wide-character support if available - line_width_fn = wcwidth.wcswidth - else: - line_width_fn = len - if is_multiline: - width_fn = lambda s: _multiline_width(s, line_width_fn) # noqa - else: - width_fn = line_width_fn - return width_fn - - -def _align_column_choose_padfn(strings, alignment, has_invisible): - if alignment == "right": - if not PRESERVE_WHITESPACE: - strings = [s.strip() for s in strings] - padfn = _padleft - elif alignment == "center": - if not PRESERVE_WHITESPACE: - strings = [s.strip() for s in strings] - padfn = _padboth - elif alignment == "decimal": - if has_invisible: - decimals = [_afterpoint(_strip_invisible(s)) for s in strings] - else: - decimals = [_afterpoint(s) for s in strings] - maxdecimals = max(decimals) - strings = [s + (maxdecimals - decs) * " " for s, decs in zip(strings, decimals)] - padfn = _padleft - elif not alignment: - padfn = _padnone - else: - if not PRESERVE_WHITESPACE: - strings = [s.strip() for s in strings] - padfn = _padright - return strings, padfn - - -def _align_column( - strings, - alignment, - minwidth=0, - has_invisible=True, - enable_widechars=False, - is_multiline=False, -): - """[string] -> [padded_string]""" - strings, padfn = _align_column_choose_padfn(strings, alignment, has_invisible) - width_fn = _choose_width_fn(has_invisible, enable_widechars, is_multiline) - - s_widths = list(map(width_fn, strings)) - maxwidth = max(max(s_widths), minwidth) - # TODO: refactor column alignment in single-line and multiline modes - if is_multiline: - if not enable_widechars and not has_invisible: - padded_strings = [ - "\n".join([padfn(maxwidth, s) for s in ms.splitlines()]) - for ms in strings - ] - else: - # enable wide-character width corrections - s_lens = [max((len(s) for s in re.split("[\r\n]", ms))) for ms in strings] - visible_widths = [maxwidth - (w - l) for w, l in zip(s_widths, s_lens)] - # wcswidth and _visible_width don't count invisible characters; - # padfn doesn't need to apply another correction - padded_strings = [ - "\n".join([padfn(w, s) for s in (ms.splitlines() or ms)]) - for ms, w in zip(strings, visible_widths) - ] - else: # single-line cell values - if not enable_widechars and not has_invisible: - padded_strings = [padfn(maxwidth, s) for s in strings] - else: - # enable wide-character width corrections - s_lens = list(map(len, strings)) - visible_widths = [maxwidth - (w - l) for w, l in zip(s_widths, s_lens)] - # wcswidth and _visible_width don't count invisible characters; - # padfn doesn't need to apply another correction - padded_strings = [padfn(w, s) for s, w in zip(strings, visible_widths)] - return padded_strings - - -def _more_generic(type1, type2): - types = { - _none_type: 0, - _bool_type: 1, - int: 2, - float: 3, - _binary_type: 4, - _text_type: 5, - } - invtypes = { - 5: _text_type, - 4: _binary_type, - 3: float, - 2: int, - 1: _bool_type, - 0: _none_type, - } - moregeneric = max(types.get(type1, 5), types.get(type2, 5)) - return invtypes[moregeneric] - - -def _column_type(strings, has_invisible=True, numparse=True): - """The least generic type all column values are convertible to. - - >>> _column_type([True, False]) is _bool_type - True - >>> _column_type(["1", "2"]) is _int_type - True - >>> _column_type(["1", "2.3"]) is _float_type - True - >>> _column_type(["1", "2.3", "four"]) is _text_type - True - >>> _column_type(["four", '\u043f\u044f\u0442\u044c']) is _text_type - True - >>> _column_type([None, "brux"]) is _text_type - True - >>> _column_type([1, 2, None]) is _int_type - True - >>> import datetime as dt - >>> _column_type([dt.datetime(1991,2,19), dt.time(17,35)]) is _text_type - True - - """ - types = [_type(s, has_invisible, numparse) for s in strings] - return reduce(_more_generic, types, _bool_type) - - -def _format(val, valtype, floatfmt, missingval="", has_invisible=True): - """Format a value accoding to its type. - - Unicode is supported: - - >>> hrow = ['\u0431\u0443\u043a\u0432\u0430', '\u0446\u0438\u0444\u0440\u0430'] ; \ - tbl = [['\u0430\u0437', 2], ['\u0431\u0443\u043a\u0438', 4]] ; \ - good_result = '\\u0431\\u0443\\u043a\\u0432\\u0430 \\u0446\\u0438\\u0444\\u0440\\u0430\\n------- -------\\n\\u0430\\u0437 2\\n\\u0431\\u0443\\u043a\\u0438 4' ; \ - tabulate(tbl, headers=hrow) == good_result - True - - """ # noqa - if val is None: - return missingval - - if valtype in [int, _text_type]: - return "{0}".format(val) - elif valtype is _binary_type: - try: - return _text_type(val, "ascii") - except TypeError: - return _text_type(val) - elif valtype is float: - is_a_colored_number = has_invisible and isinstance( - val, (_text_type, _binary_type) - ) - if is_a_colored_number: - raw_val = _strip_invisible(val) - formatted_val = format(float(raw_val), floatfmt) - return val.replace(raw_val, formatted_val) - else: - return format(float(val), floatfmt) - else: - return "{0}".format(val) - - -def _align_header( - header, alignment, width, visible_width, is_multiline=False, width_fn=None -): - "Pad string header to width chars given known visible_width of the header." - if is_multiline: - header_lines = re.split(_multiline_codes, header) - padded_lines = [ - _align_header(h, alignment, width, width_fn(h)) for h in header_lines - ] - return "\n".join(padded_lines) - # else: not multiline - ninvisible = len(header) - visible_width - width += ninvisible - if alignment == "left": - return _padright(width, header) - elif alignment == "center": - return _padboth(width, header) - elif not alignment: - return "{0}".format(header) - else: - return _padleft(width, header) - - -def _prepend_row_index(rows, index): - """Add a left-most index column.""" - if index is None or index is False: - return rows - if len(index) != len(rows): - print("index=", index) - print("rows=", rows) - raise ValueError("index must be as long as the number of data rows") - rows = [[v] + list(row) for v, row in zip(index, rows)] - return rows - - -def _bool(val): - "A wrapper around standard bool() which doesn't throw on NumPy arrays" - try: - return bool(val) - except ValueError: # val is likely to be a numpy array with many elements - return False - - -def _normalize_tabular_data(tabular_data, headers, showindex="default"): - """Transform a supported data type to a list of lists, and a list of headers. - - Supported tabular data types: - - * list-of-lists or another iterable of iterables - - * list of named tuples (usually used with headers="keys") - - * list of dicts (usually used with headers="keys") - - * list of OrderedDicts (usually used with headers="keys") - - * 2D NumPy arrays - - * NumPy record arrays (usually used with headers="keys") - - * dict of iterables (usually used with headers="keys") - - * pandas.DataFrame (usually used with headers="keys") - - The first row can be used as headers if headers="firstrow", - column indices can be used as headers if headers="keys". - - If showindex="default", show row indices of the pandas.DataFrame. - If showindex="always", show row indices for all types of data. - If showindex="never", don't show row indices for all types of data. - If showindex is an iterable, show its values as row indices. - - """ - - try: - bool(headers) - is_headers2bool_broken = False # noqa - except ValueError: # numpy.ndarray, pandas.core.index.Index, ... - is_headers2bool_broken = True # noqa - headers = list(headers) - - index = None - if hasattr(tabular_data, "keys") and hasattr(tabular_data, "values"): - # dict-like and pandas.DataFrame? - if hasattr(tabular_data.values, "__call__"): - # likely a conventional dict - keys = tabular_data.keys() - rows = list( - izip_longest(*tabular_data.values()) - ) # columns have to be transposed - elif hasattr(tabular_data, "index"): - # values is a property, has .index => it's likely a pandas.DataFrame (pandas 0.11.0) - keys = list(tabular_data) - if tabular_data.index.name is not None: - if isinstance(tabular_data.index.name, list): - keys[:0] = tabular_data.index.name - else: - keys[:0] = [tabular_data.index.name] - vals = tabular_data.values # values matrix doesn't need to be transposed - # for DataFrames add an index per default - index = list(tabular_data.index) - rows = [list(row) for row in vals] - else: - raise ValueError("tabular data doesn't appear to be a dict or a DataFrame") - - if headers == "keys": - headers = list(map(_text_type, keys)) # headers should be strings - - else: # it's a usual an iterable of iterables, or a NumPy array - rows = list(tabular_data) - - if headers == "keys" and not rows: - # an empty table (issue #81) - headers = [] - elif ( - headers == "keys" - and hasattr(tabular_data, "dtype") - and getattr(tabular_data.dtype, "names") - ): - # numpy record array - headers = tabular_data.dtype.names - elif ( - headers == "keys" - and len(rows) > 0 - and isinstance(rows[0], tuple) - and hasattr(rows[0], "_fields") - ): - # namedtuple - headers = list(map(_text_type, rows[0]._fields)) - elif len(rows) > 0 and isinstance(rows[0], dict): - # dict or OrderedDict - uniq_keys = set() # implements hashed lookup - keys = [] # storage for set - if headers == "firstrow": - firstdict = rows[0] if len(rows) > 0 else {} - keys.extend(firstdict.keys()) - uniq_keys.update(keys) - rows = rows[1:] - for row in rows: - for k in row.keys(): - # Save unique items in input order - if k not in uniq_keys: - keys.append(k) - uniq_keys.add(k) - if headers == "keys": - headers = keys - elif isinstance(headers, dict): - # a dict of headers for a list of dicts - headers = [headers.get(k, k) for k in keys] - headers = list(map(_text_type, headers)) - elif headers == "firstrow": - if len(rows) > 0: - headers = [firstdict.get(k, k) for k in keys] - headers = list(map(_text_type, headers)) - else: - headers = [] - elif headers: - raise ValueError( - "headers for a list of dicts is not a dict or a keyword" - ) - rows = [[row.get(k) for k in keys] for row in rows] - - elif ( - headers == "keys" - and hasattr(tabular_data, "description") - and hasattr(tabular_data, "fetchone") - and hasattr(tabular_data, "rowcount") - ): - # Python Database API cursor object (PEP 0249) - # print tabulate(cursor, headers='keys') - headers = [column[0] for column in tabular_data.description] - - elif headers == "keys" and len(rows) > 0: - # keys are column indices - headers = list(map(_text_type, range(len(rows[0])))) - - # take headers from the first row if necessary - if headers == "firstrow" and len(rows) > 0: - if index is not None: - headers = [index[0]] + list(rows[0]) - index = index[1:] - else: - headers = rows[0] - headers = list(map(_text_type, headers)) # headers should be strings - rows = rows[1:] - - headers = list(map(_text_type, headers)) - rows = list(map(list, rows)) - - # add or remove an index column - showindex_is_a_str = type(showindex) in [_text_type, _binary_type] - if showindex == "default" and index is not None: - rows = _prepend_row_index(rows, index) - elif isinstance(showindex, Iterable) and not showindex_is_a_str: - rows = _prepend_row_index(rows, list(showindex)) - elif showindex == "always" or (_bool(showindex) and not showindex_is_a_str): - if index is None: - index = list(range(len(rows))) - rows = _prepend_row_index(rows, index) - elif showindex == "never" or (not _bool(showindex) and not showindex_is_a_str): - pass - - # pad with empty headers for initial columns if necessary - if headers and len(rows) > 0: - nhs = len(headers) - ncols = len(rows[0]) - if nhs < ncols: - headers = [""] * (ncols - nhs) + headers - - return rows, headers - - -def tabulate( - tabular_data, - headers=(), - tablefmt="simple", - floatfmt=_DEFAULT_FLOATFMT, - numalign="decimal", - stralign="left", - missingval=_DEFAULT_MISSINGVAL, - showindex="default", - disable_numparse=False, - colalign=None, -): - """Format a fixed width table for pretty printing. - - >>> print(tabulate([[1, 2.34], [-56, "8.999"], ["2", "10001"]])) - --- --------- - 1 2.34 - -56 8.999 - 2 10001 - --- --------- - - The first required argument (`tabular_data`) can be a - list-of-lists (or another iterable of iterables), a list of named - tuples, a dictionary of iterables, an iterable of dictionaries, - a two-dimensional NumPy array, NumPy record array, or a Pandas' - dataframe. - - - Table headers - ------------- - - To print nice column headers, supply the second argument (`headers`): - - - `headers` can be an explicit list of column headers - - if `headers="firstrow"`, then the first row of data is used - - if `headers="keys"`, then dictionary keys or column indices are used - - Otherwise a headerless table is produced. - - If the number of headers is less than the number of columns, they - are supposed to be names of the last columns. This is consistent - with the plain-text format of R and Pandas' dataframes. - - >>> print(tabulate([["sex","age"],["Alice","F",24],["Bob","M",19]], - ... headers="firstrow")) - sex age - ----- ----- ----- - Alice F 24 - Bob M 19 - - By default, pandas.DataFrame data have an additional column called - row index. To add a similar column to all other types of data, - use `showindex="always"` or `showindex=True`. To suppress row indices - for all types of data, pass `showindex="never" or `showindex=False`. - To add a custom row index column, pass `showindex=some_iterable`. - - >>> print(tabulate([["F",24],["M",19]], showindex="always")) - - - -- - 0 F 24 - 1 M 19 - - - -- - - - Column alignment - ---------------- - - `tabulate` tries to detect column types automatically, and aligns - the values properly. By default it aligns decimal points of the - numbers (or flushes integer numbers to the right), and flushes - everything else to the left. Possible column alignments - (`numalign`, `stralign`) are: "right", "center", "left", "decimal" - (only for `numalign`), and None (to disable alignment). - - - Table formats - ------------- - - `floatfmt` is a format specification used for columns which - contain numeric data with a decimal point. This can also be - a list or tuple of format strings, one per column. - - `None` values are replaced with a `missingval` string (like - `floatfmt`, this can also be a list of values for different - columns): - - >>> print(tabulate([["spam", 1, None], - ... ["eggs", 42, 3.14], - ... ["other", None, 2.7]], missingval="?")) - ----- -- ---- - spam 1 ? - eggs 42 3.14 - other ? 2.7 - ----- -- ---- - - Various plain-text table formats (`tablefmt`) are supported: - 'plain', 'simple', 'grid', 'pipe', 'orgtbl', 'rst', 'mediawiki', - 'latex', 'latex_raw' and 'latex_booktabs'. Variable `tabulate_formats` - contains the list of currently supported formats. - - "plain" format doesn't use any pseudographics to draw tables, - it separates columns with a double space: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... ["strings", "numbers"], "plain")) - strings numbers - spam 41.9999 - eggs 451 - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="plain")) - spam 41.9999 - eggs 451 - - "simple" format is like Pandoc simple_tables: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... ["strings", "numbers"], "simple")) - strings numbers - --------- --------- - spam 41.9999 - eggs 451 - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="simple")) - ---- -------- - spam 41.9999 - eggs 451 - ---- -------- - - "grid" is similar to tables produced by Emacs table.el package or - Pandoc grid_tables: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... ["strings", "numbers"], "grid")) - +-----------+-----------+ - | strings | numbers | - +===========+===========+ - | spam | 41.9999 | - +-----------+-----------+ - | eggs | 451 | - +-----------+-----------+ - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="grid")) - +------+----------+ - | spam | 41.9999 | - +------+----------+ - | eggs | 451 | - +------+----------+ - - "fancy_grid" draws a grid using box-drawing characters: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... ["strings", "numbers"], "fancy_grid")) - ╒═══════════╤═══════════╕ - │ strings │ numbers │ - ╞═══════════╪═══════════╡ - │ spam │ 41.9999 │ - ├───────────┼───────────┤ - │ eggs │ 451 │ - ╘═══════════╧═══════════╛ - - "pipe" is like tables in PHP Markdown Extra extension or Pandoc - pipe_tables: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... ["strings", "numbers"], "pipe")) - | strings | numbers | - |:----------|----------:| - | spam | 41.9999 | - | eggs | 451 | - - "presto" is like tables produce by the Presto CLI: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... ["strings", "numbers"], "presto")) - strings | numbers - -----------+----------- - spam | 41.9999 - eggs | 451 - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="pipe")) - |:-----|---------:| - | spam | 41.9999 | - | eggs | 451 | - - "orgtbl" is like tables in Emacs org-mode and orgtbl-mode. They - are slightly different from "pipe" format by not using colons to - define column alignment, and using a "+" sign to indicate line - intersections: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... ["strings", "numbers"], "orgtbl")) - | strings | numbers | - |-----------+-----------| - | spam | 41.9999 | - | eggs | 451 | - - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="orgtbl")) - | spam | 41.9999 | - | eggs | 451 | - - "rst" is like a simple table format from reStructuredText; please - note that reStructuredText accepts also "grid" tables: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], - ... ["strings", "numbers"], "rst")) - ========= ========= - strings numbers - ========= ========= - spam 41.9999 - eggs 451 - ========= ========= - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="rst")) - ==== ======== - spam 41.9999 - eggs 451 - ==== ======== - - "mediawiki" produces a table markup used in Wikipedia and on other - MediaWiki-based sites: - - >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]], - ... headers="firstrow", tablefmt="mediawiki")) - {| class="wikitable" style="text-align: left;" - |+ - |- - ! strings !! align="right"| numbers - |- - | spam || align="right"| 41.9999 - |- - | eggs || align="right"| 451 - |} - - "html" produces HTML markup as an html.escape'd str - with a ._repr_html_ method so that Jupyter Lab and Notebook display the HTML - and a .str property so that the raw HTML remains accessible - the unsafehtml table format can be used if an unescaped HTML format is required: - - >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]], - ... headers="firstrow", tablefmt="html")) - - - - - - - - -
strings numbers
spam 41.9999
eggs 451
- - "latex" produces a tabular environment of LaTeX document markup: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex")) - \\begin{tabular}{lr} - \\hline - spam & 41.9999 \\\\ - eggs & 451 \\\\ - \\hline - \\end{tabular} - - "latex_raw" is similar to "latex", but doesn't escape special characters, - such as backslash and underscore, so LaTeX commands may embedded into - cells' values: - - >>> print(tabulate([["spam$_9$", 41.9999], ["\\\\emph{eggs}", "451.0"]], tablefmt="latex_raw")) - \\begin{tabular}{lr} - \\hline - spam$_9$ & 41.9999 \\\\ - \\emph{eggs} & 451 \\\\ - \\hline - \\end{tabular} - - "latex_booktabs" produces a tabular environment of LaTeX document markup - using the booktabs.sty package: - - >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex_booktabs")) - \\begin{tabular}{lr} - \\toprule - spam & 41.9999 \\\\ - eggs & 451 \\\\ - \\bottomrule - \\end{tabular} - - Number parsing - -------------- - By default, anything which can be parsed as a number is a number. - This ensures numbers represented as strings are aligned properly. - This can lead to weird results for particular strings such as - specific git SHAs e.g. "42992e1" will be parsed into the number - 429920 and aligned as such. - - To completely disable number parsing (and alignment), use - `disable_numparse=True`. For more fine grained control, a list column - indices is used to disable number parsing only on those columns - e.g. `disable_numparse=[0, 2]` would disable number parsing only on the - first and third columns. - """ - - if tabular_data is None: - tabular_data = [] - list_of_lists, headers = _normalize_tabular_data( - tabular_data, headers, showindex=showindex - ) - - # empty values in the first column of RST tables should be escaped (issue #82) - # "" should be escaped as "\\ " or ".." - if tablefmt == "rst": - list_of_lists, headers = _rst_escape_first_column(list_of_lists, headers) - - # PrettyTable formatting does not use any extra padding. - # Numbers are not parsed and are treated the same as strings for alignment. - # Check if pretty is the format being used and override the defaults so it - # does not impact other formats. - min_padding = MIN_PADDING - if tablefmt == "pretty": - min_padding = 0 - disable_numparse = True - numalign = "center" - stralign = "center" - - # optimization: look for ANSI control codes once, - # enable smart width functions only if a control code is found - plain_text = "\t".join( - ["\t".join(map(_text_type, headers))] - + ["\t".join(map(_text_type, row)) for row in list_of_lists] - ) - - has_invisible = re.search(_invisible_codes, plain_text) - enable_widechars = wcwidth is not None and WIDE_CHARS_MODE - if ( - not isinstance(tablefmt, TableFormat) - and tablefmt in multiline_formats - and _is_multiline(plain_text) - ): - tablefmt = multiline_formats.get(tablefmt, tablefmt) - is_multiline = True - else: - is_multiline = False - width_fn = _choose_width_fn(has_invisible, enable_widechars, is_multiline) - - # format rows and columns, convert numeric values to strings - cols = list(izip_longest(*list_of_lists)) - numparses = _expand_numparse(disable_numparse, len(cols)) - coltypes = [_column_type(col, numparse=np) for col, np in zip(cols, numparses)] - if isinstance(floatfmt, basestring): # old version - float_formats = len(cols) * [ - floatfmt - ] # just duplicate the string to use in each column - else: # if floatfmt is list, tuple etc we have one per column - float_formats = list(floatfmt) - if len(float_formats) < len(cols): - float_formats.extend((len(cols) - len(float_formats)) * [_DEFAULT_FLOATFMT]) - if isinstance(missingval, basestring): - missing_vals = len(cols) * [missingval] - else: - missing_vals = list(missingval) - if len(missing_vals) < len(cols): - missing_vals.extend((len(cols) - len(missing_vals)) * [_DEFAULT_MISSINGVAL]) - cols = [ - [_format(v, ct, fl_fmt, miss_v, has_invisible) for v in c] - for c, ct, fl_fmt, miss_v in zip(cols, coltypes, float_formats, missing_vals) - ] - - # align columns - aligns = [numalign if ct in [int, float] else stralign for ct in coltypes] - if colalign is not None: - assert isinstance(colalign, Iterable) - for idx, align in enumerate(colalign): - aligns[idx] = align - minwidths = ( - [width_fn(h) + min_padding for h in headers] if headers else [0] * len(cols) - ) - cols = [ - _align_column(c, a, minw, has_invisible, enable_widechars, is_multiline) - for c, a, minw in zip(cols, aligns, minwidths) - ] - - if headers: - # align headers and add headers - t_cols = cols or [[""]] * len(headers) - t_aligns = aligns or [stralign] * len(headers) - minwidths = [ - max(minw, max(width_fn(cl) for cl in c)) - for minw, c in zip(minwidths, t_cols) - ] - headers = [ - _align_header(h, a, minw, width_fn(h), is_multiline, width_fn) - for h, a, minw in zip(headers, t_aligns, minwidths) - ] - rows = list(zip(*cols)) - else: - minwidths = [max(width_fn(cl) for cl in c) for c in cols] - rows = list(zip(*cols)) - - if not isinstance(tablefmt, TableFormat): - tablefmt = _table_formats.get(tablefmt, _table_formats["simple"]) - - return _format_table(tablefmt, headers, rows, minwidths, aligns, is_multiline) - - -def _expand_numparse(disable_numparse, column_count): - """ - Return a list of bools of length `column_count` which indicates whether - number parsing should be used on each column. - If `disable_numparse` is a list of indices, each of those indices are False, - and everything else is True. - If `disable_numparse` is a bool, then the returned list is all the same. - """ - if isinstance(disable_numparse, Iterable): - numparses = [True] * column_count - for index in disable_numparse: - numparses[index] = False - return numparses - else: - return [not disable_numparse] * column_count - - -def _pad_row(cells, padding): - if cells: - pad = " " * padding - padded_cells = [pad + cell + pad for cell in cells] - return padded_cells - else: - return cells - - -def _build_simple_row(padded_cells, rowfmt): - "Format row according to DataRow format without padding." - begin, sep, end = rowfmt - return (begin + sep.join(padded_cells) + end).rstrip() - - -def _build_row(padded_cells, colwidths, colaligns, rowfmt): - "Return a string which represents a row of data cells." - if not rowfmt: - return None - if hasattr(rowfmt, "__call__"): - return rowfmt(padded_cells, colwidths, colaligns) - else: - return _build_simple_row(padded_cells, rowfmt) - - -def _append_basic_row(lines, padded_cells, colwidths, colaligns, rowfmt): - lines.append(_build_row(padded_cells, colwidths, colaligns, rowfmt)) - return lines - - -def _append_multiline_row( - lines, padded_multiline_cells, padded_widths, colaligns, rowfmt, pad -): - colwidths = [w - 2 * pad for w in padded_widths] - cells_lines = [c.splitlines() for c in padded_multiline_cells] - nlines = max(map(len, cells_lines)) # number of lines in the row - # vertically pad cells where some lines are missing - cells_lines = [ - (cl + [" " * w] * (nlines - len(cl))) for cl, w in zip(cells_lines, colwidths) - ] - lines_cells = [[cl[i] for cl in cells_lines] for i in range(nlines)] - for ln in lines_cells: - padded_ln = _pad_row(ln, pad) - _append_basic_row(lines, padded_ln, colwidths, colaligns, rowfmt) - return lines - - -def _build_line(colwidths, colaligns, linefmt): - "Return a string which represents a horizontal line." - if not linefmt: - return None - if hasattr(linefmt, "__call__"): - return linefmt(colwidths, colaligns) - else: - begin, fill, sep, end = linefmt - cells = [fill * w for w in colwidths] - return _build_simple_row(cells, (begin, sep, end)) - - -def _append_line(lines, colwidths, colaligns, linefmt): - lines.append(_build_line(colwidths, colaligns, linefmt)) - return lines - - -class JupyterHTMLStr(str): - """Wrap the string with a _repr_html_ method so that Jupyter - displays the HTML table""" - - def _repr_html_(self): - return self - - @property - def str(self): - """add a .str property so that the raw string is still accessible""" - return self - - -def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline): - """Produce a plain-text representation of the table.""" - lines = [] - hidden = fmt.with_header_hide if (headers and fmt.with_header_hide) else [] - pad = fmt.padding - headerrow = fmt.headerrow - - padded_widths = [(w + 2 * pad) for w in colwidths] - if is_multiline: - pad_row = lambda row, _: row # noqa do it later, in _append_multiline_row - append_row = partial(_append_multiline_row, pad=pad) - else: - pad_row = _pad_row - append_row = _append_basic_row - - padded_headers = pad_row(headers, pad) - padded_rows = [pad_row(row, pad) for row in rows] - - if fmt.lineabove and "lineabove" not in hidden: - _append_line(lines, padded_widths, colaligns, fmt.lineabove) - - if padded_headers: - append_row(lines, padded_headers, padded_widths, colaligns, headerrow) - if fmt.linebelowheader and "linebelowheader" not in hidden: - _append_line(lines, padded_widths, colaligns, fmt.linebelowheader) - - if padded_rows and fmt.linebetweenrows and "linebetweenrows" not in hidden: - # initial rows with a line below - for row in padded_rows[:-1]: - append_row(lines, row, padded_widths, colaligns, fmt.datarow) - _append_line(lines, padded_widths, colaligns, fmt.linebetweenrows) - # the last row without a line below - append_row(lines, padded_rows[-1], padded_widths, colaligns, fmt.datarow) - else: - for row in padded_rows: - append_row(lines, row, padded_widths, colaligns, fmt.datarow) - - if fmt.linebelow and "linebelow" not in hidden: - _append_line(lines, padded_widths, colaligns, fmt.linebelow) - - if headers or rows: - output = "\n".join(lines) - if fmt.lineabove == _html_begin_table_without_header: - return JupyterHTMLStr(output) - else: - return output - else: # a completely empty table - return "" - - -def _main(): - """\ - Usage: tabulate [options] [FILE ...] - - Pretty-print tabular data. - See also https://github.com/astanin/python-tabulate - - FILE a filename of the file with tabular data; - if "-" or missing, read data from stdin. - - Options: - - -h, --help show this message - -1, --header use the first row of data as a table header - -o FILE, --output FILE print table to FILE (default: stdout) - -s REGEXP, --sep REGEXP use a custom column separator (default: whitespace) - -F FPFMT, --float FPFMT floating point number format (default: g) - -f FMT, --format FMT set output table format; supported formats: - plain, simple, grid, fancy_grid, pipe, orgtbl, - rst, mediawiki, html, latex, latex_raw, - latex_booktabs, tsv - (default: simple) - """ - import getopt - import sys - import textwrap - - usage = textwrap.dedent(_main.__doc__) - try: - opts, args = getopt.getopt( - sys.argv[1:], - "h1o:s:F:A:f:", - ["help", "header", "output", "sep=", "float=", "align=", "format="], - ) - except getopt.GetoptError as e: - print(e) - print(usage) - sys.exit(2) - headers = [] - floatfmt = _DEFAULT_FLOATFMT - colalign = None - tablefmt = "simple" - sep = r"\s+" - outfile = "-" - for opt, value in opts: - if opt in ["-1", "--header"]: - headers = "firstrow" - elif opt in ["-o", "--output"]: - outfile = value - elif opt in ["-F", "--float"]: - floatfmt = value - elif opt in ["-C", "--colalign"]: - colalign = value.split() - elif opt in ["-f", "--format"]: - if value not in tabulate_formats: - print("%s is not a supported table format" % value) - print(usage) - sys.exit(3) - tablefmt = value - elif opt in ["-s", "--sep"]: - sep = value - elif opt in ["-h", "--help"]: - print(usage) - sys.exit(0) - files = [sys.stdin] if not args else args - with (sys.stdout if outfile == "-" else open(outfile, "w")) as out: - for f in files: - if f == "-": - f = sys.stdin - if _is_file(f): - _pprint_file( - f, - headers=headers, - tablefmt=tablefmt, - sep=sep, - floatfmt=floatfmt, - file=out, - colalign=colalign, - ) - else: - with open(f) as fobj: - _pprint_file( - fobj, - headers=headers, - tablefmt=tablefmt, - sep=sep, - floatfmt=floatfmt, - file=out, - colalign=colalign, - ) - - -def _pprint_file(fobject, headers, tablefmt, sep, floatfmt, file, colalign): - rows = fobject.readlines() - table = [re.split(sep, r.rstrip()) for r in rows if r.strip()] - print( - tabulate(table, headers, tablefmt, floatfmt=floatfmt, colalign=colalign), - file=file, - ) - - -if __name__ == "__main__": - _main() diff --git a/jans-cli/cli/scim_cli.py b/jans-cli/cli/scim_cli.py deleted file mode 100644 index 79fded18a64..00000000000 --- a/jans-cli/cli/scim_cli.py +++ /dev/null @@ -1 +0,0 @@ -config-cli.py \ No newline at end of file diff --git a/jans-cli/cli/version.py b/jans-cli/cli/version.py deleted file mode 100644 index 9fbd42fb085..00000000000 --- a/jans-cli/cli/version.py +++ /dev/null @@ -1,6 +0,0 @@ -""" - License terms and conditions for Janssen: - https://www.apache.org/licenses/LICENSE-2.0 -""" - -__version__ = "1.0.5-dev" diff --git a/jans-cli/setup.py b/jans-cli/setup.py deleted file mode 100644 index ce163b0c6f0..00000000000 --- a/jans-cli/setup.py +++ /dev/null @@ -1,84 +0,0 @@ -""" - License terms and conditions for Janssen: - https://www.apache.org/licenses/LICENSE-2.0 -""" - -import codecs -import os -import re -from setuptools import setup -from setuptools import find_packages -from setuptools.command.install import install -from urllib.request import urlretrieve - -class PostInstallCommand(install): - """Post-installation for installation mode.""" - def run(self): - install.run(self) - - urlretrieve( - 'https://raw.githubusercontent.com/JanssenProject/jans/main/jans-config-api/docs/jans-config-api-swagger.yaml', - os.path.join(self.install_lib, 'cli/jca.yaml') - ) - - urlretrieve( - 'https://raw.githubusercontent.com/JanssenProject/jans/main/jans-scim/server/src/main/resources/jans-scim-openapi.yaml', - os.path.join(self.install_lib, 'cli/scim.yaml') - ) - - -def find_version(*file_paths): - here = os.path.abspath(os.path.dirname(__file__)) - with codecs.open(os.path.join(here, *file_paths), 'r') as f: - version_file = f.read() - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", - version_file, re.M) - if version_match: - return version_match.group(1) - raise RuntimeError("Unable to find version string.") - -setup( - name="jans-cli", - version=find_version("cli", "version.py"), - url="", - copyright="Copyright 2021, Janssen", - license="Apache 2.0 ", - author="Janssen", - author_email="", - maintainer="", - status="Dev", - description="", - long_description=__doc__, - packages=find_packages(), - package_data={'': ['*.yaml']}, - zip_safe=False, - install_requires=[ - "ruamel.yaml>=0.16.5", - "PyJWT==2.3.0", - "certifi", - "six", - "prompt_toolkit", - "jca-swagger-client @ https://ox.gluu.org/icrby8xcvbcv/cli-swagger/jca_swagger_client.zip", - "scim_swagger_client @ https://ox.gluu.org/icrby8xcvbcv/cli-swagger/scim_swagger_client.zip", - - ], - classifiers=[ - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache 2.0 License", - "Topic :: Software Development :: Libraries :: Python Modules", - "Programming Language :: Python", - "Programming Language :: Python :: 3s", - "Programming Language :: Python :: 3.6", - ], - include_package_data=True, - - entry_points={ - "console_scripts": [ - "config-cli=cli.config_cli:main", - "scim-cli=cli.config_cli:main", - ], - }, - cmdclass={ - 'install': PostInstallCommand, - }, -) diff --git a/jans-linux-setup/jans_setup/app_info.json b/jans-linux-setup/jans_setup/app_info.json index e7b65c4a0ba..4de97a103b5 100644 --- a/jans-linux-setup/jans_setup/app_info.json +++ b/jans-linux-setup/jans_setup/app_info.json @@ -9,6 +9,8 @@ "EXTERNAL_LIBS": "https://ox.gluu.org/icrby8xcvbcv/", "APPLE_WEBAUTHN": "https://www.apple.com/certificateauthority/Apple_WebAuthn_Root_CA.pem", "SQLALCHEMY": "https://github.com/sqlalchemy/sqlalchemy/archive/rel_1_3_23.zip", - "PYJWT": "https://github.com/jpadilla/pyjwt/archive/refs/tags/2.3.0.zip" - + "PYJWT": "https://github.com/jpadilla/pyjwt/archive/refs/tags/2.3.0.zip", + "PROMPT_TOOLKIT": "https://github.com/prompt-toolkit/python-prompt-toolkit/archive/refs/tags/3.0.33.zip", + "WCWIDTH": "https://github.com/jquast/wcwidth/archive/refs/tags/0.2.5.zip", + "PYGMENTS": "https://github.com/pygments/pygments/archive/refs/tags/2.13.0.zip" } diff --git a/jans-linux-setup/jans_setup/jans_setup.py b/jans-linux-setup/jans_setup/jans_setup.py index d214e9548bd..621661aad15 100755 --- a/jans-linux-setup/jans_setup/jans_setup.py +++ b/jans-linux-setup/jans_setup/jans_setup.py @@ -457,17 +457,17 @@ def do_installation(): do_installation() print('\n', static.colors.OKGREEN) if Config.install_config_api or Config.install_scim_server: - msg.installation_completed += "CLI available to manage Jannsen Server:\n" + msg.installation_completed += "Experimental CLI TUI is available to manage Jannsen Server:\n" if Config.install_config_api: - msg.installation_completed += "/opt/jans/jans-cli/config-cli.py" + msg.installation_completed += '/opt/jans/jans-cli/config-cli-tui.py' if base.current_app.profile == static.SetupProfiles.OPENBANKING: ca_dir = os.path.join(Config.output_dir, 'CA') crt_fn = os.path.join(ca_dir, 'client.crt') key_fn = os.path.join(ca_dir, 'client.key') msg.installation_completed += ' -CC {} -CK {}'.format(crt_fn, key_fn) msg.installation_completed +="\n" - if Config.profile == 'jans' and Config.install_scim_server: - msg.installation_completed += "/opt/jans/jans-cli/scim-cli.py" + #if Config.profile == 'jans' and Config.install_scim_server: + # msg.installation_completed += "/opt/jans/jans-cli/scim-cli.py" msg_text = msg.post_installation if Config.installed_instance else msg.installation_completed.format( Config.hostname) diff --git a/jans-linux-setup/jans_setup/setup_app/data/package_list.json b/jans-linux-setup/jans_setup/setup_app/data/package_list.json index 5a9a3f538d9..e5bbb25617a 100644 --- a/jans-linux-setup/jans_setup/setup_app/data/package_list.json +++ b/jans-linux-setup/jans_setup/setup_app/data/package_list.json @@ -2,51 +2,51 @@ "red 7": { "optional": "", "mandatory": "httpd httpd-mod_ssl mod_auth_openidc curl wget tar xz unzip rsyslog bzip2", - "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel-yaml", "certifi":"python3-certifi", "pymysql":"python3-PyMySQL", "Crypto": "python3-cryptography", "prompt_toolkit": "python3-prompt-toolkit", "psycopg2":"python3-psycopg2"} + "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel-yaml", "certifi":"python3-certifi", "pymysql":"python3-PyMySQL", "Crypto": "python3-cryptography", "psycopg2":"python3-psycopg2"} }, "red 8": { "optional": "", "mandatory": "httpd mod_ssl mod_auth_openidc curl wget tar xz unzip rsyslog bzip2", - "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel-yaml", "certifi":"python3-certifi", "pymysql":"python3-PyMySQL", "Crypto": "python3-cryptography", "prompt_toolkit": "python3-prompt-toolkit", "psycopg2":"python3-psycopg2"} + "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel-yaml", "certifi":"python3-certifi", "pymysql":"python3-PyMySQL", "Crypto": "python3-cryptography", "psycopg2":"python3-psycopg2"} }, "centos 7": { "optional": "", "mandatory": "httpd mod_ssl curl mod_auth_openidc wget tar xz unzip bzip2 rsyslog python3-certifi", - "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel-yaml", "pymysql":"python3-PyMySQL", "Crypto": "python3-cryptography", "prompt_toolkit": "python3-prompt-toolkit", "psycopg2":"python3-psycopg2"} + "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel-yaml", "pymysql":"python3-PyMySQL", "Crypto": "python3-cryptography", "psycopg2":"python3-psycopg2"} }, "centos 8": { "optional": "", "mandatory": "httpd mod_ssl mod_auth_openidc curl wget tar xz unzip rsyslog bzip2", - "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel-yaml", "certifi":"python3-certifi", "pymysql":"python3-PyMySQL", "Crypto": "python3-cryptography", "prompt_toolkit": "python3-prompt-toolkit", "psycopg2":"python3-psycopg2"} + "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel-yaml", "certifi":"python3-certifi", "pymysql":"python3-PyMySQL", "Crypto": "python3-cryptography", "psycopg2":"python3-psycopg2"} }, "suse 15": { "optional": "", "mandatory": "apache2 apache2-mod_auth_openidc curl wget tar xz unzip rsyslog bzip2", - "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel.yaml", "certifi":"python3-certifi", "pymysql":"python3-PyMySQL", "Crypto": "python3-cryptography", "prompt_toolkit": "python3-prompt_toolkit", "psycopg2":"python3-psycopg2"} + "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel.yaml", "certifi":"python3-certifi", "pymysql":"python3-PyMySQL", "Crypto": "python3-cryptography", "psycopg2":"python3-psycopg2"} }, "debian 11": { "optional": "", "mandatory": "apache2 curl wget tar xz-utils unzip rsyslog bzip2", - "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel.yaml", "certifi":"python3-certifi", "pymysql":"python3-pymysql", "cryptography":"python3-cryptography", "distutils.util": "python3-distutils", "prompt_toolkit": "python3-prompt-toolkit", "psycopg2":"python3-psycopg2"} + "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel.yaml", "certifi":"python3-certifi", "pymysql":"python3-pymysql", "cryptography":"python3-cryptography", "distutils.util": "python3-distutils", "psycopg2":"python3-psycopg2"} }, "debian 10": { "optional": "", "mandatory": "apache2 curl wget tar xz-utils unzip rsyslog bzip2", - "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel.yaml", "certifi":"python3-certifi", "pymysql":"python3-pymysql", "Crypto": "python3-crypto", "distutils.util": "python3-distutils", "prompt_toolkit": "python3-prompt-toolkit", "psycopg2":"python3-psycopg2"} + "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel.yaml", "certifi":"python3-certifi", "pymysql":"python3-pymysql", "Crypto": "python3-crypto", "distutils.util": "python3-distutils", "psycopg2":"python3-psycopg2"} }, "debian 9": { "optional": "", "mandatory": "apache2 curl wget tar xz-utils unzip rsyslog bzip2", - "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel.yaml", "certifi":"python3-certifi", "pymysql":"python3-pymysql", "Crypto": "python3-crypto", "distutils.util": "python3-distutils", "prompt_toolkit": "python3-prompt-toolkit", "psycopg2":"python3-psycopg2"} + "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel.yaml", "certifi":"python3-certifi", "pymysql":"python3-pymysql", "Crypto": "python3-crypto", "distutils.util": "python3-distutils", "psycopg2":"python3-psycopg2"} }, "ubuntu 20": { "optional": "", "mandatory": "apache2 curl wget xz-utils unzip rsyslog bzip2 python3-urllib3 python3-certifi", - "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel.yaml", "pymysql":"python3-pymysql", "Crypto": "python3-crypto", "distutils.util": "python3-distutils", "prompt_toolkit": "python3-prompt-toolkit", "psycopg2":"python3-psycopg2"} + "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel.yaml", "pymysql":"python3-pymysql", "Crypto": "python3-crypto", "distutils.util": "python3-distutils", "psycopg2":"python3-psycopg2"} }, "ubuntu 22": { "optional": "", "mandatory": "apache2 curl wget xz-utils unzip rsyslog bzip2 python3-urllib3 python3-certifi", - "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel.yaml", "pymysql":"python3-pymysql", "cryptography":"python3-cryptography", "distutils.util": "python3-distutils", "prompt_toolkit": "python3-prompt-toolkit", "certifi":"python3-certifi", "psycopg2":"python3-psycopg2"} + "python": {"ldap3":"python3-ldap3", "requests":"python3-requests", "ruamel.yaml":"python3-ruamel.yaml", "pymysql":"python3-pymysql", "cryptography":"python3-cryptography", "distutils.util": "python3-distutils", "certifi":"python3-certifi", "psycopg2":"python3-psycopg2"} } } diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans_cli.py b/jans-linux-setup/jans_setup/setup_app/installers/jans_cli.py index 644b64f2724..1b588e6320c 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans_cli.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans_cli.py @@ -18,9 +18,10 @@ class JansCliInstaller(BaseInstaller, SetupUtils): source_files = [ - (os.path.join(Config.dist_jans_dir, 'jca-swagger-client.zip'), os.path.join(base.current_app.app_info['EXTERNAL_LIBS'], 'cli-swagger/jca_swagger_client.zip')), - (os.path.join(Config.dist_jans_dir, 'scim-swagger-client.zip'), os.path.join(base.current_app.app_info['EXTERNAL_LIBS'], 'cli-swagger/scim_swagger_client.zip')), (os.path.join(Config.dist_app_dir, 'pyjwt.zip'), base.current_app.app_info['PYJWT']), + (os.path.join(Config.dist_app_dir, 'prompt_toolkit.zip'), base.current_app.app_info['PROMPT_TOOLKIT']), + (os.path.join(Config.dist_app_dir, 'wcwidth.zip'), base.current_app.app_info['WCWIDTH']), + (os.path.join(Config.dist_app_dir, 'pygments.zip'), base.current_app.app_info['PYGMENTS']), ] def __init__(self): @@ -39,6 +40,7 @@ def __init__(self): self.config_ini_fn = config_dir.joinpath('jans-cli.ini') self.ldif_client = os.path.join(self.output_folder, 'client.ldif') self.templates_folder = os.path.join(Config.templateFolder, self.service_name) + self.pylib_dir = os.path.join(self.jans_cli_install_dir, 'cli', 'pylib') if not base.snap: self.register_progess() @@ -52,22 +54,36 @@ def install(self): if os.path.exists(self.jans_cli_install_dir): self.run(['mv', '-f', self.jans_cli_install_dir, self.jans_cli_install_dir+'_backup-{}'.format(time.ctime())]) + self.createDirs(self.pylib_dir) + ops_dir = os.path.join(self.jans_cli_install_dir, 'cli', 'ops') + self.createDirs(ops_dir) + self.createDirs(os.path.join(ops_dir, 'jca')) + self.createDirs(os.path.join(ops_dir, 'scim')) + + #extract jans-cli tgz archieve - base.extract_from_zip(base.current_app.jans_zip, 'jans-cli/cli', self.jans_cli_install_dir) + base.extract_from_zip(base.current_app.jans_zip, 'jans-cli-tui/cli_tui', self.jans_cli_install_dir) + #extract pyDes from Jans archieve + base.extract_file(base.current_app.jans_zip, 'jans-linux-setup/jans_setup/setup_app/pylib/pyDes.py', os.path.join(self.pylib_dir, 'pyDes.py'), ren=True) - self.run([paths.cmd_ln, '-s', os.path.join(self.jans_cli_install_dir, 'config_cli.py'), os.path.join(self.jans_cli_install_dir, 'config-cli.py')]) - self.run([paths.cmd_ln, '-s', os.path.join(self.jans_cli_install_dir, 'config_cli.py'), os.path.join(self.jans_cli_install_dir, 'scim-cli.py')]) - self.run([paths.cmd_chmod, '+x', os.path.join(self.jans_cli_install_dir, 'config_cli.py')]) + self.run([paths.cmd_ln, '-s', os.path.join(self.jans_cli_install_dir, 'cli', 'config_cli.py'), os.path.join(self.jans_cli_install_dir, 'config-cli.py')]) + self.run([paths.cmd_ln, '-s', os.path.join(self.jans_cli_install_dir, 'jans_cli_tui.py'), os.path.join(self.jans_cli_install_dir, 'config-cli-tui.py')]) + #self.run([paths.cmd_ln, '-s', os.path.join(self.jans_cli_install_dir, 'config_cli.py'), os.path.join(self.jans_cli_install_dir, 'scim-cli.py')]) + self.run([paths.cmd_chmod, '+x', os.path.join(self.jans_cli_install_dir, 'cli', 'config_cli.py')]) + self.run([paths.cmd_chmod, '+x', os.path.join(self.jans_cli_install_dir, 'jans_cli_tui.py')]) - base.extract_from_zip(self.source_files[0][0], 'jca', os.path.join(self.jans_cli_install_dir, 'jca')) - base.extract_from_zip(self.source_files[1][0], 'scim', os.path.join(self.jans_cli_install_dir, 'scim')) - #extract pyjwt from archieve - base.extract_from_zip(self.source_files[2][0], 'jwt', os.path.join(self.jans_cli_install_dir, 'pylib/jwt')) + #extract python libraries + base.extract_from_zip(self.source_files[0][0], 'jwt', os.path.join(self.pylib_dir, 'jwt')) + base.extract_from_zip(self.source_files[1][0], 'src/prompt_toolkit', os.path.join(self.pylib_dir, 'prompt_toolkit')) + base.extract_from_zip(self.source_files[2][0], 'wcwidth', os.path.join(self.pylib_dir, 'wcwidth')) + base.extract_from_zip(self.source_files[3][0], 'pygments', os.path.join(self.pylib_dir, 'pygments')) # extract yaml files - base.extract_file(base.current_app.jans_zip, 'jans-config-api/docs/jans-config-api-swagger.yaml', os.path.join(self.jans_cli_install_dir, 'jca.yaml'), ren=True) - base.extract_file(base.current_app.jans_zip, 'jans-scim/server/src/main/resources/jans-scim-openapi.yaml', os.path.join(self.jans_cli_install_dir, 'scim.yaml'), ren=True) + base.extract_file(base.current_app.jans_zip, 'jans-config-api/docs/jans-config-api-swagger-auto.yaml', os.path.join(ops_dir, 'jca', 'jans-config-api-swagger-auto.yaml'), ren=True) + for plugin_yaml_file in ('fido2-plugin-swagger.yaml', 'jans-admin-ui-plugin-swagger.yaml', 'scim-plugin-swagger.yaml', 'user-mgt-plugin-swagger.yaml'): + base.extract_file(base.current_app.jans_zip, 'jans-config-api/plugins/docs/'+plugin_yaml_file, os.path.join(ops_dir, 'jca', plugin_yaml_file), ren=True) + base.extract_file(base.current_app.jans_zip, 'jans-scim/server/src/main/resources/jans-scim-openapi.yaml', os.path.join(ops_dir, 'scim', 'scim.yaml'), ren=True) def generate_configuration(self): @@ -103,6 +119,11 @@ def configure(self, options={}): config['DEFAULT']['jca_plugins'] = ','.join(base.current_app.ConfigApiInstaller.get_plugins()) + # clean old token and user data + for prop in ('user_data', 'access_token_enc'): + if prop in config['DEFAULT']: + del config['DEFAULT'][prop] + config.write(self.config_ini_fn.open('w')) self.config_ini_fn.chmod(0o600)