Create a console emitter only once (in NucypherClickConfig) and propagate it everywhere

pull/1124/head
Bogdan Opanchuk 2019-07-22 22:54:24 -07:00 committed by Kieran Prasch
parent 52dbca6519
commit 0ce9458bf0
No known key found for this signature in database
GPG Key ID: 199AB839D4125A62
17 changed files with 307 additions and 313 deletions

View File

@ -318,7 +318,7 @@ class JSONRPCController(CharacterControlServer):
if not control_requests:
e = self.emitter.InvalidRequest()
return self.emitter(e=e)
return self.emitter.error(e)
batch_size = 0
for request in control_requests: # TODO: parallelism
@ -332,7 +332,7 @@ class JSONRPCController(CharacterControlServer):
control_request = json.loads(control_request)
except JSONDecodeError:
e = self.emitter.ParseError()
return self.emitter(e=e)
return self.emitter.error(e)
# Handle batch of messages
if isinstance(control_request, list):
@ -343,12 +343,12 @@ class JSONRPCController(CharacterControlServer):
return self.handle_message(message=control_request, *args, **kwargs)
except self.emitter.JSONRPCError as e:
return self.emitter(e=e)
return self.emitter.error(e)
except Exception as e:
if self.crash_on_error:
raise
return self.emitter(e=e)
return self.emitter.error(e)
class WebController(CharacterControlServer):

View File

@ -13,81 +13,63 @@ class StdoutEmitter:
transport_serializer = str
default_color = 'white'
default_sink_callable = sys.stdout.write
__stdout_trap = list()
# sys.stdout.write() doesn't work well with click_runner's output capture
default_sink_callable = print
def __init__(self,
sink: Callable = None,
capture_stdout: bool = False,
quiet: bool = False):
self.name = self.__class__.__name__.lower()
self.sink = sink or self.default_sink_callable
self.capture_stdout = capture_stdout
self.quiet = quiet
self.log = Logger(self.name)
super().__init__()
def clear(self):
if not self.quiet:
click.clear()
def __call__(self, *args, **kwargs):
try:
return self._emit(*args, **kwargs)
except Exception:
self.log.debug("Error while emitting nucypher controller output")
raise
def trap_output(self, output) -> int:
self.__stdout_trap.append(output)
return len(bytes(output)) # number of bytes written
def _emit(self,
response: dict = None,
message: str = None,
color: str = None,
bold: bool = False,
) -> None:
"""
Write pretty messages to stdout. For Human consumption only.
"""
if response and message:
raise ValueError(f'{self.__class__.__name__} received both a response and a message.')
if self.quiet:
# reduces the number of CLI conditionals by
# wrapping console output functions
return
if self.capture_stdout:
self.trap_output(response or message)
elif response:
# WARNING: Do not log in this block
for k, v in response.items():
click.secho(message=f'{k} ...... {v}',
fg=color or self.default_color,
bold=bold)
elif message:
# Most likely a message emitted without a character control instance
click.secho(message=message, fg=color or self.default_color, bold=bold)
def message(self,
message: str = None,
color: str = None,
bold: bool = False):
if not self.quiet:
self.echo(message=message, color=color or self.default_color, bold=bold)
self.log.debug(message)
else:
raise ValueError('Either "response" dict or "message" str is required, but got neither.')
def echo(self,
message: str = None,
color: str = None,
bold: bool = False,
nl: bool = True):
if not self.quiet:
click.secho(message=message, fg=color or self.default_color, bold=bold, nl=nl)
def banner(self, banner):
if not self.quiet:
click.echo(banner)
def ipc(self, response: dict, request_id: int, duration):
# WARNING: Do not log in this block
if not self.quiet:
for k, v in response.items():
click.secho(message=f'{k} ...... {v}', fg=self.default_color)
def error(self, e):
if not self.quiet:
e_str = str(e)
click.echo(message=e_str)
self.log.info(e_str)
class JSONRPCStdoutEmitter(StdoutEmitter):
transport_serializer = json.dumps
default_sink_callable = print
delimiter = '\n'
def __init__(self, sink: Callable = None, *args, **kwargs):
self.sink = sink or self.default_sink_callable
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.log = Logger("JSON-RPC-Emitter")
class JSONRPCError(RuntimeError):
@ -114,17 +96,6 @@ class JSONRPCStdoutEmitter(StdoutEmitter):
code = -32603
message = "Internal JSON-RPC error."
def __call__(self, *args, **kwargs):
if 'response' in kwargs:
return self.__emit_rpc_response(*args, **kwargs)
elif 'message' in kwargs:
if not self.quiet:
self.log.info(*args, **kwargs)
elif 'e' in kwargs:
return self.__emit_rpc_error(*args, **kwargs)
else:
raise self.JSONRPCError("Internal Error")
@staticmethod
def assemble_response(response: dict, message_id: int) -> dict:
response_data = {'jsonrpc': '2.0',
@ -162,16 +133,39 @@ class JSONRPCStdoutEmitter(StdoutEmitter):
serialized_response = self.__serialize(data=data)
# Capture Message Output
if self.capture_stdout:
return self.trap_output(serialized_response)
# Write to stdout file descriptor
else:
number_of_written_bytes = self.sink(serialized_response) # < ------ OUTLET
return number_of_written_bytes
number_of_written_bytes = self.sink(serialized_response) # < ------ OUTLET
return number_of_written_bytes
def __emit_rpc_error(self, e):
def clear(self):
pass
def message(self,
message: str = None,
color: str = None,
bold: bool = False):
if not self.quiet:
self.log.info(message)
def echo(self, *args, **kwds):
pass
def banner(self, banner):
pass
def ipc(self, response: dict, request_id: int, duration) -> int:
"""
Write RPC response object to stdout and return the number of bytes written.
"""
# Serialize JSON RPC Message
assembled_response = self.assemble_response(response=response, message_id=request_id)
size = self.__write(data=assembled_response)
if not self.quiet:
self.log.info(f"OK | Responded to IPC request #{request_id} with {size} bytes, took {duration}")
return size
def error(self, e):
"""
Write RPC error object to stdout and return the number of bytes written.
"""
@ -185,20 +179,8 @@ class JSONRPCStdoutEmitter(StdoutEmitter):
raise self.JSONRPCError
size = self.__write(data=assembled_error)
# if not self.quiet:
# self.log.info(f"Error {e.code} | {e.message}") # TODO: Restore this log message
return size
def __emit_rpc_response(self, response: dict, request_id: int, duration) -> int:
"""
Write RPC response object to stdout and return the number of bytes written.
"""
# Serialize JSON RPC Message
assembled_response = self.assemble_response(response=response, message_id=request_id)
size = self.__write(data=assembled_response)
if not self.quiet:
self.log.info(f"OK | Responded to IPC request #{request_id} with {size} bytes, took {duration}")
#if not self.quiet:
# self.log.info(f"Error {e.code} | {e.message}") # TODO: Restore this log message
return size

View File

@ -49,7 +49,7 @@ def character_control_interface(func):
duration = responding - received
# Emit
return instance.emitter(response=response, request_id=request_id, duration=duration)
return instance.emitter.ipc(response=response, request_id=request_id, duration=duration)
return wrapped
@ -76,7 +76,7 @@ class AliceInterface(CharacterPublicInterface, AliceSpecification):
expiration: maya.MayaDT,
value: int = None,
) -> dict:
from nucypher.characters.lawful import Bob
bob = Bob.from_public_keys(encrypting_key=bob_encrypting_key,
verifying_key=bob_verifying_key)

View File

@ -22,16 +22,14 @@ from typing import List
import click
import requests
from constant_sorrow.constants import NO_BLOCKCHAIN_CONNECTION, NO_PASSWORD
from constant_sorrow.constants import NO_BLOCKCHAIN_CONNECTION, NO_PASSWORD, NO_CONTROL_PROTOCOL
from nacl.exceptions import CryptoError
from twisted.logger import Logger
from nucypher.blockchain.eth.clients import NuCypherGethGoerliProcess
from nucypher.blockchain.eth.token import Stake
from nucypher.characters.control.emitters import JSONRPCStdoutEmitter
from nucypher.characters.lawful import Ursula
from nucypher.cli import painting
from nucypher.cli.config import NucypherClickConfig
from nucypher.cli.types import IPV4_ADDRESS
from nucypher.config.node import CharacterConfiguration
from nucypher.network.middleware import RestMiddleware
@ -62,8 +60,6 @@ SUCCESSFUL_DESTRUCTION = "Successfully destroyed NuCypher configuration"
LOG = Logger('cli.actions')
console_emitter = NucypherClickConfig.emit
class UnknownIPAddress(RuntimeError):
pass
@ -77,8 +73,8 @@ def get_password(confirm: bool = False) -> str:
return keyring_password
def unlock_nucypher_keyring(password: str, character_configuration: CharacterConfiguration):
console_emitter(message='Decrypting NuCypher keyring...', color='yellow')
def unlock_nucypher_keyring(emitter, password: str, character_configuration: CharacterConfiguration):
emitter.message('Decrypting NuCypher keyring...', color='yellow')
if character_configuration.dev_mode:
return True # Dev accounts are always unlocked
@ -90,7 +86,8 @@ def unlock_nucypher_keyring(password: str, character_configuration: CharacterCon
raise character_configuration.keyring.AuthenticationFailed
def load_seednodes(min_stake: int,
def load_seednodes(emitter,
min_stake: int,
federated_only: bool,
network_domains: set,
network_middleware: RestMiddleware = None,
@ -112,7 +109,7 @@ def load_seednodes(min_stake: int,
except KeyError:
# TODO: If this is a unknown domain, require the caller to pass a teacher URI explicitly?
if not teacher_uris:
console_emitter(message=f"No default teacher nodes exist for the specified network: {domain}")
emitter.message(f"No default teacher nodes exist for the specified network: {domain}")
for uri in teacher_uris:
teacher_node = Ursula.from_teacher_uri(teacher_uri=uri,
@ -122,7 +119,7 @@ def load_seednodes(min_stake: int,
teacher_nodes.append(teacher_node)
if not teacher_nodes:
console_emitter(message=f'WARNING - No Bootnodes Available')
emitter.message(f'WARNING - No Bootnodes Available')
return teacher_nodes
@ -135,7 +132,7 @@ def get_external_ip_from_centralized_source() -> str:
f"(status code {ip_request.status_code})")
def determine_external_ip_address(force: bool = False) -> str:
def determine_external_ip_address(emitter, force: bool = False) -> str:
"""
Attempts to automatically get the external IP from ifconfig.me
If the request fails, it falls back to the standard process.
@ -151,12 +148,12 @@ def determine_external_ip_address(force: bool = False) -> str:
if not click.confirm(f"Is this the public-facing IPv4 address ({rest_host}) you want to use for Ursula?"):
rest_host = click.prompt("Please enter Ursula's public-facing IPv4 address here:", type=IPV4_ADDRESS)
else:
console_emitter(message=f"WARNING: --force is set, using auto-detected IP '{rest_host}'", color='yellow')
emitter.message(f"WARNING: --force is set, using auto-detected IP '{rest_host}'", color='yellow')
return rest_host
def destroy_configuration(character_config, force: bool = False) -> None:
def destroy_configuration(emitter, character_config, force: bool = False) -> None:
if not force:
click.confirm(CHARACTER_DESTRUCTION.format(name=character_config._NAME,
root=character_config.config_root,
@ -165,17 +162,16 @@ def destroy_configuration(character_config, force: bool = False) -> None:
config=character_config.filepath), abort=True)
character_config.destroy()
SUCCESSFUL_DESTRUCTION = "Successfully destroyed NuCypher configuration"
console_emitter(message=SUCCESSFUL_DESTRUCTION, color='green')
emitter.message(SUCCESSFUL_DESTRUCTION, color='green')
character_config.log.debug(SUCCESSFUL_DESTRUCTION)
def forget(configuration):
def forget(emitter, configuration):
"""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')
emitter.message(message, color='red')
def confirm_staged_stake(stakeholder, value, duration):
@ -183,21 +179,21 @@ def confirm_staged_stake(stakeholder, value, duration):
* Ursula Node Operator Notice *
-------------------------------
By agreeing to stake {str(value)} ({str(value.to_nunits())} NuNits):
By agreeing to stake {str(value)} ({str(value.to_nunits())} NuNits):
- Staked tokens will be locked, and unavailable for transactions for the stake duration.
- You are obligated to maintain a networked and available Ursula-Worker node with the
- You are obligated to maintain a networked and available Ursula-Worker node with the
for the duration of the stake(s) ({duration} periods)
- Agree to allow NuCypher network users to carry out uninterrupted re-encryption
work orders at-will without interference.
work orders at-will without interference.
Failure to keep your node online, or violation of re-encryption work orders
will result in the loss of staked tokens as described in the NuCypher slashing protocol.
Keeping your Ursula node online during the staking period and successfully
performing accurate re-encryption work orders will result in rewards
performing accurate re-encryption work orders will result in rewards
paid out in ETH retro-actively, on-demand.
Accept ursula node operator obligation?""", abort=True)
@ -231,6 +227,8 @@ def make_cli_character(character_config,
min_stake: int = 0,
**config_args):
emitter = click_config.emitter
#
# Pre-Init
#
@ -242,13 +240,15 @@ def make_cli_character(character_config,
# Handle Keyring
if not dev:
character_config.attach_keyring()
unlock_nucypher_keyring(character_configuration=character_config,
unlock_nucypher_keyring(emitter,
character_configuration=character_config,
password=get_password(confirm=False))
# Handle Teachers
teacher_nodes = None
if teacher_uri:
teacher_nodes = load_seednodes(teacher_uris=[teacher_uri] if teacher_uri else None,
teacher_nodes = load_seednodes(emitter,
teacher_uris=[teacher_uri] if teacher_uri else None,
min_stake=min_stake,
federated_only=character_config.federated_only,
network_domains=character_config.domains,
@ -267,14 +267,12 @@ def make_cli_character(character_config,
# Post-Init
#
# TODO: Move to character configuration
# Switch to character control emitter
if click_config.json_ipc:
CHARACTER.controller.emitter = JSONRPCStdoutEmitter(quiet=click_config.quiet)
if CHARACTER.controller is not NO_CONTROL_PROTOCOL:
CHARACTER.controller.emitter = emitter # TODO: set it on object creation? Or not set at all?
# Federated
if character_config.federated_only:
console_emitter(message="WARNING: Running in Federated mode", color='yellow')
emitter.message("WARNING: Running in Federated mode", color='yellow')
return CHARACTER
@ -288,26 +286,26 @@ def select_stake(stakeholder) -> Stake:
return chosen_stake
def select_client_account(blockchain, prompt: str = None, default=0) -> str:
def select_client_account(emitter, blockchain, prompt: str = None, default=0) -> str:
enumerated_accounts = dict(enumerate(blockchain.client.accounts))
for index, account in enumerated_accounts.items():
click.secho(f"{index} | {account}")
emitter.echo(f"{index} | {account}")
prompt = prompt or "Select Account"
choice = click.prompt(prompt, type=click.IntRange(min=0, max=len(enumerated_accounts)-1), default=default)
chosen_account = enumerated_accounts[choice]
return chosen_account
def confirm_deployment(deployer) -> bool:
def confirm_deployment(emitter, deployer) -> bool:
if deployer.blockchain.client.chain_id == 'UNKNOWN' or deployer.blockchain.client.is_local:
if click.prompt("Type 'DEPLOY' to continue") != 'DEPLOY':
click.secho("Aborting Deployment", fg='red', bold=True)
emitter.echo("Aborting Deployment", fg='red', bold=True)
raise click.Abort()
else:
confirmed_chain_id = int(click.prompt("Enter the Chain ID to confirm deployment", type=click.INT))
expected_chain_id = int(deployer.blockchain.client.chain_id)
if confirmed_chain_id != expected_chain_id:
click.secho(f"Chain ID not a match ({confirmed_chain_id} != {expected_chain_id}) Aborting Deployment",
emitter.echo(f"Chain ID not a match ({confirmed_chain_id} != {expected_chain_id}) Aborting Deployment",
fg='red',
bold=True)
raise click.Abort()

View File

@ -94,9 +94,9 @@ def alice(click_config,
raise click.BadOptionUsage(option_name="--geth", message="Federated only cannot be used with the --geth flag")
# Banner
click.clear()
if not click_config.json_ipc and not click_config.quiet:
click.secho(ALICE_BANNER)
emitter = click_config.emitter
emitter.clear()
emitter.banner(ALICE_BANNER)
#
# Managed Ethereum Client
@ -135,15 +135,14 @@ def alice(click_config,
duration=duration,
rate=rate)
painting.paint_new_installation_help(new_configuration=new_alice_config)
painting.paint_new_installation_help(emitter, new_configuration=new_alice_config)
return # Exit
elif action == "view":
"""Paint an existing configuration to the console"""
configuration_file_location = config_file or AliceConfiguration.default_filepath()
response = AliceConfiguration._read_configuration_file(filepath=configuration_file_location)
click_config.emit(response)
return # Exit
return emitter.ipc(response=response, request_id=0, duration=0) # FIXME: what are request_id and duration here?
#
# Make Alice
@ -171,7 +170,7 @@ def alice(click_config,
except FileNotFoundError:
return actions.handle_missing_configuration_file(character_config_class=AliceConfiguration,
config_file=config_file)
ALICE = actions.make_cli_character(character_config=alice_config,
click_config=click_config,
dev=dev,
@ -194,7 +193,7 @@ def alice(click_config,
# HTTP
else:
ALICE.controller.emitter(message=f"Alice Verifying Key {bytes(ALICE.stamp).hex()}", color="green", bold=True)
emitter.message(f"Alice Verifying Key {bytes(ALICE.stamp).hex()}", color="green", bold=True)
controller = ALICE.make_web_controller(crash_on_error=click_config.debug)
ALICE.log.info('Starting HTTP Character Web Controller')
return controller.start(http_port=controller_port, dry_run=dry_run)
@ -204,7 +203,7 @@ def alice(click_config,
if dev:
message = "'nucypher alice destroy' cannot be used in --dev mode"
raise click.BadOptionUsage(option_name='--dev', message=message)
return actions.destroy_configuration(character_config=alice_config, force=force)
return actions.destroy_configuration(emitter, character_config=alice_config, force=force)
#
# Alice API

View File

@ -60,9 +60,9 @@ def bob(click_config,
#
# Banner
click.clear()
if not click_config.json_ipc and not click_config.quiet:
click.secho(BOB_BANNER)
emitter = click_config.emitter
emitter.clear()
emitter.banner(BOB_BANNER)
#
# Eager Actions
@ -86,13 +86,13 @@ def bob(click_config,
registry_filepath=registry_filepath,
provider_uri=provider_uri)
return painting.paint_new_installation_help(new_configuration=new_bob_config)
return painting.paint_new_installation_help(emitter, new_configuration=new_bob_config)
# TODO
# elif action == "view":
# """Paint an existing configuration to the console"""
# response = BobConfiguration._read_configuration_file(filepath=config_file or bob_config.config_file_location)
# return BOB.controller.emitter(response=response)
# return BOB.controller.emitter.ipc(response)
#
# Make Bob
@ -131,9 +131,6 @@ def bob(click_config,
if action == "run":
# Echo Public Keys
click_config.emit(message=f"Bob Verifying Key {bytes(BOB.stamp).hex()}", color='green', bold=True)
# RPC
if click_config.json_ipc:
rpc_controller = BOB.make_rpc_controller()
@ -141,9 +138,10 @@ def bob(click_config,
rpc_controller.start()
return
click_config.emit(message=f"Bob Verifying Key {bytes(BOB.stamp).hex()}", color='green', bold=True)
# Echo Public Keys
emitter.message(f"Bob Verifying Key {bytes(BOB.stamp).hex()}", color='green', bold=True)
bob_encrypting_key = bytes(BOB.public_keys(DecryptingPower)).hex()
click_config.emit(message=f"Bob Encrypting Key {bob_encrypting_key}", color="blue", bold=True)
emitter.message(f"Bob Encrypting Key {bob_encrypting_key}", color="blue", bold=True)
# Start Controller
controller = BOB.make_web_controller(crash_on_error=click_config.debug)
@ -159,7 +157,7 @@ def bob(click_config,
raise click.BadOptionUsage(option_name='--dev', message=message)
# Request
return actions.destroy_configuration(character_config=bob_config)
return actions.destroy_configuration(emitter, character_config=bob_config)
#
# Bob API Actions

View File

@ -28,9 +28,9 @@ def enrico(click_config, action, policy_encrypting_key, dry_run, http_port, mess
raise click.BadArgumentUsage('--policy-encrypting-key is required to start Enrico.')
# Banner
click.clear()
if not click_config.json_ipc and not click_config.quiet:
click.secho(ENRICO_BANNER)
emitter = click_config.emitter
emitter.clear()
emitter.banner(ENRICO_BANNER)
#
# Make Enrico
@ -38,8 +38,7 @@ def enrico(click_config, action, policy_encrypting_key, dry_run, http_port, mess
policy_encrypting_key = UmbralPublicKey.from_bytes(bytes.fromhex(policy_encrypting_key))
ENRICO = Enrico(policy_encrypting_key=policy_encrypting_key)
if click_config.json_ipc:
ENRICO.controller.emitter = JSONRPCStdoutEmitter(quiet=click_config.quiet)
ENRICO.controller.emitter = emitter # TODO: set it on object creation? Or not set at all?
#
# Actions

View File

@ -54,10 +54,11 @@ def felix(click_config,
dev,
force):
emitter = click_config.emitter
# Intro
click.clear()
if not click_config.quiet:
click.secho(FELIX_BANNER.format(checksum_address or ''))
emitter.clear()
emitter.banner(FELIX_BANNER.format(checksum_address or ''))
ETH_NODE = NO_BLOCKCHAIN_CONNECTION
if geth:
@ -87,11 +88,11 @@ def felix(click_config,
if click_config.debug:
raise
else:
click.secho(str(e), fg='red', bold=True)
emitter.echo(str(e), color='red', bold=True)
raise click.Abort
# Paint Help
painting.paint_new_installation_help(new_configuration=new_felix_config)
painting.paint_new_installation_help(emitter, new_configuration=new_felix_config)
return # <-- do not remove (conditional flow control)
@ -111,8 +112,8 @@ def felix(click_config,
poa=poa)
except FileNotFoundError:
click.secho(f"No Felix configuration file found at {config_file}. "
f"Check the filepath or run 'nucypher felix init' to create a new system configuration.")
emitter.echo(f"No Felix configuration file found at {config_file}. "
f"Check the filepath or run 'nucypher felix init' to create a new system configuration.")
raise click.Abort
try:
@ -121,10 +122,13 @@ def felix(click_config,
felix_config.get_blockchain_interface()
# Authenticate
unlock_nucypher_keyring(character_configuration=felix_config, password=get_password(confirm=False))
unlock_nucypher_keyring(emitter,
character_configuration=felix_config,
password=get_password(confirm=False))
# Produce Teacher Ursulas
teacher_nodes = actions.load_seednodes(teacher_uris=[teacher_uri] if teacher_uri else None,
teacher_nodes = actions.load_seednodes(emitter,
teacher_uris=[teacher_uri] if teacher_uri else None,
min_stake=min_stake,
federated_only=felix_config.federated_only,
network_domains=felix_config.domains,
@ -138,7 +142,7 @@ def felix(click_config,
if click_config.debug:
raise
else:
click.secho(str(e), fg='red', bold=True)
emitter.echo(str(e), color='red', bold=True)
raise click.Abort
if action == "createdb": # Initialize Database
@ -146,15 +150,15 @@ def felix(click_config,
if not force:
click.confirm("Overwrite existing database?", abort=True)
os.remove(FELIX.db_filepath)
click.secho(f"Destroyed existing database {FELIX.db_filepath}")
emitter.echo(f"Destroyed existing database {FELIX.db_filepath}")
FELIX.create_tables()
click.secho(f"\nCreated new database at {FELIX.db_filepath}", fg='green')
emitter.echo(f"\nCreated new database at {FELIX.db_filepath}", color='green')
elif action == 'view':
token_balance = FELIX.token_balance
eth_balance = FELIX.eth_balance
click.secho(f"""
emitter.echo(f"""
Address .... {FELIX.checksum_address}
NU ......... {str(token_balance)}
ETH ........ {str(eth_balance)}
@ -163,17 +167,17 @@ ETH ........ {str(eth_balance)}
elif action == "accounts":
accounts = FELIX.blockchain.client.accounts
for account in accounts:
click.secho(account)
emitter.echo(account)
elif action == "destroy":
"""Delete all configuration files from the disk"""
actions.destroy_configuration(character_config=felix_config, force=force)
actions.destroy_configuration(emitter, character_config=felix_config, force=force)
elif action == 'run': # Start web services
try:
click.secho("Waiting for blockchain sync...", fg='yellow')
click_config.emit(message=f"Running Felix on {host}:{port}")
emitter.echo("Waiting for blockchain sync...", color='yellow')
emitter.message(f"Running Felix on {host}:{port}")
FELIX.start(host=host,
port=port,
web_services=not dry_run,

View File

@ -23,14 +23,16 @@ def moe(click_config, teacher_uri, min_stake, network, ws_port, dry_run, http_po
"Moe" NuCypher node monitor CLI.
"""
emitter = click_config.emitter
# Banner
click.clear()
if not click_config.json_ipc and not click_config.quiet:
click.secho(MOE_BANNER)
emitter.clear()
emitter.banner(MOE_BANNER)
# Teacher Ursula
teacher_uris = [teacher_uri] if teacher_uri else None
teacher_nodes = actions.load_seednodes(teacher_uris=teacher_uris,
teacher_nodes = actions.load_seednodes(emitter,
teacher_uris=teacher_uris,
min_stake=min_stake,
federated_only=True, # TODO: hardcoded for now. Is Moe a Character?
network_domains={network} if network else None,
@ -45,6 +47,6 @@ def moe(click_config, teacher_uri, min_stake, network, ws_port, dry_run, http_po
# Run
MOE.start_learning_loop(now=learn_on_launch)
click_config.emit(message=f"Running Moe on 127.0.0.1:{http_port}")
emitter.message(f"Running Moe on 127.0.0.1:{http_port}")
MOE.start(http_port=http_port, ws_port=ws_port, dry_run=dry_run)

View File

@ -109,6 +109,8 @@ def ursula(click_config,
"""
emitter = click_config.emitter
#
# Validate
#
@ -123,8 +125,7 @@ def ursula(click_config,
message="Staking address canot be used in federated mode.")
# Banner
if not click_config.json_ipc and not click_config.quiet:
click.secho(URSULA_BANNER.format(worker_address or ''))
emitter.banner(URSULA_BANNER.format(worker_address or ''))
#
# Pre-Launch Warnings
@ -132,9 +133,9 @@ def ursula(click_config,
if not click_config.quiet:
if dev:
click.secho("WARNING: Running in Development mode", fg='yellow')
emitter.echo("WARNING: Running in Development mode", color='yellow')
if force:
click.secho("WARNING: Force is enabled", fg='yellow')
emitter.echo("WARNING: Force is enabled", color='yellow')
#
# Internal Ethereum Client
@ -166,16 +167,16 @@ def ursula(click_config,
blockchain.connect(fetch_registry=False)
if not staker_address:
staker_address = select_client_account(blockchain=blockchain)
staker_address = select_client_account(emitter=emitter, blockchain=blockchain)
if not worker_address:
worker_address = select_client_account(blockchain=blockchain)
worker_address = select_client_account(emitter=emitter, blockchain=blockchain)
if not config_root: # Flag
config_root = click_config.config_file # Envvar
if not rest_host:
rest_host = actions.determine_external_ip_address(force=force)
rest_host = actions.determine_external_ip_address(emitter, force=force)
ursula_config = UrsulaConfiguration.generate(password=get_password(confirm=True),
config_root=config_root,
@ -192,7 +193,7 @@ def ursula(click_config,
provider_uri=provider_uri,
poa=poa)
painting.paint_new_installation_help(new_configuration=ursula_config)
painting.paint_new_installation_help(emitter, new_configuration=ursula_config)
return
#
@ -232,7 +233,7 @@ def ursula(click_config,
if click_config.debug:
raise
else:
click.secho(str(e), fg='red', bold=True)
emitter.echo(str(e), color='red', bold=True)
raise click.Abort
#
@ -245,7 +246,7 @@ def ursula(click_config,
if dev:
message = "'nucypher ursula destroy' cannot be used in --dev mode - There is nothing to destroy."
raise click.BadOptionUsage(option_name='--dev', message=message)
return actions.destroy_configuration(character_config=ursula_config, force=force)
return actions.destroy_configuration(emitter, character_config=ursula_config, force=force)
#
# Make Ursula
@ -269,23 +270,23 @@ def ursula(click_config,
try:
# Ursula Deploy Warnings
click_config.emit(
message="Starting Ursula on {}".format(URSULA.rest_interface),
emitter.message(
f"Starting Ursula on {URSULA.rest_interface}",
color='green',
bold=True)
click_config.emit(
message="Connecting to {}".format(','.join(ursula_config.domains)),
emitter.message(
f"Connecting to {','.join(ursula_config.domains)}",
color='green',
bold=True)
click_config.emit(
message=f"Working ~ Keep Ursula Online!",
emitter.message(
"Working ~ Keep Ursula Online!",
color='blue',
bold=True)
if interactive:
stdio.StandardIO(UrsulaCommandProtocol(ursula=URSULA))
stdio.StandardIO(UrsulaCommandProtocol(ursula=URSULA, emitter=emitter))
if dry_run:
return # <-- ABORT - (Last Chance)
@ -299,47 +300,48 @@ def ursula(click_config,
# Handle Crash
except Exception as e:
ursula_config.log.critical(str(e))
click_config.emit(
message="{} {}".format(e.__class__.__name__, str(e)),
emitter.message(
f"{e.__class__.__name__} {e}",
color='red',
bold=True)
raise # Crash :-(
# Graceful Exit
finally:
click_config.emit(message="Stopping Ursula", color='green')
emitter.message("Stopping Ursula", color='green')
ursula_config.cleanup()
click_config.emit(message="Ursula Stopped", color='red')
emitter.message("Ursula Stopped", color='red')
return
elif action == "save-metadata":
"""Manually save a node self-metadata file"""
metadata_path = ursula.write_node_metadata(node=URSULA)
return click_config.emit(message="Successfully saved node metadata to {}.".format(metadata_path), color='green')
emitter.message(f"Successfully saved node metadata to {metadata_path}.", color='green')
return
elif action == "view":
"""Paint an existing configuration to the console"""
if not URSULA.federated_only:
click.secho("BLOCKCHAIN ----------\n")
painting.paint_contract_status(click_config=click_config, ursula_config=ursula_config)
emitter.echo("BLOCKCHAIN ----------\n")
painting.paint_contract_status(emitter, ursula_config=ursula_config)
current_block = URSULA.blockchain.w3.eth.blockNumber
click.secho(f'Block # {current_block}')
click.secho(f'NU Balance: {URSULA.token_balance}')
click.secho(f'ETH Balance: {URSULA.eth_balance}')
click.secho(f'Current Gas Price {URSULA.blockchain.client.gasPrice}')
emitter.echo(f'Block # {current_block}')
emitter.echo(f'NU Balance: {URSULA.token_balance}')
emitter.echo(f'ETH Balance: {URSULA.eth_balance}')
emitter.echo(f'Current Gas Price {URSULA.blockchain.client.gasPrice}')
click.secho("CONFIGURATION --------")
emitter.echo("CONFIGURATION --------")
response = UrsulaConfiguration._read_configuration_file(filepath=config_file or ursula_config.config_file_location)
return click_config.emit(response=response)
return emitter.ipc(response=response, request_id=0, duration=0) # FIXME: what are request_id and duration here?
elif action == "forget":
actions.forget(configuration=ursula_config)
actions.forget(emitter, configuration=ursula_config)
return
elif action == 'confirm-activity':
if not URSULA.stakes:
click.secho("There are no active stakes for {}".format(URSULA.checksum_address))
emitter.echo(f"There are no active stakes for {URSULA.checksum_address}")
return
URSULA.staking_agent.confirm_activity(node_address=URSULA.checksum_address)
return

View File

@ -33,7 +33,6 @@ from nucypher.utilities.sandbox.middleware import MockRestMiddleware
class NucypherClickConfig:
# Output Sinks
capture_stdout = False
__emitter = None
# Environment Variables
@ -60,9 +59,9 @@ class NucypherClickConfig:
# Session Emitter for pre and post character control engagement.
if json_ipc:
emitter = JSONRPCStdoutEmitter(quiet=quiet, capture_stdout=NucypherClickConfig.capture_stdout)
emitter = JSONRPCStdoutEmitter(quiet=quiet)
else:
emitter = StdoutEmitter(quiet=quiet, capture_stdout=NucypherClickConfig.capture_stdout)
emitter = StdoutEmitter(quiet=quiet)
self.attach_emitter(emitter)
@ -117,22 +116,22 @@ class NucypherClickConfig:
# Only used for testing outputs;
# Redirects outputs to in-memory python containers.
if mock_networking:
self.emit(message="WARNING: Mock networking is enabled")
self.emitter.message("WARNING: Mock networking is enabled")
self.middleware = MockRestMiddleware()
else:
self.middleware = RestMiddleware()
# Global Warnings
if self.verbose:
self.emit(message="Verbose mode is enabled", color='blue')
self.emitter.message("Verbose mode is enabled", color='blue')
@classmethod
def attach_emitter(cls, emitter) -> None:
cls.__emitter = emitter
@classmethod
def emit(cls, *args, **kwargs):
cls.__emitter(*args, **kwargs)
@property
def emitter(cls):
return cls.__emitter
# Register the above click configuration classes as a decorators

View File

@ -30,6 +30,7 @@ from nucypher.blockchain.eth.registry import EthereumContractRegistry
from nucypher.blockchain.eth.sol.compile import SolidityCompiler
from nucypher.characters.banners import NU_BANNER
from nucypher.cli import actions
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.cli.actions import get_password, select_client_account
from nucypher.cli.painting import paint_contract_deployment
from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS, EXISTING_READABLE_FILE
@ -79,6 +80,8 @@ def deploy(action,
ETH_NODE = None
emitter = StdoutEmitter()
#
# Validate
#
@ -115,7 +118,7 @@ def deploy(action,
try:
blockchain.connect(fetch_registry=False, sync_now=sync)
except BlockchainDeployerInterface.ConnectionFailed as e:
click.secho(str(e), fg='red', bold=True)
emitter.echo(str(e), color='red', bold=True)
raise click.Abort()
#
@ -123,7 +126,7 @@ def deploy(action,
#
if not deployer_address:
deployer_address = select_client_account(blockchain=blockchain)
deployer_address = select_client_account(emitter=emitter, blockchain=blockchain)
# Verify Address
if not force:
@ -138,9 +141,9 @@ def deploy(action,
deployer_address=deployer_address)
# Verify ETH Balance
click.secho(f"\n\nDeployer ETH balance: {deployer.eth_balance}")
emitter.echo(f"\n\nDeployer ETH balance: {deployer.eth_balance}")
if deployer.eth_balance == 0:
click.secho("Deployer address has no ETH.", fg='red', bold=True)
emitter.echo("Deployer address has no ETH.", color='red', bold=True)
raise click.Abort()
# Add ETH Bootnode or Peer
@ -179,7 +182,7 @@ def deploy(action,
contract_deployer = deployer.deployers[contract_name]
except KeyError:
message = f"No such contract {contract_name}. Available contracts are {deployer.deployers.keys()}"
click.secho(message, fg='red', bold=True)
emitter.echo(message, color='red', bold=True)
raise click.Abort()
else:
click.secho(f"Deploying {contract_name}")
@ -208,34 +211,33 @@ def deploy(action,
#
secrets = deployer.collect_deployment_secrets()
click.clear()
click.secho(NU_BANNER)
emitter.clear()
emitter.banner(NU_BANNER)
click.secho(f"Current Time ........ {maya.now().iso8601()}")
click.secho(f"Web3 Provider ....... {deployer.blockchain.provider_uri}")
click.secho(f"Block ............... {deployer.blockchain.client.block_number}")
click.secho(f"Gas Price ........... {deployer.blockchain.client.gas_price}")
emitter.echo(f"Current Time ........ {maya.now().iso8601()}")
emitter.echo(f"Web3 Provider ....... {deployer.blockchain.provider_uri}")
emitter.echo(f"Block ............... {deployer.blockchain.client.block_number}")
emitter.echo(f"Gas Price ........... {deployer.blockchain.client.gas_price}")
click.secho(f"Deployer Address .... {deployer.checksum_address}")
click.secho(f"ETH ................. {deployer.eth_balance}")
click.secho(f"Chain ID ............ {deployer.blockchain.client.chain_id}")
click.secho(f"Chain Name .......... {deployer.blockchain.client.chain_name}")
emitter.echo(f"Deployer Address .... {deployer.checksum_address}")
emitter.echo(f"ETH ................. {deployer.eth_balance}")
emitter.echo(f"Chain ID ............ {deployer.blockchain.client.chain_id}")
emitter.echo(f"Chain Name .......... {deployer.blockchain.client.chain_name}")
# Ask - Last chance to gracefully abort. This step cannot be forced.
click.secho("\nDeployment successfully staged. Take a deep breath. \n", fg='green')
emitter.echo("\nDeployment successfully staged. Take a deep breath. \n", color='green')
# Trigger Deployment
if not actions.confirm_deployment(deployer=deployer):
if not actions.confirm_deployment(emitter=emitter, deployer=deployer):
raise click.Abort()
# Delay - Last chance to crash and abort
click.secho(f"Starting deployment in 3 seconds...", fg='red')
emitter.echo(f"Starting deployment in 3 seconds...", color='red')
time.sleep(1)
click.secho(f"2...", fg='yellow')
emitter.echo(f"2...", color='yellow')
time.sleep(1)
click.secho(f"1...", fg='green')
emitter.echo(f"1...", color='green')
time.sleep(1)
click.secho(f"Deploying...", bold=True)
emitter.echo(f"Deploying...", bold=True)
#
# DEPLOY
@ -248,13 +250,14 @@ def deploy(action,
# Paint outfile paths
# TODO: Echo total gas used.
# click.secho("Cumulative Gas Consumption: {} gas".format(total_gas_used), bold=True, fg='blue')
# emitter.echo(f"Cumulative Gas Consumption: {total_gas_used} gas", bold=True, color='blue')
registry_outfile = deployer.blockchain.registry.filepath
click.secho('Generated registry {}'.format(registry_outfile), bold=True, fg='blue')
emitter.echo('Generated registry {}'.format(registry_outfile), bold=True, color='blue')
# Save transaction metadata
receipts_filepath = deployer.save_deployment_receipts(receipts=deployment_receipts)
click.secho(f"Saved deployment receipts to {receipts_filepath}", fg='blue', bold=True)
emitter.echo(f"Saved deployment receipts to {receipts_filepath}", color='blue', bold=True)
elif action == "allocations":
if not allocation_infile:
@ -267,7 +270,7 @@ def deploy(action,
token_agent = NucypherTokenAgent(blockchain=blockchain)
click.confirm(f"Transfer {amount} from {token_agent.contract_address} to {recipient_address}?", abort=True)
txhash = token_agent.transfer(amount=amount, sender_address=token_agent.contract_address, target_address=recipient_address)
click.secho(f"OK | {txhash}")
emitter.echo(f"OK | {txhash}")
else:
raise click.BadArgumentUsage(message=f"Unknown action '{action}'")

View File

@ -24,11 +24,8 @@ from constant_sorrow.constants import NO_KNOWN_NODES
from nucypher.blockchain.eth.interfaces import BlockchainInterface
from nucypher.blockchain.eth.utils import datetime_at_period
from nucypher.characters.banners import NUCYPHER_BANNER
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:
@ -37,25 +34,25 @@ def echo_version(ctx, param, value):
ctx.exit()
def paint_new_installation_help(new_configuration):
def paint_new_installation_help(emitter, new_configuration):
character_config_class = new_configuration.__class__
character_name = character_config_class._NAME.lower()
emitter(message="Generated keyring {}".format(new_configuration.keyring_root), color='green')
emitter(message="Saved configuration file {}".format(new_configuration.config_file_location), color='green')
emitter.message("Generated keyring {}".format(new_configuration.keyring_root), color='green')
emitter.message("Saved configuration file {}".format(new_configuration.config_file_location), color='green')
# Felix
if character_name == 'felix':
suggested_db_command = 'nucypher felix createdb'
how_to_proceed_message = f'\nTo initialize a new faucet database run:'
emitter(message=how_to_proceed_message, color='green')
emitter(message=f'\n\'{suggested_db_command}\'', color='green')
emitter.message(how_to_proceed_message, color='green')
emitter.message(f'\n\'{suggested_db_command}\'', color='green')
# Ursula
elif character_name == 'ursula' and not new_configuration.federated_only:
suggested_staking_command = f'nucypher ursula stake'
how_to_stake_message = f"\nTo initialize a NU stake, run '{suggested_staking_command}' or"
emitter(message=how_to_stake_message, color='green')
emitter.message(how_to_stake_message, color='green')
# Everyone: Give the use a suggestion as to what to do next
vowels = ('a', 'e', 'i', 'o', 'u')
@ -64,7 +61,7 @@ def paint_new_installation_help(new_configuration):
suggested_command = f'nucypher {character_name} run'
how_to_run_message = f"\nTo run {adjective} {character_name.capitalize()} node from the default configuration filepath run: \n\n'{suggested_command}'\n"
return emitter(message=how_to_run_message.format(suggested_command), color='green')
emitter.message(how_to_run_message.format(suggested_command), color='green')
def build_fleet_state_status(ursula) -> str:
@ -84,7 +81,7 @@ def build_fleet_state_status(ursula) -> str:
return fleet_state
def paint_node_status(ursula, start_time):
def paint_node_status(emitter, ursula, start_time):
# Build Learning status line
learning_status = "Unknown"
@ -119,10 +116,10 @@ def paint_node_status(ursula, start_time):
current_period = f'Current Period ...... {ursula.staking_agent.get_current_period()}'
stats.extend([current_period, staking_address])
click.echo('\n' + '\n'.join(stats) + '\n')
emitter.echo('\n' + '\n'.join(stats) + '\n')
def paint_known_nodes(ursula) -> None:
def paint_known_nodes(emitter, ursula) -> None:
# Gather Data
known_nodes = ursula.known_nodes
number_of_known_nodes = len(ursula.node_storage.all(federated_only=ursula.federated_only))
@ -131,17 +128,17 @@ def paint_known_nodes(ursula) -> None:
# Operating Mode
federated_only = ursula.federated_only
if federated_only:
click.secho("Configured in Federated Only mode", fg='green')
emitter.echo("Configured in Federated Only mode", color='green')
# Heading
label = "Known Nodes (connected {} / seen {})".format(number_of_known_nodes, seen_nodes)
heading = '\n' + label + " " * (45 - len(label))
click.secho(heading, bold=True, nl=True)
emitter.echo(heading, bold=True)
# Build FleetState status line
fleet_state = build_fleet_state_status(ursula=ursula)
fleet_status_line = 'Fleet State {}'.format(fleet_state)
click.secho(fleet_status_line, fg='blue', bold=True, nl=True)
emitter.echo(fleet_status_line, color='blue', bold=True)
# Legend
color_index = {
@ -152,8 +149,8 @@ def paint_known_nodes(ursula) -> None:
# Ledgend
# for node_type, color in color_index.items():
# click.secho('{0:<6} | '.format(node_type), fg=color, nl=False)
# click.echo('\n')
# emitter.echo('{0:<6} | '.format(node_type), color=color, nl=False)
# emitter.echo('\n')
seednode_addresses = list(bn.checksum_address for bn in SEEDNODES)
@ -166,10 +163,10 @@ def paint_known_nodes(ursula) -> None:
elif node.checksum_address in seednode_addresses:
node_type = 'seednode'
row_template += ' ({})'.format(node_type)
click.secho(row_template.format(node.rest_url().ljust(20), node), fg=color_index[node_type])
emitter.echo(row_template.format(node.rest_url().ljust(20), node), color=color_index[node_type])
def paint_contract_status(ursula_config, click_config):
def paint_contract_status(emitter, ursula_config):
contract_payload = """
| NuCypher ETH Contracts |
@ -187,7 +184,7 @@ PolicyManager ............ {manager}
escrow=ursula_config.staking_agent.contract_address,
manager=ursula_config.policy_agent.contract_address,
period=ursula_config.staking_agent.get_current_period())
click.secho(contract_payload)
emitter.echo(contract_payload)
network_payload = """
| Blockchain Network |
@ -199,10 +196,11 @@ Active Staking Ursulas ... {ursulas}
""".format(period=ursula_config.staking_agent.get_current_period(),
gas_price=ursula_config.blockchain.client.gasPrice,
ursulas=ursula_config.staking_agent.get_staker_population())
click.secho(network_payload)
emitter.echo(network_payload)
def paint_staged_stake(ursula,
def paint_staged_stake(emitter,
ursula,
stake_value,
duration,
start_period,
@ -210,12 +208,12 @@ def paint_staged_stake(ursula,
division_message: str = None):
if division_message:
click.secho(f"\n{'=' * 30} ORIGINAL STAKE {'=' * 28}", bold=True)
click.secho(division_message)
emitter.echo(f"\n{'=' * 30} ORIGINAL STAKE {'=' * 28}", bold=True)
emitter.echo(division_message)
click.secho(f"\n{'=' * 30} STAGED STAKE {'=' * 30}", bold=True)
emitter.echo(f"\n{'=' * 30} STAGED STAKE {'=' * 30}", bold=True)
click.echo(f"""
emitter.echo(f"""
{ursula}
~ Chain -> ID # {ursula.blockchain.client.chain_id} | {ursula.blockchain.client.chain_name}
~ Value -> {stake_value} ({Decimal(int(stake_value)):.2E} NuNits)
@ -224,20 +222,20 @@ def paint_staged_stake(ursula,
~ Expiration -> {datetime_at_period(period=end_period)} (period #{end_period})
""")
click.secho('=========================================================================', bold=True)
emitter.echo('=========================================================================', bold=True)
def paint_staking_confirmation(ursula, transactions):
click.secho(f'\nEscrow Address ... {ursula.staking_agent.contract_address}', fg='blue')
def paint_staking_confirmation(emitter, ursula, transactions):
emitter.echo(f'\nEscrow Address ... {ursula.staking_agent.contract_address}', color='blue')
for tx_name, receipt in transactions.items():
click.secho(f'{tx_name.capitalize()} .......... {receipt["transactionHash"].hex()}', fg='green')
click.secho(f'''
emitter.echo(f'{tx_name.capitalize()} .......... {receipt["transactionHash"].hex()}', color='green')
emitter.echo(f'''
Successfully transmitted stake initialization transactions.
View your stakes by running 'nucypher stake list'
or set your Ursula worker node address by running 'nucypher stake set-worker'.
''', fg='green')
''', color='green')
def prettify_stake(stake, index: int = None) -> str:
@ -259,25 +257,25 @@ def prettify_stake(stake, index: int = None) -> str:
return pretty
def paint_stakes(stakes):
def paint_stakes(emitter, stakes):
title = "=========================== Active Stakes ==============================\n"
header = f'| ~ | Staker | Worker | # | Value | Duration | Enactment '
breaky = f'| | ------ | ------ | - | -------- | ------------ | ------------------ '
click.secho(title)
click.secho(header, bold=True)
click.secho(breaky, bold=True)
emitter.echo(title)
emitter.echo(header, bold=True)
emitter.echo(breaky, bold=True)
for index, stake in enumerate(stakes):
row = prettify_stake(stake=stake, index=index)
row_color = 'yellow' if stake.worker_address == BlockchainInterface.NULL_ADDRESS else 'white'
click.secho(row, fg=row_color)
click.secho('') # newline
return
emitter.echo(row, color=row_color)
emitter.echo('') # newline
def paint_staged_stake_division(ursula,
def paint_staged_stake_division(emitter,
ursula,
original_stake,
target_value,
extension):
@ -290,7 +288,8 @@ def paint_staged_stake_division(ursula,
~ Original Stake: {prettify_stake(stake=original_stake, index=None)}
"""
paint_staged_stake(ursula=ursula,
paint_staged_stake(emitter=emitter,
ursula=ursula,
stake_value=target_value,
duration=new_duration,
start_period=original_stake.start_period,
@ -300,6 +299,8 @@ def paint_staged_stake_division(ursula,
def paint_contract_deployment(contract_name: str, contract_address: str, receipts: dict):
# TODO: switch to using an explicit emitter
# Paint heading
heading = '\n{} ({})'.format(contract_name, contract_address)
click.secho(heading, bold=True)

View File

@ -35,10 +35,11 @@ class UrsulaCommandProtocol(LineReceiver):
encoding = 'utf-8'
delimiter = os.linesep.encode(encoding=encoding)
def __init__(self, ursula):
def __init__(self, ursula, emitter):
super().__init__()
self.ursula = ursula
self.emitter = emitter
self.start_time = maya.now()
self.__history = deque(maxlen=10)
@ -78,11 +79,11 @@ class UrsulaCommandProtocol(LineReceiver):
"""
Display this help message.
"""
click.secho("\nUrsula Command Help\n===================\n")
self.emitter.echo("\nUrsula Command Help\n===================\n")
for command, func in self.__commands.items():
if '?' not in command:
try:
click.secho(f'{command}\n{"-"*len(command)}\n{func.__doc__.lstrip()}')
self.emitter.echo(f'{command}\n{"-"*len(command)}\n{func.__doc__.lstrip()}')
except AttributeError:
raise AttributeError("Ursula Command method is missing a docstring,"
" which is required for generating help text.")
@ -92,7 +93,7 @@ class UrsulaCommandProtocol(LineReceiver):
Display a list of all known nucypher peers.
"""
from nucypher.cli.painting import paint_known_nodes
paint_known_nodes(ursula=self.ursula)
paint_known_nodes(emitter=self.emitter, ursula=self.ursula)
def paintStakes(self):
"""
@ -100,23 +101,23 @@ class UrsulaCommandProtocol(LineReceiver):
"""
from nucypher.cli.painting import paint_stakes
if self.ursula.stakes:
paint_stakes(stakes=self.ursula.stakes)
paint_stakes(self.emitter, stakes=self.ursula.stakes)
else:
click.secho("No active stakes.")
self.emitter.echo("No active stakes.")
def paintStatus(self):
"""
Display the current status of the attached Ursula node.
"""
from nucypher.cli.painting import paint_node_status
paint_node_status(ursula=self.ursula, start_time=self.start_time)
paint_node_status(emitter=self.emitter, ursula=self.ursula, start_time=self.start_time)
def paintFleetState(self):
"""
Display information about the network-wide fleet state as the attached Ursula node sees it.
"""
line = '{}'.format(build_fleet_state_status(ursula=self.ursula))
click.secho(line)
self.emitter.echo(line)
def connectionMade(self):
@ -124,10 +125,10 @@ class UrsulaCommandProtocol(LineReceiver):
self.ursula.checksum_address,
self.ursula.rest_url())
click.secho(message, fg='green')
click.secho('{} | {}'.format(self.ursula.nickname_icon, self.ursula.nickname), fg='blue', bold=True)
self.emitter.echo(message, color='green')
self.emitter.echo('{} | {}'.format(self.ursula.nickname_icon, self.ursula.nickname), color='blue', bold=True)
click.secho("\nType 'help' or '?' for help")
self.emitter.echo("\nType 'help' or '?' for help")
self.transport.write(self.prompt)
def connectionLost(self, reason=connectionDone) -> None:
@ -149,7 +150,7 @@ class UrsulaCommandProtocol(LineReceiver):
# Print
except KeyError:
if line: # allow for empty string
click.secho("Invalid input. Options are {}".format(', '.join(self.__commands.keys())))
self.emitter.echo("Invalid input. Options are {}".format(', '.join(self.__commands.keys())))
else:
self.__history.append(raw_line)

View File

@ -1,6 +1,5 @@
import click
from constant_sorrow.constants import NO_STAKING_DEVICE
from web3 import Web3
from nucypher.blockchain.eth.actors import StakeHolder
@ -69,9 +68,9 @@ def stake(click_config,
) -> None:
# Banner
if not click_config.quiet:
click.clear()
click.secho(NU_BANNER)
emitter = click_config.emitter
emitter.clear()
emitter.banner(NU_BANNER)
if action == 'new-stakeholder':
@ -93,7 +92,7 @@ def stake(click_config,
blockchain=blockchain)
filepath = new_stakeholder.to_configuration_file(override=force)
click.secho(f"Wrote new stakeholder configuration to {filepath}", fg='green')
emitter.echo(f"Wrote new stakeholder configuration to {filepath}", color='green')
return # Exit
#
@ -110,21 +109,21 @@ def stake(click_config,
if action == 'list':
if not STAKEHOLDER.stakes:
click.echo(f"There are no active stakes")
emitter.echo(f"There are no active stakes")
else:
painting.paint_stakes(stakes=STAKEHOLDER.stakes)
painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.stakes)
return
elif action == 'accounts':
for address, balances in STAKEHOLDER.account_balances.items():
click.secho(f"{address} | {Web3.fromWei(balances['ETH'], 'ether')} ETH | {NU.from_nunits(balances['NU'])}")
emitter.echo(f"{address} | {Web3.fromWei(balances['ETH'], 'ether')} ETH | {NU.from_nunits(balances['NU'])}")
return # Exit
elif action == 'sync':
click.secho("Reading on-chain stake data...")
emitter.echo("Reading on-chain stake data...")
STAKEHOLDER.read_onchain_stakes()
STAKEHOLDER.to_configuration_file(override=True)
click.secho("OK!", fg='green')
emitter.echo("OK!", color='green')
return # Exit
elif action == 'set-worker':
@ -142,7 +141,7 @@ def stake(click_config,
password=password,
worker_address=worker_address)
click.secho("OK!", fg='green')
emitter.echo("OK!", color='green')
return # Exit
elif action == 'init':
@ -181,7 +180,8 @@ def stake(click_config,
#
if not force:
painting.paint_staged_stake(ursula=STAKEHOLDER,
painting.paint_staged_stake(emitter=emitter,
ursula=STAKEHOLDER,
stake_value=value,
duration=duration,
start_period=start_period,
@ -198,7 +198,9 @@ def stake(click_config,
checksum_address=staking_address,
password=password)
painting.paint_staking_confirmation(ursula=STAKEHOLDER, transactions=new_stake.transactions)
painting.paint_staking_confirmation(emitter=emitter,
ursula=STAKEHOLDER,
transactions=new_stake.transactions)
return # Exit
elif action == 'divide':
@ -227,7 +229,8 @@ def stake(click_config,
extension = duration
if not force:
painting.paint_staged_stake_division(ursula=STAKEHOLDER,
painting.paint_staged_stake_division(emitter=emitter,
ursula=STAKEHOLDER,
original_stake=current_stake,
target_value=value,
extension=extension)
@ -243,11 +246,11 @@ def stake(click_config,
duration=extension,
password=password)
if not click_config.quiet:
click.secho('Successfully divided stake', fg='green')
click.secho(f'Receipt ........... {new_stake.receipt}')
emitter.echo('Successfully divided stake', color='green')
emitter.echo(f'Receipt ........... {new_stake.receipt}')
# Show the resulting stake list
painting.paint_stakes(stakes=STAKEHOLDER.stakes)
painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.stakes)
return # Exit
elif action == 'collect-reward':

View File

@ -42,7 +42,7 @@ def status(click_config, config_file):
ursula_config.acquire_agency()
# Contracts
paint_contract_status(ursula_config=ursula_config, click_config=click_config)
paint_contract_status(click_config.emitter, ursula_config=ursula_config, click_config=click_config)
# Known Nodes
paint_known_nodes(ursula=ursula_config)
paint_known_nodes(emitter=click_config.emitter, ursula=ursula_config)

View File

@ -4,6 +4,7 @@ from contextlib import contextmanager
import pytest
from io import StringIO
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.cli.config import NucypherClickConfig
from nucypher.cli.processes import UrsulaCommandProtocol
@ -31,13 +32,15 @@ def ursula(federated_ursulas):
@pytest.fixture(scope='module')
def protocol(ursula):
protocol = UrsulaCommandProtocol(ursula=ursula)
emitter = StdoutEmitter()
protocol = UrsulaCommandProtocol(ursula=ursula, emitter=emitter)
return protocol
def test_ursula_command_protocol_creation(ursula):
protocol = UrsulaCommandProtocol(ursula=ursula)
emitter = StdoutEmitter()
protocol = UrsulaCommandProtocol(ursula=ursula, emitter=emitter)
assert protocol.ursula == ursula
assert b'Ursula' in protocol.prompt