tsrkit-types 0.1.3__tar.gz → 0.1.9__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.
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/CHANGELOG.md +6 -1
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/PKG-INFO +16 -2
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/README.md +15 -1
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/pyproject.toml +2 -2
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tests/test_int.py +19 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tests/test_struct.py +4 -3
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types/__init__.py +3 -3
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types/bytearray.py +3 -1
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types/bytes.py +26 -9
- tsrkit_types-0.1.9/tsrkit_types/bytes_common.py +134 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types/choice.py +7 -4
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types/dictionary.py +1 -1
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types/integers.py +62 -25
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types/itf/codable.py +4 -1
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types/null.py +3 -3
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types/option.py +1 -1
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types/struct.py +2 -2
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types.egg-info/PKG-INFO +16 -2
- tsrkit_types-0.1.3/tsrkit_types/bytes_common.py +0 -68
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/CONTRIBUTING.md +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/LICENSE +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/MANIFEST.in +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/pytest.ini +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/setup.cfg +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/setup.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tests/test_bits.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tests/test_bytearray.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tests/test_bytes.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tests/test_choices.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tests/test_containers.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tests/test_enums.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tests/test_integers.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tests/test_network.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tests/test_seq.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tests/test_strings.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tests/test_structs.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tests/type_hints/test_struct_serde.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types/bits.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types/bool.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types/enum.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types/sequences.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types/string.py +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types.egg-info/SOURCES.txt +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types.egg-info/dependency_links.txt +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types.egg-info/requires.txt +0 -0
- {tsrkit_types-0.1.3 → tsrkit_types-0.1.9}/tsrkit_types.egg-info/top_level.txt +0 -0
|
@@ -37,7 +37,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
37
37
|
- Zero-dependency core library
|
|
38
38
|
- Python 3.11+ support
|
|
39
39
|
|
|
40
|
-
## [0.1.
|
|
40
|
+
## [0.1.4] - 2025-06-06
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
- **Bytes type**: Corrected the name of the Bytes type from `ByteArrayNUM` to `BytesNUM`
|
|
44
|
+
|
|
45
|
+
## [0.1.3] - 2025-06-06
|
|
41
46
|
|
|
42
47
|
### Fixed
|
|
43
48
|
- **Option JSON handling**: Fixed `Option.from_json()` to properly handle `None` values by creating empty Options
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tsrkit-types
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Performant Python Typings library for type-safe binary serialization, JSON encoding, and data validation with zero dependencies
|
|
5
5
|
Author-email: chainscore-labs <hello@chainscore.finance>, prasad-kumkar <prasad@chainscore.finance>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -27,7 +27,7 @@ Classifier: Topic :: Utilities
|
|
|
27
27
|
Classifier: Topic :: System :: Archiving
|
|
28
28
|
Classifier: Topic :: Communications
|
|
29
29
|
Classifier: Typing :: Typed
|
|
30
|
-
Requires-Python:
|
|
30
|
+
Requires-Python: <3.13,>=3.11
|
|
31
31
|
Description-Content-Type: text/markdown
|
|
32
32
|
License-File: LICENSE
|
|
33
33
|
Provides-Extra: dev
|
|
@@ -794,6 +794,20 @@ pytest -v
|
|
|
794
794
|
pytest -m "not slow"
|
|
795
795
|
```
|
|
796
796
|
|
|
797
|
+
### Build and Publish
|
|
798
|
+
|
|
799
|
+
1. Build the package:
|
|
800
|
+
|
|
801
|
+
```bash
|
|
802
|
+
python3 -m build --wheel
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
2. Publish the wheels:
|
|
806
|
+
|
|
807
|
+
```bash
|
|
808
|
+
twine upload dist/*
|
|
809
|
+
```
|
|
810
|
+
|
|
797
811
|
### Test Coverage
|
|
798
812
|
|
|
799
813
|
View the test coverage report:
|
|
@@ -753,6 +753,20 @@ pytest -v
|
|
|
753
753
|
pytest -m "not slow"
|
|
754
754
|
```
|
|
755
755
|
|
|
756
|
+
### Build and Publish
|
|
757
|
+
|
|
758
|
+
1. Build the package:
|
|
759
|
+
|
|
760
|
+
```bash
|
|
761
|
+
python3 -m build --wheel
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
2. Publish the wheels:
|
|
765
|
+
|
|
766
|
+
```bash
|
|
767
|
+
twine upload dist/*
|
|
768
|
+
```
|
|
769
|
+
|
|
756
770
|
### Test Coverage
|
|
757
771
|
|
|
758
772
|
View the test coverage report:
|
|
@@ -775,4 +789,4 @@ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for gui
|
|
|
775
789
|
|
|
776
790
|
- **Python**: >= 3.11
|
|
777
791
|
- **Runtime Dependencies**: None (zero dependencies!)
|
|
778
|
-
- **Development Dependencies**: pytest and plugins (see `pyproject.toml`)
|
|
792
|
+
- **Development Dependencies**: pytest and plugins (see `pyproject.toml`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tsrkit-types"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.9"
|
|
4
4
|
description = "Performant Python Typings library for type-safe binary serialization, JSON encoding, and data validation with zero dependencies"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "chainscore-labs", email = "hello@chainscore.finance"},
|
|
@@ -8,7 +8,7 @@ authors = [
|
|
|
8
8
|
]
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
|
-
requires-python = ">=3.11"
|
|
11
|
+
requires-python = ">=3.11,<3.13"
|
|
12
12
|
keywords = [
|
|
13
13
|
"serialization", "binary", "encoding", "types", "codable", "json",
|
|
14
14
|
"validation", "data-types", "type-safety", "zero-copy", "protocol",
|
|
@@ -56,3 +56,22 @@ def test_static_type_checker():
|
|
|
56
56
|
# This is fine
|
|
57
57
|
DataStore(a=UInt[8](19), b=UInt[16](288))
|
|
58
58
|
|
|
59
|
+
def test_int_sub():
|
|
60
|
+
a = UInt[8](100)
|
|
61
|
+
b = UInt[8](80)
|
|
62
|
+
assert a - b == UInt[8](20)
|
|
63
|
+
assert str(a - b) == 'U8(20)'
|
|
64
|
+
|
|
65
|
+
def test_int_compare_with_int():
|
|
66
|
+
a = UInt[8](100)
|
|
67
|
+
assert a > 80
|
|
68
|
+
assert a < 120
|
|
69
|
+
assert a >= 100
|
|
70
|
+
assert a <= 100
|
|
71
|
+
assert a != 101
|
|
72
|
+
assert a == 100
|
|
73
|
+
assert a != 101
|
|
74
|
+
|
|
75
|
+
def test_int_min_max():
|
|
76
|
+
assert min(UInt[8](100), UInt[8](80)) == UInt[8](80)
|
|
77
|
+
assert max(UInt[8](100), UInt[8](80)) == UInt[8](100)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from tsrkit_types.integers import Int, Uint
|
|
2
3
|
from tsrkit_types.itf.codable import Codable
|
|
3
4
|
from tsrkit_types.string import String
|
|
4
5
|
from tsrkit_types.struct import struct, structure
|
|
@@ -57,6 +58,6 @@ def test_struct_inheritance():
|
|
|
57
58
|
name: String
|
|
58
59
|
age: Uint[8] = field(metadata={"default": Uint[8](0)})
|
|
59
60
|
|
|
60
|
-
p = Person(name=String("John"), age=
|
|
61
|
+
p = Person(name=String("John"), age=Int[Literal[8]](30))
|
|
61
62
|
assert isinstance(p, Codable)
|
|
62
|
-
assert isinstance(p, Person)
|
|
63
|
+
assert isinstance(p, Person)
|
|
@@ -35,7 +35,7 @@ from .sequences import (
|
|
|
35
35
|
from .dictionary import Dictionary
|
|
36
36
|
|
|
37
37
|
# Bytes types
|
|
38
|
-
from .bytes import Bytes,
|
|
38
|
+
from .bytes import Bytes, Bytes16, Bytes32, Bytes64, Bytes128, Bytes256, Bytes512, Bytes1024
|
|
39
39
|
from .bytearray import ByteArray
|
|
40
40
|
|
|
41
41
|
# Bit types
|
|
@@ -76,7 +76,7 @@ __all__ = [
|
|
|
76
76
|
"Dictionary",
|
|
77
77
|
|
|
78
78
|
# Bytes types
|
|
79
|
-
"Bytes", "
|
|
79
|
+
"Bytes", "Bytes16", "Bytes32", "Bytes64", "Bytes128", "Bytes256", "Bytes512", "Bytes1024",
|
|
80
80
|
"ByteArray",
|
|
81
81
|
|
|
82
82
|
# Bit types
|
|
@@ -90,6 +90,6 @@ __all__ = [
|
|
|
90
90
|
]
|
|
91
91
|
|
|
92
92
|
# Version information
|
|
93
|
-
__version__ = "1.
|
|
93
|
+
__version__ = "0.1.9"
|
|
94
94
|
__author__ = "TSRKit Team"
|
|
95
95
|
__license__ = "MIT"
|
|
@@ -28,10 +28,12 @@ class ByteArray(bytearray, Codable, BytesMixin):
|
|
|
28
28
|
current_offset = offset
|
|
29
29
|
_len, _inc_offset = Uint.decode_from(buffer, offset)
|
|
30
30
|
current_offset += _inc_offset
|
|
31
|
+
if len(buffer[current_offset:current_offset+_len]) < _len:
|
|
32
|
+
raise TypeError("Insufficient buffer")
|
|
31
33
|
return cls(buffer[current_offset:current_offset+_len]), current_offset + _len - offset
|
|
32
34
|
|
|
33
35
|
# ---------------------------------------------------------------------------- #
|
|
34
36
|
# JSON Serialization #
|
|
35
37
|
# ---------------------------------------------------------------------------- #
|
|
36
38
|
# JSON methods inherited from BytesMixin
|
|
37
|
-
|
|
39
|
+
|
|
@@ -4,6 +4,7 @@ from tsrkit_types.integers import Uint
|
|
|
4
4
|
from tsrkit_types.itf.codable import Codable
|
|
5
5
|
from tsrkit_types.bytes_common import BytesMixin
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
class BytesCheckMeta(abc.ABCMeta):
|
|
8
9
|
"""Meta class to check if the instance is a bytes with the same key and value types"""
|
|
9
10
|
def __instancecheck__(cls, instance):
|
|
@@ -51,20 +52,36 @@ class Bytes(bytes, Codable, BytesMixin, metaclass=BytesCheckMeta):
|
|
|
51
52
|
def decode_from(cls, buffer: Union[bytes, bytearray, memoryview], offset: int = 0) -> Tuple["Bytes", int]:
|
|
52
53
|
current_offset = offset
|
|
53
54
|
_len = cls._length
|
|
55
|
+
|
|
54
56
|
if _len is None:
|
|
55
57
|
_len, _inc_offset = Uint.decode_from(buffer, offset)
|
|
56
58
|
current_offset += _inc_offset
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
|
|
60
|
+
if len(buffer[current_offset:current_offset+_len]) < _len:
|
|
61
|
+
raise TypeError("Insufficient buffer")
|
|
62
|
+
|
|
63
|
+
result = (cls(buffer[current_offset:current_offset+_len]), current_offset + _len - offset)
|
|
64
|
+
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
def __deepcopy__(self, memo):
|
|
68
|
+
# immutable; safe to reuse or create a new same-typed instance
|
|
69
|
+
existing = memo.get(id(self))
|
|
70
|
+
if existing is not None:
|
|
71
|
+
return existing
|
|
72
|
+
new = type(self)(bytes(self))
|
|
73
|
+
memo[id(self)] = new
|
|
74
|
+
return new
|
|
75
|
+
|
|
59
76
|
# ---------------------------------------------------------------------------- #
|
|
60
77
|
# JSON Serialization #
|
|
61
78
|
# ---------------------------------------------------------------------------- #
|
|
62
79
|
# JSON methods inherited from BytesMixin
|
|
63
80
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
Bytes16 = Bytes[16]
|
|
82
|
+
Bytes32 = Bytes[32]
|
|
83
|
+
Bytes64 = Bytes[64]
|
|
84
|
+
Bytes128 = Bytes[128]
|
|
85
|
+
Bytes256 = Bytes[256]
|
|
86
|
+
Bytes512 = Bytes[512]
|
|
87
|
+
Bytes1024 = Bytes[1024]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common functionality for Bytes and ByteArray types.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Union
|
|
6
|
+
|
|
7
|
+
# Global lookup tables for maximum performance - initialized once
|
|
8
|
+
_BYTE_TO_BITS_MSB = []
|
|
9
|
+
_BITS_TO_BYTE_MSB = {}
|
|
10
|
+
_TABLES_INITIALIZED = False
|
|
11
|
+
|
|
12
|
+
def _init_lookup_tables():
|
|
13
|
+
"""Initialize lookup tables once for optimal performance."""
|
|
14
|
+
global _BYTE_TO_BITS_MSB, _BITS_TO_BYTE_MSB, _TABLES_INITIALIZED
|
|
15
|
+
if not _TABLES_INITIALIZED:
|
|
16
|
+
for i in range(256):
|
|
17
|
+
# Convert byte to 8 bits (MSB first)
|
|
18
|
+
bits = [(i >> (7 - j)) & 1 for j in range(8)]
|
|
19
|
+
_BYTE_TO_BITS_MSB.append(bits)
|
|
20
|
+
# Reverse lookup: bits tuple to byte value
|
|
21
|
+
_BITS_TO_BYTE_MSB[tuple(bits)] = i
|
|
22
|
+
_TABLES_INITIALIZED = True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BytesMixin:
|
|
26
|
+
"""Mixin providing common functionality for bytes-like types."""
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_bits(cls, bits: list[bool], bit_order: str = "msb"):
|
|
30
|
+
"""Convert a list of bits to bytes with specified bit order."""
|
|
31
|
+
# Fast path for MSB (most common case) using lookup tables
|
|
32
|
+
if bit_order == "msb":
|
|
33
|
+
_init_lookup_tables()
|
|
34
|
+
|
|
35
|
+
# Convert and pad to multiple of 8
|
|
36
|
+
int_bits = [int(bool(b)) for b in bits]
|
|
37
|
+
pad = (8 - len(int_bits) % 8) % 8
|
|
38
|
+
int_bits.extend([0] * pad)
|
|
39
|
+
|
|
40
|
+
result = []
|
|
41
|
+
for i in range(0, len(int_bits), 8):
|
|
42
|
+
byte_bits = tuple(int_bits[i:i+8])
|
|
43
|
+
result.append(_BITS_TO_BYTE_MSB[byte_bits])
|
|
44
|
+
return cls(bytes(result))
|
|
45
|
+
|
|
46
|
+
# LSB implementation
|
|
47
|
+
elif bit_order == "lsb":
|
|
48
|
+
int_bits = [int(bool(b)) for b in bits]
|
|
49
|
+
pad = (8 - len(int_bits) % 8) % 8
|
|
50
|
+
int_bits.extend([0] * pad)
|
|
51
|
+
|
|
52
|
+
result = []
|
|
53
|
+
for i in range(0, len(int_bits), 8):
|
|
54
|
+
byte_bits = int_bits[i:i + 8]
|
|
55
|
+
val = 0
|
|
56
|
+
for bit in reversed(byte_bits):
|
|
57
|
+
val = (val << 1) | bit
|
|
58
|
+
result.append(val)
|
|
59
|
+
return cls(bytes(result))
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError(f"Unknown bit_order: {bit_order}")
|
|
62
|
+
|
|
63
|
+
def to_bits(self, bit_order: str = "msb") -> list[bool]:
|
|
64
|
+
"""Convert bytes to a list of bits with specified bit order."""
|
|
65
|
+
# Fast path for MSB using lookup table
|
|
66
|
+
if bit_order == "msb":
|
|
67
|
+
_init_lookup_tables()
|
|
68
|
+
|
|
69
|
+
result = []
|
|
70
|
+
for byte in self:
|
|
71
|
+
result.extend(_BYTE_TO_BITS_MSB[byte])
|
|
72
|
+
return [bool(b) for b in result]
|
|
73
|
+
|
|
74
|
+
# LSB implementation
|
|
75
|
+
elif bit_order == "lsb":
|
|
76
|
+
bits = []
|
|
77
|
+
for byte in self:
|
|
78
|
+
bits.extend([bool((byte >> i) & 1) for i in range(8)])
|
|
79
|
+
return bits
|
|
80
|
+
else:
|
|
81
|
+
raise ValueError(f"Unknown bit_order: {bit_order}")
|
|
82
|
+
|
|
83
|
+
def to_json(self):
|
|
84
|
+
"""Convert bytes to hex string for JSON serialization."""
|
|
85
|
+
return self.hex()
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_json(cls, data: str):
|
|
89
|
+
"""Create instance from hex string."""
|
|
90
|
+
data = data.replace("0x", "")
|
|
91
|
+
return cls(bytes.fromhex(data))
|
|
92
|
+
|
|
93
|
+
def __str__(self):
|
|
94
|
+
return f"{self.__class__.__name__}({self.hex()})"
|
|
95
|
+
|
|
96
|
+
def slice_bits(self, start_bit: int, end_bit: int) -> list[bool]:
|
|
97
|
+
"""Extract bit slice efficiently without converting entire byte array."""
|
|
98
|
+
if start_bit >= end_bit:
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
start_byte = start_bit // 8
|
|
102
|
+
end_byte = (end_bit - 1) // 8 + 1
|
|
103
|
+
start_bit_offset = start_bit % 8
|
|
104
|
+
|
|
105
|
+
if start_byte >= len(self):
|
|
106
|
+
return [False] * (end_bit - start_bit)
|
|
107
|
+
|
|
108
|
+
# Extract relevant bytes and convert only what we need
|
|
109
|
+
relevant_bytes = self[start_byte:min(end_byte, len(self))]
|
|
110
|
+
|
|
111
|
+
# Use global lookup table for optimal performance
|
|
112
|
+
_init_lookup_tables()
|
|
113
|
+
|
|
114
|
+
result = []
|
|
115
|
+
for byte in relevant_bytes:
|
|
116
|
+
result.extend(_BYTE_TO_BITS_MSB[byte])
|
|
117
|
+
|
|
118
|
+
# Slice to exact range
|
|
119
|
+
local_start = start_bit_offset if start_byte < len(self) else 0
|
|
120
|
+
local_end = len(result) if end_byte > len(self) else (end_bit - start_byte * 8)
|
|
121
|
+
|
|
122
|
+
bits = result[local_start:local_end]
|
|
123
|
+
|
|
124
|
+
# Pad with False if we ran out of data
|
|
125
|
+
if len(bits) < (end_bit - start_bit):
|
|
126
|
+
bits.extend([False] * (end_bit - start_bit - len(bits)))
|
|
127
|
+
|
|
128
|
+
return [bool(b) for b in bits]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def validate_bit_order(bit_order: str) -> None:
|
|
132
|
+
"""Validate bit order parameter."""
|
|
133
|
+
if bit_order not in ("msb", "lsb"):
|
|
134
|
+
raise ValueError(f"Unknown bit_order: {bit_order}. Must be 'msb' or 'lsb'")
|
|
@@ -3,7 +3,7 @@ from typing import ClassVar, Optional, Union, Tuple, Any
|
|
|
3
3
|
from tsrkit_types.integers import Uint
|
|
4
4
|
from tsrkit_types.itf.codable import Codable
|
|
5
5
|
|
|
6
|
-
ChoiceType = Tuple[Optional[str], type]
|
|
6
|
+
ChoiceType = Union[Tuple[Optional[str], type], type]
|
|
7
7
|
|
|
8
8
|
class Choice(Codable):
|
|
9
9
|
"""
|
|
@@ -43,7 +43,7 @@ class Choice(Codable):
|
|
|
43
43
|
def _choice_keys(self) -> Tuple[Optional[str]]:
|
|
44
44
|
return tuple(self._choice[0] for self._choice in self._opt_types)
|
|
45
45
|
|
|
46
|
-
def __class_getitem__(cls, opt_t: Tuple[type]
|
|
46
|
+
def __class_getitem__(cls, opt_t: Union[Tuple[type], type]):
|
|
47
47
|
_opt_types = []
|
|
48
48
|
if isinstance(opt_t, type):
|
|
49
49
|
_opt_types.append((None, opt_t))
|
|
@@ -68,6 +68,9 @@ class Choice(Codable):
|
|
|
68
68
|
def unwrap(self) -> Any:
|
|
69
69
|
return self._value
|
|
70
70
|
|
|
71
|
+
def get_key(self):
|
|
72
|
+
return self._choice_key
|
|
73
|
+
|
|
71
74
|
def __repr__(self) -> str:
|
|
72
75
|
return f"{self.__class__.__name__}({self._value!r})"
|
|
73
76
|
|
|
@@ -95,7 +98,7 @@ class Choice(Codable):
|
|
|
95
98
|
return self._value.to_json() if not self._choice_key else {self._choice_key: self._value.to_json()}
|
|
96
99
|
|
|
97
100
|
@classmethod
|
|
98
|
-
def from_json(cls, data: dict
|
|
101
|
+
def from_json(cls, data: Union[dict, Any]) -> "Choice":
|
|
99
102
|
if isinstance(data, dict):
|
|
100
103
|
opt_type = next((x for x in cls._opt_types if x[0] == list(data.keys())[0]), None)
|
|
101
104
|
if opt_type is None:
|
|
@@ -129,4 +132,4 @@ class Choice(Codable):
|
|
|
129
132
|
tag, tag_size = Uint.decode_from(buffer, offset)
|
|
130
133
|
value, val_size = cls._opt_types[tag][1].decode_from(buffer, offset+tag_size)
|
|
131
134
|
|
|
132
|
-
return cls(value), tag_size+val_size
|
|
135
|
+
return cls(value, key=cls._opt_types[tag][0]), tag_size+val_size
|
|
@@ -126,7 +126,7 @@ class Dictionary(dict, Codable, Generic[K, V], metaclass=DictCheckMeta):
|
|
|
126
126
|
def encode_into(self, buffer: bytearray, offset: int = 0) -> int:
|
|
127
127
|
current_offset = offset
|
|
128
128
|
current_offset += Uint(len(self)).encode_into(buffer, current_offset)
|
|
129
|
-
for k, v in sorted(self.items(), key=lambda x: x[0]
|
|
129
|
+
for k, v in sorted(self.items(), key=lambda x: x[0]):
|
|
130
130
|
current_offset += k.encode_into(buffer, current_offset)
|
|
131
131
|
current_offset += v.encode_into(buffer, current_offset)
|
|
132
132
|
return current_offset - offset
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
from decimal import Decimal
|
|
3
3
|
import math
|
|
4
|
-
from typing import Any, Tuple, Union, Callable
|
|
4
|
+
from typing import Any, Optional, Tuple, Union, Callable
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from typing import Self
|
|
8
|
+
except ImportError:
|
|
9
|
+
# For Python < 3.11, use TYPE_CHECKING and forward reference
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from typing import Self
|
|
13
|
+
else:
|
|
14
|
+
Self = "Uint" # Forward reference string
|
|
15
|
+
|
|
5
16
|
from tsrkit_types.itf.codable import Codable
|
|
6
17
|
|
|
7
18
|
|
|
@@ -11,7 +22,7 @@ class IntCheckMeta(abc.ABCMeta):
|
|
|
11
22
|
return isinstance(instance, int) and getattr(instance, "byte_size", 0) == cls.byte_size
|
|
12
23
|
|
|
13
24
|
|
|
14
|
-
class
|
|
25
|
+
class Int(int, Codable, metaclass=IntCheckMeta):
|
|
15
26
|
"""
|
|
16
27
|
Unsigned integer type.
|
|
17
28
|
|
|
@@ -52,23 +63,44 @@ class Uint(int, Codable, metaclass=IntCheckMeta):
|
|
|
52
63
|
# If the byte_size is set, the integer is fixed size.
|
|
53
64
|
# Otherwise, the integer is General Integer (supports up to 2**64 - 1)
|
|
54
65
|
byte_size: int = 0
|
|
66
|
+
signed = False
|
|
67
|
+
_bound = 1 << 64
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def __class_getitem__(cls, data: Optional[Union[int, tuple, bool]]):
|
|
71
|
+
"""
|
|
72
|
+
Args:
|
|
73
|
+
data: either byte_size or (byte_size, signed)
|
|
74
|
+
"""
|
|
75
|
+
if data == None:
|
|
76
|
+
size, signed = 0, False
|
|
77
|
+
# If we have a single value arg - wither byte_size or signed
|
|
78
|
+
elif not isinstance(data, tuple):
|
|
79
|
+
if isinstance(data, int):
|
|
80
|
+
size, signed = data, False
|
|
81
|
+
else:
|
|
82
|
+
size, signed = 0, bool(data)
|
|
83
|
+
else:
|
|
84
|
+
size, signed = data
|
|
55
85
|
|
|
56
|
-
|
|
57
|
-
|
|
86
|
+
return type(f"U{size}" if size else "Int", (cls,), {
|
|
87
|
+
"byte_size": size // 8,
|
|
88
|
+
"signed": signed,
|
|
89
|
+
"_bound": 1 << size if size > 0 else 1 << 64
|
|
90
|
+
})
|
|
58
91
|
|
|
59
92
|
def __new__(cls, value: Any):
|
|
60
93
|
value = int(value)
|
|
61
94
|
if cls.byte_size > 0:
|
|
62
|
-
|
|
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}]")
|
|
95
|
+
max_v = (cls._bound // 2 if cls.signed else cls._bound) - 1
|
|
96
|
+
min_v = -1 * cls._bound // 2 if cls.signed else 0
|
|
68
97
|
else:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
98
|
+
min_v = -1 * cls._bound // 2 if cls.signed else 0
|
|
99
|
+
max_v = (cls._bound // 2 if cls.signed else cls._bound) - 1
|
|
100
|
+
|
|
101
|
+
if not (min_v <= value <= max_v):
|
|
102
|
+
raise ValueError(f"Int: {cls.__name__} out of range: {value!r} "
|
|
103
|
+
f"not in [{min_v}, {max_v}]")
|
|
72
104
|
return super().__new__(cls, value)
|
|
73
105
|
|
|
74
106
|
def __repr__(self):
|
|
@@ -105,11 +137,11 @@ class Uint(int, Codable, metaclass=IntCheckMeta):
|
|
|
105
137
|
# ---------------------------------------------------------------------------- #
|
|
106
138
|
# JSON Serde #
|
|
107
139
|
# ---------------------------------------------------------------------------- #
|
|
108
|
-
def to_json(self) ->
|
|
140
|
+
def to_json(self) -> int:
|
|
109
141
|
return int(self)
|
|
110
142
|
|
|
111
143
|
@classmethod
|
|
112
|
-
def from_json(cls, json_str: str) -> "
|
|
144
|
+
def from_json(cls, json_str: str) -> "Int":
|
|
113
145
|
return cls(int(json_str))
|
|
114
146
|
|
|
115
147
|
# ---------------------------------------------------------------------------- #
|
|
@@ -118,20 +150,24 @@ class Uint(int, Codable, metaclass=IntCheckMeta):
|
|
|
118
150
|
@staticmethod
|
|
119
151
|
def l(x):
|
|
120
152
|
return math.floor(Decimal(x).ln() / (Decimal(7) * Decimal(2).ln()))
|
|
121
|
-
|
|
153
|
+
|
|
154
|
+
def to_unsigned(self) -> "Int":
|
|
155
|
+
if not self.signed: return self
|
|
156
|
+
return int(self) + (self._bound // 2)
|
|
122
157
|
|
|
123
158
|
def encode_size(self) -> int:
|
|
124
159
|
if self.byte_size > 0:
|
|
125
160
|
return self.byte_size
|
|
126
161
|
else:
|
|
127
|
-
|
|
162
|
+
value = self.to_unsigned()
|
|
163
|
+
if value < 2**7:
|
|
128
164
|
return 1
|
|
129
|
-
elif
|
|
165
|
+
elif value < 2 ** (7 * 9):
|
|
130
166
|
return 1 + self.l(self)
|
|
131
|
-
elif
|
|
167
|
+
elif value < 2**64:
|
|
132
168
|
return 9
|
|
133
169
|
else:
|
|
134
|
-
raise ValueError("Value too large for encoding. General
|
|
170
|
+
raise ValueError("Value too large for encoding. General Int support up to 2**64 - 1")
|
|
135
171
|
|
|
136
172
|
def encode_into(self, buffer: bytearray, offset: int = 0) -> int:
|
|
137
173
|
if self.byte_size > 0:
|
|
@@ -206,7 +242,7 @@ class Uint(int, Codable, metaclass=IntCheckMeta):
|
|
|
206
242
|
raise ValueError(f"Invalid bit order: {bit_order}")
|
|
207
243
|
|
|
208
244
|
@classmethod
|
|
209
|
-
def from_bits(cls, bits: list[bool], bit_order: str = "msb") -> "
|
|
245
|
+
def from_bits(cls, bits: list[bool], bit_order: str = "msb") -> "Int":
|
|
210
246
|
"""Convert bits to an int"""
|
|
211
247
|
if bit_order == "msb":
|
|
212
248
|
return cls(int("".join(str(int(b)) for b in bits), 2))
|
|
@@ -214,7 +250,8 @@ class Uint(int, Codable, metaclass=IntCheckMeta):
|
|
|
214
250
|
return cls(int("".join(str(int(b)) for b in reversed(bits)), 2))
|
|
215
251
|
|
|
216
252
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
253
|
+
Uint = Int
|
|
254
|
+
U8 = Int[8]
|
|
255
|
+
U16 = Int[16]
|
|
256
|
+
U32 = Int[32]
|
|
257
|
+
U64 = Int[64]
|
|
@@ -80,4 +80,7 @@ class Codable(ABC, Generic[T]):
|
|
|
80
80
|
offset: The offset at which to start encoding the value.
|
|
81
81
|
"""
|
|
82
82
|
if len(buffer) - offset < size:
|
|
83
|
-
raise ValueError("Buffer too small to encode value")
|
|
83
|
+
raise ValueError("Buffer too small to encode value")
|
|
84
|
+
|
|
85
|
+
def __reduce__(self):
|
|
86
|
+
return (self.__class__.decode, (self.encode(),))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import Optional, Tuple, Union
|
|
2
|
+
from tsrkit_types.itf.codable import Codable
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
class NullType:
|
|
4
|
+
class NullType(Codable):
|
|
5
5
|
def __repr__(self):
|
|
6
6
|
return "Null"
|
|
7
7
|
|
|
@@ -40,4 +40,4 @@ class NullType:
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
Null = NullType()
|
|
43
|
+
Null = NullType()
|
|
@@ -18,7 +18,7 @@ class Option(Choice, Generic[T]):
|
|
|
18
18
|
(Option,),
|
|
19
19
|
{"_opt_types": ((None, NullType), (None, opt_t))})
|
|
20
20
|
|
|
21
|
-
def __init__(self, val: T|NullType = Null):
|
|
21
|
+
def __init__(self, val: T|NullType = Null, key = None):
|
|
22
22
|
super().__init__(val)
|
|
23
23
|
|
|
24
24
|
def set(self, value: T|NullType = Null, key: Optional[str] = None):
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from dataclasses import dataclass, fields
|
|
2
|
-
from typing import Any, Tuple, Union
|
|
2
|
+
from typing import Any, Tuple, Union, dataclass_transform
|
|
3
3
|
from tsrkit_types.itf.codable import Codable
|
|
4
4
|
from tsrkit_types.null import NullType
|
|
5
5
|
from tsrkit_types.option import Option
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
@dataclass_transform()
|
|
8
9
|
def structure(_cls=None, *, frozen=False, **kwargs):
|
|
9
10
|
"""Extension of dataclass to support serialization and json operations.
|
|
10
11
|
|
|
@@ -66,7 +67,6 @@ def structure(_cls=None, *, frozen=False, **kwargs):
|
|
|
66
67
|
init_data[field.name] = field.type.from_json(v)
|
|
67
68
|
return cls(**init_data)
|
|
68
69
|
|
|
69
|
-
|
|
70
70
|
new_cls.__init__ = __init__
|
|
71
71
|
|
|
72
72
|
# Only overwrite if the method is not already defined
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tsrkit-types
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Performant Python Typings library for type-safe binary serialization, JSON encoding, and data validation with zero dependencies
|
|
5
5
|
Author-email: chainscore-labs <hello@chainscore.finance>, prasad-kumkar <prasad@chainscore.finance>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -27,7 +27,7 @@ Classifier: Topic :: Utilities
|
|
|
27
27
|
Classifier: Topic :: System :: Archiving
|
|
28
28
|
Classifier: Topic :: Communications
|
|
29
29
|
Classifier: Typing :: Typed
|
|
30
|
-
Requires-Python:
|
|
30
|
+
Requires-Python: <3.13,>=3.11
|
|
31
31
|
Description-Content-Type: text/markdown
|
|
32
32
|
License-File: LICENSE
|
|
33
33
|
Provides-Extra: dev
|
|
@@ -794,6 +794,20 @@ pytest -v
|
|
|
794
794
|
pytest -m "not slow"
|
|
795
795
|
```
|
|
796
796
|
|
|
797
|
+
### Build and Publish
|
|
798
|
+
|
|
799
|
+
1. Build the package:
|
|
800
|
+
|
|
801
|
+
```bash
|
|
802
|
+
python3 -m build --wheel
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
2. Publish the wheels:
|
|
806
|
+
|
|
807
|
+
```bash
|
|
808
|
+
twine upload dist/*
|
|
809
|
+
```
|
|
810
|
+
|
|
797
811
|
### Test Coverage
|
|
798
812
|
|
|
799
813
|
View the test coverage report:
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Common functionality for Bytes and ByteArray types.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from typing import Union
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class BytesMixin:
|
|
9
|
-
"""Mixin providing common functionality for bytes-like types."""
|
|
10
|
-
|
|
11
|
-
@classmethod
|
|
12
|
-
def from_bits(cls, bits: list[bool], bit_order: str = "msb"):
|
|
13
|
-
"""Convert a list of bits to bytes with specified bit order."""
|
|
14
|
-
# Sanitize input: make sure bits are 0 or 1
|
|
15
|
-
bits = [int(bool(b)) for b in bits]
|
|
16
|
-
n = len(bits)
|
|
17
|
-
# Pad with zeros to multiple of 8
|
|
18
|
-
pad = (8 - n % 8) % 8
|
|
19
|
-
bits += [0] * pad
|
|
20
|
-
|
|
21
|
-
byte_arr = []
|
|
22
|
-
for i in range(0, len(bits), 8):
|
|
23
|
-
byte_bits = bits[i:i + 8]
|
|
24
|
-
if bit_order == "msb":
|
|
25
|
-
# Most significant bit first
|
|
26
|
-
val = 0
|
|
27
|
-
for bit in byte_bits:
|
|
28
|
-
val = (val << 1) | bit
|
|
29
|
-
elif bit_order == "lsb":
|
|
30
|
-
# Least significant bit first
|
|
31
|
-
val = 0
|
|
32
|
-
for bit in reversed(byte_bits):
|
|
33
|
-
val = (val << 1) | bit
|
|
34
|
-
else:
|
|
35
|
-
raise ValueError(f"Unknown bit_order: {bit_order}")
|
|
36
|
-
byte_arr.append(val)
|
|
37
|
-
return cls(bytes(byte_arr))
|
|
38
|
-
|
|
39
|
-
def to_bits(self, bit_order: str = "msb") -> list[bool]:
|
|
40
|
-
"""Convert bytes to a list of bits with specified bit order."""
|
|
41
|
-
bits = []
|
|
42
|
-
for byte in self:
|
|
43
|
-
if bit_order == "msb":
|
|
44
|
-
bits.extend([bool((byte >> i) & 1) for i in reversed(range(8))])
|
|
45
|
-
elif bit_order == "lsb":
|
|
46
|
-
bits.extend([bool((byte >> i) & 1) for i in range(8)])
|
|
47
|
-
else:
|
|
48
|
-
raise ValueError(f"Unknown bit_order: {bit_order}")
|
|
49
|
-
return bits
|
|
50
|
-
|
|
51
|
-
def to_json(self):
|
|
52
|
-
"""Convert bytes to hex string for JSON serialization."""
|
|
53
|
-
return self.hex()
|
|
54
|
-
|
|
55
|
-
@classmethod
|
|
56
|
-
def from_json(cls, data: str):
|
|
57
|
-
"""Create instance from hex string."""
|
|
58
|
-
data = data.replace("0x", "")
|
|
59
|
-
return cls(bytes.fromhex(data))
|
|
60
|
-
|
|
61
|
-
def __str__(self):
|
|
62
|
-
return f"{self.__class__.__name__}({self.hex()})"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def validate_bit_order(bit_order: str) -> None:
|
|
66
|
-
"""Validate bit order parameter."""
|
|
67
|
-
if bit_order not in ("msb", "lsb"):
|
|
68
|
-
raise ValueError(f"Unknown bit_order: {bit_order}. Must be 'msb' or 'lsb'")
|
|
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
|