async def _prefetch_related_models( self, models: Sequence["Model"], rows: List ) -> Sequence["Model"]: self.already_extracted = {self.model.get_name(): {"raw": rows}} select_dict = translate_list_to_dict(self._select_related) prefetch_dict = translate_list_to_dict(self._prefetch_related) target_model = self.model fields = self._columns exclude_fields = self._exclude_columns orders_by = self.order_dict for related in prefetch_dict.keys(): await self._extract_related_models( related=related, target_model=target_model, prefetch_dict=prefetch_dict.get(related, {}), select_dict=select_dict.get(related, {}), fields=fields, exclude_fields=exclude_fields, orders_by=orders_by.get(related, {}), ) final_models = [] for model in models: final_models.append( self._populate_nested_related( model=model, prefetch_dict=prefetch_dict, orders_by=self.order_dict ) ) return models
def _update_excluded_with_related_not_required( cls, exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None], nested: bool = False, ) -> Union[Set, Dict]: """ Used during generation of the dict(). To avoid cyclical references and max recurrence limit nested models have to exclude related models that are not mandatory. For a main model (not nested) only nullable related field names are added to exclusion, for nested models all related models are excluded. :param exclude: set/dict with fields to exclude :type exclude: Union[Set, Dict, None] :param nested: flag setting nested models (child of previous one, not main one) :type nested: bool :return: set or dict with excluded fields added. :rtype: Union[Set, Dict] """ exclude = exclude or {} related_set = cls._exclude_related_names_not_required(nested=nested) if isinstance(exclude, set): exclude.union(related_set) else: related_dict = translate_list_to_dict(related_set) exclude = update(related_dict, exclude) return exclude
def generate_model_example(model: Type["Model"], relation_map: Dict = None) -> Dict: """ Generates example to be included in schema in fastapi. :param model: ormar.Model :type model: Type["Model"] :param relation_map: dict with relations to follow :type relation_map: Optional[Dict] :return: :rtype: Dict[str, int] """ example: Dict[str, Any] = dict() relation_map = (relation_map if relation_map is not None else translate_list_to_dict(model._iterate_related_models())) for name, field in model.Meta.model_fields.items(): if not field.is_relation: example[name] = field.__sample__ elif isinstance(relation_map, dict) and name in relation_map: example[name] = get_nested_model_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) return example
def test_list_to_dict_translation(): tet_list = ["aa", "bb", "cc__aa", "cc__bb", "cc__aa__xx", "cc__aa__yy"] test = translate_list_to_dict(tet_list) assert test == { "aa": Ellipsis, "bb": Ellipsis, "cc": {"aa": {"xx": Ellipsis, "yy": Ellipsis}, "bb": Ellipsis}, }
async def _prefetch_related_models(self, models: Sequence["Model"], rows: List) -> Sequence["Model"]: """ Main method of the query. Translates select nad prefetch list into dictionaries to avoid querying the same related models multiple times. Keeps the list of already extracted models. Extracts the related models from the database and later populate all children on each of the parent models from list. :param models: list of parent models from main query :type models: List[Model] :param rows: raw response from sql query :type rows: List[sqlalchemy.engine.result.RowProxy] :return: list of models with prefetch children populated :rtype: List[Model] """ self.already_extracted = {self.model.get_name(): {"raw": rows}} select_dict = translate_list_to_dict(self._select_related) prefetch_dict = translate_list_to_dict(self._prefetch_related) target_model = self.model fields = self._columns exclude_fields = self._exclude_columns orders_by = self.order_dict for related in prefetch_dict.keys(): await self._extract_related_models( related=related, target_model=target_model, prefetch_dict=prefetch_dict.get(related, {}), select_dict=select_dict.get(related, {}), fields=fields, exclude_fields=exclude_fields, orders_by=orders_by.get(related, {}), ) final_models = [] for model in models: final_models.append( self._populate_nested_related(model=model, prefetch_dict=prefetch_dict, orders_by=self.order_dict)) return models
def dict( # type: ignore # noqa A003 self, *, include: Union[Set, Dict] = None, exclude: Union[Set, Dict] = None, by_alias: bool = False, skip_defaults: bool = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, nested: bool = False, ) -> "DictStrAny": # noqa: A003' dict_instance = super().dict( include=include, exclude=self._update_excluded_with_related_not_required( exclude, nested), by_alias=by_alias, skip_defaults=skip_defaults, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, ) if include and isinstance(include, Set): include = translate_list_to_dict(include) if exclude and isinstance(exclude, Set): exclude = translate_list_to_dict(exclude) dict_instance = self._extract_nested_models( nested=nested, dict_instance=dict_instance, include=include, # type: ignore exclude=exclude, # type: ignore ) # include model properties as fields in dict if object.__getattribute__(self, "Meta").property_fields: props = self.get_properties(include=include, exclude=exclude) if props: dict_instance.update( {prop: getattr(self, prop) for prop in props}) return dict_instance
def __init__( # noqa: CFQ002 self, model_cls: Type["Model"], fields: Optional[Union[Dict, Set]], exclude_fields: Optional[Union[Dict, Set]], prefetch_related: List, select_related: List, orders_by: List, ) -> None: self.model = model_cls self.database = self.model.Meta.database self._prefetch_related = prefetch_related self._select_related = select_related self._exclude_columns = exclude_fields self._columns = fields self.already_extracted: Dict = dict() self.models: Dict = {} self.select_dict = translate_list_to_dict(self._select_related) self.orders_by = orders_by or [] self.order_dict = translate_list_to_dict(self.orders_by, is_order=True)
def _update_excluded_with_related_not_required( cls, exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None], nested: bool = False, ) -> Union[Set, Dict]: exclude = exclude or {} related_set = cls._exclude_related_names_not_required(nested=nested) if isinstance(exclude, set): exclude.union(related_set) else: related_dict = translate_list_to_dict(related_set) exclude = update(related_dict, exclude) return exclude
def merge_two_instances( cls, one: "Model", other: "Model", relation_map: Dict = None ) -> "Model": """ Merges current (other) Model and previous one (one) and returns the current Model instance with data merged from previous one. If needed it's calling itself recurrently and merges also children models. :param relation_map: map of models relations to follow :type relation_map: Dict :param one: previous model instance :type one: Model :param other: current model instance :type other: Model :return: current Model instance with data merged from previous one. :rtype: Model """ relation_map = ( relation_map if relation_map is not None else translate_list_to_dict(one._iterate_related_models()) ) for field_name in relation_map: current_field = getattr(one, field_name) other_value = getattr(other, field_name, []) if isinstance(current_field, list): value_to_set = cls._merge_items_lists( field_name=field_name, current_field=current_field, other_value=other_value, relation_map=relation_map, ) setattr(other, field_name, value_to_set) elif ( isinstance(current_field, ormar.Model) and current_field.pk == other_value.pk ): setattr( other, field_name, cls.merge_two_instances( current_field, other_value, relation_map=one._skip_ellipsis( # type: ignore relation_map, field_name, default_return=dict() ), ), ) other.set_save_status(True) return other
def dict( # type: ignore # noqa A003 self, *, include: Union[Set, Dict] = None, exclude: Union[Set, Dict] = None, by_alias: bool = False, skip_defaults: bool = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, nested: bool = False, ) -> "DictStrAny": # noqa: A003' """ Generate a dictionary representation of the model, optionally specifying which fields to include or exclude. Nested models are also parsed to dictionaries. Additionally fields decorated with @property_field are also added. :param include: fields to include :type include: Union[Set, Dict, None] :param exclude: fields to exclude :type exclude: Union[Set, Dict, None] :param by_alias: flag to get values by alias - passed to pydantic :type by_alias: bool :param skip_defaults: flag to not set values - passed to pydantic :type skip_defaults: bool :param exclude_unset: flag to exclude not set values - passed to pydantic :type exclude_unset: bool :param exclude_defaults: flag to exclude default values - passed to pydantic :type exclude_defaults: bool :param exclude_none: flag to exclude None values - passed to pydantic :type exclude_none: bool :param nested: flag if the current model is nested :type nested: bool :return: :rtype: """ dict_instance = super().dict( include=include, exclude=self._update_excluded_with_related_not_required( exclude, nested), by_alias=by_alias, skip_defaults=skip_defaults, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, ) if include and isinstance(include, Set): include = translate_list_to_dict(include) if exclude and isinstance(exclude, Set): exclude = translate_list_to_dict(exclude) dict_instance = self._extract_nested_models( nested=nested, dict_instance=dict_instance, include=include, # type: ignore exclude=exclude, # type: ignore ) # include model properties as fields in dict if object.__getattribute__(self, "Meta").property_fields: props = self.get_properties(include=include, exclude=exclude) if props: dict_instance.update( {prop: getattr(self, prop) for prop in props}) return dict_instance
async def save_related( # noqa: CCR001 self, follow: bool = False, save_all: bool = False, relation_map: Dict = None, exclude: Union[Set, Dict] = None, update_count: int = 0, ) -> int: """ Triggers a upsert method on all related models if the instances are not already saved. By default saves only the directly related ones. If follow=True is set it saves also related models of related models. To not get stuck in an infinite loop as related models also keep a relation to parent model visited models set is kept. That way already visited models that are nested are saved, but the save do not follow them inside. So Model A -> Model B -> Model A -> Model C will save second Model A but will never follow into Model C. Nested relations of those kind need to be persisted manually. :param exclude: items to exclude during saving of relations :type exclude: Union[Set, Dict] :param relation_map: map of relations to follow :type relation_map: Dict :param save_all: flag if all models should be saved or only not saved ones :type save_all: bool :param follow: flag to trigger deep save - by default only directly related models are saved with follow=True also related models of related models are saved :type follow: bool :param update_count: internal parameter for recursive calls - number of updated instances :type update_count: int :return: number of updated/saved models :rtype: int """ relation_map = ( relation_map if relation_map is not None else translate_list_to_dict(self._iterate_related_models()) ) if exclude and isinstance(exclude, Set): exclude = translate_list_to_dict(exclude) relation_map = subtract_dict(relation_map, exclude or {}) for related in self.extract_related_names(): if relation_map and related in relation_map: value = getattr(self, related) if value: update_count = await self._update_and_follow( value=value, follow=follow, save_all=save_all, relation_map=self._skip_ellipsis( # type: ignore relation_map, related, default_return={} ), update_count=update_count, ) return update_count
async def save_related( # noqa: CCR001 self, follow: bool = False, save_all: bool = False, relation_map: Dict = None, exclude: Union[Set, Dict] = None, update_count: int = 0, previous_model: "Model" = None, relation_field: Optional["ForeignKeyField"] = None, ) -> int: """ Triggers a upsert method on all related models if the instances are not already saved. By default saves only the directly related ones. If follow=True is set it saves also related models of related models. To not get stuck in an infinite loop as related models also keep a relation to parent model visited models set is kept. That way already visited models that are nested are saved, but the save do not follow them inside. So Model A -> Model B -> Model A -> Model C will save second Model A but will never follow into Model C. Nested relations of those kind need to be persisted manually. :param relation_field: field with relation leading to this model :type relation_field: Optional[ForeignKeyField] :param previous_model: previous model from which method came :type previous_model: Model :param exclude: items to exclude during saving of relations :type exclude: Union[Set, Dict] :param relation_map: map of relations to follow :type relation_map: Dict :param save_all: flag if all models should be saved or only not saved ones :type save_all: bool :param follow: flag to trigger deep save - by default only directly related models are saved with follow=True also related models of related models are saved :type follow: bool :param update_count: internal parameter for recursive calls - number of updated instances :type update_count: int :return: number of updated/saved models :rtype: int """ relation_map = (relation_map if relation_map is not None else translate_list_to_dict(self._iterate_related_models())) if exclude and isinstance(exclude, Set): exclude = translate_list_to_dict(exclude) relation_map = subtract_dict(relation_map, exclude or {}) if relation_map: fields_to_visit = { field for field in self.extract_related_fields() if field.name in relation_map } pre_save = { field for field in fields_to_visit if not field.virtual and not field.is_multi } update_count = await self._update_relation_list( fields_list=pre_save, follow=follow, save_all=save_all, relation_map=relation_map, update_count=update_count, ) update_count = await self._upsert_model( instance=self, save_all=save_all, previous_model=previous_model, relation_field=relation_field, update_count=update_count, ) post_save = fields_to_visit - pre_save update_count = await self._update_relation_list( fields_list=post_save, follow=follow, save_all=save_all, relation_map=relation_map, update_count=update_count, ) else: update_count = await self._upsert_model( instance=self, save_all=save_all, previous_model=previous_model, relation_field=relation_field, update_count=update_count, ) return update_count
def generate_exclude_for_ids(model: Type[ormar.Model]) -> Dict: to_exclude_base = translate_list_to_dict(model._iterate_related_models()) return cast(Dict, auto_exclude_id_field(to_exclude=to_exclude_base))