Source code for tortoise.contrib.pydantic.base
from __future__ import annotations
import sys
import types
from typing import TYPE_CHECKING, Any, Union, cast, get_args, get_origin
import pydantic
from pydantic import BaseModel, ConfigDict, RootModel
if sys.version_info >= (3, 11): # pragma: nocoverage
from typing import Self
else:
from typing_extensions import Self
if TYPE_CHECKING: # pragma: nocoverage
from tortoise.models import Model
from tortoise.queryset import QuerySet, QuerySetSingle
def _get_fetch_fields(pydantic_class: type[PydanticModel], model_class: type[Model]) -> list[str]:
"""
Recursively collect fields needed to fetch
:param pydantic_class: The pydantic model class
:param model_class: The tortoise model class
:return: The list of fields to be fetched
"""
fetch_fields = []
for field_name, field_type in pydantic_class.__annotations__.items():
field_type = cast(Any, field_type)
origin = cast(Any, get_origin(field_type))
if origin is list:
args = get_args(field_type)
if args:
field_type = args[0]
elif origin is Union or origin is types.UnionType:
args = get_args(field_type)
for arg in args:
if arg is not type(None):
field_type = arg
break
if not isinstance(field_type, type):
continue
if field_name in model_class._meta.fetch_fields and issubclass(field_type, PydanticModel):
subclass = field_type
orig_model = cast(Any, subclass.model_config).get("orig_model")
subclass_fetch_fields = _get_fetch_fields(subclass, orig_model)
if subclass_fetch_fields:
fetch_fields.extend([field_name + "__" + f for f in subclass_fetch_fields])
else:
fetch_fields.append(field_name)
return fetch_fields
[docs]
class PydanticModel(BaseModel):
"""
Pydantic BaseModel for Tortoise objects.
This provides an extra method above the usual Pydantic
`model properties <https://docs.pydantic.dev/latest/usage/models/#model-properties>`__
"""
model_config = ConfigDict(from_attributes=True)
@pydantic.model_validator(mode="wrap")
@classmethod
def _tortoise_wrap(cls, values, handler):
orm_obj = values if hasattr(values, "_meta") else None
instance = handler(values)
if orm_obj is not None:
object.__setattr__(instance, "__orm_obj__", orm_obj)
return instance
[docs]
@classmethod
async def from_tortoise_orm(cls, obj: Model) -> Self:
"""
Returns a serializable pydantic model instance built from the provided model instance.
.. note::
This will prefetch all the relations automatically. It is probably what you want.
If you don't want this, or require a ``sync`` method, look to using ``.from_orm()``.
In that case you'd have to manage prefetching yourself,
or exclude relational fields from being part of the model using
:class:`tortoise.contrib.pydantic.creator.PydanticMeta`, or you would be
getting ``OperationalError`` exceptions.
This is due to how the ``asyncio`` framework forces I/O to happen in explicit ``await``
statements. Hence we can only do lazy-fetching during an awaited method.
:param obj: The Model instance you want serialized.
"""
fetch_fields = _get_fetch_fields(cls, cls.model_config["orig_model"]) # type: ignore
await obj.fetch_related(*fetch_fields)
return cls.model_validate(obj)
[docs]
@classmethod
async def from_queryset_single(cls, queryset: QuerySetSingle) -> Self:
"""
Returns a serializable pydantic model instance for a single model
from the provided queryset.
This will prefetch all the relations automatically.
:param queryset: a queryset on the model this PydanticModel is based on.
"""
fetch_fields = _get_fetch_fields(cls, cls.model_config["orig_model"]) # type: ignore
return cls.model_validate(await queryset.prefetch_related(*fetch_fields))
[docs]
@classmethod
async def from_queryset(cls, queryset: QuerySet) -> list[Self]:
"""
Returns a serializable pydantic model instance that contains a list of models,
from the provided queryset.
This will prefetch all the relations automatically.
:param queryset: a queryset on the model this PydanticModel is based on.
"""
fetch_fields = _get_fetch_fields(cls, cls.model_config["orig_model"]) # type: ignore
return [cls.model_validate(e) for e in await queryset.prefetch_related(*fetch_fields)]
[docs]
class PydanticListModel(RootModel):
"""
Pydantic BaseModel for List of Tortoise Models
This provides an extra method above the usual Pydantic
`model properties <https://docs.pydantic.dev/latest/concepts/models/#model-methods-and-properties>`__
"""
[docs]
@classmethod
async def from_queryset(cls, queryset: QuerySet) -> Self:
"""
Returns a serializable pydantic model instance that contains a list of models,
from the provided queryset.
This will prefetch all the relations automatically.
:param queryset: a queryset on the model this PydanticListModel is based on.
"""
submodel = cls.model_config["submodel"] # type: ignore
fetch_fields = _get_fetch_fields(submodel, submodel.model_config["orig_model"])
return cls.model_validate(
[submodel.model_validate(e) for e in await queryset.prefetch_related(*fetch_fields)]
)