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.
@@ -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
@@ -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