diff --git a/nucypher/cli/actions.py b/nucypher/cli/actions.py index a9a832ba6..425a7dc5b 100644 --- a/nucypher/cli/actions.py +++ b/nucypher/cli/actions.py @@ -1,15 +1,16 @@ import shutil -import sys from typing import List import click from nacl.exceptions import CryptoError from twisted.logger import Logger -import nucypher from nucypher.blockchain.eth.registry import EthereumContractRegistry +from nucypher.characters.control.emitters import StdoutEmitter from nucypher.characters.lawful import Ursula +from nucypher.cli.config import NucypherClickConfig from nucypher.config.constants import DEFAULT_CONFIG_ROOT +from nucypher.network.middleware import RestMiddleware DESTRUCTION = ''' *Permanently and irreversibly delete all* nucypher files including @@ -24,36 +25,15 @@ Delete {}?''' LOG = Logger('cli.actions') - -def handle_control_output(response: dict = None, - message: str = None, - json: bool = False, - quiet: bool = False, - color: str = 'white', - bold: bool = False, - ) -> None: - - try: - if not quiet and not json: - if response: - for k, v in response.items(): - click.secho(message=f'{k} ...... {v}', fg=color, bold=bold) - elif message: - if json: - sys.stdout({'result': message, 'version': nucypher.__version__}) - click.secho(message=message, fg=color, bold=bold) - else: - click.secho(message=message, fg=color, bold=bold) - else: - raise ValueError('Either "response" or "message" is required, but got neither.') - elif json: - sys.stdout(response) - except Exception: - LOG.debug("Error while formatting nucypher console output") - raise +console_emitter = NucypherClickConfig.emit -def load_seednodes(min_stake: int, federated_only: bool, teacher_uris: list = None) -> List[Ursula]: +def load_seednodes(min_stake: int, + federated_only: bool, + network_middleware: RestMiddleware = None, + teacher_uris: list = None + ) -> List[Ursula]: + teacher_nodes = list() if teacher_uris is None: # Default teacher nodes can be placed here @@ -61,7 +41,8 @@ def load_seednodes(min_stake: int, federated_only: bool, teacher_uris: list = No for uri in teacher_uris: teacher_node = Ursula.from_teacher_uri(teacher_uri=uri, min_stake=min_stake, - federated_only=federated_only) + federated_only=federated_only, + network_middleware=network_middleware) teacher_nodes.append(teacher_node) return teacher_nodes @@ -96,12 +77,12 @@ def destroy_system_configuration(config_class, character_config.destroy(force=force) except FileNotFoundError: message = 'Failed: No nucypher files found at {}'.format(character_config.config_root) - click.secho(message, fg='red') + console_emitter(message=message, color='red') log.debug(message) raise click.Abort() else: message = "Deleted configuration files at {}".format(character_config.config_root) - click.secho(message, fg='green') + console_emitter(message=message, color='green') log.debug(message) return config_root @@ -109,7 +90,7 @@ def destroy_system_configuration(config_class, def unlock_keyring(configuration, password): try: - click.secho("Decrypting keyring...", fg='blue') + console_emitter(message="Decrypting keyring...", color='blue') configuration.keyring.unlock(password=password) except CryptoError: raise configuration.keyring.AuthenticationFailed @@ -126,8 +107,9 @@ def connect_to_blockchain(configuration, recompile_contracts: bool = False): def forget(configuration): - """Forget all known nodes via storages""" + """Forget all known nodes via storage""" click.confirm("Permanently delete all known node data?", abort=True) configuration.forget_nodes() message = "Removed all stored node node metadata and certificates" + console_emitter(message=message, color='red') click.secho(message=message, fg='red') diff --git a/nucypher/cli/config.py b/nucypher/cli/config.py index 5e2b90f90..8845b56d1 100644 --- a/nucypher/cli/config.py +++ b/nucypher/cli/config.py @@ -19,7 +19,7 @@ import collections import os import click -from constant_sorrow.constants import NO_PASSWORD +from constant_sorrow.constants import NO_PASSWORD, NO_EMITTER from twisted.logger import Logger from twisted.logger import globalLogPublisher @@ -33,6 +33,9 @@ from nucypher.utilities.logging import ( class NucypherClickConfig: + # Output Sinks + emitters = list() + capture_stdout = False __sentry_endpoint = NUCYPHER_SENTRY_ENDPOINT # Environment Variables @@ -65,6 +68,11 @@ class NucypherClickConfig: self.__keyring_password = keyring_password return self.__keyring_password + @classmethod + def emit(cls, *args, **kwargs): + for emitter in cls.emitters: + emitter(*args, **kwargs) + class NucypherDeployerClickConfig(NucypherClickConfig): diff --git a/nucypher/cli/main.py b/nucypher/cli/main.py index c61ab505d..38c4b6df9 100644 --- a/nucypher/cli/main.py +++ b/nucypher/cli/main.py @@ -20,38 +20,101 @@ along with nucypher. If not, see . import click from nucypher.characters.banners import NUCYPHER_BANNER +from nucypher.characters.control.emitters import StdoutEmitter, IPCStdoutEmitter from nucypher.cli import status from nucypher.cli.characters import moe, ursula, alice, bob, enrico -from nucypher.cli.config import nucypher_click_config +from nucypher.cli.config import nucypher_click_config, NucypherClickConfig from nucypher.cli.painting import echo_version +from nucypher.network.middleware import RestMiddleware from nucypher.utilities.logging import GlobalConsoleLogger +from nucypher.utilities.sandbox.middleware import MockRestMiddleware @click.group() @click.option('--version', help="Echo the CLI version", is_flag=True, callback=echo_version, expose_value=False, is_eager=True) @click.option('-v', '--verbose', help="Specify verbosity level", count=True) -@click.option('-J', '--json', help="Send all output to stdout as JSON", is_flag=True, default=False) -@click.option('--no-logs', help="Disable all logging output", is_flag=True, default=False) +@click.option('-Z', '--mock-networking', help="Use in-memory transport instead of networking", count=True) +@click.option('-J', '--json-ipc', help="Send all output to stdout as JSON", is_flag=True, default=False) +@click.option('-Q', '--quiet', help="Disable console printing", is_flag=True, default=False) +@click.option('-L', '--no-logs', help="Disable all logging output", is_flag=True, default=False) +@click.option('-D', '--debug', help="Enable debugging mode", is_flag=True) +@click.option('--no-registry', help="Skip importing the default contract registry", is_flag=True) @nucypher_click_config -def nucypher_cli(click_config, verbose, json, no_logs): +def nucypher_cli(click_config, + verbose, + mock_networking, + json_ipc, + no_logs, + quiet, + debug, + no_registry): + # Session Emitter for pre and post character control engagement. + if json_ipc: + emitter = IPCStdoutEmitter(quiet=quiet, capture_stdout=NucypherClickConfig.capture_stdout) + else: + emitter = StdoutEmitter(quiet=quiet, capture_stdout=NucypherClickConfig.capture_stdout) + + NucypherClickConfig.emitter = emitter + click_config.emitter(message=NUCYPHER_BANNER) + + # Logging if not no_logs: GlobalConsoleLogger.start_if_not_started() - if not json: - click.echo(NUCYPHER_BANNER) - + # CLI Session Configuration click_config.verbose = verbose - click_config.json = json + click_config.mock_networking = mock_networking + click_config.json_ipc = json_ipc click_config.no_logs = no_logs + click_config.quiet = quiet + click_config.no_registry = no_registry + click_config.debug = debug + # only used for testing outputs; + # Redirects outputs to in-memory python containers. + if mock_networking: + click_config.emitter(message="WARNING: Mock networking is enabled") + click_config.middleware = MockRestMiddleware() + else: + click_config.middleware = RestMiddleware() + + # Global Warnings if click_config.verbose: - click.secho("Verbose mode is enabled", fg='blue') + click_config.emitter("Verbose mode is enabled", color='blue') -nucypher_cli.add_command(status.status) -nucypher_cli.add_command(alice.alice) -nucypher_cli.add_command(bob.bob) -nucypher_cli.add_command(enrico.enrico) -nucypher_cli.add_command(moe.moe) -nucypher_cli.add_command(ursula.ursula) +# +# Character CLI Entry Points (Fan Out Input) +# + +r""" + ursula + | + | moe + | / + | / +stdin --> cli.main --- alice + | \ + | \ + | bob + | + enrico + +""" + +# New character CLI modules must be added here +# for the entry point to be attached to the nucypher base command. +# Inversely, commenting out an entry point will disable it. + +ENTRTY_POINTS = ( + status.status, + alice.alice, + bob.bob, + enrico.enrico, + moe.moe, + ursula.ursula +) + +for entry_point in ENTRTY_POINTS: + nucypher_cli.add_command(entry_point) diff --git a/nucypher/cli/painting.py b/nucypher/cli/painting.py index 781247ea1..4cb82213e 100644 --- a/nucypher/cli/painting.py +++ b/nucypher/cli/painting.py @@ -21,9 +21,11 @@ import maya from constant_sorrow.constants import NO_KNOWN_NODES from nucypher.characters.banners import NUCYPHER_BANNER -from nucypher.cli import actions +from nucypher.characters.control.emitters import StdoutEmitter from nucypher.config.constants import SEEDNODES +emitter = StdoutEmitter() + def echo_version(ctx, param, value): if not value or ctx.resilient_parsing: @@ -32,29 +34,23 @@ def echo_version(ctx, param, value): ctx.exit() -def paint_new_installation_help(new_configuration, config_root=None, config_file=None, quiet: bool = False): +def paint_new_installation_help(new_configuration, config_root=None, config_file=None): character_config_class = new_configuration.__class__ - character_name = character_config_class._CHARACTER_CLASS.__name__.lower() + character_name = character_config_class._NAME.lower() - actions.handle_control_output(message="Generated keyring {}".format(new_configuration.keyring_dir), - color='green', - quiet=quiet) + emitter(message="Generated keyring {}".format(new_configuration.keyring_dir), color='green') - actions.handle_control_output(message="Saved configuration file {}".format(new_configuration.config_file_location), - color='green', - quiet=quiet) + emitter(message="Saved configuration file {}".format(new_configuration.config_file_location), color='green') # Give the use a suggestion as to what to do next... suggested_command = f'nucypher {character_name} run' how_to_run_message = f"\nTo run an {character_name.capitalize()} node from the default configuration filepath run: \n\n'{suggested_command}'\n" + if config_root is not None: config_file_location = os.path.join(config_root, config_file or character_config_class.CONFIG_FILENAME) suggested_command += ' --config-file {}'.format(config_file_location) - click.secho(how_to_run_message.format(suggested_command), fg='green') - return actions.handle_control_output(message=how_to_run_message.format(suggested_command), - color='green', - quiet=quiet) + return emitter(message=how_to_run_message.format(suggested_command), color='green') def build_fleet_state_status(ursula) -> str: