altimate-datapilot-cli 0.0.20__py3-none-any.whl → 0.0.21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: altimate-datapilot-cli
3
- Version: 0.0.20
3
+ Version: 0.0.21
4
4
  Summary: Assistant for Data Teams
5
5
  Home-page: https://github.com/AltimateAI/datapilot-cli
6
6
  Author: Altimate Inc
@@ -9,7 +9,6 @@ License: MIT
9
9
  Project-URL: Documentation, https://datapilot.readthedocs.io/
10
10
  Project-URL: Changelog, https://datapilot.readthedocs.io/en/latest/changelog.html
11
11
  Project-URL: Issue Tracker, https://github.com/AltimateAI/datapilot-cli/issues
12
- Platform: UNKNOWN
13
12
  Classifier: Development Status :: 5 - Production/Stable
14
13
  Classifier: Intended Audience :: Developers
15
14
  Classifier: License :: OSI Approved :: MIT License
@@ -39,6 +38,17 @@ Requires-Dist: sqlglot~=25.30.0
39
38
  Requires-Dist: mcp~=1.9.0
40
39
  Requires-Dist: pyperclip~=1.8.2
41
40
  Requires-Dist: python-dotenv~=1.0.0
41
+ Dynamic: author
42
+ Dynamic: author-email
43
+ Dynamic: classifier
44
+ Dynamic: description
45
+ Dynamic: home-page
46
+ Dynamic: license
47
+ Dynamic: license-file
48
+ Dynamic: project-url
49
+ Dynamic: requires-dist
50
+ Dynamic: requires-python
51
+ Dynamic: summary
42
52
 
43
53
  ========
44
54
  Overview
@@ -101,5 +111,3 @@ Changelog
101
111
  ------------------
102
112
 
103
113
  * First release on PyPI.
104
-
105
-
@@ -1,7 +1,10 @@
1
- datapilot/__init__.py,sha256=wQP0zPwrPeGkZ12uVa4mTM7oYoqji6PECSRd7QD_QXE,23
1
+ altimate_datapilot_cli-0.0.21.dist-info/licenses/AUTHORS.rst,sha256=S4H4zw_v3GVyz5_55jF5Gf_YNG3s5Y0VgbQaEov9PFk,50
2
+ altimate_datapilot_cli-0.0.21.dist-info/licenses/LICENSE,sha256=Mf7VqpsmU2QR5_s2Cb_ZeeMB2Q9KW7YXJENZPFZRK1k,1100
3
+ datapilot/__init__.py,sha256=PsqtE_T084MVsMv47JyTQ3DK2CRZJ3Kd9Q_vnw02oZk,23
2
4
  datapilot/__main__.py,sha256=I9USmeNnK-cAHb6LZfydJC0LeNSE8enieeY55wpR6uw,380
3
5
  datapilot/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- datapilot/cli/main.py,sha256=rRSysEUFFPXTIm9JxSL-JqLD73i1QvJMtgscnAGV6Is,3170
6
+ datapilot/cli/decorators.py,sha256=TYO_q-AMsq-86T3i9zI0fVLAhfwu87qAHmvZAJgt3xQ,2842
7
+ datapilot/cli/main.py,sha256=pf3Thf5zX3s4SmgB8fYVn00WnQKYJ1ARqSjJ3q-M4xo,444
5
8
  datapilot/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
9
  datapilot/clients/altimate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
10
  datapilot/clients/altimate/client.py,sha256=00TRe_ck8UgbhFMAnrLBmug3fAWxAggNl2do5Um_4oU,4083
@@ -22,7 +25,7 @@ datapilot/core/insights/sql/base/insight.py,sha256=k8UUn0qrN-QG6NCunPl7Hd6L6kd1X
22
25
  datapilot/core/insights/sql/runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
26
  datapilot/core/insights/sql/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
27
  datapilot/core/knowledge/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- datapilot/core/knowledge/cli.py,sha256=-bxf6kNVnIqaFtCTS0k7XZ07O21P5YGg9Nh3Bj5mOkQ,1415
28
+ datapilot/core/knowledge/cli.py,sha256=f9cmHTJW1Ts5slUgk7TqM2zqhjmEx5axRcnZPQgbO5s,1524
26
29
  datapilot/core/knowledge/server.py,sha256=hO6WyMUKBkukx_7JSBDvLztoFIKqEezNl4flQdtmNwQ,3191
27
30
  datapilot/core/mcp_utils/__init__.py,sha256=39zN2cGQCsEjRFeExv2bX4MoqVv4H14o_SYp_QG2jHU,18
28
31
  datapilot/core/mcp_utils/mcp.py,sha256=e-FbHmpEr673hxVBFHv996qnHCuqKqiCrMMy_g5tMjg,6027
@@ -35,7 +38,7 @@ datapilot/core/platforms/dbt/factory.py,sha256=YIQtb-FQQAJsifJ3KiLjjk0WIKTHtEPTN
35
38
  datapilot/core/platforms/dbt/formatting.py,sha256=bpfa7XmVghTq4WnGDGYC6DruwOwH8YmjFHghoo5cPD8,1638
36
39
  datapilot/core/platforms/dbt/utils.py,sha256=ozFHprR6LTLXQdrGyaRoyIBTua4P1NkP8T7LGgN-9c0,18577
37
40
  datapilot/core/platforms/dbt/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- datapilot/core/platforms/dbt/cli/cli.py,sha256=kS19eIGf6iBRZtSw0stFW108jW1wM9lSU1BzBabhg3k,7263
41
+ datapilot/core/platforms/dbt/cli/cli.py,sha256=JAEH3PSXKK8qhKxPd4pAFf_LB-FSqVVXOD9iG1Kuv20,7098
39
42
  datapilot/core/platforms/dbt/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
43
  datapilot/core/platforms/dbt/hooks/executor_hook.py,sha256=gSM50vAO7C-f1rdnHogWbqc87aCXPXysZepjp5L2qzw,2966
41
44
  datapilot/core/platforms/dbt/insights/__init__.py,sha256=hk7BAzCTDkY8WNV6L0v-CPn9mrsDyJJusoQxNxGyzAY,7634
@@ -144,10 +147,8 @@ datapilot/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
144
147
  datapilot/utils/utils.py,sha256=MY8q6ZBJ0hkrTuH7gWMxAlEAQGrajXFMabEhtGtT7sc,11524
145
148
  datapilot/utils/formatting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
146
149
  datapilot/utils/formatting/utils.py,sha256=rAVmIYuldvw9VvCSwG2kMTEgiT7cEconp_F1sAWVyCo,1377
147
- altimate_datapilot_cli-0.0.20.dist-info/AUTHORS.rst,sha256=S4H4zw_v3GVyz5_55jF5Gf_YNG3s5Y0VgbQaEov9PFk,50
148
- altimate_datapilot_cli-0.0.20.dist-info/LICENSE,sha256=Mf7VqpsmU2QR5_s2Cb_ZeeMB2Q9KW7YXJENZPFZRK1k,1100
149
- altimate_datapilot_cli-0.0.20.dist-info/METADATA,sha256=N3AphCCJ77dpuI1JkMbqQxth2lKRaxnpwjKB1THvIp0,2474
150
- altimate_datapilot_cli-0.0.20.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
151
- altimate_datapilot_cli-0.0.20.dist-info/entry_points.txt,sha256=0zwgKxN40RLVB5jSmlJz7IH_FBqRtpFdbrdZn-xuQIY,141
152
- altimate_datapilot_cli-0.0.20.dist-info/top_level.txt,sha256=gAOFOdwB00vcxv74y4M1J-nQtPvEatU8-mYViEBcToo,10
153
- altimate_datapilot_cli-0.0.20.dist-info/RECORD,,
150
+ altimate_datapilot_cli-0.0.21.dist-info/METADATA,sha256=tWGs7r6Ovf6fCsGbs7sUw01fNFFfGVqD3Hi1_obKeng,2677
151
+ altimate_datapilot_cli-0.0.21.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
152
+ altimate_datapilot_cli-0.0.21.dist-info/entry_points.txt,sha256=5fKdw1Xm1RPJ6EZkBWvjNgLP6z3OU6iUvgDARe29pN8,140
153
+ altimate_datapilot_cli-0.0.21.dist-info/top_level.txt,sha256=gAOFOdwB00vcxv74y4M1J-nQtPvEatU8-mYViEBcToo,10
154
+ altimate_datapilot_cli-0.0.21.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.44.0)
2
+ Generator: setuptools (80.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,4 +1,3 @@
1
1
  [console_scripts]
2
2
  datapilot = datapilot.cli.main:datapilot
3
3
  datapilot_run_dbt_checks = datapilot.core.platforms.dbt.hooks.executor_hook:main
4
-
datapilot/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.0.20"
1
+ __version__ = "0.0.21"
@@ -0,0 +1,82 @@
1
+ import json
2
+ import os
3
+ import re
4
+ from functools import wraps
5
+ from pathlib import Path
6
+ from typing import Dict
7
+ from typing import Optional
8
+
9
+ import click
10
+ from dotenv import load_dotenv
11
+
12
+
13
+ def load_config_from_file() -> Optional[Dict]:
14
+ """Load configuration from ~/.altimate/altimate.json if it exists."""
15
+ config_path = Path.home() / ".altimate" / "altimate.json"
16
+
17
+ if not config_path.exists():
18
+ return None
19
+
20
+ try:
21
+ with config_path.open() as f:
22
+ config = json.load(f)
23
+ return config
24
+ except (OSError, json.JSONDecodeError) as e:
25
+ click.echo(f"Warning: Failed to load config from {config_path}: {e}", err=True)
26
+ return None
27
+
28
+
29
+ def substitute_env_vars(value):
30
+ """Replace ${env:ENV_VARIABLE} patterns with actual environment variable values."""
31
+ if not isinstance(value, str):
32
+ return value
33
+
34
+ # Pattern to match ${env:VARIABLE_NAME}
35
+ pattern = r"\$\{env:([^}]+)\}"
36
+
37
+ def replacer(match):
38
+ env_var = match.group(1)
39
+ return os.environ.get(env_var, match.group(0))
40
+
41
+ return re.sub(pattern, replacer, value)
42
+
43
+
44
+ def process_config(config):
45
+ """Process configuration dictionary to substitute environment variables."""
46
+ processed = {}
47
+ for key, value in config.items():
48
+ processed[key] = substitute_env_vars(value)
49
+ return processed
50
+
51
+
52
+ def auth_options(f):
53
+ """Decorator to add authentication options to commands."""
54
+
55
+ @click.option("--token", required=False, help="Your API token for authentication.", hide_input=True)
56
+ @click.option("--instance-name", required=False, help="Your tenant ID.")
57
+ @click.option("--backend-url", required=False, help="Altimate's Backend URL", default="https://api.myaltimate.com")
58
+ @wraps(f)
59
+ def wrapper(token, instance_name, backend_url, *args, **kwargs):
60
+ # Load .env file from current directory if it exists
61
+ load_dotenv()
62
+
63
+ final_token = token
64
+ final_instance_name = instance_name
65
+ final_backend_url = backend_url
66
+
67
+ if final_token is None and final_instance_name is None:
68
+ # Try to Load configuration from file if no CLI arguments are provided
69
+ file_config = load_config_from_file()
70
+ if file_config is not None:
71
+ # File config is provided
72
+ file_config = process_config(file_config)
73
+ if "altimateApiKey" in file_config:
74
+ final_token = file_config["altimateApiKey"]
75
+ if "altimateInstanceName" in file_config:
76
+ final_instance_name = file_config["altimateInstanceName"]
77
+ if "altimateUrl" in file_config:
78
+ final_backend_url = file_config["altimateUrl"] or final_backend_url
79
+
80
+ return f(final_token, final_instance_name, final_backend_url, *args, **kwargs)
81
+
82
+ return wrapper
datapilot/cli/main.py CHANGED
@@ -1,10 +1,4 @@
1
- import json
2
- import os
3
- import re
4
- from pathlib import Path
5
-
6
1
  import click
7
- from dotenv import load_dotenv
8
2
 
9
3
  from datapilot import __version__
10
4
  from datapilot.core.knowledge.cli import cli as knowledge
@@ -12,83 +6,10 @@ from datapilot.core.mcp_utils.mcp import mcp
12
6
  from datapilot.core.platforms.dbt.cli.cli import dbt
13
7
 
14
8
 
15
- def load_config_from_file():
16
- """Load configuration from ~/.altimate/altimate.json if it exists."""
17
- config_path = Path.home() / ".altimate" / "altimate.json"
18
-
19
- if not config_path.exists():
20
- return {}
21
-
22
- try:
23
- with config_path.open() as f:
24
- config = json.load(f)
25
- return config
26
- except (OSError, json.JSONDecodeError) as e:
27
- click.echo(f"Warning: Failed to load config from {config_path}: {e}", err=True)
28
- return {}
29
-
30
-
31
- def substitute_env_vars(value):
32
- """Replace ${env:ENV_VARIABLE} patterns with actual environment variable values."""
33
- if not isinstance(value, str):
34
- return value
35
-
36
- # Pattern to match ${env:VARIABLE_NAME}
37
- pattern = r"\$\{env:([^}]+)\}"
38
-
39
- def replacer(match):
40
- env_var = match.group(1)
41
- return os.environ.get(env_var, match.group(0))
42
-
43
- return re.sub(pattern, replacer, value)
44
-
45
-
46
- def process_config(config):
47
- """Process configuration dictionary to substitute environment variables."""
48
- processed = {}
49
- for key, value in config.items():
50
- processed[key] = substitute_env_vars(value)
51
- return processed
52
-
53
-
54
9
  @click.group()
55
10
  @click.version_option(version=__version__, prog_name="datapilot")
56
- @click.option("--token", required=False, help="Your API token for authentication.", hide_input=True)
57
- @click.option("--instance-name", required=False, help="Your tenant ID.")
58
- @click.option("--backend-url", required=False, help="Altimate's Backend URL", default="https://api.myaltimate.com")
59
- @click.pass_context
60
- def datapilot(ctx, token, instance_name, backend_url):
11
+ def datapilot():
61
12
  """Altimate CLI for DBT project management."""
62
- # Load .env file from current directory if it exists
63
- load_dotenv()
64
-
65
- # Load configuration from file
66
- file_config = load_config_from_file()
67
- file_config = process_config(file_config)
68
-
69
- # Map config file keys to CLI option names
70
- config_mapping = {"altimateApiKey": "token", "altimateInstanceName": "instance_name", "altimateUrl": "backend_url"}
71
-
72
- # Store common options in context, with CLI args taking precedence
73
- ctx.ensure_object(dict)
74
-
75
- # Apply file config first
76
- for file_key, cli_key in config_mapping.items():
77
- if file_key in file_config:
78
- ctx.obj[cli_key] = file_config[file_key]
79
-
80
- # Override with CLI arguments if provided
81
- if token is not None:
82
- ctx.obj["token"] = token
83
- if instance_name is not None:
84
- ctx.obj["instance_name"] = instance_name
85
- if backend_url != "https://api.myaltimate.com": # Only override if not default
86
- ctx.obj["backend_url"] = backend_url
87
-
88
- # Set defaults if nothing was provided
89
- ctx.obj.setdefault("token", None)
90
- ctx.obj.setdefault("instance_name", None)
91
- ctx.obj.setdefault("backend_url", "https://api.myaltimate.com")
92
13
 
93
14
 
94
15
  datapilot.add_command(dbt)
@@ -2,6 +2,9 @@ from http.server import HTTPServer
2
2
 
3
3
  import click
4
4
 
5
+ from datapilot.cli.decorators import auth_options
6
+ from datapilot.clients.altimate.utils import validate_credentials
7
+
5
8
  from .server import KnowledgeBaseHandler
6
9
 
7
10
 
@@ -11,20 +14,19 @@ def cli():
11
14
 
12
15
 
13
16
  @cli.command()
17
+ @auth_options
14
18
  @click.option("--port", default=4000, help="Port to run the server on")
15
- @click.pass_context
16
- def serve(ctx, port):
19
+ def serve(token, instance_name, backend_url, port):
17
20
  """Serve knowledge bases via HTTP server."""
18
- # Get configuration from parent context
19
- token = ctx.parent.obj.get("token")
20
- instance_name = ctx.parent.obj.get("instance_name")
21
- backend_url = ctx.parent.obj.get("backend_url")
22
-
23
21
  if not token or not instance_name:
24
22
  click.echo(
25
23
  "Error: API token and instance name are required. Use --token and --instance-name options or set them in config.", err=True
26
24
  )
27
- ctx.exit(1)
25
+ raise click.Abort
26
+
27
+ if not validate_credentials(token, backend_url, instance_name):
28
+ click.echo("Error: Invalid credentials.", err=True)
29
+ raise click.Abort
28
30
 
29
31
  # Set context data for the handler
30
32
  KnowledgeBaseHandler.token = token
@@ -2,6 +2,7 @@ import logging
2
2
 
3
3
  import click
4
4
 
5
+ from datapilot.cli.decorators import auth_options
5
6
  from datapilot.clients.altimate.utils import check_token_and_instance
6
7
  from datapilot.clients.altimate.utils import get_all_dbt_configs
7
8
  from datapilot.clients.altimate.utils import onboard_file
@@ -24,14 +25,12 @@ logging.basicConfig(level=logging.INFO)
24
25
 
25
26
  # New dbt group
26
27
  @click.group()
27
- @click.pass_context
28
- def dbt(ctx):
28
+ def dbt():
29
29
  """DBT specific commands."""
30
- # Ensure context object exists
31
- ctx.ensure_object(dict)
32
30
 
33
31
 
34
32
  @dbt.command("project-health")
33
+ @auth_options
35
34
  @click.option(
36
35
  "--manifest-path",
37
36
  required=True,
@@ -58,9 +57,10 @@ def dbt(ctx):
58
57
  default=None,
59
58
  help="Selective model testing. Specify one or more models to run tests on.",
60
59
  )
61
- @click.pass_context
62
60
  def project_health(
63
- ctx,
61
+ token,
62
+ instance_name,
63
+ backend_url,
64
64
  manifest_path,
65
65
  catalog_path,
66
66
  config_path=None,
@@ -71,10 +71,6 @@ def project_health(
71
71
  Validate the DBT project's configuration and structure.
72
72
  :param manifest_path: Path to the DBT manifest file.
73
73
  """
74
- # Get common options from parent context
75
- token = ctx.parent.obj.get("token")
76
- instance_name = ctx.parent.obj.get("instance_name")
77
- backend_url = ctx.parent.obj.get("backend_url")
78
74
 
79
75
  config = None
80
76
  if config_path:
@@ -135,25 +131,34 @@ def project_health(
135
131
 
136
132
 
137
133
  @dbt.command("onboard")
138
- @click.option("--dbt_core_integration_id", prompt="DBT Core Integration ID", help="DBT Core Integration ID")
134
+ @auth_options
139
135
  @click.option(
140
- "--dbt_core_integration_environment", default="PROD", prompt="DBT Core Integration Environment", help="DBT Core Integration Environment"
136
+ "--dbt_core_integration_id",
137
+ "--dbt_integration_id",
138
+ "dbt_integration_id", # This is the parameter name that will be passed to the function
139
+ prompt="DBT Integration ID",
140
+ help="DBT Core Integration ID or DBT Integration ID",
141
+ )
142
+ @click.option(
143
+ "--dbt_core_integration_environment",
144
+ "--dbt_integration_environment",
145
+ "dbt_integration_environment", # This is the parameter name that will be passed to the function
146
+ default="PROD",
147
+ prompt="DBT Integration Environment",
148
+ help="DBT Core Integration Environment or DBT Integration Environment",
141
149
  )
142
150
  @click.option("--manifest-path", required=True, prompt="Manifest Path", help="Path to the manifest file.")
143
151
  @click.option("--catalog-path", required=False, prompt=False, help="Path to the catalog file.")
144
- @click.pass_context
145
152
  def onboard(
146
- ctx,
147
- dbt_core_integration_id,
148
- dbt_core_integration_environment,
153
+ token,
154
+ instance_name,
155
+ backend_url,
156
+ dbt_integration_id,
157
+ dbt_integration_environment,
149
158
  manifest_path,
150
159
  catalog_path,
151
160
  ):
152
161
  """Onboard a manifest file to DBT."""
153
- # Get common options from parent context
154
- token = ctx.parent.obj.get("token")
155
- instance_name = ctx.parent.obj.get("instance_name")
156
- backend_url = ctx.parent.obj.get("backend_url")
157
162
 
158
163
  # For onboard command, token and instance_name are required
159
164
  if not token:
@@ -171,16 +176,13 @@ def onboard(
171
176
  click.echo("Error: You don't have permission to perform this action.")
172
177
  return
173
178
 
174
- # This will throw error if manifest file is incorrect
175
179
  try:
176
180
  load_manifest(manifest_path)
177
181
  except Exception as e:
178
182
  click.echo(f"Error: {e}")
179
183
  return
180
184
 
181
- response = onboard_file(
182
- token, instance_name, dbt_core_integration_id, dbt_core_integration_environment, "manifest", manifest_path, backend_url
183
- )
185
+ response = onboard_file(token, instance_name, dbt_integration_id, dbt_integration_environment, "manifest", manifest_path, backend_url)
184
186
  if response["ok"]:
185
187
  click.echo("Manifest onboarded successfully!")
186
188
  else:
@@ -189,21 +191,19 @@ def onboard(
189
191
  if not catalog_path:
190
192
  return
191
193
 
192
- response = onboard_file(
193
- token, instance_name, dbt_core_integration_id, dbt_core_integration_environment, "catalog", catalog_path, backend_url
194
- )
194
+ response = onboard_file(token, instance_name, dbt_integration_id, dbt_integration_environment, "catalog", catalog_path, backend_url)
195
195
  if response["ok"]:
196
196
  click.echo("Catalog onboarded successfully!")
197
197
  else:
198
198
  click.echo(f"{response['message']}")
199
199
 
200
- response = start_dbt_ingestion(token, instance_name, dbt_core_integration_id, dbt_core_integration_environment, backend_url)
200
+ response = start_dbt_ingestion(token, instance_name, dbt_integration_id, dbt_integration_environment, backend_url)
201
201
  if response["ok"]:
202
202
  url = map_url_to_instance(backend_url, instance_name)
203
203
  if not url:
204
204
  click.echo("Manifest and catalog ingestion has started.")
205
205
  else:
206
- url = f"{url}/settings/integrations/{dbt_core_integration_id}/{dbt_core_integration_environment}"
206
+ url = f"{url}/settings/integrations/{dbt_integration_id}/{dbt_integration_environment}"
207
207
  click.echo(f"Manifest and catalog ingestion has started. You can check the status at {url}")
208
208
  else:
209
209
  click.echo(f"{response['message']}")