Skip to content

Commit 4a3c3c3

Browse files
committed
feat(python): add search
1 parent 498700c commit 4a3c3c3

24 files changed

+759
-228
lines changed

.github/workflows/stacrs.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ on:
44
push:
55
tags:
66
- 'python-*'
7+
pull_request:
8+
paths:
9+
- python/**
710
workflow_dispatch:
811

912
permissions:
@@ -25,11 +28,11 @@ jobs:
2528
- name: Install dev requirements
2629
run: pip install -r python/requirements-dev.txt
2730
- name: Build
28-
run: maturin build --manifest-path python/Cargo.toml --out dist
31+
run: maturin build --manifest-path python/Cargo.toml --out dist -F geoparquet
2932
- name: Install stacrs
3033
run: pip install stacrs --find-links dist --no-index
3134
- name: Check
32-
run: ruff check python && ruff format --check python && mypy python
35+
run: ruff check python && ruff format --check python && mypy python --config-file=python/pyproject.toml
3336
- name: Test
3437
run: pytest python/tests
3538
linux:
@@ -60,7 +63,7 @@ jobs:
6063
uses: PyO3/maturin-action@v1
6164
with:
6265
target: ${{ matrix.platform.target }}
63-
args: --release --out dist --find-interpreter --manifest-path python/Cargo.toml
66+
args: --release --out dist --find-interpreter --manifest-path python/Cargo.toml -F geoparquet
6467
sccache: 'true'
6568
- name: Upload wheels
6669
uses: actions/upload-artifact@v4
@@ -120,7 +123,7 @@ jobs:
120123
uses: PyO3/maturin-action@v1
121124
with:
122125
target: ${{ matrix.platform.target }}
123-
args: --release --out dist --find-interpreter --manifest-path python/Cargo.toml
126+
args: --release --out dist --find-interpreter --manifest-path python/Cargo.toml -F geoparquet
124127
sccache: 'true'
125128
- name: Upload wheels
126129
uses: actions/upload-artifact@v4
@@ -148,7 +151,7 @@ jobs:
148151
uses: PyO3/maturin-action@v1
149152
with:
150153
target: ${{ matrix.platform.target }}
151-
args: --release --out dist --find-interpreter --manifest-path python/Cargo.toml
154+
args: --release --out dist --find-interpreter --manifest-path python/Cargo.toml -F geoparquet
152155
sccache: 'true'
153156
- name: Upload wheels
154157
uses: actions/upload-artifact@v4

api/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
99
### Added
1010

1111
- `Client` (from now-defunkt **stac-async**) ([#372](https://github.com/stac-utils/stac-rs/pull/372))
12+
- `BlockingClient` ([#387](https://github.com/stac-utils/stac-rs/pull/387))
1213

1314
## [0.5.0] - 2024-09-05
1415

api/src/client.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//! A STAC API client.
2+
13
use crate::{Error, GetItems, Item, ItemCollection, Items, Result, Search, UrlBuilder};
24
use async_stream::try_stream;
35
use futures::{pin_mut, Stream, StreamExt};
@@ -6,7 +8,9 @@ use reqwest::{header::HeaderMap, IntoUrl, Method, StatusCode};
68
use serde::{de::DeserializeOwned, Serialize};
79
use serde_json::{Map, Value};
810
use stac::{Collection, Href, Link, Links};
11+
use std::pin::Pin;
912
use tokio::{
13+
runtime::{Builder, Runtime},
1014
sync::mpsc::{self, error::SendError},
1115
task::JoinHandle,
1216
};
@@ -21,6 +25,17 @@ pub struct Client {
2125
url_builder: UrlBuilder,
2226
}
2327

28+
/// A client for interacting with STAC APIs without async.
29+
#[derive(Debug)]
30+
pub struct BlockingClient(Client);
31+
32+
/// A blocking iterator over items.
33+
#[allow(missing_debug_implementations)]
34+
pub struct BlockingIterator {
35+
runtime: Runtime,
36+
stream: Pin<Box<dyn Stream<Item = Result<Item>>>>,
37+
}
38+
2439
impl Client {
2540
/// Creates a new API client.
2641
///
@@ -127,12 +142,12 @@ impl Client {
127142
///
128143
/// let client = Client::new("https://planetarycomputer.microsoft.com/api/stac/v1").unwrap();
129144
/// let mut search = Search { collections: Some(vec!["sentinel-2-l2a".to_string()]), ..Default::default() };
130-
/// search.items.limit = Some(1);
131145
/// # tokio_test::block_on(async {
132146
/// let items: Vec<_> = client
133147
/// .search(search)
134148
/// .await
135149
/// .unwrap()
150+
/// .take(1)
136151
/// .map(|result| result.unwrap())
137152
/// .collect()
138153
/// .await;
@@ -226,6 +241,57 @@ impl Client {
226241
}
227242
}
228243

244+
impl BlockingClient {
245+
/// Creates a new blocking client.
246+
///
247+
/// # Examples
248+
///
249+
/// ```
250+
/// use stac_api::BlockingClient;
251+
///
252+
/// let client = BlockingClient::new("https://planetarycomputer.microsoft.com/api/stac/vi").unwrap();
253+
/// ```
254+
pub fn new(url: &str) -> Result<BlockingClient> {
255+
Client::new(url).map(Self)
256+
}
257+
258+
/// Searches an API, returning an iterable of items.
259+
///
260+
/// To prevent fetching _all_ the items (which might be a lot), it is recommended to pass a `max_items`.
261+
///
262+
/// # Examples
263+
///
264+
/// ```no_run
265+
/// use stac_api::{Search, BlockingClient};
266+
///
267+
/// let client = BlockingClient::new("https://planetarycomputer.microsoft.com/api/stac/v1").unwrap();
268+
/// let mut search = Search { collections: Some(vec!["sentinel-2-l2a".to_string()]), ..Default::default() };
269+
/// let items: Vec<_> = client
270+
/// .search(search)
271+
/// .unwrap()
272+
/// .map(|result| result.unwrap())
273+
/// .take(1)
274+
/// .collect();
275+
/// assert_eq!(items.len(), 1);
276+
/// ```
277+
pub fn search(&self, search: Search) -> Result<BlockingIterator> {
278+
let runtime = Builder::new_current_thread().enable_all().build()?;
279+
let stream = runtime.block_on(async move { self.0.search(search).await })?;
280+
Ok(BlockingIterator {
281+
runtime,
282+
stream: Box::pin(stream),
283+
})
284+
}
285+
}
286+
287+
impl Iterator for BlockingIterator {
288+
type Item = Result<Item>;
289+
290+
fn next(&mut self) -> Option<Self::Item> {
291+
self.runtime.block_on(self.stream.next())
292+
}
293+
}
294+
229295
fn stream_items(
230296
client: Client,
231297
page: ItemCollection,

api/src/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ pub enum Error {
5252
#[cfg(feature = "client")]
5353
InvalidMethod(#[from] http::method::InvalidMethod),
5454

55+
/// [std::io::Error]
56+
#[error(transparent)]
57+
#[cfg(feature = "client")]
58+
Io(#[from] std::io::Error),
59+
5560
/// [tokio::task::JoinError]
5661
#[error(transparent)]
5762
#[cfg(feature = "client")]

api/src/filter.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::{convert::Infallible, str::FromStr};
2+
13
use serde::{Deserialize, Serialize};
24
use serde_json::{Map, Value};
35

@@ -20,6 +22,13 @@ impl Default for Filter {
2022
}
2123
}
2224

25+
impl FromStr for Filter {
26+
type Err = Infallible;
27+
fn from_str(s: &str) -> Result<Self, Self::Err> {
28+
Ok(Filter::Cql2Text(s.to_string()))
29+
}
30+
}
31+
2332
#[cfg(test)]
2433
mod tests {
2534
use super::Filter;

api/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
)]
6565

6666
#[cfg(feature = "client")]
67-
mod client;
67+
pub mod client;
6868
mod collections;
6969
mod conformance;
7070
mod error;
@@ -78,7 +78,7 @@ mod sort;
7878
mod url_builder;
7979

8080
#[cfg(feature = "client")]
81-
pub use client::Client;
81+
pub use client::{BlockingClient, Client};
8282
pub use {
8383
collections::Collections,
8484
conformance::{

python/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77
## [Unreleased]
88

99
- `migrate_href` ([#334](https://github.com/stac-utils/stac-rs/pull/334))
10+
- `search` and `search_to` ([#387](https://github.com/stac-utils/stac-rs/pull/387))
1011

1112
## [0.0.3] - 2024-08-29
1213

python/CODE_OF_CONDUCT

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../CODE_OF_CONDUCT

python/Cargo.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,16 @@ publish = false
1212
name = "stacrs"
1313
crate-type = ["cdylib"]
1414

15+
[features]
16+
geoparquet = ["stac/geoparquet-compression"]
17+
1518
[dependencies]
19+
geojson = "0.24"
1620
pyo3 = "0.22"
1721
pythonize = "0.22"
18-
stac = { path = "../core", features = ["reqwest"] }
22+
serde = "1"
23+
serde_json = "1"
24+
stac = { path = "../core", features = ["reqwest", "object-store-full"] }
25+
stac-api = { path = "../api", features = ["client"] }
1926
stac-validate = { path = "../validate" }
27+
tokio = { version = "1", features = ["rt"] }

python/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ Then:
2121
```python
2222
import stacrs
2323

24+
# Searches a STAC API
25+
items = stacrs.search(
26+
"https://landsatlook.usgs.gov/stac-server",
27+
collections="landsat-c2l2-sr",
28+
intersects={"type": "Point", "coordinates": [-105.119, 40.173]},
29+
sortby="-properties.datetime",
30+
max_items=1,
31+
)
32+
33+
# Validates a href using json-schema
2434
stacrs.validate_href("https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/simple-item.json")
2535
```
2636

0 commit comments

Comments
 (0)