Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fk_as_int option to get_pydantic() #893

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/models/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,14 +305,23 @@ 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.

Note that if you have nested models, it **will generate whole tree of pydantic models for you!**

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
Expand Down
49 changes: 39 additions & 10 deletions ormar/models/mixins/pydantic_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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:
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_fastapi/test_excludes_with_get_pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down