Improve error reporting and table generation
This commit is contained in:
parent
e7159bb6a4
commit
5b4999f12a
|
@ -1,20 +1,32 @@
|
|||
import inspect
|
||||
import os
|
||||
import vultron.api
|
||||
import vultron.display
|
||||
import vultron.err
|
||||
|
||||
class CmdNotFound(Exception):
|
||||
class CmdNotFound(vultron.err.Error):
|
||||
def __str__(self):
|
||||
return f"error: command not found: {self.args[0]}"
|
||||
return f"command not found: {self.args[0]}"
|
||||
|
||||
class NoApiKey(Exception):
|
||||
# FIXME: __str__ message
|
||||
pass
|
||||
class FeatureMissing(vultron.err.Error):
|
||||
def __str__(self):
|
||||
return f"feature not yet implemented: {self.args[0]}"
|
||||
|
||||
class FeatureMissing(Exception):
|
||||
# FIXME: __str__ message
|
||||
pass
|
||||
class NoApiKey(vultron.err.Error):
|
||||
def __str__(self):
|
||||
return f"no api key provided"
|
||||
|
||||
# FIXME: Move command execution into Command class, so wrapper can do pre/post
|
||||
# work.
|
||||
class NotEnoughArgs(vultron.err.Error):
|
||||
def __str__(self):
|
||||
name = self.args[0]
|
||||
more = self.args[1]
|
||||
|
||||
if self.args[1] > 1:
|
||||
return f"sub-command requires {more} more arguments: {name}"
|
||||
else:
|
||||
return f"sub-command requires 1 more argument: {name}"
|
||||
|
||||
# FIXME: Handle paging
|
||||
class Command:
|
||||
"""A nop Vultron command. This feature is not implemented yet."""
|
||||
|
||||
|
@ -33,8 +45,19 @@ class Command:
|
|||
|
||||
self.api = vultron.api.Client(api_key)
|
||||
|
||||
def init(self):
|
||||
pass
|
||||
def exec_shortcut(self, shortcut, *args):
|
||||
fn = self.find(shortcut)
|
||||
sig = inspect.signature(fn)
|
||||
|
||||
if len(args) < len(sig.parameters):
|
||||
name = fn.__name__[len(self.FTR_PRFX):].lower()
|
||||
raise NotEnoughArgs(name, len(sig.parameters) - len(args))
|
||||
|
||||
data = fn(*args)
|
||||
|
||||
if data is not None:
|
||||
display = vultron.display.FORMATS[self.out](data)
|
||||
print(display.render())
|
||||
|
||||
def find(self, name):
|
||||
needle = f"{self.FTR_PRFX}{name}".lower()
|
||||
|
@ -51,7 +74,10 @@ class Command:
|
|||
|
||||
raise CmdNotFound(name)
|
||||
|
||||
def vultron_help(self, *args):
|
||||
def init(self):
|
||||
pass
|
||||
|
||||
def vultron_help(self):
|
||||
"""This help message."""
|
||||
|
||||
prog = os.path.basename(self.prog)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import vultron.cmd
|
||||
import vultron.display
|
||||
|
||||
class Account(vultron.cmd.Command):
|
||||
"""Query for account information."""
|
||||
|
@ -7,6 +8,4 @@ class Account(vultron.cmd.Command):
|
|||
"""Display your account details."""
|
||||
|
||||
account = self.api.get("account")
|
||||
display = vultron.display.render(self.out, [account["account"]])
|
||||
|
||||
print(display)
|
||||
return [account["account"]]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import vultron.cmd
|
||||
import vultron.display
|
||||
|
||||
class Help(vultron.cmd.Command):
|
||||
def init(self):
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
import vultron.cmd
|
||||
import vultron.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"])
|
||||
def vultron_list(self):
|
||||
"""Display all available regions."""
|
||||
|
||||
print(display)
|
||||
regions = self.api.get("regions")
|
||||
return regions["regions"]
|
||||
|
||||
def vultron_plan(self, region_id):
|
||||
"""Display available plans for a given region."""
|
||||
|
||||
plans = self.api.get("regions", region_id, "availability")
|
||||
table = [{"plan": plan} for plan in plans["available_plans"]]
|
||||
return table
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import vultron.cmd
|
||||
import vultron.display
|
||||
|
||||
class Vpc(vultron.cmd.Command):
|
||||
"""Query for information on VPCs."""
|
||||
|
@ -7,6 +8,4 @@ class Vpc(vultron.cmd.Command):
|
|||
"""Display all VPCs."""
|
||||
|
||||
vpcs = self.api.get("vpcs")
|
||||
display = vultron.display.render(self.out, vpcs["vpcs"])
|
||||
|
||||
print(display)
|
||||
return vpcs["vpcs"]
|
||||
|
|
|
@ -1,99 +1,125 @@
|
|||
# FIXME: Make this whole package less fragile
|
||||
TYPES = ["tbl", "chrt", "csv", "json"]
|
||||
class OutFormat:
|
||||
def __init__(self, data):
|
||||
self.spans = { }
|
||||
self.rows = [ ]
|
||||
|
||||
def wall(len, fill="\u2501"):
|
||||
return fill * len
|
||||
for row in data:
|
||||
vals = { }
|
||||
|
||||
# FIXME: normalize/analyze table data before building for optimization
|
||||
def table(data):
|
||||
lens = dict()
|
||||
for field in row:
|
||||
vals[field] = self.normalize(row[field])
|
||||
|
||||
for row in data:
|
||||
for col in row:
|
||||
col_len = len(col)
|
||||
val_len = len(str(row[col]))
|
||||
max_len = 0
|
||||
if field in self.spans:
|
||||
self.spans[field] = max(self.spans[field], len(vals[field]))
|
||||
else:
|
||||
self.spans[field] = max(len(field), len(vals[field]))
|
||||
|
||||
if col in lens:
|
||||
max_len = lens[col]
|
||||
self.rows.append(vals)
|
||||
|
||||
lens[col] = max(col_len, val_len, max_len)
|
||||
self.cols = list(self.spans.keys())
|
||||
|
||||
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"])
|
||||
def normalize(self, field):
|
||||
if type(field) == list:
|
||||
return ", ".join(field)
|
||||
else:
|
||||
out += " " * col_hdr["length"]
|
||||
return str(field)
|
||||
|
||||
out += " "
|
||||
def render(self):
|
||||
# FIXME: consider if this should do anything
|
||||
pass
|
||||
|
||||
out += "\u2503\n"
|
||||
class Basic(OutFormat):
|
||||
def __init__(self, data):
|
||||
super().__init__(data)
|
||||
|
||||
out += f"\u2517{wall(col_hdrs[0]['length'] + 2)}"
|
||||
self.top = {
|
||||
"left": "+", "right": "+",
|
||||
"divide": "+", "fill": "-"
|
||||
}
|
||||
|
||||
for col_hdr in col_hdrs[1:]:
|
||||
out += f"\u253B{wall(col_hdr['length'] + 2)}"
|
||||
self.base = {
|
||||
"left": "+", "right": "+",
|
||||
"divide": "+", "fill": "-"
|
||||
}
|
||||
|
||||
out += "\u251B"
|
||||
self.sep = {
|
||||
"left": "|", "right": "|",
|
||||
"divide": "+", "fill": "-"
|
||||
}
|
||||
|
||||
return out
|
||||
self.row = {
|
||||
"left": "|", "right": "|",
|
||||
"divide": "|", "fill": " "
|
||||
}
|
||||
|
||||
def chrt(data):
|
||||
return data
|
||||
def render(self):
|
||||
sep = ""
|
||||
top = ""
|
||||
hdr = ""
|
||||
base = ""
|
||||
fill = self.row["fill"]
|
||||
|
||||
def csv(data):
|
||||
return data
|
||||
for col in self.cols:
|
||||
width = self.spans[col] + 2
|
||||
sep += self.sep["divide"] + self.sep["fill"] * width
|
||||
top += self.top["divide"] + self.top["fill"] * width
|
||||
hdr += self.row["divide"] + fill + col.ljust(width - 1, fill)
|
||||
base += self.base["divide"] + self.base["fill"] * width
|
||||
|
||||
def json(data):
|
||||
return data
|
||||
sep = self.sep["left"] + sep[1:] + self.sep["right"] + "\n"
|
||||
top = self.top["left"] + top[1:] + self.top["right"] + "\n"
|
||||
hdr = self.row["left"] + hdr[1:] + self.row["right"] + "\n"
|
||||
base = self.base["left"] + base[1:] + self.base["right"]
|
||||
lines = [ ]
|
||||
|
||||
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
|
||||
for row in self.rows:
|
||||
line = ""
|
||||
|
||||
for col in self.cols:
|
||||
width = self.spans[col] + 2
|
||||
|
||||
if col in row:
|
||||
line += self.row["divide"] + fill + row[col].ljust(width - 1, fill)
|
||||
else:
|
||||
line += slef.row["divide"] + fill * width
|
||||
|
||||
lines.append(self.row["left"] + line[1:] + self.row["right"] + "\n")
|
||||
|
||||
return top + sep.join([hdr, *lines]) + base
|
||||
|
||||
class Table(Basic):
|
||||
def __init__(self, data):
|
||||
super().__init__(data)
|
||||
|
||||
self.top = {
|
||||
"left": "\u250F", "right": "\u2513",
|
||||
"divide": "\u2533", "fill": "\u2501"
|
||||
}
|
||||
|
||||
self.base = {
|
||||
"left": "\u2517", "right": "\u251B",
|
||||
"divide": "\u253B", "fill": "\u2501"
|
||||
}
|
||||
|
||||
self.sep = {
|
||||
"left": "\u2523", "right": "\u252B",
|
||||
"divide": "\u254B", "fill": "\u2501"
|
||||
}
|
||||
|
||||
self.row = {
|
||||
"left": "\u2503", "right": "\u2503",
|
||||
"divide": "\u2503", "fill": " "
|
||||
}
|
||||
|
||||
class Json(OutFormat):
|
||||
pass
|
||||
|
||||
class Csv(OutFormat):
|
||||
pass
|
||||
|
||||
FORMATS = {
|
||||
"basic": Basic,
|
||||
"table": Table,
|
||||
"json": Json,
|
||||
"csv": Csv
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
class Error(Exception):
|
||||
def __str__(self):
|
||||
# FIXME: should show full traceback?
|
||||
return f"something went wrong: {self.args}"
|
|
@ -3,13 +3,15 @@ import os
|
|||
import sys
|
||||
import vultron.cmd
|
||||
import vultron.display
|
||||
import vultron.err
|
||||
|
||||
from vultron.cmds import *
|
||||
|
||||
@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.option("--out",
|
||||
type=click.Choice(vultron.display.FORMATS.keys()), default="table")
|
||||
@click.argument("args", nargs=-1)
|
||||
# FIXME: click should not show any help messages
|
||||
def app(help, api_key, out, args):
|
||||
|
@ -34,9 +36,7 @@ def app(help, api_key, out, args):
|
|||
else:
|
||||
shortcut = vultron_api.default
|
||||
|
||||
vultron_fn = vultron_api.find(shortcut)
|
||||
vultron_fn(*args)
|
||||
except vultron.cmd.CmdNotFound as err:
|
||||
# FIXME: Base Vultron error class
|
||||
print(err, file=sys.stderr)
|
||||
vultron_api.exec_shortcut(shortcut, *args)
|
||||
except vultron.err.Error as err:
|
||||
print(f"error: {err}", file=sys.stderr)
|
||||
# FIXME: Activate help command
|
||||
|
|
Loading…
Reference in New Issue