Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validate to Python API #37

Merged
merged 9 commits into from
Oct 10, 2024
Merged
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added

- Python bindings 🐍 ([#30](https://github.com/developmentseed/cql2-rs/pull/30))
- Docs ([#36](https://github.com/developmentseed/cql2-rs/pull/36))

### Changed

- `SqlQuery` attributes are now public ([#30](https://github.com/developmentseed/cql2-rs/pull/30))
- `Expr::to_json`, `Expr::to_json_pretty`, and `Expr::to_value` now return `Error` instead of `serde_json::Error` ([#37](https://github.com/developmentseed/cql2-rs/pull/37))

## [0.1.0] - 2024-10-08

Initial release.
Expand Down
14 changes: 0 additions & 14 deletions cli/CHANGELOG.md

This file was deleted.

9 changes: 7 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@

## Installation

Install [Rust](https://rustup.rs/).
Then:
Install from [PyPI](https://pypi.org/project/cql2/):

```shell
pip install cql2
```

Or, if you have [Rust](https://rustup.rs/):

```shell
cargo install cql2-cli
Expand Down
1 change: 1 addition & 0 deletions docs/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ python -m pip install cql2

::: cql2.Expr
::: cql2.SqlQuery
::: cql2.ValidationError
15 changes: 15 additions & 0 deletions python/cql2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ class Expr:
>>> expr = Expr({"op":"=","args":[{"property":"landsat:scene_id"},"LC82030282019133LGN00"]})
"""

def validate(self) -> None:
"""Validates this expression using json-schema.

Raises:
ValidationError: Raised if the validation fails

Examples:
>>> from cql2 import Expr
>>> expr = Expr("landsat:scene_id = 'LC82030282019133LGN00'")
>>> expr.validate()
"""

def to_json(self) -> dict[str, Any]:
"""Converts this cql2 expression to a cql2-json dictionary.

Expand Down Expand Up @@ -81,3 +93,6 @@ class Expr:
>>> q.params
['LC82030282019133LGN00']
"""

class ValidationError(Exception):
"""An error raised when cql2 json-schema validation fails."""
83 changes: 61 additions & 22 deletions python/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
use pyo3::{
create_exception,
exceptions::{PyException, PyIOError, PyValueError},
prelude::*,
};
use std::path::PathBuf;

create_exception!(cql2, ValidationError, PyException);

/// Crate-specific error enum.
enum Error {
Cql2(::cql2::Error),
Pythonize(pythonize::PythonizeError),
}

/// Crate specific result type.
type Result<T> = std::result::Result<T, Error>;

/// A CQL2 expression.
#[pyclass]
struct Expr(::cql2::Expr);
Expand All @@ -22,34 +34,43 @@ struct SqlQuery {
impl Expr {
/// Parses a CQL2 expression from a file path.
#[staticmethod]
fn from_path(path: PathBuf) -> PyResult<Expr> {
::cql2::parse_file(path).map(Expr).map_err(to_py_error)
fn from_path(path: PathBuf) -> Result<Expr> {
::cql2::parse_file(path).map(Expr).map_err(Error::from)
}

/// Parses a CQL2 expression.
#[new]
fn new(cql2: Bound<'_, PyAny>) -> PyResult<Self> {
fn new(cql2: Bound<'_, PyAny>) -> Result<Self> {
if let Ok(s) = cql2.extract::<&str>() {
s.parse().map(Expr).map_err(to_py_error)
s.parse().map(Expr).map_err(Error::from)
} else {
let expr: ::cql2::Expr = pythonize::depythonize(&cql2)?;
Ok(Expr(expr))
}
}

fn validate(&self) -> PyResult<()> {
let validator = ::cql2::Validator::new().map_err(Error::from)?;
if let Err(error) = validator.validate(&self.0.to_value().map_err(Error::from)?) {
Err(ValidationError::new_err(error.to_string()))
} else {
Ok(())
}
}

/// Converts this expression to a cql2-json dictionary.
fn to_json<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
pythonize::pythonize(py, &self.0).map_err(PyErr::from)
fn to_json<'py>(&self, py: Python<'py>) -> Result<Bound<'py, PyAny>> {
pythonize::pythonize(py, &self.0).map_err(Error::from)
}

/// Converts this expression to cql2-text.
fn to_text(&self) -> PyResult<String> {
self.0.to_text().map_err(to_py_error)
fn to_text(&self) -> Result<String> {
self.0.to_text().map_err(Error::from)
}

/// Converts this expression to SQL query.
fn to_sql(&self) -> PyResult<SqlQuery> {
self.0.to_sql().map(SqlQuery::from).map_err(to_py_error)
fn to_sql(&self) -> Result<SqlQuery> {
self.0.to_sql().map(SqlQuery::from).map_err(Error::from)
}
}

Expand All @@ -62,24 +83,42 @@ impl From<::cql2::SqlQuery> for SqlQuery {
}
}

fn to_py_error(error: ::cql2::Error) -> PyErr {
use ::cql2::Error::*;
match error {
InvalidCql2Text(..)
| InvalidNumberOfArguments { .. }
| MissingArgument(..)
| ParseBool(..)
| ParseFloat(..)
| ParseInt(..) => PyValueError::new_err(error.to_string()),
Io(err) => PyIOError::new_err(err.to_string()),
_ => PyException::new_err(error.to_string()),
impl From<Error> for PyErr {
fn from(error: Error) -> PyErr {
use ::cql2::Error::*;
match error {
Error::Cql2(error) => match error {
InvalidCql2Text(..)
| InvalidNumberOfArguments { .. }
| MissingArgument(..)
| ParseBool(..)
| ParseFloat(..)
| ParseInt(..) => PyValueError::new_err(error.to_string()),
Io(err) => PyIOError::new_err(err.to_string()),
_ => PyException::new_err(error.to_string()),
},
Error::Pythonize(error) => error.into(),
}
}
}

impl From<::cql2::Error> for Error {
fn from(value: ::cql2::Error) -> Self {
Error::Cql2(value)
}
}

impl From<pythonize::PythonizeError> for Error {
fn from(value: pythonize::PythonizeError) -> Self {
Error::Pythonize(value)
}
}

/// A Python module implemented in Rust.
#[pymodule]
fn cql2(m: &Bound<'_, PyModule>) -> PyResult<()> {
fn cql2(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Expr>()?;
m.add_class::<SqlQuery>()?;
m.add("ValidationError", py.get_type_bound::<ValidationError>())?;
Ok(())
}
14 changes: 13 additions & 1 deletion python/tests/test_expr.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pathlib import Path

from cql2 import Expr
import pytest
from cql2 import Expr, ValidationError


def test_from_path(fixtures: Path) -> None:
Expand Down Expand Up @@ -30,3 +31,14 @@ def test_to_sql(example01_text: str) -> None:
sql_query = Expr(example01_text).to_sql()
assert sql_query.query == '("landsat:scene_id" = $1)'
assert sql_query.params == ["LC82030282019133LGN00"]


def test_validate() -> None:
expr = Expr(
{
"op": "t_before",
"args": [{"property": "updated_at"}, {"timestamp": "invalid-timestamp"}],
}
)
with pytest.raises(ValidationError):
expr.validate()
12 changes: 6 additions & 6 deletions src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ impl Expr {
/// let expr = Expr::Bool(true);
/// let s = expr.to_json().unwrap();
/// ```
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(&self)
pub fn to_json(&self) -> Result<String, Error> {
serde_json::to_string(&self).map_err(Error::from)
}

/// Converts this expression to a pretty JSON string.
Expand All @@ -225,8 +225,8 @@ impl Expr {
/// let expr = Expr::Bool(true);
/// let s = expr.to_json_pretty().unwrap();
/// ```
pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(&self)
pub fn to_json_pretty(&self) -> Result<String, Error> {
serde_json::to_string_pretty(&self).map_err(Error::from)
}

/// Converts this expression to a [serde_json::Value].
Expand All @@ -239,8 +239,8 @@ impl Expr {
/// let expr = Expr::Bool(true);
/// let value = expr.to_value().unwrap();
/// ```
pub fn to_value(&self) -> Result<Value, serde_json::Error> {
serde_json::to_value(self)
pub fn to_value(&self) -> Result<Value, Error> {
serde_json::to_value(self).map_err(Error::from)
}

/// Returns true if this expression is valid CQL2.
Expand Down
3 changes: 2 additions & 1 deletion src/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ impl Validator {
/// validator.validate(&valid).unwrap();
///
/// let invalid = json!({
/// "op": "not an operator!",
/// "op": "t_before",
/// "args": [{"property": "updated_at"}, {"timestamp": "invalid-timestamp"}],
/// });
/// validator.validate(&invalid).unwrap_err();
/// ```
Expand Down
Loading