Skip to content

Commit a41895e

Browse files
authored
fix: update readme and fix duplicate type fields on serialization (#608)
1 parent 268ab6e commit a41895e

File tree

16 files changed

+322
-117
lines changed

16 files changed

+322
-117
lines changed

README.md

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88

99
Command Line Interface (CLI) and Rust libraries for the [SpatioTemporal Asset Catalog (STAC)](https://stacspec.org/) specification.
1010

11-
There's a couple Python projects based on **stac-rs** that might be of interest to you, as well:
11+
## Formats
1212

13-
- [stacrs](https://github.com/gadomski/stacrs) provides a Python API to **stac-rs**, including
14-
- Reading and writing [stac-geoparquet](https://github.com/stac-utils/stac-geoparquet)
15-
- Migrating to [STAC v1.1](https://github.com/radiantearth/stac-spec/releases/tag/v1.1.0)
16-
- [More...](https://www.gadom.ski/posts/stacrs-python-v0-1/)
17-
- [pgstacrs](https://github.com/stac-utils/pgstacrs) is a Python library for working with [pgstac](https://github.com/stac-utils/pgstac)
13+
**stac-rs** "speaks" three forms of STAC:
14+
15+
- **JSON**: STAC is derived from [GeoJSON](https://geojson.org/)
16+
- **Newline-delimited JSON (ndjson)**: One JSON [item](https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md) per line, often used for bulk item loading and storage
17+
- **stac-geoparquet**: A newer [specification](https://github.com/stac-utils/stac-geoparquet) for storing STAC items, and optionally collections
18+
19+
We also have interfaces to other storage backends, e.g. Postgres via [pgstac](https://github.com/stac-utils/pgstac).
1820

1921
## Command line interface
2022

@@ -30,15 +32,38 @@ cargo install stac-cli
3032
Then:
3133

3234
```shell
35+
# Search
3336
$ stacrs search https://landsatlook.usgs.gov/stac-server \
34-
-c landsat-c2l2-sr --intersects \
35-
'{"type": "Point", "coordinates": [-105.119, 40.173]}' \
37+
--collections landsat-c2l2-sr \
38+
--intersects '{"type": "Point", "coordinates": [-105.119, 40.173]}' \
3639
--sortby='-properties.datetime' \
3740
--max-items 1000 \
38-
-f 'parquet[snappy]' \
3941
items.parquet
42+
43+
# Translate formats
44+
$ stacrs translate items.parquet items.ndjson
45+
$ stacrs translate items.ndjson items.json
46+
47+
# Migrate STAC versions
48+
$ stacrs translate item-v1.0.json item-v1.1.json --migrate
49+
50+
# Search stac-geoparquet (no API server required)
51+
$ stac search items.parquet
52+
53+
# Server
54+
$ stacrs serve items.parquet # Opens a STAC API server on http://localhost:7822
4055
```
4156

57+
## Python
58+
59+
We have Python packages based on **stac-rs** that live in their own repositories:
60+
61+
- [stacrs](https://github.com/gadomski/stacrs) provides a Python API to **stac-rs**, including
62+
- Reading and writing [stac-geoparquet](https://github.com/stac-utils/stac-geoparquet)
63+
- Migrating to [STAC v1.1](https://github.com/radiantearth/stac-spec/releases/tag/v1.1.0)
64+
- [More...](https://www.gadom.ski/posts/stacrs-python-v0-1/)
65+
- [pgstacrs](https://github.com/stac-utils/pgstacrs) is a Python library for working with [pgstac](https://github.com/stac-utils/pgstac)
66+
4267
## Crates
4368

4469
This monorepo contains several crates:

crates/api/src/item_collection.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,46 @@
11
use crate::{Item, Result};
2-
use serde::{Deserialize, Serialize};
2+
use serde::{Deserialize, Deserializer, Serialize};
33
use serde_json::{Map, Value};
44
use stac::{Href, Link};
55
use stac_derive::{Links, SelfHref};
66

7+
const ITEM_COLLECTION_TYPE: &str = "FeatureCollection";
8+
9+
fn item_collection_type() -> String {
10+
ITEM_COLLECTION_TYPE.to_string()
11+
}
12+
13+
fn deserialize_item_collection_type<'de, D>(
14+
deserializer: D,
15+
) -> std::result::Result<String, D::Error>
16+
where
17+
D: Deserializer<'de>,
18+
{
19+
let r#type = String::deserialize(deserializer)?;
20+
if r#type != ITEM_COLLECTION_TYPE {
21+
Err(serde::de::Error::invalid_value(
22+
serde::de::Unexpected::Str(&r#type),
23+
&ITEM_COLLECTION_TYPE,
24+
))
25+
} else {
26+
Ok(r#type)
27+
}
28+
}
29+
730
/// The return value of the `/items` and `/search` endpoints.
831
///
932
/// This might be a [stac::ItemCollection], but if the [fields
1033
/// extension](https://github.com/stac-api-extensions/fields) is used, it might
1134
/// not be. Defined by the [itemcollection
1235
/// fragment](https://github.com/radiantearth/stac-api-spec/blob/main/fragments/itemcollection/README.md).
1336
#[derive(Debug, Serialize, Deserialize, Default, Links, SelfHref)]
14-
#[serde(tag = "type", rename = "FeatureCollection")]
1537
pub struct ItemCollection {
38+
#[serde(
39+
default = "item_collection_type",
40+
deserialize_with = "deserialize_item_collection_type"
41+
)]
42+
r#type: String,
43+
1644
/// A possibly-empty array of Item objects.
1745
#[serde(rename = "features")]
1846
pub items: Vec<Item>,
@@ -104,6 +132,7 @@ impl ItemCollection {
104132
pub fn new(items: Vec<Item>) -> Result<ItemCollection> {
105133
let number_returned = items.len();
106134
Ok(ItemCollection {
135+
r#type: item_collection_type(),
107136
items,
108137
links: Vec::new(),
109138
number_matched: None,

crates/api/src/lib.rs

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -81,22 +81,20 @@ mod url_builder;
8181

8282
#[cfg(feature = "client")]
8383
pub use client::{BlockingClient, Client};
84-
pub use {
85-
collections::Collections,
86-
conformance::{
87-
Conformance, COLLECTIONS_URI, CORE_URI, FEATURES_URI, FILTER_URIS, GEOJSON_URI,
88-
ITEM_SEARCH_URI, OGC_API_FEATURES_URI,
89-
},
90-
error::Error,
91-
fields::Fields,
92-
filter::Filter,
93-
item_collection::{Context, ItemCollection},
94-
items::{GetItems, Items},
95-
root::Root,
96-
search::{GetSearch, Search},
97-
sort::{Direction, Sortby},
98-
url_builder::UrlBuilder,
84+
pub use collections::Collections;
85+
pub use conformance::{
86+
Conformance, COLLECTIONS_URI, CORE_URI, FEATURES_URI, FILTER_URIS, GEOJSON_URI,
87+
ITEM_SEARCH_URI, OGC_API_FEATURES_URI,
9988
};
89+
pub use error::Error;
90+
pub use fields::Fields;
91+
pub use filter::Filter;
92+
pub use item_collection::{Context, ItemCollection};
93+
pub use items::{GetItems, Items};
94+
pub use root::Root;
95+
pub use search::{GetSearch, Search};
96+
pub use sort::{Direction, Sortby};
97+
pub use url_builder::UrlBuilder;
10098

10199
/// Crate-specific result type.
102100
pub type Result<T> = std::result::Result<T, Error>;

crates/cli/README.md

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,37 +23,45 @@ cargo install stac-cli
2323
Then:
2424

2525
```shell
26-
stacrs --help
26+
# Search
27+
$ stacrs search https://landsatlook.usgs.gov/stac-server \
28+
--collections landsat-c2l2-sr \
29+
--intersects '{"type": "Point", "coordinates": [-105.119, 40.173]}' \
30+
--sortby='-properties.datetime' \
31+
--max-items 1000 \
32+
items.parquet
33+
34+
# Translate formats
35+
$ stacrs translate items.parquet items.ndjson
36+
$ stacrs translate items.ndjson items.json
37+
38+
# Migrate STAC versions
39+
$ stacrs translate item-v1.0.json item-v1.1.json --migrate
40+
41+
# Search stac-geoparquet (no API server required)
42+
$ stac search items.parquet
43+
44+
# Server
45+
$ stacrs serve items.parquet # Opens a STAC API server on http://localhost:7822
2746
```
2847

2948
## Usage
3049

3150
**stacrs** provides the following subcommands:
3251

33-
- `stacrs item`: create STAC items and combine them into item collections
34-
- `stacrs migrate`: migrate a STAC object to another version
35-
- `stacrs search`: search STAC APIs (and geoparquet, with the experimental `duckdb` feature)
36-
- `stacrs serve`: serve a STAC API (optionally, with a [pgstac](https://github.com/stac-utils/pgstac) backend)
37-
- `stacrs translate`: convert STAC values from one format to another
38-
- `stacrs validate`: validate STAC items, catalogs, and collections using [json-schema](https://json-schema.org/)
52+
- `stacrs search`: searches STAC APIs and geoparquet files
53+
- `stacrs serve`: serves a STAC API
54+
- `stacrs translate`: converts STAC from one format to another
3955

4056
Use the `--help` flag to see all available options for the CLI and the subcommands:
4157

4258
## Features
4359

44-
This crate has features:
60+
This crate has two features:
4561

46-
- `duckdb`: experimental support for querying [stac-geoparquet](https://github.com/stac-utils/stac-geoparquet) files using [DuckDB](https://duckdb.org/)
47-
- `geoparquet`: read and write [stac-geoparquet](https://github.com/stac-utils/stac-geoparquet) (enabled by default)
4862
- `pgstac`: enable a [pgstac](https://github.com/stac-utils/pgstac) backend for `stacrs serve` (enabled by default)
4963
- `python`: create an entrypoint that can be called from Python (used to enable `python -m pip install stacrs-cli`)
5064

51-
If you don't want to use any of the default features:
52-
53-
```shell
54-
cargo install stac-cli --no-default-features
55-
```
56-
5765
## Other info
5866

5967
This crate is part of the [stac-rs](https://github.com/stac-utils/stac-rs) monorepo, see its README for contributing and license information.

crates/cli/src/lib.rs

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
use anyhow::{anyhow, Error, Result};
22
use clap::{Parser, Subcommand};
3-
use stac::geoparquet::Compression;
4-
use stac::{Collection, Format, Item, Links};
3+
use stac::{geoparquet::Compression, Collection, Format, Item, Links, Migrate};
54
use stac_api::{GetItems, GetSearch, Search};
65
use stac_server::Backend;
7-
use std::collections::HashMap;
8-
use std::io::Write;
9-
use std::str::FromStr;
10-
use tokio::io::AsyncReadExt;
11-
use tokio::net::TcpListener;
6+
use std::{collections::HashMap, io::Write, str::FromStr};
7+
use tokio::{io::AsyncReadExt, net::TcpListener};
128

139
/// stacrs: A command-line interface for the SpatioTemporal Asset Catalog (STAC)
1410
#[derive(Debug, Parser)]
@@ -94,6 +90,20 @@ pub enum Command {
9490
///
9591
/// To write to standard output, pass `-` or don't provide an argument at all.
9692
outfile: Option<String>,
93+
94+
/// Migrate this STAC value to another version.
95+
///
96+
/// By default, will migrate to the latest supported version. Use `--to`
97+
/// to specify a different STAC version.
98+
#[arg(long = "migrate", default_value_t = false)]
99+
migrate: bool,
100+
101+
/// Migrate to this STAC version.
102+
///
103+
/// If not provided, will migrate to the latest supported version. Will
104+
/// only be used if `--migrate` is passed.
105+
#[arg(long = "to")]
106+
to: Option<String>,
97107
},
98108

99109
/// Searches a STAC API or stac-geoparquet file.
@@ -202,9 +212,20 @@ impl Stacrs {
202212
Command::Translate {
203213
ref infile,
204214
ref outfile,
215+
migrate,
216+
ref to,
205217
} => {
206-
let value = self.get(infile.as_deref()).await?;
207-
self.put(outfile.as_deref(), value).await
218+
let mut value = self.get(infile.as_deref()).await?;
219+
if migrate {
220+
value = value.migrate(
221+
&to.as_deref()
222+
.map(|s| s.parse().unwrap())
223+
.unwrap_or_default(),
224+
)?;
225+
} else if let Some(to) = to {
226+
eprintln!("WARNING: --to was passed ({to}) without --migrate, value will not be migrated");
227+
}
228+
self.put(outfile.as_deref(), value.into()).await
208229
}
209230
Command::Search {
210231
ref href,
@@ -263,11 +284,11 @@ impl Stacrs {
263284
for href in hrefs {
264285
let value = self.get(Some(href.as_str())).await?;
265286
match value {
266-
Value::Stac(stac::Value::Collection(collection)) => {
287+
stac::Value::Collection(collection) => {
267288
if load_collection_items {
268289
for link in collection.iter_item_links() {
269290
let value = self.get(Some(link.href.as_str())).await?;
270-
if let Value::Stac(stac::Value::Item(item)) = value {
291+
if let stac::Value::Item(item) = value {
271292
items.entry(collection.id.clone()).or_default().push(item);
272293
} else {
273294
return Err(anyhow!(
@@ -278,7 +299,7 @@ impl Stacrs {
278299
}
279300
collections.push(collection);
280301
}
281-
Value::Stac(stac::Value::ItemCollection(item_collection)) => {
302+
stac::Value::ItemCollection(item_collection) => {
282303
for item in item_collection.items {
283304
if let Some(collection) = item.collection.clone() {
284305
items.entry(collection).or_default().push(item);
@@ -287,7 +308,7 @@ impl Stacrs {
287308
}
288309
}
289310
}
290-
Value::Stac(stac::Value::Item(item)) => {
311+
stac::Value::Item(item) => {
291312
if let Some(collection) = item.collection.clone() {
292313
items.entry(collection).or_default().push(item);
293314
} else {
@@ -318,17 +339,17 @@ impl Stacrs {
318339
}
319340
}
320341

321-
async fn get(&self, href: Option<&str>) -> Result<Value> {
342+
async fn get(&self, href: Option<&str>) -> Result<stac::Value> {
322343
let href = href.and_then(|s| if s == "-" { None } else { Some(s) });
323344
let format = self.input_format(href);
324345
if let Some(href) = href {
325346
let value: stac::Value = format.get_opts(href, self.opts()).await?;
326-
Ok(value.into())
347+
Ok(value)
327348
} else {
328349
let mut buf = Vec::new();
329350
let _ = tokio::io::stdin().read_to_end(&mut buf).await?;
330351
let value: stac::Value = format.from_bytes(buf)?;
331-
Ok(value.into())
352+
Ok(value)
332353
}
333354
}
334355

@@ -471,6 +492,16 @@ mod tests {
471492
.success();
472493
}
473494

495+
#[rstest]
496+
fn migrate(mut command: Command) {
497+
command
498+
.arg("translate")
499+
.arg("../../spec-examples/v1.0.0/simple-item.json")
500+
.arg("--migrate")
501+
.assert()
502+
.success();
503+
}
504+
474505
#[test]
475506
fn input_format() {
476507
let stacrs = Stacrs::parse_from(["stacrs", "translate"]);

0 commit comments

Comments
 (0)