Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 5 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ 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
# YAML config support
"click-config-file",
"pyyaml",
]

[project.urls]
Expand Down
45 changes: 41 additions & 4 deletions src/hckr/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@
#
# SPDX-License-Identifier: MIT
import logging
import os
Comment thread
pateash marked this conversation as resolved.
Outdated
from pathlib import Path

import click
import click_config_file
import yaml
from click_repl import register_repl # type: ignore

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 @@ -42,11 +46,44 @@ def __init__(self): # Note: This object must have an empty constructor.
# pass_info is a decorator for functions that pass 'Info' objects.
pass_info = click.make_pass_decorator(Info, ensure=True)

# Define the default configuration file path
config_path = Path.home() / ".hckrcfg"


# Ensure the configuration file exists
def ensure_config_file(config_path: Path):
"""Ensure the configuration file and its directory exist."""
if not config_path.exists():
config_path.parent.mkdir(parents=True, exist_ok=True) # Create the directory if it doesn't exist
config_path.touch(exist_ok=True) # Create the file if it doesn't exist
# Optionally, write some default configuration
default_config = {
'hckr': {
'version': f'hckr {__version__}'
},
'verbose': 3
}
with config_path.open('w') as config_file:
yaml.dump(default_config, config_file)


ensure_config_file(config_path)


def yaml_loader(config_file, command):
"""Load YAML configuration file."""
print(config_file, command)
with open(config_file, 'r') as yaml_file:
return yaml.safe_load(yaml_file)[command]


@click.group(
context_settings={"help_option_names": ["-h", "--help"]},
invoke_without_command=True,
)
@click_config_file.configuration_option(provider=yaml_loader, implicit=True,
# cmd_name=str(config_path),
config_file_name=str(config_path), default=config_path)
@click.option(
"--verbose",
"-v",
Expand All @@ -57,9 +94,9 @@ def __init__(self): # Note: This object must have an empty constructor.
@click.pass_context
@pass_info
def cli(
_info: Info,
ctx: click.Context,
verbose: int,
_info: Info,
ctx: click.Context,
verbose: int,
):
if verbose > 0:
logging.basicConfig(
Expand Down
86 changes: 86 additions & 0 deletions src/hckr/cli/configure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# from ..utils.MessageUtils import *
import logging
Comment thread
pateash marked this conversation as resolved.
Outdated

import click
import rich
from cron_descriptor import get_description # type: ignore

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove unused import.

The get_description import from cron_descriptor is not used in this file.

Apply this diff to remove the unused import:

-from cron_descriptor import get_description  # type: ignore
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from cron_descriptor import get_description # type: ignore
Tools
Ruff

6-6: cron_descriptor.get_description imported but unused

Remove unused import: cron_descriptor.get_description

(F401)

from rich.panel import Panel

from ..utils import ConfigUtils, MessageUtils
Comment thread
pateash marked this conversation as resolved.
Outdated
from ..utils.ConfigUtils import load_config, config_path, ensure_config_file, DEFAULT_CONFIG, configMessage
Comment thread
pateash marked this conversation as resolved.
Outdated


@click.group(
help="Config commands",
context_settings={"help_option_names": ["-h", "--help"]},
)
@click.pass_context
def configure(ctx):
"""
Defines a command group for configuration-related commands.
"""
ensure_config_file()


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


@configure.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)
ConfigUtils.set_config_value(config, key, value)
rich.print(
Panel(
f"[{config}] {key} <- {value}",
expand=True,
title="Success",
)
)

@configure.command()
@common_config_options
@click.argument('key')
def get(config, key):
"""Get a configuration value."""
configMessage(config)
try:
value = ConfigUtils.get_config_value(config, key)
rich.print(
Panel(
f"[{config}] {key} = {value}",
expand=True,
title="Success",
)
)
except ValueError as e:
rich.print(
Panel(
f"{e}",
expand=True,
title="Error",
)
)


@configure.command()
@common_config_options
def show(config):
"""List configuration values."""
configMessage(config)
ConfigUtils.list_config(config)
125 changes: 125 additions & 0 deletions src/hckr/utils/ConfigUtils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import configparser
import logging
from pathlib import Path

import click
Comment thread
pateash marked this conversation as resolved.
Outdated
import rich
from rich.panel import Panel

from . import MessageUtils
from ..__about__ import __version__

# Define the default configuration file path, this can't be changed, although user can have multile instances using --config
config_path = Path.home() / ".hckrcfg"
DEFAULT_CONFIG = "HCKR"


def load_config():
"""Load the INI configuration file."""
config = configparser.ConfigParser()
config.read(config_path)
return config


def ensure_config_file():
"""
Ensures the existence of a configuration file at the specified path.

:param config_path: The path to the configuration file.
:return: None

This function creates a configuration file at the specified path if it does not already exist. It also creates any necessary parent directories. The configuration file is empty initially, but a default configuration is written to it using configparser. The default configuration includes a 'DEFAULT' section with a 'version' option that contains the value of the __version__ global variable.

Example Usage:
--------------
ensure_config_file(Path('/path/to/config.ini'))
"""
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 = configparser.ConfigParser()
config.read_dict(default_config)
with config_path.open('w') as config_file:
config.write(config_file)
MessageUtils.info(f"Creating default config file {config_path}")
else:
logging.debug(f"Config file {config_path} already exists ")


def set_config_value(section, key, value):
"""
Sets a configuration value in a configuration file.

:param section: The name of the configuration section.
:param key: The key of the configuration value.
:param value: The value to set.
:return: None

This function sets a configuration value in a configuration file. It first logs the action using the `logging.debug()` function. Then, it loads the configuration file using the `load_config()` function. If the configuration file does not have the specified section and the section is not the default section, it adds the section to the configuration file. Next, it sets the value for the key in the specified section. Finally, it writes the updated configuration file to disk.

After setting the configuration value, the function displays an information message using the `MessageUtils.info()` function.

Note: This function assumes that the `logging` and `MessageUtils` modules are imported and configured properly.

Example Usage:
set_config_value("database", "username", "admin")
"""
logging.debug(f"Setting [{section}] {key} = {value}")
config = load_config()
if not config.has_section(section) and section != DEFAULT_CONFIG:
logging.debug(f"Adding section {section}")
config.add_section(section)
config.set(section, key, value)
with config_path.open('w') as config_file:
config.write(config_file)


def get_config_value(section, key) -> str:
"""
:param section: The section of the configuration file where the desired value is located.
:param key: The key of the value within the specified section.
:return: The value corresponding to the specified key within the specified section of the configuration file.

"""
logging.debug(f"Getting [{section}] {key} ")
config = load_config()
if not config.has_section(section):
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}'.")
return config.get(section, key)


def list_config(section):
"""
List Config

This function takes a section parameter and lists the configuration values for that section from the loaded configuration file. If the section is found in the configuration file, it will print the section name and all key-value pairs associated with that section. If the section is not found, it will display an error message.

:param section: The section name for which the configuration values should be listed.
:return: None
"""
config = load_config()
if config.has_section(section):
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="HCKR",
)
)
else:
MessageUtils.warning(f"Config '{section}' not found.")


def configMessage(config):
if config == DEFAULT_CONFIG:
MessageUtils.info(f"Using default config: [magenta]{DEFAULT_CONFIG}")
else:
MessageUtils.info(f"Using config: [magenta]{config}")
28 changes: 28 additions & 0 deletions tests/cli/test_configure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import string
from distutils.command.config import config
Comment thread
pateash marked this conversation as resolved.
Outdated
import random
from click.testing import CliRunner

from hckr.cli.configure import set, get


def _get_random_string(length):
letters = string.ascii_lowercase
result_str = ''.join(random.choice(letters) for i in range(length))
return result_str


# DEFAULT CONFIG GET AND SET
def test_configure_get_set_default():
runner = CliRunner()
_key = f"key_{_get_random_string(5)}"
_value = f"value_{_get_random_string(5)}"
result = runner.invoke(set, [_key, _value])
assert result.exit_code == 0
print(result.output)

assert (
f"Set [DEFAULT] {_key} = {_value}"
in result.output
)
result = runner.invoke(get, [_key, _value])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure the get command is tested correctly.

The test_configure_get_set_default function tests the set command but does not verify the get command output.

Consider modifying the test to check if the get command retrieves the correct value:

 result = runner.invoke(get, [_key])
 assert result.exit_code == 0
 assert (
     f"[DEFAULT] {_key} = {_value}"
     in result.output
 )
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_configure_get_set_default():
runner = CliRunner()
_key = f"key_{_get_random_string(5)}"
_value = f"value_{_get_random_string(5)}"
result = runner.invoke(set, [_key, _value])
assert result.exit_code == 0
print(result.output)
assert (
f"Set [DEFAULT] {_key} = {_value}"
in result.output
)
result = runner.invoke(get, [_key, _value])
def test_configure_get_set_default():
runner = CliRunner()
_key = f"key_{_get_random_string(5)}"
_value = f"value_{_get_random_string(5)}"
result = runner.invoke(set, [_key, _value])
assert result.exit_code == 0
print(result.output)
assert (
f"Set [DEFAULT] {_key} = {_value}"
in result.output
)
result = runner.invoke(get, [_key])
assert result.exit_code == 0
assert (
f"[DEFAULT] {_key} = {_value}"
in result.output
)