Skip to content

Commit 480e79d

Browse files
authored
Merge branch 'main' into client-traits
2 parents 22b448d + af8331e commit 480e79d

29 files changed

+418
-56
lines changed

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ geojson = "0.24.1"
6666
getrandom = { version = "0.4.0", features = ["wasm_js"] }
6767
http = "1.1"
6868
indexmap = { version = "2.10.0", features = ["serde"] }
69-
jsonschema = { version = "0.41.0", default-features = false, features = [
69+
jsonschema = { version = "0.42.0", default-features = false, features = [
7070
"resolve-async",
7171
] }
7272
libduckdb-sys = "1.3.0"
@@ -77,7 +77,7 @@ object_store = "0.12.0"
7777
parquet = { version = "56.0.0" }
7878
quote = "1.0"
7979
reqwest = { version = "0.13.1", features = ["query"] }
80-
referencing = { version = "0.41.0", features = ["retrieve-async"] }
80+
referencing = { version = "0.42.0", features = ["retrieve-async"] }
8181
rstest = "0.26.1"
8282
rustls = "0.23.22"
8383
serde = "1.0"

crates/cli/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.2.7](https://github.com/stac-utils/rustac/compare/rustac-v0.2.6...rustac-v0.2.7) - 2026-02-12
8+
9+
### Other
10+
11+
- updated the following local packages: stac, stac-io, stac-validate, pgstac, stac-duckdb, stac-server
12+
713
## [0.2.6](https://github.com/stac-utils/rustac/compare/rustac-v0.2.5...rustac-v0.2.6) - 2026-02-03
814

915
### Added

crates/cli/Cargo.toml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "rustac"
33
description = "Command line interface for rustac"
4-
version = "0.2.6"
4+
version = "0.2.7"
55
keywords = ["geospatial", "stac", "metadata", "geo", "raster"]
66
authors.workspace = true
77
edition.workspace = true
@@ -24,16 +24,16 @@ clap = { workspace = true, features = ["derive"] }
2424
clap_complete.workspace = true
2525
futures-core.workspace = true
2626
futures-util.workspace = true
27-
pgstac = { version = "0.4.5", path = "../pgstac", optional = true }
27+
pgstac = { version = "0.4.6", path = "../pgstac", optional = true }
2828
serde_json.workspace = true
29-
stac = { version = "0.16.2", path = "../core" }
30-
stac-duckdb = { version = "0.3.4", path = "../duckdb" }
31-
stac-io = { version = "0.2.4", path = "../io", features = [
29+
stac = { version = "0.16.3", path = "../core" }
30+
stac-duckdb = { version = "0.3.5", path = "../duckdb" }
31+
stac-io = { version = "0.2.5", path = "../io", features = [
3232
"store-all",
3333
"geoparquet",
3434
] }
35-
stac-server = { version = "0.4.5", path = "../server", features = ["axum", "duckdb"] }
36-
stac-validate = { version = "0.6.4", path = "../validate" }
35+
stac-server = { version = "0.4.6", path = "../server", features = ["axum", "duckdb"] }
36+
stac-validate = { version = "0.6.5", path = "../validate" }
3737
tokio = { workspace = true, features = [
3838
"macros",
3939
"io-std",

crates/cli/src/lib.rs

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -382,19 +382,30 @@ impl Rustac {
382382
migrate,
383383
ref to,
384384
} => {
385-
let mut value = self.get(infile.as_deref()).await?;
386385
if migrate {
386+
let mut value = self.get(infile.as_deref()).await?;
387387
value = value.migrate(
388388
&to.as_deref()
389389
.map(|s| s.parse().unwrap())
390390
.unwrap_or_default(),
391391
)?;
392-
} else if let Some(to) = to {
393-
eprintln!(
394-
"WARNING: --to was passed ({to}) without --migrate, value will not be migrated"
395-
);
392+
self.put(outfile.as_deref(), value.into()).await
393+
} else {
394+
if let Some(to) = to {
395+
eprintln!(
396+
"WARNING: --to was passed ({to}) without --migrate, value will not be migrated"
397+
);
398+
}
399+
let input_format = self.input_format(infile.as_deref());
400+
let can_stream = matches!(input_format, Format::NdJson | Format::Geoparquet(_));
401+
if can_stream {
402+
let items = self.get_item_stream(infile.as_deref()).await?;
403+
self.put_item_stream(outfile.as_deref(), items).await
404+
} else {
405+
let value = self.get(infile.as_deref()).await?;
406+
self.put(outfile.as_deref(), value.into()).await
407+
}
396408
}
397-
self.put(outfile.as_deref(), value.into()).await
398409
}
399410
Command::Search {
400411
ref href,
@@ -681,6 +692,41 @@ impl Rustac {
681692
}
682693
}
683694

695+
async fn get_item_stream(
696+
&self,
697+
href: Option<&str>,
698+
) -> Result<Box<dyn Iterator<Item = Result<Item>> + Send>> {
699+
let href = href.and_then(|s| if s == "-" { None } else { Some(s) });
700+
let format = self.input_format(href);
701+
if let Some(href) = href {
702+
let (store, path) = stac_io::parse_href_opts(href, self.opts())?;
703+
let iter = store.get_item_stream(path, format).await?;
704+
Ok(Box::new(iter.map(|r| r.map_err(Error::from))))
705+
} else {
706+
let mut buf = Vec::new();
707+
let _ = tokio::io::stdin().read_to_end(&mut buf).await?;
708+
match format {
709+
Format::NdJson => {
710+
let cursor = std::io::BufReader::new(std::io::Cursor::new(buf));
711+
Ok(Box::new(
712+
stac_io::ndjson_item_reader(cursor).map(|r| r.map_err(Error::from)),
713+
))
714+
}
715+
_ => {
716+
let value: stac::Value = format.from_bytes(buf)?;
717+
let items = match value {
718+
stac::Value::Item(item) => vec![item],
719+
stac::Value::ItemCollection(ic) => ic.items,
720+
other => {
721+
return Err(anyhow!("cannot stream items from {}", other.type_name()));
722+
}
723+
};
724+
Ok(Box::new(items.into_iter().map(Ok)))
725+
}
726+
}
727+
}
728+
}
729+
684730
async fn put(&self, href: Option<&str>, value: Value) -> Result<()> {
685731
let href = href.and_then(|s| if s == "-" { None } else { Some(s) });
686732
let format = self.output_format(href);
@@ -705,6 +751,46 @@ impl Rustac {
705751
}
706752
}
707753

754+
async fn put_item_stream(
755+
&self,
756+
href: Option<&str>,
757+
items: impl Iterator<Item = Result<Item>>,
758+
) -> Result<()> {
759+
let href = href.and_then(|s| if s == "-" { None } else { Some(s) });
760+
let format = self.output_format(href);
761+
if let Some(href) = href {
762+
let (store, path) = stac_io::parse_href_opts(href, self.opts())?;
763+
let items: Vec<Item> = items.collect::<Result<Vec<_>>>()?;
764+
store
765+
.put_item_stream(path, items.into_iter(), format)
766+
.await?;
767+
Ok(())
768+
} else {
769+
match format {
770+
Format::NdJson => {
771+
let stdout = std::io::stdout();
772+
let mut writer = std::io::BufWriter::new(stdout.lock());
773+
for item in items {
774+
let item = item?;
775+
serde_json::to_writer(&mut writer, &item)?;
776+
writeln!(&mut writer)?;
777+
}
778+
Ok(())
779+
}
780+
_ => {
781+
let items: Vec<Item> = items.collect::<Result<Vec<_>>>()?;
782+
let item_collection = stac::ItemCollection::from(items);
783+
let mut bytes = format.into_vec(item_collection)?;
784+
if !matches!(format, Format::NdJson) {
785+
bytes.push(b'\n');
786+
}
787+
std::io::stdout().write_all(&bytes)?;
788+
Ok(())
789+
}
790+
}
791+
}
792+
}
793+
708794
pub fn log_level(&self) -> Option<Level> {
709795
level_enum(self.verbosity())
710796
}

crates/core/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.16.3](https://github.com/stac-utils/rustac/compare/stac-v0.16.2...stac-v0.16.3) - 2026-02-12
8+
9+
### Fixed
10+
11+
- windows paths ([#955](https://github.com/stac-utils/rustac/pull/955))
12+
713
## [0.16.2](https://github.com/stac-utils/rustac/compare/stac-v0.16.1...stac-v0.16.2) - 2026-02-03
814

915
### Other

crates/core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "stac"
33
description = "Rust library for the SpatioTemporal Asset Catalog (STAC) specification"
4-
version = "0.16.2"
4+
version = "0.16.3"
55
keywords = ["geospatial", "stac", "metadata", "geo"]
66
authors.workspace = true
77
categories.workspace = true

crates/core/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ pub enum Error {
5858
#[error("invalid datetime: {0}")]
5959
InvalidDatetime(String),
6060

61+
/// A file path could not be converted to a URL.
62+
#[error("invalid file path: {0}")]
63+
InvalidFilePath(String),
64+
6165
/// [std::io::Error]
6266
#[error(transparent)]
6367
Io(#[from] std::io::Error),

crates/core/src/geoarrow/json.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ pub fn from_record_batch_reader<R: RecordBatchReader>(
517517
Ok(rows)
518518
}
519519

520-
fn record_batch_to_json_rows(
520+
pub(crate) fn record_batch_to_json_rows(
521521
record_batch: RecordBatch,
522522
) -> Result<Vec<JsonMap<String, Value>>, Error> {
523523
let mut rows: Vec<Option<JsonMap<String, Value>>> =

crates/core/src/geoarrow/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,27 @@ fn iter_items(
257257
})
258258
}
259259

260+
/// Converts a single [RecordBatch] to a vector of [Item]s.
261+
///
262+
/// # Examples
263+
///
264+
/// ```
265+
/// use stac::{Item, geoarrow};
266+
/// use geojson::{Geometry, Value};
267+
///
268+
/// let mut item = Item::new("an-id");
269+
/// item.geometry = Some(Geometry::new(Value::Point(vec![-105.1, 41.1])));
270+
/// let (record_batch, _) = geoarrow::encode(vec![item]).unwrap();
271+
/// let items = geoarrow::items_from_record_batch(record_batch).unwrap();
272+
/// assert_eq!(items.len(), 1);
273+
/// ```
274+
pub fn items_from_record_batch(record_batch: RecordBatch) -> Result<Vec<Item>> {
275+
json::record_batch_to_json_rows(record_batch)?
276+
.into_iter()
277+
.map(|item| serde_json::from_value(Value::Object(item)).map_err(Error::from))
278+
.collect()
279+
}
280+
260281
/// Converts a [RecordBatchReader] to an [ItemCollection].
261282
///
262283
/// # Examples

crates/core/src/geoparquet.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,43 @@ where
129129
crate::geoarrow::from_record_batch_reader(reader)
130130
}
131131

132+
/// Returns an iterator that yields batches of [Item]s from a [ChunkReader].
133+
///
134+
/// Unlike [from_reader], this does not collect all items into memory at once.
135+
/// Each iteration yields one record batch worth of items.
136+
///
137+
/// # Examples
138+
///
139+
/// ```
140+
/// use std::fs::File;
141+
///
142+
/// let file = File::open("data/extended-item.parquet").unwrap();
143+
/// let mut count = 0;
144+
/// for result in stac::geoparquet::from_reader_iter(file).unwrap() {
145+
/// let items = result.unwrap();
146+
/// count += items.len();
147+
/// }
148+
/// assert!(count > 0);
149+
/// ```
150+
pub fn from_reader_iter<R>(reader: R) -> Result<impl Iterator<Item = Result<Vec<Item>>>>
151+
where
152+
R: ChunkReader + 'static,
153+
{
154+
let builder = ParquetRecordBatchReaderBuilder::try_new(reader)?;
155+
let geoparquet_metadata = builder
156+
.geoparquet_metadata()
157+
.transpose()?
158+
.ok_or(Error::MissingGeoparquetMetadata)?;
159+
let geoarrow_schema =
160+
builder.geoarrow_schema(&geoparquet_metadata, true, Default::default())?;
161+
let reader = builder.build()?;
162+
let reader = GeoParquetRecordBatchReader::try_new(reader, geoarrow_schema)?;
163+
Ok(reader.map(|result| {
164+
let record_batch = result?;
165+
crate::geoarrow::items_from_record_batch(record_batch)
166+
}))
167+
}
168+
132169
/// Writes a [ItemCollection] to a [std::io::Write] as
133170
/// [stac-geoparquet](https://github.com/stac-utils/stac-geoparquet).
134171
///

0 commit comments

Comments
 (0)