Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def read(*names, **kwargs):
"sqlglot~=25.30.0",
"mcp~=1.9.0",
"pyperclip~=1.8.2",
"python-dotenv~=1.0.0",
],
extras_require={
# eg:
Expand Down
85 changes: 84 additions & 1 deletion src/datapilot/cli/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,95 @@
import json
import os
import re
from pathlib import Path

import click
from dotenv import load_dotenv

from datapilot.core.mcp_utils.mcp import mcp
from datapilot.core.platforms.dbt.cli.cli import dbt


def load_config_from_file():
"""Load configuration from ~/.altimate/altimate.json if it exists."""
config_path = Path.home() / ".altimate" / "altimate.json"

if not config_path.exists():
return {}

try:
with open(config_path, 'r') as f:
config = json.load(f)
return config
except (json.JSONDecodeError, IOError) as e:
click.echo(f"Warning: Failed to load config from {config_path}: {e}", err=True)
return {}


def substitute_env_vars(value):
"""Replace ${env:ENV_VARIABLE} patterns with actual environment variable values."""
if not isinstance(value, str):
return value

# Pattern to match ${env:VARIABLE_NAME}
pattern = r'\$\{env:([^}]+)\}'

def replacer(match):
env_var = match.group(1)
return os.environ.get(env_var, match.group(0))

return re.sub(pattern, replacer, value)


def process_config(config):
"""Process configuration dictionary to substitute environment variables."""
processed = {}
for key, value in config.items():
processed[key] = substitute_env_vars(value)
return processed


@click.group()
def datapilot():
@click.option("--token", required=False, help="Your API token for authentication.")
@click.option("--instance-name", required=False, help="Your tenant ID.")
@click.option("--backend-url", required=False, help="Altimate's Backend URL", default="https://api.myaltimate.com")
@click.pass_context
def datapilot(ctx, token, instance_name, backend_url):
"""Altimate CLI for DBT project management."""
# Load .env file from current directory if it exists
load_dotenv()

# Load configuration from file
file_config = load_config_from_file()
file_config = process_config(file_config)

# Map config file keys to CLI option names
config_mapping = {
'altimateApiKey': 'token',
'altimateInstanceName': 'instance_name',
'altimateUrl': 'backend_url'
}

# Store common options in context, with CLI args taking precedence
ctx.ensure_object(dict)

# Apply file config first
for file_key, cli_key in config_mapping.items():
if file_key in file_config:
ctx.obj[cli_key] = file_config[file_key]

# Override with CLI arguments if provided
if token is not None:
ctx.obj['token'] = token
if instance_name is not None:
ctx.obj['instance_name'] = instance_name
if backend_url != "https://api.myaltimate.com": # Only override if not default
ctx.obj['backend_url'] = backend_url

# Set defaults if nothing was provided
ctx.obj.setdefault('token', None)
ctx.obj.setdefault('instance_name', None)
ctx.obj.setdefault('backend_url', 'https://api.myaltimate.com')


datapilot.add_command(dbt)
Expand Down
37 changes: 24 additions & 13 deletions src/datapilot/core/platforms/dbt/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@

# New dbt group
@click.group()
def dbt():
@click.pass_context
def dbt(ctx):
"""DBT specific commands."""
# Ensure context object exists
ctx.ensure_object(dict)


@dbt.command("project-health")
@click.option("--token", required=False, help="Your API token for authentication.")
@click.option("--instance-name", required=False, help="Your tenant ID.")
@click.option(
"--manifest-path",
required=True,
Expand All @@ -57,21 +58,24 @@ def dbt():
default=None,
help="Selective model testing. Specify one or more models to run tests on.",
)
@click.option("--backend-url", required=False, help="Altimate's Backend URL", default="https://api.myaltimate.com")
@click.pass_context
def project_health(
token,
instance_name,
ctx,
manifest_path,
catalog_path,
config_path=None,
config_name=None,
select=None,
backend_url="https://api.myaltimate.com",
):
"""
Validate the DBT project's configuration and structure.
:param manifest_path: Path to the DBT manifest file.
"""
# Get common options from parent context
token = ctx.parent.obj.get('token')
instance_name = ctx.parent.obj.get('instance_name')
backend_url = ctx.parent.obj.get('backend_url')

config = None
if config_path:
config = load_config(config_path)
Expand Down Expand Up @@ -131,25 +135,32 @@ def project_health(


@dbt.command("onboard")
@click.option("--token", prompt="API Token", help="Your API token for authentication.")
@click.option("--instance-name", prompt="Instance Name", help="Your tenant ID.")
@click.option("--dbt_core_integration_id", prompt="DBT Core Integration ID", help="DBT Core Integration ID")
@click.option(
"--dbt_core_integration_environment", default="PROD", prompt="DBT Core Integration Environment", help="DBT Core Integration Environment"
)
@click.option("--manifest-path", required=True, prompt="Manifest Path", help="Path to the manifest file.")
@click.option("--catalog-path", required=False, prompt=False, help="Path to the catalog file.")
@click.option("--backend-url", required=False, help="Altimate's Backend URL", default="https://api.myaltimate.com")
@click.pass_context
def onboard(
token,
instance_name,
ctx,
dbt_core_integration_id,
dbt_core_integration_environment,
manifest_path,
catalog_path,
backend_url="https://api.myaltimate.com",
):
"""Onboard a manifest file to DBT."""
# Get common options from parent context
token = ctx.parent.obj.get('token')
instance_name = ctx.parent.obj.get('instance_name')
backend_url = ctx.parent.obj.get('backend_url')

# For onboard command, token and instance_name are required
if not token:
token = click.prompt("API Token")
if not instance_name:
instance_name = click.prompt("Instance Name")

check_token_and_instance(token, instance_name)

if not validate_credentials(token, backend_url, instance_name):
Expand Down
Loading