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/core.py ADDED
@@ -0,0 +1,500 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from typing import (
6
+ Any,
7
+ ClassVar,
8
+ get_type_hints,
9
+ )
10
+ from uuid import UUID
11
+
12
+ from typing_extensions import Self, dataclass_transform, override
13
+
14
+ from bytespec.base import DEFAULT_HEADER, ProtoModelBase
15
+ from bytespec.codecs import ICodec
16
+ from bytespec.codecs._utils import check_available
17
+ from bytespec.headers import Flags
18
+ from bytespec.missing import MISSING, MissingType
19
+ from bytespec.models import DefaultFactory, FieldInfo, PrefixLength
20
+ from bytespec.resolver import CodecResolver
21
+ from bytespec.resolvers import (
22
+ CodecFactory,
23
+ bool_codec_factory,
24
+ bytes_codec_factory,
25
+ datetime_codec_factory,
26
+ int_codec_factory,
27
+ str_codec_factory,
28
+ uuid_codec_factory,
29
+ )
30
+ from bytespec.schema import HeaderDecodeContext, HeaderEncodeContext, ModelSchema
31
+
32
+ from .errors import DecodeError, EncodeError, SchemaError
33
+
34
+
35
+ @dataclass
36
+ class FieldMetadata:
37
+ name: str
38
+ index: int
39
+ flag: int | None
40
+ default: Any
41
+ default_factory: DefaultFactory
42
+ annotation: Any
43
+ codec: ICodec[Any]
44
+
45
+
46
+ def field(
47
+ index: int | None = None,
48
+ *,
49
+ flag: int | None = None,
50
+ default: Any = MISSING,
51
+ default_factory: DefaultFactory = MISSING,
52
+ prefix_length: PrefixLength | None = None,
53
+ encoding: str = "utf-8",
54
+ codec: ICodec[Any] | None = None,
55
+ ) -> Any:
56
+ """Configure a serializable model field.
57
+
58
+ Args:
59
+ index: The field position, starting at zero. If omitted, the smallest available index is
60
+ selected in field declaration order.
61
+ flag: The presence bit number (0–63). Required for ``T | None``; must be unique and fit
62
+ within the model's flags size.
63
+ default: The value used when the constructor argument is omitted. Explicit ``None`` differs
64
+ from having no default and is only valid for optional fields.
65
+ default_factory: A function with no arguments that creates a value when the field is
66
+ omitted. Cannot be specified together with ``default``.
67
+ prefix_length: The length prefix size: 1, 2, 4, or 8 bytes, or ``VarUInt``. ``None`` keeps
68
+ the codec's default setting. For a list, configures the list itself, not its items.
69
+ encoding: The encoding of text values. Defaults to UTF-8.
70
+ codec: A ready-to-use codec instance that replaces automatic selection. Other field settings
71
+ do not reconfigure this instance.
72
+
73
+ Returns:
74
+ A field description for use in the body of a model class.
75
+
76
+ Note:
77
+ Setting compatibility is checked at model declaration. Unsupported or conflicting settings
78
+ raise ``SchemaError``.
79
+ """
80
+ return FieldInfo(
81
+ index=index,
82
+ flag=flag,
83
+ default=default,
84
+ default_factory=default_factory,
85
+ prefix_length=prefix_length,
86
+ encoding=encoding,
87
+ codec=codec,
88
+ )
89
+
90
+
91
+ @dataclass_transform(field_specifiers=(field,))
92
+ class ProtoModel(ProtoModelBase):
93
+ """The base model class for binary serialization.
94
+
95
+ Declare fields using annotations and create an instance with keyword arguments. ``encode()``
96
+ writes a message; ``decode()`` reconstructs an instance of the same class. Use ``field()`` to
97
+ configure individual fields.
98
+
99
+ Args:
100
+ **kwargs: Field values. Omitted fields receive a default, the result of a factory, or
101
+ ``None`` for optional fields; all other fields are required.
102
+
103
+ Note:
104
+ Annotations define the binary format, but explicitly supplied values are not fully
105
+ type-checked by the constructor. Fields are mutable. The schema is validated when a subclass
106
+ is declared. If a subclass declares its own fields, the parent's serializable fields are not
107
+ merged with them.
108
+ """
109
+
110
+ _scalar_codecs: ClassVar[dict[type, CodecFactory]] = {
111
+ int: int_codec_factory,
112
+ str: str_codec_factory,
113
+ bool: bool_codec_factory,
114
+ bytes: bytes_codec_factory,
115
+ datetime: datetime_codec_factory,
116
+ UUID: uuid_codec_factory,
117
+ }
118
+ _resolver = CodecResolver(_scalar_codecs)
119
+
120
+ def __init__(self, **kwargs: Any) -> None:
121
+ """Create an instance from named field values.
122
+
123
+ Args:
124
+ **kwargs: Values of declared fields; omitted fields use the default, factory, or
125
+ ``None`` for optional fields.
126
+
127
+ Raises:
128
+ TypeError: An unknown field, a missing required field, or an invalid type for a default
129
+ value or factory result.
130
+
131
+ Note:
132
+ After assigning the fields, ``__validate__`` is called if defined directly on the
133
+ concrete class. Its exceptions are not wrapped.
134
+ """
135
+ field_names = {field.name for field in self.__schema__.fields}
136
+ unknown = kwargs.keys() - field_names
137
+
138
+ if unknown:
139
+ raise TypeError(f"Unknown fields: {', '.join(unknown)}")
140
+
141
+ for field in self.__schema__.fields:
142
+ value = kwargs.get(field.name, MISSING)
143
+
144
+ if value is MISSING:
145
+ if field.default is not MISSING and field.default_factory is not MISSING:
146
+ raise SchemaError(
147
+ f"{field.name}: default and default_factory cannot be specified together"
148
+ )
149
+
150
+ if field.default is not MISSING:
151
+ value = field.default
152
+
153
+ elif not isinstance(field.default_factory, MissingType):
154
+ value = field.default_factory()
155
+ elif field.flag is not None:
156
+ value = None
157
+ else:
158
+ raise TypeError(f"Missing required field: {field.name}")
159
+
160
+ if not self._is_valid_value(value, field.annotation):
161
+ raise TypeError(
162
+ f"{field.name}: invalid default value {value!r}; expected {field.annotation}, got {type(value).__name__}"
163
+ )
164
+
165
+ setattr(self, field.name, value)
166
+
167
+ validator = type(self).__dict__.get("__validate__")
168
+
169
+ if validator is not None:
170
+ validator(self)
171
+
172
+ def __init_subclass__(cls) -> None:
173
+ super().__init_subclass__()
174
+
175
+ model_codecs = cls._scalar_codecs.copy()
176
+ model_codecs.update(cls.configure_codecs())
177
+ cls._scalar_codecs = model_codecs
178
+ cls._resolver.scalar_codecs = cls._scalar_codecs
179
+
180
+ if "__header__" not in cls.__dict__:
181
+ proto_bases = [base for base in cls.__bases__ if issubclass(base, ProtoModelBase)]
182
+ custom_headers = [
183
+ base.__header__ for base in proto_bases if base.__header__ is not DEFAULT_HEADER
184
+ ]
185
+
186
+ if custom_headers:
187
+ first_header = custom_headers[0]
188
+
189
+ if not all(header is first_header for header in custom_headers):
190
+ raise SchemaError(
191
+ f"{cls.__name__}: conflicting inherited headers; define __header__ explicitly"
192
+ )
193
+
194
+ cls.__header__ = first_header
195
+
196
+ fields: list[FieldMetadata] = []
197
+
198
+ indexes: set[int] = set()
199
+ flags: set[int] = set()
200
+
201
+ try:
202
+ raw_annotations = cls.__annotations__
203
+ resolved_annotations = get_type_hints(cls, include_extras=True)
204
+ except (NameError, TypeError) as exc:
205
+ raise SchemaError(f"{cls.__name__}: cannot resolve annotations: {exc}") from exc
206
+
207
+ for name, value in cls.__dict__.items():
208
+ if isinstance(value, FieldInfo) and name not in raw_annotations:
209
+ raise SchemaError(f"{cls.__name__}.{name}: field requires a type annotation")
210
+
211
+ for name in raw_annotations:
212
+ annotation = resolved_annotations[name]
213
+ raw_value = cls.__dict__.get(name, MISSING)
214
+
215
+ if raw_value is MISSING:
216
+ value = FieldInfo(None, None, MISSING, MISSING, None)
217
+ elif isinstance(raw_value, FieldInfo):
218
+ value = raw_value
219
+ else:
220
+ continue
221
+
222
+ if value.default is not MISSING and value.default_factory is not MISSING:
223
+ raise SchemaError(
224
+ f"{cls.__name__}.{name}: default and default_factory cannot be specified together"
225
+ )
226
+
227
+ try:
228
+ resolved_type = cls._resolver.resolve(annotation, value)
229
+ except SchemaError as exc:
230
+ exc.args = (f"{cls.__name__}.{name}: {exc}",)
231
+ raise
232
+
233
+ if value.default is not MISSING:
234
+ if value.default is None:
235
+ if not resolved_type.optional:
236
+ raise SchemaError(
237
+ f"{cls.__name__}.{name}: None default requires an optional field"
238
+ )
239
+ elif not cls._is_valid_value(value.default, resolved_type.annotation):
240
+ raise SchemaError(
241
+ f"Invalid default value: {value.default!r}. "
242
+ + f"Expected {resolved_type.annotation}, "
243
+ + f"got {type(value.default).__name__}"
244
+ )
245
+
246
+ if value.flag is not None and not resolved_type.optional:
247
+ raise SchemaError(f"{cls.__name__}.{name}: flag requires an optional field")
248
+
249
+ if resolved_type.optional:
250
+ if value.flag is None:
251
+ raise SchemaError(
252
+ f"{annotation} is a UnionType with NoneType but no flag index presented"
253
+ )
254
+ if not isinstance(value.flag, int) or not 0 <= value.flag < 64:
255
+ raise SchemaError(
256
+ f"{cls.__name__}.{name}: flag {value.flag!r} must be in range 0..63"
257
+ )
258
+
259
+ if value.flag in flags:
260
+ raise SchemaError(f"{cls.__name__}.{name}: duplicate flag {value.flag}")
261
+
262
+ flags.add(value.flag)
263
+
264
+ if value.index is not None:
265
+ index = value.index
266
+ if not isinstance(index, int) or index < 0:
267
+ raise SchemaError(f"{cls.__name__}.{name}: invalid field index {index!r}")
268
+ else:
269
+ index = 0
270
+ while index in indexes:
271
+ index += 1
272
+
273
+ codec = resolved_type.codec if not value.codec else value.codec
274
+
275
+ if not isinstance(codec, ICodec):
276
+ raise SchemaError(f"Invalid codec type: {type(codec)}")
277
+
278
+ fields.append(
279
+ FieldMetadata(
280
+ name=name,
281
+ index=index,
282
+ flag=value.flag,
283
+ annotation=annotation,
284
+ codec=codec,
285
+ default=value.default,
286
+ default_factory=value.default_factory,
287
+ )
288
+ )
289
+
290
+ if isinstance(raw_value, FieldInfo):
291
+ delattr(cls, name)
292
+
293
+ indexes.add(index)
294
+
295
+ fields.sort(key=lambda x: x.index)
296
+ own_fields = fields
297
+
298
+ parent_fields = cls.__schema__.fields if getattr(cls, "__schema__", None) else ()
299
+
300
+ effective_fields = tuple(own_fields or parent_fields)
301
+
302
+ has_flags = any(isinstance(element, Flags) for element in cls.__header__)
303
+
304
+ if any(field.flag is not None for field in effective_fields) and not has_flags:
305
+ raise SchemaError("Found optional fields in model but no Flags presented in header")
306
+
307
+ if fields and (
308
+ min(indexes) != 0 or max(indexes) != len(fields) - 1 or max(indexes) != len(indexes) - 1
309
+ ):
310
+ raise SchemaError(
311
+ f"{cls.__name__}: field indexes must be unique and contiguous from 0, got {[field.index for field in fields]}"
312
+ )
313
+
314
+ cls.__schema__ = ModelSchema(
315
+ model=cls,
316
+ fields=effective_fields,
317
+ header=cls.__header__,
318
+ byte_order=cls.__byte_order__,
319
+ constructor=cls.__constructor__,
320
+ )
321
+
322
+ for header in cls.__schema__.header:
323
+ header.validate(cls.__schema__)
324
+
325
+ def encode(self, include_constructor: bool = True) -> bytes:
326
+ """Write the current model state as a single binary message.
327
+
328
+ Args:
329
+ include_constructor: Write Constructor elements from __header__. False omits them
330
+ entirely, retaining the other elements.
331
+
332
+ Returns:
333
+ The __header__ elements in the specified order, followed by the fields. Optional fields
334
+ whose value is ``None`` are not written.
335
+
336
+ Raises:
337
+ EncodeError: A required value is missing, or a field value cannot be written in the
338
+ selected representation.
339
+
340
+ Note:
341
+ Implementation errors in a custom codec are not wrapped automatically. __validate__ is
342
+ not called again.
343
+ """
344
+ flags: int = 0 # u64
345
+ payload: bytes = b""
346
+
347
+ for field in self.__schema__.fields:
348
+ value = getattr(self, field.name, MISSING)
349
+
350
+ if value is MISSING or (value is None and field.flag is None):
351
+ raise EncodeError(f"{type(self).__name__}.{field.name}: missing required value")
352
+
353
+ if value is None:
354
+ continue
355
+
356
+ if field.flag is not None:
357
+ flags |= 1 << field.flag
358
+
359
+ try:
360
+ payload += field.codec.encode(value, self.__byte_order__)
361
+ except EncodeError as exc:
362
+ exc.args = (f"{type(self).__name__}.{field.name}: {exc}",)
363
+ raise
364
+
365
+ ctx = HeaderEncodeContext(self.__schema__, include_constructor, len(payload), flags)
366
+
367
+ header = b"".join(element.encode(ctx) for element in self.__header__)
368
+
369
+ return header + payload
370
+
371
+ @classmethod
372
+ def decode_from(
373
+ cls,
374
+ buffer: bytes,
375
+ offset: int,
376
+ expect_constructor: bool = True,
377
+ ) -> tuple[Self, int]:
378
+ """Read a single message from the specified position in a buffer.
379
+
380
+ Args:
381
+ buffer: A buffer containing the complete message.
382
+ offset: The nonnegative absolute position where the message starts.
383
+ expect_constructor: Read and validate Constructor elements. False omits them entirely;
384
+ the corresponding bytes must not be present in the input.
385
+
386
+ Returns:
387
+ A pair containing the new instance and the absolute position after the message. The
388
+ position can be passed to the next call to read the following message.
389
+
390
+ Raises:
391
+ DecodeError: Incomplete or invalid data, an incorrect constructor, or a field extending
392
+ beyond the declared payload bounds.
393
+ ValueError: Negative offset.
394
+
395
+ Note:
396
+ Trailing data within the declared payload and unknown flag bits are skipped without
397
+ being preserved. Bytes after the message are left to the caller. Network fragments are
398
+ not accumulated between calls. Without PayloadLength, the end is determined by the known
399
+ fields, with no separate body boundary. Creating the instance runs its own __validate__;
400
+ validator exceptions propagate without wrapping.
401
+ """
402
+ ctx = HeaderDecodeContext(cls.__schema__, expect_constructor)
403
+
404
+ for element in cls.__header__:
405
+ offset = element.decode(buffer, offset, ctx)
406
+
407
+ if ctx.payload_length is not None:
408
+ payload_start = offset
409
+ payload_end = payload_start + ctx.payload_length
410
+
411
+ check_available(buffer, payload_start, ctx.payload_length, codec=cls.__name__)
412
+ payload_buffer = buffer[:payload_end]
413
+ else:
414
+ payload_buffer = buffer
415
+ payload_end = None
416
+
417
+ args = {}
418
+
419
+ for field in cls.__schema__.fields: # it already sorted i think
420
+ if field.flag is not None and not (ctx.flags & (1 << field.flag)):
421
+ args[field.name] = None
422
+ continue
423
+
424
+ try:
425
+ value, offset = field.codec.decode(payload_buffer, cls.__byte_order__, offset)
426
+ except DecodeError as exc:
427
+ exc.args = (f"{cls.__name__}.{field.name}: {exc}",)
428
+ raise
429
+ args[field.name] = value
430
+
431
+ if payload_end is not None and offset > payload_end:
432
+ raise DecodeError(
433
+ f"{cls.__name__}.{field.name} at offset {offset}: exceeds payload boundary {payload_end}"
434
+ )
435
+
436
+ if payload_end is not None and offset > payload_end:
437
+ raise DecodeError(
438
+ f"{cls.__name__} at offset {offset}: decoded past payload boundary {payload_end}"
439
+ )
440
+
441
+ if payload_end is not None:
442
+ offset = payload_end
443
+
444
+ return cls(**args), offset
445
+
446
+ @classmethod
447
+ def decode(cls, buffer: bytes) -> Self:
448
+ """Reconstruct a model from a buffer containing exactly one message.
449
+
450
+ Args:
451
+ buffer: A complete binary message matching the model class.
452
+
453
+ Returns:
454
+ A new instance of the class with the field values read from the buffer.
455
+
456
+ Raises:
457
+ DecodeError: Incomplete/invalid data, an incorrect constructor, or extra bytes after the
458
+ end of the message.
459
+
460
+ Note:
461
+ For multiple messages in a single buffer, use ``decode_from``. Unknown trailing data
462
+ within the declared payload is skipped.
463
+ """
464
+ model, offset = cls.decode_from(buffer, 0)
465
+
466
+ if offset != len(buffer):
467
+ raise DecodeError(
468
+ f"{cls.__name__} at offset {offset}: trailing data, {len(buffer) - offset} bytes"
469
+ )
470
+
471
+ return model
472
+
473
+ @override
474
+ def __eq__(self, other: object) -> bool:
475
+ if type(self) is not type(other):
476
+ return NotImplemented
477
+
478
+ return all(
479
+ getattr(self, field.name) == getattr(other, field.name)
480
+ for field in self.__schema__.fields
481
+ )
482
+
483
+ @override
484
+ def __repr__(self) -> str:
485
+ fields = ", ".join(
486
+ f"{field.name}={getattr(self, field.name)!r}" for field in self.__schema__.fields
487
+ )
488
+ return f"{type(self).__name__}({fields})"
489
+
490
+ def __validate__(self) -> None:
491
+ """Validate relationships between field values that have already been assigned.
492
+
493
+ Override this method in a concrete model and raise an exception if an invariant is violated.
494
+ The return value is ignored. The method is called automatically at the end of __init__,
495
+ including during decode, only if it is defined on the concrete class itself. To run parent
496
+ validation, call super().__validate__() from the child method.
497
+
498
+ Attribute assignments and encode do not call the method again. User exceptions propagate
499
+ without wrapping.
500
+ """
bytespec/enums.py ADDED
@@ -0,0 +1,14 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class ByteOrder(str, Enum):
7
+ """The byte order of the model's fixed-width numbers and prefixes.
8
+
9
+ ``BIG`` means big-endian (``>``), and ``LITTLE`` means little-endian (``<``). This setting does
10
+ not affect varints, bytes contents, or the UUID representation.
11
+ """
12
+
13
+ BIG = ">"
14
+ LITTLE = "<"
bytespec/errors.py ADDED
@@ -0,0 +1,29 @@
1
+ # Copyright (c) 2026 ink-developer
2
+
3
+
4
+ class BytespecError(Exception):
5
+ """Common base class for schema, encoding, and decoding errors."""
6
+
7
+
8
+ class SchemaError(BytespecError):
9
+ """Unsupported or conflicting model or codec types/settings.
10
+
11
+ Usually raised when declaring a model or creating a codec. Some configuration errors, such as a
12
+ non-text string encoding, surface during writing or reading.
13
+ """
14
+
15
+
16
+ class EncodeError(BytespecError):
17
+ """The value cannot be written in the selected binary representation.
18
+
19
+ For example, a number outside its range, a length prefix overflow, text that cannot be
20
+ represented in the encoding, or a missing required value.
21
+ """
22
+
23
+
24
+ class DecodeError(BytespecError):
25
+ """The buffer is incomplete or does not match the expected binary format.
26
+
27
+ For example, an incorrect constructor, missing bytes, an invalid value, or data after the
28
+ message when calling ``ProtoModel.decode()``.
29
+ """
bytespec/headers.py ADDED
@@ -0,0 +1,143 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from typing_extensions import override
4
+
5
+ from bytespec.codecs.map import UINT_CODEC_MAPPING
6
+ from bytespec.errors import DecodeError, SchemaError
7
+ from bytespec.models import UIntEncoding
8
+ from bytespec.types import VarUInt
9
+
10
+ from .schema import HeaderDecodeContext, HeaderEncodeContext, ModelSchema
11
+
12
+
13
+ class HeaderElement(ABC):
14
+ @abstractmethod
15
+ def validate(self, schema: ModelSchema) -> None: ...
16
+
17
+ @abstractmethod
18
+ def encode(self, ctx: HeaderEncodeContext) -> bytes: ...
19
+
20
+ @abstractmethod
21
+ def decode(
22
+ self,
23
+ buffer: bytes,
24
+ offset: int,
25
+ ctx: HeaderDecodeContext,
26
+ ) -> int: ...
27
+
28
+
29
+ class IntegerHeaderElement(HeaderElement):
30
+ def __init__(self, encoding: UIntEncoding) -> None:
31
+ self.encoding = encoding
32
+
33
+ codec = UINT_CODEC_MAPPING.get(self.encoding)
34
+
35
+ if not codec:
36
+ raise SchemaError(f"Invalid encdoing for HeaderElement {type(self).__name__}")
37
+
38
+ self.codec = codec
39
+
40
+
41
+ class Constructor(IntegerHeaderElement):
42
+ """Model identifier from **constructor**, validated during decoding.
43
+
44
+ Args:
45
+ encoding: Unsigned integer width in bytes (1, 2, 4, 8) or VarUInt.
46
+
47
+ Note:
48
+ The int type is validated when building the schema; the value range
49
+ is validated during encoding. Nested models skip this element.
50
+ """
51
+
52
+ @override
53
+ def validate(self, schema: ModelSchema) -> None:
54
+ if type(schema.constructor) is not int:
55
+ raise SchemaError(f"Invalid constructor type: {type(schema.constructor).__name__}")
56
+
57
+ @override
58
+ def decode(self, buffer: bytes, offset: int, ctx: HeaderDecodeContext) -> int:
59
+ if not ctx.expect_constructor:
60
+ return offset
61
+
62
+ value, offset = self.codec.decode(
63
+ buffer,
64
+ ctx.schema.byte_order,
65
+ offset,
66
+ )
67
+
68
+ if value != ctx.schema.constructor:
69
+ raise DecodeError(f"wrong constructor {value:#x}, expected {ctx.schema.constructor:#x}")
70
+
71
+ return offset
72
+
73
+ @override
74
+ def encode(self, ctx: HeaderEncodeContext) -> bytes:
75
+ if not ctx.include_constructor:
76
+ return b""
77
+ return self.codec.encode(ctx.schema.constructor, ctx.schema.byte_order)
78
+
79
+
80
+ class PayloadLength(IntegerHeaderElement):
81
+ """The size of all encoded fields, excluding the entire header.
82
+
83
+ Args:
84
+ encoding: The unsigned integer width in bytes (1, 2, 4, 8), or VarUInt.
85
+
86
+ Note:
87
+ When reading, bounds the field buffer by the declared length. The position of this element
88
+ within the header does not change the meaning of the stored number.
89
+ """
90
+
91
+ @override
92
+ def validate(self, schema: ModelSchema) -> None: ...
93
+
94
+ @override
95
+ def decode(self, buffer: bytes, offset: int, ctx: HeaderDecodeContext) -> int:
96
+ value, offset = self.codec.decode(buffer, ctx.schema.byte_order, offset)
97
+ ctx.payload_length = value
98
+ return offset
99
+
100
+ @override
101
+ def encode(self, ctx: HeaderEncodeContext) -> bytes:
102
+ return self.codec.encode(ctx.payload_length, ctx.schema.byte_order)
103
+
104
+
105
+ class Flags(IntegerHeaderElement):
106
+ """A bitmap indicating the presence of optional fields, computed from their values.
107
+
108
+ Args:
109
+ encoding: The unsigned integer width in bytes (1, 2, 4, 8), or VarUInt.
110
+
111
+ Note:
112
+ A field is present if its value is not None. Bit numbers are set using field(flag=...) and
113
+ are limited to 0–63; with a fixed width, all used bits are checked to ensure they fit.
114
+ """
115
+
116
+ @override
117
+ def validate(self, schema: ModelSchema) -> None:
118
+ if self.encoding is VarUInt: # pyright: ignore[reportUnnecessaryComparison] # I hate pylance. Because of difference between static analyzer and real runtime
119
+ return
120
+
121
+ flagged_fields = [field for field in schema.fields if field.flag is not None]
122
+
123
+ if not flagged_fields:
124
+ return
125
+
126
+ max_flag = max(field.flag for field in flagged_fields if field.flag is not None)
127
+ required_bits = max_flag + 1
128
+ available_bits = self.encoding * 8
129
+
130
+ if required_bits > available_bits:
131
+ raise SchemaError(
132
+ f"Flags encoding provides {available_bits} bits, model has field with flag {max_flag}"
133
+ )
134
+
135
+ @override
136
+ def decode(self, buffer: bytes, offset: int, ctx: HeaderDecodeContext) -> int:
137
+ value, offset = self.codec.decode(buffer, ctx.schema.byte_order, offset)
138
+ ctx.flags = value
139
+ return offset
140
+
141
+ @override
142
+ def encode(self, ctx: HeaderEncodeContext) -> bytes:
143
+ return self.codec.encode(ctx.flags, ctx.schema.byte_order)