Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
100644 → 100755
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
81 changes: 80 additions & 1 deletion src/datapilot/cli/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,91 @@
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 config_path.open() as f:
config = json.load(f)
return config
except (OSError, json.JSONDecodeError) 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.", hide_input=True)
@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
26 changes: 19 additions & 7 deletions tests/core/platform/dbt/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# test_app.py
from click.testing import CliRunner

from datapilot.core.platforms.dbt.cli.cli import project_health
from datapilot.cli.main import datapilot


def test_project_health_with_required_and_optional_args():
Expand All @@ -11,7 +11,9 @@ def test_project_health_with_required_and_optional_args():
config_path = "tests/data/config.yml"

# Simulate command invocation
result = runner.invoke(project_health, ["--manifest-path", manifest_path, "--catalog-path", catalog_path, "--config-path", config_path])
result = runner.invoke(
datapilot, ["dbt", "project-health", "--manifest-path", manifest_path, "--catalog-path", catalog_path, "--config-path", config_path]
)

assert result.exit_code == 0 # Ensure the command executed successfully
# Add more assertions here to validate the behavior of your command,
Expand All @@ -25,8 +27,10 @@ def test_project_health_with_only_required_arg():

# Simulate command invocation without optional arguments
result = runner.invoke(
project_health,
datapilot,
[
"dbt",
"project-health",
"--manifest-path",
manifest_path,
],
Expand All @@ -43,8 +47,10 @@ def test_project_health_with_only_required_arg_version1_6():

# Simulate command invocation without optional arguments
result = runner.invoke(
project_health,
datapilot,
[
"dbt",
"project-health",
"--manifest-path",
manifest_path,
],
Expand All @@ -61,8 +67,10 @@ def test_project_health_with_macro_args():

# Simulate command invocation without optional arguments
result = runner.invoke(
project_health,
datapilot,
[
"dbt",
"project-health",
"--manifest-path",
manifest_path,
],
Expand All @@ -76,8 +84,10 @@ def test_project_health_with_macro_args():

# Simulate command invocation without optional arguments
result = runner.invoke(
project_health,
datapilot,
[
"dbt",
"project-health",
"--manifest-path",
manifest_path,
],
Expand All @@ -95,7 +105,9 @@ def test_project_health_with_required_and_optional_args_v12():
config_path = "tests/data/config.yml"

# Simulate command invocation
result = runner.invoke(project_health, ["--manifest-path", manifest_path, "--catalog-path", catalog_path, "--config-path", config_path])
result = runner.invoke(
datapilot, ["dbt", "project-health", "--manifest-path", manifest_path, "--catalog-path", catalog_path, "--config-path", config_path]
)

assert result.exit_code == 0 # Ensure the command executed successfully
# Add more assertions here to validate the behavior of your command,
Expand Down