Skip to content

Commit a39b3f2

Browse files
authored
feat(py): Expose Expr.matches to Python (#76)
1 parent 281179a commit a39b3f2

File tree

6 files changed

+75
-11
lines changed

6 files changed

+75
-11
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cql2.pyi

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ class Expr:
7777
>>> expr.validate()
7878
"""
7979

80+
def matches(self, item: dict[str, Any]) -> bool:
81+
"""Matches this expression against an item.
82+
83+
Args:
84+
item (dict[str, Any]): The item to match against
85+
86+
Returns:
87+
bool: True if the expression matches the item, False otherwise
88+
"""
89+
8090
def to_json(self) -> dict[str, Any]:
8191
"""Converts this cql2 expression to a cql2-json dictionary.
8292

python/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ cql2 = { path = ".." }
2121
cql2-cli = { path = "../cli" }
2222
pyo3 = { version = "0.23.3", features = ["extension-module"] }
2323
pythonize = "0.23.0"
24+
serde_json = "1.0.138"

python/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use pyo3::{
44
create_exception,
55
exceptions::{PyException, PyIOError, PyValueError},
66
prelude::*,
7+
types::PyDict,
78
};
9+
use serde_json::Value;
810
use std::path::PathBuf;
911

1012
create_exception!(cql2, ValidationError, PyException);
@@ -74,6 +76,11 @@ impl Expr {
7476
}
7577
}
7678

79+
fn matches(&self, item: Bound<'_, PyDict>) -> Result<bool> {
80+
let value: Value = pythonize::depythonize(&item)?;
81+
self.0.clone().matches(Some(&value)).map_err(Error::from)
82+
}
83+
7784
fn to_json<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>> {
7885
pythonize::pythonize(py, &self.0).map_err(Error::from)
7986
}

python/tests/test_expr.py

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import cql2
66
import pytest
7-
from cql2 import Expr, ParseError, ValidationError
87

98

109
def test_parse_file(fixtures: Path) -> None:
@@ -16,52 +15,95 @@ def test_parse_file_str(fixtures: Path) -> None:
1615

1716

1817
def test_init(example01_text: str) -> None:
19-
Expr(example01_text)
18+
cql2.Expr(example01_text)
2019

2120

2221
def test_parse_json(example01_text: str, example01_json: dict[str, Any]) -> None:
2322
cql2.parse_json(json.dumps(example01_json))
24-
with pytest.raises(ParseError):
23+
with pytest.raises(cql2.ParseError):
2524
cql2.parse_json(example01_text)
2625

2726

2827
def test_parse_text(example01_text: str, example01_json: dict[str, Any]) -> None:
2928
cql2.parse_text(example01_text)
30-
with pytest.raises(ParseError):
29+
with pytest.raises(cql2.ParseError):
3130
cql2.parse_text(json.dumps(example01_json))
3231

3332

3433
def test_to_json(example01_text: str) -> None:
35-
Expr(example01_text).to_json() == {
34+
cql2.Expr(example01_text).to_json() == {
3635
"op": "=",
3736
"args": [{"property": "landsat:scene_id"}, "LC82030282019133LGN00"],
3837
}
3938

4039

4140
def test_to_text(example01_json: dict[str, Any]) -> None:
42-
Expr(example01_json).to_text() == "landsat:scene_id = 'LC82030282019133LGN00'"
41+
cql2.Expr(example01_json).to_text() == "landsat:scene_id = 'LC82030282019133LGN00'"
4342

4443

4544
def test_to_sql(example01_text: str) -> None:
46-
sql_query = Expr(example01_text).to_sql()
45+
sql_query = cql2.Expr(example01_text).to_sql()
4746
assert sql_query.query == '("landsat:scene_id" = $1)'
4847
assert sql_query.params == ["LC82030282019133LGN00"]
4948

5049

5150
def test_validate() -> None:
52-
expr = Expr(
51+
expr = cql2.Expr(
5352
{
5453
"op": "t_before",
5554
"args": [{"property": "updated_at"}, {"timestamp": "invalid-timestamp"}],
5655
}
5756
)
58-
with pytest.raises(ValidationError):
57+
with pytest.raises(cql2.ValidationError):
5958
expr.validate()
6059

6160

6261
def test_add() -> None:
63-
assert Expr("True") + Expr("false") == Expr("true AND false")
62+
assert cql2.Expr("True") + cql2.Expr("false") == cql2.Expr("true AND false")
6463

6564

6665
def test_eq() -> None:
67-
assert Expr("True") == Expr("true")
66+
assert cql2.Expr("True") == cql2.Expr("true")
67+
68+
69+
@pytest.mark.parametrize(
70+
"expr, item, should_match",
71+
[
72+
pytest.param(
73+
"boolfield and 1 + 2 = 3",
74+
{
75+
"properties": {
76+
"eo:cloud_cover": 10,
77+
"datetime": "2020-01-01 00:00:00Z",
78+
"boolfield": True,
79+
}
80+
},
81+
True,
82+
id="pass on bool & cql2 arithmetic",
83+
),
84+
pytest.param(
85+
"eo:cloud_cover <= 9",
86+
{
87+
"properties": {
88+
"eo:cloud_cover": 10,
89+
"datetime": "2020-01-01 00:00:00Z",
90+
},
91+
},
92+
False,
93+
id="fail on property value comparison",
94+
),
95+
pytest.param(
96+
"eo:cloud_cover <= 9",
97+
{
98+
"properties": {
99+
"eo:cloud_cover": 8,
100+
"datetime": "2020-01-01 00:00:00Z",
101+
},
102+
},
103+
True,
104+
id="pass on property value comparison",
105+
),
106+
],
107+
)
108+
def test_matches(expr, item, should_match) -> None:
109+
assert cql2.Expr(expr).matches(item) == should_match

src/expr.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,9 @@ impl Expr {
353353
///
354354
/// let mut expr: Expr = "boolfield and 1 + 2 = 3".parse().unwrap();
355355
/// assert_eq!(true, expr.matches(Some(&item)).unwrap());
356+
///
357+
/// let mut expr: Expr = "eo:cloud_cover <= 9".parse().unwrap();
358+
/// assert_eq!(false, expr.matches(Some(&item)).unwrap());
356359
/// ```
357360
pub fn matches(self, j: Option<&Value>) -> Result<bool, Error> {
358361
let reduced = self.reduce(j)?;

0 commit comments

Comments
 (0)