Package: hidden-pure-protobuf
pure-protobufThis guide describes how to usepure-protobufto structure your data. It tries to followthe standard developer guide. It also assumes that you're familiar with Protocol Buffers.Defining a message typeLet's look atthe simple example. Here's how it looks like inproto3syntax:syntax="proto3";messageSearchRequest{stringquery=1;int32page_number=2;int32result_per_page=3;}And this is how you define it withpure-protobuf:fromdataclassesimportdataclassfrompure_protobuf.dataclasses_importfield,messagefrompure_protobuf.typesimportint32@message@dataclassclassSearchRequest:query:str=field(1,default='')page_number:int32=field(2,default=int32(0))result_per_page:int32=field(3,default=int32(0))assertSearchRequest(query='hello',page_number=int32(1),result_per_page=int32(10),).dumps()==b'\x0A\x05hello\x10\x01\x18\x0A'Keep in mind that@messagedecoratormuststay on top of@dataclass.SerializingEach class wrapped with@messagegets two methods attached:dumps() -> bytesto serialize message into a byte stringdump(io: IO)to serialize message into a file-like objectDeserializingEach classes wrapped with@messagegets two class methods attached:loads(bytes_: bytes) -> TMessageto deserialize a message from a byte stringload(io: IO) -> TMessageto deserialize a message from a file-like objectThese methods are also available as standalone functions inpure_protobuf.dataclasses_:load(cls: Type[T], io: IO) -> Tloads(cls: Type[T], bytes_: bytes) -> TSpecifying field typesInpure-protobuftypes are specified withtype hints. Native Pythonfloat,str,bytesandbooltypes are supported. Since other Protocol Buffers types don't exist as native Python types, the package usesNewTypeto define them. They're available viapure_protobuf.typesand named in the same way.Assigning field numbersField numbers are provided via themetadataparameter of thefieldfunction:field(..., metadata={'number': number}). However, to improve readability and save some characters,pure-protobufprovides a helper functionpure_protobuf.dataclasses_.fieldwhich accepts field number as the first positional parameter and just passes it to the standardfieldfunction.Specifying field rulestyping.Listandtyping.Iterableannotations are automatically converted torepeated fields. Repeated fields of scalar numeric types use packed encoding by default:fromdataclassesimportdataclassfromtypingimportListfrompure_protobuf.dataclasses_importfield,messagefrompure_protobuf.typesimportint32@message@dataclassclassMessage:foo:List[int32]=field(1,default_factory=list)In case, unpacked encoding is explicitly wanted, thepacked-argument offieldcan be used as in:fromdataclassesimportdataclassfromtypingimportListfrompure_protobuf.dataclasses_importfield,messagefrompure_protobuf.typesimportint32@message@dataclassclassMessage:foo:List[int32]=field(1,default_factory=list,packed=False)It's also possible to wrap a field type withtyping.Optional. IfNoneis assigned to anOptionalfield, then the field will be skipped during serialization.Default valuesInpure-protobufit's developer's responsibility to take care of default values. If encoded message does not contain a particular element, the corresponding field stays unassigned. It means that the standarddefaultanddefault_factoryparameters of thefieldfunction work as usual:fromdataclassesimportdataclassfromtypingimportOptionalfrompure_protobuf.dataclasses_importfield,messagefrompure_protobuf.typesimportint32@message@dataclassclassFoo:bar:int32=field(1,default=42)qux:Optional[int32]=field(2,default=None)assertFoo().dumps()==b'\x08\x2A'assertFoo.loads(b'')==Foo(bar=42)In fact, the patternqux: Optional[int32] = field(2, default=None)is so common that there's a convenience functionoptional_fieldto define anOptionalfield withNonevalue by default:fromdataclassesimportdataclassfromtypingimportOptionalfrompure_protobuf.dataclasses_importoptional_field,messagefrompure_protobuf.typesimportint32@message@dataclassclassFoo:qux:Optional[int32]=optional_field(2)assertFoo().dumps()==b''assertFoo.loads(b'')==Foo(qux=None)EnumerationsSubclasses of the standardIntEnumclass are supported:fromdataclassesimportdataclassfromenumimportIntEnumfrompure_protobuf.dataclasses_importfield,messageclassTestEnum(IntEnum):BAR=1@message@dataclassclassTest:foo:TestEnum=field(1)assertTest(foo=TestEnum.BAR).dumps()==b'\x08\x01'assertTest.loads(b'\x08\x01')==Test(foo=TestEnum.BAR)Using other message typesEmbedded messages are defined the same way as normal dataclasses:fromdataclassesimportdataclassfrompure_protobuf.dataclasses_importfield,messagefrompure_protobuf.typesimportint32@message@dataclassclassTest1:a:int32=field(1,default=0)@message@dataclassclassTest3:c:Test1=field(3,default_factory=Test1)assertTest3(c=Test1(a=int32(150))).dumps()==b'\x1A\x03\x08\x96\x01'Well-known message typespure_protobuf.googlealso provides built-in definitions for the followingwell-known message types:Annotationpure_protobuf.types.google.protodatetimeTimestampTimestamptimedeltaDurationDurationtyping.AnyAny_AnyThey're handled automatically, you have nothing to do but use them normally in type hints:fromdataclassesimportdataclassfromdatetimeimportdatetimefromtypingimportOptionalfrompure_protobuf.dataclasses_importfield,message@message@dataclassclassTest:timestamp:Optional[datetime]=field(1,default=None)AnySincepure-protobufis not able to download or parse.protodefinitions, it provides a limited implementation of theAnymessage type. That is, you still have to define all message classes in the usual way. Then,pure-protobufwill be able to import and instantiate an encoded value:fromdataclassesimportdataclassfromtypingimportAny,Optionalfrompure_protobuf.dataclasses_importfield,messagefrompure_protobuf.types.googleimportTimestamp@message@dataclassclassMessage:value:Optional[Any]=field(1)# Here `Timestamp` is used just as an example, in principle any importable user type works.message=Message(value=Timestamp(seconds=42))assertMessage.loads(message.dumps())==message