Skip to content

Commit

Permalink
feat: add support for configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
qdelamea-aneo committed Nov 13, 2024
1 parent 332be3f commit 719a42c
Show file tree
Hide file tree
Showing 12 changed files with 447 additions and 57 deletions.
68 changes: 65 additions & 3 deletions src/armonik_cli/cli.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,77 @@
import rich_click as click

from typing import cast

from rich_click.utils import OptionGroupDict

from armonik_cli import commands, __version__
from armonik_cli.core import Configuration, console


COMMON_OPTIONS = cast(
OptionGroupDict, {"name": "Common options", "options": ["--debug", "--output", "--help"]}
)
CONNECTION_OPTIONS = cast(
OptionGroupDict, {"name": "Connection options", "options": ["--endpoint"]}
)
click.rich_click.OPTION_GROUPS = {
"armonik": [
{
"name": "Common options",
"options": ["--version", "--help"],
}
],
"armonik session create": [
{
"name": "Session configuration options",
"options": [
"--max-duration",
"--max-retries",
"--priority",
"--partition",
"--default-partition",
"--application-name",
"--application-version",
"--application-namespace",
"--application-service",
"--engine-type",
"--option",
],
},
CONNECTION_OPTIONS,
COMMON_OPTIONS,
],
"armonik session list": [CONNECTION_OPTIONS, COMMON_OPTIONS],
"armonik session get": [CONNECTION_OPTIONS, COMMON_OPTIONS],
"armonik session pause": [CONNECTION_OPTIONS, COMMON_OPTIONS],
"armonik session resume": [CONNECTION_OPTIONS, COMMON_OPTIONS],
"armonik session delete": [CONNECTION_OPTIONS, COMMON_OPTIONS],
"armonik session cancel": [CONNECTION_OPTIONS, COMMON_OPTIONS],
"armonik session purge": [CONNECTION_OPTIONS, COMMON_OPTIONS],
"armonik session close": [CONNECTION_OPTIONS, COMMON_OPTIONS],
"armonik session stop-submission": [
{
"name": "Submission interruption options",
"options": ["--clients-only", "--workers-only"],
},
CONNECTION_OPTIONS,
COMMON_OPTIONS,
],
}


@click.group(name="armonik")
@click.version_option(version=__version__, prog_name="armonik")
def cli() -> None:
@click.pass_context
def cli(ctx: click.Context) -> None:
"""
ArmoniK CLI is a tool to monitor and manage ArmoniK clusters.
"""
pass
if "help" not in ctx.args:
if not Configuration.default_path.exists():
console.print(f"Created configuration file at {Configuration.default_path}")
Configuration.create_default_if_not_exists()


cli.add_command(commands.sessions)
cli.add_command(commands.session)
cli.add_command(commands.config)
5 changes: 3 additions & 2 deletions src/armonik_cli/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .sessions import sessions
from armonik_cli.commands.config import config
from armonik_cli.commands.session import session


__all__ = ["sessions"]
__all__ = ["config", "session"]
90 changes: 90 additions & 0 deletions src/armonik_cli/commands/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import json

import rich_click as click

from pathlib import Path

from rich.prompt import Prompt

from armonik_cli.core import console, Configuration, MutuallyExclusiveOption, base_command


key_argument = click.argument("key", required=True, type=str, metavar="KEY")


@click.group()
def config():
"""Display or change configuration settings for ArmoniK CLI."""
pass


@config.command()
@key_argument
@base_command(connection_args=False)
def get(key: str, output: str, debug: bool) -> None:
"""Retrieve the value of a configuration setting by its KEY."""
config = Configuration.load_default()
if config.has(key):
return console.formatted_print({key: config.get(key)}, format=output)
return console.print(f"Warning: '{key}' is not a known configuration key.")


# The function cannot be called 'set' directly, as this causes a conflict with the constructor of the built-in set object.
@config.command("set")
@key_argument
@click.argument("value", required=True, type=str, metavar="VALUE")
@base_command(connection_args=False)
def set_(key: str, value: str, output: str, debug: bool) -> None:
"""Update a configuration setting with a VALUE for the given KEY."""
config = Configuration.load_default()
if config.has(key):
config.set(key, value)
return console.print(f"Updated '{key}' configuration with value '{value}'.")
return console.print(f"Warning: '{key}' is not a known configuration key.")


@config.command()
@base_command(connection_args=False)
def list(output: str, debug: bool) -> None:
"""Display all configuration settings."""
config = Configuration.load_default()
console.formatted_print(config.to_dict(), format=output)


@config.command()
@click.option(
"--local",
is_flag=True,
help="Set to a local cluster without TLS enabled.",
cls=MutuallyExclusiveOption,
mutual=["interactive", "source"],
)
@click.option(
"--interactive",
is_flag=True,
help="Use interactive prompts.",
cls=MutuallyExclusiveOption,
mutual=["local", "source"],
)
@click.option(
"--source",
type=click.Path(exists=True, file_okay=False, dir_okay=True),
help="Use the deployment generated folder to retrieve connection details.",
cls=MutuallyExclusiveOption,
mutual=["local", "interactive"],
)
@base_command(connection_args=False)
def set_connection(local: bool, interactive: bool, source: str, output: str, debug: bool) -> None:
"""Update all cluster connection configuration settings at once."""
endpoint = ""
if local:
endpoint = "172.17.119.85:5001"
elif interactive:
endpoint = Prompt.get_input(console, "Endpoint: ", password=False)
elif source:
with (Path(source) / "armonik-output.json").open() as output_file:
endpoint = json.loads(output_file.read())["armonik"]["control_plane_url"]
else:
return
config = Configuration.load_default()
config.set("endpoint", endpoint)
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@


@click.group(name="session")
def sessions() -> None:
def session() -> None:
"""Manage cluster sessions."""
pass


@sessions.command()
@session.command()
@base_command
def list(endpoint: str, output: str, debug: bool) -> None:
"""List the sessions of an ArmoniK cluster."""
Expand All @@ -36,7 +36,7 @@ def list(endpoint: str, output: str, debug: bool) -> None:
# console.print(f"\n{total} sessions found.")


@sessions.command()
@session.command()
@session_argument
@base_command
def get(endpoint: str, output: str, session_id: str, debug: bool) -> None:
Expand All @@ -48,7 +48,7 @@ def get(endpoint: str, output: str, session_id: str, debug: bool) -> None:
console.formatted_print(session, format=output, table_cols=SESSION_TABLE_COLS)


@sessions.command()
@session.command()
@click.option(
"--max-retries",
type=int,
Expand Down Expand Up @@ -156,7 +156,7 @@ def create(
console.formatted_print(session, format=output, table_cols=SESSION_TABLE_COLS)


@sessions.command()
@session.command()
@click.confirmation_option("--confirm", prompt="Are you sure you want to cancel this session?")
@session_argument
@base_command
Expand All @@ -169,7 +169,7 @@ def cancel(endpoint: str, output: str, session_id: str, debug: bool) -> None:
console.formatted_print(session, format=output, table_cols=SESSION_TABLE_COLS)


@sessions.command()
@session.command()
@session_argument
@base_command
def pause(endpoint: str, output: str, session_id: str, debug: bool) -> None:
Expand All @@ -181,7 +181,7 @@ def pause(endpoint: str, output: str, session_id: str, debug: bool) -> None:
console.formatted_print(session, format=output, table_cols=SESSION_TABLE_COLS)


@sessions.command()
@session.command()
@session_argument
@base_command
def resume(endpoint: str, output: str, session_id: str, debug: bool) -> None:
Expand All @@ -193,7 +193,7 @@ def resume(endpoint: str, output: str, session_id: str, debug: bool) -> None:
console.formatted_print(session, format=output, table_cols=SESSION_TABLE_COLS)


@sessions.command()
@session.command()
@click.confirmation_option("--confirm", prompt="Are you sure you want to close this session?")
@session_argument
@base_command
Expand All @@ -206,7 +206,7 @@ def close(endpoint: str, output: str, session_id: str, debug: bool) -> None:
console.formatted_print(session, format=output, table_cols=SESSION_TABLE_COLS)


@sessions.command()
@session.command()
@click.confirmation_option("--confirm", prompt="Are you sure you want to purge this session?")
@session_argument
@base_command
Expand All @@ -219,7 +219,7 @@ def purge(endpoint: str, output: str, session_id: str, debug: bool) -> None:
console.formatted_print(session, format=output, table_cols=SESSION_TABLE_COLS)


@sessions.command()
@session.command()
@click.confirmation_option("--confirm", prompt="Are you sure you want to delete this session?")
@session_argument
@base_command
Expand All @@ -232,7 +232,7 @@ def delete(endpoint: str, output: str, session_id: str, debug: bool) -> None:
console.formatted_print(session, format=output, table_cols=SESSION_TABLE_COLS)


@sessions.command()
@session.command()
@click.option(
"--clients-only",
is_flag=True,
Expand Down
11 changes: 10 additions & 1 deletion src/armonik_cli/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
from armonik_cli.core.config import Configuration
from armonik_cli.core.console import console
from armonik_cli.core.decorators import base_command
from armonik_cli.core.options import MutuallyExclusiveOption
from armonik_cli.core.params import KeyValuePairParam, TimeDeltaParam


__all__ = ["base_command", "KeyValuePairParam", "TimeDeltaParam", "console"]
__all__ = [
"base_command",
"KeyValuePairParam",
"TimeDeltaParam",
"console",
"Configuration",
"MutuallyExclusiveOption",
]
102 changes: 102 additions & 0 deletions src/armonik_cli/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import json
import rich_click as click

from pathlib import Path
from typing import Union, Dict


class Configuration:
"""
A class to manage application configuration settings, providing methods
for loading, creating, updating, and saving the configuration.
Attributes:
default_path: The default path to the configuration file.
_config_keys: The list of valid configuration keys.
_default_config: The default configuration values.
"""

default_path = Path(click.get_app_dir("armonik_cli")) / "config"
_config_keys = ["endpoint"]
_default_config = {"endpoint": None}

def __init__(self, endpoint: str) -> None:
self.endpoint = endpoint

@classmethod
def create_default_if_not_exists(cls) -> None:
"""
Create a default configuration file if it does not already exist.
This method creates the configuration directory if needed, and writes
the default configuration to a file if it is not already present.
"""
cls.default_path.parent.mkdir(exist_ok=True)
if not (cls.default_path.is_file() and cls.default_path.exists()):
with cls.default_path.open("w") as config_file:
config_file.write(json.dumps(cls._default_config, indent=4))

@classmethod
def load_default(cls) -> "Configuration":
"""
Load the configuration from the default configuration file.
Returns:
An instance of Configuration populated with values from the default file.
"""
with cls.default_path.open("r") as config_file:
return cls(**json.loads(config_file.read()))

def has(self, key: str) -> bool:
"""
Check if a specified configuration key is valid.
Args:
key: The configuration key to check.
Returns:
True if the key is valid, False otherwise.
"""
return key in self._config_keys

def get(self, key: str) -> Union[str, None]:
"""
Retrieve the value of a specified configuration key.
Args:
key: The configuration key to retrieve.
Returns:
The value of the configuration key, or None if the key is invalid.
"""
if self.has(key):
return getattr(self, key)
return None

def set(self, key: str, value: str) -> None:
"""
Set the value of a specified configuration key and save the configuration.
Args:
key: The configuration key to set.
value: The value to assign to the configuration key.
"""
if self.has(key):
setattr(self, key, value)
self._save()

def to_dict(self) -> Dict[str, str]:
"""
Convert the configuration to a dictionary format.
Returns:
A dictionary representation of the configuration.
"""
return {key: getattr(self, key) for key in self._config_keys}

def _save(self):
"""
Save the current configuration values to the default configuration file.
"""
with self.default_path.open("w") as config_file:
config_file.write(json.dumps(self.to_dict(), indent=4))
Loading

0 comments on commit 719a42c

Please sign in to comment.