From 5b4ffda68977bb84c18321b8432965b8f48d1348 Mon Sep 17 00:00:00 2001 From: Steven Elliott Date: Wed, 10 Sep 2025 23:15:19 +0000 Subject: [PATCH] Adding ability to take in a properly formatted JSON string and set the FIREWHEEL config. --- docs/source/cli/commands.rst | 13 ++++- docs/source/cli/helper_docs.rst | 11 ++-- src/firewheel/cli/configure_firewheel.py | 66 ++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/docs/source/cli/commands.rst b/docs/source/cli/commands.rst index 611ceea8..0f67bb5a 100644 --- a/docs/source/cli/commands.rst +++ b/docs/source/cli/commands.rst @@ -46,6 +46,7 @@ Edit the FIREWHEEL configuration with a text editor. The user must set either th environment variable or use the provided flag to override these environment variables. options: + -e EDITOR, --editor EDITOR Use the specified text editor. @@ -67,6 +68,7 @@ positional arguments: command: ``firewheel config get logging.level``. options: + -a, --all Get the entire FIREWHEEL configuration. @@ -90,13 +92,21 @@ positional arguments: config set ^^^^^^^^^^ -usage: firewheel config set (-f FILE | -s SETTING [VALUE ...]) +usage: firewheel config set (-f FILE | -j JSON | -s SETTING [VALUE ...]) Set a FIREWHEEL configuration. options: -f FILE, --file FILE Add config from a file. + + -j JSON, --json JSON Pass in a JSON string that can set/replace a subset of the configuration. + This should include the top-level config key as well as any sub-keys. + Any keys or sub-keys not present will not be impacted. + For example, to change the value for the config key ``logging.level``, you + can use the command: + ``firewheel config set -j '{"logging":{"level":"INFO"}}'``. + -s SETTING [VALUE ...], --single SETTING [VALUE ...] Set (or create) a particular configuration value. Nested settings can be used with a period separating them. For example, to change @@ -356,3 +366,4 @@ Example: $ firewheel version 2.6.0 + diff --git a/docs/source/cli/helper_docs.rst b/docs/source/cli/helper_docs.rst index e17ee176..9552d176 100644 --- a/docs/source/cli/helper_docs.rst +++ b/docs/source/cli/helper_docs.rst @@ -725,16 +725,18 @@ The repository should be an existing directory on the filesystem. The path may be specified absolute or relative. If the path does not exist, an error message is printed. -Some Model Components may provide an additional install script called ``INSTALL`` which can be executed to perform other setup steps (e.g. installing an extra python package or downloading an external VM resource). -INSTALL scripts can be can be any executable file type as defined by a `shebang `_ line. +Some Model Components may provide an additional installation details which can be executed to perform other setup steps (e.g. installing an extra python package or downloading an external VM resource). +This takes the form of either a ``INSTALL`` directory with a ``vars.yml`` and a ``tasks.yml`` that are Ansible tasks which can be executed. +Alternatively, it can be a single ``INSTALL`` script that can be can be any executable file type as defined by a `shebang `_ line. .. warning:: - The execution of Model Component ``INSTALL`` scripts can be a **DANGEROUS** operation. Please ensure that you **fully trust** the repository developer prior to executing these scripts. + The execution of Model Component ``INSTALL`` scripts can be a **DANGEROUS** operation. + Please ensure that you **fully trust** the repository developer prior to executing these scripts. .. seealso:: - See :ref:`mc_install` for more information on INSTALL scripts. + See :ref:`mc_install` for more information on ``INSTALL`` scripts. When installing a Model Component, users will have a variety of choices to select: @@ -1509,3 +1511,4 @@ Example ``firewheel vm resume --all`` + diff --git a/src/firewheel/cli/configure_firewheel.py b/src/firewheel/cli/configure_firewheel.py index 2b49d6f0..ff18959f 100644 --- a/src/firewheel/cli/configure_firewheel.py +++ b/src/firewheel/cli/configure_firewheel.py @@ -1,5 +1,6 @@ import os import cmd +import json import pprint import argparse import operator @@ -142,6 +143,24 @@ def do_reset(self, args: str = "") -> None: fw_config = Config(config_path=cmd_args.config_path) fw_config.generate_config_from_defaults() + def _argparse_check_json_type(self, json_string): + """ + Parse a JSON string into a Python dictionary. + + Args: + json_string (str): A string representation of a JSON object. + + Returns: + dict: The parsed JSON object as a Python dictionary. + + Raises: + argparse.ArgumentTypeError: If the input string is not a valid JSON. + """ + try: + return json.loads(json_string.replace("'", "")) + except json.decoder.JSONDecodeError as exc: + raise argparse.ArgumentTypeError(f"Invalid JSON string: {json_string}\n\n") from exc + def define_set_parser(self) -> argparse.ArgumentParser: """Create an :py:class:`argparse.ArgumentParser` for :ref:`command_config_set`. @@ -162,6 +181,19 @@ def define_set_parser(self) -> argparse.ArgumentParser: type=argparse.FileType("r"), help="Add config from a file.", ) + group.add_argument( + "-j", + "--json", + type=self._argparse_check_json_type, + help=( + "Pass in a JSON string that can set/replace a subset of the configuration.\n" + "This should include the top-level config key as well as any sub-keys.\n" + "Any keys or sub-keys not present will not be impacted.\n" + "For example, to change the value for the config key ``logging.level``, you\n" + "can use the command:\n" + '``firewheel config set -j \'{"logging":{"level":"INFO"}}\'``.' + ), + ) group.add_argument( "-s", "--single", @@ -177,6 +209,29 @@ def define_set_parser(self) -> argparse.ArgumentParser: ) return parser + def _update_nested_dict(self, original: dict, updates: dict) -> dict: + """ + This function recursively updates the original dictionary with values + from the updates dictionary. If a key in the updates dictionary is a + nested dictionary, it will update the corresponding key in the original + dictionary without removing any existing keys that are not specified in updates. + + Args: + original (dict): The original dictionary to be updated. + updates (dict): A dictionary containing the updates to apply. + + Returns: + dict: The updated original dictionary. + """ + for key, value in updates.items(): + if isinstance(value, dict) and key in original: + # If the key exists and the value is a dictionary, recurse + self._update_nested_dict(original[key], value) + else: + # Update or add the key in the original dictionary + original[key] = value + return original + def do_set(self, args: str) -> None: # noqa: DOC502 """Enable a user to set a particular FIREWHEEL configuration option. @@ -210,6 +265,15 @@ def do_set(self, args: str) -> None: # noqa: DOC502 fw_config.resolve_set(key, value) fw_config.write() + if cmd_args.json is not None: + value = cmd_args.json + self.log.debug("Setting the FIREWHEEL config value for `%s`", value) + fw_config = Config(writable=True) + curr_config = fw_config.get_config() + future_config = self._update_nested_dict(curr_config, value) + fw_config.set_config(future_config) + fw_config.write() + def define_get_parser(self) -> argparse.ArgumentParser: """Create an :py:class:`argparse.ArgumentParser` for :ref:`command_config_get`. @@ -310,6 +374,8 @@ def get_docs(self) -> str: for num, line in enumerate(help_list): if line.startswith(" -") and help_list[num + 1].startswith(" -"): clean_text += line + "\n" + elif line.startswith(" -"): + clean_text += "\n" + line else: clean_text += line clean_text += "\n"