-
-
Notifications
You must be signed in to change notification settings - Fork 387
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* document how to tell litestar to en/decode a custum type via msgspec * fixes from linting checks * Apply suggestions from review Co-authored-by: Jacob Coffee <[email protected]> * add example for pydantic custom type * present type encoders/decoders example as well as Pydantic custom class example * fix test for Python 3.8 * fix linting error --------- Co-authored-by: stewit <> Co-authored-by: Jacob Coffee <[email protected]>
- Loading branch information
1 parent
60e17cd
commit 5223d2d
Showing
10 changed files
with
214 additions
and
3 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
Empty file.
77 changes: 77 additions & 0 deletions
77
docs/examples/encoding_decoding/custom_type_encoding_decoding.py
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,77 @@ | ||
from typing import Any, Type | ||
|
||
from msgspec import Struct | ||
|
||
from litestar import Litestar, post | ||
|
||
|
||
class TenantUser: | ||
"""Custom Type that represents a user associated to a tenant | ||
Parsed from / serialized to a combined tenant + user id string of the form | ||
TENANTPREFIX_USERID | ||
i.e. separated by underscore. | ||
""" | ||
|
||
tenant_prefix: str | ||
user_id: str | ||
|
||
def __init__(self, tenant_prefix: str, user_id: str) -> None: | ||
self.tenant_prefix = tenant_prefix | ||
self.user_id = user_id | ||
|
||
@classmethod | ||
def from_string(cls, s: str) -> "TenantUser": | ||
splits = s.split("_", maxsplit=1) | ||
if len(splits) < 2: | ||
raise ValueError( | ||
"Could not split up tenant user id string. " | ||
"Expecting underscore for separation of tenant prefix and user id." | ||
) | ||
return cls(tenant_prefix=splits[0], user_id=splits[1]) | ||
|
||
def to_combined_str(self) -> str: | ||
return self.tenant_prefix + "_" + self.user_id | ||
|
||
|
||
def tenant_user_type_predicate(type: Type) -> bool: | ||
return type is TenantUser | ||
|
||
|
||
def tenant_user_enc_hook(u: TenantUser) -> Any: | ||
return u.to_combined_str() | ||
|
||
|
||
def tenant_user_dec_hook(tenant_user_id_str: str) -> TenantUser: | ||
return TenantUser.from_string(tenant_user_id_str) | ||
|
||
|
||
def general_dec_hook(type: Type, obj: Any) -> Any: | ||
if tenant_user_type_predicate(type): | ||
return tenant_user_dec_hook(obj) | ||
|
||
raise NotImplementedError(f"Encountered unknown type during decoding: {type!s}") | ||
|
||
|
||
class UserAsset(Struct): | ||
user: TenantUser | ||
name: str | ||
|
||
|
||
@post("/asset", sync_to_thread=False) | ||
def create_asset( | ||
data: UserAsset, | ||
) -> UserAsset: | ||
assert isinstance(data.user, TenantUser) | ||
return data | ||
|
||
|
||
app = Litestar( | ||
[create_asset], | ||
type_encoders={TenantUser: tenant_user_enc_hook}, # tell litestar how to encode TenantUser | ||
type_decoders=[(tenant_user_type_predicate, general_dec_hook)], # tell litestar how to decode TenantUser | ||
) | ||
|
||
# run: /asset -X POST -H "Content-Type: application/json" -d '{"name":"SomeAsset","user":"TenantA_Somebody"}' |
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,65 @@ | ||
from pydantic import BaseModel, BeforeValidator, ConfigDict, PlainSerializer, WithJsonSchema | ||
from typing_extensions import Annotated | ||
|
||
from litestar import Litestar, post | ||
|
||
|
||
class TenantUser: | ||
"""Custom Type that represents a user associated to a tenant | ||
Parsed from / serialized to a combined tenant + user id string of the form | ||
TENANTPREFIX_USERID | ||
i.e. separated by underscore. | ||
""" | ||
|
||
tenant_prefix: str | ||
user_id: str | ||
|
||
def __init__(self, tenant_prefix: str, user_id: str) -> None: | ||
self.tenant_prefix = tenant_prefix | ||
self.user_id = user_id | ||
|
||
@classmethod | ||
def from_string(cls, s: str) -> "TenantUser": | ||
splits = s.split("_", maxsplit=1) | ||
if len(splits) < 2: | ||
raise ValueError( | ||
"Could not split up tenant user id string. " | ||
"Expecting underscore for separation of tenant prefix and user id." | ||
) | ||
return cls(tenant_prefix=splits[0], user_id=splits[1]) | ||
|
||
def to_combined_str(self) -> str: | ||
return self.tenant_prefix + "_" + self.user_id | ||
|
||
|
||
PydAnnotatedTenantUser = Annotated[ | ||
TenantUser, | ||
BeforeValidator(lambda x: TenantUser.from_string(x)), | ||
PlainSerializer(lambda x: x.to_combined_str(), return_type=str), | ||
WithJsonSchema({"type": "string"}, mode="serialization"), | ||
] | ||
|
||
|
||
class UserAsset(BaseModel): | ||
model_config = ConfigDict(arbitrary_types_allowed=True) | ||
|
||
user: PydAnnotatedTenantUser | ||
name: str | ||
|
||
|
||
@post("/asset", sync_to_thread=False) | ||
def create_asset( | ||
data: UserAsset, | ||
) -> UserAsset: | ||
assert isinstance(data.user, TenantUser) | ||
return data | ||
|
||
|
||
app = Litestar( | ||
[create_asset], | ||
) | ||
|
||
# run: /asset -X POST -H "Content-Type: application/json" -d '{"name":"SomeAsset","user":"TenantA_Somebody"}' |
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,34 @@ | ||
Custom types | ||
============ | ||
|
||
Data serialization / deserialization (encoding / decoding) and validation are important parts of any API framework. | ||
|
||
In addition to being capable to encode / decode and validate many standard types, litestar supports Python's builtin dataclasses and libraries like Pydantic and msgspec. | ||
|
||
However, sometimes you may need to employ a custom type. | ||
|
||
Using type encoders / decoders | ||
------------------------------ | ||
|
||
Litestar supports a mechanism where you provide encoding and decoding hook functions which translate your type in / to a type that it knows. You can provide them via the ``type_encoders`` and ``type_decoders`` :term:`parameters <parameter>` which can be defined on every layer. For example see the :doc:`litestar app reference </reference/app>`. | ||
|
||
.. admonition:: Layered architecture | ||
|
||
``type_encoders`` and ``type_decoders`` are part of Litestar's layered architecture, which means you can set them on every layer of the application. If you set them on multiple layers, | ||
the layer closest to the route handler will take precedence. | ||
|
||
You can read more about this here: | ||
:ref:`Layered architecture <usage/applications:layered architecture>` | ||
|
||
Here is an example: | ||
|
||
.. literalinclude:: /examples/encoding_decoding/custom_type_encoding_decoding.py | ||
:caption: Tell Litestar how to encode and decode a custom type | ||
|
||
Custom Pydantic types | ||
--------------------- | ||
|
||
If you use a custom Pydantic type you can use it directly: | ||
|
||
.. literalinclude:: /examples/encoding_decoding/custom_type_pydantic.py | ||
:caption: Tell Litestar how to encode and decode a custom Pydantic type |
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 |
---|---|---|
|
@@ -26,6 +26,7 @@ Usage | |
responses | ||
security/index | ||
static-files | ||
custom-types | ||
stores | ||
templating | ||
testing | ||
|
Empty file.
17 changes: 17 additions & 0 deletions
17
tests/examples/test_encoding_decoding/test_custom_type_encoding_decoding.py
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,17 @@ | ||
from docs.examples.encoding_decoding.custom_type_encoding_decoding import app | ||
|
||
from litestar.status_codes import HTTP_201_CREATED | ||
from litestar.testing import TestClient | ||
|
||
|
||
def test_custom_type_encoding_decoding_works() -> None: | ||
with TestClient(app) as client: | ||
response = client.post( | ||
"/asset", | ||
json={ | ||
"user": "TenantA_Somebody", | ||
"name": "Some Asset", | ||
}, | ||
) | ||
|
||
assert response.status_code == HTTP_201_CREATED |
17 changes: 17 additions & 0 deletions
17
tests/examples/test_encoding_decoding/test_custom_type_pydantic.py
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,17 @@ | ||
from docs.examples.encoding_decoding.custom_type_pydantic import app | ||
|
||
from litestar.status_codes import HTTP_201_CREATED | ||
from litestar.testing import TestClient | ||
|
||
|
||
def test_custom_type_encoding_decoding_works() -> None: | ||
with TestClient(app) as client: | ||
response = client.post( | ||
"/asset", | ||
json={ | ||
"user": "TenantA_Somebody", | ||
"name": "Some Asset", | ||
}, | ||
) | ||
|
||
assert response.status_code == HTTP_201_CREATED |