Строки, длины и настройки полей¶
До сих пор мы использовали формат по умолчанию. Теперь рассмотрим случаи, когда для поля нужны другие настройки.
Выбрать кодировку¶
Например, сообщение должно содержать только ASCII-текст:
from bytespec import ProtoModel, field
class Message(ProtoModel):
text: str = field(encoding="ascii")
message = Message(text="Hello!")
print(Message.decode(message.encode()).text) # Hello!
Без encoding используется UTF-8. В ASCII нельзя записать кириллицу —
encode() выдаст EncodeError. Неизвестное имя кодировки вызывает
SchemaError. Те же настройки применяются к строковым enum и datetime.
Изменить префикс длины¶
Перед строкой библиотека записывает её длину в байтах. Это число называется префиксом длины и по умолчанию занимает 4 байта. Для короткого текста можно выбрать более узкий префикс:
class Message(ProtoModel):
text: str = field(prefix_length=1)
message = Message(text="Hello!")
print(Message.decode(message.encode()).text) # Hello!
prefix_length=1 отводит один байт для длины, не для самого текста.
Теперь поле вмещает до 255 закодированных байтов. Для UTF-8 это не всегда
255 символов: например, "я" занимает 2 байта.
|
Размер префикса |
Максимальная длина данных |
|---|---|---|
|
1 байт |
255 байт |
|
2 байта |
65 535 байт |
|
4 байта |
|
|
8 байт |
|
|
1–10 байт |
|
Это предел префикса поля. Размер содержимого всей модели имеет отдельный
предел, заданный PayloadLength в Header и framing (по умолчанию
2**32 - 1 байт). Сам префикс в записанное им число не входит.
Для bytes действуют те же правила. Вот префикс переменной длины:
from bytespec.types import VarUInt
class Packet(ProtoModel):
data: bytes = field(prefix_length=VarUInt)
packet = Packet(data=b"ABC")
print(Packet.decode(packet.encode()).data) # b'ABC'
Передаётся именно VarUInt. VarInt как префикс не поддерживается.
prefix_length=None в field() оставляет значение по умолчанию,
а не отключает префикс. Для фиксированного количества байтов без префикса
есть готовое решение — Взять готовый codec.
Переиспользовать настройку¶
Когда несколько полей имеют одинаковый формат, его удобно назвать.
Annotated связывает Python-тип с настройками bytespec:
from typing import Annotated
from bytespec.types import CodecSpec
ShortText = Annotated[str, CodecSpec(prefix_length=1, encoding="ascii")]
class User(ProtoModel):
name: ShortText
city: ShortText
user = User(name="Anna", city="Oslo")
print(User.decode(user.encode()).city) # Oslo
ShortText остаётся строкой для Python-кода, но каждое такое поле
записывается в ASCII с однобайтовой длиной. CodecSpec объединяет
настройки, которые до этого мы передавали в field().
Настроить элементы списка¶
Теперь можно использовать ShortText внутри списка:
class Message(ProtoModel):
tags: list[ShortText] = field(prefix_length=2)
message = Message(tags=["red", "blue"])
print(Message.decode(message.encode()).tags) # ['red', 'blue']
Здесь длина всего списка записывается в 2 байта, длина каждой строки — в 1 байт. Длина списка означает общий размер закодированных элементов в байтах, не их количество.
field() списка настраивает только сам список. Поэтому
list[str] = field(prefix_length=2, encoding="ascii") оставил бы элементы
в UTF-8 с четырёхбайтовыми длинами. Настройки элементов задаются через их тип,
как в примере выше.
Для чисел и UUID prefix_length ничего не меняет. Вложенные модели тоже
сохраняют собственные настройки. Если совместить field() и CodecSpec
на одном поле, применяются Совместное использование настроек.
Далее — Собираем сообщение: соберём знакомые возможности в одно сообщение.