add possibility to exclude/include fields (refactor to excludableitems), fix for through model only on related side of the relation, fix for exclude of through model related models
This commit is contained in:
@ -54,7 +54,7 @@ from ormar.fields import (
|
|||||||
UUID,
|
UUID,
|
||||||
UniqueColumns,
|
UniqueColumns,
|
||||||
) # noqa: I100
|
) # noqa: I100
|
||||||
from ormar.models import Model
|
from ormar.models import ExcludableItems, Model
|
||||||
from ormar.models.metaclass import ModelMeta
|
from ormar.models.metaclass import ModelMeta
|
||||||
from ormar.queryset import OrderAction, QuerySet
|
from ormar.queryset import OrderAction, QuerySet
|
||||||
from ormar.relations import RelationType
|
from ormar.relations import RelationType
|
||||||
@ -107,4 +107,5 @@ __all__ = [
|
|||||||
"ManyToManyField",
|
"ManyToManyField",
|
||||||
"ForeignKeyField",
|
"ForeignKeyField",
|
||||||
"OrderAction",
|
"OrderAction",
|
||||||
|
"ExcludableItems",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -93,8 +93,7 @@ class BaseField(FieldInfo):
|
|||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
field_name not in ["default", "default_factory", "alias",
|
field_name not in ["default", "default_factory", "alias", "allow_mutation"]
|
||||||
"allow_mutation"]
|
|
||||||
and not field_name.startswith("__")
|
and not field_name.startswith("__")
|
||||||
and hasattr(cls, field_name)
|
and hasattr(cls, field_name)
|
||||||
and not callable(getattr(cls, field_name))
|
and not callable(getattr(cls, field_name))
|
||||||
|
|||||||
@ -17,11 +17,9 @@ if TYPE_CHECKING: # pragma no cover
|
|||||||
def Through( # noqa CFQ002
|
def Through( # noqa CFQ002
|
||||||
to: "ToType", *, name: str = None, related_name: str = None, **kwargs: Any,
|
to: "ToType", *, name: str = None, related_name: str = None, **kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
# TODO: clean docstring
|
|
||||||
"""
|
"""
|
||||||
Despite a name it's a function that returns constructed ForeignKeyField.
|
Despite a name it's a function that returns constructed ThroughField.
|
||||||
This function is actually used in model declaration (as ormar.ForeignKey(ToModel)).
|
It's a special field populated only for m2m relations.
|
||||||
|
|
||||||
Accepts number of relation setting parameters as well as all BaseField ones.
|
Accepts number of relation setting parameters as well as all BaseField ones.
|
||||||
|
|
||||||
:param to: target related ormar Model
|
:param to: target related ormar Model
|
||||||
@ -30,15 +28,13 @@ def Through( # noqa CFQ002
|
|||||||
:type name: str
|
:type name: str
|
||||||
:param related_name: name of reversed FK relation populated for you on to model
|
:param related_name: name of reversed FK relation populated for you on to model
|
||||||
:type related_name: str
|
:type related_name: str
|
||||||
:param virtual: marks if relation is virtual.
|
|
||||||
It is for reversed FK and auto generated FK on through model in Many2Many relations.
|
It is for reversed FK and auto generated FK on through model in Many2Many relations.
|
||||||
:type virtual: bool
|
|
||||||
:param kwargs: all other args to be populated by BaseField
|
:param kwargs: all other args to be populated by BaseField
|
||||||
:type kwargs: Any
|
:type kwargs: Any
|
||||||
:return: ormar ForeignKeyField with relation to selected model
|
:return: ormar ForeignKeyField with relation to selected model
|
||||||
:rtype: ForeignKeyField
|
:rtype: ForeignKeyField
|
||||||
"""
|
"""
|
||||||
|
nullable = kwargs.pop("nullable", False)
|
||||||
owner = kwargs.pop("owner", None)
|
owner = kwargs.pop("owner", None)
|
||||||
namespace = dict(
|
namespace = dict(
|
||||||
__type__=to,
|
__type__=to,
|
||||||
@ -49,7 +45,7 @@ def Through( # noqa CFQ002
|
|||||||
related_name=related_name,
|
related_name=related_name,
|
||||||
virtual=True,
|
virtual=True,
|
||||||
owner=owner,
|
owner=owner,
|
||||||
nullable=False,
|
nullable=nullable,
|
||||||
unique=False,
|
unique=False,
|
||||||
column_type=None,
|
column_type=None,
|
||||||
primary_key=False,
|
primary_key=False,
|
||||||
|
|||||||
@ -7,5 +7,6 @@ ass well as vast number of helper functions for pydantic, sqlalchemy and relatio
|
|||||||
from ormar.models.newbasemodel import NewBaseModel # noqa I100
|
from ormar.models.newbasemodel import NewBaseModel # noqa I100
|
||||||
from ormar.models.model_row import ModelRow # noqa I100
|
from ormar.models.model_row import ModelRow # noqa I100
|
||||||
from ormar.models.model import Model, T # noqa I100
|
from ormar.models.model import Model, T # noqa I100
|
||||||
|
from ormar.models.excludable import ExcludableItems # noqa I100
|
||||||
|
|
||||||
__all__ = ["T", "NewBaseModel", "Model", "ModelRow"]
|
__all__ = ["T", "NewBaseModel", "Model", "ModelRow", "ExcludableItems"]
|
||||||
|
|||||||
@ -7,19 +7,12 @@ if TYPE_CHECKING: # pragma: no cover
|
|||||||
from ormar import Model
|
from ormar import Model
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Add docstrings
|
||||||
@dataclass
|
@dataclass
|
||||||
class Excludable:
|
class Excludable:
|
||||||
include: Set = field(default_factory=set)
|
include: Set = field(default_factory=set)
|
||||||
exclude: Set = field(default_factory=set)
|
exclude: Set = field(default_factory=set)
|
||||||
|
|
||||||
@property
|
|
||||||
def include_all(self):
|
|
||||||
return ... in self.include
|
|
||||||
|
|
||||||
@property
|
|
||||||
def exclude_all(self):
|
|
||||||
return ... in self.exclude
|
|
||||||
|
|
||||||
def get_copy(self) -> "Excludable":
|
def get_copy(self) -> "Excludable":
|
||||||
_copy = self.__class__()
|
_copy = self.__class__()
|
||||||
_copy.include = {x for x in self.include}
|
_copy.include = {x for x in self.include}
|
||||||
@ -28,9 +21,6 @@ class Excludable:
|
|||||||
|
|
||||||
def set_values(self, value: Set, is_exclude: bool) -> None:
|
def set_values(self, value: Set, is_exclude: bool) -> None:
|
||||||
prop = "exclude" if is_exclude else "include"
|
prop = "exclude" if is_exclude else "include"
|
||||||
if ... in getattr(self, prop) or ... in value:
|
|
||||||
setattr(self, prop, {...})
|
|
||||||
else:
|
|
||||||
current_value = getattr(self, prop)
|
current_value = getattr(self, prop)
|
||||||
current_value.update(value)
|
current_value.update(value)
|
||||||
setattr(self, prop, current_value)
|
setattr(self, prop, current_value)
|
||||||
@ -61,7 +51,11 @@ class ExcludableItems:
|
|||||||
|
|
||||||
def get(self, model_cls: Type["Model"], alias: str = "") -> Excludable:
|
def get(self, model_cls: Type["Model"], alias: str = "") -> Excludable:
|
||||||
key = f"{alias + '_' if alias else ''}{model_cls.get_name(lower=True)}"
|
key = f"{alias + '_' if alias else ''}{model_cls.get_name(lower=True)}"
|
||||||
return self.items.get(key, Excludable())
|
excludable = self.items.get(key)
|
||||||
|
if not excludable:
|
||||||
|
excludable = Excludable()
|
||||||
|
self.items[key] = excludable
|
||||||
|
return excludable
|
||||||
|
|
||||||
def build(
|
def build(
|
||||||
self,
|
self,
|
||||||
@ -122,14 +116,13 @@ class ExcludableItems:
|
|||||||
if value is ...:
|
if value is ...:
|
||||||
self_fields.add(key)
|
self_fields.add(key)
|
||||||
elif isinstance(value, set):
|
elif isinstance(value, set):
|
||||||
related_items.append(key)
|
|
||||||
(
|
(
|
||||||
table_prefix,
|
table_prefix,
|
||||||
target_model,
|
target_model,
|
||||||
_,
|
_,
|
||||||
_,
|
_,
|
||||||
) = get_relationship_alias_model_and_str(
|
) = get_relationship_alias_model_and_str(
|
||||||
source_model=source_model, related_parts=related_items
|
source_model=source_model, related_parts=related_items + [key]
|
||||||
)
|
)
|
||||||
self._set_excludes(
|
self._set_excludes(
|
||||||
items=value,
|
items=value,
|
||||||
|
|||||||
@ -4,12 +4,12 @@ from typing import (
|
|||||||
Dict,
|
Dict,
|
||||||
List,
|
List,
|
||||||
Mapping,
|
Mapping,
|
||||||
Optional,
|
|
||||||
Set,
|
Set,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Type,
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union, cast,
|
Union,
|
||||||
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ormar.models.excludable import ExcludableItems
|
from ormar.models.excludable import ExcludableItems
|
||||||
@ -52,84 +52,6 @@ class ExcludableMixin(RelationMixin):
|
|||||||
return items.get(key, {})
|
return items.get(key, {})
|
||||||
return items
|
return items
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_excluded(
|
|
||||||
exclude: Union[Set, Dict, None], key: str = None
|
|
||||||
) -> Union[Set, Dict, None]:
|
|
||||||
"""
|
|
||||||
Proxy to ExcludableMixin.get_child for exclusions.
|
|
||||||
|
|
||||||
:param exclude: bag of items to exclude
|
|
||||||
:type exclude: Union[Set, Dict, None]
|
|
||||||
:param key: name of the child to extract
|
|
||||||
:type key: str
|
|
||||||
:return: child extracted from items if exists
|
|
||||||
:rtype: Union[Set, Dict, None]
|
|
||||||
"""
|
|
||||||
return ExcludableMixin.get_child(items=exclude, key=key)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_included(
|
|
||||||
include: Union[Set, Dict, None], key: str = None
|
|
||||||
) -> Union[Set, Dict, None]:
|
|
||||||
"""
|
|
||||||
Proxy to ExcludableMixin.get_child for inclusions.
|
|
||||||
|
|
||||||
:param include: bag of items to include
|
|
||||||
:type include: Union[Set, Dict, None]
|
|
||||||
:param key: name of the child to extract
|
|
||||||
:type key: str
|
|
||||||
:return: child extracted from items if exists
|
|
||||||
:rtype: Union[Set, Dict, None]
|
|
||||||
"""
|
|
||||||
return ExcludableMixin.get_child(items=include, key=key)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_excluded(exclude: Union[Set, Dict, None], key: str = None) -> bool:
|
|
||||||
"""
|
|
||||||
Checks if given key should be excluded on model/ dict.
|
|
||||||
|
|
||||||
:param exclude: bag of items to exclude
|
|
||||||
:type exclude: Union[Set, Dict, None]
|
|
||||||
:param key: name of the child to extract
|
|
||||||
:type key: str
|
|
||||||
:return: child extracted from items if exists
|
|
||||||
:rtype: Union[Set, Dict, None]
|
|
||||||
"""
|
|
||||||
if exclude is None:
|
|
||||||
return False
|
|
||||||
if exclude is Ellipsis: # pragma: nocover
|
|
||||||
return True
|
|
||||||
to_exclude = ExcludableMixin.get_excluded(exclude=exclude, key=key)
|
|
||||||
if isinstance(to_exclude, Set):
|
|
||||||
return key in to_exclude
|
|
||||||
if to_exclude is ...:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_included(include: Union[Set, Dict, None], key: str = None) -> bool:
|
|
||||||
"""
|
|
||||||
Checks if given key should be included on model/ dict.
|
|
||||||
|
|
||||||
:param include: bag of items to include
|
|
||||||
:type include: Union[Set, Dict, None]
|
|
||||||
:param key: name of the child to extract
|
|
||||||
:type key: str
|
|
||||||
:return: child extracted from items if exists
|
|
||||||
:rtype: Union[Set, Dict, None]
|
|
||||||
"""
|
|
||||||
if include is None:
|
|
||||||
return True
|
|
||||||
if include is Ellipsis:
|
|
||||||
return True
|
|
||||||
to_include = ExcludableMixin.get_included(include=include, key=key)
|
|
||||||
if isinstance(to_include, Set):
|
|
||||||
return key in to_include
|
|
||||||
if to_include is ...:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _populate_pk_column(
|
def _populate_pk_column(
|
||||||
model: Union[Type["Model"], Type["ModelRow"]],
|
model: Union[Type["Model"], Type["ModelRow"]],
|
||||||
@ -163,10 +85,9 @@ class ExcludableMixin(RelationMixin):
|
|||||||
cls,
|
cls,
|
||||||
model: Union[Type["Model"], Type["ModelRow"]],
|
model: Union[Type["Model"], Type["ModelRow"]],
|
||||||
excludable: ExcludableItems,
|
excludable: ExcludableItems,
|
||||||
alias: str = '',
|
alias: str = "",
|
||||||
use_alias: bool = False,
|
use_alias: bool = False,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
# TODO update docstring
|
|
||||||
"""
|
"""
|
||||||
Returns list of aliases or field names for given model.
|
Returns list of aliases or field names for given model.
|
||||||
Aliases/names switch is use_alias flag.
|
Aliases/names switch is use_alias flag.
|
||||||
@ -176,6 +97,10 @@ class ExcludableMixin(RelationMixin):
|
|||||||
|
|
||||||
Primary key field is always added and cannot be excluded (will be added anyway).
|
Primary key field is always added and cannot be excluded (will be added anyway).
|
||||||
|
|
||||||
|
:param alias: relation prefix
|
||||||
|
:type alias: str
|
||||||
|
:param excludable: structure of fields to include and exclude
|
||||||
|
:type excludable: ExcludableItems
|
||||||
:param model: model on columns are selected
|
:param model: model on columns are selected
|
||||||
:type model: Type["Model"]
|
:type model: Type["Model"]
|
||||||
:param use_alias: flag if aliases or field names should be used
|
:param use_alias: flag if aliases or field names should be used
|
||||||
@ -183,7 +108,7 @@ class ExcludableMixin(RelationMixin):
|
|||||||
:return: list of column field names or aliases
|
:return: list of column field names or aliases
|
||||||
:rtype: List[str]
|
:rtype: List[str]
|
||||||
"""
|
"""
|
||||||
model_excludable = excludable.get(model_cls=model, alias=alias)
|
model_excludable = excludable.get(model_cls=model, alias=alias) # type: ignore
|
||||||
columns = [
|
columns = [
|
||||||
model.get_column_name_from_alias(col.name) if not use_alias else col.name
|
model.get_column_name_from_alias(col.name) if not use_alias else col.name
|
||||||
for col in model.Meta.table.columns
|
for col in model.Meta.table.columns
|
||||||
@ -243,11 +168,7 @@ class ExcludableMixin(RelationMixin):
|
|||||||
return exclude
|
return exclude
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_names_to_exclude(
|
def get_names_to_exclude(cls, excludable: ExcludableItems, alias: str) -> Set:
|
||||||
cls,
|
|
||||||
excludable: ExcludableItems,
|
|
||||||
alias: str
|
|
||||||
) -> Set:
|
|
||||||
"""
|
"""
|
||||||
Returns a set of models field names that should be explicitly excluded
|
Returns a set of models field names that should be explicitly excluded
|
||||||
during model initialization.
|
during model initialization.
|
||||||
@ -268,7 +189,7 @@ class ExcludableMixin(RelationMixin):
|
|||||||
model = cast(Type["Model"], cls)
|
model = cast(Type["Model"], cls)
|
||||||
model_excludable = excludable.get(model_cls=model, alias=alias)
|
model_excludable = excludable.get(model_cls=model, alias=alias)
|
||||||
fields_names = cls.extract_db_own_fields()
|
fields_names = cls.extract_db_own_fields()
|
||||||
if model_excludable.include and model_excludable.include_all:
|
if model_excludable.include:
|
||||||
fields_to_keep = model_excludable.include.intersection(fields_names)
|
fields_to_keep = model_excludable.include.intersection(fields_names)
|
||||||
else:
|
else:
|
||||||
fields_to_keep = fields_names
|
fields_to_keep = fields_names
|
||||||
|
|||||||
@ -3,11 +3,9 @@ from typing import (
|
|||||||
Dict,
|
Dict,
|
||||||
List,
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
Set,
|
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Type,
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,7 +15,6 @@ from ormar.models import NewBaseModel # noqa: I202
|
|||||||
from ormar.models.excludable import ExcludableItems
|
from ormar.models.excludable import ExcludableItems
|
||||||
from ormar.models.helpers.models import group_related_list
|
from ormar.models.helpers.models import group_related_list
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from ormar.fields import ForeignKeyField
|
from ormar.fields import ForeignKeyField
|
||||||
from ormar.models import T
|
from ormar.models import T
|
||||||
@ -36,6 +33,7 @@ class ModelRow(NewBaseModel):
|
|||||||
related_field: Type["ForeignKeyField"] = None,
|
related_field: Type["ForeignKeyField"] = None,
|
||||||
excludable: ExcludableItems = None,
|
excludable: ExcludableItems = None,
|
||||||
current_relation_str: str = "",
|
current_relation_str: str = "",
|
||||||
|
proxy_source_model: Optional[Type["ModelRow"]] = None,
|
||||||
) -> Optional[T]:
|
) -> Optional[T]:
|
||||||
"""
|
"""
|
||||||
Model method to convert raw sql row from database into ormar.Model instance.
|
Model method to convert raw sql row from database into ormar.Model instance.
|
||||||
@ -91,12 +89,10 @@ class ModelRow(NewBaseModel):
|
|||||||
excludable=excludable,
|
excludable=excludable,
|
||||||
current_relation_str=current_relation_str,
|
current_relation_str=current_relation_str,
|
||||||
source_model=source_model,
|
source_model=source_model,
|
||||||
|
proxy_source_model=proxy_source_model, # type: ignore
|
||||||
)
|
)
|
||||||
item = cls.extract_prefixed_table_columns(
|
item = cls.extract_prefixed_table_columns(
|
||||||
item=item,
|
item=item, row=row, table_prefix=table_prefix, excludable=excludable
|
||||||
row=row,
|
|
||||||
table_prefix=table_prefix,
|
|
||||||
excludable=excludable
|
|
||||||
)
|
)
|
||||||
|
|
||||||
instance: Optional[T] = None
|
instance: Optional[T] = None
|
||||||
@ -117,6 +113,7 @@ class ModelRow(NewBaseModel):
|
|||||||
related_models: Any,
|
related_models: Any,
|
||||||
excludable: ExcludableItems,
|
excludable: ExcludableItems,
|
||||||
current_relation_str: str = None,
|
current_relation_str: str = None,
|
||||||
|
proxy_source_model: Type[T] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Traverses structure of related models and populates the nested models
|
Traverses structure of related models and populates the nested models
|
||||||
@ -165,20 +162,22 @@ class ModelRow(NewBaseModel):
|
|||||||
excludable=excludable,
|
excludable=excludable,
|
||||||
current_relation_str=relation_str,
|
current_relation_str=relation_str,
|
||||||
source_model=source_model,
|
source_model=source_model,
|
||||||
|
proxy_source_model=proxy_source_model,
|
||||||
)
|
)
|
||||||
item[model_cls.get_column_name_from_alias(related)] = child
|
item[model_cls.get_column_name_from_alias(related)] = child
|
||||||
if field.is_multi and child:
|
if field.is_multi and child:
|
||||||
# TODO: way to figure out which side should be populated?
|
|
||||||
through_name = cls.Meta.model_fields[related].through.get_name()
|
through_name = cls.Meta.model_fields[related].through.get_name()
|
||||||
# for now it's nested dict, should be instance?
|
|
||||||
through_child = cls.populate_through_instance(
|
through_child = cls.populate_through_instance(
|
||||||
row=row,
|
row=row,
|
||||||
related=related,
|
related=related,
|
||||||
through_name=through_name,
|
through_name=through_name,
|
||||||
excludable=excludable
|
excludable=excludable,
|
||||||
)
|
)
|
||||||
item[through_name] = through_child
|
|
||||||
|
if child.__class__ != proxy_source_model:
|
||||||
setattr(child, through_name, through_child)
|
setattr(child, through_name, through_child)
|
||||||
|
else:
|
||||||
|
item[through_name] = through_child
|
||||||
child.set_save_status(True)
|
child.set_save_status(True)
|
||||||
|
|
||||||
return item
|
return item
|
||||||
@ -189,19 +188,24 @@ class ModelRow(NewBaseModel):
|
|||||||
row: sqlalchemy.engine.ResultProxy,
|
row: sqlalchemy.engine.ResultProxy,
|
||||||
through_name: str,
|
through_name: str,
|
||||||
related: str,
|
related: str,
|
||||||
excludable: ExcludableItems
|
excludable: ExcludableItems,
|
||||||
) -> Dict:
|
) -> "ModelRow":
|
||||||
# TODO: fix excludes and includes and docstring
|
|
||||||
model_cls = cls.Meta.model_fields[through_name].to
|
model_cls = cls.Meta.model_fields[through_name].to
|
||||||
table_prefix = cls.Meta.alias_manager.resolve_relation_alias(
|
table_prefix = cls.Meta.alias_manager.resolve_relation_alias(
|
||||||
from_model=cls, relation_name=related
|
from_model=cls, relation_name=related
|
||||||
)
|
)
|
||||||
child = model_cls.extract_prefixed_table_columns(
|
# remove relations on through field
|
||||||
item={},
|
model_excludable = excludable.get(model_cls=model_cls, alias=table_prefix)
|
||||||
row=row,
|
model_excludable.set_values(
|
||||||
excludable=excludable,
|
value=model_cls.extract_related_names(), is_exclude=True
|
||||||
table_prefix=table_prefix
|
|
||||||
)
|
)
|
||||||
|
child_dict = model_cls.extract_prefixed_table_columns(
|
||||||
|
item={}, row=row, excludable=excludable, table_prefix=table_prefix
|
||||||
|
)
|
||||||
|
child_dict["__excluded__"] = model_cls.get_names_to_exclude(
|
||||||
|
excludable=excludable, alias=table_prefix
|
||||||
|
)
|
||||||
|
child = model_cls(**child_dict) # type: ignore
|
||||||
return child
|
return child
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -210,7 +214,7 @@ class ModelRow(NewBaseModel):
|
|||||||
item: dict,
|
item: dict,
|
||||||
row: sqlalchemy.engine.result.ResultProxy,
|
row: sqlalchemy.engine.result.ResultProxy,
|
||||||
table_prefix: str,
|
table_prefix: str,
|
||||||
excludable: ExcludableItems
|
excludable: ExcludableItems,
|
||||||
) -> Dict:
|
) -> Dict:
|
||||||
"""
|
"""
|
||||||
Extracts own fields from raw sql result, using a given prefix.
|
Extracts own fields from raw sql result, using a given prefix.
|
||||||
@ -242,10 +246,7 @@ class ModelRow(NewBaseModel):
|
|||||||
source = row._row if cls.db_backend_name() == "postgresql" else row
|
source = row._row if cls.db_backend_name() == "postgresql" else row
|
||||||
|
|
||||||
selected_columns = cls.own_table_columns(
|
selected_columns = cls.own_table_columns(
|
||||||
model=cls,
|
model=cls, excludable=excludable, alias=table_prefix, use_alias=False,
|
||||||
excludable=excludable,
|
|
||||||
alias=table_prefix,
|
|
||||||
use_alias=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for column in cls.Meta.table.columns:
|
for column in cls.Meta.table.columns:
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Dict,
|
|
||||||
List,
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
Set,
|
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Tuple,
|
Tuple,
|
||||||
Type,
|
Type,
|
||||||
Union,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
@ -16,12 +13,12 @@ from sqlalchemy import text
|
|||||||
|
|
||||||
import ormar # noqa I100
|
import ormar # noqa I100
|
||||||
from ormar.exceptions import RelationshipInstanceError
|
from ormar.exceptions import RelationshipInstanceError
|
||||||
from ormar.models.excludable import ExcludableItems
|
|
||||||
from ormar.relations import AliasManager
|
from ormar.relations import AliasManager
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma no cover
|
if TYPE_CHECKING: # pragma no cover
|
||||||
from ormar import Model
|
from ormar import Model
|
||||||
from ormar.queryset import OrderAction
|
from ormar.queryset import OrderAction
|
||||||
|
from ormar.models.excludable import ExcludableItems
|
||||||
|
|
||||||
|
|
||||||
class SqlJoin:
|
class SqlJoin:
|
||||||
@ -30,7 +27,7 @@ class SqlJoin:
|
|||||||
used_aliases: List,
|
used_aliases: List,
|
||||||
select_from: sqlalchemy.sql.select,
|
select_from: sqlalchemy.sql.select,
|
||||||
columns: List[sqlalchemy.Column],
|
columns: List[sqlalchemy.Column],
|
||||||
excludable: ExcludableItems,
|
excludable: "ExcludableItems",
|
||||||
order_columns: Optional[List["OrderAction"]],
|
order_columns: Optional[List["OrderAction"]],
|
||||||
sorted_orders: OrderedDict,
|
sorted_orders: OrderedDict,
|
||||||
main_model: Type["Model"],
|
main_model: Type["Model"],
|
||||||
@ -296,7 +293,6 @@ class SqlJoin:
|
|||||||
|
|
||||||
self._get_order_bys()
|
self._get_order_bys()
|
||||||
|
|
||||||
# TODO: fix fields and exclusions for through model?
|
|
||||||
self_related_fields = self.next_model.own_table_columns(
|
self_related_fields = self.next_model.own_table_columns(
|
||||||
model=self.next_model,
|
model=self.next_model,
|
||||||
excludable=self.excludable,
|
excludable=self.excludable,
|
||||||
|
|||||||
@ -1,19 +1,15 @@
|
|||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
|
||||||
Dict,
|
Dict,
|
||||||
List,
|
List,
|
||||||
Optional,
|
|
||||||
Sequence,
|
Sequence,
|
||||||
Set,
|
Set,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Tuple,
|
Tuple,
|
||||||
Type,
|
Type,
|
||||||
Union,
|
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
import ormar
|
import ormar
|
||||||
from ormar.models.excludable import ExcludableItems
|
|
||||||
from ormar.queryset.clause import QueryClause
|
from ormar.queryset.clause import QueryClause
|
||||||
from ormar.queryset.query import Query
|
from ormar.queryset.query import Query
|
||||||
from ormar.queryset.utils import extract_models_to_dict_of_lists, translate_list_to_dict
|
from ormar.queryset.utils import extract_models_to_dict_of_lists, translate_list_to_dict
|
||||||
@ -22,29 +18,7 @@ if TYPE_CHECKING: # pragma: no cover
|
|||||||
from ormar import Model
|
from ormar import Model
|
||||||
from ormar.fields import ForeignKeyField, BaseField
|
from ormar.fields import ForeignKeyField, BaseField
|
||||||
from ormar.queryset import OrderAction
|
from ormar.queryset import OrderAction
|
||||||
|
from ormar.models.excludable import ExcludableItems
|
||||||
|
|
||||||
def add_relation_field_to_fields(
|
|
||||||
fields: Union[Set[Any], Dict[Any, Any], None], related_field_name: str
|
|
||||||
) -> Union[Set[Any], Dict[Any, Any], None]:
|
|
||||||
"""
|
|
||||||
Adds related field into fields to include as otherwise it would be skipped.
|
|
||||||
Related field is added only if fields are already populated.
|
|
||||||
Empty fields implies all fields.
|
|
||||||
|
|
||||||
:param fields: Union[Set[Any], Dict[Any, Any], None]
|
|
||||||
:type fields: Dict
|
|
||||||
:param related_field_name: name of the field with relation
|
|
||||||
:type related_field_name: str
|
|
||||||
:return: updated fields dict
|
|
||||||
:rtype: Union[Set[Any], Dict[Any, Any], None]
|
|
||||||
"""
|
|
||||||
if fields and related_field_name not in fields:
|
|
||||||
if isinstance(fields, dict):
|
|
||||||
fields[related_field_name] = ...
|
|
||||||
elif isinstance(fields, set):
|
|
||||||
fields.add(related_field_name)
|
|
||||||
return fields
|
|
||||||
|
|
||||||
|
|
||||||
def sort_models(models: List["Model"], orders_by: Dict) -> List["Model"]:
|
def sort_models(models: List["Model"], orders_by: Dict) -> List["Model"]:
|
||||||
@ -126,7 +100,7 @@ class PrefetchQuery:
|
|||||||
def __init__( # noqa: CFQ002
|
def __init__( # noqa: CFQ002
|
||||||
self,
|
self,
|
||||||
model_cls: Type["Model"],
|
model_cls: Type["Model"],
|
||||||
excludable: ExcludableItems,
|
excludable: "ExcludableItems",
|
||||||
prefetch_related: List,
|
prefetch_related: List,
|
||||||
select_related: List,
|
select_related: List,
|
||||||
orders_by: List["OrderAction"],
|
orders_by: List["OrderAction"],
|
||||||
@ -390,7 +364,7 @@ class PrefetchQuery:
|
|||||||
target_model: Type["Model"],
|
target_model: Type["Model"],
|
||||||
prefetch_dict: Dict,
|
prefetch_dict: Dict,
|
||||||
select_dict: Dict,
|
select_dict: Dict,
|
||||||
excludable: ExcludableItems,
|
excludable: "ExcludableItems",
|
||||||
orders_by: Dict,
|
orders_by: Dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@ -443,15 +417,16 @@ class PrefetchQuery:
|
|||||||
related_field_name = parent_model.get_related_field_name(
|
related_field_name = parent_model.get_related_field_name(
|
||||||
target_field=target_field
|
target_field=target_field
|
||||||
)
|
)
|
||||||
table_prefix, rows = await self._run_prefetch_query(
|
table_prefix, exclude_prefix, rows = await self._run_prefetch_query(
|
||||||
target_field=target_field,
|
target_field=target_field,
|
||||||
excludable=excludable,
|
excludable=excludable,
|
||||||
filter_clauses=filter_clauses,
|
filter_clauses=filter_clauses,
|
||||||
related_field_name=related_field_name
|
related_field_name=related_field_name,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
rows = []
|
rows = []
|
||||||
table_prefix = ""
|
table_prefix = ""
|
||||||
|
exclude_prefix = ""
|
||||||
|
|
||||||
if prefetch_dict and prefetch_dict is not Ellipsis:
|
if prefetch_dict and prefetch_dict is not Ellipsis:
|
||||||
for subrelated in prefetch_dict.keys():
|
for subrelated in prefetch_dict.keys():
|
||||||
@ -472,6 +447,7 @@ class PrefetchQuery:
|
|||||||
parent_model=parent_model,
|
parent_model=parent_model,
|
||||||
target_field=target_field,
|
target_field=target_field,
|
||||||
table_prefix=table_prefix,
|
table_prefix=table_prefix,
|
||||||
|
exclude_prefix=exclude_prefix,
|
||||||
excludable=excludable,
|
excludable=excludable,
|
||||||
prefetch_dict=prefetch_dict,
|
prefetch_dict=prefetch_dict,
|
||||||
orders_by=orders_by,
|
orders_by=orders_by,
|
||||||
@ -486,10 +462,10 @@ class PrefetchQuery:
|
|||||||
async def _run_prefetch_query(
|
async def _run_prefetch_query(
|
||||||
self,
|
self,
|
||||||
target_field: Type["BaseField"],
|
target_field: Type["BaseField"],
|
||||||
excludable: ExcludableItems,
|
excludable: "ExcludableItems",
|
||||||
filter_clauses: List,
|
filter_clauses: List,
|
||||||
related_field_name: str
|
related_field_name: str,
|
||||||
) -> Tuple[str, List]:
|
) -> Tuple[str, str, List]:
|
||||||
"""
|
"""
|
||||||
Actually runs the queries against the database and populates the raw response
|
Actually runs the queries against the database and populates the raw response
|
||||||
for given related model.
|
for given related model.
|
||||||
@ -509,17 +485,22 @@ class PrefetchQuery:
|
|||||||
select_related = []
|
select_related = []
|
||||||
query_target = target_model
|
query_target = target_model
|
||||||
table_prefix = ""
|
table_prefix = ""
|
||||||
|
exclude_prefix = target_field.to.Meta.alias_manager.resolve_relation_alias(
|
||||||
|
from_model=target_field.owner, relation_name=target_field.name
|
||||||
|
)
|
||||||
if target_field.is_multi:
|
if target_field.is_multi:
|
||||||
query_target = target_field.through
|
query_target = target_field.through
|
||||||
select_related = [target_name]
|
select_related = [target_name]
|
||||||
table_prefix = target_field.to.Meta.alias_manager.resolve_relation_alias(
|
table_prefix = target_field.to.Meta.alias_manager.resolve_relation_alias(
|
||||||
from_model=query_target, relation_name=target_name
|
from_model=query_target, relation_name=target_name
|
||||||
)
|
)
|
||||||
|
exclude_prefix = table_prefix
|
||||||
self.already_extracted.setdefault(target_name, {})["prefix"] = table_prefix
|
self.already_extracted.setdefault(target_name, {})["prefix"] = table_prefix
|
||||||
|
|
||||||
model_excludable = excludable.get(model_cls=target_model, alias=table_prefix)
|
model_excludable = excludable.get(model_cls=target_model, alias=exclude_prefix)
|
||||||
if model_excludable.include and not model_excludable.is_included(
|
if model_excludable.include and not model_excludable.is_included(
|
||||||
related_field_name):
|
related_field_name
|
||||||
|
):
|
||||||
model_excludable.set_values({related_field_name}, is_exclude=False)
|
model_excludable.set_values({related_field_name}, is_exclude=False)
|
||||||
|
|
||||||
qry = Query(
|
qry = Query(
|
||||||
@ -537,7 +518,7 @@ class PrefetchQuery:
|
|||||||
# print(expr.compile(compile_kwargs={"literal_binds": True}))
|
# print(expr.compile(compile_kwargs={"literal_binds": True}))
|
||||||
rows = await self.database.fetch_all(expr)
|
rows = await self.database.fetch_all(expr)
|
||||||
self.already_extracted.setdefault(target_name, {}).update({"raw": rows})
|
self.already_extracted.setdefault(target_name, {}).update({"raw": rows})
|
||||||
return table_prefix, rows
|
return table_prefix, exclude_prefix, rows
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_select_related_if_apply(related: str, select_dict: Dict) -> Dict:
|
def _get_select_related_if_apply(related: str, select_dict: Dict) -> Dict:
|
||||||
@ -583,7 +564,8 @@ class PrefetchQuery:
|
|||||||
target_field: Type["ForeignKeyField"],
|
target_field: Type["ForeignKeyField"],
|
||||||
parent_model: Type["Model"],
|
parent_model: Type["Model"],
|
||||||
table_prefix: str,
|
table_prefix: str,
|
||||||
excludable: ExcludableItems,
|
exclude_prefix: str,
|
||||||
|
excludable: "ExcludableItems",
|
||||||
prefetch_dict: Dict,
|
prefetch_dict: Dict,
|
||||||
orders_by: Dict,
|
orders_by: Dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -617,13 +599,10 @@ class PrefetchQuery:
|
|||||||
# TODO Fix fields
|
# TODO Fix fields
|
||||||
field_name = parent_model.get_related_field_name(target_field=target_field)
|
field_name = parent_model.get_related_field_name(target_field=target_field)
|
||||||
item = target_model.extract_prefixed_table_columns(
|
item = target_model.extract_prefixed_table_columns(
|
||||||
item={},
|
item={}, row=row, table_prefix=table_prefix, excludable=excludable,
|
||||||
row=row,
|
|
||||||
table_prefix=table_prefix,
|
|
||||||
excludable=excludable,
|
|
||||||
)
|
)
|
||||||
item["__excluded__"] = target_model.get_names_to_exclude(
|
item["__excluded__"] = target_model.get_names_to_exclude(
|
||||||
excludable=excludable, alias=table_prefix
|
excludable=excludable, alias=exclude_prefix
|
||||||
)
|
)
|
||||||
instance = target_model(**item)
|
instance = target_model(**item)
|
||||||
instance = self._populate_nested_related(
|
instance = self._populate_nested_related(
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
import copy
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import Dict, List, Optional, Set, TYPE_CHECKING, Tuple, Type, Union
|
from typing import List, Optional, TYPE_CHECKING, Tuple, Type
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
import ormar # noqa I100
|
import ormar # noqa I100
|
||||||
from ormar.models.excludable import ExcludableItems
|
|
||||||
from ormar.models.helpers.models import group_related_list
|
from ormar.models.helpers.models import group_related_list
|
||||||
from ormar.queryset import FilterQuery, LimitQuery, OffsetQuery, OrderQuery
|
from ormar.queryset import FilterQuery, LimitQuery, OffsetQuery, OrderQuery
|
||||||
from ormar.queryset.actions.filter_action import FilterAction
|
from ormar.queryset.actions.filter_action import FilterAction
|
||||||
@ -15,6 +13,7 @@ from ormar.queryset.join import SqlJoin
|
|||||||
if TYPE_CHECKING: # pragma no cover
|
if TYPE_CHECKING: # pragma no cover
|
||||||
from ormar import Model
|
from ormar import Model
|
||||||
from ormar.queryset import OrderAction
|
from ormar.queryset import OrderAction
|
||||||
|
from ormar.models.excludable import ExcludableItems
|
||||||
|
|
||||||
|
|
||||||
class Query:
|
class Query:
|
||||||
@ -26,7 +25,7 @@ class Query:
|
|||||||
select_related: List,
|
select_related: List,
|
||||||
limit_count: Optional[int],
|
limit_count: Optional[int],
|
||||||
offset: Optional[int],
|
offset: Optional[int],
|
||||||
excludable: ExcludableItems,
|
excludable: "ExcludableItems",
|
||||||
order_bys: Optional[List["OrderAction"]],
|
order_bys: Optional[List["OrderAction"]],
|
||||||
limit_raw_sql: bool,
|
limit_raw_sql: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -103,9 +102,7 @@ class Query:
|
|||||||
:rtype: sqlalchemy.sql.selectable.Select
|
:rtype: sqlalchemy.sql.selectable.Select
|
||||||
"""
|
"""
|
||||||
self_related_fields = self.model_cls.own_table_columns(
|
self_related_fields = self.model_cls.own_table_columns(
|
||||||
model=self.model_cls,
|
model=self.model_cls, excludable=self.excludable, use_alias=True,
|
||||||
excludable=self.excludable,
|
|
||||||
use_alias=True,
|
|
||||||
)
|
)
|
||||||
self.columns = self.model_cls.Meta.alias_manager.prefixed_columns(
|
self.columns = self.model_cls.Meta.alias_manager.prefixed_columns(
|
||||||
"", self.table, self_related_fields
|
"", self.table, self_related_fields
|
||||||
|
|||||||
@ -20,18 +20,17 @@ from sqlalchemy import bindparam
|
|||||||
import ormar # noqa I100
|
import ormar # noqa I100
|
||||||
from ormar import MultipleMatches, NoMatch
|
from ormar import MultipleMatches, NoMatch
|
||||||
from ormar.exceptions import ModelError, ModelPersistenceError, QueryDefinitionError
|
from ormar.exceptions import ModelError, ModelPersistenceError, QueryDefinitionError
|
||||||
from ormar.models.excludable import ExcludableItems
|
|
||||||
from ormar.queryset import FilterQuery
|
from ormar.queryset import FilterQuery
|
||||||
from ormar.queryset.actions.order_action import OrderAction
|
from ormar.queryset.actions.order_action import OrderAction
|
||||||
from ormar.queryset.clause import QueryClause
|
from ormar.queryset.clause import QueryClause
|
||||||
from ormar.queryset.prefetch_query import PrefetchQuery
|
from ormar.queryset.prefetch_query import PrefetchQuery
|
||||||
from ormar.queryset.query import Query
|
from ormar.queryset.query import Query
|
||||||
from ormar.queryset.utils import update, update_dict_from_list
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma no cover
|
if TYPE_CHECKING: # pragma no cover
|
||||||
from ormar.models import T
|
from ormar.models import T
|
||||||
from ormar.models.metaclass import ModelMeta
|
from ormar.models.metaclass import ModelMeta
|
||||||
from ormar.relations.querysetproxy import QuerysetProxy
|
from ormar.relations.querysetproxy import QuerysetProxy
|
||||||
|
from ormar.models.excludable import ExcludableItems
|
||||||
else:
|
else:
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
@ -49,11 +48,13 @@ class QuerySet(Generic[T]):
|
|||||||
select_related: List = None,
|
select_related: List = None,
|
||||||
limit_count: int = None,
|
limit_count: int = None,
|
||||||
offset: int = None,
|
offset: int = None,
|
||||||
excludable: ExcludableItems = None,
|
excludable: "ExcludableItems" = None,
|
||||||
order_bys: List = None,
|
order_bys: List = None,
|
||||||
prefetch_related: List = None,
|
prefetch_related: List = None,
|
||||||
limit_raw_sql: bool = False,
|
limit_raw_sql: bool = False,
|
||||||
|
proxy_source_model: Optional[Type[T]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self.proxy_source_model = proxy_source_model
|
||||||
self.model_cls = model_cls
|
self.model_cls = model_cls
|
||||||
self.filter_clauses = [] if filter_clauses is None else filter_clauses
|
self.filter_clauses = [] if filter_clauses is None else filter_clauses
|
||||||
self.exclude_clauses = [] if exclude_clauses is None else exclude_clauses
|
self.exclude_clauses = [] if exclude_clauses is None else exclude_clauses
|
||||||
@ -61,7 +62,7 @@ class QuerySet(Generic[T]):
|
|||||||
self._prefetch_related = [] if prefetch_related is None else prefetch_related
|
self._prefetch_related = [] if prefetch_related is None else prefetch_related
|
||||||
self.limit_count = limit_count
|
self.limit_count = limit_count
|
||||||
self.query_offset = offset
|
self.query_offset = offset
|
||||||
self._excludable = excludable or ExcludableItems()
|
self._excludable = excludable or ormar.ExcludableItems()
|
||||||
self.order_bys = order_bys or []
|
self.order_bys = order_bys or []
|
||||||
self.limit_sql_raw = limit_raw_sql
|
self.limit_sql_raw = limit_raw_sql
|
||||||
|
|
||||||
@ -105,6 +106,51 @@ class QuerySet(Generic[T]):
|
|||||||
raise ValueError("Model class of QuerySet is not initialized")
|
raise ValueError("Model class of QuerySet is not initialized")
|
||||||
return self.model_cls
|
return self.model_cls
|
||||||
|
|
||||||
|
def rebuild_self( # noqa: CFQ002
|
||||||
|
self,
|
||||||
|
filter_clauses: List = None,
|
||||||
|
exclude_clauses: List = None,
|
||||||
|
select_related: List = None,
|
||||||
|
limit_count: int = None,
|
||||||
|
offset: int = None,
|
||||||
|
excludable: "ExcludableItems" = None,
|
||||||
|
order_bys: List = None,
|
||||||
|
prefetch_related: List = None,
|
||||||
|
limit_raw_sql: bool = None,
|
||||||
|
proxy_source_model: Optional[Type[T]] = None,
|
||||||
|
) -> "QuerySet":
|
||||||
|
"""
|
||||||
|
Method that returns new instance of queryset based on passed params,
|
||||||
|
all not passed params are taken from current values.
|
||||||
|
"""
|
||||||
|
overwrites = {
|
||||||
|
"select_related": "_select_related",
|
||||||
|
"offset": "query_offset",
|
||||||
|
"excludable": "_excludable",
|
||||||
|
"prefetch_related": "_prefetch_related",
|
||||||
|
"limit_raw_sql": "limit_sql_raw",
|
||||||
|
}
|
||||||
|
passed_args = locals()
|
||||||
|
|
||||||
|
def replace_if_none(arg_name: str) -> Any:
|
||||||
|
if passed_args.get(arg_name) is None:
|
||||||
|
return getattr(self, overwrites.get(arg_name, arg_name))
|
||||||
|
return passed_args.get(arg_name)
|
||||||
|
|
||||||
|
return self.__class__(
|
||||||
|
model_cls=self.model_cls,
|
||||||
|
filter_clauses=replace_if_none("filter_clauses"),
|
||||||
|
exclude_clauses=replace_if_none("exclude_clauses"),
|
||||||
|
select_related=replace_if_none("select_related"),
|
||||||
|
limit_count=replace_if_none("limit_count"),
|
||||||
|
offset=replace_if_none("offset"),
|
||||||
|
excludable=replace_if_none("excludable"),
|
||||||
|
order_bys=replace_if_none("order_bys"),
|
||||||
|
prefetch_related=replace_if_none("prefetch_related"),
|
||||||
|
limit_raw_sql=replace_if_none("limit_raw_sql"),
|
||||||
|
proxy_source_model=replace_if_none("proxy_source_model"),
|
||||||
|
)
|
||||||
|
|
||||||
async def _prefetch_related_models(
|
async def _prefetch_related_models(
|
||||||
self, models: Sequence[Optional["T"]], rows: List
|
self, models: Sequence[Optional["T"]], rows: List
|
||||||
) -> Sequence[Optional["T"]]:
|
) -> Sequence[Optional["T"]]:
|
||||||
@ -142,6 +188,7 @@ class QuerySet(Generic[T]):
|
|||||||
select_related=self._select_related,
|
select_related=self._select_related,
|
||||||
excludable=self._excludable,
|
excludable=self._excludable,
|
||||||
source_model=self.model,
|
source_model=self.model,
|
||||||
|
proxy_source_model=self.proxy_source_model,
|
||||||
)
|
)
|
||||||
for row in rows
|
for row in rows
|
||||||
]
|
]
|
||||||
@ -254,17 +301,10 @@ class QuerySet(Generic[T]):
|
|||||||
exclude_clauses = self.exclude_clauses
|
exclude_clauses = self.exclude_clauses
|
||||||
filter_clauses = filter_clauses
|
filter_clauses = filter_clauses
|
||||||
|
|
||||||
return self.__class__(
|
return self.rebuild_self(
|
||||||
model_cls=self.model,
|
|
||||||
filter_clauses=filter_clauses,
|
filter_clauses=filter_clauses,
|
||||||
exclude_clauses=exclude_clauses,
|
exclude_clauses=exclude_clauses,
|
||||||
select_related=select_related,
|
select_related=select_related,
|
||||||
limit_count=self.limit_count,
|
|
||||||
offset=self.query_offset,
|
|
||||||
excludable=self._excludable,
|
|
||||||
order_bys=self.order_bys,
|
|
||||||
prefetch_related=self._prefetch_related,
|
|
||||||
limit_raw_sql=self.limit_sql_raw,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def exclude(self, **kwargs: Any) -> "QuerySet": # noqa: A003
|
def exclude(self, **kwargs: Any) -> "QuerySet": # noqa: A003
|
||||||
@ -309,18 +349,7 @@ class QuerySet(Generic[T]):
|
|||||||
related = [related]
|
related = [related]
|
||||||
|
|
||||||
related = list(set(list(self._select_related) + related))
|
related = list(set(list(self._select_related) + related))
|
||||||
return self.__class__(
|
return self.rebuild_self(select_related=related,)
|
||||||
model_cls=self.model,
|
|
||||||
filter_clauses=self.filter_clauses,
|
|
||||||
exclude_clauses=self.exclude_clauses,
|
|
||||||
select_related=related,
|
|
||||||
limit_count=self.limit_count,
|
|
||||||
offset=self.query_offset,
|
|
||||||
excludable=self._excludable,
|
|
||||||
order_bys=self.order_bys,
|
|
||||||
prefetch_related=self._prefetch_related,
|
|
||||||
limit_raw_sql=self.limit_sql_raw,
|
|
||||||
)
|
|
||||||
|
|
||||||
def prefetch_related(self, related: Union[List, str]) -> "QuerySet":
|
def prefetch_related(self, related: Union[List, str]) -> "QuerySet":
|
||||||
"""
|
"""
|
||||||
@ -344,21 +373,11 @@ class QuerySet(Generic[T]):
|
|||||||
related = [related]
|
related = [related]
|
||||||
|
|
||||||
related = list(set(list(self._prefetch_related) + related))
|
related = list(set(list(self._prefetch_related) + related))
|
||||||
return self.__class__(
|
return self.rebuild_self(prefetch_related=related,)
|
||||||
model_cls=self.model,
|
|
||||||
filter_clauses=self.filter_clauses,
|
|
||||||
exclude_clauses=self.exclude_clauses,
|
|
||||||
select_related=self._select_related,
|
|
||||||
limit_count=self.limit_count,
|
|
||||||
offset=self.query_offset,
|
|
||||||
excludable=self._excludable,
|
|
||||||
order_bys=self.order_bys,
|
|
||||||
prefetch_related=related,
|
|
||||||
limit_raw_sql=self.limit_sql_raw,
|
|
||||||
)
|
|
||||||
|
|
||||||
def fields(self, columns: Union[List, str, Set, Dict],
|
def fields(
|
||||||
_is_exclude: bool = False) -> "QuerySet":
|
self, columns: Union[List, str, Set, Dict], _is_exclude: bool = False
|
||||||
|
) -> "QuerySet":
|
||||||
"""
|
"""
|
||||||
With `fields()` you can select subset of model columns to limit the data load.
|
With `fields()` you can select subset of model columns to limit the data load.
|
||||||
|
|
||||||
@ -396,29 +415,22 @@ class QuerySet(Generic[T]):
|
|||||||
|
|
||||||
To include whole nested model specify model related field name and ellipsis.
|
To include whole nested model specify model related field name and ellipsis.
|
||||||
|
|
||||||
|
:param _is_exclude: flag if it's exclude or include operation
|
||||||
|
:type _is_exclude: bool
|
||||||
:param columns: columns to include
|
:param columns: columns to include
|
||||||
:type columns: Union[List, str, Set, Dict]
|
:type columns: Union[List, str, Set, Dict]
|
||||||
:return: QuerySet
|
:return: QuerySet
|
||||||
:rtype: QuerySet
|
:rtype: QuerySet
|
||||||
"""
|
"""
|
||||||
excludable = ExcludableItems.from_excludable(self._excludable)
|
excludable = ormar.ExcludableItems.from_excludable(self._excludable)
|
||||||
excludable.build(items=columns,
|
excludable.build(
|
||||||
model_cls=self.model_cls,
|
items=columns,
|
||||||
is_exclude=_is_exclude)
|
model_cls=self.model_cls, # type: ignore
|
||||||
|
is_exclude=_is_exclude,
|
||||||
return self.__class__(
|
|
||||||
model_cls=self.model,
|
|
||||||
filter_clauses=self.filter_clauses,
|
|
||||||
exclude_clauses=self.exclude_clauses,
|
|
||||||
select_related=self._select_related,
|
|
||||||
limit_count=self.limit_count,
|
|
||||||
offset=self.query_offset,
|
|
||||||
excludable=excludable,
|
|
||||||
order_bys=self.order_bys,
|
|
||||||
prefetch_related=self._prefetch_related,
|
|
||||||
limit_raw_sql=self.limit_sql_raw,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return self.rebuild_self(excludable=excludable,)
|
||||||
|
|
||||||
def exclude_fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet":
|
def exclude_fields(self, columns: Union[List, str, Set, Dict]) -> "QuerySet":
|
||||||
"""
|
"""
|
||||||
With `exclude_fields()` you can select subset of model columns that will
|
With `exclude_fields()` you can select subset of model columns that will
|
||||||
@ -489,18 +501,7 @@ class QuerySet(Generic[T]):
|
|||||||
]
|
]
|
||||||
|
|
||||||
order_bys = self.order_bys + [x for x in orders_by if x not in self.order_bys]
|
order_bys = self.order_bys + [x for x in orders_by if x not in self.order_bys]
|
||||||
return self.__class__(
|
return self.rebuild_self(order_bys=order_bys,)
|
||||||
model_cls=self.model,
|
|
||||||
filter_clauses=self.filter_clauses,
|
|
||||||
exclude_clauses=self.exclude_clauses,
|
|
||||||
select_related=self._select_related,
|
|
||||||
limit_count=self.limit_count,
|
|
||||||
offset=self.query_offset,
|
|
||||||
excludable=self._excludable,
|
|
||||||
order_bys=order_bys,
|
|
||||||
prefetch_related=self._prefetch_related,
|
|
||||||
limit_raw_sql=self.limit_sql_raw,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def exists(self) -> bool:
|
async def exists(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -601,18 +602,7 @@ class QuerySet(Generic[T]):
|
|||||||
|
|
||||||
limit_count = page_size
|
limit_count = page_size
|
||||||
query_offset = (page - 1) * page_size
|
query_offset = (page - 1) * page_size
|
||||||
return self.__class__(
|
return self.rebuild_self(limit_count=limit_count, offset=query_offset,)
|
||||||
model_cls=self.model,
|
|
||||||
filter_clauses=self.filter_clauses,
|
|
||||||
exclude_clauses=self.exclude_clauses,
|
|
||||||
select_related=self._select_related,
|
|
||||||
limit_count=limit_count,
|
|
||||||
offset=query_offset,
|
|
||||||
excludable=self._excludable,
|
|
||||||
order_bys=self.order_bys,
|
|
||||||
prefetch_related=self._prefetch_related,
|
|
||||||
limit_raw_sql=self.limit_sql_raw,
|
|
||||||
)
|
|
||||||
|
|
||||||
def limit(self, limit_count: int, limit_raw_sql: bool = None) -> "QuerySet":
|
def limit(self, limit_count: int, limit_raw_sql: bool = None) -> "QuerySet":
|
||||||
"""
|
"""
|
||||||
@ -629,18 +619,7 @@ class QuerySet(Generic[T]):
|
|||||||
:rtype: QuerySet
|
:rtype: QuerySet
|
||||||
"""
|
"""
|
||||||
limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql
|
limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql
|
||||||
return self.__class__(
|
return self.rebuild_self(limit_count=limit_count, limit_raw_sql=limit_raw_sql,)
|
||||||
model_cls=self.model,
|
|
||||||
filter_clauses=self.filter_clauses,
|
|
||||||
exclude_clauses=self.exclude_clauses,
|
|
||||||
select_related=self._select_related,
|
|
||||||
limit_count=limit_count,
|
|
||||||
offset=self.query_offset,
|
|
||||||
excludable=self._excludable,
|
|
||||||
order_bys=self.order_bys,
|
|
||||||
prefetch_related=self._prefetch_related,
|
|
||||||
limit_raw_sql=limit_raw_sql,
|
|
||||||
)
|
|
||||||
|
|
||||||
def offset(self, offset: int, limit_raw_sql: bool = None) -> "QuerySet":
|
def offset(self, offset: int, limit_raw_sql: bool = None) -> "QuerySet":
|
||||||
"""
|
"""
|
||||||
@ -657,18 +636,7 @@ class QuerySet(Generic[T]):
|
|||||||
:rtype: QuerySet
|
:rtype: QuerySet
|
||||||
"""
|
"""
|
||||||
limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql
|
limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql
|
||||||
return self.__class__(
|
return self.rebuild_self(offset=offset, limit_raw_sql=limit_raw_sql,)
|
||||||
model_cls=self.model,
|
|
||||||
filter_clauses=self.filter_clauses,
|
|
||||||
exclude_clauses=self.exclude_clauses,
|
|
||||||
select_related=self._select_related,
|
|
||||||
limit_count=self.limit_count,
|
|
||||||
offset=offset,
|
|
||||||
excludable=self._excludable,
|
|
||||||
order_bys=self.order_bys,
|
|
||||||
prefetch_related=self._prefetch_related,
|
|
||||||
limit_raw_sql=limit_raw_sql,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def first(self, **kwargs: Any) -> T:
|
async def first(self, **kwargs: Any) -> T:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -119,7 +119,9 @@ class RelationProxy(list):
|
|||||||
self._check_if_model_saved()
|
self._check_if_model_saved()
|
||||||
kwargs = {f"{related_field.get_alias()}__{pkname}": self._owner.pk}
|
kwargs = {f"{related_field.get_alias()}__{pkname}": self._owner.pk}
|
||||||
queryset = (
|
queryset = (
|
||||||
ormar.QuerySet(model_cls=self.relation.to)
|
ormar.QuerySet(
|
||||||
|
model_cls=self.relation.to, proxy_source_model=self._owner.__class__
|
||||||
|
)
|
||||||
.select_related(related_field.name)
|
.select_related(related_field.name)
|
||||||
.filter(**kwargs)
|
.filter(**kwargs)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -22,7 +22,7 @@ uuid1 = uuid.uuid4()
|
|||||||
uuid2 = uuid.uuid4()
|
uuid2 = uuid.uuid4()
|
||||||
|
|
||||||
|
|
||||||
class TestEnum(Enum):
|
class EnumTest(Enum):
|
||||||
val1 = "Val1"
|
val1 = "Val1"
|
||||||
val2 = "Val2"
|
val2 = "Val2"
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ class Organisation(ormar.Model):
|
|||||||
)
|
)
|
||||||
random_json: pydantic.Json = ormar.JSON(choices=["aa", '{"aa":"bb"}'])
|
random_json: pydantic.Json = ormar.JSON(choices=["aa", '{"aa":"bb"}'])
|
||||||
random_uuid: uuid.UUID = ormar.UUID(choices=[uuid1, uuid2])
|
random_uuid: uuid.UUID = ormar.UUID(choices=[uuid1, uuid2])
|
||||||
enum_string: str = ormar.String(max_length=100, choices=list(TestEnum))
|
enum_string: str = ormar.String(max_length=100, choices=list(EnumTest))
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
@ -110,7 +110,7 @@ def test_all_endpoints():
|
|||||||
"random_decimal": 12.4,
|
"random_decimal": 12.4,
|
||||||
"random_json": '{"aa":"bb"}',
|
"random_json": '{"aa":"bb"}',
|
||||||
"random_uuid": str(uuid1),
|
"random_uuid": str(uuid1),
|
||||||
"enum_string": TestEnum.val1.value,
|
"enum_string": EnumTest.val1.value,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Any
|
from typing import Any, List, Sequence, cast
|
||||||
|
|
||||||
import databases
|
import databases
|
||||||
import pytest
|
import pytest
|
||||||
@ -131,6 +131,7 @@ async def test_getting_additional_fields_from_queryset() -> Any:
|
|||||||
)
|
)
|
||||||
|
|
||||||
await post.categories.all()
|
await post.categories.all()
|
||||||
|
assert post.postcategory is None
|
||||||
assert post.categories[0].postcategory.sort_order == 1
|
assert post.categories[0].postcategory.sort_order == 1
|
||||||
assert post.categories[1].postcategory.sort_order == 2
|
assert post.categories[1].postcategory.sort_order == 2
|
||||||
|
|
||||||
@ -138,8 +139,31 @@ async def test_getting_additional_fields_from_queryset() -> Any:
|
|||||||
categories__name="Test category2"
|
categories__name="Test category2"
|
||||||
)
|
)
|
||||||
assert post2.categories[0].postcategory.sort_order == 2
|
assert post2.categories[0].postcategory.sort_order == 2
|
||||||
# if TYPE_CHECKING:
|
|
||||||
# reveal_type(post2)
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_only_one_side_has_through() -> Any:
|
||||||
|
async with database:
|
||||||
|
post = await Post(title="Test post").save()
|
||||||
|
await post.categories.create(
|
||||||
|
name="Test category1", postcategory={"sort_order": 1}
|
||||||
|
)
|
||||||
|
await post.categories.create(
|
||||||
|
name="Test category2", postcategory={"sort_order": 2}
|
||||||
|
)
|
||||||
|
|
||||||
|
post2 = await Post.objects.select_related("categories").get()
|
||||||
|
assert post2.postcategory is None
|
||||||
|
assert post2.categories[0].postcategory is not None
|
||||||
|
|
||||||
|
await post2.categories.all()
|
||||||
|
assert post2.postcategory is None
|
||||||
|
assert post2.categories[0].postcategory is not None
|
||||||
|
|
||||||
|
categories = await Category.objects.select_related("posts").all()
|
||||||
|
categories = cast(Sequence[Category], categories)
|
||||||
|
assert categories[0].postcategory is None
|
||||||
|
assert categories[0].posts[0].postcategory is not None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -294,7 +318,6 @@ async def test_update_through_from_related() -> Any:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.skip # TODO: Restore after finished exclude refactor
|
|
||||||
async def test_excluding_fields_on_through_model() -> Any:
|
async def test_excluding_fields_on_through_model() -> Any:
|
||||||
async with database:
|
async with database:
|
||||||
post = await Post(title="Test post").save()
|
post = await Post(title="Test post").save()
|
||||||
@ -323,6 +346,17 @@ async def test_excluding_fields_on_through_model() -> Any:
|
|||||||
assert post2.categories[2].postcategory.param_name is None
|
assert post2.categories[2].postcategory.param_name is None
|
||||||
assert post2.categories[2].postcategory.sort_order == 3
|
assert post2.categories[2].postcategory.sort_order == 3
|
||||||
|
|
||||||
|
post3 = (
|
||||||
|
await Post.objects.select_related("categories")
|
||||||
|
.fields({"postcategory": ..., "title": ...})
|
||||||
|
.exclude_fields({"postcategory": {"param_name", "sort_order"}})
|
||||||
|
.get()
|
||||||
|
)
|
||||||
|
assert len(post3.categories) == 3
|
||||||
|
for category in post3.categories:
|
||||||
|
assert category.postcategory.param_name is None
|
||||||
|
assert category.postcategory.sort_order is None
|
||||||
|
|
||||||
|
|
||||||
# TODO: check/ modify following
|
# TODO: check/ modify following
|
||||||
|
|
||||||
@ -337,9 +371,9 @@ async def test_excluding_fields_on_through_model() -> Any:
|
|||||||
# ordering by in order_by (V)
|
# ordering by in order_by (V)
|
||||||
# updating in query (V)
|
# updating in query (V)
|
||||||
# updating from querysetproxy (V)
|
# updating from querysetproxy (V)
|
||||||
|
# including/excluding in fields?
|
||||||
|
|
||||||
# modifying from instance (both sides?) (X) <= no, the loaded one doesn't have relations
|
# modifying from instance (both sides?) (X) <= no, the loaded one doesn't have relations
|
||||||
|
|
||||||
# including/excluding in fields?
|
|
||||||
# allowing to change fk fields names in through model?
|
# allowing to change fk fields names in through model?
|
||||||
# make through optional? auto-generated for cases other fields are missing?
|
# make through optional? auto-generated for cases other fields are missing?
|
||||||
|
|||||||
@ -8,11 +8,6 @@ from ormar.queryset.utils import translate_list_to_dict, update_dict_from_list,
|
|||||||
from tests.settings import DATABASE_URL
|
from tests.settings import DATABASE_URL
|
||||||
|
|
||||||
|
|
||||||
def test_empty_excludable():
|
|
||||||
assert ExcludableMixin.is_included(None, "key") # all fields included if empty
|
|
||||||
assert not ExcludableMixin.is_excluded(None, "key") # none field excluded if empty
|
|
||||||
|
|
||||||
|
|
||||||
def test_list_to_dict_translation():
|
def test_list_to_dict_translation():
|
||||||
tet_list = ["aa", "bb", "cc__aa", "cc__bb", "cc__aa__xx", "cc__aa__yy"]
|
tet_list = ["aa", "bb", "cc__aa", "cc__bb", "cc__aa__xx", "cc__aa__yy"]
|
||||||
test = translate_list_to_dict(tet_list)
|
test = translate_list_to_dict(tet_list)
|
||||||
|
|||||||
Reference in New Issue
Block a user