Skip to content

Commit

Permalink
feat(api): add python optional feature (#558)
Browse files Browse the repository at this point in the history
This lets both stacrs and pgstacrs use it without pgstacrs having to
depend on stacrs.
  • Loading branch information
gadomski authored Dec 6, 2024
1 parent ab57715 commit c4a4706
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 0 deletions.
3 changes: 3 additions & 0 deletions crates/api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ client = [
"stac-types/reqwest",
]
geo = ["dep:geo", "stac/geo"]
python = ["dep:pyo3", "dep:pythonize"]

[dependencies]
async-stream = { workspace = true, optional = true }
Expand All @@ -37,6 +38,8 @@ serde_urlencoded.workspace = true
stac.workspace = true
stac-derive.workspace = true
stac-types.workspace = true
pyo3 = { workspace = true, optional = true }
pythonize = { workspace = true, optional = true }
thiserror.workspace = true
tracing.workspace = true
tokio = { workspace = true, optional = true }
Expand Down
2 changes: 2 additions & 0 deletions crates/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ mod fields;
mod filter;
mod item_collection;
mod items;
#[cfg(feature = "python")]
pub mod python;
mod root;
mod search;
mod sort;
Expand Down
130 changes: 130 additions & 0 deletions crates/api/src/python.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//! Functions for convert [pyo3] objects into **stac-api** structures.
use crate::{Error, Fields, Filter, Items, Search, Sortby};
use geojson::Geometry;
use pyo3::{
exceptions::{PyException, PyValueError},
types::PyDict,
Bound, FromPyObject, PyErr, PyResult,
};
use stac::Bbox;

/// Creates a [Search] from Python arguments.
#[allow(clippy::too_many_arguments)]
pub fn search<'py>(
intersects: Option<StringOrDict<'py>>,
ids: Option<StringOrList>,
collections: Option<StringOrList>,
limit: Option<u64>,
bbox: Option<Vec<f64>>,
datetime: Option<String>,
include: Option<StringOrList>,
exclude: Option<StringOrList>,
sortby: Option<StringOrList>,
filter: Option<StringOrDict<'py>>,
query: Option<Bound<'py, PyDict>>,
) -> PyResult<Search> {
let mut fields = Fields::default();
if let Some(include) = include {
fields.include = include.into();
}
if let Some(exclude) = exclude {
fields.exclude = exclude.into();
}
let fields = if fields.include.is_empty() && fields.exclude.is_empty() {
None
} else {
Some(fields)
};
let query = query
.map(|query| pythonize::depythonize(&query))
.transpose()?;
let bbox = bbox.map(Bbox::try_from).transpose().map_err(Error::from)?;
let sortby = sortby.map(|sortby| {
Vec::<String>::from(sortby)
.into_iter()
.map(|s| s.parse::<Sortby>().unwrap()) // the parse is infallible
.collect::<Vec<_>>()
});
let filter = filter
.map(|filter| match filter {
StringOrDict::Dict(cql_json) => pythonize::depythonize(&cql_json).map(Filter::Cql2Json),
StringOrDict::String(cql2_text) => Ok(Filter::Cql2Text(cql2_text)),
})
.transpose()?;
let filter = filter
.map(|filter| filter.into_cql2_json())
.transpose()
.map_err(Error::from)?;
let items = Items {
limit,
bbox,
datetime,
query,
fields,
sortby,
filter,
..Default::default()
};

let intersects = intersects
.map(|intersects| match intersects {
StringOrDict::Dict(json) => pythonize::depythonize(&json)
.map_err(PyErr::from)
.and_then(|json| {
Geometry::from_json_object(json)
.map_err(|err| PyValueError::new_err(err.to_string()))
}),
StringOrDict::String(s) => s
.parse::<Geometry>()
.map_err(|err| PyValueError::new_err(err.to_string())),
})
.transpose()?;
let ids = ids.map(|ids| ids.into());
let collections = collections.map(|ids| ids.into());
Ok(Search {
items,
intersects,
ids,
collections,
})
}

/// A string or dictionary.
///
/// Used for the CQL2 filter argument and for intersects.
#[derive(Debug, FromPyObject)]
pub enum StringOrDict<'py> {
/// Text
String(String),

/// Json
Dict(Bound<'py, PyDict>),
}

/// A string or a list.
///
/// Used for collections, ids, etc.
#[derive(Debug, FromPyObject)]
pub enum StringOrList {
/// A string.
String(String),

/// A list.
List(Vec<String>),
}

impl From<StringOrList> for Vec<String> {
fn from(value: StringOrList) -> Vec<String> {
match value {
StringOrList::List(list) => list,
StringOrList::String(s) => vec![s],
}
}
}

impl From<Error> for PyErr {
fn from(value: Error) -> Self {
PyException::new_err(value.to_string())
}
}
1 change: 1 addition & 0 deletions crates/pgstac/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added

- `Pgstac` trait ([#551](https://github.com/stac-utils/stac-rs/pull/551))
- `python` feature ([#558](https://github.com/stac-utils/stac-rs/pull/558))

### Changed

Expand Down

0 comments on commit c4a4706

Please sign in to comment.