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/enum.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from enum import EnumMeta
|
|
2
|
+
from typing import Tuple, Type, Union, Any, TypeVar, cast
|
|
3
|
+
from tsrkit_types.integers import Uint
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T", bound="Enum")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Enum(metaclass=EnumMeta):
|
|
9
|
+
"""Decodable Enum type - Extending the built-in Enum type to add encoding and decoding methods
|
|
10
|
+
|
|
11
|
+
How to use it:
|
|
12
|
+
>>> class MyEnum(Enum):
|
|
13
|
+
>>> A = 1
|
|
14
|
+
>>> B = 2
|
|
15
|
+
>>> C = 3
|
|
16
|
+
>>>
|
|
17
|
+
>>> value = MyEnum.A
|
|
18
|
+
>>> encoded = value.encode()
|
|
19
|
+
>>> decoded, bytes_read = MyEnum.decode_from(encoded)
|
|
20
|
+
>>> assert decoded == value
|
|
21
|
+
>>> assert bytes_read == 1
|
|
22
|
+
>>>
|
|
23
|
+
>>> assert MyEnum.from_json(1,) == MyEnum.A
|
|
24
|
+
>>> assert MyEnum.from_json("A",) == MyEnum.A
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def value(self) -> Any:
|
|
29
|
+
return self._value_
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def _missing_(cls, value: Any) -> T:
|
|
33
|
+
raise ValueError(f"Invalid value: {value}")
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------- #
|
|
36
|
+
# Serialization #
|
|
37
|
+
# ---------------------------------------------------------------------------- #
|
|
38
|
+
|
|
39
|
+
def encode_size(self) -> int:
|
|
40
|
+
"""Return the size in bytes needed to encode this enum value"""
|
|
41
|
+
return 1
|
|
42
|
+
|
|
43
|
+
def encode(self) -> bytes:
|
|
44
|
+
"""Encode the value into a new bytes object."""
|
|
45
|
+
size = self.encode_size()
|
|
46
|
+
buffer = bytearray(size)
|
|
47
|
+
written = self.encode_into(buffer)
|
|
48
|
+
return bytes(buffer[:written])
|
|
49
|
+
|
|
50
|
+
def encode_into(self, buffer: bytearray, offset: int = 0) -> int:
|
|
51
|
+
"""Encode this enum value into the given buffer at the given offset
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
buffer: The buffer to encode into
|
|
55
|
+
offset: The offset to start encoding at
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
The number of bytes written
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: If the enum has too many variants to encode in a byte
|
|
62
|
+
"""
|
|
63
|
+
# Get the index of the enum value in all enums
|
|
64
|
+
# Encode the index as a byte
|
|
65
|
+
all_enums = self.__class__._member_names_
|
|
66
|
+
index = all_enums.index(self._name_)
|
|
67
|
+
if index > 255:
|
|
68
|
+
raise ValueError("Enum index is too large to encode into a single byte")
|
|
69
|
+
return Uint(index).encode_into(buffer, offset)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def decode_from(
|
|
73
|
+
cls, buffer: Union[bytes, bytearray, memoryview], offset: int = 0
|
|
74
|
+
) -> Tuple[T, int]:
|
|
75
|
+
"""Decode an enum value from the given buffer at the given offset
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
buffer: The buffer to decode from
|
|
79
|
+
offset: The offset to start decoding at
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
A tuple of (decoded enum value, number of bytes read)
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
ValueError: If the encoded index is invalid
|
|
86
|
+
"""
|
|
87
|
+
# Decode the byte (index of enum) into an Enum
|
|
88
|
+
# Return the enum value
|
|
89
|
+
index, bytes_read = Uint.decode_from(buffer, offset)
|
|
90
|
+
value = cast(T, cls._member_map_[cls._member_names_[index]])
|
|
91
|
+
return value, bytes_read
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------- #
|
|
95
|
+
# JSON Serde #
|
|
96
|
+
# ---------------------------------------------------------------------------- #
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_json(cls: Type[T], data: Any) -> T:
|
|
100
|
+
"""Convert a JSON value to an enum value
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
data: The JSON value (either the enum value or name)
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The corresponding enum value
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
JsonDeserializationError: If the value is invalid
|
|
110
|
+
"""
|
|
111
|
+
for v in cls.__members__.values():
|
|
112
|
+
if v._value_ == data or v._name_ == data:
|
|
113
|
+
return cast(T, v)
|
|
114
|
+
raise ValueError(f"Invalid value: {data}")
|
|
115
|
+
|
|
116
|
+
def to_json(self) -> Any:
|
|
117
|
+
"""Convert this enum value to a JSON value
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The enum's value for JSON serialization
|
|
121
|
+
"""
|
|
122
|
+
return self._value_
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def decode(cls, buffer: Union[bytes, bytearray, memoryview], offset: int = 0) -> T:
|
|
126
|
+
"""Decode a value from the provided buffer starting at the specified offset."""
|
|
127
|
+
value, bytes_read = cls.decode_from(buffer, offset)
|
|
128
|
+
return value
|
tsrkit_types/integers.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
import math
|
|
4
|
+
from typing import Any, Tuple, Union, Callable
|
|
5
|
+
from tsrkit_types.itf.codable import Codable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IntCheckMeta(abc.ABCMeta):
|
|
9
|
+
"""Meta class to check if the instance is an integer with the same byte size"""
|
|
10
|
+
def __instancecheck__(cls, instance):
|
|
11
|
+
return isinstance(instance, int) and getattr(instance, "byte_size", 0) == cls.byte_size
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Uint(int, Codable, metaclass=IntCheckMeta):
|
|
15
|
+
"""
|
|
16
|
+
Unsigned integer type.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
>>> # Fixed Integer with defined class
|
|
21
|
+
>>> U8 = Uint[8]
|
|
22
|
+
>>> U8(10)
|
|
23
|
+
U8(10)
|
|
24
|
+
>>> U8.encode(10)
|
|
25
|
+
b'\n'
|
|
26
|
+
>>> U8.decode(b'\n')
|
|
27
|
+
U8(10)
|
|
28
|
+
|
|
29
|
+
>>> # Fixed Integer w dynamic usage
|
|
30
|
+
>>> num = Uint[8](10)
|
|
31
|
+
>>> num
|
|
32
|
+
U8(10)
|
|
33
|
+
>>> num.encode()
|
|
34
|
+
b'\n'
|
|
35
|
+
>>> Uint[8].decode(b'\n')
|
|
36
|
+
U8(10)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
>>> # If you want to use the General Integer (supports up to 2**64 - 1),
|
|
40
|
+
>>> # you can use the Uint class without specifying the byte size.
|
|
41
|
+
>>>
|
|
42
|
+
>>> num = Uint(10)
|
|
43
|
+
>>> num
|
|
44
|
+
Uint(10)
|
|
45
|
+
>>> num.encode()
|
|
46
|
+
b'\n'
|
|
47
|
+
>>> Uint.decode(b'\n')
|
|
48
|
+
Uint(10)
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# If the byte_size is set, the integer is fixed size.
|
|
53
|
+
# Otherwise, the integer is General Integer (supports up to 2**64 - 1)
|
|
54
|
+
byte_size: int = 0
|
|
55
|
+
|
|
56
|
+
def __class_getitem__(cls, size: int):
|
|
57
|
+
return type(f"U{size}" if size else "Int", (cls,), {"byte_size": size // 8})
|
|
58
|
+
|
|
59
|
+
def __new__(cls, value: Any):
|
|
60
|
+
value = int(value)
|
|
61
|
+
if cls.byte_size > 0:
|
|
62
|
+
bits = 8 * cls.byte_size
|
|
63
|
+
min_v = 0
|
|
64
|
+
max_v = (1 << bits) - 1
|
|
65
|
+
if not (min_v <= value <= max_v):
|
|
66
|
+
raise ValueError(f"Fixed Int: {cls.__name__} out of range: {value!r} "
|
|
67
|
+
f"not in [{min_v}, {max_v}]")
|
|
68
|
+
else:
|
|
69
|
+
if not 0 <= value < 2 ** 64:
|
|
70
|
+
raise ValueError(f"General Int: Value must be between 0 and 2**64 - 1, got {value}")
|
|
71
|
+
|
|
72
|
+
return super().__new__(cls, value)
|
|
73
|
+
|
|
74
|
+
def __repr__(self):
|
|
75
|
+
return f"{self.__class__.__name__}({int(self)})"
|
|
76
|
+
|
|
77
|
+
def _wrap_op(self, other: Any, op: Callable[[int, int], int]):
|
|
78
|
+
res = op(int(self), int(other))
|
|
79
|
+
return type(self)(res)
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------- #
|
|
82
|
+
# Arithmetic #
|
|
83
|
+
# ---------------------------------------------------------------------------- #
|
|
84
|
+
def __add__(self, other):
|
|
85
|
+
return self._wrap_op(other, int.__add__)
|
|
86
|
+
|
|
87
|
+
def __sub__(self, other):
|
|
88
|
+
return self._wrap_op(other, int.__sub__)
|
|
89
|
+
|
|
90
|
+
def __mul__(self, other):
|
|
91
|
+
return self._wrap_op(other, int.__mul__)
|
|
92
|
+
|
|
93
|
+
def __floordiv__(self, other):
|
|
94
|
+
return self._wrap_op(other, int.__floordiv__)
|
|
95
|
+
|
|
96
|
+
def __and__(self, other):
|
|
97
|
+
return self._wrap_op(other, int.__and__)
|
|
98
|
+
|
|
99
|
+
def __or__(self, other):
|
|
100
|
+
return self._wrap_op(other, int.__or__)
|
|
101
|
+
|
|
102
|
+
def __xor__(self, other):
|
|
103
|
+
return self._wrap_op(other, int.__xor__)
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------- #
|
|
106
|
+
# JSON Serde #
|
|
107
|
+
# ---------------------------------------------------------------------------- #
|
|
108
|
+
def to_json(self) -> str:
|
|
109
|
+
return int(self)
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def from_json(cls, json_str: str) -> "Uint":
|
|
113
|
+
return cls(int(json_str))
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------- #
|
|
116
|
+
# Serialization #
|
|
117
|
+
# ---------------------------------------------------------------------------- #
|
|
118
|
+
@staticmethod
|
|
119
|
+
def l(x):
|
|
120
|
+
return math.floor(Decimal(x).ln() / (Decimal(7) * Decimal(2).ln()))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def encode_size(self) -> int:
|
|
124
|
+
if self.byte_size > 0:
|
|
125
|
+
return self.byte_size
|
|
126
|
+
else:
|
|
127
|
+
if self < 2**7:
|
|
128
|
+
return 1
|
|
129
|
+
elif self < 2 ** (7 * 9):
|
|
130
|
+
return 1 + self.l(self)
|
|
131
|
+
elif self < 2**64:
|
|
132
|
+
return 9
|
|
133
|
+
else:
|
|
134
|
+
raise ValueError("Value too large for encoding. General Uint support up to 2**64 - 1")
|
|
135
|
+
|
|
136
|
+
def encode_into(self, buffer: bytearray, offset: int = 0) -> int:
|
|
137
|
+
if self.byte_size > 0:
|
|
138
|
+
buffer[offset:offset+self.byte_size] = self.to_bytes(self.byte_size, "little")
|
|
139
|
+
return self.byte_size
|
|
140
|
+
else:
|
|
141
|
+
if self < 2**7:
|
|
142
|
+
buffer[offset:offset+1] = self.to_bytes(1, "little")
|
|
143
|
+
return 1
|
|
144
|
+
|
|
145
|
+
size = self.encode_size()
|
|
146
|
+
self._check_buffer_size(buffer, size, offset)
|
|
147
|
+
if self < 2 ** (7 * 8):
|
|
148
|
+
_l = self.l(self)
|
|
149
|
+
# Create temporary U8 for encoding the prefix
|
|
150
|
+
prefix_value = (2**8 - 2 ** (8 - _l) +
|
|
151
|
+
math.floor(Decimal(self) / (Decimal(2) ** (_l * 8))))
|
|
152
|
+
buffer[offset] = int(prefix_value)
|
|
153
|
+
offset += 1
|
|
154
|
+
# Encode the remaining bytes
|
|
155
|
+
remaining = self % (2 ** (_l * 8))
|
|
156
|
+
remaining_bytes = remaining.to_bytes(_l, "little")
|
|
157
|
+
buffer[offset : offset + _l] = remaining_bytes
|
|
158
|
+
elif self < 2**64:
|
|
159
|
+
buffer[offset] = 2**8 - 1 # Full 64-bit marker
|
|
160
|
+
offset += 1
|
|
161
|
+
buffer[offset : offset + 8] = self.to_bytes(8, "little")
|
|
162
|
+
else:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"Value too large for encoding. General Uint support up to 2**64 - 1, got {self}"
|
|
165
|
+
)
|
|
166
|
+
return size
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def decode_from(
|
|
170
|
+
cls, buffer: Union[bytes, bytearray, memoryview], offset: int = 0
|
|
171
|
+
) -> Tuple[Any, int]:
|
|
172
|
+
if cls.byte_size > 0:
|
|
173
|
+
value, size = int.from_bytes(buffer[offset : offset + cls.byte_size], "little"), cls.byte_size
|
|
174
|
+
return cls.__new__(cls, value), size
|
|
175
|
+
else:
|
|
176
|
+
tag = int.from_bytes(buffer[offset:offset+1], "little")
|
|
177
|
+
|
|
178
|
+
if tag < 2**7:
|
|
179
|
+
return cls(tag), 1
|
|
180
|
+
|
|
181
|
+
if tag == 2**8 - 1:
|
|
182
|
+
# Full 64-bit encoding
|
|
183
|
+
if len(buffer) - offset < 9:
|
|
184
|
+
raise ValueError("Buffer too small to decode 64-bit integer")
|
|
185
|
+
value = int.from_bytes(buffer[offset + 1 : offset + 9], "little")
|
|
186
|
+
return cls(value), 9
|
|
187
|
+
else:
|
|
188
|
+
# Variable length encoding
|
|
189
|
+
_l = math.floor(
|
|
190
|
+
Decimal(8) - (Decimal(2**8) - Decimal(tag)).ln() / Decimal(2).ln()
|
|
191
|
+
)
|
|
192
|
+
if len(buffer) - offset < _l + 1:
|
|
193
|
+
raise ValueError("Buffer too small to decode variable-length integer")
|
|
194
|
+
alpha = tag + 2 ** (8 - _l) - 2**8
|
|
195
|
+
beta = int.from_bytes(buffer[offset + 1 : offset + 1 + _l], "little")
|
|
196
|
+
value = alpha * 2 ** (_l * 8) + beta
|
|
197
|
+
return cls(value), _l + 1
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
U8 = Uint[8]
|
|
201
|
+
U16 = Uint[16]
|
|
202
|
+
U32 = Uint[32]
|
|
203
|
+
U64 = Uint[64]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import TypeVar, Generic, Tuple, Union
|
|
3
|
+
|
|
4
|
+
T = TypeVar("T")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Codable(ABC, Generic[T]):
|
|
8
|
+
"""Abstract base class defining the interface for encoding and decoding data."""
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def encode_size(self) -> int:
|
|
12
|
+
"""
|
|
13
|
+
Calculate the number of bytes needed to encode the value.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
The number of bytes needed to encode the value.
|
|
17
|
+
"""
|
|
18
|
+
raise NotImplementedError("Subclasses must implement this method")
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def encode_into(self, buffer: bytearray, offset: int = 0) -> int:
|
|
22
|
+
"""
|
|
23
|
+
Encode the value into the provided buffer at the specified offset.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
buffer: The buffer to encode the value into.
|
|
27
|
+
offset: The offset at which to start encoding the value.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The number of bytes written to the buffer.
|
|
31
|
+
"""
|
|
32
|
+
raise NotImplementedError("Subclasses must implement this method")
|
|
33
|
+
|
|
34
|
+
def encode(self) -> bytes:
|
|
35
|
+
"""
|
|
36
|
+
Encode the value into a new bytes object.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The encoded value as a bytes object.
|
|
40
|
+
"""
|
|
41
|
+
size = self.encode_size()
|
|
42
|
+
buffer = bytearray(size)
|
|
43
|
+
written = self.encode_into(buffer)
|
|
44
|
+
return bytes(buffer[:written])
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def decode_from(cls, buffer: Union[bytes, bytearray, memoryview], offset: int = 0) -> Tuple[T, int]:
|
|
48
|
+
"""
|
|
49
|
+
Decode a value from the provided buffer starting at the specified offset.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
buffer: The buffer to decode the value from.
|
|
53
|
+
offset: The offset at which to start decoding the value.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
A tuple containing the decoded value and the number of bytes read from the buffer.
|
|
57
|
+
"""
|
|
58
|
+
raise NotImplementedError("Subclasses must implement this method")
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def decode(cls, buffer: Union[bytes, bytearray, memoryview], offset: int = 0) -> T:
|
|
62
|
+
"""
|
|
63
|
+
Decode a value from the provided buffer starting at the specified offset.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
buffer: The buffer to decode the value from.
|
|
67
|
+
offset: The offset at which to start decoding the value.
|
|
68
|
+
"""
|
|
69
|
+
value, bytes_read = cls.decode_from(buffer, offset)
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
def _check_buffer_size(self, buffer: bytearray, size: int, offset: int) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Check if the buffer has enough space to encode the value.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
buffer: The buffer to check the size of.
|
|
78
|
+
size: The size of the value to encode.
|
|
79
|
+
offset: The offset at which to start encoding the value.
|
|
80
|
+
"""
|
|
81
|
+
if len(buffer) - offset < size:
|
|
82
|
+
raise ValueError("Buffer too small to encode value")
|
tsrkit_types/null.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import Optional, Self, Tuple, Union
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NullType:
|
|
5
|
+
def __repr__(self):
|
|
6
|
+
return "Null"
|
|
7
|
+
|
|
8
|
+
def __eq__(self, other):
|
|
9
|
+
return not other
|
|
10
|
+
|
|
11
|
+
def __bool__(self):
|
|
12
|
+
return False
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------- #
|
|
15
|
+
# Serialization #
|
|
16
|
+
# ---------------------------------------------------------------------------- #
|
|
17
|
+
|
|
18
|
+
def encode_size(self) -> int:
|
|
19
|
+
return 0
|
|
20
|
+
|
|
21
|
+
def encode_into(self, buffer: bytearray, offset: int = 0) -> int:
|
|
22
|
+
return 0
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def decode_from(cls, buffer: Union[bytes, bytearray, memoryview], offset: int = 0) -> Tuple[Self, int]:
|
|
26
|
+
return cls(), 0
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------- #
|
|
29
|
+
# JSON Parse #
|
|
30
|
+
# ---------------------------------------------------------------------------- #
|
|
31
|
+
|
|
32
|
+
def to_json(self) -> str:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_json(cls, json_str: Optional[str]) -> Self:
|
|
37
|
+
if json_str is None:
|
|
38
|
+
return cls()
|
|
39
|
+
raise ValueError("Invalid JSON string for NullType")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
Null = NullType()
|
tsrkit_types/option.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Generic, Optional, TypeVar
|
|
2
|
+
from tsrkit_types.choice import Choice
|
|
3
|
+
from tsrkit_types.null import Null, NullType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
class Option(Choice, Generic[T]):
|
|
9
|
+
"""
|
|
10
|
+
Option[T] wraps either no value (None) or a T.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __class_getitem__(cls, opt_t: T):
|
|
14
|
+
if not isinstance(opt_t, type):
|
|
15
|
+
raise TypeError("Option[...] only accepts a single type")
|
|
16
|
+
name = f"Option[{opt_t.__class__.__name__}]"
|
|
17
|
+
return type(name,
|
|
18
|
+
(Option,),
|
|
19
|
+
{"_opt_types": ((None, opt_t), (None, NullType))})
|
|
20
|
+
|
|
21
|
+
def __init__(self, val: T|NullType = Null):
|
|
22
|
+
super().__init__(val)
|
|
23
|
+
|
|
24
|
+
def set(self, value: T|NullType = Null, key: Optional[str] = None):
|
|
25
|
+
if value is None:
|
|
26
|
+
value = Null
|
|
27
|
+
super().set(value, key)
|
|
28
|
+
|
|
29
|
+
def __bool__(self):
|
|
30
|
+
return self._value != Null
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import TypeVar, Type, ClassVar, Tuple, Generic
|
|
3
|
+
from tsrkit_types.integers import Uint
|
|
4
|
+
from tsrkit_types.itf.codable import Codable
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
class SeqCheckMeta(abc.ABCMeta):
|
|
9
|
+
"""Meta class to check if the instance is an integer with the same byte size"""
|
|
10
|
+
def __instancecheck__(cls, instance):
|
|
11
|
+
# String comparison is used to avoid identity comparison issues - like Uint[8] and Uint[8]
|
|
12
|
+
# TODO - This needs more false positive testing
|
|
13
|
+
_matches_element_type = str(getattr(cls, "_element_type", None)) == str(getattr(instance, "_element_type", None))
|
|
14
|
+
_matches_min_length = getattr(cls, "_min_length", 0) == getattr(instance, "_min_length", 0)
|
|
15
|
+
_matches_max_length = getattr(cls, "_max_length", 2**64) == getattr(instance, "_max_length", 2**64)
|
|
16
|
+
return isinstance(instance, list) and _matches_element_type and _matches_min_length and _matches_max_length
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Seq(list, Codable, Generic[T], metaclass=SeqCheckMeta):
|
|
20
|
+
"""
|
|
21
|
+
Sequence Type
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
>>> # Create a reusable type
|
|
25
|
+
>>> class Eta(Seq[bytes, 4]): ...
|
|
26
|
+
>>>
|
|
27
|
+
>>> # (or) Disposable
|
|
28
|
+
>>> val_indexes = Seq[int, 1023]([0] * 1028)
|
|
29
|
+
>>>
|
|
30
|
+
>>> # Supports codec [both variable and fixed length] given that the element type must support codec
|
|
31
|
+
>>> Seq[U16, 1023]([0] * 1028).encode()
|
|
32
|
+
"""
|
|
33
|
+
_element_type: ClassVar[Type[T]]
|
|
34
|
+
_min_length: ClassVar[int] = 0
|
|
35
|
+
_max_length: ClassVar[int] = 2 ** 64
|
|
36
|
+
|
|
37
|
+
def __class_getitem__(cls, params):
|
|
38
|
+
# To overwrite previous cls values
|
|
39
|
+
min_l, max_l, elem_t = 0, 2**64, None
|
|
40
|
+
|
|
41
|
+
if isinstance(params, int) or isinstance(params, (type, TypeVar)):
|
|
42
|
+
if isinstance(params, (type, TypeVar)):
|
|
43
|
+
elem_t = params
|
|
44
|
+
elif isinstance(params, int):
|
|
45
|
+
max_l, min_l = params, params
|
|
46
|
+
else:
|
|
47
|
+
raise TypeError(f"Invalid param to define {__class__.__name__}: {params}")
|
|
48
|
+
elif len(params) == 2:
|
|
49
|
+
if isinstance(params[0], type) and isinstance(params[1], int):
|
|
50
|
+
elem_t, min_l, max_l = params[0], params[1], params[1]
|
|
51
|
+
elif isinstance(params[0], int) and isinstance(params[1], int):
|
|
52
|
+
min_l, max_l = params[0], params[1]
|
|
53
|
+
else:
|
|
54
|
+
raise TypeError(f"Invalid param to define {cls.__class__.__name__}: {params}")
|
|
55
|
+
elif len(params) == 3 and isinstance(params[0], (type, TypeVar)) and isinstance(params[1], int) and isinstance(params[2], int):
|
|
56
|
+
elem_t, min_l, max_l = params
|
|
57
|
+
else:
|
|
58
|
+
raise TypeError(f"Invalid param to define {cls.__class__.__name__}: {params}")
|
|
59
|
+
|
|
60
|
+
# build a nice name
|
|
61
|
+
parts = []
|
|
62
|
+
if elem_t:
|
|
63
|
+
parts.append(elem_t.__name__)
|
|
64
|
+
if min_l == max_l:
|
|
65
|
+
parts.append(f"N={min_l}")
|
|
66
|
+
else:
|
|
67
|
+
if min_l:
|
|
68
|
+
parts.append(f"min={min_l}")
|
|
69
|
+
if max_l != 2 ** 64:
|
|
70
|
+
parts.append(f"max={max_l}")
|
|
71
|
+
|
|
72
|
+
name = f"{cls.__name__}[{','.join(parts)}]"
|
|
73
|
+
|
|
74
|
+
return type(name, (cls,), {
|
|
75
|
+
"_element_type": elem_t,
|
|
76
|
+
"_min_length": min_l,
|
|
77
|
+
"_max_length": max_l,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
def _validate(self, value):
|
|
81
|
+
"""For TypeChecks - added to fns that alter elements"""
|
|
82
|
+
if getattr(self, "_element_type", None) is not None:
|
|
83
|
+
if not isinstance(value, self._element_type):
|
|
84
|
+
raise TypeError(f"{value!r} is not an instance of {self._element_type!r}")
|
|
85
|
+
|
|
86
|
+
def _validate_self(self):
|
|
87
|
+
"""For Resultant Self check - added to fns that alter size"""
|
|
88
|
+
if len(self) < self._min_length:
|
|
89
|
+
raise ValueError(f"Vector: Expected sequence size to be >= {self._min_length}, resultant size {len(self)}")
|
|
90
|
+
elif len(self) > self._max_length:
|
|
91
|
+
raise ValueError(f"Vector: Expected sequence size to be <= {self._max_length}, resultant size {len(self)}")
|
|
92
|
+
|
|
93
|
+
def __init__(self, initial: list[T]):
|
|
94
|
+
super().__init__()
|
|
95
|
+
self.extend(initial)
|
|
96
|
+
|
|
97
|
+
def append(self, v: T):
|
|
98
|
+
self._validate(v)
|
|
99
|
+
super().append(v)
|
|
100
|
+
self._validate_self()
|
|
101
|
+
|
|
102
|
+
def insert(self, i, v: T):
|
|
103
|
+
self._validate(v)
|
|
104
|
+
super().insert(i, v)
|
|
105
|
+
self._validate_self()
|
|
106
|
+
|
|
107
|
+
def extend(self, seq: list[T]):
|
|
108
|
+
for val in seq:
|
|
109
|
+
self._validate(val)
|
|
110
|
+
super().extend(seq)
|
|
111
|
+
self._validate_self()
|
|
112
|
+
|
|
113
|
+
def __setitem__(self, i, v: T):
|
|
114
|
+
self._validate(v)
|
|
115
|
+
super().__setitem__(i, v)
|
|
116
|
+
|
|
117
|
+
def __repr__(self):
|
|
118
|
+
return f"{self.__class__.__name__}({list(self)})"
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def _length(self):
|
|
122
|
+
if self._min_length == self._max_length:
|
|
123
|
+
return self._min_length
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
# ---------------------------------------------------------------------------- #
|
|
127
|
+
# Serialization #
|
|
128
|
+
# ---------------------------------------------------------------------------- #
|
|
129
|
+
def encode_size(self):
|
|
130
|
+
size = 0
|
|
131
|
+
|
|
132
|
+
# If length is not defined
|
|
133
|
+
if self._length is None:
|
|
134
|
+
size += Uint(len(self)).encode_size()
|
|
135
|
+
|
|
136
|
+
for item in self:
|
|
137
|
+
if not isinstance(item, Codable):
|
|
138
|
+
raise TypeError(0, 0, f"Expected Codable, got {type(item)}")
|
|
139
|
+
size += item.encode_size()
|
|
140
|
+
|
|
141
|
+
return size
|
|
142
|
+
|
|
143
|
+
def encode_into(self, buffer: bytearray, offset: int = 0) -> int:
|
|
144
|
+
current_offset = offset
|
|
145
|
+
# If length is not defined
|
|
146
|
+
if(self._min_length != len(self)) and (self._max_length != len(self)):
|
|
147
|
+
current_offset += Uint(len(self)).encode_into(buffer, current_offset)
|
|
148
|
+
|
|
149
|
+
for item in self:
|
|
150
|
+
written = item.encode_into(buffer, current_offset)
|
|
151
|
+
current_offset += written
|
|
152
|
+
|
|
153
|
+
return current_offset - offset
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def decode_from(cls, buffer: bytes, offset: int = 0) -> Tuple["Seq", int]:
|
|
157
|
+
current_offset = offset
|
|
158
|
+
|
|
159
|
+
# Determine if this is variable length
|
|
160
|
+
if cls._min_length == cls._max_length:
|
|
161
|
+
# Fixed length
|
|
162
|
+
_len = cls._min_length
|
|
163
|
+
else:
|
|
164
|
+
# Variable length - decode length from buffer
|
|
165
|
+
_len, _inc_offset = Uint.decode_from(buffer, current_offset)
|
|
166
|
+
current_offset += _inc_offset
|
|
167
|
+
|
|
168
|
+
items = []
|
|
169
|
+
for _ in range(_len):
|
|
170
|
+
item, _inc_offset = cls._element_type.decode_from(buffer, current_offset)
|
|
171
|
+
current_offset += _inc_offset
|
|
172
|
+
items.append(item)
|
|
173
|
+
|
|
174
|
+
return cls(items), current_offset - offset
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------------------- #
|
|
177
|
+
# JSON Serde #
|
|
178
|
+
# ---------------------------------------------------------------------------- #
|
|
179
|
+
def to_json(self):
|
|
180
|
+
"""Convert to JSON representation."""
|
|
181
|
+
return [item.to_json() for item in self]
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def from_json(cls, data):
|
|
185
|
+
"""Create instance from JSON representation."""
|
|
186
|
+
if cls._element_type:
|
|
187
|
+
items = [cls._element_type.from_json(item) for item in data]
|
|
188
|
+
else:
|
|
189
|
+
items = data
|
|
190
|
+
return cls(items)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# All params supported-
|
|
194
|
+
# Union[Type, int, Tuple[Type, int], Tuple[int, int], Tuple[Type, int, int]]
|
|
195
|
+
|
|
196
|
+
class Vector(Seq):
|
|
197
|
+
def __class_getitem__(cls, params: None): return super().__class_getitem__(params)
|
|
198
|
+
|
|
199
|
+
class Array(Seq):
|
|
200
|
+
def __class_getitem__(cls, params: int): return super().__class_getitem__(params)
|
|
201
|
+
|
|
202
|
+
class TypedArray(Seq):
|
|
203
|
+
def __class_getitem__(cls, params: Tuple[Type, int]): return super().__class_getitem__(params)
|
|
204
|
+
|
|
205
|
+
class TypedVector(Seq):
|
|
206
|
+
def __class_getitem__(cls, params: Type): return super().__class_getitem__(params)
|
|
207
|
+
|
|
208
|
+
class BoundedVector(Seq):
|
|
209
|
+
def __class_getitem__(cls, params: Tuple[int, int]): return super().__class_getitem__(params)
|
|
210
|
+
|
|
211
|
+
class TypedBoundedVector(Seq):
|
|
212
|
+
def __class_getitem__(cls, params: Tuple[Type, int, int]): return super().__class_getitem__(params)
|