From a3b209be9f886c2e72df37bc07957d6a5a345d89 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 26 Aug 2024 19:16:44 +0200 Subject: [PATCH 1/8] Initial support for HTML table outputs --- cli.py | 22 +- requirements.txt | 1 + src/bittensor/async_substrate_interface.py | 4 +- src/commands/subnets.py | 379 ++++++++++++--------- src/commands/wallets.py | 2 +- src/templates/table.j2 | 117 +++++++ src/utils.py | 55 +++ 7 files changed, 410 insertions(+), 170 deletions(-) create mode 100644 src/templates/table.j2 diff --git a/cli.py b/cli.py index 3047c517..0b589a8e 100755 --- a/cli.py +++ b/cli.py @@ -1167,9 +1167,9 @@ def wallet_create_wallet( wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hk_req, n_words: Optional[int] = None, - use_password: Optional[bool] = Options.use_password, - overwrite_hotkey: Optional[bool] = Options.overwrite_hotkey, - overwrite_coldkey: Optional[bool] = Options.overwrite_coldkey, + use_password: bool = Options.use_password, + overwrite_hotkey: bool = Options.overwrite_hotkey, + overwrite_coldkey: bool = Options.overwrite_coldkey, ): """ # wallet create @@ -3208,6 +3208,16 @@ def subnets_metagraph( netuid: int = Options.netuid, network: str = Options.network, chain: str = Options.chain, + reuse_last: bool = typer.Option( + False, + help="Reuse the metagraph data you last retrieved. Only use this if you have already retrieved metagraph" + "data", + ), + html_output: bool = typer.Option( + False, + "--html", + help="Display the table as HTML in the browser, rather than in the Terminal.", + ), ): """ Executes the `metagraph` command to retrieve and display the entire metagraph for a specified network. @@ -3269,8 +3279,12 @@ def subnets_metagraph( It is useful for network analysis and diagnostics. It is intended to be used as part of the Bittensor CLI and not as a standalone function within user code. """ + if reuse_last: + subtensor = None + else: + subtensor = self.initialize_chain(network, chain) return self._run_command( - subnets.metagraph_cmd(self.initialize_chain(network, chain), netuid) + subnets.metagraph_cmd(subtensor, netuid, reuse_last, html_output) ) def weights_reveal( diff --git a/requirements.txt b/requirements.txt index 05d0853c..e8f5a97c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ GitPython>=3.0.0 fuzzywuzzy~=0.18.0 netaddr~=1.3.0 numpy>=2.0.1 +Jinja2 pycryptodome # Crypto PyYAML~=6.0.1 rich~=13.7 diff --git a/src/bittensor/async_substrate_interface.py b/src/bittensor/async_substrate_interface.py index 57988aa0..10911989 100644 --- a/src/bittensor/async_substrate_interface.py +++ b/src/bittensor/async_substrate_interface.py @@ -1345,9 +1345,7 @@ async def result_handler(message: dict, subscription_id) -> tuple[dict, bool]: ): # Created as a task because we don't actually care about the result self._forgettable_task = asyncio.create_task( - self.rpc_request( - "author_unwatchExtrinsic", [subscription_id] - ) + self.rpc_request("author_unwatchExtrinsic", [subscription_id]) ) return { "block_hash": message_result["inblock"], diff --git a/src/commands/subnets.py b/src/commands/subnets.py index bcd769e7..8aaefbbe 100644 --- a/src/commands/subnets.py +++ b/src/commands/subnets.py @@ -19,6 +19,8 @@ millify, RAO_PER_TAO, format_error_message, + render_table, + create_table, ) if TYPE_CHECKING: @@ -347,168 +349,221 @@ async def register(wallet: Wallet, subtensor: "SubtensorInterface", netuid: int) ) -async def metagraph_cmd(subtensor: "SubtensorInterface", netuid: int): +async def metagraph_cmd( + subtensor: "SubtensorInterface", netuid: int, reuse_last: bool, html_output: bool +): """Prints an entire metagraph.""" - console.print( - f":satellite: Syncing with chain: [white]{subtensor.network}[/white] ..." - ) - block_hash = await subtensor.substrate.get_chain_head() - neurons, difficulty_, total_issuance_, block = await asyncio.gather( - subtensor.neurons(netuid, block_hash=block_hash), - subtensor.get_hyperparameter( - param_name="Difficulty", netuid=netuid, block_hash=block_hash - ), - subtensor.substrate.query( - module="SubtensorModule", - storage_function="TotalIssuance", - params=[], - block_hash=block_hash, - ), - subtensor.substrate.get_block_number(block_hash=block_hash), - ) + if not reuse_last: + with console.status( + f":satellite: Syncing with chain: [white]{subtensor.network}[/white] ..." + ): + block_hash = await subtensor.substrate.get_chain_head() + neurons, difficulty_, total_issuance_, block = await asyncio.gather( + subtensor.neurons(netuid, block_hash=block_hash), + subtensor.get_hyperparameter( + param_name="Difficulty", netuid=netuid, block_hash=block_hash + ), + subtensor.substrate.query( + module="SubtensorModule", + storage_function="TotalIssuance", + params=[], + block_hash=block_hash, + ), + subtensor.substrate.get_block_number(block_hash=block_hash), + ) - difficulty = int(difficulty_) - total_issuance = Balance.from_rao(total_issuance_.value) - metagraph = MiniGraph( - netuid=netuid, neurons=neurons, subtensor=subtensor, block=block - ) - # metagraph.save() TODO maybe? - table_data = [] - total_stake = 0.0 - total_rank = 0.0 - total_validator_trust = 0.0 - total_trust = 0.0 - total_consensus = 0.0 - total_incentive = 0.0 - total_dividends = 0.0 - total_emission = 0 - for uid in metagraph.uids: - neuron = metagraph.neurons[uid] - ep = metagraph.axons[uid] - row = [ - str(neuron.uid), - "{:.5f}".format(metagraph.total_stake[uid]), - "{:.5f}".format(metagraph.ranks[uid]), - "{:.5f}".format(metagraph.trust[uid]), - "{:.5f}".format(metagraph.consensus[uid]), - "{:.5f}".format(metagraph.incentive[uid]), - "{:.5f}".format(metagraph.dividends[uid]), - "{}".format(int(metagraph.emission[uid] * 1000000000)), - "{:.5f}".format(metagraph.validator_trust[uid]), - "*" if metagraph.validator_permit[uid] else "", - str((metagraph.block.item() - metagraph.last_update[uid].item())), - str(metagraph.active[uid].item()), - (ep.ip + ":" + str(ep.port) if ep.is_serving else "[yellow]none[/yellow]"), - ep.hotkey[:10], - ep.coldkey[:10], - ] - total_stake += metagraph.total_stake[uid] - total_rank += metagraph.ranks[uid] - total_validator_trust += metagraph.validator_trust[uid] - total_trust += metagraph.trust[uid] - total_consensus += metagraph.consensus[uid] - total_incentive += metagraph.incentive[uid] - total_dividends += metagraph.dividends[uid] - total_emission += int(metagraph.emission[uid] * 1000000000) - table_data.append(row) - total_neurons = len(metagraph.uids) - table = Table(show_footer=False) - table.title = ( - f"[white]Metagraph: " - f"net: {subtensor.network}:{metagraph.netuid}, " - f"block: {metagraph.block.item()}," - f"N: {sum(metagraph.active.tolist())}/{metagraph.n.item()}, " - f"stake: {Balance.from_tao(total_stake)}, " - f"issuance: {total_issuance}, " - f"difficulty: {difficulty}" - ) - table.add_column( - "[overline white]UID", - str(total_neurons), - footer_style="overline white", - style="yellow", - ) - table.add_column( - "[overline white]STAKE(\u03c4)", - "\u03c4{:.5f}".format(total_stake), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]RANK", - "{:.5f}".format(total_rank), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]TRUST", - "{:.5f}".format(total_trust), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]CONSENSUS", - "{:.5f}".format(total_consensus), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]INCENTIVE", - "{:.5f}".format(total_incentive), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]DIVIDENDS", - "{:.5f}".format(total_dividends), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]EMISSION(\u03c1)", - "\u03c1{}".format(int(total_emission)), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]VTRUST", - "{:.5f}".format(total_validator_trust), - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - table.add_column( - "[overline white]VAL", justify="right", style="green", no_wrap=True - ) - table.add_column("[overline white]UPDATED", justify="right", no_wrap=True) - table.add_column( - "[overline white]ACTIVE", justify="right", style="green", no_wrap=True - ) - table.add_column( - "[overline white]AXON", justify="left", style="dim blue", no_wrap=True - ) - table.add_column("[overline white]HOTKEY", style="dim blue", no_wrap=False) - table.add_column("[overline white]COLDKEY", style="dim purple", no_wrap=False) - table.show_footer = True + difficulty = int(difficulty_) + total_issuance = Balance.from_rao(total_issuance_.value) + metagraph = MiniGraph( + netuid=netuid, neurons=neurons, subtensor=subtensor, block=block + ) + # metagraph.save() TODO maybe? + table_data = [] + db_table = [] + total_stake = 0.0 + total_rank = 0.0 + total_validator_trust = 0.0 + total_trust = 0.0 + total_consensus = 0.0 + total_incentive = 0.0 + total_dividends = 0.0 + total_emission = 0 + for uid in metagraph.uids: + neuron = metagraph.neurons[uid] + ep = metagraph.axons[uid] + row = [ + str(neuron.uid), + "{:.5f}".format(metagraph.total_stake[uid]), + "{:.5f}".format(metagraph.ranks[uid]), + "{:.5f}".format(metagraph.trust[uid]), + "{:.5f}".format(metagraph.consensus[uid]), + "{:.5f}".format(metagraph.incentive[uid]), + "{:.5f}".format(metagraph.dividends[uid]), + "{}".format(int(metagraph.emission[uid] * 1000000000)), + "{:.5f}".format(metagraph.validator_trust[uid]), + "*" if metagraph.validator_permit[uid] else "", + str(metagraph.block.item() - metagraph.last_update[uid].item()), + str(metagraph.active[uid].item()), + ( + ep.ip + ":" + str(ep.port) + if ep.is_serving + else "[yellow]none[/yellow]" + ), + ep.hotkey[:10], + ep.coldkey[:10], + ] + db_row = [ + neuron.uid, + float(metagraph.total_stake[uid]), + float(metagraph.ranks[uid]), + float(metagraph.trust[uid]), + float(metagraph.consensus[uid]), + float(metagraph.incentive[uid]), + float(metagraph.dividends[uid]), + int(metagraph.emission[uid] * 1000000000), + float(metagraph.validator_trust[uid]), + bool(metagraph.validator_permit[uid]), + metagraph.block.item() - metagraph.last_update[uid].item(), + metagraph.active[uid].item(), + (ep.ip + ":" + str(ep.port) if ep.is_serving else "ERROR"), + ep.hotkey[:10], + ep.coldkey[:10], + ] + db_table.append(db_row) + total_stake += metagraph.total_stake[uid] + total_rank += metagraph.ranks[uid] + total_validator_trust += metagraph.validator_trust[uid] + total_trust += metagraph.trust[uid] + total_consensus += metagraph.consensus[uid] + total_incentive += metagraph.incentive[uid] + total_dividends += metagraph.dividends[uid] + total_emission += int(metagraph.emission[uid] * 1000000000) + table_data.append(row) + total_neurons = len(metagraph.uids) + table = Table(show_footer=False) + table.title = ( + f"[white]Metagraph: " + f"net: {subtensor.network}:{metagraph.netuid}, " + f"block: {metagraph.block.item()}," + f"N: {sum(metagraph.active.tolist())}/{metagraph.n.item()}, " + f"stake: {Balance.from_tao(total_stake)}, " + f"issuance: {total_issuance}, " + f"difficulty: {difficulty}" + ) + table.add_column( + "[overline white]UID", + str(total_neurons), + footer_style="overline white", + style="yellow", + ) + table.add_column( + "[overline white]STAKE(\u03c4)", + "\u03c4{:.5f}".format(total_stake), + footer_style="overline white", + justify="right", + style="green", + no_wrap=True, + ) + table.add_column( + "[overline white]RANK", + "{:.5f}".format(total_rank), + footer_style="overline white", + justify="right", + style="green", + no_wrap=True, + ) + table.add_column( + "[overline white]TRUST", + "{:.5f}".format(total_trust), + footer_style="overline white", + justify="right", + style="green", + no_wrap=True, + ) + table.add_column( + "[overline white]CONSENSUS", + "{:.5f}".format(total_consensus), + footer_style="overline white", + justify="right", + style="green", + no_wrap=True, + ) + table.add_column( + "[overline white]INCENTIVE", + "{:.5f}".format(total_incentive), + footer_style="overline white", + justify="right", + style="green", + no_wrap=True, + ) + table.add_column( + "[overline white]DIVIDENDS", + "{:.5f}".format(total_dividends), + footer_style="overline white", + justify="right", + style="green", + no_wrap=True, + ) + table.add_column( + "[overline white]EMISSION(\u03c1)", + "\u03c1{}".format(int(total_emission)), + footer_style="overline white", + justify="right", + style="green", + no_wrap=True, + ) + table.add_column( + "[overline white]VTRUST", + "{:.5f}".format(total_validator_trust), + footer_style="overline white", + justify="right", + style="green", + no_wrap=True, + ) + table.add_column( + "[overline white]VAL", justify="right", style="green", no_wrap=True + ) + table.add_column("[overline white]UPDATED", justify="right", no_wrap=True) + table.add_column( + "[overline white]ACTIVE", justify="right", style="green", no_wrap=True + ) + table.add_column( + "[overline white]AXON", justify="left", style="dim blue", no_wrap=True + ) + table.add_column("[overline white]HOTKEY", style="dim blue", no_wrap=False) + table.add_column("[overline white]COLDKEY", style="dim purple", no_wrap=False) + table.show_footer = True + + for row in table_data: + table.add_row(*row) + + create_table( + "metagraph", + columns=[ + ("UID", "INTEGER"), + ("STAKE", "REAL"), + ("RANK", "REAL"), + ("TRUST", "REAL"), + ("CONSENSUS", "REAL"), + ("INCENTIVE", "REAL"), + ("DIVIDENDS", "REAL"), + ("EMISSION", "INTEGER"), + ("VTRUST", "REAL"), + ("VAL", "INTEGER"), + ("UPDATED", "INTEGER"), + ("ACTIVE", "INTEGER"), + ("AXON", "TEXT"), + ("HOTKEY", "TEXT"), + ("COLDKEY", "TEXT"), + ], + rows=db_table, + ) - for row in table_data: - table.add_row(*row) - table.box = None - table.pad_edge = False - table.width = None - console.print(table) + table.box = None + table.pad_edge = False + table.width = None + # TODO add support for reuse-last with the CLI table output + if html_output: + render_table("metagraph") + else: + console.print(table) diff --git a/src/commands/wallets.py b/src/commands/wallets.py index ff80e7b9..9c8d744e 100644 --- a/src/commands/wallets.py +++ b/src/commands/wallets.py @@ -1301,7 +1301,7 @@ async def faucet( log_verbose: bool, max_successes: int = 3, ): - # TODO: - work out prompts to be passed through the cli + # TODO: - work out prompts to be passed through the cli success = await run_faucet_extrinsic( subtensor, wallet, diff --git a/src/templates/table.j2 b/src/templates/table.j2 new file mode 100644 index 00000000..889b671d --- /dev/null +++ b/src/templates/table.j2 @@ -0,0 +1,117 @@ + + + + + {{ title }} + + + + + +
+ + + + + + + +
+ +
+ + + + + + \ No newline at end of file diff --git a/src/utils.py b/src/utils.py index 11e34fb0..ceffeff0 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,13 +1,17 @@ import os import math from pathlib import Path +import sqlite3 from typing import Union, Any, Collection, Optional, TYPE_CHECKING +import webbrowser import aiohttp import scalecodec from bittensor_wallet import Wallet from bittensor_wallet.keyfile import Keypair from bittensor_wallet.utils import SS58_FORMAT, ss58 +from jinja2 import Template +from markupsafe import Markup import numpy as np from numpy.typing import NDArray from rich.console import Console @@ -502,3 +506,54 @@ def normalize_hyperparameters( normalized_values.append((param, str(value), str(norm_value))) return normalized_values + + +def create_table(title: str, columns: list[tuple[str, str]], rows: list[list]) -> bool: + """ + + :param title: title of the table + :param columns: [(column name, column type), ...] + :param rows: [(element, element, ...), ...] + :return: + """ + conn = sqlite3.connect(os.path.expanduser("~/.bittensor/bittensor.db")) + cursor = conn.cursor() + columns_ = ", ".join([" ".join(x) for x in columns]) + creation_query = f"CREATE TABLE IF NOT EXISTS {title} ({columns_})" + cursor.execute(creation_query) + conn.commit() + cursor.execute(f"DELETE FROM {title};") + conn.commit() + query = f"INSERT INTO {title} ({', '.join([x[0] for x in columns])}) VALUES ({', '.join(['?'] * len(columns))})" + cursor.executemany(query, rows) + conn.commit() + conn.close() + return True + + +def read_table(table_name: str) -> tuple[list, list]: + conn = sqlite3.connect(os.path.expanduser("~/.bittensor/bittensor.db")) + cursor = conn.cursor() + cursor.execute(f"PRAGMA table_info({table_name})") + columns_info = cursor.fetchall() + column_names = [info[1] for info in columns_info] + cursor.execute(f"SELECT * FROM {table_name}") + rows = cursor.fetchall() + return column_names, rows + + +def render_table(table_name: str): + columns, rows = read_table(table_name) + template_dir = os.path.join(os.path.dirname(__file__), "templates") + with open(os.path.join(template_dir, "table.j2"), "r") as f: + template = Template(f.read()) + rendered = template.render( + title=table_name, + columns=Markup([{"title": c, "field": c} for c in columns]), + rows=Markup([{c: v for (c, v) in zip(columns, r)} for r in rows]), + column_names=columns, + ) + output_file = "/tmp/bittensor_table.html" + with open(output_file, "w+") as f: + f.write(rendered) + webbrowser.open(f"file://{output_file}") From ed9ff0f9c717cf62eefb10d28fa1cfc797d510c4 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 26 Aug 2024 22:41:40 +0200 Subject: [PATCH 2/8] Allows reusing metagraph data from the previous call correctly (in CLI and HTML). --- src/commands/subnets.py | 118 ++++++++++++++++++++++++---------------- src/utils.py | 85 ++++++++++++++++++++++------- 2 files changed, 136 insertions(+), 67 deletions(-) diff --git a/src/commands/subnets.py b/src/commands/subnets.py index 8aaefbbe..b75bbdec 100644 --- a/src/commands/subnets.py +++ b/src/commands/subnets.py @@ -1,4 +1,5 @@ import asyncio +import json from typing import TYPE_CHECKING, Optional from bittensor_wallet import Wallet @@ -21,6 +22,8 @@ format_error_message, render_table, create_table, + update_metadata_table, + get_metadata_table, ) if TYPE_CHECKING: @@ -329,7 +332,7 @@ async def register(wallet: Wallet, subtensor: "SubtensorInterface", netuid: int) ) return - if not True: # TODO no-prompt + if not False: # TODO no-prompt if not ( Confirm.ask( f"Your balance is: [bold green]{balance}[/bold green]\nThe cost to register by recycle is " @@ -353,6 +356,7 @@ async def metagraph_cmd( subtensor: "SubtensorInterface", netuid: int, reuse_last: bool, html_output: bool ): """Prints an entire metagraph.""" + # TODO allow config to set certain columns if not reuse_last: with console.status( f":satellite: Syncing with chain: [white]{subtensor.network}[/white] ..." @@ -377,7 +381,6 @@ async def metagraph_cmd( metagraph = MiniGraph( netuid=netuid, neurons=neurons, subtensor=subtensor, block=block ) - # metagraph.save() TODO maybe? table_data = [] db_table = [] total_stake = 0.0 @@ -439,26 +442,74 @@ async def metagraph_cmd( total_dividends += metagraph.dividends[uid] total_emission += int(metagraph.emission[uid] * 1000000000) table_data.append(row) - total_neurons = len(metagraph.uids) - table = Table(show_footer=False) + metadata_info = { + "stake": str(Balance.from_tao(total_stake)), + "total_stake": "\u03c4{:.5f}".format(total_stake), + "rank": "{:.5f}".format(total_rank), + "validator_trust": "{:.5f}".format(total_validator_trust), + "trust": "{:.5f}".format(total_trust), + "consensus": "{:.5f}".format(total_consensus), + "incentive": "{:.5f}".format(total_incentive), + "dividends": "{:.5f}".format(total_dividends), + "emission": "\u03c1{}".format(int(total_emission)), + "net": f"{subtensor.network}:{metagraph.netuid}", + "block": str(metagraph.block.item()), + "N": f"{sum(metagraph.active.tolist())}/{metagraph.n.item()}", + "issuance": str(total_issuance), + "difficulty": str(difficulty), + "total_neurons": str(len(metagraph.uids)), + "table_data": json.dumps(table_data), + } + update_metadata_table("metagraph", metadata_info) + create_table( + "metagraph", + columns=[ + ("UID", "INTEGER"), + ("STAKE", "REAL"), + ("RANK", "REAL"), + ("TRUST", "REAL"), + ("CONSENSUS", "REAL"), + ("INCENTIVE", "REAL"), + ("DIVIDENDS", "REAL"), + ("EMISSION", "INTEGER"), + ("VTRUST", "REAL"), + ("VAL", "INTEGER"), + ("UPDATED", "INTEGER"), + ("ACTIVE", "INTEGER"), + ("AXON", "TEXT"), + ("HOTKEY", "TEXT"), + ("COLDKEY", "TEXT"), + ], + rows=db_table, + ) + else: + metadata_info = get_metadata_table("metagraph") + table_data = json.loads(metadata_info["table_data"]) + + if html_output: + # TODO add the metadata_info to the table + render_table("metagraph") + else: + table = Table(show_footer=False, box=None, pad_edge=False, width=None) + table.title = ( f"[white]Metagraph: " - f"net: {subtensor.network}:{metagraph.netuid}, " - f"block: {metagraph.block.item()}," - f"N: {sum(metagraph.active.tolist())}/{metagraph.n.item()}, " - f"stake: {Balance.from_tao(total_stake)}, " - f"issuance: {total_issuance}, " - f"difficulty: {difficulty}" + f"net: {metadata_info['net']}, " + f"block: {metadata_info['block']}," + f"N: {metadata_info['N']}, " + f"stake: {metadata_info['stake']}, " + f"issuance: {metadata_info['issuance']}, " + f"difficulty: {metadata_info['difficulty']}" ) table.add_column( "[overline white]UID", - str(total_neurons), + metadata_info["total_neurons"], footer_style="overline white", style="yellow", ) table.add_column( "[overline white]STAKE(\u03c4)", - "\u03c4{:.5f}".format(total_stake), + metadata_info["total_stake"], footer_style="overline white", justify="right", style="green", @@ -466,7 +517,7 @@ async def metagraph_cmd( ) table.add_column( "[overline white]RANK", - "{:.5f}".format(total_rank), + metadata_info["rank"], footer_style="overline white", justify="right", style="green", @@ -474,7 +525,7 @@ async def metagraph_cmd( ) table.add_column( "[overline white]TRUST", - "{:.5f}".format(total_trust), + metadata_info["trust"], footer_style="overline white", justify="right", style="green", @@ -482,7 +533,7 @@ async def metagraph_cmd( ) table.add_column( "[overline white]CONSENSUS", - "{:.5f}".format(total_consensus), + metadata_info["consensus"], footer_style="overline white", justify="right", style="green", @@ -490,7 +541,7 @@ async def metagraph_cmd( ) table.add_column( "[overline white]INCENTIVE", - "{:.5f}".format(total_incentive), + metadata_info["incentive"], footer_style="overline white", justify="right", style="green", @@ -498,7 +549,7 @@ async def metagraph_cmd( ) table.add_column( "[overline white]DIVIDENDS", - "{:.5f}".format(total_dividends), + metadata_info["dividends"], footer_style="overline white", justify="right", style="green", @@ -506,7 +557,7 @@ async def metagraph_cmd( ) table.add_column( "[overline white]EMISSION(\u03c1)", - "\u03c1{}".format(int(total_emission)), + metadata_info["emission"], footer_style="overline white", justify="right", style="green", @@ -514,7 +565,7 @@ async def metagraph_cmd( ) table.add_column( "[overline white]VTRUST", - "{:.5f}".format(total_validator_trust), + metadata_info["validator_trust"], footer_style="overline white", justify="right", style="green", @@ -537,33 +588,4 @@ async def metagraph_cmd( for row in table_data: table.add_row(*row) - create_table( - "metagraph", - columns=[ - ("UID", "INTEGER"), - ("STAKE", "REAL"), - ("RANK", "REAL"), - ("TRUST", "REAL"), - ("CONSENSUS", "REAL"), - ("INCENTIVE", "REAL"), - ("DIVIDENDS", "REAL"), - ("EMISSION", "INTEGER"), - ("VTRUST", "REAL"), - ("VAL", "INTEGER"), - ("UPDATED", "INTEGER"), - ("ACTIVE", "INTEGER"), - ("AXON", "TEXT"), - ("HOTKEY", "TEXT"), - ("COLDKEY", "TEXT"), - ], - rows=db_table, - ) - - table.box = None - table.pad_edge = False - table.width = None - # TODO add support for reuse-last with the CLI table output - if html_output: - render_table("metagraph") - else: console.print(table) diff --git a/src/utils.py b/src/utils.py index ceffeff0..d6ef182e 100644 --- a/src/utils.py +++ b/src/utils.py @@ -508,40 +508,87 @@ def normalize_hyperparameters( return normalized_values +class DB: + def __init__( + self, + db_path: str = os.path.expanduser("~/.bittensor/bittensor.db"), + row_factory=None, + ): + self.db_path = db_path + self.conn: Optional[sqlite3.Connection] = None + self.row_factory = row_factory + + def __enter__(self): + self.conn = sqlite3.connect(self.db_path) + self.conn.autocommit = True + self.conn.row_factory = self.row_factory + return self.conn, self.conn.cursor() + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.conn: + self.conn.close() + + def create_table(title: str, columns: list[tuple[str, str]], rows: list[list]) -> bool: """ + Creates and populates the rows of a table in the SQLite database. :param title: title of the table :param columns: [(column name, column type), ...] :param rows: [(element, element, ...), ...] :return: """ - conn = sqlite3.connect(os.path.expanduser("~/.bittensor/bittensor.db")) - cursor = conn.cursor() - columns_ = ", ".join([" ".join(x) for x in columns]) - creation_query = f"CREATE TABLE IF NOT EXISTS {title} ({columns_})" - cursor.execute(creation_query) - conn.commit() - cursor.execute(f"DELETE FROM {title};") - conn.commit() - query = f"INSERT INTO {title} ({', '.join([x[0] for x in columns])}) VALUES ({', '.join(['?'] * len(columns))})" - cursor.executemany(query, rows) - conn.commit() - conn.close() + with DB() as (conn, cursor): + columns_ = ", ".join([" ".join(x) for x in columns]) + creation_query = f"CREATE TABLE IF NOT EXISTS {title} ({columns_})" + cursor.execute(creation_query) + cursor.execute(f"DELETE FROM {title};") + query = f"INSERT INTO {title} ({', '.join([x[0] for x in columns])}) VALUES ({', '.join(['?'] * len(columns))})" + cursor.executemany(query, rows) return True def read_table(table_name: str) -> tuple[list, list]: - conn = sqlite3.connect(os.path.expanduser("~/.bittensor/bittensor.db")) - cursor = conn.cursor() - cursor.execute(f"PRAGMA table_info({table_name})") - columns_info = cursor.fetchall() - column_names = [info[1] for info in columns_info] - cursor.execute(f"SELECT * FROM {table_name}") - rows = cursor.fetchall() + with DB() as (conn, cursor): + cursor.execute(f"PRAGMA table_info({table_name})") + columns_info = cursor.fetchall() + column_names = [info[1] for info in columns_info] + cursor.execute(f"SELECT * FROM {table_name}") + rows = cursor.fetchall() return column_names, rows +def update_metadata_table(table_name: str, values: dict[str, str]) -> None: + with DB() as (conn, cursor): + cursor.execute( + "CREATE TABLE IF NOT EXISTS metadata (" + "TableName TEXT, " + "Key TEXT, " + "Value TEXT" + ")" + ) + for key, value in values.items(): + cursor.execute( + "UPDATE metadata SET TableName = ?, Value = ? WHERE Key = ?", + (table_name, value, key), + ) + if cursor.rowcount == 0: + cursor.execute( + "INSERT INTO metadata (TableName, Key, Value) VALUES (?, ?, ?)", + (table_name, key, value), + ) + return + + +def get_metadata_table(table_name: str) -> dict[str, str]: + with DB() as (conn, cursor): + cursor.execute( + "SELECT Key, Value FROM metadata WHERE TableName = ?", (table_name,) + ) + data = cursor.fetchall() + return dict(data) + + def render_table(table_name: str): columns, rows = read_table(table_name) template_dir = os.path.join(os.path.dirname(__file__), "templates") From 82ab9227d44e1122a0e7d696e22bf5736d9fb9d6 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 26 Aug 2024 22:55:24 +0200 Subject: [PATCH 3/8] Handle netuid not set --- cli.py | 13 ++++++++++++- src/commands/subnets.py | 5 ++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/cli.py b/cli.py index 0b589a8e..bfccb853 100755 --- a/cli.py +++ b/cli.py @@ -3205,7 +3205,11 @@ def subnets_register( def subnets_metagraph( self, - netuid: int = Options.netuid, + netuid: Optional[int] = typer.Option( + None, + help="The netuid (network unique identifier) of the subnet within the root network, (e.g. 1). This does" + "is ignored when used with `--reuse-last`.", + ), network: str = Options.network, chain: str = Options.chain, reuse_last: bool = typer.Option( @@ -3280,8 +3284,15 @@ def subnets_metagraph( not as a standalone function within user code. """ if reuse_last: + if netuid is not None: + console.print("Cannot specify netuid when using `--reuse-last`") + raise typer.Exit() subtensor = None else: + if netuid is None: + netuid = rich.prompt.IntPrompt.ask( + "Enter the netuid (network unique identifier) of the subnet within the root network, (e.g. 1)." + ) subtensor = self.initialize_chain(network, chain) return self._run_command( subnets.metagraph_cmd(subtensor, netuid, reuse_last, html_output) diff --git a/src/commands/subnets.py b/src/commands/subnets.py index b75bbdec..1ce51538 100644 --- a/src/commands/subnets.py +++ b/src/commands/subnets.py @@ -353,7 +353,10 @@ async def register(wallet: Wallet, subtensor: "SubtensorInterface", netuid: int) async def metagraph_cmd( - subtensor: "SubtensorInterface", netuid: int, reuse_last: bool, html_output: bool + subtensor: "SubtensorInterface", + netuid: Optional[int], + reuse_last: bool, + html_output: bool, ): """Prints an entire metagraph.""" # TODO allow config to set certain columns From 3dc48b6300f17c0e5d3656e8be3d855b7115ac16 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 26 Aug 2024 23:26:36 +0200 Subject: [PATCH 4/8] Style updates. --- src/templates/table.j2 | 50 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/templates/table.j2 b/src/templates/table.j2 index 889b671d..05c59dec 100644 --- a/src/templates/table.j2 +++ b/src/templates/table.j2 @@ -3,9 +3,46 @@ {{ title }} + +
@@ -41,11 +78,16 @@ let table = new Tabulator("#my-table", { columns:[ - {"title": "UID", "field": "UID"}, - {title: "Stake", field: "STAKE", formatter: "money", formatterParams:{ - symbol:"τ", + {title: "UID", field: "UID", minWidth: 70}, + { + title: "Stake", + field: "STAKE", + formatter: "money", + formatterParams:{ + symbol:"τ", precision:5 - }}, + } + }, {title: "Rank", field:"RANK", formatter: "money", formatterParams: {precision: 5}}, {title: "Trust", field: "TRUST", formatter: "money", formatterParams: {precision: 5}}, {title: "Consensus", field: "CONSENSUS", formatter: "money", formatterParams: {precision: 5}}, From 61813ca36d510935227c96ce07cb805b43875d78 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 27 Aug 2024 10:01:34 +0200 Subject: [PATCH 5/8] Style updates. --- src/commands/subnets.py | 66 +++++++++++++- src/templates/table.j2 | 194 ++++++++++++++++++++++------------------ src/utils.py | 11 +-- 3 files changed, 178 insertions(+), 93 deletions(-) diff --git a/src/commands/subnets.py b/src/commands/subnets.py index 1ce51538..e6b8c9bd 100644 --- a/src/commands/subnets.py +++ b/src/commands/subnets.py @@ -490,15 +490,75 @@ async def metagraph_cmd( table_data = json.loads(metadata_info["table_data"]) if html_output: - # TODO add the metadata_info to the table - render_table("metagraph") + render_table( + table_name="metagraph", + table_info=f"Metagraph | " + f"net: {metadata_info['net']}, " + f"block: {metadata_info['block']}, " + f"N: {metadata_info['N']}, " + f"stake: {metadata_info['stake']}, " + f"issuance: {metadata_info['issuance']}, " + f"difficulty: {metadata_info['difficulty']}", + columns=[ + {"title": "UID", "field": "UID"}, + { + "title": "Stake", + "field": "STAKE", + "formatter": "money", + "formatterParams": {"symbol": "τ", "precision": 5}, + }, + { + "title": "Rank", + "field": "RANK", + "formatter": "money", + "formatterParams": {"precision": 5}, + }, + { + "title": "Trust", + "field": "TRUST", + "formatter": "money", + "formatterParams": {"precision": 5}, + }, + { + "title": "Consensus", + "field": "CONSENSUS", + "formatter": "money", + "formatterParams": {"precision": 5}, + }, + { + "title": "Incentive", + "field": "INCENTIVE", + "formatter": "money", + "formatterParams": {"precision": 5}, + }, + { + "title": "Dividends", + "field": "DIVIDENDS", + "formatter": "money", + "formatterParams": {"precision": 5}, + }, + {"title": "Emission", "field": "EMISSION"}, + { + "title": "VTrust", + "field": "VTRUST", + "formatter": "money", + "formatterParams": {"precision": 5}, + }, + {"title": "Validated", "field": "VAL"}, + {"title": "Updated", "field": "UPDATED"}, + {"title": "Active", "field": "ACTIVE"}, + {"title": "Axon", "field": "AXON"}, + {"title": "Hotkey", "field": "HOTKEY"}, + {"title": "Coldkey", "field": "COLDKEY"}, + ], + ) else: table = Table(show_footer=False, box=None, pad_edge=False, width=None) table.title = ( f"[white]Metagraph: " f"net: {metadata_info['net']}, " - f"block: {metadata_info['block']}," + f"block: {metadata_info['block']}, " f"N: {metadata_info['N']}, " f"stake: {metadata_info['stake']}, " f"issuance: {metadata_info['issuance']}, " diff --git a/src/templates/table.j2 b/src/templates/table.j2 index 05c59dec..09bb11a7 100644 --- a/src/templates/table.j2 +++ b/src/templates/table.j2 @@ -5,6 +5,7 @@ {{ title }} + - - -
- - - - - - - + + +
+ +
+ τ +
+ +
+ {{ table_info }} +
-
+ +
+
+ + + + + + + +
+
+ + +
+ +
+ +
+ + + + +
\ No newline at end of file diff --git a/src/utils.py b/src/utils.py index d6ef182e..0f3c91c4 100644 --- a/src/utils.py +++ b/src/utils.py @@ -589,16 +589,17 @@ def get_metadata_table(table_name: str) -> dict[str, str]: return dict(data) -def render_table(table_name: str): - columns, rows = read_table(table_name) +def render_table(table_name: str, table_info: str, columns: list[dict]): + db_cols, rows = read_table(table_name) template_dir = os.path.join(os.path.dirname(__file__), "templates") with open(os.path.join(template_dir, "table.j2"), "r") as f: template = Template(f.read()) rendered = template.render( title=table_name, - columns=Markup([{"title": c, "field": c} for c in columns]), - rows=Markup([{c: v for (c, v) in zip(columns, r)} for r in rows]), - column_names=columns, + columns=Markup(columns), + rows=Markup([{c: v for (c, v) in zip(db_cols, r)} for r in rows]), + column_names=db_cols, + table_info=table_info, ) output_file = "/tmp/bittensor_table.html" with open(output_file, "w+") as f: From a8828b08101ecf898be8d4b8d1af4ca434d6d83f Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 26 Aug 2024 19:16:44 +0200 Subject: [PATCH 6/8] Initial support for HTML table outputs --- src/commands/subnets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/subnets.py b/src/commands/subnets.py index e6b8c9bd..b817548b 100644 --- a/src/commands/subnets.py +++ b/src/commands/subnets.py @@ -1,5 +1,4 @@ import asyncio -import json from typing import TYPE_CHECKING, Optional from bittensor_wallet import Wallet From 482a85036d70b522602deb679d83c47d40ccd770 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 27 Aug 2024 12:29:27 +0200 Subject: [PATCH 7/8] Add `subnets list` se `--reuse-last` and `--html` options --- cli.py | 36 ++-- src/commands/stake.py | 4 +- src/commands/subnets.py | 394 +++++++++++++++++++++++++--------------- src/templates/table.j2 | 18 +- src/utils.py | 22 ++- 5 files changed, 315 insertions(+), 159 deletions(-) diff --git a/cli.py b/cli.py index bfccb853..c801a5ab 100755 --- a/cli.py +++ b/cli.py @@ -104,6 +104,16 @@ class Options: help="The netuid (network unique identifier) of the subnet within the root network, (e.g. 1)", prompt=True, ) + reuse_last = typer.Option( + False, + help="Reuse the metagraph data you last retrieved. Only use this if you have already retrieved metagraph" + "data", + ) + html_output = typer.Option( + False, + "--html", + help="Display the table as HTML in the browser, rather than in the Terminal.", + ) def list_prompt(init_var: list, list_type: type, help_text: str) -> list: @@ -2884,7 +2894,13 @@ def sudo_get( sudo.get_hyperparameters(self.initialize_chain(network, chain), netuid) ) - def subnets_list(self, network: str = Options.network, chain: str = Options.chain): + def subnets_list( + self, + network: str = Options.network, + chain: str = Options.chain, + reuse_last: bool = Options.reuse_last, + html_output: bool = Options.html_output, + ): """ # subnets list Executes the `list` command to list all subnets and their detailed information on the Bittensor network. @@ -2932,8 +2948,12 @@ def subnets_list(self, network: str = Options.network, chain: str = Options.chai This command is particularly useful for users seeking an overview of the Bittensor network's structure and the distribution of its resources and ownership information for each subnet. """ + if reuse_last: + subtensor = None + else: + subtensor = self.initialize_chain(network, chain) return self._run_command( - subnets.subnets_list(self.initialize_chain(network, chain)) + subnets.subnets_list(subtensor, reuse_last, html_output) ) def subnets_lock_cost( @@ -3212,16 +3232,8 @@ def subnets_metagraph( ), network: str = Options.network, chain: str = Options.chain, - reuse_last: bool = typer.Option( - False, - help="Reuse the metagraph data you last retrieved. Only use this if you have already retrieved metagraph" - "data", - ), - html_output: bool = typer.Option( - False, - "--html", - help="Display the table as HTML in the browser, rather than in the Terminal.", - ), + reuse_last: bool = Options.reuse_last, + html_output: bool = Options.html_output, ): """ Executes the `metagraph` command to retrieve and display the entire metagraph for a specified network. diff --git a/src/commands/stake.py b/src/commands/stake.py index ab77c049..b5fa2e23 100644 --- a/src/commands/stake.py +++ b/src/commands/stake.py @@ -551,7 +551,7 @@ async def unstake_extrinsic( with console.status( f":satellite: Unstaking from chain: [white]{subtensor}[/white] ..." - ): + ): unstaking_balance = Balance.from_tao(unstaking_balance) call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -1423,7 +1423,7 @@ async def unstake( hotkey_ss58=final_hotkeys[0][1], amount=None if unstake_all else final_amounts[0], wait_for_inclusion=True, - prompt=False, #TODO: Add no prompt + prompt=False, # TODO: Add no prompt ) else: await unstake_multiple_extrinsic( diff --git a/src/commands/subnets.py b/src/commands/subnets.py index b817548b..b2a7be12 100644 --- a/src/commands/subnets.py +++ b/src/commands/subnets.py @@ -1,9 +1,12 @@ import asyncio +import json +import sqlite3 +from textwrap import dedent from typing import TYPE_CHECKING, Optional from bittensor_wallet import Wallet from rich.prompt import Confirm -from rich.table import Table, Column +from rich.table import Table from src import Constants, DelegatesDetails from src.bittensor.balances import Balance @@ -135,7 +138,9 @@ def _find_event_attributes_in_extrinsic_receipt(response_, event_name: str) -> l # commands -async def subnets_list(subtensor: "SubtensorInterface"): +async def subnets_list( + subtensor: "SubtensorInterface", reuse_last: bool, html_output: bool +): """List all subnet netuids in the network.""" async def _get_all_subnets_info(): @@ -150,90 +155,177 @@ async def _get_all_subnets_info(): else [] ) - subnets: list[SubnetInfo] - delegate_info: dict[str, DelegatesDetails] + if not reuse_last: + subnets: list[SubnetInfo] + delegate_info: dict[str, DelegatesDetails] - subnets, delegate_info = await asyncio.gather( - _get_all_subnets_info(), - get_delegates_details_from_github(url=Constants.delegates_detail_url), - ) + subnets, delegate_info = await asyncio.gather( + _get_all_subnets_info(), + get_delegates_details_from_github(url=Constants.delegates_detail_url), + ) - if not subnets: - err_console.print("[red]No subnets found[/red]") - return + if not subnets: + err_console.print("[red]No subnets found[/red]") + return - rows = [] - total_neurons = 0 - - for subnet in subnets: - total_neurons += subnet.max_n - rows.append( - ( - str(subnet.netuid), - str(subnet.subnetwork_n), - str(millify(subnet.max_n)), - f"{subnet.emission_value / RAO_PER_TAO * 100:0.2f}%", - str(subnet.tempo), - f"{subnet.burn!s:8.8}", - str(millify(subnet.difficulty)), - f"{delegate_info[subnet.owner_ss58].name if subnet.owner_ss58 in delegate_info else subnet.owner_ss58}", + rows = [] + db_rows = [] + total_neurons = 0 + + for subnet in subnets: + total_neurons += subnet.max_n + rows.append( + ( + str(subnet.netuid), + str(subnet.subnetwork_n), + str(millify(subnet.max_n)), + f"{subnet.emission_value / RAO_PER_TAO * 100:0.2f}%", + str(subnet.tempo), + f"{subnet.burn!s:8.8}", + str(millify(subnet.difficulty)), + f"{delegate_info[subnet.owner_ss58].name if subnet.owner_ss58 in delegate_info else subnet.owner_ss58}", + ) + ) + db_rows.append( + [ + int(subnet.netuid), + int(subnet.subnetwork_n), + int(subnet.max_n), # need to millify on table + float( + subnet.emission_value / RAO_PER_TAO * 100 + ), # show as percentage in table + int(subnet.tempo), + float(subnet.burn), + int(subnet.difficulty), # millify on table + str( + delegate_info[subnet.owner_ss58].name + if subnet.owner_ss58 in delegate_info + else subnet.owner_ss58 + ), + ] ) + metadata = { + "network": subtensor.network, + "netuid_count": len(subnets), + "N": total_neurons, + "rows": json.dumps(rows), + } + create_table( + "subnetslist", + [ + ("NETUID", "INTEGER"), + ("N", "INTEGER"), + ("MAX_N", "BLOB"), + ("EMISSION", "REAL"), + ("TEMPO", "INTEGER"), + ("RECYCLE", "REAL"), + ("DIFFICULTY", "BLOB"), + ("SUDO", "TEXT"), + ], + db_rows, + ) + update_metadata_table("subnetslist", values=metadata) + else: + try: + metadata = get_metadata_table("subnetslist") + rows = json.loads(metadata["rows"]) + except sqlite3.OperationalError: + err_console.print( + "[red]Error[/red] Unable to retrieve table data. This is usually caused by attempting to use " + "`--reuse-last` before running the command a first time. In rare cases, this could also be due to " + "a corrupted database. Re-run the command (do not use `--reuse-last`) and see if that resolves your " + "issue." + ) + return + if not html_output: + table_width = console.width - 20 + + table = Table( + title=f"[bold magenta]Subnets - {metadata['network']}[/bold magenta]", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_style="bold white", + title_justify="center", + show_lines=False, + expand=True, + width=table_width, + pad_edge=True, ) - table_width = console.width - 20 - - table = Table( - title=f"[bold magenta]Subnets - {subtensor.network}[/bold magenta]", - show_footer=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_style="bold white", - title_justify="center", - show_lines=False, - expand=True, - width=table_width, - pad_edge=True, - ) + table.add_column( + "[bold white]NETUID", + footer=f"[white]{metadata['netuid_count']}[/white]", + style="white", + justify="center", + ) + table.add_column( + "[bold white]N", + footer=f"[white]{metadata['N']}[/white]", + style="bright_cyan", + justify="center", + ) + table.add_column("[bold white]MAX_N", style="bright_yellow", justify="center") + table.add_column( + "[bold white]EMISSION", style="bright_yellow", justify="center" + ) + table.add_column("[bold white]TEMPO", style="magenta", justify="center") + table.add_column("[bold white]RECYCLE", style="bright_red", justify="center") + table.add_column("[bold white]POW", style="medium_purple", justify="center") + table.add_column("[bold white]SUDO", style="bright_magenta", justify="center") - table.add_column( - "[bold white]NETUID", - footer=f"[white]{len(subnets)}[/white]", - style="white", - justify="center", - ) - table.add_column( - "[bold white]N", - footer=f"[white]{total_neurons}[/white]", - style="bright_cyan", - justify="center", - ) - table.add_column("[bold white]MAX_N", style="bright_yellow", justify="center") - table.add_column("[bold white]EMISSION", style="bright_yellow", justify="center") - table.add_column("[bold white]TEMPO", style="magenta", justify="center") - table.add_column("[bold white]RECYCLE", style="bright_red", justify="center") - table.add_column("[bold white]POW", style="medium_purple", justify="center") - table.add_column("[bold white]SUDO", style="bright_magenta", justify="center") - - for row in rows: - table.add_row(*row) - - console.print(table) - console.print( - """ -Description: - The table displays the list of subnets registered in the Bittensor network. - - NETIUID: The network identifier of the subnet. - - N: The current UIDs registered to the network. - - MAX_N: The total UIDs allowed on the network. - - EMISSION: The emission accrued by this subnet in the network. - - TEMPO: A duration of a number of blocks. Several subnet events occur at the end of every tempo period. - - RECYCLE: Cost to register to the subnet. - - POW: Proof of work metric of the subnet. - - SUDO: Owner's identity. -""" - ) + for row in rows: + table.add_row(*row) + + console.print(table) + console.print( + dedent( + """ + Description: + The table displays the list of subnets registered in the Bittensor network. + - NETUID: The network identifier of the subnet. + - N: The current UIDs registered to the network. + - MAX_N: The total UIDs allowed on the network. + - EMISSION: The emission accrued by this subnet in the network. + - TEMPO: A duration of a number of blocks. Several subnet events occur at the end of every tempo period. + - RECYCLE: Cost to register to the subnet. + - POW: Proof of work metric of the subnet. + - SUDO: Owner's identity. + """ + ) + ) + else: + render_table( + "subnetslist", + f"Subnets List | Network: {metadata['network']} - " + f"Netuids: {metadata['netuid_count']} - N: {metadata['N']}", + columns=[ + {"title": "NetUID", "field": "NETUID"}, + {"title": "N", "field": "N"}, + {"title": "MAX_N", "field": "MAX_N", "customFormatter": "millify"}, + { + "title": "EMISSION", + "field": "EMISSION", + "formatter": "money", + "formatterParams": {"symbolAfter": "%", "precision": 2}, + }, + {"title": "Tempo", "field": "TEMPO"}, + { + "title": "Recycle", + "field": "RECYCLE", + "formatter": "money", + "formatterParams": {"symbol": "", "precision": 5}, + }, + { + "title": "Difficulty", + "field": "DIFFICULTY", + "customFormatter": "millify", + }, + {"title": "sudo", "field": "SUDO"}, + ], + ) async def lock_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: @@ -485,72 +577,88 @@ async def metagraph_cmd( rows=db_table, ) else: - metadata_info = get_metadata_table("metagraph") - table_data = json.loads(metadata_info["table_data"]) + try: + metadata_info = get_metadata_table("metagraph") + table_data = json.loads(metadata_info["table_data"]) + except sqlite3.OperationalError: + err_console.print( + "[red]Error[/red] Unable to retrieve table data. This is usually caused by attempting to use " + "`--reuse-last` before running the command a first time. In rare cases, this could also be due to " + "a corrupted database. Re-run the command (do not use `--reuse-last`) and see if that resolves your " + "issue." + ) + return if html_output: - render_table( - table_name="metagraph", - table_info=f"Metagraph | " - f"net: {metadata_info['net']}, " - f"block: {metadata_info['block']}, " - f"N: {metadata_info['N']}, " - f"stake: {metadata_info['stake']}, " - f"issuance: {metadata_info['issuance']}, " - f"difficulty: {metadata_info['difficulty']}", - columns=[ - {"title": "UID", "field": "UID"}, - { - "title": "Stake", - "field": "STAKE", - "formatter": "money", - "formatterParams": {"symbol": "τ", "precision": 5}, - }, - { - "title": "Rank", - "field": "RANK", - "formatter": "money", - "formatterParams": {"precision": 5}, - }, - { - "title": "Trust", - "field": "TRUST", - "formatter": "money", - "formatterParams": {"precision": 5}, - }, - { - "title": "Consensus", - "field": "CONSENSUS", - "formatter": "money", - "formatterParams": {"precision": 5}, - }, - { - "title": "Incentive", - "field": "INCENTIVE", - "formatter": "money", - "formatterParams": {"precision": 5}, - }, - { - "title": "Dividends", - "field": "DIVIDENDS", - "formatter": "money", - "formatterParams": {"precision": 5}, - }, - {"title": "Emission", "field": "EMISSION"}, - { - "title": "VTrust", - "field": "VTRUST", - "formatter": "money", - "formatterParams": {"precision": 5}, - }, - {"title": "Validated", "field": "VAL"}, - {"title": "Updated", "field": "UPDATED"}, - {"title": "Active", "field": "ACTIVE"}, - {"title": "Axon", "field": "AXON"}, - {"title": "Hotkey", "field": "HOTKEY"}, - {"title": "Coldkey", "field": "COLDKEY"}, - ], - ) + try: + render_table( + table_name="metagraph", + table_info=f"Metagraph | " + f"net: {metadata_info['net']}, " + f"block: {metadata_info['block']}, " + f"N: {metadata_info['N']}, " + f"stake: {metadata_info['stake']}, " + f"issuance: {metadata_info['issuance']}, " + f"difficulty: {metadata_info['difficulty']}", + columns=[ + {"title": "UID", "field": "UID"}, + { + "title": "Stake", + "field": "STAKE", + "formatter": "money", + "formatterParams": {"symbol": "τ", "precision": 5}, + }, + { + "title": "Rank", + "field": "RANK", + "formatter": "money", + "formatterParams": {"precision": 5}, + }, + { + "title": "Trust", + "field": "TRUST", + "formatter": "money", + "formatterParams": {"precision": 5}, + }, + { + "title": "Consensus", + "field": "CONSENSUS", + "formatter": "money", + "formatterParams": {"precision": 5}, + }, + { + "title": "Incentive", + "field": "INCENTIVE", + "formatter": "money", + "formatterParams": {"precision": 5}, + }, + { + "title": "Dividends", + "field": "DIVIDENDS", + "formatter": "money", + "formatterParams": {"precision": 5}, + }, + {"title": "Emission", "field": "EMISSION"}, + { + "title": "VTrust", + "field": "VTRUST", + "formatter": "money", + "formatterParams": {"precision": 5}, + }, + {"title": "Validated", "field": "VAL"}, + {"title": "Updated", "field": "UPDATED"}, + {"title": "Active", "field": "ACTIVE"}, + {"title": "Axon", "field": "AXON"}, + {"title": "Hotkey", "field": "HOTKEY"}, + {"title": "Coldkey", "field": "COLDKEY"}, + ], + ) + except sqlite3.OperationalError: + err_console.print( + "[red]Error[/red] Unable to retrieve table data. This may indicate that your database is corrupted, " + "or was not able to load with the most recent data." + ) + return else: table = Table(show_footer=False, box=None, pad_edge=False, width=None) diff --git a/src/templates/table.j2 b/src/templates/table.j2 index 09bb11a7..0df9fd3b 100644 --- a/src/templates/table.j2 +++ b/src/templates/table.j2 @@ -101,10 +101,17 @@