Source code for shell_logger.html_utilities

"""Various utilities for building the HTML log file."""

# © 2023 National Technology & Engineering Solutions of Sandia, LLC
# (NTESS).  Under the terms of Contract DE-NA0003525 with NTESS, the
# U.S. Government retains certain rights in this software.

# SPDX-License-Identifier: BSD-3-Clause

import pkgutil
import re
import textwrap
from collections.abc import Iterable, Iterator, Mapping
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
from typing import TextIO, Union


[docs] def nested_simplenamespace_to_dict( namespace: Union[str, bytes, tuple, Mapping, Iterable, SimpleNamespace], ) -> Union[str, bytes, tuple, dict, list]: """ Convert a ``SimpleNamespace`` to a ``dict``. Convert a ``SimpleNamespace``, which may include nested namespaces, iterables, and mappings, to a ``dict`` containing the equivalent items. Parameters: namespace: The given namespace to convert, or some nested element therein. Returns: Recursively returns a base Python type equivalent of whatever was given. """ if "_asdict" in dir(namespace): # noinspection PyProtectedMember return nested_simplenamespace_to_dict(namespace._asdict()) if isinstance(namespace, (str, bytes, tuple)): return namespace if isinstance(namespace, Mapping): return { k: nested_simplenamespace_to_dict(v) for k, v in namespace.items() } if isinstance(namespace, Iterable): return [nested_simplenamespace_to_dict(x) for x in namespace] if isinstance(namespace, SimpleNamespace): return nested_simplenamespace_to_dict(namespace.__dict__) return namespace
[docs] def get_human_time(milliseconds: float) -> str: """ Get a human-readable date/time. Parameters: milliseconds: The number of milliseconds since epoch. Returns: A string representation of the date and time. """ seconds = milliseconds / 1000.0 return datetime.fromtimestamp(seconds).strftime("%Y-%m-%d %H:%M:%S.%f")
[docs] def opening_html_text() -> str: """ Get the opening HTML text. Returns: A string containing the first line of the HTML document through ``</head>``. """ return "<!DOCTYPE html><html>" + html_header()
[docs] def closing_html_text() -> str: """ Get the closing HTML tag. Returns: A string with the closing HTML tag in it. """ return "</html>"
[docs] def append_html(*args: Union[str, Iterator[str]], output: Path) -> None: """ Append whatever is given to the ``output`` HTML file. Parameters: *args: The argument(s) to write. output: The HTML file to append to. """ def _append_html( f: TextIO, *inner_args: Union[str, bytes, Iterable] ) -> None: """ Write some text to the given HTML log file. Parameters: f: The HTML file to write to. *inner_args: The argument(s) to write. """ for arg in inner_args: if isinstance(arg, str): f.write(arg) elif isinstance(arg, bytes): f.write(arg.decode()) elif isinstance(arg, Iterable): _append_html(f, *arg) else: message = f"Unsupported type: {type(arg)}" raise TypeError(message) with output.open("a") as output_file: _append_html(output_file, *args)
[docs] def fixed_width(text: str) -> str: """ Convert the text to a fixed-width font. Wrap the given ``text`` in a ``<pre><code>...</code></pre>`` block such that it displays in a fixed-width font. Parameters: text: The text to wrap. Returns: The ``<pre><code>...</code></pre>`` block. """ return f"<pre><code>{html_encode(text)}</code></pre>"
[docs] def flatten(element: Union[str, bytes, Iterable]) -> Iterator[str]: """ Turn a tree of lists into a flat iterable of strings. Parameters: element: An element of a tree. Yields: The string representation of the given element. """ if isinstance(element, str): yield element elif isinstance(element, bytes): yield element.decode() elif isinstance(element, Iterable): for _element in element: yield from flatten(_element) else: yield element
[docs] def parent_logger_card_html( name: str, *args: list[Iterator[str]] ) -> Iterator[str]: """ Generate the HTML for a parent logger card. Generate the HTML for the card corresponding to the parent :class:`ShellLogger`. The HTML elements are yielded one at a time to avoid loading *all* the data from the :class:`ShellLogger` into memory at once. Parameters: name: The name of the :class:`ShellLogger`. *args: A list of generators to lazily yield string HTML elements for the contents of the parent card. Yields: The header, followed by all the contents of the :class:`ShellLogger`, and then the footer. """ header, indent, footer = split_template( parent_logger_template, "parent_body", name=name ) yield header for arg in flatten(args): yield textwrap.indent(arg, indent) yield footer
[docs] def child_logger_card(log) -> Iterator[str]: """ Generate a child logger card. Create a card to go in the HTML log file containing everything pertaining to a child :class:`ShellLogger`. Parameters: log (ShellLogger): The child :class:`ShellLogger` for which to generate the card. Returns: A generator that will lazily yield the elements of the HTML for the card one at a time. Todo: * The type hinting for ``log`` is done in the docstring instead of the function signature, because to put it in the signature would create a circular dependency between ``shell_logger.py`` and ``html_utilities.py``. This function needs to be reworked such that there's no longer a dependency on ``ShellLogger``. """ child_html = log.to_html() return child_logger_card_html(log.name, log.duration, *child_html)
[docs] def child_logger_card_html( name: str, duration: str, *args: Union[Iterator[str], list[Iterator[str]]] ) -> Iterator[str]: """ Generate the HTML for a child logger card. Generate the HTML for a card corresponding to the child :class:`ShellLogger`. The HTML elements are yielded one at a time to avoid loading *all* the data from the :class:`ShellLogger` into memory at once. Parameters: name: The name of the child :class:`ShellLogger`. duration: The duration of the child :class:`ShellLogger`. *args: A generator (or list of generators) to lazily yield string HTML elements for the contents of the child card. Yields: The header, followed by all the contents of the child :class:`ShellLogger`, and then the footer. Todo: * Should we replace the ``for`` loop with the one found in :func:`parent_logger_card_html`? """ header, indent, footer = split_template( child_logger_template, "child_body", name=name, duration=duration ) yield header for arg in args: if isinstance(arg, str): yield textwrap.indent(arg, indent) elif isinstance(arg, Iterable): for _arg in arg: yield textwrap.indent(_arg, indent) yield footer
[docs] def command_card_html( log: dict, *args: Iterator[Union[str, Iterable]] ) -> Iterator[str]: """ Generate the HTML for a command card. Generate the HTML for a card corresponding to a command that was run. The HTML elements are yielded one at a time to avoid loading *all* the data into memory at once. Parameters: log: An entry from the :class:`ShellLogger` 's log book corresponding to a command that was run. *args: A generator that will yield all the elements to be included in the command card one at a time. Yields: The header, followed by all the contents of the command card, and then the footer. """ header, indent, footer = split_template( command_template, "more_info", cmd_id=log["cmd_id"], command=fixed_width(log["cmd"]), message=log["msg"], return_code=log["return_code"], duration=log["duration"], ) yield header for arg in args: if isinstance(arg, str): yield textwrap.indent(arg, indent) elif isinstance(arg, Iterable): for _arg in arg: yield textwrap.indent(_arg, indent) yield footer
[docs] def html_message_card(log: dict) -> Iterator[str]: """ Generate the HTML for a message card. Generate the HTML for a card corresponding to a message to only be included in the HTML log file (e.g., not printed to ``stdout`` as well). Parameters: log: An entry from the :class:`ShellLogger` 's log book corresponding to a message. Yields: The header, followed by the contents of the message card, and then the footer. """ timestamp = ( log["timestamp"] .replace(" ", "_") .replace(":", "-") .replace("/", "_") .replace(".", "-") ) header, indent, footer = split_template( html_message_template, "message", title=log["msg_title"], timestamp=timestamp, ) text = html_encode(log["msg"]) text = "<pre>" + text.replace("\n", "<br>") + "</pre>" yield header yield textwrap.indent(text, indent) + "\n" yield footer
[docs] def message_card(log: dict) -> Iterator[str]: """ Generate a message card. Generate the HTML for a card corresponding to a message to be both printed to ``stdout`` and included in the HTML log file. Parameters: log: An entry from the :class:`ShellLogger` 's log book corresponding to a message. Yields: The header, followed by the contents of the message card, and then the footer. """ header, indent, footer = split_template(message_template, "message") text = html_encode(log["msg"]) text = "<pre>" + text.replace("\n", "<br>") + "</pre>" yield header yield textwrap.indent(text, indent) + "\n" yield footer
[docs] def command_detail_list(cmd_id: str, *args: Iterator[str]) -> Iterator[str]: """ Generate the list of command details. Generate the HTML for a list of details associated with a command that was run. Parameters: cmd_id: The unique identifier associated with the command that was run. *args: All of the details associated with a command that was run. Yields: The header, followed by each of the details associated with the command that was run, and then the footer. """ header, indent, footer = split_template( command_detail_list_template, "details", cmd_id=cmd_id ) yield header for arg in args: if isinstance(arg, str): yield textwrap.indent(arg, indent) yield footer
[docs] def command_detail( cmd_id: str, name: str, value: str, *, hidden: bool = False ) -> str: """ Generate the HTML for a command detail. Create the HTML snippet for a detail associated with a command that was run. Parameters: cmd_id: The unique identifier associated with the command that was run. name: The name of the detail being recorded. value: The value of the detail being recorded. hidden: Whether or not this detail should be hidden (collapsed) in the HTML by default. Returns: The HTML snippet for this command detail. """ if hidden: return hidden_command_detail_template.format( cmd_id=cmd_id, name=name, value=value ) return command_detail_template.format(name=name, value=value)
[docs] def command_card(log: dict, stream_dir: Path) -> Iterator[str]: """ Generate a command card. Create a card in the HTML log file containing the output of a command, along with all its corresponding data (environment information, trace output, memory/CPU/disk statistics, etc.). Parameters: log: An entry from the :class:`ShellLogger` 's log book corresponding to a command that was run. stream_dir: The stream directory containing the ``stdout``, ``stderr``, and ``trace`` output from the command. Returns: A generator to lazily yield the elements of the command card one at a time. """ cmd_id = log["cmd_id"] stdout_path = stream_dir / f"{log['timestamp']}_{cmd_id}_stdout" stderr_path = stream_dir / f"{log['timestamp']}_{cmd_id}_stderr" trace_path = stream_dir / f"{log['timestamp']}_{cmd_id}_trace" # Collect all the details associated with the command that was run. info = [ command_detail_list( cmd_id, command_detail(cmd_id, "Time", log["timestamp"]), command_detail(cmd_id, "Command", fixed_width(log["cmd"])), command_detail(cmd_id, "CWD", log["pwd"], hidden=True), command_detail(cmd_id, "Hostname", log["hostname"], hidden=True), command_detail(cmd_id, "User", log["user"], hidden=True), command_detail(cmd_id, "Group", log["group"], hidden=True), command_detail(cmd_id, "Shell", log["shell"], hidden=True), command_detail(cmd_id, "umask", log["umask"], hidden=True), command_detail(cmd_id, "Return Code", log["return_code"]), ), output_block_card("stdout", stdout_path, cmd_id, collapsed=False), output_block_card("stderr", stderr_path, cmd_id, collapsed=False), ] # Compile the additional diagnostic information. diagnostics = [ output_block_card("Environment", log["environment"], cmd_id), output_block_card("ulimit", log["ulimit"], cmd_id), ] if trace_path.exists(): diagnostics.append(output_block_card("trace", trace_path, cmd_id)) # Add in any available statistics (from `StatsCollector`s). if log.get("stats"): stats = [("memory", "Memory Usage"), ("cpu", "CPU Usage")] for stat, stat_title in stats: if log["stats"].get(stat): data = log["stats"][stat] diagnostics.append(time_series_plot(cmd_id, data, stat_title)) if log["stats"].get("disk"): uninteresting_disks = [ "/var", "/var/log", "/var/log/audit", "/boot", "/boot/efi", ] disk_stats = { x: y for x, y in log["stats"]["disk"].items() if x not in uninteresting_disks } # We sort because JSON deserialization may change # the ordering of the map. for disk, data in sorted(disk_stats.items()): diagnostics.append(disk_time_series_plot(cmd_id, data, disk)) info.append(diagnostics_card(cmd_id, *diagnostics)) return command_card_html(log, *info)
[docs] def time_series_plot( cmd_id: str, data_tuples: list[tuple[float, float]], series_title: str ) -> Iterator[str]: """ Create the HTML for a plot of time series data. Parameters: cmd_id: The unique identifier associated with the command that was run. data_tuples: A list of :math:`x` and :math:`y` locations. series_title: The title of the plot. Returns: A HTML snippet for a plot of the given data. """ labels = [get_human_time(x) for x, _ in data_tuples] values = [y for _, y in data_tuples] identifier = f"{cmd_id}-{series_title.lower().replace(' ', '-')}-chart" return stat_chart_card(labels, values, series_title, identifier)
[docs] def disk_time_series_plot( cmd_id: str, data_tuples: list[tuple[float, float]], volume_name: str ) -> Iterator[str]: """ Generate a time series plot of disk usage. Create the HTML for a plot of the disk usage time series data for a particular volume. Parameters: cmd_id: The unique identifier associated with the command that was run. data_tuples: A list of :math:`x` and :math:`y` locations. volume_name: The name of the disk volume who's data is being plotted. Returns: A HTML snippet for a plot of the given data. Todo: * Should we combine this with :func:`time_series_plot`? """ labels = [get_human_time(x) for x, _ in data_tuples] values = [y for _, y in data_tuples] identifier = f"{cmd_id}-volume{volume_name.replace('/', '_')}-usage" stat_title = f"Used Space on {volume_name}" return stat_chart_card(labels, values, stat_title, identifier)
[docs] def stat_chart_card( labels: list[str], data: list[float], title: str, identifier: str ) -> Iterator[str]: """ Create the HTML for a two-dimensional plot. Parameters: labels: The :math:`x` values. data: The :math:`y` values. title: The title for the plot. identifier: A unique identifier for the chart. Yields: A HTML snippet for the chart with all the details filled in. """ yield stat_chart_template.format( labels=labels, data=data, title=title, id=identifier )
[docs] def output_block_card( title: str, output: Union[Path, str], cmd_id: str, *, collapsed: bool = True, ) -> Iterator[str]: """ Generate an output block card. Given the output from a command, generate a corresponding HTML card for inclusion in the log file. Parameters: title: The title for the output block. output: The output from a command. cmd_id: The unique identifier associated with the command that was run. collapsed: Whether or not the output block should be collapsed by default in the HTML log file. Yields: The header, followed by each line of the output, and then the footer. """ name = title.replace(" ", "_").lower() template = ( output_card_collapsed_template if collapsed else output_card_template ) header, indent, footer = split_template( template, "output_block", name=name, title=title, cmd_id=cmd_id ) yield header for line in output_block(output, name, cmd_id): yield textwrap.indent(line, indent) yield footer
[docs] def output_block( output: Union[Path, str], name: str, cmd_id: str ) -> Iterator[str]: """ Generate an output block. Given the output from a command, generate the HTML equivalent for inclusion in the log file. Parameters: output: The output from a command. name: The name (title) of the output block. cmd_id: The unique identifier associated with the command that was run. Yields: The HTML equivalent of each line of the output in turn. """ if isinstance(output, Path): with output.open() as f: for string in output_block_html(f, name, cmd_id): yield string if isinstance(output, str): for string in output_block_html(output, name, cmd_id): yield string
[docs] def diagnostics_card(cmd_id: str, *args: Iterator[str]) -> Iterator[str]: """ Generate a diagnostics card. Generate a card containing system diagnostic information associated with a command that was run. Parameters: cmd_id: The unique identifier associated with the command that was run. *args: A generator to lazily yield all the diagnostic information, one piece at a time. Yields: The header, followed by each piece of diagnostic information, and then the footer. """ header, indent, footer = split_template( diagnostics_template, "diagnostics", cmd_id=cmd_id ) yield header for arg in args: if isinstance(arg, str): yield textwrap.indent(arg, indent) elif isinstance(arg, Iterable): for _arg in arg: yield textwrap.indent(_arg, indent) yield footer
[docs] def output_block_html( lines: Union[TextIO, str], name: str, cmd_id: str ) -> Iterator[str]: """ Generate the HTML for an output block. Given the output of a command, generate its HTML equivalent for inclusion in the log file. Parameters: lines: The lines of output. name: The name (title) for this output block. cmd_id: The unique identifier associated with the command that was run. Yields: The header, followed by the HTML corresponding to each line of output, and then the footer. """ if isinstance(lines, str): lines = lines.split("\n") header, indent, footer = split_template( output_block_template, "table_contents", name=name, cmd_id=cmd_id ) yield header for line_no, line in enumerate(lines): yield textwrap.indent(output_line_html(line, line_no), indent) yield footer
[docs] def split_template( template: str, split_at: str, **kwargs ) -> tuple[str, str, str]: """ Subdivide a HTML template. Take a templated HTML snippet and split it into a header and footer, meaning everything that comes before and after the line containing ``split_at``. Also determine the indentation for the content that will be inserted between the header and footer. Example: If the following snippet is ``template`` and ``split_at`` is ``child_body``, then the header is lines 1-6, the footer is lines 8-9, and the indent is eight spaces. .. code-block:: html :linenos: :emphasize-lines: 7 <details class="child-logger"> <summary> <h6 class="child-logger-heading">{name}</h6> <span class="duration"> (Duration: {duration})</span> </summary> <div class="child-logger-body"> {child_body} </div> </details> Parameters: template: A templated HTML snippet. split_at: A substring used to split the ``template`` into before and after chunks. **kwargs: Additional keyword arguments used to replace keywords in the ``template``. Returns: The header, indent, and footer. """ fmt = {k: v for k, v in kwargs.items() if k != split_at} pattern = re.compile( f"(.*\\n)(\\s*)\\{{{split_at}\\}}\\n(.*)", flags=re.DOTALL ) before, indent, after = pattern.search(template).groups() return before.format(**fmt), indent, after.format(**fmt)
[docs] def output_line_html(line: str, line_no: int) -> str: """ Generate the HTML for a line of output. Given a line of output from a command, along with the corresponding line number, create the HTML equivalent to be included in the log file. Parameters: line: A line of output. line_no: The corresponding line number. Returns: The corresponding HTML snippet. """ encoded_line = html_encode(line).rstrip() return output_line_template.format(line=encoded_line, line_no=line_no)
[docs] def html_encode(text: str) -> str: """ Replace special characters with their HTML encodings. Parameters: text: The text to encode. Returns: The encoded text. """ return sgr_to_html( text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;") )
[docs] def sgr_to_html(text: str) -> str: """ Convert SGR to HTML. Translate Select Graphic Rendition (SGR, a.k.a. ANSI escape codes) to valid HTML/CSS. Parameters: text: The input text. Returns: The same text, with the escape codes translated to HTML/CSS. """ span_count = 0 while text.find("\x1b[") >= 0: start = text.find("\x1b[") finish = text.find("m", start) sgrs = text[start + 2 : finish].split(";") span_string = "" if len(sgrs) == 0: span_string += "</span>" * span_count span_count = 0 else: while sgrs: if sgrs[0] == "0": span_string += "</span>" * span_count span_count = 0 sgrs = sgrs[1:] elif len(sgrs) >= 5 and sgrs[:2] in [["38", "2"], ["48", "2"]]: span_count += 1 span_string += sgr_24bit_color_to_html(sgrs[:5]) sgrs = sgrs[5:] elif len(sgrs) >= 3 and sgrs[:2] in [["38", "5"], ["48", "5"]]: span_count += 1 span_string += sgr_8bit_color_to_html(sgrs[:3]) sgrs = sgrs[3:] else: span_count += 1 span_string += sgr_4bit_color_and_style_to_html(sgrs[0]) sgrs = sgrs[1:] text = text[:start] + span_string + text[finish + 1 :] return text
[docs] def sgr_4bit_color_and_style_to_html(sgr: str) -> str: """ Convert from Select Graphic Rendition (SGR) codes to CSS styles. Parameters: sgr: The SGR code specified. Returns: A HTML ``span`` with the corresponding CSS style definition. """ sgr_to_css = { "1": "font-weight: bold;", "2": "font-weight: lighter;", "3": "font-style: italic;", "4": "text-decoration: underline;", "9": "text-decoration: line-through;", "30": "color: black;", "40": "background-color: black;", "31": "color: red;", "41": "background-color: red;", "32": "color: green;", "42": "background-color: green;", "33": "color: yellow;", "43": "background-color: yellow;", "34": "color: blue;", "44": "background-color: blue;", "35": "color: magenta;", "45": "background-color: magenta;", "36": "color: cyan;", "46": "background-color: cyan;", "37": "color: white;", "47": "background-color: white;", "90": "color: black;", "100": "background-color: black;", "91": "color: red;", "101": "background-color: red;", "92": "color: green;", "102": "background-color: green;", "93": "color: yellow;", "103": "background-color: yellow;", "94": "color: blue;", "104": "background-color: blue;", "95": "color: magenta;", "105": "background-color: magenta;", "96": "color: cyan;", "106": "background-color: cyan;", "97": "color: white;", "107": "background-color: white;", "39": "color: inherit;", "49": "background-color: inherit;", } return f'<span style="{sgr_to_css.get(sgr) or ""}">'
[docs] def sgr_8bit_color_to_html(sgr_params: list[str]) -> str: # noqa: PLR0911 """ Convert 8-bit SGR colors to HTML. Convert an 8-bit Select Graphic Rendition (SGR) code to valid HTML/CSS. Parameters: sgr_params: The SGR codes to convert. Returns: A HTML ``span`` with the appropriate CSS style. Raises: RuntimeError: If the code somehow falls through all the `if` blocks without returning. This should never happen. """ sgr_256 = int(sgr_params[2]) if len(sgr_params) > 2 else 0 if sgr_256 < 0 or sgr_256 > 255 or not sgr_params: return "<span>" if 15 < sgr_256 < 232: red_6cube = (sgr_256 - 16) // 36 green_6cube = (sgr_256 - (16 + red_6cube * 36)) // 6 blue_6cube = (sgr_256 - 16) % 6 red = str(51 * red_6cube) green = str(51 * green_6cube) blue = str(51 * blue_6cube) return sgr_24bit_color_to_html([sgr_params[0], "2", red, green, blue]) if 231 < sgr_256 < 256: gray = str(8 + (sgr_256 - 232) * 10) return sgr_24bit_color_to_html([sgr_params[0], "2", gray, gray, gray]) if sgr_params[0] == "38": if sgr_256 < 8: return sgr_4bit_color_and_style_to_html(str(30 + sgr_256)) if sgr_256 < 16: return sgr_4bit_color_and_style_to_html(str(82 + sgr_256)) if sgr_params[0] == "48": if sgr_256 < 8: return sgr_4bit_color_and_style_to_html(str(40 + sgr_256)) if sgr_256 < 16: return sgr_4bit_color_and_style_to_html(str(92 + sgr_256)) message = "THIS SHOULD NEVER HAPPEN" raise RuntimeError(message)
[docs] def sgr_24bit_color_to_html(sgr_params: list[str]) -> str: """ Convert 24-bit SGR colors to HTML. Convert a 24-bit Select Graphic Rendition (SGR) code to valid HTML/CSS. Parameters: sgr_params: The SGR codes to convert. Returns: A HTML ``span`` with the appropriate CSS style. """ r, g, b = sgr_params[2:5] if len(sgr_params) == 5 else ("0", "0", "0") if len(sgr_params) > 1 and sgr_params[:2] == ["38", "2"]: return f'<span style="color: rgb({r}, {g}, {b})">' if len(sgr_params) > 1 and sgr_params[:2] == ["48", "2"]: return f'<span style="background-color: rgb({r}, {g}, {b})">' return "<span>"
[docs] def html_header() -> str: """ Get the HTML header, complete with embedded styles and scripts. Returns: A string with the ``<head>...</head>`` contents. """ return ( "<head>" + embed_style("bootstrap.min.css") + embed_style("Chart.min.css") + embed_style("top_level_style_adjustments.css") + embed_style("parent_logger_style.css") + embed_style("child_logger_style.css") + embed_style("command_style.css") + embed_style("message_style.css") + embed_style("detail_list_style.css") + embed_style("code_block_style.css") + embed_style("output_style.css") + embed_style("diagnostics_style.css") + embed_style("search_controls.css") + embed_script("jquery.slim.min.js") + embed_script("bootstrap.bundle.min.js") + embed_script("Chart.bundle.min.js") + embed_script("search_output.js") + embed_html("search_icon.svg") + "</head>" )
[docs] def embed_style(resource: str) -> str: """ Embed a style in the HTML. Wrap the given ``resource`` in an appropriate ``<style>...</style>`` block for embedding in the HTML header. Parameters: resource: The name of a style file to embed. Returns: A string containing the ``<style>...</style>`` block. Todo: * Should we combine this with :func:`embed_script` and :func:`embed_html`? """ return ( "<style>\n" + pkgutil.get_data(__name__, f"resources/{resource}").decode() + "\n</style>\n" )
[docs] def embed_script(resource: str) -> str: """ Embed a script in the HTML. Wrap the given ``resource`` in an appropriate ``<script>...</script>`` block for embedding in the HTML header. Parameters: resource: The name of a script file to embed. Returns: A string containing the ``<script>...</script>`` block. """ return ( "<script>\n" + pkgutil.get_data(__name__, f"resources/{resource}").decode() + "\n</script>\n" )
[docs] def embed_html(resource: str) -> str: """ Embed the contents of a file in the HTML. Get a HTML ``resource`` from a file for the sake of embedding it into the HTML header. Parameters: resource: The name of a HTML file to embed. Returns: The contents of the file. Todo: * Why do we use ``pkgutil.get_data()`` instead of a simple ``read()``. """ return pkgutil.get_data(__name__, f"resources/{resource}").decode()
[docs] def load_template(template: str) -> str: """ Load a template HTML file. Parameters: template: The file name to load. Returns: A string containing the contents of the file. Todo: * Should we combine this with :func:`embed_html`? """ template_file = f"resources/templates/{template}" return pkgutil.get_data(__name__, template_file).decode()
command_detail_list_template = load_template("command_detail_list.html") command_detail_template = load_template("command_detail.html") hidden_command_detail_template = load_template("hidden_command_detail.html") stat_chart_template = load_template("stat_chart.html") diagnostics_template = load_template("diagnostics.html") output_card_template = load_template("output_card.html") output_card_collapsed_template = load_template("output_card_collapsed.html") output_block_template = load_template("output_block.html") output_line_template = load_template("output_line.html") message_template = load_template("message.html") html_message_template = load_template("html_message.html") command_template = load_template("command.html") child_logger_template = load_template("child_logger.html") parent_logger_template = load_template("parent_logger.html")