From 1fda2d6fc03b039bae2025e49e93981ee0d3d58c Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 27 Oct 2026 20:02:40 +0000 Subject: [PATCH] Parse dashboard operators independently of quoted filter values --- .../dashboard/get_filter_from_datatable.py | 47 +++++++++++-------- tests/test_dashboard_regressions.py | 32 +++++++++++++ 2 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 tests/test_dashboard_regressions.py diff --git a/great_ai/deploy/routes/dashboard/get_filter_from_datatable.py b/great_ai/deploy/routes/dashboard/get_filter_from_datatable.py index 8bb08e5..c1d9345 100644 --- a/great_ai/deploy/routes/dashboard/get_filter_from_datatable.py +++ b/great_ai/deploy/routes/dashboard/get_filter_from_datatable.py @@ -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]: - for operator in operators: - if operator in description: - name_part, value_part = description.split(operator, 1) - value_part = value_part.strip() - name_part = name_part[name_part.find("{") + 1 : name_part.rfind("}")] - - v0 = value_part[0] - if v0 == value_part[-1] and v0 in ("'", '"', "`"): - value: Union[str, float] = value_part[1:-1].replace("\\" + v0, v0) - else: - try: - value = float(value_part) - except ValueError: - value = value_part - return Filter(property=name_part, operator=operator, value=value) - - return None + match = _expression.fullmatch(description) + if match is None: + return None + name, token, raw = match.groups() + if not raw: + return None + if raw[0] in ("'", '"', "`"): + if len(raw) < 2 or raw[-1] != raw[0]: + return None + value: Union[str, float] = re.sub(r"\\([\\'\"`])", r"\1", raw[1:-1]) + else: + try: + value = float(raw) + except ValueError: + value = raw + return Filter( + property=name, operator=cast(Operator, _aliases.get(token, token)), value=value + ) diff --git a/tests/test_dashboard_regressions.py b/tests/test_dashboard_regressions.py new file mode 100644 index 0000000..a43ee8b --- /dev/null +++ b/tests/test_dashboard_regressions.py @@ -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