diff --git a/src/armonik_cli/cli.py b/src/armonik_cli/cli.py index 469bd5c..1fc70d5 100644 --- a/src/armonik_cli/cli.py +++ b/src/armonik_cli/cli.py @@ -1,12 +1,10 @@ import rich_click as click - -from armonik_cli import commands, version -from armonik_cli.console import console +from armonik_cli import commands, __version__ @click.group(name="armonik") -@click.version_option(version=version.__version__, prog_name="armonik") +@click.version_option(version=__version__, prog_name="armonik") def cli() -> None: """ ArmoniK CLI is a tool to monitor and manage ArmoniK clusters. diff --git a/src/armonik_cli/commands/common.py b/src/armonik_cli/commands/common.py index ef3c5af..3171871 100644 --- a/src/armonik_cli/commands/common.py +++ b/src/armonik_cli/commands/common.py @@ -3,6 +3,8 @@ import rich_click as click from datetime import timedelta +from typing import cast, Tuple, Union + endpoint_option = click.option( "-e", @@ -18,7 +20,7 @@ type=click.Choice(["yaml", "json", "table"], case_sensitive=False), default="json", show_default=True, - help="Endpoint of the cluster to connect to.", + help="Commands output format.", metavar="FORMAT", ) debug_option = click.option( @@ -26,14 +28,37 @@ ) -class KeyValuePairParamType(click.ParamType): +class KeyValuePairParam(click.ParamType): + """ + A custom Click parameter type that parses a key-value pair in the format "key=value". + + Attributes: + name: The name of the parameter type, used by Click. + """ + name = "key_value_pair" - def convert(self, value, param, ctx): + def convert( + self, value: str, param: Union[click.Parameter, None], ctx: Union[click.Context, None] + ) -> Tuple[str, str]: + """ + Converts the input value into a tuple of (key, value) if it matches the required format. + + Args: + value: The input value to be converted. + param: The parameter object passed by Click. + ctx: The context in which the parameter is being used. + + Returns: + A tuple (key, value) if the input matches the format "key=value". + + Raises: + click.BadParameter: If the input does not match the expected format. + """ pattern = r"^([a-zA-Z0-9_-]+)=([a-zA-Z0-9_-]+)$" match_result = re.match(pattern, value) if match_result: - return match_result.groups() + return cast(Tuple[str, str], match_result.groups()) self.fail( f"{value} is not a valid key value pair. Use key=value where both key and value contain only alphanumeric characters, dashes (-), and underscores (_).", param, @@ -41,10 +66,33 @@ def convert(self, value, param, ctx): ) -class TimeDeltaParamType(click.ParamType): +class TimeDeltaParam(click.ParamType): + """ + A custom Click parameter type that parses a time duration string in the format "HH:MM:SS.MS". + + Attributes: + name: The name of the parameter type, used by Click. + """ + name = "timedelta" - def convert(self, value, param, ctx): + def convert( + self, value: str, param: Union[click.Parameter, None], ctx: Union[click.Context, None] + ) -> timedelta: + """ + Converts the input value into a timedelta object if it matches the required time format. + + Args: + value: The input value to be converted. + param: The parameter object passed by Click. + ctx: The context in which the parameter is being used. + + Returns: + A timedelta object representing the parsed time duration. + + Raises: + click.BadParameter: If the input does not match the expected time format. + """ try: return self._parse_time_delta(value) except ValueError: diff --git a/src/armonik_cli/commands/sessions.py b/src/armonik_cli/commands/sessions.py index 3424ad1..daabecc 100644 --- a/src/armonik_cli/commands/sessions.py +++ b/src/armonik_cli/commands/sessions.py @@ -13,8 +13,8 @@ endpoint_option, output_option, debug_option, - KeyValuePairParamType, - TimeDeltaParamType, + KeyValuePairParam, + TimeDeltaParam, ) @@ -41,9 +41,7 @@ def list(endpoint: str, output: str, debug: bool) -> None: if total > 0: sessions = [_clean_up_status(s) for s in sessions] - console.formatted_print( - sessions, format=output, table_cols=SESSION_TABLE_COLS - ) + console.formatted_print(sessions, format=output, table_cols=SESSION_TABLE_COLS) console.print(f"\n{total} sessions found.") @@ -52,7 +50,7 @@ def list(endpoint: str, output: str, debug: bool) -> None: @endpoint_option @output_option @debug_option -@click.argument("session-id", required=True, type=str, metavar="SESSION_ID") +@session_argument @error_handler def get(endpoint: str, output: str, session_id: str, debug: bool) -> None: """Get details of a given session.""" @@ -74,7 +72,7 @@ def get(endpoint: str, output: str, session_id: str, debug: bool) -> None: ) @click.option( "--max-duration", - type=TimeDeltaParamType(), + type=TimeDeltaParam(), required=True, help="Maximum default task execution time (format HH:MM:SS.MS).", metavar="DURATION", @@ -126,7 +124,7 @@ def get(endpoint: str, output: str, session_id: str, debug: bool) -> None: ) @click.option( "--option", - type=KeyValuePairParamType(), + type=KeyValuePairParam(), required=False, multiple=True, help="Additional default options.", diff --git a/src/armonik_cli/console.py b/src/armonik_cli/console.py index bf2b60b..3a38270 100644 --- a/src/armonik_cli/console.py +++ b/src/armonik_cli/console.py @@ -10,9 +10,26 @@ class ArmoniKCLIConsole(Console): + """ + Custom console that extends the Rich Console to support formatted printing of ArmoniK API objects + in YAML, JSON, or table formats. + """ + def formatted_print( self, obj: object, format: str, table_cols: Union[List[Tuple[str, str]], None] = None - ): + ) -> None: + """ + Print an object in a specified format: JSON, YAML, or a table. + + Args: + obj: The object to format and print. + format: The format in which to print the object. Supported values are 'yaml', 'json', and 'table'. + table_cols: Columns for the table format as a list of tuples containing the column name and + corresponding key in the object. Required if format is 'table'. + + Raises: + ValueError: If `format` is 'table' and `table_cols` is not provided. + """ obj = cast(Dict[str, Any], json.loads(json.dumps(obj, cls=CLIJSONEncoder))) if format == "yaml": @@ -30,6 +47,17 @@ def formatted_print( @staticmethod def _build_table(obj: Dict[str, Any], table_cols: List[Tuple[str, str]]) -> Table: + """ + Build a Rich Table object from a dictionary and column specifications. + + Args: + obj: The object or list of objects to display in a table. + table_cols: List of tuples where each tuple contains the table column name and + the key in `obj` corresponding to the data to display. + + Returns: + A Rich Table object with the specified columns and rows based on `obj`. + """ table = Table(box=None) for col_name, _ in table_cols: diff --git a/src/armonik_cli/utils.py b/src/armonik_cli/utils.py index e99a5eb..861bc4c 100644 --- a/src/armonik_cli/utils.py +++ b/src/armonik_cli/utils.py @@ -13,7 +13,7 @@ class CLIJSONEncoder(json.JSONEncoder): JSONs. Attributes: - __api_types (list): The list of ArmoniK API Python objects managed by this encoder. + __api_types: The list of ArmoniK API Python objects managed by this encoder. """ __api_types = [Session, TaskOptions] @@ -23,10 +23,10 @@ def default(self, obj: object) -> Union[str, Dict[str, Any]]: Override the `default` method to serialize non-serializable objects to JSON. Args: - obj: The object to be serialized. + The object to be serialized. Returns: - str or dict: The object serialized. + The object serialized. """ if isinstance(obj, timedelta): return str(obj) @@ -47,9 +47,9 @@ def camel_case(value: str) -> str: Convert snake_case strings to CamelCase. Args: - value (str): The snake_case string to be converted. + value: The snake_case string to be converted. Returns: - str: The CamelCase equivalent of the input string. + The CamelCase equivalent of the input string. """ return "".join(word.capitalize() for word in value.split("_")) diff --git a/tests/test_cli.py b/tests/test_cli.py index 7ee2c60..cfd046b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -7,4 +7,4 @@ def test_armonik_version(): runner = CliRunner() result = runner.invoke(cli.cli, ["--version"]) assert result.exit_code == 0 - assert result.output == "armonik, version 0.0.1\n" + assert result.output.startswith("armonik, version ")