Skip to content

Commit 87cbdec

Browse files
authored
feat: move in search (#81)
1 parent d968382 commit 87cbdec

File tree

7 files changed

+219
-37
lines changed

7 files changed

+219
-37
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- Construct `stac_api::Search` (moved from `stac_api` crate) ([#81](https://github.com/stac-utils/stacrs/pull/81))
12+
913
### Fixed
1014

1115
- Swallow broken pipe errors ([#73](https://github.com/stac-utils/stacrs/pull/73))

Cargo.lock

Lines changed: 9 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ stac = { features = [
3030
], git = "https://github.com/stac-utils/stac-rs", branch = "main" }
3131
stac-api = { features = [
3232
"client",
33-
"python",
3433
], git = "https://github.com/stac-utils/stac-rs", branch = "main" }
3534
stac-cli = { git = "https://github.com/stac-utils/stac-rs", features = [
3635
"pgstac",

src/duckdb.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
use crate::Result;
1+
use crate::{
2+
search::{PySortby, StringOrDict, StringOrList},
3+
Result,
4+
};
25
use pyo3::{
36
exceptions::PyException,
47
prelude::*,
58
types::{PyDict, PyList},
69
IntoPyObjectExt,
710
};
811
use pyo3_arrow::PyTable;
9-
use stac_api::python::{StringOrDict, StringOrList};
1012
use stac_duckdb::{Client, Config};
1113
use std::sync::Mutex;
1214

@@ -16,11 +18,24 @@ pub struct DuckdbClient(Mutex<Client>);
1618
#[pymethods]
1719
impl DuckdbClient {
1820
#[new]
19-
#[pyo3(signature = (use_s3_credential_chain=true, use_hive_partitioning=false))]
20-
fn new(use_s3_credential_chain: bool, use_hive_partitioning: bool) -> Result<DuckdbClient> {
21+
#[pyo3(signature = (*, use_s3_credential_chain=true, use_azure_credential_chain=true, use_httpfs=true, use_hive_partitioning=false, install_extensions=true, custom_extension_repository=None, extension_directory=None))]
22+
fn new(
23+
use_s3_credential_chain: bool,
24+
use_azure_credential_chain: bool,
25+
use_httpfs: bool,
26+
use_hive_partitioning: bool,
27+
install_extensions: bool,
28+
custom_extension_repository: Option<String>,
29+
extension_directory: Option<String>,
30+
) -> Result<DuckdbClient> {
2131
let config = Config {
2232
use_s3_credential_chain,
33+
use_azure_credential_chain,
34+
use_httpfs,
2335
use_hive_partitioning,
36+
install_extensions,
37+
custom_extension_repository,
38+
extension_directory,
2439
convert_wkb: true,
2540
};
2641
let client = Client::with_config(config)?;
@@ -40,12 +55,12 @@ impl DuckdbClient {
4055
datetime: Option<String>,
4156
include: Option<StringOrList>,
4257
exclude: Option<StringOrList>,
43-
sortby: Option<StringOrList>,
58+
sortby: Option<PySortby<'py>>,
4459
filter: Option<StringOrDict>,
4560
query: Option<Bound<'py, PyDict>>,
4661
kwargs: Option<Bound<'py, PyDict>>,
4762
) -> Result<Bound<'py, PyDict>> {
48-
let search = stac_api::python::search(
63+
let search = crate::search::build(
4964
intersects,
5065
ids,
5166
collections,
@@ -84,12 +99,12 @@ impl DuckdbClient {
8499
datetime: Option<String>,
85100
include: Option<StringOrList>,
86101
exclude: Option<StringOrList>,
87-
sortby: Option<StringOrList>,
102+
sortby: Option<PySortby<'py>>,
88103
filter: Option<StringOrDict>,
89104
query: Option<Bound<'py, PyDict>>,
90105
kwargs: Option<Bound<'py, PyDict>>,
91106
) -> Result<PyObject> {
92-
let search = stac_api::python::search(
107+
let search = crate::search::build(
93108
intersects,
94109
ids,
95110
collections,

src/search.rs

Lines changed: 150 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use crate::{Error, Json, Result};
2-
use pyo3::{prelude::*, types::PyDict};
2+
use geojson::Geometry;
3+
use pyo3::prelude::*;
4+
use pyo3::{exceptions::PyValueError, types::PyDict, Bound, FromPyObject, PyErr, PyResult};
5+
use stac::Bbox;
36
use stac::Format;
4-
use stac_api::{
5-
python::{StringOrDict, StringOrList},
6-
Search,
7-
};
7+
use stac_api::{Fields, Filter, Items, Search, Sortby};
88

99
#[pyfunction]
1010
#[pyo3(signature = (href, *, intersects=None, ids=None, collections=None, max_items=None, limit=None, bbox=None, datetime=None, include=None, exclude=None, sortby=None, filter=None, query=None, use_duckdb=None, **kwargs))]
@@ -21,13 +21,13 @@ pub fn search<'py>(
2121
datetime: Option<String>,
2222
include: Option<StringOrList>,
2323
exclude: Option<StringOrList>,
24-
sortby: Option<StringOrList>,
24+
sortby: Option<PySortby<'py>>,
2525
filter: Option<StringOrDict>,
2626
query: Option<Bound<'py, PyDict>>,
2727
use_duckdb: Option<bool>,
2828
kwargs: Option<Bound<'_, PyDict>>,
2929
) -> PyResult<Bound<'py, PyAny>> {
30-
let search = stac_api::python::search(
30+
let search = build(
3131
intersects,
3232
ids,
3333
collections,
@@ -72,15 +72,15 @@ pub fn search_to<'py>(
7272
datetime: Option<String>,
7373
include: Option<StringOrList>,
7474
exclude: Option<StringOrList>,
75-
sortby: Option<StringOrList>,
75+
sortby: Option<PySortby<'py>>,
7676
filter: Option<StringOrDict>,
7777
query: Option<Bound<'py, PyDict>>,
7878
format: Option<String>,
7979
options: Option<Vec<(String, String)>>,
8080
use_duckdb: Option<bool>,
8181
kwargs: Option<Bound<'_, PyDict>>,
8282
) -> PyResult<Bound<'py, PyAny>> {
83-
let search = stac_api::python::search(
83+
let search = build(
8484
intersects,
8585
ids,
8686
collections,
@@ -150,3 +150,144 @@ async fn search_api(
150150
let value = stac_api::client::search(&href, search, max_items).await?;
151151
Ok(value)
152152
}
153+
154+
/// Creates a [Search] from Python arguments.
155+
#[allow(clippy::too_many_arguments)]
156+
pub fn build<'py>(
157+
intersects: Option<StringOrDict<'py>>,
158+
ids: Option<StringOrList>,
159+
collections: Option<StringOrList>,
160+
limit: Option<u64>,
161+
bbox: Option<Vec<f64>>,
162+
datetime: Option<String>,
163+
include: Option<StringOrList>,
164+
exclude: Option<StringOrList>,
165+
sortby: Option<PySortby<'py>>,
166+
filter: Option<StringOrDict<'py>>,
167+
query: Option<Bound<'py, PyDict>>,
168+
kwargs: Option<Bound<'py, PyDict>>,
169+
) -> PyResult<Search> {
170+
let mut fields = Fields::default();
171+
if let Some(include) = include {
172+
fields.include = include.into();
173+
}
174+
if let Some(exclude) = exclude {
175+
fields.exclude = exclude.into();
176+
}
177+
let fields = if fields.include.is_empty() && fields.exclude.is_empty() {
178+
None
179+
} else {
180+
Some(fields)
181+
};
182+
let query = query
183+
.map(|query| pythonize::depythonize(&query))
184+
.transpose()?;
185+
let bbox = bbox.map(Bbox::try_from).transpose().map_err(Error::from)?;
186+
let sortby: Vec<Sortby> = sortby
187+
.map(|sortby| match sortby {
188+
PySortby::ListOfDicts(list) => list
189+
.into_iter()
190+
.map(|d| pythonize::depythonize(&d).map_err(Error::from))
191+
.collect::<Result<Vec<_>>>(),
192+
PySortby::ListOfStrings(list) => list
193+
.into_iter()
194+
.map(|s| Ok(s.parse().unwrap())) // infallible
195+
.collect::<Result<Vec<_>>>(),
196+
PySortby::String(s) => Ok(vec![s.parse().unwrap()]),
197+
})
198+
.transpose()?
199+
.unwrap_or_default();
200+
let filter = filter
201+
.map(|filter| match filter {
202+
StringOrDict::Dict(cql_json) => pythonize::depythonize(&cql_json).map(Filter::Cql2Json),
203+
StringOrDict::String(cql2_text) => Ok(Filter::Cql2Text(cql2_text)),
204+
})
205+
.transpose()?;
206+
let filter = filter
207+
.map(|filter| filter.into_cql2_json())
208+
.transpose()
209+
.map_err(Error::from)?;
210+
let mut items = Items {
211+
limit,
212+
bbox,
213+
datetime,
214+
query,
215+
fields,
216+
sortby,
217+
filter,
218+
..Default::default()
219+
};
220+
if let Some(kwargs) = kwargs {
221+
items.additional_fields = pythonize::depythonize(&kwargs)?;
222+
}
223+
224+
let intersects = intersects
225+
.map(|intersects| match intersects {
226+
StringOrDict::Dict(json) => pythonize::depythonize(&json)
227+
.map_err(PyErr::from)
228+
.and_then(|json| {
229+
Geometry::from_json_object(json)
230+
.map_err(|err| PyValueError::new_err(err.to_string()))
231+
}),
232+
StringOrDict::String(s) => s
233+
.parse::<Geometry>()
234+
.map_err(|err| PyValueError::new_err(err.to_string())),
235+
})
236+
.transpose()?;
237+
let ids = ids.map(|ids| ids.into()).unwrap_or_default();
238+
let collections = collections.map(|ids| ids.into()).unwrap_or_default();
239+
Ok(Search {
240+
items,
241+
intersects,
242+
ids,
243+
collections,
244+
})
245+
}
246+
247+
/// A string or dictionary.
248+
///
249+
/// Used for the CQL2 filter argument and for intersects.
250+
#[derive(Debug, FromPyObject)]
251+
pub enum StringOrDict<'py> {
252+
/// Text
253+
String(String),
254+
255+
/// Json
256+
Dict(Bound<'py, PyDict>),
257+
}
258+
259+
/// A string or a list.
260+
///
261+
/// Used for collections, ids, etc.
262+
#[derive(Debug, FromPyObject)]
263+
pub enum StringOrList {
264+
/// A string.
265+
String(String),
266+
267+
/// A list.
268+
List(Vec<String>),
269+
}
270+
271+
/// A sortby structure.
272+
///
273+
/// This can be a string, a list of strings, or a list of dictionaries.
274+
#[derive(Debug, FromPyObject)]
275+
pub enum PySortby<'py> {
276+
/// A string.
277+
String(String),
278+
279+
/// A list.
280+
ListOfStrings(Vec<String>),
281+
282+
/// A list.
283+
ListOfDicts(Vec<Bound<'py, PyDict>>),
284+
}
285+
286+
impl From<StringOrList> for Vec<String> {
287+
fn from(value: StringOrList) -> Vec<String> {
288+
match value {
289+
StringOrList::List(list) => list,
290+
StringOrList::String(s) => vec![s],
291+
}
292+
}
293+
}

0 commit comments

Comments
 (0)