Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] json output for commands #369

Draft
wants to merge 15 commits into
base: staging
Choose a base branch
from
32 changes: 23 additions & 9 deletions bittensor_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@
import rich
import typer
import numpy as np
from async_substrate_interface.errors import SubstrateRequestException
from bittensor_wallet import Wallet
from rich import box
from rich.prompt import Confirm, FloatPrompt, Prompt, IntPrompt
from rich.table import Column, Table
from rich.tree import Tree
from typing_extensions import Annotated
from websockets import ConnectionClosed, InvalidHandshake
from yaml import safe_dump, safe_load

from bittensor_cli.src import (
defaults,
HELP_PANELS,
Expand All @@ -31,7 +36,6 @@
from bittensor_cli.version import __version__, __version_as_int__
from bittensor_cli.src.bittensor import utils
from bittensor_cli.src.bittensor.balances import Balance
from async_substrate_interface.errors import SubstrateRequestException
from bittensor_cli.src.commands import sudo, wallets, view
from bittensor_cli.src.commands import weights as weights_cmds
from bittensor_cli.src.commands.subnets import price, subnets
Expand Down Expand Up @@ -61,9 +65,6 @@
is_linux,
validate_rate_tolerance,
)
from typing_extensions import Annotated
from websockets import ConnectionClosed, InvalidHandshake
from yaml import safe_dump, safe_load

try:
from git import Repo, GitError
Expand Down Expand Up @@ -267,6 +268,12 @@ class Options:
"--dashboard.path",
help="Path to save the dashboard HTML file. For example: `~/.bittensor/dashboard`.",
)
json_output = typer.Option(
False,
"--json-output",
"--json-out",
help="Outputs the result of the command as JSON.",
)


def list_prompt(init_var: list, list_type: type, help_text: str) -> list:
Expand Down Expand Up @@ -933,6 +940,7 @@ def initialize_chain(
"""
if not self.subtensor:
if network:
network_ = None
for item in network:
if item.startswith("ws"):
network_ = item
Expand All @@ -950,7 +958,8 @@ def initialize_chain(
elif self.config["network"]:
self.subtensor = SubtensorInterface(self.config["network"])
console.print(
f"Using the specified network [{COLOR_PALETTE['GENERAL']['LINKS']}]{self.config['network']}[/{COLOR_PALETTE['GENERAL']['LINKS']}] from config"
f"Using the specified network [{COLOR_PALETTE['GENERAL']['LINKS']}]{self.config['network']}"
f"[/{COLOR_PALETTE['GENERAL']['LINKS']}] from config"
)
else:
self.subtensor = SubtensorInterface(defaults.subtensor.network)
Expand Down Expand Up @@ -1201,7 +1210,8 @@ def set_config(
elif arg == "rate_tolerance":
while True:
val = FloatPrompt.ask(
f"What percentage would you like to set for [red]{arg}[/red]?\nValues are percentages (e.g. 0.05 for 5%)",
f"What percentage would you like to set for [red]{arg}[/red]?\n"
f"Values are percentages (e.g. 0.05 for 5%)",
default=0.05,
)
try:
Expand Down Expand Up @@ -1501,7 +1511,7 @@ def wallet_ask(
wallet_name: Optional[str],
wallet_path: Optional[str],
wallet_hotkey: Optional[str],
ask_for: list[str] = [],
ask_for: list[str] = None,
validate: WV = WV.WALLET,
) -> Wallet:
"""
Expand All @@ -1510,9 +1520,10 @@ def wallet_ask(
:param wallet_path: root path of the wallets
:param wallet_hotkey: name of the wallet hotkey file
:param validate: flag whether to check for the wallet's validity
:param ask_type: aspect of the wallet (name, path, hotkey) to prompt the user for
:param ask_for: aspect of the wallet (name, path, hotkey) to prompt the user for
:return: created Wallet object
"""
ask_for = ask_for or []
# Prompt for missing attributes specified in ask_for
if WO.NAME in ask_for and not wallet_name:
if self.config.get("wallet_name"):
Expand Down Expand Up @@ -1585,6 +1596,7 @@ def wallet_list(
wallet_path: str = Options.wallet_path,
quiet: bool = Options.quiet,
verbose: bool = Options.verbose,
json_output: bool = Options.json_output,
):
"""
Displays all the wallets and their corresponding hotkeys that are located in the wallet path specified in the config.
Expand All @@ -1604,7 +1616,7 @@ def wallet_list(
wallet = self.wallet_ask(
None, wallet_path, None, ask_for=[WO.PATH], validate=WV.NONE
)
return self._run_command(wallets.wallet_list(wallet.path))
return self._run_command(wallets.wallet_list(wallet.path, json_output))

def wallet_overview(
self,
Expand Down Expand Up @@ -1644,6 +1656,7 @@ def wallet_overview(
network: Optional[list[str]] = Options.network,
quiet: bool = Options.quiet,
verbose: bool = Options.verbose,
json_output: bool = Options.json_output,
):
"""
Displays a detailed overview of the user's registered accounts on the Bittensor network.
Expand Down Expand Up @@ -1705,6 +1718,7 @@ def wallet_overview(
exclude_hotkeys,
netuids_filter=netuids,
verbose=verbose,
json_output=json_output,
)
)

Expand Down
112 changes: 93 additions & 19 deletions bittensor_cli/src/commands/wallets.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import itertools
import json
import os
from collections import defaultdict
from typing import Generator, Optional
Expand Down Expand Up @@ -525,14 +526,15 @@ async def wallet_history(wallet: Wallet):
console.print(table)


async def wallet_list(wallet_path: str):
async def wallet_list(wallet_path: str, json_output: bool):
"""Lists wallets."""
wallets = utils.get_coldkey_wallets_for_path(wallet_path)
print_verbose(f"Using wallets path: {wallet_path}")
if not wallets:
err_console.print(f"[red]No wallets found in dir: {wallet_path}[/red]")

root = Tree("Wallets")
main_data_dict = {"wallets": []}
for wallet in wallets:
if (
wallet.coldkeypub_file.exists_on_device()
Expand All @@ -545,23 +547,40 @@ async def wallet_list(wallet_path: str):
wallet_tree = root.add(
f"[bold blue]Coldkey[/bold blue] [green]{wallet.name}[/green] ss58_address [green]{coldkeypub_str}[/green]"
)
wallet_hotkeys = []
wallet_dict = {
"name": wallet.name,
"ss58_address": coldkeypub_str,
"hotkeys": wallet_hotkeys,
}
main_data_dict["wallets"].append(wallet_dict)
hotkeys = utils.get_hotkey_wallets_for_wallet(
wallet, show_nulls=True, show_encrypted=True
)
for hkey in hotkeys:
data = f"[bold red]Hotkey[/bold red][green] {hkey}[/green] (?)"
hk_data = {"name": hkey.name, "ss58_address": "?"}
if hkey:
try:
data = f"[bold red]Hotkey[/bold red] [green]{hkey.hotkey_str}[/green] ss58_address [green]{hkey.hotkey.ss58_address}[/green]\n"
data = (
f"[bold red]Hotkey[/bold red] [green]{hkey.hotkey_str}[/green] "
f"ss58_address [green]{hkey.hotkey.ss58_address}[/green]\n"
)
hk_data["name"] = hkey.hotkey_str
hk_data["ss58_address"] = hkey.hotkey.ss58_address
except UnicodeDecodeError:
pass
wallet_tree.add(data)
wallet_hotkeys.append(hk_data)

if not wallets:
print_verbose(f"No wallets found in path: {wallet_path}")
root.add("[bold red]No wallets found.")

console.print(root)
if json_output:
console.print("[JSON_OUTPUT]")
console.print(json.dumps(main_data_dict))
else:
console.print(root)


async def _get_total_balance(
Expand Down Expand Up @@ -638,23 +657,33 @@ async def overview(
exclude_hotkeys: Optional[list[str]] = None,
netuids_filter: Optional[list[int]] = None,
verbose: bool = False,
json_output: bool = False,
):
"""Prints an overview for the wallet's coldkey."""

total_balance = Balance(0)

# We are printing for every coldkey.
block_hash = await subtensor.substrate.get_chain_head()
all_hotkeys, total_balance = await _get_total_balance(
total_balance, subtensor, wallet, all_wallets, block_hash=block_hash
)
_dynamic_info = await subtensor.all_subnets()
dynamic_info = {info.netuid: info for info in _dynamic_info}

with console.status(
f":satellite: Synchronizing with chain [white]{subtensor.network}[/white]",
spinner="aesthetic",
) as status:
# We are printing for every coldkey.
block_hash = await subtensor.substrate.get_chain_head()
(
(all_hotkeys, total_balance),
_dynamic_info,
block,
all_netuids,
) = await asyncio.gather(
_get_total_balance(
total_balance, subtensor, wallet, all_wallets, block_hash=block_hash
),
subtensor.all_subnets(block_hash=block_hash),
subtensor.substrate.get_block_number(block_hash=block_hash),
subtensor.get_all_subnet_netuids(block_hash=block_hash),
)
dynamic_info = {info.netuid: info for info in _dynamic_info}

# We are printing for a select number of hotkeys from all_hotkeys.
if include_hotkeys or exclude_hotkeys:
all_hotkeys = _get_hotkeys(include_hotkeys, exclude_hotkeys, all_hotkeys)
Expand All @@ -666,10 +695,6 @@ async def overview(

# Pull neuron info for all keys.
neurons: dict[str, list[NeuronInfoLite]] = {}
block, all_netuids = await asyncio.gather(
subtensor.substrate.get_block_number(None),
subtensor.get_all_subnet_netuids(),
)

netuids = await subtensor.filter_netuids_by_registered_hotkeys(
all_netuids, netuids_filter, all_hotkeys, reuse_block=True
Expand Down Expand Up @@ -704,16 +729,27 @@ async def overview(
neurons = _process_neuron_results(results, neurons, netuids)
# Setup outer table.
grid = Table.grid(pad_edge=True)
data_dict = {
"wallet": "",
"network": subtensor.network,
"subnets": [],
"total_balance": 0.0,
}

# Add title
if not all_wallets:
title = "[underline dark_orange]Wallet[/underline dark_orange]\n"
details = f"[bright_cyan]{wallet.name}[/bright_cyan] : [bright_magenta]{wallet.coldkeypub.ss58_address}[/bright_magenta]"
details = (
f"[bright_cyan]{wallet.name}[/bright_cyan] : "
f"[bright_magenta]{wallet.coldkeypub.ss58_address}[/bright_magenta]"
)
grid.add_row(Align(title, vertical="middle", align="center"))
grid.add_row(Align(details, vertical="middle", align="center"))
data_dict["wallet"] = f"{wallet.name}|{wallet.coldkeypub.ss58_address}"
else:
title = "[underline dark_orange]All Wallets:[/underline dark_orange]"
grid.add_row(Align(title, vertical="middle", align="center"))
data_dict["wallet"] = "All"

grid.add_row(
Align(
Expand All @@ -731,6 +767,14 @@ async def overview(
)
for netuid, subnet_tempo in zip(netuids, tempos):
table_data = []
subnet_dict = {
"netuid": netuid,
"tempo": subnet_tempo,
"neurons": [],
"name": "",
"symbol": "",
}
data_dict["subnets"].append(subnet_dict)
total_rank = 0.0
total_trust = 0.0
total_consensus = 0.0
Expand Down Expand Up @@ -785,6 +829,26 @@ async def overview(
),
nn.hotkey[:10],
]
neuron_dict = {
"coldkey": hotwallet.name,
"hotkey": hotwallet.hotkey_str,
"uid": uid,
"active": active,
"stake": stake,
"rank": rank,
"trust": trust,
"consensus": consensus,
"incentive": incentive,
"dividends": dividends,
"emission": emission,
"validator_trust": validator_trust,
"validator_permit": validator_permit,
"last_update": last_update,
"axon": int_to_ip(nn.axon_info.ip) + ":" + str(nn.axon_info.port)
if nn.axon_info.port != 0
else None,
"hotkey_ss58": nn.hotkey,
}

total_rank += rank
total_trust += trust
Expand All @@ -797,11 +861,16 @@ async def overview(
total_neurons += 1

table_data.append(row)
subnet_dict["neurons"].append(neuron_dict)

# Add subnet header
sn_name = get_subnet_name(dynamic_info[netuid])
sn_symbol = dynamic_info[netuid].symbol
grid.add_row(
f"Subnet: [dark_orange]{netuid}: {get_subnet_name(dynamic_info[netuid])} {dynamic_info[netuid].symbol}[/dark_orange]"
f"Subnet: [dark_orange]{netuid}: {sn_name} {sn_symbol}[/dark_orange]"
)
subnet_dict["name"] = sn_name
subnet_dict["symbol"] = sn_symbol
width = console.width
table = Table(
show_footer=False,
Expand Down Expand Up @@ -936,14 +1005,19 @@ def overview_sort_function(row_):
caption = "\n[italic][dim][bright_cyan]Wallet balance: [dark_orange]\u03c4" + str(
total_balance.tao
)
data_dict["total_balance"] = total_balance.tao
grid.add_row(Align(caption, vertical="middle", align="center"))

if console.width < 150:
console.print(
"[yellow]Warning: Your terminal width might be too small to view all information clearly"
)
# Print the entire table/grid
console.print(grid, width=None)
if not json_output:
console.print(grid, width=None)
else:
console.print("[JSON_OUTPUT]")
console.print(json.dumps(data_dict))


def _get_hotkeys(
Expand Down
1 change: 1 addition & 0 deletions bittensor_cli/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ def version_as_int(version):
__new_signature_version__ = 360
return __version_as_int__


__version__ = "9.1.1"
__version_as_int__ = version_as_int(__version__)