-
-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Update black * Update mypy * Add documentation about the encoders
- Loading branch information
Showing
11 changed files
with
316 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
# Encoders | ||
|
||
Esmerald being built on top of Lilya, brings another level of flexibility, the **encoders**. | ||
|
||
Pretty much like Lilya, an Encoder is what allows a specific type of object to be understood, | ||
encoded and serialized by Esmerald without breaking the application. | ||
|
||
An example of default existing encoders in Esmerald would be the support for **Pydantic** and **MsgSpec**. | ||
|
||
!!! Warning | ||
The encoders came to Esmerald after the version **3.1.2**. If you are using a version prior | ||
to that, this won't be available. | ||
|
||
## Benefits of encoders | ||
|
||
The greatest benefit of supporting the encoders is that you don't need to rely on a specific framework | ||
to support a specific library for you to use. | ||
|
||
With Esmerald `Encoder` you can design it yourself and simply add it to Esmerald to be used making it | ||
**future proof** and extremely dynamic. | ||
|
||
## How to use it | ||
|
||
To take advantage of the Encoders **you must subclass the Encoder from Esmerald and implement three mandatory functions**. | ||
|
||
```python | ||
from esmerald.encoders import Encoder | ||
``` | ||
|
||
When subclassing the `Encoder`, the mandatory functions are: | ||
|
||
* [`is_type()`](#is_type) | ||
* [`serialize()`](#serialize) | ||
* [`encode()`](#encode) | ||
|
||
Esmerald extends the native functionality of Lilya regarding the encoders and adds some extra flavours to it. | ||
|
||
The reasoning behind it its because Esmerald internally manages signatures and data validations that are | ||
unique to Esmerald. | ||
|
||
### is_type | ||
|
||
This function might sound confusing but it is in fact something simple. This function is used to check | ||
if the object of type X is an instance or a subclass of that same type. | ||
|
||
!!! Danger | ||
Here is where it is different from Lilya. With Lilya you can use the `__type__` as well but | ||
**not in Esmerald. In Esmerald you must implement the `is_type` function. | ||
|
||
#### Example | ||
|
||
This is what currently Esmerald is doing for Pydantic and MsgSpec. | ||
|
||
```python | ||
{!> ../docs_src/encoders/is_type.py !} | ||
``` | ||
|
||
As you can see, this is how we check and verify if an object of type `BaseModel` and `Struct` are | ||
properly validated by Esmerald. | ||
|
||
### serialize | ||
|
||
This function is what tells Esmerald how to serialize the given object type into a JSON readable | ||
format. | ||
|
||
Quite simple and intuitive. | ||
|
||
#### Example | ||
|
||
```python | ||
{!> ../docs_src/encoders/serialize.py !} | ||
``` | ||
|
||
### encode | ||
|
||
Finally, this functionality is what converts a given piece of data (JSON usually) into an object | ||
of the type of the Encoder. | ||
|
||
For example, a dictionary into Pydantic models or MsgSpec Structs. | ||
|
||
#### Example | ||
|
||
```python | ||
{!> ../docs_src/encoders/encode.py !} | ||
``` | ||
|
||
### The flexibility | ||
|
||
As you can see, there are many ways of you building your encoders. Esmerald internally already brings | ||
two of them out of the box but you are free to build your own [custom encoder](#custom-encoders) and | ||
apply your own logic and validations. | ||
|
||
You have 100% the power and control over any validator you would love to have in your Esmerald application. | ||
|
||
### Custom Encoders | ||
|
||
Well, this is where it becomes interesting. What if you actually want to build an Encoder that is not | ||
currently supported by Esmerald natively, for example, the library `attrs`? | ||
|
||
It is in fact very simple as well, following the previous steps and explanations, it would look | ||
like this: | ||
|
||
```python | ||
{!> ../docs_src/encoders/custom.py !} | ||
``` | ||
|
||
Do you see any differences compared to `Pydantic` and `MsgSpec`? | ||
|
||
Well, the `is_type` does not check for an `isinstance` or `is_class_and_subclass` and the reason | ||
for that its because when using `attrs` there is not specific object of type X like we have in others, | ||
in fact, the `attrs` uses decorators for it and by default provides a `has()` function that is used | ||
to check the `attrs` object types, so we can simply use it. | ||
|
||
Every library has its own ways, object types and everything in between to check and | ||
**this is the reason why the `is_type` exists, to make sure you have the control over the way the typing is checked**. | ||
|
||
Now imagine what you can do with any other library at your choice. | ||
|
||
### Register the Encoder | ||
|
||
Well, building the encoders is good fun but it does nothing to Esmerald unless you make it aware those | ||
in fact exist and should be used. | ||
|
||
There are different ways of registering the encoders. | ||
|
||
* Via [settings](#via-settings) | ||
* Via [instance](#via-instance) | ||
|
||
Esmerald also provides a function to register anywhere in your application but **it is not recommended** | ||
to use it without understanding the ramifications, mostly if you have handlers relying on a given | ||
object type that needs the encoder to be available before assembling the routing system. | ||
|
||
```python | ||
from esmerald.encoders import register_esmerald_encoder | ||
``` | ||
|
||
#### Via Settings | ||
|
||
Like everything in Esmerald, you can use the settings for basically everything in your application. | ||
|
||
Let us use the example of the [custom encoder](#custom-encoders) `AttrsEncoder`. | ||
|
||
```python | ||
{!> ../docs_src/encoders/via_settings.py !} | ||
``` | ||
|
||
#### Via Instance | ||
|
||
Classic approach and also available in any Esmerald or ChildEsmerald instance. | ||
|
||
```python | ||
{!> ../docs_src/encoders/via_instance.py !} | ||
``` | ||
|
||
#### Adding an encoder via app instance function | ||
|
||
This is also available in any Esmerald and ChildEsmerald application. If you would like to add | ||
an encoder after instantiation you can do it but again, **it is not recommended** | ||
to use it without understanding the ramifications, mostly if you have handlers relying on a given | ||
object type that needs the encoder to be available before assembling the routing system. | ||
|
||
```python | ||
{!> ../docs_src/encoders/via_func.py !} | ||
``` | ||
|
||
### Notes | ||
|
||
Having this level of flexibility is great in any application and Esmerald makes it easy for you but | ||
it is also important to understand that this level of control also comes with risks, meaning, when | ||
you build an encoder, make sure you test all the cases possible and more importantly, you implement | ||
**all the functions** mentioned above or else your application will break. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from __future__ import annotations | ||
|
||
from typing import Any | ||
|
||
from attrs import asdict, define, field, has | ||
|
||
from esmerald.encoders import Encoder | ||
|
||
|
||
class AttrsEncoder(Encoder): | ||
|
||
def is_type(self, value: Any) -> bool: | ||
return has(value) | ||
|
||
def serialize(self, obj: Any) -> Any: | ||
return asdict(obj) | ||
|
||
def encode(self, annotation: Any, value: Any) -> Any: | ||
return annotation(**value) | ||
|
||
|
||
# The way an `attr` object is defined | ||
@define | ||
class AttrItem: | ||
name: str = field() | ||
age: int = field() | ||
email: str |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
from __future__ import annotations | ||
|
||
from typing import Any | ||
|
||
import msgspec | ||
from msgspec import Struct | ||
from pydantic import BaseModel | ||
|
||
from esmerald.encoders import Encoder | ||
from lilya._utils import is_class_and_subclass | ||
|
||
|
||
class MsgSpecEncoder(Encoder): | ||
|
||
def is_type(self, value: Any) -> bool: | ||
return isinstance(value, Struct) or is_class_and_subclass(value, Struct) | ||
|
||
def serialize(self, obj: Any) -> Any: | ||
return msgspec.json.decode(msgspec.json.encode(obj)) | ||
|
||
def encode(self, annotation: Any, value: Any) -> Any: | ||
return msgspec.json.decode(msgspec.json.encode(value), type=annotation) | ||
|
||
|
||
class PydanticEncoder(Encoder): | ||
|
||
def is_type(self, value: Any) -> bool: | ||
return isinstance(value, BaseModel) or is_class_and_subclass(value, BaseModel) | ||
|
||
def serialize(self, obj: BaseModel) -> dict[str, Any]: | ||
return obj.model_dump() | ||
|
||
def encode(self, annotation: Any, value: Any) -> Any: | ||
if isinstance(value, BaseModel): | ||
return value | ||
return annotation(**value) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from __future__ import annotations | ||
|
||
from typing import Any | ||
|
||
from msgspec import Struct | ||
from pydantic import BaseModel | ||
|
||
from esmerald.encoders import Encoder | ||
from lilya._utils import is_class_and_subclass | ||
|
||
|
||
class MsgSpecEncoder(Encoder): | ||
|
||
def is_type(self, value: Any) -> bool: | ||
return isinstance(value, Struct) or is_class_and_subclass(value, Struct) | ||
|
||
|
||
class PydanticEncoder(Encoder): | ||
|
||
def is_type(self, value: Any) -> bool: | ||
return isinstance(value, BaseModel) or is_class_and_subclass(value, BaseModel) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
from __future__ import annotations | ||
|
||
from typing import Any | ||
|
||
import msgspec | ||
from msgspec import Struct | ||
from pydantic import BaseModel | ||
|
||
from esmerald.encoders import Encoder | ||
from lilya._utils import is_class_and_subclass | ||
|
||
|
||
class MsgSpecEncoder(Encoder): | ||
|
||
def is_type(self, value: Any) -> bool: | ||
return isinstance(value, Struct) or is_class_and_subclass(value, Struct) | ||
|
||
def serialize(self, obj: Any) -> Any: | ||
return msgspec.json.decode(msgspec.json.encode(obj)) | ||
|
||
|
||
class PydanticEncoder(Encoder): | ||
|
||
def is_type(self, value: Any) -> bool: | ||
return isinstance(value, BaseModel) or is_class_and_subclass(value, BaseModel) | ||
|
||
def serialize(self, obj: BaseModel) -> dict[str, Any]: | ||
return obj.model_dump() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from myapp.encoders import AttrsEncoder | ||
|
||
from esmerald import Esmerald | ||
|
||
app = Esmerald( | ||
routes=[...], | ||
) | ||
app.register_encoder(AttrsEncoder) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from myapp.encoders import AttrsEncoder | ||
|
||
from esmerald import Esmerald | ||
|
||
app = Esmerald( | ||
routes=[...], | ||
encoders=[AttrsEncoder], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
from typing import List, Union | ||
|
||
from myapp.encoders import AttrsEncoder | ||
|
||
from esmerald import EsmeraldAPISettings | ||
from esmerald.encoders import Encoder | ||
|
||
|
||
class AppSettings(EsmeraldAPISettings): | ||
@property | ||
def encoders(self) -> Union[List[Encoder], None]: | ||
return [AttrsEncoder] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters