diff --git a/.gitignore b/.gitignore index a9eeef1..7381fb3 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,6 @@ _build/ out/ *.sqlite !docs/source/commands/env + +# config +.hckrcfg diff --git a/README.md b/README.md index d403af9..7bca500 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ [![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-black.svg)](https://sonarcloud.io/summary/new_code?id=hckr-cli_hckr) [![Quality gate](https://sonarcloud.io/api/project_badges/quality_gate?project=hckr-cli_hckr)](https://sonarcloud.io/summary/new_code?id=hckr-cli_hckr) + [//]: # ([![GitHub commit activity](https://img.shields.io/github/commit-activity/m/hckr-cli/hckr)](https://github.com/hckr-cli/hckr/graphs/commit-activity)) ## Introduction diff --git a/dev/CONTRIBUTING.md b/dev/CONTRIBUTING.md index 1a0cf88..dbea0f4 100644 --- a/dev/CONTRIBUTING.md +++ b/dev/CONTRIBUTING.md @@ -35,3 +35,7 @@ pre-commit install ## Homebrew formulae Please find contributing guide for `Homebrew formulae` here [HOMEBREW.md](HOMEBREW.md) + + +## Senty integration - +[Sentry console](https://hckr-cli.sentry.io/projects/hckr/?project=4507910060572672) diff --git a/docs/source/commands/database/db.rst b/docs/source/commands/database/db.rst index 64e64e9..59d4ae5 100644 --- a/docs/source/commands/database/db.rst +++ b/docs/source/commands/database/db.rst @@ -1,5 +1,5 @@ .. important:: - Before using these commands you need to add your database configuration in config (``.hckrcfg`` file), please refer :ref:`Configuring your databases ` + Before using these commands you need to add your database configuration in config (``~/.hckrcfg`` file), please refer :ref:`Configuring your databases ` .. click:: hckr.cli.db:db diff --git a/docs/source/commands/database/index.rst b/docs/source/commands/database/index.rst index f2f6796..d32f6ac 100644 --- a/docs/source/commands/database/index.rst +++ b/docs/source/commands/database/index.rst @@ -3,7 +3,7 @@ Database query ``hckr`` supports various of database commands. .. important:: - Before using these commands you need to add your database configuration in config (``.hckrcfg`` file), please refer :ref:`Configuring your database ` + Before using these commands you need to add your database configuration in config (``~/.hckrcfg`` file), please refer :ref:`Configuring your database ` .. tip:: More commands will be added in future updates. Stay tuned! diff --git a/pyproject.toml b/pyproject.toml index 28d54f6..f48858a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "snowflake-sqlalchemy", # For Snowflake, # Error and debugging - "sentry-sdk" + "sentry-sdk", ] [project.urls] diff --git a/src/hckr/__about__.py b/src/hckr/__about__.py index 2547421..e329859 100644 --- a/src/hckr/__about__.py +++ b/src/hckr/__about__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: 2024-present Ashish Patel # # SPDX-License-Identifier: MIT -__version__ = "0.5.0.dev1" +__version__ = "0.5.0" diff --git a/src/hckr/cli/config.py b/src/hckr/cli/config.py index 1ae5615..f1821ff 100644 --- a/src/hckr/cli/config.py +++ b/src/hckr/cli/config.py @@ -3,11 +3,12 @@ from ..utils.MessageUtils import PError, PSuccess from ..utils.config.ConfigUtils import ( init_config, - DEFAULT_CONFIG, configMessage, list_config, set_config_value, get_config_value, + common_config_options, + config_file_path_option, ) @@ -23,21 +24,11 @@ def config(ctx): 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): +def set(config, config_path, key, value): """ This command adds a new entry to the config file with key and value @@ -59,14 +50,14 @@ def set(config, key, value): """ configMessage(config) - set_config_value(config, key, value) + set_config_value(config, config_path, key, value) PSuccess(f"[{config}] {key} <- {value}") @config.command() @common_config_options @click.argument("key") -def get(config, key): +def get(config, config_path, key): """ This command returns value for a key in a configuration @@ -91,25 +82,36 @@ def get(config, key): configMessage(config) try: - value = get_config_value(config, key) + value = get_config_value(config, config_path, key) PSuccess(f"[{config}] {key} = {value}") except ValueError as e: PError(f"{e}") +@config.command("list") +@config_file_path_option +def list_configs(config_path): + """ + This command show list all configurations and their key values + + **Example Usage**: + + * We can also see all configuration list command + + .. code-block:: shell + + $ hckr config list + + **Command Reference**: + """ + list_config(config_path, _all=True) + + @config.command() @common_config_options -@click.option( - "-a", - "--all", - default=False, - is_flag=True, - help="Whether to shows a list of all configs (default: False)", -) -def list(config, all): +def show(config, config_path): """ - This command show list of all keys available in given configuration, - we can also see values in all configurations by providing -a/--all flag + This command show list of all keys available in the given configuration, **Example Usage**: @@ -119,23 +121,20 @@ def list(config, all): .. code-block:: shell - $ hckr config list + $ hckr config show * Similarly, we can also get all values in a specific configuration using -c/--config flag .. code-block:: shell - $ hckr config list -c MY_DATABASE - - * Additionally, we can also see all configurations using -a/--all flag - - .. code-block:: shell - - $ hckr config list --all + $ hckr config show -c MY_DATABASE **Command Reference**: """ - list_config(config, all) + list_config( + config_path, + config, + ) @config.command() diff --git a/src/hckr/cli/configure.py b/src/hckr/cli/configure.py index 397d2aa..998c76c 100644 --- a/src/hckr/cli/configure.py +++ b/src/hckr/cli/configure.py @@ -4,6 +4,8 @@ from ..utils.config.ConfigUtils import ( list_config, set_config_value, + set_default_config, + config_file_path_option, ) from ..utils.config.ConfigureUtils import configure_host, configure_creds from ..utils.config.Constants import ( @@ -16,7 +18,7 @@ @click.group( - help="easy configurations for other commands (eg. db)", + help="Easy configurations for other commands (eg. db)", context_settings={"help_option_names": ["-h", "--help"]}, ) @click.pass_context @@ -27,6 +29,39 @@ def configure(ctx): pass +@configure.command() +@config_file_path_option +@click.argument("service", type=click.Choice([str(ConfigType.DATABASE)])) +@click.argument("config_name") +def set_default(service, config_name, config_path): + """Set the default configuration for a service configured via ``hckr configure`` + This command configures database credentials based on the selected database type. + + .. hint:: + We currently support ``database`` default configuration which corresponds to ``hckr configure``, + + **Example Usage**: + + * Setting up your default database configuration in [DEFAULT] configuration + + + .. code-block:: shell + + $ hckr configure set-default db MY_DB_CONFIG + + .. note:: + Please note that the ``MY_DB_CONFIG`` config must be configured before running this using ``hckr configure db`` command + + .. important:: This command will add an entry in ``[DEFAULT]`` configuration like ``database = MY_DB_CONFIG`` and + if you run any database command like ``hckr db query `` without providing configuration using + ``-c/--config`` flag this config will be used. + + **Command Reference**: + """ + + set_default_config(service, config_name, config_path) + + @configure.command("db") @click.option( "--config-name", @@ -54,8 +89,10 @@ def configure(ctx): @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") +@config_file_path_option def configure_db( config_name, + config_path, database_type, host, port, @@ -109,21 +146,29 @@ def configure_db( **Command Reference**: """ - set_config_value(config_name, CONFIG_TYPE, ConfigType.DATABASE) + set_config_value(config_name, config_path, CONFIG_TYPE, ConfigType.DATABASE) selected_db_type = db_type_mapping[database_type] - set_config_value(config_name, DB_TYPE, selected_db_type) + set_config_value(config_name, config_path, DB_TYPE, selected_db_type) - configure_creds(config_name, password, selected_db_type, user) + configure_creds(config_name, config_path, 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) + set_config_value(config_name, config_path, DB_NAME, database_name) configure_host( - account, config_name, host, port, role, schema, selected_db_type, warehouse + config_name, + config_path, + account, + host, + port, + role, + schema, + selected_db_type, + warehouse, ) PSuccess( f"Database configuration saved successfully in config instance '{config_name}'" ) - list_config(config_name) + list_config(config_path, config_name) diff --git a/src/hckr/cli/db.py b/src/hckr/cli/db.py index 6ecb482..7c5cfbb 100644 --- a/src/hckr/cli/db.py +++ b/src/hckr/cli/db.py @@ -1,12 +1,17 @@ +import warnings + import click -import pandas as pd -from sqlalchemy import create_engine, text -from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.exc import SAWarning +from yaspin import yaspin # type: ignore 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 +from hckr.utils.DbUtils import get_db_url, execute_query +from hckr.utils.MessageUtils import PError + +# Suppress the specific SQLAlchemy warning +warnings.filterwarnings("ignore", category=SAWarning, message=".*flatten.*") + +# Your SQLAlchemy code @click.group( @@ -35,7 +40,7 @@ def db(): required=False, ) @click.pass_context -def query(ctx, config, query, num_rows=None, num_cols=None): +def query(ctx, config, config_path, query, num_rows=None, num_cols=None): """ This command executes a SQL query on your configured database and show you result in a table format ( in ``SELECT/SHOW/DESC`` queries ) @@ -71,42 +76,9 @@ def query(ctx, config, query, num_rows=None, num_cols=None): **Command Reference**: """ - db_url = get_db_url(section=config) + db_url = get_db_url(section=config, config_path=config_path) if not db_url: PError("Database credentials are not properly configured.") - - query = query.strip() - 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}") + with yaspin(text="Running query...", color="green", timer=True) as spinner: + execute_query(db_url, query, num_rows, num_cols) + spinner.ok("✔") diff --git a/src/hckr/utils/CliUtils.py b/src/hckr/utils/CliUtils.py index ba4ccd2..148e037 100644 --- a/src/hckr/utils/CliUtils.py +++ b/src/hckr/utils/CliUtils.py @@ -26,7 +26,7 @@ class Info: def __init__(self): # Note: This object must have an empty constructor. """Create a new instance.""" - self.verbose: int = 0 + self.verbose = 0 def check_latest_version(): diff --git a/src/hckr/utils/DbUtils.py b/src/hckr/utils/DbUtils.py index 13e7249..fb8cc53 100644 --- a/src/hckr/utils/DbUtils.py +++ b/src/hckr/utils/DbUtils.py @@ -1,8 +1,17 @@ import logging from configparser import NoOptionError -from hckr.utils.MessageUtils import PError +import click +import pandas as pd +from sqlalchemy import create_engine +from sqlalchemy import text +from sqlalchemy.exc import SQLAlchemyError + +from hckr.utils import MessageUtils +from hckr.utils.DataUtils import print_df_as_table +from hckr.utils.MessageUtils import PError, PInfo from hckr.utils.config import ConfigUtils +from hckr.utils.config.ConfigUtils import list_config, load_config from hckr.utils.config.Constants import ( ConfigType, DBType, @@ -17,25 +26,56 @@ DB_WAREHOUSE, DB_ROLE, DB_SCHEMA, + DEFAULT_CONFIG, ) -def get_db_url(section): +def get_db_url(section, config_path): """ This function retrieves a database URL based on the given configuration section. :param section: The name of the configuration section to retrieve the database URL from. :return: The database URL. """ - config = ConfigUtils.load_config() + # Load the default config file to get the default section if none is provided + if section == DEFAULT_CONFIG: + MessageUtils.info( + "No config is provided, trying to fetch [yellow]default database config[/yellow]" + " from [magenta]\[DEFAULT][/magenta] config" + ) + config = load_config(config_path) + if not config.has_option(DEFAULT_CONFIG, str(ConfigType.DATABASE)): + list_config(config_path, DEFAULT_CONFIG) + PError( + f"No configuration provided, and no default is set inside [yellow]{DEFAULT_CONFIG}[/yellow] config" + "\nPlease provide a configuration using [yellow]-c/--config[/yellow] " + "or configure a default using [magenta]hckr configure set-default db" + ) + else: + MessageUtils.info( + f"No configuration section provided, Using default set [yellow]{section}" + ) + section = ConfigUtils.get_config_value( + DEFAULT_CONFIG, config_path, ConfigType.DATABASE + ) + MessageUtils.info( + f"Default database config [magenta]{section}[/magenta] inferred from" + " [yellow]\[DEFAULT][/yellow] config" + ) + + config = ConfigUtils.load_config(config_path) + if not config.has_section(section): + PError( + f"Config [yellow]{section}[/yellow] is not present in the config file [magenta]{config_path}" + ) + try: config_type = config.get(section, CONFIG_TYPE) if config_type != ConfigType.DATABASE: PError( - f"The configuration {section} is not database type\n" + f"The configuration [yellow]{section}[/yellow] is not database type\n" " Please use [magenta]hckr configure db[/magenta] to configure database." ) db_type = config.get(section, DB_TYPE) - if db_type == DBType.SQLite: database_name = config.get(section, DB_NAME) return f"sqlite:///{database_name}" @@ -77,3 +117,42 @@ def _get_snowflake_url(config, section): f"snowflake://{user}:{password}@{account}/{database_name}/{schema}" f"?warehouse={warehouse}&role={role}" ) + + +def execute_query(db_url, query, num_rows, num_cols): + try: + query = query.strip() + engine = create_engine(db_url) + 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: + click.echo("\n") + PError(f"Error executing query: \n[red]{e}") diff --git a/src/hckr/utils/config/ConfigUtils.py b/src/hckr/utils/config/ConfigUtils.py index 58c12ab..a10a42b 100644 --- a/src/hckr/utils/config/ConfigUtils.py +++ b/src/hckr/utils/config/ConfigUtils.py @@ -1,19 +1,39 @@ import configparser import logging +from pathlib import Path -import rich -from rich.panel import Panel +import click -from .Constants import config_path, DEFAULT_CONFIG +from .Constants import DEFAULT_CONFIG, DEFAULT_CONFIG_PATH from .. import MessageUtils from ..MessageUtils import PWarn, PSuccess, PInfo, PError -from ...__about__ import __version__ -def load_config(): +def config_file_path_option(func): + func = click.option( + "-f", + "--config-path", + help=f"Config file path, default: ``~/.hckrcfg``", + default=DEFAULT_CONFIG_PATH, + )(func) + return func + + +def common_config_options(func): + func = click.option( + "-c", + "--config", + help=f"Config instance, default: ``{DEFAULT_CONFIG}``", + default=DEFAULT_CONFIG, + )(func) + return config_file_path_option(func) + + +def load_config(config_path: str): """Load the INI configuration file.""" + logging.debug(f"Loading config from {config_path}") config = configparser.ConfigParser() - if not config_exists(): + if not config_exists(config_path): PError( f"Config file [magenta]{config_path}[/magenta] doesn't exists or empty," f" Please run init command to create one \n " @@ -23,32 +43,32 @@ def load_config(): return config -def config_exists() -> bool: +def config_exists(config_path) -> bool: """ Check if config file exists and is not empty. """ - if not config_path.exists(): + _config_path = Path(config_path) + if not _config_path.exists(): return False - if config_path.stat().st_size == 0: + if _config_path.stat().st_size == 0: return False - with config_path.open("r") as file: + with _config_path.open("r") as file: content = file.read().strip() if not content: return False return True -def init_config(overwrite): +def init_config(config_path, overwrite): if not config_path.exists(): config_path.parent.mkdir(parents=True, exist_ok=True) config_path.touch(exist_ok=True) default_config = { DEFAULT_CONFIG: { - "version": f"{__version__}", "config_type": "default", }, "CUSTOM": { - "key": f"value", + "key": "value", }, } config = configparser.ConfigParser() @@ -62,7 +82,7 @@ def init_config(overwrite): "[yellow]Deleting existing file" ) config_path.unlink() - init_config(overwrite=False) + init_config(config_path, overwrite=False) else: PWarn( f"Config file already exists at [yellow]{config_path}[/yellow]," @@ -70,74 +90,80 @@ def init_config(overwrite): ) -def set_config_value(section, key, value, override=False): +def set_config_value(section, config_path, key, value): """ Sets a configuration value in a configuration file. """ logging.debug(f"Setting [{section}] {key} = {value}") - config = load_config() + config = load_config(config_path) if not config.has_section(section) and section != DEFAULT_CONFIG: - PInfo(f"Config \[{section}] doesn't exist, Adding") + PInfo(f"Config [yellow]\[{section}][/yellow] doesn't exist, Adding a new one") config.add_section(section) - config.set(section, key, value) - with config_path.open("w") as config_file: + with Path(config_path).open("w") as config_file: config.write(config_file) -def get_config_value(section, key) -> str: +def set_default_config(service, config_name, config_path): + config = load_config(config_path=config_path) + if config.has_section(config_name): + if get_config_value(config_name, config_path, "config_type") == service: + set_config_value(DEFAULT_CONFIG, config_path, service, config_name) + PSuccess( + f"[{DEFAULT_CONFIG}] [yellow]{service} = {config_name}", + title="[green]Default config for " + f"[magenta]{service}[/magenta] configured", + ) + else: + PError( + f"Config [red]\[{config_name}][/red] is not of config type [yellow]{service}" + ) + else: + PError(f"Config [red]\[{config_name}][/red] doesn't exists.") + + +def get_config_value(section, config_path, key) -> str: logging.debug(f"Getting [{section}] {key} ") - config = load_config() + config = load_config(config_path) if section != DEFAULT_CONFIG and not config.has_section(section): - raise ValueError(f"Section '{section}' not found in the configuration.") + PError(f"config '{section}' not found in the configuration.") + # raise ValueError(f"Section '{section}' not found in the configuration.") if not config.has_option(section, key): - raise ValueError(f"Key '{key}' not found in section '{section}'.") + PError(f"Key '{key}' not found in config '{section}'.") + # raise ValueError(f"Key '{key}' not found in section '{section}'.") return config.get(section, key) def _list_config_util(config, section): if section == DEFAULT_CONFIG: - rich.print( - Panel( - ( - "\n".join( - [f"{key} = {value}" for key, value in config.items("DEFAULT")] - ) - if config.items("DEFAULT") - else "NOTHING FOUND" - ), - expand=True, - title="\[DEFAULT]", - ) + PSuccess( + ( + "\n".join( + [f"{key} = {value}" for key, value in config.items("DEFAULT")] + ) + if config.items(section) + else "NOTHING FOUND" + ), + title=f"[green]\[DEFAULT]", ) elif config.has_section(section): - # TODO: replace these with PSuccess() - rich.print( - Panel( - ( - "\n".join( - [f"{key} = {value}" for key, value in config.items(section)] - ) - if config.items(section) - else "NOTHING FOUND" - ), - expand=True, - title=f"\[{section}]", - ) + PSuccess( + ( + "\n".join([f"{key} = {value}" for key, value in config.items(section)]) + if config.items(section) + else "NOTHING FOUND" + ), + title=f"[green]\[{section}]", ) else: - rich.print( - Panel( - f"config {section} not found", - expand=True, - title="Error", - ) + PError( + f"Config [yellow]\[{section}][/yellow] not found\nAvailable configs: [yellow]{config.sections()}" ) -def list_config(section, all=False): - config = load_config() - if all: +def list_config(config_path, section=DEFAULT_CONFIG, _all=False): + config = load_config(config_path) + if _all: MessageUtils.info("Listing all config") _list_config_util(config, DEFAULT_CONFIG) for section in config.sections(): diff --git a/src/hckr/utils/config/ConfigureUtils.py b/src/hckr/utils/config/ConfigureUtils.py index 8ba7ef7..47135d3 100644 --- a/src/hckr/utils/config/ConfigureUtils.py +++ b/src/hckr/utils/config/ConfigureUtils.py @@ -15,7 +15,15 @@ def configure_host( - account, config_name, host, port, role, schema, selected_db_type, warehouse + config_name, + config_path, + account, + host, + port, + role, + schema, + selected_db_type, + warehouse, ): if selected_db_type in [DBType.PostgreSQL, DBType.MySQL]: if not host: @@ -23,8 +31,8 @@ def configure_host( if not port: port = click.prompt("Enter the database port (e.g., 5432 for PostgreSQL)") - set_config_value(config_name, DB_HOST, host) - set_config_value(config_name, DB_PORT, port) + set_config_value(config_name, config_path, DB_HOST, host) + set_config_value(config_name, config_path, DB_PORT, port) elif selected_db_type == DBType.Snowflake: if not account: @@ -35,13 +43,13 @@ def configure_host( warehouse = click.prompt("Enter the Snowflake warehouse name") if not role: role = click.prompt("Enter the Snowflake role") - set_config_value(config_name, DB_ACCOUNT, account) - set_config_value(config_name, DB_WAREHOUSE, warehouse) - set_config_value(config_name, DB_SCHEMA, schema) - set_config_value(config_name, DB_ROLE, role) + set_config_value(config_name, config_path, DB_ACCOUNT, account) + set_config_value(config_name, config_path, DB_WAREHOUSE, warehouse) + set_config_value(config_name, config_path, DB_SCHEMA, schema) + set_config_value(config_name, config_path, DB_ROLE, role) -def configure_creds(config_name, password, selected_db_type, user): +def configure_creds(config_name, config_path, password, selected_db_type, user): if selected_db_type != DBType.SQLite: if not user: user = click.prompt("Enter the database user (e.g., root)") @@ -52,5 +60,5 @@ def configure_creds(config_name, password, selected_db_type, user): confirmation_prompt=True, ) # common values - set_config_value(config_name, DB_USER, user) - set_config_value(config_name, DB_PASSWORD, password) + set_config_value(config_name, config_path, DB_USER, user) + set_config_value(config_name, config_path, DB_PASSWORD, password) diff --git a/src/hckr/utils/config/Constants.py b/src/hckr/utils/config/Constants.py index 0ad59c5..33ef6a7 100644 --- a/src/hckr/utils/config/Constants.py +++ b/src/hckr/utils/config/Constants.py @@ -1,7 +1,7 @@ from enum import Enum from pathlib import Path -config_path = Path.home() / ".hckrcfg" +DEFAULT_CONFIG_PATH = Path.home() / ".hckrcfg" DEFAULT_CONFIG = "DEFAULT" diff --git a/tests/cli/resources/db/.gitkeep b/tests/cli/resources/db/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 6db4928..7fb83a6 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -19,7 +19,7 @@ def test_hckr(): Commands: config Config commands - configure easy configurations for other commands (eg. + configure Easy configurations for other commands (eg. cron cron commands crypto crypto commands data data related commands diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py index 4dd3419..8523b1d 100644 --- a/tests/cli/test_config.py +++ b/tests/cli/test_config.py @@ -1,9 +1,8 @@ import random import string -from click.testing import CliRunner - -from hckr.cli.config import set, get, list +from hckr.cli.config import set, get, list_configs, show +from tests.testUtils import _get_args_with_config_path def _get_random_string(length): @@ -16,12 +15,12 @@ def _get_random_string(length): def test_config_get_set_default(cli_runner): _key = f"key_{_get_random_string(5)}" _value = f"value_{_get_random_string(5)}" - result = cli_runner.invoke(set, [_key, _value]) + result = cli_runner.invoke(set, _get_args_with_config_path([_key, _value])) assert result.exit_code == 0 assert f"[DEFAULT] {_key} <- {_value}" in result.output # testing get - result = cli_runner.invoke(get, [_key]) + result = cli_runner.invoke(get, _get_args_with_config_path([_key])) assert result.exit_code == 0 assert f"[DEFAULT] {_key} = {_value}" in result.output @@ -30,43 +29,49 @@ def test_config_get_set_custom_config(cli_runner): _key = f"key_{_get_random_string(5)}" _value = f"value_{_get_random_string(5)}" _CONFIG = "CUSTOM" - result = cli_runner.invoke(set, ["--config", _CONFIG, _key, _value]) + result = cli_runner.invoke( + set, _get_args_with_config_path(["--config", _CONFIG, _key, _value]) + ) assert result.exit_code == 0 assert f"[{_CONFIG}] {_key} <- {_value}" in result.output # testing get - result = cli_runner.invoke(get, ["--config", _CONFIG, _key]) + result = cli_runner.invoke( + get, _get_args_with_config_path(["--config", _CONFIG, _key]) + ) assert result.exit_code == 0 assert f"[{_CONFIG}] {_key} = {_value}" in result.output -def test_config_list(cli_runner): - _CONFIG = "CUSTOM" - result = cli_runner.invoke(list) +def test_config_show(cli_runner): + result = cli_runner.invoke(show, _get_args_with_config_path([])) assert result.exit_code == 0 assert "[DEFAULT]" in result.output - result = cli_runner.invoke(list, ["--all"]) + +def test_config_show_custom(cli_runner): + _CONFIG = "CUSTOM" + result = cli_runner.invoke(show, _get_args_with_config_path(["--config", _CONFIG])) + assert result.exit_code == 0 + assert "[CUSTOM]" in result.output + + +def test_config_list(cli_runner): + result = cli_runner.invoke(list_configs, _get_args_with_config_path([])) assert "[DEFAULT]" in result.output assert "[CUSTOM]" in result.output # NEGATIVE USE CASES def test_config_get_set_missing_key(cli_runner): - result = cli_runner.invoke(set, []) - print(result.output) - assert result.exit_code != 0 - assert "Error: Missing argument 'KEY'" in result.output - - runner = CliRunner() - result = runner.invoke(get, []) + result = cli_runner.invoke(set, _get_args_with_config_path([])) print(result.output) assert result.exit_code != 0 assert "Error: Missing argument 'KEY'" in result.output def test_config_set_missing_value(cli_runner): - result = cli_runner.invoke(set, ["key"]) + result = cli_runner.invoke(set, _get_args_with_config_path(["key"])) print(result.output) assert result.exit_code != 0 assert "Error: Missing argument 'VALUE'" in result.output diff --git a/tests/cli/test_configure.py b/tests/cli/test_configure.py index 0d28bbc..89f525a 100644 --- a/tests/cli/test_configure.py +++ b/tests/cli/test_configure.py @@ -1,5 +1,7 @@ -from hckr.cli.configure import configure_db -from hckr.utils.MessageUtils import _PMsg, PInfo +from hckr.cli.configure import configure_db, set_default +from hckr.cli.db import query +from hckr.utils.config.ConfigUtils import list_config +from tests.testUtils import _get_args_with_config_path, TEST_HCKRCFG_FILE def test_configure_postgres(cli_runner, postgres_options): @@ -34,6 +36,42 @@ def test_configure_sqlite(cli_runner, sqlite_options): assert "[testdb_sqlite]" in result.output -def test_test(): - hi = "ashish" - PInfo(f"hello {hi}") +def test_configure_set_default_db(cli_runner, sqlite_options): + # first we have to configure a sqlite database + result = cli_runner.invoke(configure_db, sqlite_options) + assert result.exit_code == 0 + assert "Database configuration saved successfully" in result.output + assert "[testdb_sqlite]" in result.output + + result = cli_runner.invoke( + set_default, _get_args_with_config_path(["database", "testdb_sqlite"]) + ) + print(result.output) + assert result.exit_code == 0 + assert "Default config for database configured" in result.output + + # query with default config + result = cli_runner.invoke(query, _get_args_with_config_path(["select 1"])) + print(result.output) + assert result.exit_code == 0 + assert ( + "Default database config testdb_sqlite inferred from [DEFAULT] config" + in result.output + ) + + +def test_configure_set_default_missing_arg(cli_runner): + result = cli_runner.invoke(set_default, _get_args_with_config_path([])) + print(result.output) + assert result.exit_code != 0 + assert "Error: Missing argument '{database}'" in result.output + + +def test_configure_set_default_invalid_arg(cli_runner): + result = cli_runner.invoke(set_default, _get_args_with_config_path(["invalid"])) + print(result.output) + assert result.exit_code != 0 + assert ( + "Error: Invalid value for '{database}': 'invalid' is not 'database'." + in result.output + ) diff --git a/tests/cli/test_db.py b/tests/cli/test_db.py index 1d845d8..881c222 100644 --- a/tests/cli/test_db.py +++ b/tests/cli/test_db.py @@ -2,10 +2,13 @@ from hckr.cli.db import query from hckr.cli.configure import configure_db +from tests.testUtils import _get_args_with_config_path def _run_query_and_assert(cli_runner, sql_query, value_assert=None): - result = cli_runner.invoke(query, [sql_query, "-c", "testdb_sqlite"]) + result = cli_runner.invoke( + query, _get_args_with_config_path([sql_query, "-c", "testdb_sqlite"]) + ) print(result.output) if value_assert: assert value_assert in result.output @@ -42,17 +45,30 @@ def test_db_query_sqlite(cli_runner, sqlite_options): # NEGATIVE USE CASES -def test_db_query_missing_or_invalid_config(): +def test_db_query_missing_config(): runner = CliRunner() - result = runner.invoke(query, ["select 1"]) + result = runner.invoke( + query, _get_args_with_config_path(["select 1", "-c", "INVALID"]) + ) print(result.output) assert result.exit_code != 0 - assert "The configuration DEFAULT is not database type" in result.output + assert "Config INVALID is not present in the config file" in result.output + + +def test_db_query_invalid_config(): + runner = CliRunner() + result = runner.invoke( + query, _get_args_with_config_path(["select 1", "-c", "CUSTOM"]) + ) + # print(result.output) + print(result.stdout) + assert result.exit_code != 0 + assert "The configuration CUSTOM is not database type" in result.output def test_db_query_missing_query(): runner = CliRunner() - result = runner.invoke(query) + result = runner.invoke(query, _get_args_with_config_path([])) print(result.output) assert result.exit_code != 0 assert "Error: Missing argument 'QUERY'" in result.output diff --git a/tests/conftest.py b/tests/conftest.py index b3abe46..df2e3a9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,9 +7,7 @@ from click.testing import CliRunner from hckr.utils.config.ConfigUtils import init_config - -current_directory = Path(__file__).parent -SQLITE_DB = current_directory / "cli" / "resources" / "db" / "test_db.sqlite" +from .testUtils import SQLITE_DB, TEST_HCKRCFG_FILE, _get_args_with_config_path @pytest.fixture(scope="package") @@ -19,81 +17,89 @@ def local_sqlite_db(): @pytest.fixture(scope="package") def cli_runner(): - init_config(overwrite=True) # recreate the config file + init_config(TEST_HCKRCFG_FILE, overwrite=True) # recreate the config file return CliRunner() @pytest.fixture(scope="package") def postgres_options(): - return [ - "--config-name", - "testdb_postgres", - "--database-type", - "1", - "--user", - "user", - "--password", - "password", - "--host", - "host.postgres", - "--port", - "11143", - "--database-name", - "defaultdb", - ] + return _get_args_with_config_path( + [ + "--config-name", + "testdb_postgres", + "--database-type", + "1", + "--user", + "user", + "--password", + "password", + "--host", + "host.postgres", + "--port", + "11143", + "--database-name", + "defaultdb", + ] + ) @pytest.fixture(scope="package") def mysql_options(): - return [ - "--config-name", - "testdb_mysql", - "--database-type", - "2", - "--user", - "user", - "--password", - "password", - "--host", - "host", - "--port", - "123", - "--database-name", - "defaultdb", - ] + return _get_args_with_config_path( + [ + "--config-name", + "testdb_mysql", + "--database-type", + "2", + "--user", + "user", + "--password", + "password", + "--host", + "host", + "--port", + "123", + "--database-name", + "defaultdb", + ] + ) @pytest.fixture(scope="package") def snowflake_options(): - return [ - "--config-name", - "testdb_snowflake", - "--database-type", - "4", - "--user", - "user", - "--password", - "password", - "--database-name", - "database", - "--schema", - "PUBLIC", - "--account", - "account-id", - "--warehouse", - "COMPUTE_WH", - "--role", - "ACCOUNTADMIN", - ] + return _get_args_with_config_path( + [ + "--config-name", + "testdb_snowflake", + "--database-type", + "4", + "--user", + "user", + "--password", + "password", + "--database-name", + "database", + "--schema", + "PUBLIC", + "--account", + "account-id", + "--warehouse", + "COMPUTE_WH", + "--role", + "ACCOUNTADMIN", + ] + ) @pytest.fixture(scope="package") def sqlite_options(): - return [ - "--config-name", - "testdb_sqlite", - "--database-type", - "3", - "--database-name", - f"{SQLITE_DB}", - ] + return _get_args_with_config_path( + [ + "--config-name", + "testdb_sqlite", + "--database-type", + "3", + "--database-name", + f"{SQLITE_DB}", + ] + ) diff --git a/tests/testUtils.py b/tests/testUtils.py new file mode 100644 index 0000000..1385c14 --- /dev/null +++ b/tests/testUtils.py @@ -0,0 +1,10 @@ +from pathlib import Path + +current_directory = Path(__file__).parent +SQLITE_DB = current_directory / "cli" / "resources" / "db" / "test_db.sqlite" +TEST_HCKRCFG_FILE = current_directory / "cli" / "resources" / "config" / ".hckrcfg" + + +def _get_args_with_config_path(args): + args.extend(["--config-path", str(TEST_HCKRCFG_FILE)]) + return args