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={}>&nbsp;</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={}>&nbsp;</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={}>&nbsp;</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>&nbsp;</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()