# 🌶️ Chili
[![PyPI version](https://badge.fury.io/py/chili.svg)](https://pypi.org/project/chili) [![codecov](https://codecov.io/gh/kodemore/chili/branch/main/graph/badge.svg?token=TCG7SRQFD5)](https://codecov.io/gh/kodemore/chili) [![CI](https://github.com/kodemore/chili/actions/workflows/main.yaml/badge.svg?branch=main)](https://github.com/kodemore/chili/actions/workflows/main.yaml) [![Release](https://github.com/kodemore/chili/actions/workflows/release.yml/badge.svg)](https://github.com/kodemore/chili/actions/workflows/release.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
Chili 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 the `typing` package; 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.
# Installation
To install the library, simply use pip:
```shell
pip install chili
```
or poetry:
```shell
poetry add chili
```
# Usage
The library provides three main classes for encoding and decoding objects, `chili.Encoder` and `chili.Decoder`, and `chili.Serializer`, which combines both functionalities.
Functional interface is also provided through `chili.encode` and `chili.decode` functions.
Additionally, library by default supports json serialization and deserialization, so you can use `chili.JsonDecoder`, and `chili.JsonDecoder`, and `chili.JsonSerializer` or its functional replacement `chili.json_encode` and `chili.json_decode` to serialize and deserialize objects to and from json.
## Defining encodable/decodable properties
To define the properties of a class that should be encoded and decoded, you need to define them with type annotations.
The `@encodable`, `@decodable`, or `@serializable` decorator 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.
```python
from chili import encodable
@encodable
class Pet:
name: str
age: int
breed: str
def __init__(self, name: str, age: int, breed: str):
self.name = name
self.age = age
self.breed = breed
```
## Encoding
To encode an object, you need to create an instance of the `chili.Encoder` class, and then call the `encode()` method, passing the object to be encoded as an argument.
> Note: The `chili.Encoder` class is a generic class, and you need to pass the type of the object to be encoded as a type argument.
```python
from chili import Encoder
encoder = Encoder[Pet]()
my_pet = Pet("Max", 3, "Golden Retriever")
encoded = encoder.encode(my_pet)
assert encoded == {"name": "Max", "age": 3, "breed": "Golden Retriever"}
```
Alternatevely you can use `chili.encode` function:
```python
from chili import encode
my_pet = Pet("Max", 3, "Golden Retriever")
encoded = encode(my_pet, Pet)
assert encoded == {"name": "Max", "age": 3, "breed": "Golden Retriever"}
```
> `chili.encode` function by default only encodes `@encodable` objects, this behavior might be amended with the `force` flag.
## Decoding
To decode an object, you need to create an instance of the `chili.Decoder` class, and then call the `decode()` method, passing the dictionary to be decoded as an argument.
> Note: The `chili.Decoder` class is a generic class, and you need to pass the type of the object to be decoded as a type argument.
```python
from chili import Decoder
decoder = Decoder[Pet]()
data = {"name": "Max", "age": 3, "breed": "Golden Retriever"}
decoded = decoder.decode(data)
assert isinstance(decoded, Pet)
```
Alternatevely you can use `chili.decode` function:
```python
from chili import decode
data = {"name": "Max", "age": 3, "breed": "Golden Retriever"}
decoded = decode(data, Pet)
assert isinstance(decoded, Pet)
```
> `chili.decode` function by default only decodes `@decodable` objects, this behavior might be amended with the `force` flag.
## Missing Properties
If a property is not present in the dictionary when decoding, the `chili.Decoder` class 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, the `chili.Encoder` class will hide the property in the resulting dictionary.
## Using Default Values
To 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:
```python
from typing import List
from chili import Decoder, decodable
@decodable
class Book:
name: str
author: str
isbn: str = "1234567890"
tags: List[str] = []
book_data = {"name": "The Hobbit", "author": "J.R.R. Tolkien"}
decoder = Decoder[Book]()
book = decoder.decode(book_data)
assert book.tags == []
assert book.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 Encoders
You can also specify custom type encoders by defining a class that implements the `chili.TypeEncoder` protocol and passing it as a dictionary to the `encoders` argument of the Encoder constructor.
```python
from chili import Encoder, TypeEncoder
class MyCustomEncoder(TypeEncoder):
def encode(self, value: MyCustomType) -> str:
return value.encode()
type_encoders = {MyCustomType: MyCustomEncoder()}
encoder = Encoder[Pet](encoders=type_encoders)
```
## Custom Type Decoders
You can also specify custom type decoders by defining a class that implements the `chili.TypeDecoder` protocol and passing it as a dictionary to the `decoders` argument of the Decoder constructor.
```python
from chili import Decoder, TypeDecoder
class MyCustomDecoder(TypeDecoder):
def decode(self, value: str) -> MyCustomType:
return MyCustomType.decode(value)
type_decoders = {MyCustomType: MyCustomDecoder()}
decoder = Decoder[Pet](decoders=type_decoders)
```
## Convenient Functions
The library also provides convenient functions for encoding and decoding objects.
The `chili.encode` function takes an object and an optional type hint and returns a dictionary.
The `chili.decode` function takes a dictionary, a type hint, and returns an object.
```python
from chili import encode, decode
my_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 the `chili.encode` and `chili.decode` functions.
## Serialization
If your object is both encodable and decodable, you can use the `@serializable` decorator to mark it as such. You can then use the `chili.Serializer` class to encode and decode objects.
```python
from chili import Serializer, serializable
@serializable
class Pet:
name: str
age: int
breed: str
def __init__(self, name: str, age: int, breed: str):
self.name = name
self.age = age
self.breed = breed
my_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 `@serializable` decorator for objects that are both @encodable and @decodable.
## JSON Serialization
The library also provides classes for encoding and decoding objects to and from JSON formats. The `chili.JsonEncoder` and `chili.JsonDecoder` classes provide JSON serialization.
```python
from chili import JsonEncoder, JsonDecoder, JsonSerializer
# JSON Serialization
encoder = JsonEncoder[Pet]()
decoder = JsonDecoder[Pet]()
serializer = JsonSerializer[Pet]()
my_pet = Pet("Max", 3, "Golden Retriever")
encoded = encoder.encode(my_pet)
decoded = decoder.decode(encoded)
```
The `encoded` value will be a json string:
```json
{"name": "Max", "age": 3, "breed": "Golden Retriever"}
```
The `decoded` value will be an instance of a Pet object.
> Functional interface is also available through the `chili.json_encode`, `chili.json_decode` functions.
## Private properties
Chili 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:
```python
from chili import encodable, encode
@encodable
class Pet:
name: str
def __init__(self, name: str) -> None:
self._name = name
@property
def name(self) -> str:
return self._name
pet = Pet("Bobik")
data = encode(pet)
assert data == {
"name": "Bobik",
}
```
## Mapping
Mapping 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 mapping
Here's an example of how to use the `chili.Mapper` class from the library with a Pet class:
```python
from chili import Mapper
# Create a Mapper instance with the specified scheme
mapper = 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 data
mapped_data = mapper.map(data)
print(mapped_data)
```
The `mapped_data` output would be:
```python
{
"name": "Max",
"age": 3,
"pet_tags": [
{"tag": "cute", "type": "description"},
{"tag": "furry", "type": "description"},
],
}
```
### Using KeyScheme
`KeyScheme` can 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 use `chili.KeyScheme` with the `chili.Mapper` class:
```python
from chili import Mapper, KeyScheme
# Create a Mapper instance with the specified scheme
mapper = 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 data
mapped_data = mapper.map(pet_dict)
print(mapped_data)
```
The `mapped_data` output would be:
```python
{
"name": "Max",
"age": 3,
"tags": [
{"tag": "cute", "type": "description"},
{"tag": "furry", "type": "description"},
],
}
```
### Using wildcards in mapping
The `chili.Mapper` supports 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 the `chili.Mapper` class:
```python
from chili import Mapper
# Create a Mapper instance with the specified scheme containing a wildcard ...
mapper = Mapper({
"pet_name": "name",
"pet_age": "age",
...: lambda k, v: (f"extra_{k}", v.upper() if isinstance(v, str) else v),
})
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 data
mapped_data = mapper.map(pet_dict)
print(mapped_data)
```
The `mapped_data` output would be:
```python
{
"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 objects
You can also use mapping by setting `mapper` parameter in `@chili.encodable` and `@chili.decodable` decorators.
```python
from typing import List
from chili import encodable, Mapper, encode
mapper = Mapper({
"pet_name": "name",
"pet_age": "age",
"pet_tags": {
"tag_name": "tag",
"tag_type": "type",
},
})
@encodable(mapper=mapper)
class Pet:
name: str
age: int
tags: List[str]
def __init__(self, name: str, age: int, tags: List[str]):
self.name = name
self.age = age
self.tags = tags
pet = Pet("Max", 3, ["cute", "furry"])
encoded = encode(pet)
assert encoded == {
"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 in `Encoder` and `Decoder` classes:
```python
encoder = Encoder[Pet](mapper=mapper)
pet = Pet("Max", 3, ["cute", "furry"])
encoded = encoder.encode(pet)
```
## Error handling
The library raises errors if an invalid type is passed to the Encoder or Decoder, or if an invalid dictionary is passed to the Decoder.
```python
from chili import Encoder, Decoder
from chili.error import EncoderError, DecoderError
# Invalid Type
encoder = Encoder[MyInvalidType]() # Raises EncoderError.invalid_type
decoder = Decoder[MyInvalidType]() # Raises DecoderError.invalid_type
# Invalid Dictionary
decoder = Decoder[Pet]()
invalid_data = {"name": "Max", "age": "three", "breed": "Golden Retriever"}
decoded = decoder.decode(invalid_data) # Raises DecoderError.invalid_input
```
## Performance
The table below shows the results of the benchmarks for encoding and decoding objects with the library, [Pydantic](https://pydantic-docs.helpmanual.io/), and [attrs](https://www.attrs.org/en/stable/).
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `poetry run python benchmarks/chili_decode.py` | 249.4 ± 4.1 | 245.5 | 258.8 | 1.01 ± 0.02 |
| `poetry run python benchmarks/pydantic_decode.py` | 295.5 ± 12.5 | 287.6 | 327.1 | 1.19 ± 0.05 |
| `poetry run python benchmarks/attrs_decode.py` | 260.9 ± 8.6 | 253.2 | 283.5 | 1.05 ± 0.04 |
| `poetry run python benchmarks/chili_encode.py` | 247.8 ± 2.3 | 245.4 | 253.0 | 1.00 |
| `poetry run python benchmarks/pydantic_encode.py` | 292.4 ± 4.7 | 287.1 | 302.5 | 1.18 ± 0.02 |
| `poetry run python benchmarks/attrs_encode.py` | 258.2 ± 2.1 | 254.4 | 261.4 | 1.04 ± 0.01 |
## Supported types
The 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 like `bool`, `dict`, `float`, `int`, `list`, `set`, `str`, and `tuple`, as well as more complex types like `collections.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 types
Simple 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 to [chili.encoder.ProxyEncoder](chili/encoder.py#L50) and [chili.decoder.ProxyDecoder](chili/decoder.py#L65).
#### `bool`
Passed value is automatically cast to a boolean with python's built-in `bool` type during decoding and encoding process.
#### `int`
Passed value is automatically cast to an int with python's built-in `int` type during decoding and encoding process.
#### `float`
Passed value is automatically cast to float with python's built-in `float` type during decoding and encoding process.
#### `str`
Passed value is automatically cast to string with python's built-in `str` during encoding and decoding process.
#### `set`
Passed value is automatically cast to either `set` during decoding process or `list` during encoding process.
#### `frozenset`
Passed value is automatically cast to either `frozenset` during decoding process or `list` during encoding process.
#### `list`
Passed value is automatically cast to list with python's built-in `list` during encoding and decoding process.
#### `tuple`
Passed value is automatically cast either to `tuple` during decoding process or to `list` during encoding process.
#### `dict`
Passed value is automatically cast to dict with python's built-in `dict` during encoding and decoding process.
### Complex types
Complex types are handled by corresponding Encoder and Decoder classes.
#### `collections.namedtuple`
Passed value is automatically cast to either `collections.namedtuple` during decoding process or `list` during encoding process.
#### `collections.deque`
Passed value is automatically cast to either `collections.deque` during decoding process or `list` during encoding process.
#### `collections.OrderedDict`
Passed value is automatically cast to either `collections.OrderedDict` during decoding process or `list` where each item is a `list` of two elements corresponding to `key` and `value`, during encoding process.
#### `datetime.date`
Passed value is automatically cast to either `datetime.date` during decoding process or `str` (valid ISO-8601 date string) during encoding process.
#### `datetime.datetime`
Passed value must be valid ISO-8601 date time string, then it is automatically hydrated to an instance of `datetime.datetime`
class and extracted to ISO-8601 format compatible string.
#### `datetime.time`
Passed value must be valid ISO-8601 time string, then it is automatically hydrated to an instance of `datetime.time`
class and extracted to ISO-8601 format compatible string.
#### `datetime.timedelta`
Passed value must be valid ISO-8601 duration string, then it is automatically hydrated to an instance of `datetime.timedelta`
class and extracted to ISO-8601 format compatible string.
#### `decimal.Decimal`
Passed value must be a string containing valid decimal number representation, for more please read python's manual
about [`decimal.Decimal`](https://docs.python.org/3/library/decimal.html#decimal.Decimal), on extraction value is
extracted back to string.
#### `enum.Enum`
Supports hydration of all instances of `enum.Enum` subclasses as long as value can be assigned
to one of the members defined in the specified `enum.Enum` subclass. During extraction the value is
extracted to value of the enum member.
#### `enum.IntEnum`
Same as `enum.Enum`.
#### `pathlib.Path`
Supported hydration for all instances of `pathlib.Path` class, during extraction value is extracted to string.
### Typing module support
#### `typing.Any`
Passed value is unchanged during hydration and extraction process.
#### `typing.AnyStr`
Same as `str`
#### `typing.Deque`
Same as `collection.dequeue` with one exception, if subtype is defined, eg `typing.Deque[int]` each item inside queue
is hydrated accordingly to subtype.
#### `typing.Dict`
Same as `dict` with exception that keys and values are respectively hydrated and extracted to match
annotated type.
#### `typing.FrozenSet`
Same as `frozenset` with exception that values of a frozen set are respectively hydrated and extracted to
match annotated type.
#### `typing.List`
Same as `list` with exception that values of a list are respectively hydrated and extracted to match annotated type.
#### `typing.NamedTuple`
Same as `namedtuple`.
#### `typing.Set`
Same as `set` with exception that values of a set are respectively hydrated and extracted to match annotated type.
#### `typing.Tuple`
Same as `tuple` with exception that values of a set are respectively hydrated and extracted to match annotated types.
Ellipsis operator (`...`) is also supported.
#### `typing.TypedDict`
Same as `dict` but values of a dict are respectively hydrated and extracted to match annotated types.
#### `typing.Generic`
Only parametrised generic classes are supported, dataclasses that extends other Generic classes without parametrisation will fail.
#### `typing.Optional`
Optional types can carry additional `None` value which chili's hydration process will respect, so for example
if your type is `typing.Optional[int]` `None` value is not hydrated to `int`.
#### `typing.Union`
Limited support for Unions.
#### `typing.Pattern`
Passed value must be a valid regex pattern, if contains flags regex should start with `/` and flags should be passed after `/` only `ismx` flags are supported.
Raw data
{
"_id": null,
"home_page": "https://github.com/kodemore/chili",
"name": "chili",
"maintainer": null,
"docs_url": null,
"requires_python": "<4.0,>=3.8",
"maintainer_email": null,
"keywords": "class, decode, deserialise, encode, object, serialise",
"author": "Dawid Kraczkowski",
"author_email": "dawid.kraczkowski@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/0f/e7/d8eb7703d81573760a062b5e30d8d0e63283517aa0bde66c985e66fddcdd/chili-2.9.0.tar.gz",
"platform": null,
"description": "# \ud83c\udf36\ufe0f Chili \n[![PyPI version](https://badge.fury.io/py/chili.svg)](https://pypi.org/project/chili) [![codecov](https://codecov.io/gh/kodemore/chili/branch/main/graph/badge.svg?token=TCG7SRQFD5)](https://codecov.io/gh/kodemore/chili) [![CI](https://github.com/kodemore/chili/actions/workflows/main.yaml/badge.svg?branch=main)](https://github.com/kodemore/chili/actions/workflows/main.yaml) [![Release](https://github.com/kodemore/chili/actions/workflows/release.yml/badge.svg)](https://github.com/kodemore/chili/actions/workflows/release.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nChili is an extensible library which provides a simple and efficient way to encode and decode complex Python objects to and from their dictionary representation.\n\nIt offers complete coverage for the `typing` package; including generics, and supports custom types, allowing you to extend the library to handle your specific needs. \n\nWith 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.\n\n> Note: Chili is not a validation library, although it ensures the type integrity. \n\n# Installation\n\nTo install the library, simply use pip:\n\n```shell\npip install chili\n```\n\nor poetry:\n\n```shell\npoetry add chili\n```\n\n# Usage\n\nThe library provides three main classes for encoding and decoding objects, `chili.Encoder` and `chili.Decoder`, and `chili.Serializer`, which combines both functionalities.\nFunctional interface is also provided through `chili.encode` and `chili.decode` functions.\n\nAdditionally, library by default supports json serialization and deserialization, so you can use `chili.JsonDecoder`, and `chili.JsonDecoder`, and `chili.JsonSerializer` or its functional replacement `chili.json_encode` and `chili.json_decode` to serialize and deserialize objects to and from json.\n\n## Defining encodable/decodable properties\nTo define the properties of a class that should be encoded and decoded, you need to define them with type annotations. \nThe `@encodable`, `@decodable`, or `@serializable` decorator should also be used to mark the class as encodable/decodable or serializable.\n\n> Note: Dataclasses are supported automatically, so you don't need to use the decorator.\n\n```python\nfrom chili import encodable\n\n@encodable\nclass Pet:\n name: str\n age: int\n breed: str\n\n def __init__(self, name: str, age: int, breed: str):\n self.name = name\n self.age = age\n self.breed = breed\n```\n\n\n## Encoding\nTo encode an object, you need to create an instance of the `chili.Encoder` class, and then call the `encode()` method, passing the object to be encoded as an argument.\n\n> Note: The `chili.Encoder` class is a generic class, and you need to pass the type of the object to be encoded as a type argument.\n\n```python\nfrom chili import Encoder\n\nencoder = Encoder[Pet]()\n\nmy_pet = Pet(\"Max\", 3, \"Golden Retriever\")\nencoded = encoder.encode(my_pet)\n\nassert encoded == {\"name\": \"Max\", \"age\": 3, \"breed\": \"Golden Retriever\"}\n```\n\nAlternatevely you can use `chili.encode` function:\n\n```python\nfrom chili import encode\n\nmy_pet = Pet(\"Max\", 3, \"Golden Retriever\")\nencoded = encode(my_pet, Pet)\n\nassert encoded == {\"name\": \"Max\", \"age\": 3, \"breed\": \"Golden Retriever\"}\n```\n\n> `chili.encode` function by default only encodes `@encodable` objects, this behavior might be amended with the `force` flag.\n\n## Decoding\nTo decode an object, you need to create an instance of the `chili.Decoder` class, and then call the `decode()` method, passing the dictionary to be decoded as an argument.\n\n\n> Note: The `chili.Decoder` class is a generic class, and you need to pass the type of the object to be decoded as a type argument.\n\n```python\nfrom chili import Decoder\n\ndecoder = Decoder[Pet]()\n\ndata = {\"name\": \"Max\", \"age\": 3, \"breed\": \"Golden Retriever\"}\ndecoded = decoder.decode(data)\n\nassert isinstance(decoded, Pet)\n```\n\nAlternatevely you can use `chili.decode` function:\n\n```python\nfrom chili import decode\n\ndata = {\"name\": \"Max\", \"age\": 3, \"breed\": \"Golden Retriever\"}\ndecoded = decode(data, Pet)\n\nassert isinstance(decoded, Pet)\n```\n\n> `chili.decode` function by default only decodes `@decodable` objects, this behavior might be amended with the `force` flag.\n\n## Missing Properties\nIf a property is not present in the dictionary when decoding, the `chili.Decoder` class 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, the `chili.Encoder` class will hide the property in the resulting dictionary.\n\n## Using Default Values\nTo 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:\n\n```python\nfrom typing import List\nfrom chili import Decoder, decodable\n\n@decodable\nclass Book:\n name: str\n author: str\n isbn: str = \"1234567890\"\n tags: List[str] = []\n\nbook_data = {\"name\": \"The Hobbit\", \"author\": \"J.R.R. Tolkien\"}\ndecoder = Decoder[Book]()\n\nbook = decoder.decode(book_data)\n\nassert book.tags == []\nassert book.isbn == \"1234567890\"\n```\n\n> 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.\n\n## Custom Type Encoders\nYou can also specify custom type encoders by defining a class that implements the `chili.TypeEncoder` protocol and passing it as a dictionary to the `encoders` argument of the Encoder constructor.\n\n```python\nfrom chili import Encoder, TypeEncoder\n\nclass MyCustomEncoder(TypeEncoder):\n def encode(self, value: MyCustomType) -> str:\n return value.encode()\n\n \ntype_encoders = {MyCustomType: MyCustomEncoder()}\nencoder = Encoder[Pet](encoders=type_encoders)\n```\n\n## Custom Type Decoders\nYou can also specify custom type decoders by defining a class that implements the `chili.TypeDecoder` protocol and passing it as a dictionary to the `decoders` argument of the Decoder constructor.\n\n```python\nfrom chili import Decoder, TypeDecoder\n\nclass MyCustomDecoder(TypeDecoder):\n def decode(self, value: str) -> MyCustomType:\n return MyCustomType.decode(value)\n\ntype_decoders = {MyCustomType: MyCustomDecoder()}\ndecoder = Decoder[Pet](decoders=type_decoders)\n```\n\n## Convenient Functions\nThe library also provides convenient functions for encoding and decoding objects. \n\nThe `chili.encode` function takes an object and an optional type hint and returns a dictionary. \n\nThe `chili.decode` function takes a dictionary, a type hint, and returns an object.\n\n```python\nfrom chili import encode, decode\n\nmy_pet = Pet(\"Max\", 3, \"Golden Retriever\")\n\nencoded = encode(my_pet)\ndecoded = decode(encoded, Pet)\n```\n\n> To specify custom type encoders and decoders, you can pass them as keyword arguments to the `chili.encode` and `chili.decode` functions.\n\n## Serialization\nIf your object is both encodable and decodable, you can use the `@serializable` decorator to mark it as such. You can then use the `chili.Serializer` class to encode and decode objects.\n\n```python\nfrom chili import Serializer, serializable\n\n@serializable\nclass Pet:\n name: str\n age: int\n breed: str\n\n def __init__(self, name: str, age: int, breed: str):\n self.name = name\n self.age = age\n self.breed = breed\n\nmy_pet = Pet(\"Max\", 3, \"Golden Retriever\")\nserializer = Serializer[Pet]()\n\nencoded = serializer.encode(my_pet)\ndecoded = serializer.decode(encoded)\n```\n\n> Note: that you should only use the `@serializable` decorator for objects that are both @encodable and @decodable.\n\n\n## JSON Serialization\nThe library also provides classes for encoding and decoding objects to and from JSON formats. The `chili.JsonEncoder` and `chili.JsonDecoder` classes provide JSON serialization.\n\n```python\nfrom chili import JsonEncoder, JsonDecoder, JsonSerializer\n\n# JSON Serialization\nencoder = JsonEncoder[Pet]()\ndecoder = JsonDecoder[Pet]()\nserializer = JsonSerializer[Pet]()\n\nmy_pet = Pet(\"Max\", 3, \"Golden Retriever\")\nencoded = encoder.encode(my_pet)\ndecoded = decoder.decode(encoded)\n```\n\nThe `encoded` value will be a json string:\n\n```json\n{\"name\": \"Max\", \"age\": 3, \"breed\": \"Golden Retriever\"}\n```\n\nThe `decoded` value will be an instance of a Pet object.\n\n> Functional interface is also available through the `chili.json_encode`, `chili.json_decode` functions.\n\n## Private properties\nChili 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).\n\nHere is an example:\n\n```python\nfrom chili import encodable, encode\n\n@encodable\nclass Pet:\n name: str\n\n def __init__(self, name: str) -> None:\n self._name = name\n\n @property\n def name(self) -> str:\n return self._name\n\npet = Pet(\"Bobik\")\ndata = encode(pet)\nassert data == {\n \"name\": \"Bobik\",\n}\n```\n\n## Mapping\n\nMapping 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.\n\n### Simple mapping\nHere's an example of how to use the `chili.Mapper` class from the library with a Pet class:\n\n```python\nfrom chili import Mapper\n\n# Create a Mapper instance with the specified scheme\nmapper = Mapper({\n \"pet_name\": \"name\",\n \"pet_age\": \"age\",\n \"pet_tags\": {\n \"tag_name\": \"tag\",\n \"tag_type\": \"type\",\n },\n})\n\ndata = {\n \"pet_name\": \"Max\",\n \"pet_age\": 3,\n \"pet_tags\": [\n {\"tag_name\": \"cute\", \"tag_type\": \"description\"},\n {\"tag_name\": \"furry\", \"tag_type\": \"description\"},\n ],\n}\n\n# Apply the mapping to your input data\nmapped_data = mapper.map(data)\n\nprint(mapped_data)\n```\n\nThe `mapped_data` output would be:\n\n```python\n{\n \"name\": \"Max\",\n \"age\": 3,\n \"pet_tags\": [\n {\"tag\": \"cute\", \"type\": \"description\"},\n {\"tag\": \"furry\", \"type\": \"description\"},\n ],\n}\n```\n\n### Using KeyScheme\n\n`KeyScheme` can be used to define mapping rules for nested structures more explicitly. \nIt 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.\n\nHere's an example of how to use `chili.KeyScheme` with the `chili.Mapper` class:\n\n```python\nfrom chili import Mapper, KeyScheme\n\n# Create a Mapper instance with the specified scheme\nmapper = Mapper({\n \"pet_name\": \"name\",\n \"pet_age\": \"age\",\n \"pet_tags\": KeyScheme(\"tags\", {\n \"tag_name\": \"tag\",\n \"tag_type\": \"type\",\n }),\n})\n\npet_dict = {\n \"pet_name\": \"Max\",\n \"pet_age\": 3,\n \"pet_tags\": [\n {\"tag_name\": \"cute\", \"tag_type\": \"description\"},\n {\"tag_name\": \"furry\", \"tag_type\": \"description\"},\n ],\n}\n\n# Apply the mapping to your input data\nmapped_data = mapper.map(pet_dict)\n\nprint(mapped_data)\n```\n\nThe `mapped_data` output would be:\n\n```python\n{\n \"name\": \"Max\",\n \"age\": 3,\n \"tags\": [\n {\"tag\": \"cute\", \"type\": \"description\"},\n {\"tag\": \"furry\", \"type\": \"description\"},\n ],\n}\n```\n\n### Using wildcards in mapping\n\nThe `chili.Mapper` supports 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.\n\nYou can use a lambda function with the `...` wildcard to apply a transformation to the keys or values that match the wildcard.\n\nHere's an example of how to use the `...` wildcard with the `chili.Mapper` class:\n\n```python\nfrom chili import Mapper\n\n# Create a Mapper instance with the specified scheme containing a wildcard ...\nmapper = Mapper({\n \"pet_name\": \"name\",\n \"pet_age\": \"age\",\n ...: lambda k, v: (f\"extra_{k}\", v.upper() if isinstance(v, str) else v),\n})\n\npet_dict = {\n \"pet_name\": \"Max\",\n \"pet_age\": 3,\n \"pet_color\": \"white\",\n \"pet_breed\": \"Golden Retriever\",\n \"pet_tags\": [\n {\"tag\": \"cute\", \"type\": \"description\"},\n {\"tag\": \"furry\", \"type\": \"description\"},\n ],\n}\n\n# Apply the mapping to your input data\nmapped_data = mapper.map(pet_dict)\n\nprint(mapped_data)\n```\n\nThe `mapped_data` output would be:\n\n```python\n{\n \"pet_name\": \"Fluffy\",\n \"pet_age\": 3,\n \"extra_color\": \"WHITE\",\n \"extra_breed\": \"POODLE\",\n \"extra_tags\": [\n {\n \"tag\": \"cute\",\n \"type\": \"description\",\n },\n {\n \"tag\": \"furry\",\n \"type\": \"description\",\n },\n ],\n}\n```\n\n### Using mapping in decodable/encodable objects\n\nYou can also use mapping by setting `mapper` parameter in `@chili.encodable` and `@chili.decodable` decorators.\n\n```python\nfrom typing import List\n\nfrom chili import encodable, Mapper, encode\n\nmapper = Mapper({\n \"pet_name\": \"name\",\n \"pet_age\": \"age\",\n \"pet_tags\": {\n \"tag_name\": \"tag\",\n \"tag_type\": \"type\",\n },\n})\n\n\n@encodable(mapper=mapper)\nclass Pet:\n name: str\n age: int\n tags: List[str]\n\n def __init__(self, name: str, age: int, tags: List[str]):\n self.name = name\n self.age = age\n self.tags = tags\n\n\npet = Pet(\"Max\", 3, [\"cute\", \"furry\"])\nencoded = encode(pet)\n\nassert encoded == {\n \"pet_name\": \"Max\",\n \"pet_age\": 3,\n \"pet_tags\": [\n {\"tag_name\": \"cute\", \"tag_type\": \"description\"},\n {\"tag_name\": \"furry\", \"tag_type\": \"description\"},\n ],\n}\n```\n\nAlternatively you can set mapper in `Encoder` and `Decoder` classes:\n\n```python\nencoder = Encoder[Pet](mapper=mapper)\n\npet = Pet(\"Max\", 3, [\"cute\", \"furry\"])\nencoded = encoder.encode(pet)\n```\n\n## Error handling\nThe library raises errors if an invalid type is passed to the Encoder or Decoder, or if an invalid dictionary is passed to the Decoder.\n\n```python\nfrom chili import Encoder, Decoder\nfrom chili.error import EncoderError, DecoderError\n\n# Invalid Type\nencoder = Encoder[MyInvalidType]() # Raises EncoderError.invalid_type\n\ndecoder = Decoder[MyInvalidType]() # Raises DecoderError.invalid_type\n\n# Invalid Dictionary\ndecoder = Decoder[Pet]()\ninvalid_data = {\"name\": \"Max\", \"age\": \"three\", \"breed\": \"Golden Retriever\"}\ndecoded = decoder.decode(invalid_data) # Raises DecoderError.invalid_input\n```\n\n## Performance\n\nThe table below shows the results of the benchmarks for encoding and decoding objects with the library, [Pydantic](https://pydantic-docs.helpmanual.io/), and [attrs](https://www.attrs.org/en/stable/).\n\n| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |\n|:---|---:|---:|---:|---:|\n| `poetry run python benchmarks/chili_decode.py` | 249.4 \u00b1 4.1 | 245.5 | 258.8 | 1.01 \u00b1 0.02 |\n| `poetry run python benchmarks/pydantic_decode.py` | 295.5 \u00b1 12.5 | 287.6 | 327.1 | 1.19 \u00b1 0.05 |\n| `poetry run python benchmarks/attrs_decode.py` | 260.9 \u00b1 8.6 | 253.2 | 283.5 | 1.05 \u00b1 0.04 |\n| `poetry run python benchmarks/chili_encode.py` | 247.8 \u00b1 2.3 | 245.4 | 253.0 | 1.00 |\n| `poetry run python benchmarks/pydantic_encode.py` | 292.4 \u00b1 4.7 | 287.1 | 302.5 | 1.18 \u00b1 0.02 |\n| `poetry run python benchmarks/attrs_encode.py` | 258.2 \u00b1 2.1 | 254.4 | 261.4 | 1.04 \u00b1 0.01 |\n\n\n## Supported types\n\nThe 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 like `bool`, `dict`, `float`, `int`, `list`, `set`, `str`, and `tuple`, as well as more complex types like `collections.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.\n\n### Simple types\n\nSimple type are handled by a ProxyEncoder and ProxyDecoder. These types are decoded and encoded by casting the value to the specified type.\n\n\n> For more details please refer to [chili.encoder.ProxyEncoder](chili/encoder.py#L50) and [chili.decoder.ProxyDecoder](chili/decoder.py#L65).\n\n#### `bool`\n\nPassed value is automatically cast to a boolean with python's built-in `bool` type during decoding and encoding process.\n\n#### `int`\n\nPassed value is automatically cast to an int with python's built-in `int` type during decoding and encoding process.\n\n#### `float`\n\nPassed value is automatically cast to float with python's built-in `float` type during decoding and encoding process.\n\n\n#### `str`\n\nPassed value is automatically cast to string with python's built-in `str` during encoding and decoding process.\n\n\n#### `set`\n\nPassed value is automatically cast to either `set` during decoding process or `list` during encoding process.\n\n\n#### `frozenset`\n\nPassed value is automatically cast to either `frozenset` during decoding process or `list` during encoding process.\n\n#### `list`\n\nPassed value is automatically cast to list with python's built-in `list` during encoding and decoding process.\n\n#### `tuple`\n\nPassed value is automatically cast either to `tuple` during decoding process or to `list` during encoding process.\n\n\n#### `dict`\n\nPassed value is automatically cast to dict with python's built-in `dict` during encoding and decoding process.\n\n### Complex types\n\nComplex types are handled by corresponding Encoder and Decoder classes. \n\n#### `collections.namedtuple`\n\nPassed value is automatically cast to either `collections.namedtuple` during decoding process or `list` during encoding process.\n\n\n#### `collections.deque`\n\nPassed value is automatically cast to either `collections.deque` during decoding process or `list` during encoding process.\n\n\n#### `collections.OrderedDict`\n\nPassed value is automatically cast to either `collections.OrderedDict` during decoding process or `list` where each item is a `list` of two elements corresponding to `key` and `value`, during encoding process.\n\n\n#### `datetime.date`\n\nPassed value is automatically cast to either `datetime.date` during decoding process or `str` (valid ISO-8601 date string) during encoding process.\n\n#### `datetime.datetime`\n\n\n\nPassed value must be valid ISO-8601 date time string, then it is automatically hydrated to an instance of `datetime.datetime` \nclass and extracted to ISO-8601 format compatible string.\n\n#### `datetime.time`\n\nPassed value must be valid ISO-8601 time string, then it is automatically hydrated to an instance of `datetime.time` \nclass and extracted to ISO-8601 format compatible string.\n\n#### `datetime.timedelta`\n\nPassed value must be valid ISO-8601 duration string, then it is automatically hydrated to an instance of `datetime.timedelta`\nclass and extracted to ISO-8601 format compatible string.\n\n#### `decimal.Decimal`\n\nPassed value must be a string containing valid decimal number representation, for more please read python's manual\nabout [`decimal.Decimal`](https://docs.python.org/3/library/decimal.html#decimal.Decimal), on extraction value is\nextracted back to string.\n\n#### `enum.Enum`\n\nSupports hydration of all instances of `enum.Enum` subclasses as long as value can be assigned\nto one of the members defined in the specified `enum.Enum` subclass. During extraction the value is\nextracted to value of the enum member.\n\n#### `enum.IntEnum`\n\nSame as `enum.Enum`.\n\n\n#### `pathlib.Path`\nSupported hydration for all instances of `pathlib.Path` class, during extraction value is extracted to string.\n\n### Typing module support\n\n#### `typing.Any`\n\nPassed value is unchanged during hydration and extraction process.\n\n#### `typing.AnyStr`\n\nSame as `str`\n\n#### `typing.Deque`\n\nSame as `collection.dequeue` with one exception, if subtype is defined, eg `typing.Deque[int]` each item inside queue\nis hydrated accordingly to subtype.\n\n#### `typing.Dict`\n\nSame as `dict` with exception that keys and values are respectively hydrated and extracted to match\nannotated type.\n\n#### `typing.FrozenSet`\n\nSame as `frozenset` with exception that values of a frozen set are respectively hydrated and extracted to\nmatch annotated type.\n\n#### `typing.List`\n\nSame as `list` with exception that values of a list are respectively hydrated and extracted to match annotated type.\n\n#### `typing.NamedTuple`\n\nSame as `namedtuple`.\n\n#### `typing.Set`\n\nSame as `set` with exception that values of a set are respectively hydrated and extracted to match annotated type.\n\n#### `typing.Tuple`\n\nSame as `tuple` with exception that values of a set are respectively hydrated and extracted to match annotated types.\nEllipsis operator (`...`) is also supported.\n\n#### `typing.TypedDict`\n\nSame as `dict` but values of a dict are respectively hydrated and extracted to match annotated types. \n\n\n#### `typing.Generic`\n\nOnly parametrised generic classes are supported, dataclasses that extends other Generic classes without parametrisation will fail.\n\n\n#### `typing.Optional`\n\nOptional types can carry additional `None` value which chili's hydration process will respect, so for example \nif your type is `typing.Optional[int]` `None` value is not hydrated to `int`.\n\n\n#### `typing.Union`\n\nLimited support for Unions.\n\n#### `typing.Pattern`\n\nPassed value must be a valid regex pattern, if contains flags regex should start with `/` and flags should be passed after `/` only `ismx` flags are supported.\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Chili is serialisation library. It can serialise/deserialise almost any object.",
"version": "2.9.0",
"project_urls": {
"Documentation": "https://github.com/kodemore/chili",
"Homepage": "https://github.com/kodemore/chili",
"Repository": "https://github.com/kodemore/chili"
},
"split_keywords": [
"class",
" decode",
" deserialise",
" encode",
" object",
" serialise"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "9c75f6aa2f57d614404695c73ed1f394b5d79c22e11ed3a2c17cc34efb1d332a",
"md5": "546db23218bf95385e2a3ec6fc20e31e",
"sha256": "140cc82d3748227d6641334796b7b62afe41987dc821deb247e77bb9b2a9b7d7"
},
"downloads": -1,
"filename": "chili-2.9.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "546db23218bf95385e2a3ec6fc20e31e",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.8",
"size": 24166,
"upload_time": "2024-05-28T05:14:16",
"upload_time_iso_8601": "2024-05-28T05:14:16.867032Z",
"url": "https://files.pythonhosted.org/packages/9c/75/f6aa2f57d614404695c73ed1f394b5d79c22e11ed3a2c17cc34efb1d332a/chili-2.9.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "0fe7d8eb7703d81573760a062b5e30d8d0e63283517aa0bde66c985e66fddcdd",
"md5": "5a7d585136d480a6f10d82b84b3d4f7a",
"sha256": "5fa7d1d1406ce0e9808fe60e9dd1d1c28e7fc93574e57380ae99cd90269951d6"
},
"downloads": -1,
"filename": "chili-2.9.0.tar.gz",
"has_sig": false,
"md5_digest": "5a7d585136d480a6f10d82b84b3d4f7a",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4.0,>=3.8",
"size": 25010,
"upload_time": "2024-05-28T05:14:18",
"upload_time_iso_8601": "2024-05-28T05:14:18.814005Z",
"url": "https://files.pythonhosted.org/packages/0f/e7/d8eb7703d81573760a062b5e30d8d0e63283517aa0bde66c985e66fddcdd/chili-2.9.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-05-28 05:14:18",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "kodemore",
"github_project": "chili",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "chili"
}