Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion py_rql/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class FilterLookups:
I_LIKE = 'ilike'
"""`Case-insensitive like` operator"""

RANGE = 'range'
"""`range` operator"""

@classmethod
def numeric(cls, with_null: bool = True) -> set:
"""
Expand All @@ -73,7 +76,7 @@ def numeric(cls, with_null: bool = True) -> set:
(set): a set with the default lookups.
"""
return cls._add_null(
{cls.EQ, cls.NE, cls.GE, cls.GT, cls.LT, cls.LE, cls.IN, cls.OUT}, with_null,
{cls.EQ, cls.NE, cls.GE, cls.GT, cls.LT, cls.LE, cls.IN, cls.OUT, cls.RANGE}, with_null,
)

@classmethod
Expand Down Expand Up @@ -122,6 +125,7 @@ class ComparisonOperators:
class ListOperators:
IN = 'in'
OUT = 'out'
RANGE = 'range'


class LogicalOperators:
Expand Down
4 changes: 4 additions & 0 deletions py_rql/grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
| searching
| ordering
| select
| range
| _L_BRACE expr_term _R_BRACE

logical: and_op
Expand Down Expand Up @@ -65,6 +66,8 @@
select: select_term _signed_props
_signed_props: _L_BRACE _R_BRACE
| _L_BRACE sign_prop (_COMMA sign_prop)* _R_BRACE

range: range_term _L_BRACE prop _COMMA val _COMMA val _R_BRACE

val: prop
| tuple
Expand All @@ -89,6 +92,7 @@
!search_term: "like" | "ilike"
!ordering_term: "ordering"
!select_term: "select"
!range_term: "range"


PROP: /[a-zA-Z]/ /[\w\-\.]/*
Expand Down
5 changes: 5 additions & 0 deletions py_rql/operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ def ilike(a, b):
return like(a.lower(), b.lower())


def range_op(a, b):
return a >= b[0] and a <= b[1]


def get_operator_func_by_operator(op):
mapping = {
ComparisonOperators.EQ: eq,
Expand All @@ -64,6 +68,7 @@ def get_operator_func_by_operator(op):
ComparisonOperators.LT: lt,
ListOperators.IN: in_op,
ListOperators.OUT: out_op,
ListOperators.RANGE: range_op,
f'{LogicalOperators.AND}_op': and_op,
f'{LogicalOperators.OR}_op': or_op,
f'{LogicalOperators.NOT}_op': not_op,
Expand Down
4 changes: 4 additions & 0 deletions py_rql/transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ def searching(self, args):
operation, prop, val = tuple(self._get_value(args[index]) for index in range(3))
return self._get_func_for_lookup(prop, operation, val)

def range(self, args):
operation, prop, *val = tuple(self._get_value(args[index]) for index in range(4))
return self._get_func_for_lookup(prop, operation, val)

def _get_func_for_lookup(self, prop, operation, val):
self.filter_cls.validate_lookup(prop, operation)

Expand Down
80 changes: 80 additions & 0 deletions tests/test_transformer/test_range.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#
# Copyright © 2023 Ingram Micro Inc. All rights reserved.
#
import pytest

from py_rql.cast import get_default_cast_func_for_type
from py_rql.constants import FilterTypes, ListOperators
from py_rql.helpers import apply_operator
from py_rql.operators import get_operator_func_by_operator


@pytest.mark.parametrize(
'value',
(
['10', '10.3'],
['10', '-10.3'],
),
)
@pytest.mark.parametrize('filter_type', (FilterTypes.DECIMAL, FilterTypes.FLOAT))
@pytest.mark.parametrize('op', (ListOperators.RANGE,))
def test_numeric(mocker, filter_factory, filter_type, op, value):
functools = mocker.patch('py_rql.transformer.functools')
flt = filter_factory([{'filter': 'prop', 'type': filter_type}])
query = f'{op}(prop,{",".join(value)})'
flt.filter(query, [])
cast_func = get_default_cast_func_for_type(filter_type)
functools.partial.assert_called_once_with(
apply_operator,
'prop',
get_operator_func_by_operator(op),
[cast_func(v) for v in value],
)


@pytest.mark.parametrize('value', (['10', '-103'],))
@pytest.mark.parametrize('op', (ListOperators.RANGE,))
def test_numeric_int(mocker, filter_factory, op, value):
functools = mocker.patch('py_rql.transformer.functools')
flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.INT}])
query = f'{op}(prop,{",".join(value)})'
flt.filter(query, [])
cast_func = get_default_cast_func_for_type(FilterTypes.INT)
functools.partial.assert_called_once_with(
apply_operator,
'prop',
get_operator_func_by_operator(op),
[cast_func(v) for v in value],
)


@pytest.mark.parametrize('value', (['2020-01-01', '1932-03-31'],))
@pytest.mark.parametrize('op', (ListOperators.RANGE,))
def test_numeric_date(mocker, filter_factory, op, value):
functools = mocker.patch('py_rql.transformer.functools')
flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.DATE}])
query = f'{op}(prop,{",".join(value)})'
flt.filter(query, [])
cast_func = get_default_cast_func_for_type(FilterTypes.DATE)
functools.partial.assert_called_once_with(
apply_operator,
'prop',
get_operator_func_by_operator(op),
[cast_func(v) for v in value],
)


@pytest.mark.parametrize('value', (['2022-02-08T07:57:57+01:00', '2022-02-08T07:57:57'],))
@pytest.mark.parametrize('op', (ListOperators.RANGE,))
def test_numeric_datetime(mocker, filter_factory, op, value):
functools = mocker.patch('py_rql.transformer.functools')
flt = filter_factory([{'filter': 'prop', 'type': FilterTypes.DATETIME}])
query = f'{op}(prop,{",".join(value)})'
flt.filter(query, [])
cast_func = get_default_cast_func_for_type(FilterTypes.DATETIME)
functools.partial.assert_called_once_with(
apply_operator,
'prop',
get_operator_func_by_operator(op),
[cast_func(v) for v in value],
)