Name | tortoise-serializer JSON |
Version |
1.0.3
JSON |
| download |
home_page | None |
Summary | Pydantic serialization for tortoise-orm |
upload_time | 2024-12-27 19:27:34 |
maintainer | None |
docs_url | None |
author | Sebastien Nicolet |
requires_python | <4.0,>=3.11 |
license | MIT |
keywords |
|
VCS |
|
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
# Tortoise Serializer
## Motivation
This project was created to address some of the limitations of `pydantic_model_creator`, including:
- The ability to use a `context` in serialization at the field level.
- Access to the actual Tortoise `Model` instance during serialization.
- Improved readability.
- Support for adding extra logic to specific serializers.
- The ability to document fields in a way that is visible in Swagger.
## Installation
```shell
pip add tortoise-serializer
```
## Core concept
A `Serializer` does not need to know which model it will serialize. For example:
```python
from tortoise_serializer import Serializer
class ItemByNameSerializer(Serializer):
id: int
name: str
products = await ItemByNameSerializer.from_queryset(Product.all())
users = await ItemByNameSerializer.from_queryset(User.all())
```
This is entirely valid.
`Serializers` are `pydantic.BaseModel` objects, which means you can directly return them from FastAPI endpoints or use any functionality provided by BaseModel.
## Usage
### Reading
```python
from tortoise_serializer import Serializer
from tortoise import Model, fields
from pydantic import Field
from fastapi.routing import APIRouter
class MyUser(Model):
id = fields.IntegerField(primary_key=True)
name = fields.CharField(max_length=100, unique=True)
class MyUserSerializer(Serializer):
id: int
name: str = Field(max_length=100, description="User unique name")
router = APIRouter(prefix="/users")
@router.get("")
async def get_users() -> list[MyUserSerializer]:
return await MyUserSerializer.from_queryset(MyUser.all(), context={"user": ...})
```
(Note: You can specify a `context` to pass additional information to serializers, but it is not mandatory.)
### Writing
```python
from fastapi import Body
from pydantic import Field
class MyUserCreationSerializer(Serializer):
name: str = Field(max_length=200)
@router.post("")
async def create_user(user_serializer: MyUserCreationSerializer = Body(...)) -> MyUserSerializer:
user = await user_serializer.create_tortoise_instance(MyUser)
# Here you can also pass a `context=` to this function.
return await MyUserSerializer.from_tortoise_orm(user)
```
> Note: It is currently not possible to handle ForeignKeys directly using serializers. You need to manage such logic in your views.
### Context
The context in serializers is immutable.
### Resolvers
Sometimes, you need to compute values or restrict access to sensitive data. This can be achieved with `resolvers` and `context`. Here's an example:
```python
from tortoise_serializer import ContextType, Serializer, require_permission_or_unset
from tortoise import Model, fields
class UserModel(Model):
id = fields.IntegerField(primary_key=True)
address = fields.CharField(max_length=1000)
def is_self(instance: UserModel, context: ContextType) -> bool:
current_user = context.get("user")
if not current_user:
return False
return current_user.id == instance.id
class UserSerializer(Serializer):
id: int
# Default is set to None, but the field will be omitted.
address: str | None = None
@classmethod
@require_permission_or_unset(is_self)
async def resolve_address(cls, instance: UserModel, context: ContextType) -> str:
return instance.address
@app.get("/users", response_model_exclude_unset=True)
async def list_users(user: UserModel = Depends(...)) -> list[UserSerializer]:
return await UserSerializer.from_queryset(UserModel.all(), context={"user": user})
```
This ensures that the `address` field is not exposed to unauthorized users.
Async resolvers are called concurrently during serializer instantiation.
## Relations
### ForeignKeys & OneToOne
To serialize relations, declare a field in the serializer as another serializer:
```python
from tortoise import Model, fields
from tortoise_serializer import Serializer
class BookShelf(Model):
id = fields.IntField(primary_key=True)
name = fields.CharField(unique=True)
class Book(Model):
id = fields.IntField(primary_key=True)
title = fields.CharField(db_index=True)
shelf = fields.ForeignKeyField(
"models.BookShelf",
on_delete=fields.SET_NULL,
null=True,
related_name="books",
)
class BookSerializer(Serializer):
id: int
title: str
class ShelfSerializer(Serializer):
id: int
name: str
books: list[BookSerializer] = []
# Prefetching related fields is optional but improves performance.
serializer = ShelfSerializer.from_queryset(
BookShelf.all().prefetch_related("books").order_by("name")
)
```
For a normal ForeignKey relationship:
```python
class ShelfSerializer(Serializer):
id: int
name: str
class BookSerializer(Serializer):
id: int
title: str
shelf: ShelfSerializer | None
```
Reverse relations are `list[Serializer]`
Limitations:
Limitations: You cannot declare a field like this:
```python
class SerializerA(Serializer):
...
class SerializerB(Serializer):
...
class MyWrongSerializer(Serializer):
my_field = SerializerA | SerializerB
```
but you can still use `None` like:
```python
class MySerializer(Serializer):
some_relation: SerializerA | None = None
```
### Many2Many
There are two ways to handle Many-to-Many relationships:
- Use an intermediate Serializer with two ForeignKeys.
- Use a resolver in the serializer.
### Computed fields
Serialization involves resolving fields in the following order:
- Resolvers (computed fields)
- ForeignKeys
- Model fields
This order allows hiding fields based on the request.
Example of a computed field:
```python
from pydantic import Field
from tortoise_serializer import Serializer, ContextType
from tortoise.queryset import QuerySet
class Book(Model):
id = fields.IntField(primary_key=True)
title = fields.CharField(db_index=True)
shelf = fields.ForeignKeyField(
"models.BookShelf",
on_delete=fields.SET_NULL,
null=True,
related_name="books",
)
class BookSerializer(Serializer):
id: int
title: str
path: str
# This description will appear in Swagger's schema.
answer_to_the_question: int = Field(description="The answer to the big question of life")
@classmethod
async def resolve_path(cls, instance: Book, context: ContextType) -> str:
if not instance.shelf:
return instance.title
if isinstance(instance.shelf, QuerySet):
await instance.fetch_related("shelf")
return f'{instance.shelf.name}/{instance.title}'
@classmethod
def resolve_answer_to_the_question(cls, instance: Book, context: ContextType) -> int:
return 42
main_shelf = await Shelf.create(title="main")
my_book = await Book.create(title="Serializers 101", shelf=main_shelf)
serializer = await BookSerializer.from_tortoise_orm(my_book)
assert serializer.path == "main/Serializers 101"
assert serializer.answer_to_the_question == 42
```
All async resolvers will be resolved in concurency in a `asyncio.gather`, non-async ones will be resolved one after the other
Raw data
{
"_id": null,
"home_page": null,
"name": "tortoise-serializer",
"maintainer": null,
"docs_url": null,
"requires_python": "<4.0,>=3.11",
"maintainer_email": null,
"keywords": null,
"author": "Sebastien Nicolet",
"author_email": "snicolet95@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/20/dc/aea410a844b22fa46cc11d4ac72e5edd4fe4b9f744ca05e7dbbefdf20880/tortoise_serializer-1.0.3.tar.gz",
"platform": null,
"description": "# Tortoise Serializer\n## Motivation\nThis project was created to address some of the limitations of `pydantic_model_creator`, including:\n- The ability to use a `context` in serialization at the field level.\n- Access to the actual Tortoise `Model` instance during serialization.\n- Improved readability.\n- Support for adding extra logic to specific serializers.\n- The ability to document fields in a way that is visible in Swagger.\n\n\n## Installation\n```shell\npip add tortoise-serializer\n```\n\n## Core concept\nA `Serializer` does not need to know which model it will serialize. For example:\n```python\nfrom tortoise_serializer import Serializer\n\n\nclass ItemByNameSerializer(Serializer):\n id: int\n name: str\n\n\nproducts = await ItemByNameSerializer.from_queryset(Product.all())\nusers = await ItemByNameSerializer.from_queryset(User.all())\n\n```\nThis is entirely valid.\n\n`Serializers` are `pydantic.BaseModel` objects, which means you can directly return them from FastAPI endpoints or use any functionality provided by BaseModel.\n\n\n## Usage\n### Reading\n```python\nfrom tortoise_serializer import Serializer\nfrom tortoise import Model, fields\nfrom pydantic import Field\nfrom fastapi.routing import APIRouter\n\n\nclass MyUser(Model):\n id = fields.IntegerField(primary_key=True)\n name = fields.CharField(max_length=100, unique=True)\n\n\nclass MyUserSerializer(Serializer):\n id: int\n name: str = Field(max_length=100, description=\"User unique name\")\n\n\nrouter = APIRouter(prefix=\"/users\")\n\n\n@router.get(\"\")\nasync def get_users() -> list[MyUserSerializer]:\n return await MyUserSerializer.from_queryset(MyUser.all(), context={\"user\": ...})\n```\n\n(Note: You can specify a `context` to pass additional information to serializers, but it is not mandatory.)\n\n### Writing\n```python\nfrom fastapi import Body\nfrom pydantic import Field\n\n\nclass MyUserCreationSerializer(Serializer):\n name: str = Field(max_length=200)\n\n\n@router.post(\"\")\nasync def create_user(user_serializer: MyUserCreationSerializer = Body(...)) -> MyUserSerializer:\n user = await user_serializer.create_tortoise_instance(MyUser)\n # Here you can also pass a `context=` to this function.\n return await MyUserSerializer.from_tortoise_orm(user)\n```\n\n> Note: It is currently not possible to handle ForeignKeys directly using serializers. You need to manage such logic in your views.\n\n\n### Context\nThe context in serializers is immutable.\n\n\n### Resolvers\nSometimes, you need to compute values or restrict access to sensitive data. This can be achieved with `resolvers` and `context`. Here's an example:\n\n```python\nfrom tortoise_serializer import ContextType, Serializer, require_permission_or_unset\nfrom tortoise import Model, fields\n\n\nclass UserModel(Model):\n id = fields.IntegerField(primary_key=True)\n address = fields.CharField(max_length=1000)\n\n\ndef is_self(instance: UserModel, context: ContextType) -> bool:\n current_user = context.get(\"user\")\n if not current_user:\n return False\n return current_user.id == instance.id\n\n\nclass UserSerializer(Serializer):\n id: int\n # Default is set to None, but the field will be omitted.\n address: str | None = None\n\n @classmethod\n @require_permission_or_unset(is_self)\n async def resolve_address(cls, instance: UserModel, context: ContextType) -> str:\n return instance.address\n\n\n@app.get(\"/users\", response_model_exclude_unset=True)\nasync def list_users(user: UserModel = Depends(...)) -> list[UserSerializer]:\n return await UserSerializer.from_queryset(UserModel.all(), context={\"user\": user})\n```\n\nThis ensures that the `address` field is not exposed to unauthorized users.\n\nAsync resolvers are called concurrently during serializer instantiation.\n\n## Relations\n### ForeignKeys & OneToOne\nTo serialize relations, declare a field in the serializer as another serializer:\n\n```python\nfrom tortoise import Model, fields\nfrom tortoise_serializer import Serializer\n\n\nclass BookShelf(Model):\n id = fields.IntField(primary_key=True)\n name = fields.CharField(unique=True)\n\n\nclass Book(Model):\n id = fields.IntField(primary_key=True)\n title = fields.CharField(db_index=True)\n shelf = fields.ForeignKeyField(\n \"models.BookShelf\",\n on_delete=fields.SET_NULL,\n null=True,\n related_name=\"books\",\n )\n\n\nclass BookSerializer(Serializer):\n id: int\n title: str\n\n\nclass ShelfSerializer(Serializer):\n id: int\n name: str\n books: list[BookSerializer] = []\n\n\n# Prefetching related fields is optional but improves performance.\nserializer = ShelfSerializer.from_queryset(\n BookShelf.all().prefetch_related(\"books\").order_by(\"name\")\n)\n```\n\nFor a normal ForeignKey relationship:\n\n```python\nclass ShelfSerializer(Serializer):\n id: int\n name: str\n\n\nclass BookSerializer(Serializer):\n id: int\n title: str\n shelf: ShelfSerializer | None\n```\n\n\nReverse relations are `list[Serializer]`\n\nLimitations:\nLimitations: You cannot declare a field like this:\n```python\nclass SerializerA(Serializer):\n ...\n\n\nclass SerializerB(Serializer):\n ...\n\n\nclass MyWrongSerializer(Serializer):\n my_field = SerializerA | SerializerB\n```\n\nbut you can still use `None` like:\n```python\nclass MySerializer(Serializer):\n some_relation: SerializerA | None = None\n```\n\n### Many2Many\nThere are two ways to handle Many-to-Many relationships:\n\n- Use an intermediate Serializer with two ForeignKeys.\n- Use a resolver in the serializer.\n\n### Computed fields\nSerialization involves resolving fields in the following order:\n\n- Resolvers (computed fields)\n- ForeignKeys\n- Model fields\nThis order allows hiding fields based on the request.\n\nExample of a computed field:\n```python\nfrom pydantic import Field\nfrom tortoise_serializer import Serializer, ContextType\nfrom tortoise.queryset import QuerySet\n\n\nclass Book(Model):\n id = fields.IntField(primary_key=True)\n title = fields.CharField(db_index=True)\n shelf = fields.ForeignKeyField(\n \"models.BookShelf\",\n on_delete=fields.SET_NULL,\n null=True,\n related_name=\"books\",\n )\n\n\nclass BookSerializer(Serializer):\n id: int\n title: str\n path: str\n # This description will appear in Swagger's schema.\n answer_to_the_question: int = Field(description=\"The answer to the big question of life\")\n\n @classmethod\n async def resolve_path(cls, instance: Book, context: ContextType) -> str:\n if not instance.shelf:\n return instance.title\n if isinstance(instance.shelf, QuerySet):\n await instance.fetch_related(\"shelf\")\n return f'{instance.shelf.name}/{instance.title}'\n\n @classmethod\n def resolve_answer_to_the_question(cls, instance: Book, context: ContextType) -> int:\n return 42\n\nmain_shelf = await Shelf.create(title=\"main\")\nmy_book = await Book.create(title=\"Serializers 101\", shelf=main_shelf)\nserializer = await BookSerializer.from_tortoise_orm(my_book)\n\nassert serializer.path == \"main/Serializers 101\"\nassert serializer.answer_to_the_question == 42\n\n```\n\nAll async resolvers will be resolved in concurency in a `asyncio.gather`, non-async ones will be resolved one after the other\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Pydantic serialization for tortoise-orm",
"version": "1.0.3",
"project_urls": null,
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "d517e3cfff87c3203b47055dda6779c1efe2fef18bdb0b19bda0119e6f1eee7b",
"md5": "96ee895e0edef6b075c4b95518b0294a",
"sha256": "1be4d2b2f0df0fb46371058d064051de5eec1d3825997a15aee58b814d98ec20"
},
"downloads": -1,
"filename": "tortoise_serializer-1.0.3-py3-none-any.whl",
"has_sig": false,
"md5_digest": "96ee895e0edef6b075c4b95518b0294a",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.11",
"size": 9497,
"upload_time": "2024-12-27T19:27:31",
"upload_time_iso_8601": "2024-12-27T19:27:31.977877Z",
"url": "https://files.pythonhosted.org/packages/d5/17/e3cfff87c3203b47055dda6779c1efe2fef18bdb0b19bda0119e6f1eee7b/tortoise_serializer-1.0.3-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "20dcaea410a844b22fa46cc11d4ac72e5edd4fe4b9f744ca05e7dbbefdf20880",
"md5": "559b26e4aa1b8bf879679f8e6e2f7490",
"sha256": "8c90e02397e0d081af3ae2f6dbe032ab0cb781fc3aaf5167258305e59d611370"
},
"downloads": -1,
"filename": "tortoise_serializer-1.0.3.tar.gz",
"has_sig": false,
"md5_digest": "559b26e4aa1b8bf879679f8e6e2f7490",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4.0,>=3.11",
"size": 10353,
"upload_time": "2024-12-27T19:27:34",
"upload_time_iso_8601": "2024-12-27T19:27:34.325971Z",
"url": "https://files.pythonhosted.org/packages/20/dc/aea410a844b22fa46cc11d4ac72e5edd4fe4b9f744ca05e7dbbefdf20880/tortoise_serializer-1.0.3.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-12-27 19:27:34",
"github": false,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"lcname": "tortoise-serializer"
}