tsrkit-types 0.1.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.
- tsrkit_types/__init__.py +76 -0
- tsrkit_types/bits.py +115 -0
- tsrkit_types/bool.py +41 -0
- tsrkit_types/bytes.py +102 -0
- tsrkit_types/choice.py +127 -0
- tsrkit_types/dictionary.py +145 -0
- tsrkit_types/enum.py +128 -0
- tsrkit_types/integers.py +203 -0
- tsrkit_types/itf/codable.py +82 -0
- tsrkit_types/null.py +43 -0
- tsrkit_types/option.py +30 -0
- tsrkit_types/sequences.py +212 -0
- tsrkit_types/string.py +66 -0
- tsrkit_types/struct.py +82 -0
- tsrkit_types-0.1.0.dist-info/METADATA +750 -0
- tsrkit_types-0.1.0.dist-info/RECORD +19 -0
- tsrkit_types-0.1.0.dist-info/WHEEL +5 -0
- tsrkit_types-0.1.0.dist-info/licenses/LICENSE +21 -0
- tsrkit_types-0.1.0.dist-info/top_level.txt +1 -0
tsrkit_types/__init__.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# from jam.types.base.integers import (
|
|
2
|
+
# U8,
|
|
3
|
+
# U16,
|
|
4
|
+
# U32,
|
|
5
|
+
# U64,
|
|
6
|
+
# U128,
|
|
7
|
+
# U256,
|
|
8
|
+
# Int,
|
|
9
|
+
# )
|
|
10
|
+
# from jam.types.base.string import String
|
|
11
|
+
# from jam.types.base.null import Null, Nullable
|
|
12
|
+
# from jam.types.base.dictionary import Dictionary, decodable_dictionary
|
|
13
|
+
# from jam.types.base.boolean import Boolean
|
|
14
|
+
# from jam.types.base.bit import Bit
|
|
15
|
+
# from jam.types.base.composite import Choice, Option, decodable_choice, decodable_option
|
|
16
|
+
# from jam.types.base.sequences.array import Array, decodable_array
|
|
17
|
+
# from jam.types.base.sequences.vector import Vector, decodable_vector
|
|
18
|
+
# from jam.types.base.bytes.bytes import Bytes
|
|
19
|
+
# # from jam.types.base.sequences.bytes import BitArray, decodable_bit_array, Byte
|
|
20
|
+
# # from jam.types.base.bytes.byte_array import (
|
|
21
|
+
# # ByteArray8,
|
|
22
|
+
# # ByteArray16,
|
|
23
|
+
# # ByteArray32,
|
|
24
|
+
# # ByteArray64,
|
|
25
|
+
# # ByteArray96,
|
|
26
|
+
# # ByteArray128,
|
|
27
|
+
# # ByteArray144,
|
|
28
|
+
# # ByteArray256,
|
|
29
|
+
# # ByteArray784,
|
|
30
|
+
# # )
|
|
31
|
+
#
|
|
32
|
+
# __all__ = [
|
|
33
|
+
# # Integer types
|
|
34
|
+
# "Int",
|
|
35
|
+
# "U8",
|
|
36
|
+
# "U16",
|
|
37
|
+
# "U32",
|
|
38
|
+
# "U64",
|
|
39
|
+
# "U128",
|
|
40
|
+
# "U256",
|
|
41
|
+
# # Choice and Null types
|
|
42
|
+
# "Choice",
|
|
43
|
+
# "Option",
|
|
44
|
+
# "Null",
|
|
45
|
+
# "Nullable",
|
|
46
|
+
# # Dictionary type
|
|
47
|
+
# "Dictionary",
|
|
48
|
+
# # Boolean and Bit types
|
|
49
|
+
# "Boolean",
|
|
50
|
+
# "Bit",
|
|
51
|
+
# # String type
|
|
52
|
+
# "String",
|
|
53
|
+
# # Sequence types
|
|
54
|
+
# # "Array",
|
|
55
|
+
# # "Vector",
|
|
56
|
+
# # # Byte types
|
|
57
|
+
# # "ByteArray8",
|
|
58
|
+
# # "ByteArray16",
|
|
59
|
+
# # "ByteArray32",
|
|
60
|
+
# # "ByteArray64",
|
|
61
|
+
# # "ByteArray96",
|
|
62
|
+
# # "ByteArray128",
|
|
63
|
+
# # "ByteArray144",
|
|
64
|
+
# # "ByteArray256",
|
|
65
|
+
# # "ByteArray784",
|
|
66
|
+
# # "BitArray",
|
|
67
|
+
# # "Byte",
|
|
68
|
+
# "Bytes",
|
|
69
|
+
# # Decodable types
|
|
70
|
+
# "decodable_array",
|
|
71
|
+
# "decodable_bit_array",
|
|
72
|
+
# "decodable_vector",
|
|
73
|
+
# "decodable_dictionary",
|
|
74
|
+
# "decodable_choice",
|
|
75
|
+
# "decodable_option",
|
|
76
|
+
# ]
|
tsrkit_types/bits.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from typing import ClassVar, Sequence, Tuple, Union
|
|
2
|
+
|
|
3
|
+
from tsrkit_types.bytes import Bytes
|
|
4
|
+
from tsrkit_types.integers import Uint
|
|
5
|
+
from tsrkit_types.sequences import Seq
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Bits(Seq):
|
|
9
|
+
"""Bits[size, order]"""
|
|
10
|
+
_element_type = bool
|
|
11
|
+
_min_length: ClassVar[int] = 0
|
|
12
|
+
_max_length: ClassVar[int] = 2 ** 64
|
|
13
|
+
_order: ClassVar[str] = "msb"
|
|
14
|
+
|
|
15
|
+
def __class_getitem__(cls, params):
|
|
16
|
+
min_l, max_l, _bo = 0, 2**64, "msb"
|
|
17
|
+
if isinstance(params, tuple):
|
|
18
|
+
min_l, max_l, _bo = params[0], params[0], params[1]
|
|
19
|
+
else:
|
|
20
|
+
if isinstance(params, int):
|
|
21
|
+
min_l, max_l = params, params
|
|
22
|
+
else:
|
|
23
|
+
_bo = params
|
|
24
|
+
|
|
25
|
+
return type(cls.__class__.__name__, (cls,), {"_min_length": min_l, "_max_length": max_l, "_order": _bo})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------- #
|
|
29
|
+
# JSON Parse #
|
|
30
|
+
# ---------------------------------------------------------------------------- #
|
|
31
|
+
|
|
32
|
+
def to_json(self) -> str:
|
|
33
|
+
return Bytes.from_bits(self, bit_order=self._order).hex()
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_json(cls, json_str: str) -> "Bits":
|
|
37
|
+
return cls(Bytes.from_hex(json_str).to_bits(bit_order=cls._order))
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------- #
|
|
40
|
+
# Serialization #
|
|
41
|
+
# ---------------------------------------------------------------------------- #
|
|
42
|
+
|
|
43
|
+
def encode_size(self) -> int:
|
|
44
|
+
# Calculate the number of bytes needed
|
|
45
|
+
bit_enc = 0
|
|
46
|
+
if self._length is None:
|
|
47
|
+
bit_enc = Uint(len(self)).encode_size()
|
|
48
|
+
|
|
49
|
+
return bit_enc + ((len(self) + 7) // 8)
|
|
50
|
+
|
|
51
|
+
def encode_into(
|
|
52
|
+
self, buffer: bytearray, offset: int = 0
|
|
53
|
+
) -> int:
|
|
54
|
+
total_size = self.encode_size()
|
|
55
|
+
self._check_buffer_size(buffer, total_size, offset)
|
|
56
|
+
|
|
57
|
+
# Initialize all bytes to 0
|
|
58
|
+
for i in range(0, total_size):
|
|
59
|
+
buffer[offset + i] = 0
|
|
60
|
+
|
|
61
|
+
if self._length is None:
|
|
62
|
+
# Encode the bit length first
|
|
63
|
+
offset += Uint(len(self)).encode_into(buffer, offset)
|
|
64
|
+
else:
|
|
65
|
+
# Ensure bit length is size of value
|
|
66
|
+
if len(self) != self._length:
|
|
67
|
+
raise ValueError("Bit sequence length mismatch")
|
|
68
|
+
|
|
69
|
+
if not all(
|
|
70
|
+
isinstance(bit, (bool, int)) and bit in (0, 1, True, False)
|
|
71
|
+
for bit in self
|
|
72
|
+
):
|
|
73
|
+
raise ValueError(f"Bit sequence must contain only 0s and 1s, got an sequence of {self}")
|
|
74
|
+
|
|
75
|
+
buffer[offset : offset + total_size] = Bytes.from_bits(
|
|
76
|
+
self, bit_order=self._order
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return total_size
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def decode_from(
|
|
83
|
+
cls,
|
|
84
|
+
buffer: Union[bytes, bytearray, memoryview],
|
|
85
|
+
offset: int = 0,
|
|
86
|
+
) -> Tuple[Sequence[bool], int]:
|
|
87
|
+
"""
|
|
88
|
+
Decode bit sequence from buffer.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
buffer: Source buffer
|
|
92
|
+
offset: Starting offset
|
|
93
|
+
bit_length: Expected number of bits (required)
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Tuple of (decoded bit list, bytes read)
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
DecodeError: If buffer too small or bit_length not specified
|
|
100
|
+
"""
|
|
101
|
+
_len = cls._length
|
|
102
|
+
if _len is None:
|
|
103
|
+
# Assume first byte is the bit length
|
|
104
|
+
_len, size = Uint.decode_from(buffer, offset)
|
|
105
|
+
offset += size
|
|
106
|
+
|
|
107
|
+
if _len == 0:
|
|
108
|
+
return [], 0
|
|
109
|
+
|
|
110
|
+
# Calculate required bytes
|
|
111
|
+
byte_count = (_len + 7) // 8
|
|
112
|
+
cls._check_buffer_size(buffer, byte_count, offset)
|
|
113
|
+
|
|
114
|
+
result = Bytes(buffer[offset : offset + byte_count]).to_bits(bit_order=cls._order)
|
|
115
|
+
return cls(result), byte_count
|
tsrkit_types/bool.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Tuple, Union
|
|
2
|
+
from tsrkit_types.itf.codable import Codable
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Bool(Codable):
|
|
6
|
+
_value: bool
|
|
7
|
+
def __init__(self, value: bool):
|
|
8
|
+
self._value = value
|
|
9
|
+
|
|
10
|
+
def __bool__(self):
|
|
11
|
+
return bool(self._value)
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------- #
|
|
14
|
+
# Serialization #
|
|
15
|
+
# ---------------------------------------------------------------------------- #
|
|
16
|
+
|
|
17
|
+
def encode_size(self) -> int:
|
|
18
|
+
return 1
|
|
19
|
+
|
|
20
|
+
def encode_into(self, buffer: bytearray, offset: int = 0) -> int:
|
|
21
|
+
buffer[offset] = int(self._value)
|
|
22
|
+
return 1
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def decode_from(cls, buffer: Union[bytes, bytearray, memoryview], offset: int = 0) -> Tuple["Bool", int]:
|
|
26
|
+
return cls(bool(buffer[offset])), 1
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------- #
|
|
29
|
+
# JSON Parse #
|
|
30
|
+
# ---------------------------------------------------------------------------- #
|
|
31
|
+
|
|
32
|
+
def to_json(self) -> str:
|
|
33
|
+
return "true" if self._value else "false"
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_json(cls, json_str: str) -> "Bool":
|
|
37
|
+
if json_str == "true":
|
|
38
|
+
return cls(True)
|
|
39
|
+
if json_str == "false":
|
|
40
|
+
return cls(False)
|
|
41
|
+
raise ValueError("Invalid JSON string for Bool")
|
tsrkit_types/bytes.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from typing import Tuple, Union, ClassVar, Self
|
|
2
|
+
import base64
|
|
3
|
+
|
|
4
|
+
from tsrkit_types.integers import Uint
|
|
5
|
+
from tsrkit_types.itf.codable import Codable
|
|
6
|
+
|
|
7
|
+
class Bytes(bytes, Codable):
|
|
8
|
+
|
|
9
|
+
_length: ClassVar[Union[None, int]] = None
|
|
10
|
+
|
|
11
|
+
def __class_getitem__(cls, params):
|
|
12
|
+
_len = None
|
|
13
|
+
name = cls.__class__.__name__
|
|
14
|
+
if params and params > 0:
|
|
15
|
+
_len = params
|
|
16
|
+
name = f"ByteArray{_len}"
|
|
17
|
+
return type(name, (cls,), {
|
|
18
|
+
"_length": _len,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
def __str__(self):
|
|
22
|
+
return f"{self.__class__.__name__}({self.hex()})"
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_bits(cls, bits: list[bool], bit_order = "msb") -> Self:
|
|
26
|
+
# Sanitize input: make sure bits are 0 or 1
|
|
27
|
+
bits = [int(bool(b)) for b in bits]
|
|
28
|
+
n = len(bits)
|
|
29
|
+
# Pad with zeros to multiple of 8
|
|
30
|
+
pad = (8 - n % 8) % 8
|
|
31
|
+
bits += [0] * pad
|
|
32
|
+
|
|
33
|
+
byte_arr = []
|
|
34
|
+
for i in range(0, len(bits), 8):
|
|
35
|
+
byte_bits = bits[i:i + 8]
|
|
36
|
+
if bit_order == "msb":
|
|
37
|
+
# Most significant bit first
|
|
38
|
+
val = 0
|
|
39
|
+
for bit in byte_bits:
|
|
40
|
+
val = (val << 1) | bit
|
|
41
|
+
elif bit_order == "lsb":
|
|
42
|
+
# Least significant bit first
|
|
43
|
+
val = 0
|
|
44
|
+
for bit in reversed(byte_bits):
|
|
45
|
+
val = (val << 1) | bit
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError(f"Unknown bit_order: {bit_order}")
|
|
48
|
+
# noinspection PyUnreachableCode
|
|
49
|
+
byte_arr.append(val)
|
|
50
|
+
return cls(bytes(byte_arr))
|
|
51
|
+
|
|
52
|
+
def to_bits(self, bit_order="msb") -> list[bool]:
|
|
53
|
+
bits = []
|
|
54
|
+
for byte in self:
|
|
55
|
+
if bit_order == "msb":
|
|
56
|
+
bits.extend([(byte >> i) & 1 for i in reversed(range(8))])
|
|
57
|
+
elif bit_order == "lsb":
|
|
58
|
+
bits.extend([(byte >> i) & 1 for i in range(8)])
|
|
59
|
+
else:
|
|
60
|
+
raise ValueError(f"Unknown bit_order: {bit_order}")
|
|
61
|
+
return bits
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------- #
|
|
64
|
+
# Serialization #
|
|
65
|
+
# ---------------------------------------------------------------------------- #
|
|
66
|
+
def encode_size(self) -> int:
|
|
67
|
+
if self._length is None:
|
|
68
|
+
return Uint(len(self)).encode_size() + len(self)
|
|
69
|
+
return self._length
|
|
70
|
+
|
|
71
|
+
def encode_into(self, buf: bytearray, offset: int = 0) -> int:
|
|
72
|
+
current_offset = offset
|
|
73
|
+
_len = self._length
|
|
74
|
+
if _len is None:
|
|
75
|
+
_len = len(self)
|
|
76
|
+
current_offset += Uint(_len).encode_into(buf, current_offset)
|
|
77
|
+
buf[current_offset:current_offset+_len] = self
|
|
78
|
+
current_offset += _len
|
|
79
|
+
return current_offset - offset
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def decode_from(cls, buffer: Union[bytes, bytearray, memoryview], offset: int = 0) -> Tuple[Self, int]:
|
|
83
|
+
current_offset = offset
|
|
84
|
+
_len = cls._length
|
|
85
|
+
if _len is None:
|
|
86
|
+
_len, _inc_offset = Uint.decode_from(buffer, offset)
|
|
87
|
+
current_offset += _inc_offset
|
|
88
|
+
return cls(buffer[current_offset:current_offset+_len]), current_offset + _len - offset
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------- #
|
|
91
|
+
# JSON Serialization #
|
|
92
|
+
# ---------------------------------------------------------------------------- #
|
|
93
|
+
def to_json(self):
|
|
94
|
+
"""Convert bytes to hex string for JSON serialization"""
|
|
95
|
+
return self.hex()
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_json(cls, data: str):
|
|
99
|
+
"""Create Bytes instance from hex string"""
|
|
100
|
+
data = data.replace("0x", "")
|
|
101
|
+
return cls(bytes.fromhex(data))
|
|
102
|
+
|
tsrkit_types/choice.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from typing import ClassVar, Optional, Union, Tuple, Any, get_origin
|
|
2
|
+
from typing_extensions import get_args
|
|
3
|
+
|
|
4
|
+
from tsrkit_types.integers import Uint
|
|
5
|
+
from tsrkit_types.itf.codable import Codable
|
|
6
|
+
|
|
7
|
+
ChoiceType = Tuple[Optional[str], type] | type
|
|
8
|
+
|
|
9
|
+
class Choice(Codable):
|
|
10
|
+
"""
|
|
11
|
+
Can either be defined as:
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
>>> dir_choice = Choice[T1, T2, ...](T1(XYZ))
|
|
16
|
+
-> makes _opt_types = (None, T1), (None, T2), ...
|
|
17
|
+
|
|
18
|
+
>>> dir_choice.set(T2(ABC))
|
|
19
|
+
-> changes the choice to T2(ABC)
|
|
20
|
+
|
|
21
|
+
>>> class NamedChoice(Choice):
|
|
22
|
+
>>> type1: T1
|
|
23
|
+
>>> type2: T2
|
|
24
|
+
>>> type3: T2
|
|
25
|
+
-> makes _opt_types = (type1, T1), (type2, T2), ...
|
|
26
|
+
>>> a = NamedChoice(T1(XYZ))
|
|
27
|
+
# If setting a choice with multiple types, the first type is the default, or give an optional key string
|
|
28
|
+
>>> b = NamedChoice(T2(ABC))
|
|
29
|
+
>>> b.to_json()
|
|
30
|
+
-> {"type2": "ABC"}
|
|
31
|
+
>>> c = NamedChoice(T2(ABC), key="type3")
|
|
32
|
+
>>> c.to_json()
|
|
33
|
+
-> {"type3": "ABC"}
|
|
34
|
+
"""
|
|
35
|
+
_opt_types: ClassVar[Tuple[ChoiceType]]
|
|
36
|
+
_choice_key: Optional[str]
|
|
37
|
+
_value: Any
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def _choice_types(self) -> Tuple[type]:
|
|
41
|
+
return tuple(self._choice[1] for self._choice in self._opt_types)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def _choice_keys(self) -> Tuple[Optional[str]]:
|
|
45
|
+
return tuple(self._choice[0] for self._choice in self._opt_types)
|
|
46
|
+
|
|
47
|
+
def __class_getitem__(cls, opt_t: Tuple[type] | type):
|
|
48
|
+
_opt_types = []
|
|
49
|
+
if isinstance(opt_t, type):
|
|
50
|
+
_opt_types.append((None, opt_t))
|
|
51
|
+
else:
|
|
52
|
+
for op in opt_t:
|
|
53
|
+
_opt_types.append((None, op))
|
|
54
|
+
name = f"Choice[{'/'.join(op[1].__class__.__name__ for op in _opt_types)}]"
|
|
55
|
+
return type(name,
|
|
56
|
+
(Choice,),
|
|
57
|
+
{"_opt_types": tuple(_opt_types)})
|
|
58
|
+
|
|
59
|
+
def __init_subclass__(cls, **kwargs):
|
|
60
|
+
super().__init_subclass__(**kwargs)
|
|
61
|
+
ann = getattr(cls, "__annotations__", {})
|
|
62
|
+
if ann:
|
|
63
|
+
cls._opt_types = tuple((field, ann[field]) for field in ann)
|
|
64
|
+
|
|
65
|
+
def __init__(self, value: Any, key: Optional[str] = None) -> None:
|
|
66
|
+
super().__init__()
|
|
67
|
+
self.set(value, key)
|
|
68
|
+
|
|
69
|
+
def unwrap(self) -> Any:
|
|
70
|
+
return self._value
|
|
71
|
+
|
|
72
|
+
def __repr__(self) -> str:
|
|
73
|
+
return f"{self.__class__.__name__}({self._value!r})"
|
|
74
|
+
|
|
75
|
+
def __eq__(self, other):
|
|
76
|
+
if isinstance(other, Choice):
|
|
77
|
+
return other._value == self._value
|
|
78
|
+
return other == self._value
|
|
79
|
+
|
|
80
|
+
def set(self, value: Any, key: Optional[str] = None):
|
|
81
|
+
if not isinstance(value, self._choice_types):
|
|
82
|
+
raise TypeError(f"{value!r} is not a {self._choice_types}")
|
|
83
|
+
|
|
84
|
+
key = key or tuple(self._choice[0] for self._choice in self._opt_types if isinstance(value, self._choice[1]))[0]
|
|
85
|
+
if key not in self._choice_keys:
|
|
86
|
+
raise ValueError(f"Key {key!r} not in {self._choice_keys}")
|
|
87
|
+
|
|
88
|
+
self._choice_key = key
|
|
89
|
+
self._value = value
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------- #
|
|
92
|
+
# JSON Serde #
|
|
93
|
+
# ---------------------------------------------------------------------------- #
|
|
94
|
+
|
|
95
|
+
def to_json(self):
|
|
96
|
+
return self._value.to_json() if not self._choice_key else {self._choice_key: self._value.to_json()}
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_json(cls, data: dict | Any) -> "Choice":
|
|
100
|
+
if isinstance(data, dict):
|
|
101
|
+
opt_type = next((x for x in cls._opt_types if x[0] == list(data.keys())[0]), None)
|
|
102
|
+
if opt_type is None:
|
|
103
|
+
raise ValueError(f"Key {list(data.keys())[0]} not in {cls._opt_types}")
|
|
104
|
+
return cls(opt_type[1].from_json(list(data.values())[0]), key=opt_type[0])
|
|
105
|
+
return cls(cls._opt_types[0][1].from_json(data), key=cls._opt_types[0][0])
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------- #
|
|
108
|
+
# Serialization #
|
|
109
|
+
# ---------------------------------------------------------------------------- #
|
|
110
|
+
|
|
111
|
+
def encode_size(self) -> int:
|
|
112
|
+
return Uint(len(self._opt_types)).encode_size() + self._value.encode_size()
|
|
113
|
+
|
|
114
|
+
def encode_into(self, buf: bytearray, offset: int = 0) -> int:
|
|
115
|
+
current_offset = offset
|
|
116
|
+
current_offset += Uint(self._choice_types.index(type(self._value))).encode_into(buf, current_offset)
|
|
117
|
+
current_offset += self._value.encode_into(buf, current_offset)
|
|
118
|
+
return current_offset - offset
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def decode_from(
|
|
122
|
+
cls, buffer: Union[bytes, bytearray, memoryview], offset: int = 0
|
|
123
|
+
) -> Tuple[Any, int]:
|
|
124
|
+
tag, tag_size = Uint.decode_from(buffer, offset)
|
|
125
|
+
value, val_size = cls._opt_types[tag][1].decode_from(buffer, offset+tag_size)
|
|
126
|
+
|
|
127
|
+
return cls(value), tag_size+val_size
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import (
|
|
3
|
+
Generic,
|
|
4
|
+
Mapping,
|
|
5
|
+
Optional,
|
|
6
|
+
Tuple,
|
|
7
|
+
Type,
|
|
8
|
+
TypeVar,
|
|
9
|
+
Union,
|
|
10
|
+
Dict,
|
|
11
|
+
Any,
|
|
12
|
+
Sequence,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from tsrkit_types.integers import Uint
|
|
16
|
+
from tsrkit_types.itf.codable import Codable
|
|
17
|
+
|
|
18
|
+
K = TypeVar("K", bound=Codable)
|
|
19
|
+
V = TypeVar("V", bound=Codable)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DictCheckMeta(abc.ABCMeta):
|
|
23
|
+
"""Meta class to check if the instance is a dictionary with the same key and value types"""
|
|
24
|
+
def __instancecheck__(cls, instance):
|
|
25
|
+
_matches_key_type = getattr(cls, "_key_type", None) == getattr(instance, "_key_type", None)
|
|
26
|
+
_matches_value_type = getattr(cls, "_value_type", None) == getattr(instance, "_value_type", None)
|
|
27
|
+
return isinstance(instance, dict) and _matches_key_type and _matches_value_type
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Dictionary(dict, Codable, Generic[K, V], metaclass=DictCheckMeta):
|
|
31
|
+
"""
|
|
32
|
+
Dictionary implementation that supports codec operations.
|
|
33
|
+
|
|
34
|
+
A dictionary that maps keys to values, providing both standard
|
|
35
|
+
dictionary operations and codec functionality for serialization/deserialization.
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
>>> from tsrkit_types.string import String
|
|
39
|
+
>>> from tsrkit_types.integers import Uint
|
|
40
|
+
>>> d = Dictionary({String("key"): Uint(42)})
|
|
41
|
+
>>> d[String("key")]
|
|
42
|
+
Uint(42)
|
|
43
|
+
>>> encoded = d.encode()
|
|
44
|
+
>>> decoded, _ = d.decode_from(encoded)
|
|
45
|
+
>>> decoded == d
|
|
46
|
+
True
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
_key_type: Type[K]
|
|
50
|
+
_value_type: Type[V]
|
|
51
|
+
|
|
52
|
+
_key_name: Optional[str]
|
|
53
|
+
_value_name: Optional[str]
|
|
54
|
+
|
|
55
|
+
def __class_getitem__(cls, params):
|
|
56
|
+
if len(params) >= 2:
|
|
57
|
+
return type(cls.__name__, (cls,), {
|
|
58
|
+
"_key_type": params[0],
|
|
59
|
+
"_value_type": params[1],
|
|
60
|
+
"_key_name": params[2] if len(params) == 4 else None,
|
|
61
|
+
"_value_name": params[3] if len(params) == 4 else None,
|
|
62
|
+
})
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError("Dictionary must be initialized with types as such - Dictionary[K, V, key_name(optional), value_name(optional)]")
|
|
65
|
+
|
|
66
|
+
def __init__(self, initial: Optional[Mapping[K, V]] = None):
|
|
67
|
+
self.update(initial or {})
|
|
68
|
+
|
|
69
|
+
def _validate(self, key: K, value: V):
|
|
70
|
+
if not isinstance(key, self._key_type):
|
|
71
|
+
raise TypeError(f"Dictionary keys must be {self._key_type} but got {type(key)}")
|
|
72
|
+
if not isinstance(value, self._value_type):
|
|
73
|
+
raise TypeError(f"Dictionary values must be {self._value_type} but got {type(value)}")
|
|
74
|
+
|
|
75
|
+
def __setitem__(self, key: K, value: V) -> None:
|
|
76
|
+
"""Set value for key."""
|
|
77
|
+
self._validate(key, value)
|
|
78
|
+
super().__setitem__(key, value)
|
|
79
|
+
|
|
80
|
+
def __repr__(self) -> str:
|
|
81
|
+
"""Get string representation."""
|
|
82
|
+
items = [f"{k!r}: {v!r}" for k, v in self.items()]
|
|
83
|
+
return f"Dictionary({{{', '.join(items)}}})"
|
|
84
|
+
|
|
85
|
+
def update(self, other: Mapping[K, V]) -> None:
|
|
86
|
+
for key, value in other.items():
|
|
87
|
+
self._validate(key, value)
|
|
88
|
+
super().update(other)
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------- #
|
|
91
|
+
# JSON Serde #
|
|
92
|
+
# ---------------------------------------------------------------------------- #
|
|
93
|
+
|
|
94
|
+
def to_json(self) -> Dict[Any, Any]:
|
|
95
|
+
"""Convert to JSON representation."""
|
|
96
|
+
return {k.to_json(): v.to_json() for k, v in self.items()}
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_json(cls: Type["Dictionary[K, V]"], data: Sequence[Any]) -> "Dictionary[K, V]":
|
|
100
|
+
"""Create instance from JSON representation."""
|
|
101
|
+
if not isinstance(data, dict):
|
|
102
|
+
_value = cls({})
|
|
103
|
+
for val in data:
|
|
104
|
+
_value[cls.key_type.from_json(val[cls.key_name])] = cls.value_type.from_json(val[cls.value_name])
|
|
105
|
+
return _value
|
|
106
|
+
else:
|
|
107
|
+
return cls(
|
|
108
|
+
{
|
|
109
|
+
cls._key_type.from_json(k): cls._value_type.from_json(v)
|
|
110
|
+
for k, v in data.items()
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------- #
|
|
115
|
+
# Serialization #
|
|
116
|
+
# ---------------------------------------------------------------------------- #
|
|
117
|
+
|
|
118
|
+
def encode_size(self) -> int:
|
|
119
|
+
total_size = 0
|
|
120
|
+
total_size += Uint(len(self)).encode_size()
|
|
121
|
+
for k, v in self.items():
|
|
122
|
+
total_size += k.encode_size() + v.encode_size()
|
|
123
|
+
return total_size
|
|
124
|
+
|
|
125
|
+
def encode_into(self, buffer: bytearray, offset: int = 0) -> int:
|
|
126
|
+
current_offset = offset
|
|
127
|
+
current_offset += Uint(len(self)).encode_into(buffer, current_offset)
|
|
128
|
+
for k, v in sorted(self.items(), key=lambda x: x[0].encode()):
|
|
129
|
+
current_offset += k.encode_into(buffer, current_offset)
|
|
130
|
+
current_offset += v.encode_into(buffer, current_offset)
|
|
131
|
+
return current_offset - offset
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def decode_from(cls, buffer: Union[bytes, bytearray, memoryview], offset: int = 0) -> Tuple["Dictionary[K, V]", int]:
|
|
135
|
+
current_offset = offset
|
|
136
|
+
dict_len, size = Uint.decode_from(buffer, offset)
|
|
137
|
+
current_offset += size
|
|
138
|
+
res = cls()
|
|
139
|
+
for _ in range(dict_len):
|
|
140
|
+
key, size = cls._key_type.decode_from(buffer, current_offset)
|
|
141
|
+
current_offset += size
|
|
142
|
+
value, size = cls._value_type.decode_from(buffer, current_offset)
|
|
143
|
+
current_offset += size
|
|
144
|
+
res[key] = value
|
|
145
|
+
return res, current_offset - offset
|