From 7700a736414fd5c6ad8ac889f33f249bc780d916 Mon Sep 17 00:00:00 2001 From: Nick Chambers Date: Fri, 19 Aug 2022 06:02:37 -0500 Subject: [PATCH] Implement basic usability and rendering for some commands --- vultron/cmd.py | 55 +++++++++++++++++++++++--- vultron/display.py | 99 ++++++++++++++++++++++++++++++++++++++++++++++ vultron/vultron.py | 42 +++++++++++++++++--- 3 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 vultron/display.py diff --git a/vultron/cmd.py b/vultron/cmd.py index 5ac6dfb..55822c5 100644 --- a/vultron/cmd.py +++ b/vultron/cmd.py @@ -1,17 +1,28 @@ +import os +import vultron.api + class CmdNotFound(Exception): - pass + def __str__(self): + return f"error: command not found: {self.args[0]}" class NoApiKey(Exception): + # FIXME: __str__ message pass class FeatureMissing(Exception): + # FIXME: __str__ message pass +# FIXME: Move command execution into Command class, so wrapper can do pre/post +# work. class Command: + """A nop Vultron command. This feature is not implemented yet.""" + FTR_PRFX = "vultron_" - def __init__(self, cmd, api_key): - self.cmd = cmd + def __init__(self, prog, api_key, out): + self.prog = prog + self.out = out self.uses_api = True self.default = "help" @@ -20,11 +31,13 @@ class Command: if self.uses_api and api_key is None: raise NoApiKey() + self.api = vultron.api.Client(api_key) + def init(self): pass def find(self, name): - needle = "{prfx}{name}".format(prfx=self.FTR_PRFX, name=name.lower()) + needle = f"{self.FTR_PRFX}{name}".lower() haystack = dir(self) for legume in haystack: @@ -39,7 +52,39 @@ class Command: raise CmdNotFound(name) def vultron_help(self, *args): - pass + """This help message.""" + + prog = os.path.basename(self.prog) + cmd = type(self).__name__.lower() + subs = dir(self) + + print(f"usage: {prog} [options] {cmd} ") + print() + print("About:") + + if self.__doc__ is None: + print(Command.__doc__) + else: + print(self.__doc__) + + print() + # FIXME: print global options here + print("Sub-commands:") + + for sub in subs: + name = sub.lower() + + if name.startswith(self.FTR_PRFX): + name = name[len(self.FTR_PRFX):] + fn = getattr(self, sub) + + if callable(fn): + desc = name + + if fn.__doc__ is not None: + desc += f" - {fn.__doc__}" + + print(f"\t{desc}") def find_cmd(needle): needle = needle.lower() diff --git a/vultron/display.py b/vultron/display.py new file mode 100644 index 0000000..bdfd4cd --- /dev/null +++ b/vultron/display.py @@ -0,0 +1,99 @@ +# FIXME: Make this whole package less fragile +TYPES = ["tbl", "chrt", "csv", "json"] + +def wall(len, fill="\u2501"): + return fill * len + +# FIXME: normalize/analyze table data before building for optimization +def table(data): + lens = dict() + + for row in data: + for col in row: + col_len = len(col) + val_len = len(str(row[col])) + max_len = 0 + + if col in lens: + max_len = lens[col] + + lens[col] = max(col_len, val_len, max_len) + + col_hdrs = [{"column": col_hdr, "length": lens[col_hdr]} for col_hdr in lens] + out = f"\u250F{wall(col_hdrs[0]['length'] + 2)}" + + for col_hdr in col_hdrs[1:]: + out += f"\u2533{wall(col_hdr['length'] + 2)}" + + out += "\u2513\n" + + for col_hdr in col_hdrs: + out += f"\u2503 {col_hdr['column'].ljust(col_hdr['length'])} " + + out += f"\u2503\n\u2523{wall(col_hdrs[0]['length'] + 2)}" + + for col_hdr in col_hdrs[1:]: + out += f"\u254B{wall(col_hdr['length'] + 2)}" + + out += "\u252B\n" + + for row in data[:-1]: + for col_hdr in col_hdrs: + out += "\u2503 " + + if col_hdr["column"] in row: + out += str(row[col_hdr["column"]]).ljust(col_hdr["length"]) + else: + out += " " * col_hdr["length"] + + out += " " + + out += f"\u2503\n\u2523{wall(col_hdrs[0]['length'] + 2)}" + + for col_hdr in col_hdrs[1:]: + out += f"\u254B{wall(col_hdr['length'] + 2)}" + + out += "\u252B\n" + + for col_hdr in col_hdrs: + out += "\u2503 " + + if col_hdr["column"] in data[-1]: + out += str(data[-1][col_hdr["column"]]).ljust(col_hdr["length"]) + else: + out += " " * col_hdr["length"] + + out += " " + + out += "\u2503\n" + + out += f"\u2517{wall(col_hdrs[0]['length'] + 2)}" + + for col_hdr in col_hdrs[1:]: + out += f"\u253B{wall(col_hdr['length'] + 2)}" + + out += "\u251B" + + return out + +def chrt(data): + return data + +def csv(data): + return data + +def json(data): + return data + +def render(out, data): + # FIXME: option to pipe into pager, cut off at max column, squeeze, or do + # nothing (depends on TTY/PAGER) + if out == "tbl": + return table(data) + elif out == "chrt": + return chart(data) + elif out == "csv": + return csv(data) + elif out == "json": + return json(data) + # FIXME: error unrecognized out format diff --git a/vultron/vultron.py b/vultron/vultron.py index 9178e5c..238e9c5 100644 --- a/vultron/vultron.py +++ b/vultron/vultron.py @@ -2,26 +2,54 @@ import click import os import sys import vultron.cmd +import vultron.display class Help(vultron.cmd.Command): def init(self): self.uses_api = False def vultron_help(self, *args): + # FIXME: print global help message pass class Account(vultron.cmd.Command): - def init(self): - self.default = "info" + """Query for account information.""" def vultron_info(self, *args): - pass + """Display your account details.""" + + account = self.api.get("account") + display = vultron.display.render(self.out, [account["account"]]) + + print(display) + +class VPC(vultron.cmd.Command): + """Query for information on VPCs.""" + + def vultron_list(self, *args): + """Display all VPCs.""" + + vpcs = self.api.get("vpcs") + display = vultron.display.render(self.out, vpcs["vpcs"]) + + print(display) + +class Region(vultron.cmd.Command): + """Query for information on Vultr regions.""" + + def vultron_list(self, *args): + regions = self.api.get("regions") + display = vultron.display.render(self.out, regions["regions"]) + + print(display) @click.command() @click.option("--help", is_flag=True) @click.option("--api-key", type=str) +@click.option("--out", type=click.Choice(vultron.display.TYPES), default="tbl") @click.argument("args", nargs=-1) -def app(help, api_key, args): +# FIXME: click should not show any help messages +def app(help, api_key, out, args): env_key = os.getenv("VULTR_API_KEY") if api_key is None and env_key: @@ -35,7 +63,7 @@ def app(help, api_key, args): try: vultron_cmd = vultron.cmd.find_cmd(shortcut) - vultron_api = vultron_cmd(sys.argv[0], api_key=api_key) + vultron_api = vultron_cmd(sys.argv[0], api_key=api_key, out=out) if len(args): shortcut = args[0] @@ -46,4 +74,6 @@ def app(help, api_key, args): vultron_fn = vultron_api.find(shortcut) vultron_fn(*args) except vultron.cmd.CmdNotFound as err: - print(err) + # FIXME: Base Vultron error class + print(err, file=sys.stderr) + # FIXME: Activate help command