grepros 1.2.2
grep for ROS bag files and live topics
Loading...
Searching...
No Matches
html.py
Go to the documentation of this file.
1# -*- coding: utf-8 -*-
2"""
3HTML output plugin.
4
5------------------------------------------------------------------------------
6This file is part of grepros - grep for ROS bag files and live topics.
7Released under the BSD License.
8
9@author Erki Suurjaak
10@created 03.12.2021
11@modified 29.04.2024
12------------------------------------------------------------------------------
13"""
14## @namespace grepros.plugins.auto.html
15import atexit
16import os
17try: import queue # Py3
18except ImportError: import Queue as queue # Py2
19import re
20import threading
21
22from ... import api
23from ... import common
24from ... import main
25from ... common import ConsolePrinter, MatchMarkers
26from ... outputs import RolloverSinkMixin, Sink, TextSinkMixin
27from ... 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 = None
98 self._template_path = None
99 self._close_printed = False
100 self._tag_repls = {}
101 self._tag_rgx = None
102
103 atexit.register(self.closecloseclose)
104
105 def emit(self, topic, msg, stamp=None, match=None, index=None):
106 """Writes message to output file."""
107 if not self.validatevalidatevalidatevalidate(): raise Exception("invalid")
108 stamp, index = self._ensure_stamp_index(topic, msg, stamp, index)
109 RolloverSinkMixin.ensure_rollover(self, topic, msg, stamp)
110 self._queue.put((topic, msg, stamp, match, index))
111 if not self._writer:
112 self._writer = threading.Thread(target=self._stream)
113 self._writer.start()
114 self._close_printed = False
115 if "size" in self._rollover_limits or self._queue.qsize() > 100: self._queue.join()
116
117 def validate(self):
118 """
119 Returns whether write options are valid and ROS environment is set and file is writable,
120 emits error if not.
121 """
122 if self.validvalid is not None: return self.validvalid
123 result = all([Sink.validate(self), TextSinkMixin.validate(self)])
124 if not RolloverSinkMixin.validate(self):
125 result = False
126 if self.args.WRITE_OPTIONS.get("template") and not os.path.isfile(self._template_path):
127 result = False
128 ConsolePrinter.error("Template does not exist: %s.", self._template_path)
129 if self.args.WRITE_OPTIONS.get("overwrite") not in (None, True, False, "true", "false"):
130 ConsolePrinter.error("Invalid overwrite option for HTML: %r. "
131 "Choose one of {true, false}.",
132 self.args.WRITE_OPTIONS["overwrite"])
133 result = False
134 if not common.verify_io(self.args.WRITE, "w"):
135 result = False
136 self.validvalid = api.validate() and result
137 if self.validvalid:
138 self._overwrite = (self.args.WRITE_OPTIONS.get("overwrite") in (True, "true"))
139 self._template_path = self.args.WRITE_OPTIONS.get("template") or self.TEMPLATE_PATH
140
141 WRAPS, START = ((self.args.MATCH_WRAPPER or [""]) * 2)[:2], ""
142 if self.args.HIGHLIGHT: START = ('<span class="match">' + step.escape_html(WRAPS[0]))
143 END = (step.escape_html(WRAPS[1]) + '</span>') if self.args.HIGHLIGHT else ""
144 self._tag_repls = {MatchMarkers.START: START,
145 MatchMarkers.END: END,
146 ConsolePrinter.STYLE_LOWLIGHT: '<span class="lowlight">',
147 ConsolePrinter.STYLE_RESET: '</span>'}
148 self._tag_rgx = re.compile("(%s)" % "|".join(map(re.escape, self._tag_repls)))
149 self._format_repls.clear() # TextSinkMixin member
150
151 return self.validvalid
152
153 def close(self):
154 """Closes output file, if any, emits metainfo."""
156 finally:
157 if not self._close_printed and self._counts:
158 self._close_printed = True
159 ConsolePrinter.debug("Wrote HTML for %s", self.format_output_meta())
160 super(HtmlSink, self).close()
161
162 def close_output(self):
163 """Closes output file, if any."""
164 if self._writer:
165 writer, self._writer = self._writer, None
166 self._queue.put(None)
167 writer.is_alive() and writer.join()
168
169 def flush(self):
170 """Writes out any pending data to disk."""
171 self._queue.join()
172
173 def format_message(self, msg, highlight=False):
174 """Returns message as formatted string, optionally highlighted for matches if configured."""
175 text = TextSinkMixin.format_message(self, msg, self.args.HIGHLIGHT and highlight)
176 text = "".join(self._tag_repls.get(x) or step.escape_html(x)
177 for x in self._tag_rgx.split(text))
178 return text
179
180 def is_highlighting(self):
181 """Returns True if sink is configured to highlight matched values."""
182 return bool(self.args.HIGHLIGHT)
183
184 def _stream(self):
185 """Writer-loop, streams HTML template to file."""
186 if not self._writer:
187 return
188
189 try:
190 with open(self._template_path, encoding="utf-8") as f: tpl = f.read()
191 template = step.Template(tpl, escape=True, strip=False, postprocess=convert_lf)
192 ns = dict(source=self.source, sink=self, messages=self._produce(),
193 args=None, timeline=not self.args.ORDERBY)
194 if main.CLI_ARGS: ns.update(args=main.CLI_ARGS)
195 self.filenamefilename = self.filenamefilename or RolloverSinkMixin.make_filename(self)
196 if self.args.VERBOSE:
197 sz = os.path.isfile(self.filenamefilename) and os.path.getsize(self.filenamefilename)
198 action = "Overwriting" if sz and self._overwrite else "Creating"
199 ConsolePrinter.debug("%s HTML output %s.", action, self.filenamefilename)
200 common.makedirs(os.path.dirname(self.filenamefilename))
201 with open(self.filenamefilename, "wb") as f:
202 template.stream(f, ns, buffer_size=0)
203 except Exception as e:
204 self.thread_excepthook("Error writing HTML output %r: %r" % (self.filenamefilename, e), e)
205 finally:
206 self._writer = None
207
208 def _produce(self):
209 """Yields messages from emit queue, as (topic, msg, stamp, match, index)."""
210 while True:
211 entry = self._queue.get()
212 if entry is None:
213 self._queue.task_done()
214 break # while
215 (topic, msg, stamp, match, index) = entry
216 topickey = api.TypeMeta.make(msg, topic).topickey
217 if self.args.VERBOSE and topickey not in self._counts:
218 ConsolePrinter.debug("Adding topic %s in HTML output.", topic)
219 yield entry
220 super(HtmlSink, self).emit(topic, msg, stamp, match, index)
221 self._queue.task_done()
222 try:
223 while self._queue.get_nowait() or True: self._queue.task_done()
224 except queue.Empty: pass
225
226
227def convert_lf(s, newline=os.linesep):
228 r"""Returns string with \r \n \r\n linefeeds replaced with given."""
229 return re.sub("(\r(?!\n))|((?<!\r)\n)|(\r\n)", newline, s)
230
231
232
233def init(*_, **__):
234 """Adds HTML output format support."""
235 from ... import plugins # Late import to avoid circular
236 plugins.add_write_format("html", HtmlSink, "HTML", [
237 ("template=/my/path.tpl", "custom template to use for HTML output"),
238 ("overwrite=true|false", "overwrite existing file in HTML output\n"
239 "instead of appending unique counter (default false)")
240 ] + RolloverSinkMixin.get_write_options("HTML"))
241 plugins.add_output_label("HTML", ["--emit-field", "--no-emit-field", "--matched-fields-only",
242 "--lines-around-match", "--lines-per-field", "--start-line",
243 "--end-line", "--lines-per-message", "--match-wrapper"])
244
245
246__all__ = ["HtmlSink", "init"]
Provides output file rollover by size, duration, or message count.
Definition outputs.py:353
filename
Current output file path.
Definition outputs.py:403
close_output(self)
Closes output file, if any.
Definition outputs.py:476
validate(self)
Returns whether write options are valid, emits error if not, else populates options.
Definition outputs.py:406
format_output_meta(self)
Returns output file metainfo string, with names and sizes and message/topic counts.
Definition outputs.py:493
Output base class.
Definition outputs.py:32
thread_excepthook(self, text, exc)
Handles exception, used by background threads.
Definition outputs.py:115
source
inputs.Source instance bound to this sink
Definition outputs.py:55
valid
Result of validate()
Definition outputs.py:53
validate(self)
Returns whether sink prerequisites are met (like ROS environment set if LiveSink).
Definition outputs.py:99
close(self)
Shuts down output, closing any files or connections.
Definition outputs.py:106
Provides message formatting as text.
Definition outputs.py:137
validate(self)
Returns whether arguments are valid, emits error if not, else populates options.
Definition outputs.py:176
Writes messages to an HTML file.
Definition html.py:30
__init__(self, args=None, **kwargs)
Definition html.py:88
format_message(self, msg, highlight=False)
Returns message as formatted string, optionally highlighted for matches if configured.
Definition html.py:175
close_output(self)
Closes output file, if any.
Definition html.py:164
emit(self, topic, msg, stamp=None, match=None, index=None)
Writes message to output file.
Definition html.py:107
int WRAP_WIDTH
Character wrap width for message YAML.
Definition html.py:40
is_highlighting(self)
Returns True if sink is configured to highlight matched values.
Definition html.py:182
flush(self)
Writes out any pending data to disk.
Definition html.py:171
validate(self)
Returns whether write options are valid and ROS environment is set and file is writable,...
Definition html.py:123
close(self)
Closes output file, if any, emits metainfo.
Definition html.py:155
TEMPLATE_PATH
HTML template path.
Definition html.py:37
convert_lf(s, newline=os.linesep)
Returns string with \r \r linefeeds replaced with given.
Definition html.py:233
init(*_, **__)
Adds HTML output format support.
Definition html.py:239