add and/or/not to filtergroups, add left and right shift to operators, add some tests, add *args to other functions that read data and use filter

This commit is contained in:
collerek
2021-04-19 19:49:42 +02:00
parent c49d21f605
commit 7a27778b44
8 changed files with 298 additions and 121 deletions

View File

@ -25,21 +25,34 @@ class FilterGroup:
"""
def __init__(
self, *args: Any, _filter_type: FilterType = FilterType.AND, **kwargs: Any,
self, *args: Any,
_filter_type: FilterType = FilterType.AND,
_exclude: bool = False,
**kwargs: Any,
) -> None:
self.filter_type = _filter_type
self.exclude = False
self.exclude = _exclude
self._nested_groups: List["FilterGroup"] = list(args)
self._resolved = False
self.is_source_model_filter = False
self._kwargs_dict = kwargs
self.actions: List[FilterAction] = []
def __and__(self, other: "FilterGroup") -> "FilterGroup":
return FilterGroup(self, other)
def __or__(self, other) -> "FilterGroup":
return FilterGroup(self, other, _filter_type=FilterType.OR)
def __invert__(self) -> "FilterGroup":
self.exclude = not self.exclude
return self
def resolve(
self,
model_cls: Type["Model"],
select_related: List = None,
filter_clauses: List = None,
self,
model_cls: Type["Model"],
select_related: List = None,
filter_clauses: List = None,
) -> Tuple[List[FilterAction], List[str]]:
"""
Resolves the FilterGroups actions to use proper target model, replace
@ -107,13 +120,16 @@ class FilterGroup:
:return: complied and escaped clause
:rtype: sqlalchemy.sql.elements.TextClause
"""
prefix = " NOT " if self.exclude else ""
if self.filter_type == FilterType.AND:
clause = sqlalchemy.text(
"( " + str(sqlalchemy.sql.and_(*self._get_text_clauses())) + " )"
f"{prefix}( " + str(
sqlalchemy.sql.and_(*self._get_text_clauses())) + " )"
)
else:
clause = sqlalchemy.text(
"( " + str(sqlalchemy.sql.or_(*self._get_text_clauses())) + " )"
f"{prefix}( " + str(
sqlalchemy.sql.or_(*self._get_text_clauses())) + " )"
)
return clause
@ -166,7 +182,7 @@ class QueryClause:
"""
def __init__(
self, model_cls: Type["Model"], filter_clauses: List, select_related: List,
self, model_cls: Type["Model"], filter_clauses: List, select_related: List,
) -> None:
self._select_related = select_related[:]
@ -176,7 +192,7 @@ class QueryClause:
self.table = self.model_cls.Meta.table
def prepare_filter( # noqa: A003
self, _own_only: bool = False, **kwargs: Any
self, _own_only: bool = False, **kwargs: Any
) -> Tuple[List[FilterAction], List[str]]:
"""
Main external access point that processes the clauses into sqlalchemy text
@ -201,7 +217,7 @@ class QueryClause:
return filter_clauses, select_related
def _populate_filter_clauses(
self, _own_only: bool, **kwargs: Any
self, _own_only: bool, **kwargs: Any
) -> Tuple[List[FilterAction], List[str]]:
"""
Iterates all clauses and extracts used operator and field from related
@ -282,7 +298,7 @@ class QueryClause:
return prefixes
def _switch_filter_action_prefixes(
self, filter_clauses: List[FilterAction]
self, filter_clauses: List[FilterAction]
) -> List[FilterAction]:
"""
Substitutes aliases for filter action if the complex key (whole relation str) is

View File

@ -1,8 +1,8 @@
from typing import Any
from ormar.queryset.actions import OrderAction
from ormar.queryset.actions import FilterAction
from ormar.queryset.actions.filter_action import METHODS_TO_OPERATORS
from ormar.queryset.clause import FilterGroup
class FieldAccessor:
@ -14,6 +14,10 @@ class FieldAccessor:
self._model = model
self._access_chain = access_chain
def __bool__(self):
# hack to avoid pydantic name check from parent model
return False
def __getattr__(self, item: str) -> Any:
if self._field and item == self._field.name:
return self._field
@ -32,7 +36,7 @@ class FieldAccessor:
field=field,
access_chain=self._access_chain + f"__{item}",
)
return object.__getattribute__(self, item)
return object.__getattribute__(self, item) # pragma: no cover
def _check_field(self) -> None:
if not self._field:
@ -40,57 +44,60 @@ class FieldAccessor:
"Cannot filter by Model, you need to provide model name"
)
def _select_operator(self, op: str, other: Any) -> FilterAction:
def _select_operator(self, op: str, other: Any) -> FilterGroup:
self._check_field()
return FilterAction(
filter_str=self._access_chain + f"__{METHODS_TO_OPERATORS[op]}",
value=other,
model_cls=self._source_model,
)
filter_kwg = {self._access_chain + f"__{METHODS_TO_OPERATORS[op]}": other}
return FilterGroup(**filter_kwg)
def __eq__(self, other: Any) -> FilterAction: # type: ignore
def __eq__(self, other: Any) -> FilterGroup: # type: ignore
return self._select_operator(op="__eq__", other=other)
def __ge__(self, other: Any) -> FilterAction:
def __ge__(self, other: Any) -> FilterGroup:
return self._select_operator(op="__ge__", other=other)
def __gt__(self, other: Any) -> FilterAction:
def __gt__(self, other: Any) -> FilterGroup:
return self._select_operator(op="__gt__", other=other)
def __le__(self, other: Any) -> FilterAction:
def __le__(self, other: Any) -> FilterGroup:
return self._select_operator(op="__le__", other=other)
def __lt__(self, other) -> FilterAction:
def __lt__(self, other) -> FilterGroup:
return self._select_operator(op="__lt__", other=other)
def __mod__(self, other) -> FilterAction:
def __mod__(self, other) -> FilterGroup:
return self._select_operator(op="__mod__", other=other)
def __contains__(self, item) -> FilterAction:
return self._select_operator(op="in", other=item)
def __lshift__(self, other) -> FilterGroup:
return self._select_operator(op="in", other=other)
def iexact(self, other) -> FilterAction:
def __rshift__(self, other) -> FilterGroup:
return self._select_operator(op="isnull", other=True)
def in_(self, other) -> FilterGroup:
return self._select_operator(op="in", other=other)
def iexact(self, other) -> FilterGroup:
return self._select_operator(op="iexact", other=other)
def contains(self, other) -> FilterAction:
def contains(self, other) -> FilterGroup:
return self._select_operator(op="contains", other=other)
def icontains(self, other) -> FilterAction:
def icontains(self, other) -> FilterGroup:
return self._select_operator(op="icontains", other=other)
def startswith(self, other) -> FilterAction:
def startswith(self, other) -> FilterGroup:
return self._select_operator(op="startswith", other=other)
def istartswith(self, other) -> FilterAction:
def istartswith(self, other) -> FilterGroup:
return self._select_operator(op="istartswith", other=other)
def endswith(self, other) -> FilterAction:
def endswith(self, other) -> FilterGroup:
return self._select_operator(op="endswith", other=other)
def iendswith(self, other) -> FilterAction:
def iendswith(self, other) -> FilterGroup:
return self._select_operator(op="iendswith", other=other)
def isnull(self, other) -> FilterAction:
def isnull(self, other) -> FilterGroup:
return self._select_operator(op="isnull", other=other)
def asc(self) -> OrderAction:

View File

@ -504,7 +504,7 @@ class QuerySet(Generic[T]):
"""
return self.fields(columns=columns, _is_exclude=True)
def order_by(self, columns: Union[List, str]) -> "QuerySet[T]":
def order_by(self, columns: Union[List, str, OrderAction]) -> "QuerySet[T]":
"""
With `order_by()` you can order the results from database based on your
choice of fields.
@ -541,6 +541,7 @@ class QuerySet(Generic[T]):
orders_by = [
OrderAction(order_str=x, model_cls=self.model_cls) # type: ignore
if not isinstance(x, OrderAction) else x
for x in columns
]
@ -671,7 +672,7 @@ class QuerySet(Generic[T]):
)
return await self.database.execute(expr)
async def delete(self, each: bool = False, **kwargs: Any) -> int:
async def delete(self, *args, each: bool = False, **kwargs: Any) -> int:
"""
Deletes from the model table after applying the filters from kwargs.
@ -685,8 +686,8 @@ class QuerySet(Generic[T]):
:return: number of deleted rows
:rtype:int
"""
if kwargs:
return await self.filter(**kwargs).delete()
if kwargs or args:
return await self.filter(*args, **kwargs).delete()
if not each and not (self.filter_clauses or self.exclude_clauses):
raise QueryDefinitionError(
"You cannot delete without filtering the queryset first. "
@ -753,7 +754,7 @@ class QuerySet(Generic[T]):
limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql
return self.rebuild_self(offset=offset, limit_raw_sql=limit_raw_sql,)
async def first(self, **kwargs: Any) -> "T":
async def first(self, *args, **kwargs: Any) -> "T":
"""
Gets the first row from the db ordered by primary key column ascending.
@ -764,8 +765,8 @@ class QuerySet(Generic[T]):
:return: returned model
:rtype: Model
"""
if kwargs:
return await self.filter(**kwargs).first()
if kwargs or args:
return await self.filter(*args, **kwargs).first()
expr = self.build_select_expression(
limit=1,
@ -784,7 +785,7 @@ class QuerySet(Generic[T]):
self.check_single_result_rows_count(processed_rows)
return processed_rows[0] # type: ignore
async def get_or_none(self, **kwargs: Any) -> Optional["T"]:
async def get_or_none(self, *args, **kwargs: Any) -> Optional["T"]:
"""
Get's the first row from the db meeting the criteria set by kwargs.
@ -800,11 +801,11 @@ class QuerySet(Generic[T]):
:rtype: Model
"""
try:
return await self.get(**kwargs)
return await self.get(*args, **kwargs)
except ormar.NoMatch:
return None
async def get(self, **kwargs: Any) -> "T":
async def get(self, *args, **kwargs: Any) -> "T":
"""
Get's the first row from the db meeting the criteria set by kwargs.
@ -819,8 +820,8 @@ class QuerySet(Generic[T]):
:return: returned model
:rtype: Model
"""
if kwargs:
return await self.filter(**kwargs).get()
if kwargs or args:
return await self.filter(*args, **kwargs).get()
if not self.filter_clauses:
expr = self.build_select_expression(
@ -843,7 +844,7 @@ class QuerySet(Generic[T]):
self.check_single_result_rows_count(processed_rows)
return processed_rows[0] # type: ignore
async def get_or_create(self, **kwargs: Any) -> "T":
async def get_or_create(self, *args, **kwargs: Any) -> "T":
"""
Combination of create and get methods.
@ -857,7 +858,7 @@ class QuerySet(Generic[T]):
:rtype: Model
"""
try:
return await self.get(**kwargs)
return await self.get(*args, **kwargs)
except NoMatch:
return await self.create(**kwargs)
@ -878,7 +879,7 @@ class QuerySet(Generic[T]):
model = await self.get(pk=kwargs[pk_name])
return await model.update(**kwargs)
async def all(self, **kwargs: Any) -> List[Optional["T"]]: # noqa: A003
async def all(self, *args, **kwargs: Any) -> List[Optional["T"]]: # noqa: A003
"""
Returns all rows from a database for given model for set filter options.
@ -891,8 +892,8 @@ class QuerySet(Generic[T]):
:return: list of returned models
:rtype: List[Model]
"""
if kwargs:
return await self.filter(**kwargs).all()
if kwargs or args:
return await self.filter(*args, **kwargs).all()
expr = self.build_select_expression()
rows = await self.database.fetch_all(expr)

View File

@ -22,7 +22,7 @@ if TYPE_CHECKING: # pragma no cover
from ormar.relations import Relation
from ormar.models import Model, T
from ormar.queryset import QuerySet
from ormar import RelationType
from ormar import OrderAction, RelationType
else:
T = TypeVar("T", bound="Model")
@ -276,7 +276,7 @@ class QuerysetProxy(Generic[T]):
)
return await queryset.delete(**kwargs) # type: ignore
async def first(self, **kwargs: Any) -> "T":
async def first(self, *args, **kwargs: Any) -> "T":
"""
Gets the first row from the db ordered by primary key column ascending.
@ -289,12 +289,12 @@ class QuerysetProxy(Generic[T]):
:return:
:rtype: _asyncio.Future
"""
first = await self.queryset.first(**kwargs)
first = await self.queryset.first(*args, **kwargs)
self._clean_items_on_load()
self._register_related(first)
return first
async def get_or_none(self, **kwargs: Any) -> Optional["T"]:
async def get_or_none(self, *args, **kwargs: Any) -> Optional["T"]:
"""
Get's the first row from the db meeting the criteria set by kwargs.
@ -310,7 +310,7 @@ class QuerysetProxy(Generic[T]):
:rtype: Model
"""
try:
get = await self.queryset.get(**kwargs)
get = await self.queryset.get(*args, **kwargs)
except ormar.NoMatch:
return None
@ -318,7 +318,7 @@ class QuerysetProxy(Generic[T]):
self._register_related(get)
return get
async def get(self, **kwargs: Any) -> "T":
async def get(self, *args, **kwargs: Any) -> "T":
"""
Get's the first row from the db meeting the criteria set by kwargs.
@ -337,12 +337,12 @@ class QuerysetProxy(Generic[T]):
:return: returned model
:rtype: Model
"""
get = await self.queryset.get(**kwargs)
get = await self.queryset.get(*args, **kwargs)
self._clean_items_on_load()
self._register_related(get)
return get
async def all(self, **kwargs: Any) -> List[Optional["T"]]: # noqa: A003
async def all(self, *args, **kwargs: Any) -> List[Optional["T"]]: # noqa: A003
"""
Returns all rows from a database for given model for set filter options.
@ -359,7 +359,7 @@ class QuerysetProxy(Generic[T]):
:return: list of returned models
:rtype: List[Model]
"""
all_items = await self.queryset.all(**kwargs)
all_items = await self.queryset.all(*args, **kwargs)
self._clean_items_on_load()
self._register_related(all_items)
return all_items
@ -425,7 +425,7 @@ class QuerysetProxy(Generic[T]):
)
return len(children)
async def get_or_create(self, **kwargs: Any) -> "T":
async def get_or_create(self, *args, **kwargs: Any) -> "T":
"""
Combination of create and get methods.
@ -439,7 +439,7 @@ class QuerysetProxy(Generic[T]):
:rtype: Model
"""
try:
return await self.get(**kwargs)
return await self.get(*args, **kwargs)
except ormar.NoMatch:
return await self.create(**kwargs)
@ -739,7 +739,7 @@ class QuerysetProxy(Generic[T]):
relation=self.relation, type_=self.type_, to=self.to, qryset=queryset
)
def order_by(self, columns: Union[List, str]) -> "QuerysetProxy[T]":
def order_by(self, columns: Union[List, str, "OrderAction"]) -> "QuerysetProxy[T]":
"""
With `order_by()` you can order the results from database based on your
choice of fields.