Skip to content

Commit

Permalink
feat: add search to CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
gadomski committed Oct 11, 2023
1 parent 60b7455 commit 27d7248
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 37 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ Validation error at /extent/temporal: [["2020-12-11T22:38:32.125Z","2020-12-14T1
ERROR: validation errors
```

Search a STAC API:

```shell
stac search https://earth-search.aws.element84.com/v1 \
-c sentinel-2-l2a \
--max-items 1 \
--sortby='-properties.datetime' \
--intersects '{"type":"Point","coordinates":[-105.1019,40.1672]}'
```

To see a full list of available commands:

```shell
Expand Down
1 change: 1 addition & 0 deletions stac-cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added

- Sort ([#197](https://github.com/stac-utils/stac-rs/pull/197))
- Search ([#200](https://github.com/stac-utils/stac-rs/pull/200))

### Removed

Expand Down
2 changes: 2 additions & 0 deletions stac-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ reqwest = "0.11"
serde = "1"
serde_json = "1"
stac = { version = "0.5", path = "../stac" }
stac-api = { version = "0.3", path = "../stac-api" }
stac-async = { version = "0.4", path = "../stac-async" }
stac-validate = { version = "0.1", path = "../stac-validate" }
thiserror = "1"
tokio = { version = "1.23", features = ["macros", "rt-multi-thread"] }
tokio-stream = "0.1"
url = "2"

[[bin]]
Expand Down
12 changes: 12 additions & 0 deletions stac-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ Use the cli `--help` flag to see all available options:
stac --help
```

### Search

Search a STAC API:

```shell
stac search https://earth-search.aws.element84.com/v1 \
-c sentinel-2-l2a \
--max-items 1 \
--sortby='-properties.datetime' \
--intersects '{"type":"Point","coordinates":[-105.1019,40.1672]}'
```

### Validate

Validate a STAC item:
Expand Down
142 changes: 105 additions & 37 deletions stac-cli/src/command.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,78 @@
use crate::Result;
use clap::Subcommand;
use stac_validate::Validate;
use stac_api::GetSearch;

#[derive(Debug, Subcommand)]
pub enum Command {
/// Searches a STAC API.
Search {
/// The href of the STAC API.
href: String,

/// The maximum number of items to print.
#[arg(short, long)]
max_items: Option<usize>,

/// The maximum number of results to return (page size).
#[arg(short, long)]
limit: Option<String>,

/// Requested bounding box.
#[arg(short, long)]
bbox: Option<String>,

/// Requested bounding box.
/// Use double dots `..` for open date ranges.
#[arg(short, long)]
datetime: Option<String>,

/// Searches items by performing intersection between their geometry and provided GeoJSON geometry.
///
/// All GeoJSON geometry types must be supported.
#[arg(long)]
intersects: Option<String>,

/// Array of Item ids to return.
#[arg(short, long)]
ids: Option<Vec<String>>,

/// Array of one or more Collection IDs that each matching Item must be in.
#[arg(short, long)]
collections: Option<Vec<String>>,

/// Include/exclude fields from item collections.
#[arg(long)]
fields: Option<String>,

/// Fields by which to sort results.
#[arg(short, long)]
sortby: Option<String>,

/// Recommended to not be passed, but server must only accept
/// <http://www.opengis.net/def/crs/OGC/1.3/CRS84> as a valid value, may
/// reject any others
#[arg(long)]
filter_crs: Option<String>,

/// CQL2 filter expression.
#[arg(long)]
filter_lang: Option<String>,

/// CQL2 filter expression.
#[arg(short, long)]
filter: Option<String>,

/// Stream the items to standard output as ndjson.
#[arg(long)]
stream: bool,

/// Do not pretty print the output.
///
/// Only used if stream is false.
#[arg(long)]
compact: bool,
},

/// Sorts the fields of STAC object.
Sort {
/// The href of the STAC object.
Expand All @@ -25,43 +94,42 @@ impl Command {
pub async fn execute(self) -> Result<()> {
use Command::*;
match self {
Sort { href, compact } => sort(&href, compact).await,
Validate { href } => validate(&href).await,
}
}
}

async fn validate(href: &str) -> Result<()> {
let value: serde_json::Value = stac_async::read_json(href).await?;
let result = {
let value = value.clone();
tokio::task::spawn_blocking(move || value.validate()).await?
};
match result {
Ok(()) => {
println!("OK!");
Ok(())
}
Err(stac_validate::Error::Validation(errors)) => {
for err in &errors {
println!("Validation error at {}: {}", err.instance_path, err)
Search {
href,
max_items,
limit,
bbox,
datetime,
intersects,
ids,
collections,
fields,
sortby,
filter_crs,
filter_lang,
filter,
stream,
compact,
} => {
let get_search = GetSearch {
limit,
bbox,
datetime,
intersects,
ids,
collections,
fields,
sortby,
filter_crs,
filter_lang,
filter,
additional_fields: Default::default(),
};
let search = get_search.try_into()?;
crate::commands::search(&href, search, max_items, stream, !(compact | stream)).await
}
Err(stac_validate::Error::Validation(errors).into())
}
Err(err) => {
println!("Error while validating: {}", err);
Err(err.into())
Sort { href, compact } => crate::commands::sort(&href, compact).await,
Validate { href } => crate::commands::validate(&href).await,
}
}
}

async fn sort(href: &str, compact: bool) -> Result<()> {
let value: stac::Value = stac_async::read_json(href).await?;
let output = if compact {
serde_json::to_string(&value).unwrap()
} else {
serde_json::to_string_pretty(&value).unwrap()
};
println!("{}", output);
Ok(())
}
5 changes: 5 additions & 0 deletions stac-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod search;
mod sort;
mod validate;

pub use {search::search, sort::sort, validate::validate};
51 changes: 51 additions & 0 deletions stac-cli/src/commands/search.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use crate::Result;
use stac_api::{Item, ItemCollection, Search};
use stac_async::ApiClient;
use tokio_stream::StreamExt;

pub async fn search(
href: &str,
search: Search,
max_items: Option<usize>,
stream: bool,
pretty: bool,
) -> Result<()> {
let client = ApiClient::new(href)?;
let item_stream = client.search(search).await?;
tokio::pin!(item_stream);
let mut num_items = 0;
if stream {
assert!(!pretty);
while let Some(result) = item_stream.next().await {
let item: Item = result?;
num_items += 1;
println!("{}", serde_json::to_string(&item)?);
if max_items
.map(|max_items| num_items >= max_items)
.unwrap_or(false)
{
break;
}
}
} else {
let mut items = Vec::new();
while let Some(result) = item_stream.next().await {
num_items += 1;
items.push(result?);
if max_items
.map(|max_items| num_items >= max_items)
.unwrap_or(false)
{
break;
}
}
let item_collection = ItemCollection::new(items)?;
let output = if pretty {
serde_json::to_string_pretty(&item_collection)?
} else {
serde_json::to_string(&item_collection)?
};
println!("{}", output);
}
Ok(())
}
12 changes: 12 additions & 0 deletions stac-cli/src/commands/sort.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use crate::Result;

pub async fn sort(href: &str, compact: bool) -> Result<()> {
let value: stac::Value = stac_async::read_json(href).await?;
let output = if compact {
serde_json::to_string(&value).unwrap()
} else {
serde_json::to_string_pretty(&value).unwrap()
};
println!("{}", output);
Ok(())
}
26 changes: 26 additions & 0 deletions stac-cli/src/commands/validate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use crate::Result;
use stac_validate::Validate;

pub async fn validate(href: &str) -> Result<()> {
let value: serde_json::Value = stac_async::read_json(href).await?;
let result = {
let value = value.clone();
tokio::task::spawn_blocking(move || value.validate()).await?
};
match result {
Ok(()) => {
println!("OK!");
Ok(())
}
Err(stac_validate::Error::Validation(errors)) => {
for err in &errors {
println!("Validation error at {}: {}", err.instance_path, err)
}
Err(stac_validate::Error::Validation(errors).into())
}
Err(err) => {
println!("Error while validating: {}", err);
Err(err.into())
}
}
}
6 changes: 6 additions & 0 deletions stac-cli/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ pub enum Error {
#[error("invalid STAC")]
InvalidValue(Value),

#[error(transparent)]
SerdeJson(#[from] serde_json::Error),

#[error(transparent)]
StacApi(#[from] stac_api::Error),

#[error(transparent)]
StacAsync(#[from] stac_async::Error),

Expand Down
1 change: 1 addition & 0 deletions stac-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod args;
mod command;
mod commands;
mod error;

pub use {args::Args, command::Command, error::Error};
Expand Down

0 comments on commit 27d7248

Please sign in to comment.