Skip to content

Commit a396837

Browse files
committed
feat: progress with python binding
1 parent bef4f79 commit a396837

14 files changed

Lines changed: 2835 additions & 0 deletions

File tree

‎crates/python/Cargo.toml‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[package]
2+
name = "superstac-python"
3+
description = "Python bindings for superstac via PyO3."
4+
keywords = ["stac", "geospatial", "federated", "satellite", "python"]
5+
categories = ["science::geo"]
6+
readme = "../../README.md"
7+
version.workspace = true
8+
edition.workspace = true
9+
rust-version.workspace = true
10+
license.workspace = true
11+
repository.workspace = true
12+
homepage.workspace = true
13+
authors.workspace = true
14+
publish = false
15+
16+
[lib]
17+
name = "_superstac"
18+
crate-type = ["cdylib"]
19+
20+
[dependencies]
21+
superstac-core = { path = "../core", version = "0.1.0" }
22+
superstac-config = { path = "../config", version = "0.1.0" }
23+
superstac-engine = { path = "../engine", version = "0.1.0" }
24+
superstac-search = { path = "../search", version = "0.1.0" }
25+
pyo3 = { version = "0.26", features = ["extension-module", "abi3-py39"] }
26+
pyo3-async-runtimes = { version = "0.26", features = ["tokio-runtime"] }
27+
pythonize = "0.26"
28+
parking_lot = "0.12"
29+
serde = { version = "1.0", features = ["derive"] }
30+
serde_json = "1.0"
31+
tokio = { version = "1", features = ["rt-multi-thread"] }

‎crates/python/README.md‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# superstac (Python)
2+
3+
Python bindings for [superstac](https://github.com/spatialnode/superstac) —
4+
federated STAC search across multiple catalogs.
5+
6+
> **Status: alpha.** APIs are not yet stable. Pre-1.0; expect breaking changes.
7+
8+
`superstac` ships a sync `Client` (drop-in for `pystac_client.Client` in most
9+
code) and an `AsyncClient` for asyncio users.
10+
11+
## Install
12+
13+
```bash
14+
pip install superstac
15+
```
16+
17+
Or from source:
18+
19+
```bash
20+
git clone https://github.com/spatialnode/superstac
21+
cd superstac/crates/python
22+
pip install maturin
23+
maturin develop
24+
```
25+
26+
## Quickstart
27+
28+
### Drop-in for pystac-client (single catalog)
29+
30+
```python
31+
from superstac import Client
32+
33+
client = Client.open("https://earth-search.aws.element84.com/v1")
34+
35+
search = client.search(
36+
collections=["sentinel-2-l2a"],
37+
bbox=[6.0, 49.0, 7.0, 50.0],
38+
datetime="2024-01-01/2024-01-31",
39+
limit=50,
40+
)
41+
42+
for item in search.items():
43+
print(item["id"], item["properties"]["datetime"])
44+
45+
print(search.matched(), "items")
46+
fc = search.as_geojson() # GeoJSON FeatureCollection dict
47+
```
48+
49+
### Federated across multiple catalogs
50+
51+
Pass a YAML-shaped dict in one call:
52+
53+
```python
54+
from superstac import Client
55+
56+
client = Client({
57+
"catalogs": [
58+
{"id": "earth-search", "url": "https://earth-search.aws.element84.com/v1"},
59+
{"id": "microsoft", "url": "https://planetarycomputer.microsoft.com/api/stac/v1"},
60+
],
61+
"settings": {"deduplicate_items": True, "max_concurrent_catalogs": 8},
62+
})
63+
client.start()
64+
65+
search = client.search(collections=["sentinel-2-l2a"], limit=50)
66+
items = list(search.items())
67+
print(search.metadata) # per-catalog stats: queried, succeeded, failed, dedupes
68+
```
69+
70+
Or build it up incrementally (any combination is fine):
71+
72+
```python
73+
client = Client()
74+
client.add_catalogs([
75+
{"id": "earth-search", "url": "https://earth-search.aws.element84.com/v1"},
76+
{"id": "microsoft", "url": "https://planetarycomputer.microsoft.com/api/stac/v1"},
77+
])
78+
client.update_settings({"deduplicate_items": True})
79+
client.start()
80+
```
81+
82+
### Async
83+
84+
```python
85+
import asyncio
86+
from superstac import AsyncClient
87+
88+
async def main():
89+
client = await AsyncClient.open("https://earth-search.aws.element84.com/v1")
90+
search = await client.search(collections=["sentinel-2-l2a"], limit=50)
91+
for item in search.items():
92+
print(item["id"])
93+
await client.shutdown()
94+
95+
asyncio.run(main())
96+
```
97+
98+
### Load from YAML
99+
100+
```python
101+
client = Client.from_yaml("superstac.yml") # sync
102+
client = AsyncClient.from_yaml("superstac.yml") # async
103+
```
104+
105+
## License
106+
MIT.

‎crates/python/pyproject.toml‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[build-system]
2+
requires = ["maturin>=1.0,<2.0"]
3+
build-backend = "maturin"
4+
5+
[project]
6+
name = "superstac"
7+
description = "High performance federated STAC search."
8+
readme = "README.md"
9+
license = "MIT"
10+
authors = [{ name = "Emmanuel Jolaiya", email = "emmanuel@spatialnode.com" }]
11+
requires-python = ">=3.7"
12+
keywords = ["stac", "geospatial", "federated", "satellite"]
13+
classifiers = [
14+
"Development Status :: 3 - Alpha",
15+
"License :: OSI Approved :: MIT License",
16+
"Programming Language :: Python :: 3",
17+
"Programming Language :: Python :: 3 :: Only",
18+
"Programming Language :: Rust",
19+
"Topic :: Scientific/Engineering :: GIS",
20+
]
21+
dynamic = ["version"]
22+
dependencies = [
23+
"maturin",
24+
]
25+
26+
27+
[project.urls]
28+
Homepage = "https://github.com/spatialnode/superstac"
29+
Repository = "https://github.com/spatialnode/superstac"
30+
31+
[tool.maturin]
32+
# Build into the `superstac` Python package; the compiled module is
33+
# importable as `superstac._superstac` and re-exported by __init__.py.
34+
python-source = "python"
35+
module-name = "superstac._superstac"
36+
features = ["pyo3/extension-module"]
37+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""High performance federated STAC search."""
2+
3+
from ._superstac import (
4+
AsyncClient,
5+
Client,
6+
Search,
7+
__version__,
8+
)
9+
10+
__all__ = [
11+
"Client",
12+
"AsyncClient",
13+
"Search",
14+
"__version__",
15+
]
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
from typing import Any, Iterator
2+
3+
__version__: str
4+
5+
6+
class Search:
7+
def matched(self) -> int: ...
8+
def items(self) -> Iterator[dict[str, Any]]: ...
9+
def item_collection_as_dict(self) -> dict[str, Any]: ...
10+
@property
11+
def metadata(self) -> dict[str, Any]: ...
12+
def __len__(self) -> int: ...
13+
14+
class Client:
15+
def __init__(
16+
self,
17+
config: dict[str, Any] | None = None,
18+
*,
19+
storage: str = "memory",
20+
) -> None: ...
21+
@classmethod
22+
def open(
23+
cls,
24+
url: str,
25+
*,
26+
id: str | None = None,
27+
storage: str = "memory",
28+
) -> "Client": ...
29+
@classmethod
30+
def from_yaml(cls, yaml_path: str, *, storage: str = "memory") -> "Client": ...
31+
32+
# engine lifecycle
33+
def start(self) -> None: ...
34+
def shutdown(self) -> None: ...
35+
36+
# catalogs
37+
def add_catalog(
38+
self, catalog: dict[str, Any], *, provider: str | None = None
39+
) -> dict[str, Any]: ...
40+
def add_catalogs(
41+
self, catalogs: list[dict[str, Any]]
42+
) -> list[dict[str, Any]]: ...
43+
def get_catalog(self, id: str) -> dict[str, Any]: ...
44+
def list_catalogs(self) -> list[dict[str, Any]]: ...
45+
def update_catalog(self, id: str, update: dict[str, Any]) -> dict[str, Any]: ...
46+
def delete_catalog(self, id: str) -> None: ...
47+
48+
# providers
49+
def add_provider(self, provider: dict[str, Any]) -> dict[str, Any]: ...
50+
def add_providers(
51+
self, providers: list[dict[str, Any]]
52+
) -> list[dict[str, Any]]: ...
53+
def get_provider(self, id: str) -> dict[str, Any]: ...
54+
def list_providers(self) -> list[dict[str, Any]]: ...
55+
def update_provider(self, id: str, update: dict[str, Any]) -> dict[str, Any]: ...
56+
def delete_provider(self, id: str) -> None: ...
57+
58+
# settings
59+
def get_settings(self) -> dict[str, Any]: ...
60+
def update_settings(self, update: dict[str, Any]) -> None: ...
61+
62+
# search & discovery (sync)
63+
def search(self, **kwargs: Any) -> Search: ...
64+
def list_collections(self) -> list[dict[str, Any]]: ...
65+
def get_collections(self) -> list[dict[str, Any]]: ...
66+
def get_collection(self, id: str) -> dict[str, Any]: ...
67+
def catalogs_supporting(self, collection_id: str) -> list[str]: ...
68+
def collections_by_catalog(self) -> dict[str, list[str]]: ...
69+
def describe_collection(
70+
self, catalog_id: str, collection_id: str
71+
) -> dict[str, Any] | None: ...
72+
73+
class AsyncClient:
74+
def __init__(
75+
self,
76+
config: dict[str, Any] | None = None,
77+
*,
78+
storage: str = "memory",
79+
) -> None: ...
80+
@classmethod
81+
async def open(
82+
cls,
83+
url: str,
84+
*,
85+
id: str | None = None,
86+
storage: str = "memory",
87+
) -> "AsyncClient": ...
88+
@classmethod
89+
def from_yaml(
90+
cls, yaml_path: str, *, storage: str = "memory"
91+
) -> "AsyncClient": ...
92+
93+
# engine lifecycle
94+
async def start(self) -> None: ...
95+
async def shutdown(self) -> None: ...
96+
97+
# catalogs (sync — config-time operations)
98+
def add_catalog(
99+
self, catalog: dict[str, Any], *, provider: str | None = None
100+
) -> dict[str, Any]: ...
101+
def add_catalogs(
102+
self, catalogs: list[dict[str, Any]]
103+
) -> list[dict[str, Any]]: ...
104+
def get_catalog(self, id: str) -> dict[str, Any]: ...
105+
def list_catalogs(self) -> list[dict[str, Any]]: ...
106+
def update_catalog(self, id: str, update: dict[str, Any]) -> dict[str, Any]: ...
107+
def delete_catalog(self, id: str) -> None: ...
108+
109+
# providers (sync)
110+
def add_provider(self, provider: dict[str, Any]) -> dict[str, Any]: ...
111+
def add_providers(
112+
self, providers: list[dict[str, Any]]
113+
) -> list[dict[str, Any]]: ...
114+
def get_provider(self, id: str) -> dict[str, Any]: ...
115+
def list_providers(self) -> list[dict[str, Any]]: ...
116+
def update_provider(self, id: str, update: dict[str, Any]) -> dict[str, Any]: ...
117+
def delete_provider(self, id: str) -> None: ...
118+
119+
# settings (sync)
120+
def get_settings(self) -> dict[str, Any]: ...
121+
def update_settings(self, update: dict[str, Any]) -> None: ...
122+
123+
# search & discovery (async)
124+
async def search(self, **kwargs: Any) -> Search: ...
125+
async def list_collections(self) -> list[dict[str, Any]]: ...
126+
async def get_collections(self) -> list[dict[str, Any]]: ...
127+
async def get_collection(self, id: str) -> dict[str, Any]: ...
128+
async def catalogs_supporting(self, collection_id: str) -> list[str]: ...
129+
async def collections_by_catalog(self) -> dict[str, list[str]]: ...
130+
async def describe_collection(
131+
self, catalog_id: str, collection_id: str
132+
) -> dict[str, Any] | None: ...

‎crates/python/python/superstac/py.typed‎

Whitespace-only changes.

0 commit comments

Comments
 (0)