Skip to content

Commit 523cc4b

Browse files
authored
Merge branch 'main' into feat/generic-integration-id
2 parents c636ad5 + 249d093 commit 523cc4b

File tree

4 files changed

+101
-103
lines changed

4 files changed

+101
-103
lines changed

src/datapilot/cli/decorators.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import json
2+
import os
3+
import re
4+
from functools import wraps
5+
from pathlib import Path
6+
7+
import click
8+
from dotenv import load_dotenv
9+
10+
11+
def load_config_from_file():
12+
"""Load configuration from ~/.altimate/altimate.json if it exists."""
13+
config_path = Path.home() / ".altimate" / "altimate.json"
14+
15+
if not config_path.exists():
16+
return {}
17+
18+
try:
19+
with config_path.open() as f:
20+
config = json.load(f)
21+
return config
22+
except (OSError, json.JSONDecodeError) as e:
23+
click.echo(f"Warning: Failed to load config from {config_path}: {e}", err=True)
24+
return {}
25+
26+
27+
def substitute_env_vars(value):
28+
"""Replace ${env:ENV_VARIABLE} patterns with actual environment variable values."""
29+
if not isinstance(value, str):
30+
return value
31+
32+
# Pattern to match ${env:VARIABLE_NAME}
33+
pattern = r"\$\{env:([^}]+)\}"
34+
35+
def replacer(match):
36+
env_var = match.group(1)
37+
return os.environ.get(env_var, match.group(0))
38+
39+
return re.sub(pattern, replacer, value)
40+
41+
42+
def process_config(config):
43+
"""Process configuration dictionary to substitute environment variables."""
44+
processed = {}
45+
for key, value in config.items():
46+
processed[key] = substitute_env_vars(value)
47+
return processed
48+
49+
50+
def auth_options(f):
51+
"""Decorator to add authentication options to commands."""
52+
53+
@click.option("--token", required=False, help="Your API token for authentication.", hide_input=True)
54+
@click.option("--instance-name", required=False, help="Your tenant ID.")
55+
@click.option("--backend-url", required=False, help="Altimate's Backend URL", default="https://api.myaltimate.com")
56+
@wraps(f)
57+
def wrapper(token, instance_name, backend_url, *args, **kwargs):
58+
# Load .env file from current directory if it exists
59+
load_dotenv()
60+
61+
# Load configuration from file
62+
file_config = load_config_from_file()
63+
file_config = process_config(file_config)
64+
65+
# Apply file config first, then override with CLI arguments if provided
66+
final_token = token
67+
final_instance_name = instance_name
68+
final_backend_url = backend_url
69+
70+
# Use file config if CLI argument not provided
71+
if final_token is None and "altimateApiKey" in file_config:
72+
final_token = file_config["altimateApiKey"]
73+
if final_instance_name is None and "altimateInstanceName" in file_config:
74+
final_instance_name = file_config["altimateInstanceName"]
75+
if final_backend_url == "https://api.myaltimate.com" and "altimateUrl" in file_config:
76+
final_backend_url = file_config["altimateUrl"]
77+
78+
# Set defaults if nothing was provided
79+
if final_token is None:
80+
final_token = None
81+
if final_instance_name is None:
82+
final_instance_name = None
83+
if final_backend_url is None:
84+
final_backend_url = "https://api.myaltimate.com"
85+
86+
return f(final_token, final_instance_name, final_backend_url, *args, **kwargs)
87+
88+
return wrapper

src/datapilot/cli/main.py

Lines changed: 1 addition & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,15 @@
1-
import json
2-
import os
3-
import re
4-
from pathlib import Path
5-
61
import click
7-
from dotenv import load_dotenv
82

93
from datapilot import __version__
104
from datapilot.core.knowledge.cli import cli as knowledge
115
from datapilot.core.mcp_utils.mcp import mcp
126
from datapilot.core.platforms.dbt.cli.cli import dbt
137

148

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-
549
@click.group()
5510
@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():
6112
"""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")
9213

9314

9415
datapilot.add_command(dbt)

src/datapilot/core/knowledge/cli.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import click
44

5+
from datapilot.cli.decorators import auth_options
6+
57
from .server import KnowledgeBaseHandler
68

79

@@ -11,20 +13,15 @@ def cli():
1113

1214

1315
@cli.command()
16+
@auth_options
1417
@click.option("--port", default=4000, help="Port to run the server on")
15-
@click.pass_context
16-
def serve(ctx, port):
18+
def serve(token, instance_name, backend_url, port):
1719
"""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-
2320
if not token or not instance_name:
2421
click.echo(
2522
"Error: API token and instance name are required. Use --token and --instance-name options or set them in config.", err=True
2623
)
27-
ctx.exit(1)
24+
raise click.Abort
2825

2926
# Set context data for the handler
3027
KnowledgeBaseHandler.token = token

src/datapilot/core/platforms/dbt/cli/cli.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import click
44

5+
from datapilot.cli.decorators import auth_options
56
from datapilot.clients.altimate.utils import check_token_and_instance
67
from datapilot.clients.altimate.utils import get_all_dbt_configs
78
from datapilot.clients.altimate.utils import onboard_file
@@ -24,14 +25,12 @@
2425

2526
# New dbt group
2627
@click.group()
27-
@click.pass_context
28-
def dbt(ctx):
28+
def dbt():
2929
"""DBT specific commands."""
30-
# Ensure context object exists
31-
ctx.ensure_object(dict)
3230

3331

3432
@dbt.command("project-health")
33+
@auth_options
3534
@click.option(
3635
"--manifest-path",
3736
required=True,
@@ -58,9 +57,10 @@ def dbt(ctx):
5857
default=None,
5958
help="Selective model testing. Specify one or more models to run tests on.",
6059
)
61-
@click.pass_context
6260
def project_health(
63-
ctx,
61+
token,
62+
instance_name,
63+
backend_url,
6464
manifest_path,
6565
catalog_path,
6666
config_path=None,
@@ -71,10 +71,6 @@ def project_health(
7171
Validate the DBT project's configuration and structure.
7272
:param manifest_path: Path to the DBT manifest file.
7373
"""
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")
7874

7975
config = None
8076
if config_path:
@@ -135,6 +131,7 @@ def project_health(
135131

136132

137133
@dbt.command("onboard")
134+
@auth_options
138135
@click.option("--dbt_core_integration_id", prompt="DBT Core Integration ID", help="DBT Core Integration ID")
139136
@click.option(
140137
"--dbt_core_integration_id",
@@ -153,7 +150,6 @@ def project_health(
153150
)
154151
@click.option("--manifest-path", required=True, prompt="Manifest Path", help="Path to the manifest file.")
155152
@click.option("--catalog-path", required=False, prompt=False, help="Path to the catalog file.")
156-
@click.pass_context
157153
def onboard(
158154
ctx,
159155
dbt_integration_id,
@@ -162,10 +158,6 @@ def onboard(
162158
catalog_path,
163159
):
164160
"""Onboard a manifest file to DBT."""
165-
# Get common options from parent context
166-
token = ctx.parent.obj.get("token")
167-
instance_name = ctx.parent.obj.get("instance_name")
168-
backend_url = ctx.parent.obj.get("backend_url")
169161

170162
# For onboard command, token and instance_name are required
171163
if not token:

0 commit comments

Comments
 (0)