[cli] BREAKING CHANGE - Refactor to static module

pull/2/head
Daniel Van Der Maden 5 years ago
parent 162abebc77
commit 08991c1a48
  1. 360
      pyhmy/cli.py

@ -4,187 +4,179 @@ import os
import shutil
import re
from .util import get_bls_build_variables
DEFAULT_BIN_FILENAME = 'hmy'
def get_environment():
"""
:returns All the environment variables needed to run the CLI with dynamic linking.
Note that this assumes that the BLS & MCL repo are in the appropriate directory
as stated here: https://github.com/harmony-one/harmony/blob/master/README.md
"""
environment = {"HOME": os.environ.get("HOME")}
environment.update(get_bls_build_variables())
return environment
class HmyCLI:
def __init__(self, environment, hmy_binary_path=None):
"""
:param environment: Dictionary of environment variables to be used when calling the CLI.
:param hmy_binary_path: The optional path to the CLI binary.
"""
self.environment = environment
self.hmy_binary_path = ""
self.version = ""
self.keystore_path = ""
self._addresses = {}
if hmy_binary_path:
assert os.path.isfile(hmy_binary_path), f"{hmy_binary_path} is not a file"
self.hmy_binary_path = hmy_binary_path
else:
self._set_default_hmy_binary_path()
self._set_version()
self._set_keystore_path()
self._sync_addresses()
def __repr__(self):
return f"<{self.version} @ {self.hmy_binary_path}>"
def _set_default_hmy_binary_path(self, file_name=DEFAULT_BIN_FILENAME):
"""
Internal method to set the binary path by looking for the first file with
the same name as the param in the current working directory.
:param file_name: The file name to look for.
"""
assert '/' not in file_name, "file name must not be a path."
for root, dirs, files in os.walk(os.getcwd()):
if file_name in files:
self.hmy_binary_path = os.path.join(root, file_name)
break
assert self.hmy_binary_path, f"CLI binary `{file_name}` not found in current working directory."
def _set_version(self):
"""
Internal method to set this instance's version according to the binary's version.
"""
proc = subprocess.Popen([self.hmy_binary_path, "version"], env=self.environment,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate()
if not err:
raise RuntimeError(f"Could not get version.\n"
f"\tGot exit code {proc.returncode}. Expected non-empty error message.")
self.version = err.decode().strip()
def _set_keystore_path(self):
"""
Internal method to set this instance's keystore path with the binary's keystore path.
"""
response = self.single_call("hmy keys location").strip()
if not os.path.exists(response):
os.mkdir(response)
self.keystore_path = response
def _sync_addresses(self):
"""
Internal method to sync this instance's address with the binary's keystore addresses.
"""
addresses = {}
response = self.single_call("hmy keys list")
lines = response.split("\n")
if "NAME" not in lines[0] or "ADDRESS" not in lines[0]:
raise ValueError("Name or Address not found on first line of key list")
if lines[1] != "":
raise ValueError("Unknown format: No blank line between label and data")
for line in lines[2:]:
columns = line.split("\t")
if len(columns) != 2:
break # Done iterating through all of the addresses.
name, address = columns
addresses[name.strip()] = address
self._addresses = addresses
def check_address(self, address):
"""
:param address: A 'one1...' address.
:return: Boolean of if the address is in the CLI's keystore.
"""
if address in self._addresses.values():
return True
else:
self._sync_addresses()
return address in self._addresses.values()
def get_address(self, name):
"""
:param name: The alias of a key used in the CLI's keystore.
:return: The associated 'one1...' address.
"""
if name in self._addresses:
return self._addresses[name]
else:
self._sync_addresses()
return self._addresses.get(name, None)
def get_accounts(self, address):
"""
:param address: The 'one1...' address
:return: A list of account names associated with
"""
self._sync_addresses()
return [acc for acc, addr in self._addresses.items() if address == addr]
def remove_account(self, name):
"""
Note that this edits the keystore directly since there is currently no
way to remove an address using the CLI.
:param name: The alias of a key used in the CLI's keystore.
:raises RuntimeError: If it failed to remove an account.
"""
if not self.get_address(name):
return
try:
shutil.rmtree(f"{self.keystore_path}/{name}")
except (shutil.Error, FileNotFoundError) as err:
raise RuntimeError(f"Failed to delete dir: {self.keystore_path}/{name}\n"
f"\tException: {err}") from err
del self._addresses[name]
def remove_address(self, address):
"""
:param address: The 'one1...' address to be removed.
"""
for name in self.get_accounts(address):
self.remove_account(name)
def single_call(self, command, timeout=60):
"""
:param command: String fo command to execute on CLI
:param timeout: Optional timeout in seconds
:returns: Decoded string of response from hmy CLI call
:raises: RuntimeError if bad command
"""
command_toks = command.split(" ")
if re.match(".*hmy", command_toks[0]):
command_toks = command_toks[1:]
command_toks = [self.hmy_binary_path] + command_toks
try:
response = subprocess.check_output(command_toks, env=self.environment, timeout=timeout).decode()
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as err:
raise RuntimeError(f"Bad arguments for CLI.\n "
f"\tException: {err}") from err
return response
def expect_call(self, command, timeout=60):
"""
:param command: String fo command to execute on CLI
:param timeout: Optional timeout in seconds
:return: A pexpect child program
:raises: RuntimeError if bad command
"""
command_toks = command.split(" ")
if re.match(".*hmy", command_toks[0]):
command_toks = command_toks[1:]
try:
proc = pexpect.spawn(f"{self.hmy_binary_path}", command_toks, env=self.environment, timeout=timeout)
except (pexpect.ExceptionPexpect, pexpect.TIMEOUT) as err:
raise RuntimeError(f"Bad arguments for CLI.\n "
f"\tException: {err}") from err
return proc
from .util import get_bls_build_variables, get_gopath
_addresses = {} # Internal address keystore, not guaranteed to be up-to-date unless needed.
_binary_path = "hmy" # Internal binary path.
_environment = os.environ.copy() # Internal environment dict for Subprocess & Pexpect.
def _get_default_hmy_binary_path(file_name="hmy"):
"""
Internal function to get the binary path by looking for the first file with
the same name as the param in the current working directory.
:param file_name: The file name to look for.
"""
assert '/' not in file_name, "file name must not be a path."
for root, dirs, files in os.walk(os.getcwd()):
if file_name in files:
return os.path.join(root, file_name)
return ""
def _sync_addresses():
"""
Internal function to sync address with the binary's keystore addresses.
"""
global _addresses
curr_addresses = {}
response = single_call("hmy keys list")
lines = response.split("\n")
if "NAME" not in lines[0] or "ADDRESS" not in lines[0]:
raise ValueError("Name or Address not found on first line of key list")
if lines[1] != "":
raise ValueError("Unknown format: No blank line between label and data")
for line in lines[2:]:
columns = line.split("\t")
if len(columns) != 2:
break # Done iterating through all of the addresses.
name, address = columns
curr_addresses[name.strip()] = address
_addresses = curr_addresses
def set_binary_path(path):
"""
:param path: The path of the CLI binary to use.
"""
global _binary_path
assert os.path.isfile(path), f"`{path}` is not a file"
_binary_path = path
_sync_addresses()
def get_version():
"""
:return: The version string of the CLI binary.
"""
proc = subprocess.Popen([_binary_path, "version"], env=_environment,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate()
if not err:
raise RuntimeError(f"Could not get version.\n"
f"\tGot exit code {proc.returncode}. Expected non-empty error message.")
return err.decode().strip()
def get_account_keystore_path():
"""
:return: The account keystore path of the CLI binary.
"""
response = single_call("hmy keys location").strip()
if not os.path.exists(response):
os.mkdir(response)
return response
def check_address(address):
"""
:param address: A 'one1...' address.
:return: Boolean of if the address is in the CLI's keystore.
"""
if address in _addresses.values():
return True
else:
_sync_addresses()
return address in _addresses.values()
def get_address(name):
"""
:param name: The alias of a key used in the CLI's keystore.
:return: The associated 'one1...' address.
"""
if name in _addresses:
return _addresses[name]
else:
_sync_addresses()
return _addresses.get(name, None)
def get_accounts(address):
"""
:param address: The 'one1...' address
:return: A list of account names associated with the param
"""
_sync_addresses()
return [acc for acc, addr in _addresses.items() if address == addr]
def remove_account(name):
"""
Note that this edits the keystore directly since there is currently no
way to remove an address using the CLI.
:param name: The alias of a key used in the CLI's keystore.
:raises RuntimeError: If it failed to remove an account.
"""
if not get_address(name):
return
keystore_path = f"{get_account_keystore_path()}/{name}"
try:
shutil.rmtree(keystore_path)
except (shutil.Error, FileNotFoundError) as err:
raise RuntimeError(f"Failed to delete dir: {keystore_path}\n"
f"\tException: {err}") from err
del _addresses[name]
def remove_address(address):
"""
:param address: The 'one1...' address to be removed.
"""
for name in get_accounts(address):
remove_account(name)
def single_call(command, timeout=60):
"""
:param command: String of command to execute on CLI
:param timeout: Optional timeout in seconds
:returns: Decoded string of response from hmy CLI call
:raises: RuntimeError if bad command
"""
command_toks = command.split(" ")
if re.match(".*hmy", command_toks[0]):
command_toks = command_toks[1:]
command_toks = [_binary_path] + command_toks
try:
response = subprocess.check_output(command_toks, env=_environment, timeout=timeout).decode()
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as err:
raise RuntimeError(f"Bad arguments for CLI.\n "
f"\tException: {err}") from err
return response
def expect_call(command, timeout=60):
"""
:param command: String fo command to execute on CLI
:param timeout: Optional timeout in seconds
:return: A pexpect child program
:raises: RuntimeError if bad command
"""
command_toks = command.split(" ")
if re.match(".*hmy", command_toks[0]):
command_toks = command_toks[1:]
try:
proc = pexpect.spawn(f"{_binary_path}", command_toks, env=_environment, timeout=timeout)
except (pexpect.ExceptionPexpect, pexpect.TIMEOUT) as err:
raise RuntimeError(f"Bad arguments for CLI.\n "
f"\tException: {err}") from err
return proc
_binary_path = _get_default_hmy_binary_path()
if os.path.exists(f"{get_gopath()}/src/github.com/harmony-one/bls") \
and os.path.exists(f"{get_gopath()}/src/github.com/harmony-one/mcl"): # Check prevents needless import fail.
_environment.update(get_bls_build_variables()) # Needed if using dynamically linked CLI binary.

Loading…
Cancel
Save