Parse dashboard operators independently of quoted filter values
This commit is contained in:
parent
eba404bc96
commit
1fda2d6fc0
2 changed files with 60 additions and 19 deletions
|
|
@ -1,23 +1,32 @@
|
||||||
from typing import Optional, Union
|
import re
|
||||||
|
from typing import Optional, Union, cast
|
||||||
|
|
||||||
from ....views import Filter, operators
|
from ....views import Filter
|
||||||
|
from ....views.operators import Operator
|
||||||
|
|
||||||
|
_expression = re.compile(
|
||||||
|
r"\s*\{([^}]+)\}\s*(>=|<=|!=|=|>|<|contains\b|ge\b|le\b|lt\b|gt\b|ne\b|eq\b)\s*(.*?)\s*",
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
_aliases = {"ge": ">=", "le": "<=", "lt": "<", "gt": ">", "ne": "!=", "eq": "="}
|
||||||
|
|
||||||
|
|
||||||
def get_filter_from_datatable(description: str) -> Optional[Filter]:
|
def get_filter_from_datatable(description: str) -> Optional[Filter]:
|
||||||
for operator in operators:
|
match = _expression.fullmatch(description)
|
||||||
if operator in description:
|
if match is None:
|
||||||
name_part, value_part = description.split(operator, 1)
|
return None
|
||||||
value_part = value_part.strip()
|
name, token, raw = match.groups()
|
||||||
name_part = name_part[name_part.find("{") + 1 : name_part.rfind("}")]
|
if not raw:
|
||||||
|
return None
|
||||||
v0 = value_part[0]
|
if raw[0] in ("'", '"', "`"):
|
||||||
if v0 == value_part[-1] and v0 in ("'", '"', "`"):
|
if len(raw) < 2 or raw[-1] != raw[0]:
|
||||||
value: Union[str, float] = value_part[1:-1].replace("\\" + v0, v0)
|
return None
|
||||||
else:
|
value: Union[str, float] = re.sub(r"\\([\\'\"`])", r"\1", raw[1:-1])
|
||||||
try:
|
else:
|
||||||
value = float(value_part)
|
try:
|
||||||
except ValueError:
|
value = float(raw)
|
||||||
value = value_part
|
except ValueError:
|
||||||
return Filter(property=name_part, operator=operator, value=value)
|
value = raw
|
||||||
|
return Filter(
|
||||||
return None
|
property=name, operator=cast(Operator, _aliases.get(token, token)), value=value
|
||||||
|
)
|
||||||
|
|
|
||||||
32
tests/test_dashboard_regressions.py
Normal file
32
tests/test_dashboard_regressions.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from great_ai.deploy.routes.dashboard.get_filter_from_datatable import (
|
||||||
|
get_filter_from_datatable,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"expression, operator, value",
|
||||||
|
[
|
||||||
|
('{value} contains "a=b"', "contains", "a=b"),
|
||||||
|
("{value} eq 7", "=", 7),
|
||||||
|
("{value} ge 2.5", ">=", 2.5),
|
||||||
|
('{value} contains ""', "contains", ""),
|
||||||
|
(r"{value} contains 'it\'s > 2'", "contains", "it's > 2"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_dashboard_filter_grammar(expression: str, operator: str, value: Any) -> None:
|
||||||
|
result = get_filter_from_datatable(expression)
|
||||||
|
assert result is not None
|
||||||
|
assert result.property == "value"
|
||||||
|
assert result.operator == operator
|
||||||
|
assert result.value == value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"expression", ["{value} > ", '{value} contains "unfinished', "invalid"]
|
||||||
|
)
|
||||||
|
def test_incomplete_filters_are_ignored(expression: str) -> None:
|
||||||
|
assert get_filter_from_datatable(expression) is None
|
||||||
Loading…
Reference in a new issue