outputs.py
Go to the documentation of this file.
1 # -*- coding: utf-8 -*-
2 """
3 Main outputs for emitting messages.
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 23.10.2021
11 @modified 28.12.2023
12 ------------------------------------------------------------------------------
13 """
14 
15 from __future__ import print_function
16 import atexit
17 import collections
18 import datetime
19 import os
20 import re
21 import sys
22 
23 import six
24 import yaml
25 
26 from . import api
27 from . import common
28 from . common import ConsolePrinter, MatchMarkers
29 from . inputs import Source
30 
31 
32 class Sink(object):
33  """Output base class."""
34 
35 
36  FILE_EXTENSIONS = ()
37 
38 
39  DEFAULT_ARGS = dict(META=False)
40 
41  def __init__(self, args=None, **kwargs):
42  """
43  @param args arguments as namespace or dictionary, case-insensitive
44  @param args.meta whether to emit metainfo
45  @param kwargs any and all arguments as keyword overrides, case-insensitive
46  """
47  self._batch_meta = {} # {source batch: "source metadata"}
48  self._counts = {} # {(topic, typename, typehash): count}
49 
50  self.args = common.ensure_namespace(args, Sink.DEFAULT_ARGS, **kwargs)
51 
52  self.valid = None
53 
54  self.source = Source(self.args)
55 
56  def __enter__(self):
57  """Context manager entry."""
58  return self
59 
60  def __exit__(self, exc_type, exc_value, traceback):
61  """Context manager exit, closes sink."""
62  self.close()
63 
64  def emit_meta(self):
65  """Outputs source metainfo like bag header as debug stream, if not already emitted."""
66  batch = self.args.META and self.source.get_batch()
67  if self.args.META and batch not in self._batch_meta:
68  meta = self._batch_meta[batch] = self.source.format_meta()
69  meta and ConsolePrinter.debug(meta)
70 
71  def emit(self, topic, msg, stamp=None, match=None, index=None):
72  """
73  Outputs ROS message.
74 
75  @param topic full name of ROS topic the message is from
76  @param msg ROS message
77  @param stamp message ROS timestamp, if not current ROS time
78  @param match ROS message with values tagged with match markers if matched, else None
79  @param index message index in topic, if any
80  """
81  topickey = api.TypeMeta.make(msg, topic).topickey
82  self._counts[topickey] = self._counts.get(topickey, 0) + 1
83 
84  def bind(self, source):
85  """Attaches source to sink."""
86  self.source = source
87 
88  def validate(self):
89  """Returns whether sink prerequisites are met (like ROS environment set if TopicSink)."""
90  if self.valid is None: self.valid = True
91  return self.valid
92 
93  def close(self):
94  """Shuts down output, closing any files or connections."""
95  self._batch_meta.clear()
96  self._counts.clear()
97 
98  def flush(self):
99  """Writes out any pending data to disk."""
100 
101  def thread_excepthook(self, text, exc):
102  """Handles exception, used by background threads."""
103  ConsolePrinter.error(text)
104 
105  def is_highlighting(self):
106  """Returns whether this sink requires highlighted matches."""
107  return False
108 
109  @classmethod
110  def autodetect(cls, target):
111  """Returns true if target is recognizable as output for this sink class."""
112  ext = os.path.splitext(target or "")[-1].lower()
113  return ext in cls.FILE_EXTENSIONS
114 
115  def _ensure_stamp_index(self, topic, msg, stamp=None, index=None):
116  """Returns (stamp, index) populated with current ROS time and topic index if `None`."""
117  if stamp is None: stamp = api.get_rostime(fallback=True)
118  if index is None: index = self._counts.get(api.TypeMeta.make(msg, topic).topickey, 0) + 1
119  return stamp, index
120 
121 
122 class TextSinkMixin(object):
123  """Provides message formatting as text."""
124 
125 
126  NOCOLOR_HIGHLIGHT_WRAPPERS = "**", "**"
127 
128 
129  DEFAULT_ARGS = dict(COLOR=True, EMIT_FIELD=(), NOEMIT_FIELD=(), HIGHLIGHT=True,
130  MAX_FIELD_LINES=None, START_LINE=None, END_LINE=None,
131  MAX_MESSAGE_LINES=None, LINES_AROUND_MATCH=None, MATCHED_FIELDS_ONLY=False,
132  WRAP_WIDTH=None, MATCH_WRAPPER=None)
133 
134  def __init__(self, args=None, **kwargs):
135  """
136  @param args arguments as namespace or dictionary, case-insensitive
137  @param args.color False or "never" for not using colors in replacements
138  @param args.highlight highlight matched values (default true)
139  @param args.emit_field message fields to emit if not all
140  @param args.noemit_field message fields to skip in output
141  @param args.max_field_lines maximum number of lines to output per field
142  @param args.start_line message line number to start output from
143  @param args.end_line message line number to stop output at
144  @param args.max_message_lines maximum number of lines to output per message
145  @param args.lines_around_match number of message lines around matched fields to output
146  @param args.matched_fields_only output only the fields where match was found
147  @param args.wrap_width character width to wrap message YAML output at
148  @param args.match_wrapper string to wrap around matched values,
149  both sides if one value, start and end if more than one,
150  or no wrapping if zero values
151  @param kwargs any and all arguments as keyword overrides, case-insensitive
152  """
153  self._prefix = "" # Put before each message line (filename if grepping 1+ files)
154  self._wrapper = None # TextWrapper instance
155  self._patterns = {} # {key: [(() if any field else ('path', ), re.Pattern), ]}
156  self._format_repls = {} # {text to replace if highlight: replacement text}
157  self._styles = collections.defaultdict(str) # {label: ANSI code string}
158 
159  self._configure(common.ensure_namespace(args, TextSinkMixin.DEFAULT_ARGS, **kwargs))
160 
161 
162  def format_message(self, msg, highlight=False):
163  """Returns message as formatted string, optionally highlighted for matches if configured."""
164  text = self.message_to_yaml(msg).rstrip("\n")
165 
166  highlight = highlight and self.args.HIGHLIGHT
167  if self._prefix or self.args.START_LINE or self.args.END_LINE \
168  or self.args.MAX_MESSAGE_LINES or (self.args.LINES_AROUND_MATCH and highlight):
169  lines = text.splitlines()
170 
171  if self.args.START_LINE or self.args.END_LINE or self.args.MAX_MESSAGE_LINES:
172  start = self.args.START_LINE or 0
173  start = max(start, -len(lines)) - (start > 0) # <0 to sanity, >0 to 0-base
174  end = self.args.END_LINE or len(lines)
175  end = max(end, -len(lines)) - (end > 0) # <0 to sanity, >0 to 0-base
176  if self.args.MAX_MESSAGE_LINES: end = min(end, start + self.args.MAX_MESSAGE_LINES)
177  lines = lines[start:end + 1]
178  lines = lines and (lines[:-1] + [lines[-1] + self._styles["rst"]])
179 
180  if self.args.LINES_AROUND_MATCH and highlight:
181  spans, NUM = [], self.args.LINES_AROUND_MATCH
182  for i, l in enumerate(lines):
183  if MatchMarkers.START in l:
184  spans.append([max(0, i - NUM), min(i + NUM + 1, len(lines))])
185  if MatchMarkers.END in l and spans:
186  spans[-1][1] = min(i + NUM + 1, len(lines))
187  lines = sum((lines[a:b - 1] + [lines[b - 1] + self._styles["rst"]]
188  for a, b in common.merge_spans(spans)), [])
189 
190  if self._prefix:
191  lines = [self._prefix + l for l in lines]
192 
193  text = "\n".join(lines)
194 
195  for a, b in self._format_repls.items() if highlight else ():
196  text = re.sub(r"(%s)\1+" % re.escape(a), r"\1", text) # Remove consecutive duplicates
197  text = text.replace(a, b)
198 
199  return text
200 
201 
202  def message_to_yaml(self, val, top=(), typename=None):
203  """Returns ROS message or other value as YAML."""
204  # Refactored from genpy.message.strify_message().
205  unquote = lambda v: v[1:-1] if v[:1] == v[-1:] == '"' else v
206 
207  def retag_match_lines(lines):
208  """Adds match tags to lines where wrapping separated start and end."""
209  PH = self._wrapper.placeholder
210  for i, l in enumerate(lines):
211  startpos0, endpos0 = l.find (MatchMarkers.START), l.find (MatchMarkers.END)
212  startpos1, endpos1 = l.rfind(MatchMarkers.START), l.rfind(MatchMarkers.END)
213  if endpos0 >= 0 and (startpos0 < 0 or startpos0 > endpos0):
214  lines[i] = l = re.sub(r"^(\s*)", r"\1" + MatchMarkers.START, l)
215  if startpos1 >= 0 and endpos1 < startpos1 and i + 1 < len(lines):
216  lines[i + 1] = re.sub(r"^(\s*)", r"\1" + MatchMarkers.START, lines[i + 1])
217  if startpos1 >= 0 and startpos1 > endpos1:
218  CUT, EXTRA = (-len(PH), PH) if PH and l.endswith(PH) else (len(l), "")
219  lines[i] = l[:CUT] + MatchMarkers.END + EXTRA
220  return lines
221 
222  def truncate(v):
223  """Returns text or list/tuple truncated to length used in final output."""
224  if self.args.LINES_AROUND_MATCH \
225  or (not self.args.MAX_MESSAGE_LINES and (self.args.END_LINE or 0) <= 0): return v
226 
227  MAX_CHAR_LEN = 1 + len(MatchMarkers.START) + len(MatchMarkers.END)
228  # For list/tuple, account for comma and space
229  if isinstance(v, (list, tuple)): textlen = bytelen = 2 + len(v) * (2 + MAX_CHAR_LEN)
230  else: textlen, bytelen = self._wrapper.strlen(v), len(v)
231  if textlen < 10000: return v
232 
233  # Heuristic optimization: shorten superlong texts before converting to YAML
234  # if outputting a maximum number of lines per message
235  # (e.g. a lidar pointcloud can be 10+MB of text and take 10+ seconds to format).
236  MIN_CHARS_PER_LINE = self._wrapper.width
237  if MAX_CHAR_LEN != 1:
238  MIN_CHARS_PER_LINE = self._wrapper.width // MAX_CHAR_LEN * 2
239  MAX_LINES = self.args.MAX_MESSAGE_LINES or self.args.END_LINE
240  MAX_CHARS = MAX_LEN = MAX_LINES * MIN_CHARS_PER_LINE * self._wrapper.width + 100
241  if bytelen > MAX_CHARS: # Use worst-case max length plus some extra
242  if isinstance(v, (list, tuple)): MAX_LEN = MAX_CHARS // 3
243  v = v[:MAX_LEN]
244  return v
245 
246  indent = " " * len(top)
247  if isinstance(val, six.integer_types + (float, bool)):
248  return str(val)
249  if isinstance(val, common.TEXT_TYPES):
250  if val in ("", MatchMarkers.EMPTY):
251  return MatchMarkers.EMPTY_REPL if val else "''"
252  # default_style='"' avoids trailing "...\n"
253  return yaml.safe_dump(truncate(val), default_style='"', width=sys.maxsize).rstrip("\n")
254  if isinstance(val, (list, tuple)):
255  if not val:
256  return "[]"
257  if api.scalar(typename) in api.ROS_STRING_TYPES:
258  yaml_str = yaml.safe_dump(truncate(val)).rstrip('\n')
259  return "\n" + "\n".join(indent + line for line in yaml_str.splitlines())
260  vals = [x for v in truncate(val) for x in [self.message_to_yaml(v, top, typename)] if x]
261  if api.scalar(typename) in api.ROS_NUMERIC_TYPES:
262  return "[%s]" % ", ".join(unquote(str(v)) for v in vals)
263  return ("\n" + "\n".join(indent + "- " + v for v in vals)) if vals else ""
264  if api.is_ros_message(val):
265  MATCHED_ONLY = self.args.MATCHED_FIELDS_ONLY and not self.args.LINES_AROUND_MATCH
266  vals, fieldmap = [], api.get_message_fields(val)
267  prints, noprints = self._patterns["print"], self._patterns["noprint"]
268  fieldmap = api.filter_fields(fieldmap, top, include=prints, exclude=noprints)
269  for k, t in fieldmap.items():
270  v = self.message_to_yaml(api.get_message_value(val, k, t), top + (k, ), t)
271  if not v or MATCHED_ONLY and MatchMarkers.START not in v:
272  continue # for k, t
273 
274  if t not in api.ROS_STRING_TYPES: v = unquote(v)
275  if api.scalar(t) in api.ROS_BUILTIN_TYPES:
276  is_strlist = t.endswith("]") and api.scalar(t) in api.ROS_STRING_TYPES
277  is_num = api.scalar(t) in api.ROS_NUMERIC_TYPES
278  extra_indent = indent if is_strlist else " " * len(indent + k + ": ")
279  self._wrapper.reserve_width(self._prefix + extra_indent)
280  self._wrapper.drop_whitespace = t.endswith("]") and not is_strlist
281  self._wrapper.break_long_words = not is_num
282  v = ("\n" + extra_indent).join(retag_match_lines(self._wrapper.wrap(v)))
283  if is_strlist and self._wrapper.strip(v) != "[]": v = "\n" + v
284  vals.append("%s%s: %s" % (indent, k, api.format_message_value(val, k, v)))
285  return ("\n" if indent and vals else "") + "\n".join(vals)
286 
287  return str(val)
288 
289 
290  def _configure(self, args):
291  """Initializes output settings."""
292  prints, noprints = args.EMIT_FIELD, args.NOEMIT_FIELD
293  for key, vals in [("print", prints), ("noprint", noprints)]:
294  self._patterns[key] = [(tuple(v.split(".")), common.wildcard_to_regex(v)) for v in vals]
295 
296  if args.COLOR not in ("never", False):
297  self._styles.update({"hl0": ConsolePrinter.STYLE_HIGHLIGHT if self.args.HIGHLIGHT
298  else "",
299  "ll0": ConsolePrinter.STYLE_LOWLIGHT,
300  "pfx0": ConsolePrinter.STYLE_SPECIAL, # Content line prefix start
301  "sep0": ConsolePrinter.STYLE_SPECIAL2})
302  self._styles.default_factory = lambda: ConsolePrinter.STYLE_RESET
303 
304  WRAPS = args.MATCH_WRAPPER if self.args.HIGHLIGHT else ""
305  if WRAPS is None and args.COLOR in ("never", False): WRAPS = self.NOCOLOR_HIGHLIGHT_WRAPPERS
306  WRAPS = ((WRAPS or [""]) * 2)[:2]
307  self._styles["hl0"] = self._styles["hl0"] + WRAPS[0]
308  self._styles["hl1"] = WRAPS[1] + self._styles["hl1"]
309 
310  custom_widths = {MatchMarkers.START: len(WRAPS[0]), MatchMarkers.END: len(WRAPS[1]),
311  self._styles["ll0"]: 0, self._styles["ll1"]: 0,
312  self._styles["pfx0"]: 0, self._styles["pfx1"]: 0,
313  self._styles["sep0"]: 0, self._styles["sep1"]: 0}
314  wrapargs = dict(max_lines=args.MAX_FIELD_LINES,
315  placeholder="%s ...%s" % (self._styles["ll0"], self._styles["ll1"]))
316  if args.WRAP_WIDTH is not None: wrapargs.update(width=args.WRAP_WIDTH)
317  self._wrapper = common.TextWrapper(custom_widths=custom_widths, **wrapargs)
318  self._format_repls = {MatchMarkers.START: self._styles["hl0"],
319  MatchMarkers.END: self._styles["hl1"]}
320 
321 
322 
323 class RolloverSinkMixin(object):
324  """Provides output file rollover by size, duration, or message count."""
325 
326 
327  DEFAULT_ARGS = dict(VERBOSE=False, WRITE=None, WRITE_OPTIONS={})
328 
329 
330  OPTIONS_TEMPLATES = [
331  ("rollover-size=NUM", "size limit for individual files\nin {label} output\n"
332  "as bytes (supports abbreviations like 1K or 2M or 3G)"),
333  ("rollover-count=NUM", "message limit for individual files\nin {label} output\n"
334  "(supports abbreviations like 1K or 2M or 3G)"),
335  ("rollover-duration=INTERVAL", "message time span limit for individual files\n"
336  "in {label} output\n"
337  "as seconds (supports abbreviations like 60m or 2h or 1d)"),
338  ("rollover-template=STR", "output filename template for individual files\n"
339  "in {label} output,\n"
340  'supporting strftime format codes like "%%H-%%M-%%S"\n'
341  'and "%%(index)s" as output file index'),
342  ]
343 
344  START_META_TEMPLATE = "{mcount} in {tcount} to "
345 
346  FILE_META_TEMPLATE = "{name} ({size})"
347 
348  MULTI_META_TEMPLATE = "\n- {name} ({size}, {mcount}, {tcount})"
349 
350 
351  def __init__(self, args=None, **kwargs):
352  """
353  @param args arguments as namespace or dictionary, case-insensitive
354  @param args.write base name of output file to write if not using rollover-template
355  @param args.write_options {"rollover-size": bytes limit for individual output files,
356  "rollover-count": message limit for individual output files,
357  "rollover-duration": time span limit for individual output files,
358  as ROS duration or convertible seconds,
359  "rollover-template": output filename template, supporting
360  strftime format codes like "%H-%M-%S"
361  and "%(index)s" as output file index,
362  "overwrite": whether to overwrite existing file
363  (default false)}
364  @param kwargs any and all arguments as keyword overrides, case-insensitive
365  """
366  self._rollover_limits = {} # {?"size": int, ?"count": int, ?"duration": ROS duration}
367  self._rollover_template = None
368  self._rollover_files = collections.OrderedDict() # {path: {"counts", "start", "size"}}
369 
370 
371  self.filename = None
372 
373 
374  def validate(self):
375  """Returns whether write options are valid, emits error if not, else populates options."""
376  ok = True
377  for k in ("size", "count", "duration"):
378  value = value0 = self.args.WRITE_OPTIONS.get("rollover-%s" % k)
379  if value is None: continue # for k
380  SUFFIXES = dict(zip("smhd", [1, 60, 3600, 24*3600])) if "duration" == k else \
381  dict(zip("KMGT", [2**10, 2**20, 2**30, 2**40])) if "size" == k else \
382  dict(zip("KMGT", [10**3, 10**6, 10**9, 10**12]))
383  try:
384  if isinstance(value, (six.binary_type, six.text_type)):
385  value = common.parse_number(value, SUFFIXES)
386  value = (api.to_duration if "duration" == k else int)(value)
387  except Exception: pass
388  if (value is None or value < 0) if "duration" != k \
389  else (k != api.get_ros_time_category(value) or api.to_sec(value) < 0):
390  ConsolePrinter.error("Invalid rollover %s option: %r. "
391  "Value must be a non-negative %s.", k, value0, k)
392  ok = False
393  elif value:
394  self._rollover_limits[k] = value
395  if self.args.WRITE_OPTIONS.get("rollover-template"):
396  value = self.args.WRITE_OPTIONS["rollover-template"]
397  value = re.sub(r"(^|[^%])%\(index\)", r"\1%%(index)", value)
398  try: datetime.datetime.now().strftime(value)
399  except Exception:
400  ConsolePrinter.error("Invalid rollover template option: %r. "
401  "Value must contain valid strftime codes.", value)
402  ok = False
403  else:
404  self._rollover_template = value
405  if ok and not self._rollover_limits:
406  ConsolePrinter.warn("Ignoring rollover template option: "
407  "no rollover limits given.")
408  return ok
409 
410 
411  def ensure_rollover(self, topic, msg, stamp):
412  """
413  Closes current output file and prepares new filename if rollover limit reached.
414  """
415  if not self._rollover_limits: return
416 
417  self.filename = self.filename or self.make_filename()
418  do_rollover, props = False, self._rollover_files.setdefault(self.filename, {})
419  stamp = api.time_message(stamp, to_message=False) # Ensure rclpy stamp in ROS2
420 
421  if self._rollover_limits.get("size") and props:
422  props["size"] = self.size
423  do_rollover = (props["size"] or 0) >= self._rollover_limits["size"]
424  if not do_rollover and self._rollover_limits.get("count") and props:
425  do_rollover = (sum(props["counts"].values()) >= self._rollover_limits["count"])
426  if not do_rollover and self._rollover_limits.get("duration") and props:
427  stamps = [stamp, props["start"]]
428  do_rollover = (max(stamps) - min(stamps) >= self._rollover_limits["duration"])
429  if do_rollover:
430  self.close_output()
431  props["size"] = self.size
432  self.filename = self.make_filename()
433  props = self._rollover_files[self.filename] = {}
434 
435  topickey = api.TypeMeta.make(msg, topic).topickey
436  if not props: props.update({"counts": {}, "start": stamp, "size": None})
437  props["start"] = min((props["start"], stamp))
438  props["counts"][topickey] = props["counts"].get(topickey, 0) + 1
439 
440 
441  def close_output(self):
442  """Closes output file, if any."""
443  raise NotImplementedError
444 
445 
446  def make_filename(self):
447  """Returns new filename for output, accounting for rollover template and overwrite."""
448  result = self.args.WRITE
449  if self._rollover_template and self._rollover_limits:
450  result = datetime.datetime.now().strftime(self._rollover_template)
451  try: result %= {"index": len(self._rollover_files)}
452  except Exception: pass
453  if self.args.WRITE_OPTIONS.get("overwrite") not in (True, "true"):
454  result = common.unique_path(result, empty_ok=True)
455  return result
456 
457 
459  """Returns output file metainfo string, with names and sizes and message/topic counts."""
460  if not self._counts: return ""
461  SIZE_ERROR = "error getting size"
462  result = self.START_META_TEMPLATE.format(
463  mcount=common.plural("message", sum(self._counts.values())),
464  tcount=common.plural("topic", self._counts)
465  )
466  if len(self._rollover_files) < 2:
467  sz = self.size
468  sizestr = SIZE_ERROR if sz is None else common.format_bytes(sz)
469  result += self.FILE_META_TEMPLATE.format(name=self.filename, size=sizestr) + "."
470  else:
471  for path, props in self._rollover_files.items():
472  if props["size"] is None:
473  try: props["size"] = os.path.getsize(path)
474  except Exception as e:
475  ConsolePrinter.warn("Error getting size of %s: %s", path, e)
476  sizesum = sum(x["size"] for x in self._rollover_files.values() if x["size"] is not None)
477  result += self.FILE_META_TEMPLATE.format(
478  name=common.plural("file", self._rollover_files),
479  size=common.format_bytes(sizesum)
480  ) + ":"
481  for path, props in self._rollover_files.items():
482  sizestr = SIZE_ERROR if props["size"] is None else common.format_bytes(props["size"])
483  result += self.MULTI_META_TEMPLATE.format(name=path, size=sizestr,
484  mcount=common.plural("message", sum(props["counts"].values())),
485  tcount=common.plural("topic", props["counts"])
486  )
487  return result
488 
489 
490  @property
491  def size(self):
492  """Returns current file size in bytes, or None if size lookup failed."""
493  try: return os.path.getsize(self.filename)
494  except Exception as e:
495  ConsolePrinter.warn("Error getting size of %s: %s", self.filename, e)
496  return None
497 
498 
499  @classmethod
500  def get_write_options(cls, label):
501  """Returns command-line help texts for rollover options, as [(name, help)]."""
502  return [(k, v.format(label=label)) for k, v in cls.OPTIONS_TEMPLATES]
503 
504 
505 
507  """Prints messages to console."""
508 
509  META_LINE_TEMPLATE = "{ll0}{sep} {line}{ll1}"
510  MESSAGE_SEP_TEMPLATE = "{ll0}{sep}{ll1}"
511  PREFIX_TEMPLATE = "{pfx0}{batch}{pfx1}{sep0}{sep}{sep1}"
512  MATCH_PREFIX_SEP = ":" # Printed after bag filename for matched message lines
513  CONTEXT_PREFIX_SEP = "-" # Printed after bag filename for context message lines
514  SEP = "---" # Prefix of message separators and metainfo lines
515 
516 
517  DEFAULT_ARGS = dict(COLOR=True, EMIT_FIELD=(), NOEMIT_FIELD=(), HIGHLIGHT=True, META=False,
518  LINE_PREFIX=True, MAX_FIELD_LINES=None, START_LINE=None,
519  END_LINE=None, MAX_MESSAGE_LINES=None, LINES_AROUND_MATCH=None,
520  MATCHED_FIELDS_ONLY=False, WRAP_WIDTH=None, MATCH_WRAPPER=None)
521 
522 
523  def __init__(self, args=None, **kwargs):
524  """
525  @param args arguments as namespace or dictionary, case-insensitive
526  @param args.color False or "never" for not using colors in replacements
527  @param args.highlight highlight matched values (default true)
528  @param args.meta whether to print metainfo
529  @param args.emit_field message fields to emit if not all
530  @param args.noemit_field message fields to skip in output
531  @param args.line_prefix print source prefix like bag filename on each message line
532  @param args.max_field_lines maximum number of lines to print per field
533  @param args.start_line message line number to start output from
534  @param args.end_line message line number to stop output at
535  @param args.max_message_lines maximum number of lines to output per message
536  @param args.lines_around_match number of message lines around matched fields to output
537  @param args.matched_fields_only output only the fields where match was found
538  @param args.wrap_width character width to wrap message YAML output at
539  @param args.match_wrapper string to wrap around matched values,
540  both sides if one value, start and end if more than one,
541  or no wrapping if zero values
542  @param kwargs any and all arguments as keyword overrides, case-insensitive
543  """
544  args = common.ensure_namespace(args, ConsoleSink.DEFAULT_ARGS, **kwargs)
545  if args.WRAP_WIDTH is None:
546  args = common.structcopy(args)
547  args.WRAP_WIDTH = ConsolePrinter.WIDTH
548 
549  super(ConsoleSink, self).__init__(args)
550  TextSinkMixin.__init__(self, args)
551 
552 
553  def emit_meta(self):
554  """Prints source metainfo like bag header, if not already printed."""
555  batch = self.args.META and self.source.get_batch()
556  if self.args.META and batch not in self._batch_meta:
557  meta = self._batch_meta[batch] = self.source.format_meta()
558  kws = dict(self._styles, sep=self.SEP)
559  meta = "\n".join(x and self.META_LINE_TEMPLATE.format(**dict(kws, line=x))
560  for x in meta.splitlines())
561  meta and ConsolePrinter.print(meta)
562 
563 
564  def emit(self, topic, msg, stamp=None, match=None, index=None):
565  """Prints separator line and message text."""
566  self._prefix = ""
567  stamp, index = self._ensure_stamp_index(topic, msg, stamp, index)
568  if self.args.LINE_PREFIX and self.source.get_batch():
569  sep = self.MATCH_PREFIX_SEP if match else self.CONTEXT_PREFIX_SEP
570  kws = dict(self._styles, sep=sep, batch=self.source.get_batch())
571  self._prefix = self.PREFIX_TEMPLATE.format(**kws)
572  kws = dict(self._styles, sep=self.SEP)
573  if self.args.META:
574  meta = self.source.format_message_meta(topic, msg, stamp, index)
575  meta = "\n".join(x and self.META_LINE_TEMPLATE.format(**dict(kws, line=x))
576  for x in meta.splitlines())
577  meta and ConsolePrinter.print(meta)
578  elif self._counts:
579  sep = self.MESSAGE_SEP_TEMPLATE.format(**kws)
580  sep and ConsolePrinter.print(sep)
581  ConsolePrinter.print(self.format_message(match or msg, highlight=bool(match)))
582  super(ConsoleSink, self).emit(topic, msg, stamp, match, index)
583 
584 
585  def is_highlighting(self):
586  """Returns True if sink is configured to highlight matched values."""
587  return bool(self.args.HIGHLIGHT)
588 
589 
590 
592  """Writes messages to bagfile."""
593 
594 
595  DEFAULT_ARGS = dict(META=False, WRITE_OPTIONS={}, VERBOSE=False)
596 
597  def __init__(self, args=None, **kwargs):
598  """
599  @param args arguments as namespace or dictionary, case-insensitive;
600  or a single path as the ROS bagfile to write,
601  or a stream or {@link grepros.api.Bag Bag} instance to write to
602  @param args.write name of ROS bagfile to create or append to,
603  or a stream to write to
604  @param args.write_options {"overwrite": whether to overwrite existing file
605  (default false),
606  "rollover-size": bytes limit for individual output files,
607  "rollover-count": message limit for individual output files,
608  "rollover-duration": time span limit for individual output files,
609  as ROS duration or convertible seconds,
610  "rollover-template": output filename template, supporting
611  strftime format codes like "%H-%M-%S"
612  and "%(index)s" as output file index}
613  @param args.meta whether to emit metainfo
614  @param args.verbose whether to emit debug information
615  @param kwargs any and all arguments as keyword overrides, case-insensitive
616  """
617 
618  args0 = args
619  args = {"WRITE": str(args)} if isinstance(args, common.PATH_TYPES) else \
620  {"WRITE": args} if common.is_stream(args) else \
621  {} if isinstance(args, api.Bag) else args
622  args = common.ensure_namespace(args, BagSink.DEFAULT_ARGS, **kwargs)
623  super(BagSink, self).__init__(args)
624  RolloverSinkMixin.__init__(self, args)
625  self._bag = args0 if isinstance(args0, api.Bag) else None
626  self._overwrite = (args.WRITE_OPTIONS.get("overwrite") in ("true", True))
627  self._close_printed = False
628  self._is_pathed = self._bag is None and not common.is_stream(self.args.WRITE)
629 
630  atexit.register(self.close)
631 
632  def emit(self, topic, msg, stamp=None, match=None, index=None):
633  """Writes message to output bagfile."""
634  if not self.validate(): raise Exception("invalid")
635  stamp, index = self._ensure_stamp_index(topic, msg, stamp, index)
636  if self._is_pathed: RolloverSinkMixin.ensure_rollover(self, topic, msg, stamp)
637  self._ensure_open()
638  topickey = api.TypeMeta.make(msg, topic).topickey
639  if topickey not in self._counts and self.args.VERBOSE:
640  ConsolePrinter.debug("Adding topic %s in bag output.", topic)
641 
642  qoses = self.source.get_message_meta(topic, msg, stamp).get("qoses")
643  self._bag.write(topic, msg, stamp, qoses=qoses)
644  super(BagSink, self).emit(topic, msg, stamp, match, index)
645 
646  def validate(self):
647  """Returns whether write options are valid and ROS environment set, emits error if not."""
648  if self.valid is not None: return self.valid
649  result = RolloverSinkMixin.validate(self)
650  if self.args.WRITE_OPTIONS.get("overwrite") not in (None, True, False, "true", "false"):
651  ConsolePrinter.error("Invalid overwrite option for bag: %r. "
652  "Choose one of {true, false}.",
653  self.args.WRITE_OPTIONS["overwrite"])
654  result = False
655  if not self._bag \
656  and not common.verify_io(self.make_filename() if self._is_pathed else self.args.WRITE, "w"):
657  ConsolePrinter.error("File not writable.")
658  result = False
659  if not self._bag and common.is_stream(self.args.WRITE) \
660  and not any(c.STREAMABLE for c in api.Bag.WRITER_CLASSES):
661  ConsolePrinter.error("Bag format does not support writing streams.")
662  result = False
663  if self._bag and self._bag.mode not in ("a", "w"):
664  ConsolePrinter.error("Bag not in write mode.")
665  result = False
666  self.valid = api.validate() and result
667  return self.valid
668 
669  def close(self):
670  """Closes output bag, if any, emits metainfo."""
671  self._bag and self._bag.close()
672  if not self._close_printed and self._counts and self._bag:
673  self._close_printed = True
674  ConsolePrinter.debug("Wrote bag output for %s", self.format_output_meta())
675  super(BagSink, self).close()
676 
677  def close_output(self):
678  """Closes output bag, if any."""
679  self._bag and self._bag.close()
680  self._bag = None
681 
682  @property
683  def size(self):
684  """Returns current file size in bytes, or None if size lookup failed."""
685  try: return os.path.getsize(self._bag.filename if self._bag else self.filename) \
686  if not self._bag or (self._bag.filename and api.ROS1) else self._bag.size
687  except Exception as e:
688  ConsolePrinter.warn("Error getting size of %s: %s", self.filename, e)
689  return None
690 
691  def _ensure_open(self):
692  """Opens output file if not already open."""
693  if self._bag is not None:
694  if self._bag.closed:
695  self._bag.open()
696  self._close_printed = False
697  return
698  self._close_printed = False
699  if common.is_stream(self.args.WRITE):
700  self._bag = api.Bag(self.args.WRITE, mode=getattr(self.args.WRITE, "mode", "w"))
701  self.filename = "<stream>"
702  return
703 
704  filename = self.filename = self.filename or self.make_filename()
705  if not self._overwrite and os.path.isfile(filename) and os.path.getsize(filename):
706  cls = api.Bag.autodetect(filename)
707  if cls and "a" not in getattr(cls, "MODES", ("a", )):
708  filename = self.filename = common.unique_path(filename)
709  if self.args.VERBOSE:
710  ConsolePrinter.debug("Making unique filename %r, as %s does not support "
711  "appending.", filename, cls.__name___)
712  if self.args.VERBOSE:
713  sz = os.path.isfile(filename) and os.path.getsize(filename)
714  ConsolePrinter.debug("%s bag output %s%s.",
715  "Overwriting" if sz and self._overwrite else
716  "Appending to" if sz else "Creating",
717  filename, (" (%s)" % common.format_bytes(sz)) if sz else "")
718  common.makedirs(os.path.dirname(filename))
719  self._bag = api.Bag(filename, mode="w" if self._overwrite else "a")
720 
721  @classmethod
722  def autodetect(cls, target):
723  """Returns true if target is recognizable as a ROS bag."""
724  ext = os.path.splitext(target or "")[-1].lower()
725  return ext in api.BAG_EXTENSIONS
726 
727 
729  """Publishes messages to ROS topics."""
730 
731 
732  DEFAULT_ARGS = dict(LIVE=False, META=False, QUEUE_SIZE_OUT=10, PUBLISH_PREFIX="",
733  PUBLISH_SUFFIX="", PUBLISH_FIXNAME="", VERBOSE=False)
734 
735  def __init__(self, args=None, **kwargs):
736  """
737  @param args arguments as namespace or dictionary, case-insensitive
738  @param args.live whether reading messages from live ROS topics
739  @param args.queue_size_out publisher queue size (default 10)
740  @param args.publish_prefix output topic prefix, prepended to input topic
741  @param args.publish_suffix output topic suffix, appended to output topic
742  @param args.publish_fixname single output topic name to publish to,
743  overrides prefix and suffix if given
744  @param args.meta whether to emit metainfo
745  @param args.verbose whether to emit debug information
746  @param kwargs any and all arguments as keyword overrides, case-insensitive
747  """
748  args = common.ensure_namespace(args, TopicSink.DEFAULT_ARGS, **kwargs)
749  super(TopicSink, self).__init__(args)
750  self._pubs = {} # {(intopic, typename, typehash): ROS publisher}
751  self._close_printed = False
752 
753  def emit(self, topic, msg, stamp=None, match=None, index=None):
754  """Publishes message to output topic."""
755  if not self.validate(): raise Exception("invalid")
756  api.init_node()
757  with api.TypeMeta.make(msg, topic) as m:
758  topickey, cls = (m.topickey, m.typeclass)
759  if topickey not in self._pubs:
760  topic2 = self.args.PUBLISH_PREFIX + topic + self.args.PUBLISH_SUFFIX
761  topic2 = self.args.PUBLISH_FIXNAME or topic2
762  if self.args.VERBOSE:
763  ConsolePrinter.debug("Publishing from %s to %s.", topic, topic2)
764 
765  pub = None
766  if self.args.PUBLISH_FIXNAME:
767  pub = next((v for (_, c), v in self._pubs.items() if c == cls), None)
768  pub = pub or api.create_publisher(topic2, cls, queue_size=self.args.QUEUE_SIZE_OUT)
769  self._pubs[topickey] = pub
770 
771  self._pubs[topickey].publish(msg)
772  self._close_printed = False
773  super(TopicSink, self).emit(topic, msg, stamp, match, index)
774 
775  def bind(self, source):
776  """Attaches source to sink and blocks until connected to ROS."""
777  if not self.validate(): raise Exception("invalid")
778  super(TopicSink, self).bind(source)
779  api.init_node()
780 
781  def validate(self):
782  """
783  Returns whether ROS environment is set for publishing,
784  and output topic configuration is valid, emits error if not.
785  """
786  if self.valid is not None: return self.valid
787  result = api.validate(live=True)
788  config_ok = True
789  if self.args.LIVE and not any((self.args.PUBLISH_PREFIX, self.args.PUBLISH_SUFFIX,
790  self.args.PUBLISH_FIXNAME)):
791  ConsolePrinter.error("Need topic prefix or suffix or fixname "
792  "when republishing messages from live ROS topics.")
793  config_ok = False
794  self.valid = result and config_ok
795  return self.valid
796 
797  def close(self):
798  """Shuts down publishers."""
799  if not self._close_printed and self._counts:
800  self._close_printed = True
801  ConsolePrinter.debug("Published %s to %s.",
802  common.plural("message", sum(self._counts.values())),
803  common.plural("topic", self._pubs))
804  for k in list(self._pubs):
805  try: self._pubs.pop(k).unregister()
806  except Exception as e:
807  if self.args.VERBOSE:
808  ConsolePrinter.warn("Error closing publisher on topic %r: %s", k[0], e)
809  super(TopicSink, self).close()
810 
811 
812 class AppSink(Sink):
813  """Provides messages to callback function."""
814 
815 
816  DEFAULT_ARGS = dict(EMIT=None, METAEMIT=None, HIGHLIGHT=False)
817 
818  def __init__(self, args=None, **kwargs):
819  """
820  @param args arguments as namespace or dictionary, case-insensitive;
821  or emit callback
822  @param args.emit callback(topic, msg, stamp, highlighted msg, index in topic), if any
823  @param args.metaemit callback(metadata dict) if any, invoked before first emit from source batch
824  @param args.highlight whether to expect highlighted matching fields from source messages
825  @param kwargs any and all arguments as keyword overrides, case-insensitive
826  """
827  if callable(args): args = common.ensure_namespace(None, emit=args)
828  args = common.ensure_namespace(args, AppSink.DEFAULT_ARGS, **kwargs)
829  super(AppSink, self).__init__(args)
830 
831  def emit_meta(self):
832  """Invokes registered metaemit callback, if any, and not already invoked."""
833  if not self.source: return
834  batch = self.source.get_batch() if self.args.METAEMIT else None
835  if self.args.METAEMIT and batch not in self._batch_meta:
836  meta = self._batch_meta[batch] = self.source.get_meta()
837  self.args.METAEMIT(meta)
838 
839  def emit(self, topic, msg, stamp=None, match=None, index=None):
840  """Registers message and invokes registered emit callback, if any."""
841  stamp, index = self._ensure_stamp_index(topic, msg, stamp, index)
842  super(AppSink, self).emit(topic, msg, stamp, match, index)
843  if self.args.EMIT: self.args.EMIT(topic, msg, stamp, match, index)
844 
845  def is_highlighting(self):
846  """Returns whether emitted matches are highlighted."""
847  return self.args.HIGHLIGHT
848 
849 
851  """Combines any number of sinks."""
852 
853 
854  FLAG_CLASSES = {"PUBLISH": TopicSink, "CONSOLE": ConsoleSink, "APP": AppSink}
855 
856 
857  FORMAT_CLASSES = {"bag": BagSink}
858 
859  def __init__(self, args=None, sinks=(), **kwargs):
860  """
861  Accepts more arguments, given to the real sinks constructed.
862 
863  @param args arguments as namespace or dictionary, case-insensitive
864  @param args.console print matches to console
865  @param args.write [[target, format=FORMAT, key=value, ], ]
866  @param args.publish publish matches to live topics
867  @param args.app provide messages to given callback function
868  @param sinks pre-created sinks, arguments will be ignored
869  @param kwargs any and all arguments as keyword overrides, case-insensitive
870  """
871  args = common.ensure_namespace(args, **kwargs)
872  super(MultiSink, self).__init__(args)
873  self.valid = True
874 
875 
876  self.sinks = [cls(args) for flag, cls in self.FLAG_CLASSES.items()
877  if getattr(args, flag, None)] if not sinks else list(sinks)
878 
879  for dumpopts in getattr(args, "WRITE", []) if not sinks else ():
880  kwargs = dict(x.split("=", 1) for x in dumpopts[1:] if isinstance(x, common.TEXT_TYPES))
881  kwargs.update(kv for x in dumpopts[1:] if isinstance(x, dict) for kv in x.items())
882  target, cls = dumpopts[0], self.FORMAT_CLASSES.get(kwargs.pop("format", None))
883  if not cls:
884  cls = next((c for c in sorted(self.FORMAT_CLASSES.values(),
885  key=lambda x: x is BagSink)
886  if callable(getattr(c, "autodetect", None))
887  and c.autodetect(target)), None)
888  if not cls:
889  ConsolePrinter.error('Unknown output format in "%s"' % " ".join(map(str, dumpopts)))
890  self.valid = False
891  continue # for dumpopts
892  clsargs = common.structcopy(args)
893  clsargs.WRITE, clsargs.WRITE_OPTIONS = target, kwargs
894  self.sinks += [cls(clsargs)]
895 
896  def emit_meta(self):
897  """Outputs source metainfo in one sink, if not already emitted."""
898  sink = next((s for s in self.sinks if isinstance(s, ConsoleSink)), None)
899  # Emit meta in one sink only, prefer console
900  sink = sink or self.sinks[0] if self.sinks else None
901  sink and sink.emit_meta()
902 
903  def emit(self, topic, msg, stamp=None, match=None, index=None):
904  """Outputs ROS message to all sinks."""
905  stamp, index = self._ensure_stamp_index(topic, msg, stamp, index)
906  for sink in self.sinks:
907  sink.emit(topic, msg, stamp, match, index)
908  super(MultiSink, self).emit(topic, msg, stamp, match, index)
909 
910  def bind(self, source):
911  """Attaches source to all sinks, sets thread_excepthook on all sinks."""
912  super(MultiSink, self).bind(source)
913  for sink in self.sinks:
914  sink.bind(source)
915  sink.thread_excepthook = self.thread_excepthook
916 
917  def validate(self):
918  """Returns whether prerequisites are met for all sinks."""
919  if not self.sinks:
920  ConsolePrinter.error("No output configured.")
921  return bool(self.sinks) and all([sink.validate() for sink in self.sinks]) and self.valid
922 
923  def close(self):
924  """Closes all sinks."""
925  for sink in self.sinks:
926  sink.close()
927 
928  def flush(self):
929  """Flushes all sinks."""
930  for sink in self.sinks:
931  sink.flush()
932 
933  def is_highlighting(self):
934  """Returns whether any sink requires highlighted matches."""
935  return any(s.is_highlighting() for s in self.sinks)
936 
937 
938 __all__ = [
939  "AppSink", "BagSink", "ConsoleSink", "MultiSink", "RolloverSinkMixin", "Sink", "TextSinkMixin",
940  "TopicSink"
941 ]
grepros.outputs.RolloverSinkMixin.OPTIONS_TEMPLATES
list OPTIONS_TEMPLATES
Command-line help templates for rollover options, as [(name, text with s label placeholder)].
Definition: outputs.py:330
grepros.outputs.TextSinkMixin.format_message
def format_message(self, msg, highlight=False)
Definition: outputs.py:162
grepros.outputs.ConsoleSink.CONTEXT_PREFIX_SEP
string CONTEXT_PREFIX_SEP
Definition: outputs.py:513
grepros.outputs.ConsoleSink.PREFIX_TEMPLATE
string PREFIX_TEMPLATE
Definition: outputs.py:511
grepros.outputs.TextSinkMixin
Definition: outputs.py:122
grepros.outputs.BagSink
Definition: outputs.py:591
grepros.outputs.BagSink._bag
_bag
Definition: outputs.py:625
grepros.outputs.TextSinkMixin._prefix
_prefix
Definition: outputs.py:153
grepros.outputs.Sink.__enter__
def __enter__(self)
Definition: outputs.py:56
grepros.outputs.TextSinkMixin.__init__
def __init__(self, args=None, **kwargs)
Definition: outputs.py:134
grepros.outputs.Sink.args
args
Definition: outputs.py:50
grepros.outputs.ConsoleSink
Definition: outputs.py:506
grepros.outputs.ConsoleSink.MESSAGE_SEP_TEMPLATE
string MESSAGE_SEP_TEMPLATE
Definition: outputs.py:510
grepros.outputs.AppSink.emit
def emit(self, topic, msg, stamp=None, match=None, index=None)
Definition: outputs.py:839
grepros.outputs.MultiSink
Definition: outputs.py:850
grepros.outputs.RolloverSinkMixin._rollover_template
_rollover_template
Definition: outputs.py:367
grepros.outputs.TextSinkMixin.NOCOLOR_HIGHLIGHT_WRAPPERS
string NOCOLOR_HIGHLIGHT_WRAPPERS
Default highlight wrappers if not color output.
Definition: outputs.py:126
grepros.outputs.BagSink.emit
def emit(self, topic, msg, stamp=None, match=None, index=None)
Definition: outputs.py:632
grepros.outputs.ConsoleSink.emit
def emit(self, topic, msg, stamp=None, match=None, index=None)
Definition: outputs.py:564
grepros.outputs.AppSink.emit_meta
def emit_meta(self)
Definition: outputs.py:831
grepros.outputs.MultiSink.FORMAT_CLASSES
dictionary FORMAT_CLASSES
Autobinding between --write TARGET format=FORMAT and sink classes.
Definition: outputs.py:857
grepros.outputs.BagSink.validate
def validate(self)
Definition: outputs.py:646
grepros.outputs.Sink.bind
def bind(self, source)
Definition: outputs.py:84
grepros.outputs.TextSinkMixin._wrapper
_wrapper
Definition: outputs.py:154
grepros.outputs.RolloverSinkMixin.FILE_META_TEMPLATE
string FILE_META_TEMPLATE
Definition: outputs.py:346
grepros.outputs.Sink
Definition: outputs.py:32
grepros.outputs.RolloverSinkMixin.__init__
def __init__(self, args=None, **kwargs)
Definition: outputs.py:351
grepros.outputs.RolloverSinkMixin._rollover_limits
_rollover_limits
Definition: outputs.py:366
grepros.outputs.TextSinkMixin._format_repls
_format_repls
Definition: outputs.py:156
grepros.outputs.TopicSink.bind
def bind(self, source)
Definition: outputs.py:775
grepros.outputs.TopicSink._pubs
_pubs
Definition: outputs.py:750
grepros.outputs.Sink._batch_meta
_batch_meta
Definition: outputs.py:47
grepros.outputs.BagSink._is_pathed
_is_pathed
Definition: outputs.py:628
grepros.outputs.TopicSink.__init__
def __init__(self, args=None, **kwargs)
Definition: outputs.py:735
grepros.outputs.AppSink
Definition: outputs.py:812
grepros.outputs.TextSinkMixin._patterns
_patterns
Definition: outputs.py:155
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.MultiSink.bind
def bind(self, source)
Definition: outputs.py:910
grepros.outputs.Sink._counts
_counts
Definition: outputs.py:48
grepros.outputs.RolloverSinkMixin.make_filename
def make_filename(self)
Definition: outputs.py:446
grepros.outputs.Sink.thread_excepthook
def thread_excepthook(self, text, exc)
Definition: outputs.py:101
grepros.outputs.Sink.emit
def emit(self, topic, msg, stamp=None, match=None, index=None)
Definition: outputs.py:71
grepros.outputs.RolloverSinkMixin.MULTI_META_TEMPLATE
string MULTI_META_TEMPLATE
Definition: outputs.py:348
grepros.outputs.AppSink.__init__
def __init__(self, args=None, **kwargs)
Definition: outputs.py:818
grepros.outputs.Sink.validate
def validate(self)
Definition: outputs.py:88
grepros.outputs.BagSink._overwrite
_overwrite
Definition: outputs.py:626
grepros.outputs.TopicSink
Definition: outputs.py:728
grepros.outputs.ConsoleSink.emit_meta
def emit_meta(self)
Definition: outputs.py:553
grepros.outputs.RolloverSinkMixin.ensure_rollover
def ensure_rollover(self, topic, msg, stamp)
Definition: outputs.py:411
grepros.outputs.MultiSink.close
def close(self)
Definition: outputs.py:923
grepros.outputs.MultiSink.sinks
sinks
List of all combined sinks.
Definition: outputs.py:876
grepros.outputs.Sink.emit_meta
def emit_meta(self)
Definition: outputs.py:64
grepros.outputs.Sink.__init__
def __init__(self, args=None, **kwargs)
Definition: outputs.py:41
grepros.outputs.ConsoleSink.META_LINE_TEMPLATE
string META_LINE_TEMPLATE
Definition: outputs.py:509
grepros.outputs.RolloverSinkMixin.filename
filename
Current output file path.
Definition: outputs.py:371
grepros.outputs.AppSink.is_highlighting
def is_highlighting(self)
Definition: outputs.py:845
grepros.outputs.RolloverSinkMixin.START_META_TEMPLATE
string START_META_TEMPLATE
Definition: outputs.py:344
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.outputs.ConsoleSink.MATCH_PREFIX_SEP
string MATCH_PREFIX_SEP
Definition: outputs.py:512
grepros.outputs.TopicSink.close
def close(self)
Definition: outputs.py:797
grepros.outputs.Sink.FILE_EXTENSIONS
tuple FILE_EXTENSIONS
Auto-detection file extensions for subclasses, as (".ext", )
Definition: outputs.py:36
grepros.outputs.RolloverSinkMixin.close_output
def close_output(self)
Definition: outputs.py:441
grepros.outputs.MultiSink.flush
def flush(self)
Definition: outputs.py:928
grepros.outputs.BagSink.close
def close(self)
Definition: outputs.py:669
grepros.outputs.RolloverSinkMixin
Definition: outputs.py:323
grepros.outputs.MultiSink.emit
def emit(self, topic, msg, stamp=None, match=None, index=None)
Definition: outputs.py:903
grepros.api.Bag
Definition: api.py:350
grepros.outputs.ConsoleSink.SEP
string SEP
Definition: outputs.py:514
grepros.outputs.BagSink.close_output
def close_output(self)
Definition: outputs.py:677
grepros.outputs.BagSink._ensure_open
def _ensure_open(self)
Definition: outputs.py:691
grepros.outputs.BagSink._close_printed
_close_printed
Definition: outputs.py:627
grepros.outputs.MultiSink.validate
def validate(self)
Definition: outputs.py:917
grepros.outputs.Sink.is_highlighting
def is_highlighting(self)
Definition: outputs.py:105
grepros.outputs.BagSink.__init__
def __init__(self, args=None, **kwargs)
Definition: outputs.py:597
grepros.outputs.RolloverSinkMixin._rollover_files
_rollover_files
Definition: outputs.py:368
grepros.outputs.MultiSink.FLAG_CLASSES
dictionary FLAG_CLASSES
Autobinding between argument flags and sink classes.
Definition: outputs.py:854
grepros.outputs.TextSinkMixin._styles
_styles
Definition: outputs.py:157
grepros.outputs.TopicSink.validate
def validate(self)
Definition: outputs.py:781
grepros.outputs.RolloverSinkMixin.size
def size(self)
Definition: outputs.py:491
grepros.inputs.Source
Definition: inputs.py:34
grepros.outputs.RolloverSinkMixin.validate
def validate(self)
Definition: outputs.py:374
grepros.outputs.MultiSink.__init__
def __init__(self, args=None, sinks=(), **kwargs)
Definition: outputs.py:859
grepros.outputs.Sink.__exit__
def __exit__(self, exc_type, exc_value, traceback)
Definition: outputs.py:60
grepros.outputs.Sink._ensure_stamp_index
def _ensure_stamp_index(self, topic, msg, stamp=None, index=None)
Definition: outputs.py:115
grepros.outputs.TopicSink._close_printed
_close_printed
Definition: outputs.py:751
grepros.outputs.ConsoleSink.is_highlighting
def is_highlighting(self)
Definition: outputs.py:585
grepros.outputs.BagSink.size
def size(self)
Definition: outputs.py:683
grepros.outputs.Sink.flush
def flush(self)
Definition: outputs.py:98
grepros.outputs.MultiSink.emit_meta
def emit_meta(self)
Definition: outputs.py:896
grepros.common.TextWrapper
Definition: common.py:482
grepros.outputs.MultiSink.is_highlighting
def is_highlighting(self)
Definition: outputs.py:933
grepros.outputs.Sink.autodetect
def autodetect(cls, target)
Definition: outputs.py:110
grepros.outputs.TopicSink.emit
def emit(self, topic, msg, stamp=None, match=None, index=None)
Definition: outputs.py:753
grepros.outputs.TextSinkMixin.message_to_yaml
def message_to_yaml(self, val, top=(), typename=None)
Definition: outputs.py:202
grepros.outputs.RolloverSinkMixin.get_write_options
def get_write_options(cls, label)
Definition: outputs.py:500
grepros.outputs.BagSink.autodetect
def autodetect(cls, target)
Definition: outputs.py:722
grepros.outputs.ConsoleSink.__init__
def __init__(self, args=None, **kwargs)
Definition: outputs.py:523
grepros.outputs.TextSinkMixin._configure
def _configure(self, args)
Definition: outputs.py:290


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