Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add CLI #1560

Merged
merged 3 commits into from
Jan 30, 2025
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 pandasai/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

162 changes: 162 additions & 0 deletions pandasai/cli/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import os
import re

import click

from pandasai import DatasetLoader
from pandasai.data_loader.semantic_layer_schema import (
SemanticLayerSchema,
Source,
SQLConnectionConfig,
)
from pandasai.helpers.path import find_project_root, get_validated_dataset_path


def validate_api_key(api_key: str) -> bool:
"""Validate PandaBI API key format."""
pattern = r"^PAI-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
return bool(re.match(pattern, api_key))


@click.group()
def cli():
"""🐼 PandaAI CLI - Manage your datasets with ease"""
pass


@cli.group()
def dataset():
"""📊 Dataset management commands"""
pass


@dataset.command()
def create():
"""🎨 Create a new dataset through a guided process"""
click.echo("🚀 Let's create a new dataset!\n")

# Get organization and dataset name
while True:
path = click.prompt("📁 Enter the dataset path (format: organization/dataset)")
try:
org_name, dataset_name = get_validated_dataset_path(path)
break
except ValueError as e:
click.echo(f"❌ Error: {str(e)}")

dataset_directory = os.path.join(
find_project_root(), "datasets", org_name, dataset_name
)

# Check if dataset already exists
if os.path.exists(dataset_directory):
schema_path = os.path.join(dataset_directory, "schema.yaml")
if os.path.exists(schema_path):
click.echo(f"❌ Error: Dataset already exists at path: {path}")
return

# Get dataset metadata
name = click.prompt("📝 Enter dataset name", default=dataset_name)
description = click.prompt("📋 Enter dataset description", default="")

# Get source configuration
source_type = click.prompt(
"🔌 Enter source type",
type=click.Choice(["mysql", "postgres"]),
default="mysql",
)

table_name = click.prompt("📦 Enter table name")

# Build connection configuration
connection_config = {
"host": click.prompt("🌐 Enter host", default="localhost"),
"port": click.prompt("🔍 Enter port", type=int),
"database": click.prompt("💾 Enter database name"),
"user": click.prompt("👤 Enter username"),
"password": click.prompt("🔑 Enter password", hide_input=True),
}

# Create source configuration
source = {
"type": source_type,
"table": table_name,
"connection": SQLConnectionConfig(**connection_config),
}

# Create schema
schema = SemanticLayerSchema(
name=name, description=description, source=Source(**source)
)

# Create directory and save schema
os.makedirs(dataset_directory, exist_ok=True)
schema_path = os.path.join(dataset_directory, "schema.yaml")

with open(schema_path, "w") as yml_file:
yml_file.write(schema.to_yaml())

click.echo(f"\n✨ Dataset created successfully at: {dataset_directory}")


@cli.command()
@click.argument("api_key")
def login(api_key: str):
"""🔑 Authenticate with your PandaBI API key"""
if not validate_api_key(api_key):
click.echo(
"❌ Invalid API key format. Expected format: PAI-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
)
return

env_path = os.path.join(find_project_root(), ".env")
env_content = ""
new_line = f"PANDABI_API_KEY={api_key}\n"

# Read existing .env if it exists
if os.path.exists(env_path):
with open(env_path, "r") as f:
lines = f.readlines()
# Filter out existing PANDABI_API_KEY line if present
lines = [line for line in lines if not line.startswith("PANDABI_API_KEY=")]
env_content = "".join(lines)
if env_content and not env_content.endswith("\n"):
env_content += "\n"

# Write updated content
with open(env_path, "w") as f:
f.write(env_content + new_line)

click.echo("✅ Successfully authenticated with PandaBI!")


@cli.command()
@click.argument("dataset_path")
def pull(dataset_path):
"""📥 Pull a dataset from a remote source"""
try:
click.echo(f"🔄 Pulling dataset from: {dataset_path}")
dataset_loader = DatasetLoader()
df = dataset_loader.load(dataset_path)
df.pull()
click.echo(f"\n✨ Dataset successfully pulled from path: {dataset_path}")
except Exception as e:
click.echo(f"❌ Error pulling dataset: {str(e)}")


@cli.command()
@click.argument("dataset_path")
def push(dataset_path):
"""📤 Push a dataset to a remote source"""
try:
click.echo(f"🔄 Pushing dataset to: {dataset_path}")
dataset_loader = DatasetLoader()
df = dataset_loader.load(dataset_path)
df.push()
click.echo(f"\n✨ Dataset successfully pushed to path: {dataset_path}")
except Exception as e:
click.echo(f"❌ Error pushing dataset: {str(e)}")


if __name__ == "__main__":
cli()
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ mkdocs = "1.5.3"
mkdocstrings-python = "1.7.2"
markdown-include = "^0.6.0"

[tool.poetry.scripts]
pai = "pandasai.cli.main:cli"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Expand Down
Loading
Loading