llvmbot monitor: Convert table output to a builder class
We could import a library to do this but it doesn't seem
worth it just for tables.
This uses a content manager so you don't have to write the
begin and end of each cell/row/table. Each method returns
self so you can just keep calling methods to build what you
need.
Change-Id: I8eef664f41039a9fbc662a148f292efd61866694
diff --git a/monitor/bot-status.py b/monitor/bot-status.py
index be1801f..9bf60d7 100755
--- a/monitor/bot-status.py
+++ b/monitor/bot-status.py
@@ -21,6 +21,7 @@
# to download multiple files.
import requests
from textwrap import dedent
+from make_table import Table
from buildkite_status import get_buildkite_bots_status
@@ -130,6 +131,7 @@
return bot_cache
+
def write_bot_status(config, output_file, bots_status):
temp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
@@ -157,8 +159,6 @@
"Build In Progress",
]
num_columns = len(column_titles)
- column_titles_html = "<tr>{}</tr>\n".format(
- "".join(["<th>{}</th>".format(t) for t in column_titles]))
# The first table should also say when this was generated.
# If we were to put this in its own header only table, it would
@@ -167,85 +167,83 @@
# Dump all servers / bots
for server in filter(not_ignored, config):
- base_url = server['base_url']
- builder_url = server['builder_url']
- build_url = server['build_url']
+ with Table(temp) as table:
+ table.Border(0).Cellspacing(1).Cellpadding(2)
- temp.write("<table border=0 cellspacing=1 cellpadding=2>\n")
- temp.write("<tr><td colspan={}> </td><tr>\n".format(num_columns))
- if first:
- temp.write("<tr><th colspan={}>Generated {} ({})</td><tr>\n"
- .format(num_columns, datetime.today().ctime(),
- time.tzname[time.daylight]))
- temp.write("<tr><td colspan={}> </td><tr>\n".format(num_columns))
- first = False
+ table.AddRow().AddCell().Colspan(num_columns)
- temp.write("<tr><th colspan={}>{}</td><tr>\n"
- .format(num_columns, server['name']))
+ if first:
+ table.AddRow().AddHeader("Generated {} ({})".format(
+ datetime.today().ctime(), time.tzname[time.daylight])).Colspan(num_columns)
+ table.AddRow().AddCell().Colspan(num_columns)
+ first = False
- for builder in server['builders']:
- temp.write("<tr><td colspan={}> </td><tr>\n".format(num_columns))
- temp.write("<tr><th colspan={}>{}</th><tr>\n".format(num_columns, builder['name']))
- temp.write(column_titles_html)
- temp.write("<tbody>\n")
- for bot in builder['bots']:
- temp.write("<tr>\n")
- logging.debug("Writing out status for {}".format(bot['name']))
- try:
- status = bots_status[(base_url, bot['name'])]
- except KeyError:
- temp.write(" <td colspan={}>{} is offline!</td>\n</tr>\n"
- .format(num_columns, bot['name']))
- continue
- else:
- if not status.get('valid', True):
- temp.write(" <td colspan={}>Could not read status for {}!</td>\n</tr>\n"
- .format(num_columns, bot['name']))
+ table.AddRow().AddHeader(server['name']).Colspan(num_columns)
+
+ for builder in server['builders']:
+ table.AddRow().AddCell().Colspan(num_columns)
+ table.AddRow().AddHeader(builder['name']).Colspan(num_columns)
+ title_row = table.AddRow()
+ for title in column_titles:
+ title_row.AddHeader(title)
+
+ table.BeginBody()
+
+ for bot in builder['bots']:
+ logging.debug("Writing out status for {}".format(bot['name']))
+
+ row = table.AddRow()
+ base_url = server['base_url']
+ try:
+ status = bots_status[(base_url, bot['name'])]
+ except KeyError:
+ row.AddCell("{} is offline!".format(bot['name'])).Colspan(num_columns)
+ continue
+ else:
+ if not status.get('valid', True):
+ row.AddCell("Could not read status for {}!".format(
+ bot['name'])).Colspan(num_columns)
continue
- temp.write(" <td><a href='{}'>{}</a></td>\n".format(
- status['builder_url'], bot['name']))
- temp.write(" <td><font color='{}'>{}</font></td>\n"
- .format('red' if status['fail'] else 'green',
- 'FAIL' if status['fail'] else 'PASS'))
- empty_cell=" <td> </td>\n"
- if 'time_since' in status:
- time_since = status['time_since']
- # No build should be taking more than a day
- if time_since > timedelta(hours=24):
- time_since = "<p style=\"color:red\">{}</p>".format(
- time_since)
- else:
- time_since = str(time_since)
+ row.AddCell("<a href='{}'>{}</a>".format(status['builder_url'], bot['name']))
+ row.AddCell("<font color='{}'>{}</font>"
+ .format('red' if status['fail'] else 'green',
+ 'FAIL' if status['fail'] else 'PASS'))
- temp.write(" <td>{}</td>\n".format(time_since))
- else:
- temp.write(empty_cell)
- if 'duration' in status:
- temp.write(" <td>{}</td>\n".format(status['duration']))
- else:
- temp.write(empty_cell)
- if 'number' in status:
- temp.write(" <td><a href='{}'>{}</a></td>\n".format(
- status['build_url'], status['number']))
- else:
- temp.write(empty_cell)
- if 'steps' in status and status['steps']:
- def render_step(name, result):
- return "<font color='{}'>{}</font>".format(RESULT_COLORS[result], name)
- step_list = ', '.join(render_step(name, result) for name, result in status['steps'])
- temp.write(" <td style=\"text-align:center\">{}</td>\n".format(step_list))
- else:
- temp.write(empty_cell)
- if 'next_in_progress' in status:
- temp.write(" <td>{}</td>\n".format(
- "Yes" if status['next_in_progress'] else "No"))
- else:
- # No value means we don't know either way.
- temp.write(empty_cell)
- temp.write("</tr>\n")
- temp.write("</tbody>\n")
- temp.write("</table>\n")
+ time_since_cell = row.AddCell()
+ if 'time_since' in status:
+ time_since = status['time_since']
+ # No build should be taking more than a day
+ if time_since > timedelta(hours=24):
+ time_since = "<p style=\"color:red\">{}</p>".format(
+ time_since)
+ else:
+ time_since = str(time_since)
+
+ time_since_cell.Content(time_since)
+
+ duration_cell = row.AddCell()
+ if 'duration' in status:
+ duration_cell.Content(status['duration'])
+
+ number_cell = row.AddCell()
+ if 'number' in status:
+ number_cell.Content("<a href='{}'>{}</a>".format(
+ status['build_url'], status['number']))
+
+ steps_cell = row.AddCell()
+ if 'steps' in status and status['steps']:
+ def render_step(name, result):
+ return "<font color='{}'>{}</font>".format(RESULT_COLORS[result], name)
+ step_list = ', '.join(render_step(name, result) for name, result in status['steps'])
+ steps_cell.Style("text-align:center").Content(step_list)
+
+ next_in_progress_cell = row.AddCell()
+ if 'next_in_progress' in status:
+ next_in_progress_cell.Content(
+ "Yes" if status['next_in_progress'] else "No")
+
+ table.EndBody()
# Move temp to main (atomic change)
temp.close()
diff --git a/monitor/make_table.py b/monitor/make_table.py
new file mode 100644
index 0000000..2f115b5
--- /dev/null
+++ b/monitor/make_table.py
@@ -0,0 +1,132 @@
+# This file contains a basic "builder" style API for making HTML tables
+# and writing them to a file once finished.
+#
+# Use it as follows:
+# with Table(outfile) as table:
+# table.AddRow().AddCell("foo")
+#
+# To get:
+# <table>
+# <td>foo</td>
+# </table>
+#
+# Methods return a reference to self, or to the new thing you added.
+# This means you can keep chaining calls to build what you want.
+#
+# table.AddRow().AddCell("foo").Colspan(1).Style("mystyle")
+
+
+class TableCell(object):
+ def __init__(self, name, content=None):
+ self.name = name
+ self.content = content
+ self.style = None
+ self.colspan = None
+
+ def Style(self, style):
+ self.style = style
+ return self
+
+ def Colspan(self, colspan):
+ self.colspan = colspan
+ return self
+
+ def Content(self, content):
+ self.content = content
+ return self
+
+ def __str__(self):
+ return " <{}{}{}>{}</{}>".format(
+ self.name,
+ "" if self.style is None else ' style="{}"'.format(self.style),
+ "" if self.colspan is None else " colspan={}".format(self.colspan),
+ " " if self.content is None else self.content,
+ self.name,
+ )
+
+
+class Cell(TableCell):
+ def __init__(self, content=None):
+ super(Cell, self).__init__("td", content)
+
+
+class Header(TableCell):
+ def __init__(self, content=None):
+ super(Header, self).__init__("th", content)
+
+
+class Row(object):
+ def __init__(self):
+ self.cells = []
+
+ def AddCell(self, content=None):
+ self.cells.append(Cell(content))
+ return self.cells[-1]
+
+ def AddHeader(self, content=None):
+ self.cells.append(Header(content))
+ return self.cells[-1]
+
+ def __str__(self):
+ return "\n".join([" <tr>", *map(str, self.cells), " </tr>"])
+
+
+class TableBody(object):
+ def __init__(self, close=False):
+ self.close = close
+
+ def __str__(self):
+ return "<{}tbody>".format("/" if self.close else "")
+
+
+class Table(object):
+ def __init__(self, out):
+ self.out = out
+ self.rows = []
+ self.border = None
+ self.cellspacing = None
+ self.cellpadding = None
+ self.body_begins = None
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ self.out.write("\n" + str(self))
+
+ def __str__(self):
+ open_tag = "<table{}{}{}>".format(
+ "" if self.border is None else " border={}".format(self.border),
+ ""
+ if self.cellspacing is None
+ else " cellspacing={}".format(self.cellspacing),
+ ""
+ if self.cellpadding is None
+ else " cellpadding={}".format(self.cellpadding),
+ )
+ rows = map(str, self.rows)
+ close_tag = "</table>"
+
+ return "\n".join([open_tag, *rows, close_tag])
+
+ def AddRow(self):
+ self.rows.append(Row())
+ return self.rows[-1]
+
+ def Border(self, border):
+ self.border = border
+ return self
+
+ def Cellspacing(self, cellspacing):
+ self.cellspacing = cellspacing
+ return self
+
+ def Cellpadding(self, cellpadding):
+ self.cellpadding = cellpadding
+ return self
+
+ def BeginBody(self):
+ self.rows.append(TableBody())
+
+ def EndBody(self):
+ self.rows.append(TableBody(close=True))