Файлы и несколько сообщений

После encode() у вас обычные байты. Их можно сохранить или соединить с другими сообщениями. Для примеров используем небольшую модель:

from bytespec import ProtoModel

class Message(ProtoModel):
    text: str

Сохранить и прочитать файл

from pathlib import Path

message = Message(text="Hello")
path = Path("message.bin")
path.write_bytes(message.encode())

restored = Message.decode(path.read_bytes())
print(restored.text)  # Hello

Используйте бинарные операции write_bytes() и read_bytes(): дополнительная текстовая кодировка здесь не нужна.

Изменить и записать заново

restored.text = "Updated"
print(Message.decode(restored.encode()).text)  # Updated

Атрибуты модели изменяемы. Каждый вызов encode() читает их текущие значения, в том числе заново определяет присутствие optional-полей. Байты, полученные предыдущим вызовом, не меняются.

Прочитать два сообщения из одного буфера

decode() ожидает ровно одно сообщение. Если в буфере их несколько, используйте decode_from():

buffer = Message(text="One").encode() + Message(text="Two").encode()

first, offset = Message.decode_from(buffer, 0)
second, offset = Message.decode_from(buffer, offset)

print(first.text, second.text)  # One Two
print(buffer[offset:])         # b''

Второй результат — абсолютная позиция после прочитанного сообщения. Передавайте её следующему вызову. Начальная позиция должна быть неотрицательной.

Начать чтение после префикса транспорта

buffer = b"MSG:" + Message(text="Hello").encode() + b"NEXT"
message, end = Message.decode_from(buffer, 4)

print(message.text)  # Hello
print(buffer[end:])  # b'NEXT'

end относится ко всему buffer, а не к срезу после MSG:. Следующие байты остаются вызывающему коду.

Оба метода читают готовый буфер. Они не накапливают сетевые фрагменты между вызовами: если сообщение неполное, возникает DecodeError. Сначала соберите его средствами своего транспорта. Проверки размеров и правила дополнительных данных описаны в Границы и неизвестные данные.

Если в буфере сообщения разных типов, приложение должно выбрать правильный класс для каждого вызова. Автоматического выбора модели по её идентификатору нет; детали заголовка — Binary / wire format.

Запись с пропуском Constructor

Если внешний слой уже записывает идентификатор, можно пропустить этот элемент, сохранив остальные части header:

from bytespec import Constructor, Flags, PayloadLength
from bytespec.types import UInt8

class Reading(ProtoModel):
    __header__ = (Flags(1), Constructor(1), PayloadLength(1))
    value: UInt8

reading = Reading(value=7)
encoded = reading.encode(include_constructor=False)
restored, end = Reading.decode_from(b"xx" + encoded, 2, expect_constructor=False)
assert restored == reading
print(encoded.hex(" "), end)
00 01 07 5

Оба параметра означают полное отсутствие байтов Constructor, где бы он ни стоял в header. expect_constructor=False не читает и не игнорирует произвольный идентификатор во входе. decode() такого параметра не имеет; используйте decode_from() и при необходимости проверьте итоговое смещение. ModelCodec применяет эту пару параметров автоматически при вложении.

Далее — Binary / wire format: точные байты, границы и неизвестные данные.