diff --git a/docs/models/methods.md b/docs/models/methods.md index c7afb6f3f..43c64e9c2 100644 --- a/docs/models/methods.md +++ b/docs/models/methods.md @@ -305,7 +305,7 @@ Of course the end result is a string with json representation and not a dictiona ## get_pydantic -`get_pydantic(include: Union[Set, Dict] = None, exclude: Union[Set, Dict] = None)` +`get_pydantic(include: Union[Set, Dict] = None, exclude: Union[Set, Dict] = None, fk_as_int: Union[Set, Dict] = None)` This method allows you to generate `pydantic` models from your ormar models without you needing to retype all the fields. @@ -313,6 +313,15 @@ Note that if you have nested models, it **will generate whole tree of pydantic m Moreover, you can pass `exclude` and/or `include` parameters to keep only the fields that you want to, including in nested models. +If you only want an ID instead of a nested model representation for a related field, add it to the `fk_as_int` parameter. + +!!!Note + It is currently only possible to convert models nested once, meaning they must be directly related to a model that is in turn directly related to the model that `get_pydantic` got called on. + E.g. if you have a `User` model that has an FK to a `Person` model which in turn has an FK to a `Nation` model, you can have the full Person model (but in it the Nation FK instead of the full Nation model) in the resulting pydantic model like this: + ```python + User.get_pdyantic(fk_as_int={"person__nation"}) + ``` + That means that this way you can effortlessly create pydantic models for requests and responses in `fastapi`. !!!Note diff --git a/ormar/models/mixins/pydantic_mixin.py b/ormar/models/mixins/pydantic_mixin.py index 5d186bb09..adb45c0d0 100644 --- a/ormar/models/mixins/pydantic_mixin.py +++ b/ormar/models/mixins/pydantic_mixin.py @@ -2,13 +2,13 @@ import string from random import choices from typing import ( + TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, - TYPE_CHECKING, Type, Union, cast, @@ -32,7 +32,11 @@ class PydanticMixin(RelationMixin): @classmethod def get_pydantic( - cls, *, include: Union[Set, Dict] = None, exclude: Union[Set, Dict] = None + cls, + *, + include: Union[Set, Dict] = None, + exclude: Union[Set, Dict] = None, + fk_as_int: Union[Set, Dict] = None, ) -> Type[pydantic.BaseModel]: """ Returns a pydantic model out of ormar model. @@ -49,7 +53,10 @@ def get_pydantic( relation_map = translate_list_to_dict(cls._iterate_related_models()) return cls._convert_ormar_to_pydantic( - include=include, exclude=exclude, relation_map=relation_map + include=include, + exclude=exclude, + fk_as_int=fk_as_int, + relation_map=relation_map, ) @classmethod @@ -58,11 +65,14 @@ def _convert_ormar_to_pydantic( relation_map: Dict[str, Any], include: Union[Set, Dict] = None, exclude: Union[Set, Dict] = None, + fk_as_int: Union[Set, Dict] = None, ) -> Type[pydantic.BaseModel]: if include and isinstance(include, Set): include = translate_list_to_dict(include) if exclude and isinstance(exclude, Set): exclude = translate_list_to_dict(exclude) + if fk_as_int and isinstance(fk_as_int, Set): + fk_as_int = translate_list_to_dict(fk_as_int) fields_dict: Dict[str, Any] = dict() defaults: Dict[str, Any] = dict() fields_to_process = cls._get_not_excluded_fields( @@ -82,6 +92,7 @@ def _convert_ormar_to_pydantic( defaults=defaults, include=include, exclude=exclude, + fk_as_int=fk_as_int, relation_map=relation_map, ) if field is not None: @@ -103,18 +114,36 @@ def _determine_pydantic_field_type( defaults: Dict, include: Union[Set, Dict, None], exclude: Union[Set, Dict, None], + fk_as_int: Union[Set, Dict, None], relation_map: Dict[str, Any], ) -> Any: field = cls.Meta.model_fields[name] target: Any = None if field.is_relation and name in relation_map: # type: ignore - target = field.to._convert_ormar_to_pydantic( - include=cls._skip_ellipsis(include, name), - exclude=cls._skip_ellipsis(exclude, name), - relation_map=cls._skip_ellipsis( - relation_map, name, default_return=dict() - ), - ) + if fk_as_int and name in fk_as_int: + if not isinstance(fk_as_int[name], Dict): # type: ignore + target = pydantic.PositiveInt + + else: + current_level = fk_as_int[name] # type: ignore + fk_as_int.update(current_level) + fk_as_int.pop(name) # type: ignore + target = field.to._convert_ormar_to_pydantic( + include=cls._skip_ellipsis(include, name), + exclude=cls._skip_ellipsis(exclude, name), + fk_as_int=fk_as_int, + relation_map=cls._skip_ellipsis( + relation_map, name, default_return=dict() + ), + ) + else: + target = field.to._convert_ormar_to_pydantic( + include=cls._skip_ellipsis(include, name), + exclude=cls._skip_ellipsis(exclude, name), + relation_map=cls._skip_ellipsis( + relation_map, name, default_return=dict() + ), + ) if field.is_multi or field.virtual: target = List[target] # type: ignore elif not field.is_relation: diff --git a/tests/test_fastapi/test_excludes_with_get_pydantic.py b/tests/test_fastapi/test_excludes_with_get_pydantic.py index 26cd9717f..4ff319cf5 100644 --- a/tests/test_fastapi/test_excludes_with_get_pydantic.py +++ b/tests/test_fastapi/test_excludes_with_get_pydantic.py @@ -5,7 +5,7 @@ from httpx import AsyncClient from tests.settings import DATABASE_URL -from tests.test_inheritance_and_pydantic_generation.test_geting_pydantic_models import ( +from tests.test_inheritance_and_pydantic_generation.test_getting_pydantic_models import ( Category, SelfRef, database, diff --git a/tests/test_inheritance_and_pydantic_generation/test_geting_pydantic_models.py b/tests/test_inheritance_and_pydantic_generation/test_getting_pydantic_models.py similarity index 86% rename from tests/test_inheritance_and_pydantic_generation/test_geting_pydantic_models.py rename to tests/test_inheritance_and_pydantic_generation/test_getting_pydantic_models.py index 146c87f0b..efdf28a92 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_geting_pydantic_models.py +++ b/tests/test_inheritance_and_pydantic_generation/test_getting_pydantic_models.py @@ -3,10 +3,11 @@ import databases import pydantic import sqlalchemy -from pydantic import ConstrainedStr +from pydantic import ConstrainedStr, PositiveInt from pydantic.typing import ForwardRef import ormar +from ormar.fields.foreign_key import ForeignKey from tests.settings import DATABASE_URL metadata = sqlalchemy.MetaData() @@ -47,6 +48,24 @@ class Meta(BaseMeta): category: Optional[Category] = ormar.ForeignKey(Category, nullable=True) +class OrderPosition(ormar.Model): + class Meta(BaseMeta): + pass + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + item: Optional[Item] = ormar.ForeignKey(Item, skip_reverse=True) + + +class Order(ormar.Model): + class Meta(BaseMeta): + pass + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + position: Optional[OrderPosition] = ForeignKey(OrderPosition) + + class MutualA(ormar.Model): class Meta(BaseMeta): tablename = "mutual_a" @@ -167,6 +186,25 @@ def test_getting_pydantic_model_exclude_dict(): assert "name" not in PydanticCategory.__fields__ +def test_getting_pydantic_model_fk_as_int(): + PydanticItem = Item.get_pydantic( + include={"category", "name"}, fk_as_int={"category", "name"} + ) + assert len(PydanticItem.__fields__) == 2 + assert PydanticItem.__fields__["category"].type_ == PositiveInt + assert PydanticItem.__fields__["name"].type_ != PositiveInt + + +def test_getting_pydantic_model_nested_fk_as_int(): + PydanticOrder = Order.get_pydantic( + include={"name", "position"}, fk_as_int={"position__item"} + ) + assert len(PydanticOrder.__fields__) == 2 + PydanticPosition = PydanticOrder.__fields__["position"].type_ + assert len(PydanticPosition.__fields__) == 3 + assert PydanticPosition.__fields__["item"].type_ == PositiveInt + + def test_getting_pydantic_model_self_ref(): PydanticSelfRef = SelfRef.get_pydantic() assert len(PydanticSelfRef.__fields__) == 4