bytespec 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.
- bytespec/__init__.py +21 -0
- bytespec/base.py +52 -0
- bytespec/codecs/__init__.py +48 -0
- bytespec/codecs/_utils.py +16 -0
- bytespec/codecs/base.py +60 -0
- bytespec/codecs/bool.py +45 -0
- bytespec/codecs/bytes.py +105 -0
- bytespec/codecs/datetime.py +59 -0
- bytespec/codecs/enum.py +49 -0
- bytespec/codecs/float.py +17 -0
- bytespec/codecs/int.py +118 -0
- bytespec/codecs/list.py +77 -0
- bytespec/codecs/map.py +17 -0
- bytespec/codecs/model.py +40 -0
- bytespec/codecs/str.py +88 -0
- bytespec/codecs/uuid.py +36 -0
- bytespec/codecs/varint.py +122 -0
- bytespec/core.py +500 -0
- bytespec/enums.py +14 -0
- bytespec/errors.py +29 -0
- bytespec/headers.py +143 -0
- bytespec/missing.py +17 -0
- bytespec/models.py +47 -0
- bytespec/py.typed +0 -0
- bytespec/resolver.py +196 -0
- bytespec/resolvers.py +114 -0
- bytespec/schema.py +35 -0
- bytespec/types/__init__.py +25 -0
- bytespec/types/spec.py +75 -0
- bytespec-0.1.0.dist-info/METADATA +103 -0
- bytespec-0.1.0.dist-info/RECORD +33 -0
- bytespec-0.1.0.dist-info/WHEEL +4 -0
- bytespec-0.1.0.dist-info/licenses/LICENSE +21 -0
bytespec/codecs/model.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Copyright (c) 2026 ink-developer
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from typing_extensions import override
|
|
8
|
+
|
|
9
|
+
from bytespec.codecs import ICodec
|
|
10
|
+
from bytespec.enums import ByteOrder
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from bytespec.core import ProtoModel
|
|
14
|
+
from bytespec.enums import ByteOrder
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ModelCodec(ICodec["ProtoModel"]):
|
|
18
|
+
"""A nested model with its own framing, excluding Constructor, and its own byte order.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
model_type: The concrete class to read via decode_from(). Subclasses are not selected
|
|
22
|
+
automatically by constructor.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, model_type: type[ProtoModel]) -> None:
|
|
26
|
+
self.model_type = model_type
|
|
27
|
+
|
|
28
|
+
@override
|
|
29
|
+
def encode(self, value: ProtoModel, _: ByteOrder) -> bytes:
|
|
30
|
+
"""Write value using its own settings, skipping Constructor elements."""
|
|
31
|
+
return value.encode(include_constructor=False)
|
|
32
|
+
|
|
33
|
+
@override
|
|
34
|
+
def decode(self, buffer: bytes, byte_order: ByteOrder, offset: int) -> tuple[ProtoModel, int]:
|
|
35
|
+
"""Read model_type and return the instance with its absolute end offset.
|
|
36
|
+
|
|
37
|
+
The outer byte_order is ignored: model_type settings are used. Errors propagate from
|
|
38
|
+
model_type.decode_from().
|
|
39
|
+
"""
|
|
40
|
+
return self.model_type.decode_from(buffer, offset, expect_constructor=False)
|
bytespec/codecs/str.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Copyright (c) 2026 ink-developer
|
|
2
|
+
|
|
3
|
+
import codecs
|
|
4
|
+
|
|
5
|
+
from typing_extensions import override
|
|
6
|
+
|
|
7
|
+
from bytespec.codecs import ICodec
|
|
8
|
+
from bytespec.enums import ByteOrder
|
|
9
|
+
from bytespec.errors import DecodeError, EncodeError, SchemaError
|
|
10
|
+
from bytespec.models import PrefixLength
|
|
11
|
+
|
|
12
|
+
from ._utils import check_available
|
|
13
|
+
from .map import UINT_CODEC_MAPPING
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StrCodec(ICodec[str]):
|
|
17
|
+
"""Text prefixed with the length in encoded bytes, not characters.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
prefix_length: Prefix size: 1, 2, 4, or 8 bytes, or ``VarUInt``. Defaults to 4 bytes.
|
|
21
|
+
encoding: Python encoding, UTF-8 by default.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
SchemaError: Unsupported prefix or unknown encoding.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, prefix_length: PrefixLength = 4, encoding: str = "utf-8") -> None:
|
|
28
|
+
self.prefix_length = prefix_length
|
|
29
|
+
self.encoding = encoding
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
codecs.lookup(encoding)
|
|
33
|
+
except LookupError as exc:
|
|
34
|
+
raise SchemaError(f"StrCodec: invalid encoding {encoding!r}") from exc
|
|
35
|
+
|
|
36
|
+
codec = UINT_CODEC_MAPPING.get(prefix_length)
|
|
37
|
+
if codec is None:
|
|
38
|
+
raise SchemaError(f"StrCodec: invalid length prefix {prefix_length!r}")
|
|
39
|
+
|
|
40
|
+
self.codec = codec
|
|
41
|
+
|
|
42
|
+
super().__init__()
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
def encode(self, value: str, byte_order: ByteOrder) -> bytes:
|
|
46
|
+
"""Encode text and prefix it with its byte length.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
EncodeError: The text cannot be represented in the encoding, or its length does not fit
|
|
50
|
+
in the prefix.
|
|
51
|
+
SchemaError: The selected Python codec is not a text encoding.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
encoded = value.encode(self.encoding)
|
|
55
|
+
except UnicodeError as exc:
|
|
56
|
+
raise EncodeError(f"StrCodec ({self.encoding}): cannot encode {value!r}") from exc
|
|
57
|
+
except LookupError as exc:
|
|
58
|
+
raise SchemaError(f"StrCodec: invalid text encoding {self.encoding!r}") from exc
|
|
59
|
+
return self.codec.encode(len(encoded), byte_order) + encoded
|
|
60
|
+
|
|
61
|
+
@override
|
|
62
|
+
def decode(
|
|
63
|
+
self,
|
|
64
|
+
buffer: bytes,
|
|
65
|
+
byte_order: ByteOrder,
|
|
66
|
+
offset: int,
|
|
67
|
+
) -> tuple[str, int]:
|
|
68
|
+
"""Read a string and return it with its absolute end offset.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
DecodeError: There are not enough bytes, or the prefix or text is invalid.
|
|
72
|
+
SchemaError: The selected Python codec is not a text encoding.
|
|
73
|
+
ValueError: Negative offset.
|
|
74
|
+
"""
|
|
75
|
+
length, start = self.codec.decode(buffer, byte_order, offset)
|
|
76
|
+
end = start + length
|
|
77
|
+
|
|
78
|
+
check_available(buffer, start, length, codec=type(self).__name__)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
value = buffer[start:end].decode(self.encoding)
|
|
82
|
+
except UnicodeError as exc:
|
|
83
|
+
raise DecodeError(
|
|
84
|
+
f"StrCodec ({self.encoding}) at offset {start}: invalid string bytes"
|
|
85
|
+
) from exc
|
|
86
|
+
except LookupError as exc:
|
|
87
|
+
raise SchemaError(f"StrCodec: invalid text encoding {self.encoding!r}") from exc
|
|
88
|
+
return value, end
|
bytespec/codecs/uuid.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Copyright (c) 2026 ink-developer
|
|
2
|
+
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from typing_extensions import override
|
|
6
|
+
|
|
7
|
+
from bytespec.codecs import ICodec
|
|
8
|
+
from bytespec.enums import ByteOrder
|
|
9
|
+
|
|
10
|
+
from .bytes import FixedBytesCodec
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UUIDCodec(ICodec[UUID]):
|
|
14
|
+
"""A UUID as exactly 16 bytes from UUID.bytes, without a length prefix.
|
|
15
|
+
|
|
16
|
+
The model's byte order does not switch this representation to bytes_le.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self.codec = FixedBytesCodec(length=16)
|
|
21
|
+
|
|
22
|
+
@override
|
|
23
|
+
def encode(self, value: UUID, byte_order: ByteOrder) -> bytes:
|
|
24
|
+
"""Return the 16 bytes of value.bytes regardless of byte_order."""
|
|
25
|
+
return self.codec.encode(value.bytes, byte_order)
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
def decode(self, buffer: bytes, byte_order: ByteOrder, offset: int) -> tuple[UUID, int]:
|
|
29
|
+
"""Read a UUID and return it with the position offset + 16.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
DecodeError: There are not enough bytes for a UUID.
|
|
33
|
+
ValueError: Negative offset.
|
|
34
|
+
"""
|
|
35
|
+
value, offset = self.codec.decode(buffer, byte_order, offset)
|
|
36
|
+
return UUID(bytes=value), offset
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Copyright (c) 2026 ink-developer
|
|
2
|
+
|
|
3
|
+
from typing_extensions import override
|
|
4
|
+
|
|
5
|
+
from bytespec.enums import ByteOrder
|
|
6
|
+
from bytespec.errors import DecodeError, EncodeError
|
|
7
|
+
|
|
8
|
+
from ._utils import check_available
|
|
9
|
+
from .base import ICodec
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VarUIntCodec(ICodec[int]):
|
|
13
|
+
"""Canonical unsigned varint: 0 .. 2**64 - 1, 1 to 10 bytes.
|
|
14
|
+
|
|
15
|
+
Groups of 7 bits are written starting with the least significant. The model's byte order does
|
|
16
|
+
not affect this representation.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@override
|
|
20
|
+
def encode(self, value: int, _: ByteOrder) -> bytes:
|
|
21
|
+
"""Write a nonnegative integer in canonical varint form.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
EncodeError: The value is outside the UInt64 range.
|
|
25
|
+
"""
|
|
26
|
+
if value < 0:
|
|
27
|
+
raise EncodeError(f"VarUIntCodec cannot encode negative value {value}")
|
|
28
|
+
|
|
29
|
+
if value > 2**64 - 1:
|
|
30
|
+
raise EncodeError(f"VarUIntCodec overflow: {value}")
|
|
31
|
+
|
|
32
|
+
result = bytearray()
|
|
33
|
+
|
|
34
|
+
while True:
|
|
35
|
+
byte = value & 0x7F
|
|
36
|
+
value >>= 7
|
|
37
|
+
|
|
38
|
+
if value:
|
|
39
|
+
byte |= 0x80
|
|
40
|
+
|
|
41
|
+
result.append(byte)
|
|
42
|
+
|
|
43
|
+
if not value:
|
|
44
|
+
break
|
|
45
|
+
|
|
46
|
+
return bytes(result)
|
|
47
|
+
|
|
48
|
+
@override
|
|
49
|
+
def decode(self, buffer: bytes, _: ByteOrder, offset: int) -> tuple[int, int]:
|
|
50
|
+
"""Read a varint and return the number with its absolute end offset.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
DecodeError: The varint is truncated, overflows, or is not in canonical form.
|
|
54
|
+
ValueError: Negative offset.
|
|
55
|
+
"""
|
|
56
|
+
value = 0
|
|
57
|
+
count = 0
|
|
58
|
+
shift = 0
|
|
59
|
+
|
|
60
|
+
while True:
|
|
61
|
+
check_available(buffer, offset, 1, codec=type(self).__name__)
|
|
62
|
+
|
|
63
|
+
byte = buffer[offset]
|
|
64
|
+
offset += 1
|
|
65
|
+
count += 1
|
|
66
|
+
|
|
67
|
+
if count == 10:
|
|
68
|
+
if byte & 0x80:
|
|
69
|
+
raise DecodeError(f"VarUIntCodec at offset {offset - 1}: varint is too long")
|
|
70
|
+
|
|
71
|
+
if byte & 0x7F > 1:
|
|
72
|
+
raise DecodeError(
|
|
73
|
+
f"VarUIntCodec at offset {offset - 1}: overflow byte {byte:#x}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
data = byte & 0x7F
|
|
77
|
+
value |= data << shift
|
|
78
|
+
|
|
79
|
+
if not byte & 0x80:
|
|
80
|
+
if count > 1 and data == 0:
|
|
81
|
+
raise DecodeError(f"VarUIntCodec at offset {offset - 1}: non-canonical varint")
|
|
82
|
+
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
shift += 7
|
|
86
|
+
|
|
87
|
+
return value, offset
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class VarIntCodec(VarUIntCodec):
|
|
91
|
+
"""Signed varint: ZigZag + unsigned varint in the Int64 range.
|
|
92
|
+
|
|
93
|
+
Uses 1–10 bytes regardless of the model's byte order.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
@override
|
|
97
|
+
def encode(self, value: int, _: ByteOrder) -> bytes:
|
|
98
|
+
"""Apply ZigZag and write the integer as an unsigned varint.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
EncodeError: The value is outside the range -2**63 .. 2**63 - 1.
|
|
102
|
+
"""
|
|
103
|
+
if value >= 0:
|
|
104
|
+
value *= 2
|
|
105
|
+
else:
|
|
106
|
+
value = abs(value) * 2 - 1
|
|
107
|
+
|
|
108
|
+
return super().encode(value, _)
|
|
109
|
+
|
|
110
|
+
@override
|
|
111
|
+
def decode(self, buffer: bytes, _: ByteOrder, offset: int) -> tuple[int, int]:
|
|
112
|
+
"""Read a ZigZag varint and return the number with its absolute end offset.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
DecodeError: The varint is truncated, overflows, or is not in canonical form.
|
|
116
|
+
ValueError: Negative offset.
|
|
117
|
+
"""
|
|
118
|
+
encoded, offset = super().decode(buffer, _, offset)
|
|
119
|
+
|
|
120
|
+
value = encoded // 2 if encoded & 1 == 0 else -(encoded // 2) - 1
|
|
121
|
+
|
|
122
|
+
return value, offset
|