telemetry-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. telemetry_cli/__init__.py +8 -0
  2. telemetry_cli/_cli.py +18 -0
  3. telemetry_cli/_getopt.py +114 -0
  4. telemetry_cli/_msg.py +39 -0
  5. telemetry_cli/_pack.py +183 -0
  6. telemetry_cli/_perl.py +183 -0
  7. telemetry_cli/pick/__init__.py +1 -0
  8. telemetry_cli/pick/__main__.py +7 -0
  9. telemetry_cli/pick/cli.py +157 -0
  10. telemetry_cli/pick/commandline.py +219 -0
  11. telemetry_cli/pick/fields.py +166 -0
  12. telemetry_cli/pick/formats.py +191 -0
  13. telemetry_cli/pick/help.py +536 -0
  14. telemetry_cli/pick/symbols/airmoc +24 -0
  15. telemetry_cli/pick/symbols/aux +28 -0
  16. telemetry_cli/pick/symbols/corr +31 -0
  17. telemetry_cli/pick/symbols/corrOld +29 -0
  18. telemetry_cli/pick/symbols/hw +16 -0
  19. telemetry_cli/pick/symbols/moc +42 -0
  20. telemetry_cli/pick/symbols/subcom +83 -0
  21. telemetry_cli/pick/symbols/subcom_2002 +111 -0
  22. telemetry_cli/recl/__init__.py +1 -0
  23. telemetry_cli/recl/__main__.py +7 -0
  24. telemetry_cli/recl/cli.py +214 -0
  25. telemetry_cli/recl/corr.py +83 -0
  26. telemetry_cli/recs/__init__.py +1 -0
  27. telemetry_cli/recs/__main__.py +7 -0
  28. telemetry_cli/recs/cli.py +182 -0
  29. telemetry_cli/recs/extract.py +399 -0
  30. telemetry_cli/tgen/__init__.py +1 -0
  31. telemetry_cli/tgen/__main__.py +7 -0
  32. telemetry_cli/tgen/cli.py +128 -0
  33. telemetry_cli/tgen/cmdfile.py +68 -0
  34. telemetry_cli/tgen/convert.py +278 -0
  35. telemetry_cli/tgen/examples/arb_data.tgen +15 -0
  36. telemetry_cli/tgen/examples/calsweep.tgen +32 -0
  37. telemetry_cli/tgen/examples/cond_fill.tgen +17 -0
  38. telemetry_cli/tgen/examples/counter.tgen +11 -0
  39. telemetry_cli/tgen/examples/null_data.tgen +11 -0
  40. telemetry_cli/tgen/examples/random_size.tgen +20 -0
  41. telemetry_cli/tgen/examples/subcom.tgen +14 -0
  42. telemetry_cli/tgen/examples/valueV_value.tgen +20 -0
  43. telemetry_cli/tgen/runtime.py +390 -0
  44. telemetry_cli-1.0.0.dist-info/METADATA +106 -0
  45. telemetry_cli-1.0.0.dist-info/RECORD +48 -0
  46. telemetry_cli-1.0.0.dist-info/WHEEL +4 -0
  47. telemetry_cli-1.0.0.dist-info/entry_points.txt +5 -0
  48. telemetry_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,8 @@
1
+ """telemetry-cli: command-line tools for binary telemetry."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("telemetry-cli")
7
+ except PackageNotFoundError: # running from a source tree without installing
8
+ __version__ = "0+unknown"
telemetry_cli/_cli.py ADDED
@@ -0,0 +1,18 @@
1
+ """Helpers shared by the command-line programs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import os
7
+ import sys
8
+
9
+
10
+ def broken_pipe(stdout) -> int:
11
+ """Handle stdout closing early (``tool | head``): exit quietly, with status 0.
12
+
13
+ Python would otherwise report the error when it flushes stdout at exit.
14
+ """
15
+ if stdout is sys.stdout.buffer:
16
+ with contextlib.suppress(OSError, ValueError):
17
+ os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
18
+ return 0
@@ -0,0 +1,114 @@
1
+ """Command-line options as Perl's Getopt::Long read them (with its default settings).
2
+
3
+ recs, recl and tgen used Getopt::Long, so their options behave like this:
4
+
5
+ - an option starts with one or two dashes: ``-mml 4``, ``--mml=4``;
6
+ - names are case-insensitive and can be shortened to any unique prefix
7
+ (``-ma`` for ``-max_reclen``);
8
+ - a value comes after ``=`` or as the next argument, even one starting with a
9
+ dash (``-e -r`` sets ``e`` to ``-r``);
10
+ - options and other arguments can be mixed; ``--`` ends the options, and a
11
+ lone ``-`` is an argument.
12
+
13
+ Specs use Getopt::Long's syntax: ``"name|alias"`` for a flag, and ``=i``,
14
+ ``=f`` or ``=s`` for an integer, float or string value.
15
+
16
+ Unlike the Perl tools, which printed a warning and carried on, a bad option is
17
+ an error here.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import re
23
+ from dataclasses import dataclass
24
+
25
+ _INT = re.compile(r"^[-+]?\d+$")
26
+ _FLOAT = re.compile(r"^[-+]?(?=\d|\.\d)\d*(\.\d*)?([eE][-+]?\d+)?$")
27
+
28
+
29
+ class OptionError(ValueError):
30
+ pass
31
+
32
+
33
+ @dataclass
34
+ class _Spec:
35
+ key: str # the first name, used as the result key
36
+ names: list[str]
37
+ kind: str | None # None (a flag), "i", "f" or "s"
38
+
39
+
40
+ def _parse_specs(specs: list[str]) -> list[_Spec]:
41
+ out = []
42
+ for spec in specs:
43
+ m = re.fullmatch(r"([\w|]+)(?:=([ifs]))?", spec)
44
+ if not m:
45
+ raise ValueError(f"bad option spec {spec!r}")
46
+ names = m.group(1).split("|")
47
+ out.append(_Spec(names[0], names, m.group(2)))
48
+ return out
49
+
50
+
51
+ def getoptions(argv: list[str], specs: list[str]) -> tuple[dict, list[str]]:
52
+ """Parse argv. Returns (options by their first name, remaining arguments)."""
53
+ table = _parse_specs(specs)
54
+ opts: dict = {}
55
+ args: list[str] = []
56
+ i = 0
57
+ while i < len(argv):
58
+ a = argv[i]
59
+ i += 1
60
+ if a == "--":
61
+ args += argv[i:]
62
+ break
63
+ if not a.startswith("-") or a == "-":
64
+ args.append(a)
65
+ continue
66
+ body = a[2:] if a.startswith("--") else a[1:]
67
+ name, eq, value = body.partition("=")
68
+ spec, name = _find(table, name)
69
+ if spec.kind is None:
70
+ if eq:
71
+ raise OptionError(f"Option {name} does not take an argument")
72
+ opts[spec.key] = 1
73
+ continue
74
+ if not eq:
75
+ if i >= len(argv):
76
+ raise OptionError(f"Option {name} requires an argument")
77
+ value = argv[i]
78
+ i += 1
79
+ elif value == "":
80
+ raise OptionError(f"Option {name} requires an argument")
81
+ opts[spec.key] = _convert(spec, name, value)
82
+ return opts, args
83
+
84
+
85
+ def _find(table: list[_Spec], name: str) -> tuple[_Spec, str]:
86
+ """The option a (possibly shortened) name refers to, and its full name."""
87
+ low = name.lower()
88
+ for s in table:
89
+ for n in s.names:
90
+ if n.lower() == low:
91
+ return s, n
92
+ matches: dict[str, tuple[_Spec, str]] = {}
93
+ for s in table:
94
+ for n in s.names:
95
+ if n.lower().startswith(low) and low:
96
+ matches.setdefault(s.key, (s, n))
97
+ if len(matches) == 1:
98
+ return next(iter(matches.values()))
99
+ if not matches:
100
+ raise OptionError(f"Unknown option: {low}")
101
+ names = ", ".join(sorted(n for _, n in matches.values()))
102
+ raise OptionError(f"Option {name} is ambiguous ({names})")
103
+
104
+
105
+ def _convert(spec: _Spec, name: str, value: str):
106
+ if spec.kind == "i":
107
+ if not _INT.match(value):
108
+ raise OptionError(f'Value "{value}" invalid for option {name} (number expected)')
109
+ return int(value)
110
+ if spec.kind == "f":
111
+ if not _FLOAT.match(value):
112
+ raise OptionError(f'Value "{value}" invalid for option {name} (real number expected)')
113
+ return float(value)
114
+ return value
telemetry_cli/_msg.py ADDED
@@ -0,0 +1,39 @@
1
+ """Tagged diagnostic messages, replacing the Perl Util::Msg module.
2
+
3
+ Util::Msg printed each message as ``TAG text``, with ``NOTE`` as the default
4
+ tag, and could hide or select tags. recl's result lines (``RESULT 48``,
5
+ ``CORR 97.5``) use this format, so it is part of recl's output.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from typing import TextIO
12
+
13
+
14
+ class Messenger:
15
+ """Print ``TAG text`` lines to a stream, optionally filtering by tag."""
16
+
17
+ def __init__(
18
+ self,
19
+ stream: TextIO | None = None,
20
+ *,
21
+ hide: set[str] | frozenset[str] = frozenset(),
22
+ default_tag: str = "NOTE",
23
+ ):
24
+ self._stream = stream
25
+ self.hide = set(hide)
26
+ self.default_tag = default_tag
27
+ self.counts: dict[str, int] = {}
28
+
29
+ @property
30
+ def stream(self) -> TextIO:
31
+ # Look up sys.stderr at call time, so pytest's capsys sees the output.
32
+ return self._stream if self._stream is not None else sys.stderr
33
+
34
+ def __call__(self, text: object = "", tag: str | None = None) -> None:
35
+ tag = tag or self.default_tag
36
+ if tag in self.hide:
37
+ return
38
+ self.counts[tag] = self.counts.get(tag, 0) + 1
39
+ print(f"{tag} {text}", file=self.stream)
telemetry_cli/_pack.py ADDED
@@ -0,0 +1,183 @@
1
+ """A subset of Perl's pack/unpack, built on struct.
2
+
3
+ tgen command files give their field types as Perl pack templates ('L', 'a10',
4
+ 'C256', 'd2'), and pick's data types map onto the same letters. This module
5
+ reproduces Perl's behavior for the letters those tools use:
6
+
7
+ c C 8-bit signed / unsigned
8
+ s S 16-bit signed / unsigned
9
+ l L i I 32-bit signed / unsigned
10
+ q Q 64-bit signed / unsigned
11
+ n N 16 / 32-bit unsigned, big-endian
12
+ v V 16 / 32-bit unsigned, little-endian
13
+ f d single / double float
14
+ a A Z byte strings: null-padded, space-padded, null-terminated
15
+ x a null byte
16
+
17
+ Each letter can take a repeat count (``C4``) or ``*``, and the integer and
18
+ float letters wider than a byte take a ``<`` or ``>`` byte-order modifier. Without a modifier
19
+ the byte order is native, as in Perl.
20
+
21
+ As in Perl, integers are truncated toward zero and wrap to the field width, so
22
+ ``pack('C', 257)`` is ``b'\\x01'`` and ``pack('L', -1)`` is ``b'\\xff\\xff\\xff\\xff'``.
23
+ Missing values pack as zero (or as empty strings for a/A/Z).
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import math
29
+ import re
30
+ import struct
31
+ import sys
32
+
33
+ from ._perl import numify as _to_number
34
+
35
+ # letter -> (struct code, size in bytes, signed?) for the integer letters
36
+ _INTS = {
37
+ "c": ("b", 1, True),
38
+ "C": ("B", 1, False),
39
+ "s": ("h", 2, True),
40
+ "S": ("H", 2, False),
41
+ "l": ("i", 4, True),
42
+ "L": ("I", 4, False),
43
+ "i": ("i", 4, True),
44
+ "I": ("I", 4, False),
45
+ "q": ("q", 8, True),
46
+ "Q": ("Q", 8, False),
47
+ "n": ("H", 2, False),
48
+ "N": ("I", 4, False),
49
+ "v": ("H", 2, False),
50
+ "V": ("I", 4, False),
51
+ }
52
+ _FIXED_ORDER = {"n": ">", "N": ">", "v": "<", "V": "<"}
53
+ _FLOATS = {"f": "f", "d": "d"}
54
+ _STRINGS = "aAZ"
55
+
56
+ _ITEM = re.compile(r"\s*([cCsSlLiIqQnNvVfdaAZx])([<>])?(\*|\d+)?\s*")
57
+ NATIVE = "<" if sys.byteorder == "little" else ">"
58
+
59
+
60
+ class PackError(ValueError):
61
+ """A template this module can't handle."""
62
+
63
+
64
+ def parse(template: str) -> list[tuple[str, str, int | None]]:
65
+ """Split a template into (letter, byte order, count) items.
66
+
67
+ The count is None when omitted and -1 for ``*``.
68
+ """
69
+ items = []
70
+ pos = 0
71
+ while pos < len(template):
72
+ m = _ITEM.match(template, pos)
73
+ if not m or m.end() == pos:
74
+ if template[pos:].strip() == "":
75
+ break
76
+ raise PackError(f"unsupported pack template {template!r} at {template[pos:]!r}")
77
+ letter, order, count = m.groups()
78
+ if order and (letter in _STRINGS or letter in _FIXED_ORDER or letter in "cCx"):
79
+ raise PackError(f"'{order}' is not allowed after '{letter}' in {template!r}")
80
+ order = _FIXED_ORDER.get(letter, order or NATIVE)
81
+ n = None if count is None else (-1 if count == "*" else int(count))
82
+ items.append((letter, order, n))
83
+ pos = m.end()
84
+ return items
85
+
86
+
87
+ def _to_int(v, size: int, signed: bool) -> int:
88
+ x = _to_number(v)
89
+ if isinstance(x, float):
90
+ x = 0 if math.isnan(x) else (int(x) if math.isfinite(x) else (-1 if x < 0 else 2**64 - 1))
91
+ bits = 8 * size
92
+ x &= (1 << bits) - 1
93
+ if signed and x >= 1 << (bits - 1):
94
+ x -= 1 << bits
95
+ return x
96
+
97
+
98
+ def _to_bytes(v) -> bytes:
99
+ if v is None:
100
+ return b""
101
+ if isinstance(v, (bytes, bytearray)):
102
+ return bytes(v)
103
+ if isinstance(v, float) and v.is_integer():
104
+ v = int(v)
105
+ return str(v).encode("latin-1")
106
+
107
+
108
+ def pack(template: str, *values) -> bytes:
109
+ """Pack values like Perl's pack(template, values)."""
110
+ out = bytearray()
111
+ vals = list(values)
112
+ i = 0
113
+
114
+ def take():
115
+ nonlocal i
116
+ v = vals[i] if i < len(vals) else None
117
+ i += 1
118
+ return v
119
+
120
+ for letter, order, count in parse(template):
121
+ if letter == "x":
122
+ out += b"\0" * (1 if count is None else max(count, 0))
123
+ elif letter in _STRINGS:
124
+ s = _to_bytes(take())
125
+ if count == -1:
126
+ out += s + (b"\0" if letter == "Z" else b"")
127
+ else:
128
+ n = 1 if count is None else count
129
+ pad = b" " if letter == "A" else b"\0"
130
+ if letter == "Z" and n > 0:
131
+ s = s[: n - 1]
132
+ out += s[:n].ljust(n, pad)
133
+ else:
134
+ reps = len(vals) - i if count == -1 else (1 if count is None else count)
135
+ for _ in range(max(reps, 0)):
136
+ v = take()
137
+ if letter in _FLOATS:
138
+ x = float(_to_number(0 if v is None else v))
139
+ out += struct.pack(order + _FLOATS[letter], x)
140
+ else:
141
+ code, size, signed = _INTS[letter]
142
+ out += struct.pack(order + code, _to_int(0 if v is None else v, size, signed))
143
+ return bytes(out)
144
+
145
+
146
+ def unpack(template: str, data: bytes) -> tuple:
147
+ """Unpack data like Perl's unpack(template, data).
148
+
149
+ Strings come back as bytes. Like Perl, unpacking stops quietly when the
150
+ data run out.
151
+ """
152
+ out = []
153
+ pos = 0
154
+ for letter, order, count in parse(template):
155
+ if letter == "x":
156
+ pos += 1 if count is None else max(count, 0)
157
+ elif letter in _STRINGS:
158
+ n = len(data) - pos if count == -1 else (1 if count is None else count)
159
+ s = data[pos : pos + n]
160
+ pos += len(s)
161
+ if letter == "A":
162
+ s = s.rstrip(b" \0\t\n\r\f")
163
+ elif letter == "Z":
164
+ s = s.split(b"\0", 1)[0]
165
+ out.append(s)
166
+ else:
167
+ if letter in _FLOATS:
168
+ fmt, size = order + _FLOATS[letter], struct.calcsize(_FLOATS[letter])
169
+ else:
170
+ code, size, _ = _INTS[letter]
171
+ fmt = order + code
172
+ reps = (len(data) - pos) // size if count == -1 else (1 if count is None else count)
173
+ for _ in range(reps):
174
+ if pos + size > len(data):
175
+ break
176
+ out.append(struct.unpack_from(fmt, data, pos)[0])
177
+ pos += size
178
+ return tuple(out)
179
+
180
+
181
+ def calcsize(template: str) -> int:
182
+ """Bytes produced by a template with fixed counts (no ``*``)."""
183
+ return len(pack(template))
telemetry_cli/_perl.py ADDED
@@ -0,0 +1,183 @@
1
+ """Small pieces of Perl semantics that the ported tools' output depends on.
2
+
3
+ - numify: Perl's numeric value of a string (leading number, else 0)
4
+ - to_iv / to_uv: Perl's conversion of a number to a signed / unsigned 64-bit
5
+ integer, as used by printf's %d, %u, %x and %o
6
+ - sprintf: printf for the conversions pick uses, including Perl's spelling of
7
+ infinities and NaN ("Inf", "-Inf", "NaN")
8
+ - substr: Perl's substr, which counts negative offsets from the end
9
+ - truthy: Perl's idea of a true string ("" and "0" are false)
10
+ - perl_str: a value as Perl prints it (floats with 15 significant digits)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import math
16
+ import re
17
+
18
+ _NUMBER = re.compile(
19
+ r"\s*([+-]?)(?:(inf(?:inity)?|nan)|(\d+\.?\d*(?:[eE][+-]?\d+)?|\.\d+(?:[eE][+-]?\d+)?))",
20
+ re.IGNORECASE,
21
+ )
22
+ IV_MIN, IV_MAX, UV_MAX = -(2**63), 2**63 - 1, 2**64 - 1
23
+
24
+
25
+ def numify(v) -> int | float:
26
+ """Perl's numeric value of v. None (undef) and non-numeric strings are 0.
27
+
28
+ Handles ordinary numeric strings, "inf"/"nan", and Perl's odd reading of a
29
+ leading sign followed by another signed number ("--1" is 1, "- 1" is -1).
30
+ """
31
+ if v is None:
32
+ return 0
33
+ if isinstance(v, bool):
34
+ return int(v)
35
+ if isinstance(v, (int, float)):
36
+ return v
37
+ if isinstance(v, (bytes, bytearray)):
38
+ v = bytes(v).decode("latin-1")
39
+ m = _NUMBER.match(v)
40
+ if m:
41
+ return _number(m)
42
+ m = re.match(r"\s*([+-])\s*", v)
43
+ if m:
44
+ inner = _NUMBER.match(v, m.end())
45
+ if inner and not inner.group(0)[:1].isspace():
46
+ x = _number(inner)
47
+ return -x if m.group(1) == "-" else x
48
+ return 0
49
+
50
+
51
+ def _number(m: re.Match) -> int | float:
52
+ sign, special, digits = m.groups()
53
+ if special:
54
+ x = math.inf if special[0] in "iI" else math.nan
55
+ elif any(c in digits for c in ".eE"):
56
+ x = float(digits)
57
+ else:
58
+ x = int(digits)
59
+ return -x if sign == "-" else x
60
+
61
+
62
+ def to_iv(v) -> int:
63
+ """Perl's IV (signed 64-bit) value of a finite number."""
64
+ x = numify(v)
65
+ if isinstance(x, int):
66
+ if x < IV_MIN:
67
+ return IV_MIN
68
+ return x if x <= IV_MAX else min(x, UV_MAX) - 2**64
69
+ if x < IV_MIN:
70
+ return IV_MIN
71
+ if x < 2**63:
72
+ return int(x)
73
+ uv = UV_MAX if x >= 2**64 else int(x) # Perl goes through UV here
74
+ return uv - 2**64
75
+
76
+
77
+ def to_uv(v) -> int:
78
+ """Perl's UV (unsigned 64-bit) value of a finite number."""
79
+ x = numify(v)
80
+ if isinstance(x, int):
81
+ return min(x, UV_MAX) if x >= 0 else max(x, IV_MIN) & UV_MAX
82
+ if x < 0:
83
+ return to_iv(x) & UV_MAX
84
+ return UV_MAX if x >= 2**64 else int(x)
85
+
86
+
87
+ def multiply(a, b) -> int | float:
88
+ """Perl's a * b: integer arithmetic when both are integer-valued (so -0.0 * 180 is 0)."""
89
+
90
+ def as_int(x):
91
+ x = numify(x)
92
+ if isinstance(x, float) and x.is_integer() and IV_MIN <= x <= UV_MAX:
93
+ return int(x)
94
+ return x
95
+
96
+ a, b = as_int(a), as_int(b)
97
+ if isinstance(a, int) and isinstance(b, int) and IV_MIN <= a * b <= UV_MAX:
98
+ return a * b
99
+ return float(a) * float(b)
100
+
101
+
102
+ def _special(x) -> str | None:
103
+ """Perl's text for an infinity or NaN, else None."""
104
+ if isinstance(x, float) and not math.isfinite(x):
105
+ if math.isnan(x):
106
+ return "NaN"
107
+ return "Inf" if x > 0 else "-Inf"
108
+ return None
109
+
110
+
111
+ _CONV = re.compile(r"%(-?)(0?)(\d*)(?:\.(\d+))?([dugGxosc%])")
112
+
113
+
114
+ def sprintf(fmt: str, *args) -> str:
115
+ """Perl's sprintf for the flag/width/precision/conversion subset pick uses."""
116
+ it = iter(args)
117
+
118
+ def conv(m: re.Match) -> str:
119
+ left, zero, width, prec, c = m.groups()
120
+ if c == "%":
121
+ return "%"
122
+ v = next(it, None)
123
+ x = numify(v) if c != "s" else v
124
+ text = _special(x) if c != "s" else None
125
+ if text and c in "duxo" and isinstance(v, str) and not re.search("[a-zA-Z]{3}", v):
126
+ # Perl's integer conversions read a numeric string that overflows
127
+ # (like "1e400") as the largest integer, not as Inf
128
+ text = None
129
+ if text is not None:
130
+ w = int(width or 0)
131
+ return text.ljust(w) if left else text.rjust(w)
132
+ spec = f"%{left}{zero}{width}" + (f".{prec}" if prec is not None else "")
133
+ if c == "d":
134
+ return (spec + "d") % to_iv(x)
135
+ if c == "u":
136
+ return (spec + "d") % to_uv(x)
137
+ if c in "xo":
138
+ return (spec + c) % to_uv(x)
139
+ if c in "gG":
140
+ return (spec + c) % float(x)
141
+ if c == "c":
142
+ return (spec + "c") % chr(to_iv(x) & 0x10FFFF)
143
+ return (spec + "s") % ("" if v is None else v)
144
+
145
+ return _CONV.sub(conv, fmt)
146
+
147
+
148
+ def substr(data: bytes, offset: int, length: int | None = None) -> bytes:
149
+ """Perl's substr(data, offset, length) for non-negative length.
150
+
151
+ A negative offset counts from the end; a start before the beginning is
152
+ clipped (so substr("abc", -5, 4) is "ab"). Past the end gives b"".
153
+ """
154
+ n = len(data)
155
+ start = n + offset if offset < 0 else offset
156
+ if start > n:
157
+ return b""
158
+ end = n if length is None else start + max(length, 0)
159
+ return data[max(start, 0) : max(min(end, n), 0)]
160
+
161
+
162
+ def truthy(s) -> bool:
163
+ """Perl truth of a scalar: undef, "", "0" and 0 are false."""
164
+ if s is None:
165
+ return False
166
+ if isinstance(s, str):
167
+ return s not in ("", "0")
168
+ return bool(s)
169
+
170
+
171
+ def perl_str(v) -> str:
172
+ """A value as Perl turns it into text: floats with 15 significant digits."""
173
+ if v is None:
174
+ return ""
175
+ if isinstance(v, bool):
176
+ return "1" if v else ""
177
+ if isinstance(v, float):
178
+ if v.is_integer() and abs(v) < 1e15:
179
+ return str(int(v))
180
+ return sprintf("%.15g", v)
181
+ if isinstance(v, bytes):
182
+ return v.decode("latin-1")
183
+ return str(v)
@@ -0,0 +1 @@
1
+ """pick: extract and print fields from fixed-length binary records."""
@@ -0,0 +1,7 @@
1
+ """Allow ``python -m telemetry_cli.pick``."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ sys.exit(main())