blob: 47f119b6461fed4491163bb1e2b92a7b87a0d79e [file] [log] [blame]
#
# Copyright (c) 2010,2011,2012,2013 Big Switch Networks, Inc.
#
# Licensed under the Eclipse Public License, Version 1.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.eclipse.org/legal/epl-v10.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied. See the License for the specific language governing
# permissions and limitations under the License.
#
#
# PRETTYPRINT
#
# This module contains classes that help formatting text for the CLI,
# including tables, individual records etc.
#
# Formatting information is stored in table_info, a dict of dicts.
import model_info_list
import os
import array
import datetime
import utif
import re
import fmtcnv
class PrettyPrinter():
table_info = None # Annotated table format. This is a dict of dicts. Set by cli.
def __init__(self, bs):
self.sdnsh = bs
self.table_info = {}
self.format_version = {}
self.format_added_modules = {}
self.add_format('MODEL',
'model_info',
model_info_list.model_info_dict)
def add_format(self, name, origin, format_dict):
"""
Add a format to the saved formats, from the dictionary
with 'name'. The 'name' can help identify the source
for this format when errors arise
"""
# very interesting items in a format are the field_orderings,
# and the fields description. It could make sense to only
# allow specific items from the fields, to prevent field-output
# formatting from using uninnteded database fields.
for (format_name, format) in format_dict.items():
if not format_name in self.table_info:
self.table_info[format_name] = { }
# combine details: top level items
for (item_name, item_value) in format.items():
if item_name == 'fields':
if not item_name in self.table_info[format_name]:
self.table_info[format_name][item_name] = {}
for (term_name, term_value) in item_value.items():
if not term_name in self.table_info[format_name][item_name]:
self.table_info[format_name][item_name][term_name] = {}
self.table_info[format_name][item_name][term_name].update(term_value)
if item_name == 'field-orderings':
if not item_name in self.table_info[format_name]:
self.table_info[format_name][item_name] = {}
self.table_info[format_name][item_name].update(item_value)
#
# save the name of the source for this format
if not 'self' in self.table_info[format_name]:
self.table_info[format_name]['self'] = name
if not 'origin' in self.table_info[format_name]:
self.table_info[format_name]['origin'] = origin
#
# Add the 'Idx' field if it's not there.
if format_name in self.table_info:
if not 'fields' in self.table_info[format_name]:
self.table_info[format_name]['fields'] = {}
if not 'Idx' in self.table_info[format_name]['fields']:
self.table_info[format_name]['fields']['Idx'] = {
'verbose-name' : '#',
'type' : 'CharField'
}
def add_module_name(self, version, module):
"""
Save some state about the version/module
"""
if version not in self.format_version:
self.format_version[version] = [module]
else:
self.format_version[version].append(module)
def add_formats_from_module(self, version, module):
self.add_module_name(version, module)
for name in dir(module):
if re.match(r'.*_FORMAT$', name):
if module.__name__ not in self.format_added_modules:
self.format_added_modules[module.__name__] = [name]
if name not in self.format_added_modules[module.__name__]:
self.format_added_modules[module.__name__].append(name)
self.add_format(name, module.__name__, getattr(module, name))
# Utility Functions
def get_field_info(self, obj_type_info, field_name):
return obj_type_info['fields'].get(field_name, None)
def format_to_alias_update(self, display_format, update):
"""
Given a format, find all the formatters, and for each
formatter, determine which aliases need to be updated.
"""
if display_format not in self.table_info:
# this will fail soon enough when final output is attempted
return
format_dict = self.table_info[display_format]
if not 'fields' in format_dict:
return
for (field_name, field_dict) in format_dict['fields'].items():
if 'formatter' in field_dict:
fmtcnv.formatter_to_alias_update(field_dict['formatter'], update)
elif 'entry_formatter' in field_dict:
fmtcnv.formatter_to_alias_update(field_dict['entry-formatter'], update)
return update
def formats(self):
return self.table_info.keys()
def format_details(self):
"""
Return a table of formats suitable as a format_table parameter
"""
return [ { 'format' : x,
'format dict' : self.table_info[x]['self'],
'origin' : self.table_info[x]['origin'] }
for x in self.formats()]
def format_as_header(self, field):
# LOOK! could prob do something fancy to handle camelCapStrings or underscore_strings
return field[0].capitalize() + field[1:]
def get_header_for_field(self, obj_type_info, field_name):
field_info = self.get_field_info(obj_type_info, field_name)
if not field_info:
return self.format_as_header(field_name)
default_header = self.format_as_header(field_name)
return field_info.get('verbose-name', default_header)
def format_table(self, data_list, display_format = None, field_ordering="default"):
"""
Takes in list of dicts and generates nice table, e.g.
id slice_id MAC Address ip Switch ID
------------------------------------------------
1 1 00:00:00:00:01:03 10013 150861407404
2 2 00:00:00:00:01:01 10011 150861407404
3 1 00:00:00:00:02:03 10023 150866955514
@param data_list - a list of dicts
@param format describes the format of the output table to display
@param field_ordering is the field_ordering identifier in the model,
there can be multiple ("default", "brief", "detailed")
"""
#
# first, determine a list of fields to be printed, then using that list,
# determine the fields width of each field, including calling any formatting
# function, then format the result
#
# during the format computation, replace the value with the 'formatted value'
# to prevent multiple calls to the same formatting funcition
#
if not data_list or not type(data_list) == list:
if type(data_list) == dict and "error_type" in data_list:
return data_list.get("description", "Internal error")
return "None."
format_info = self.table_info.get(display_format, None)
if self.sdnsh.description:
if format_info == None:
print 'format_table: missing format %s' % display_format
else:
format_from = format_info.get('self', '')
print 'format_table: %s %s %d entries' % (
display_format, format_from, len(data_list))
field_widths = {}
field_headers = {}
fields_to_print = []
#
# do the 'figur'n for which fields will get printed.
#
# to determine the length, call the formatting function, and replace the
# value for that field with the updated value; then 'cypher the length.
#
# note that field_widths.keys() are all the possible fields
# check if the headers makes any field wider and set fields_to_print
#
if format_info:
if 'field-orderings' in format_info:
fields_to_print = format_info['field-orderings'].get(field_ordering, [])
if len(fields_to_print) == 0: # either no field_orderings or couldn't find specific
fields_to_print = format_info['fields'].keys()
for f in fields_to_print:
header = self.get_header_for_field(format_info, f)
field_headers[f] = header
field_widths[f] = max(len(header), field_widths.get(f, 0))
# LOOK! not done now... add in extra fields discovered in data_list if desired
# right now, fields_to_print is a projection on the data
else:
# get fields_to_print from the field names in data_list,
# which is (intended to be) a list of dictionaries
all_fields = utif.unique_list_from_list(sum([x.keys()
for x in data_list], []))
fields_to_print = sorted(all_fields)
if self.sdnsh.description:
print 'format_table: field order "%s" fields %s' % \
(field_ordering, fields_to_print)
#
# generate a fields_to_print ordered list with field_widths for each
# by going through all data and then using field_ordering if avail.
#
row_index = 0
for row in data_list:
row_index += 1
if not 'Idx' in row:
row['Idx'] = row_index
for key in fields_to_print:
#for (k,v) in row.items():
if format_info:
# don't worry about header here - do that soon below
info = self.get_field_info(format_info, key)
if info and info.get('formatter'):
row[key] = str(info['formatter'](row.get(key, ''), row))
w = len(row[key])
else:
w = len(str(row.get(key, '')))
else:
field_headers[key] = self.format_as_header(key)
w = max(len(str(row[key])), len(field_headers[key]))
field_widths[key] = max(w, field_widths.get(key, 0))
#
# generate the format_str and header lines based on fields_to_print
#
format_str_per_field = []
for f in fields_to_print:
format_str_per_field.append("%%(%s)-%ds" % (f, field_widths[f]))
row_format_str = " ".join(format_str_per_field) + "\n"
#
# finally print! only caveat is to handle sparse data with a blank_dict
# let result be a list, and append new strings to generate the final result,
# (for better python performance)
#
result= []
result.append(" ".join(format_str_per_field) % field_headers + "\n")
result.append("|".join(["-"*field_widths[f] for f in fields_to_print]) + "\n") # I <3 python too
blank_dict = dict([(f,"") for f in fields_to_print])
for row in data_list:
result.append(row_format_str % dict(blank_dict, **row))
return ''.join(result)
def format_entry(self, data, display_format=None, field_ordering="default", debug=False):
"""
Takes in parsed JSON object, generates nice single entry printout,
intended for 'details' display
@param data list of dictionaries, values for output
@param format name of format description to use for printing
@param field_ordering list of field to print
@param debug print values of compound keys
"""
if not data:
return "None."
elif type(data) == dict and "error_type" in data:
return data.get("description", "Internal error")
format_info = self.table_info.get(display_format, None)
# Print. Pretty please.
if format_info:
fields = format_info['fields']
else:
fields = dict([[x, {}] for x in data.keys()])
format_info = { 'fields' : fields }
if self.sdnsh.description:
print "format_entry: Missing format ", display_format, fields
# Find widest data field name
label_w = len( max(data, key=lambda x:len(x)) )
if format_info:
verbose_len = max([len(self.get_header_for_field(format_info, x)) for x in fields.keys()])
# This isn't exactly right, the verbose names for the fields ought to be replaced first
label_w = max(verbose_len, label_w)
label_str = "%%-%ds :" % label_w
# Use format_info for this table to order fields if possible
fields_to_print = None
if format_info:
if 'field-orderings' in format_info:
fields_to_print = format_info['field-orderings'].get(field_ordering, [])
else:
if self.sdnsh.description:
print 'Error: internal: %s field ordering %s not present for %s' % \
(display_format, field_ordering, format_info)
if fields_to_print == None or len(fields_to_print) == 0:
# either no field_orderings or couldn't find specific
fields_to_print = format_info['fields'].keys()
else:
fields_to_print = sorted(fields.keys())
result = ""
tmp_merged_dict = dict([(f,"") for f in fields_to_print], **data)
blank_dict = dict([(f,"") for f in fields_to_print])
# first print the requested fields
all_fields_in_correct_order = list(fields_to_print)
# then the remaining fields
all_fields_in_correct_order.extend([x for x in fields.keys()
if x not in fields_to_print])
# give all the formatter's a shot, save the updates
updated = {}
for e in all_fields_in_correct_order:
if format_info:
info = self.get_field_info(format_info, e)
if not info:
continue
if 'entry-formatter' in info:
updated[e] = info['entry-formatter'](
tmp_merged_dict.get(e, ''), tmp_merged_dict)
elif 'formatter' in info:
updated[e] = info['formatter'](
tmp_merged_dict.get(e, ''), tmp_merged_dict)
tmp_merged_dict.update(updated)
data.update(updated)
all_fields_in_correct_order = filter(lambda x: x in data,
all_fields_in_correct_order)
for e in all_fields_in_correct_order:
if format_info:
info = self.get_field_info(format_info, e)
if not debug and info and 'help_text' in info and info['help_text'][0] == '#':
# sdnsh._is_compound_key(), please skip display of compound key
continue
result += (label_str % self.get_header_for_field(format_info, e)+" "+
str(tmp_merged_dict[e]) + "\n");
return result[:-1]
def get_terminal_size(self):
def ioctl_GWINSZ(fd):
try:
import fcntl, termios, struct, os
cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ,
'1234'))
except:
return None
return cr
cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
if not cr:
try:
fd = os.open(os.ctermid(), os.O_RDONLY)
cr = ioctl_GWINSZ(fd)
os.close(fd)
except:
pass
if not cr:
try:
cr = (os.environ['LINES'], os.environ['COLUMNS'])
except:
cr = (24, 80)
if (cr[1] == 0 or cr[0] == 0):
return (80, 24)
return int(cr[1]), int(cr[0])
def format_time_series_graph(self, data, obj_type=None, field_ordering="default"):
if not data:
return "None."
elif type(data) == dict and "error_type" in data:
return data.get("description", "Internal error")
obj_type_info = self.table_info.get(obj_type, None)
if self.sdnsh.description:
print "format_time_series_graph", obj_type, obj_type_info
yunits = None
ylabel = "Value"
if (obj_type_info):
ylabel = obj_type_info['fields']['value']['verbose-name']
if 'units' in obj_type_info['fields']['value']:
yunits = obj_type_info['fields']['value']['units']
miny = None
maxy = None
minx = None
maxx = None
for (x,y) in data:
if miny == None or y < miny:
miny = y
if maxy == None or y > maxy:
maxy = y
if minx == None or x < minx:
minx = x
if maxx == None or x > maxx:
maxx = x
if (yunits == '%'):
maxy = 100
if isinstance(maxy, float) and maxy < 10.0:
axisyw = len('%.5f' % maxy)
else:
axisyw = len('%s' % maxy)
(twidth, theight) = self.get_terminal_size()
width = twidth-axisyw
height = theight-5
ybucket = float(maxy)/height;
xbucket = (maxx-minx)/width;
if (xbucket == 0):
minx = maxx - 1800000
maxx += 1800000
xbucket = maxx/width;
graph = array.array('c')
graph.fromstring(' ' * (width * height))
for (x,y) in data:
if (ybucket == 0):
yc = height
else:
yc = int(round((maxy-y)/ybucket))
if (xbucket == 0):
xy = width
else:
xc = int(round((x-minx)/xbucket))
if (yc < 0):
yc = 0
if (yc >= height):
yc = height-1
if (xc < 0):
xc = 0
if (xc >= width):
xc = width-1
#print (xc,yc, x, y, yc*width + xc)
for i in range(yc,height):
graph[i*width + xc] = '#'
b = '%s\n' % (ylabel)
if isinstance(maxy, float) and maxy < 10.0:
form = '%%%d.5f|%%s\n'
else:
form = '%%%ds|%%s\n'
for i in range(0,height-1):
ylabel = maxy - i*ybucket
if not isinstance(maxy, float) or maxy >= 10.0:
ylabel = int(round(ylabel))
b += (form % axisyw) % \
(ylabel,
''.join(graph[i*width:(i+1)*width-1]))
b += (form % axisyw) % \
(0, ''.join(graph[(height-1)*width:height*width-1]).replace(' ', '_'))
b += '%s' % (' ' * axisyw)
d = ' ' * axisyw
olddate = None
interval = (maxx - minx)/(width/7.0)
for i in range(0, width/7):
curtimestamp = minx + interval*i
if i == width/7-1:
df = (' ' * (width % 7)) + " %m/%d^"
tf = (' ' * (width % 7)) + " %H:%M^"
curtimestamp = maxx
else:
df = "^%m/%d "
tf = "^%H:%M "
curtime = datetime.datetime.fromtimestamp(curtimestamp/1000.0)
date = curtime.strftime(df)
b += curtime.strftime(tf)
if (date != olddate):
olddate = date
d += date
else:
d += ' ' * 7
b += '\n%s\n' % d
b += '%s%sTime' % (' ' * axisyw, ' ' * (width/2-2))
return b