jamcodec 0.1.3__tar.gz → 0.1.5__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.
Files changed (38) hide show
  1. {jamcodec-0.1.3 → jamcodec-0.1.5}/PKG-INFO +1 -1
  2. {jamcodec-0.1.3 → jamcodec-0.1.5}/jamcodec/base.py +7 -0
  3. {jamcodec-0.1.3 → jamcodec-0.1.5}/jamcodec/types.py +63 -2
  4. {jamcodec-0.1.3 → jamcodec-0.1.5}/jamcodec.egg-info/PKG-INFO +1 -1
  5. {jamcodec-0.1.3 → jamcodec-0.1.5}/jamcodec.egg-info/SOURCES.txt +1 -0
  6. jamcodec-0.1.5/test/test_bounded_vec.py +191 -0
  7. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_map.py +19 -0
  8. {jamcodec-0.1.3 → jamcodec-0.1.5}/LICENSE +0 -0
  9. {jamcodec-0.1.3 → jamcodec-0.1.5}/README.md +0 -0
  10. {jamcodec-0.1.3 → jamcodec-0.1.5}/jamcodec/__init__.py +0 -0
  11. {jamcodec-0.1.3 → jamcodec-0.1.5}/jamcodec/constants.py +0 -0
  12. {jamcodec-0.1.3 → jamcodec-0.1.5}/jamcodec/exceptions.py +0 -0
  13. {jamcodec-0.1.3 → jamcodec-0.1.5}/jamcodec/mixins.py +0 -0
  14. {jamcodec-0.1.3 → jamcodec-0.1.5}/jamcodec.egg-info/dependency_links.txt +0 -0
  15. {jamcodec-0.1.3 → jamcodec-0.1.5}/jamcodec.egg-info/requires.txt +0 -0
  16. {jamcodec-0.1.3 → jamcodec-0.1.5}/jamcodec.egg-info/top_level.txt +0 -0
  17. {jamcodec-0.1.3 → jamcodec-0.1.5}/pyproject.toml +0 -0
  18. {jamcodec-0.1.3 → jamcodec-0.1.5}/requirements.txt +0 -0
  19. {jamcodec-0.1.3 → jamcodec-0.1.5}/setup.cfg +0 -0
  20. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_array.py +0 -0
  21. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_bitarray.py +0 -0
  22. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_bitvec.py +0 -0
  23. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_boolean.py +0 -0
  24. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_bytes.py +0 -0
  25. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_compact.py +0 -0
  26. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_enum.py +0 -0
  27. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_float.py +0 -0
  28. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_hashmap.py +0 -0
  29. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_integer.py +0 -0
  30. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_mixins.py +0 -0
  31. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_option.py +0 -0
  32. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_scale_types.py +0 -0
  33. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_scalebytes.py +0 -0
  34. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_string.py +0 -0
  35. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_struct.py +0 -0
  36. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_tuple.py +0 -0
  37. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_varint.py +0 -0
  38. {jamcodec-0.1.3 → jamcodec-0.1.5}/test/test_vec.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jamcodec
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Python JAM Codec Library
5
5
  Author-email: JAMdot Technologies <dev@jamdot.tech>
6
6
  Project-URL: Homepage, https://github.com/JAMdotTech/py-jam-codec
@@ -115,6 +115,13 @@ class JamBytes:
115
115
  return False
116
116
  return self.data == other.data
117
117
 
118
+
119
+ def __lt__(self, other):
120
+ return self.data < other.data
121
+
122
+ def __gt__(self, other):
123
+ return self.data > other.data
124
+
118
125
  def __len__(self):
119
126
  return len(self.data)
120
127
 
@@ -675,6 +675,67 @@ class Vec(JamCodecTypeDef):
675
675
  return [self.type_def.example_value()]
676
676
 
677
677
 
678
+ class BoundedVec(Vec):
679
+ """A Vec with inclusive cardinality bounds and a canonical length prefix.
680
+
681
+ ``multiple`` requires the element count to be divisible by that value.
682
+ Decoding checks the count before constructing or decoding any element.
683
+ Like Vec(U8), BoundedVec(U8, ...) represents bytes; bounds count bytes,
684
+ including when encoding or deserializing a hex or UTF-8 string.
685
+ """
686
+
687
+ def __init__(self, type_def: JamCodecTypeDef, maximum: int, minimum: int = 0, multiple: int = 1):
688
+ if any(type(bound) is not int for bound in (maximum, minimum, multiple)):
689
+ raise ValueError('Sequence bounds and multiple must be integers')
690
+ if not 0 <= minimum <= maximum or multiple <= 0:
691
+ raise ValueError('Expected 0 <= minimum <= maximum and multiple > 0')
692
+ super().__init__(type_def)
693
+ self.maximum = maximum
694
+ self.minimum = minimum
695
+ self.multiple = multiple
696
+
697
+ def _check_count(self, count: int) -> None:
698
+ if not self.minimum <= count <= self.maximum or count % self.multiple:
699
+ raise ValueError(
700
+ f'invalid sequence length {count}: expected {self.minimum}..'
701
+ f'{self.maximum} in multiples of {self.multiple}'
702
+ )
703
+
704
+ def decode(self, data: JamBytes) -> Union[list, bytes]:
705
+ start = data.offset
706
+ count = VarInt64.decode(data)
707
+ if data.data[start:data.offset] != VarInt64.encode(count).data:
708
+ raise ValueError('non-canonical sequence length')
709
+ self._check_count(count)
710
+
711
+ if self.type_def is U8:
712
+ return bytes(data.get_next_bytes(count))
713
+
714
+ values = []
715
+ for _ in range(count):
716
+ item = self.type_def.new()
717
+ item.decode(data)
718
+ values.append(item)
719
+ return values
720
+
721
+ def encode(self, value: Union[list, bytes, bytearray, str]) -> JamBytes:
722
+ if self.type_def is U8:
723
+ value = Bytes.deserialize(value)
724
+ self._check_count(len(value))
725
+ return super().encode(value)
726
+
727
+ def deserialize(self, value: Union[list, bytes, bytearray, str]) -> Union[list, bytes]:
728
+ if self.type_def is U8:
729
+ value = Bytes.deserialize(value)
730
+ self._check_count(len(value))
731
+ return super().deserialize(value)
732
+
733
+ def to_serializable_obj(self, value_object: Union[list, bytes]):
734
+ if self.type_def is U8:
735
+ return value_object
736
+ return super().to_serializable_obj(value_object)
737
+
738
+
678
739
  class BitVec(JamCodecTypeDef):
679
740
  """
680
741
  A BitVec that represents an array of bits. The bits are however stored encoded. The difference between this
@@ -977,8 +1038,8 @@ class Map(JamCodecTypeDef):
977
1038
  # Convert to list
978
1039
  value = value.items()
979
1040
 
980
- # Sort Map by keys
981
- value = sorted(value, key=lambda x: x[0])
1041
+ # Sort Map by keys and then values
1042
+ value = sorted(value)
982
1043
 
983
1044
  # Encode length of Vec
984
1045
  data = VarInt64.encode(len(value))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jamcodec
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Python JAM Codec Library
5
5
  Author-email: JAMdot Technologies <dev@jamdot.tech>
6
6
  Project-URL: Homepage, https://github.com/JAMdotTech/py-jam-codec
@@ -17,6 +17,7 @@ test/test_array.py
17
17
  test/test_bitarray.py
18
18
  test/test_bitvec.py
19
19
  test/test_boolean.py
20
+ test/test_bounded_vec.py
20
21
  test/test_bytes.py
21
22
  test/test_compact.py
22
23
  test/test_enum.py
@@ -0,0 +1,191 @@
1
+ import unittest
2
+ from dataclasses import dataclass, field
3
+
4
+ from jamcodec.base import JamBytes, JamCodecTypeDef
5
+ from jamcodec.exceptions import RemainingScaleBytesNotEmptyException
6
+ from jamcodec.mixins import Serializable
7
+ from jamcodec.types import BoundedVec, U8, U16, VarInt64, Vec
8
+
9
+
10
+ class NoElementAllocation(JamCodecTypeDef):
11
+ def new(self, **kwargs):
12
+ raise AssertionError('Invalid length must be rejected before creating elements')
13
+
14
+
15
+ @dataclass
16
+ class BoundedItem(Serializable):
17
+ number: int = field(metadata={'codec': U16})
18
+
19
+
20
+ @dataclass
21
+ class BoundedPacket(Serializable):
22
+ items: list = field(metadata={'codec': BoundedVec(BoundedItem.to_codec_def(), 2, minimum=1)})
23
+ groups: list = field(metadata={'codec': BoundedVec(BoundedVec(U16, 2), 2)})
24
+ payload: bytes = field(metadata={'codec': BoundedVec(U8, 4, minimum=2, multiple=2)})
25
+
26
+
27
+ class TestBoundedVec(unittest.TestCase):
28
+
29
+ def test_invalid_constructor_constraints(self):
30
+ invalid = (
31
+ {'maximum': -1},
32
+ {'maximum': 2, 'minimum': -1},
33
+ {'maximum': 2, 'minimum': 3},
34
+ {'maximum': 2, 'multiple': 0},
35
+ {'maximum': 2, 'multiple': -1},
36
+ {'maximum': 2.0},
37
+ {'maximum': 2, 'minimum': '0'},
38
+ {'maximum': 2, 'multiple': 1.0},
39
+ {'maximum': True},
40
+ {'maximum': 2, 'minimum': False},
41
+ {'maximum': 2, 'multiple': True},
42
+ )
43
+ for constraints in invalid:
44
+ with self.subTest(constraints=constraints):
45
+ with self.assertRaises(ValueError):
46
+ BoundedVec(U16, **constraints)
47
+
48
+ def test_inclusive_bounds_and_multiple_roundtrip(self):
49
+ codec = BoundedVec(U16, maximum=6, minimum=2, multiple=2)
50
+ for count in (2, 4, 6):
51
+ with self.subTest(count=count):
52
+ value = list(range(count))
53
+ obj = codec.new()
54
+ encoded = obj.encode(value)
55
+ self.assertEqual(value, codec.new().decode(encoded))
56
+ self.assertEqual(value, codec.new(value=value).serialize())
57
+
58
+ def test_empty_vector_with_default_minimum(self):
59
+ codec = BoundedVec(U16, maximum=0)
60
+ self.assertEqual(JamBytes('0x00'), codec.encode([]))
61
+ self.assertEqual([], codec.deserialize([]))
62
+ self.assertEqual([], codec.decode(JamBytes('0x00')))
63
+
64
+ def test_encode_rejects_invalid_counts(self):
65
+ codec = BoundedVec(U16, maximum=4, minimum=2, multiple=2)
66
+ for count in (0, 1, 3, 5, 6):
67
+ with self.subTest(count=count):
68
+ with self.assertRaises(ValueError):
69
+ codec.encode([0] * count)
70
+
71
+ def test_deserialize_rejects_invalid_counts(self):
72
+ codec = BoundedVec(U16, maximum=4, minimum=2, multiple=2)
73
+ for count in (0, 1, 3, 5, 6):
74
+ with self.subTest(count=count):
75
+ with self.assertRaises(ValueError):
76
+ codec.new(value=[0] * count)
77
+
78
+ def test_decode_checks_count_before_element_allocation(self):
79
+ codec = BoundedVec(NoElementAllocation(), maximum=4, minimum=2, multiple=2)
80
+ for count in (0, 1, 3, 5, 6):
81
+ with self.subTest(count=count):
82
+ with self.assertRaises(ValueError):
83
+ codec.decode(VarInt64.encode(count))
84
+
85
+ def test_huge_count_is_rejected_before_element_allocation(self):
86
+ codec = BoundedVec(NoElementAllocation(), maximum=4)
87
+ data = VarInt64.encode(2 ** 64 - 1)
88
+ with self.assertRaises(ValueError):
89
+ codec.decode(data)
90
+ self.assertEqual(data.length, data.offset)
91
+
92
+ def test_noncanonical_prefix_at_nonzero_offset(self):
93
+ codec = BoundedVec(NoElementAllocation(), maximum=128)
94
+ for prefix in (b'\x80\x00', b'\x80\x01', b'\x80\x7f', b'\xc0\x80\x00', b'\xff' + bytes(8)):
95
+ with self.subTest(prefix=prefix.hex()):
96
+ data = JamBytes(b'\x99' + prefix + b'\x42')
97
+ data.offset = 1
98
+ with self.assertRaises(ValueError):
99
+ codec.decode(data)
100
+ self.assertEqual(1 + len(prefix), data.offset)
101
+
102
+ def test_truncated_prefix_at_nonzero_offset(self):
103
+ codec = BoundedVec(NoElementAllocation(), maximum=128)
104
+ prefixes = [b''] + [bytes([prefix]) + bytes(length - 1) for prefix, length in (
105
+ (0x80, 1), (0xc0, 2), (0xe0, 3), (0xf0, 4),
106
+ (0xf8, 5), (0xfc, 6), (0xfe, 7), (0xff, 8),
107
+ )]
108
+ for prefix in prefixes:
109
+ with self.subTest(prefix=prefix.hex()):
110
+ data = JamBytes(b'\x99' + prefix)
111
+ data.offset = 1
112
+ with self.assertRaises(RemainingScaleBytesNotEmptyException):
113
+ codec.decode(data)
114
+
115
+ def test_wire_encoding_matches_vec_across_prefix_boundary(self):
116
+ codec = BoundedVec(U16, maximum=128)
117
+ for count in (0, 1, 127, 128):
118
+ with self.subTest(count=count):
119
+ value = list(range(count))
120
+ encoded = codec.encode(value)
121
+ self.assertEqual(Vec(U16).encode(value), encoded)
122
+ self.assertEqual(value, codec.new().decode(encoded))
123
+
124
+ def test_new_preserves_vec_wrapper_semantics(self):
125
+ codec = BoundedVec(U16, maximum=2)
126
+ self.assertIsInstance(codec, Vec)
127
+ obj = codec.new(value=[1, 258])
128
+ self.assertEqual(2, len(obj))
129
+ self.assertEqual([1, 258], [item.value for item in obj.value_object])
130
+ encoded = obj.encode()
131
+ self.assertEqual(JamBytes('0x0201000201'), encoded)
132
+ decoded = codec.new(scale=encoded)
133
+ self.assertEqual([1, 258], decoded.value)
134
+ self.assertEqual([1, 258], decoded.to_serializable_obj())
135
+
136
+ def test_decode_consumes_only_vector_at_nonzero_offset(self):
137
+ data = JamBytes('0x99020100020142')
138
+ data.offset = 1
139
+ self.assertEqual([1, 258], BoundedVec(U16, 2).new().decode(data))
140
+ self.assertEqual(6, data.offset)
141
+ self.assertEqual(bytearray(b'\x42'), data.get_remaining_bytes())
142
+
143
+ def test_byte_inputs_use_normalized_cardinality(self):
144
+ codec = BoundedVec(U8, maximum=2, minimum=2, multiple=2)
145
+ for value in (b'\xc3\xa9', bytearray(b'\xc3\xa9'), [195, 169], '0xc3a9', 'é'):
146
+ with self.subTest(value=value):
147
+ self.assertEqual(JamBytes('0x02c3a9'), codec.encode(value))
148
+ self.assertEqual(b'\xc3\xa9', codec.deserialize(value))
149
+
150
+ def test_byte_bounds_reject_normalized_lengths(self):
151
+ codec = BoundedVec(U8, maximum=2, minimum=2, multiple=2)
152
+ for value in ('0x00', '0x000102', 'a', 'éa'):
153
+ for operation in (codec.encode, codec.deserialize):
154
+ with self.subTest(value=value, operation=operation.__name__):
155
+ with self.assertRaises(ValueError):
156
+ operation(value)
157
+
158
+ def test_bytes_decode_and_wrapper_match_vec(self):
159
+ codec = BoundedVec(U8, maximum=2)
160
+ self.assertEqual(b'\xc3\xa9', codec.decode(JamBytes('0x02c3a9')))
161
+ obj = codec.new(scale=JamBytes('0x02c3a9'))
162
+ self.assertEqual(Vec(U8).new().decode(JamBytes('0x02c3a9')), obj.value)
163
+ self.assertEqual('0xc3a9', obj.value)
164
+ self.assertEqual(b'\xc3\xa9', obj.value_object)
165
+ self.assertEqual(b'\xc3\xa9', obj.to_serializable_obj())
166
+ self.assertEqual(JamBytes('0x02c3a9'), obj.encode())
167
+
168
+ def test_truncated_element_payload(self):
169
+ for element, payload in ((U16, b'\x02\x01\x00\x02'), (U8, b'\x02\x01')):
170
+ with self.subTest(element=element):
171
+ with self.assertRaises(RemainingScaleBytesNotEmptyException):
172
+ BoundedVec(element, maximum=2, minimum=1).new().decode(JamBytes(payload))
173
+
174
+ def test_nested_serializable_dataclass_roundtrip(self):
175
+ packet = BoundedPacket(
176
+ items=[BoundedItem(1), BoundedItem(258)],
177
+ groups=[[3, 4], []],
178
+ payload=b'\xc3\xa9',
179
+ )
180
+ serialized = {
181
+ 'items': [{'number': 1}, {'number': 258}],
182
+ 'groups': [[3, 4], []],
183
+ 'payload': '0xc3a9',
184
+ }
185
+ self.assertEqual(serialized, packet.serialize())
186
+ self.assertEqual(packet, BoundedPacket.deserialize(serialized))
187
+ self.assertEqual(packet, BoundedPacket.from_jam_bytes(packet.to_jam_bytes()))
188
+
189
+
190
+ if __name__ == '__main__':
191
+ unittest.main()
@@ -56,6 +56,25 @@ class TestMap(unittest.TestCase):
56
56
  data
57
57
  )
58
58
 
59
+ def test_map_encode_sort_key_values(self):
60
+ obj = Map(U32, U32).new()
61
+ value = [
62
+ (2, 1),
63
+ (2, 2),
64
+ (1, 2),
65
+ (1, 1)
66
+ ]
67
+
68
+ data = obj.encode(value)
69
+
70
+ self.assertEqual(
71
+ JamBytes(
72
+ "0x040100000001000000010000000200000002000000010000000200000002000000"
73
+ ),
74
+ data
75
+ )
76
+
77
+
59
78
  def test_map_decode(self):
60
79
  obj = Map(U32, Bytes).new()
61
80
 
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