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

Adding Config and db query Support #71

Merged
merged 22 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a058e72
Add YAML config file support
pateash Aug 22, 2024
bd5c589
Add configuration command group with set and get functionalities
pateash Aug 23, 2024
9c429b6
Refactor configuration management and add DB config command
pateash Aug 23, 2024
3d13dd2
Add database utilities and commands
pateash Aug 24, 2024
f3a8fd8
Add config command group and enhance database configuration
pateash Aug 24, 2024
6147396
Update DB config handling and fix import errors
pateash Aug 24, 2024
af46876
Add initialization command for config and refactor tests
pateash Aug 24, 2024
ce69287
Add initialization command for config and refactor tests
pateash Aug 24, 2024
068dc34
Refactor database configuration, enhance tests, add logging
pateash Aug 24, 2024
bbf4db8
Refactor query command to return and display DataFrame
pateash Aug 24, 2024
45e2397
Enhance table printing in `print_df_as_table`
pateash Aug 25, 2024
406f0f7
Add options for number of rows and columns to query command.
pateash Aug 25, 2024
bd31853
Refactor message logging and remove redundant tests
pateash Aug 27, 2024
8d4ec34
Update src/hckr/cli/config.py
pateash Aug 29, 2024
f765031
Remove error exits and improve config CLI documentation
pateash Aug 29, 2024
f45a99e
Merge branch 'hckr-23' of github.com:hckr-cli/hckr into hckr-23
pateash Aug 29, 2024
55ac924
Refactor query handling and remove unused imports
pateash Aug 29, 2024
0beaf9f
Remove redundant import statement from config.py
pateash Aug 29, 2024
470aead
Add comprehensive configuration documentation and examples
pateash Aug 29, 2024
bf8de96
Add database command documentation and examples
pateash Aug 29, 2024
62eab53
Refactor CLI tests to use cli_runner fixture
pateash Aug 29, 2024
7513969
Update sonar exclusions to ignore specific Python files
pateash Aug 29, 2024
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,4 @@ output/
!output/.gitkeep
_build/
out/
*.sqlite
12 changes: 9 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,17 @@ dependencies = [
"pyarrow", # For saving data in Parquet format.
"fastavro", # For handling Avro file format.
"requests",
# "tomli",
"packaging",
"kubernetes",
"kubernetes", # for k8s commands
"yaspin",
"speedtest-cli"
"speedtest-cli", # for net speed command

# SQL Support
"sqlalchemy", # for SQL ORM
"psycopg2-binary", # for Postgres
"pymysql", # for MySQL
# "sqlite", # for SQLITE
"snowflake-sqlalchemy", # For Snowflake
]

[project.urls]
Expand Down
2 changes: 1 addition & 1 deletion src/hckr/__about__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2024-present Ashish Patel <ashishpatel0720@gmail.com>
#
# SPDX-License-Identifier: MIT
__version__ = "0.3.3.dev0"
__version__ = "0.4.0.dev0"
13 changes: 12 additions & 1 deletion src/hckr/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
import click
from click_repl import register_repl # type: ignore

from hckr.cli.db import db
from hckr.cli.configure import configure
from hckr.cli.config import config
from hckr.cli.k8s.context import context
from hckr.cli.k8s.namespace import namespace
from hckr.cli.k8s.pod import pod
from .net import net
from .crypto.fernet import fernet
from .data import data
from .info import info
from .k8s import k8s
from .net import net
from ..__about__ import __version__
from ..cli.cron import cron
from ..cli.crypto import crypto
Expand Down Expand Up @@ -103,6 +106,14 @@ def cli(
# NETWORK command
cli.add_command(net)

# config
cli.add_command(config)
cli.add_command(configure)

# database
cli.add_command(db)


# implementing this so that if the user just uses `hckr` we show them something
if __name__ == "__main__":
cli()
102 changes: 102 additions & 0 deletions src/hckr/cli/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# from ..utils.MessageUtils import *

import click
import rich
from cron_descriptor import get_description # type: ignore
pateash marked this conversation as resolved.
Show resolved Hide resolved

from ..utils.MessageUtils import PError, PSuccess
from ..utils.config.ConfigUtils import (
init_config,
DEFAULT_CONFIG,
configMessage,
list_config,
set_config_value,
get_config_value,
)


@click.group(
help="Config commands",
context_settings={"help_option_names": ["-h", "--help"]},
)
@click.pass_context
def config(ctx):
"""
Defines a command group for managing application configurations. This group includes commands to set, get, show, and initialize configuration values.
"""
pass


def common_config_options(func):
func = click.option(
"-c",
"--config",
help="Config instance, default: DEFAULT",
default=DEFAULT_CONFIG,
)(func)
return func


@config.command()
@common_config_options
@click.argument("key")
@click.argument("value")
def set(config, key, value):
"""
Sets a configuration value.

Args:
config (str): The configuration instance name. Default is defined by DEFAULT_CONFIG.
key (str): The key of the config setting to change.
value (str): The value to set for the specified key.

Example:
$ cli_tool configure set database_host 127.0.0.1
"""
configMessage(config)
set_config_value(config, key, value)
PSuccess(f"[{config}] {key} <- {value}")

pateash marked this conversation as resolved.
Show resolved Hide resolved

@config.command()
@common_config_options
@click.argument("key")
def get(config, key):
"""Get a configuration value."""
configMessage(config)
try:
value = get_config_value(config, key)
PSuccess(f"[{config}] {key} = {value}")
except ValueError as e:
PError(f"{e}")

pateash marked this conversation as resolved.
Show resolved Hide resolved

@config.command()
@common_config_options
@click.option(
"-a",
"--all",
default=False,
is_flag=True,
help="Whether to show all configs (default: False)",
)
def show(config, all):
"""List configuration values."""
list_config(config, all)


@config.command()
@click.option(
"-o",
"--overwrite",
default=False,
is_flag=True,
help="Whether to delete and recreate .hckrcfg file (default: False)",
)
def init(overwrite):
"""
Initializes the configuration for the application.

:return: None
"""
init_config(overwrite)
98 changes: 98 additions & 0 deletions src/hckr/cli/configure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import click

from ..utils.config.ConfigUtils import (
list_config,
set_config_value,
DBType,
)
pateash marked this conversation as resolved.
Show resolved Hide resolved
from ..utils.MessageUtils import PSuccess
from ..utils.config.ConfigureUtils import configure_host, configure_creds
from ..utils.config.Constants import (
CONFIG_TYPE,
DB_TYPE,
ConfigType,
db_type_mapping,
DB_HOST,
DB_PORT,
DB_ACCOUNT,
DB_WAREHOUSE,
DB_SCHEMA,
DB_ROLE,
DB_PASSWORD,
DB_NAME,
DB_USER,
)


@click.group(
help="easy configurations for other commands (eg. db)",
context_settings={"help_option_names": ["-h", "--help"]},
)
@click.pass_context
def configure(ctx):
"""
Defines a command group for configuration-related commands.
"""
pass


@configure.command("db")
@click.option(
"--config-name",
prompt="Enter a name for this database configuration",
help="Name of the config instance",
)
@click.option(
"--database-type",
prompt="Select the type of database (1=PostgreSQL, 2=MySQL, 3=SQLite, 4=Snowflake)",
type=click.Choice(["1", "2", "3", "4"]),
help="Database type",
)
@click.option("--host", prompt=False, help="Database host")
@click.option("--port", prompt=False, help="Database port")
@click.option("--user", prompt=False, help="Database user")
@click.option(
"--password",
prompt=False, # we will get this value later if it is not provided
hide_input=True,
confirmation_prompt=True,
help="Database password",
)
@click.option("--database-name", prompt=False, help="Database name")
@click.option("--schema", prompt=False, help="Database schema")
@click.option("--account", prompt=False, help="Snowflake Account Id")
@click.option("--warehouse", prompt=False, help="Snowflake warehouse")
@click.option("--role", prompt=False, help="Snowflake role")
def configure_db(
config_name,
database_type,
host,
port,
user,
password,
database_name,
schema,
account,
warehouse,
role,
):
"""Configure database credentials based on the selected database type."""

set_config_value(config_name, CONFIG_TYPE, ConfigType.DATABASE)
selected_db_type = db_type_mapping[database_type]
set_config_value(config_name, DB_TYPE, selected_db_type)

configure_creds(config_name, password, selected_db_type, user)

if not database_name:
database_name = click.prompt("Enter the database name")
set_config_value(config_name, DB_NAME, database_name)

configure_host(
account, config_name, host, port, role, schema, selected_db_type, warehouse
)

PSuccess(
f"Database configuration saved successfully in config instance '{config_name}'"
)
list_config(config_name)
pateash marked this conversation as resolved.
Show resolved Hide resolved
79 changes: 79 additions & 0 deletions src/hckr/cli/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import click
import pandas as pd
from sqlalchemy import create_engine, text
from sqlalchemy.exc import SQLAlchemyError
from yaspin import yaspin # type: ignore
pateash marked this conversation as resolved.
Show resolved Hide resolved

from hckr.cli.config import common_config_options
from hckr.utils.DataUtils import print_df_as_table
from hckr.utils.DbUtils import get_db_url
from hckr.utils.MessageUtils import PError, PInfo, PSuccess


@click.group(
help="Database commands",
context_settings={"help_option_names": ["-h", "--help"]},
)
def db():
pass
pateash marked this conversation as resolved.
Show resolved Hide resolved


@db.command()
@common_config_options
@click.argument("query")
@click.option(
"-nr",
"--num-rows",
default=10,
help="Number of rows to show.",
required=False,
)
@click.option(
"-nc",
"--num-cols",
default=10,
help="Number of cols to show.",
required=False,
)
@click.pass_context
def query(ctx, config, query, num_rows=None, num_cols=None):
"""Execute a SQL query on Snowflake and return a DataFrame or handle non-data-returning queries."""
db_url = get_db_url(section=config)
query = query.strip()
if db_url:
engine = create_engine(db_url)
try:
with engine.connect() as connection:
# Normalize and determine the type of query
normalized_query = query.lower()
is_data_returning_query = normalized_query.startswith(
("select", "desc", "describe", "show", "explain")
)
is_ddl_query = normalized_query.startswith(
("create", "alter", "drop", "truncate")
)

if is_data_returning_query:
# Execute and fetch results for queries that return data
df = pd.read_sql_query(text(query), connection)

# Optionally limit rows and columns if specified
if num_rows is not None:
df = df.head(num_rows)
if num_cols is not None:
df = df.iloc[:, :num_cols]

print_df_as_table(df, title=query)
return df
else:
# Execute DDL or non-data-returning DML queries
with connection.begin(): # this will automatically commit at the end
result = connection.execute(text(query))
if is_ddl_query:
PInfo(query, "Success")
else:
PInfo(query, f"[Success] Rows affected: {result.rowcount}")
except SQLAlchemyError as e:
PError(f"Error executing query: {e}")
else:
PError("Database credentials are not properly configured.")
pateash marked this conversation as resolved.
Show resolved Hide resolved
9 changes: 4 additions & 5 deletions src/hckr/utils/DataUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ def safe_faker_method(faker_instance, method_name, *args):
raise ValueError(f"No such Faker method: {method_name}")


def print_df_as_table(df, title="Data Sample", count=3):
MAX_COLS_TO_SHOW = 10
def print_df_as_table(df, title="Data Sample", count=3, col_count=10):
ROWS_TO_SHOW = min(count, df.shape[0])
COLS_TO_SHOW = min(MAX_COLS_TO_SHOW, df.shape[1])
COLS_TO_SHOW = min(col_count, df.shape[1])
table = Table(
show_header=True,
title=title,
Expand All @@ -36,14 +35,14 @@ def print_df_as_table(df, title="Data Sample", count=3):
for column in df.columns[:COLS_TO_SHOW]:
table.add_column(column, no_wrap=False, overflow="fold")
msg = f"Data has total {colored(df.shape[0], 'yellow')} rows and {colored(df.shape[1], 'yellow')} columns, showing first {colored(ROWS_TO_SHOW, 'yellow')} rows"
if len(df.columns) > MAX_COLS_TO_SHOW:
if len(df.columns) > col_count:
warning(f"{msg} and {colored(COLS_TO_SHOW, 'yellow')} columns")
else:
info(msg)
# Add rows to the table
for index, row in df.head(count).iterrows():
# Convert each row to string format, necessary to handle different data types
table.add_row(*[str(item) for item in row.values[:MAX_COLS_TO_SHOW]])
table.add_row(*[str(item) for item in row.values[:col_count]])
rich.print(table)


Expand Down
Loading
Loading