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/string.py ADDED
@@ -0,0 +1,66 @@
1
+ from typing import Union, Tuple
2
+
3
+ from tsrkit_types.integers import Uint
4
+ from tsrkit_types.itf.codable import Codable
5
+
6
+
7
+ class String(str, Codable):
8
+ """
9
+ UTF-8 encoded string type that implements the Codable interface.
10
+
11
+ Examples:
12
+ >>> s = String("Hello")
13
+ >>> str(s)
14
+ 'Hello'
15
+ >>> len(s)
16
+ 5
17
+ >>> s.encode()
18
+ b'\\x05Hello' # Length prefix followed by UTF-8 bytes
19
+
20
+ Note:
21
+ String length is measured in UTF-16 code units, which means some Unicode
22
+ characters (like emojis) may count as 2 units. This matches Python's
23
+ string length behavior.
24
+ """
25
+
26
+ # ---------------------------------------------------------------------------- #
27
+ # Serialization #
28
+ # ---------------------------------------------------------------------------- #
29
+ def encode(self) -> bytes:
30
+ buffer = bytearray(self.encode_size())
31
+ self.encode_into(buffer)
32
+ return buffer
33
+
34
+ def encode_size(self) -> int:
35
+ utf8_bytes = str(self).encode('utf-8')
36
+ return Uint(len(utf8_bytes)).encode_size() + len(utf8_bytes)
37
+
38
+ def encode_into(self, buffer: bytearray, offset: int = 0) -> int:
39
+ current_offset = offset
40
+ utf8_bytes = str(self).encode('utf-8')
41
+ current_offset += Uint(len(utf8_bytes)).encode_into(buffer, current_offset)
42
+ buffer[current_offset:current_offset + len(utf8_bytes)] = utf8_bytes
43
+ return current_offset + len(utf8_bytes) - offset
44
+
45
+ @classmethod
46
+ def decode_from(cls, buffer: Union[bytes, bytearray, memoryview], offset: int = 0) -> Tuple["String", int]:
47
+ current_offset = offset
48
+ byte_len, size = Uint.decode_from(buffer, current_offset)
49
+ current_offset += size
50
+ utf8_bytes = buffer[current_offset:current_offset + byte_len]
51
+ return cls(utf8_bytes.decode('utf-8')), current_offset + byte_len - offset
52
+
53
+ @classmethod
54
+ def decode(cls, buffer: Union[bytes, bytearray, memoryview], offset: int = 0) -> Tuple["String", int]:
55
+ value, bytes_read = cls.decode_from(buffer, offset)
56
+ return value
57
+
58
+ # ---------------------------------------------------------------------------- #
59
+ # JSON Serde #
60
+ # ---------------------------------------------------------------------------- #
61
+ def to_json(self) -> str:
62
+ return self
63
+
64
+ @classmethod
65
+ def from_json(cls, data: str) -> "String":
66
+ return cls(data)
tsrkit_types/struct.py ADDED
@@ -0,0 +1,82 @@
1
+ from dataclasses import dataclass, fields
2
+ from typing import Any, Tuple, Union
3
+ from tsrkit_types.itf.codable import Codable
4
+
5
+
6
+ def struct(_cls=None, *, frozen=False, **kwargs):
7
+ """Extension of dataclass to support serialization and json operations.
8
+
9
+ Usage:
10
+ >>> @struct
11
+ >>> class Person:
12
+ >>> name: String = field(metadata={"name": "first_name"})
13
+ >>> age: Uint[8] = field(metadata={"default": 0})
14
+
15
+ """
16
+ def wrap(cls):
17
+ new_cls = dataclass(cls, frozen=frozen, **kwargs)
18
+
19
+ orig_init = new_cls.__init__
20
+
21
+ def __init__(self, *args, **kwargs):
22
+ for field in fields(self):
23
+ # If the field is not found, but has a default, set it
24
+ if field.name not in kwargs and field.metadata.get("default") is not None:
25
+ kwargs[field.name] = field.metadata.get("default")
26
+ orig_init(self, *args, **kwargs)
27
+
28
+ def encode_size(self) -> int:
29
+ return sum(getattr(self, field.name).encode_size() for field in fields(self))
30
+
31
+ def encode_into(self, buffer: bytes, offset = 0) -> int:
32
+ current_offset = offset
33
+ for field in fields(self):
34
+ item = getattr(self, field.name)
35
+ size = item.encode_into(buffer, current_offset)
36
+ current_offset += size
37
+
38
+ return current_offset - offset
39
+
40
+ @classmethod
41
+ def decode_from(cls, buffer: Union[bytes, bytearray, memoryview], offset: int = 0) -> Tuple[Any, int]:
42
+ current_offset = offset
43
+ decoded_values = {}
44
+ for field in fields(cls):
45
+ field_type = field.type
46
+ value, size = field_type.decode_from(buffer, current_offset)
47
+ decoded_values[field.name] = value
48
+ current_offset += size
49
+ instance = cls(**decoded_values)
50
+ return instance, current_offset - offset
51
+
52
+ def to_json(self) -> dict:
53
+ return {field.metadata.get("name", field.name): getattr(self, field.name).to_json() for field in fields(self)}
54
+
55
+ @classmethod
56
+ def from_json(cls, data: dict) -> Any:
57
+ init_data = {}
58
+ for field in fields(cls):
59
+ k = field.metadata.get("name", field.name)
60
+ v = data.get(k)
61
+ if v is None:
62
+ if field.metadata.get("default") is not None:
63
+ init_data[field.name] = field.metadata.get("default")
64
+ else:
65
+ raise ValueError(f"Field {k} not found in {cls}")
66
+ else:
67
+ init_data[field.name] = field.type.from_json(v)
68
+ return cls(**init_data)
69
+
70
+
71
+ new_cls.__init__ = __init__
72
+ new_cls.encode_size = encode_size
73
+ new_cls.decode_from = decode_from
74
+ new_cls.encode_into = encode_into
75
+ new_cls.to_json = to_json
76
+ new_cls.from_json = from_json
77
+
78
+ new_cls = type(new_cls.__name__, (Codable, new_cls), dict(new_cls.__dict__))
79
+
80
+ return new_cls
81
+
82
+ return wrap if _cls is None else wrap(_cls)