v1.9.4 · Actively Developed

pygeofetch Documentation

Universal satellite data pipeline — unified access to 24 providers. Handles auth, federated search, parallel downloads, caching, band selection, and post-processing.

WHO THIS IS FOR

  • Geospatial researchers who need to process data from multiple providers without learning 24 different APIs
  • Engineers automating satellite data pipelines that need to run unattended, on schedule
  • Organizations that need cost-effective, open-source Earth observation tooling
24
Providers (Varying Status)
10
Free (No Login)
7
Output Formats
5
Hardened & Verified
On the provider count: 24 providers are implemented in the codebase. A confirmed-hardened subset — Copernicus, USGS, AWS Earth, Planetary Computer, Element84, and Sentinel Hub — are tested end-to-end, including real footprint geometry display. A further 12 providers had a real, systematic bug fixed this session (real footprint geometry was being silently discarded even when the provider's own API supplied it); the fix is verified against synthetic data, but not yet independently confirmed against each provider's live API. One, ESA SciHub, is confirmed pointing at a dead service (Copernicus Open Access Hub, decommissioned November 2, 2023) — use copernicus instead. See the Providers section for the honest per-provider breakdown.
$ install
pip install pygeofetch
MIT License Python 3.9+
Getting Started

Quick Start (5 Minutes)

You need satellite imagery for a specific location and date range. Here's the real, complete path from nothing installed to a file on disk — no account, no credentials, no setup beyond one install command.

Every command below uses a real, free provider — no login, no API key, no waiting on an approval email. This whole sequence genuinely runs in about five minutes.
1
Install
pip install pygeofetch
2
Search
pygeofetch search run \ --bbox "-74.1,40.6,-73.7,40.9" \ --start-date 2024-01-01 \ --end-date 2024-03-01 \ --cloud-cover 0-10 \ --providers aws_earth \ --output results.geojson
3
Download
pygeofetch download run \ --from-search results.geojson \ --output ./data/ \ --max-items 1
4
Verify
ls -la ./data/ # A real Sentinel-2 scene, on disk, right there

What actually happens at each step

Step 2 searches AWS Earth's open Sentinel-2 archive, real, live data, no account needed, and writes the results to a real GeoJSON file rather than just printing them:

real search output
┌ SEARCH PARAMETERS ───────────────────────────────────────────────────────┐ │ Providers : aws_earth │ │ BBox : [-74.100, 40.600, -73.700, 40.900] │ │ Date range : 2024-01-01 → 2024-03-01 │ │ Cloud max : 10% │ └──────────────────────────────────────────────────────────────────────────┘ ✓ aws_earth 6 scenes 1.4s ┌────────────────────────────────────┬────────────┬──────────────┬────────┐ │ SCENE ID │ DATE │ SATELLITE │ CLOUD │ ├────────────────────────────────────┼────────────┼──────────────┼────────┤ │ S2B_18TWL_20240214_0_L2A │ 2024-02-14 │ SENTINEL-2B │ 2.1% │ │ S2A_18TWL_20240209_0_L2A │ 2024-02-09 │ SENTINEL-2A │ 6.8% │ └────────────────────────────────────┴────────────┴──────────────┴────────┘ 6 scenes found · saved to results.geojson

Step 3 reads that same file and downloads the top-ranked scene:

real download output
Downloading S2B_18TWL_20240214_0_L2A... ████████████████████████████████░░░░ 87% 312 MB / 358 MB ✓ Downloaded to ./data/S2B_18TWL_20240214_0_L2A.tif (358 MB, verified)

Step 4 is just confirming it's real — a genuine file, on your disk, ready to open in QGIS or load with rasterio.

Interactive demo — try commands

Select a command from the dropdown and click Run to see the expected output:

pygeofetch — interactive demo
$ pygeofetch doctor Select a command above and click ▶ Run
$
That's the whole core loop — search, download, done. Real accounts, band selection to cut download size, and post-processing are all real and useful, but none of them were needed to get here. Verify your own setup with pygeofetch --version and pygeofetch doctor (checks Python, packages, keyring, and live connectivity), then see What's Next for where to go from a bare file to something useful — including setting up authenticated providers, band selection, and post-processing once you're ready for them.

What's Next?

You've searched and downloaded a scene. Here's where to go from a bare file to something useful:

See it on a map first

Before committing to a download, see real footprints and hover for scene details on an actual map. See Search Footprints.

Compute something

NDVI, water indices, burn severity, and 14 more — a single line each. See Spectral Indices.

Working with SAR/Sentinel-1?

Real despeckling, calibration, and the full InSAR chain, both are genuinely different from optical workflows. See SARProcessor or InSAR Processing.

Doing this on a schedule?

Turn a one-off search into a repeatable, scheduled pipeline. See YAML Pipeline Orchestration.

Something went wrong?

Auth errors, timeouts, zero results, keyring issues on Docker/SSH — real, common first-run problems with real fixes. See Common Errors and Fixes.

Real-World Example

Obuasi Mining Belt — 6-Year Vegetation Trend

Built entirely with PyGeoFetch's clip-first pipeline and TimeSeriesAnalyzer, using live Landsat data pulled from USGS over the Obuasi Municipal District, Ashanti Region, Ghana — not a synthetic demo.

Obuasi NDVI trend 2018-2024 Obuasi vegetation trend classification

KEY FINDINGS

  • 32.0% of the district shows measurable vegetation decline over 2018–2024 (9.2% strong decline + 22.8% moderate decline).
  • The decline forms a spatially coherent cluster in the western portion of the AOI — not scattered noise, visible in both maps above.
  • 57.5% of the district is stable, with 10.5% showing an increasing trend.

The decline isn't scattered randomly — it forms a clear, spatially coherent cluster in the western portion of the AOI, visible in both maps above. Real degradation signals tend to cluster like this (mining concessions, roads, river corridors); sensor noise scatters more randomly.

What this can and can't claim: an NDVI trend map alone can't distinguish why vegetation declined — illegal small-scale mining (galamsey), selective logging, agricultural clearing, and urban expansion can all produce a similar signature. Given Obuasi's well-documented galamsey activity, the pattern here is consistent with mining-driven clearance — confirming that specific cause would need higher-resolution imagery or ground verification, not this analysis alone. The scattered dark-blue linear features in the trend map are very likely water bodies (rivers), not vegetation gain — NDVI trend over open water isn't a meaningful signal.

Built from: real Obuasi Municipal District boundary polygon (not a bounding-box rectangle) → federated USGS search with a real geoJson spatial filter → clip-first processing (boundary-clipped before any scaling or masking) → TimeSeriesAnalyzer.build_index_stack() across 6 dry-season composites → .trend() and .zonal_timeseries()Plotter.plot_raster() and .plot_classification(), entirely via PyGeoFetch's own visualization module.

Example Notebooks

21 Real, Runnable Notebooks

Every notebook below is a real workflow built and run against live data throughout this project — not templates. Each card opens directly from GitHub, no manual download or Drive upload needed.

Before you click Colab: pygeofetch isn't preinstalled — add !pip install "pygeofetch[insar]" (or whichever extra the notebook needs) as your first cell. Several notebooks need real provider credentials (Copernicus, USGS, OpenTopography) to run their live-data cells — Colab can open and read every notebook regardless, but running the authenticated cells needs your own account. A couple of the earlier notebooks also expect an uploaded boundary file; re-upload it to the Colab session if that cell fails.

NEW TO PYGEOFETCH? START WITH THESE NOTEBOOKS

01Getting Started— install, first search, first download, in five real minutes. 02Authentication & Providers— set up credentials once you need a provider beyond the free ones. 06Real-World Workflows— NDVI time series and change detection, the pattern behind the Obuasi example. 10SAR / InSAR Complete Pipeline— the full chain, no SNAP or ISCE, if you're working with Sentinel-1. 15Obuasi Vegetation Trend— the real-world example on this page, in full, runnable form.
01Getting StartedStart Here
Install, run doctor diagnostics, first search, first download — the fastest path to a working setup.
02Authentication & Providers
All 22 providers, credential storage via system keyring, capability-based filtering.
03Advanced Search
Federated search across providers, CQL2 filters, all 7 output formats, result caching.
04Download & Postprocessing
Band selection, parallel downloads with resume support, the full postprocessing chain.
05Pipelines & Scheduling
YAML pipeline orchestration, cron scheduling, the Python pipeline builder API.
06Real-World Workflows
NDVI time series, change detection, and multi-sensor fusion end to end.
07Copernicus & Authenticated Providers
Copernicus, USGS, NASA Earthdata, Planet, ASF, and OpenTopography — real auth flows.
08CLI Complete Reference
Every CLI command demonstrated with runnable examples, not just documented.
09Processing Engine — Complete
The full processing engine: preprocessing, all 17 spectral indices, SAR, pipelines.
10SAR / InSAR Complete Pipeline
Despeckling, calibration, and the full InSAR chain from SLC extraction to unwrapping.
11InSAR Subsidence Monitoring
A real subsidence-monitoring project built on the InSAR pipeline, reference-pixel included.
12USGS Landsat — Complete Pipeline
Full Landsat 1–9 workflow via USGS Machine-to-Machine API, search through indices.
13Ghana Deforestation Monitoring
Real Landsat-based forest-loss detection over a Ghanaian AOI, boundary-clipped.
15Obuasi Vegetation Trend (2018–2024)Start Here
The real-world example above, in full — 32% measurable decline, clip-first, live USGS data.
16Accra Flood Recession — SAR
SAR-based flood extent and recession mapping over Accra using real Sentinel-1 data.
17Accra (GAMA) Flood — Government Boundary
Flood analysis clipped to the real Greater Accra Metropolitan Area administrative boundary.
18Atewa Forest Deforestation Template
A reusable deforestation-monitoring template applied to the Atewa Range Forest Reserve.
19Accra Urban Expansion
Multi-year built-up area growth analysis over Accra using real classified imagery.
20Afadjato — Multi-Source DEM & 3D Terrain
SRTM, Copernicus DEM, NASADEM, and AW3D30 compared over Ghana's tallest peak, in 3D.
21Accra Terrain Flood Susceptibility
TWI, curvature, terrain ruggedness, and depression analysis for real flood-risk screening.
22Obuasi InSAR Subsidence — Full SBAS
The complete, real orbit-based InSAR chain: search → SBAS inversion → subsidence hotspots.
Authentication

Managing Provider Credentials

Credentials are stored in your system keyring — never in plain-text files. Supports username/password, API keys, and OAuth2 client credentials.

auth commands
# Username / password (USGS, NASA, Copernicus)
pygeofetch auth add usgs --username YOUR_USER --password YOUR_PASS
pygeofetch auth add copernicus --username email@example.com --password PASS
pygeofetch auth add nasa_earthdata --username USER --password PASS

# API key (Planet, OpenTopography, TerraBotics, Airbus)
pygeofetch auth add planet --api-key YOUR_API_KEY
pygeofetch auth add opentopography --api-key YOUR_KEY

# OAuth2 client credentials (Sentinel Hub)
pygeofetch auth add sentinel_hub --client-id YOUR_ID --client-secret YOUR_SECRET

# Interactive login — prompts for all fields
pygeofetch auth login copernicus

# List, test, remove
pygeofetch auth list
pygeofetch auth test usgs
pygeofetch auth remove planet --yes

# Export backup (WARNING: contains secrets — store securely)
pygeofetch auth export --output creds_backup.json

Environment variables

.env / CI/CD
# Format: pygeofetch_{PROVIDER}_{FIELD}
export pygeofetch_USGS_USERNAME=myuser
export pygeofetch_USGS_PASSWORD=mypass
export pygeofetch_PLANET_API_KEY=PL-abc123
export pygeofetch_COPERNICUS_USERNAME=email@example.com
export pygeofetch_COPERNICUS_PASSWORD=pass
export pygeofetch_NASA_EARTHDATA_USERNAME=user
export pygeofetch_OPENTOPOGRAPHY_API_KEY=mykey
export pygeofetch_SENTINEL_HUB_CLIENT_ID=id
export pygeofetch_SENTINEL_HUB_CLIENT_SECRET=secret

# Global settings
export pygeofetch_LOG_LEVEL=DEBUG
export pygeofetch_DOWNLOAD__PARALLEL=8
export pygeofetch_CACHE__TTL_SECONDS=7200
Headless Linux: If no D-Bus/keyring daemon is running (Docker, SSH), set PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring and use environment variables instead.

Provider auth types

ProviderAuth TypeFree?Register At
usgsUsername / Password✓ FreeRegister →
copernicusUsername / Password (OAuth2)✓ FreeRegister →
nasa_earthdataUsername / Password✓ FreeRegister →
nasa_earthdata_cloudUsername / Password + S3 creds✓ FreeRegister →
planetAPI KeySubscriptionRegister →
sentinel_hubOAuth2 client credentialsFreemiumRegister →
opentopographyAPI Key✓ Free tierRegister →
maxar_gbdxAPI TokenSubscriptionRegister →
airbus_oneatlasAPI KeySubscriptionRegister →
alaska_satellite_facilityEarthdata (same as NASA)✓ FreeRegister →
google_earth_engineService Account JSONFree tierRegister →
terraboticsAPI KeySubscriptionRegister →
Search

Searching Satellite Data

Federated search across multiple providers simultaneously. Results are deduplicated, scored by cloud cover and recency, and returned in STAC 1.0 format.

search examples
# Free providers — works immediately, no credentials
pygeofetch search run \
  --bbox "-74.1,40.6,-73.7,40.9" \
  --start-date 2024-01-01 --end-date 2024-03-01 \
  --cloud-cover 0-10 \
  --providers aws_earth,planetary_computer,element84 \
  --format table

# Save to GeoJSON for download, open in QGIS, or share
pygeofetch search run \
  --bbox "-74.1,40.6,-73.7,40.9" \
  --cloud-cover 0-5 \
  --providers aws_earth \
  --output results.geojson

# Specific satellite, sorted by cloud cover ascending
pygeofetch search run \
  --bbox "-10,35,10,55" \
  --start-date 2024-06-01 \
  --providers copernicus \
  --satellites Sentinel-2 \
  --cloud-cover 0-15 \
  --sort-by cloud_cover --sort-order asc \
  --max-results 50

# CQL2 advanced filter (Planetary Computer, Element84)
pygeofetch search run \
  --bbox "-74.1,40.6,-73.7,40.9" \
  --providers planetary_computer \
  --cql2 "eo:cloud_cover < 5 AND platform = 'sentinel-2b'"

# Search using a GeoJSON geometry file
pygeofetch search run \
  --geometry-file my_area.geojson \
  --cloud-cover 0-10 \
  --providers aws_earth

# CSV output — pipe to spreadsheet or analytics tools
pygeofetch search run \
  --bbox "-74.1,40.6,-73.7,40.9" \
  --providers aws_earth \
  --format csv --output results.csv

Search flags

Flag
Type
Description
--bbox
string
Bounding box "minlon,minlat,maxlon,maxlat". Longitude first.
--start-date
date
Temporal filter start. Format: YYYY-MM-DD.
--cloud-cover
range
Cloud cover range min-max in percent. E.g. 0-20.
--providers
list
Comma-separated provider IDs. E.g. aws_earth,copernicus,usgs.
--output
path
Save results to this file — needed if you plan to download from them next.
Show all 18 search flags →
Flag
Type
Description
--bbox
string
Bounding box "minlon,minlat,maxlon,maxlat". Longitude first. Alternative to --geometry-file.
--geometry-file
path
GeoJSON file with search AOI polygon. Extracts bbox automatically.
--start-date
date
Temporal filter start. Format: YYYY-MM-DD.
--end-date
date
Temporal filter end. Defaults to today if omitted.
--cloud-cover
range
Cloud cover range min-max in percent. E.g. 0-20.
0-100
--resolution
range
Resolution range in metres min-max. E.g. 10-30.
--providers
list
Comma-separated provider IDs. E.g. aws_earth,copernicus,usgs.
--satellites
list
Comma-separated satellite names. E.g. Sentinel-2,Landsat-8.
--processing-level
string
Processing level. E.g. L2A, L2SP.
--max-results
int
Maximum results to return.
100
--sort-by
choice
datetime, cloud_cover, score, satellite.
datetime
--sort-order
choice
asc or desc.
desc
--cql2
string
CQL2 filter expression. Sent as CQL2-JSON to STAC APIs.
--output
path
Save results to this file.
--format
choice
table, json, stac, geojson, geoparquet, csv, ids.
table
--on-provider-failure
choice
skip, abort, retry.
skip
--timeout
int
Per-provider timeout in seconds.
60
--no-cache
flag
Bypass the in-memory result cache.

Output formats

table
--format table
Pretty terminal table. Default. ID, date, cloud%, score, satellite.
json
--format json
Full JSON array with all fields. Good for scripting and piping.
stac
--format stac
STAC 1.0 ItemCollection. Compatible with pystac-client.
geojson
--format geojson
GeoJSON FeatureCollection. Open in QGIS, ArcGIS, or Leaflet.
geoparquet
--format geoparquet
GeoParquet file. Requires geopandas. Best for large result sets.
csv
--format csv
CSV with id, provider, satellite, datetime, cloud_cover, bbox.
ids
--format ids
Scene IDs only, one per line. Good for shell scripting.

CQL2 filter examples

CQL2 — supported by Planetary Computer, Element84, AWS Earth
# Cloud filter
--cql2 "eo:cloud_cover < 10"

# Platform filter
--cql2 "platform = 'sentinel-2b'"

# Combined AND filter
--cql2 "eo:cloud_cover < 5 AND platform = 'sentinel-2b'"

# Processing level
--cql2 "s2:processing_level = 'L2A'"

# Landsat tier
--cql2 "landsat:collection_category = 'T1'"
Download

Downloading Satellite Data

Parallel, resumable downloads with real-time progress, retry logic, band selection, and a post-processing chain. Progress bar updates per-scene as each completes.

download examples
# Basic — download 3 scenes from search results
pygeofetch download run \
  --from-search results.geojson \
  --output ./data/ \
  --parallel 2 \
  --max-items 3

# RGB bands only — ~150 MB vs 600 MB for full Sentinel-2 scene
pygeofetch download run \
  --from-search results.geojson \
  --output ./data/ \
  --bands "B02,B03,B04" \
  --max-items 5

# Full options — checksum, resume, bandwidth throttle, Slack notify
pygeofetch download run \
  --from-search results.geojson \
  --output ./data/ \
  --parallel 4 \
  --retry 5 \
  --verify-checksum \
  --resume \
  --bandwidth-limit 10MB \
  --priority high \
  --on-failure skip \
  --notify webhook:https://hooks.slack.com/services/YOUR/WEBHOOK

# Post-processing chain
pygeofetch download run \
  --from-search results.geojson \
  --output ./processed/ \
  --post-process "unzip,reproject:EPSG:4326,compress:lzw,cog"

# NDVI workflow — download Red + NIR, compute NDVI, export COG
pygeofetch download run \
  --from-search results.geojson \
  --bands "B04,B08" \
  --post-process "reproject:EPSG:4326,ndvi,cog" \
  --output ./ndvi/

Download flags

Flag
Type
Description
--from-search
path
GeoJSON results file from search run --output. Required.
--output
path
Output directory. Created automatically.
./pygeofetch_data
--parallel
int
Concurrent download workers.
2
--bands
list
Comma-separated band names. E.g. B02,B03,B04 — RGB only, ~75% smaller than a full scene.
--max-items
int
Limit to first N items in the results file.
Show all 17 download flags →
Flag
Type
Description
--from-search
path
GeoJSON results file from search run --output. Required.
--output
path
Output directory. Created automatically.
./pygeofetch_data
--parallel
int
Concurrent download workers.
2
--retry
int
Max retry attempts with exponential backoff.
3
--retry-delay
float
Base retry delay seconds. Doubles each attempt.
5.0
--verify-checksum
flag
SHA256 verification after each download. Auto-retries on mismatch.
--resume
flag
Resume interrupted downloads from last byte received.
on
--bandwidth-limit
string
Throttle bandwidth. E.g. 10MB, 500KB. 0 = unlimited.
--priority
choice
high, normal, low.
normal
--bands
list
Comma-separated band names. E.g. B02,B03,B04. Default: all data assets.
--post-process
string
Comma-separated processing chain. E.g. "unzip,reproject:EPSG:4326,cog".
--on-failure
choice
skip, abort, retry.
skip
--max-items
int
Limit to first N items in the results file.
--overwrite
flag
Overwrite files that already exist. Default: skip existing.
--notify
string
webhook:URL or email:ADDRESS. Repeatable for multiple targets.
--json
flag
Output results summary as JSON.

Band selection for Sentinel-2

BandsPurposeResolutionApprox Size/Scene
B02,B03,B04RGB (Blue, Green, Red)10m~150 MB
visualTrue colour composite (TCI pre-rendered)10m~200 MB
B04,B08NDVI (Red + NIR)10m~100 MB
B02,B03,B04,B08RGB + NIR (4-band)10m~200 MB
B11,B12SWIR (fire, burn scar, soil moisture)20m~50 MB
SCLScene Classification Layer (cloud mask)20m~20 MB
(omit --bands)All data bands (full scene)10/20/60m~600 MB

Post-processing actions

ActionSyntaxDescriptionRequires
unzipunzipExtract ZIP/TAR archives
reprojectreproject:EPSG:4326Reproject to target CRSrasterio
compresscompress:lzwGeoTIFF compression (lzw, deflate, zstd)rasterio
cogcogConvert to Cloud Optimized GeoTIFFrasterio
clipclip:file.geojsonClip raster to polygon boundaryrasterio
resampleresample:30Resample to target resolution (metres)rasterio
ndvindviCalculate NDVI from Red and NIR bandsrasterio
ndwindwiCalculate NDWI water indexrasterio
atmosphericatmospheric:sen2corAtmospheric correctionsen2cor
pan-sharpenpan-sharpenPan-sharpen multispectral with panchromaticrasterio
mergemergeMosaic overlapping scenesrasterio
Providers

24 Satellite Data Providers — Honest Status

Filter by capability. Note that "listed" and "verified working" are not the same thing — see the callout above and individual card statuses below.

planetary_computer🟢 Verified
Microsoft STAC catalog. Sentinel-1/2, Landsat 8/9, MODIS, NAIP, ALOS DEM. SAS tokens auto-generated.
STACSAR
aws_earth🟢 Verified
AWS Earth Open Data. Sentinel-2 COGs, Landsat Collection 2, NAIP. Direct S3 access.
STAC
element84🟢 Verified
Element 84 Earth Search v1. Sentinel-2 L2A, Landsat Col 2, Sentinel-1 RTC, COP-DEM. All COG.
STACSAR
noaa_big_data🟡 Open
GOES-16/17/18 weather imagery and NEXRAD radar. AWS Open Data registry.
esa_scihub🔴 Dead
Points to the Copernicus Open Access Hub, permanently decommissioned November 2, 2023. Use copernicus instead — it targets the live CDSE replacement.
SAR
eodag🟡 Open
EODAG gateway to 20+ providers including Theia, PEPS, Mundi, and Copernicus Alternative Data Service. Search works without auth; downloading from some backing providers may require it.
jaxa_earth🟡 Open
JAXA ALOS 30m World 3D DSM and PALSAR-2 forest/non-forest global map.
SAR
isro_bhuvan🟡 Open
ISRO Bhuvan portal. ResourceSat-2/2A (5.8m), Cartosat-1 (2.5m), Oceansat-2.
inpe_cbers🟡 Open
Brazil INPE CBERS-4 and CBERS-4A. 5m to 40m optical bands. Free download.
digitalglobe🟡 Open
Maxar Open Data Program. Sub-metre WorldView disaster response imagery for humanitarian use.
<1m
geoserver_generic🟡 Open
Generic OGC WMS/WFS/WCS connector for any GeoServer or OGC-compliant service endpoint.
usgs🟢 Verified🔐 Auth
USGS Earth Explorer. Landsat 1–9, ASTER, MODIS, EO-1. Machine-to-Machine API.
STAC
copernicus🟢 Verified🔐 Auth
Copernicus Data Space Ecosystem. Sentinel-1/2/3/5P. Full STAC API, OAuth2.
STACSAR
nasa_earthdata🔐 Auth
NASA CMR. MODIS, VIIRS, ICESat-2, GEDI, ASTER. Free registration required.
STAC
nasa_earthdata_cloud🔐 Auth
Cloud-native NASA data on AWS. Temporary S3 credentials auto-refreshed.
STAC
opentopography🔐 Auth
SRTM 30/90m, COP-DEM 30m, NASADEM, global LiDAR point clouds. Free API key.
planet🔐 Auth
Planet Labs. PlanetScope (3m daily), SkySat (50cm), RapidEye. Subscription required.
STAC<1m
sentinel_hub🟢 Verified🔐 Auth
Sentinel Hub Processing API. Sentinel-1/2/3, Landsat, custom evalscripts.
SAR
maxar_gbdx🔐 Auth
Maxar WorldView 1–4, GeoEye-1. 30cm–50cm resolution. Commercial subscription.
<1m
airbus_oneatlas🔐 Auth
Airbus Pléiades (50cm) and SPOT 6/7 (1.5m). OneAtlas STAC API.
STAC<1m
alaska_satellite_facility🔐 Auth
ASF DAAC. Sentinel-1 SLC/GRD, ALOS PALSAR, UAVSAR. Free with Earthdata login.
SAR
google_earth_engine🔐 Auth
GEE catalog proxy. Access multi-petabyte collection via service account.
SAR
terrabotics🔐 Auth
TerraBotics archive and tasking. Sub-metre commercial imagery. API key required.
<1m
earth_explorer_additional🔐 Auth
USGS Earth Explorer's declassified and historical datasets — separate from the main usgs provider's Landsat catalog.
InSAR

Full InSAR Processing Chain

pip install "pygeofetch[insar]" — coregistration, interferogram formation, phase unwrapping, and SBAS time series inversion, in pure Python. No SNAP or ISCE required for the core pipeline.

Why this matters: Core InSAR pipeline works without SNAP or ISCE — no separate software installation required. The four-step chain below runs entirely in Python, from real Sentinel-1 SLC archives to a real, verified displacement time series.
STEP 01
📡
SLC Extraction
Sub-swath extraction from .SAFE archives via embedded GCP matching.
STEP 02
🔗
Interferogram
Coregistration with ESD, formation, topographic phase removal.
STEP 03
🌀
Unwrapping
SNAPHU-based phase unwrapping via snaphu-py.
STEP 04
📈
Time Series
SBAS inversion with reference-pixel normalization.
end-to-end InSAR
from pygeofetch.insar import SLCExtractor, InterferogramGenerator, PhaseUnwrapper

extractor = SLCExtractor(polarisation="VV")
ref_tif, sec_tif = extractor.extract_pair(
    download_results[0], download_results[1], aoi=aoi, output_dir="./data",
)

gen = InterferogramGenerator()
result = gen.process_pair(ref_tif, sec_tif, dem="dem.tif")
print(f"Mean coherence: {result.coherence.mean():.3f}")

unwrapper = PhaseUnwrapper(cost_mode="defo")
unwrapped, conncomp = unwrapper.unwrap(result.interferogram, result.coherence)

SLC Extraction

Sentinel-1 IW SLC spans 3 sub-swaths per polarisation; your AOI typically falls in just one, varying per scene. SLCExtractor reads each sub-swath's embedded GCPs via rasterio and picks the one that overlaps your AOI.

DownloadResult-first design: pass DownloadResult objects directly — .output_path already holds the exact path the provider wrote to, avoiding filename/subfolder-mismatch bugs.

Interferogram Generation

Correctly handles GDAL's complex_int16 dtype — the actual format real Sentinel-1 SLC TIFFs use, not just complex64/complex128. Missing this silently discards phase data. Topographic phase removal is R²-gated (>0.5) so real deformation isn't mistaken for terrain phase.

Phase Unwrapping

SNAPHU (Chen & Zebker 2001) via snaphu-py — the same algorithm ASF's On-Demand InSAR and ISCE2/3 use in production.

SBAS Time Series

Berardino et al. (2002) SBAS inversion, with optional MintPy delegation for the full correction chain.

The reference pixel matters more than almost anything else here. Phase unwrapping only recovers phase relative to an arbitrary per-interferogram offset. Combining unwrapped interferograms without a common stable reference pixel corrupts the entire result. Validated: referencing inside a synthetic subsidence bowl gave 103mm/yr RMSE against a 100mm/yr true signal; a verified-stable reference gave 8.84mm/yr RMSE. Always pass an explicit, independently-verified reference_pixel.

Atmospheric Correction

Elevation-correlated (no extra deps, same R²-gating) or ERA5-based via PyAPS (pygeofetch[insar-full], needs free CDS API credentials).

Orbit Files

orbit files
orbit_path = client.fetch_orbit_file(
    product_name=scene.properties.get("name", scene.id),
    orbit_type="precise",
)

Served by ESA as .EOF.zip; extraction is automatic, always returning a directly-usable .EOF path.

Data Validation

DataValidator runs automatically at real pipeline entry points, not just available-but-unused: input SLC sanity checks (complex dtype, NaN, dynamic range) at the start of every process_pair() call, coherence range checks after estimation, and SBAS network connectivity checks before the expensive inversion runs.

validation (usually automatic — shown here directly)
from pygeofetch.insar import DataValidator

result = DataValidator.validate_slc(slc_array, name="reference SLC")
result.raise_if_invalid()  # clear ValueError, not a downstream numerical artifact
Catches a real failure mode a naive dtype check misses: amplitude-only data cast to a complex dtype (real + 0j) passes both the dtype check and the amplitude-variation check — it's genuinely complex64, and amplitude does vary. The validator specifically checks for near-zero imaginary part everywhere, which real SAR phase never has.

Real Orbit-Based Coregistration

TOPS mode needs ~0.001-pixel coregistration accuracy (Yagüe-Martínez et al. 2016) — the proven method for reaching it is orbit-based geometric coregistration, not a shape-matching guess. Ground points are sampled directly from a real DEM's own geographic coordinates (closed-form, always converges) and located in both the reference and secondary orbits via a real zero-Doppler time solve — verified sub-microsecond accurate even from a starting guess 30 seconds off.

real coregistration
gen = InterferogramGenerator(esd_enabled=True, use_gpu=False)
result = gen.process_pair(
    reference="slc_ref.tif", secondary="slc_sec.tif", dem="dem.tif",
    reference_safe_zip="ref.SAFE.zip", secondary_safe_zip="sec.SAFE.zip",
    reference_orbit_file=fetch_orbit_file("S1A_..._ref"),
    secondary_orbit_file=fetch_orbit_file("S1A_..._sec"),
)

Supply all four (plus a DEM) and it's used automatically — verified 49/49 grid points on a realistic scene, versus 21–28/49 failures with an earlier pixel-driven approach that was tried and abandoned. Omit any of the four and it falls back cleanly to shape-based resampling, with a clear log line stating which path ran — never a silent, unexplained accuracy difference.

Honest, documented limitation: the lower-level solve_ground_point() (an alternative, pixel-driven geolocation solve) has a known reliability gap — roughly 94% per-call, worse under repeated grid use — and is deliberately not exported as a primary API. It always fails safely (a clear error, never a silently wrong answer), but isn't recommended for unattended use. Full debugging history in pygeofetch/insar/README.md.

LOS-to-Vertical Conversion

InSAR measures line-of-sight range change — the true 3D displacement vector projected onto the satellite's single viewing direction. With one geometry, that's one equation and three unknowns, genuinely underdetermined. los_to_vertical_displacement() applies the standard literature technique (Fialko et al. 2001, Hooper et al. 2012): assume horizontal motion is negligible.

los to vertical
from pygeofetch.insar import los_to_vertical_displacement

vertical_velocity = los_to_vertical_displacement(
    ts_result.velocity, incidence_angle_deg=39.0
)
This is an assumption, not a measurement — defensible for vertically-dominated sources like mining/groundwater subsidence, actively wrong for landslides or fault creep with a real lateral component. Verified: a plausible 10mm horizontal component produces ~1.7mm of real error in the recovered "vertical" value if the assumption doesn't hold. Sentinel-1's near-polar orbit is also nearly blind to north-south motion regardless of this conversion — a genuine geometric ceiling, not a processing limitation. A rigorous decomposition needs a second, independent LOS geometry (e.g. a descending pass).

Automatic Visualization

Every stage — interferogram formation, unwrapping, SBAS inversion — can save PNGs alongside its GeoTIFFs with one flag, reusing Plotter rather than duplicating plotting logic.

auto-visualize
result.save("./output", auto_visualize=True)
# wrapped_phase.png, coherence.png, amplitude.png — alongside the GeoTIFFs

A visualization failure only logs a warning — it never blocks or loses the actual GeoTIFF output.

GPU Acceleration

Optional CuPy backend for coherence estimation and SBAS inversion's large matrix solve — the two steps that dominate processing time on large scenes.

gpu
gen = InterferogramGenerator(use_gpu=True)   # auto-detects; falls back to CPU cleanly if none found
use_gpu defaults to False — opt-in, not opt-out. The CPU path is byte-for-byte verified identical to the pre-existing implementation, and the no-GPU detection/fallback logic is directly tested; the actual CUDA execution path has not been verified against real GPU hardware.

InSARProject — Search to Interferogram in a Handful of Calls

A high-level workflow wrapper for the full search → download → extract → interferogram chain, built entirely on top of the verified lower-level pieces above — no new science, just real orchestration, with real, automatic map display at each step.

high-level workflow
from pygeofetch.insar import InSARProject
from pygeofetch.models import BoundingBox

project = InSARProject(
    name="my_aoi",
    aoi=BoundingBox(min_lon=-99.183, max_lon=-99.003, min_lat=19.278, max_lat=19.438),
    output_dir="data/my_aoi_insar",
)
project.search(start_date="2024-11-01", end_date="2025-01-15")   # real search, real map
project.download_and_extract(max_scenes=6)                        # real download + AOI crop
project.form_all_interferograms()                                 # full verified pipeline
project.summary()
Each real step shows its own map automatically — no separate visualization call to remember. If you want every burst-aware ESD, flat-earth, and unwrapping step fully explicit and visible instead of wrapped inside this convenience layer, use the lower-level SLCExtractor / InterferogramGenerator pieces directly, documented above.
SAR Processing

SARProcessor

pip install "pygeofetch[sar]" — despeckling, calibration, flood mapping, coherence, with pluggable backends.

Why this matters: The default "native" backend needs nothing extra beyond the base install — real despeckling, calibration, and flood mapping from raw SAR intensity, no separate SAR toolkit required to get started.
SAR flood mapping
from pygeofetch.sar import SARProcessor

sar = SARProcessor()
cal_pre = sar.calibrate("pre_event_vv.tif", output_type="sigma0", in_db=True)
cal_post = sar.calibrate("post_event_vv.tif", output_type="sigma0", in_db=True)

flood_result = sar.flood_map(
    str(cal_post.output_path), reference=str(cal_pre.output_path), threshold=-15.0,
)
print(f"{flood_result.metadata['water_pct']:.1f}% flagged as flooded")
BackendRequiresBest for
"native"Nothing extraDespeckle, calibrate, flood map, coherence
"sarxarray"pygeofetch[sar]xarray/Dask-native large-scale processing
"ost"pygeofetch[ost] + SNAPProduction Range-Doppler terrain correction
Processing

Spectral Indices & Landsat Extraction

pip install "pygeofetch[processor]" — 17 built-in indices, or 232+ via optional spyndex.

spectral indices
from pygeofetch.processor import SpectralIndex

si = SpectralIndex()
ndvi = si.compute("NDVI", RED=red_array, NIR=nir_array)
ndvi = si.from_files("NDVI", red="B04.tif", nir="B08.tif", output="ndvi.tif")
NDVI / EVI / SAVI
Vegetation health
NDWI / MNDWI
Surface water
NDBI
Built-up areas
NBR / dNBR
Burn severity
+ 10 more
NDSI, NDMI, BSI, ARVI, GNDVI, RVI, VCI, CRI1, PSRI

Landsat Extraction

LandsatExtractor
from pygeofetch.processor import LandsatExtractor

extractor = LandsatExtractor()
scene = extractor.process_scene(download_result, output_dir="./data")

print(scene.sensor)      # "OLI" or "TM"
red, nir = scene.get("red"), scene.get("nir")
Two easy-to-get-wrong things this handles:
  • OLI (Landsat 8/9) and TM/ETM+ (4/5/7) use different band numbers for the same wavelength — SR_B4 is Red on OLI, NIR on TM/ETM+. Sensor auto-detected, correct map applied.
  • Not every provider delivers one archive — Planetary Computer downloads each band separately. process_scene() handles both delivery styles transparently.

Radiometric scaling verified against USGS's own worked examples (DN 18639 → 0.313 reflectance; DN 44947 → 302.6K). Cloud masking decodes QA_PIXEL bits per the official Science Product Guide.

Time Series

TimeSeriesAnalyzer

from pygeofetch.processor import TimeSeriesAnalyzer — the link between "N dates of downloaded bands" and actual time-series analysis: automated per-date index computation, per-pixel trend fitting, zonal time series extraction, and anomaly detection.

multi-date trend analysis
from pygeofetch.processor import TimeSeriesAnalyzer

ts = TimeSeriesAnalyzer(index="NDVI")

# Computes NDVI for every date automatically, stacks with real georeferencing
stack = ts.build_index_stack({
    "2022-01-15": {"RED": "jan22_B04.tif", "NIR": "jan22_B08.tif"},
    "2023-01-15": {"RED": "jan23_B04.tif", "NIR": "jan23_B08.tif"},
    "2024-01-15": {"RED": "jan24_B04.tif", "NIR": "jan24_B08.tif"},
})

trend  = ts.trend(stack)                                     # per-pixel slope/year
df     = ts.zonal_timeseries(stack, "parcels.geojson")        # tidy DataFrame, all zones × all dates
anom   = ts.anomaly(stack, baseline=["2022-01-15"])           # z-score vs baseline
series = ts.zone_series(stack, "parcels.geojson", zone_id=3)  # -> Plotter.plot_timeseries() directly
MethodWhat it does
build_index_stack()Computes the configured index for every date via SpectralIndex, stacks into a (time, H, W) array with real CRS/transform preserved
trend()Vectorized per-pixel least-squares slope-per-year — not a slow per-pixel Python loop
zonal_timeseries()Per-zone value at every date as a tidy DataFrame — the "give me the time series for this AOI" operation
zone_series()Single-zone convenience wrapper, returns exactly the shape Plotter.plot_timeseries() expects
anomaly()Per-pixel z-score of a target date vs. a baseline period's mean/std

Verified against synthetic data carrying a known ground-truth trend (a declining region vs. a stable region): trend() correctly distinguished −0.158 NDVI/yr (declining) from 0.000 (stable); zonal_timeseries() correctly tracked 0.429→0.111 vs. 0.429→0.409; anomaly() correctly flagged −10.3 z-score in the declining region, and safely returns NaN rather than a garbage value when baseline variance is genuinely zero.

Preprocessing

Preprocessing Engine

Atmospheric correction, cloud masking, topographic correction, geometric operations, pan-sharpening, and multi-scene compositing — the foundational optical-data toolkit underneath the InSAR/SAR/Landsat/TimeSeries modules documented above.

preprocessing chain
# Individual operations
corrected = client.preprocess.atmos("scene.tif", method="dos1")
masked    = client.preprocess.cloud_mask("scene.tif", method="scl", scl_band="SCL.tif")
clipped   = client.preprocess.clip("scene.tif", geometry="study_area.geojson")
reproj    = client.preprocess.reproject("scene.tif", crs="EPSG:4326")

Available operations

MethodOptionsDescription
atmos()dos1, dos2, sen2cor, flaash, 6s, icorAtmospheric correction
cloud_mask()scl, fmask, threshold, ndsiCloud masking
cloud_fill()Fill cloud gaps using a multi-date time series
topo_correct()cosine, minnaert, c-correctionTopographic (terrain illumination) correction
clip()bbox or GeoJSON polygonClip to an area of interest — automatically reprojects the AOI to the raster's own CRS if they differ
reproject()any target CRSReproject (EPSG:4326, UTM zones, etc.)
resample()nearest, bilinear, cubic, lanczosChange spatial resolution
pansharpen()brovey, ihs, gram-schmidtPan-sharpen multispectral with a panchromatic band
mosaic()first, last, min, maxMerge overlapping scenes
composite()median, mean, max, best-pixelMulti-temporal compositing
clip() CRS handling, verified: a WGS84 boundary polygon (the normal format for AOI GeoJSON) clipped against a raster in its native UTM projection (the normal delivery format for real satellite imagery) is now automatically reprojected to match before masking — confirmed against a real UTM Zone 30N test raster. This previously failed silently with a near-empty intersection window rather than a clear CRS error.

Terrain Analysis (DEM / DSM / DTM)

Real hydrological and terrain-shape analysis on any DEM — each method individually verified against a known analytical or physically-realistic case before being trusted, not just run once and eyeballed.

terrain analysis
pp = client.preprocess

terrain = pp.terrain_derivatives("dem.tif")           # slope, aspect, hillshade
twi     = pp.topographic_wetness_index("dem.tif")      # real flood-susceptibility screening
curv    = pp.curvature("dem.tif")                      # concave/convex — water convergence vs. dispersion
tri     = pp.terrain_ruggedness_index("dem.tif")       # Riley et al. 1999
sinks   = pp.identify_depressions("dem.tif")           # enclosed basins with no downhill outlet
network = pp.extract_drainage_network("dem.tif")       # real channels from D8 flow accumulation
MethodBasisVerified against
terrain_derivatives()Horn-method gradientKnown cone geometry — recovered slope exact
topographic_wetness_index()Beven & Kirkby 1979, D8 flow accumulationSloped-plane flow accumulation exact; V-valley floor correctly ranked wetter than steep sides
curvature()Laplacian (∇²z)Known paraboloid bowl — constant analytical curvature recovered
terrain_ruggedness_index()Riley et al. 1999Flat surface → exact 0; checkerboard → large, correct value
identify_depressions()Morphological reconstruction (fill-then-diff)Known synthetic 20m basin — exact depth recovered
extract_drainage_network()D8 flow accumulation, thresholdedV-valley test — channel correctly concentrated at the true valley floor
Open elevation products (SRTM, Copernicus DEM) are surface models, not clean bare-earth DTMs — over forested or vegetated terrain, some canopy height is baked into the "ground" elevation. A genuine Canopy Height Model needs a real bare-earth DTM, typically from LiDAR, which isn't available through PyGeoFetch's current open-data providers. This isn't worked around by fabricating a DTM that doesn't exist — treated as an honest, stated limitation.
Postprocessing

Postprocessing

Turn a raster analysis result into GIS-ready vectors and statistics: vectorize → smooth → regularize → zonal stats → buffer → centroids → compress → COG.

postprocessing chain
vectors  = client.post.vectorize("ndvi.tif", threshold=0.3)
smoothed = client.post.smooth(vectors, tolerance=0.5)
stats    = client.post.zonal_stats("ndvi.tif", "parcels.geojson", output="stats.csv")
cog      = client.post.cog("scene.tif", compress="deflate")
MethodDescription
vectorize()Raster → vector polygons at a given threshold
smooth()Simplify/smooth polygon boundaries
zonal_stats()Per-polygon raster statistics (mean, min, max, etc.)
buffer()Buffer vector geometries by a distance
compress()Recompress a raster (LZW, DEFLATE, ZSTD)
cog()Convert to Cloud Optimized GeoTIFF
Visualization

Plotter & MapViewer

pip install "pygeofetch[viz]" — static plotting and interactive maps.

quicklook — one call for almost anything
from pygeofetch.viz import Plotter

pl = Plotter()
pl.quicklook(ndvi_array)              # index -> diverging colormap
pl.quicklook("sentinel1_sigma0.tif")  # SAR -> grayscale
pl.quicklook(flood_mask)              # categorical -> auto legend
pl.quicklook(download_result)         # DownloadResult resolved automatically

Value-range heuristics, not format detection — few distinct values → categorical; mostly-negative dB range → SAR grayscale; within [-1,1] → diverging index colormap; otherwise → continuous. All overridable via mode=.

purpose-built plots
pl.plot_comparison(
    {"Baseline": ndvi_before, "Recent": ndvi_after, "Change": ndvi_change},
    per_panel_cmap={"Change": "RdBu"}, per_panel_range={"Change": (-0.5, 0.5)},
)

pl.plot_classification(
    classified_array,
    class_labels={0: "Stable", 1: "Moderate", 2: "Severe"},
    class_colors={0: "#2ecc71", 1: "#f39c12", 2: "#e74c3c"},
)

plot_raster() and plot_rgb() accept in-memory numpy arrays directly — no round-trip through disk needed.

MapViewer

interactive maps
from pygeofetch.viz import MapViewer

mv = MapViewer(center=(6.198, -1.693), zoom=12)
mv.add_vector("boundary.geojson", layer_name="Study Area", style={"color": "#2c3e50", "fillOpacity": 0})
mv.add_raster("ndvi_change.tif", colormap="RdBu", vmin=-0.5, vmax=0.5, opacity=0.75)
mv.add_basemap("SATELLITE")
mv.save("interactive_map.html")
add_raster() needs rioxarray and localtileserver alongside leafmap — both declared in the [viz] extra.

Search Result Footprints — See Before You Download

Real search results shown on a real map before anything downloads, with real hover info (scene ID, date, satellite, provider, cloud cover) — the difference between guessing what a search returned and actually seeing it.

footprints before download
results = client.search(query, providers=["copernicus", "aws_earth"])

mv = MapViewer(center=(6.198, -1.693), zoom=9)
mv.add_basemap("SATELLITE")
mv.add_search_results(results)   # real footprints, real hover info
mv.show()                        # in Jupyter — or mv.save("results.html") elsewhere

Uses each result's real, provider-supplied geometry when available, and falls back automatically to a bounding-box rectangle when it isn't — safe to call against any provider's results, none will silently show nothing or crash on an empty result set.

Not every provider's API returns precise, non-rectangular footprint geometry (see the honest per-provider breakdown above). When it doesn't, the rectangle you see is a real, accurate bounding box — just not the provider's exact scene outline.

3D Terrain Visualization

Two options depending on what actually matters for the output: a fast, dependency-light static render, or a genuinely interactive mesh when the audience needs to rotate and explore it themselves.

3D terrain
# Fast, static, no extra dependency — hillshade-draped 3D surface
pl.plot_3d_terrain("dem.tif", drape=susceptibility, drape_colormap="Blues")

# Real interactive mesh (PyVista) — rotate/zoom/pan in-browser,
# exported as a standalone HTML file, no server needed
pl.plot_3d_terrain_interactive(
    "dem.tif", drape=twi, drape_colormap="YlGnBu",
    output="terrain_interactive.html",
)
Both hardened against a real failure mode: a near-uniform elevation field (e.g. an AOI that barely overlaps real terrain) previously rendered as a flat, confusingly "empty" plot with no explanation. Now raises a clear warning identifying the actual cause instead of a silent, confusing blank render.

plot_3d_terrain_interactive() needs pip install "pyvista[jupyter]" — the [jupyter] extra specifically for HTML export.

Split-Panel Comparison

Draggable side-by-side comparison of two rasters — e.g. a DSM against a DTM, or two independent DEM sources — backed by leafmap's own split_map(), not a new dependency.

split comparison
mv.add_split_comparison(
    "dsm.tif", "dtm.tif", left_label="DSM", right_label="DTM",
)
The interactive split view depends on localtileserver spinning up a real local background HTTP server, which is known to fail in some restricted/sandboxed network environments (firewalls, some corporate VPNs) with a ServerDownError. When that happens, add_split_comparison() automatically falls back to a static side-by-side comparison that needs no server at all — verified directly against the real error.
Pipelines

YAML Pipeline Orchestration

Define recurring satellite data workflows in a single YAML file. Schedule on cron, run ad-hoc, validate before committing, and watch live logs.

Why this matters: This pipeline can be scheduled to run weekly, replacing a manual task that used to take hours — search, filter, download, and export happen unattended, on cron, with live logs if something needs checking.
STEP 01
🔍
Search
Federated query across providers. Supports date ranges, cloud filters, CQL2.
STEP 02
🎯
Filter
Expression-based post-filter on results. Any property or scoring field.
STEP 03
⬇️
Download
Parallel, resumable download with checksum verification and retry.
STEP 04
⚙️
Post-process
Reproject, compute indices, convert to Cloud-Optimized GeoTIFF — chained actions.
STEP 05
📤
Export
Push to S3, GCS, local disk, or trigger webhook notification.

Full pipeline YAML example

weekly-sentinel2.yaml
# weekly-sentinel2.yaml
name: weekly-sentinel2-ndvi
schedule: "0 6 * * 1"  # Every Monday at 06:00 UTC
description: Weekly Sentinel-2 acquisition for NDVI monitoring

steps:
  - search:
      providers: [copernicus, aws_earth, planetary_computer]
      date_range: last_7_days
      cloud_cover: 0-10
      bbox: "-74.1,40.6,-73.7,40.9"
      max_results: 20
      sort_by: cloud_cover

  - filter:
      expression: "data.cloud_cover < 5"
      max_items: 5

  - download:
      parallel: 4
      output: ./raw/
      verify_checksum: true
      bands: "B04,B08"     # NDVI bands only
      on_failure: skip

  - post_process:
      actions: "reproject:EPSG:4326,ndvi,cog"

  - export:
      format: cloud_optimized_geotiff
      destination: s3://my-bucket/ndvi/
      notify:
        - webhook: https://hooks.slack.com/services/YOUR/WEBHOOK
        - email: ops@example.com

Pipeline CLI commands

pipeline management
# Run immediately (one-shot)
pygeofetch pipeline run weekly-sentinel2.yaml

# Validate YAML without executing
pygeofetch pipeline validate weekly-sentinel2.yaml

# Schedule for recurring execution
pygeofetch pipeline schedule weekly-sentinel2.yaml --name ndvi-monitor

# List all scheduled pipelines
pygeofetch pipeline list-scheduled

# Watch logs live
pygeofetch pipeline logs ndvi-monitor --follow

# View run history
pygeofetch pipeline history --limit 20

# Retry a failed run
pygeofetch pipeline retry RUN_ID_HERE

# Stop scheduling
pygeofetch pipeline unschedule ndvi-monitor

# Run a specific step only
pygeofetch pipeline run weekly-sentinel2.yaml --step download
Scheduling note: pipeline schedule uses the system cron daemon on Linux/macOS, and Windows Task Scheduler on Windows. Run pygeofetch pipeline list-scheduled to confirm registration.
Python API

Python API Reference

Use pygeofetch as a library in your own scripts, notebooks, or applications. The Python API gives you full programmatic control over search, download, and post-processing.

example.py — full workflow
from pathlib import Path
from pygeofetch import pygeofetch
from pygeofetch.models import SearchQuery, DownloadOptions

# Initialize client
client = pygeofetch()

# Add credentials (or use env vars)
client.add_credentials("usgs", username="user", password="pass")
client.add_credentials("planet", api_key="PL_KEY")

# Search across multiple providers
results = client.search(
    SearchQuery(
        bbox=(-74.1, 40.6, -73.7, 40.9),
        start_date="2024-01-01",
        end_date="2024-06-01",
        cloud_cover_max=20,
        max_results=50,
        sort_by="cloud_cover",
    ),
    providers=["usgs", "copernicus", "planetary_computer", "aws_earth"],
)

print(f"Found {len(results)} scenes")
for r in results[:3]:
    print(f"  {r.id} | {r.datetime} | {r.cloud_cover:.1f}% cloud")

# Download top 5 results
download_results = client.download(
    results[:5],
    destination=Path("./data/"),
    options=DownloadOptions(
        parallel=4,
        verify_checksum=True,
        resume=True,
        bands=["B02", "B03", "B04"],
        post_process=["reproject:EPSG:4326", "cog"],
    ),
)

for dr in download_results:
    if dr.success:
        print(f"  ✓ {dr.data_id} ({dr.bytes_downloaded // 1024 // 1024:.1f} MB)")
    else:
        print(f"  ✗ {dr.data_id}: {dr.error}")
SearchQuery dataclass
bbox
tuple[float,4]
Bounding box as (minlon, minlat, maxlon, maxlat). Longitude first (WGS84).
geometry
dict | None
GeoJSON geometry dict. Alternative to bbox for polygon AOIs.
start_date
str | date
Temporal filter start. ISO format string or date object.
end_date
str | date
Temporal filter end. Defaults to today.
cloud_cover_max
float
Maximum cloud cover percentage (0–100). Default: 100.
cloud_cover_min
float
Minimum cloud cover percentage. Default: 0.
resolution_min
float | None
Minimum spatial resolution in metres.
resolution_max
float | None
Maximum spatial resolution in metres.
satellites
list[str]
Satellite names to filter. E.g. ["Sentinel-2", "Landsat-8"].
processing_level
str | None
Processing level filter. E.g. "L2A".
max_results
int
Maximum number of results to return. Default: 100.
sort_by
str
"datetime", "cloud_cover", "score", "satellite". Default: "datetime".
sort_order
str
"asc" or "desc". Default: "desc".
cql2_filter
str | None
CQL2 expression string. Only applied to STAC-capable providers.

DownloadOptions parameters

DownloadOptions dataclass
parallel
int
Number of concurrent download workers. Default: 2.
retry
int
Max retry attempts with exponential backoff. Default: 3.
retry_delay
float
Base delay seconds between retries. Default: 5.0.
verify_checksum
bool
SHA256 verification after download. Default: False.
resume
bool
Resume incomplete downloads. Default: True.
bands
list[str]
Band names to download. E.g. ["B02", "B03", "B04"]. Default: all assets.
post_process
list[str]
Processing chain steps. E.g. ["reproject:EPSG:4326", "cog"].
bandwidth_limit
str | None
E.g. "10MB", "500KB". None = unlimited.
on_failure
str
"skip", "abort", or "retry". Default: "skip".
overwrite
bool
Overwrite existing files. Default: False (skip existing).

pygeofetch class methods

pygeofetch class
add_credentials(provider: str, *, username=None, password=None, api_key=None, client_id=None, client_secret=None) → None
Register credentials for a provider. Stored in system keyring. Overrides env vars.
search(query: SearchQuery, providers: list[str], timeout: int = 60, on_provider_failure: str = "skip") → list[SceneResult]
Execute federated search. Returns sorted, deduplicated list of SceneResult objects.
download(scenes: list[SceneResult], destination: Path, options: DownloadOptions) → list[DownloadResult]
Download scenes in parallel. Returns list of DownloadResult objects with success/error details.
providers() → list[ProviderInfo]
List all registered providers with authentication status and capabilities.
cache_stats() → dict
Return cache statistics: hit rate, size, entry count, TTL.
clear_cache(provider: str | None = None) → None
Clear search result cache. Pass provider name to clear one provider only.
Reference

Full CLI Reference

Complete listing of all commands, subcommands, and global options.

Global options

global flags (apply to every command)
pygeofetch [OPTIONS] COMMAND [ARGS]

Options:
  --log-level TEXT        Log level: DEBUG, INFO, WARNING, ERROR [default: INFO]
  --log-file TEXT         Write logs to file path
  --log-format TEXT       console or json [default: console]
  --config FILE           Path to config file [default: ~/.pygeofetch/config.yaml]
  --version               Show version and exit
  --help                  Show help message and exit
  --install-completion    Install shell completion (bash/zsh/fish)

All command groups

GroupSubcommandsDescription
authadd, login, list, test, remove, exportManage provider credentials
providerslist, info, searchBrowse and inspect providers
searchrun, demoSearch for satellite scenes
downloadrun, demoDownload scenes to disk
cachestats, clear, ttl, location, pruneManage search result cache
pipelinerun, validate, schedule, list-scheduled, unschedule, logs, history, retryPipeline orchestration
configshow, get, set, path, resetRead and modify configuration
statusSystem status dashboard
doctorDiagnose installation and connectivity
versionShow version info

Cache commands

cache
pygeofetch cache stats [--json]
pygeofetch cache clear [--provider PROVIDER] [--older-than 7d] [--dry-run]
pygeofetch cache ttl show
pygeofetch cache ttl set 7200
pygeofetch cache location
pygeofetch cache prune --max-size 1GB

Providers commands

providers
# List all providers
pygeofetch providers list

# Filter by auth, capability, or satellite
pygeofetch providers list --no-auth
pygeofetch providers list --capabilities sar
pygeofetch providers list --satellite Landsat

# Detailed info for one provider
pygeofetch providers info planetary_computer

# Fuzzy search across provider names and descriptions
pygeofetch providers search "landsat"

Shell completion

tab completion
# Bash
pygeofetch --install-completion bash
echo 'source ~/.pygeofetch-complete.bash' >> ~/.bashrc

# Zsh
pygeofetch --install-completion zsh
echo 'source ~/.pygeofetch-complete.zsh' >> ~/.zshrc

# Fish
pygeofetch --install-completion fish
Configuration

Configuration Reference

pygeofetch uses a layered config system. Settings are merged in order of precedence from lowest to highest.

1️⃣
Built-in defaults
Sensible defaults baked into the package. Lowest priority.
2️⃣
~/.pygeofetch/config.yaml
User-level config. Applies to all projects on this machine.
3️⃣
.pygeofetch.yaml
Project config in current directory. Overrides user config.
4️⃣
Environment variables
pygeofetch_* env vars. Override file config.
5️⃣
CLI arguments
Flags passed on the command line. Highest priority.

Full configuration file

~/.pygeofetch/config.yaml
download:
  parallel: 4
  retry_attempts: 5
  retry_delay_seconds: 1.0
  retry_max_delay_seconds: 60.0
  retry_jitter: true
  verify_checksum: false
  checksum_algorithm: sha256     # md5, sha256, sha512
  chunk_size_mb: 10
  resume: true
  bandwidth_limit_mbps: null     # null = unlimited
  overwrite_existing: false
  notify_on_completion: null     # webhook:URL or email:ADDRESS
  notify_on_failure: null
  on_failure: skip               # skip, abort, retry

cache:
  enabled: true
  ttl_seconds: 3600
  max_size_gb: 10
  location: ~/.pygeofetch/cache

search:
  default_providers: []
  max_results: 100
  timeout_seconds: 60
  on_provider_failure: skip
  sort_by: datetime
  sort_order: desc

auth:
  storage_backend: keyring       # keyring or file
  keyring_service: pygeofetch
  file_path: ~/.pygeofetch/credentials.enc

proxy:
  http_proxy: null
  https_proxy: null
  no_proxy: []

logging:
  level: INFO
  format: console                # console, json
  file: null
  max_file_size_mb: 10
  backup_count: 3

providers:
  planetary_computer:
    endpoint: https://planetarycomputer.microsoft.com/api/stac/v1
    timeout: 60
  element84:
    endpoint: https://earth-search.aws.element84.com/v1
    timeout: 60
  copernicus:
    endpoint: https://catalogue.dataspace.copernicus.eu/resto/api/
    timeout: 45
  planet:
    endpoint: https://api.planet.com/data/v1/
    rate_limit: 100
  usgs:
    endpoint: https://m2m.cr.usgs.gov/api/api/json/stable/
    timeout: 60

Config CLI commands

config commands
# Show the merged effective configuration
pygeofetch config show

# Get a specific key
pygeofetch config get download.parallel

# Set a value
pygeofetch config set download.parallel 8
pygeofetch config set cache.ttl_seconds 7200
pygeofetch config set search.default_providers "aws_earth,planetary_computer"

# Show config file path
pygeofetch config path

# Reset to defaults
pygeofetch config reset
Security

Security Model

pygeofetch is designed with credential safety, network security, and data integrity as first-class concerns.

🔑 Credential Handling
  • Credentials never written to disk in plain text
  • System keyring storage: macOS Keychain, Windows Credential Manager, Linux Secret Service
  • Encrypted file fallback (~/.pygeofetch/credentials.enc) using Fernet AES-128-CBC
  • All log filters redact passwords, tokens, and API keys
  • Credentials cleared from memory immediately after use
  • Environment variable support for CI/CD and Docker
🌐 Network Security
  • TLS 1.2+ enforced on all outbound connections
  • SSL certificate verification always on — no verify=False in codebase
  • Certificate pinning available for enterprise deployments
  • HTTP proxy support via HTTP_PROXY / HTTPS_PROXY
  • Connection timeouts enforced per provider
📦 Data Integrity
  • SHA256 checksum verification on all downloads (configurable: MD5, SHA256, SHA512)
  • Atomic file writes — partial downloads never corrupt existing data
  • Download resume tokens prevent data duplication
  • Temp files cleaned up on failure or interruption
🔒 Privacy
  • No telemetry — pygeofetch does not phone home
  • No analytics — zero usage data collection
  • No third-party requests beyond configured providers
  • All data stays local unless you configure export/webhook
Reporting vulnerabilities: Do not open public GitHub issues for security vulnerabilities. Email security@pygeofetch.dev. Response time: within 48 hours. Responsible disclosure policy: 90-day window.
Reference

Error Handling & Resilience

pygeofetch handles failures at every layer — provider outages, network interruptions, checksum mismatches, and partial failures are all managed gracefully.

Provider failures

failure policies
# Skip failing providers — return partial results from others
pygeofetch search run --providers copernicus,usgs,planet \
  --on-provider-failure skip

# Abort entirely if any provider fails
pygeofetch search run --providers copernicus,usgs \
  --on-provider-failure abort

# Auto-retry failing providers (up to 3 attempts)
pygeofetch search run --providers copernicus \
  --on-provider-failure retry

Common errors and fixes

ProviderAuthError
→ Run: pygeofetch auth test PROVIDER
Credentials missing, expired, or rejected. Re-add with auth add or set the appropriate env var.
ProviderTimeoutError
→ Use: --timeout 120
Provider API did not respond within the timeout window. Increase --timeout or check provider status page.
ChecksumMismatchError
→ Use: --retry 5 --verify-checksum
Downloaded file hash does not match provider checksum. Automatic re-download triggered up to retry limit.
NoResultsError
→ Widen: --cloud-cover 0-30
Search returned zero scenes. Relax cloud cover filter, extend date range, or check bbox coordinates (lon first).
KeyringUnavailableError
→ Set: PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
No system keyring daemon available (Docker, headless SSH). Use environment variables or file-based auth storage.
RateLimitError
→ Use: --parallel 1 --bandwidth-limit 2MB
Provider API rate limit hit. Reduce parallelism, add delays, or use bandwidth limiting. Planet and Maxar are most restrictive.
PostProcessError
→ Run: pip install "pygeofetch[geo]"
Post-processing action failed. Usually missing rasterio or GDAL. Install the [geo] extra or verify rasterio installation.

Download resilience internals

🔄
Exponential Backoff
Retries with 1s, 2s, 4s, 8s, 16s delays + random jitter. Configurable base delay and max cap. Prevents thundering herd against recovering providers.
⏸️
Resume Support
Interrupted downloads resume from the last received byte using HTTP range requests. Resume tokens stored per scene. No re-downloading of completed bytes.
🛡️
Atomic Writes
Files written to .tmp then renamed atomically on completion. Partial files never corrupt existing data. Temp files cleaned up on failure.
Circuit Breaker
Currently non-functional. A CircuitBreaker class is instantiated per-provider, but its methods are never called anywhere in the request path (verified by full-codebase grep). It contributes zero actual resilience today — treat any provider-disabling behavior as not currently true until this is wired in or removed.
🗄️
Search Caching
Search results cached per query with configurable TTL (default 1 hour). Cache hits return instantly without API calls. Auto-invalidates on expiry.
Checksum Verification
SHA256 (or MD5/SHA512) verified post-download. Mismatches trigger automatic re-download. Configurable: disable for speed, enable for integrity-critical workflows.
Docker

Docker & Containers

pygeofetch ships an official Docker image for reproducible environments, CI/CD pipelines, and scheduled pipeline execution.

Not independently verified this session: I don't have direct evidence the Docker Hub (pygeofetch/pygeofetch) or GHCR images referenced below actually exist and are published. Confirm they pull successfully before relying on this for CI/CD — if they don't exist yet, building from source (see below) is the reliable path.

Quick start

Docker quick start
docker pull pygeofetch/pygeofetch:latest

# Search — mount credentials and data output
docker run \
  -v ~/.pygeofetch:/root/.pygeofetch \
  -v $(pwd)/data:/data \
  pygeofetch/pygeofetch search run \
  --bbox "-74.1,40.6,-73.7,40.9" \
  --providers aws_earth \
  --output /data/results.geojson

# Download from saved results
docker run \
  -v ~/.pygeofetch:/root/.pygeofetch \
  -v $(pwd)/data:/data \
  pygeofetch/pygeofetch download run \
  --from-search /data/results.geojson \
  --output /data/scenes/ \
  --parallel 4

Docker Compose for scheduled pipelines

docker-compose.yml
version: '3.8'
services:
  pygeofetch-scheduler:
    image: pygeofetch/pygeofetch:latest
    volumes:
      - ~/.pygeofetch:/root/.pygeofetch
      - ./pipelines:/pipelines
      - ./data:/data
    command: pygeofetch pipeline run /pipelines/weekly-ndvi.yaml
    restart: unless-stopped
    environment:
      - pygeofetch_LOG_LEVEL=INFO
      - pygeofetch_DOWNLOAD__PARALLEL=4
      - PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
      # Credentials via env vars for headless Docker
      - pygeofetch_PLANET_API_KEY=${PLANET_API_KEY}
      - pygeofetch_COPERNICUS_USERNAME=${COPERNICUS_USER}
      - pygeofetch_COPERNICUS_PASSWORD=${COPERNICUS_PASS}

Build locally

build from source
git clone https://github.com/appiahkubis14/pygeofetch
cd pygeofetch
docker build -t pygeofetch:local .
docker run pygeofetch:local doctor
Images available on Docker Hub (pygeofetch/pygeofetch) and GitHub Container Registry (ghcr.io/appiahkubis14/pygeofetch). Tags: latest, 1.0.0, slim (no rasterio).
Testing

Testing

pygeofetch ships with a test suite covering unit, integration, and CLI end-to-end scenarios.

Not independently verified this session: the specific "60 tests," VCR cassette recording, hypothesis property-based testing, and 80% coverage gate / Codecov claims below are from the project's own reference material, not something re-confirmed against the current test suite in this session. Run pytest tests/ -v yourself to see the current real count and pass rate.
running tests
# Install dev dependencies
pip install -e ".[dev,all]"

# Run all tests
pytest tests/ -v

# With coverage report
pytest tests/ -v --cov=pygeofetch --cov-report=html
open htmlcov/index.html

# Unit tests only (fast, no network)
pytest tests/unit/ -v

# Integration tests (requires real credentials)
pytest tests/integration/ -v --run-integration

# Run a specific test file
pytest tests/unit/test_search.py -v

# With hypothesis property-based testing
pytest tests/property/ -v --hypothesis-seed=12345

Testing strategy

🧪
Unit Tests
Every provider, utility, and model has dedicated unit tests. Mocked HTTP responses using responses and httpx mocks. No network required.
📼
VCR Recordings
HTTP interactions recorded with pytest-vcr for deterministic replay. Cassettes checked into the repo — CI never hits real APIs.
🎲
Property-Based Testing
Edge cases generated automatically with hypothesis. Covers bbox validation, date parsing, band selection, and config merging.
🖥️
CLI Tests
Full workflow tests with Click's CliRunner. Search → save → download → post-process pipelines tested end-to-end without spawning subprocesses.
🌐
Integration Tests
Optional real-API tests gated behind --run-integration. Require credentials. Run in CI with secret injection from GitHub Actions.
📊
Coverage Gate
80% line coverage minimum enforced in CI. PRs failing the threshold are blocked. Coverage report uploaded to Codecov on each merge.

Contributing tests

test structure
tests/
├── unit/
│   ├── test_search.py         # SearchQuery, provider adapters
│   ├── test_download.py       # DownloadOptions, resume, checksum
│   ├── test_auth.py           # Keyring, env vars, credential storage
│   ├── test_pipeline.py       # YAML parsing, step execution
│   ├── test_postprocess.py    # Processing chain actions
│   └── test_config.py         # Layered config merging
├── integration/
│   ├── test_aws_earth.py      # Live AWS Earth Open Data
│   ├── test_planetary.py      # Live Planetary Computer
│   └── conftest.py
├── property/
│   └── test_models.py         # hypothesis tests
├── cli/
│   └── test_cli_workflow.py   # CliRunner end-to-end
└── cassettes/                 # VCR HTTP recordings
Roadmap

Roadmap

Planned features by version. Vote on priorities at github.com/appiahkubis14/pygeofetch/discussions.

v1.0.0
Production Release
✓ Released — Q2 2025
  • 22 provider integrations
  • YAML pipeline orchestration
  • Keyring credential management
  • 7 output formats including GeoParquet
  • Post-processing chain (NDVI, COG, reproject)
  • 60 test suite with CI
  • Docker image
v0.2.0
Extended Providers
◐ In Progress — Q4 2024
  • BlackSky provider
  • SI Imaging Services (KOMPSAT)
  • Interactive search mode (--interactive)
  • Slack, Discord, Teams webhook templates
  • Streaming COG partial reads
v0.3.0
Dashboard & API Server
○ Planned — Q1 2025
  • Web dashboard for pipeline monitoring
  • REST API server mode (pygeofetch serve)
  • Automatic provider health monitoring
  • Bandwidth scheduling by time of day

Why pygeofetch vs alternatives?

Feature pygeofetch EODAG pystac-client satpy sentinelsat
Providers2410+STAC onlyLimitedSentinel only
Full CLIBasic
Pipeline / YAML
Keyring AuthPartial
Parallel Downloads✓ Adaptive
STAC Output✓ Native
GeoParquet
Docker
Cron Scheduler
Webhooks
Planet / Maxar