open-sar-triad 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- open_sar_triad-1.0.0/PKG-INFO +391 -0
- open_sar_triad-1.0.0/README.md +350 -0
- open_sar_triad-1.0.0/open_sar_triad.egg-info/PKG-INFO +391 -0
- open_sar_triad-1.0.0/open_sar_triad.egg-info/SOURCES.txt +9 -0
- open_sar_triad-1.0.0/open_sar_triad.egg-info/dependency_links.txt +1 -0
- open_sar_triad-1.0.0/open_sar_triad.egg-info/requires.txt +23 -0
- open_sar_triad-1.0.0/open_sar_triad.egg-info/top_level.txt +1 -0
- open_sar_triad-1.0.0/opensartriad/__init__.py +35 -0
- open_sar_triad-1.0.0/opensartriad/client.py +480 -0
- open_sar_triad-1.0.0/pyproject.toml +44 -0
- open_sar_triad-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: open-sar-triad
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Discover and download open SAR scenes from ICEYE, Umbra and Capella through one API
|
|
5
|
+
Author: Pavan Muguda Sanjeevamurthy
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://www.pmuguda.com/open-sar-triad/
|
|
8
|
+
Project-URL: Source, https://github.com/pmuguda/open-sar-triad
|
|
9
|
+
Project-URL: Issues, https://github.com/pmuguda/open-sar-triad/issues
|
|
10
|
+
Project-URL: STAC catalog, https://www.pmuguda.com/open-sar-triad/api/v1/catalog.json
|
|
11
|
+
Keywords: sar,synthetic aperture radar,stac,earth observation,remote sensing,iceye,umbra,capella,open data
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Provides-Extra: pandas
|
|
24
|
+
Requires-Dist: pandas>=2.0; extra == "pandas"
|
|
25
|
+
Provides-Extra: geo
|
|
26
|
+
Requires-Dist: geopandas>=0.14; extra == "geo"
|
|
27
|
+
Requires-Dist: shapely>=2.0; extra == "geo"
|
|
28
|
+
Provides-Extra: stac
|
|
29
|
+
Requires-Dist: pystac>=1.10; extra == "stac"
|
|
30
|
+
Provides-Extra: notebooks
|
|
31
|
+
Requires-Dist: pandas>=2.0; extra == "notebooks"
|
|
32
|
+
Requires-Dist: matplotlib>=3.7; extra == "notebooks"
|
|
33
|
+
Requires-Dist: geopandas>=0.14; extra == "notebooks"
|
|
34
|
+
Requires-Dist: jupyter>=1.0; extra == "notebooks"
|
|
35
|
+
Provides-Extra: all
|
|
36
|
+
Requires-Dist: pandas>=2.0; extra == "all"
|
|
37
|
+
Requires-Dist: geopandas>=0.14; extra == "all"
|
|
38
|
+
Requires-Dist: shapely>=2.0; extra == "all"
|
|
39
|
+
Requires-Dist: pystac>=1.10; extra == "all"
|
|
40
|
+
Requires-Dist: matplotlib>=3.7; extra == "all"
|
|
41
|
+
|
|
42
|
+
# open-sar-triad (Python client)
|
|
43
|
+
|
|
44
|
+
Discover and download open Synthetic Aperture Radar scenes from **ICEYE**, **Umbra** and **Capella** through one interface.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from opensartriad import Catalog
|
|
48
|
+
|
|
49
|
+
cat = Catalog()
|
|
50
|
+
scenes = cat.search(bbox=(5.9, 47.2, 10.5, 55.1), start="2025-01-01", family="complex")
|
|
51
|
+
scenes.download("data/", family="complex", dry_run=True)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- **No API key, no account, no rate limits.** The backend is a static catalog on a CDN.
|
|
55
|
+
- **No required dependencies.** The client is standard library only.
|
|
56
|
+
- **Imagery is never proxied.** Downloads go straight to each provider's own storage.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Contents
|
|
61
|
+
|
|
62
|
+
- [Install](#install)
|
|
63
|
+
- [Quick start](#quick-start)
|
|
64
|
+
- [Searching](#searching)
|
|
65
|
+
- [Working with a scene](#working-with-a-scene)
|
|
66
|
+
- [Product families](#product-families)
|
|
67
|
+
- [Downloading](#downloading)
|
|
68
|
+
- [Exporting](#exporting)
|
|
69
|
+
- [STAC interoperability](#stac-interoperability)
|
|
70
|
+
- [How it works](#how-it-works)
|
|
71
|
+
- [API reference](#api-reference)
|
|
72
|
+
- [Licence and attribution](#licence-and-attribution)
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Install
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install open-sar-triad
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The client has **no required dependencies**: it is standard library only, so this
|
|
83
|
+
pulls in nothing else.
|
|
84
|
+
|
|
85
|
+
Optional extras:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install "open-sar-triad[pandas]" # to_dataframe()
|
|
89
|
+
pip install "open-sar-triad[geo]" # geopandas / shapely workflows
|
|
90
|
+
pip install "open-sar-triad[stac]" # pystac interop
|
|
91
|
+
pip install "open-sar-triad[notebooks]" # everything the example notebooks use
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
From source:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
git clone https://github.com/pmuguda/open-sar-triad.git
|
|
98
|
+
pip install -e open-sar-triad/python
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Quick start
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from opensartriad import Catalog
|
|
107
|
+
|
|
108
|
+
cat = Catalog()
|
|
109
|
+
|
|
110
|
+
# What is in the catalog?
|
|
111
|
+
stats = cat.stats()
|
|
112
|
+
print(stats["total"]) # 14798
|
|
113
|
+
print(stats["by_provider"]) # {'umbra': 11892, 'capella': 2469, 'iceye': 437}
|
|
114
|
+
print(stats["temporal_extent"]) # ['2020-12-31', '2026-08-19']
|
|
115
|
+
|
|
116
|
+
# Find scenes over an area, in a date window
|
|
117
|
+
scenes = cat.search(
|
|
118
|
+
bbox=(5.9, 47.2, 10.5, 55.1), # west, south, east, north (EPSG:4326)
|
|
119
|
+
start="2025-01-01",
|
|
120
|
+
end="2026-01-01",
|
|
121
|
+
)
|
|
122
|
+
print(scenes)
|
|
123
|
+
# SceneCollection(129 scenes; umbra 128, iceye 1; 2025-01-30 to 2026-07-09)
|
|
124
|
+
|
|
125
|
+
# Look at one
|
|
126
|
+
s = scenes[0]
|
|
127
|
+
print(s.id, s.provider, s.date, s.mode, s.formats)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The first `search()` downloads a ~0.6 MB index and caches it on the `Catalog`
|
|
131
|
+
instance. Later searches are local and instant, so reuse one `Catalog`.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Searching
|
|
136
|
+
|
|
137
|
+
All filters combine with **AND**. Omitted filters do nothing.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
scenes = cat.search(
|
|
141
|
+
bbox=(72.8, 18.9, 73.1, 19.3), # (west, south, east, north)
|
|
142
|
+
start="2025-06-01", # inclusive; str, date or datetime
|
|
143
|
+
end="2025-12-31", # inclusive
|
|
144
|
+
providers=["umbra", "capella"], # or a single string
|
|
145
|
+
mode="spotlight", # sensor mode, case-insensitive
|
|
146
|
+
orbit="ascending", # or "descending"
|
|
147
|
+
look="right", # or "left"
|
|
148
|
+
family="complex", # only scenes that can satisfy this family
|
|
149
|
+
formats=["SLC", "SICD"], # or exact formats
|
|
150
|
+
limit=100, # stop after N matches
|
|
151
|
+
)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`SceneCollection` behaves like a list:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
len(scenes)
|
|
158
|
+
scenes[0]
|
|
159
|
+
scenes[:10]
|
|
160
|
+
for s in scenes:
|
|
161
|
+
...
|
|
162
|
+
[s for s in scenes if s.year == 2025]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**bbox and the antimeridian.** Matching is a bounding-box intersection test, so
|
|
166
|
+
it is inclusive at the edges: a scene whose footprint box touches your box is
|
|
167
|
+
returned even if the polygon itself does not overlap. A box crossing ±180°
|
|
168
|
+
should be passed as two searches.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Working with a scene
|
|
173
|
+
|
|
174
|
+
Cheap fields come from the search index and cost nothing:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
s.id # provider scene id
|
|
178
|
+
s.provider # 'iceye' | 'umbra' | 'capella'
|
|
179
|
+
s.date # 'YYYY-MM-DD'
|
|
180
|
+
s.year # int
|
|
181
|
+
s.mode # sensor mode, lowercase
|
|
182
|
+
s.orbit # 'ascending' | 'descending' | None
|
|
183
|
+
s.look # 'left' | 'right' | None
|
|
184
|
+
s.formats # ['GEC', 'CSI', 'SICD', 'SIDD', 'CPHD']
|
|
185
|
+
s.bbox # [west, south, east, north]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
These trigger a lazy fetch of that provider's full records the first time (then cached):
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
s.url("SICD") # download URL for an exact format
|
|
192
|
+
s.metadata_url("SICD") # provider metadata sidecar, or None if not published
|
|
193
|
+
s.properties # polarization, resolution, incidence angle, ...
|
|
194
|
+
s.geometry # GeoJSON footprint
|
|
195
|
+
s.assets # full STAC asset map
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Product families
|
|
201
|
+
|
|
202
|
+
Each provider names the same kind of product differently. Asking for `SLC` by
|
|
203
|
+
name returns **nothing** from Umbra, even though all of its scenes carry complex
|
|
204
|
+
data, because Umbra labels it `SICD`.
|
|
205
|
+
|
|
206
|
+
Families solve this. Ask for what the product **is**, and each scene resolves to
|
|
207
|
+
its own provider's equivalent:
|
|
208
|
+
|
|
209
|
+
| Family | Resolves in order |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `detected` | `GEO` → `GRD` → `GEC` → `SIDD` |
|
|
212
|
+
| `complex` | `SLC` → `SICD` |
|
|
213
|
+
| `phase` | `CPHD` |
|
|
214
|
+
| `visual` | `CSI` → `VID` |
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
s.resolve("complex") # 'SICD' for Umbra, 'SLC' for ICEYE/Capella
|
|
218
|
+
s.resolve("detected") # 'GEC' for Umbra, 'GRD' for ICEYE, 'GEO' for Capella
|
|
219
|
+
s.resolve("phase") # 'CPHD', or None if that scene has none
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Each scene contributes **one file per family**, so you get the best available
|
|
223
|
+
product rather than several near-duplicates.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Downloading
|
|
228
|
+
|
|
229
|
+
> **SAR products are large.** A single GRD can exceed 1 GB. Always `dry_run` first.
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
# See what would happen; downloads nothing
|
|
233
|
+
scenes.download("data/", family="complex", dry_run=True)
|
|
234
|
+
# 6 file(s): 3 data + 3 metadata, from 3 scene(s)
|
|
235
|
+
# [data ] umbra/2025-07-15-06-10-12_UMBRA-08_SICD.nitf
|
|
236
|
+
# [metadata] umbra/2025-07-15-06-10-12_UMBRA-08.stac.v2.json
|
|
237
|
+
|
|
238
|
+
# Then commit
|
|
239
|
+
paths = scenes.download("data/", family="complex")
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Files land in `data/<provider>/`, with each provider's **metadata sidecar saved
|
|
243
|
+
next to every data file**, so your archive arrives documented rather than as a
|
|
244
|
+
pile of anonymous rasters.
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
scenes.download(
|
|
248
|
+
dest="data/",
|
|
249
|
+
family="complex", # or formats=["SLC", "SICD"]
|
|
250
|
+
metadata=True, # fetch sidecars (default)
|
|
251
|
+
skip_existing=True, # resume-friendly (default)
|
|
252
|
+
dry_run=False,
|
|
253
|
+
quiet=False,
|
|
254
|
+
)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Inspect the plan without downloading:
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
jobs = scenes.download_urls(family="detected")
|
|
261
|
+
len(jobs)
|
|
262
|
+
jobs[0] # {'scene': ..., 'provider': 'umbra', 'format': 'GEC', 'url': ..., 'kind': 'data'}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Downloads are resumable: existing non-empty files are skipped, partial files are
|
|
266
|
+
written to `.part` and only renamed on success, and one failed asset does not
|
|
267
|
+
abort the batch.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Exporting
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
scenes.to_geojson() # GeoJSON FeatureCollection (dict)
|
|
275
|
+
scenes.save_geojson("aoi.geojson")
|
|
276
|
+
scenes.to_stac() # STAC ItemCollection, for pystac / stackstac
|
|
277
|
+
scenes.to_dataframe() # pandas DataFrame (needs pandas)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
df = scenes.to_dataframe()
|
|
282
|
+
df.groupby("provider").size()
|
|
283
|
+
df.groupby(df["date"].str[:7]).size() # scenes per month
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## STAC interoperability
|
|
289
|
+
|
|
290
|
+
The backend **is** a STAC catalog, so standard tools work without this client:
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
import pystac
|
|
294
|
+
|
|
295
|
+
cat = pystac.Catalog.from_file(
|
|
296
|
+
"https://www.pmuguda.com/open-sar-triad/api/v1/catalog.json"
|
|
297
|
+
)
|
|
298
|
+
for child in cat.get_children():
|
|
299
|
+
print(child.id, child.title)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Or take search results straight into a STAC workflow:
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
items = scenes.to_stac()["features"]
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Raw endpoints, if you want to skip the client entirely:
|
|
309
|
+
|
|
310
|
+
| Endpoint | What |
|
|
311
|
+
|---|---|
|
|
312
|
+
| `/api/v1/catalog.json` | STAC root catalog |
|
|
313
|
+
| `/api/v1/collections/{provider}.json` | STAC Collection |
|
|
314
|
+
| `/api/v1/items/{provider}.json` | STAC ItemCollection |
|
|
315
|
+
| `/api/v1/index.json` | compact search index |
|
|
316
|
+
| `/api/v1/stats.json` | counts and extents |
|
|
317
|
+
| `/api/v1/scenes/{provider}.geojson` | raw FeatureCollection |
|
|
318
|
+
|
|
319
|
+
Base URL: `https://www.pmuguda.com/open-sar-triad/api/v1`
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## How it works
|
|
324
|
+
|
|
325
|
+
There is no server. Every endpoint is a file, rebuilt weekly by CI and served
|
|
326
|
+
from a CDN. That has real consequences worth knowing:
|
|
327
|
+
|
|
328
|
+
- **Search is local.** `search()` filters a cached index in memory, so it is
|
|
329
|
+
instant and unmetered. There is nothing to rate limit.
|
|
330
|
+
- **Detail is lazy.** Download URLs live in the larger per-provider files, which
|
|
331
|
+
are fetched only when you first ask for a URL, then cached.
|
|
332
|
+
- **Reuse one `Catalog`.** A new instance means a new cold cache.
|
|
333
|
+
- **Data refreshes weekly.** `stats()["generated"]` tells you when.
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
cat = Catalog() # default hosted API
|
|
337
|
+
cat = Catalog("http://localhost:8000/api/v1") # a local build, for testing
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## API reference
|
|
343
|
+
|
|
344
|
+
### `Catalog(base_url=DEFAULT_BASE_URL, timeout=60)`
|
|
345
|
+
|
|
346
|
+
| Method | Returns | Notes |
|
|
347
|
+
|---|---|---|
|
|
348
|
+
| `search(...)` | `SceneCollection` | see [Searching](#searching) |
|
|
349
|
+
| `get(scene_id)` | `Scene \| None` | exact id lookup |
|
|
350
|
+
| `all()` | `SceneCollection` | every scene |
|
|
351
|
+
| `stats()` | `dict` | counts, extents, families |
|
|
352
|
+
| `stac_catalog()` | `dict` | STAC root |
|
|
353
|
+
| `stac_collection(provider)` | `dict` | STAC Collection |
|
|
354
|
+
| `stac_items(provider)` | `dict` | full ItemCollection (large) |
|
|
355
|
+
| `license()` | `dict` | attribution notice |
|
|
356
|
+
|
|
357
|
+
### `Scene`
|
|
358
|
+
|
|
359
|
+
Attributes `id`, `provider`, `date`, `year`, `mode`, `orbit`, `look`, `formats`, `bbox`
|
|
360
|
+
(free), and `assets`, `properties`, `geometry` (lazy).
|
|
361
|
+
Methods `url(fmt)`, `metadata_url(fmt)`, `resolve(family)`.
|
|
362
|
+
|
|
363
|
+
### `SceneCollection`
|
|
364
|
+
|
|
365
|
+
A `Sequence` of `Scene`. Methods `download(...)`, `download_urls(...)`,
|
|
366
|
+
`to_geojson()`, `save_geojson(path)`, `to_stac()`, `to_dataframe()`.
|
|
367
|
+
|
|
368
|
+
### Exceptions
|
|
369
|
+
|
|
370
|
+
`OpenSarTriadError` for any network or API failure.
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Licence and attribution
|
|
375
|
+
|
|
376
|
+
The **client code** is MIT.
|
|
377
|
+
|
|
378
|
+
**Scene metadata is CC-BY 4.0**, published by ICEYE, Umbra and Capella under
|
|
379
|
+
their open data programs. **When you publish derived work, credit the
|
|
380
|
+
originating provider.**
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
print(Catalog().license()["attribution"])
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Imagery is **not** redistributed by this project. Every download URL points at
|
|
387
|
+
the provider's own storage and remains subject to that provider's terms.
|
|
388
|
+
|
|
389
|
+
This is an independent third-party catalog, not endorsed by or affiliated with
|
|
390
|
+
ICEYE, Umbra or Capella. Scene records are normalized from the STAC catalogs
|
|
391
|
+
maintained by [Jack-Hayes/commerical-sar-stac](https://github.com/Jack-Hayes/commerical-sar-stac) (MIT).
|