ltcodecs 4.4.2__tar.gz → 4.5.1__tar.gz
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.
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/PKG-INFO +2 -2
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/docs/codec_configs.rst +24 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/docs/reference/codec_aliases.rst +3 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/docs/reference/codecs.rst +4 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/docs/reference/config_schema.rst +10 -3
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/docs/reference/python_api.rst +8 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/pyproject.toml +7 -2
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/__init__.py +2 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/inspection.py +24 -2
- ltcodecs-4.5.1/src/ltcodecs/variable_len_array_optional_codec.py +103 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs.egg-info/PKG-INFO +2 -2
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs.egg-info/SOURCES.txt +1 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs.egg-info/requires.txt +1 -1
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs.egg-info/scm_file_list.json +1 -0
- ltcodecs-4.5.1/src/ltcodecs.egg-info/scm_version.json +8 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/tests/test_codecs.py +65 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/tests/test_dict_codec.py +61 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/tests/test_inspection.py +32 -0
- ltcodecs-4.4.2/src/ltcodecs.egg-info/scm_version.json +0 -8
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/.gitignore +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/.gitlab/merge_request_templates/Default.md +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/.gitlab-ci.yml +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/CONTRIBUTING.md +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/LICENSE +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/README.md +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/docs/Makefile +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/docs/conf.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/docs/index.rst +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/docs/quickstart.rst +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/docs/requirements.txt +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/docs/ros_messages.rst +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/docs/tests.rst +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/requirements.txt +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/setup.cfg +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/_length_modes.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/bool_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/bytes_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/ccl_latlon_bcd_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/ccl_latlon_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/constant_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/dict_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/dict_field_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/exceptions.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/exponentialgolomb_integer_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/field_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/fixed_len_array_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/fixedint_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/float_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/ieee_float_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/inspect_cli.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/linspace_float_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/lzma_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/metadata_field_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/multiple_field_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/optional_field_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/padding_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/ros_message_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/ros_msg_field_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/rostime_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/string_codecs.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/string_enum_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/variable_len_array_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/variable_len_bit_array_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs/varint_codec.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs.egg-info/dependency_links.txt +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs.egg-info/entry_points.txt +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/src/ltcodecs.egg-info/top_level.txt +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/tests/__init__.py +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/tests/dict_codec.yaml +0 -0
- {ltcodecs-4.4.2 → ltcodecs-4.5.1}/tests/string_msg_codec.yaml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ltcodecs
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.5.1
|
|
4
4
|
Summary: LT Codecs: Lightweight codecs for use over low-throughput links
|
|
5
5
|
Author: Woods Hole Oceanographic Institution, Acoustic Communications Group
|
|
6
6
|
License: LGPL-3.0-or-later
|
|
@@ -16,7 +16,7 @@ Requires-Python: >=3.6
|
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: armw
|
|
19
|
-
Requires-Dist: bitstring
|
|
19
|
+
Requires-Dist: bitstring<5,>=4
|
|
20
20
|
Requires-Dist: msgpack
|
|
21
21
|
Requires-Dist: crccheck
|
|
22
22
|
Requires-Dist: rospy-yaml-include
|
|
@@ -284,6 +284,30 @@ target fields use no bits.
|
|
|
284
284
|
The input dictionary or ROS message must contain both ``has_extra`` and
|
|
285
285
|
``extra`` when ``has_extra`` is true.
|
|
286
286
|
|
|
287
|
+
Array-Optional Fields
|
|
288
|
+
---------------------
|
|
289
|
+
|
|
290
|
+
Use ``variable_len_array_optional`` when the presence of additional fields
|
|
291
|
+
depends on a variable-length array. It uses the same array parameters as
|
|
292
|
+
``variable_len_array``. By default, ``target_fields`` are encoded immediately
|
|
293
|
+
after the array only when the encoded array is nonempty. Set
|
|
294
|
+
``target_fields_length_threshold`` to require more than that many elements.
|
|
295
|
+
|
|
296
|
+
.. code-block:: yaml
|
|
297
|
+
|
|
298
|
+
samples:
|
|
299
|
+
codec: variable_len_array_optional
|
|
300
|
+
element_type: uint8
|
|
301
|
+
max_length: 10
|
|
302
|
+
target_fields:
|
|
303
|
+
sample_time:
|
|
304
|
+
codec: uint32
|
|
305
|
+
value: 0
|
|
306
|
+
|
|
307
|
+
When ``samples`` is empty, ``sample_time`` uses no bits and decodes to ``0``.
|
|
308
|
+
Without ``value``, the field is absent from the decoded result. When the array
|
|
309
|
+
is nonempty, the input must provide ``sample_time`` and its value is encoded.
|
|
310
|
+
|
|
287
311
|
Padding
|
|
288
312
|
-------
|
|
289
313
|
|
|
@@ -51,6 +51,9 @@ Alias Codec Class
|
|
|
51
51
|
``variable_len_bit_array``
|
|
52
52
|
:py:class:`~ltcodecs.variable_len_bit_array_codec.VariableLenBitArrayCodec`
|
|
53
53
|
|
|
54
|
+
``variable_len_array_optional``
|
|
55
|
+
:py:class:`~ltcodecs.variable_len_array_optional_codec.VariableLenArrayOptionalCodec`
|
|
56
|
+
|
|
54
57
|
Metadata Aliases
|
|
55
58
|
----------------
|
|
56
59
|
|
|
@@ -85,6 +85,10 @@ Container Codecs
|
|
|
85
85
|
and a three-bit count of unused final-byte bits. Configure the derived field
|
|
86
86
|
with ``bit_length_field``; it is not a separate configured wire field.
|
|
87
87
|
|
|
88
|
+
``variable_len_array_optional``
|
|
89
|
+
Encodes a variable-length array followed by ``target_fields`` when the
|
|
90
|
+
encoded array length exceeds ``target_fields_length_threshold`` (default 0).
|
|
91
|
+
|
|
88
92
|
``fixed_len_array``
|
|
89
93
|
Encodes a fixed number of elements. The encoded bitstream does not include
|
|
90
94
|
a length field, so the decoder always reads ``length`` elements.
|
|
@@ -38,8 +38,9 @@ Common Field Keys
|
|
|
38
38
|
codecs.
|
|
39
39
|
|
|
40
40
|
``min_length``
|
|
41
|
-
Minimum number of elements for ``variable_len_array
|
|
42
|
-
the provided list is
|
|
41
|
+
Minimum number of elements for ``variable_len_array`` and
|
|
42
|
+
``variable_len_array_optional``. Encoding fails when the provided list is
|
|
43
|
+
shorter than this value.
|
|
43
44
|
|
|
44
45
|
``length_mode``
|
|
45
46
|
Length handling mode for ``string``, ``bytes``, and
|
|
@@ -69,7 +70,13 @@ Common Field Keys
|
|
|
69
70
|
absent/present bit.
|
|
70
71
|
|
|
71
72
|
``target_fields``
|
|
72
|
-
Field mapping controlled by an ``optional``
|
|
73
|
+
Field mapping controlled by an ``optional`` or
|
|
74
|
+
``variable_len_array_optional`` field. For the latter, an individual target
|
|
75
|
+
may set ``value`` to supply its decoded value when it is not encoded.
|
|
76
|
+
|
|
77
|
+
``target_fields_length_threshold``
|
|
78
|
+
For ``variable_len_array_optional``, target fields are present when the
|
|
79
|
+
encoded array length is greater than this value. Defaults to 0.
|
|
73
80
|
|
|
74
81
|
``ros_type``
|
|
75
82
|
ROS message type for ``msg`` fields. ROS 1 and ROS 2 type names are
|
|
@@ -197,6 +197,14 @@ ltcodecs.variable\_len\_array\_codec module
|
|
|
197
197
|
:undoc-members:
|
|
198
198
|
:show-inheritance:
|
|
199
199
|
|
|
200
|
+
ltcodecs.variable\_len\_array\_optional\_codec module
|
|
201
|
+
-----------------------------------------------------
|
|
202
|
+
|
|
203
|
+
.. automodule:: ltcodecs.variable_len_array_optional_codec
|
|
204
|
+
:members:
|
|
205
|
+
:undoc-members:
|
|
206
|
+
:show-inheritance:
|
|
207
|
+
|
|
200
208
|
ltcodecs.variable\_len\_bit\_array\_codec module
|
|
201
209
|
---------------------------------------------------
|
|
202
210
|
|
|
@@ -11,7 +11,7 @@ classifiers = [
|
|
|
11
11
|
"Topic :: Scientific/Engineering",
|
|
12
12
|
]
|
|
13
13
|
requires-python = ">=3.6"
|
|
14
|
-
dependencies = ["armw", "bitstring", "msgpack", "crccheck", "rospy-yaml-include"]
|
|
14
|
+
dependencies = ["armw", "bitstring>=4,<5", "msgpack", "crccheck", "rospy-yaml-include"]
|
|
15
15
|
|
|
16
16
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
17
17
|
|
|
@@ -32,7 +32,12 @@ inspect_codec = "ltcodecs.inspect_cli:main"
|
|
|
32
32
|
# No optional dependencies
|
|
33
33
|
|
|
34
34
|
[build-system]
|
|
35
|
-
requires = [
|
|
35
|
+
requires = [
|
|
36
|
+
"setuptools>=64",
|
|
37
|
+
"wheel",
|
|
38
|
+
"setuptools_scm>=8; python_version >= '3.8'",
|
|
39
|
+
"setuptools_scm>=7,<8; python_version < '3.8'",
|
|
40
|
+
]
|
|
36
41
|
build-backend = "setuptools.build_meta"
|
|
37
42
|
|
|
38
43
|
[tool.setuptools_scm]
|
|
@@ -12,6 +12,7 @@ from .string_enum_codec import StringEnumCodec
|
|
|
12
12
|
from .bytes_codec import BytesCodec
|
|
13
13
|
from .metadata_field_codec import MetadataFieldCodec
|
|
14
14
|
from .variable_len_array_codec import VariableLenArrayCodec
|
|
15
|
+
from .variable_len_array_optional_codec import VariableLenArrayOptionalCodec
|
|
15
16
|
from .variable_len_bit_array_codec import VariableLenBitArrayCodec
|
|
16
17
|
from .fixed_len_array_codec import FixedLenArrayCodec
|
|
17
18
|
from .dict_field_codec import DictFieldCodec
|
|
@@ -56,6 +57,7 @@ field_codec_classes = {
|
|
|
56
57
|
"msg": RosMsgFieldCodec,
|
|
57
58
|
"ros_msg": RosMsgFieldCodec,
|
|
58
59
|
"variable_len_array": VariableLenArrayCodec,
|
|
60
|
+
"variable_len_array_optional": VariableLenArrayOptionalCodec,
|
|
59
61
|
"variable_len_bit_array": VariableLenBitArrayCodec,
|
|
60
62
|
"fixed_len_array": FixedLenArrayCodec,
|
|
61
63
|
"ccl_latlon": CclLatLonCodec,
|
|
@@ -209,9 +209,9 @@ class CodecInspector:
|
|
|
209
209
|
continue
|
|
210
210
|
if codec_name in ("dict", "dictionary"):
|
|
211
211
|
issues.extend(cls._validate_fields(params.get("fields"), path))
|
|
212
|
-
elif codec_name
|
|
212
|
+
elif codec_name in ("optional", "variable_len_array_optional"):
|
|
213
213
|
issues.extend(cls._validate_fields(params.get("target_fields"), path))
|
|
214
|
-
|
|
214
|
+
if codec_name in ("variable_len_array", "variable_len_array_optional", "fixed_len_array"):
|
|
215
215
|
element_type = params.get("element_type")
|
|
216
216
|
if not isinstance(element_type, str):
|
|
217
217
|
issues.append(ValidationIssue(path, "Missing required string parameter 'element_type'"))
|
|
@@ -269,6 +269,28 @@ class CodecInspector:
|
|
|
269
269
|
cls._make_conditional(child)
|
|
270
270
|
return FieldReport(path, codec_name, "controls following fields", 1, 1 + child_max, children), []
|
|
271
271
|
|
|
272
|
+
if codec_name == "variable_len_array_optional":
|
|
273
|
+
array_params = dict(params)
|
|
274
|
+
array_params["codec"] = "variable_len_array"
|
|
275
|
+
array_report, issues = cls._inspect_array(array_params, path)
|
|
276
|
+
if issues:
|
|
277
|
+
return None, issues
|
|
278
|
+
children, issues, _child_min, child_max = cls._inspect_fields(params["target_fields"], path)
|
|
279
|
+
if issues:
|
|
280
|
+
return None, issues
|
|
281
|
+
try:
|
|
282
|
+
field_codec = ltcodecs.VariableLenArrayOptionalCodec(**params)
|
|
283
|
+
except Exception as err:
|
|
284
|
+
return None, [ValidationIssue(path, str(err))]
|
|
285
|
+
if field_codec.array_codec.min_length <= field_codec.target_fields_length_threshold:
|
|
286
|
+
for child in children:
|
|
287
|
+
cls._make_conditional(child)
|
|
288
|
+
return FieldReport(
|
|
289
|
+
path, codec_name, "array controls following fields",
|
|
290
|
+
field_codec.min_length_bits, field_codec.max_length_bits,
|
|
291
|
+
array_report.children + children,
|
|
292
|
+
), []
|
|
293
|
+
|
|
272
294
|
if codec_name in ("variable_len_array", "fixed_len_array"):
|
|
273
295
|
return cls._inspect_array(params, path)
|
|
274
296
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Variable-length arrays that control optional fields."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
|
|
7
|
+
from bitstring import BitArray, ConstBitStream
|
|
8
|
+
|
|
9
|
+
import ltcodecs as ltcodecs
|
|
10
|
+
|
|
11
|
+
from .exceptions import EncodingFailed
|
|
12
|
+
from .field_codec import encode_configured_field
|
|
13
|
+
from .multiple_field_codec import MultipleFieldCodec
|
|
14
|
+
from .variable_len_array_codec import VariableLenArrayCodec
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VariableLenArrayOptionalCodec(MultipleFieldCodec):
|
|
18
|
+
"""Encode an array, followed by target fields when its length exceeds a threshold.
|
|
19
|
+
|
|
20
|
+
Array parameters match :class:`VariableLenArrayCodec`. The default
|
|
21
|
+
``target_fields_length_threshold`` of zero includes targets for any
|
|
22
|
+
nonempty encoded array. A target field's optional ``value`` parameter
|
|
23
|
+
supplies its decoded value when the array omits that target.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self, element_type: str, max_length: int, target_fields: dict,
|
|
28
|
+
target_fields_length_threshold: int = 0, **kwargs,
|
|
29
|
+
) -> None:
|
|
30
|
+
if not isinstance(target_fields_length_threshold, int) or not 0 <= target_fields_length_threshold < max_length:
|
|
31
|
+
raise ValueError("target_fields_length_threshold must be an integer from 0 to max_length - 1")
|
|
32
|
+
if kwargs.get("length_mode") == "fill_outer":
|
|
33
|
+
raise ValueError("VariableLenArrayOptionalCodec requires an encoded array length")
|
|
34
|
+
|
|
35
|
+
self.array_codec = VariableLenArrayCodec(
|
|
36
|
+
element_type=element_type, max_length=max_length, **kwargs
|
|
37
|
+
)
|
|
38
|
+
self.target_fields = target_fields or {}
|
|
39
|
+
self.target_fields_length_threshold = target_fields_length_threshold
|
|
40
|
+
self.absent_values = {
|
|
41
|
+
field_name: field_params["value"]
|
|
42
|
+
for field_name, field_params in self.target_fields.items()
|
|
43
|
+
if "value" in field_params
|
|
44
|
+
}
|
|
45
|
+
self.field_codecs = {}
|
|
46
|
+
for field_name, field_params in self.target_fields.items():
|
|
47
|
+
try:
|
|
48
|
+
self.field_codecs[field_name] = ltcodecs.field_codec_classes[field_params["codec"]](**field_params)
|
|
49
|
+
except KeyError as err:
|
|
50
|
+
raise KeyError(
|
|
51
|
+
f"Error parsing codec config for {field_name}. Got params:\n{field_params}\nError: {err}"
|
|
52
|
+
) from err
|
|
53
|
+
|
|
54
|
+
def encode_multiple(self, value: list, message_dict: dict) -> tuple[BitArray, list, dict]:
|
|
55
|
+
bits, encoded_array = self.array_codec.encode(value)
|
|
56
|
+
if len(encoded_array) <= self.target_fields_length_threshold:
|
|
57
|
+
return bits, encoded_array, deepcopy(self.absent_values)
|
|
58
|
+
|
|
59
|
+
encoded_fields = {}
|
|
60
|
+
for field_name in self.target_fields:
|
|
61
|
+
field_codec = self.field_codecs[field_name]
|
|
62
|
+
try:
|
|
63
|
+
if isinstance(field_codec, MultipleFieldCodec):
|
|
64
|
+
field_bits, encoded_fields[field_name], extra_fields = field_codec.encode_multiple(
|
|
65
|
+
message_dict[field_name], message_dict
|
|
66
|
+
)
|
|
67
|
+
encoded_fields.update(extra_fields)
|
|
68
|
+
else:
|
|
69
|
+
field_bits, encoded_fields[field_name] = encode_configured_field(
|
|
70
|
+
field_codec, message_dict, field_name
|
|
71
|
+
)
|
|
72
|
+
bits.append(field_bits)
|
|
73
|
+
except Exception as err:
|
|
74
|
+
raise EncodingFailed(f'Error encoding target field "{field_name}"') from err
|
|
75
|
+
return bits, encoded_array, encoded_fields
|
|
76
|
+
|
|
77
|
+
def decode_multiple(self, bits_to_decode: ConstBitStream) -> tuple[list, dict]:
|
|
78
|
+
decoded_array = self.array_codec.decode(bits_to_decode)
|
|
79
|
+
if len(decoded_array) <= self.target_fields_length_threshold:
|
|
80
|
+
return decoded_array, deepcopy(self.absent_values)
|
|
81
|
+
|
|
82
|
+
decoded_fields = {}
|
|
83
|
+
for field_name in self.target_fields:
|
|
84
|
+
field_codec = self.field_codecs[field_name]
|
|
85
|
+
if isinstance(field_codec, MultipleFieldCodec):
|
|
86
|
+
decoded_fields[field_name], extra_fields = field_codec.decode_multiple(bits_to_decode)
|
|
87
|
+
decoded_fields.update(extra_fields)
|
|
88
|
+
else:
|
|
89
|
+
decoded_fields[field_name] = field_codec.decode(bits_to_decode)
|
|
90
|
+
return decoded_array, decoded_fields
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def min_length_bits(self) -> int:
|
|
94
|
+
target_bits = 0
|
|
95
|
+
if self.array_codec.min_length > self.target_fields_length_threshold:
|
|
96
|
+
target_bits = sum(codec.min_length_bits for codec in self.field_codecs.values())
|
|
97
|
+
return self.array_codec.min_length_bits + target_bits
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def max_length_bits(self) -> int:
|
|
101
|
+
return self.array_codec.max_length_bits + sum(
|
|
102
|
+
codec.max_length_bits for codec in self.field_codecs.values()
|
|
103
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ltcodecs
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.5.1
|
|
4
4
|
Summary: LT Codecs: Lightweight codecs for use over low-throughput links
|
|
5
5
|
Author: Woods Hole Oceanographic Institution, Acoustic Communications Group
|
|
6
6
|
License: LGPL-3.0-or-later
|
|
@@ -16,7 +16,7 @@ Requires-Python: >=3.6
|
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: armw
|
|
19
|
-
Requires-Dist: bitstring
|
|
19
|
+
Requires-Dist: bitstring<5,>=4
|
|
20
20
|
Requires-Dist: msgpack
|
|
21
21
|
Requires-Dist: crccheck
|
|
22
22
|
Requires-Dist: rospy-yaml-include
|
|
@@ -48,6 +48,7 @@ src/ltcodecs/rostime_codec.py
|
|
|
48
48
|
src/ltcodecs/string_codecs.py
|
|
49
49
|
src/ltcodecs/string_enum_codec.py
|
|
50
50
|
src/ltcodecs/variable_len_array_codec.py
|
|
51
|
+
src/ltcodecs/variable_len_array_optional_codec.py
|
|
51
52
|
src/ltcodecs/variable_len_bit_array_codec.py
|
|
52
53
|
src/ltcodecs/varint_codec.py
|
|
53
54
|
src/ltcodecs.egg-info/PKG-INFO
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"src/ltcodecs/string_codecs.py",
|
|
51
51
|
"src/ltcodecs/string_enum_codec.py",
|
|
52
52
|
"src/ltcodecs/variable_len_array_codec.py",
|
|
53
|
+
"src/ltcodecs/variable_len_array_optional_codec.py",
|
|
53
54
|
"src/ltcodecs/variable_len_bit_array_codec.py",
|
|
54
55
|
"src/ltcodecs/varint_codec.py",
|
|
55
56
|
"tests/__init__.py",
|
|
@@ -600,6 +600,71 @@ class TestCodecs:
|
|
|
600
600
|
assert isinstance(decoded["nested"], String)
|
|
601
601
|
assert decoded["nested"].data == msg.data
|
|
602
602
|
|
|
603
|
+
def test_variable_len_array_optional_length_controls_targets(self) -> None:
|
|
604
|
+
# An empty array carries no target bits; a nonempty array carries both targets.
|
|
605
|
+
codec = ltcodecs.VariableLenArrayOptionalCodec(
|
|
606
|
+
element_type="uint8",
|
|
607
|
+
max_length=3,
|
|
608
|
+
target_fields={
|
|
609
|
+
"sample_time": {"codec": "uint8", "value": 0},
|
|
610
|
+
"quality": {"codec": "uint8"},
|
|
611
|
+
},
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
empty_bits, empty_array, empty_fields = codec.encode_multiple([], {})
|
|
615
|
+
decoded_array, decoded_fields = codec.decode_multiple(ConstBitStream(empty_bits))
|
|
616
|
+
assert empty_array == decoded_array == []
|
|
617
|
+
assert empty_fields == decoded_fields == {"sample_time": 0}
|
|
618
|
+
assert len(empty_bits) == len(codec.array_codec.encode([])[0])
|
|
619
|
+
|
|
620
|
+
source = {"sample_time": 37, "quality": 5}
|
|
621
|
+
present_bits, present_array, present_fields = codec.encode_multiple([9], source)
|
|
622
|
+
decoded_array, decoded_fields = codec.decode_multiple(ConstBitStream(present_bits))
|
|
623
|
+
assert present_array == decoded_array == [9]
|
|
624
|
+
assert present_fields == decoded_fields == source
|
|
625
|
+
assert len(present_bits) == len(codec.array_codec.encode([9])[0]) + 16
|
|
626
|
+
|
|
627
|
+
def test_variable_len_array_optional_copies_absent_value(self) -> None:
|
|
628
|
+
# Mutating one decoded fallback must not change later decoded messages.
|
|
629
|
+
codec = ltcodecs.VariableLenArrayOptionalCodec(
|
|
630
|
+
element_type="uint8",
|
|
631
|
+
max_length=2,
|
|
632
|
+
target_fields={
|
|
633
|
+
"details": {
|
|
634
|
+
"codec": "dict",
|
|
635
|
+
"fields": {"count": {"codec": "uint8"}},
|
|
636
|
+
"value": {"count": 0},
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
bits, _, _ = codec.encode_multiple([], {})
|
|
642
|
+
first = codec.decode_multiple(ConstBitStream(bits))[1]
|
|
643
|
+
first["details"]["count"] = 99
|
|
644
|
+
second = codec.decode_multiple(ConstBitStream(bits))[1]
|
|
645
|
+
assert second == {"details": {"count": 0}}
|
|
646
|
+
|
|
647
|
+
def test_variable_len_array_optional_absent_value_matches_checksum(self) -> None:
|
|
648
|
+
# The checksum must use the same fallback or wire value that decode returns.
|
|
649
|
+
codec = ltcodecs.DictCodec(
|
|
650
|
+
{
|
|
651
|
+
"samples": {
|
|
652
|
+
"codec": "variable_len_array_optional",
|
|
653
|
+
"element_type": "uint8",
|
|
654
|
+
"max_length": 2,
|
|
655
|
+
"target_fields": {"sample_time": {"codec": "uint8", "value": 0}},
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
checksum="crc8",
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
for source, expected in (
|
|
662
|
+
({"samples": []}, {"samples": [], "sample_time": 0}),
|
|
663
|
+
({"samples": [9], "sample_time": 37}, {"samples": [9], "sample_time": 37}),
|
|
664
|
+
):
|
|
665
|
+
bits, _ = codec.encode(source)
|
|
666
|
+
assert codec.decode(ConstBitStream(bits)) == expected
|
|
667
|
+
|
|
603
668
|
def test_ros_message_field_codec(self) -> None:
|
|
604
669
|
"""test functionality of the ros message field codec"""
|
|
605
670
|
|
|
@@ -184,3 +184,64 @@ def test_dict_codec_variable_length_bit_array_derives_bit_length() -> None:
|
|
|
184
184
|
decoded_msg = codec.decode(ConstBitStream(encoded_msg[0]))
|
|
185
185
|
|
|
186
186
|
assert decoded_msg == {"payload": [0xAB, 0xC0], "payload_size_in_bits": 13}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_variable_len_array_optional_omits_targets_for_empty_array() -> None:
|
|
190
|
+
# At the dictionary boundary, target fields appear only for nonempty arrays.
|
|
191
|
+
codec = ltcodecs.DictCodec({
|
|
192
|
+
"samples": {
|
|
193
|
+
"codec": "variable_len_array_optional",
|
|
194
|
+
"element_type": "uint8",
|
|
195
|
+
"max_length": 4,
|
|
196
|
+
"target_fields": {"sample_time": {"codec": "uint32"}},
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
for source in ({"samples": []}, {"samples": [3, 5], "sample_time": 123}):
|
|
201
|
+
bits, encoded = codec.root_field_codec.encode(source)
|
|
202
|
+
assert encoded == source
|
|
203
|
+
assert codec.decode(ConstBitStream(bits)) == source
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_variable_len_array_optional_uses_encoded_length_and_threshold() -> None:
|
|
207
|
+
# With a threshold of one, one element omits targets and two include them.
|
|
208
|
+
codec = ltcodecs.DictCodec({
|
|
209
|
+
"samples": {
|
|
210
|
+
"codec": "variable_len_array_optional",
|
|
211
|
+
"element_type": "uint8",
|
|
212
|
+
"max_length": 2,
|
|
213
|
+
"target_fields_length_threshold": 1,
|
|
214
|
+
"target_fields": {"sample_time": {"codec": "uint32"}},
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
for source in ({"samples": [3]}, {"samples": [3, 5], "sample_time": 123}):
|
|
219
|
+
bits, encoded = codec.root_field_codec.encode(source)
|
|
220
|
+
assert codec.decode(ConstBitStream(bits)) == encoded
|
|
221
|
+
|
|
222
|
+
# Truncation to max_length determines the array length used on the wire.
|
|
223
|
+
bits, encoded = codec.root_field_codec.encode({"samples": [3, 5, 7], "sample_time": 123})
|
|
224
|
+
assert encoded == {"samples": [3, 5], "sample_time": 123}
|
|
225
|
+
assert codec.decode(ConstBitStream(bits)) == encoded
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_variable_len_array_optional_supports_nested_target_controller() -> None:
|
|
229
|
+
# A present target can itself control another optional field.
|
|
230
|
+
codec = ltcodecs.DictCodec({
|
|
231
|
+
"samples": {
|
|
232
|
+
"codec": "variable_len_array_optional",
|
|
233
|
+
"element_type": "uint8",
|
|
234
|
+
"max_length": 2,
|
|
235
|
+
"target_fields": {
|
|
236
|
+
"has_extra": {
|
|
237
|
+
"codec": "optional",
|
|
238
|
+
"target_fields": {"extra": {"codec": "uint8"}},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
source = {"samples": [1], "has_extra": True, "extra": 9}
|
|
244
|
+
|
|
245
|
+
bits, encoded = codec.root_field_codec.encode(source)
|
|
246
|
+
assert encoded == source
|
|
247
|
+
assert codec.decode(ConstBitStream(bits)) == source
|
|
@@ -31,6 +31,38 @@ def test_inspect_fields_reports_nested_and_optional_sizes() -> None:
|
|
|
31
31
|
assert (optional_target.min_length_bits, optional_target.max_length_bits) == (0, 8)
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def test_inspect_array_controlled_fields() -> None:
|
|
35
|
+
# With an empty array allowed, the target contributes zero minimum bits.
|
|
36
|
+
report = CodecInspector.inspect_fields({
|
|
37
|
+
"samples": {
|
|
38
|
+
"codec": "variable_len_array_optional",
|
|
39
|
+
"element_type": "uint8",
|
|
40
|
+
"max_length": 3,
|
|
41
|
+
"target_fields": {"sample_time": {"codec": "uint32"}},
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
assert report.is_valid
|
|
46
|
+
assert report.fields[0].children[0].path == "samples[]"
|
|
47
|
+
assert report.fields[0].children[1].path == "samples.sample_time"
|
|
48
|
+
assert report.fields[0].children[1].min_length_bits == 0
|
|
49
|
+
assert report.max_length_bits == 58
|
|
50
|
+
|
|
51
|
+
# Requiring an element makes the target mandatory in the size report.
|
|
52
|
+
required = CodecInspector.inspect_fields({
|
|
53
|
+
"samples": {
|
|
54
|
+
"codec": "variable_len_array_optional",
|
|
55
|
+
"element_type": "uint8",
|
|
56
|
+
"max_length": 3,
|
|
57
|
+
"min_length": 1,
|
|
58
|
+
"target_fields": {"sample_time": {"codec": "uint32"}},
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
assert required.is_valid
|
|
62
|
+
assert required.fields[0].children[1].min_length_bits == 32
|
|
63
|
+
assert required.min_length_bits == 42
|
|
64
|
+
|
|
65
|
+
|
|
34
66
|
def test_inspect_file_resolves_include_directory(tmp_path: Path) -> None:
|
|
35
67
|
include_directory = tmp_path / "includes"
|
|
36
68
|
include_directory.mkdir()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|