Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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 Path.open(config_path) 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