html.py
Go to the documentation of this file.
1 # -*- coding: utf-8 -*-
2 """
3 HTML output plugin.
4 
5 ------------------------------------------------------------------------------
6 This file is part of grepros - grep for ROS bag files and live topics.
7 Released under the BSD License.
8 
9 @author Erki Suurjaak
10 @created 03.12.2021
11 @modified 28.12.2023
12 ------------------------------------------------------------------------------
13 """
14 
15 import atexit
16 import os
17 try: import queue # Py3
18 except ImportError: import Queue as queue # Py2
19 import re
20 import threading
21 
22 from ... import api
23 from ... import common
24 from ... import main
25 from ... common import ConsolePrinter, MatchMarkers, plural
26 from ... outputs import RolloverSinkMixin, Sink, TextSinkMixin
27 from ... vendor import step
28 
29 
31  """Writes messages to an HTML file."""
32 
33 
34  FILE_EXTENSIONS = (".htm", ".html")
35 
36 
37  TEMPLATE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "html.tpl")
38 
39 
40  WRAP_WIDTH = 120
41 
42 
43  DEFAULT_ARGS = dict(META=False, WRITE_OPTIONS={}, HIGHLIGHT=True, MATCH_WRAPPER=None,
44  ORDERBY=None, VERBOSE=False, COLOR=True, EMIT_FIELD=(), NOEMIT_FIELD=(),
45  MAX_FIELD_LINES=None, START_LINE=None, END_LINE=None,
46  MAX_MESSAGE_LINES=None, LINES_AROUND_MATCH=None, MATCHED_FIELDS_ONLY=False,
47  WRAP_WIDTH=None)
48 
49  def __init__(self, args=None, **kwargs):
50  """
51  @param args arguments as namespace or dictionary, case-insensitive;
52  or a single path as the name of HTML file to write
53  @param args.write name of HTML file to write,
54  will add counter like .2 to filename if exists
55  @param args.write_options ```
56  {"template": path to custom HTML template, if any,
57  "overwrite": whether to overwrite existing file
58  (default false),
59  "rollover-size": bytes limit for individual output files,
60  "rollover-count": message limit for individual output files,
61  "rollover-duration": time span limit for individual output files,
62  as ROS duration or convertible seconds,
63  "rollover-template": output filename template, supporting
64  strftime format codes like "%H-%M-%S"
65  and "%(index)s" as output file index}
66  ```
67  @param args.highlight highlight matched values (default true)
68  @param args.orderby "topic" or "type" if any to group results by
69  @param args.color False or "never" for not using colors in replacements
70  @param args.emit_field message fields to emit if not all
71  @param args.noemit_field message fields to skip in output
72  @param args.max_field_lines maximum number of lines to output per field
73  @param args.start_line message line number to start output from
74  @param args.end_line message line number to stop output at
75  @param args.max_message_lines maximum number of lines to output per message
76  @param args.lines_around_match number of message lines around matched fields to output
77  @param args.matched_fields_only output only the fields where match was found
78  @param args.wrap_width character width to wrap message YAML output at
79  @param args.match_wrapper string to wrap around matched values,
80  both sides if one value, start and end if more than one,
81  or no wrapping if zero values
82  @param args.meta whether to emit metainfo
83  @param args.verbose whether to emit debug information
84  @param kwargs any and all arguments as keyword overrides,
85  case-insensitive
86  """
87  args = {"WRITE": str(args)} if isinstance(args, common.PATH_TYPES) else args
88  args = common.ensure_namespace(args, HtmlSink.DEFAULT_ARGS, **kwargs)
89  args.WRAP_WIDTH = self.WRAP_WIDTH
90  args.COLOR = bool(args.HIGHLIGHT)
91 
92  super(HtmlSink, self).__init__(args)
93  RolloverSinkMixin.__init__(self, args)
94  TextSinkMixin.__init__(self, args)
95  self._queue = queue.Queue()
96  self._writer = None # threading.Thread running _stream()
97  self._overwrite = (args.WRITE_OPTIONS.get("overwrite") in (True, "true"))
98  self._template_path = args.WRITE_OPTIONS.get("template") or self.TEMPLATE_PATH
99  self._close_printed = False
100 
101  WRAPS = ((args.MATCH_WRAPPER or [""]) * 2)[:2]
102  START = ('<span class="match">' + step.escape_html(WRAPS[0])) if args.HIGHLIGHT else ""
103  END = (step.escape_html(WRAPS[1]) + '</span>') if args.HIGHLIGHT else ""
104  self._tag_repls = {MatchMarkers.START: START,
105  MatchMarkers.END: END,
106  ConsolePrinter.STYLE_LOWLIGHT: '<span class="lowlight">',
107  ConsolePrinter.STYLE_RESET: '</span>'}
108  self._tag_rgx = re.compile("(%s)" % "|".join(map(re.escape, self._tag_repls)))
109 
110  self._format_repls.clear()
111  atexit.register(self.close)
112 
113  def emit(self, topic, msg, stamp=None, match=None, index=None):
114  """Writes message to output file."""
115  if not self.validate(): raise Exception("invalid")
116  stamp, index = self._ensure_stamp_index(topic, msg, stamp, index)
117  RolloverSinkMixin.ensure_rollover(self, topic, msg, stamp)
118  self._queue.put((topic, msg, stamp, match, index))
119  if not self._writer:
120  self._writer = threading.Thread(target=self._stream)
121  self._writer.start()
122  self._close_printed = False
123  if "size" in self._rollover_limits or self._queue.qsize() > 100: self._queue.join()
124 
125  def validate(self):
126  """
127  Returns whether write options are valid and ROS environment is set and file is writable,
128  emits error if not.
129  """
130  if self.valid is not None: return self.valid
131  result = RolloverSinkMixin.validate(self)
132  if self.args.WRITE_OPTIONS.get("template") and not os.path.isfile(self._template_path):
133  result = False
134  ConsolePrinter.error("Template does not exist: %s.", self._template_path)
135  if self.args.WRITE_OPTIONS.get("overwrite") not in (None, True, False, "true", "false"):
136  ConsolePrinter.error("Invalid overwrite option for HTML: %r. "
137  "Choose one of {true, false}.",
138  self.args.WRITE_OPTIONS["overwrite"])
139  result = False
140  if not common.verify_io(self.args.WRITE, "w"):
141  result = False
142  self.valid = api.validate() and result
143  return self.valid
144 
145  def close(self):
146  """Closes output file, if any, emits metainfo."""
147  try: self.close_output()
148  finally:
149  if not self._close_printed and self._counts:
150  self._close_printed = True
151  ConsolePrinter.debug("Wrote HTML for %s", self.format_output_meta())
152  super(HtmlSink, self).close()
153 
154  def close_output(self):
155  """Closes output file, if any."""
156  if self._writer:
157  writer, self._writer = self._writer, None
158  self._queue.put(None)
159  writer.is_alive() and writer.join()
160 
161  def flush(self):
162  """Writes out any pending data to disk."""
163  self._queue.join()
164 
165  def format_message(self, msg, highlight=False):
166  """Returns message as formatted string, optionally highlighted for matches if configured."""
167  text = TextSinkMixin.format_message(self, msg, self.args.HIGHLIGHT and highlight)
168  text = "".join(self._tag_repls.get(x) or step.escape_html(x)
169  for x in self._tag_rgx.split(text))
170  return text
171 
172  def is_highlighting(self):
173  """Returns True if sink is configured to highlight matched values."""
174  return bool(self.args.HIGHLIGHT)
175 
176  def _stream(self):
177  """Writer-loop, streams HTML template to file."""
178  if not self._writer:
179  return
180 
181  try:
182  with open(self._template_path, "r") as f: tpl = f.read()
183  template = step.Template(tpl, escape=True, strip=False, postprocess=convert_lf)
184  ns = dict(source=self.source, sink=self, messages=self._produce(),
185  args=None, timeline=not self.args.ORDERBY)
186  if main.CLI_ARGS: ns.update(args=main.CLI_ARGS)
187  self.filename = self.filename or RolloverSinkMixin.make_filename(self)
188  if self.args.VERBOSE:
189  sz = os.path.isfile(self.filename) and os.path.getsize(self.filename)
190  action = "Overwriting" if sz and self._overwrite else "Creating"
191  ConsolePrinter.debug("%s HTML output %s.", action, self.filename)
192  common.makedirs(os.path.dirname(self.filename))
193  with open(self.filename, "wb") as f:
194  template.stream(f, ns, buffer_size=0)
195  except Exception as e:
196  self.thread_excepthook("Error writing HTML output %r: %r" % (self.filename, e), e)
197  finally:
198  self._writer = None
199 
200  def _produce(self):
201  """Yields messages from emit queue, as (topic, msg, stamp, match, index)."""
202  while True:
203  entry = self._queue.get()
204  if entry is None:
205  self._queue.task_done()
206  break # while
207  (topic, msg, stamp, match, index) = entry
208  topickey = api.TypeMeta.make(msg, topic).topickey
209  if self.args.VERBOSE and topickey not in self._counts:
210  ConsolePrinter.debug("Adding topic %s in HTML output.", topic)
211  yield entry
212  super(HtmlSink, self).emit(topic, msg, stamp, match, index)
213  self._queue.task_done()
214  try:
215  while self._queue.get_nowait() or True: self._queue.task_done()
216  except queue.Empty: pass
217 
218 
219 def convert_lf(s, newline=os.linesep):
220  r"""Returns string with \r \n \r\n linefeeds replaced with given."""
221  return re.sub("(\r(?!\n))|((?<!\r)\n)|(\r\n)", newline, s)
222 
223 
224 
225 def init(*_, **__):
226  """Adds HTML output format support."""
227  from ... import plugins # Late import to avoid circular
228  plugins.add_write_format("html", HtmlSink, "HTML", [
229  ("template=/my/path.tpl", "custom template to use for HTML output"),
230  ("overwrite=true|false", "overwrite existing file in HTML output\n"
231  "instead of appending unique counter (default false)")
232  ] + RolloverSinkMixin.get_write_options("HTML"))
233  plugins.add_output_label("HTML", ["--emit-field", "--no-emit-field", "--matched-fields-only",
234  "--lines-around-match", "--lines-per-field", "--start-line",
235  "--end-line", "--lines-per-message", "--match-wrapper"])
236 
237 
238 __all__ = ["HtmlSink", "init"]
grepros.plugins.auto.html.HtmlSink._queue
_queue
Definition: html.py:95
grepros.outputs.TextSinkMixin
Definition: outputs.py:122
grepros.plugins.auto.html.HtmlSink._close_printed
_close_printed
Definition: html.py:99
grepros.plugins.auto.html.init
def init(*_, **__)
Definition: html.py:225
grepros.outputs.Sink.args
args
Definition: outputs.py:50
grepros.plugins.auto.html.HtmlSink._writer
_writer
Definition: html.py:96
grepros.plugins.auto.html.convert_lf
def convert_lf(s, newline=os.linesep)
Definition: html.py:219
grepros.plugins.auto.html.HtmlSink.validate
def validate(self)
Definition: html.py:125
grepros.plugins.auto.html.HtmlSink.TEMPLATE_PATH
TEMPLATE_PATH
HTML template path.
Definition: html.py:37
grepros.outputs.Sink
Definition: outputs.py:32
grepros.outputs.RolloverSinkMixin._rollover_limits
_rollover_limits
Definition: outputs.py:366
grepros.outputs.TextSinkMixin._format_repls
_format_repls
Definition: outputs.py:156
grepros.outputs.Sink.valid
valid
Result of validate()
Definition: outputs.py:52
grepros.outputs.Sink.source
source
inputs.Source instance bound to this sink
Definition: outputs.py:54
grepros.outputs.Sink._counts
_counts
Definition: outputs.py:48
grepros.plugins.auto.html.HtmlSink.__init__
def __init__(self, args=None, **kwargs)
Definition: html.py:49
grepros.outputs.Sink.thread_excepthook
def thread_excepthook(self, text, exc)
Definition: outputs.py:101
grepros.plugins.auto.html.HtmlSink.format_message
def format_message(self, msg, highlight=False)
Definition: html.py:165
grepros.outputs.Sink.validate
def validate(self)
Definition: outputs.py:88
grepros.plugins.auto.html.HtmlSink._produce
def _produce(self)
Definition: html.py:200
grepros.plugins.auto.html.HtmlSink.close_output
def close_output(self)
Definition: html.py:154
grepros.outputs.RolloverSinkMixin.filename
filename
Current output file path.
Definition: outputs.py:371
grepros.outputs.RolloverSinkMixin.format_output_meta
def format_output_meta(self)
Definition: outputs.py:458
grepros.outputs.Sink.close
def close(self)
Definition: outputs.py:93
grepros.plugins.auto.html.HtmlSink._overwrite
_overwrite
Definition: html.py:97
grepros.outputs.RolloverSinkMixin.close_output
def close_output(self)
Definition: outputs.py:441
grepros.outputs.RolloverSinkMixin
Definition: outputs.py:323
grepros.plugins.auto.html.HtmlSink.WRAP_WIDTH
int WRAP_WIDTH
Character wrap width for message YAML.
Definition: html.py:40
grepros.plugins.auto.html.HtmlSink.close
def close(self)
Definition: html.py:145
grepros.plugins.auto.html.HtmlSink
Definition: html.py:30
grepros.plugins.auto.html.HtmlSink._template_path
_template_path
Definition: html.py:98
grepros.plugins.auto.html.HtmlSink._stream
def _stream(self)
Definition: html.py:176
grepros.plugins.auto.html.HtmlSink.flush
def flush(self)
Definition: html.py:161
grepros.outputs.Sink._ensure_stamp_index
def _ensure_stamp_index(self, topic, msg, stamp=None, index=None)
Definition: outputs.py:115
grepros.plugins.auto.html.HtmlSink._tag_rgx
_tag_rgx
Definition: html.py:108
grepros.plugins.auto.html.HtmlSink.is_highlighting
def is_highlighting(self)
Definition: html.py:172
grepros.plugins.auto.html.HtmlSink.emit
def emit(self, topic, msg, stamp=None, match=None, index=None)
Definition: html.py:113
grepros.plugins.auto.html.HtmlSink._tag_repls
_tag_repls
Definition: html.py:104


grepros
Author(s): Erki Suurjaak
autogenerated on Sat Jan 6 2024 03:11:29