commit
07a6530764
@ -0,0 +1,14 @@ |
||||
# |
||||
# This package relies on the fact that there is a CLI binary from some path |
||||
# starting at the root of the file script importing this package. |
||||
# This package requires the pexpect package: https://pypi.org/project/pexpect/ . |
||||
# |
||||
|
||||
from pyhmy.cli import HmyCLI, get_environment |
||||
import os |
||||
|
||||
# Find the CLI binary for HmyCLI. |
||||
for root, dirs, files in os.walk(os.path.curdir): |
||||
if "hmy" in files: |
||||
HmyCLI.hmy_binary_path = os.path.join(root, "hmy") |
||||
break |
@ -0,0 +1,164 @@ |
||||
import subprocess |
||||
import pexpect |
||||
import json |
||||
import os |
||||
import shutil |
||||
import re |
||||
|
||||
|
||||
def get_environment() -> dict: |
||||
""" |
||||
Fetches the environment variables from the 'setup_bls_build_flags.sh' script |
||||
in the harmony main repo. Also fetches the 'HOME' environment variable for HmyCLI. |
||||
""" |
||||
go_path = subprocess.check_output(["go", "env", "GOPATH"]).decode().strip() |
||||
setup_script_path = f"{go_path}/src/github.com/harmony-one/harmony/scripts/setup_bls_build_flags.sh" |
||||
response = subprocess.check_output(["bash", setup_script_path, "-v"], timeout=5) |
||||
environment = json.loads(response) |
||||
environment["HOME"] = os.environ.get("HOME") |
||||
return environment |
||||
|
||||
|
||||
class HmyCLI: |
||||
hmy_binary_path = None # This class attr should be set by the __init__.py of this module. |
||||
|
||||
def __init__(self, environment, hmy_binary_path=None): |
||||
""" |
||||
:param environment: Dictionary of environment variables to be used in the CLI |
||||
:param hmy_binary_path: An optional path to the harmony binary; defaults to |
||||
class attribute. |
||||
""" |
||||
if hmy_binary_path: |
||||
assert os.path.isfile(hmy_binary_path) |
||||
self.hmy_binary_path = hmy_binary_path.replace("./", "") |
||||
if not self.hmy_binary_path: |
||||
raise FileNotFoundError("Path to harmony CLI binary is not found") |
||||
self.environment = environment |
||||
self.version = "" |
||||
self.keystore_path = "" |
||||
self._addresses = {} |
||||
self._set_version() |
||||
self._set_keystore_path() |
||||
self._sync_addresses() |
||||
|
||||
def __repr__(self) -> str: |
||||
return f"<{self.version} @ {self.hmy_binary_path}>" |
||||
|
||||
def _set_version(self) -> None: |
||||
""" |
||||
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) -> None: |
||||
""" |
||||
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) -> None: |
||||
""" |
||||
Internal method to sync this instance's address with the binary's keystore 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(f"Name or Address not found on first line of key list") |
||||
for line in lines[1:]: |
||||
if line: |
||||
columns = line.split("\t") |
||||
if len(columns) != 2: |
||||
raise ValueError("Unexpected format of keys list") |
||||
name, address = columns |
||||
self._addresses[name.strip()] = address |
||||
|
||||
def check_address(self, address) -> bool: |
||||
""" |
||||
:param address: A 'one1...' address. |
||||
:return: T/F If the address is in the CLI's keystore. |
||||
""" |
||||
self._sync_addresses() |
||||
return address in self._addresses.values() |
||||
|
||||
def get_address(self, name) -> str: |
||||
""" |
||||
: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) -> list: |
||||
""" |
||||
: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) -> None: |
||||
""" |
||||
:param name: The alias of a key used in the CLI's keystore. |
||||
""" |
||||
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) -> None: |
||||
""" |
||||
:param address: The 'one1...' address to be removed. |
||||
""" |
||||
for name in self.get_names(address): |
||||
self.remove_account(name) |
||||
|
||||
def single_call(self, command, timeout=30) -> str: |
||||
""" |
||||
: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=30) -> pexpect.pty_spawn.spawn: |
||||
""" |
||||
: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 |
@ -0,0 +1,17 @@ |
||||
import pathlib |
||||
from setuptools import setup |
||||
|
||||
HERE = pathlib.Path(__file__).parent |
||||
README = (HERE / "README.md").read_text() |
||||
|
||||
setup( |
||||
name='pyhmy', |
||||
version='0.11', |
||||
long_description=README, |
||||
long_description_content_type="text/markdown", |
||||
author='Daniel Van Der Maden', |
||||
author_email='daniel@harmony.one', |
||||
url="http://harmony.one/", |
||||
packages=['pyhmy'], |
||||
install_requires=['pexpect'], |
||||
) |
Loading…
Reference in new issue