add get_pydantic and basic tests

This commit is contained in:
collerek
2021-05-23 16:44:24 +02:00
parent 40f1076443
commit f93ab413de
13 changed files with 266 additions and 22 deletions

View File

@ -131,7 +131,7 @@ def generate_model_example(model: Type["Model"], relation_map: Dict = None) -> D
:type model: Type["Model"]
:param relation_map: dict with relations to follow
:type relation_map: Optional[Dict]
:return:
:return: dict with example values
:rtype: Dict[str, int]
"""
example: Dict[str, Any] = dict()
@ -141,13 +141,9 @@ def generate_model_example(model: Type["Model"], relation_map: Dict = None) -> D
else translate_list_to_dict(model._iterate_related_models())
)
for name, field in model.Meta.model_fields.items():
if not field.is_relation:
is_bytes_str = field.__type__ == bytes and field.represent_as_base64_str
example[name] = field.__sample__ if not is_bytes_str else "string"
elif isinstance(relation_map, dict) and name in relation_map:
example[name] = get_nested_model_example(
name=name, field=field, relation_map=relation_map
)
populates_sample_fields_values(
example=example, name=name, field=field, relation_map=relation_map
)
to_exclude = {name for name in model.Meta.model_fields}
pydantic_repr = generate_pydantic_example(pydantic_model=model, exclude=to_exclude)
example.update(pydantic_repr)
@ -155,6 +151,30 @@ def generate_model_example(model: Type["Model"], relation_map: Dict = None) -> D
return example
def populates_sample_fields_values(
example: Dict[str, Any], name: str, field: BaseField, relation_map: Dict = None
) -> None:
"""
Iterates the field and sets fields to sample values
:param field: ormar field
:type field: BaseField
:param name: name of the field
:type name: str
:param example: example dict
:type example: Dict[str, Any]
:param relation_map: dict with relations to follow
:type relation_map: Optional[Dict]
"""
if not field.is_relation:
is_bytes_str = field.__type__ == bytes and field.represent_as_base64_str
example[name] = field.__sample__ if not is_bytes_str else "string"
elif isinstance(relation_map, dict) and name in relation_map:
example[name] = get_nested_model_example(
name=name, field=field, relation_map=relation_map
)
def get_nested_model_example(
name: str, field: "BaseField", relation_map: Dict
) -> Union[List, Dict]:

View File

@ -8,6 +8,7 @@ from ormar.models.mixins.alias_mixin import AliasMixin
from ormar.models.mixins.excludable_mixin import ExcludableMixin
from ormar.models.mixins.merge_mixin import MergeModelMixin
from ormar.models.mixins.prefetch_mixin import PrefetchQueryMixin
from ormar.models.mixins.pydantic_mixin import PydanticMixin
from ormar.models.mixins.save_mixin import SavePrepareMixin
__all__ = [
@ -16,4 +17,5 @@ __all__ = [
"PrefetchQueryMixin",
"SavePrepareMixin",
"ExcludableMixin",
"PydanticMixin",
]

View File

@ -0,0 +1,95 @@
from typing import Any, Callable, Dict, List, Set, TYPE_CHECKING, Type, Union, cast
import pydantic
from pydantic.fields import ModelField
from ormar.models.mixins.relation_mixin import RelationMixin # noqa: I100, I202
from ormar.queryset.utils import translate_list_to_dict
class PydanticMixin(RelationMixin):
if TYPE_CHECKING: # pragma: no cover
__fields__: Dict[str, ModelField]
_skip_ellipsis: Callable
_get_not_excluded_fields: Callable
@classmethod
def get_pydantic(
cls, *, include: Union[Set, Dict] = None, exclude: Union[Set, Dict] = None,
) -> Type[pydantic.BaseModel]:
"""
Returns a pydantic model out of ormar model.
Converts also nested ormar models into pydantic models.
Can be used to fully exclude certain fields in fastapi response and requests.
:param include: fields of own and nested models to include
:type include: Union[Set, Dict, None]
:param exclude: fields of own and nested models to exclude
:type exclude: Union[Set, Dict, None]
"""
relation_map = translate_list_to_dict(cls._iterate_related_models())
return cls._convert_ormar_to_pydantic(
include=include, exclude=exclude, relation_map=relation_map
)
@classmethod
def _convert_ormar_to_pydantic(
cls,
relation_map: Dict[str, Any],
include: Union[Set, Dict] = None,
exclude: 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)
fields_dict: Dict[str, Any] = dict()
defaults: Dict[str, Any] = dict()
fields_to_process = cls._get_not_excluded_fields(
fields={*cls.Meta.model_fields.keys()}, include=include, exclude=exclude
)
for name in fields_to_process:
field = cls._determine_pydantic_field_type(
name=name,
defaults=defaults,
include=include,
exclude=exclude,
relation_map=relation_map,
)
if field is not None:
fields_dict[name] = field
model = type(
cls.__name__,
(pydantic.BaseModel,),
{"__annotations__": fields_dict, **defaults},
)
return cast(Type[pydantic.BaseModel], model)
@classmethod
def _determine_pydantic_field_type(
cls,
name: str,
defaults: Dict,
include: Union[Set, Dict, None],
exclude: Union[Set, Dict, None],
relation_map: Dict[str, Any],
) -> Any:
field = cls.Meta.model_fields[name]
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, field, default_return=dict()
),
)
if field.is_multi or field.virtual:
return List[target] # type: ignore
return target
elif not field.is_relation:
defaults[name] = cls.__fields__[name].field_info
return field.__type__
return None

View File

@ -2,12 +2,17 @@ from ormar.models.mixins import (
ExcludableMixin,
MergeModelMixin,
PrefetchQueryMixin,
PydanticMixin,
SavePrepareMixin,
)
class ModelTableProxy(
PrefetchQueryMixin, MergeModelMixin, SavePrepareMixin, ExcludableMixin
PrefetchQueryMixin,
MergeModelMixin,
SavePrepareMixin,
ExcludableMixin,
PydanticMixin,
):
"""
Used to combine all mixins with different set of functionalities.

View File

@ -454,8 +454,9 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
super().update_forward_refs(**localns)
cls.Meta.requires_ref_update = False
def _get_related_not_excluded_fields(
self, include: Optional[Dict], exclude: Optional[Dict],
@staticmethod
def _get_not_excluded_fields(
fields: Union[List, Set], include: Optional[Dict], exclude: Optional[Dict],
) -> List:
"""
Returns related field names applying on them include and exclude set.
@ -467,7 +468,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
:return:
:rtype: List of fields with relations that is not excluded
"""
fields = [field for field in self.extract_related_names()]
fields = [*fields] if not isinstance(fields, list) else fields
if include:
fields = [field for field in fields if field in include]
if exclude:
@ -519,8 +520,9 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
continue
return result
@classmethod
def _skip_ellipsis(
self, items: Union[Set, Dict, None], key: str, default_return: Any = None
cls, items: Union[Set, Dict, None], key: str, default_return: Any = None
) -> Union[Set, Dict, None]:
"""
Helper to traverse the include/exclude dictionaries.
@ -534,10 +536,11 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
:return: nested value of the items
:rtype: Union[Set, Dict, None]
"""
result = self.get_child(items, key)
result = cls.get_child(items, key)
return result if result is not Ellipsis else default_return
def _convert_all(self, items: Union[Set, Dict, None]) -> Union[Set, Dict, None]:
@staticmethod
def _convert_all(items: Union[Set, Dict, None]) -> Union[Set, Dict, None]:
"""
Helper to convert __all__ pydantic special index to ormar which does not
support index based exclusions.
@ -573,8 +576,9 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
:return: current model dict with child models converted to dictionaries
:rtype: Dict
"""
fields = self._get_related_not_excluded_fields(include=include, exclude=exclude)
fields = self._get_not_excluded_fields(
fields=self.extract_related_names(), include=include, exclude=exclude
)
for field in fields:
if not relation_map or field not in relation_map:

View File

@ -26,7 +26,7 @@ quick_access_set = {
"_extract_nested_models_from_list",
"_extract_own_model_fields",
"_extract_related_model_instead_of_field",
"_get_related_not_excluded_fields",
"_get_not_excluded_fields",
"_get_value",
"_init_private_attributes",
"_is_conversion_to_json_needed",