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 ADDED
@@ -0,0 +1,21 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .core import ProtoModel, field
6
+ from .enums import ByteOrder
7
+ from .errors import BytespecError, DecodeError, EncodeError, SchemaError
8
+ from .headers import Constructor, Flags, PayloadLength
9
+
10
+ __all__ = [
11
+ "ByteOrder",
12
+ "BytespecError",
13
+ "Constructor",
14
+ "DecodeError",
15
+ "EncodeError",
16
+ "Flags",
17
+ "PayloadLength",
18
+ "ProtoModel",
19
+ "SchemaError",
20
+ "field",
21
+ ]
bytespec/base.py ADDED
@@ -0,0 +1,52 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+ from types import NoneType, UnionType
4
+ from typing import Annotated, Any, ClassVar, Final, Union, get_args, get_origin
5
+
6
+ from bytespec.enums import ByteOrder
7
+ from bytespec.headers import Constructor, Flags, HeaderElement, PayloadLength
8
+ from bytespec.resolvers import CodecFactory
9
+ from bytespec.schema import ModelSchema
10
+
11
+ DEFAULT_HEADER: Final[tuple[HeaderElement, ...]] = (Constructor(2), Flags(8), PayloadLength(4))
12
+
13
+
14
+ class ProtoModelBase:
15
+ __constructor__: int = 0x1
16
+ __byte_order__: ByteOrder = ByteOrder.BIG
17
+ __header__: ClassVar[tuple[HeaderElement, ...]] = DEFAULT_HEADER
18
+ __schema__: ModelSchema
19
+
20
+ @classmethod
21
+ def _is_valid_value(cls, value: Any, annotation: Any) -> bool:
22
+ origin = get_origin(annotation)
23
+
24
+ if origin is Annotated:
25
+ base_type, *_ = get_args(annotation)
26
+ return cls._is_valid_value(value, base_type)
27
+
28
+ if origin in (Union, UnionType):
29
+ return any(
30
+ (arg is NoneType and value is None)
31
+ or (arg is not NoneType and cls._is_valid_value(value, arg))
32
+ for arg in get_args(annotation)
33
+ )
34
+
35
+ if origin is list:
36
+ return isinstance(value, list)
37
+
38
+ return isinstance(value, annotation)
39
+
40
+ @classmethod
41
+ def configure_codecs(cls) -> dict[type, CodecFactory]:
42
+ """Define additional codec selection rules for field types.
43
+
44
+ Override this classmethod in the model. Rules apply at class declaration, extending
45
+ inherited rules and replacing matching keys.
46
+
47
+ Returns:
48
+ A mapping from Python types to codec factories. A factory receives the annotation and
49
+ ``FieldInfo`` and returns a ready-to-use codec instance. There are no additional rules
50
+ by default.
51
+ """
52
+ return {}
@@ -0,0 +1,48 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+ from .base import ICodec
4
+ from .bool import BoolCodec
5
+ from .bytes import BytesCodec, FixedBytesCodec
6
+ from .datetime import DatetimeCodec
7
+ from .enum import EnumCodec
8
+ from .float import Float32Codec, Float64Codec
9
+ from .int import (
10
+ Int8Codec,
11
+ Int16Codec,
12
+ Int32Codec,
13
+ Int64Codec,
14
+ UInt8Codec,
15
+ UInt16Codec,
16
+ UInt32Codec,
17
+ UInt64Codec,
18
+ )
19
+ from .list import ListCodec
20
+ from .model import ModelCodec
21
+ from .str import StrCodec
22
+ from .uuid import UUIDCodec
23
+ from .varint import VarIntCodec, VarUIntCodec
24
+
25
+ __all__ = [
26
+ "BoolCodec",
27
+ "BytesCodec",
28
+ "DatetimeCodec",
29
+ "EnumCodec",
30
+ "FixedBytesCodec",
31
+ "Float32Codec",
32
+ "Float64Codec",
33
+ "ICodec",
34
+ "Int8Codec",
35
+ "Int16Codec",
36
+ "Int32Codec",
37
+ "Int64Codec",
38
+ "ListCodec",
39
+ "ModelCodec",
40
+ "StrCodec",
41
+ "UInt8Codec",
42
+ "UInt16Codec",
43
+ "UInt32Codec",
44
+ "UInt64Codec",
45
+ "UUIDCodec",
46
+ "VarIntCodec",
47
+ "VarUIntCodec",
48
+ ]
@@ -0,0 +1,16 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+ from bytespec.errors import DecodeError
4
+
5
+
6
+ def check_available(buffer: bytes, offset: int, size: int, *, codec: str) -> None:
7
+ if offset < 0:
8
+ raise ValueError(f"offset must be non-negative, got {offset}")
9
+ if size < 0:
10
+ raise ValueError(f"size must be non-negative, got {size}")
11
+
12
+ available = max(0, len(buffer) - offset)
13
+ if available < size:
14
+ raise DecodeError(
15
+ f"{codec} at offset {offset}: expected {size} bytes, available {available}"
16
+ )
@@ -0,0 +1,60 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+ from typing import Any, Generic, Protocol, TypeVar
4
+
5
+ from typing_extensions import runtime_checkable
6
+
7
+ from bytespec.enums import ByteOrder
8
+
9
+ T = TypeVar("T")
10
+
11
+
12
+ @runtime_checkable
13
+ class ICodec(Protocol, Generic[T]):
14
+ """Interface for writing and reading a single value of type T.
15
+
16
+ An instance can be passed to ``field(codec=...)``. Inheriting from ICodec is optional: encode
17
+ and decode methods that follow this contract are sufficient. A codec can be reused; the read
18
+ position is passed through offset.
19
+ """
20
+
21
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
22
+ super().__init__()
23
+
24
+ def encode(self, value: T, byte_order: ByteOrder, /) -> bytes:
25
+ """Write a single value in the selected representation.
26
+
27
+ Args:
28
+ value: The value to write.
29
+ byte_order: The byte order of fixed-width numbers and prefixes.
30
+
31
+ Returns:
32
+ The binary representation of the value.
33
+
34
+ Raises:
35
+ EncodeError: The value cannot be represented. A custom codec must raise this error
36
+ itself for data errors.
37
+ """
38
+ ...
39
+
40
+ def decode(self, buffer: bytes, byte_order: ByteOrder, offset: int, /) -> tuple[T, int]:
41
+ """Read a single value starting at offset.
42
+
43
+ Args:
44
+ buffer: The buffer containing the encoded value.
45
+ byte_order: The byte order of fixed-width numbers and prefixes.
46
+ offset: The nonnegative absolute position where the value starts.
47
+
48
+ Returns:
49
+ The value and the absolute position after it in the same buffer. The remaining bytes do
50
+ not have to be consumed.
51
+
52
+ Raises:
53
+ DecodeError: There are not enough bytes, or the value is invalid. The implementation
54
+ must check bounds and contents itself.
55
+
56
+ Note:
57
+ A list item codec must advance offset by a positive number of bytes while staying within
58
+ the supplied buffer.
59
+ """
60
+ ...
@@ -0,0 +1,45 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+ import struct
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
10
+
11
+ from ._utils import check_available
12
+
13
+
14
+ class BoolCodec(ICodec[bool]):
15
+ """A Boolean value in one byte: 0 or 1."""
16
+
17
+ @override
18
+ def encode(
19
+ self,
20
+ value: bool,
21
+ byte_order: ByteOrder,
22
+ ) -> bytes:
23
+ """Write the truth value of value as 0/1 without strict type checking."""
24
+ return struct.pack(byte_order.value + "B", 1 if value else 0)
25
+
26
+ @override
27
+ def decode(
28
+ self,
29
+ buffer: bytes,
30
+ byte_order: ByteOrder,
31
+ offset: int,
32
+ ) -> tuple[bool, int]:
33
+ """Read a bool and return it with its absolute end offset.
34
+
35
+ Raises:
36
+ DecodeError: The byte is missing or is neither 0 nor 1.
37
+ ValueError: Negative offset.
38
+ """
39
+ check_available(buffer, offset, 1, codec=type(self).__name__)
40
+ value = struct.unpack_from(byte_order.value + "B", buffer, offset)[0]
41
+
42
+ if value not in (0, 1):
43
+ raise DecodeError(f"BoolCodec at offset {offset}: invalid bool value {value}")
44
+
45
+ return bool(value), offset + 1
@@ -0,0 +1,105 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+ from typing_extensions import override
4
+
5
+ from bytespec.codecs import ICodec
6
+ from bytespec.enums import ByteOrder
7
+ from bytespec.errors import EncodeError, SchemaError
8
+ from bytespec.models import PrefixLength
9
+
10
+ from ._utils import check_available
11
+ from .map import UINT_CODEC_MAPPING
12
+
13
+
14
+ class BytesCodec(ICodec[bytes]):
15
+ """Bytes prefixed with their length.
16
+
17
+ Args:
18
+ prefix_length: Prefix size: 1, 2, 4, or 8 bytes, or ``VarUInt``. Defaults to 4 bytes. The
19
+ prefix itself is not included in the stored length.
20
+
21
+ Raises:
22
+ SchemaError: Unsupported prefix, including ``VarInt``.
23
+ """
24
+
25
+ def __init__(self, prefix_length: PrefixLength = 4) -> None:
26
+ self.prefix_length = prefix_length
27
+
28
+ codec = UINT_CODEC_MAPPING.get(prefix_length)
29
+ if codec is None:
30
+ raise SchemaError(f"BytesCodec: invalid length prefix {prefix_length!r}")
31
+
32
+ self.codec = codec
33
+
34
+ super().__init__()
35
+
36
+ @override
37
+ def encode(self, value: bytes, byte_order: ByteOrder) -> bytes:
38
+ """Write the length of value followed by the bytes themselves.
39
+
40
+ Raises:
41
+ EncodeError: The length does not fit in the selected prefix.
42
+ """
43
+ return self.codec.encode(len(value), byte_order) + value
44
+
45
+ @override
46
+ def decode(self, buffer: bytes, byte_order: ByteOrder, offset: int) -> tuple[bytes, int]:
47
+ """Read the prefix and return the bytes with their absolute end offset.
48
+
49
+ Raises:
50
+ DecodeError: The prefix or declared data is incomplete or invalid.
51
+ ValueError: Negative offset.
52
+ """
53
+ length, start = self.codec.decode(buffer, byte_order, offset)
54
+ end = start + length
55
+
56
+ check_available(buffer, start, length, codec=type(self).__name__)
57
+
58
+ return buffer[start:end], end
59
+
60
+
61
+ class FixedBytesCodec(BytesCodec):
62
+ """Exactly length bytes without a length prefix.
63
+
64
+ Args:
65
+ length: The nonnegative fixed size of the value.
66
+
67
+ Raises:
68
+ SchemaError: Negative length.
69
+ """
70
+
71
+ def __init__(self, length: int) -> None:
72
+ if length < 0:
73
+ raise SchemaError(f"FixedBytesCodec: length must be non-negative, got {length}")
74
+ self.length = length
75
+
76
+ @override
77
+ def encode(self, value: bytes, byte_order: ByteOrder) -> bytes:
78
+ """Return value after checking its length; byte order has no effect.
79
+
80
+ Raises:
81
+ EncodeError: The size of value is not equal to length.
82
+ """
83
+ if len(value) != self.length:
84
+ raise EncodeError(f"FixedBytesCodec: expected {self.length} bytes, got {len(value)}")
85
+
86
+ return value
87
+
88
+ @override
89
+ def decode(
90
+ self,
91
+ buffer: bytes,
92
+ byte_order: ByteOrder,
93
+ offset: int,
94
+ ) -> tuple[bytes, int]:
95
+ """Read length bytes and return them with their absolute end offset.
96
+
97
+ Raises:
98
+ DecodeError: There are not enough bytes.
99
+ ValueError: Negative offset.
100
+ """
101
+ end = offset + self.length
102
+
103
+ check_available(buffer, offset, self.length, codec=type(self).__name__)
104
+
105
+ return buffer[offset:end], end
@@ -0,0 +1,59 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+ from datetime import datetime
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
10
+ from bytespec.models import PrefixLength
11
+
12
+ from .str import StrCodec
13
+
14
+
15
+ class DatetimeCodec(ICodec[datetime]):
16
+ """A datetime as a length-prefixed ISO 8601 string.
17
+
18
+ Preserves the UTC offset from isoformat(), but not the time zone name. A datetime without tzinfo
19
+ remains naive after reading.
20
+
21
+ Args:
22
+ prefix_length: Prefix size: 1, 2, 4, or 8 bytes, or ``VarUInt``.
23
+ encoding: Encoding of the ISO string, UTF-8 by default.
24
+
25
+ Raises:
26
+ SchemaError: Unsupported prefix or unknown encoding.
27
+ """
28
+
29
+ def __init__(self, prefix_length: PrefixLength = 4, encoding: str = "utf-8") -> None:
30
+ self.codec = StrCodec(prefix_length, encoding=encoding)
31
+
32
+ @override
33
+ def encode(self, value: datetime, byte_order: ByteOrder) -> bytes:
34
+ """Write value.isoformat() as a length-prefixed string.
35
+
36
+ Raises:
37
+ EncodeError: The string cannot be represented in the encoding, or its length does not
38
+ fit in the prefix.
39
+ SchemaError: The selected codec is not a text encoding.
40
+ """
41
+ return self.codec.encode(value.isoformat(), byte_order)
42
+
43
+ @override
44
+ def decode(self, buffer: bytes, byte_order: ByteOrder, offset: int) -> tuple[datetime, int]:
45
+ """Restore a datetime and return it with its absolute end offset.
46
+
47
+ Raises:
48
+ DecodeError: There are not enough bytes, or the string or ISO date is invalid.
49
+ SchemaError: The selected codec is not a text encoding.
50
+ ValueError: Negative offset.
51
+ """
52
+ value, end = self.codec.decode(buffer, byte_order, offset)
53
+ try:
54
+ decoded = datetime.fromisoformat(value)
55
+ except ValueError as exc:
56
+ raise DecodeError(
57
+ f"DatetimeCodec at offset {offset}: invalid datetime {value!r}"
58
+ ) from exc
59
+ return decoded, end
@@ -0,0 +1,49 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+ from enum import Enum
4
+ from typing import Any
5
+
6
+ from typing_extensions import override
7
+
8
+ from bytespec.codecs import ICodec
9
+ from bytespec.enums import ByteOrder
10
+ from bytespec.errors import DecodeError
11
+
12
+
13
+ class EnumCodec(ICodec[Enum]):
14
+ """An enum encoded using a codec for its value.
15
+
16
+ Args:
17
+ enum_type: The enum class used to reconstruct members via enum_type(value).
18
+ value_codec: The codec for .value, such as StrCodec or UInt8Codec.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ enum_type: type[Enum],
24
+ value_codec: ICodec[Any],
25
+ ) -> None:
26
+ self.enum_type = enum_type
27
+ self.value_codec = value_codec
28
+
29
+ @override
30
+ def encode(self, value: Enum, byte_order: ByteOrder) -> bytes:
31
+ """Write value.value using the selected value codec."""
32
+ return self.value_codec.encode(value.value, byte_order)
33
+
34
+ @override
35
+ def decode(self, buffer: bytes, byte_order: ByteOrder, offset: int) -> tuple[Enum, int]:
36
+ """Read an enum member and return it with its absolute end offset.
37
+
38
+ Raises:
39
+ DecodeError: The decoded value is not an enum member, or the value codec rejected the
40
+ data.
41
+ """
42
+ value, end = self.value_codec.decode(buffer, byte_order, offset)
43
+ try:
44
+ decoded = self.enum_type(value)
45
+ except ValueError as exc:
46
+ raise DecodeError(
47
+ f"EnumCodec ({self.enum_type.__name__}) at offset {offset}: invalid value {value!r}"
48
+ ) from exc
49
+ return decoded, end
@@ -0,0 +1,17 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+ from .int import IntegerCodec
4
+
5
+
6
+ class Float32Codec(IntegerCodec[float]):
7
+ """IEEE 754 binary32: four bytes, rounding the Python float."""
8
+
9
+ def __init__(self, struct_format: str = "f", length: int = 4) -> None:
10
+ super().__init__(struct_format, length)
11
+
12
+
13
+ class Float64Codec(Float32Codec):
14
+ """IEEE 754 binary64: eight bytes in the model's byte order."""
15
+
16
+ def __init__(self, struct_format: str = "d", length: int = 8) -> None:
17
+ super().__init__(struct_format, length)
bytespec/codecs/int.py ADDED
@@ -0,0 +1,118 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+ import struct
4
+ from typing import Generic, TypeVar
5
+
6
+ from typing_extensions import override
7
+
8
+ from bytespec.codecs import ICodec
9
+ from bytespec.enums import ByteOrder
10
+ from bytespec.errors import EncodeError, SchemaError
11
+
12
+ from ._utils import check_available
13
+
14
+ T = TypeVar("T")
15
+
16
+
17
+ class IntegerCodec(ICodec[T], Generic[T]):
18
+ def __init__(self, struct_format: str, length: int) -> None:
19
+ try:
20
+ struct.calcsize(ByteOrder.BIG.value + struct_format)
21
+ except struct.error as exc:
22
+ raise SchemaError(
23
+ f"{type(self).__name__}: invalid struct format {struct_format!r}"
24
+ ) from exc
25
+ self.struct_format = struct_format
26
+ self.length = length
27
+
28
+ @override
29
+ def encode(
30
+ self,
31
+ value: T,
32
+ byte_order: ByteOrder,
33
+ ) -> bytes:
34
+ """Write a fixed-width number in the specified byte order.
35
+
36
+ Raises:
37
+ EncodeError: The value cannot be packed in the selected numeric format.
38
+ """
39
+ try:
40
+ return struct.pack(byte_order.value + self.struct_format, value)
41
+ except (struct.error, OverflowError) as exc:
42
+ raise EncodeError(
43
+ f"{type(self).__name__} ({self.struct_format}): cannot encode {value!r}"
44
+ ) from exc
45
+
46
+ @override
47
+ def decode(
48
+ self,
49
+ buffer: bytes,
50
+ byte_order: ByteOrder,
51
+ offset: int,
52
+ ) -> tuple[T, int]:
53
+ """Read a number and return it with its absolute end offset.
54
+
55
+ Raises:
56
+ DecodeError: There are not enough bytes for the number.
57
+ ValueError: Negative offset.
58
+ """
59
+ size = struct.calcsize(byte_order.value + self.struct_format)
60
+ check_available(buffer, offset, size, codec=type(self).__name__)
61
+ value = struct.unpack_from(byte_order.value + self.struct_format, buffer, offset)[0]
62
+ return value, offset + self.length
63
+
64
+
65
+ class UInt8Codec(IntegerCodec[int]):
66
+ """Unsigned 8-bit integer: 0–255, one byte by default."""
67
+
68
+ def __init__(self, struct_format: str = "B", length: int = 1) -> None:
69
+ super().__init__(struct_format, length)
70
+
71
+
72
+ class Int8Codec(UInt8Codec):
73
+ """Signed 8-bit integer: -128–127, one byte by default."""
74
+
75
+ def __init__(self, struct_format: str = "b", length: int = 1) -> None:
76
+ super().__init__(struct_format, length)
77
+
78
+
79
+ class UInt16Codec(IntegerCodec[int]):
80
+ """Unsigned 16-bit integer: 0–65535, two bytes by default."""
81
+
82
+ def __init__(self, struct_format: str = "H", length: int = 2) -> None:
83
+ super().__init__(struct_format, length)
84
+
85
+
86
+ class Int16Codec(UInt16Codec):
87
+ """Signed 16-bit integer: -32768–32767, two bytes by default."""
88
+
89
+ def __init__(self, struct_format: str = "h", length: int = 2) -> None:
90
+ super().__init__(struct_format, length)
91
+
92
+
93
+ class UInt32Codec(IntegerCodec[int]):
94
+ """Unsigned 32-bit integer: 0 .. 2**32 - 1, four bytes."""
95
+
96
+ def __init__(self, struct_format: str = "I", length: int = 4) -> None:
97
+ super().__init__(struct_format, length)
98
+
99
+
100
+ class Int32Codec(UInt32Codec):
101
+ """Signed 32-bit integer: -2**31 .. 2**31 - 1, four bytes."""
102
+
103
+ def __init__(self, struct_format: str = "i", length: int = 4) -> None:
104
+ super().__init__(struct_format, length)
105
+
106
+
107
+ class UInt64Codec(IntegerCodec[int]):
108
+ """Unsigned 64-bit integer: 0 .. 2**64 - 1, eight bytes."""
109
+
110
+ def __init__(self, struct_format: str = "Q", length: int = 8) -> None:
111
+ super().__init__(struct_format, length)
112
+
113
+
114
+ class Int64Codec(UInt64Codec):
115
+ """Signed 64-bit integer: -2**63 .. 2**63 - 1, eight bytes."""
116
+
117
+ def __init__(self, struct_format: str = "q", length: int = 8) -> None:
118
+ super().__init__(struct_format, length)
@@ -0,0 +1,77 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+ from typing import Any
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 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 ListCodec(ICodec[list[Any]]):
17
+ """A list prefixed with the total size of its encoded items in bytes.
18
+
19
+ Args:
20
+ item_codec: The codec for a single item. When reading, it must advance offset by a positive
21
+ number of bytes within the supplied buffer.
22
+ prefix_length: Prefix size: 1, 2, 4, or 8 bytes, or ``VarUInt``. Defaults to 4 bytes. The
23
+ prefix does not store the item count.
24
+
25
+ Raises:
26
+ SchemaError: Unsupported length prefix.
27
+ """
28
+
29
+ def __init__(self, item_codec: ICodec[Any], prefix_length: PrefixLength = 4) -> None:
30
+ self.item_codec = item_codec
31
+ self.prefix_length = prefix_length
32
+
33
+ codec = UINT_CODEC_MAPPING.get(prefix_length)
34
+ if codec is None:
35
+ raise SchemaError(f"ListCodec: invalid length prefix {prefix_length!r}")
36
+
37
+ self.codec = codec
38
+
39
+ @override
40
+ def encode(self, data: list[Any], byte_order: ByteOrder) -> bytes:
41
+ """Write consecutive items prefixed with their total byte length.
42
+
43
+ Raises:
44
+ EncodeError: The size does not fit in the prefix, or an item cannot be encoded by the
45
+ selected codec.
46
+ """
47
+ payload = bytearray()
48
+
49
+ for value in data:
50
+ payload.extend(self.item_codec.encode(value, byte_order))
51
+
52
+ return self.codec.encode(len(payload), byte_order) + payload
53
+
54
+ @override
55
+ def decode(self, buffer: bytes, byte_order: ByteOrder, offset: int) -> tuple[list[Any], int]:
56
+ """Read a list and return it with its absolute end offset.
57
+
58
+ Items are read from a separate buffer containing the list contents; offsets in their errors
59
+ refer to that buffer.
60
+
61
+ Raises:
62
+ DecodeError: The prefix or data is incomplete, or the item codec rejected the data.
63
+ ValueError: Negative offset.
64
+ """
65
+ length, start = self.codec.decode(buffer, byte_order, offset)
66
+ end = start + length
67
+
68
+ check_available(buffer, start, length, codec=type(self).__name__)
69
+
70
+ values = buffer[start:end]
71
+ data = []
72
+ item_offset = 0
73
+ while item_offset < len(values):
74
+ value, item_offset = self.item_codec.decode(values, byte_order, item_offset)
75
+ data.append(value)
76
+
77
+ return data, end
bytespec/codecs/map.py ADDED
@@ -0,0 +1,17 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+ from typing import Annotated, Any
4
+
5
+ from bytespec.codecs.base import ICodec
6
+ from bytespec.codecs.varint import VarUIntCodec
7
+ from bytespec.types import VarUInt
8
+
9
+ from .int import UInt8Codec, UInt16Codec, UInt32Codec, UInt64Codec
10
+
11
+ UINT_CODEC_MAPPING: dict[int | Annotated[Any, Any], ICodec[Any]] = {
12
+ 1: UInt8Codec(),
13
+ 2: UInt16Codec(),
14
+ 4: UInt32Codec(),
15
+ 8: UInt64Codec(),
16
+ VarUInt: VarUIntCodec(),
17
+ }