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.
@@ -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).