219 lines
5.8 KiB
Python
Executable File
219 lines
5.8 KiB
Python
Executable File
#!/usr/bin/python
|
|
"""Reads a CSV file on stdin, and prints an an HTML table on stdout.
|
|
|
|
The static HTML can then be made made dynamic with JavaScript, e.g. jQuery
|
|
DataTable.
|
|
|
|
Use Cases:
|
|
|
|
- overview.csv -- each row is a metric
|
|
- links: to metric page
|
|
|
|
- status.csv -- each row is a day
|
|
- links: to log.txt, to results.html
|
|
"""
|
|
|
|
import cgi
|
|
import csv
|
|
import optparse
|
|
import sys
|
|
|
|
import util
|
|
|
|
|
|
def CreateOptionsParser():
|
|
p = optparse.OptionParser()
|
|
|
|
# We are taking a path, and not using stdin, because we read it twice.
|
|
p.add_option(
|
|
'--col-format', dest='col_formats', metavar="'COLNAME FMT'", type='str',
|
|
default=[], action='append',
|
|
help='Add HTML links to the named column, using the given Python '
|
|
'.format() string')
|
|
|
|
p.add_option(
|
|
'--def', dest='defs', metavar="'NAME VALUE'", type='str',
|
|
default=[], action='append',
|
|
help='Define varaibles for use in format strings')
|
|
|
|
p.add_option(
|
|
'--as-percent', dest='percent_cols', metavar="COLNAME", type='str',
|
|
default=[], action='append',
|
|
help='Format this floating point column as a percentage string')
|
|
|
|
# TODO: We could include this by default, and then change all the HTML to
|
|
# have <div> placeholders instead of <table>.
|
|
p.add_option(
|
|
'--table', dest='table', default=False, action='store_true',
|
|
help='Add <table></table> tags (useful for testing)')
|
|
|
|
return p
|
|
|
|
|
|
def ParseSpec(arg_list):
|
|
"""Given an argument list, return a string -> string dictionary."""
|
|
# The format string is passed the cell value. Escaped as HTML?
|
|
d = {}
|
|
for s in arg_list:
|
|
try:
|
|
name, value = s.split(' ', 1)
|
|
except ValueError:
|
|
raise RuntimeError('Invalid column format %r' % s)
|
|
d[name] = value
|
|
return d
|
|
|
|
|
|
def PrintRow(row, col_names, col_formats, defs, percent_cols):
|
|
"""Print a CSV row as HTML, using the given formatting.
|
|
|
|
Returns:
|
|
An array of booleans indicating whether each cell is a number.
|
|
"""
|
|
is_number_flags = [False] * len(col_names)
|
|
|
|
for i, cell in enumerate(row):
|
|
# The cell as a string. By default we leave it as is; it may be mutated
|
|
# below.
|
|
cell_str = cell
|
|
css_class = '' # CSS class for the cell.
|
|
col_name = col_names[i] # column that the cell is under
|
|
|
|
# Does the cell look like a float?
|
|
try:
|
|
cell_float = float(cell)
|
|
if col_name in percent_cols: # Floats can be formatted as percentages.
|
|
cell_str = '{:.1f}%'.format(cell_float * 100)
|
|
else:
|
|
# Arbitrarily use 3 digits of precision for display
|
|
cell_str = '{:.3f}'.format(cell_float)
|
|
css_class = 'num'
|
|
is_number_flags[i] = True
|
|
except ValueError:
|
|
pass
|
|
|
|
# Does it look lik an int?
|
|
try:
|
|
cell_int = int(cell)
|
|
cell_str = '{:,}'.format(cell_int)
|
|
css_class = 'num'
|
|
is_number_flags[i] = True
|
|
except ValueError:
|
|
pass
|
|
|
|
# Special CSS class for R NA values.
|
|
if cell_str.strip() == 'NA':
|
|
css_class = 'num na' # num should right justify; na should make it red
|
|
is_number_flags[i] = True
|
|
|
|
if css_class:
|
|
print ' <td class="{}">'.format(css_class),
|
|
else:
|
|
print ' <td>',
|
|
|
|
cell_safe = cgi.escape(cell_str)
|
|
|
|
# If the cell has a format string, print it this way.
|
|
|
|
fmt = col_formats.get(col_name) # e.g. "../{date}.html"
|
|
if fmt:
|
|
# Copy variable bindings
|
|
bindings = dict(defs)
|
|
|
|
# Also let the format string use other column names. TODO: Is there a
|
|
# more efficient way?
|
|
bindings.update(zip(col_names, [cgi.escape(c) for c in row]))
|
|
|
|
bindings[col_name] = cell_safe
|
|
|
|
print fmt.format(**bindings), # no newline
|
|
else:
|
|
print cell_safe, # no newline
|
|
|
|
print '</td>'
|
|
|
|
return is_number_flags
|
|
|
|
|
|
def ReadCsv(f):
|
|
"""Read the CSV file, returning the column names and rows."""
|
|
c = csv.reader(f)
|
|
|
|
# The first row of the CSV is assumed to be a header. The rest are data.
|
|
col_names = []
|
|
rows = []
|
|
for i, row in enumerate(c):
|
|
if i == 0:
|
|
col_names = row
|
|
continue
|
|
rows.append(row)
|
|
return col_names, rows
|
|
|
|
|
|
def PrintColGroup(col_names, col_is_numeric):
|
|
"""Print HTML colgroup element, used for JavaScript sorting."""
|
|
print '<colgroup>'
|
|
for i, col in enumerate(col_names):
|
|
# CSS class is used for sorting
|
|
if col_is_numeric[i]:
|
|
css_class = 'number'
|
|
else:
|
|
css_class = 'case-insensitive'
|
|
|
|
# NOTE: id is a comment only; not used
|
|
print ' <col id="{}" type="{}" />'.format(col, css_class)
|
|
print '</colgroup>'
|
|
|
|
|
|
def main(argv):
|
|
(opts, argv) = CreateOptionsParser().parse_args(argv)
|
|
|
|
col_formats = ParseSpec(opts.col_formats)
|
|
defs = ParseSpec(opts.defs)
|
|
|
|
col_names, rows = ReadCsv(sys.stdin)
|
|
|
|
for col in opts.percent_cols:
|
|
if col not in col_names:
|
|
raise RuntimeError('--percent-col %s is not a valid column' % col)
|
|
|
|
# By default, we don't print the <table> bit -- that's up to the host page
|
|
if opts.table:
|
|
print '<table>'
|
|
|
|
print '<thead>'
|
|
for col in col_names:
|
|
# change _ to space so long column names can wrap
|
|
print ' <td>%s</td>' % cgi.escape(col.replace('_', ' '))
|
|
print '</thead>'
|
|
|
|
# Assume all columns are numeric at first. Look at each row for non-numeric
|
|
# values.
|
|
col_is_numeric = [True] * len(col_names)
|
|
|
|
print '<tbody>'
|
|
for row in rows:
|
|
print ' <tr>'
|
|
is_number_flags = PrintRow(row, col_names, col_formats, defs,
|
|
opts.percent_cols)
|
|
|
|
# If one cell in a column is not a number, then the whole cell isn't.
|
|
for (i, is_number) in enumerate(is_number_flags):
|
|
if not is_number:
|
|
col_is_numeric[i] = False
|
|
|
|
print ' </tr>'
|
|
print '</tbody>'
|
|
|
|
PrintColGroup(col_names, col_is_numeric)
|
|
|
|
if opts.table:
|
|
print '</table>'
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
main(sys.argv)
|
|
except RuntimeError, e:
|
|
print >>sys.stderr, 'FATAL: %s' % e
|
|
sys.exit(1)
|