Skip to content

Commit 1187b38

Browse files
committed
feat: add duckdb client
1 parent 4be8b70 commit 1187b38

File tree

5 files changed

+115
-1
lines changed

5 files changed

+115
-1
lines changed

data/100-sentinel-2-items.parquet

752 KB
Binary file not shown.

src/duckdb.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use crate::{Error, Result};
2+
use pyo3::{exceptions::PyException, prelude::*, types::PyDict};
3+
use stac_api::python::{StringOrDict, StringOrList};
4+
use stac_duckdb::Client;
5+
use std::sync::Mutex;
6+
7+
#[pyclass(frozen)]
8+
pub struct DuckdbClient(Mutex<Client>);
9+
10+
#[pymethods]
11+
impl DuckdbClient {
12+
#[new]
13+
fn new() -> Result<DuckdbClient> {
14+
let client = Client::new()?;
15+
Ok(DuckdbClient(Mutex::new(client)))
16+
}
17+
18+
#[pyo3(signature = (href, *, intersects=None, ids=None, collections=None, limit=None, bbox=None, datetime=None, include=None, exclude=None, sortby=None, filter=None, query=None, **kwargs))]
19+
fn search<'py>(
20+
&self,
21+
py: Python<'py>,
22+
href: String,
23+
intersects: Option<StringOrDict>,
24+
ids: Option<StringOrList>,
25+
collections: Option<StringOrList>,
26+
limit: Option<u64>,
27+
bbox: Option<Vec<f64>>,
28+
datetime: Option<String>,
29+
include: Option<StringOrList>,
30+
exclude: Option<StringOrList>,
31+
sortby: Option<StringOrList>,
32+
filter: Option<StringOrDict>,
33+
query: Option<Bound<'py, PyDict>>,
34+
kwargs: Option<Bound<'py, PyDict>>,
35+
) -> PyResult<Bound<'py, PyDict>> {
36+
let search = stac_api::python::search(
37+
intersects,
38+
ids,
39+
collections,
40+
limit,
41+
bbox,
42+
datetime,
43+
include,
44+
exclude,
45+
sortby,
46+
filter,
47+
query,
48+
kwargs,
49+
)?;
50+
let item_collection = {
51+
let client = self
52+
.0
53+
.lock()
54+
.map_err(|err| PyException::new_err(err.to_string()))?;
55+
client.search(&href, search).map_err(Error::from)?
56+
};
57+
let dict = pythonize::pythonize(py, &item_collection)?;
58+
dict.extract()
59+
}
60+
}

src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
#![deny(unused_crate_dependencies, warnings)]
22

3+
mod duckdb;
34
mod error;
45
mod migrate;
56
mod read;
67
mod search;
78
mod version;
89
mod write;
910

10-
use duckdb as _;
11+
use ::duckdb as _;
1112
use error::Error;
1213
use pyo3::prelude::*;
1314

@@ -16,6 +17,7 @@ type Result<T> = std::result::Result<T, Error>;
1617
/// A collection of functions for working with STAC, using Rust under the hood.
1718
#[pymodule]
1819
fn stacrs(m: &Bound<'_, PyModule>) -> PyResult<()> {
20+
m.add_class::<duckdb::DuckdbClient>()?;
1921
m.add_function(wrap_pyfunction!(migrate::migrate, m)?)?;
2022
m.add_function(wrap_pyfunction!(migrate::migrate_href, m)?)?;
2123
m.add_function(wrap_pyfunction!(read::read, m)?)?;

stacrs.pyi

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
from typing import Any, Optional, Tuple
22

3+
class DuckdbClient:
4+
"""A client for querying stac-geoparquet with DuckDB."""
5+
6+
def search(
7+
href: str,
8+
*,
9+
intersects: Optional[str | dict[str, Any]] = None,
10+
ids: Optional[str | list[str]] = None,
11+
collections: Optional[str | list[str]] = None,
12+
max_items: Optional[int] = None,
13+
limit: Optional[int] = None,
14+
bbox: Optional[list[float]] = None,
15+
datetime: Optional[str] = None,
16+
include: Optional[str | list[str]] = None,
17+
exclude: Optional[str | list[str]] = None,
18+
sortby: Optional[str | list[str]] = None,
19+
filter: Optional[str | dict[str, Any]] = None,
20+
query: Optional[dict[str, Any]] = None,
21+
**kwargs: str,
22+
):
23+
"""Search a stac-geoparquet file with duckdb"""
24+
325
def migrate_href(href: str, version: Optional[str] = None) -> dict[str, Any]:
426
"""
527
Migrates a STAC dictionary at the given href to another version.

tests/test_duckdb.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import pytest
2+
from stacrs import DuckdbClient
3+
4+
5+
@pytest.fixture
6+
def client() -> DuckdbClient:
7+
return DuckdbClient()
8+
9+
10+
def test_search(client: DuckdbClient) -> None:
11+
item_collection = client.search("data/extended-item.parquet")
12+
assert len(item_collection["features"]) == 1
13+
14+
15+
def test_search_offset(client: DuckdbClient) -> None:
16+
item_collection = client.search(
17+
"data/100-sentinel-2-items.parquet", offset=0, limit=1
18+
)
19+
assert (
20+
item_collection["features"][0]["id"]
21+
== "S2B_MSIL2A_20241203T174629_R098_T13TDE_20241203T211406"
22+
)
23+
24+
item_collection = client.search(
25+
"data/100-sentinel-2-items.parquet", offset=1, limit=1
26+
)
27+
assert (
28+
item_collection["features"][0]["id"]
29+
== "S2A_MSIL2A_20241201T175721_R141_T13TDE_20241201T213150"
30+
)

0 commit comments

Comments
 (0)