2010-10-13 00:56:45 +08:00
|
|
|
#!/usr/bin/env python
|
|
|
|
|
from optparse import OptionParser
|
|
|
|
|
import sys
|
|
|
|
|
import httplib
|
|
|
|
|
import urllib
|
|
|
|
|
import base64
|
|
|
|
|
import json
|
2010-10-19 18:52:38 +08:00
|
|
|
import os
|
2010-10-13 00:56:45 +08:00
|
|
|
|
|
|
|
|
LISTABLE = ['connections', 'channels', 'exchanges', 'queues', 'bindings',
|
2010-10-19 17:39:53 +08:00
|
|
|
'users', 'vhosts', 'permissions']
|
2010-10-13 00:56:45 +08:00
|
|
|
|
2010-10-19 01:21:10 +08:00
|
|
|
PROMOTE_COLUMNS = ['vhost', 'name', 'type',
|
|
|
|
|
'source', 'destination', 'destination_type', 'routing_key']
|
|
|
|
|
|
2010-10-19 18:17:11 +08:00
|
|
|
URIS = {
|
|
|
|
|
'exchange': '/exchanges/{vhost}/{name}',
|
|
|
|
|
'queue': '/queues/{vhost}/{name}',
|
|
|
|
|
'binding': '/bindings/{vhost}/e/{source}/{destination_char}/{destination}',
|
|
|
|
|
'binding_del':'/bindings/{vhost}/e/{source}/{destination_char}/{destination}/{properties_key}',
|
|
|
|
|
'vhost': '/vhosts/{name}',
|
|
|
|
|
'user': '/users/{name}',
|
|
|
|
|
'permission': '/permissions/{vhost}/{user}'
|
|
|
|
|
}
|
|
|
|
|
|
2010-10-19 17:39:25 +08:00
|
|
|
DECLARABLE = {
|
2010-10-19 18:17:11 +08:00
|
|
|
'exchange': {'mandatory': ['name', 'type'],
|
|
|
|
|
'optional': {'auto_delete': 'false', 'durable': 'true'}},
|
|
|
|
|
'queue': {'mandatory': ['name'],
|
|
|
|
|
'optional': {'auto_delete': 'false', 'durable': 'true'}},
|
|
|
|
|
'binding': {'mandatory': ['source', 'destination_type', 'destination',
|
|
|
|
|
'routing_key'],
|
|
|
|
|
'optional': {}},
|
|
|
|
|
'vhost': {'mandatory': ['name'],
|
|
|
|
|
'optional': {}},
|
|
|
|
|
'user': {'mandatory': ['name', 'password', 'administrator'],
|
|
|
|
|
'optional': {}},
|
2010-10-19 17:39:25 +08:00
|
|
|
'permission': {'mandatory': ['vhost', 'user', 'scope',
|
|
|
|
|
'configure', 'write', 'read'],
|
2010-10-19 18:17:11 +08:00
|
|
|
'optional': {}}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DELETABLE = {
|
|
|
|
|
'exchange': {'mandatory': ['name']},
|
|
|
|
|
'queue': {'mandatory': ['name']},
|
|
|
|
|
'binding': {'mandatory': ['source', 'destination_type', 'destination',
|
|
|
|
|
'properties_key']},
|
|
|
|
|
'vhost': {'mandatory': ['name']},
|
|
|
|
|
'user': {'mandatory': ['name']},
|
|
|
|
|
'permission': {'mandatory': ['vhost', 'user']}
|
2010-10-19 17:39:25 +08:00
|
|
|
}
|
2010-10-19 01:51:42 +08:00
|
|
|
|
2010-10-19 18:29:39 +08:00
|
|
|
CLOSABLE = {
|
|
|
|
|
'connection': {'mandatory': ['name'],
|
|
|
|
|
'optional': {},
|
|
|
|
|
'uri': '/connections/{name}'}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PURGABLE = {
|
|
|
|
|
'queue': {'mandatory': ['name'],
|
|
|
|
|
'optional': {},
|
|
|
|
|
'uri': '/queues/{vhost}/{name}/contents'}
|
|
|
|
|
}
|
|
|
|
|
|
2010-10-19 18:17:11 +08:00
|
|
|
for k in DECLARABLE:
|
|
|
|
|
DECLARABLE[k]['uri'] = URIS[k]
|
|
|
|
|
|
|
|
|
|
for k in DELETABLE:
|
|
|
|
|
DELETABLE[k]['uri'] = URIS[k]
|
|
|
|
|
DELETABLE[k]['optional'] = {}
|
|
|
|
|
DELETABLE['binding']['uri'] = URIS['binding_del']
|
|
|
|
|
|
2010-10-13 00:56:45 +08:00
|
|
|
def make_usage():
|
2010-10-19 01:51:42 +08:00
|
|
|
usage = """usage: %prog [options] cmd
|
|
|
|
|
where cmd is one of:
|
2010-10-19 18:17:11 +08:00
|
|
|
|
2010-10-13 00:56:45 +08:00
|
|
|
"""
|
|
|
|
|
for l in LISTABLE:
|
2010-10-19 01:51:42 +08:00
|
|
|
usage += " list {0}\n".format(l)
|
2010-10-19 18:29:39 +08:00
|
|
|
usage += fmt_usage_stanza(DECLARABLE, 'declare')
|
|
|
|
|
usage += fmt_usage_stanza(DELETABLE, 'delete')
|
|
|
|
|
usage += fmt_usage_stanza(CLOSABLE, 'close')
|
|
|
|
|
usage += fmt_usage_stanza(PURGABLE, 'purge')
|
2010-10-19 18:17:11 +08:00
|
|
|
usage += """
|
|
|
|
|
export FILE
|
2010-10-13 00:56:45 +08:00
|
|
|
import FILE"""
|
|
|
|
|
return usage
|
|
|
|
|
|
2010-10-19 18:29:39 +08:00
|
|
|
def fmt_usage_stanza(root, verb):
|
2010-10-19 18:40:10 +08:00
|
|
|
def fmt_args(args):
|
|
|
|
|
res = " ".join(["<{0}>".format(a) for a in args['mandatory']])
|
|
|
|
|
opts = " ".join("{0}=...".format(o) for o in args['optional'].keys())
|
|
|
|
|
if opts != "":
|
|
|
|
|
res += " [{0}]".format(opts)
|
|
|
|
|
return res
|
2010-10-19 18:29:39 +08:00
|
|
|
|
|
|
|
|
text = "\n"
|
|
|
|
|
for k in root.keys():
|
2010-10-19 18:40:10 +08:00
|
|
|
text += " {0} {1} {2}\n".format(verb, k, fmt_args(root[k]))
|
2010-10-19 18:29:39 +08:00
|
|
|
return text
|
2010-10-19 01:51:42 +08:00
|
|
|
|
2010-10-13 00:56:45 +08:00
|
|
|
parser = OptionParser(usage=make_usage())
|
|
|
|
|
|
|
|
|
|
def make_parser():
|
|
|
|
|
parser.add_option("-H", "--host", dest="hostname", default="localhost",
|
|
|
|
|
help="connect to host HOST [default: %default]",
|
|
|
|
|
metavar="HOST")
|
|
|
|
|
parser.add_option("-P", "--port", dest="port", default="55672",
|
|
|
|
|
help="connect to port PORT [default: %default]",
|
|
|
|
|
metavar="PORT")
|
|
|
|
|
parser.add_option("-V", "--vhost", dest="vhost",
|
2010-10-19 01:51:42 +08:00
|
|
|
help="connect to vhost VHOST [default: all vhosts for list, '/' for declare]",
|
2010-10-13 00:56:45 +08:00
|
|
|
metavar="VHOST")
|
|
|
|
|
parser.add_option("-u", "--username", dest="username", default="guest",
|
|
|
|
|
help="connect using username USERNAME [default: %default]",
|
|
|
|
|
metavar="USERNAME")
|
|
|
|
|
parser.add_option("-p", "--password", dest="password", default="guest",
|
|
|
|
|
help="connect using password PASSWORD [default: %default]",
|
|
|
|
|
metavar="PASSWORD")
|
|
|
|
|
parser.add_option("-q", "--quiet", action="store_false", dest="verbose",
|
|
|
|
|
default=True, help="suppress status messages")
|
|
|
|
|
parser.add_option("-f", "--format", dest="format", default="table",
|
2010-10-19 01:21:10 +08:00
|
|
|
help="format for listing commands - one of [pretty_json, raw_json, table, tsv, long] [default: %default]")
|
|
|
|
|
parser.add_option("-d", "--depth", dest="depth", default="1",
|
|
|
|
|
help="maximum depth to recurse for listing tables [default: %default]")
|
|
|
|
|
|
2010-10-13 00:56:45 +08:00
|
|
|
|
2010-10-19 18:52:38 +08:00
|
|
|
def assert_usage(expr, error):
|
|
|
|
|
if not expr:
|
|
|
|
|
if error:
|
|
|
|
|
print "\nERROR: {0}\n".format(error)
|
|
|
|
|
print "{0} --help for help\n".format(os.path.basename(sys.argv[0]))
|
2010-10-13 00:56:45 +08:00
|
|
|
sys.exit(1)
|
|
|
|
|
|
2010-10-19 01:21:10 +08:00
|
|
|
def column_sort_key(col):
|
|
|
|
|
if col in PROMOTE_COLUMNS:
|
|
|
|
|
return (1, PROMOTE_COLUMNS.index(col))
|
|
|
|
|
else:
|
|
|
|
|
return (2, col)
|
|
|
|
|
|
2010-10-13 00:56:45 +08:00
|
|
|
def main():
|
|
|
|
|
make_parser()
|
|
|
|
|
(options, args) = parser.parse_args()
|
2010-10-19 18:52:38 +08:00
|
|
|
assert_usage(len(args) > 0, 'Action not specified')
|
2010-10-13 00:56:45 +08:00
|
|
|
mgmt = Management(options, args[1:])
|
|
|
|
|
mode = "invoke_" + args[0]
|
2010-10-19 18:52:38 +08:00
|
|
|
assert_usage(hasattr(mgmt, mode),
|
|
|
|
|
'Action {0} not understood'.format(args[0]))
|
2010-10-13 00:56:45 +08:00
|
|
|
method = getattr(mgmt, "invoke_%s" % args[0])
|
|
|
|
|
method()
|
|
|
|
|
|
|
|
|
|
class Management:
|
|
|
|
|
def __init__(self, options, args):
|
|
|
|
|
self.options = options
|
|
|
|
|
self.args = args
|
|
|
|
|
|
|
|
|
|
def get(self, path):
|
|
|
|
|
return self.http("GET", path, "")
|
|
|
|
|
|
|
|
|
|
def put(self, path, body):
|
|
|
|
|
return self.http("PUT", path, body)
|
|
|
|
|
|
|
|
|
|
def post(self, path, body):
|
|
|
|
|
return self.http("POST", path, body)
|
|
|
|
|
|
2010-10-19 18:17:11 +08:00
|
|
|
def delete(self, path):
|
|
|
|
|
return self.http("DELETE", path, "")
|
|
|
|
|
|
2010-10-13 00:56:45 +08:00
|
|
|
def http(self, method, path, body):
|
|
|
|
|
conn = httplib.HTTPConnection(self.options.hostname, self.options.port)
|
|
|
|
|
headers = {"Authorization":
|
|
|
|
|
"Basic " + base64.b64encode(self.options.username + ":" +
|
|
|
|
|
self.options.password)}
|
|
|
|
|
if body != "":
|
|
|
|
|
headers["Content-Type"] = "application/json"
|
|
|
|
|
conn.request(method, "/api%s" % path, body, headers)
|
|
|
|
|
resp = conn.getresponse()
|
|
|
|
|
if resp.status < 200 or resp.status >= 400:
|
|
|
|
|
raise Exception("Received %d %s for path %s\n%s"
|
|
|
|
|
% (resp.status, resp.reason, path, resp.read()))
|
|
|
|
|
return resp.read()
|
|
|
|
|
|
|
|
|
|
def verbose(self, string):
|
|
|
|
|
if self.options.verbose:
|
|
|
|
|
print string
|
|
|
|
|
|
|
|
|
|
def get_arg(self):
|
2010-10-19 18:52:38 +08:00
|
|
|
assert_usage(len(self.args) == 1, 'Exactly one argument required')
|
|
|
|
|
return self.args[0]
|
2010-10-13 00:56:45 +08:00
|
|
|
|
|
|
|
|
def invoke_export(self):
|
|
|
|
|
path = self.get_arg()
|
|
|
|
|
config = self.get("/all-configuration")
|
|
|
|
|
with open(path, 'w') as f:
|
|
|
|
|
f.write(config)
|
|
|
|
|
self.verbose("Exported configuration for %s to \"%s\""
|
|
|
|
|
% (self.options.hostname, path))
|
|
|
|
|
|
|
|
|
|
def invoke_import(self):
|
|
|
|
|
path = self.get_arg()
|
|
|
|
|
with open(path, 'r') as f:
|
|
|
|
|
config = f.read()
|
|
|
|
|
self.post("/all-configuration", config)
|
|
|
|
|
self.verbose("Imported configuration for %s from \"%s\""
|
|
|
|
|
% (self.options.hostname, path))
|
|
|
|
|
|
|
|
|
|
def invoke_list(self):
|
|
|
|
|
obj_type = self.get_arg()
|
2010-10-19 18:52:38 +08:00
|
|
|
assert_usage(obj_type in LISTABLE,
|
|
|
|
|
"Don't know how to list {0}".format(obj_type))
|
2010-10-13 00:56:45 +08:00
|
|
|
uri = "/%s" % obj_type
|
|
|
|
|
if self.options.vhost:
|
|
|
|
|
uri += "/%s" % urllib.quote_plus(self.options.vhost)
|
2010-10-19 01:21:10 +08:00
|
|
|
format_list(self.get(uri), self.options)
|
|
|
|
|
|
2010-10-19 01:51:42 +08:00
|
|
|
def invoke_declare(self):
|
2010-10-19 18:17:11 +08:00
|
|
|
(obj_type, uri, payload) = self.declare_delete_parse(DECLARABLE)
|
|
|
|
|
if obj_type == 'binding':
|
|
|
|
|
self.post(uri, json.dumps(payload))
|
|
|
|
|
else:
|
|
|
|
|
self.put(uri, json.dumps(payload))
|
|
|
|
|
self.verbose("{0} declared".format(obj_type))
|
|
|
|
|
|
|
|
|
|
def invoke_delete(self):
|
|
|
|
|
(obj_type, uri, payload) = self.declare_delete_parse(DELETABLE)
|
|
|
|
|
self.delete(uri)
|
|
|
|
|
self.verbose("{0} deleted".format(obj_type))
|
|
|
|
|
|
2010-10-19 18:29:39 +08:00
|
|
|
def invoke_close(self):
|
|
|
|
|
(obj_type, uri, payload) = self.declare_delete_parse(CLOSABLE)
|
|
|
|
|
self.delete(uri)
|
|
|
|
|
self.verbose("{0} closed".format(obj_type))
|
|
|
|
|
|
|
|
|
|
def invoke_purge(self):
|
|
|
|
|
(obj_type, uri, payload) = self.declare_delete_parse(PURGABLE)
|
|
|
|
|
self.delete(uri)
|
|
|
|
|
self.verbose("{0} purged".format(obj_type))
|
|
|
|
|
|
2010-10-19 18:17:11 +08:00
|
|
|
def declare_delete_parse(self, root):
|
2010-10-19 18:52:38 +08:00
|
|
|
assert_usage(len(self.args) > 0, 'Type not specified')
|
2010-10-19 01:51:42 +08:00
|
|
|
obj_type = self.args[0]
|
2010-10-19 18:52:38 +08:00
|
|
|
assert_usage(obj_type in root,
|
|
|
|
|
'Type {0} not recognised'.format(obj_type))
|
2010-10-19 18:17:11 +08:00
|
|
|
args = self.args[1:]
|
|
|
|
|
mandatory = root[obj_type]['mandatory']
|
|
|
|
|
optional = root[obj_type]['optional']
|
2010-10-19 01:51:42 +08:00
|
|
|
args = self.args[1:]
|
2010-10-19 18:52:38 +08:00
|
|
|
assert_usage(len(args) >= len(mandatory),
|
|
|
|
|
'{0} mandatory arguments required'.format(len(mandatory)))
|
2010-10-19 01:51:42 +08:00
|
|
|
payload = {}
|
|
|
|
|
for i in xrange(0, len(mandatory)):
|
|
|
|
|
payload[mandatory[i]] = args[i]
|
|
|
|
|
for k in optional.keys():
|
|
|
|
|
payload[k] = optional[k]
|
2010-10-19 18:40:10 +08:00
|
|
|
for optional_arg in args[len(mandatory):]:
|
2010-10-19 18:52:38 +08:00
|
|
|
assert_usage("=" in optional_arg,
|
|
|
|
|
'Optional argument {0} not in format name=value'.format(optional_arg))
|
2010-10-19 18:40:10 +08:00
|
|
|
(name, value) = optional_arg.split("=")
|
2010-10-19 18:52:38 +08:00
|
|
|
assert_usage(name in optional.keys(),
|
|
|
|
|
'Optional argument {0} not recognised'.format(name))
|
2010-10-19 18:40:10 +08:00
|
|
|
payload[name] = value
|
2010-10-19 01:51:42 +08:00
|
|
|
payload['arguments'] = {}
|
2010-10-19 17:39:25 +08:00
|
|
|
payload['vhost'] = self.options.vhost or '/'
|
|
|
|
|
uri_args = {}
|
|
|
|
|
for k in payload:
|
|
|
|
|
uri_args[k] = urllib.quote_plus(payload[k])
|
|
|
|
|
if k == 'destination_type':
|
|
|
|
|
uri_args['destination_char'] = payload[k][0]
|
2010-10-19 18:17:11 +08:00
|
|
|
uri = root[obj_type]['uri'].format(**uri_args)
|
|
|
|
|
return (obj_type, uri, payload)
|
2010-10-19 01:51:42 +08:00
|
|
|
|
2010-10-19 01:21:10 +08:00
|
|
|
def format_list(json_list, options):
|
|
|
|
|
format = options.format
|
|
|
|
|
formatter = None
|
|
|
|
|
if format == "raw_json":
|
|
|
|
|
print json_list
|
|
|
|
|
return
|
|
|
|
|
elif format == "pretty_json":
|
2010-10-13 00:56:45 +08:00
|
|
|
enc = json.JSONEncoder(False, False, True, True, True, 2)
|
2010-10-19 01:21:10 +08:00
|
|
|
print enc.encode(json.loads(json_list))
|
|
|
|
|
return
|
|
|
|
|
elif format == "tsv":
|
|
|
|
|
formatter = TSVList
|
|
|
|
|
elif format == "long":
|
|
|
|
|
formatter = LongList
|
|
|
|
|
elif format == "table":
|
|
|
|
|
formatter = TableList
|
2010-10-19 18:52:38 +08:00
|
|
|
assert_usage(formatter != None,
|
|
|
|
|
"Format {0} not recognised".format(format))
|
2010-10-19 01:21:10 +08:00
|
|
|
formatter_instance = formatter(options)
|
|
|
|
|
formatter_instance.display(json_list)
|
2010-10-13 00:56:45 +08:00
|
|
|
|
2010-10-19 01:21:10 +08:00
|
|
|
class Lister:
|
|
|
|
|
def verbose(self, string):
|
|
|
|
|
if self.options.verbose:
|
|
|
|
|
print string
|
2010-10-13 00:56:45 +08:00
|
|
|
|
2010-10-19 01:21:10 +08:00
|
|
|
def display(self, json_list):
|
|
|
|
|
(columns, table) = self.list_to_table(json.loads(json_list),
|
|
|
|
|
int(self.options.depth))
|
|
|
|
|
if len(table) > 0:
|
|
|
|
|
self.display_list(columns, table)
|
2010-10-13 00:56:45 +08:00
|
|
|
else:
|
|
|
|
|
self.verbose("No items")
|
|
|
|
|
|
2010-10-19 01:21:10 +08:00
|
|
|
def list_to_table(self, items, max_depth):
|
|
|
|
|
columns = {}
|
|
|
|
|
column_ix = {}
|
|
|
|
|
row = None
|
|
|
|
|
table = []
|
2010-10-13 00:56:45 +08:00
|
|
|
|
2010-10-19 01:21:10 +08:00
|
|
|
def add(prefix, depth, item, fun):
|
2010-10-13 00:56:45 +08:00
|
|
|
for key in item:
|
2010-10-19 01:21:10 +08:00
|
|
|
column = prefix == '' and key or (prefix + '_' + key)
|
|
|
|
|
if type(item[key]) == dict:
|
|
|
|
|
if depth < max_depth:
|
|
|
|
|
add(column, depth + 1, item[key], fun)
|
|
|
|
|
else:
|
|
|
|
|
fun(column, item[key])
|
|
|
|
|
|
|
|
|
|
def add_to_columns(col, val):
|
|
|
|
|
columns[col] = True
|
|
|
|
|
|
|
|
|
|
def add_to_row(col, val):
|
|
|
|
|
row[column_ix[col]] = str(val)
|
|
|
|
|
|
|
|
|
|
for item in items:
|
|
|
|
|
add('', 1, item, add_to_columns)
|
|
|
|
|
columns = columns.keys()
|
|
|
|
|
columns.sort(key=column_sort_key)
|
|
|
|
|
for i in xrange(0, len(columns)):
|
|
|
|
|
column_ix[columns[i]] = i
|
|
|
|
|
for item in items:
|
|
|
|
|
row = len(columns) * ['']
|
|
|
|
|
add('', 1, item, add_to_row)
|
|
|
|
|
table.append(row)
|
|
|
|
|
|
|
|
|
|
return (columns, table)
|
|
|
|
|
|
|
|
|
|
class TSVList(Lister):
|
|
|
|
|
def __init__(self, options):
|
|
|
|
|
self.options = options
|
|
|
|
|
|
|
|
|
|
def display_list(self, columns, table):
|
|
|
|
|
head = ""
|
|
|
|
|
for col in columns:
|
|
|
|
|
head += col + "\t"
|
|
|
|
|
self.verbose(head)
|
|
|
|
|
|
|
|
|
|
body = ""
|
|
|
|
|
for row in table:
|
|
|
|
|
for cell in row:
|
|
|
|
|
body += cell + "\t"
|
|
|
|
|
body += "\n"
|
|
|
|
|
print body
|
|
|
|
|
|
|
|
|
|
class LongList(Lister):
|
|
|
|
|
def __init__(self, options):
|
|
|
|
|
self.options = options
|
|
|
|
|
|
|
|
|
|
def display_list(self, columns, table):
|
|
|
|
|
sep = "\n" + "-" * 80 + "\n"
|
|
|
|
|
max_width = 0
|
|
|
|
|
for col in columns:
|
|
|
|
|
max_width = max(max_width, len(col))
|
|
|
|
|
fmt = "{0:>" + str(max_width) + "}: {1}"
|
|
|
|
|
print sep
|
|
|
|
|
for i in xrange(0, len(table)):
|
|
|
|
|
for j in xrange(0, len(columns)):
|
|
|
|
|
print fmt.format(columns[j], table[i][j])
|
|
|
|
|
print sep
|
|
|
|
|
|
|
|
|
|
class TableList(Lister):
|
|
|
|
|
def __init__(self, options):
|
|
|
|
|
self.options = options
|
|
|
|
|
|
|
|
|
|
def display_list(self, columns, table):
|
|
|
|
|
total = [columns]
|
|
|
|
|
total.extend(table)
|
|
|
|
|
self.ascii_table(total)
|
2010-10-13 00:56:45 +08:00
|
|
|
|
|
|
|
|
def ascii_table(self, rows):
|
|
|
|
|
table = ""
|
|
|
|
|
col_widths = [0] * len(rows[0])
|
|
|
|
|
for i in xrange(0, len(rows[0])):
|
|
|
|
|
for j in xrange(0, len(rows)):
|
|
|
|
|
col_widths[i] = max(col_widths[i], len(rows[j][i]))
|
|
|
|
|
self.ascii_bar(col_widths)
|
|
|
|
|
self.ascii_row(col_widths, rows[0], "^")
|
|
|
|
|
self.ascii_bar(col_widths)
|
|
|
|
|
for row in rows[1:]:
|
|
|
|
|
self.ascii_row(col_widths, row, "<")
|
|
|
|
|
self.ascii_bar(col_widths)
|
|
|
|
|
|
|
|
|
|
def ascii_row(self, col_widths, row, align):
|
|
|
|
|
txt = "|"
|
|
|
|
|
for i in xrange(0, len(col_widths)):
|
|
|
|
|
fmt = " {0:" + align + str(col_widths[i]) + "} "
|
|
|
|
|
txt += fmt.format(row[i]) + "|"
|
|
|
|
|
print txt
|
|
|
|
|
|
|
|
|
|
def ascii_bar(self, col_widths):
|
|
|
|
|
txt = "+"
|
|
|
|
|
for w in col_widths:
|
|
|
|
|
txt += ("-" * (w + 2)) + "+"
|
|
|
|
|
print txt
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|