Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,16 @@ jobs:
run: |
npm install --no-save @bjorn3/browser_wasi_shim@^0.4.2
node ../examples/node-demo.mjs
- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Test the Python wrapper against the freshly built runtime
env:
# Use the runtime just built by build.sh instead of downloading it.
GEOLIBRE_WASM: ${{ github.workspace }}/npm/geolibre-cli.wasm
run: |
python -m pip install --upgrade pip
python -m pip install ./python pytest
pytest python/tests -q
31 changes: 31 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ jobs:
- name: Upgrade npm for Trusted Publishing
run: npm install -g npm@latest

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Build both WASM artifacts (library + WASI runner)
run: ./build.sh

Expand All @@ -60,6 +64,31 @@ jobs:
working-directory: npm
run: npm publish --provenance --access public

# Keep the Python package version and the runtime version it downloads in
# lockstep with the tag, so a tag is the single source of truth.
- name: Sync Python package version to tag
if: startsWith(github.ref, 'refs/tags/v')
run: |
ver="${GITHUB_REF_NAME#v}"
sed -i "s/^version = .*/version = \"$ver\"/" python/pyproject.toml
sed -i "s/^__version__ = .*/__version__ = \"$ver\"/" python/src/geolibre_wasm/__init__.py
sed -i "s|^RUNTIME_VERSION = .*|RUNTIME_VERSION = \"v$ver\"|" python/src/geolibre_wasm/_core.py

- name: Build Python wheel + sdist
run: |
python -m pip install --upgrade build
python -m build --outdir python-dist python

# No token needed: PyPI authenticates via the OIDC id-token from the
# configured trusted publisher. Requires a PyPI trusted publisher for
# project "geolibre-wasm" (owner opengeos, repo geolibre-rust, workflow
# release.yml); for the first release, configure it as a pending publisher.
- name: Publish to PyPI (Trusted Publishing)
if: startsWith(github.ref, 'refs/tags/v')
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: python-dist

- name: Stage release assets
if: startsWith(github.ref, 'refs/tags/v')
run: |
Expand All @@ -72,6 +101,8 @@ jobs:
cp npm/geolibre-cli.wasm dist/geolibre-cli-${GITHUB_REF_NAME}.wasm
cp npm/tools.mjs dist/tools-${GITHUB_REF_NAME}.mjs
cp npm/tools.d.ts dist/tools-${GITHUB_REF_NAME}.d.ts
# Python wheel + sdist
cp python-dist/*.whl python-dist/*.tar.gz dist/
(cd dist && sha256sum * > SHA256SUMS.txt)

- name: Attach to GitHub Release
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,11 @@ npm/geolibre_wasm.d.ts
npm/geolibre_wasm_bg.wasm.d.ts
npm/snippets/
*.wasm-opt.wasm
# Python wrapper build artifacts
__pycache__/
*.egg-info/
python/build/
python/dist/
.pytest_cache/
# Cargo.lock is committed: this workspace ships a binary, and the lock pins the
# exact whitebox_next_gen fork commit for reproducible WASI builds.
39 changes: 36 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# geolibre-rust

[![npm version](https://img.shields.io/npm/v/geolibre-wasm.svg)](https://www.npmjs.com/package/geolibre-wasm)
[![PyPI version](https://img.shields.io/pypi/v/geolibre-wasm.svg)](https://pypi.org/project/geolibre-wasm/)
[![npm downloads](https://img.shields.io/npm/dm/geolibre-wasm.svg)](https://www.npmjs.com/package/geolibre-wasm)
[![CI](https://github.com/opengeos/geolibre-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/opengeos/geolibre-rust/actions/workflows/ci.yml)
[![license](https://img.shields.io/npm/l/geolibre-wasm.svg)](https://github.com/opengeos/geolibre-rust#license)
Expand All @@ -22,9 +23,10 @@ The published npm package (`geolibre-wasm`) ships two layers:
GeoLibre's own tools**, run over an in-memory `/work` filesystem via
[`@bjorn3/browser_wasi_shim`](https://github.com/bjorn3/browser_wasi_shim).

No server, no Python, no native install. New tools live in the `geolibre-tools`
crate and are registered alongside whitebox's, so GeoLibre sees them through the
same interface as the built-ins.
No server, no GDAL, no native install. Use it from JavaScript (npm
`geolibre-wasm`) or Python (PyPI `geolibre-wasm`). New tools live in the
`geolibre-tools` crate and are registered alongside whitebox's, so GeoLibre sees
them through the same interface as the built-ins.

## Try it in the browser

Expand Down Expand Up @@ -160,6 +162,10 @@ When `kdtree 0.8.1` (or later) is published, delete `vendor/kdtree/` and the
> Note: the repository is `geolibre-rust` (the Rust source), but the published
> npm package is **`geolibre-wasm`** (the WASM artifact), mirroring `whitebox-wasm`.

```bash
npm install geolibre-wasm
```

Browser library (the `.` export) -- typed GeoTIFF/projection/vector/LiDAR APIs:

```js
Expand All @@ -185,6 +191,33 @@ const { files } = await runTool("slope", {
const slopeCog = files["slope.tif"]; // Uint8Array (COG GeoTIFF)
```

## Use from Python

The `python/` package (`geolibre-wasm` on PyPI, `import geolibre_wasm`) runs the
same WASI tool runner in-process via `wasmtime`, mirroring the JS `./tools` API.
No native install, GDAL, or server.

```bash
pip install geolibre-wasm
```

```python
import geolibre_wasm as gl

tools = gl.list_tools() # every tool id
manifests = gl.list_manifests() # schemas + "source": geolibre|whitebox

res = gl.run_tool(
"slope",
args=["--input=/work/dem.tif", "--output=/work/slope.tif", "--units=degrees"],
input={"dem.tif": open("dem.tif", "rb").read()},
)
open("slope.tif", "wb").write(res.files["slope.tif"]) # COG GeoTIFF bytes
```

The runtime `.wasm` is downloaded from the matching release on first use (or set
`GEOLIBRE_WASM`). See [`python/README.md`](python/README.md) for details.

## GeoLibre integration

The interface is byte-compatible with the existing `whitebox-wasm/tools` client:
Expand Down
90 changes: 90 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# geolibre-wasm (Python)

Run the [`geolibre-rust`](https://github.com/opengeos/geolibre-rust) geospatial
tool suite (the `whitebox_next_gen` tools plus GeoLibre's own) from Python. The
tools are a single WebAssembly (WASI) module executed in-process via
[`wasmtime`](https://github.com/bytecodealliance/wasmtime-py), so there is **no
native install, no GDAL, and no server** — just `pip install`.

This mirrors the JavaScript `geolibre-wasm/tools` API (`list_tools`,
`list_manifests`, `run_tool`), so the two stay in sync.

> The import package is `geolibre_wasm` (the distribution is `geolibre-wasm`),
> matching the npm package name and avoiding a clash with the separate
> `geolibre` application package.

## Install

```bash
pip install geolibre-wasm
```

On first use the runtime (`geolibre-cli.wasm`, ~20 MB) is downloaded from the
matching GitHub release and cached under `~/.cache/geolibre/`. To use a local
copy instead, set `GEOLIBRE_WASM=/path/to/geolibre-cli.wasm` or pass
`wasm_path=` to any call.

## Usage

Inputs are passed as `bytes` under `/work`; the tool reads/writes there and any
new files come back as `bytes`.

```python
import geolibre_wasm as gl

# Discover tools (each manifest carries a "source": "geolibre" | "whitebox")
tools = gl.list_tools()
manifests = gl.list_manifests()

# Raster: compute slope from a DEM
dem = open("dem.tif", "rb").read()
res = gl.run_tool(
"slope",
args=["--input=/work/dem.tif", "--output=/work/slope.tif", "--units=degrees"],
input={"dem.tif": dem},
)
assert res.exit_code == 0
open("slope.tif", "wb").write(res.files["slope.tif"])

# Reproject (warp) to a target EPSG
res = gl.run_tool(
"reproject_raster",
args=["--input=/work/dem.tif", "--output=/work/wgs84.tif", "--epsg=4326"],
input={"dem.tif": dem},
)

# Vector: GeoJSON -> GeoParquet (Hilbert-sorted, bbox covering, ZSTD by default)
gj = open("cities.geojson", "rb").read()
res = gl.run_tool(
"write_geoparquet",
args=["--input=/work/in.geojson", "--output=/work/out.parquet"],
input={"in.geojson": gj},
)
open("cities.parquet", "wb").write(res.files["out.parquet"])
```

Tools that write a directory tree (e.g. `raster_to_tiles`) return nested keys:

```python
res = gl.run_tool(
"raster_to_tiles",
args=["--input=/work/dem.tif", "--output_dir=/work/tiles", "--min_zoom=16", "--max_zoom=18"],
input={"dem.tif": dem},
)
for path, data in res.files.items():
# e.g. "tiles/16/9559/32767.png"
...
```

## API

- `list_tools(wasm_path=None) -> list[str]`
- `list_manifests(wasm_path=None) -> list[dict]`
- `run_tool(tool, args=None, input=None, wasm_path=None) -> ToolResult`
- `ToolResult(exit_code: int, stdout: list[str], files: dict[str, bytes])`
- `runtime_path(wasm_path=None) -> str` — resolve the runtime (explicit > `GEOLIBRE_WASM` > cached download)
- `download_runtime(dest=None) -> str` — fetch the runtime ahead of time

## License

MIT
28 changes: 28 additions & 0 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "geolibre-wasm"
version = "0.4.0"
description = "Run the GeoLibre / whitebox_next_gen geospatial tool suite (WASM/WASI) from Python."
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
authors = [{ name = "Qiusheng Wu", email = "giswqs@gmail.com" }]
keywords = ["geospatial", "gis", "wasm", "wasi", "raster", "vector", "geoparquet", "whitebox", "geolibre"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Scientific/Engineering :: GIS",
]
dependencies = ["wasmtime>=20,<50"]

[project.urls]
Homepage = "https://github.com/opengeos/geolibre-rust"
Repository = "https://github.com/opengeos/geolibre-rust"
Issues = "https://github.com/opengeos/geolibre-rust/issues"

[tool.hatch.build.targets.wheel]
packages = ["src/geolibre_wasm"]
40 changes: 40 additions & 0 deletions python/src/geolibre_wasm/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""GeoLibre: run the whitebox_next_gen + GeoLibre geospatial tool suite from Python.

The tools are compiled to a single WebAssembly (WASI) module and executed
in-process via wasmtime, so there is no native install, GDAL, or server. The API
mirrors the JavaScript ``geolibre-wasm/tools`` package.

Example:
>>> import geolibre_wasm as gl
>>> dem = open("dem.tif", "rb").read()
>>> result = gl.run_tool(
... "slope",
... args=["--input=/work/dem.tif", "--output=/work/slope.tif", "--units=degrees"],
... input={"dem.tif": dem},
... )
>>> result.exit_code
0
>>> open("slope.tif", "wb").write(result.files["slope.tif"])
"""

from ._core import (
RUNTIME_VERSION,
ToolResult,
download_runtime,
list_manifests,
list_tools,
run_tool,
runtime_path,
)

__all__ = [
"RUNTIME_VERSION",
"ToolResult",
"download_runtime",
"list_manifests",
"list_tools",
"run_tool",
"runtime_path",
]
Comment thread
coderabbitai[bot] marked this conversation as resolved.

__version__ = "0.4.0"
Loading
Loading