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/missing.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright (c) 2026 ink-developer
|
|
2
|
+
|
|
3
|
+
from typing import Final, TypeAlias
|
|
4
|
+
|
|
5
|
+
from typing_extensions import override
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _MissingType:
|
|
9
|
+
__slots__ = ()
|
|
10
|
+
|
|
11
|
+
@override
|
|
12
|
+
def __repr__(self) -> str:
|
|
13
|
+
return "MISSING"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
MISSING: Final = _MissingType()
|
|
17
|
+
MissingType: TypeAlias = _MissingType
|
bytespec/models.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Copyright (c) 2026 ink-developer
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Literal, TypeAlias
|
|
6
|
+
|
|
7
|
+
from bytespec.codecs import ICodec
|
|
8
|
+
from bytespec.missing import MissingType
|
|
9
|
+
from bytespec.types import VarUInt
|
|
10
|
+
|
|
11
|
+
DefaultFactory: TypeAlias = Callable[[], Any] | MissingType
|
|
12
|
+
|
|
13
|
+
UIntEncoding = Literal[1, 2, 4, 8] | VarUInt
|
|
14
|
+
PrefixLength = UIntEncoding
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class FieldInfo:
|
|
19
|
+
"""Field settings passed to a user-defined codec factory.
|
|
20
|
+
|
|
21
|
+
In models, create them via `field()`. The factory may read
|
|
22
|
+
`prefix_length`, `encoding`, and other settings to construct a codec.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
index: Explicit field position, or None for automatic selection.
|
|
26
|
+
flag: Presence bit index for an optional field, or None.
|
|
27
|
+
default: Default value or the MISSING sentinel.
|
|
28
|
+
default_factory: Zero-argument factory or MISSING.
|
|
29
|
+
prefix_length: Length prefix size, or None to use the default.
|
|
30
|
+
encoding: Text encoding, UTF-8 by default.
|
|
31
|
+
codec: Explicitly assigned codec instance, or None.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
index: int | None
|
|
35
|
+
flag: int | None
|
|
36
|
+
default: Any
|
|
37
|
+
default_factory: DefaultFactory
|
|
38
|
+
prefix_length: PrefixLength | None
|
|
39
|
+
encoding: str = "utf-8"
|
|
40
|
+
codec: ICodec[Any] | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ResolvedType:
|
|
45
|
+
codec: ICodec[Any]
|
|
46
|
+
optional: bool
|
|
47
|
+
annotation: Any
|
bytespec/py.typed
ADDED
|
File without changes
|
bytespec/resolver.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Copyright (c) 2026 ink-developer
|
|
2
|
+
|
|
3
|
+
import codecs
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from types import NoneType, UnionType
|
|
6
|
+
from typing import Annotated, Any, Union, get_args, get_origin
|
|
7
|
+
|
|
8
|
+
from bytespec.base import ProtoModelBase
|
|
9
|
+
from bytespec.codecs import (
|
|
10
|
+
Float32Codec,
|
|
11
|
+
Float64Codec,
|
|
12
|
+
ICodec,
|
|
13
|
+
UInt8Codec,
|
|
14
|
+
VarIntCodec,
|
|
15
|
+
VarUIntCodec,
|
|
16
|
+
)
|
|
17
|
+
from bytespec.codecs.int import (
|
|
18
|
+
Int8Codec,
|
|
19
|
+
Int16Codec,
|
|
20
|
+
Int32Codec,
|
|
21
|
+
Int64Codec,
|
|
22
|
+
UInt16Codec,
|
|
23
|
+
UInt32Codec,
|
|
24
|
+
UInt64Codec,
|
|
25
|
+
)
|
|
26
|
+
from bytespec.errors import SchemaError
|
|
27
|
+
from bytespec.models import FieldInfo, ResolvedType
|
|
28
|
+
from bytespec.resolvers import (
|
|
29
|
+
CodecFactory,
|
|
30
|
+
ResolveCallback,
|
|
31
|
+
enum_codec_factory,
|
|
32
|
+
list_codec_factory,
|
|
33
|
+
model_codec_factory,
|
|
34
|
+
)
|
|
35
|
+
from bytespec.types import CodecSpec, FloatSpec, IntegerSpec, Spec
|
|
36
|
+
from bytespec.types.spec import VarIntSpec
|
|
37
|
+
|
|
38
|
+
INTEGER_CODECS: dict[tuple[int, bool], type[ICodec[Any]]] = {
|
|
39
|
+
(8, False): UInt8Codec,
|
|
40
|
+
(16, False): UInt16Codec,
|
|
41
|
+
(32, False): UInt32Codec,
|
|
42
|
+
(64, False): UInt64Codec,
|
|
43
|
+
(8, True): Int8Codec,
|
|
44
|
+
(16, True): Int16Codec,
|
|
45
|
+
(32, True): Int32Codec,
|
|
46
|
+
(64, True): Int64Codec,
|
|
47
|
+
}
|
|
48
|
+
FLOAT_CODECS: dict[int, type[ICodec[Any]]] = {
|
|
49
|
+
32: Float32Codec,
|
|
50
|
+
64: Float64Codec,
|
|
51
|
+
}
|
|
52
|
+
VARINT_CODECS: dict[bool, type[ICodec[Any]]] = {
|
|
53
|
+
True: VarIntCodec,
|
|
54
|
+
False: VarUIntCodec,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolve_integer(_: Any, spec: IntegerSpec, __: FieldInfo, ___: ResolveCallback) -> ICodec[Any]:
|
|
59
|
+
codec = INTEGER_CODECS.get((spec.bits, spec.signed))
|
|
60
|
+
|
|
61
|
+
if not codec:
|
|
62
|
+
raise SchemaError(f"Unsupported numeric spec: {spec!r}")
|
|
63
|
+
|
|
64
|
+
return codec()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def resolve_float(_: Any, spec: FloatSpec, __: FieldInfo, ___: ResolveCallback) -> ICodec[Any]:
|
|
68
|
+
codec = FLOAT_CODECS.get(spec.bits)
|
|
69
|
+
|
|
70
|
+
if not codec:
|
|
71
|
+
raise SchemaError(f"Unsupported numeric spec: {spec!r}")
|
|
72
|
+
|
|
73
|
+
return codec()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def resolve_varint(_: Any, spec: VarIntSpec, __: FieldInfo, ___: ResolveCallback) -> ICodec[Any]:
|
|
77
|
+
codec = VARINT_CODECS.get(spec.signed)
|
|
78
|
+
|
|
79
|
+
if not codec:
|
|
80
|
+
raise SchemaError(f"Unsupported numeric spec: {spec!r}")
|
|
81
|
+
|
|
82
|
+
return codec()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class CodecResolver:
|
|
86
|
+
def __init__(self, scalar_codecs: dict[type, CodecFactory]) -> None:
|
|
87
|
+
self.scalar_codecs = scalar_codecs
|
|
88
|
+
|
|
89
|
+
def _is_type_of(self, annotation: Any, subclass: type) -> bool:
|
|
90
|
+
return isinstance(annotation, type) and issubclass(annotation, subclass)
|
|
91
|
+
|
|
92
|
+
def _normalize_union(self, annotation: Any | UnionType) -> tuple[Any, bool]:
|
|
93
|
+
is_optional = False
|
|
94
|
+
metadata = list(get_args(annotation))
|
|
95
|
+
|
|
96
|
+
if NoneType in metadata:
|
|
97
|
+
is_optional = True
|
|
98
|
+
metadata.pop(metadata.index(NoneType))
|
|
99
|
+
|
|
100
|
+
if len(metadata) > 1:
|
|
101
|
+
raise SchemaError(f"Unsupported union type: {annotation}")
|
|
102
|
+
|
|
103
|
+
return metadata[0], is_optional
|
|
104
|
+
|
|
105
|
+
def _check_encoding(self, encoding: str) -> None:
|
|
106
|
+
try:
|
|
107
|
+
codecs.lookup(encoding)
|
|
108
|
+
except LookupError as exc:
|
|
109
|
+
raise SchemaError(f"Invalid encoding: {encoding!r}") from exc
|
|
110
|
+
|
|
111
|
+
def _resolve_spec(
|
|
112
|
+
self,
|
|
113
|
+
base_type: Any,
|
|
114
|
+
spec: Spec,
|
|
115
|
+
field_info: FieldInfo,
|
|
116
|
+
resolve: ResolveCallback,
|
|
117
|
+
) -> ICodec[Any]:
|
|
118
|
+
if isinstance(spec, IntegerSpec):
|
|
119
|
+
return resolve_integer(base_type, spec, field_info, resolve)
|
|
120
|
+
|
|
121
|
+
if isinstance(spec, FloatSpec):
|
|
122
|
+
return resolve_float(base_type, spec, field_info, resolve)
|
|
123
|
+
|
|
124
|
+
if isinstance(spec, VarIntSpec):
|
|
125
|
+
return resolve_varint(base_type, spec, field_info, resolve)
|
|
126
|
+
|
|
127
|
+
raise SchemaError(f"Unsupported spec: {type(spec).__name__}")
|
|
128
|
+
|
|
129
|
+
def resolve(
|
|
130
|
+
self, annotation: Any, field_info: FieldInfo, is_optional: bool = False
|
|
131
|
+
) -> ResolvedType:
|
|
132
|
+
self._check_encoding(field_info.encoding)
|
|
133
|
+
|
|
134
|
+
origin = get_origin(annotation)
|
|
135
|
+
|
|
136
|
+
if origin in (UnionType, Union):
|
|
137
|
+
annotation, is_optional = self._normalize_union(annotation)
|
|
138
|
+
|
|
139
|
+
if field_info.codec is not None:
|
|
140
|
+
return ResolvedType(
|
|
141
|
+
field_info.codec,
|
|
142
|
+
is_optional,
|
|
143
|
+
annotation,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
scalar_codec = self.scalar_codecs.get(annotation)
|
|
147
|
+
|
|
148
|
+
if scalar_codec:
|
|
149
|
+
return ResolvedType(scalar_codec(annotation, field_info), is_optional, annotation)
|
|
150
|
+
|
|
151
|
+
if get_origin(annotation) is Annotated:
|
|
152
|
+
base_type, *metadata = get_args(annotation)
|
|
153
|
+
|
|
154
|
+
specs = [meta for meta in metadata if isinstance(meta, Spec)]
|
|
155
|
+
|
|
156
|
+
if len(specs) > 1:
|
|
157
|
+
raise SchemaError("Only one Spec is allowed")
|
|
158
|
+
|
|
159
|
+
if not specs:
|
|
160
|
+
return self.resolve(base_type, field_info, is_optional)
|
|
161
|
+
|
|
162
|
+
spec = specs[0]
|
|
163
|
+
|
|
164
|
+
if isinstance(spec, CodecSpec):
|
|
165
|
+
if spec.prefix_length is not None:
|
|
166
|
+
field_info.prefix_length = spec.prefix_length
|
|
167
|
+
|
|
168
|
+
if spec.encoding:
|
|
169
|
+
self._check_encoding(spec.encoding)
|
|
170
|
+
field_info.encoding = spec.encoding
|
|
171
|
+
|
|
172
|
+
if spec.codec is not None:
|
|
173
|
+
field_info.codec = spec.codec
|
|
174
|
+
|
|
175
|
+
return self.resolve(base_type, field_info, is_optional)
|
|
176
|
+
|
|
177
|
+
return ResolvedType(
|
|
178
|
+
self._resolve_spec(base_type, spec, field_info, self.resolve),
|
|
179
|
+
is_optional,
|
|
180
|
+
annotation,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if self._is_type_of(annotation, ProtoModelBase):
|
|
184
|
+
return ResolvedType(
|
|
185
|
+
model_codec_factory(annotation, field_info), is_optional, annotation
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if self._is_type_of(annotation, Enum):
|
|
189
|
+
return ResolvedType(enum_codec_factory(annotation, field_info), is_optional, annotation)
|
|
190
|
+
|
|
191
|
+
if get_origin(annotation) is list:
|
|
192
|
+
return ResolvedType(
|
|
193
|
+
list_codec_factory(annotation, field_info, self.resolve), is_optional, annotation
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
raise SchemaError(f"Unknown type: {annotation}")
|
bytespec/resolvers.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Copyright (c) 2026 ink-developer
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Protocol, TypeAlias, get_args
|
|
6
|
+
|
|
7
|
+
from bytespec.codecs import ICodec
|
|
8
|
+
from bytespec.errors import SchemaError
|
|
9
|
+
|
|
10
|
+
from .codecs import (
|
|
11
|
+
BoolCodec,
|
|
12
|
+
BytesCodec,
|
|
13
|
+
DatetimeCodec,
|
|
14
|
+
EnumCodec,
|
|
15
|
+
ListCodec,
|
|
16
|
+
ModelCodec,
|
|
17
|
+
StrCodec,
|
|
18
|
+
UUIDCodec,
|
|
19
|
+
VarIntCodec,
|
|
20
|
+
)
|
|
21
|
+
from .missing import MISSING
|
|
22
|
+
from .models import FieldInfo, ResolvedType
|
|
23
|
+
|
|
24
|
+
ResolveCallback: TypeAlias = Callable[[Any, FieldInfo], ResolvedType]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CodecFactory(Protocol):
|
|
28
|
+
"""Codec factory returned by rules configured via `configure_codecs()`.
|
|
29
|
+
|
|
30
|
+
When selecting a codec for a scalar field, it is called with two arguments:
|
|
31
|
+
the annotation and `FieldInfo`. It returns a ready-to-use codec instance.
|
|
32
|
+
The third protocol argument is optional and is not passed in this path.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __call__(
|
|
36
|
+
self, annotation: Any, field_info: FieldInfo, resolve: ResolveCallback | None = None, /
|
|
37
|
+
) -> ICodec[Any]: ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def str_codec_factory(_: Any, field_info: FieldInfo, __: ResolveCallback | None = None) -> StrCodec:
|
|
41
|
+
|
|
42
|
+
if field_info.prefix_length is not None:
|
|
43
|
+
return StrCodec(prefix_length=field_info.prefix_length, encoding=field_info.encoding)
|
|
44
|
+
else:
|
|
45
|
+
return StrCodec(encoding=field_info.encoding)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def bytes_codec_factory(
|
|
49
|
+
_: Any, field_info: FieldInfo, __: ResolveCallback | None = None
|
|
50
|
+
) -> BytesCodec:
|
|
51
|
+
if field_info.prefix_length is not None:
|
|
52
|
+
return BytesCodec(prefix_length=field_info.prefix_length)
|
|
53
|
+
else:
|
|
54
|
+
return BytesCodec()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def datetime_codec_factory(
|
|
58
|
+
_: Any, field_info: FieldInfo, __: ResolveCallback | None = None
|
|
59
|
+
) -> DatetimeCodec:
|
|
60
|
+
if field_info.prefix_length is not None:
|
|
61
|
+
return DatetimeCodec(prefix_length=field_info.prefix_length, encoding=field_info.encoding)
|
|
62
|
+
else:
|
|
63
|
+
return DatetimeCodec(encoding=field_info.encoding)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def model_codec_factory(
|
|
67
|
+
annotation: Any, _: FieldInfo, __: ResolveCallback | None = None
|
|
68
|
+
) -> ModelCodec:
|
|
69
|
+
return ModelCodec(annotation)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def int_codec_factory(_: Any, __: FieldInfo, ___: ResolveCallback | None = None) -> VarIntCodec:
|
|
73
|
+
return VarIntCodec()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def enum_codec_factory(
|
|
77
|
+
annotation: Any, field_info: FieldInfo, __: ResolveCallback | None = None
|
|
78
|
+
) -> EnumCodec:
|
|
79
|
+
if not issubclass(annotation, Enum):
|
|
80
|
+
raise SchemaError(f"Unknown enum type: {annotation}")
|
|
81
|
+
|
|
82
|
+
if issubclass(annotation, str):
|
|
83
|
+
return EnumCodec(annotation, str_codec_factory(annotation, field_info, __))
|
|
84
|
+
elif issubclass(annotation, int):
|
|
85
|
+
return EnumCodec(annotation, int_codec_factory(annotation, field_info, __))
|
|
86
|
+
else:
|
|
87
|
+
raise SchemaError(f"Unknown enum type: {annotation}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def bool_codec_factory(_: Any, __: FieldInfo, ___: ResolveCallback | None = None) -> BoolCodec:
|
|
91
|
+
return BoolCodec()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def uuid_codec_factory(_: Any, __: FieldInfo, ___: ResolveCallback | None = None) -> UUIDCodec:
|
|
95
|
+
return UUIDCodec()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def list_codec_factory(
|
|
99
|
+
annotation: Any, field_info: FieldInfo, callback: ResolveCallback
|
|
100
|
+
) -> ListCodec:
|
|
101
|
+
metadata = list(get_args(annotation))
|
|
102
|
+
|
|
103
|
+
if len(metadata) != 1:
|
|
104
|
+
raise SchemaError(f"Unsupported list type: {annotation}")
|
|
105
|
+
|
|
106
|
+
resolved_type = callback(metadata[0], FieldInfo(None, None, MISSING, MISSING, None))
|
|
107
|
+
|
|
108
|
+
if resolved_type.optional:
|
|
109
|
+
raise SchemaError(f"Unsupported list type: {annotation}")
|
|
110
|
+
|
|
111
|
+
if field_info.prefix_length is not None:
|
|
112
|
+
return ListCodec(resolved_type.codec, field_info.prefix_length)
|
|
113
|
+
else:
|
|
114
|
+
return ListCodec(resolved_type.codec)
|
bytespec/schema.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from .enums import ByteOrder
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .core import FieldMetadata, ProtoModel
|
|
10
|
+
from .headers import HeaderElement
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class ModelSchema:
|
|
15
|
+
model: type[ProtoModel]
|
|
16
|
+
fields: tuple[FieldMetadata, ...]
|
|
17
|
+
header: tuple[HeaderElement, ...]
|
|
18
|
+
byte_order: ByteOrder
|
|
19
|
+
constructor: int | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(slots=True)
|
|
23
|
+
class HeaderEncodeContext:
|
|
24
|
+
schema: ModelSchema
|
|
25
|
+
include_constructor: bool
|
|
26
|
+
payload_length: int
|
|
27
|
+
flags: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class HeaderDecodeContext:
|
|
32
|
+
schema: ModelSchema
|
|
33
|
+
expect_constructor: bool
|
|
34
|
+
payload_length: int | None = None
|
|
35
|
+
flags: int = 0
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright (c) 2026 ink-developer
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, TypeAlias
|
|
4
|
+
|
|
5
|
+
from .spec import CodecSpec as CodecSpec
|
|
6
|
+
from .spec import FloatSpec, IntegerSpec, VarIntSpec
|
|
7
|
+
from .spec import Spec as Spec
|
|
8
|
+
|
|
9
|
+
UInt8 = Annotated[int, IntegerSpec(8, signed=False)]
|
|
10
|
+
UInt16 = Annotated[int, IntegerSpec(16, signed=False)]
|
|
11
|
+
UInt32 = Annotated[int, IntegerSpec(32, signed=False)]
|
|
12
|
+
UInt64 = Annotated[int, IntegerSpec(64, signed=False)]
|
|
13
|
+
|
|
14
|
+
Int8 = Annotated[int, IntegerSpec(8, signed=True)]
|
|
15
|
+
Int16 = Annotated[int, IntegerSpec(16, signed=True)]
|
|
16
|
+
Int32 = Annotated[int, IntegerSpec(32, signed=True)]
|
|
17
|
+
Int64 = Annotated[int, IntegerSpec(64, signed=True)]
|
|
18
|
+
|
|
19
|
+
Float32 = Annotated[float, FloatSpec(32)]
|
|
20
|
+
Float64 = Annotated[float, FloatSpec(64)]
|
|
21
|
+
|
|
22
|
+
VarUInt = Annotated[int, VarIntSpec(signed=False)]
|
|
23
|
+
VarInt = Annotated[int, VarIntSpec(signed=True)]
|
|
24
|
+
|
|
25
|
+
UIntUnion: TypeAlias = UInt16 | UInt32 | UInt64 | UInt8
|
bytespec/types/spec.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Copyright (c) 2026 ink-developer
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal, TypeAlias
|
|
7
|
+
|
|
8
|
+
from bytespec.codecs import ICodec
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from bytespec.models import PrefixLength
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class IntegerSpec:
|
|
16
|
+
"""Select a fixed-width integer in ``Annotated[int, ...]``.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
bits: The number of bits: 8, 16, 32, or 64.
|
|
20
|
+
signed: Allow negative values (signed representation).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
bits: Literal[8, 16, 32, 64]
|
|
24
|
+
signed: bool
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class FloatSpec:
|
|
29
|
+
"""Select an IEEE 754 number in ``Annotated[float, ...]``.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
bits: The number of bits: 32 or 64. Float32 rounds a Python float to its representation.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
bits: Literal[32, 64]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class VarIntSpec:
|
|
40
|
+
"""Select a varint in ``Annotated[int, ...]``.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
signed: If True, use ZigZag and the Int64 range; if False, use an unsigned varint and the
|
|
44
|
+
UInt64 range.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
signed: bool
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class StructSpec:
|
|
52
|
+
struct_format: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class CodecSpec:
|
|
57
|
+
"""Define reusable field settings using ``Annotated``.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
prefix_length: Prefix size: 1, 2, 4, or 8 bytes, or ``VarUInt``. ``None`` leaves the field
|
|
61
|
+
setting unchanged; ``VarInt`` is not supported.
|
|
62
|
+
encoding: The text encoding. A nonempty value replaces the field encoding.
|
|
63
|
+
codec: A ready-to-use codec instance instead of automatic selection.
|
|
64
|
+
|
|
65
|
+
Note:
|
|
66
|
+
One supported specification is allowed per Annotated. An explicit ``field(codec=...)`` takes
|
|
67
|
+
precedence over CodecSpec.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
prefix_length: PrefixLength | None = None
|
|
71
|
+
encoding: str | None = None
|
|
72
|
+
codec: ICodec[Any] | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
Spec: TypeAlias = IntegerSpec | FloatSpec | VarIntSpec | CodecSpec
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bytespec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Declarative binary serialization and deserialization for Python
|
|
5
|
+
Keywords: binary,serialization,deserialization,codec,protocol
|
|
6
|
+
Author: ink-developer
|
|
7
|
+
Author-email: ink-developer <142109011+ink-developer@users.noreply.github.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: typing-extensions>=4.16.0
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Project-URL: Homepage, https://github.com/ink-developer/bytespec
|
|
22
|
+
Project-URL: Repository, https://github.com/ink-developer/bytespec
|
|
23
|
+
Project-URL: Issues, https://github.com/ink-developer/bytespec/issues
|
|
24
|
+
Project-URL: Documentation, https://bytespec.pymax.org
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# bytespec
|
|
28
|
+
|
|
29
|
+
English · [Русский](README.md)
|
|
30
|
+
|
|
31
|
+
Typed Python models over an explicit sequential binary representation.
|
|
32
|
+
One model reads and writes numbers, strings, lists, and nested objects;
|
|
33
|
+
you choose their representation, and the library tracks lengths and offsets.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from bytespec import Constructor, PayloadLength, ProtoModel, field
|
|
37
|
+
from bytespec.types import UInt32
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class User(ProtoModel):
|
|
41
|
+
__constructor__ = 0x12
|
|
42
|
+
__header__ = (Constructor(1), PayloadLength(1))
|
|
43
|
+
|
|
44
|
+
id: UInt32
|
|
45
|
+
name: str = field(prefix_length=1)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
user = User(id=42, name="Anna")
|
|
49
|
+
data = user.encode()
|
|
50
|
+
restored = User.decode(data)
|
|
51
|
+
|
|
52
|
+
print(data.hex(" ")) # 12 09 00 00 00 2a 04 41 6e 6e 61
|
|
53
|
+
print(restored.name) # Anna
|
|
54
|
+
assert restored == user
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`12` identifies the message; `09` is the computed length of its fields.
|
|
58
|
+
Next come the four-byte `id` and a UTF-8 name with a one-byte length prefix.
|
|
59
|
+
In your application, these are ordinary `int` and `str` values. Your first
|
|
60
|
+
models can use the default framing without setting `__header__`; for an
|
|
61
|
+
existing format, you can customize or remove it.
|
|
62
|
+
|
|
63
|
+
bytespec fits custom or existing sequential binary protocols when you want
|
|
64
|
+
to work with classes without a separate schema language or code generation.
|
|
65
|
+
It supports optional fields through flags, `IntEnum`, UUID, datetime, numeric
|
|
66
|
+
encodings, and custom codecs. This is a small 0.1.0 library: for a flexible
|
|
67
|
+
binary parser DSL, consider Construct; for standard JSON/MessagePack, msgspec;
|
|
68
|
+
for a schema ecosystem with its own wire format, protobuf.
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
Python 3.10+:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install bytespec
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Or `uv add bytespec`.
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
|
|
82
|
+
The full guide and API reference are available in Russian and English.
|
|
83
|
+
To build both versions from a checkout:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
uv sync --locked --group docs
|
|
87
|
+
uv run --no-sync python -m sphinx -E -a -n -W --keep-going -b html -D language=ru docs docs/_build/html/ru
|
|
88
|
+
uv run --no-sync python -m sphinx -E -a -n -W --keep-going -b html -D language=en docs docs/_build/html/en
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Open `docs/_build/html/en/index.html`. The language switcher keeps you on
|
|
92
|
+
the same page. Start with **Getting started**, **Why bytespec?**, or
|
|
93
|
+
**Headers and framing**, then consult the **API reference**.
|
|
94
|
+
The [documentation sources](docs/index.rst) and
|
|
95
|
+
[build instructions](docs/building.rst) are in Russian; Sphinx applies
|
|
96
|
+
the [English translation catalogs](docs/locale/en/LC_MESSAGES/index.po)
|
|
97
|
+
when building the English version.
|
|
98
|
+
|
|
99
|
+
Take a known packet from your protocol, describe a few fields, and compare
|
|
100
|
+
the re-encoded result with the original bytes. That is a useful first check
|
|
101
|
+
of whether the library fits your format.
|
|
102
|
+
|
|
103
|
+
[MIT license](LICENSE).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
bytespec/__init__.py,sha256=L2s5JSDjoixP9aSOftmNLSEKfiZZ70TkLpX7EURARw0,446
|
|
2
|
+
bytespec/base.py,sha256=OSx_NxVK0SaayWBv37RMYvreqQzPMfVxh1xUBNYtMBU,1847
|
|
3
|
+
bytespec/codecs/__init__.py,sha256=3vuT_fvaGSsu6mHagqAXqo1ldCK7MHJ1980XYh5Z1Lk,973
|
|
4
|
+
bytespec/codecs/_utils.py,sha256=LGgt7damTPbyJ9B96jzov-P9C4HfPSZqvsWLBX19wDc,531
|
|
5
|
+
bytespec/codecs/base.py,sha256=gjmKYwJjq3r8cKDQESd0ETRQZFBVnqibbaQjoZVDBc4,2026
|
|
6
|
+
bytespec/codecs/bool.py,sha256=zBiL7sXKKURuMh_lWWkPxk6TES6Zcv1-MhYCG_RFf34,1249
|
|
7
|
+
bytespec/codecs/bytes.py,sha256=epICoyDjSd8iYGE9NApye8D5qVTB0L2Wy9W7VeqeBhM,3178
|
|
8
|
+
bytespec/codecs/datetime.py,sha256=Xeyrh-TH4dsqdlqTo4XGNpXhUpgd-yEov9kmzMcv9Nw,2091
|
|
9
|
+
bytespec/codecs/enum.py,sha256=JW_txevzslLhycgeCALoJ7lcHQNt96ONpnXhf3BaN9c,1573
|
|
10
|
+
bytespec/codecs/float.py,sha256=sFnGwnMEU4tBe2DHm2wapFUBSCIIswudbUWt3BZQDeI,529
|
|
11
|
+
bytespec/codecs/int.py,sha256=C-ktOUwLMA12jH-sv-HvmZETW6T7viJYHoY4vvyTNCI,3754
|
|
12
|
+
bytespec/codecs/list.py,sha256=TIAzORqZaD2q5EBSeKX8KmhXdr0gMeBrJwTUFa92NIM,2620
|
|
13
|
+
bytespec/codecs/map.py,sha256=XdnJDrIqN6JQO5XqLCiNB5eMxNljF1IFOuWp4BDohSo,450
|
|
14
|
+
bytespec/codecs/model.py,sha256=MUfTi3VOYtZcdTmNuSp2SzlW3909jfobPRe-mmveJEU,1342
|
|
15
|
+
bytespec/codecs/str.py,sha256=cCEz9BmTgHE3pQXSit01Byz2R5mpwkK5_-E_NPizwTU,3032
|
|
16
|
+
bytespec/codecs/uuid.py,sha256=75waS0z5A4RUcAv96O_uQobI_CCkmCiG1w4H6r_Jnv4,1118
|
|
17
|
+
bytespec/codecs/varint.py,sha256=3ejY0ncFA2zLoA04wLtDUYsiSp2h6yyFN94-DGNCqVE,3471
|
|
18
|
+
bytespec/core.py,sha256=AxsNQ7oq0CwudXhgAHFlkfYieOHrdnSFDNV8CiKaekI,18921
|
|
19
|
+
bytespec/enums.py,sha256=Fo6FjGsNgA6t93eI4C543RS7qb3FU80STup42uMIF6k,368
|
|
20
|
+
bytespec/errors.py,sha256=nlOgCkYVK7roR4ZlLRlAopWAu4XP5BgQA8Yy7ElYH-o,954
|
|
21
|
+
bytespec/headers.py,sha256=mZHrE5-Z5wXzwSUVQ2bhQV__zr8aK2vTf6sSveSI2go,4657
|
|
22
|
+
bytespec/missing.py,sha256=ABFcpZl_gJMU6wO-MfLluUOV4Bxb3n5wi2hqQFyOOcc,296
|
|
23
|
+
bytespec/models.py,sha256=ZZOxKWHj0hrZe23UC9UqYjh6kAoSO8WJQGBGXyOTaaU,1377
|
|
24
|
+
bytespec/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
+
bytespec/resolver.py,sha256=iOB63wFCjb95pMBb49xSRRwWSHZMeQiGUnp01t04Pto,5914
|
|
26
|
+
bytespec/resolvers.py,sha256=tDNin1KH1J-kXp_6ZS4DBM0BAosCdKI_IWQaS5A_JM0,3590
|
|
27
|
+
bytespec/schema.py,sha256=i5hqVeUC52tyUmOvRt7spsA2bT9vTmXL4yu54ttLQxE,761
|
|
28
|
+
bytespec/types/__init__.py,sha256=gNi2fkiwx16T8Ssv3tI9xPb0bThtgDnLvDeTxvU-viY,875
|
|
29
|
+
bytespec/types/spec.py,sha256=ntTnvhwwXk6qXAU9pNil9fkh4TxaSsAvKlvyqULkod4,1902
|
|
30
|
+
bytespec-0.1.0.dist-info/licenses/LICENSE,sha256=b4HOpR9v54uGV7-BR4xWN5pu3pjhJWWRQRdWutlWPwA,1070
|
|
31
|
+
bytespec-0.1.0.dist-info/WHEEL,sha256=Qb5DWjqM6GZuPp3VmTlAFSGqNRK8vceyVqRFxrfa8YA,80
|
|
32
|
+
bytespec-0.1.0.dist-info/METADATA,sha256=9NM3l750FAl-tTCcAsHlYXmm1d_mKBrSMZbxaH7-Hog,3773
|
|
33
|
+
bytespec-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ink-developer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|