tortoise-serializer


Nametortoise-serializer JSON
Version 1.0.3 PyPI version JSON
download
home_pageNone
SummaryPydantic serialization for tortoise-orm
upload_time2024-12-27 19:27:34
maintainerNone
docs_urlNone
authorSebastien Nicolet
requires_python<4.0,>=3.11
licenseMIT
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"
}
        
Elapsed time: 0.44702s