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
copernicus instead. See the Providers section for the honest per-provider breakdown.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.
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:
Step 3 reads that same file and downloads the top-ranked scene:
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 --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:
Before committing to a download, see real footprints and hover for scene details on an actual map. See Search Footprints.
NDVI, water indices, burn severity, and 14 more — a single line each. See Spectral Indices.
Real despeckling, calibration, and the full InSAR chain, both are genuinely different from optical workflows. See SARProcessor or InSAR Processing.
Turn a one-off search into a repeatable, scheduled pipeline. See YAML Pipeline Orchestration.
Auth errors, timeouts, zero results, keyring issues on Docker/SSH — real, common first-run problems with real fixes. See Common Errors and Fixes.
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.
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.
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.
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.
!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
Managing Provider Credentials
Credentials are stored in your system keyring — never in plain-text files. Supports username/password, API keys, and OAuth2 client credentials.
# 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
# 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
PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring and use environment variables instead.Provider auth types
| Provider | Auth Type | Free? | Register At |
|---|---|---|---|
usgs | Username / Password | ✓ Free | Register → |
copernicus | Username / Password (OAuth2) | ✓ Free | Register → |
nasa_earthdata | Username / Password | ✓ Free | Register → |
nasa_earthdata_cloud | Username / Password + S3 creds | ✓ Free | Register → |
planet | API Key | Subscription | Register → |
sentinel_hub | OAuth2 client credentials | Freemium | Register → |
opentopography | API Key | ✓ Free tier | Register → |
maxar_gbdx | API Token | Subscription | Register → |
airbus_oneatlas | API Key | Subscription | Register → |
alaska_satellite_facility | Earthdata (same as NASA) | ✓ Free | Register → |
google_earth_engine | Service Account JSON | Free tier | Register → |
terrabotics | API Key | Subscription | Register → |
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.
# 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
"minlon,minlat,maxlon,maxlat". Longitude first.YYYY-MM-DD.min-max in percent. E.g. 0-20.aws_earth,copernicus,usgs.Show all 18 search flags →
"minlon,minlat,maxlon,maxlat". Longitude first. Alternative to --geometry-file.YYYY-MM-DD.min-max in percent. E.g. 0-20.min-max. E.g. 10-30.aws_earth,copernicus,usgs.Sentinel-2,Landsat-8.L2A, L2SP.datetime, cloud_cover, score, satellite.asc or desc.skip, abort, retry.Output formats
CQL2 filter examples
# 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'"
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.
# 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
search run --output. Required.B02,B03,B04 — RGB only, ~75% smaller than a full scene.Show all 17 download flags →
search run --output. Required.10MB, 500KB. 0 = unlimited.high, normal, low.B02,B03,B04. Default: all data assets."unzip,reproject:EPSG:4326,cog".skip, abort, retry.webhook:URL or email:ADDRESS. Repeatable for multiple targets.Band selection for Sentinel-2
| Bands | Purpose | Resolution | Approx Size/Scene |
|---|---|---|---|
B02,B03,B04 | RGB (Blue, Green, Red) | 10m | ~150 MB |
visual | True colour composite (TCI pre-rendered) | 10m | ~200 MB |
B04,B08 | NDVI (Red + NIR) | 10m | ~100 MB |
B02,B03,B04,B08 | RGB + NIR (4-band) | 10m | ~200 MB |
B11,B12 | SWIR (fire, burn scar, soil moisture) | 20m | ~50 MB |
SCL | Scene Classification Layer (cloud mask) | 20m | ~20 MB |
| (omit --bands) | All data bands (full scene) | 10/20/60m | ~600 MB |
Post-processing actions
| Action | Syntax | Description | Requires |
|---|---|---|---|
unzip | unzip | Extract ZIP/TAR archives | — |
reproject | reproject:EPSG:4326 | Reproject to target CRS | rasterio |
compress | compress:lzw | GeoTIFF compression (lzw, deflate, zstd) | rasterio |
cog | cog | Convert to Cloud Optimized GeoTIFF | rasterio |
clip | clip:file.geojson | Clip raster to polygon boundary | rasterio |
resample | resample:30 | Resample to target resolution (metres) | rasterio |
ndvi | ndvi | Calculate NDVI from Red and NIR bands | rasterio |
ndwi | ndwi | Calculate NDWI water index | rasterio |
atmospheric | atmospheric:sen2cor | Atmospheric correction | sen2cor |
pan-sharpen | pan-sharpen | Pan-sharpen multispectral with panchromatic | rasterio |
merge | merge | Mosaic overlapping scenes | rasterio |
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.
copernicus instead — it targets the live CDSE replacement.usgs provider's Landsat catalog.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.
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.
.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.
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_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.
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
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.
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.
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.
from pygeofetch.insar import los_to_vertical_displacement
vertical_velocity = los_to_vertical_displacement(
ts_result.velocity, incidence_angle_deg=39.0
)
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.
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.
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.
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()
SLCExtractor / InterferogramGenerator pieces directly, documented above.SARProcessor
pip install "pygeofetch[sar]" — despeckling, calibration, flood mapping, coherence, with pluggable backends.
"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.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")
| Backend | Requires | Best for |
|---|---|---|
"native" | Nothing extra | Despeckle, calibrate, flood map, coherence |
"sarxarray" | pygeofetch[sar] | xarray/Dask-native large-scale processing |
"ost" | pygeofetch[ost] + SNAP | Production Range-Doppler terrain correction |
Spectral Indices & Landsat Extraction
pip install "pygeofetch[processor]" — 17 built-in indices, or 232+ via optional spyndex.
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")
Landsat Extraction
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")
- 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.
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.
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
| Method | What 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 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.
# 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
| Method | Options | Description |
|---|---|---|
atmos() | dos1, dos2, sen2cor, flaash, 6s, icor | Atmospheric correction |
cloud_mask() | scl, fmask, threshold, ndsi | Cloud masking |
cloud_fill() | — | Fill cloud gaps using a multi-date time series |
topo_correct() | cosine, minnaert, c-correction | Topographic (terrain illumination) correction |
clip() | bbox or GeoJSON polygon | Clip to an area of interest — automatically reprojects the AOI to the raster's own CRS if they differ |
reproject() | any target CRS | Reproject (EPSG:4326, UTM zones, etc.) |
resample() | nearest, bilinear, cubic, lanczos | Change spatial resolution |
pansharpen() | brovey, ihs, gram-schmidt | Pan-sharpen multispectral with a panchromatic band |
mosaic() | first, last, min, max | Merge overlapping scenes |
composite() | median, mean, max, best-pixel | Multi-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.
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
| Method | Basis | Verified against |
|---|---|---|
terrain_derivatives() | Horn-method gradient | Known cone geometry — recovered slope exact |
topographic_wetness_index() | Beven & Kirkby 1979, D8 flow accumulation | Sloped-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. 1999 | Flat 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, thresholded | V-valley test — channel correctly concentrated at the true valley floor |
Postprocessing
Turn a raster analysis result into GIS-ready vectors and statistics: vectorize → smooth → regularize → zonal stats → buffer → centroids → compress → COG.
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")
| Method | Description |
|---|---|
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 |
Plotter & MapViewer
pip install "pygeofetch[viz]" — static plotting and interactive maps.
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=.
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
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.
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.
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.
# 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", )
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.
mv.add_split_comparison(
"dsm.tif", "dtm.tif", left_label="DSM", right_label="DTM",
)
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.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.
Full pipeline YAML example
# 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
# 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
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 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.
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 parameters
["Sentinel-2", "Landsat-8"]."L2A"."datetime", "cloud_cover", "score", "satellite". Default: "datetime"."asc" or "desc". Default: "desc".DownloadOptions parameters
["B02", "B03", "B04"]. Default: all assets.["reproject:EPSG:4326", "cog"]."10MB", "500KB". None = unlimited."skip", "abort", or "retry". Default: "skip".pygeofetch class methods
Full CLI Reference
Complete listing of all commands, subcommands, and global options.
Global options
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
| Group | Subcommands | Description |
|---|---|---|
auth | add, login, list, test, remove, export | Manage provider credentials |
providers | list, info, search | Browse and inspect providers |
search | run, demo | Search for satellite scenes |
download | run, demo | Download scenes to disk |
cache | stats, clear, ttl, location, prune | Manage search result cache |
pipeline | run, validate, schedule, list-scheduled, unschedule, logs, history, retry | Pipeline orchestration |
config | show, get, set, path, reset | Read and modify configuration |
status | — | System status dashboard |
doctor | — | Diagnose installation and connectivity |
version | — | Show version info |
Cache commands
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
# 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
# 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 Reference
pygeofetch uses a layered config system. Settings are merged in order of precedence from lowest to highest.
pygeofetch_* env vars. Override file config.Full configuration file
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
# 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 Model
pygeofetch is designed with credential safety, network security, and data integrity as first-class concerns.
- 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
- TLS 1.2+ enforced on all outbound connections
- SSL certificate verification always on — no
verify=Falsein codebase - Certificate pinning available for enterprise deployments
- HTTP proxy support via
HTTP_PROXY/HTTPS_PROXY - Connection timeouts enforced per provider
- 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
- 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
Error Handling & Resilience
pygeofetch handles failures at every layer — provider outages, network interruptions, checksum mismatches, and partial failures are all managed gracefully.
Provider failures
# 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
auth add or set the appropriate env var.--timeout or check provider status page.[geo] extra or verify rasterio installation.Download resilience internals
.tmp then renamed atomically on completion. Partial files never corrupt existing data. Temp files cleaned up on failure.Docker & Containers
pygeofetch ships an official Docker image for reproducible environments, CI/CD pipelines, and scheduled pipeline execution.
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 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
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
git clone https://github.com/appiahkubis14/pygeofetch cd pygeofetch docker build -t pygeofetch:local . docker run pygeofetch:local doctor
pygeofetch/pygeofetch) and GitHub Container Registry (ghcr.io/appiahkubis14/pygeofetch). Tags: latest, 1.0.0, slim (no rasterio).Testing
pygeofetch ships with a test suite covering unit, integration, and CLI end-to-end scenarios.
pytest tests/ -v yourself to see the current real count and pass rate.# 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
responses and httpx mocks. No network required.pytest-vcr for deterministic replay. Cassettes checked into the repo — CI never hits real APIs.hypothesis. Covers bbox validation, date parsing, band selection, and config merging.CliRunner. Search → save → download → post-process pipelines tested end-to-end without spawning subprocesses.--run-integration. Require credentials. Run in CI with secret injection from GitHub Actions.Contributing tests
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
Planned features by version. Vote on priorities at github.com/appiahkubis14/pygeofetch/discussions.
- 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
- BlackSky provider
- SI Imaging Services (KOMPSAT)
- Interactive search mode (--interactive)
- Slack, Discord, Teams webhook templates
- Streaming COG partial reads
- 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 |
|---|---|---|---|---|---|
| Providers | 24 | 10+ | STAC only | Limited | Sentinel only |
| Full CLI | ✓ | — | — | — | Basic |
| Pipeline / YAML | ✓ | — | — | — | — |
| Keyring Auth | ✓ | Partial | — | — | ✓ |
| Parallel Downloads | ✓ Adaptive | ✓ | — | — | — |
| STAC Output | ✓ Native | — | ✓ | — | — |
| GeoParquet | ✓ | — | — | — | — |
| Docker | ✓ | — | — | — | — |
| Cron Scheduler | ✓ | — | — | — | — |
| Webhooks | ✓ | — | — | — | — |
| Planet / Maxar | ✓ | — | — | — | — |