diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 771e16a..bcac2ea 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -27,5 +27,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Sync run: uv sync + - name: Lint + run: scripts/lint - name: Test - run: uv run pytest + run: scripts/test diff --git a/CHANGELOG.md b/CHANGELOG.md index 5547ec8..520dcba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - DuckDB client ([#15](https://github.com/gadomski/stacrs/pull/15)) +### Changed + +- `read` and `write` are now async ([#18](https://github.com/gadomski/stacrs/pull/18)) + ## [0.3.0] - 2024-11-21 ### Removed diff --git a/Cargo.lock b/Cargo.lock index 5cf712e..2b75f76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2374,6 +2374,19 @@ dependencies = [ "unindent", ] +[[package]] +name = "pyo3-async-runtimes" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "977dc837525cfd22919ba6a831413854beb7c99a256c03bf8624ad707e45810e" +dependencies = [ + "futures", + "once_cell", + "pin-project-lite", + "pyo3", + "tokio", +] + [[package]] name = "pyo3-build-config" version = "0.23.3" @@ -3177,8 +3190,10 @@ dependencies = [ "duckdb", "geojson", "pyo3", + "pyo3-async-runtimes", "pyo3-log", "pythonize", + "serde", "serde_json", "stac", "stac-api", diff --git a/Cargo.toml b/Cargo.toml index 56169d9..a7b1f6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,11 @@ crate-type = ["cdylib"] duckdb = { version = "1.1.1", features = ["bundled"] } geojson = "0.24.1" pyo3 = "0.23.3" +pyo3-async-runtimes = { version = "0.23.0", features = ["tokio", "tokio-runtime"] } pyo3-log = "0.12.1" pythonize = "0.23.0" -serde_json = "1.0.134" +serde = "1.0.217" +serde_json = "1.0.135" stac = { version = "0.11.1", features = [ "geoparquet-compression", "object-store-all", diff --git a/README.md b/README.md index ac8e4bf..f6f633a 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,7 @@ Then: ```shell git clone git@github.com:gadomski/stacrs.git cd stacrs -uv sync # This will take a little while while the Rust dependencies build -uv run pytest +scripts/test # This will take a little while while the Rust dependencies build, especially DuckDB ``` See [CONTRIBUTING.md](./CONTRIBUTING.md) for more information about contributing to this project. diff --git a/docs/api/duckdb.md b/docs/api/duckdb.md new file mode 100644 index 0000000..a0e1c4f --- /dev/null +++ b/docs/api/duckdb.md @@ -0,0 +1,7 @@ +--- +description: Query stac-geoparquet with DuckDB +--- + +# DuckDB + +::: stacrs.DuckdbClient diff --git a/docs/api/validate.md b/docs/api/validate.md deleted file mode 100644 index f9b6a20..0000000 --- a/docs/api/validate.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: Validate STAC with json-schema ---- - -# Validate - -::: stacrs.validate -::: stacrs.validate_href diff --git a/docs/example.ipynb b/docs/example.ipynb index b2885eb..9c9ea44 100644 --- a/docs/example.ipynb +++ b/docs/example.ipynb @@ -134,7 +134,8 @@ } ], "source": [ - "stacrs.search_to(\"items-compressed.parquet\",\n", + "stacrs.search_to(\n", + " \"items-compressed.parquet\",\n", " \"https://landsatlook.usgs.gov/stac-server\",\n", " collections=\"landsat-c2l2-sr\",\n", " intersects={\"type\": \"Point\", \"coordinates\": [-105.119, 40.173]},\n", diff --git a/mkdocs.yml b/mkdocs.yml index 338b366..d038763 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,10 +20,10 @@ nav: - Example notebook: example.ipynb - API: - api/index.md + - duckdb: api/duckdb.md - migrate: api/migrate.md - read: api/read.md - search: api/search.md - - validate: api/validate.md - version: api/version.md - write: api/write.md - CONTRIBUTING.md @@ -37,10 +37,16 @@ plugins: python: load_external_modules: false options: + allow_inspection: false + docstring_section_style: list + docstring_style: google + line_length: 80 + separate_signature: true show_root_heading: true - show_signature: true show_signature_annotations: true - separate_signature: true + show_source: false + show_symbol_type_toc: true + signature_crossrefs: true - search - social: cards_layout_options: diff --git a/pyproject.toml b/pyproject.toml index 1f6acd9..21bd484 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stacrs" -description = "A no-dependency Python package for STAC, using Rust under the hood." +description = "A small, no-dependency Python package for STAC, using Rust under the hood." readme = "README.md" authors = [{ name = "Pete Gadomski", email = "pete.gadomski@gmail.com" }] requires-python = ">=3.10" @@ -26,10 +26,17 @@ Repository = "https://github.com/gadomski/stacrs" Documentation = "https://gadom.ski/stacrs" Issues = "https://github.com/gadomski/stacrs/issues" +[tool.mypy] +files = "**/*.py" + [[tool.mypy.overrides]] module = "pyarrow.*" ignore_missing_imports = true +[tool.pytest.ini_options] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" + [tool.uv] dev-dependencies = [ "maturin>=1.7.4", @@ -38,7 +45,10 @@ dev-dependencies = [ "mkdocs-material[imaging]>=9.5.45", "mkdocstrings[python]>=0.27.0", "mypy>=1.11.2", + "pyarrow>=18.0.0", + "pystac[validation]>=1.11.0", "pytest>=8.3.3", + "pytest-asyncio>=0.25.1", "ruff>=0.6.9", "stac-geoparquet>=0.6.0", ] diff --git a/scripts/format b/scripts/format new file mode 100755 index 0000000..bd717b3 --- /dev/null +++ b/scripts/format @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +set -e + +uv run ruff check --fix +uv run ruff format diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 0000000..5dbb053 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +set -e + +uv run ruff check +uv run ruff format --check +uv run mypy diff --git a/scripts/test b/scripts/test index 97e737d..29b8868 100755 --- a/scripts/test +++ b/scripts/test @@ -2,4 +2,5 @@ set -e -maturin dev --uv && pytest "$@" +uv run maturin dev --uv +uv run pytest "$@" diff --git a/src/duckdb.rs b/src/duckdb.rs index 005140d..63c9c7d 100644 --- a/src/duckdb.rs +++ b/src/duckdb.rs @@ -1,4 +1,4 @@ -use crate::{Error, Result}; +use crate::Result; use pyo3::{ exceptions::PyException, prelude::*, @@ -36,7 +36,7 @@ impl DuckdbClient { filter: Option, query: Option>, kwargs: Option>, - ) -> PyResult> { + ) -> Result> { let search = stac_api::python::search( intersects, ids, @@ -56,18 +56,20 @@ impl DuckdbClient { .0 .lock() .map_err(|err| PyException::new_err(err.to_string()))?; - client.search(&href, search).map_err(Error::from)? + client.search(&href, search)? }; let dict = pythonize::pythonize(py, &item_collection)?; - dict.extract() + let dict = dict.extract()?; + Ok(dict) } - fn get_collections<'py>(&self, py: Python<'py>, href: String) -> PyResult> { + fn get_collections<'py>(&self, py: Python<'py>, href: String) -> Result> { let client = self .0 .lock() .map_err(|err| PyException::new_err(err.to_string()))?; - let collections = client.collections(&href).map_err(Error::from)?; - pythonize::pythonize(py, &collections)?.extract() + let collections = client.collections(&href)?; + let collections = pythonize::pythonize(py, &collections)?.extract()?; + Ok(collections) } } diff --git a/src/error.rs b/src/error.rs index 7f0c0ab..65494fa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,14 +1,26 @@ -use pyo3::{exceptions::PyException, PyErr}; +use pyo3::{ + create_exception, + exceptions::{PyException, PyIOError}, + PyErr, +}; use thiserror::Error; +create_exception!(stacrs, StacrsError, PyException); + #[derive(Debug, Error)] pub enum Error { #[error(transparent)] Geojson(#[from] geojson::Error), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] Pythonize(#[from] pythonize::PythonizeError), + #[error(transparent)] + Py(#[from] PyErr), + #[error(transparent)] SerdeJson(#[from] serde_json::Error), @@ -23,14 +35,11 @@ pub enum Error { } impl From for PyErr { - fn from(value: Error) -> Self { - match value { - Error::Geojson(err) => PyException::new_err(err.to_string()), - Error::Pythonize(err) => PyException::new_err(err.to_string()), - Error::SerdeJson(err) => PyException::new_err(err.to_string()), - Error::Stac(err) => PyException::new_err(err.to_string()), - Error::StacApi(err) => PyException::new_err(err.to_string()), - Error::StacDuckdb(err) => PyException::new_err(err.to_string()), + fn from(err: Error) -> Self { + match err { + Error::Py(err) => err, + Error::Io(err) => PyIOError::new_err(err.to_string()), + _ => StacrsError::new_err(err.to_string()), } } } diff --git a/src/lib.rs b/src/lib.rs index 42e918f..cfc0db4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![deny(unused_crate_dependencies, warnings)] +#![deny(unused_crate_dependencies)] mod duckdb; mod error; @@ -14,11 +14,14 @@ use pyo3::prelude::*; type Result = std::result::Result; -/// A collection of functions for working with STAC, using Rust under the hood. #[pymodule] -fn stacrs(m: &Bound<'_, PyModule>) -> PyResult<()> { +fn stacrs(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { pyo3_log::init(); + + m.add("StacrsError", py.get_type::())?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(migrate::migrate, m)?)?; m.add_function(wrap_pyfunction!(migrate::migrate_href, m)?)?; m.add_function(wrap_pyfunction!(read::read, m)?)?; @@ -26,5 +29,17 @@ fn stacrs(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(search::search_to, m)?)?; m.add_function(wrap_pyfunction!(version::version, m)?)?; m.add_function(wrap_pyfunction!(write::write, m)?)?; + Ok(()) } + +struct Json(T); + +impl<'py, T: serde::Serialize> IntoPyObject<'py> for Json { + type Error = pythonize::PythonizeError; + type Output = Bound<'py, PyAny>; + type Target = PyAny; + fn into_pyobject(self, py: Python<'py>) -> std::result::Result { + pythonize::pythonize(py, &self.0) + } +} diff --git a/src/migrate.rs b/src/migrate.rs index ae1ac33..119bbcd 100644 --- a/src/migrate.rs +++ b/src/migrate.rs @@ -1,4 +1,4 @@ -use crate::Error; +use crate::{Error, Result}; use pyo3::{prelude::*, types::PyDict}; use stac::{Migrate, Value}; @@ -7,7 +7,7 @@ use stac::{Migrate, Value}; pub fn migrate<'py>( value: &Bound<'py, PyDict>, version: Option<&str>, -) -> PyResult> { +) -> Result> { let py = value.py(); let value: Value = pythonize::depythonize(value)?; let version = version @@ -15,7 +15,8 @@ pub fn migrate<'py>( .unwrap_or_default(); let value = value.migrate(&version).map_err(Error::from)?; let value = pythonize::pythonize(py, &value)?; - value.downcast_into().map_err(PyErr::from) + let value = value.extract()?; + Ok(value) } #[pyfunction] @@ -24,12 +25,13 @@ pub fn migrate_href<'py>( py: Python<'py>, href: &str, version: Option<&str>, -) -> PyResult> { +) -> Result> { let value: Value = stac::read(href).map_err(Error::from)?; let version = version .map(|version| version.parse().unwrap()) .unwrap_or_default(); - let value = value.migrate(&version).map_err(Error::from)?; + let value = value.migrate(&version)?; let value = pythonize::pythonize(py, &value)?; - value.downcast_into().map_err(PyErr::from) + let value = value.extract()?; + Ok(value) } diff --git a/src/read.rs b/src/read.rs index 095dc4f..86f7b21 100644 --- a/src/read.rs +++ b/src/read.rs @@ -1,11 +1,6 @@ -use crate::Error; -use pyo3::{ - pyfunction, - types::{PyAnyMethods, PyDict}, - Bound, PyErr, PyResult, Python, -}; +use crate::{Error, Json}; +use pyo3::{pyfunction, types::PyAny, Bound, PyResult, Python}; use stac::{Format, Value}; -use tokio::runtime::Builder; #[pyfunction] #[pyo3(signature = (href, *, format=None, options=None))] @@ -14,17 +9,17 @@ pub fn read( href: String, format: Option, options: Option>, -) -> PyResult> { +) -> PyResult> { let format = format .and_then(|f| f.parse::().ok()) .or_else(|| Format::infer_from_href(&href)) .unwrap_or_default(); let options = options.unwrap_or_default(); - let runtime = Builder::new_current_thread().enable_all().build()?; - let value = runtime - .block_on(async move { format.get_opts::(href, options).await }) - .map_err(Error::from)?; - pythonize::pythonize(py, &value) - .map_err(PyErr::from) - .and_then(|v| v.extract()) + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let value = format + .get_opts::(href, options) + .await + .map_err(Error::from)?; + Ok(Json(value)) + }) } diff --git a/src/write.rs b/src/write.rs index a87bce0..234f35f 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1,12 +1,7 @@ -use crate::{Error, Result}; -use pyo3::{ - pyfunction, - types::{PyAnyMethods, PyDict}, - Bound, PyAny, PyResult, Python, -}; +use crate::{Error, Json, Result}; +use pyo3::{pyfunction, Bound, PyAny, PyResult, Python}; use serde_json::Value; use stac::{Format, Item, ItemCollection}; -use tokio::runtime::Builder; #[pyfunction] #[pyo3(signature = (href, value, *, format=None, options=None))] @@ -16,7 +11,7 @@ pub fn write<'py>( value: Bound<'_, PyAny>, format: Option, options: Option>, -) -> PyResult>> { +) -> PyResult> { let value: Value = pythonize::depythonize(&value)?; let value = if let Value::Array(array) = value { let items = array @@ -27,28 +22,23 @@ pub fn write<'py>( } else { serde_json::from_value(value).map_err(Error::from)? }; - let format = format - .and_then(|f| f.parse::().ok()) - .or_else(|| Format::infer_from_href(&href)) - .unwrap_or_default(); - let runtime = Builder::new_current_thread().enable_all().build()?; - let put_result = runtime - .block_on(async move { - format - .put_opts(href, value, options.unwrap_or_default()) - .await - }) - .map_err(Error::from)?; - if let Some(put_result) = put_result { - let dict = PyDict::new(py); - if let Some(e_tag) = put_result.e_tag { - dict.set_item("e_tag", e_tag)?; + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let format = format + .and_then(|f| f.parse::().ok()) + .or_else(|| Format::infer_from_href(&href)) + .unwrap_or_default(); + let put_result = format + .put_opts(href, value, options.unwrap_or_default()) + .await + .map_err(Error::from)?; + if let Some(put_result) = put_result { + let put_result = serde_json::json!({ + "e_tag": put_result.e_tag, + "version": put_result.version, + }); + Ok(Some(Json(put_result))) + } else { + Ok(None) } - if let Some(version) = put_result.version { - dict.set_item("version", version)?; - } - Ok(Some(dict)) - } else { - Ok(None) - } + }) } diff --git a/stacrs.pyi b/stacrs.pyi index 1990982..1563061 100644 --- a/stacrs.pyi +++ b/stacrs.pyi @@ -11,6 +11,7 @@ class DuckdbClient: collections: Optional[str | list[str]] = None, intersects: Optional[str | dict[str, Any]] = None, limit: Optional[int] = None, + offset: Optional[int] = None, bbox: Optional[list[float]] = None, datetime: Optional[str] = None, include: Optional[str | list[str]] = None, @@ -29,8 +30,8 @@ class DuckdbClient: Item must be in. intersects: Searches items by performing intersection between their geometry and provided GeoJSON geometry. - limit: The page size returned from the server. Use `max_items` to - actually limit the number of items returned from this function. + limit: The number of items to return. + offset: The number of items to skip before returning. bbox: Requested bounding box. datetime: Single date+time, or a range (`/` separator), formatted to RFC 3339, section 5.6. Use double dots .. for open date ranges. @@ -118,7 +119,7 @@ def migrate(value: dict[str, Any], version: Optional[str] = None) -> dict[str, A >>> assert item["stac_version"] == "1.1.0-beta.1" """ -def read( +async def read( href: str, *, format: str | None = None, @@ -138,7 +139,7 @@ def read( dict[str, Any]: The STAC value Examples: - >>> item = stacrs.read("item.json") + >>> item = await stacrs.read("item.json") """ def search( @@ -283,41 +284,7 @@ def search_to( ... ) """ -def validate_href(href: str) -> None: - """ - Validates a single href with json-schema. - - Args: - href (str): The href of the STAC value to validate - - Raises: - Exception: On any input/output error, or on a validation error - - Examples: - >>> stacrs.validate_href("examples/simple-item.json") - >>> stacrs.validate_href("data/invalid-item.json") - Traceback (most recent call last): - File "", line 1, in - Exception: Validation errors: "collection" is a required property - """ - -def validate(value: dict[str, Any]) -> None: - """ - Validates a STAC dictionary with json-schema. - - Args: - value (dict[str, Any]): The STAC value to validate - - Raises: - Exception: On a validation error - - Examples: - >>> with open("examples/simple-item.json") as f: - >>> data = json.load(f) - >>> stacrs.validate(data) - """ - -def write( +async def write( href: str, value: dict[str, Any] | list[dict[str, Any]], *, @@ -344,7 +311,7 @@ def write( Examples: >>> with open("items.json") as f: ... items = json.load(f) - >>> stacrs.write("items.parquet", items) + >>> await stacrs.write("items.parquet", items) """ def version(name: str | None = None) -> str | None: diff --git a/tests/test_read.py b/tests/test_read.py index ea3cb35..9cf7dd1 100644 --- a/tests/test_read.py +++ b/tests/test_read.py @@ -1,7 +1,9 @@ from pathlib import Path import stacrs +from pystac import Item -def test_read(examples: Path) -> None: - stacrs.read(str(examples / "simple-item.json")) +async def test_read(examples: Path) -> None: + item = Item.from_dict(await stacrs.read(str(examples / "simple-item.json"))) + item.validate() diff --git a/tests/test_version.py b/tests/test_version.py index d5e8d58..613eb2c 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -2,8 +2,7 @@ def test_version() -> None: - stacrs.version() - stacrs.version("stac") - stacrs.version("stac-api") - stacrs.version("stac-duckdb") - stacrs.version("duckdb") + assert stacrs.version() is not None + assert stacrs.version("stac") is not None + assert stacrs.version("stac-api") is not None + assert stacrs.version("stac-duckdb") is not None diff --git a/tests/test_write.py b/tests/test_write.py index b101858..f5f13f6 100644 --- a/tests/test_write.py +++ b/tests/test_write.py @@ -6,9 +6,9 @@ import stacrs -def test_write(item: dict[str, Any], tmp_path: Path) -> None: +async def test_write(item: dict[str, Any], tmp_path: Path) -> None: path = str(tmp_path / "out.parquet") - stacrs.write(path, [item]) + await stacrs.write(path, [item]) table = pyarrow.parquet.read_table(path) items = list(stac_geoparquet.arrow.stac_table_to_items(table)) assert len(items) == 1 diff --git a/uv.lock b/uv.lock index e20b50c..a32a145 100644 --- a/uv.lock +++ b/uv.lock @@ -268,7 +268,7 @@ name = "click" version = "8.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } wheels = [ @@ -480,7 +480,7 @@ name = "ipykernel" version = "6.29.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "appnope", marker = "platform_system == 'Darwin'" }, + { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, { name = "ipython" }, @@ -806,7 +806,7 @@ version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, { name = "jinja2" }, { name = "markdown" }, @@ -1552,6 +1552,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/5b/60dc94cbf6af2fd3a3d3fae52de7e294819af2dfe7a1bea4d246beb7e0b6/pystac-1.11.0-py3-none-any.whl", hash = "sha256:10ac7c7b4ea6c5ec8333829a09ec1a33b596f02d1a97ffbbd72cd1b6c10598c1", size = 183925 }, ] +[package.optional-dependencies] +validation = [ + { name = "jsonschema" }, +] + [[package]] name = "pytest" version = "8.3.3" @@ -1569,6 +1574,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, ] +[[package]] +name = "pytest-asyncio" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/04/0477a4bdd176ad678d148c075f43620b3f7a060ff61c7da48500b1fa8a75/pytest_asyncio-0.25.1.tar.gz", hash = "sha256:79be8a72384b0c917677e00daa711e07db15259f4d23203c59012bcd989d4aee", size = 53760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/fb/efc7226b384befd98d0e00d8c4390ad57f33c8fde00094b85c5e07897def/pytest_asyncio-0.25.1-py3-none-any.whl", hash = "sha256:c84878849ec63ff2ca509423616e071ef9cd8cc93c053aa33b5b8fb70a990671", size = 19357 }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2034,7 +2051,10 @@ dev = [ { name = "mkdocs-material", extra = ["imaging"] }, { name = "mkdocstrings", extra = ["python"] }, { name = "mypy" }, + { name = "pyarrow" }, + { name = "pystac", extra = ["validation"] }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "ruff" }, { name = "stac-geoparquet" }, ] @@ -2049,7 +2069,10 @@ dev = [ { name = "mkdocs-material", extras = ["imaging"], specifier = ">=9.5.45" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.27.0" }, { name = "mypy", specifier = ">=1.11.2" }, + { name = "pyarrow", specifier = ">=18.0.0" }, + { name = "pystac", extras = ["validation"], specifier = ">=1.11.0" }, { name = "pytest", specifier = ">=8.3.3" }, + { name = "pytest-asyncio", specifier = ">=0.25.1" }, { name = "ruff", specifier = ">=0.6.9" }, { name = "stac-geoparquet", specifier = ">=0.6.0" }, ]