Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
25 changes: 16 additions & 9 deletions pygeofilter/backends/cql2_json/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,17 @@ def comparison(self, node, *args):

@handle(ast.Between)
def between(self, node, lhs, low, high):
return {"op": "between", "args": [lhs, [low, high]]}
ret = {"op": "between", "args": [lhs, low, high]}
if node.not_:
ret = {"op": "not", "args": [ret]}
return ret

@handle(ast.Like)
def like(self, node, *subargs):
return {"op": "like", "args": [subargs[0], node.pattern]}
ret = {"op": "like", "args": [subargs[0], node.pattern]}
if node.not_:
ret = {"op": "not", "args": [ret]}
return ret

@handle(ast.IsNull)
def isnull(self, node, arg):
Expand All @@ -84,16 +90,17 @@ def isnull(self, node, arg):
def function(self, node, *args):
name = node.name.lower()
if name == "lower":
ret = {"lower": args[0]}
elif name == "upper":
ret = {"upper": args[0]}
else:
ret = {"function": name, "args": [*args]}
return ret
return {"op": "casei", "args": [args[0]]}
elif name == "accenti":
return {"op": "accenti", "args": [args[0]]}
return {"op": node.name, "args": [*args]}

@handle(ast.In)
def in_(self, node, lhs, *options):
return {"op": "in", "args": [lhs, options]}
ret = {"op": "in", "args": [lhs, list(options)]}
if node.not_:
ret = {"op": "not", "args": [ret]}
return ret

@handle(ast.Attribute)
def attribute(self, node: ast.Attribute):
Expand Down
15 changes: 12 additions & 3 deletions pygeofilter/parsers/cql2_json/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ def walk_cql_json(node: JsonType): # noqa: C901
elif op == "between":
return ast.Between(
cast(ast.Node, walk_cql_json(args[0])),
cast(ast.ScalarAstType, walk_cql_json(args[1][0])),
cast(ast.ScalarAstType, walk_cql_json(args[1][1])),
cast(ast.ScalarAstType, walk_cql_json(args[1])),
cast(ast.ScalarAstType, walk_cql_json(args[2])),
not_=False,
)

Expand All @@ -158,13 +158,22 @@ def walk_cql_json(node: JsonType): # noqa: C901
not_=False,
)

elif op == "casei":
elif op in ("casei", "lower"):
return ast.Function("lower", [cast(ast.Node, walk_cql_json(args[0]))])

elif op == "accenti":
return ast.Function("accenti", [cast(ast.Node, walk_cql_json(args[0]))])

elif op in BINARY_OP_PREDICATES_MAP:
args = [cast(ast.Node, walk_cql_json(arg)) for arg in args]
return BINARY_OP_PREDICATES_MAP[op](*args)

else:
return ast.Function(
op,
[walk_cql_json(arg) for arg in args],
)

raise ValueError(f"Unable to parse expression node {node!r}")


Expand Down
4 changes: 2 additions & 2 deletions tests/parsers/cql2_json/fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
},
"Example 10": {
"text": "filter=eo:cloud_cover BETWEEN 0 AND 50",
"json": "{\"filter-lang\": \"cql2-json\", \"filter\": {\"op\": \"between\", \"args\": [{\"property\": \"eo:cloud_cover\"}, [0, 50]]}}"
"json": "{\"filter-lang\": \"cql2-json\", \"filter\": {\"op\": \"between\", \"args\": [{\"property\": \"eo:cloud_cover\"}, 0, 50]}}"
},
"Example 11": {
"text": "filter=mission LIKE 'sentinel%'",
Expand All @@ -47,4 +47,4 @@
"text": "filter=CASEI(provider) = 'coolsat'",
"json": "{\"filter-lang\": \"cql2-json\", \"filter\": {\"op\": \"=\", \"args\": [{\"lower\": {\"property\": \"provider\"}}, \"coolsat\"]}}"
}
}
}
68 changes: 66 additions & 2 deletions tests/parsers/cql2_json/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from pygeoif import geometry

from pygeofilter import ast, values
from pygeofilter.backends.cql2_json.evaluate import to_cql2
from pygeofilter.parsers.cql2_json import parse


Expand Down Expand Up @@ -82,7 +83,7 @@ def test_attribute_gte_literal():


def test_attribute_between():
result = parse({"op": "between", "args": [{"property": "attr"}, [2, 5]]})
result = parse({"op": "between", "args": [{"property": "attr"}, 2, 5]})
assert result == ast.Between(
ast.Attribute("attr"),
2,
Expand All @@ -92,7 +93,7 @@ def test_attribute_between():


def test_attribute_between_negative_positive():
result = parse({"op": "between", "args": [{"property": "attr"}, [-1, 1]]})
result = parse({"op": "between", "args": [{"property": "attr"}, -1, 1]})
assert result == ast.Between(
ast.Attribute("attr"),
-1,
Expand Down Expand Up @@ -717,3 +718,66 @@ def test_function_attr_string_arg():
],
),
)


# --- CQL2 Advanced Comparison conformance tests ---

def test_between_flat_args_parse():
result = parse({"op": "between", "args": [{"property": "attr"}, 2, 5]})
assert result == ast.Between(ast.Attribute("attr"), 2, 5, False)


def test_between_encode_flat_args():

node = ast.Between(ast.Attribute("attr"), 2, 5, False)
decoded = json.loads(to_cql2(node))
assert decoded == {"op": "between", "args": [{"property": "attr"}, 2, 5]}


def test_not_between_encodes_with_not_wrapper():

node = ast.Between(ast.Attribute("attr"), 2, 5, not_=True)
decoded = json.loads(to_cql2(node))
assert decoded["op"] == "not"
assert decoded["args"][0]["op"] == "between"


def test_not_like_encodes_with_not_wrapper():

node = ast.Like(ast.Attribute("attr"), "val%", nocase=False, not_=True,
wildcard="%", singlechar=".", escapechar="\\")
decoded = json.loads(to_cql2(node))
assert decoded["op"] == "not"
assert decoded["args"][0]["op"] == "like"


def test_not_in_encodes_with_not_wrapper():

node = ast.In(ast.Attribute("attr"), [1, 2, 3], not_=True)
decoded = json.loads(to_cql2(node))
assert decoded["op"] == "not"
assert decoded["args"][0]["op"] == "in"


def test_casei_json_parse():
result = parse({"op": "casei", "args": [{"property": "name"}]})
assert result == ast.Function("lower", [ast.Attribute("name")])


def test_casei_json_encode():

node = ast.Function("lower", [ast.Attribute("name")])
decoded = json.loads(to_cql2(node))
assert decoded == {"op": "casei", "args": [{"property": "name"}]}


def test_accenti_json_parse():
result = parse({"op": "accenti", "args": [{"property": "name"}]})
assert result == ast.Function("accenti", [ast.Attribute("name")])


def test_accenti_json_encode():

node = ast.Function("accenti", [ast.Attribute("name")])
decoded = json.loads(to_cql2(node))
assert decoded == {"op": "accenti", "args": [{"property": "name"}]}
Loading