├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── connect └── cli │ ├── __init__.py │ ├── ccli.py │ ├── core │ ├── __init__.py │ ├── account │ │ ├── __init__.py │ │ ├── commands.py │ │ └── helpers.py │ ├── base.py │ ├── config.py │ ├── constants.py │ ├── http.py │ ├── plugins.py │ ├── terminal.py │ └── utils.py │ └── plugins │ ├── __init__.py │ ├── commerce │ ├── commands.py │ └── utils.py │ ├── customer │ ├── __init__.py │ ├── commands.py │ ├── constants.py │ ├── export.py │ └── sync.py │ ├── locale │ ├── __init__.py │ └── commands.py │ ├── play │ ├── __init__.py │ ├── commands.py │ ├── context.py │ ├── save.py │ └── script.py │ ├── product │ ├── __init__.py │ ├── api.py │ ├── clone.py │ ├── commands.py │ ├── constants.py │ ├── export.py │ ├── sync │ │ ├── __init__.py │ │ ├── actions.py │ │ ├── capabilities.py │ │ ├── configuration_values.py │ │ ├── general.py │ │ ├── items.py │ │ ├── media.py │ │ ├── messages.py │ │ ├── params.py │ │ ├── static_resources.py │ │ └── templates.py │ └── utils.py │ ├── project │ ├── __init__.py │ ├── commands.py │ ├── extension │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── helpers.py │ │ ├── templates │ │ │ └── bootstrap │ │ │ │ └── ${project_slug} │ │ │ │ ├── ${package_name} │ │ │ │ ├── __init__.py.j2 │ │ │ │ ├── anvil.py.j2 │ │ │ │ ├── events.py.j2 │ │ │ │ ├── extension.json.j2 │ │ │ │ ├── schemas.py.j2 │ │ │ │ ├── static │ │ │ │ │ └── .gitkeep.j2 │ │ │ │ ├── tfnapp.py.j2 │ │ │ │ └── webapp.py.j2 │ │ │ │ ├── .${project_slug}_dev.env.j2 │ │ │ │ ├── .eslintrc.yaml.j2 │ │ │ │ ├── .flake8.j2 │ │ │ │ ├── .github │ │ │ │ └── workflows │ │ │ │ │ └── test.yml.j2 │ │ │ │ ├── .gitignore.j2 │ │ │ │ ├── CHANGELOG.md.j2 │ │ │ │ ├── Dockerfile.j2 │ │ │ │ ├── HOWTO.md.j2 │ │ │ │ ├── LICENSE.j2 │ │ │ │ ├── README.md.j2 │ │ │ │ ├── __mocks__ │ │ │ │ ├── fileMock.js.j2 │ │ │ │ └── styleMock.js.j2 │ │ │ │ ├── babel.config.json.j2 │ │ │ │ ├── docker-compose.yml.j2 │ │ │ │ ├── jest.config.js.j2 │ │ │ │ ├── package.json.j2 │ │ │ │ ├── poetry.toml.j2 │ │ │ │ ├── pyproject.toml.j2 │ │ │ │ ├── tests │ │ │ │ ├── __init__.py.j2 │ │ │ │ ├── conftest.py.j2 │ │ │ │ ├── test_anvil.py.j2 │ │ │ │ ├── test_events.py.j2 │ │ │ │ ├── test_tfnapp.py.j2 │ │ │ │ └── test_webapp.py.j2 │ │ │ │ ├── ui │ │ │ │ ├── images │ │ │ │ │ └── mkp.svg.j2 │ │ │ │ ├── pages │ │ │ │ │ ├── index.html.j2 │ │ │ │ │ ├── settings.html.j2 │ │ │ │ │ └── transformations │ │ │ │ │ │ ├── copy.html.j2 │ │ │ │ │ │ └── manual.html.j2 │ │ │ │ ├── src │ │ │ │ │ ├── components.js.j2 │ │ │ │ │ ├── pages.js.j2 │ │ │ │ │ ├── pages │ │ │ │ │ │ ├── index.js.j2 │ │ │ │ │ │ ├── settings.js.j2 │ │ │ │ │ │ └── transformations │ │ │ │ │ │ │ ├── copy.js.j2 │ │ │ │ │ │ │ └── manual.js.j2 │ │ │ │ │ └── utils.js.j2 │ │ │ │ ├── styles │ │ │ │ │ ├── index.css.j2 │ │ │ │ │ └── manual.css.j2 │ │ │ │ └── tests │ │ │ │ │ ├── components.spec.js.j2 │ │ │ │ │ ├── pages.spec.js.j2 │ │ │ │ │ └── utils.spec.js.j2 │ │ │ │ └── webpack.config.js.j2 │ │ ├── utils.py │ │ ├── validators.py │ │ └── wizard.py │ ├── renderer.py │ ├── report │ │ ├── __init__.py │ │ ├── helpers.py │ │ ├── templates │ │ │ ├── add │ │ │ │ └── ${initial_report_slug} │ │ │ │ │ ├── README.md.j2 │ │ │ │ │ ├── __init__.py.j2 │ │ │ │ │ ├── entrypoint.py.j2 │ │ │ │ │ └── templates │ │ │ │ │ ├── pdf │ │ │ │ │ ├── template.css.j2 │ │ │ │ │ └── template.html.j2.j2 │ │ │ │ │ └── xml │ │ │ │ │ └── template.xml.j2.j2 │ │ │ └── bootstrap │ │ │ │ └── ${project_slug} │ │ │ │ ├── ${package_name} │ │ │ │ └── ${initial_report_slug} │ │ │ │ │ ├── README.md.j2 │ │ │ │ │ ├── __init__.py.j2 │ │ │ │ │ ├── entrypoint.py.j2 │ │ │ │ │ └── templates │ │ │ │ │ ├── pdf │ │ │ │ │ ├── template.css.j2 │ │ │ │ │ └── template.html.j2.j2 │ │ │ │ │ └── xml │ │ │ │ │ └── template.xml.j2.j2 │ │ │ │ ├── .flake8.j2 │ │ │ │ ├── .github │ │ │ │ └── workflows │ │ │ │ │ └── build.yml.j2 │ │ │ │ ├── .gitignore.j2 │ │ │ │ ├── HOWTO.md.j2 │ │ │ │ ├── LICENSE.j2 │ │ │ │ ├── README.md.j2 │ │ │ │ ├── poetry.toml.j2 │ │ │ │ ├── pyproject.toml.j2 │ │ │ │ ├── reports.json.j2 │ │ │ │ └── tests │ │ │ │ ├── __init__.py.j2 │ │ │ │ ├── conftest.py.j2 │ │ │ │ └── test_${project_slug}.py.j2 │ │ ├── validations.py │ │ └── wizard.py │ ├── utils.py │ └── validators.py │ ├── report │ ├── __init__.py │ ├── commands.py │ ├── constants.py │ ├── helpers.py │ ├── utils.py │ └── wizard.py │ ├── shared │ ├── __init__.py │ ├── base.py │ ├── constants.py │ ├── exceptions.py │ ├── export.py │ ├── sync_stats.py │ ├── translation_attr_sync.py │ ├── translation_sync.py │ ├── translations_synchronizers.py │ └── utils.py │ └── translation │ ├── __init__.py │ ├── activate.py │ ├── commands.py │ ├── constants.py │ ├── export.py │ ├── primarize.py │ ├── translation_sync.py │ └── utils.py ├── docs ├── core_usage.md ├── customers_usage.md ├── linux_deps_install.md ├── locales_usage.md ├── osx_deps_install.md ├── products_usage.md ├── project_usage.md ├── reports_usage.md ├── stream_usage.md ├── translations_usage.md └── win_deps_install.md ├── poetry.lock ├── pyproject.toml ├── resources ├── Dockerfile ├── README.md ├── ccli_shell.ps1 ├── connect.ico ├── generate_pynsist_config.py └── get_latest_reports.py ├── sonar-project.properties └── tests ├── __init__.py ├── conftest.py ├── core ├── __init__.py ├── account │ ├── __init__.py │ ├── test_commands.py │ └── test_helpers.py ├── test_base.py ├── test_config.py ├── test_http.py ├── test_plugins.py ├── test_terminal.py └── test_utils.py ├── data.py ├── fixtures ├── actions_response.json ├── actions_sync.xlsx ├── capabilities_sync.xlsx ├── categories_response.json ├── commerce │ ├── attachments_response.json │ ├── billing_streams_response.json │ ├── columns_response.json │ ├── pricing_streams_response.json │ ├── stream_retrieve_response.json │ ├── stream_sync.xlsx │ └── transformations_response.json ├── comparation_product.xlsx ├── configuration_parameters_response.json ├── configuration_sync.xlsx ├── configurations_response.json ├── customer │ ├── customer.json │ ├── customers.xlsx │ └── reseller.json ├── extensions │ └── basic_ext │ │ ├── connect_ext │ │ ├── __init__.py │ │ ├── extension.json │ │ └── extension.py │ │ ├── docker-compose.yml │ │ └── pyproject.toml ├── fulfillment_parameters_response.json ├── image.png ├── items_response.json ├── items_sync.xlsx ├── locales_response.json ├── media_response.json ├── media_sync.xlsx ├── messages_sync.xlsx ├── new_translation_response.json ├── ordering_parameters_response.json ├── params_sync.xlsx ├── product_messages_response.json ├── product_response.json ├── product_response_modifications.json ├── product_translations_response.json ├── report.json.j2 ├── reports.json ├── reports │ ├── basic_report │ │ ├── README.md │ │ ├── endpoint │ │ │ ├── Readme.md │ │ │ ├── __init__.py │ │ │ ├── entrypoint.py │ │ │ └── template.xlsx │ │ └── reports.json │ ├── connect_exception │ │ ├── README.md │ │ ├── entrypoint │ │ │ ├── Readme.md │ │ │ ├── __init__.py │ │ │ ├── entrypoint.py │ │ │ └── template.xlsx │ │ └── reports.json │ ├── custom_exception │ │ ├── README.md │ │ ├── entry__point │ │ │ ├── Readme.md │ │ │ ├── __init__.py │ │ │ ├── entrypoint.py │ │ │ └── template.xlsx │ │ └── reports.json │ ├── generic_exception │ │ ├── README.md │ │ ├── entry_point │ │ │ ├── Readme.md │ │ │ ├── __init__.py │ │ │ ├── entrypoint.py │ │ │ └── template.xlsx │ │ └── reports.json │ ├── no_reports │ │ ├── README.md │ │ └── reports.json │ ├── report_v2 │ │ ├── Readme.md │ │ ├── reports.json │ │ └── reports │ │ │ ├── __init__.py │ │ │ └── test_v2 │ │ │ ├── Readme.md │ │ │ ├── __init__.py │ │ │ ├── entrypoint.py │ │ │ └── templates │ │ │ ├── pdf │ │ │ ├── template.css │ │ │ └── template.html.j2 │ │ │ └── xlsx │ │ │ └── template.xlsx │ └── report_with_inputs │ │ ├── README.md │ │ ├── executor │ │ ├── Readme.md │ │ ├── __init__.py │ │ ├── entrypoint.py │ │ └── template.xlsx │ │ └── reports.json ├── templates_response.json ├── templates_sync.xlsx ├── translation.xlsx ├── translation_attributes_response.json ├── translation_attributes_response.xlsx ├── translation_response.json ├── translations_sync.xlsx └── units_response.json ├── plugins ├── __init__.py ├── commerce │ ├── __init__.py │ ├── test_commands.py │ └── test_utils.py ├── customer │ ├── test_commands.py │ ├── test_export.py │ └── test_sync.py ├── locale │ ├── __init__.py │ └── test_commands.py ├── play │ ├── context.json │ ├── scripts │ │ ├── __init__.py │ │ ├── data.json │ │ ├── script1.py │ │ └── script2.py │ ├── test_context.py │ ├── test_play_commands.py │ └── test_script.py ├── product │ ├── __init__.py │ ├── sync │ │ ├── __init__.py │ │ ├── test_actions.py │ │ ├── test_capabilities.py │ │ ├── test_conf_values.py │ │ ├── test_general.py │ │ ├── test_items.py │ │ ├── test_media.py │ │ ├── test_messages.py │ │ ├── test_params.py │ │ └── test_template.py │ ├── test_api.py │ ├── test_clone.py │ ├── test_commands.py │ └── test_export.py ├── project │ ├── __init__.py │ ├── test_commands.py │ ├── test_extension_helpers.py │ ├── test_extension_utils.py │ ├── test_extension_validators.py │ ├── test_renderer.py │ ├── test_report_helpers.py │ ├── test_report_validations.py │ ├── test_utils.py │ └── test_validators.py ├── report │ ├── __init__.py │ ├── test_commands.py │ ├── test_helpers.py │ ├── test_utils.py │ └── test_wizard.py ├── shared │ ├── __init__.py │ ├── test_base.py │ ├── test_sync_stats.py │ ├── test_translation_attr_sync.py │ ├── test_translation_sync.py │ ├── test_translations_synchronizers.py │ └── test_utils.py └── translation │ ├── __init__.py │ ├── test_activate.py │ ├── test_commands.py │ ├── test_export.py │ ├── test_primarize.py │ ├── test_translation_sync.py │ └── test_utils.py └── test_ccli.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Connect Command Line Client 2 | 3 | on: 4 | push: 5 | branches: '*' 6 | tags: 7 | - '*' 8 | pull_request: 9 | branches: 10 | - master 11 | - 'release/**' 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: ["3.8", "3.9", "3.10"] 19 | steps: 20 | - uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | submodules: true 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v4 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install poetry 32 | poetry install 33 | - name: Linting 34 | run: | 35 | poetry run flake8 36 | - name: Testing 37 | run: | 38 | poetry run pytest -v 39 | sonar: 40 | needs: [test] 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v3 44 | with: 45 | fetch-depth: 0 46 | - name: Set up Python 3.10 47 | uses: actions/setup-python@v4 48 | with: 49 | python-version: '3.10' 50 | - name: Install dependencies 51 | run: | 52 | python -m pip install --upgrade pip 53 | pip install poetry 54 | poetry install 55 | - name: Generate coverage report 56 | run: | 57 | poetry run pytest 58 | - name: SonarCloud 59 | uses: SonarSource/sonarcloud-github-action@master 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 63 | - name: SonarQube Quality Gate check 64 | uses: sonarsource/sonarqube-quality-gate-action@master 65 | timeout-minutes: 5 66 | env: 67 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .pytest_cache 3 | *.py[cod] 4 | venv*/ 5 | 6 | build/ 7 | dist/ 8 | *.egg-info 9 | .eggs 10 | 11 | .idea 12 | .vscode 13 | .devcontainer 14 | 15 | coverage/ 16 | .coverage* 17 | /htmlcov/ 18 | docs/_build 19 | temp/ 20 | /*.xlsx 21 | coverage.xml 22 | 23 | connect/.data 24 | .DS_Store 25 | */.DS_Store 26 | 27 | context.json -------------------------------------------------------------------------------- /connect/cli/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli 4 | # Copyright (c) 2019-2021 Ingram Micro. All Rights Reserved. 5 | from importlib.metadata import version 6 | 7 | 8 | try: 9 | __version__ = version('connect-cli') 10 | except Exception: # pragma: no cover 11 | __version__ = '0.0.0' 12 | 13 | 14 | def get_version(): 15 | return __version__ 16 | -------------------------------------------------------------------------------- /connect/cli/ccli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2019-2021 Ingram Micro. All Rights Reserved. 5 | import io 6 | import sys 7 | 8 | import click 9 | 10 | from connect.cli.core.base import cli 11 | from connect.cli.core.constants import CAIRO_NOT_FOUND_ERROR 12 | from connect.cli.core.plugins import load_plugins 13 | 14 | 15 | def main(): 16 | _set_stdout_unbuffered() 17 | _ignore_openpyxl_warnings() 18 | try: 19 | import uvloop 20 | 21 | uvloop.install() 22 | except ImportError: 23 | pass 24 | print('') 25 | try: 26 | load_plugins(cli) 27 | cli(prog_name='ccli', standalone_mode=False) 28 | except OSError as oe: 29 | if 'no library called "cairo" was found' in str(oe): 30 | click.secho(CAIRO_NOT_FOUND_ERROR, fg='yellow') 31 | else: 32 | click.secho(str(oe), fg='red') 33 | except click.ClickException as ce: 34 | click.secho(str(ce), fg='red') 35 | except click.exceptions.Abort: 36 | pass 37 | finally: 38 | print('') 39 | 40 | 41 | def _ignore_openpyxl_warnings(): 42 | """ 43 | Ignore warning about DataValidation extension not supported. This is shown when a xlsx file 44 | with unsupported data validation is opened (tipically after saving the file from Excel, which 45 | uses some custom extension). 46 | To avoid losing data validation, it should be re-created each time the file is saved by the cli. 47 | """ 48 | import warnings 49 | 50 | warnings.filterwarnings('ignore', category=UserWarning, module='openpyxl.worksheet._reader') 51 | 52 | 53 | def _set_stdout_unbuffered(): # pragma: no cover 54 | if 'pytest' not in sys.modules: 55 | sys.stdout = io.TextIOWrapper(open(sys.stdout.fileno(), 'wb', 0), write_through=True) 56 | sys.stdout.reconfigure(encoding='utf-8') 57 | 58 | 59 | if __name__ == '__main__': 60 | main() # pragma: no cover 61 | -------------------------------------------------------------------------------- /connect/cli/core/__init__.py: -------------------------------------------------------------------------------- 1 | from connect.cli.core.base import group # noqa: F401 2 | -------------------------------------------------------------------------------- /connect/cli/core/account/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/core/account/__init__.py -------------------------------------------------------------------------------- /connect/cli/core/account/commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2019-2021 Ingram Micro. All Rights Reserved. 5 | 6 | import click 7 | from click import ClickException 8 | 9 | from connect.cli.core.account.helpers import activate_account, add_account, remove_account 10 | from connect.cli.core.config import pass_config 11 | from connect.cli.core.constants import DEFAULT_ENDPOINT 12 | from connect.cli.core.terminal import console 13 | from connect.cli.core.utils import field_to_check_mark 14 | 15 | 16 | @click.group(name='account', short_help='Manage configured accounts.') 17 | def grp_account(): 18 | pass # pragma: no cover 19 | 20 | 21 | @grp_account.command( 22 | name='add', 23 | short_help='Add a new account.', 24 | ) 25 | @click.argument('api_key', metavar='API_KEY', nargs=1, required=True) # noqa: E304 26 | @click.option( 27 | '--endpoint', 28 | '-e', 29 | 'endpoint', 30 | default=DEFAULT_ENDPOINT, 31 | help='API endpoint.', 32 | ) 33 | @pass_config 34 | def cmd_add_account(config, api_key, endpoint): 35 | account_id, name = add_account(config, api_key, endpoint) 36 | console.secho(f'New account added: {account_id} - {name}', fg='green') 37 | 38 | 39 | @grp_account.command( 40 | name='list', 41 | short_help='List configured accounts.', 42 | ) 43 | @pass_config 44 | def cmd_list_account(config): 45 | if not config.accounts: 46 | raise ClickException('No account configured.') 47 | 48 | console.header('Configured accounts') 49 | 50 | console.table( 51 | columns=[ 52 | 'ID', 53 | 'Name', 54 | ('center', 'Active'), 55 | ], 56 | rows=[ 57 | ( 58 | acc.id, 59 | acc.name, 60 | field_to_check_mark(acc.id == config.active.id), 61 | ) 62 | for acc in config.accounts.values() 63 | ], 64 | ) 65 | 66 | 67 | @grp_account.command( 68 | name='activate', 69 | short_help='Set the current active account.', 70 | ) 71 | @click.argument('id', metavar='ACCOUNT_ID', nargs=1, required=True) # noqa: E304 72 | @pass_config 73 | def cmd_activate_account(config, id): 74 | acc = activate_account(config, id) 75 | 76 | console.secho( 77 | f'Current active account is: {acc.id} - {acc.name}', 78 | fg='green', 79 | ) 80 | 81 | 82 | @grp_account.command( 83 | name='remove', 84 | short_help='Remove a configured account.', 85 | ) 86 | @click.argument('id', metavar='ACCOUNT_ID', nargs=1, required=True) # noqa: E304 87 | @pass_config 88 | def cmd_remove_account(config, id): 89 | acc = remove_account(config, id) 90 | console.secho( 91 | f'Account removed: {acc.id} - {acc.name}', 92 | fg='green', 93 | ) 94 | -------------------------------------------------------------------------------- /connect/cli/core/account/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2019-2021 Ingram Micro. All Rights Reserved. 5 | 6 | import click 7 | from connect.client import ClientError, ConnectClient 8 | 9 | from connect.cli.core.http import RequestLogger 10 | 11 | 12 | def add_account(config, api_key, endpoint): 13 | try: 14 | client = ConnectClient( 15 | api_key=api_key, 16 | endpoint=endpoint, 17 | validate_using_specs=False, 18 | use_specs=False, 19 | logger=RequestLogger(), 20 | ) 21 | account_data = client.accounts.all().first() 22 | config.add_account( 23 | account_data['id'], 24 | account_data['name'], 25 | api_key, 26 | endpoint, 27 | ) 28 | config.store() 29 | return account_data['id'], account_data['name'] 30 | 31 | except ClientError as h: 32 | msg = f'Unexpected error: {h}' 33 | if h.status_code == 401: 34 | msg = 'Unauthorized: the provided api key is invalid.' 35 | raise click.ClickException(msg) 36 | 37 | 38 | def activate_account(config, id): 39 | config.activate(id) 40 | config.store() 41 | return config.active 42 | 43 | 44 | def remove_account(config, id): 45 | acc = config.remove_account(id) 46 | config.store() 47 | return acc 48 | -------------------------------------------------------------------------------- /connect/cli/core/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2019-2021 Ingram Micro. All Rights Reserved. 5 | 6 | DEFAULT_ENDPOINT = 'https://api.connect.cloudblue.com/public/v1' 7 | 8 | CAIRO_NOT_FOUND_ERROR = """Connect CLI depends on Cairo which is not present on the system. 9 | If so, please follow the instructions to install it at https://github.com/cloudblue/connect-cli 10 | and make sure your PATH environment variable includes also the Cairo shared libraries folder. 11 | """ 12 | 13 | PYPI_JSON_API_URL = 'https://pypi.org/pypi/connect-cli/json' 14 | -------------------------------------------------------------------------------- /connect/cli/core/http.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | from http import HTTPStatus 4 | 5 | import click 6 | from connect.client import ClientError, RequestLogger as _RequestLogger 7 | 8 | from connect.cli import get_version 9 | from connect.cli.core.terminal import console 10 | 11 | 12 | def get_user_agent(): 13 | version = get_version() 14 | pimpl = platform.python_implementation() 15 | pver = platform.python_version() 16 | sysname = platform.system() 17 | sysver = platform.release() 18 | ua = f'connect-cli/{version} {pimpl}/{pver} {sysname}/{sysver}' 19 | return {'User-Agent': ua} 20 | 21 | 22 | def format_http_status(status_code): 23 | status = HTTPStatus(status_code) 24 | description = status.name.replace('_', ' ').title() 25 | return f'{status_code} - {description}' 26 | 27 | 28 | def handle_http_error(res: ClientError): 29 | status = format_http_status(res.status_code) 30 | 31 | if res.status_code in (401, 403): 32 | raise click.ClickException(f'{status}: please check your credentials.') 33 | 34 | if res.status_code == 400: 35 | code = res.error_code if res.error_code else 'Generic error' 36 | message = ','.join(res.errors) if res.errors else '' 37 | raise click.ClickException(f'{status}: {code} - {message}') 38 | 39 | raise click.ClickException(f'{status}: unexpected error.') 40 | 41 | 42 | class RequestLogger(_RequestLogger): 43 | def __init__(self): 44 | if not console.verbose or console.silent: 45 | super().__init__(file=open(os.devnull, 'w')) 46 | else: 47 | super().__init__(file=None) 48 | -------------------------------------------------------------------------------- /connect/cli/core/plugins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2019-2022 Ingram Micro. All Rights Reserved. 5 | import click 6 | 7 | from connect.cli.core.utils import iter_entry_points 8 | 9 | 10 | def load_plugins(cli): 11 | @click.group(name='plugin', short_help='Third party plugins.') 12 | def grp_plugins(): 13 | pass # pragma: no cover 14 | 15 | has_3rd_party_plugins = False 16 | 17 | for entrypoint in iter_entry_points('connect.cli.plugins'): 18 | if entrypoint.value.startswith('connect.cli.plugins.'): 19 | command_fn = entrypoint.load() 20 | cli.add_command(command_fn()) 21 | else: 22 | has_3rd_party_plugins = True 23 | command_fn = entrypoint.load() 24 | grp_plugins.add_command(command_fn()) 25 | 26 | if has_3rd_party_plugins: 27 | cli.add_command(grp_plugins) 28 | -------------------------------------------------------------------------------- /connect/cli/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/plugins/__init__.py -------------------------------------------------------------------------------- /connect/cli/plugins/customer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/plugins/customer/__init__.py -------------------------------------------------------------------------------- /connect/cli/plugins/customer/commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2019-2022 Ingram Micro. All Rights Reserved. 5 | import warnings 6 | 7 | import click 8 | 9 | from connect.cli.core import group 10 | from connect.cli.core.config import pass_config 11 | from connect.cli.core.terminal import console 12 | from connect.cli.plugins.customer.export import dump_customers 13 | from connect.cli.plugins.customer.sync import CustomerSynchronizer 14 | 15 | 16 | @group(name='customer', short_help='Export/synchronize customers.') 17 | def grp_customer(): 18 | pass # pragma: no cover 19 | 20 | 21 | @grp_customer.command( 22 | name='export', 23 | short_help='Export customers to an excel file.', 24 | ) 25 | @click.option( 26 | '--output_path', 27 | '-p', 28 | 'output_path', 29 | type=click.Path(exists=True, file_okay=False, dir_okay=True), 30 | help='Directory where to store the export.', 31 | ) 32 | @click.option( 33 | '--out', 34 | '-o', 35 | 'output_file', 36 | type=click.Path(exists=False, file_okay=True, dir_okay=False), 37 | help='Output Excel file name.', 38 | ) 39 | @pass_config 40 | def cmd_export_customers(config, output_path, output_file): 41 | acc_id = config.active.id 42 | 43 | outfile = dump_customers( 44 | client=config.active.client, 45 | output_file=output_file, 46 | output_path=output_path, 47 | account_id=acc_id, 48 | ) 49 | 50 | console.secho( 51 | f'\nCustomers of account {acc_id} have been successfully exported to {outfile}', 52 | fg='green', 53 | ) 54 | 55 | 56 | @grp_customer.command( 57 | name='sync', 58 | short_help='Synchronize customers from an excel file.', 59 | ) 60 | @click.argument('input_file', metavar='input_file', nargs=1, required=True) # noqa: E304 61 | @pass_config 62 | def cmd_sync_customers(config, input_file): 63 | acc_id = config.active.id 64 | 65 | if '.xlsx' not in input_file: 66 | input_file = f'{input_file}/{input_file}.xlsx' 67 | 68 | synchronizer = CustomerSynchronizer( 69 | client=config.active.client, 70 | account_id=acc_id, 71 | ) 72 | warnings.filterwarnings('ignore', category=UserWarning) 73 | synchronizer.open(input_file, 'Customers') 74 | synchronizer.sync() 75 | synchronizer.save(input_file) 76 | synchronizer.stats.print() 77 | 78 | 79 | def get_group(): 80 | return grp_customer 81 | -------------------------------------------------------------------------------- /connect/cli/plugins/customer/constants.py: -------------------------------------------------------------------------------- 1 | COL_HEADERS = { 2 | 'A': 'ID', 3 | 'B': 'External ID', 4 | 'C': 'External UID', 5 | 'D': 'Action', 6 | 'E': 'Hub ID', 7 | 'F': 'Parent Search Criteria', 8 | 'G': 'Parent Search Value', 9 | 'H': 'Type', 10 | 'I': 'Tax ID', 11 | 'J': 'Company Name', 12 | 'K': 'Address Line 1', 13 | 'L': 'Address Line 2', 14 | 'M': 'City', 15 | 'N': 'State', 16 | 'O': 'Zip', 17 | 'P': 'Country', 18 | 'Q': 'Technical Contact First Name', 19 | 'R': 'Technical Contact Last Name', 20 | 'S': 'Technical Contact Email', 21 | 'T': 'Technical Contact Phone', 22 | } 23 | -------------------------------------------------------------------------------- /connect/cli/plugins/locale/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/plugins/locale/__init__.py -------------------------------------------------------------------------------- /connect/cli/plugins/locale/commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2019-2022 Ingram Micro. All Rights Reserved. 5 | 6 | 7 | import click 8 | 9 | from connect.cli.core import group 10 | from connect.cli.core.config import pass_config 11 | from connect.cli.core.terminal import console 12 | from connect.cli.core.utils import field_to_check_mark 13 | 14 | 15 | @group(name='locale', short_help='List all locales available.') 16 | def grp_locales(): 17 | pass # pragma: no cover 18 | 19 | 20 | @grp_locales.command( 21 | name='list', 22 | short_help='List locales.', 23 | ) 24 | @click.option( 25 | '--query', 26 | '-q', 27 | 'query', 28 | help='RQL query expression.', 29 | ) 30 | @pass_config 31 | def cmd_list_locales(config, query): 32 | default_query = config.active.client.ns('localization').locales.all() 33 | query_locales = default_query.filter(query) if query else default_query 34 | 35 | console.table( 36 | columns=[ 37 | 'ID', 38 | 'Name', 39 | ('center', 'Autotranslation'), 40 | ('center', 'Translations'), 41 | ], 42 | rows=[ 43 | ( 44 | resource['id'], 45 | resource['name'], 46 | field_to_check_mark(resource['auto_translation'], false_value='\u2716'), 47 | resource['stats']['translations'] or '-', 48 | ) 49 | for resource in query_locales 50 | ], 51 | ) 52 | 53 | 54 | def get_group(): 55 | return grp_locales 56 | -------------------------------------------------------------------------------- /connect/cli/plugins/play/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2021 Ingram Micro. All Rights Reserved. 5 | from .context import Context 6 | from .save import Save 7 | from .script import OptionWrapper, Script 8 | 9 | 10 | __all__ = ('Context', 'OptionWrapper', 'Script', 'Save') 11 | -------------------------------------------------------------------------------- /connect/cli/plugins/play/commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2021 Ingram Micro. All Rights Reserved. 5 | import os 6 | import sys 7 | 8 | import click 9 | 10 | from connect.cli.core import group 11 | from connect.cli.core.config import pass_config 12 | from connect.cli.plugins.play.context import Context 13 | 14 | 15 | @group(name='play', short_help='Play connect scripts.') 16 | def grp_play(): 17 | pass 18 | 19 | 20 | class PlayOptions: 21 | context_file = 'context.json' 22 | 23 | 24 | class PassArgumentDecorator: 25 | def __init__(self, arg): 26 | self.obj = arg 27 | 28 | def __call__(self, f): 29 | def wrapped(*args, **kwargs): 30 | f(self.obj, *args, **kwargs) 31 | 32 | return wrapped 33 | 34 | 35 | pass_arg = PassArgumentDecorator 36 | 37 | 38 | def setup_script_command(cls): 39 | @pass_config 40 | @pass_arg(cls) 41 | def cmd_play_custom(script_class, config, **kwargs): 42 | Context.context_file_name = PlayOptions.context_file 43 | ctx = Context.create(**kwargs) 44 | 45 | if 'endpoint' not in ctx or not ctx.endpoint: 46 | ctx.endpoint = config.active.endpoint 47 | 48 | if 'distributor_account_token' not in ctx or not ctx.distributor_account_token: 49 | ctx.distributor_account_token = config.active.api_key 50 | 51 | ctx | script_class() 52 | ctx.save() 53 | 54 | for o in cls.options(): 55 | cmd_play_custom = click.option(*o.args, **o.kwargs)(cmd_play_custom) 56 | 57 | grp_play.command(name=cls.command(), short_help=cls.help())(cmd_play_custom) 58 | 59 | 60 | def load_one_script(scripts, filename): 61 | modname = filename[0:-3] 62 | 63 | try: 64 | mod = __import__(modname, globals={'__name__': __name__}, fromlist=['*']) 65 | 66 | if not hasattr(mod, '__all__'): 67 | print(f'Warning: {filename} has no __all__ defined', file=sys.stderr) 68 | return 69 | 70 | for cname in mod.__all__: 71 | cls = getattr(mod, cname) 72 | setup_script_command(cls) 73 | 74 | except Exception as e: 75 | print(f'Failed to import {scripts}/{filename}: {e}') 76 | 77 | 78 | def load_scripts_actions(): 79 | scripts = os.environ.get('CCLI_SCRIPTS', 'scripts') 80 | if scripts[0] != '/': 81 | scripts = os.path.join(os.getcwd(), scripts) 82 | 83 | if os.path.isdir(scripts): 84 | print(f'Reading scripts library from {scripts}') 85 | sys.path.append(scripts) 86 | 87 | for filename in sorted(os.listdir(scripts)): 88 | if not filename.endswith('.py') or filename[0] == '_': 89 | continue 90 | 91 | load_one_script(scripts, filename) 92 | 93 | 94 | load_scripts_actions() 95 | 96 | 97 | def get_group(): 98 | return grp_play 99 | -------------------------------------------------------------------------------- /connect/cli/plugins/play/context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2021 Ingram Micro. All Rights Reserved. 5 | import inspect 6 | import json 7 | import sys 8 | 9 | 10 | class Context(dict): 11 | context_file_name = None 12 | 13 | @classmethod 14 | def create_from_file(cls, filename=None): 15 | ctx = cls() 16 | try: 17 | ctx.load(filename) 18 | except FileNotFoundError: 19 | pass 20 | 21 | return ctx 22 | 23 | @classmethod 24 | def create(cls, args=None, filename=None, **kwargs): 25 | ctx = cls() 26 | try: 27 | ctx.load(filename) 28 | except FileNotFoundError: 29 | pass 30 | 31 | if args: 32 | ctx.parse_args(args) 33 | 34 | if kwargs: 35 | for k, v in kwargs.items(): 36 | if v is not None: 37 | ctx[k] = v 38 | 39 | return ctx 40 | 41 | def parse_args(self, args): 42 | for k, v in [a.split('=') for a in args]: 43 | self[k] = v 44 | 45 | def load(self, filename=None): 46 | if filename is None: 47 | filename = self.__class__.context_file_name 48 | if filename: 49 | with open(filename) as f: 50 | print(f'Loading context from {filename}', file=sys.stderr) 51 | self.clear() 52 | for k, v in json.load(f).items(): 53 | self[k] = v 54 | 55 | def save(self, filename=None): 56 | if filename is None: 57 | filename = self.__class__.context_file_name 58 | if filename: 59 | with open(filename, 'w') as f: 60 | print(f'Saving context into {filename}', file=sys.stderr) 61 | json.dump(self, f, indent=4) 62 | 63 | def __str__(self): 64 | return json.dumps(self, indent=4) 65 | 66 | def __getattr__(self, item): 67 | if item in self: 68 | return self[item] 69 | 70 | raise KeyError(item) 71 | 72 | def __setattr__(self, key, value): 73 | self[key] = value 74 | 75 | def __ior__(self, kv): 76 | key, value = kv 77 | if isinstance(value, dict): 78 | if key not in self: 79 | self[key] = {} 80 | self[key].update(value) 81 | else: 82 | if not isinstance(value, list): 83 | value = [value] 84 | 85 | if key not in self: 86 | self[key] = [] 87 | 88 | self[key].extend(value) 89 | 90 | return self 91 | 92 | def __or__(self, step): 93 | if inspect.isclass(step): 94 | step = step() 95 | 96 | step.do(context=self) 97 | return self 98 | -------------------------------------------------------------------------------- /connect/cli/plugins/play/save.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2021 Ingram Micro. All Rights Reserved. 5 | from connect.cli.plugins.play.context import Context 6 | from connect.cli.plugins.play.script import Script 7 | 8 | 9 | class Save(Script): 10 | def do(self, filename=Context.context_file_name, context=None): 11 | super().do(context=context) 12 | self.context.save(filename=filename) 13 | -------------------------------------------------------------------------------- /connect/cli/plugins/play/script.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2021 Ingram Micro. All Rights Reserved. 5 | import re 6 | from typing import List 7 | 8 | from connect.client import ConnectClient 9 | 10 | from connect.cli.plugins.play.context import Context 11 | 12 | 13 | class OptionWrapper: 14 | def __init__(self, *args, **kwargs): 15 | self.args = args 16 | self.kwargs = kwargs 17 | 18 | 19 | class Script: 20 | context: Context = None 21 | endpoint: str = None 22 | 23 | def __init__(self, context=None, **kwargs): 24 | self.context = context if context is not None else Context() 25 | self.context.update(kwargs) 26 | 27 | @classmethod 28 | def command(cls) -> str: 29 | return str( 30 | re.sub( 31 | r'^([A-Z])', 32 | lambda x: x.group(1).lower(), 33 | re.sub( 34 | r'([a-z])([A-Z])', 35 | lambda x: f'{x.group(1)}-{x.group(2).lower()}', 36 | cls.__name__, 37 | ), 38 | ), 39 | ) 40 | 41 | @classmethod 42 | def help(cls) -> str: 43 | return cls.__doc__ 44 | 45 | @classmethod 46 | def options(cls) -> List[OptionWrapper]: 47 | return [] 48 | 49 | def client(self, token) -> ConnectClient: 50 | return ConnectClient(token, endpoint=self.context.endpoint, use_specs=False) 51 | 52 | @property 53 | def dclient(self) -> ConnectClient: 54 | return self.client(self.context.distributor_account_token) 55 | 56 | @property 57 | def vclient(self) -> ConnectClient: 58 | return self.client(self.context.vendor_account_token) 59 | 60 | def do(self, context=None): 61 | if context: 62 | context.update(self.context) 63 | self.context = context 64 | -------------------------------------------------------------------------------- /connect/cli/plugins/product/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/plugins/product/__init__.py -------------------------------------------------------------------------------- /connect/cli/plugins/product/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2019-2021 Ingram Micro. All Rights Reserved. 5 | 6 | from connect.client import ClientError, R 7 | 8 | from connect.cli.core.http import handle_http_error 9 | 10 | 11 | def create_unit(client, data): 12 | try: 13 | res = client.ns('settings').units.create(data) 14 | except ClientError as error: 15 | handle_http_error(error) 16 | return res 17 | 18 | 19 | def get_item(client, product_id, item_id): 20 | try: 21 | res = client.products[product_id].items[item_id].get() 22 | except ClientError as error: 23 | if error.status_code == 404: 24 | return 25 | handle_http_error(error) 26 | return res 27 | 28 | 29 | def get_item_by_mpn(client, product_id, mpn): 30 | rql = R().mpn.eq(mpn) 31 | 32 | try: 33 | res = client.products[product_id].items.filter(rql) 34 | return res.first() 35 | 36 | except ClientError as error: 37 | if error.status_code == 404: 38 | return 39 | handle_http_error(error) 40 | 41 | 42 | def create_item(client, product_id, data): 43 | try: 44 | res = client.products[product_id].items.create(data) 45 | except ClientError as error: 46 | handle_http_error(error) 47 | 48 | return res 49 | 50 | 51 | def update_item(client, product_id, item_id, data): 52 | try: 53 | res = client.products[product_id].items[item_id].update(data) 54 | except ClientError as error: 55 | handle_http_error(error) 56 | 57 | return res 58 | 59 | 60 | def delete_item(client, product_id, item_id): 61 | try: 62 | client.products[product_id].items[item_id].delete() 63 | except ClientError as error: 64 | handle_http_error(error) 65 | -------------------------------------------------------------------------------- /connect/cli/plugins/product/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2019-2022 Ingram Micro. All Rights Reserved. 5 | 6 | PARAM_TYPES = [ 7 | 'email', 8 | 'address', 9 | 'checkbox', 10 | 'choice', 11 | 'domain', 12 | 'subdomain', 13 | 'url', 14 | 'dropdown', 15 | 'object', 16 | 'password', 17 | 'phone', 18 | 'text', 19 | ] 20 | 21 | 22 | PRECISIONS = ('integer', 'decimal(1)', 'decimal(2)', 'decimal(4)', 'decimal(8)') 23 | COMMITMENT = ('-', '1 year', '2 years', '3 years', '4 years', '5 years', '6 years') 24 | BILLING_PERIOD = ( 25 | 'onetime', 26 | 'monthly', 27 | 'yearly', 28 | '2 years', 29 | '3 years', 30 | '4 years', 31 | '5 years', 32 | '6 years', 33 | ) 34 | 35 | 36 | ALLOWED_COMMITMENTS = { 37 | 'monthly': COMMITMENT, 38 | 'yearly': COMMITMENT, 39 | '2 years': ('-', '4 years', '6 years'), 40 | '3 years': ('-', '6 years'), 41 | '4 years': ('-',), 42 | '5 years': ('-',), 43 | '6 years': ('-',), 44 | 'onetime': ('-',), 45 | } 46 | 47 | 48 | CAPABILITIES = ( 49 | 'Pay-as-you-go support and schema', 50 | 'Pay-as-you-go dynamic items support', 51 | 'Pay-as-you-go future charges support', 52 | 'Pay-as-you-go late charges support', 53 | 'Consumption reporting for Reservation Items', 54 | 'Dynamic Validation of the Draft Requests', 55 | 'Dynamic Validation of the Inquiring Form', 56 | 'Reseller Authorization Level', 57 | 'Tier Accounts Sync', 58 | 'Administrative Hold', 59 | ) 60 | -------------------------------------------------------------------------------- /connect/cli/plugins/product/sync/__init__.py: -------------------------------------------------------------------------------- 1 | from connect.cli.plugins.product.sync.actions import ActionsSynchronizer # noqa: F401 2 | from connect.cli.plugins.product.sync.capabilities import CapabilitiesSynchronizer # noqa: F401 3 | from connect.cli.plugins.product.sync.configuration_values import ( # noqa: F401 4 | ConfigurationValuesSynchronizer, 5 | ) 6 | from connect.cli.plugins.product.sync.general import GeneralSynchronizer # noqa: F401 7 | from connect.cli.plugins.product.sync.items import ItemSynchronizer # noqa: F401 8 | from connect.cli.plugins.product.sync.media import MediaSynchronizer # noqa: F401 9 | from connect.cli.plugins.product.sync.messages import MessageSynchronizer # noqa: F401 10 | from connect.cli.plugins.product.sync.params import ParamsSynchronizer # noqa: F401 11 | from connect.cli.plugins.product.sync.static_resources import ( # noqa: F401 12 | StaticResourcesSynchronizer, 13 | ) 14 | from connect.cli.plugins.product.sync.templates import TemplatesSynchronizer # noqa: F401 15 | -------------------------------------------------------------------------------- /connect/cli/plugins/product/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from copy import deepcopy 3 | 4 | 5 | def cleanup_product_for_update(product): 6 | del product['icon'] 7 | ppu = product['capabilities'].get('ppu', False) 8 | if ( 9 | product['capabilities']['subscription'] 10 | and 'schema' in product['capabilities']['subscription'] 11 | ): 12 | del product['capabilities']['subscription']['schema'] 13 | if ppu and 'predictive' in ppu: 14 | del product['capabilities']['ppu']['predictive'] 15 | return product 16 | 17 | 18 | def get_json_object_for_param(original_param): 19 | param = deepcopy(original_param) 20 | del param['id'] 21 | del param['name'] 22 | del param['title'] 23 | del param['description'] 24 | del param['phase'] 25 | del param['scope'] 26 | del param['type'] 27 | del param['constraints']['required'] 28 | del param['constraints']['unique'] 29 | del param['constraints']['hidden'] 30 | del param['position'] 31 | del param['events'] 32 | 33 | return json.dumps(param, indent=4, sort_keys=True) 34 | 35 | 36 | class ParamSwitchNotSupported(Exception): 37 | pass 38 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/plugins/project/__init__.py -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/plugins/project/extension/__init__.py -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2021 CloudBlue. All rights reserved. 2 | 3 | 4 | PYPI_EXTENSION_RUNNER_URL = 'https://pypi.org/pypi/connect-extension-runner/json' 5 | 6 | PRE_COMMIT_HOOK = """#! /bin/sh 7 | 8 | set -e 9 | 10 | if [ -f /usr/local/bin/extension-check-static ]; then 11 | exec /usr/local/bin/extension-check-static {package_name} 12 | else 13 | exec docker-compose run -T {project_slug}_bash /usr/local/bin/extension-check-static {package_name} 14 | fi 15 | """ 16 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/${package_name}/__init__.py.j2: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 4 | # All rights reserved. 5 | # 6 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/${package_name}/anvil.py.j2: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 4 | # All rights reserved. 5 | # 6 | from connect.eaas.core.decorators import anvil_callable, anvil_key_variable{% if include_variables_example == 'y' -%}, variables{% endif %} 7 | from connect.eaas.core.extension import AnvilApplicationBase 8 | 9 | 10 | @anvil_key_variable('ANVIL_API_KEY') 11 | {% if include_variables_example == 'y' -%} 12 | @variables([{ 13 | 'name': 'VAR_NAME_1', 14 | 'initial_value': 'VAR_VALUE_1', 15 | 'secure': False, 16 | }]) 17 | {% endif -%} 18 | class {{ project_slug|replace("_", " ")|title|replace(" ", "") }}AnvilApplication(AnvilApplicationBase): 19 | @anvil_callable( 20 | summary='This is an example method', 21 | description='This method is just an example.', 22 | ) 23 | def example_method(self, example_argument): 24 | return f'Example method invoked, argument = {example_argument}' 25 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/${package_name}/extension.json.j2: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ project_name }}", 3 | "description": "{{ description }}", 4 | "version": "{{ version }}", 5 | "audience": {% if extension_type == 'products' %}["vendor"]{% elif extension_type == 'hub' %}["distributor", "reseller"]{% else %}{{ extension_audience|tojson }}{% endif %}, 6 | "readme_url": "https://example.com/README.md", 7 | "changelog_url": "https://example.com/CHANGELOG.md"{% if webapp_supports_ui == 'y' %}, 8 | "icon": "googleExtensionBaseline"{% endif %} 9 | } -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/${package_name}/schemas.py.j2: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 4 | # All rights reserved. 5 | # 6 | from typing import {% if extension_type == 'multiaccount' %}List, {% endif %}Optional 7 | 8 | from pydantic import BaseModel, validator 9 | 10 | 11 | class Marketplace(BaseModel): 12 | id: str 13 | name: str 14 | description: str 15 | icon: Optional[str] 16 | 17 | @validator('icon') 18 | def set_icon(cls, icon): 19 | return icon or '/static/images/mkp.svg' 20 | 21 | {% if extension_type == 'multiaccount' %} 22 | class Settings(BaseModel): 23 | marketplaces: List[Marketplace] = [] 24 | {% endif -%} 25 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/${package_name}/static/.gitkeep.j2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/${package_name}/static/.gitkeep.j2 -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/${package_name}/tfnapp.py.j2: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 4 | # All rights reserved. 5 | # 6 | from connect.eaas.core.decorators import manual_transformation, transformation{% if include_variables_example == 'y' -%}, variables{% endif %} 7 | from connect.eaas.core.extension import TransformationsApplicationBase 8 | from connect.eaas.core.responses import RowTransformationResponse 9 | 10 | 11 | {% if include_variables_example == 'y' -%} 12 | @variables([{ 13 | 'name': 'VAR_NAME_1', 14 | 'initial_value': 'VAR_VALUE_1', 15 | 'secure': False, 16 | }]) 17 | {% endif -%} 18 | class {{ project_slug|replace("_", " ")|title|replace(" ", "") }}TransformationsApplication(TransformationsApplicationBase): 19 | @transformation( 20 | name='Manual transformation', 21 | description=( 22 | 'This transformation function allows to describe a manual ' 23 | 'procedure to be done.' 24 | ), 25 | edit_dialog_ui='/static/transformations/manual.html', 26 | ) 27 | @manual_transformation() 28 | {% if use_asyncio == 'y' %}async {% endif %}def manual_transformation(self, row: dict): 29 | pass 30 | 31 | @transformation( 32 | name='Copy Column(s)', 33 | description=( 34 | 'This transformation function allows copy values from Input to Output columns, ' 35 | 'which can be used in case of change column name in the output data or ' 36 | 'create a copy of values in table.' 37 | ), 38 | edit_dialog_ui='/static/transformations/copy.html', 39 | ) 40 | def copy_columns(self, row: dict): 41 | tfn_settings = ( 42 | self.transformation_request['transformation']['settings'] 43 | ) 44 | result = {} 45 | 46 | for setting in tfn_settings: 47 | result[setting['to']] = row[setting['from']] 48 | 49 | return RowTransformationResponse.done(result) 50 | 51 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/.${project_slug}_dev.env.j2: -------------------------------------------------------------------------------- 1 | # In order to use this extension locally, 2 | # you must obtain an API Key from the integration module of your account. 3 | # Please use a Custom token when running locally. 4 | export API_KEY="{{ api_key }}" 5 | 6 | # You can obtain your environment ID going to DevOps module, 7 | # there you will find this key for the concrete environment you want to use, 8 | # either development, testing, or production 9 | export ENVIRONMENT_ID="{{ environment_id }}" 10 | 11 | # Please provide the hostname of Connect API. 12 | # This one can be found in the general section on the integrations module 13 | export SERVER_ADDRESS="{{ server_address }}" 14 | 15 | 16 | # Please note that this file shall ONLY be present when running locally 17 | # and never in a cloud environment. -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/.eslintrc.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - airbnb-base 4 | - plugin:import/warnings 5 | 6 | env: 7 | browser: true 8 | jest: true 9 | 10 | plugins: 11 | - import 12 | 13 | settings: 14 | import/resolver: 15 | webpack: 16 | node: { } 17 | 18 | rules: 19 | sort-imports: 20 | - error 21 | - ignoreDeclarationSort: true 22 | no-multiple-empty-lines: 0 23 | arrow-parens: 0 24 | max-classes-per-file: 0 25 | import/extensions: 0 26 | import/no-cycle: 27 | - error 28 | - maxDepth: 2 29 | import/order: off 30 | import/newline-after-import: 31 | - error 32 | - count: 2 33 | padding-line-between-statements: 34 | - error 35 | - blankLine: always 36 | prev: "*" 37 | next: return 38 | - blankLine: always 39 | prev: "*" 40 | next: export 41 | no-param-reassign: 42 | - 2 43 | - props: false 44 | no-console: 45 | - error 46 | - allow: 47 | - warn 48 | - error 49 | object-curly-newline: 50 | - error 51 | - ImportDeclaration: 52 | minProperties: 1 53 | multiline: true 54 | consistent: true 55 | ObjectExpression: 56 | consistent: true 57 | minProperties: 3 58 | require-await: 59 | - error 60 | 61 | parserOptions: 62 | parser: "@babel/eslint-parser" 63 | requireConfigFile: false 64 | 65 | globals: 66 | inject: false 67 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/.flake8.j2: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .idea,.vscode,.git,pg_data,venv,env, 3 | show-source = True 4 | max-line-length = 100 5 | application-import-names = {{ package_name }} 6 | import-order-style = smarkets 7 | max-cognitive-complexity = 15 8 | ignore = B008,FI1,I100,W503 9 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/.github/workflows/test.yml.j2: -------------------------------------------------------------------------------- 1 | name: Test {{ project_name }} 2 | {% raw %} 3 | on: 4 | push: 5 | branches: '*' 6 | tags: 7 | - '*' 8 | pull_request: 9 | branches: [ master ] 10 | 11 | jobs: 12 | backend: 13 | runs-on: ubuntu-latest 14 | name: Backend tests 15 | strategy: 16 | matrix: 17 | python-version: ['3.8', '3.9', '3.10'] 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install poetry 30 | poetry config virtualenvs.create false --local 31 | poetry install --no-root 32 | - name: Linting 33 | run: | 34 | flake8 . 35 | - name: Testing 36 | run: | 37 | pytest 38 | {% endraw %} 39 | {%- if webapp_supports_ui == 'y' -%} 40 | {%- raw %} 41 | frontend: 42 | runs-on: ubuntu-latest 43 | name: Frontend tests 44 | steps: 45 | - uses: actions/checkout@v3 46 | - name: Setup node 47 | uses: actions/setup-node@v3 48 | with: 49 | node-version: 18 50 | - name: Install dependencies 51 | run: npm install 52 | - name: Linting 53 | run: npm run lint 54 | - name: Testing 55 | run: npm run test 56 | {% endraw -%} 57 | {% endif -%} 58 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/.gitignore.j2: -------------------------------------------------------------------------------- 1 | *.pyc 2 | htmlcov 3 | .coverage 4 | coverage.xml 5 | .{{ project_slug }}_dev.env 6 | node_modules 7 | ui/tests/coverage 8 | 9 | .DS_Store 10 | # Editor directories and files 11 | .vscode/* 12 | !.vscode/extensions.json 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | *.sw? 18 | .idea 19 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/CHANGELOG.md.j2: -------------------------------------------------------------------------------- 1 | # EaaS: {{ project_name }} changelog 2 | 3 | * 1.0.0: initial version 4 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/Dockerfile.j2: -------------------------------------------------------------------------------- 1 | FROM cloudblueconnect/connect-extension-runner:{{runner_version}} 2 | 3 | COPY pyproject.toml poetry.* package*.json /extension/ 4 | WORKDIR /extension 5 | RUN poetry update && poetry install --no-root 6 | RUN if [ -f "/extension/package.json" ]; then npm install; fi 7 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/HOWTO.md.j2: -------------------------------------------------------------------------------- 1 | # Welcome to Connect Extension project {{ project_name }} 2 | 3 | ## Next steps 4 | 5 | You may open your favourite IDE and start working with your project, please note that this project runs using docker. 6 | You may modify at any time the credentials used to authenticate to connect modifying the file *{{ project_slug }}/.{{ project_slug }}_dev.env*. 7 | 8 | 9 | 10 | In order to start your extension as a standalone docker container first of all you need to build the docker image for your project. To do that run: 11 | 12 | ```sh 13 | $ docker compose build 14 | ``` 15 | 16 | Once your container is built, you can access the project folder and run: 17 | 18 | ```sh 19 | $ docker compose up {{ project_slug }}_dev 20 | ``` 21 | 22 | > please note that in this way you will run the docker container and if you do changes on the code you will need to stop it and start it again. 23 | 24 | 25 | If you would like to develop and test at the same time, we recommend you run your project using the command: 26 | 27 | ```sh 28 | $ docker compose run {{ project_slug }}_bash 29 | ``` 30 | 31 | Once you get the interactive shell an help banner will be displayed to inform you about the included tools that can help you with the development of your extension. 32 | 33 | 34 | Additionally, a basic boilerplate for writing unit tests has been created, you can run the tests using: 35 | 36 | ```sh 37 | $ docker compose run {{ project_slug }}_test 38 | ``` 39 | 40 | 41 | ## Community Resources 42 | 43 | Please take note of these links in order to get additional information: 44 | 45 | * https://connect.cloudblue.com/ 46 | * https://connect.cloudblue.com/community/modules/devops/ 47 | * https://connect.cloudblue.com/community/sdk/python-openapi-client/ 48 | * https://connect-openapi-client.readthedocs.io/en/latest/ 49 | * https://connect-eaas-core.readthedocs.io/en/latest/ 50 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/README.md.j2: -------------------------------------------------------------------------------- 1 | # Welcome to {{ project_name }} ! 2 | 3 | 4 | {{ description }} 5 | 6 | 7 | {% if license != "Other, not Open-source" %} 8 | ## License 9 | 10 | **{{ project_name }}** is licensed under the *{{ license }}* license. 11 | {% endif %} 12 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/__mocks__/fileMock.js.j2: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 3 | All rights reserved. 4 | */ 5 | module.exports = 'test-file-stub'; -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/__mocks__/styleMock.js.j2: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 3 | All rights reserved. 4 | */ 5 | module.exports = {}; -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/babel.config.json.j2: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/docker-compose.yml.j2: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | {%- if webapp_supports_ui == 'y'%} 4 | volumes: 5 | node_modules: 6 | {%- endif %} 7 | 8 | services: 9 | {{project_slug}}_dev: 10 | container_name: {{project_slug}}_dev 11 | build: 12 | context: . 13 | working_dir: /extension 14 | command: cextrun -d --no-rich-logging 15 | volumes: 16 | - .:/extension 17 | {%- if webapp_supports_ui == 'y'%} 18 | - node_modules:/extension/node_modules 19 | {%- endif %} 20 | env_file: 21 | - .{{ project_slug }}_dev.env 22 | 23 | {{project_slug}}_bash: 24 | container_name: {{project_slug}}_bash 25 | build: 26 | context: . 27 | working_dir: /extension 28 | command: /bin/bash 29 | stdin_open: true 30 | tty: true 31 | volumes: 32 | - .:/extension 33 | {%- if webapp_supports_ui == 'y'%} 34 | - node_modules:/extension/node_modules 35 | {%- endif %} 36 | env_file: 37 | - .{{ project_slug }}_dev.env 38 | 39 | {{project_slug}}_test: 40 | container_name: {{project_slug}}_test 41 | build: 42 | context: . 43 | working_dir: /extension 44 | command: extension-test 45 | volumes: 46 | - .:/extension 47 | {%- if webapp_supports_ui == 'y'%} 48 | - node_modules:/extension/node_modules 49 | {%- endif %} 50 | env_file: 51 | - .{{ project_slug }}_dev.env 52 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/jest.config.js.j2: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 3 | All rights reserved. 4 | */ 5 | module.exports = { 6 | moduleFileExtensions: [ 7 | 'js', 8 | ], 9 | 10 | clearMocks: true, 11 | 12 | transform: { 13 | '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 14 | '^.+\\.js$': 'babel-jest', 15 | }, 16 | transformIgnorePatterns: [ 17 | '/node_modules/', 18 | ], 19 | 20 | 21 | testMatch: [ 22 | '/ui/tests(**/*\\.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx))', 23 | ], 24 | 25 | collectCoverage: true, 26 | 27 | collectCoverageFrom: [ 28 | 'ui/src/**/*.js', 29 | ], 30 | 31 | moduleNameMapper: { 32 | '^@/(.*)$': '/ui/src/$1', 33 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/__mocks__/fileMock.js', 34 | '\\.(css|less)$': '/__mocks__/styleMock.js', 35 | }, 36 | 37 | coverageDirectory: '/ui/tests/coverage/', 38 | 39 | coveragePathIgnorePatterns: ['/ui/src/pages/'], 40 | 41 | testEnvironment: 'jsdom', 42 | }; 43 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/package.json.j2: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ project_slug|replace("_", "-") }}", 3 | "version": "{{ version }}", 4 | "description": "{{ description }}", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=v18.19.0" 8 | }, 9 | "directories": { 10 | "test": "ui/tests" 11 | }, 12 | "scripts": { 13 | "build": "webpack", 14 | "watch": "webpack watch", 15 | "lint": "eslint ui/src --ext .js", 16 | "lint:fix": "npm run lint -- --fix", 17 | "test": "NODE_ENV=test jest --no-cache" 18 | }, 19 | "author": "{{ author }}", 20 | "license": "{{ license }}", 21 | "dependencies": { 22 | "@cloudblueconnect/connect-ui-toolkit": "^30.1.0", 23 | "@fontsource/roboto": "^5.0.8", 24 | "css-minimizer-webpack-plugin": "^6.0.0" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.23.7", 28 | "@babel/preset-env": "^7.23.8", 29 | "css-loader": "^6.9.1", 30 | "eslint": "^8.56.0", 31 | "eslint-config-airbnb-base": "^15.0.0", 32 | "eslint-import-resolver-webpack": "^0.13.8", 33 | "eslint-plugin-import": "2.29.1", 34 | "eslint-webpack-plugin": "^4.0.1", 35 | "html-webpack-plugin": "^5.6.0", 36 | "jest": "^29.7.0", 37 | "jest-environment-jsdom": "29.7.0", 38 | "jest-transform-stub": "^2.0.0", 39 | "mini-css-extract-plugin": "^2.7.7", 40 | "style-loader": "^3.3.4", 41 | "webpack": "^5.90.0", 42 | "webpack-cli": "^5.1.4", 43 | "copy-webpack-plugin": "^12.0.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/poetry.toml.j2: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | create = false 3 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/pyproject.toml.j2: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "{{ project_slug|replace("_", "-") }}" 3 | version = "{{ version }}" 4 | description = "{{ description }}" 5 | authors = ["{{ author }}"] 6 | license = "{{ license }}" 7 | packages = [ 8 | { include = "{{ package_name }}" } 9 | ] 10 | readme = "./README.md" 11 | 12 | [tool.poetry.plugins."connect.eaas.ext"] 13 | {%- if 'events' in application_types %} 14 | "eventsapp" = "{{ package_name }}.events:{{ project_slug|replace("_", " ")|title|replace(" ", "") }}EventsApplication" 15 | {%- endif %} 16 | {%- if 'webapp' in application_types %} 17 | "webapp" = "{{ package_name }}.webapp:{{ project_slug|replace("_", " ")|title|replace(" ", "") }}WebApplication" 18 | {%- endif %} 19 | {%- if 'anvil' in application_types %} 20 | "anvilapp" = "{{ package_name }}.anvil:{{ project_slug|replace("_", " ")|title|replace(" ", "") }}AnvilApplication" 21 | {%- endif %} 22 | {%- if 'tfnapp' in application_types %} 23 | "tfnapp" = "{{ package_name }}.tfnapp:{{ project_slug|replace("_", " ")|title|replace(" ", "") }}TransformationsApplication" 24 | {%- endif %} 25 | 26 | [tool.poetry.dependencies] 27 | python = ">=3.8,<4" 28 | connect-eaas-core = ">=30" 29 | 30 | [tool.poetry.dev-dependencies] 31 | pytest = ">=6.1.2,<8" 32 | pytest-cov = ">=2.10.1,<5" 33 | pytest-mock = "^3.3.1" 34 | mock = { version = "^4.0.3", markers = "python_version < '3.8'" } 35 | coverage = {extras = ["toml"], version = ">=5.3,<7"} 36 | flake8 = ">=3.8,<6" 37 | flake8-bugbear = ">=20,<23" 38 | flake8-cognitive-complexity = "^0.1" 39 | flake8-commas = "~2.0" 40 | flake8-future-import = "~0.4" 41 | flake8-import-order = "~0.18" 42 | flake8-broken-line = ">=0.3,<0.7" 43 | flake8-comprehensions = "^3.3.1" 44 | flake8-debugger = "^4.0.0" 45 | flake8-eradicate = "^1.0.0" 46 | flake8-string-format = "^0.3.0" 47 | pytest-asyncio = "^0.15.1" 48 | 49 | [build-system] 50 | requires = ["poetry-core>=1.0.0"] 51 | build-backend = "poetry.core.masonry.api" 52 | 53 | [tool.pytest.ini_options] 54 | testpaths = "tests" 55 | addopts = "--cov={{ package_name }} --cov-report=term-missing --cov-report=html --cov-report=xml" 56 | 57 | [tool.coverage.run] 58 | relative_files = true 59 | branch = true 60 | 61 | [tool.coverage.report] 62 | omit = [ 63 | ] 64 | 65 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/tests/__init__.py.j2: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 4 | # All rights reserved. 5 | # 6 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/tests/conftest.py.j2: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 4 | # All rights reserved. 5 | # 6 | import pytest 7 | from connect.client import AsyncConnectClient, ConnectClient 8 | 9 | 10 | @pytest.fixture 11 | def connect_client(): 12 | return ConnectClient( 13 | 'ApiKey fake_api_key', 14 | endpoint='https://localhost/public/v1', 15 | ) 16 | 17 | 18 | @pytest.fixture 19 | def async_connect_client(): 20 | return AsyncConnectClient( 21 | 'ApiKey fake_api_key', 22 | endpoint='https://localhost/public/v1', 23 | ) 24 | 25 | 26 | @pytest.fixture 27 | def logger(mocker): 28 | return mocker.MagicMock() 29 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/tests/test_anvil.py.j2: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2022, Globex Corporation 4 | # All rights reserved. 5 | # 6 | from {{ package_name }}.anvil import {{ project_slug|replace("_", " ")|title|replace(" ", "") }}AnvilApplication 7 | 8 | 9 | def test_example_method( 10 | connect_client, 11 | client_mocker_factory, 12 | logger, 13 | ): 14 | config = {} 15 | anvil_app = {{ project_slug|replace("_", " ")|title|replace(" ", "") }}AnvilApplication(connect_client, logger, config) 16 | assert anvil_app.example_method('an_argument') == ( 17 | 'Example method invoked, argument = an_argument' 18 | ) 19 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/tests/test_events.py.j2: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 4 | # All rights reserved. 5 | # 6 | {% if use_asyncio == 'y' -%} 7 | import pytest 8 | 9 | {% endif -%} 10 | from {{ package_name }}.events import {{ project_slug|replace("_", " ")|title|replace(" ", "") }}EventsApplication 11 | 12 | {% for bg_event in background -%} 13 | {% if use_asyncio == 'y' %} 14 | @pytest.mark.asyncio 15 | {%- endif %} 16 | {% if use_asyncio == 'y' %}async {% endif %}def test_handle_{{ bg_event }}( 17 | {% if use_asyncio == 'y' %}async_connect_client{% else %}connect_client{% endif %}, 18 | {% if use_asyncio == 'y' %}async_client_mocker_factory{% else %}client_mocker_factory{% endif %}, 19 | logger, 20 | ): 21 | config = {} 22 | request = {'id': 1} 23 | ext = {{ project_slug|replace("_", " ")|title|replace(" ", "") }}EventsApplication({% if use_asyncio == 'y' %}async_connect_client{% else %}connect_client{% endif %}, logger, config) 24 | result = {% if use_asyncio == 'y' %}await {% endif %}ext.handle_{{ bg_event }}(request) 25 | assert result.status == 'success' 26 | 27 | {% endfor -%} 28 | 29 | {% for interactive_event in interactive -%} 30 | {% if use_asyncio == 'y' %} 31 | @pytest.mark.asyncio 32 | {%- endif %} 33 | {% if use_asyncio == 'y' %}async {% endif %}def test_handle_{{ interactive_event }}( 34 | {% if use_asyncio == 'y' %}async_connect_client{% else %}connect_client{% endif %}, 35 | {% if use_asyncio == 'y' %}async_client_mocker_factory{% else %}client_mocker_factory{% endif %}, 36 | logger, 37 | ): 38 | config = {} 39 | request = {'id': 1} 40 | ext = {{ project_slug|replace("_", " ")|title|replace(" ", "") }}EventsApplication({% if use_asyncio == 'y' %}async_connect_client{% else %}connect_client{% endif %}, logger, config) 41 | result = {% if use_asyncio == 'y' %}await {% endif %}ext.handle_{{ interactive_event }}(request) 42 | assert result.status == 'success' 43 | assert result.body == request 44 | 45 | {% endfor -%} 46 | 47 | {% if 'scheduled' in event_types -%} 48 | {% if use_asyncio == 'y' %} 49 | @pytest.mark.asyncio 50 | {%- endif %} 51 | {% if use_asyncio == 'y' %}async {% endif %}def test_execute_scheduled_processing( 52 | {% if use_asyncio == 'y' %}async_connect_client{% else %}connect_client{% endif %}, 53 | {% if use_asyncio == 'y' %}async_client_mocker_factory{% else %}client_mocker_factory{% endif %}, 54 | logger, 55 | ): 56 | config = {} 57 | request = {'id': 1} 58 | ext = {{ project_slug|replace("_", " ")|title|replace(" ", "") }}EventsApplication({% if use_asyncio == 'y' %}async_connect_client{% else %}connect_client{% endif %}, logger, config) 59 | result = {% if use_asyncio == 'y' %}await {% endif %}ext.execute_scheduled_processing(request) 60 | assert result.status == 'success' 61 | {% endif -%} 62 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/tests/test_tfnapp.py.j2: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2022, Globex Corporation 4 | # All rights reserved. 5 | # 6 | from connect.eaas.core.enums import ResultType 7 | 8 | from {{ package_name }}.tfnapp import {{ project_slug|replace("_", " ")|title|replace(" ", "") }}TransformationsApplication 9 | 10 | 11 | def test_copy_columns(connect_client, logger, mocker): 12 | app = {{ project_slug|replace("_", " ")|title|replace(" ", "") }}TransformationsApplication( 13 | connect_client, 14 | logger, 15 | mocker.MagicMock(), 16 | installation_client=connect_client, 17 | installation={'id': 'EIN-0000'}, 18 | context=mocker.MagicMock(), 19 | transformation_request={ 20 | 'transformation': { 21 | 'settings': [ 22 | {'from': 'ColumnA', 'to': 'NewColC'}, 23 | {'from': 'ColumnB', 'to': 'NewColD'}, 24 | ], 25 | }, 26 | }, 27 | ) 28 | 29 | response = app.copy_columns( 30 | { 31 | 'ColumnA': 'ContentColumnA', 32 | 'ColumnB': 'ContentColumnB', 33 | }, 34 | ) 35 | assert response.status == ResultType.SUCCESS 36 | assert response.transformed_row == { 37 | 'NewColC': 'ContentColumnA', 38 | 'NewColD': 'ContentColumnB', 39 | } 40 | 41 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/ui/images/mkp.svg.j2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/ui/pages/index.html.j2: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <%= htmlWebpackPlugin.options.title %> 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 |
    25 |
26 |
27 |
28 |
29 |
30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/ui/pages/settings.html.j2: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <%= htmlWebpackPlugin.options.title %> 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 |
20 |
    21 |
22 |
23 |
24 | 25 |
26 |
27 |
28 |
29 | 30 |
31 |

Something went wrong. Please try to reload the page.

32 |
33 |
34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/ui/pages/transformations/copy.html.j2: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <%= htmlWebpackPlugin.options.title %> 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/ui/pages/transformations/manual.html.j2: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <%= htmlWebpackPlugin.options.title %> 12 | 13 | 14 | 15 | 16 |
17 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/ui/src/pages/index.js.j2: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 3 | All rights reserved. 4 | */ 5 | import createApp, { 6 | Card, 7 | } from '@cloudblueconnect/connect-ui-toolkit'; 8 | import { 9 | index, 10 | } from '../pages'; 11 | import '@fontsource/roboto/500.css'; 12 | import '../../styles/index.css'; 13 | 14 | 15 | createApp({ 'ui-card': Card }) 16 | .then(index); 17 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/ui/src/pages/settings.js.j2: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 3 | All rights reserved. 4 | */ 5 | import createApp, { 6 | Card, 7 | } from '@cloudblueconnect/connect-ui-toolkit'; 8 | import { 9 | settings, 10 | } from '../pages'; 11 | import '@fontsource/roboto/500.css'; 12 | import '../../styles/index.css'; 13 | 14 | 15 | createApp({ 'ui-card': Card }) 16 | .then(settings); 17 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/ui/src/pages/transformations/copy.js.j2: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023, CloudBlue LLC 3 | All rights reserved. 4 | */ 5 | import createApp from '@cloudblueconnect/connect-ui-toolkit'; 6 | import { 7 | copy, 8 | } from '../../pages'; 9 | import '@fontsource/roboto/500.css'; 10 | import '../../../styles/index.css'; 11 | 12 | 13 | createApp({ }) 14 | .then(copy); 15 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/ui/src/pages/transformations/manual.js.j2: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023, CloudBlue LLC 3 | All rights reserved. 4 | */ 5 | import createApp from '@cloudblueconnect/connect-ui-toolkit'; 6 | import { 7 | manual, 8 | } from '../../pages'; 9 | import '@fontsource/roboto/500.css'; 10 | import '../../../styles/index.css'; 11 | import '../../../styles/manual.css'; 12 | 13 | 14 | createApp({ }) 15 | .then(manual); 16 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/ui/src/utils.js.j2: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 4 | All rights reserved. 5 | */ 6 | // API calls to the backend 7 | {%- if extension_type != 'transformations' %} 8 | export const getSettings = () => fetch('/api/settings').then((response) => response.json()); 9 | 10 | export const getChart = () => fetch('/api/chart').then((response) => response.json()); 11 | 12 | export const getMarketplaces = () => fetch('/api/marketplaces').then((response) => response.json()); 13 | 14 | export const updateSettings = (settings) => fetch('/api/settings', { 15 | method: 'POST', 16 | headers: { 'Content-Type': 'application/json' }, 17 | body: JSON.stringify(settings), 18 | }).then((response) => response.json()); 19 | 20 | // data processing 21 | export const processMarketplaces = ( 22 | allMarketplaces, 23 | selectedMarketplaces, 24 | ) => allMarketplaces.map((marketplace) => { 25 | const checked = !!selectedMarketplaces.find( 26 | (selectedMarketplace) => selectedMarketplace.id === marketplace.id, 27 | ); 28 | 29 | return { ...marketplace, checked }; 30 | }); 31 | 32 | export const processSelectedMarketplaces = ( 33 | allMarketplaces, 34 | checkboxes, 35 | ) => checkboxes.map((checkbox) => allMarketplaces.find( 36 | (marketplace) => marketplace.id === checkbox.value, 37 | )); 38 | 39 | export const processCheckboxes = ( 40 | checkboxes, 41 | ) => Array.from(checkboxes).filter(checkbox => checkbox.checked); 42 | 43 | {%- else %} 44 | /* eslint-disable import/prefer-default-export */ 45 | export const validate = (functionName, data) => fetch(`/api/validate/${functionName}`, { 46 | method: 'POST', 47 | headers: { 48 | 'Content-Type': 'application/json', 49 | }, 50 | body: JSON.stringify(data), 51 | }).then((response) => response.json()); 52 | 53 | {%- endif %} -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/ui/styles/manual.css.j2: -------------------------------------------------------------------------------- 1 | #description, 2 | #edit-input, 3 | #settings, 4 | #output { 5 | width: 466px; 6 | border: 1px solid #e0e0e0; 7 | border-radius: 8px; 8 | padding: 8px; 9 | margin-bottom: 8px; 10 | } 11 | 12 | #edit-input-columns-table { 13 | width: 450px; 14 | border-collapse: collapse; 15 | } 16 | 17 | #edit-input-columns-table th { 18 | background-color: #F5F5F5; 19 | color: #707070; 20 | font-size: 12px; 21 | font-weight: 500; 22 | height: 32px; 23 | text-align: left; 24 | } 25 | 26 | #edit-input-columns-table td { 27 | font-size: 14px; 28 | line-height: 20px; 29 | color: #212121; 30 | height: 32px; 31 | border-top: 1px solid #e0e0e0; 32 | } 33 | 34 | #description-text, 35 | #settings-text { 36 | width: 450px; 37 | resize: none; 38 | border: 1px solid #e0e0e0; 39 | border-radius: 4px; 40 | } -------------------------------------------------------------------------------- /connect/cli/plugins/project/extension/validators.py: -------------------------------------------------------------------------------- 1 | from interrogatio.core.exceptions import ValidationError 2 | from interrogatio.validators import Validator 3 | 4 | 5 | class AppTypesValidator(Validator): 6 | def validate(self, value, context=None): 7 | if context.get('extension_type') == 'transformations' and ( 8 | 'webapp' not in value or 'tfnapp' not in value 9 | ): 10 | raise ValidationError( 11 | 'Web Application and Transformations Application ' 12 | 'are mandatory for Commerce type extensions.', 13 | ) 14 | 15 | 16 | class UISupportValidator(Validator): 17 | def validate(self, value, context=None): 18 | if context.get('extension_type') == 'transformations' and value != 'y': 19 | raise ValidationError( 20 | 'Web Application UI support is mandatory for Commerce type extensions.', 21 | ) 22 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/plugins/project/report/__init__.py -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/add/${initial_report_slug}/README.md.j2: -------------------------------------------------------------------------------- 1 | # Report {{ initial_report_name }} 2 | 3 | This report provides all data I need 4 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/add/${initial_report_slug}/__init__.py.j2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/plugins/project/report/templates/add/${initial_report_slug}/__init__.py.j2 -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/add/${initial_report_slug}/entrypoint.py.j2: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 4 | # All rights reserved. 5 | 6 | {% if use_asyncio == 'y' %}async {% endif %}def generate( 7 | client=None, 8 | input_data=None, 9 | progress_callback=None, 10 | renderer_type='{{ initial_report_renderer }}', 11 | extra_context_callback=None, 12 | ): 13 | """ 14 | Extracts data from Connect using the ConnectClient instance 15 | and input data provided as arguments, applies 16 | required transformations (if any) and returns the data rendered 17 | by the given renderer on the arguments list. 18 | Some renderers may require extra context data to generate the report 19 | output, for example in the case of the Jinja2 renderer... 20 | 21 | :param client: An instance of the CloudBlue Connect 22 | client. 23 | :type client: cnct.ConnectClient 24 | :param input_data: Input data used to calculate the 25 | resulting dataset. 26 | :type input_data: dict 27 | :param progress_callback: A function that accepts t 28 | argument of type int that must 29 | be invoked to notify the progress 30 | of the report generation. 31 | :type progress_callback: func 32 | :param renderer_type: Renderer required for generating report 33 | output. 34 | :type renderer_type: string 35 | :param extra_context_callback: Extra content required by some 36 | renderers. 37 | :type extra_context_callback: func 38 | """ 39 | pass 40 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/add/${initial_report_slug}/templates/pdf/template.css.j2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/plugins/project/report/templates/add/${initial_report_slug}/templates/pdf/template.css.j2 -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/add/${initial_report_slug}/templates/pdf/template.html.j2.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Report 7 | 8 | 9 | 10 | {% raw %} 11 | {% for row in data %} 12 | 13 | {% for col in row %} 14 | 15 | {% endfor %} 16 | 17 | {% endfor %} 18 | {% endraw %} 19 |
{{ col }}
20 | 21 | 22 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/add/${initial_report_slug}/templates/xml/template.xml.j2.j2: -------------------------------------------------------------------------------- 1 | 2 | {% raw %} 3 | {% for row in data %} 4 | 5 | {% for col in row %} 6 | {{ col }} 7 | {% endfor %} 8 | 9 | {% endfor %} 10 | {% endraw %} 11 | 12 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/${package_name}/${initial_report_slug}/README.md.j2: -------------------------------------------------------------------------------- 1 | # Report {{ initial_report_name }} 2 | 3 | {{ initial_report_description }} 4 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/${package_name}/${initial_report_slug}/__init__.py.j2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/${package_name}/${initial_report_slug}/__init__.py.j2 -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/${package_name}/${initial_report_slug}/entrypoint.py.j2: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 4 | # All rights reserved. 5 | 6 | {% if use_asyncio == 'y' %}async {% endif %}def generate( 7 | client=None, 8 | input_data=None, 9 | progress_callback=None, 10 | renderer_type='{{ initial_report_renderer }}', 11 | extra_context_callback=None, 12 | ): 13 | """ 14 | Extracts data from Connect using the ConnectClient instance 15 | and input data provided as arguments, applies 16 | required transformations (if any) and returns the data rendered 17 | by the given renderer on the arguments list. 18 | Some renderers may require extra context data to generate the report 19 | output, for example in the case of the Jinja2 renderer... 20 | 21 | :param client: An instance of the CloudBlue Connect 22 | client. 23 | :type client: cnct.ConnectClient 24 | :param input_data: Input data used to calculate the 25 | resulting dataset. 26 | :type input_data: dict 27 | :param progress_callback: A function that accepts t 28 | argument of type int that must 29 | be invoked to notify the progress 30 | of the report generation. 31 | :type progress_callback: func 32 | :param renderer_type: Renderer required for generating report 33 | output. 34 | :type renderer_type: string 35 | :param extra_context_callback: Extra content required by some 36 | renderers. 37 | :type extra_context_callback: func 38 | """ 39 | pass 40 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/${package_name}/${initial_report_slug}/templates/pdf/template.css.j2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/${package_name}/${initial_report_slug}/templates/pdf/template.css.j2 -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/${package_name}/${initial_report_slug}/templates/pdf/template.html.j2.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ initial_report_name }} 7 | 8 | 9 | 10 | {% raw %} 11 | {% for row in data %} 12 | 13 | {% for col in row %} 14 | 15 | {% endfor %} 16 | 17 | {% endfor %} 18 | {% endraw %} 19 |
{{ col }}
20 | 21 | 22 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/${package_name}/${initial_report_slug}/templates/xml/template.xml.j2.j2: -------------------------------------------------------------------------------- 1 | 2 | {% raw %} 3 | {% for row in data %} 4 | 5 | {% for col in row %} 6 | {{ col }} 7 | {% endfor %} 8 | 9 | {% endfor %} 10 | {% endraw %} 11 | 12 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/.flake8.j2: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .idea,.vscode,.git,pg_data,venv,env,msmiddleware/settings/*.py,*/migrations/*.py, 3 | show-source = True 4 | max-line-length = 100 5 | application-import-names = {{ package_name }} 6 | import-order-style = smarkets 7 | max-cognitive-complexity = 15 8 | ignore = FI1,I100,W503 9 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/.github/workflows/build.yml.j2: -------------------------------------------------------------------------------- 1 | name: Build {{ project_name }} 2 | {% raw %} 3 | on: 4 | push: 5 | branches: '*' 6 | tags: 7 | - '*' 8 | pull_request: 9 | branches: [ master ] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python-version: ['3.8', '3.9', '3.10'] 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install poetry 29 | poetry config virtualenvs.create false --local 30 | poetry install --no-root 31 | - name: Linting 32 | run: | 33 | flake8 . 34 | - name: Testing 35 | run: | 36 | pytest 37 | {% endraw %} -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/.gitignore.j2: -------------------------------------------------------------------------------- 1 | *.pyc 2 | htmlcov 3 | .coverage 4 | coverage.xml 5 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/HOWTO.md.j2: -------------------------------------------------------------------------------- 1 | # Welcome to Connect Report project {{ project_name }} 2 | 3 | ## Next steps 4 | 5 | You may open your favorite IDE and start working with your project. 6 | 7 | 8 | ## Community Resources 9 | 10 | Please take note of these links in order to get additional information: 11 | 12 | * https://connect.cloudblue.com/ 13 | * https://connect.cloudblue.com/community/modules/reports/ 14 | * https://connect.cloudblue.com/community/sdk/python-openapi-client/ 15 | * https://connect-openapi-client.readthedocs.io/en/latest/ 16 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/README.md.j2: -------------------------------------------------------------------------------- 1 | # Welcome to {{ project_name }} ! 2 | 3 | 4 | {{ description }} 5 | 6 | 7 | ## License 8 | 9 | **{{ project_name }}** is licensed under the *{{ license }}* license. 10 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/poetry.toml.j2: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | create = false 3 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/pyproject.toml.j2: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "{{ project_slug|replace("_", "-") }}" 3 | version = "{{ version }}" 4 | description = "{{ description }}" 5 | authors = ["{{ author }}"] 6 | license = "{{ license }}" 7 | packages = [ 8 | { include = "{{ package_name }}" } 9 | ] 10 | readme = "./README.md" 11 | 12 | [tool.poetry.dependencies] 13 | python = ">=3.8,<4" 14 | 15 | [tool.poetry.dev-dependencies] 16 | connect-cli = "^{{ cli_version }}" 17 | connect-openapi-client = "^24" 18 | {%+ if use_asyncio == 'y' %}pytest-asyncio = "^0.19"{% endif %} 19 | {%+ if use_asyncio == 'y' %}pytest-httpx = ">=0.21"{% endif %} 20 | pytest = "^6.2.1" 21 | pytest-cov = "^2.10.1" 22 | pytest-mock = "^3.4" 23 | responses = "^0.12" 24 | coverage = {extras = ["toml"], version = "^5.3.1"} 25 | isort = {extras = ["pyproject"], version = "^5.6.4"} 26 | flake8 = "^3.8.4" 27 | flake8-broken-line = "^0" 28 | flake8-bugbear = "~20" 29 | flake8-comprehensions = "~3" 30 | flake8-string-format = "^0" 31 | flake8-debugger = "~3" 32 | flake8-eradicate = "~1" 33 | 34 | [build-system] 35 | requires = ["poetry-core>=1.0.0"] 36 | build-backend = "poetry.core.masonry.api" 37 | 38 | [tool.pytest.ini_options] 39 | testpaths = "tests" 40 | addopts = "--cov={{ package_name }} --cov-report=term-missing:skip-covered --cov-report=html --cov-report=xml" 41 | 42 | [tool.coverage.run] 43 | branch = true 44 | 45 | [tool.coverage.report] 46 | omit = [ 47 | ] 48 | 49 | exclude_lines = [ 50 | "pragma: no cover", 51 | "def __str__", 52 | "def __repr__", 53 | "raise NotImplementedError", 54 | "if __name__ == .__main__.:", 55 | ] 56 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/reports.json.j2: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ project_name }}", 3 | "readme_file": "README.md", 4 | "version": "{{ version }}", 5 | "language": "python", 6 | "reports": [ 7 | { 8 | "name": "{{ initial_report_name }}", 9 | "readme_file": "{{ package_name }}/{{ initial_report_slug }}/README.md", 10 | "entrypoint": "{{ package_name }}.{{ initial_report_slug }}.entrypoint.generate", 11 | "audience": [ 12 | "provider", 13 | "vendor" 14 | ], 15 | "report_spec": "2", 16 | "parameters": [], 17 | "renderers": [ 18 | { 19 | "id": "xlsx", 20 | "type": "xlsx", 21 | "default": {% if initial_report_renderer=='xlsx' %}true{% else %}false{% endif %}, 22 | "description": "Export data in Microsoft Excel 2020 format.", 23 | "template": "{{ package_name }}/{{ initial_report_slug }}/templates/xlsx/template.xlsx", 24 | "args": { 25 | "start_row": 2, 26 | "start_col": 1 27 | } 28 | }, 29 | { 30 | "id": "json", 31 | "type": "json", 32 | "default": {% if initial_report_renderer=='json' %}true{% else %}false{% endif %}, 33 | "description": "Export data as JSON" 34 | }, 35 | { 36 | "id": "csv", 37 | "type": "csv", 38 | "default": {% if initial_report_renderer=='csv' %}true{% else %}false{% endif %}, 39 | "description": "Export data as CSV" 40 | }, 41 | { 42 | "id": "xml", 43 | "type": "jinja2", 44 | "default": {% if initial_report_renderer=='xml' %}true{% else %}false{% endif %}, 45 | "description": "Export data as XML", 46 | "template": "{{ package_name }}/{{ initial_report_slug }}/templates/xml/template.xml.j2" 47 | }, 48 | { 49 | "id": "pdf-portrait", 50 | "type": "pdf", 51 | "default": {% if initial_report_renderer=='pdf' %}true{% else %}false{% endif %}, 52 | "description": "Export data as PDF (portrait)", 53 | "template": "{{ package_name }}/{{ initial_report_slug }}/templates/pdf/template.html.j2", 54 | "args": { 55 | "css_file": "{{ package_name }}/{{ initial_report_slug }}/templates/pdf/template.css" 56 | } 57 | } 58 | ] 59 | } 60 | ] 61 | } -------------------------------------------------------------------------------- /connect/cli/plugins/project/report/templates/bootstrap/${project_slug}/tests/__init__.py.j2: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) {% now 'utc', '%Y' %}, {{ author }} 4 | # All rights reserved. 5 | # 6 | -------------------------------------------------------------------------------- /connect/cli/plugins/project/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import stat 4 | 5 | from rich import box 6 | from rich.markdown import Markdown 7 | from rich.syntax import Syntax 8 | from rich.table import Table 9 | 10 | from connect.cli.core.terminal import console 11 | 12 | 13 | def force_delete(func, path, exc_info): 14 | os.chmod(path, stat.S_IWRITE) 15 | func(path) 16 | 17 | 18 | def purge_dir(dir): 19 | if os.path.isdir(dir): 20 | shutil.rmtree(dir, onerror=force_delete) 21 | 22 | 23 | def slugify(name): 24 | return ( 25 | name.lower().strip().replace(' ', '_').replace('-', '_').replace('.', '_').replace(',', '') 26 | ) 27 | 28 | 29 | def show_validation_result_table(validation_items): 30 | for item in validation_items: 31 | table = Table( 32 | box=box.ROUNDED, 33 | show_header=False, 34 | ) 35 | table.add_column('Field', style='blue') 36 | table.add_column('Value') 37 | level_color = 'red' if item.level == 'ERROR' else 'yellow' 38 | table.add_row('Level', f'[bold {level_color}]{item.level}[/]') 39 | table.add_row('Message', Markdown(item.message)) 40 | table.add_row('File', item.file or '-') 41 | table.add_row( 42 | 'Code', 43 | Syntax( 44 | item.code, 45 | 'python3', 46 | theme='ansi_light', 47 | dedent=True, 48 | line_numbers=True, 49 | start_line=item.start_line, 50 | highlight_lines={item.lineno}, 51 | ) 52 | if item.code 53 | else '-', 54 | ) 55 | console.print(table) 56 | -------------------------------------------------------------------------------- /connect/cli/plugins/report/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/plugins/report/__init__.py -------------------------------------------------------------------------------- /connect/cli/plugins/report/commands.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | 4 | import click 5 | 6 | from connect.cli.core import group 7 | from connect.cli.core.config import pass_config 8 | from connect.cli.plugins.report.helpers import execute_report, list_reports, show_report_info 9 | 10 | 11 | DEFAULT_REPORT_DIR = os.path.normpath( 12 | os.path.join(os.path.dirname(__file__), '../../../.data/connect_reports'), 13 | ) 14 | 15 | 16 | @group(name='report', short_help='Reports execution and development.') 17 | def grp_report(): 18 | pass # pragma: no cover 19 | 20 | 21 | @grp_report.command( 22 | name='execute', 23 | short_help='Execute a report.', 24 | ) 25 | @click.argument('report_id', metavar='REPORT_ID', nargs=1, required=True) 26 | @click.option( 27 | '--reports-dir', 28 | '-d', 29 | 'reports_dir', 30 | default=DEFAULT_REPORT_DIR, 31 | type=click.Path(exists=True, file_okay=False, dir_okay=True), 32 | help='Report project root directory.', 33 | ) 34 | @click.option( 35 | '--output-file', 36 | '-o', 37 | 'output_file', 38 | type=click.Path(exists=False, file_okay=True, dir_okay=False), 39 | help='Output file.', 40 | ) 41 | @click.option( 42 | '--output-format', 43 | '-f', 44 | 'output_format', 45 | help='Output format.', 46 | ) 47 | @pass_config 48 | def cmd_execute_report(config, reports_dir, report_id, output_file, output_format): 49 | if not output_file: 50 | output_file = os.path.join( 51 | os.getcwd(), 52 | f'report_{report_id}_{datetime.now().strftime("%Y%m%d_%H%M")}', 53 | ) 54 | execute_report(config, reports_dir, report_id, output_file, output_format) 55 | 56 | 57 | @grp_report.command( 58 | name='list', 59 | short_help='List available reports.', 60 | ) 61 | @click.option( 62 | '--reports-dir', 63 | '-d', 64 | 'reports_dir', 65 | default=DEFAULT_REPORT_DIR, 66 | type=click.Path(exists=True, file_okay=False, dir_okay=True), 67 | help='Report project root directory. Do not specify for listing default reports.', 68 | ) 69 | def cmd_list_reports(reports_dir): 70 | list_reports(reports_dir) 71 | 72 | 73 | @grp_report.command( 74 | name='info', 75 | short_help='Get additional information for a given report.', 76 | ) 77 | @click.argument('report_id', metavar='REPORT_ID', nargs=1, required=True) 78 | @click.option( 79 | '--reports-dir', 80 | '-d', 81 | 'reports_dir', 82 | default=DEFAULT_REPORT_DIR, 83 | type=click.Path(exists=True, file_okay=False, dir_okay=True), 84 | help='Report project root directory. Do not specify for listing default reports', 85 | ) 86 | def get_report_info(reports_dir, report_id): 87 | show_report_info(reports_dir, report_id) 88 | 89 | 90 | def get_group(): 91 | return grp_report 92 | -------------------------------------------------------------------------------- /connect/cli/plugins/report/constants.py: -------------------------------------------------------------------------------- 1 | AVAILABLE_REPORTS = """ 2 | # Available reports 3 | 4 | 5 | | ID | Name | 6 | |:--------|:--------| 7 | """ 8 | -------------------------------------------------------------------------------- /connect/cli/plugins/shared/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/plugins/shared/__init__.py -------------------------------------------------------------------------------- /connect/cli/plugins/shared/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2019-2022 Ingram Micro. All Rights Reserved. 5 | 6 | from zipfile import BadZipFile 7 | 8 | from click import ClickException 9 | from openpyxl import load_workbook 10 | from openpyxl.utils.exceptions import InvalidFileException 11 | 12 | from connect.cli.plugins.shared.exceptions import SheetNotFoundError 13 | from connect.cli.plugins.shared.utils import ( 14 | get_col_headers_by_ws_type, 15 | get_col_limit_by_ws_type, 16 | get_ws_type_by_worksheet_name, 17 | ) 18 | 19 | 20 | class ProductSynchronizer: 21 | def __init__(self, client, progress): 22 | self._client = client 23 | self._progress = progress 24 | self._product_id = None 25 | self._wb = None 26 | self._ws = None 27 | 28 | def open(self, input_file, worksheet): 29 | self._open_workbook(input_file) 30 | if worksheet not in self._wb.sheetnames: 31 | raise SheetNotFoundError(f'File does not contain {worksheet} to synchronize, skipping') 32 | ws = self._wb['General Information'] 33 | product_id = ws['B5'].value 34 | if not self._client.products[product_id].exists(): 35 | raise ClickException(f'Product {product_id} not found, create it first.') 36 | self._ws = self._wb[worksheet] 37 | self._validate_worksheet_sheet(self._ws, worksheet) 38 | 39 | self._product_id = product_id 40 | return self._product_id 41 | 42 | def sync(self): 43 | raise NotImplementedError('Not implemented') 44 | 45 | def save(self, output_file): 46 | self._wb.save(output_file) 47 | 48 | def _open_workbook(self, input_file): 49 | try: 50 | self._wb = load_workbook( 51 | input_file, 52 | data_only=True, 53 | ) 54 | except InvalidFileException as ife: 55 | raise ClickException(str(ife)) 56 | except BadZipFile: 57 | raise ClickException(f'{input_file} is not a valid xlsx file.') 58 | 59 | @staticmethod 60 | def _validate_worksheet_sheet(ws, worksheet): 61 | ws_type = get_ws_type_by_worksheet_name(worksheet) 62 | max_letter = get_col_limit_by_ws_type(ws_type) 63 | col_headers = get_col_headers_by_ws_type(ws_type) 64 | cels = ws['A1':f'{max_letter}1'] 65 | for cel in cels[0]: 66 | if cel.value != col_headers[cel.column_letter]: 67 | raise ClickException( 68 | f'Invalid input file: column {cel.column_letter} ' 69 | f'must be {col_headers[cel.column_letter]}', 70 | ) 71 | -------------------------------------------------------------------------------- /connect/cli/plugins/shared/constants.py: -------------------------------------------------------------------------------- 1 | ATTRIBUTES_SHEET_COLUMNS = [ 2 | 'key', 3 | 'original value', 4 | 'action', 5 | 'value', 6 | 'comment', 7 | 'editor', 8 | ] 9 | 10 | ITEMS_COLS_HEADERS = { 11 | 'A': 'ID', 12 | 'B': 'MPN', 13 | 'C': 'Action', 14 | 'D': 'Name', 15 | 'E': 'Description', 16 | 'F': 'Type', 17 | 'G': 'Precision', 18 | 'H': 'Unit', 19 | 'I': 'Billing Period', 20 | 'J': 'Commitment', 21 | 'K': 'Status', 22 | 'L': 'Created', 23 | 'M': 'Modified', 24 | } 25 | 26 | PARAMS_COLS_HEADERS = { 27 | 'A': 'Verbose ID', 28 | 'B': 'ID', 29 | 'C': 'Action', 30 | 'D': 'Title', 31 | 'E': 'Description', 32 | 'F': 'Phase', 33 | 'G': 'Scope', 34 | 'H': 'Type', 35 | 'I': 'Required', 36 | 'J': 'Unique', 37 | 'K': 'Hidden', 38 | 'L': 'JSON Properties', 39 | 'M': 'Created', 40 | 'N': 'Modified', 41 | } 42 | 43 | MEDIA_COLS_HEADERS = { 44 | 'A': 'Position', 45 | 'B': 'ID', 46 | 'C': 'Action', 47 | 'D': 'Type', 48 | 'E': 'Image File', 49 | 'F': 'Video URL Location', 50 | } 51 | 52 | CAPABILITIES_COLS_HEADERS = { 53 | 'A': 'Capability', 54 | 'B': 'Action', 55 | 'C': 'Value', 56 | } 57 | 58 | STATIC_LINK_HEADERS = { 59 | 'A': 'Type', 60 | 'B': 'Title', 61 | 'C': 'Action', 62 | 'D': 'Url', 63 | } 64 | 65 | TEMPLATES_HEADERS = { 66 | 'A': 'ID', 67 | 'B': 'Title', 68 | 'C': 'Action', 69 | 'D': 'Scope', 70 | 'E': 'Type', 71 | 'F': 'Content', 72 | 'G': 'Created', 73 | 'H': 'Modified', 74 | } 75 | 76 | CONFIGURATION_HEADERS = { 77 | 'A': 'ID', 78 | 'B': 'Parameter', 79 | 'C': 'Scope', 80 | 'D': 'Action', 81 | 'E': 'Item ID', 82 | 'F': 'Item Name', 83 | 'G': 'Marketplace ID', 84 | 'H': 'Marketplace Name', 85 | 'I': 'Value', 86 | } 87 | 88 | ACTIONS_HEADERS = { 89 | 'A': 'Verbose ID', 90 | 'B': 'ID', 91 | 'C': 'Action', 92 | 'D': 'Name', 93 | 'E': 'Title', 94 | 'F': 'Description', 95 | 'G': 'Scope', 96 | 'H': 'Created', 97 | 'I': 'Modified', 98 | } 99 | 100 | TRANSLATION_HEADERS = { 101 | 'A': 'Translation ID', 102 | 'B': 'Action', 103 | 'C': 'Context ID', 104 | 'D': 'Context', 105 | 'E': 'Owner ID', 106 | 'F': 'Owner', 107 | 'G': 'Locale', 108 | 'H': 'Description', 109 | 'I': 'Autotranslation', 110 | 'J': 'Completion', 111 | 'K': 'Status', 112 | 'L': 'Is primary', 113 | 'M': 'Created', 114 | 'N': 'Updated', 115 | } 116 | 117 | MESSAGES_HEADERS = { 118 | 'A': 'ID', 119 | 'B': 'Action', 120 | 'C': 'External ID', 121 | 'D': 'Value', 122 | 'E': 'Auto', 123 | } 124 | -------------------------------------------------------------------------------- /connect/cli/plugins/shared/exceptions.py: -------------------------------------------------------------------------------- 1 | class SheetNotFoundError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /connect/cli/plugins/shared/export.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | from io import BytesIO 3 | 4 | from click import ClickException 5 | from connect.client import ClientError 6 | from openpyxl import load_workbook 7 | from openpyxl.styles import Alignment 8 | from openpyxl.utils import get_column_letter 9 | from openpyxl.worksheet.datavalidation import DataValidation 10 | from openpyxl.worksheet.filters import AutoFilter 11 | 12 | from connect.cli.core.http import format_http_status, handle_http_error 13 | from connect.cli.plugins.translation.utils import insert_column_ws 14 | 15 | 16 | EXCEL_CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 17 | 18 | 19 | def get_translation_workbook(client, translation_id): 20 | try: 21 | xls_data = ( 22 | client.ns('localization') 23 | .translations[translation_id] 24 | .action('attributes') 25 | .get( 26 | headers={'Accept': EXCEL_CONTENT_TYPE}, 27 | ) 28 | ) 29 | return load_workbook(filename=BytesIO(xls_data)) 30 | except ClientError as error: 31 | if error.status_code == 404: 32 | status = format_http_status(error.status_code) 33 | raise ClickException(f'{status}: Translation {translation_id} not found.') 34 | handle_http_error(error) 35 | 36 | 37 | def alter_attributes_sheet(ws): 38 | # Search for the 'value' column 39 | for col_idx in range(1, ws.max_column + 1): 40 | if ws.cell(1, col_idx).value.lower() == 'value': 41 | new_column = col_idx 42 | break 43 | else: 44 | return 45 | 46 | # Add new 'action' column before 'value' column 47 | fill = copy(ws.cell(1, 1).fill) 48 | font = copy(ws.cell(1, 1).font) 49 | insert_column_ws(ws, new_column, 20) 50 | header_cell = ws.cell(1, new_column) 51 | header_cell.value = 'action' 52 | header_cell.fill = fill 53 | header_cell.font = font 54 | 55 | # Setup 'action' column cells 56 | if ws.max_row > 1: 57 | action_validation = DataValidation(type='list', formula1='"-,update"', allow_blank=False) 58 | ws.add_data_validation(action_validation) 59 | for row_idx in range(2, ws.max_row + 1): 60 | cell = ws.cell(row_idx, new_column) 61 | cell.alignment = Alignment(vertical='top') 62 | cell.value = '-' 63 | action_validation.add(cell) 64 | 65 | # (re)set auto filter 66 | ws.auto_filter = AutoFilter(ref=f'A:{get_column_letter(ws.max_column)}') 67 | for col_idx in range(1, ws.max_column + 1): 68 | ws.auto_filter.add_filter_column(col_idx, [], blank=False) 69 | -------------------------------------------------------------------------------- /connect/cli/plugins/shared/translations_synchronizers.py: -------------------------------------------------------------------------------- 1 | from click import ClickException 2 | 3 | from connect.cli.plugins.shared.exceptions import SheetNotFoundError 4 | from connect.cli.plugins.shared.translation_attr_sync import TranslationAttributesSynchronizer 5 | from connect.cli.plugins.shared.translation_sync import TranslationsSynchronizer 6 | from connect.cli.plugins.shared.utils import ( 7 | get_translation_attributes_sheets, 8 | wait_for_autotranslation, 9 | ) 10 | 11 | 12 | def translations_sync(client, progress, input_file, stats, save): 13 | synchronizer = TranslationsSynchronizer(client, progress, stats) 14 | synchronizer.open(input_file, 'Translations') 15 | synchronizer.sync() 16 | if save: 17 | synchronizer.save(input_file) 18 | return synchronizer 19 | 20 | 21 | def translation_attributes_sync( 22 | worksheet, 23 | translation, 24 | client, 25 | progress, 26 | input_file, 27 | stats, 28 | save, 29 | is_clone, 30 | ): 31 | synchronizer = TranslationAttributesSynchronizer(client, progress, stats) 32 | synchronizer.open(input_file, worksheet) 33 | synchronizer.sync(translation, is_clone) 34 | if save: 35 | synchronizer.save(input_file) 36 | 37 | 38 | def sync_product_translations(client, progress, input_file, stats, save=True, is_clone=False): 39 | translations_autotranslating = [] 40 | try: 41 | synchronizer = translations_sync(client, progress, input_file, stats, save) 42 | translations_autotranslating = synchronizer.translations_autotranslating 43 | new_translations = synchronizer.new_translations 44 | except SheetNotFoundError as e: 45 | raise ClickException(str(e)) 46 | 47 | new_translations.insert(0, None) 48 | iter_translation = iter(new_translations) 49 | for sheetname in get_translation_attributes_sheets(input_file): 50 | translation = sheetname.split()[1][1:-1] 51 | if is_clone: 52 | translation = next(iter_translation) 53 | if translation in translations_autotranslating: 54 | wait_for_autotranslation(client, progress, translation) 55 | translation_attributes_sync( 56 | sheetname, 57 | translation, 58 | client, 59 | progress, 60 | input_file, 61 | stats, 62 | save, 63 | is_clone, 64 | ) 65 | -------------------------------------------------------------------------------- /connect/cli/plugins/translation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/connect/cli/plugins/translation/__init__.py -------------------------------------------------------------------------------- /connect/cli/plugins/translation/activate.py: -------------------------------------------------------------------------------- 1 | import click 2 | from connect.client import ClientError 3 | 4 | from connect.cli.core.http import format_http_status, handle_http_error 5 | 6 | 7 | def activate_translation(client, translation_id): 8 | try: 9 | payload = { 10 | 'status': 'active', 11 | } 12 | translation = ( 13 | client.ns('localization') 14 | .translations[translation_id] 15 | .action('activate') 16 | .post(payload=payload) 17 | ) 18 | except ClientError as error: 19 | if error.status_code == 404: 20 | status = format_http_status(error.status_code) 21 | raise click.ClickException(f'{status}: Translation {translation_id} not found.') 22 | handle_http_error(error) 23 | return translation 24 | -------------------------------------------------------------------------------- /connect/cli/plugins/translation/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2019-2022 Ingram Micro. All Rights Reserved. 5 | 6 | from collections import namedtuple 7 | 8 | 9 | FieldSettings = namedtuple('FieldSettings', ['row_idx', 'title']) 10 | 11 | GENERAL_SHEET_FIELDS = { 12 | 'translation_id': FieldSettings(1, 'Translation'), 13 | 'owner_id': FieldSettings(2, 'Translation Owner ID'), 14 | 'owner_name': FieldSettings(3, 'Translation Owner Name'), 15 | 'locale_id': FieldSettings(4, 'Locale'), 16 | 'context_id': FieldSettings(5, 'Localization Context'), 17 | 'context_instance_id': FieldSettings(6, 'Instance ID'), 18 | 'context_name': FieldSettings(7, 'Instance Name'), 19 | 'description': FieldSettings(8, 'Description'), 20 | 'auto_enabled': FieldSettings(9, 'Auto-translation'), 21 | } 22 | -------------------------------------------------------------------------------- /connect/cli/plugins/translation/export.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2019-2022 Ingram Micro. All Rights Reserved. 5 | 6 | from openpyxl.worksheet.datavalidation import DataValidation 7 | 8 | from connect.cli.core.utils import validate_output_options 9 | from connect.cli.plugins.shared.export import alter_attributes_sheet, get_translation_workbook 10 | 11 | 12 | def dump_translation( 13 | client, 14 | translation_id, 15 | output_file, 16 | output_path=None, 17 | ): 18 | output_file = validate_output_options(output_path, output_file, default_dir_name=translation_id) 19 | wb = get_translation_workbook(client, translation_id) 20 | _alter_general_sheet(wb['General']) 21 | alter_attributes_sheet(wb['Attributes']) 22 | wb.save(output_file) 23 | return output_file 24 | 25 | 26 | def _alter_general_sheet(ws): 27 | for row_idx in range(1, ws.max_row + 1): # pragma: no branch 28 | if ws.cell(row_idx, 1).value.lower() == 'auto-translation': 29 | disabled_enabled = DataValidation( 30 | type='list', 31 | formula1='"Disabled,Enabled"', 32 | allow_blank=False, 33 | ) 34 | ws.add_data_validation(disabled_enabled) 35 | disabled_enabled.add(ws.cell(row_idx, 2)) 36 | break 37 | -------------------------------------------------------------------------------- /connect/cli/plugins/translation/primarize.py: -------------------------------------------------------------------------------- 1 | import click 2 | from connect.client import ClientError, ConnectClient 3 | 4 | from connect.cli.core.http import RequestLogger, format_http_status, handle_http_error 5 | 6 | 7 | def primarize_translation( 8 | api_url, 9 | api_key, 10 | translation_id, 11 | ): 12 | try: 13 | client = ConnectClient( 14 | api_key=api_key, 15 | endpoint=api_url, 16 | use_specs=False, 17 | max_retries=3, 18 | logger=RequestLogger(), 19 | ) 20 | payload = { 21 | 'primary': True, 22 | } 23 | translation = ( 24 | client.ns('localization') 25 | .translations[translation_id] 26 | .action('primarize') 27 | .post(payload=payload) 28 | ) 29 | except ClientError as error: 30 | if error.status_code == 404: 31 | status = format_http_status(error.status_code) 32 | raise click.ClickException(f'{status}: Translation {translation_id} not found.') 33 | handle_http_error(error) 34 | return translation 35 | -------------------------------------------------------------------------------- /connect/cli/plugins/translation/utils.py: -------------------------------------------------------------------------------- 1 | from openpyxl.utils import get_column_letter 2 | 3 | 4 | def insert_column_ws(ws, column, width=None): 5 | """ 6 | Insert a column in a worksheet keeping the width of existing columns. 7 | """ 8 | column_widths = _get_column_widths(ws, total_columns=max(column, ws.max_column)) 9 | if width is None: 10 | width = column_widths[column - 1] 11 | column_widths.insert(column - 1, width) 12 | ws.insert_cols(idx=column, amount=1) 13 | _set_column_widths(ws, column_widths) 14 | 15 | 16 | def _get_column_widths(ws, total_columns): 17 | return [ 18 | ws.column_dimensions[get_column_letter(column)].width 19 | for column in range(1, total_columns + 1) 20 | ] 21 | 22 | 23 | def _set_column_widths(ws, column_widths): 24 | for column, column_width in enumerate(column_widths, 1): 25 | ws.column_dimensions[get_column_letter(column)].width = column_width 26 | -------------------------------------------------------------------------------- /docs/core_usage.md: -------------------------------------------------------------------------------- 1 | # CloudBlue Connect CLI core functionalities 2 | 3 | ## Accounts management 4 | 5 | `connect-cli` need to configure at least one account to authenticate against the 6 | Connect API. 7 | 8 | Accounts will be stored in the `connect-cli` configuration file that by default 9 | will be located in the .ccli folder inside the user home directory. 10 | 11 | Multiple accounts can be configured but the active one will be used. 12 | 13 | To access the group of commands related to the management of accounts you must invoke the CLI with the `account` command: 14 | 15 | ```sh 16 | $ ccli account 17 | 18 | Usage: ccli account [OPTIONS] COMMAND [ARGS]... 19 | 20 | Options: 21 | --help Show this message and exit. 22 | 23 | Commands: 24 | activate Set the current active account. 25 | add Add a new account. 26 | list List configured accounts. 27 | remove Remove a configured account. 28 | 29 | ``` 30 | 31 | 32 | ## Add a new account 33 | 34 | First of all you need to add an account the `connect-cli` with the CloudBlue Connect API *key*. 35 | 36 | ``` 37 | $ ccli account add "ApiKey XXXXX:YYYYY" 38 | ``` 39 | 40 | ## List configured accounts 41 | 42 | To get a list of all configured account run: 43 | 44 | ``` 45 | $ ccli account list 46 | ``` 47 | 48 | 49 | ## Set the current active account 50 | 51 | To set the current active account run: 52 | 53 | ``` 54 | $ ccli account activate VA-000-000 55 | ``` 56 | 57 | ## Remove an account 58 | 59 | To remove an account run: 60 | 61 | ``` 62 | $ ccli account remove VA-000-000 63 | ``` 64 | 65 | ## Getting help 66 | 67 | To get help about the `connect-cli` commands type: 68 | 69 | ``` 70 | $ ccli --help 71 | ``` -------------------------------------------------------------------------------- /docs/customers_usage.md: -------------------------------------------------------------------------------- 1 | # Customers management 2 | 3 | The customers management area offers commands to export and synchronize customers 4 | to/from an excel file. 5 | 6 | To access the group of commands related to the management of customers you must invoke the CLI with the `customer` command: 7 | 8 | ```sh 9 | $ ccli customer 10 | 11 | Usage: ccli customer [OPTIONS] COMMAND [ARGS]... 12 | 13 | Options: 14 | --help Show this message and exit. 15 | 16 | Commands: 17 | export Export customers to an excel file. 18 | sync Synchronize customers from an excel file. 19 | ``` 20 | 21 | 22 | ## Export customers 23 | 24 | To export customers data to an excel file type: 25 | 26 | ```sh 27 | $ ccli customer export 28 | ``` 29 | 30 | This command will create a folder named with the current active account ID and 31 | will generate a customers.xlsx file within that folder. 32 | 33 | ## Syncrhonize customers 34 | 35 | To synchronize customers from an excel file type: 36 | 37 | ```sh 38 | $ ccli customer sync customers.xlsx 39 | ``` 40 | 41 | This command will output the total number of processed customers 42 | and how many of them have been created, updated, deleted, skipped or generate an error. 43 | -------------------------------------------------------------------------------- /docs/linux_deps_install.md: -------------------------------------------------------------------------------- 1 | # Install Python, Cairo, Pango and GDK-PixBuf on linux 2 | 3 | ## Debian/Ubuntu 4 | 5 | ```sh 6 | $ sudo apt-get install build-essential python3-dev python3-pip python3-setuptools python3-wheel python3-cffi libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev shared-mime-info git 7 | ``` 8 | 9 | ## Fedora 10 | 11 | ```sh 12 | $ sudo yum install redhat-rpm-config python-devel python-pip python-setuptools python-wheel python-cffi libffi-devel cairo pango gdk-pixbuf2 git 13 | ``` 14 | 15 | ## Gentoo 16 | 17 | ```sh 18 | $ emerge pip setuptools wheel cairo pango gdk-pixbuf cffi dev-vcs/git 19 | ``` 20 | 21 | ## Archlinux 22 | 23 | ```sh 24 | $ sudo pacman -S python-pip python-setuptools python-wheel cairo pango gdk-pixbuf2 libffi pkg-config git 25 | ``` 26 | 27 | ## Alpine 28 | 29 | For Alpine Linux 3.6 or newer: 30 | 31 | ```sh 32 | $ apk --update --upgrade add gcc musl-dev jpeg-dev zlib-dev libffi-dev cairo-dev pango-dev gdk-pixbuf-dev git 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/locales_usage.md: -------------------------------------------------------------------------------- 1 | # Locales management 2 | 3 | The locale management area offers an useful command to list all locales available and supported by the CloudBlue Connect platform. 4 | 5 | To access the group of commands related to the management of locales you must invoke the CLI with the `locale` command: 6 | 7 | ```sh 8 | $ ccli locale 9 | 10 | Usage: ccli locale [OPTIONS] COMMAND [ARGS]... 11 | 12 | Options: 13 | -h, --help Show this message and exit. 14 | 15 | Commands: 16 | list List locales. 17 | ``` 18 | 19 | 20 | ## List locales options 21 | 22 | To get the options for the locale list command run: 23 | 24 | ```sh 25 | $ ccli locale list -h 26 | 27 | Usage: ccli locale list [OPTIONS] 28 | 29 | Options: 30 | -q, --query TEXT RQL query expression. 31 | -p, --page-size INTEGER Number of locales per page. 32 | -c, --always-continue Do not prompt to continue. 33 | -h, --help Show this message and exit. 34 | ``` 35 | You can also filter the results by adding the ``--query`` flag followed by a RQL query. 36 | For more information about RQL see the [Resource Query Language](https://connect.cloudblue.com/community/api/rql/) 37 | article in the Connect community documentation portal. 38 | 39 | ## List locales command 40 | ```sh 41 | $ ccli locale list 42 | ``` 43 | This command will output a list of all locales (id, name, autotranslation) available and the `stats` of translations within the current active account in a nice table format way: 44 | 45 | ```sh 46 | ┌──┬───────┬───────────────┬────────────┐ 47 | │ID│Name │Autotranslation│Translations│ 48 | ├──┼───────┼───────────────┼────────────┤ 49 | │ES│Spanish│ ✓ │ 2 │ 50 | └──┴───────┴───────────────┴────────────┘ 51 | ``` 52 | You could also check if the locale of interest have support for `autotranslation`: ✓ (Yes) | ✖ (No) -------------------------------------------------------------------------------- /docs/osx_deps_install.md: -------------------------------------------------------------------------------- 1 | # Install Python, Cairo, Pango and GDK-PixBuf on Mac OSX 2 | 3 | 4 | Installation on Mac OSX relies on [Homebrew](https://brew.sh/). 5 | Please install Homebrew if you haven't already. 6 | 7 | 8 | ## Install 9 | 10 | ```sh 11 | $ brew install python3 cairo pango gdk-pixbuf libffi git 12 | ``` 13 | 14 | -------------------------------------------------------------------------------- /docs/products_usage.md: -------------------------------------------------------------------------------- 1 | # Products management 2 | 3 | The products management area offers useful commands to list, export, synchronize and clone a product definition. 4 | 5 | To access the group of commands related to the management of products you must invoke the CLI with the `product` command: 6 | 7 | ```sh 8 | $ ccli product 9 | 10 | Usage: ccli product [OPTIONS] COMMAND [ARGS]... 11 | 12 | Options: 13 | --help Show this message and exit. 14 | 15 | Commands: 16 | clone Create a clone of a product. 17 | export Export a product to an excel file. 18 | list List products. 19 | sync Synchronize a product from an excel file. 20 | ``` 21 | 22 | 23 | ## List products 24 | 25 | To get a list of available products run: 26 | 27 | ``` 28 | $ ccli product list 29 | ``` 30 | 31 | This command will output a list of all products (id and name) available within the current active account. 32 | You can also filter the results by adding the ``--query`` flag followed by a RQL query. 33 | For more information about RQL see the [Resource Query Language](https://connect.cloudblue.com/community/api/rql/) 34 | article in the Connect community documentation portal. 35 | 36 | 37 | ## Export a product to Excel 38 | 39 | To export a product to Excel run: 40 | 41 | ``` 42 | $ ccli product export PRD-000-000-000 43 | ``` 44 | 45 | This command will generate an excel file named PRD-000-000-000.xlsx in the current working directory. The file will contain a sheet with a list of all the [translations](translations_usage.md) related to the product, and also a sheet of *translation attributes* per translation present in the list. 46 | 47 | 48 | ## Synchronize a product from Excel 49 | 50 | To synchronize a product from Excel run: 51 | 52 | ``` 53 | $ ccli product sync PRD-000-000-000 54 | ``` 55 | 56 | 57 | ## Clone a product 58 | 59 | To clone a product you can run this command: 60 | 61 | ``` 62 | $ ccli product clone PRD-000-000-000 63 | ``` 64 | 65 | this command also accepts as additional parameters: 66 | 67 | * -s: to specify the source account 68 | * -d: to specify the destination account 69 | * -n: to specify the name for the cloned one -------------------------------------------------------------------------------- /docs/reports_usage.md: -------------------------------------------------------------------------------- 1 | # Reports management 2 | 3 | The reports management area offers commands that allow users to generate reports 4 | and allow developers to create custom reports projects. 5 | 6 | To access the group of commands related to the management of customers you must invoke the CLI with the `report` command: 7 | 8 | ```sh 9 | $ ccli report 10 | 11 | Usage: ccli report [OPTIONS] COMMAND [ARGS]... 12 | 13 | Options: 14 | --help Show this message and exit. 15 | 16 | Commands: 17 | execute Execute a report. 18 | info Get additional information for a given report. 19 | list List available reports. 20 | ``` 21 | 22 | ## Built-in reports 23 | 24 | Multiple reports are available to the `connect-cli` tool, additionally you can create your owns and run them. 25 | 26 | If the report is located to another folder you could use the flag `-d` to reference the project location. 27 | 28 | ### List built-in reports 29 | 30 | To list available reports you can run: 31 | 32 | ``` 33 | $ ccli report list 34 | ``` 35 | 36 | ### Get information about a report 37 | 38 | To get the information of an specific report run: 39 | 40 | ``` 41 | $ ccli report info {ID} 42 | ``` 43 | 44 | ### Execute a report 45 | 46 | To execute a concrete report, you must get the id from the list before and execute: 47 | 48 | ``` 49 | $ ccli report execute {ID} 50 | ``` 51 | 52 | For example: 53 | 54 | ``` 55 | $ ccli report execute fulfillment_requests 56 | ``` 57 | 58 | If the report is located to another folder you could run: 59 | 60 | ``` 61 | $ ccli report execute fulfillment_requests -d /my/other/folder 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/stream_usage.md: -------------------------------------------------------------------------------- 1 | # Streams management 2 | 3 | The streams management area offers commands that allow users to clone and get listing of streams, 4 | export or synchronize to/from Excel file. 5 | 6 | To access the group of commands related to the management of streams you must invoke the CLI with the `commerce stream` command: 7 | 8 | ```sh 9 | $ ccli commerce stream 10 | 11 | Usage: ccli commerce [OPTIONS] COMMAND [ARGS]... 12 | 13 | Options: 14 | -h, --help Show this message and exit. 15 | 16 | Commands: 17 | clone Create a clone of a stream. 18 | export Export commerce billing or pricing streams. 19 | list List commerce billing and pricing streams. 20 | sync Synchronize a stream from an excel file. 21 | ``` 22 | 23 | ### Clone streams 24 | 25 | To clone existing stream you can run: 26 | 27 | ```sh 28 | $ccli commerce stream clone [OPTIONS] stream_id 29 | ``` 30 | 31 | ``` 32 | Options: 33 | -d, --destination_account TEXT Destination account ID 34 | -n, --new-stream-name TEXT Cloned stream name 35 | -v, --validate Executes the validate action after the clone. 36 | ``` 37 | 38 | ### List streams 39 | 40 | To list available streams you can run: 41 | 42 | ```sh 43 | $ ccli commerce stream list [OPTIONS] 44 | ``` 45 | 46 | ``` 47 | Options: 48 | -q, --query TEXT RQL query expression. 49 | ``` 50 | 51 | ### Export streams 52 | 53 | To export stream you can run: 54 | 55 | ```sh 56 | $ ccli commerce stream export [OPTIONS] stream_id 57 | ``` 58 | 59 | ``` 60 | Options: 61 | -o, --out FILE Output Excel file name. 62 | -p, --output_path DIRECTORY Directory where to store the export. 63 | ``` 64 | 65 | ### Synchronize streams 66 | 67 | To synchronize a stream from an Excel file you can run: 68 | 69 | ```sh 70 | $ ccli commerce stream sync input_file 71 | ``` 72 | 73 | Structure of sync file can be taken from `tests/fixtures/commerce/stream_sync.xlsx` file. 74 | -------------------------------------------------------------------------------- /docs/win_deps_install.md: -------------------------------------------------------------------------------- 1 | # Install Git, Cairo, Pango and GDK-PixBuf on Windows 2 | 3 | ## Install Git, Cairo, Pango and GDK-PixBuf (GTK3+) using MSYS2 4 | 5 | 6 | Install MSYS2 using the [MSYS2 installer](http://www.msys2.org/) for your system architecture. 7 | 8 | Follow the instructions on the [MSYS2 website](https://github.com/msys2/msys2/wiki/MSYS2-installation) to update your MSYS2 shell. 9 | 10 | 11 | Install GTK3+ and `git` for 32 bit architecture: 12 | 13 | ```sh 14 | $ pacman -S mingw-w64-i686-gtk3 15 | $ pacman -S git 16 | ``` 17 | 18 | Or for 64 bit architecure: 19 | 20 | ```sh 21 | $ pacman -S mingw-w64-x86_64-gtk3 22 | $ pacman -S git 23 | ``` 24 | 25 | Finally add following folders to your PATH environment variable: 26 | * `C:\msys64\mingw64\bin` and `C:\msys64\usr\bin` in case of 64 bit architecture 27 | * `C:\msys32\mingw32\bin` and `C:\msys32\usr\bin` in case of 32 bit architecture 28 | 29 | -------------------------------------------------------------------------------- /resources/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | 3 | RUN apt-get update && apt-get install -y fonts-liberation 4 | 5 | COPY ./dist /install_temp/dist 6 | 7 | WORKDIR /install_temp 8 | RUN pip install dist/*.whl 9 | 10 | RUN rm -rf /install_temp 11 | 12 | RUN groupadd connect && useradd -g connect -d /home/connect -s /bin/bash connect 13 | 14 | USER connect 15 | 16 | WORKDIR /home/connect 17 | -------------------------------------------------------------------------------- /resources/README.md: -------------------------------------------------------------------------------- 1 | # CloudBlue Connect Command Line Interface 2 | 3 | ## What is CloudBlue Connect Command Line Interface 4 | 5 | The CloudBlue Connect Command Line Interface (CLI) is an extensible unified tool to perform various automation scenarios. With just one tool, you can control multiple Connect modules from the command line and automate them through scripts. 6 | 7 | Since it is extensible, users can write their own plugins to extend its functionalities. 8 | 9 | 10 | ## How to use this image 11 | 12 | ``` 13 | $ docker run -it -v $HOME/.ccli:/home/connect/.ccli cloudblueconnect/connect-cli ccli 14 | ``` 15 | 16 | ## Version upgrade 17 | 18 | ``` 19 | $ docker pull cloudblueconnect/connect-cli 20 | ``` 21 | 22 | ## Where to get help 23 | 24 | [CloudBlue Connect community portal](https://connect.cloudblue.com/community/sdk/cli/), [Project Github repository](https://github.com/cloudblue/connect-cli) 25 | 26 | 27 | ## Where to file an issue 28 | 29 | [https://github.com/cloudblue/connect-cli/issues](https://github.com/cloudblue/connect-cli/issues) 30 | 31 | ## License 32 | 33 | `connect-cli` is released under the [Apache License Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). -------------------------------------------------------------------------------- /resources/ccli_shell.ps1: -------------------------------------------------------------------------------- 1 | Clear-Host [] 2 | Write-Output "" 3 | Write-Host 'Welcome to the ' -NoNewline; Write-Host -ForegroundColor Blue 'CloudBlue Connect CLI' 4 | Write-Output "" 5 | Write-Host 'To get help type ' -NoNewLine; Write-Host -NoNewLine -ForegroundColor Yellow 'ccli --help'; Write-Host ' and press enter' 6 | Write-Output "" 7 | $env:PYTHONUTF8 = "1" 8 | -------------------------------------------------------------------------------- /resources/connect.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/resources/connect.ico -------------------------------------------------------------------------------- /resources/get_latest_reports.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import subprocess 4 | 5 | 6 | CONNECT_REPORTS_REPO_URL = 'https://github.com/cloudblue/connect-reports.git' 7 | 8 | 9 | BASE_DIR = os.path.abspath( 10 | os.path.normpath( 11 | os.path.join( 12 | os.path.dirname(__file__), 13 | '..', 14 | ), 15 | ), 16 | ) 17 | 18 | 19 | REPO_EMBED_DIR = os.path.join( 20 | BASE_DIR, 21 | 'connect/.data/connect_reports', 22 | ) 23 | 24 | 25 | def get_latest_reports(): 26 | if os.path.exists(REPO_EMBED_DIR): 27 | shutil.rmtree(REPO_EMBED_DIR) 28 | 29 | print(f'Cloning {CONNECT_REPORTS_REPO_URL}...') 30 | 31 | subprocess.check_call( 32 | [ 33 | 'git', 34 | 'clone', 35 | CONNECT_REPORTS_REPO_URL, 36 | REPO_EMBED_DIR, 37 | ], 38 | ) 39 | 40 | result = subprocess.run( 41 | [ 42 | 'git', 43 | '-C', 44 | REPO_EMBED_DIR, 45 | 'rev-list', 46 | '--tags', 47 | '--max-count=1', 48 | ], 49 | capture_output=True, 50 | stdin=subprocess.DEVNULL, 51 | start_new_session=True, 52 | ) 53 | result.check_returncode() 54 | commit_id = result.stdout.decode().replace('\n', '') 55 | 56 | print(f'Checkout latest tag ({commit_id})...') 57 | 58 | subprocess.check_call( 59 | [ 60 | 'git', 61 | '-C', 62 | REPO_EMBED_DIR, 63 | 'checkout', 64 | commit_id, 65 | ], 66 | ) 67 | 68 | print(f'Latest reports saved in {REPO_EMBED_DIR}') 69 | 70 | 71 | if __name__ == '__main__': 72 | get_latest_reports() 73 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectName=Connect CLI Client 2 | sonar.projectKey=connect-cli 3 | sonar.organization=cloudbluesonarcube 4 | 5 | sonar.language=py 6 | 7 | sonar.sources=connect 8 | sonar.tests=tests 9 | sonar.inclusions=connect/** 10 | sonar.exclusions=tests/** 11 | 12 | sonar.python.coverage.reportPaths=./coverage.xml 13 | sonar.python.version=3 14 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/__init__.py -------------------------------------------------------------------------------- /tests/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/core/__init__.py -------------------------------------------------------------------------------- /tests/core/account/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/core/account/__init__.py -------------------------------------------------------------------------------- /tests/core/test_base.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from connect.cli.core.base import cli, print_version 4 | 5 | 6 | def test_cli_confdir_exists(mocker, fs): 7 | mocked_makedirs = mocker.patch('connect.cli.core.base.os.makedirs') 8 | fs.makedir('.ccli') 9 | runner = CliRunner() 10 | runner.invoke(cli, ['-c', f'{fs.root_path}/.ccli', 'account', 'list']) 11 | mocked_makedirs.assert_not_called() 12 | 13 | 14 | def test_cli_confdir_not_exists(mocker, fs): 15 | mocked_makedirs = mocker.patch('connect.cli.core.base.os.makedirs') 16 | runner = CliRunner() 17 | runner.invoke(cli, ['-c', f'{fs.root_path}/.ccli', 'account', 'list']) 18 | mocked_makedirs.assert_called_once_with(f'{fs.root_path}/.ccli') 19 | 20 | 21 | def test_cli_print_version(mocker, capsys): 22 | mocker.patch('connect.cli.core.base.get_version', return_value='1.0.0') 23 | mocked = mocker.patch('connect.cli.core.base.check_for_updates') 24 | 25 | print_version(mocker.MagicMock(resilient_parsing=False), 'version', True) 26 | captured = capsys.readouterr() 27 | assert 'CloudBlue Connect CLI, version 1.0.0' in captured.out 28 | mocked.assert_called_once() 29 | -------------------------------------------------------------------------------- /tests/core/test_http.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | import pytest 4 | from click import ClickException 5 | from connect.client import ClientError 6 | 7 | from connect.cli import get_version 8 | from connect.cli.core.http import ( 9 | RequestLogger, 10 | format_http_status, 11 | get_user_agent, 12 | handle_http_error, 13 | ) 14 | 15 | 16 | def test_format_http_status(): 17 | assert format_http_status(401) == '401 - Unauthorized' 18 | assert format_http_status(404) == '404 - Not Found' 19 | assert format_http_status(500) == '500 - Internal Server Error' 20 | 21 | with pytest.raises(Exception): 22 | format_http_status(1) 23 | 24 | 25 | def test_handle_http_error_400(mocker): 26 | res = ClientError() 27 | res.status_code = 400 28 | res.errors = ['error1', 'error2'] 29 | res.error_code = 'SYS-000' 30 | with pytest.raises(ClickException) as e: 31 | handle_http_error(res) 32 | 33 | assert str(e.value) == '400 - Bad Request: SYS-000 - error1,error2' 34 | 35 | 36 | @pytest.mark.parametrize( 37 | ('code', 'description', 'message'), 38 | ( 39 | (401, 'Unauthorized', 'please check your credentials.'), 40 | (403, 'Forbidden', 'please check your credentials.'), 41 | (500, 'Internal Server Error', 'unexpected error.'), 42 | (502, 'Bad Gateway', 'unexpected error.'), 43 | ), 44 | ) 45 | def test_handle_http_error_others(mocker, code, description, message): 46 | res = mocker.MagicMock() 47 | res.status_code = code 48 | 49 | with pytest.raises(ClickException) as e: 50 | handle_http_error(res) 51 | 52 | assert str(e.value) == f'{code} - {description}: {message}' 53 | 54 | 55 | def test_get_user_agent(): 56 | user_agent_header = get_user_agent() 57 | ua = user_agent_header['User-Agent'] 58 | 59 | cli, python, system = ua.split() 60 | assert cli == f'connect-cli/{get_version()}' 61 | assert python == f'{platform.python_implementation()}/{platform.python_version()}' 62 | assert system == f'{platform.system()}/{platform.release()}' 63 | 64 | 65 | def test_request_logger_creation(mocker): 66 | mocker.patch('connect.cli.core.http.console') 67 | req_logger = RequestLogger() 68 | assert req_logger._file is not None 69 | 70 | 71 | def test_request_logger_creation_empty(mocker): 72 | mocked_console = mocker.MagicMock() 73 | mocked_console.verbose = True 74 | mocked_console.silent = False 75 | mocker.patch('connect.cli.core.http.console', mocked_console) 76 | req_logger = RequestLogger() 77 | assert req_logger._file is None 78 | -------------------------------------------------------------------------------- /tests/core/test_plugins.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import EntryPoint 2 | 3 | import click 4 | 5 | from connect.cli.core.plugins import load_plugins 6 | 7 | 8 | def test_load_plugins(mocker): 9 | cli = click.Group() 10 | 11 | grp_internal = click.MultiCommand('internal') 12 | grp_external = click.MultiCommand('external') 13 | 14 | mocker.patch.object( 15 | EntryPoint, 16 | 'load', 17 | side_effect=[ 18 | lambda: grp_internal, 19 | lambda: grp_external, 20 | ], 21 | ) 22 | mocker.patch( 23 | 'connect.cli.core.plugins.iter_entry_points', 24 | return_value=iter( 25 | [ 26 | EntryPoint('internal', 'connect.cli.plugins.internal', None), 27 | EntryPoint('external', 'external.cli.plugin', None), 28 | ], 29 | ), 30 | ) 31 | 32 | load_plugins(cli) 33 | 34 | assert 'internal' in cli.commands 35 | assert 'plugin' in cli.commands 36 | assert 'external' in cli.commands['plugin'].commands 37 | 38 | 39 | def test_load_no_external(mocker): 40 | cli = click.Group() 41 | grp_internal = click.MultiCommand('internal') 42 | 43 | mocker.patch.object( 44 | EntryPoint, 45 | 'load', 46 | side_effect=[ 47 | lambda: grp_internal, 48 | ], 49 | ) 50 | mocker.patch( 51 | 'connect.cli.core.plugins.iter_entry_points', 52 | return_value=iter( 53 | [ 54 | EntryPoint('internal', 'connect.cli.plugins.internal', None), 55 | ], 56 | ), 57 | ) 58 | 59 | load_plugins(cli) 60 | 61 | assert 'internal' in cli.commands 62 | assert 'plugin' not in cli.commands 63 | -------------------------------------------------------------------------------- /tests/fixtures/actions_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "action": "sso_action", 4 | "name": "Action 1", 5 | "type": "button", 6 | "scope": "asset", 7 | "description": "Sign in into the service by clicking at this button", 8 | "id": "ACT-276-377-545-001", 9 | "events": { 10 | "created": { 11 | "at": "2020-10-13T00:34:04+00:00" 12 | }, 13 | "updated": { 14 | "at": "2020-10-13T04:33:03+00:00" 15 | } 16 | }, 17 | "title": "Action 1" 18 | }, 19 | { 20 | "action": "action_2", 21 | "name": "Action 2", 22 | "type": "button", 23 | "scope": "asset", 24 | "description": "Action 2 Description", 25 | "id": "ACT-276-377-545-002", 26 | "events": { 27 | "created": { 28 | "at": "2020-10-13T04:37:29+00:00" 29 | }, 30 | "updated": { 31 | "at": "2020-10-13T04:37:29+00:00" 32 | } 33 | }, 34 | "title": "Action 2" 35 | }, 36 | { 37 | "action": "action_3", 38 | "name": "Action 3 - No created at", 39 | "type": "button", 40 | "scope": "asset", 41 | "description": "Action 3 Description", 42 | "id": "ACT-276-377-545-002", 43 | "events": { 44 | "updated": { 45 | "at": "2020-10-13T04:37:29+00:00" 46 | } 47 | }, 48 | "title": "Action 3" 49 | }, 50 | { 51 | "action": "action_4", 52 | "name": "Action 4 - No events", 53 | "type": "button", 54 | "scope": "asset", 55 | "description": "Action 4 Description", 56 | "id": "ACT-276-377-545-002", 57 | "title": "Action 4" 58 | } 59 | ] -------------------------------------------------------------------------------- /tests/fixtures/actions_sync.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/actions_sync.xlsx -------------------------------------------------------------------------------- /tests/fixtures/capabilities_sync.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/capabilities_sync.xlsx -------------------------------------------------------------------------------- /tests/fixtures/commerce/attachments_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "MFL-2481-0572-9001", 4 | "file": "/public/v1/media/folders/streams_attachments/STR-7755-7115-2464/files/MFL-2481-0572-9001/for_testing_auxiliar_1.xlsx", 5 | "size": 9599, 6 | "folder": { 7 | "name": "STR-7755-7115-2464", 8 | "type": "streams_attachments" 9 | }, 10 | "owner": { 11 | "id": "VA-050-000", 12 | "name": "Vendor account 00" 13 | }, 14 | "name": "for_testing_auxiliar_1.xlsx", 15 | "mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 16 | "events": { 17 | "created": { 18 | "at": "2023-07-31T15:24:18+00:00", 19 | "by": { 20 | "id": "UR-050-000-003", 21 | "name": "Gerard" 22 | } 23 | }, 24 | "confirmed": { 25 | "at": "2023-07-31T15:24:18+00:00", 26 | "by": { 27 | "id": "UR-050-000-003", 28 | "name": "Gerard" 29 | } 30 | } 31 | }, 32 | "access": { 33 | "VA-050-000": { 34 | "view": true, 35 | "delete": true 36 | } 37 | } 38 | }, 39 | { 40 | "id": "MFL-8784-6884-8192", 41 | "file": "/public/v1/media/folders/streams_attachments/STR-7755-7115-2464/files/MFL-8784-6884-8192/demo_styles.xlsx", 42 | "size": 10005, 43 | "folder": { 44 | "name": "STR-7755-7115-2464", 45 | "type": "streams_attachments" 46 | }, 47 | "owner": { 48 | "id": "VA-050-000", 49 | "name": "Vendor account 00" 50 | }, 51 | "name": "demo_styles.xlsx", 52 | "mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 53 | "events": { 54 | "created": { 55 | "at": "2023-07-31T15:25:58+00:00", 56 | "by": { 57 | "id": "UR-050-000-003", 58 | "name": "Gerard" 59 | } 60 | }, 61 | "confirmed": { 62 | "at": "2023-07-31T15:25:58+00:00", 63 | "by": { 64 | "id": "UR-050-000-003", 65 | "name": "Gerard" 66 | } 67 | } 68 | }, 69 | "access": { 70 | "VA-050-000": { 71 | "view": true, 72 | "delete": true 73 | } 74 | } 75 | } 76 | ] 77 | -------------------------------------------------------------------------------- /tests/fixtures/commerce/columns_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "SCOL-7755-7115-2464-001", 4 | "name": "Customer Name", 5 | "type": "string", 6 | "position": 10000, 7 | "nullable": true, 8 | "required": false, 9 | "output": true 10 | }, 11 | { 12 | "id": "SCOL-7755-7115-2464-002", 13 | "name": "Subscription ID", 14 | "type": "string", 15 | "position": 20000, 16 | "nullable": true, 17 | "required": false, 18 | "output": true, 19 | "description": "Subscription ID column" 20 | }, 21 | { 22 | "id": "SCOL-7755-7115-2464-003", 23 | "name": "Unexpected Column", 24 | "type": "string", 25 | "position": 30000, 26 | "nullable": true, 27 | "required": false, 28 | "output": true 29 | }, 30 | { 31 | "id": "SCOL-7755-7115-2464-004", 32 | "name": "copy column", 33 | "type": "string", 34 | "position": 40000, 35 | "nullable": true, 36 | "required": false, 37 | "output": true 38 | }, 39 | { 40 | "id": "SCOL-7755-7115-2464-005", 41 | "name": "split column", 42 | "type": "string", 43 | "position": 50000, 44 | "nullable": true, 45 | "required": false, 46 | "output": true 47 | }, 48 | { 49 | "id": "SCOL-7755-7115-2464-006", 50 | "name": "currency converter", 51 | "type": "decimal", 52 | "constraints": { 53 | "precision": 2 54 | }, 55 | "position": 60000, 56 | "nullable": true, 57 | "required": false, 58 | "output": true 59 | }, 60 | { 61 | "id": "SCOL-7755-7115-2464-007", 62 | "name": "decimal string", 63 | "type": "string", 64 | "position": 70000, 65 | "nullable": true, 66 | "required": false, 67 | "output": true 68 | }, 69 | { 70 | "id": "SCOL-7755-7115-2464-008", 71 | "name": "integer", 72 | "type": "integer", 73 | "position": 80000, 74 | "nullable": true, 75 | "required": false, 76 | "output": true 77 | }, 78 | { 79 | "id": "SCOL-7755-7115-2464-009", 80 | "name": "string", 81 | "type": "string", 82 | "position": 90000, 83 | "nullable": true, 84 | "required": false, 85 | "output": true 86 | }, 87 | { 88 | "id": "SCOL-7755-7115-2464-010", 89 | "name": "datetime", 90 | "type": "datetime", 91 | "position": 100000, 92 | "nullable": true, 93 | "required": false, 94 | "output": true 95 | } 96 | ] -------------------------------------------------------------------------------- /tests/fixtures/commerce/stream_retrieve_response.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": "STR-7755-7115-2464", 3 | "name": "Simple pricing stream", 4 | "status": "active", 5 | "description": "Some description", 6 | "sources": [], 7 | "visibility": "published", 8 | "samples": { 9 | "input": { 10 | "id": "MFL-9059-7665-2037", 11 | "name": "/public/v1/media/folders/streams_samples/STR-7755-7115-2464/files/MFL-9059-7665-2037/for_test.xlsx?session=f4dccfe4-7c86-4e16-b990-b68c6f9d40ce" 12 | }, 13 | "output": { 14 | "id": "MFL-7908-1914-3192", 15 | "name": "/public/v1/media/folders/streams_samples/STR-7755-7115-2464/files/MFL-7908-1914-3192/STR-7755-7115-2464-out.xlsx?session=f4dccfe4-7c86-4e16-b990-b68c6f9d40ce" 16 | } 17 | }, 18 | "owner": { 19 | "id": "VA-000", 20 | "name": "Vendor account 00" 21 | }, 22 | "type": "pricing", 23 | "events": { 24 | "created": { 25 | "at": "2023-07-26T13:47:56+00:00", 26 | "by": { 27 | "id": "UR-050-000-003", 28 | "name": "Gerardu" 29 | } 30 | }, 31 | "updated": { 32 | "at": "2023-07-31T09:02:45+00:00", 33 | "by": { 34 | "id": "UR-050-000-003", 35 | "name": "Gerardu" 36 | } 37 | }, 38 | "activated": { 39 | "at": "2023-07-31T09:02:45+00:00", 40 | "by": { 41 | "id": "UR-050-000-003", 42 | "name": "Gerardu" 43 | } 44 | } 45 | }, 46 | "validation": { 47 | "id": "BAT-6217-6818-2034", 48 | "name": "Stream STR-7755-7115-2464 configuration test", 49 | "status": "published" 50 | }, 51 | "context": { 52 | "account": { 53 | "id": "PA-050-101", 54 | "name": "Provider account 01" 55 | }, 56 | "product": { 57 | "id": "PRD-054-258-626", 58 | "name": "Product P03", 59 | "icon": "/media/VA-050-000/PRD-054-258-626/media/PRD-054-258-626-logo.png" 60 | }, 61 | "marketplace": { 62 | "id": "MP-05011", 63 | "name": "Marketplace M11 for galonso", 64 | "icon": "/media/PA-050-101/marketplaces/MP-05011/icon.png", 65 | "currency": "USD" 66 | }, 67 | "pricelist": { 68 | "id": "PL-123" 69 | }, 70 | "listing": { 71 | "id": "LST-760-660-960" 72 | } 73 | } 74 | }] 75 | -------------------------------------------------------------------------------- /tests/fixtures/commerce/stream_sync.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/commerce/stream_sync.xlsx -------------------------------------------------------------------------------- /tests/fixtures/comparation_product.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/comparation_product.xlsx -------------------------------------------------------------------------------- /tests/fixtures/configuration_parameters_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "PRM-276-377-545-0022", 4 | "name": "asdf", 5 | "title": "asdf", 6 | "description": "asdf", 7 | "type": "text", 8 | "scope": "item", 9 | "phase": "configuration", 10 | "hint": "I am the hint text", 11 | "placeholder": "I am the placeholder text", 12 | "constraints": { 13 | "required": false, 14 | "hidden": false, 15 | "unique": false, 16 | "shared": null 17 | }, 18 | "position": 220000, 19 | "events": { 20 | "created": { 21 | "at": "2020-11-27T08:37:16+00:00", 22 | "by": { 23 | "id": "UR-841-574-187", 24 | "name": "Marc Serrat" 25 | } 26 | }, 27 | "updated": { 28 | "at": "2020-11-27T08:37:16+00:00" 29 | } 30 | } 31 | } 32 | ] -------------------------------------------------------------------------------- /tests/fixtures/configuration_sync.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/configuration_sync.xlsx -------------------------------------------------------------------------------- /tests/fixtures/customer/customer.json: -------------------------------------------------------------------------------- 1 | { 2 | "id":"TA-4889-0575-1415", 3 | "version":2, 4 | "name":"Kirlin LLC", 5 | "type":"customer", 6 | "external_id":"16547", 7 | "external_uid":"5663c260-328b-4021-b7e5-d290afb19e9a", 8 | "parent":{ 9 | "id":"TA-7374-0753-1907", 10 | "name":"Walsh and Sons", 11 | "external_id":"39878" 12 | }, 13 | "scopes":[ 14 | 15 | ], 16 | "hub":{ 17 | "id":"HB-0000-0000", 18 | "name":"None" 19 | }, 20 | "events":{ 21 | "created":{ 22 | "at":"2021-03-01T16:56:45+00:00", 23 | "by":{ 24 | "id":"UR-024-000-000", 25 | "name":"Marc vendor" 26 | } 27 | }, 28 | "updated":{ 29 | "at":"2021-03-01T16:56:45+00:00" 30 | } 31 | }, 32 | "contact_info":{ 33 | "address_line1":"McLaughlin Brooks", 34 | "address_line2":"Christ Haven", 35 | "city":"al-Balqa", 36 | "state":"al-Balqa'", 37 | "postal_code":"-", 38 | "country":"JO", 39 | "contact":{ 40 | "first_name":"Cory", 41 | "last_name":"Marvin", 42 | "email":"mserrat+Cory_Marvin@connect.cloudblue.com", 43 | "phone_number":{ 44 | "country_code":"+962", 45 | "area_code":"", 46 | "phone_number":"62001234", 47 | "extension":"-" 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /tests/fixtures/customer/customers.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/customer/customers.xlsx -------------------------------------------------------------------------------- /tests/fixtures/customer/reseller.json: -------------------------------------------------------------------------------- 1 | { 2 | "id":"TA-7374-0753-1907", 3 | "version":1, 4 | "name":"Walsh and Sons", 5 | "type":"reseller", 6 | "external_id":"39878", 7 | "external_uid":"f7320e27-23ef-4c04-9296-fd01250120d5", 8 | "scopes":[ 9 | 10 | ], 11 | "hub":{ 12 | "id":"HB-0000-0000", 13 | "name":"None" 14 | }, 15 | "events":{ 16 | "created":{ 17 | "at":"2021-03-01T16:56:34+00:00", 18 | "by":{ 19 | "id":"UR-024-000-000", 20 | "name":"Marc vendor" 21 | } 22 | }, 23 | "updated":{ 24 | "at":"2021-03-01T16:56:34+00:00" 25 | } 26 | }, 27 | "contact_info":{ 28 | "address_line1":"Esperanza Ferry", 29 | "address_line2":"Harris Pass", 30 | "city":"al-Manamah", 31 | "state":"al-Manamah", 32 | "postal_code":"111111", 33 | "country":"BH", 34 | "contact":{ 35 | "first_name":"Hosea", 36 | "last_name":"Pfannerstill", 37 | "email":"mserrat+Hosea_Pfannerstill@connect.cloudblue.com", 38 | "phone_number":{ 39 | "country_code":"+973", 40 | "area_code":"170", 41 | "phone_number":"01234", 42 | "extension":"" 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /tests/fixtures/extensions/basic_ext/connect_ext/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/extensions/basic_ext/connect_ext/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/extensions/basic_ext/connect_ext/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Basic Extension", 3 | "description": "Project description", 4 | "version": "1.0", 5 | "readme_url": "https://example.com/README.md", 6 | "changelog_url": "https://example.com/CHANGELOG.md", 7 | "schedulables": [{ 8 | "name": "Schedulable method mock", 9 | "description": "It can be used to test DevOps scheduler.", 10 | "method": "execute_scheduled_processing" 11 | }], 12 | "capabilities": { 13 | "asset_purchase_request_processing": [ 14 | "approved" 15 | ], 16 | "tier_config_setup_request_processing": [ 17 | "tiers_setup", 18 | "pending", 19 | "inquiring", 20 | "approved", 21 | "failed" 22 | ], 23 | "product_action_execution": [], 24 | "product_custom_event_processing": [] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/fixtures/extensions/basic_ext/connect_ext/extension.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2021, Enterprise 4 | # All rights reserved. 5 | # 6 | from connect.eaas.extension import Extension 7 | 8 | 9 | class BasicExtension(Extension): 10 | def process_asset_purchase_request(self, request): 11 | pass 12 | 13 | def process_tier_config_setup_request(self, request): 14 | pass 15 | 16 | def execute_product_action(self, request): 17 | pass 18 | 19 | def process_product_custom_event(self, request): 20 | pass 21 | 22 | def execute_scheduled_processing(self, schedule): 23 | pass 24 | -------------------------------------------------------------------------------- /tests/fixtures/extensions/basic_ext/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | dev: 3 | container_name: ext_dev 4 | image: runner:1.0 5 | test: 6 | container_name: ext_test 7 | image: runner:1.0 -------------------------------------------------------------------------------- /tests/fixtures/extensions/basic_ext/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "basic-ext" 3 | version = "1.0" 4 | description = "Project description" 5 | authors = ["Enterprise"] 6 | license = "Apache Software License 2.0" 7 | packages = [ 8 | { include = "connect_ext" } 9 | ] 10 | readme = "./README.md" 11 | 12 | [tool.poetry.plugins."connect.eaas.ext"] 13 | "extension" = "connect_ext.extension:BasicExtension" 14 | 15 | [tool.poetry.dependencies] 16 | python = "^3.8" 17 | connect-openapi-client = "^22.0.8" 18 | connect-extension-runner = "^2.5" 19 | 20 | [tool.poetry.dev-dependencies] 21 | pytest = "^6.1.2" 22 | pytest-cov = "^2.10.1" 23 | pytest-mock = "^3.3.1" 24 | pytest-asyncio = "^0.15.1" 25 | pytest-httpx = "^0.12.0" 26 | coverage = {extras = ["toml"], version = "^5.3"} 27 | responses = "^0.12.0" 28 | flake8 = "~3.8" 29 | flake8-bugbear = "~20" 30 | flake8-cognitive-complexity = "^0.1" 31 | flake8-commas = "~2.0" 32 | flake8-future-import = "~0.4" 33 | flake8-import-order = "~0.18" 34 | flake8-broken-line = "~0.3" 35 | flake8-comprehensions = "^3.3.1" 36 | flake8-debugger = "^4.0.0" 37 | flake8-eradicate = "^1.0.0" 38 | flake8-string-format = "^0.3.0" 39 | 40 | [build-system] 41 | requires = ["poetry-core>=1.0.0"] 42 | build-backend = "poetry.core.masonry.api" 43 | 44 | [tool.pytest.ini_options] 45 | testpaths = "tests" 46 | addopts = "--cov=connect_ext --cov-report=term-missing:skip-covered --cov-report=html --cov-report=xml" 47 | 48 | [tool.coverage.run] 49 | branch = true 50 | 51 | [tool.coverage.report] 52 | omit = [ 53 | ] 54 | 55 | exclude_lines = [ 56 | "pragma: no cover", 57 | "def __str__", 58 | "def __repr__", 59 | "raise NotImplementedError", 60 | "if __name__ == .__main__.:", 61 | ] 62 | -------------------------------------------------------------------------------- /tests/fixtures/fulfillment_parameters_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "PRM-276-377-545-0015", 4 | "name": "t0_f_password", 5 | "title": "t0_f_password T", 6 | "description": "t0_f_password D", 7 | "type": "password", 8 | "scope": "asset", 9 | "phase": "fulfillment", 10 | "constraints": { 11 | "required": true, 12 | "hidden": false, 13 | "unique": false, 14 | "reconciliation": false, 15 | "max_length": 100, 16 | "min_length": 1 17 | }, 18 | "position": 150000, 19 | "events": { 20 | "created": { 21 | "at": "2020-10-13T01:29:43+00:00", 22 | "by": { 23 | "id": "UR-539-008-555", 24 | "name": "Robert Balboa Oct 12" 25 | } 26 | }, 27 | "updated": { 28 | "at": "2020-10-13T01:29:43+00:00" 29 | } 30 | } 31 | }, 32 | { 33 | "id": "PRM-276-377-545-0016", 34 | "name": "t0_f_text", 35 | "title": "t0_f_text T", 36 | "description": "t0_f_text D", 37 | "type": "text", 38 | "scope": "asset", 39 | "phase": "fulfillment", 40 | "hint": "I am the hint text", 41 | "placeholder": "I am the placeholder text", 42 | "constraints": { 43 | "required": true, 44 | "hidden": false, 45 | "unique": false, 46 | "reconciliation": false 47 | }, 48 | "position": 160000, 49 | "events": { 50 | "created": { 51 | "at": "2020-10-13T01:30:35+00:00", 52 | "by": { 53 | "id": "UR-539-008-555", 54 | "name": "Robert Balboa Oct 12" 55 | } 56 | }, 57 | "updated": { 58 | "at": "2020-10-13T01:30:35+00:00" 59 | } 60 | } 61 | } 62 | ] -------------------------------------------------------------------------------- /tests/fixtures/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/image.png -------------------------------------------------------------------------------- /tests/fixtures/items_sync.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/items_sync.xlsx -------------------------------------------------------------------------------- /tests/fixtures/media_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "position": 1, 4 | "id": "PRDM-276-377-545-67072", 5 | "type": "image", 6 | "thumbnail": "/media/VA-392-495/PRD-276-377-545/media/media.png", 7 | "url": "/media/VA-392-495/PRD-276-377-545/media/media.png" 8 | }, 9 | { 10 | "position": 2, 11 | "id": "PRDM-276-377-545-90025", 12 | "type": "image", 13 | "thumbnail": "/media/VA-392-495/PRD-276-377-545/media/media_jxr1ifH.png", 14 | "url": "/media/VA-392-495/PRD-276-377-545/media/media_jxr1ifH.png" 15 | }, 16 | { 17 | "position": 3, 18 | "id": "PRDM-276-377-545-82237", 19 | "type": "image", 20 | "thumbnail": "/media/VA-392-495/PRD-276-377-545/media/media_cqhgp78.png", 21 | "url": "/media/VA-392-495/PRD-276-377-545/media/media_cqhgp78.png" 22 | } 23 | ] -------------------------------------------------------------------------------- /tests/fixtures/media_sync.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/media_sync.xlsx -------------------------------------------------------------------------------- /tests/fixtures/messages_sync.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/messages_sync.xlsx -------------------------------------------------------------------------------- /tests/fixtures/new_translation_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "TRN-1079-0833-9999", 3 | "context": { 4 | "id": "LCX-1278-0537-9908", 5 | "instance_id": "PRD-276-377-545", 6 | "name": "My Product", 7 | "icon": "/media/VA-392-495/PRD-276-377-545/media/PRD-276-377-545-logo_aJD74iQ.png", 8 | "type": "product" 9 | }, 10 | "owner": { 11 | "id": "VA-392-495", 12 | "name": "Adrian Inc Oct 12" 13 | }, 14 | "locale": { 15 | "id": "JA", 16 | "name": "Japanese" 17 | }, 18 | "description": "This is the japanese translation", 19 | "stats": { 20 | "total": 30, 21 | "translated": 30 22 | }, 23 | "auto": { 24 | "enabled": true, 25 | "status": "off" 26 | }, 27 | "status": "inactive", 28 | "events": { 29 | "updated": { 30 | "at": "2020-10-29T12:00:00+00:00", 31 | "by": { 32 | "id": "UR-539-008-555", 33 | "name": "Robert Balboa Oct 12" 34 | } 35 | }, 36 | "created": { 37 | "at": "2020-10-29T12:00:00+00:00", 38 | "by": { 39 | "id": "UR-539-008-555", 40 | "name": "Robert Balboa Oct 12" 41 | } 42 | } 43 | }, 44 | "primary": false 45 | } -------------------------------------------------------------------------------- /tests/fixtures/params_sync.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/params_sync.xlsx -------------------------------------------------------------------------------- /tests/fixtures/product_messages_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "PRDMSG-276-377-545-0001", 4 | "product": { 5 | "id": "PRD-276-377-545", 6 | "icon": "/media/VA-050-000/PRD-276-377-545/media/PRD-276-377-545-logo.png", 7 | "name": "ProductCLI", 8 | "status": "published" 9 | }, 10 | "external_id": "error1", 11 | "value": "The value does not exists", 12 | "auto": true 13 | }, 14 | { 15 | "id": "PRDMSG-276-377-545-0002", 16 | "product": { 17 | "id": "PRD-276-377-545", 18 | "icon": "/media/VA-050-000/PRD-276-377-545/media/PRD-276-377-545-logo.png", 19 | "name": "ProductCLI", 20 | "status": "published" 21 | }, 22 | "external_id": "error2", 23 | "value": "The operation is not possible", 24 | "auto": false 25 | } 26 | ] -------------------------------------------------------------------------------- /tests/fixtures/product_translations_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "TRN-1079-0833-9890", 4 | "context": { 5 | "id": "LCX-1278-0537-9908", 6 | "instance_id": "PRD-276-377-545", 7 | "name": "My Product", 8 | "icon": "/media/VA-392-495/PRD-276-377-545/media/PRD-276-377-545-logo_aJD74iQ.png", 9 | "type": "product" 10 | }, 11 | "owner": { 12 | "id": "VA-392-495", 13 | "name": "Adrian Inc Oct 12" 14 | }, 15 | "locale": { 16 | "id": "FA", 17 | "name": "Persian" 18 | }, 19 | "stats": { 20 | "total": 30, 21 | "translated": 30 22 | }, 23 | "auto": { 24 | "enabled": false, 25 | "status": "off" 26 | }, 27 | "status": "inactive", 28 | "events": { 29 | "updated": { 30 | "at": "2020-10-15T01:19:20+00:00", 31 | "by": { 32 | "id": "UR-539-008-555", 33 | "name": "Robert Balboa Oct 12" 34 | } 35 | }, 36 | "created": { 37 | "at": "2020-10-14T12:11:24+00:00", 38 | "by": { 39 | "id": "UR-539-008-555", 40 | "name": "Robert Balboa Oct 12" 41 | } 42 | } 43 | }, 44 | "primary": true 45 | }, 46 | { 47 | "id": "TRN-1079-0833-9891", 48 | "context": { 49 | "id": "LCX-1278-0537-9908", 50 | "instance_id": "PRD-276-377-545", 51 | "name": "My Product", 52 | "icon": "/media/VA-392-495/PRD-276-377-545/media/PRD-276-377-545-logo_aJD74iQ.png", 53 | "type": "product" 54 | }, 55 | "owner": { 56 | "id": "VA-392-495", 57 | "name": "Adrian Inc Oct 12" 58 | }, 59 | "locale": { 60 | "id": "ES-AR", 61 | "name": "Argentinian Spanish" 62 | }, 63 | "stats": { 64 | "total": 30, 65 | "translated": 30 66 | }, 67 | "auto": { 68 | "enabled": false, 69 | "status": "off" 70 | }, 71 | "status": "inactive", 72 | "events": { 73 | "updated": { 74 | "at": "2020-10-15T01:19:20+00:00", 75 | "by": { 76 | "id": "UR-539-008-555", 77 | "name": "Robert Balboa Oct 12" 78 | } 79 | }, 80 | "created": { 81 | "at": "2020-10-14T12:11:24+00:00", 82 | "by": { 83 | "id": "UR-539-008-555", 84 | "name": "Robert Balboa Oct 12" 85 | } 86 | } 87 | }, 88 | "primary": false 89 | } 90 | ] -------------------------------------------------------------------------------- /tests/fixtures/report.json.j2: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ initial_report_name }}", 3 | "readme_file": "/{{ initial_report_slug }}/README.md", 4 | "entrypoint": "{{ project_dir }}.{{ package_dir }}.{{ initial_report_slug }}.entrypoint.generate", 5 | "audience": [ 6 | "provider", 7 | "vendor" 8 | ], 9 | "report_spec": "2", 10 | "parameters": [], 11 | "renderers": [ 12 | { 13 | "id": "xlsx", 14 | "type": "xlsx", 15 | "default": {% if initial_report_renderer=='xlsx' %}true{% else %}false{% endif %}, 16 | "description": "Export data in Microsoft Excel 2020 format.", 17 | "template": "{{ project_dir }}/{{ package_dir }}/{{ initial_report_slug }}/templates/xlsx/template.xlsx", 18 | "args": { 19 | "start_row": 2, 20 | "start_col": 1 21 | } 22 | }, 23 | { 24 | "id": "json", 25 | "type": "json", 26 | "default": {% if initial_report_renderer=='json' %}true{% else %}false{% endif %}, 27 | "description": "Export data as JSON" 28 | }, 29 | { 30 | "id": "csv", 31 | "type": "csv", 32 | "default": {% if initial_report_renderer=='csv' %}true{% else %}false{% endif %}, 33 | "description": "Export data as CSV" 34 | }, 35 | { 36 | "id": "xml", 37 | "type": "jinja2", 38 | "default": {% if initial_report_renderer=='xml' %}true{% else %}false{% endif %}, 39 | "description": "Export data as XML", 40 | "template": "{{ project_dir }}/{{ package_dir }}/{{ initial_report_slug }}/templates/xml/template.xml.j2" 41 | }, 42 | { 43 | "id": "pdf-portrait", 44 | "type": "pdf", 45 | "default": {% if initial_report_renderer=='pdf' %}true{% else %}false{% endif %}, 46 | "description": "Export data as PDF (portrait)", 47 | "template": "{{ project_dir }}/{{ package_dir }}/{{ initial_report_slug }}/templates/pdf/template.html.j2", 48 | "args": { 49 | "css_file": "{{ project_dir }}/{{ package_dir }}/{{ initial_report_slug }}/templates/pdf/template.css" 50 | } 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /tests/fixtures/reports/basic_report/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Connect Reports fixture! 2 | -------------------------------------------------------------------------------- /tests/fixtures/reports/basic_report/endpoint/Readme.md: -------------------------------------------------------------------------------- 1 | # Basic report info -------------------------------------------------------------------------------- /tests/fixtures/reports/basic_report/endpoint/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/reports/basic_report/endpoint/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/reports/basic_report/endpoint/entrypoint.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020, CloudBlue 4 | # All rights reserved. 5 | # 6 | 7 | 8 | def generate(client, parameters, progress_callback): 9 | output = [ 10 | [ 11 | 1, 12 | ], 13 | [ 14 | 2, 15 | ], 16 | ] 17 | progress_callback(1, 1) 18 | 19 | return output 20 | -------------------------------------------------------------------------------- /tests/fixtures/reports/basic_report/endpoint/template.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/reports/basic_report/endpoint/template.xlsx -------------------------------------------------------------------------------- /tests/fixtures/reports/basic_report/reports.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Connect Reports Fixture", 3 | "readme_file":"README.md", 4 | "version":"1.0.0", 5 | "language":"python", 6 | "reports":[ 7 | { 8 | "name":"test report", 9 | "readme_file":"endpoint/Readme.md", 10 | "template":"endpoint/template.xlsx", 11 | "start_row":2, 12 | "start_col":1, 13 | "entrypoint":"endpoint.entrypoint.generate", 14 | "audience":[ 15 | "provider", 16 | "vendor" 17 | ], 18 | "report_spec": "1", 19 | "parameters":[] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /tests/fixtures/reports/connect_exception/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Connect Reports fixture! 2 | -------------------------------------------------------------------------------- /tests/fixtures/reports/connect_exception/entrypoint/Readme.md: -------------------------------------------------------------------------------- 1 | # Basic report info -------------------------------------------------------------------------------- /tests/fixtures/reports/connect_exception/entrypoint/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/reports/connect_exception/entrypoint/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/reports/connect_exception/entrypoint/entrypoint.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020, CloudBlue 4 | # All rights reserved. 5 | # 6 | 7 | from connect.client import ClientError 8 | 9 | 10 | def generate(client, parameters, progress_callback): 11 | raise ClientError( 12 | message='Test', 13 | status_code=409, 14 | error_code=409, 15 | errors=[ 16 | 'some error', 17 | ], 18 | ) 19 | -------------------------------------------------------------------------------- /tests/fixtures/reports/connect_exception/entrypoint/template.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/reports/connect_exception/entrypoint/template.xlsx -------------------------------------------------------------------------------- /tests/fixtures/reports/connect_exception/reports.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Connect Reports Fixture", 3 | "readme_file":"README.md", 4 | "version":"1.0.0", 5 | "language":"python", 6 | "reports":[ 7 | { 8 | "name":"test report", 9 | "readme_file":"entrypoint/Readme.md", 10 | "template":"entrypoint/template.xlsx", 11 | "start_row":2, 12 | "start_col":1, 13 | "entrypoint":"entrypoint.entrypoint.generate", 14 | "audience":[ 15 | "provider", 16 | "vendor" 17 | ], 18 | "report_spec": "1", 19 | "parameters":[] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tests/fixtures/reports/custom_exception/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Connect Reports fixture! 2 | -------------------------------------------------------------------------------- /tests/fixtures/reports/custom_exception/entry__point/Readme.md: -------------------------------------------------------------------------------- 1 | # Basic report info -------------------------------------------------------------------------------- /tests/fixtures/reports/custom_exception/entry__point/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/reports/custom_exception/entry__point/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/reports/custom_exception/entry__point/entrypoint.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020, CloudBlue 4 | # All rights reserved. 5 | # 6 | 7 | from connect.client import ClientError 8 | 9 | 10 | def generate(client, parameters, progress_callback): 11 | raise RuntimeError('Custom error') 12 | -------------------------------------------------------------------------------- /tests/fixtures/reports/custom_exception/entry__point/template.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/reports/custom_exception/entry__point/template.xlsx -------------------------------------------------------------------------------- /tests/fixtures/reports/custom_exception/reports.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Connect Reports Fixture", 3 | "readme_file":"README.md", 4 | "version":"1.0.0", 5 | "language":"python", 6 | "reports":[ 7 | { 8 | "name":"test report", 9 | "readme_file":"entry__point/Readme.md", 10 | "template":"entry__point/template.xlsx", 11 | "start_row":2, 12 | "start_col":1, 13 | "entrypoint":"entry__point.entrypoint.generate", 14 | "audience":[ 15 | "provider", 16 | "vendor" 17 | ], 18 | "report_spec": "1", 19 | "parameters":[] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tests/fixtures/reports/generic_exception/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Connect Reports fixture! 2 | -------------------------------------------------------------------------------- /tests/fixtures/reports/generic_exception/entry_point/Readme.md: -------------------------------------------------------------------------------- 1 | # Basic report info -------------------------------------------------------------------------------- /tests/fixtures/reports/generic_exception/entry_point/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/reports/generic_exception/entry_point/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/reports/generic_exception/entry_point/entrypoint.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020, CloudBlue 4 | # All rights reserved. 5 | # 6 | 7 | from connect.client import ClientError 8 | 9 | 10 | def generate(client, parameters, progress_callback): 11 | raise Exception('Some error') 12 | -------------------------------------------------------------------------------- /tests/fixtures/reports/generic_exception/entry_point/template.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/reports/generic_exception/entry_point/template.xlsx -------------------------------------------------------------------------------- /tests/fixtures/reports/generic_exception/reports.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Connect Reports Fixture", 3 | "readme_file":"README.md", 4 | "version":"1.0.0", 5 | "language":"python", 6 | "reports":[ 7 | { 8 | "name":"test report", 9 | "readme_file":"entry_point/Readme.md", 10 | "template":"entry_point/template.xlsx", 11 | "start_row":2, 12 | "start_col":1, 13 | "entrypoint":"entry_point.entrypoint.generate", 14 | "audience":[ 15 | "provider", 16 | "vendor" 17 | ], 18 | "report_spec": "1", 19 | "parameters":[] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tests/fixtures/reports/no_reports/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Connect Reports fixture! 2 | -------------------------------------------------------------------------------- /tests/fixtures/reports/no_reports/reports.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Connect Reports fixture", 3 | "readme_file":"README.md", 4 | "version":"1.0.0", 5 | "language":"python", 6 | "reports":[] 7 | } -------------------------------------------------------------------------------- /tests/fixtures/reports/report_v2/Readme.md: -------------------------------------------------------------------------------- 1 | # Test v2 repository 2 | -------------------------------------------------------------------------------- /tests/fixtures/reports/report_v2/reports.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reportsV2", 3 | "readme_file": "Readme.md", 4 | "version": "0.1.0", 5 | "language": "python", 6 | "reports": [ 7 | { 8 | "name": "reportsV2", 9 | "readme_file": "reports/test_v2/Readme.md", 10 | "renderers": [ 11 | { 12 | "id": "xlsx", 13 | "type": "xlsx", 14 | "description": "Export data in Microsoft Excel 2020 format.", 15 | "template": "reports/test_v2/templates/xlsx/template.xlsx", 16 | "args": { 17 | "start_row": 2, 18 | "start_col": 1 19 | } 20 | }, 21 | { 22 | "id": "json", 23 | "type": "json", 24 | "description": "Export data as JSON" 25 | }, 26 | { 27 | "id": "pdf-portrait", 28 | "type": "pdf", 29 | "description": "Export data as PDF", 30 | "default": true, 31 | "template": "reports/test_v2/templates/pdf/template.html.j2", 32 | "args": { 33 | "css_file": "reports/test_v2/templates/pdf/template.css" 34 | } 35 | } 36 | 37 | ], 38 | "audience": [ 39 | "provider" 40 | ], 41 | "report_spec": "2", 42 | "entrypoint": "reports.test_v2.entrypoint.generate", 43 | "parameters": [ 44 | ] 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /tests/fixtures/reports/report_v2/reports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/reports/report_v2/reports/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/reports/report_v2/reports/test_v2/Readme.md: -------------------------------------------------------------------------------- 1 | # Test report v2 2 | -------------------------------------------------------------------------------- /tests/fixtures/reports/report_v2/reports/test_v2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/reports/report_v2/reports/test_v2/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/reports/report_v2/reports/test_v2/entrypoint.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2021, Globex Corporation 4 | # All rights reserved. 5 | # 6 | 7 | 8 | def generate(client, parameters, progress_callback, extra_context): 9 | """ 10 | Extracts data from Connect using the ConnectClient instance 11 | and input parameters provided as arguments, applies 12 | required transformations (if any) and returns an iterator of rows 13 | that will be used to fill the Excel file. 14 | Each element returned by the iterator must be an iterator over 15 | the columns value. 16 | 17 | :param client: An instance of the CloudBlue Connect 18 | client. 19 | :type client: cnct.ConnectClient 20 | :param parameters: Input parameters used to calculate the 21 | resulting dataset. 22 | :type parameters: dict 23 | :param progress_callback: A function that accepts t 24 | argument of type int that must 25 | be invoked to notify the progress 26 | of the report generation. 27 | :type progress_callback: func 28 | """ 29 | pass 30 | -------------------------------------------------------------------------------- /tests/fixtures/reports/report_v2/reports/test_v2/templates/pdf/template.css: -------------------------------------------------------------------------------- 1 | /* this is a css file */ -------------------------------------------------------------------------------- /tests/fixtures/reports/report_v2/reports/test_v2/templates/pdf/template.html.j2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/reports/report_v2/reports/test_v2/templates/xlsx/template.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/reports/report_v2/reports/test_v2/templates/xlsx/template.xlsx -------------------------------------------------------------------------------- /tests/fixtures/reports/report_with_inputs/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Connect Reports fixture! 2 | -------------------------------------------------------------------------------- /tests/fixtures/reports/report_with_inputs/executor/Readme.md: -------------------------------------------------------------------------------- 1 | # Basic report info -------------------------------------------------------------------------------- /tests/fixtures/reports/report_with_inputs/executor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/reports/report_with_inputs/executor/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/reports/report_with_inputs/executor/entrypoint.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2020, CloudBlue 4 | # All rights reserved. 5 | # 6 | 7 | 8 | def generate(client, parameters, progress_callback): 9 | output = [ 10 | [ 11 | 1, 12 | ], 13 | [ 14 | 2, 15 | ], 16 | ] 17 | progress_callback(1, 1) 18 | 19 | return output 20 | -------------------------------------------------------------------------------- /tests/fixtures/reports/report_with_inputs/executor/template.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/reports/report_with_inputs/executor/template.xlsx -------------------------------------------------------------------------------- /tests/fixtures/reports/report_with_inputs/reports.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Connect Reports Fixture", 3 | "readme_file":"README.md", 4 | "version":"1.0.0", 5 | "language":"python", 6 | "reports":[ 7 | { 8 | "name":"test report", 9 | "readme_file":"executor/Readme.md", 10 | "template":"executor/template.xlsx", 11 | "start_row":2, 12 | "start_col":1, 13 | "entrypoint":"executor.entrypoint.generate", 14 | "audience":[ 15 | "provider", 16 | "vendor" 17 | ], 18 | "report_spec": "1", 19 | "parameters":[ 20 | { 21 | "id":"status", 22 | "type":"checkbox", 23 | "name":"Contract status", 24 | "description": "Select the status of contracts to include in report", 25 | "choices": [ 26 | { 27 | "value": "active", 28 | "label": "Active" 29 | }, 30 | { 31 | "value": "enrolling", 32 | "label": "Enrolling" 33 | }, 34 | { 35 | "value": "rejected", 36 | "label": "Rejected" 37 | }, 38 | { 39 | "value": "pending", 40 | "label": "Pending" 41 | }, 42 | { 43 | "value": "terminated", 44 | "label": "Terminated" 45 | } 46 | ] 47 | }, 48 | { 49 | "id":"date", 50 | "type":"date_range", 51 | "name":"Report period", 52 | "description": "Provide the time period to create the report", 53 | "required": true 54 | } 55 | ] 56 | } 57 | ] 58 | } -------------------------------------------------------------------------------- /tests/fixtures/templates_sync.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/templates_sync.xlsx -------------------------------------------------------------------------------- /tests/fixtures/translation.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/translation.xlsx -------------------------------------------------------------------------------- /tests/fixtures/translation_attributes_response.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/translation_attributes_response.xlsx -------------------------------------------------------------------------------- /tests/fixtures/translation_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "TRN-8100-3865-4869", 3 | "context": { 4 | "id": "LCX-2604-7704-5548", 5 | "instance_id": "PRD-746-555-769", 6 | "name": "translation test product", 7 | "icon": "/media/VA-063-000/PRD-746-555-769/media/PRD-746-555-769-logo_ty6JA3T.png", 8 | "type": "product" 9 | }, 10 | "owner": { 11 | "id": "VA-063-000", 12 | "name": "Vendor account 00" 13 | }, 14 | "locale": { 15 | "id": "ES", 16 | "name": "Spanish" 17 | }, 18 | "description": "Translation for spanish", 19 | "stats": { 20 | "total": 30, 21 | "translated": 20 22 | }, 23 | "auto": { 24 | "enabled": false, 25 | "status": "off" 26 | }, 27 | "status": "active", 28 | "events": { 29 | "updated": { 30 | "at": "2022-04-28T10:51:29+00:00", 31 | "by": { 32 | "id": "UR-063-000-003", 33 | "name": "Carlos" 34 | } 35 | }, 36 | "created": { 37 | "at": "2022-04-14T15:32:37+00:00", 38 | "by": { 39 | "id": "UR-063-000-003", 40 | "name": "Carlos" 41 | } 42 | } 43 | }, 44 | "primary": false 45 | } -------------------------------------------------------------------------------- /tests/fixtures/translations_sync.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/fixtures/translations_sync.xlsx -------------------------------------------------------------------------------- /tests/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/plugins/__init__.py -------------------------------------------------------------------------------- /tests/plugins/commerce/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/plugins/commerce/__init__.py -------------------------------------------------------------------------------- /tests/plugins/customer/test_commands.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from click.testing import CliRunner 3 | 4 | 5 | def test_export_customers(mocker, config_mocker, ccli): 6 | mocked_dump = mocker.patch( 7 | 'connect.cli.plugins.customer.commands.dump_customers', 8 | ) 9 | 10 | runner = CliRunner() 11 | result = runner.invoke( 12 | ccli, 13 | [ 14 | 'customer', 15 | 'export', 16 | ], 17 | ) 18 | 19 | assert result.exit_code == 0 20 | mocked_dump.assert_called_once() 21 | 22 | 23 | @pytest.mark.parametrize('filename', ('customers', 'customers.xlsx')) 24 | def test_sync_customers(mocker, config_mocker, ccli, filename): 25 | mocked_sync = mocker.MagicMock() 26 | mocker.patch( 27 | 'connect.cli.plugins.customer.commands.CustomerSynchronizer', 28 | return_value=mocked_sync, 29 | ) 30 | 31 | runner = CliRunner() 32 | result = runner.invoke( 33 | ccli, 34 | [ 35 | 'customer', 36 | 'sync', 37 | filename, 38 | ], 39 | ) 40 | 41 | assert result.exit_code == 0 42 | mocked_sync.open.assert_called_once() 43 | mocked_sync.sync.assert_called_once() 44 | mocked_sync.save.assert_called_once() 45 | mocked_sync.stats.print.assert_called_once() 46 | -------------------------------------------------------------------------------- /tests/plugins/customer/test_export.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from connect.client import ConnectClient 4 | from openpyxl import load_workbook 5 | 6 | from connect.cli.plugins.customer.export import dump_customers 7 | 8 | 9 | def test_dump_customers(fs, mocked_responses, mocked_customer, mocked_reseller): 10 | warnings.filterwarnings('ignore', category=UserWarning) 11 | mocked_responses.add( 12 | method='GET', 13 | url='https://localhost/public/v1/tier/accounts', 14 | json=[mocked_customer, mocked_reseller], 15 | headers={ 16 | 'Content-Range': 'items 0-1/1', 17 | }, 18 | ) 19 | 20 | output_file = dump_customers( 21 | ConnectClient( 22 | 'ApiKey XXX', 23 | endpoint='https://localhost/public/v1', 24 | use_specs=False, 25 | ), 26 | account_id='PA-1234', 27 | output_path=fs.root_path, 28 | output_file='Customers.xlsx', 29 | ) 30 | customers_wb = load_workbook( 31 | output_file, 32 | data_only=True, 33 | ) 34 | ws = customers_wb['Customers'] 35 | assert len(ws['A']) == 3 36 | assert ws['A2'].value == mocked_customer['id'] 37 | 38 | 39 | def test_dump_customers_client_error(mocker, fs, mocked_responses): 40 | warnings.filterwarnings('ignore', category=UserWarning) 41 | mocked_responses.add( 42 | method='GET', 43 | url='https://localhost/public/v1/tier/accounts', 44 | status=400, 45 | ) 46 | mocked_handle_error = mocker.patch( 47 | 'connect.cli.plugins.customer.export.handle_http_error', 48 | ) 49 | 50 | dump_customers( 51 | ConnectClient( 52 | 'ApiKey XXX', 53 | endpoint='https://localhost/public/v1', 54 | use_specs=False, 55 | ), 56 | account_id='PA-1234', 57 | output_path=fs.root_path, 58 | output_file='Customers.xlsx', 59 | ) 60 | 61 | mocked_handle_error.assert_called_once() 62 | -------------------------------------------------------------------------------- /tests/plugins/locale/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/plugins/locale/__init__.py -------------------------------------------------------------------------------- /tests/plugins/locale/test_commands.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | 4 | def test_list_locales(config_mocker, mocked_responses, mocked_locales_response, ccli): 5 | mocked_responses.add( 6 | method='GET', 7 | url='https://localhost/public/v1/localization/locales', 8 | headers={ 9 | 'Content-Range': 'items 0-0/1', 10 | }, 11 | json=[ 12 | { 13 | 'id': 'EN-AU', 14 | 'name': 'Australian English', 15 | 'local_name': 'Australian English', 16 | 'auto_translation': False, 17 | 'stats': { 18 | 'translations': 0, 19 | }, 20 | }, 21 | ], 22 | ) 23 | runner = CliRunner() 24 | result = runner.invoke( 25 | ccli, 26 | [ 27 | 'locale', 28 | 'list', 29 | ], 30 | ) 31 | assert result.exit_code == 0 32 | assert 'Current active account: VA-000 - Account 0' in result.output 33 | assert '│ EN-AU │ Australian English │ ✖ │ - │' in result.output 34 | 35 | 36 | def test_list_with_page_size_less_than_zero(config_mocker, ccli): 37 | runner = CliRunner() 38 | result = runner.invoke( 39 | ccli, 40 | [ 41 | '-p', 42 | '-1', 43 | 'locale', 44 | 'list', 45 | ], 46 | ) 47 | assert result.exit_code == 2 48 | assert ( 49 | "Error: Invalid value for '--page-size' / '-p': -1 is not in the range x>=1." 50 | in result.output 51 | ) 52 | -------------------------------------------------------------------------------- /tests/plugins/play/context.json: -------------------------------------------------------------------------------- 1 | { 2 | "endpoint": "https://api.cnct.info/public/v1", 3 | "program_agreement_id": "AGP-927-440-678", 4 | "distribution_agreements": [ 5 | "AGD-199-236-391", 6 | "AGD-669-983-907" 7 | ], 8 | "program_contract_id": "CRP-41033-36725-76650", 9 | "vendor_account_id": "VA-677-276" 10 | } 11 | -------------------------------------------------------------------------------- /tests/plugins/play/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/plugins/play/scripts/__init__.py -------------------------------------------------------------------------------- /tests/plugins/play/scripts/data.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/plugins/play/scripts/script1.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2021 Ingram Micro. All Rights Reserved. 5 | import sys 6 | 7 | from connect.cli.plugins.play.context import Context 8 | from connect.cli.plugins.play.save import Save 9 | from connect.cli.plugins.play.script import OptionWrapper, Script 10 | 11 | 12 | class Script1(Script): 13 | """CLI help for Script1.""" 14 | 15 | @classmethod 16 | def options(cls): 17 | return [ 18 | OptionWrapper('--some_id', help='Script1 IDs'), 19 | OptionWrapper('--account_token', help='Script1 account token'), 20 | ] 21 | 22 | def do(self, context=None): 23 | super().do(context=context) 24 | print('--- Init Script 1 ---') 25 | 26 | 27 | __all__ = ('Script1',) 28 | 29 | if __name__ == '__main__': 30 | try: 31 | ctx = Context.create(sys.argv[1:]) 32 | ctx | Script1 | Save 33 | print(ctx) 34 | except Exception as e: 35 | print(e) 36 | -------------------------------------------------------------------------------- /tests/plugins/play/scripts/script2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2021 Ingram Micro. All Rights Reserved. 5 | import sys 6 | 7 | from connect.cli.plugins.play.context import Context 8 | from connect.cli.plugins.play.save import Save 9 | from connect.cli.plugins.play.script import OptionWrapper, Script 10 | 11 | 12 | class Script2(Script): 13 | """CLI help for Script2.""" 14 | 15 | @classmethod 16 | def options(cls): 17 | return [ 18 | OptionWrapper('--account_token', help='Script2 account token'), 19 | ] 20 | 21 | def do(self, context=None): 22 | super().do(context=context) 23 | print('--- Init Script 2 ---') 24 | 25 | 26 | if __name__ == '__main__': 27 | try: 28 | ctx = Context.create(sys.argv[1:]) 29 | ctx | Script2 | Save 30 | print(ctx) 31 | except Exception as e: 32 | print(e) 33 | -------------------------------------------------------------------------------- /tests/plugins/play/test_play_commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2021 Ingram Micro. All Rights Reserved. 5 | import os 6 | import sys 7 | 8 | 9 | def unimport(): 10 | for m in ('connect.cli.plugins.play.commands', 'connect.cli.ccli'): 11 | if m in sys.modules: 12 | del sys.modules[m] 13 | 14 | 15 | def test_play_commands(fs, mocker): 16 | os.environ['CCLI_SCRIPTS'] = os.path.join(os.path.dirname(__file__), 'scripts') 17 | 18 | unimport() 19 | from connect.cli.ccli import main 20 | 21 | mocker.patch('connect.cli.plugins.play.commands.PlayOptions.context_file', None) 22 | mocker.patch('sys.argv', ['cmd', 'play', 'script1']) 23 | main() 24 | 25 | 26 | def test_play_commands_rel(fs, mocker): 27 | os.environ['CCLI_SCRIPTS'] = 'tests/plugins/play/scripts' 28 | 29 | unimport() 30 | from connect.cli.ccli import main 31 | 32 | mocker.patch('connect.cli.plugins.play.commands.PlayOptions.context_file', None) 33 | mocker.patch('sys.argv', ['cmd', 'play', 'script1']) 34 | main() 35 | -------------------------------------------------------------------------------- /tests/plugins/play/test_script.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # This file is part of the Ingram Micro Cloud Blue Connect connect-cli. 4 | # Copyright (c) 2021 Ingram Micro. All Rights Reserved. 5 | from connect.client import ConnectClient 6 | 7 | from connect.cli.plugins.play.context import Context 8 | from connect.cli.plugins.play.script import OptionWrapper, Script 9 | 10 | 11 | def test_script(): 12 | ow = OptionWrapper(1, 2, 3, a=1, b=2, c=3) 13 | assert ow.args == (1, 2, 3) 14 | assert ow.kwargs == {'a': 1, 'b': 2, 'c': 3} 15 | 16 | class BasicInitScript(Script): 17 | """Some Help Message""" 18 | 19 | assert BasicInitScript.command() == 'basic-init-script' 20 | assert BasicInitScript.help() == 'Some Help Message' 21 | assert BasicInitScript.options() == [] 22 | 23 | ctx = Context() 24 | ctx.endpoint = 'https://api.cnct.info/public/v1' 25 | ctx.distributor_account_token = 'ApiKey v1' 26 | ctx.vendor_account_token = 'ApiKey v2' 27 | 28 | s = BasicInitScript(context=ctx) 29 | assert type(s.dclient) == ConnectClient 30 | assert type(s.vclient) == ConnectClient 31 | 32 | s.do() 33 | s.do(context={'endpoint': 'https://api.cnct.tech/public/v1'}) 34 | -------------------------------------------------------------------------------- /tests/plugins/product/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/plugins/product/__init__.py -------------------------------------------------------------------------------- /tests/plugins/product/sync/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/plugins/product/sync/__init__.py -------------------------------------------------------------------------------- /tests/plugins/product/test_export.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from connect.cli.plugins.product.export import _primary_translation_str 4 | 5 | 6 | def test_primary_translation_str_ok(): 7 | translation = { 8 | 'locale': { 9 | 'id': 'jp', 10 | 'name': 'Japanese', 11 | } 12 | } 13 | 14 | assert 'jp (Japanese)' == _primary_translation_str(translation) 15 | 16 | 17 | def test_primary_translation_str_none(): 18 | assert '' == _primary_translation_str(None) 19 | -------------------------------------------------------------------------------- /tests/plugins/project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/plugins/project/__init__.py -------------------------------------------------------------------------------- /tests/plugins/project/test_extension_validators.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from interrogatio.core.exceptions import ValidationError 3 | 4 | from connect.cli.plugins.project.extension.validators import AppTypesValidator, UISupportValidator 5 | 6 | 7 | @pytest.mark.parametrize( 8 | 'app_types', 9 | ( 10 | ['anvil'], 11 | ['events', 'webapp'], 12 | ['tfnapp'], 13 | ), 14 | ) 15 | def test_app_types_validator_error(app_types): 16 | context = { 17 | 'extension_type': 'transformations', 18 | } 19 | validator = AppTypesValidator() 20 | with pytest.raises(ValidationError): 21 | validator.validate(app_types, context) 22 | 23 | 24 | def test_app_types_validator(): 25 | context = { 26 | 'extension_type': 'transformations', 27 | } 28 | validator = AppTypesValidator() 29 | assert validator.validate(['webapp', 'tfnapp'], context) is None 30 | 31 | 32 | def test_ui_support_validator_error(): 33 | context = { 34 | 'extension_type': 'transformations', 35 | } 36 | validator = UISupportValidator() 37 | with pytest.raises(ValidationError): 38 | validator.validate('n', context) 39 | 40 | 41 | def test_ui_support_validator(): 42 | context = { 43 | 'extension_type': 'transformations', 44 | } 45 | validator = UISupportValidator() 46 | assert validator.validate('y', context) is None 47 | -------------------------------------------------------------------------------- /tests/plugins/project/test_utils.py: -------------------------------------------------------------------------------- 1 | import stat 2 | 3 | from connect.cli.plugins.project.utils import force_delete, purge_dir 4 | 5 | 6 | def test_force_delete(mocker): 7 | mocked_os_chmod = mocker.patch('connect.cli.plugins.project.utils.os.chmod') 8 | mocked_f = mocker.MagicMock() 9 | force_delete(mocked_f, 'somepath', None) 10 | mocked_os_chmod.assert_called_with('somepath', stat.S_IWRITE) 11 | assert mocked_f.call_count == 1 12 | 13 | 14 | def test_purge_dir(mocker): 15 | mocked_os_isdir = mocker.patch( 16 | 'connect.cli.plugins.project.utils.os.path.isdir', 17 | return_value=True, 18 | ) 19 | mocked_shutil_rmtree = mocker.patch('connect.cli.plugins.project.utils.shutil.rmtree') 20 | mocked_force_delete = mocker.patch('connect.cli.plugins.project.utils.force_delete') 21 | purge_dir('somepath') 22 | mocked_os_isdir.assert_called_with('somepath') 23 | mocked_shutil_rmtree.assert_called_with('somepath', onerror=mocked_force_delete) 24 | -------------------------------------------------------------------------------- /tests/plugins/report/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/plugins/report/__init__.py -------------------------------------------------------------------------------- /tests/plugins/report/test_utils.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | import pytest 4 | from click import ClickException 5 | 6 | from connect.cli.plugins.report.utils import ( 7 | get_renderer_by_id, 8 | get_report_by_id, 9 | get_report_entrypoint, 10 | ) 11 | 12 | 13 | Repo = namedtuple('Repo', ('reports',)) 14 | Report = namedtuple('Report', ('local_id', 'renderers', 'root_path', 'entrypoint')) 15 | Renderer = namedtuple('Renderer', ('id',)) 16 | 17 | 18 | def test_get_renderer_by_id(): 19 | renderers = [Renderer('renderer_id'), Renderer('renderer1_id')] 20 | report = Report('local_id', renderers, None, None) 21 | 22 | renderer = get_renderer_by_id(report, 'renderer_id') 23 | assert renderer == renderers[0] 24 | 25 | 26 | def test_get_renderer_by_id_not_found(): 27 | renderers = [Renderer('renderer_id'), Renderer('renderer1_id')] 28 | report = Report('local_id', renderers, None, None) 29 | 30 | with pytest.raises(ClickException) as cv: 31 | get_renderer_by_id(report, 'renderer2_id') 32 | 33 | assert str(cv.value) == 'The output format `renderer2_id` is not available for report local_id.' 34 | 35 | 36 | def test_get_report_by_id(): 37 | reports = [Report('local_id', None, None, None), Report('local2_id', None, None, None)] 38 | repo = Repo(reports) 39 | 40 | report = get_report_by_id(repo, 'local_id') 41 | assert report == reports[0] 42 | 43 | 44 | def test_get_report_by_id_not_found(): 45 | reports = [Report('local_id', None, None, None), Report('local2_id', None, None, None)] 46 | repo = Repo(reports) 47 | 48 | with pytest.raises(ClickException) as cv: 49 | get_report_by_id(repo, 'local3_id') 50 | 51 | assert str(cv.value) == 'The report `local3_id` does not exist.' 52 | 53 | 54 | def test_get_report_entrypoint(mocker): 55 | entrypoint = mocker.MagicMock() 56 | 57 | module = mocker.MagicMock() 58 | module.entrypoint = entrypoint 59 | 60 | report = Report( 61 | 'local_id', 62 | None, 63 | 'root_path', 64 | 'reports.my_report.entrypoint', 65 | ) 66 | mocker.patch('connect.cli.plugins.report.utils.import_module', return_value=module) 67 | 68 | ep = get_report_entrypoint(report) 69 | assert ep == entrypoint 70 | 71 | 72 | @pytest.mark.parametrize('exc', (ImportError, AttributeError)) 73 | def test_get_report_entrypoint_fail(mocker, exc): 74 | entrypoint = mocker.MagicMock() 75 | 76 | module = mocker.MagicMock() 77 | module.entrypoint = entrypoint 78 | 79 | report = Report( 80 | 'local_id', 81 | None, 82 | 'root_path', 83 | 'reports.my_report.entrypoint', 84 | ) 85 | mocker.patch('connect.cli.plugins.report.utils.import_module', side_effect=exc('test')) 86 | 87 | with pytest.raises(ClickException) as cv: 88 | get_report_entrypoint(report) 89 | 90 | assert 'Cannot load report code for report local_id:' in str(cv.value) 91 | -------------------------------------------------------------------------------- /tests/plugins/shared/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/plugins/shared/__init__.py -------------------------------------------------------------------------------- /tests/plugins/translation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudblue/connect-cli/99abc21edeae64211e4ff41a482f751c8dbf7373/tests/plugins/translation/__init__.py -------------------------------------------------------------------------------- /tests/plugins/translation/test_activate.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pytest 3 | from connect.client import ConnectClient 4 | 5 | from connect.cli.plugins.translation.activate import activate_translation 6 | 7 | 8 | def test_activate_translation( 9 | mocked_responses, 10 | mocked_translation_response, 11 | ): 12 | mocked_responses.add( 13 | method='POST', 14 | url='https://localhost/public/v1/localization/translations/TRN-8100-3865-4869/activate', 15 | json=mocked_translation_response, 16 | status=200, 17 | ) 18 | 19 | translation = activate_translation( 20 | ConnectClient( 21 | 'ApiKey XXX', 22 | endpoint='https://localhost/public/v1', 23 | use_specs=False, 24 | ), 25 | translation_id='TRN-8100-3865-4869', 26 | ) 27 | 28 | assert translation['status'] == 'active' 29 | 30 | 31 | def test_translation_already_activated(mocked_responses): 32 | mocked_responses.add( 33 | method='POST', 34 | url='https://localhost/public/v1/localization/translations/TRN-8100-3865-4869/activate', 35 | json={ 36 | 'error_code': 'TRE_003', 37 | 'errors': [ 38 | 'This translation is already activated.', 39 | ], 40 | }, 41 | status=400, 42 | ) 43 | 44 | with pytest.raises(click.ClickException) as e: 45 | activate_translation( 46 | ConnectClient( 47 | 'ApiKey XXX', 48 | endpoint='https://localhost/public/v1', 49 | use_specs=False, 50 | ), 51 | translation_id='TRN-8100-3865-4869', 52 | ) 53 | 54 | assert str(e.value) == '400 - Bad Request: TRE_003 - This translation is already activated.' 55 | 56 | 57 | def test_activate_translation_not_exists(mocked_responses): 58 | mocked_responses.add( 59 | method='POST', 60 | url='https://localhost/public/v1/localization/translations/TRN-0000-0000-0000/activate', 61 | status=404, 62 | ) 63 | with pytest.raises(click.ClickException) as e: 64 | activate_translation( 65 | ConnectClient( 66 | 'ApiKey XXX', 67 | endpoint='https://localhost/public/v1', 68 | use_specs=False, 69 | ), 70 | translation_id='TRN-0000-0000-0000', 71 | ) 72 | assert str(e.value) == '404 - Not Found: Translation TRN-0000-0000-0000 not found.' 73 | -------------------------------------------------------------------------------- /tests/plugins/translation/test_export.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | import pytest 4 | from click import ClickException 5 | from connect.client import ConnectClient 6 | from openpyxl import load_workbook 7 | 8 | from connect.cli.plugins.translation.export import dump_translation 9 | 10 | 11 | _LAST_COLUMN_BY_SHEET = { 12 | 'Instructions': 'B', 13 | 'General': 'B', 14 | 'Attributes': 'D', 15 | } 16 | 17 | 18 | def test_dump_translation( 19 | fs, 20 | mocked_responses, 21 | mocked_translation_attributes_xlsx_response, 22 | sample_translation_workbook, 23 | ): 24 | mocked_responses.add( 25 | method='GET', 26 | url='https://localhost/public/v1/localization/translations/TRN-8100-3865-4869/attributes', 27 | body=mocked_translation_attributes_xlsx_response, 28 | headers={ 29 | 'Contet-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 30 | }, 31 | ) 32 | 33 | output_file = dump_translation( 34 | ConnectClient( 35 | 'ApiKey XXX', 36 | endpoint='https://localhost/public/v1', 37 | use_specs=False, 38 | ), 39 | translation_id='TRN-8100-3865-4869', 40 | output_file='translation.xlsx', 41 | output_path=fs.root_path, 42 | ) 43 | 44 | translation_wb = load_workbook(output_file, data_only=True) 45 | for sheetname in sample_translation_workbook.sheetnames: 46 | assert sheetname in translation_wb.sheetnames 47 | 48 | for sheetname in sample_translation_workbook.sheetnames: 49 | sample_sheet = sample_translation_workbook[sheetname] 50 | translation_sheet = translation_wb[sheetname] 51 | letter_limit = _LAST_COLUMN_BY_SHEET[sheetname] 52 | for col in string.ascii_uppercase: 53 | if col == letter_limit: 54 | break 55 | for row in range(1, sample_sheet.max_row + 1): 56 | cell = f'{col}{row}' 57 | assert translation_sheet[cell].value == sample_sheet[cell].value 58 | 59 | 60 | def test_dump_translation_not_exists(fs, mocked_responses): 61 | mocked_responses.add( 62 | method='GET', 63 | url='https://localhost/public/v1/localization/translations/TRN-0000-0000-0000/attributes', 64 | status=404, 65 | ) 66 | with pytest.raises(ClickException) as e: 67 | dump_translation( 68 | ConnectClient( 69 | 'ApiKey XXX', 70 | endpoint='https://localhost/public/v1', 71 | use_specs=False, 72 | ), 73 | translation_id='TRN-0000-0000-0000', 74 | output_file='translation.xlsx', 75 | output_path=fs.root_path, 76 | ) 77 | 78 | assert str(e.value) == '404 - Not Found: Translation TRN-0000-0000-0000 not found.' 79 | -------------------------------------------------------------------------------- /tests/plugins/translation/test_primarize.py: -------------------------------------------------------------------------------- 1 | import click 2 | import pytest 3 | 4 | from connect.cli.plugins.translation.primarize import primarize_translation 5 | 6 | 7 | def test_primarize_translation( 8 | mocked_responses, 9 | mocked_translation_response, 10 | ): 11 | mocked_translation_response['primary'] = True 12 | mocked_responses.add( 13 | method='POST', 14 | url='https://localhost/public/v1/localization/translations/TRN-8100-3865-4869/primarize', 15 | json=mocked_translation_response, 16 | status=200, 17 | ) 18 | 19 | translation = primarize_translation( 20 | api_url='https://localhost/public/v1', 21 | api_key='ApiKey XXX', 22 | translation_id='TRN-8100-3865-4869', 23 | ) 24 | 25 | assert translation['primary'] 26 | 27 | 28 | def test_translation_already_primary(mocked_responses): 29 | mocked_responses.add( 30 | method='POST', 31 | url='https://localhost/public/v1/localization/translations/TRN-8100-3865-4869/primarize', 32 | json={ 33 | 'error_code': 'TRE_005', 34 | 'errors': [ 35 | 'This translation is already a primary translation.', 36 | ], 37 | }, 38 | status=400, 39 | ) 40 | 41 | with pytest.raises(click.ClickException) as e: 42 | primarize_translation( 43 | api_url='https://localhost/public/v1', 44 | api_key='ApiKey XXX', 45 | translation_id='TRN-8100-3865-4869', 46 | ) 47 | 48 | assert ( 49 | str(e.value) 50 | == '400 - Bad Request: TRE_005 - This translation is already a primary translation.' 51 | ) 52 | 53 | 54 | def test_primarize_translation_not_exists(mocked_responses): 55 | mocked_responses.add( 56 | method='POST', 57 | url='https://localhost/public/v1/localization/translations/TRN-0000-0000-0000/primarize', 58 | status=404, 59 | ) 60 | with pytest.raises(click.ClickException) as e: 61 | primarize_translation( 62 | api_url='https://localhost/public/v1', 63 | api_key='ApiKey XXX', 64 | translation_id='TRN-0000-0000-0000', 65 | ) 66 | assert str(e.value) == '404 - Not Found: Translation TRN-0000-0000-0000 not found.' 67 | -------------------------------------------------------------------------------- /tests/plugins/translation/test_utils.py: -------------------------------------------------------------------------------- 1 | from openpyxl import Workbook 2 | 3 | from connect.cli.plugins.translation.utils import insert_column_ws 4 | 5 | 6 | def test_insert_column_ws(): 7 | ws = Workbook().active 8 | ws.append(['Column A', 'Column B', 'Column C']) 9 | ws.append(['A.1', 'B.1', 'C.1']) 10 | ws.append(['A.2', 'B.2', 'C.2']) 11 | ws.column_dimensions['A'].width = 20 12 | ws.column_dimensions['B'].width = 35 13 | ws.column_dimensions['C'].width = 90 14 | 15 | insert_column_ws(ws, 2, 50) 16 | 17 | assert ws.column_dimensions['A'].width == 20 18 | assert ws.column_dimensions['B'].width == 50 19 | assert ws.column_dimensions['C'].width == 35 20 | assert ws.column_dimensions['D'].width == 90 21 | 22 | 23 | def test_insert_column_ws_default_width(): 24 | ws = Workbook().active 25 | ws.append(['Column A', 'Column B', 'Column C']) 26 | ws.append(['A.1', 'B.1', 'C.1']) 27 | ws.append(['A.2', 'B.2', 'C.2']) 28 | ws.column_dimensions['A'].width = 20 29 | ws.column_dimensions['B'].width = 35 30 | ws.column_dimensions['C'].width = 90 31 | 32 | insert_column_ws(ws, 2) 33 | 34 | assert ws.column_dimensions['A'].width == 20 35 | assert ws.column_dimensions['B'].width == 35 36 | assert ws.column_dimensions['C'].width == 35 37 | assert ws.column_dimensions['D'].width == 90 38 | 39 | 40 | def test_insert_column_ws_extra_right(): 41 | ws = Workbook().active 42 | ws.append(['Column A', 'Column B', 'Column C']) 43 | ws.append(['A.1', 'B.1', 'C.1']) 44 | ws.append(['A.2', 'B.2', 'C.2']) 45 | ws.column_dimensions['A'].width = 20 46 | ws.column_dimensions['B'].width = 35 47 | ws.column_dimensions['C'].width = 90 48 | 49 | insert_column_ws(ws, 4, 50) 50 | 51 | assert ws.column_dimensions['A'].width == 20 52 | assert ws.column_dimensions['B'].width == 35 53 | assert ws.column_dimensions['C'].width == 90 54 | assert ws.column_dimensions['D'].width == 50 55 | -------------------------------------------------------------------------------- /tests/test_ccli.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from connect.cli.ccli import main 4 | from connect.cli.core.constants import CAIRO_NOT_FOUND_ERROR 5 | 6 | 7 | def test_run_ok(mocker): 8 | mock_cli = mocker.patch('connect.cli.ccli.cli') 9 | mock_load_plugins = mocker.patch('connect.cli.ccli.load_plugins') 10 | 11 | main() 12 | mock_load_plugins.assert_called_once_with(mock_cli) 13 | mock_cli.assert_called_once_with(prog_name='ccli', standalone_mode=False) 14 | 15 | 16 | def test_run_click_exception(mocker): 17 | mock_cli = mocker.patch('connect.cli.ccli.cli', side_effect=click.ClickException('test exc')) 18 | mock_load_plugins = mocker.patch('connect.cli.ccli.load_plugins') 19 | mock_secho = mocker.patch('connect.cli.ccli.click.secho') 20 | main() 21 | mock_load_plugins.assert_called_once_with(mock_cli) 22 | mock_cli.assert_called_once_with(prog_name='ccli', standalone_mode=False) 23 | mock_secho.assert_called_once_with('test exc', fg='red') 24 | 25 | 26 | def test_run_abort_exception(mocker): 27 | mock_cli = mocker.patch('connect.cli.ccli.cli', side_effect=click.exceptions.Abort('abort')) 28 | mock_load_plugins = mocker.patch('connect.cli.ccli.load_plugins') 29 | mock_secho = mocker.patch('connect.cli.ccli.click.secho') 30 | main() 31 | mock_load_plugins.assert_called_once_with(mock_cli) 32 | mock_cli.assert_called_once_with(prog_name='ccli', standalone_mode=False) 33 | mock_secho.assert_not_called() 34 | 35 | 36 | def test_run_no_cairo(mocker): 37 | mocker.patch( 38 | 'connect.cli.ccli.cli', 39 | side_effect=OSError('no library called "cairo" was found'), 40 | ) 41 | mocker.patch('connect.cli.ccli.load_plugins') 42 | mock_secho = mocker.patch('connect.cli.ccli.click.secho') 43 | 44 | main() 45 | mock_secho.assert_called_once_with(CAIRO_NOT_FOUND_ERROR, fg='yellow') 46 | 47 | 48 | def test_run_other_os_error(mocker): 49 | mocker.patch( 50 | 'connect.cli.ccli.cli', 51 | side_effect=OSError('other error'), 52 | ) 53 | mocker.patch('connect.cli.ccli.load_plugins') 54 | mock_secho = mocker.patch('connect.cli.ccli.click.secho') 55 | 56 | main() 57 | mock_secho.assert_called_once_with('other error', fg='red') 58 | --------------------------------------------------------------------------------