Package: chili
🌶️ ChiliChili is an extensible library which provides a simple and efficient way to encode and decode complex Python objects to and from their dictionary representation.It offers complete coverage for thetypingpackage; including generics, and supports custom types, allowing you to extend the library to handle your specific needs.With support for nested data structures, default values, forward references, and data mapping and transformation, Chili is designed to be both easy to use and powerful enough to handle complex data structures.Note: Chili is not a validation library, although it ensures the type integrity.InstallationTo install the library, simply use pip:pipinstallchilior poetry:poetryaddchiliUsageThe library provides three main classes for encoding and decoding objects,chili.Encoderandchili.Decoder, andchili.Serializer, which combines both functionalities.
Functional interface is also provided throughchili.encodeandchili.decodefunctions.Additionally, library by default supports json serialization and deserialization, so you can usechili.JsonDecoder, andchili.JsonDecoder, andchili.JsonSerializeror its functional replacementchili.json_encodeandchili.json_decodeto serialize and deserialize objects to and from json.Defining encodable/decodable propertiesTo define the properties of a class that should be encoded and decoded, you need to define them with type annotations.
The@encodable,@decodable, or@serializabledecorator should also be used to mark the class as encodable/decodable or serializable.Note: Dataclasses are supported automatically, so you don't need to use the decorator.fromchiliimportencodable@encodableclassPet:name:strage:intbreed:strdef__init__(self,name:str,age:int,breed:str):self.name=nameself.age=ageself.breed=breedEncodingTo encode an object, you need to create an instance of thechili.Encoderclass, and then call theencode()method, passing the object to be encoded as an argument.Note: Thechili.Encoderclass is a generic class, and you need to pass the type of the object to be encoded as a type argument.fromchiliimportEncoderencoder=Encoder[Pet]()my_pet=Pet("Max",3,"Golden Retriever")encoded=encoder.encode(my_pet)assertencoded=={"name":"Max","age":3,"breed":"Golden Retriever"}Alternatevely you can usechili.encodefunction:fromchiliimportencodemy_pet=Pet("Max",3,"Golden Retriever")encoded=encode(my_pet,Pet)assertencoded=={"name":"Max","age":3,"breed":"Golden Retriever"}chili.encodefunction by default only encodes@encodableobjects, this behavior might be amended with theforceflag.DecodingTo decode an object, you need to create an instance of thechili.Decoderclass, and then call thedecode()method, passing the dictionary to be decoded as an argument.Note: Thechili.Decoderclass is a generic class, and you need to pass the type of the object to be decoded as a type argument.fromchiliimportDecoderdecoder=Decoder[Pet]()data={"name":"Max","age":3,"breed":"Golden Retriever"}decoded=decoder.decode(data)assertisinstance(decoded,Pet)Alternatevely you can usechili.decodefunction:fromchiliimportdecodedata={"name":"Max","age":3,"breed":"Golden Retriever"}decoded=decode(data,Pet)assertisinstance(decoded,Pet)chili.decodefunction by default only decodes@decodableobjects, this behavior might be amended with theforceflag.Missing PropertiesIf a property is not present in the dictionary when decoding, thechili.Decoderclass will not fill in the property value, unless there is a default value defined in the type annotation. Similarly, if a property is not defined on the class, thechili.Encoderclass will hide the property in the resulting dictionary.Using Default ValuesTo provide default values for class properties that are not present in the encoded dictionary, you can define the properties with an equal sign and the default value. For example:fromtypingimportListfromchiliimportDecoder,decodable@decodableclassBook:name:strauthor:strisbn:str="1234567890"tags:List[str]=[]book_data={"name":"The Hobbit","author":"J.R.R. Tolkien"}decoder=Decoder[Book]()book=decoder.decode(book_data)assertbook.tags==[]assertbook.isbn=="1234567890"Note: When using default values with mutable objects, such as lists or dictionaries, be aware that the default value is shared among all instances of the class that do not have that property defined in the encoded dictionary. However, if the default value is empty (e.g.[]for a list,{}for a dictionary), it is not shared among instances.Custom Type EncodersYou can also specify custom type encoders by defining a class that implements thechili.TypeEncoderprotocol and passing it as a dictionary to theencodersargument of the Encoder constructor.fromchiliimportEncoder,TypeEncoderclassMyCustomEncoder(TypeEncoder):defencode(self,value:MyCustomType)->str:returnvalue.encode()type_encoders={MyCustomType:MyCustomEncoder()}encoder=Encoder[Pet](encoders=type_encoders)Custom Type DecodersYou can also specify custom type decoders by defining a class that implements thechili.TypeDecoderprotocol and passing it as a dictionary to thedecodersargument of the Decoder constructor.fromchiliimportDecoder,TypeDecoderclassMyCustomDecoder(TypeDecoder):defdecode(self,value:str)->MyCustomType:returnMyCustomType.decode(value)type_decoders={MyCustomType:MyCustomDecoder()}decoder=Decoder[Pet](decoders=type_decoders)Convenient FunctionsThe library also provides convenient functions for encoding and decoding objects.Thechili.encodefunction takes an object and an optional type hint and returns a dictionary.Thechili.decodefunction takes a dictionary, a type hint, and returns an object.fromchiliimportencode,decodemy_pet=Pet("Max",3,"Golden Retriever")encoded=encode(my_pet)decoded=decode(encoded,Pet)To specify custom type encoders and decoders, you can pass them as keyword arguments to thechili.encodeandchili.decodefunctions.SerializationIf your object is both encodable and decodable, you can use the@serializabledecorator to mark it as such. You can then use thechili.Serializerclass to encode and decode objects.fromchiliimportSerializer,serializable@serializableclassPet:name:strage:intbreed:strdef__init__(self,name:str,age:int,breed:str):self.name=nameself.age=ageself.breed=breedmy_pet=Pet("Max",3,"Golden Retriever")serializer=Serializer[Pet]()encoded=serializer.encode(my_pet)decoded=serializer.decode(encoded)Note: that you should only use the@serializabledecorator for objects that are both @encodable and @decodable.JSON SerializationThe library also provides classes for encoding and decoding objects to and from JSON formats. Thechili.JsonEncoderandchili.JsonDecoderclasses provide JSON serialization.fromchiliimportJsonEncoder,JsonDecoder,JsonSerializer# JSON Serializationencoder=JsonEncoder[Pet]()decoder=JsonDecoder[Pet]()serializer=JsonSerializer[Pet]()my_pet=Pet("Max",3,"Golden Retriever")encoded=encoder.encode(my_pet)decoded=decoder.decode(encoded)Theencodedvalue will be a json string:{"name":"Max","age":3,"breed":"Golden Retriever"}Thedecodedvalue will be an instance of a Pet object.Functional interface is also available through thechili.json_encode,chili.json_decodefunctions.Private propertiesChili recognizes private attributes within a class, enabling it to serialize these attributes when a class specifies a getter for an attribute and an associated private storage (must be denoted with a_prefix).Here is an example:fromchiliimportencodable,encode@encodableclassPet:name:strdef__init__(self,name:str)->None:self._name=name@propertydefname(self)->str:returnself._namepet=Pet("Bobik")data=encode(pet)assertdata=={"name":"Bobik",}MappingMapping allows you to remap keys, apply functions to the values, and even change the structure of the input dictionary. This is particularly useful when you need to convert data from one format to another, such as when interacting with different APIs or data sources that use different naming conventions.Simple mappingHere's an example of how to use thechili.Mapperclass from the library with a Pet class:fromchiliimportMapper# Create a Mapper instance with the specified schememapper=Mapper({"pet_name":"name","pet_age":"age","pet_tags":{"tag_name":"tag","tag_type":"type",},})data={"pet_name":"Max","pet_age":3,"pet_tags":[{"tag_name":"cute","tag_type":"description"},{"tag_name":"furry","tag_type":"description"},],}# Apply the mapping to your input datamapped_data=mapper.map(data)print(mapped_data)Themapped_dataoutput would be:{"name":"Max","age":3,"pet_tags":[{"tag":"cute","type":"description"},{"tag":"furry","type":"description"},],}Using KeySchemeKeySchemecan be used to define mapping rules for nested structures more explicitly.
It allows you to specify both the old key and the nested mapping scheme in a single, concise object. This can be particularly useful when you want to map a nested structure but need to maintain clarity in your mapping scheme.Here's an example of how to usechili.KeySchemewith thechili.Mapperclass:fromchiliimportMapper,KeyScheme# Create a Mapper instance with the specified schememapper=Mapper({"pet_name":"name","pet_age":"age","pet_tags":KeyScheme("tags",{"tag_name":"tag","tag_type":"type",}),})pet_dict={"pet_name":"Max","pet_age":3,"pet_tags":[{"tag_name":"cute","tag_type":"description"},{"tag_name":"furry","tag_type":"description"},],}# Apply the mapping to your input datamapped_data=mapper.map(pet_dict)print(mapped_data)Themapped_dataoutput would be:{"name":"Max","age":3,"tags":[{"tag":"cute","type":"description"},{"tag":"furry","type":"description"},],}Using wildcards in mappingThechili.Mappersupports using...(Ellipsis) as a wildcard for keys that you want to include in the mapping but do not want to explicitly define. This can be useful when you want to map all keys in the input data, or when you want to map specific keys and leave the remaining keys unchanged.You can use a lambda function with the...wildcard to apply a transformation to the keys or values that match the wildcard.Here's an example of how to use the...wildcard with thechili.Mapperclass:fromchiliimportMapper# Create a Mapper instance with the specified scheme containing a wildcard ...mapper=Mapper({"pet_name":"name","pet_age":"age",...:lambdak,v:(f"extra_{k}",v.upper()ifisinstance(v,str)elsev),})pet_dict={"pet_name":"Max","pet_age":3,"pet_color":"white","pet_breed":"Golden Retriever","pet_tags":[{"tag":"cute","type":"description"},{"tag":"furry","type":"description"},],}# Apply the mapping to your input datamapped_data=mapper.map(pet_dict)print(mapped_data)Themapped_dataoutput would be:{"pet_name":"Fluffy","pet_age":3,"extra_color":"WHITE","extra_breed":"POODLE","extra_tags":[{"tag":"cute","type":"description",},{"tag":"furry","type":"description",},],}Using mapping in decodable/encodable objectsYou can also use mapping by settingmapperparameter in@chili.encodableand@chili.decodabledecorators.fromtypingimportListfromchiliimportencodable,Mapper,encodemapper=Mapper({"pet_name":"name","pet_age":"age","pet_tags":{"tag_name":"tag","tag_type":"type",},})@encodable(mapper=mapper)classPet:name:strage:inttags:List[str]def__init__(self,name:str,age:int,tags:List[str]):self.name=nameself.age=ageself.tags=tagspet=Pet("Max",3,["cute","furry"])encoded=encode(pet)assertencoded=={"pet_name":"Max","pet_age":3,"pet_tags":[{"tag_name":"cute","tag_type":"description"},{"tag_name":"furry","tag_type":"description"},],}Alternatively you can set mapper inEncoderandDecoderclasses:encoder=Encoder[Pet](mapper=mapper)pet=Pet("Max",3,["cute","furry"])encoded=encoder.encode(pet)Error handlingThe library raises errors if an invalid type is passed to the Encoder or Decoder, or if an invalid dictionary is passed to the Decoder.fromchiliimportEncoder,Decoderfromchili.errorimportEncoderError,DecoderError# Invalid Typeencoder=Encoder[MyInvalidType]()# Raises EncoderError.invalid_typedecoder=Decoder[MyInvalidType]()# Raises DecoderError.invalid_type# Invalid Dictionarydecoder=Decoder[Pet]()invalid_data={"name":"Max","age":"three","breed":"Golden Retriever"}decoded=decoder.decode(invalid_data)# Raises DecoderError.invalid_inputPerformanceThe table below shows the results of the benchmarks for encoding and decoding objects with the library,Pydantic, andattrs.CommandMean [ms]Min [ms]Max [ms]Relativepoetry run python benchmarks/chili_decode.py249.4 ± 4.1245.5258.81.01 ± 0.02poetry run python benchmarks/pydantic_decode.py295.5 ± 12.5287.6327.11.19 ± 0.05poetry run python benchmarks/attrs_decode.py260.9 ± 8.6253.2283.51.05 ± 0.04poetry run python benchmarks/chili_encode.py247.8 ± 2.3245.4253.01.00poetry run python benchmarks/pydantic_encode.py292.4 ± 4.7287.1302.51.18 ± 0.02poetry run python benchmarks/attrs_encode.py258.2 ± 2.1254.4261.41.04 ± 0.01Supported typesThe following section lists all the data types supported by the library and explains how they are decoded and encoded. The supported data types include built-in Python types likebool,dict,float,int,list,set,str, andtuple, as well as more complex types likecollections.namedtuple,collections.deque,collections.OrderedDict,datetime.date,datetime.datetime,datetime.time,datetime.timedelta,decimal.Decimal,enum.Enum,enum.IntEnum,pathlib.Path, and various types defined in the typing module.Simple typesSimple type are handled by a ProxyEncoder and ProxyDecoder. These types are decoded and encoded by casting the value to the specified type.For more details please refer tochili.encoder.ProxyEncoderandchili.decoder.ProxyDecoder.boolPassed value is automatically cast to a boolean with python's built-inbooltype during decoding and encoding process.intPassed value is automatically cast to an int with python's built-ininttype during decoding and encoding process.floatPassed value is automatically cast to float with python's built-infloattype during decoding and encoding process.strPassed value is automatically cast to string with python's built-instrduring encoding and decoding process.setPassed value is automatically cast to eithersetduring decoding process orlistduring encoding process.frozensetPassed value is automatically cast to eitherfrozensetduring decoding process orlistduring encoding process.listPassed value is automatically cast to list with python's built-inlistduring encoding and decoding process.tuplePassed value is automatically cast either totupleduring decoding process or tolistduring encoding process.dictPassed value is automatically cast to dict with python's built-indictduring encoding and decoding process.Complex typesComplex types are handled by corresponding Encoder and Decoder classes.collections.namedtuplePassed value is automatically cast to eithercollections.namedtupleduring decoding process orlistduring encoding process.collections.dequePassed value is automatically cast to eithercollections.dequeduring decoding process orlistduring encoding process.collections.OrderedDictPassed value is automatically cast to eithercollections.OrderedDictduring decoding process orlistwhere each item is alistof two elements corresponding tokeyandvalue, during encoding process.datetime.datePassed value is automatically cast to eitherdatetime.dateduring decoding process orstr(valid ISO-8601 date string) during encoding process.datetime.datetimePassed value must be valid ISO-8601 date time string, then it is automatically hydrated to an instance ofdatetime.datetimeclass and extracted to ISO-8601 format compatible string.datetime.timePassed value must be valid ISO-8601 time string, then it is automatically hydrated to an instance ofdatetime.timeclass and extracted to ISO-8601 format compatible string.datetime.timedeltaPassed value must be valid ISO-8601 duration string, then it is automatically hydrated to an instance ofdatetime.timedeltaclass and extracted to ISO-8601 format compatible string.decimal.DecimalPassed value must be a string containing valid decimal number representation, for more please read python's manual
aboutdecimal.Decimal, on extraction value is
extracted back to string.enum.EnumSupports hydration of all instances ofenum.Enumsubclasses as long as value can be assigned
to one of the members defined in the specifiedenum.Enumsubclass. During extraction the value is
extracted to value of the enum member.enum.IntEnumSame asenum.Enum.pathlib.PathSupported hydration for all instances ofpathlib.Pathclass, during extraction value is extracted to string.Typing module supporttyping.AnyPassed value is unchanged during hydration and extraction process.typing.AnyStrSame asstrtyping.DequeSame ascollection.dequeuewith one exception, if subtype is defined, egtyping.Deque[int]each item inside queue
is hydrated accordingly to subtype.typing.DictSame asdictwith exception that keys and values are respectively hydrated and extracted to match
annotated type.typing.FrozenSetSame asfrozensetwith exception that values of a frozen set are respectively hydrated and extracted to
match annotated type.typing.ListSame aslistwith exception that values of a list are respectively hydrated and extracted to match annotated type.typing.NamedTupleSame asnamedtuple.typing.SetSame assetwith exception that values of a set are respectively hydrated and extracted to match annotated type.typing.TupleSame astuplewith exception that values of a set are respectively hydrated and extracted to match annotated types.
Ellipsis operator (...) is also supported.typing.TypedDictSame asdictbut values of a dict are respectively hydrated and extracted to match annotated types.typing.GenericOnly parametrised generic classes are supported, dataclasses that extends other Generic classes without parametrisation will fail.typing.OptionalOptional types can carry additionalNonevalue which chili's hydration process will respect, so for example
if your type istyping.Optional[int]Nonevalue is not hydrated toint.typing.UnionLimited support for Unions.typing.PatternPassed value must be a valid regex pattern, if contains flags regex should start with/and flags should be passed after/onlyismxflags are supported.