-#
-# format.py
-#
-# This file contains global functions for formatting and printing data. This
-# file should be imported into individual log-parsing scripts located in
-# logs/*. Data is formatted in HTML or plaintext. Writing to disk and/or
-# emailng data is left to __main__.py.
-#
+# -*- coding: utf-8 -*-
+
+"""
+This file contains global functions for formatting and printing data. This file
+should be imported into individual log-parsing scripts located in the default
+logparse.parsers module or in the user-supplied parsers directory. Data is
+formatted in HTML or plaintext. Writing to disk and/or emailng data is left to
+interface.py.
+"""
import os
import re
import locale
from string import Template
+from math import floor, ceil
+from tabulate import tabulate
+import textwrap
import logparse
-from . import interface, util, config
+from logparse import interface, util, mail, config
import logging
+logger = None
logger = logging.getLogger(__name__)
+
locale.setlocale(locale.LC_ALL, '') # inherit system locale
-#DEG = "°".encode('unicode_escape')
+
+
DEG = u'\N{DEGREE SIGN}'
CEL = "C"
TIMEFMT = "%X"
CORNERCHARS_SINGLE = ['└', '┘', '┐', '┌']
LINECHARS_DOUBLE = ['║', '═']
LINECHARS_SINGLE = ['│', '─']
+JXNCHARS_DOUBLE = ['╠', '╣', '╦', '╩', '╬']
+JXNCHARS_SINGLE = ['├', '┤', '┬', '┴', '┼']
+BULLET = "• "
+INDENT = " "
+SPLIT_CHARS = ['.', '(', ')', '[', ']', '&', r"/", "\\", ',', '-', '_']
+
+
+global VARSUBST
+
+
+def init_var():
+ """
+ Initialise variable substitution templates (should be called before doing
+ any substitutions)
+ """
+
+ global VARSUBST
+ css_path = config.prefs.get("html", "css")
+ if config.prefs.getboolean("html", "css-relpath"):
+ if interface.argparser.parse_args().no_write:
+ css_path = os.path.relpath(css_path, ".")
+ elif interface.argparser.parse_args().destination:
+ css_path = os.path.relpath(
+ css_path, interface.argparser.parse_args().destination())
+ elif config.prefs.get("logparse", "output"):
+ css_path = os.path.relpath(
+ css_path, config.prefs.get("logparse", "output"))
+ VARSUBST = {
+ "title": config.prefs.get("logparse", "title"),
+ "date": interface.start.strftime(DATEFMT),
+ "time": interface.start.strftime(TIMEFMT),
+ "hostname": util.hostname(config.prefs.get(
+ "logparse", "hostname-path")),
+ "version": logparse.__version__,
+ "css": css_path,
+ "period": util.LogPeriod("logparse").startdate.strftime(
+ TIMEFMT + " " + DATEFMT)
+ }
+
class Output:
"""
self.destination = ""
def append(self, content):
+ """
+ Add a string
+ """
+
self.content += content
def write(self, destination=""):
+ """
+ Output contents into a file
+ """
+
if destination == "":
destination = self.destination
if destination == "":
f.write(self.content)
logger.info("Written output to {}".format(destination))
+ def print_stdout(self, lines=False):
+ """
+ Echo the contents to the console
+ """
+
+ print()
+ if lines:
+ line = PlaintextLine(linewidth=
+ config.prefs.getint("plain", "linewidth"), double=True)
+ print(line.draw())
+ print(self.content)
+ if lines:
+ print(line.draw())
+ print()
+
class PlaintextOutput(Output):
"""
- Processes & outputs data in a plaintext form which can be read with cat or plaintext email.
+ Processes & outputs data in a plaintext form which can be read with cat or
+ plaintext email.
"""
def __init__(self, linewidth=80):
self.linewidth = linewidth;
def append_header(self, template=''):
- init_varfilter()
- box = PlaintextBox(content=Template("$title $version on $hostname\n\n$time $date").safe_substitute(varsubst), vpadding=2, hpadding="\t\t", linewidth=config.prefs['linewidth'])
- line = PlaintextLine(self.linewidth)
- self.append(box.draw() + line.draw())
+ """
+ Print details with some primitive formatting
+ """
+ box = PlaintextBox(content=
+ Template("$title $version on $hostname\n\n$time $date"
+ "\nParsing logs since $period")
+ .safe_substitute(VARSUBST),
+ vpadding=2, hpadding="\t\t", linewidth=self.linewidth)
+ self.append(box.draw() + "\n"*2)
def append_footer(self):
- init_varfilter()
- self.append(PlaintextLine(self.linewidth, vpadding=1).draw())
- self.append(Template("$hostname $time $date").safe_substitute(varsubst))
+ """
+ Append a horizontal line and some details
+ """
+ self.append(PlaintextLine(self.linewidth).draw())
+ self.append(Template("$hostname $time $date").safe_substitute(VARSUBST))
def append_section(self, section):
- self.append(PlaintextBox(content=section.title, double=False, fullwidth=False, vpadding=0, hpadding=" ").draw())
+ """
+ Call the appropriate methods to format a section (provided by a parser).
+ This should be run by interface.py after every instance of parse_log().
+ """
+
+ if section == None:
+ logger.warning("Received null section")
+ return
+ self.append(PlaintextBox(
+ content=section.title, double=False,
+ fullwidth=False, vpadding=0, hpadding=" ").draw())
+ if section.period and section.period.unique:
+ self.append("\n(since {0})".format(
+ section.period.startdate.strftime(DATEFMT + " " + TIMEFMT)))
self.append('\n'*2)
for data in section.data:
self.append(self._fmt_data(data.subtitle, data.items))
- self.append('\n')
+ self.append('\n')
+ for table in section.tables:
+ self.append(table.draw_plain())
+ self.append("\n")
def _fmt_data(self, subtitle, data = None): # write title and data
+ """
+ Format the properties of a data object into usable plaintext form with
+ a few fancy symbols/formatting tricks. Subtitle is required, data is
+ not. If only subtitle is supplied or subtitle + one data item, a single
+ line will be printed.
+ """
+
if (subtitle == ""):
logger.warning("No subtitle provided.. skipping section")
return
+ logger.debug("Processing data {}".format(subtitle))
+
if (data == None or len(data) == 0):
- logger.debug("No data provided.. just printing subtitle")
- return subtitle + '\n'
+ # If no list items are provided, just print the subtitle
+ return subtitle + "\n"
+ elif (len(data) == 1):
+ # If only one item is provided, print it inline with subtitle
+ return self._wrap_datum("{}: {}".format(subtitle, data[0]),
+ bullet=False, indent=False) + "\n"
else:
- logger.debug("Received data " + str(data))
- subtitle += ':'
- if (len(data) == 1):
- return subtitle + ' ' + data[0] + '\n'
- else:
- itemoutput = subtitle + '\n'
- for datum in data:
- datum = '• ' + datum
- if len(datum) > config.prefs['linewidth']:
- words = datum.split()
- if max(map(len, words)) > config.prefs['linewidth']:
- raise ValueError("Content width is too small")
- res, part, others = [], words[0], words[1:]
- for word in others:
- if len(' ') + len(word) > config.prefs['linewidth'] - len(part):
- res.append(part)
- part = word
- else:
- part += ' ' + word
- if part:
- res.append(part)
- datum = '\n'.join(res)
- itemoutput += datum + '\n'
- return itemoutput
+ # If many items are provided, print them all as a bulleted list
+ itemoutput = subtitle + ":\n"
+ for datum in data:
+ itemoutput += self._wrap_datum(datum) + "\n"
+ return itemoutput
+
+ def _wrap_datum(self, text, bullet=True, indent=True):
+ """
+ Use cpython's textwrap module to limit line width to the value
+ specified in self.linewidth. This is much easier than doing it all
+ from scratch (which I tried to do originally). Note that line
+ continuations are automatically indented even if they don't have a
+ bullet. This is to make it clear which lines are continuations.
+ """
+
+ wrapper = textwrap.TextWrapper(
+ initial_indent=(INDENT if indent else "") \
+ + (BULLET if bullet else ""),
+ subsequent_indent=INDENT + (' '*len(BULLET) if bullet else ""),
+ width=self.linewidth,
+ replace_whitespace=True)
+
+ return wrapper.fill(text)
class HtmlOutput(Output):
"""
- Process and output data in HTML format.
- All HTML formatting functions now reside in this class to differentiate them from plaintext.
+ Process and output data in HTML format. All HTML formatting functions now
+ reside in this class to differentiate them from plain text.
"""
def __init__(self):
+ """
+ Initialise variables (no parameters required for initialisation)
+ """
+
self.content = ""
self.destination = ""
self.css = ""
+ self._embedded = ""
def embed_css(self, css):
- self.content = mail.mailprep(self.content, css)
+ """
+ Convert stylesheet to inline tags
+ """
+
+ if not self._embedded:
+ self._embedded = mail.mailprep(re.sub(
+ ".*" + re.escape(VARSUBST['css']) + ".*\n", "", self.content),
+ css)
+ return self._embedded
+
+ def write_embedded(self, destination = ""):
+ """
+ Write contents to file with inline CSS tags
+ """
+
+ logger.debug("Writing HTML with embedded styles to " + destination)
+ if not self._embedded:
+ logger.warning("Call to write_embedded before embed_css - \
+ embedding stylesheets immediately")
+ self.embed_css(config.prefs.get("html", "css"))
+ if destination == "":
+ destination = self.destination
+ if destination == "":
+ logger.warning("No destination path provided")
+ return 1
+ with open(destination, 'w') as f:
+ f.write(self._embedded)
+ logger.info("Written output to {}".format(destination))
+
+
+ def append_header(self, template):
+ """
+ Insert variables into header template file and append HTML tags
+ """
- def append_header(self, template): # insert variables into header template file
- init_varfilter()
+ self.headertemplate = template
headercontent = Template(open(template, 'r').read())
- self.append(headercontent.safe_substitute(varsubst))
- self.append(self.opentag('div', id='main'))
+ self.append(headercontent.safe_substitute(VARSUBST))
+ self.append(opentag('div', id='main'))
def append_footer(self):
+ """
+ Close HTML tags that were opened in the template.
+ TODO: add footer template similar to header template.
+ """
+
self.append(closetag('div') + closetag('body') + closetag('html'))
def append_section(self, section):
- self.append(self.opentag('div', 1, section.title, 'section'))
+ """
+ Call the appropriate methods to generate HTML tags for a section
+ (provided by a parser). This should be run by interface.py after every
+ instance of parse_log().
+ """
+
+ if section == None:
+ logger.warning("Received null section")
+ return
+ self.append(opentag('div', 1, section.title, 'section'))
self.append(self._gen_title(section.title))
+ if section.period and section.period.unique:
+ self.append(self._fmt_period(section.period))
for data in section.data:
- self.append(self._fmt_data(data.subtitle, data.items))
+ self.append(self._fmt_data(data.subtitle, data.items, data.severity))
+ for table in section.tables:
+ self.append(table.draw_html())
self.append(closetag('div', 1))
- def _gen_title(self, title): # write title for a section
+ def _gen_title(self, title):
+ """
+ Format the title for a section
+ """
+
if (title == '' or '\n' in title):
logger.error("Invalid title")
raise ValueError
logger.debug("Writing title for " + title)
- return tag('h2', 0, title)
+ return tag('h2', False, title)
+
+ def _fmt_data(self, subtitle, data=None, severity=0):
+ """
+ Format the properties of a data object into usable HTML tags.
+ Subtitle is required, data is not. If only subtitle is supplied or
+ subtitle + one data item, a single line will be printed.
+ """
- def _fmt_data(self, subtitle, data = None): # write title and data
if (subtitle == ""):
logger.warning("No subtitle provided.. skipping section")
return
if (data == None or len(data) == 0):
logger.debug("No data provided.. just printing subtitle")
- return tag('p', 0, subtitle)
+ return tag('p', False, subtitle, cl="severity-" + str(severity))
else:
- logger.debug("Received data " + str(data))
+ logger.debug("Received data {}: {}".format(subtitle, data))
subtitle += ':'
if (len(data) == 1):
- return tag('p', 0, subtitle + ' ' + data[0])
+ return tag('p', False, subtitle + ' ' + data[0],
+ cl="severity-" + str(severity))
else:
output = ""
- output += tag('p', 0, subtitle)
- output += self.opentag('ul', 1)
+ output += tag('p', False, subtitle,
+ cl="severity-" + str(severity))
+ output += opentag('ul', 1)
coderegex = re.compile('`(.*)`')
for datum in data:
if datum == "" or datum == None:
continue
- datum = coderegex.sub(r'<code>{\1}</code>', str(datum))
- output += tag('li', 0, datum)
- output += closetag('ul', 1)
+ datum = coderegex.sub(r"<code>\1</code>", str(datum))
+ output += tag('li', False, datum)
+ output += closetag('ul', True)
return output
- def opentag(self, tag, block = 0, id = None, cl = None): # write html opening tag
- output = ""
- if (block):
- output += '\n'
- output += '<' + tag
- if (id != None):
- output += " id='" + id + "'"
- if (cl != None):
- output += " class='" + cl + "'"
- output += '>'
- if (block):
- output += '\n'
+ def _fmt_period(self, period):
+ output = ''
+ output += opentag('span', cl='period')
+ output += "since " + period.startdate.strftime(DATEFMT + " " + TIMEFMT)
+ output += closetag('span')
return output
- def closetag(self, tag, block = 0): # write html closing tag
- if (block == 0):
- return "</" + tag + ">"
- else:
- return "\n</" + tag + ">\n"
-
- def tag(self, tag, block = 0, content = ""): # write html opening tag, content, and html closing tag
- o = self.opentag(tag, block)
- c = self.closetag(tag, block)
- return o + content + c
+ def print_stdout_embedded(self, lines=False):
+ """
+ Echo the version with embedded style tags to the console
+ """
+ if self._embedded == "":
+ self.embed_css(config.prefs.get("html", "css"))
+ print()
+ if lines:
+ line = PlaintextLine(linewidth=
+ config.prefs.getint("plain", "linewidth"), double=True)
+ print(line.draw())
+ print(self._embedded)
+ if lines:
+ print(line.draw())
+ print()
class Section:
"""
- Each parser should output a Section() which contains the title and returned data.
+ Each parser should output a Section() which contains the title, returned
+ data, and applicable time period.
"""
- def __init__(self, title):
+ def __init__(self, title, period=None):
self.title = title
self.data = []
+ self.tables = []
+ self.period = util.LogPeriod(self.title)
- def add_data(self, data):
+ def append_data(self, data):
self.data.append(data)
+ def append_table(self, table):
+ self.tables.append(table)
+
+
class Data:
"""
- Each section (parser) can have one or more Data() objects which are essentially glorified lists.
+ Each section (parser) can have one or more Data() objects which are
+ essentially glorified lists with titles (`self.subtitle`).
"""
- def __init__(self, subtitle=None, items=[]):
+ def __init__(self, subtitle="", items=[], severity=0):
+ """
+ Initialise variables. No parameters are enforced upon initialisation,
+ but at least the subtitle is required for valid output. Severity refers
+ to the importance of the data (integer from 0 to 5). e.g. a failed
+ system should have severity 5 and will be formatted appropriately by
+ the Output object.
+ """
+
self.subtitle = subtitle
self.items = items
+ self.severity = severity
def truncl(self, limit): # truncate list
+ """
+ Truncate self.items to a specified value and state how many items are
+ hidden. Set limit to -1 to avoid truncating any items.
+ """
+
+ if limit == -1:
+ return self
if (len(self.items) > limit):
- more = str(len(self.items) - limit)
+ more = len(self.items) - limit
+ if more == 1:
+ return 0
self.items = self.items[:limit]
- self.items..append("+ " + more + " more")
+ self.items.append("+ {0} more".format(str(more)))
+ return self
+
+ def orderbyfreq(self):
+ """
+ Order a list by frequency of each item, then remove duplicates and
+ append frequency in parentheses.
+ """
+
+ unsorted = list(self.items)
+ self.items = ["{0} ({1})".format(y, unsorted.count(y)) for y in sorted(
+ set(unsorted), key = lambda x: -unsorted.count(x))]
+ return self
+
+
+class Table(object):
+ """
+ A wrapper for python-tabulate's Tabulate type.
+ """
+
+ def __init__(self, double=False, borders=False, hpadding=" ",
+ maxwidth=80, headers=[]):
+ """
+ Initialise variables. Note the keymap is used for a faster index map,
+ but is not currently used anywhere (may be removed in future).
+ """
+
+ self.rows = [] # List of row objects
+ self.keymap = {} # For fast lookup of row by value of first column
+ self.double = double
+ self.borders = borders
+ self.align_cols = []
+ self.hpadding = hpadding
+ self.maxwidth = maxwidth
+ self.headers = headers
+ self._align_cols = []
+
+ def add_row(self, row):
+ """
+ Append a row to the list and amend index mapping
+ """
+
+ self.rows.append(row)
+ if len(row.columns) > 0:
+ self.keymap[row.columns[0]] = row
+
+ logger.debug("Added row with {0} columns".format(str(len(row.columns))))
+
+ def align_column(self, i, align):
+ """
+ Set alignment for the 'i'th column (`align` should be 'l', 'c' or 'r')
+ """
+
+ while len(self._align_cols) -1 < i:
+ self._align_cols.append("")
+ self._align_cols[i] = align
+ for row in self.rows:
+ row.columns[i].align = align
+ logger.debug("Column alignment is now {0}".format(self._align_cols))
+
+ def _gen_list(self):
+ """
+ Used locally for organising rows and columns into a 2D list structure
+ """
+
+ hierarchy = []
+ for row in self.rows:
+ row_data = []
+ for column in row.columns:
+ row_data.append(column.content)
+ hierarchy.append(row_data)
+ return hierarchy
+
+ def draw_html(self):
+ """
+ Output HTML string (wrapper for tabulate)
+ """
+
+ output = tabulate(self._gen_list(), self.headers, tablefmt="html",
+ colalign=tuple(self._align_cols))
+ return output
+
+ def draw_plain(self):
+ """
+ Output plain text string (wrapper for tabulate)
+ """
+
+ output = tabulate(self._gen_list(), self.headers,
+ tablefmt="fancy_grid" if self.borders
+ else "plain", colalign=tuple(self._align_cols))
+ return output + "\n"*2
+
+
+class Row(object):
+ """
+ Object representing a literal row in a 2D table with the individual cells
+ in the row represented by columns[].
+ """
+
+ def __init__(self, columns=[], header=False):
+ """
+ Initialise variables. The variable n is used locally to keep track of
+ the row width.
+ """
+
+ self.columns = columns
+ self.header = header
+ self.n = len(self.columns)
+
+ def add_column(self, column):
+ """
+ Append a single cell horizontally and increment the cell count
+ """
+
+ self.columns.append(column)
+ self.n += 1
+
+ def rm_column(self, column):
+ """
+ Remove the specified column object and decrement the cell count
+ """
+
+ self.remove(column)
+ self.n -= 1
- def orderbyfreq(self, l): # order a list by the frequency of its elements and remove duplicates
- temp_l = l[:]
- l = list(set(l))
- l = [[i, temp_l.count(i)] for i in l] # add count of each element
- l.sort(key=lambda x:temp_l.count(x[0])) # sort by count
- l = [i[0] + ' (' + str(i[1]) + ')' for i in l] # put element and count into string
- l = l[::-1] # reverse
- self.items = l
+
+class Column(object):
+ """
+ Object representing a single table cell. "Column" is somewhat of a misnomer
+ - one column object exists for each cell in the table. Columns are children
+ of rows.
+ """
+
+ def __init__(self, content="", align="right"):
+ """
+ Initialise variables. The align property sets the alignment of a single
+ cell ('l', 'c', or 'r').
+ """
+
+ self.content = content
+ self.align = align
class PlaintextLine:
"""
- Draw a horizontal line for plain text format, with optional padding/styling
+ Draw a horizontal line for plain text format, with optional padding/styling.
"""
- def __init__(self, linewidth=80, double=True, vpadding=1, hpadding=""):
+ def __init__(self, linewidth=80, double=True, vpadding=0, hpadding=""):
+ """
+ Initialise variables
+ """
+
self.linewidth = linewidth
- self.double = False
+ self.double = double
self.vpadding = vpadding
self.hpadding = hpadding
def draw(self):
+ """
+ Output a plain text string based on the current object parameters
+ """
+
line = (LINECHARS_DOUBLE[1] if self.double else LINECHARS_SINGLE[1])
- return "\n" * self.vpadding + self.hpadding + line * (self.linewidth - 2 * len(self.hpadding)) + self.hpadding + "\n" * self.vpadding
+ return "\n" * self.vpadding + self.hpadding \
+ + line * (self.linewidth - 2 * len(self.hpadding)) \
+ + self.hpadding + "\n" * (self.vpadding + 1)
+
class PlaintextBox:
"""
Draw a rectangular box around text, with customisable padding/size/style
"""
- def __init__(self, content="", double=True, fullwidth=True, linewidth=80, hpadding="\t", vpadding=1):
+ def __init__(self, content="", double=True, fullwidth=True, linewidth=80,
+ hpadding="\t", vpadding=1):
+ """
+ Initialise variables
+ """
self.content = content
self.fullwidth = fullwidth
self.linewidth = linewidth
self.double = double
def draw(self):
+ """
+ Output a plain text string based on the current object parameters. This
+ involves calculating the text width, breaking text at the maximum line
+ length, and then drawing a box around it all.
+ """
if self.double == True:
cornerchars = CORNERCHARS_DOUBLE
# Calculate number of characters per line
contentlines = self.content.splitlines()
- contentwidth = int((self.linewidth if self.linewidth > 0 else 80) if self.content.splitlines() else len(max(contentlines, key=len)))
- logger.debug("Contentwidth is {0}".format(str(contentwidth)))
- logger.debug("Longest line is {0}".format(len(max(contentlines, key=len))))
+ contentwidth = int((self.linewidth if self.linewidth > 0 else 80)
+ if self.content.splitlines()
+ else len(max(contentlines, key=len)))
+ logger.debug("Content width is {0}".format(str(contentwidth)))
+ logger.debug("Longest line is {0}".format(
+ len(max(contentlines, key=len))))
contentwidth += -2*(len(self.hpadding)+1)
if not self.fullwidth:
longestline = len(max(contentlines, key=len))
if len(line) > contentwidth:
words = line.split()
if max(map(len, words)) > contentwidth:
- raise ValueError("Content width is too small")
+ continue
res, part, others = [], words[0], words[1:]
for word in others:
if len(' ') + len(word) > contentwidth - len(part):
contentlines[i] = res
# Flatten list
- # Note list comprehension doesn't work here, so we must iterate through each item
+ # Note list comprehension doesn't work here, so we must iterate
+ # through each item
newlines = []
for line in contentlines:
if isinstance(line, list):
contentlines.append(' '*contentwidth)
# Insert horizontal padding on lines that are too short
- contentlines = [linechars[0] + self.hpadding + x + ' '*(self.linewidth-(len(x)+2*len(self.hpadding)+2) if len(x) < contentwidth else 0) + self.hpadding + linechars[0] for x in contentlines]
- contentlines.insert(0, cornerchars[3] + linechars[1] * (contentwidth + len(self.hpadding)*2) + cornerchars[2])
- contentlines.append(cornerchars[0] + linechars[1] * (contentwidth + len(self.hpadding)*2) + cornerchars[1])
+ contentlines = [linechars[0] + self.hpadding + x
+ + ' '*(self.linewidth-(len(x)+2*len(self.hpadding)+2)
+ if len(x) < contentwidth else 0)
+ + self.hpadding + linechars[0] for x in contentlines]
+ contentlines.insert(0, cornerchars[3] + linechars[1]
+ * (contentwidth + len(self.hpadding)*2) + cornerchars[2])
+ contentlines.append(cornerchars[0] + linechars[1]
+ * (contentwidth + len(self.hpadding)*2) + cornerchars[1])
return ('\n').join(contentlines)
-def init_varfilter():
- global varfilter
- global varpattern
- global varsubst
- varfilter = {"$title$": config.prefs['title'], "$date$": interface.start.strftime(DATEFMT),"$time$": interface.start.strftime(TIMEFMT), "$hostname$": util.hostname(config.prefs['hostname-path']), "$version$": logparse.__version__, "$css$": os.path.relpath(config.prefs['css'], os.path.dirname(config.prefs['output']))}
- varfilter = dict((re.escape(k), v) for k, v in varfilter.items())
- varpattern = re.compile("|".join(varfilter.keys()))
- varsubst = dict(title=config.prefs['title'], date=interface.start.strftime(DATEFMT), time=interface.start.strftime(TIMEFMT), hostname=util.hostname(config.prefs['hostname-path']), version=logparse.__version__, css=os.path.relpath(config.prefs['css'], os.path.dirname(config.prefs['output'])))
-
-def writetitle(title): # write title for a section
- if (title == '' or '\n' in title):
- logger.error("Invalid title")
- raise ValueError
- logger.debug("Writing title for " + title)
- return tag('h2', 0, title)
-
-def addtag(l, tag): # add prefix and suffix tags to each item in a list
- l2 = ['<' + tag + '>' + i + '</' + tag + '>' for i in l]
- return l2
def backticks(l):
+ """
+ Surround every item in a list by backticks. Used for showing code in both
+ HTML and plain text formats (converted to <code> tags for HTML)
+ """
+
return ["`" + x + "`" for x in l]
-def plural(noun, quantity): # return "1 noun" or "n nouns"
+
+def plural(noun, quantity, print_quantity=True):
+ """
+ Return "1 noun" or "n nouns"
+ """
+
if (quantity == 1):
- return(str(quantity) + " " + noun)
+ if print_quantity:
+ return(str(quantity) + " " + noun)
+ else:
+ return noun
else:
- return(str(quantity) + " " + noun + "s")
+ if noun.endswith("s"):
+ noun += "e"
+ if print_quantity:
+ return(str(quantity) + " " + noun + "s")
+ else:
+ return noun + "s"
+
+
+def parsesize(num, suffix='B'):
+ """
+ Return human-readable size from number of bytes
+ """
-def parsesize(num, suffix='B'): # return human-readable size from number of bytes
for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
if abs(num) < 1024.0:
return "%3.1f %s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, 'Yi', suffix)
-def fsubject(template): # Replace variables in the title template provided in config
- r = varpattern.sub(lambda m: varfilter[re.escape(m.group(0))], template)
+
+def fsubject(subject):
+ """
+ Replace variables in the title template provided in config
+ """
+
+ r = Template(subject).safe_substitute(VARSUBST)
logger.debug("Returning subject line " + r)
return r
+
+
+def opentag(tag, block=False, id=None, cl=None, style=None):
+ """
+ Write HTML opening tag
+ """
+
+ output = ""
+ if block:
+ output += '\n'
+ output += '<' + tag
+ if id:
+ output += " id='" + id + "'"
+ if cl:
+ output += " class='" + cl + "'"
+ if style:
+ output += " style='"
+ output += " ".join("{0}: {1};".format(attr, value)
+ for attr, value in style.items())
+ output += "'"
+ output += '>'
+ if block:
+ output += '\n'
+ return output
+
+
+def closetag(tag, block=False):
+ """
+ Write HTML closing tag
+ """
+
+ if block:
+ return "\n</" + tag + ">\n"
+ else:
+ return "</" + tag + ">"
+
+
+def tag(tag, block=False, content="", id=None, cl=None, style=None):
+ """
+ Write HTML opening tag, content, and closing tag
+ """
+
+ o = opentag(tag, block, id, cl, style)
+ c = closetag(tag, block)
+ return o + content + c