PyGeoFetch 2.6.1__tar.gz → 2.6.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/CHANGELOG.md +5 -5
  2. {pygeofetch-2.6.1/pygeofetch.egg-info → pygeofetch-2.6.2.1}/PKG-INFO +25 -28
  3. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/README.md +23 -27
  4. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/__init__.py +1 -1
  5. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/cli/auth_commands.py +6 -3
  6. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/cli/main.py +10 -6
  7. pygeofetch-2.6.2.1/pygeofetch/cli/monitor_commands.py +141 -0
  8. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/cli/search_commands.py +1 -1
  9. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/core/authenticator.py +5 -3
  10. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/core/downloader.py +31 -13
  11. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/core/engine.py +1 -1
  12. pygeofetch-2.6.2.1/pygeofetch/core/monitor.py +164 -0
  13. pygeofetch-2.6.2.1/pygeofetch/core/state.py +246 -0
  14. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/__init__.py +175 -48
  15. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/analysis.py +62 -40
  16. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/annotation.py +116 -70
  17. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/atmosphere.py +168 -70
  18. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/coregister.py +126 -58
  19. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/deburst.py +39 -12
  20. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/esd.py +65 -28
  21. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/extraction.py +126 -67
  22. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/flatearth.py +45 -18
  23. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/geolocation.py +176 -27
  24. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/gpu.py +3 -1
  25. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/interferogram.py +502 -193
  26. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/ionosphere.py +118 -45
  27. pygeofetch-2.6.2.1/pygeofetch/insar/offset_geometry.py +203 -0
  28. pygeofetch-2.6.2.1/pygeofetch/insar/offset_tracking.py +440 -0
  29. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/preflight.py +288 -169
  30. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/provenance.py +2 -1
  31. pygeofetch-2.6.2.1/pygeofetch/insar/ps_selection.py +359 -0
  32. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/splitspectrum_diagnostic.py +12 -4
  33. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/stack_selection.py +179 -77
  34. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/synthetic.py +53 -17
  35. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/timeseries.py +663 -268
  36. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/unwrap.py +408 -156
  37. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/validate.py +146 -9
  38. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/visualize.py +15 -5
  39. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/insar/workflow.py +102 -29
  40. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/meta.yaml +1 -1
  41. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/models/satellite_data.py +5 -3
  42. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/processing/indices.py +5 -0
  43. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/processing/preprocessor.py +55 -25
  44. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/processing/sar.py +13 -4
  45. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/processor/indices.py +4 -0
  46. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/processor/stacker.py +4 -0
  47. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/processor/timeseries.py +41 -13
  48. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/airbus_oneatlas.py +17 -4
  49. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/alaska_satellite_facility.py +5 -2
  50. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/aws_earth.py +1 -1
  51. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/copernicus.py +19 -8
  52. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/copernicus_nodes.py +73 -23
  53. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/digitalglobe.py +5 -2
  54. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/earth_explorer_additional.py +5 -2
  55. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/element84.py +1 -1
  56. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/eodag_provider.py +9 -5
  57. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/esa_scihub.py +17 -4
  58. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/geoserver_generic.py +5 -2
  59. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/google_earth_engine.py +17 -4
  60. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/inpe_cbers.py +5 -2
  61. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/isro_bhuvan.py +5 -2
  62. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/jaxa_earth.py +5 -2
  63. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/maxar_gbdx.py +5 -2
  64. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/nasa_earthdata.py +6 -4
  65. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/nasa_earthdata_cloud.py +2 -2
  66. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/noaa_big_data.py +5 -2
  67. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/opentopography.py +6 -2
  68. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/planet.py +4 -3
  69. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/planetary_computer.py +1 -1
  70. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/sentinel_hub.py +11 -7
  71. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/terrabotics.py +4 -3
  72. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/usgs.py +20 -5
  73. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/sar/__init__.py +1 -1
  74. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/sar/_native.py +1 -1
  75. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/sar/extraction.py +8 -4
  76. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/utils/gcp_recovery.py +1 -3
  77. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/viz/map.py +82 -33
  78. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/viz/plot.py +91 -38
  79. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1/pygeofetch.egg-info}/PKG-INFO +25 -28
  80. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch.egg-info/SOURCES.txt +13 -4
  81. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch.egg-info/requires.txt +1 -0
  82. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pyproject.toml +9 -2
  83. pygeofetch-2.6.1/tests/test_atmospheric_circular_regression (1).py → pygeofetch-2.6.2.1/tests/test_atmospheric_circular_regression.py +21 -6
  84. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_build_sbas_network.py +68 -38
  85. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_burst_metadata.py +20 -6
  86. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_burst_processing_integration.py +110 -31
  87. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_burst_sync_large_gap.py +49 -35
  88. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_burst_sync_prescreening.py +68 -43
  89. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_burst_synchronization.py +56 -36
  90. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_copernicus_nodes.py +63 -36
  91. pygeofetch-2.6.2.1/tests/test_coregister_integration.py +353 -0
  92. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_coregister_upgrades.py +110 -47
  93. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_era5_atmospheric_correction.py +17 -6
  94. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_extraction.py +46 -12
  95. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_flat_earth_phase.py +72 -24
  96. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_gcp_reprojection.py +28 -8
  97. pygeofetch-2.6.2.1/tests/test_insar.py +691 -0
  98. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_ionosphere.py +102 -33
  99. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_map_preview_consolidation.py +41 -24
  100. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_models.py +10 -3
  101. pygeofetch-2.6.2.1/tests/test_monitor.py +134 -0
  102. pygeofetch-2.6.2.1/tests/test_offset_tracking.py +273 -0
  103. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_pgv_integration_contract.py +42 -42
  104. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_preflight_gate.py +398 -107
  105. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_provider_geometry_audit.py +26 -12
  106. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_provider_scene_disambiguation.py +31 -12
  107. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_providers.py +1 -0
  108. pygeofetch-2.6.2.1/tests/test_ps_selection.py +299 -0
  109. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_raster_collocation_integration.py +75 -36
  110. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_reference_pixel_and_despike.py +64 -38
  111. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_risk_mapping.py +99 -43
  112. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_screen_stack_orchestration.py +68 -41
  113. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_search_download.py +3 -1
  114. pygeofetch-2.6.2.1/tests/test_sign_convention.py +209 -0
  115. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_splitspectrum_diagnostic.py +7 -2
  116. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_stack_selection_consolidation.py +104 -45
  117. pygeofetch-2.6.2.1/tests/test_state.py +132 -0
  118. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_topo_r2_bug.py +42 -21
  119. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_utils.py +2 -1
  120. pygeofetch-2.6.2.1/tests/test_wls_inversion.py +237 -0
  121. pygeofetch-2.6.1/tests/test_atmospheric_circular_regression.py +0 -104
  122. pygeofetch-2.6.1/tests/test_coregister_integration.py +0 -354
  123. pygeofetch-2.6.1/tests/test_insar.py +0 -578
  124. pygeofetch-2.6.1/tests/test_preflight_gate (1).py +0 -319
  125. pygeofetch-2.6.1/tests/test_provider_geometry_audit (1).py +0 -200
  126. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/LICENSE +0 -0
  127. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/MANIFEST.in +0 -0
  128. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/cli/__init__.py +0 -0
  129. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/cli/config_commands.py +0 -0
  130. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/cli/download_commands.py +0 -0
  131. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/cli/index_commands.py +0 -0
  132. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/cli/pipeline_process_commands.py +0 -0
  133. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/cli/postprocess_commands.py +0 -0
  134. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/cli/preprocess_commands.py +0 -0
  135. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/cli/sar_commands.py +0 -0
  136. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/config/__init__.py +0 -0
  137. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/config/defaults.yaml +0 -0
  138. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/config/settings.py +0 -0
  139. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/core/__init__.py +0 -0
  140. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/core/cache_manager.py +0 -0
  141. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/core/logging.py +0 -0
  142. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/core/orbits.py +0 -0
  143. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/core/scheduler.py +0 -0
  144. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/core/searcher.py +0 -0
  145. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/models/__init__.py +0 -0
  146. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/models/download_task.py +0 -0
  147. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/models/search_query.py +0 -0
  148. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/models/user_auth.py +0 -0
  149. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/processing/__init__.py +0 -0
  150. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/processing/base.py +0 -0
  151. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/processing/batch.py +0 -0
  152. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/processing/pipeline.py +0 -0
  153. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/processing/postprocessor.py +0 -0
  154. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/processor/__init__.py +0 -0
  155. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/processor/landsat.py +0 -0
  156. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/processor/loader.py +0 -0
  157. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/__init__.py +0 -0
  158. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/providers/base.py +0 -0
  159. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/sar/_ost.py +0 -0
  160. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/sar/_sarxarray.py +0 -0
  161. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/sar/processor.py +0 -0
  162. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/utils/__init__.py +0 -0
  163. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/utils/file_utils.py +0 -0
  164. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/utils/geo_utils.py +0 -0
  165. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/utils/logging_setup.py +0 -0
  166. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/utils/retry_handler.py +0 -0
  167. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/utils/validators.py +0 -0
  168. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch/viz/__init__.py +0 -0
  169. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch.egg-info/dependency_links.txt +0 -0
  170. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch.egg-info/entry_points.txt +0 -0
  171. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch.egg-info/not-zip-safe +0 -0
  172. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/pygeofetch.egg-info/top_level.txt +0 -0
  173. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/setup.cfg +0 -0
  174. {pygeofetch-2.6.1 → pygeofetch-2.6.2.1}/tests/test_viz_plot.py +0 -0
@@ -1,9 +1,9 @@
1
1
  # Changelog
2
2
 
3
3
  All notable changes to PyGeoFetch are documented here.
4
- Format follows [Keep a Changelog](https://keepachangelog.com/en/2.6.1/).
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/2.6.2.1/).
5
5
 
6
- ## [2.6.1] — 2026-07-29
6
+ ## [2.6.2.1] — 2026-07-29
7
7
 
8
8
  ### Added
9
9
  - Major SNAPHU unwrapping improvements for urban InSAR applications
@@ -72,7 +72,7 @@ unwrapped = unwrapper.unwrap(
72
72
 
73
73
  ---
74
74
 
75
- ## [2.6.1] — 2026-07-12
75
+ ## [2.6.2.1] — 2026-07-12
76
76
 
77
77
  ### Added
78
78
  - Federated search across 22+ satellite data providers
@@ -188,8 +188,8 @@ unwrapped = unwrapper.unwrap(
188
188
 
189
189
  | Version | Date | Key Changes |
190
190
  |---------|------------|-------------|
191
- | **2.6.1** | 2026-07-29 | SNAPHU unwrapping fixes, 87.1% reliable pixels |
192
- | **2.6.1** | 2026-07-12 | 22 providers, 17 indices, Sentinel-1C/D |
191
+ | **2.6.2.1** | 2026-07-29 | SNAPHU unwrapping fixes, 87.1% reliable pixels |
192
+ | **2.6.2.1** | 2026-07-12 | 22 providers, 17 indices, Sentinel-1C/D |
193
193
  | **1.9.2** | 2026-06-15 | Batch processing, parallel downloads |
194
194
  | **1.9.1** | 2026-05-20 | 3D terrain, SNAPHU integration, time series |
195
195
  | **1.9.0** | 2026-04-25 | Initial public release |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygeofetch
3
- Version: 2.6.1
3
+ Version: 2.6.2.1
4
4
  Summary: Universal satellite data pipeline + geospatial processing platform
5
5
  Author-email: Samuel Appiah Kubi <appiahkubis14@gmail.com>
6
6
  License: MIT License
@@ -121,6 +121,7 @@ Requires-Dist: pytest>=8.0; extra == "dev"
121
121
  Requires-Dist: pytest-cov>=4.1; extra == "dev"
122
122
  Requires-Dist: pytest-mock>=3.12; extra == "dev"
123
123
  Requires-Dist: ruff>=0.3; extra == "dev"
124
+ Requires-Dist: black<25.0,>=24.0; extra == "dev"
124
125
  Requires-Dist: mypy>=1.8; extra == "dev"
125
126
  Requires-Dist: build>=1.0; extra == "dev"
126
127
  Requires-Dist: twine>=5.0; extra == "dev"
@@ -135,11 +136,10 @@ Dynamic: license-file
135
136
 
136
137
  <div align="center">
137
138
 
138
- <img src="https://raw.githubusercontent.com/EOCoreINT/pygeofetch/main/icon/pygeofetch_logo.png" alt="PyGeoFetch Logo" width="350">
139
-
140
139
  [![PyPI version](https://badge.fury.io/py/pygeofetch.svg)](https://pypi.org/project/pygeofetch/)
141
140
  [![Python Versions](https://img.shields.io/pypi/pyversions/pygeofetch.svg)](https://pypi.org/project/pygeofetch/)
142
141
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
142
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22087230.svg)](https://doi.org/10.5281/zenodo.22087230)
143
143
  [![Tests](https://github.com/appiahkubis14/pygeofetch/actions/workflows/tests.yml/badge.svg)](https://github.com/appiahkubis14/pygeofetch/actions)
144
144
  [![Coverage](https://codecov.io/gh/appiahkubis14/pygeofetch/branch/main/graph/badge.svg)](https://codecov.io/gh/appiahkubis14/pygeofetch)
145
145
 
@@ -147,6 +147,10 @@ Dynamic: license-file
147
147
 
148
148
  [Quick Start](#-quick-start) · [Mexico City Case Study](#-case-study-mapping-mexico-city-land-subsidence-with-insar) · [Case Study Repo ↗](https://github.com/EOCoreINT/mexico-subsidence-project) · [Documentation](https://appiahkubis14.github.io/pygeofetch-docs/) · [Notebooks](#-notebooks)
149
149
 
150
+ <br>
151
+ <img src="https://raw.githubusercontent.com/EOCoreINT/pygeofetch/main/icon/pygeofetch_logo.png" alt="PyGeoFetch Logo" width="350">
152
+ <br>
153
+
150
154
  </div>
151
155
 
152
156
  ---
@@ -213,7 +217,7 @@ Every provider has been directly verified to return at minimum a correct boundin
213
217
  pip install pygeofetch # core — free providers work immediately
214
218
  pip install "pygeofetch[geo]" # + rasterio, geopandas, shapely
215
219
  pip install "pygeofetch[insar]" # + native SBAS inversion, phase unwrapping
216
- pip install "pygeofetch[insar-full]" # + MintPy passthrough for advanced corrections
220
+ pip install "pygeofetch[insar-full]" # + For Linux Users
217
221
  pip install "pygeofetch[all]" # everything
218
222
  ```
219
223
 
@@ -365,29 +369,22 @@ Run `pygeofetch --help` or any subcommand with `--help` for the complete, curren
365
369
 
366
370
  ---
367
371
 
368
- <!-- ## 📚 Notebooks
369
-
370
- | Notebook | Topics |
371
- |---|---|
372
- | `01_getting_started.ipynb` | Install, doctor, first search, first download |
373
- | `02_authentication_and_providers.ipynb` | All 24 providers, credentials, capability filters |
374
- | `03_advanced_search.ipynb` | Federated search, CQL2 filters, output formats, caching |
375
- | `04_download_and_postprocessing.ipynb` | Band selection, parallel downloads, post-processing |
376
- | `05_pipelines_and_scheduling.ipynb` | YAML pipelines, scheduling, Python builder API |
377
- | `06_real_world_workflows.ipynb` | NDVI time series, change detection, multi-sensor fusion |
378
- | `07_copernicus_and_authenticated_providers.ipynb` | Copernicus, USGS, NASA, Planet, ASF, OpenTopography |
379
- | `08_cli_complete_reference.ipynb` | Every CLI command with runnable examples |
380
- | `09_processing_complete.ipynb` | Full processing engine: preprocessing, indices, SAR, pipelines |
381
- | [`mexico_city_full_confirmed (2).ipynb`](<https://github.com/EOCoreINT/mexico-subsidence-project/blob/main/mexico_city_full_confirmed%20(2).ipynb>) ↗ | The [Mexico City case study](#-case-study-mapping-mexico-city-land-subsidence-with-insar) above, in a separate companion repo — real search/download, full InSAR chain, SBAS inversion |
382
- | `piton_fournaise_full.ipynb` | Verified InSAR pipeline against the real April 2021 Piton de la Fournaise eruption |
383
- | `amatrice_full.ipynb` | Verified InSAR pipeline against the real 2016 Amatrice, Italy earthquake |
384
- | `provider_search_footprint_test.ipynb` | Search and footprint-map validation across all 24 providers |
385
-
386
- ```bash
387
- cd notebooks/ && jupyter lab
388
- ``` -->
389
-
390
- ---
372
+ ## 📚 Citation
373
+
374
+ If you use PyGeoFetch in your research or operational work, please cite it using the following:
375
+
376
+ ### Software Citation
377
+ ```bibtex
378
+ @software{pygeofetch_2026,
379
+ author = {Appiah Kubi, Samuel},
380
+ title = {PyGeoFetch: A Unified Python Framework for Multi-Provider Satellite Data Acquisition, Pre-download Quality Control, and Geospatial Processing},
381
+ year = {2026},
382
+ publisher = {Zenodo},
383
+ version = {2.6.2.1},
384
+ doi = {10.5281/zenodo.22087230},
385
+ url = {https://doi.org/10.5281/zenodo.22087230}
386
+ }
387
+ ```
391
388
 
392
389
  ## Documentation
393
390
 
@@ -400,7 +397,7 @@ Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Good first
400
397
  ```bash
401
398
  git clone git@github.com:EOCoreINT/pygeofetch.git && cd pygeofetch
402
399
  pip install -e ".[dev,all]" && pytest tests/unit/ -v
403
- ```
400
+
404
401
 
405
402
  ## License
406
403
 
@@ -1,10 +1,9 @@
1
1
  <div align="center">
2
2
 
3
- <img src="https://raw.githubusercontent.com/EOCoreINT/pygeofetch/main/icon/pygeofetch_logo.png" alt="PyGeoFetch Logo" width="350">
4
-
5
3
  [![PyPI version](https://badge.fury.io/py/pygeofetch.svg)](https://pypi.org/project/pygeofetch/)
6
4
  [![Python Versions](https://img.shields.io/pypi/pyversions/pygeofetch.svg)](https://pypi.org/project/pygeofetch/)
7
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22087230.svg)](https://doi.org/10.5281/zenodo.22087230)
8
7
  [![Tests](https://github.com/appiahkubis14/pygeofetch/actions/workflows/tests.yml/badge.svg)](https://github.com/appiahkubis14/pygeofetch/actions)
9
8
  [![Coverage](https://codecov.io/gh/appiahkubis14/pygeofetch/branch/main/graph/badge.svg)](https://codecov.io/gh/appiahkubis14/pygeofetch)
10
9
 
@@ -12,6 +11,10 @@
12
11
 
13
12
  [Quick Start](#-quick-start) · [Mexico City Case Study](#-case-study-mapping-mexico-city-land-subsidence-with-insar) · [Case Study Repo ↗](https://github.com/EOCoreINT/mexico-subsidence-project) · [Documentation](https://appiahkubis14.github.io/pygeofetch-docs/) · [Notebooks](#-notebooks)
14
13
 
14
+ <br>
15
+ <img src="https://raw.githubusercontent.com/EOCoreINT/pygeofetch/main/icon/pygeofetch_logo.png" alt="PyGeoFetch Logo" width="350">
16
+ <br>
17
+
15
18
  </div>
16
19
 
17
20
  ---
@@ -78,7 +81,7 @@ Every provider has been directly verified to return at minimum a correct boundin
78
81
  pip install pygeofetch # core — free providers work immediately
79
82
  pip install "pygeofetch[geo]" # + rasterio, geopandas, shapely
80
83
  pip install "pygeofetch[insar]" # + native SBAS inversion, phase unwrapping
81
- pip install "pygeofetch[insar-full]" # + MintPy passthrough for advanced corrections
84
+ pip install "pygeofetch[insar-full]" # + For Linux Users
82
85
  pip install "pygeofetch[all]" # everything
83
86
  ```
84
87
 
@@ -230,29 +233,22 @@ Run `pygeofetch --help` or any subcommand with `--help` for the complete, curren
230
233
 
231
234
  ---
232
235
 
233
- <!-- ## 📚 Notebooks
234
-
235
- | Notebook | Topics |
236
- |---|---|
237
- | `01_getting_started.ipynb` | Install, doctor, first search, first download |
238
- | `02_authentication_and_providers.ipynb` | All 24 providers, credentials, capability filters |
239
- | `03_advanced_search.ipynb` | Federated search, CQL2 filters, output formats, caching |
240
- | `04_download_and_postprocessing.ipynb` | Band selection, parallel downloads, post-processing |
241
- | `05_pipelines_and_scheduling.ipynb` | YAML pipelines, scheduling, Python builder API |
242
- | `06_real_world_workflows.ipynb` | NDVI time series, change detection, multi-sensor fusion |
243
- | `07_copernicus_and_authenticated_providers.ipynb` | Copernicus, USGS, NASA, Planet, ASF, OpenTopography |
244
- | `08_cli_complete_reference.ipynb` | Every CLI command with runnable examples |
245
- | `09_processing_complete.ipynb` | Full processing engine: preprocessing, indices, SAR, pipelines |
246
- | [`mexico_city_full_confirmed (2).ipynb`](<https://github.com/EOCoreINT/mexico-subsidence-project/blob/main/mexico_city_full_confirmed%20(2).ipynb>) ↗ | The [Mexico City case study](#-case-study-mapping-mexico-city-land-subsidence-with-insar) above, in a separate companion repo — real search/download, full InSAR chain, SBAS inversion |
247
- | `piton_fournaise_full.ipynb` | Verified InSAR pipeline against the real April 2021 Piton de la Fournaise eruption |
248
- | `amatrice_full.ipynb` | Verified InSAR pipeline against the real 2016 Amatrice, Italy earthquake |
249
- | `provider_search_footprint_test.ipynb` | Search and footprint-map validation across all 24 providers |
250
-
251
- ```bash
252
- cd notebooks/ && jupyter lab
253
- ``` -->
254
-
255
- ---
236
+ ## 📚 Citation
237
+
238
+ If you use PyGeoFetch in your research or operational work, please cite it using the following:
239
+
240
+ ### Software Citation
241
+ ```bibtex
242
+ @software{pygeofetch_2026,
243
+ author = {Appiah Kubi, Samuel},
244
+ title = {PyGeoFetch: A Unified Python Framework for Multi-Provider Satellite Data Acquisition, Pre-download Quality Control, and Geospatial Processing},
245
+ year = {2026},
246
+ publisher = {Zenodo},
247
+ version = {2.6.2.1},
248
+ doi = {10.5281/zenodo.22087230},
249
+ url = {https://doi.org/10.5281/zenodo.22087230}
250
+ }
251
+ ```
256
252
 
257
253
  ## Documentation
258
254
 
@@ -265,7 +261,7 @@ Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Good first
265
261
  ```bash
266
262
  git clone git@github.com:EOCoreINT/pygeofetch.git && cd pygeofetch
267
263
  pip install -e ".[dev,all]" && pytest tests/unit/ -v
268
- ```
264
+
269
265
 
270
266
  ## License
271
267
 
@@ -30,7 +30,7 @@ CLI::
30
30
  pygeofetch doctor
31
31
  """
32
32
 
33
- __version__ = "2.6.1"
33
+ __version__ = "2.6.2.1"
34
34
  __author__ = "PyGeoFetch Contributors"
35
35
  __license__ = "MIT"
36
36
 
@@ -221,8 +221,11 @@ def auth_remove(provider: str, yes: bool) -> None:
221
221
  click.confirm(f"Remove credentials for {provider!r}?", abort=True)
222
222
 
223
223
  mgr = AuthManager()
224
- mgr.remove_credentials(provider)
225
- console.print(f"[green]Credentials for [bold]{provider}[/] removed.[/]")
224
+ removed = mgr.remove(provider)
225
+ if removed:
226
+ console.print(f"[green]Credentials for [bold]{provider}[/] removed.[/]")
227
+ else:
228
+ console.print(f"[yellow]No stored credentials found for {provider!r}.[/]")
226
229
 
227
230
 
228
231
  @auth.command(name="export")
@@ -250,7 +253,7 @@ def auth_export(provider: str | None, output: str) -> None:
250
253
  from pygeofetch.core.authenticator import AuthManager
251
254
 
252
255
  mgr = AuthManager()
253
- data = mgr.export_credentials(provider_filter=provider)
256
+ data = mgr.export_credentials(providers=[provider] if provider else None)
254
257
 
255
258
  with open(output, "w") as f:
256
259
  json.dump(data, f, indent=2, default=str)
@@ -484,9 +484,11 @@ def providers_info(provider_id: str, as_json: bool) -> None:
484
484
  table.add_row("Auth type", caps.auth_type)
485
485
  table.add_row(
486
486
  "Auth status",
487
- f"[green]{auth_status}[/]"
488
- if "✓" in auth_status or "🌐" in auth_status
489
- else f"[red]{auth_status}[/]",
487
+ (
488
+ f"[green]{auth_status}[/]"
489
+ if "✓" in auth_status or "🌐" in auth_status
490
+ else f"[red]{auth_status}[/]"
491
+ ),
490
492
  )
491
493
  table.add_row("Satellites", ", ".join(caps.satellites) if caps.satellites else "—")
492
494
  table.add_row("SAR support", "[green]Yes[/]" if caps.supports_sar else "No")
@@ -495,9 +497,11 @@ def providers_info(provider_id: str, as_json: bool) -> None:
495
497
  table.add_row("CQL2 filter", "[green]Yes[/]" if caps.supports_cql2 else "No")
496
498
  table.add_row(
497
499
  "Resolution",
498
- f"{caps.resolution_min_m}m – {caps.resolution_max_m}m"
499
- if caps.resolution_min_m
500
- else "Varies",
500
+ (
501
+ f"{caps.resolution_min_m}m – {caps.resolution_max_m}m"
502
+ if caps.resolution_min_m
503
+ else "Varies"
504
+ ),
501
505
  )
502
506
  table.add_row("Regions", ", ".join(caps.regions) if caps.regions else "global")
503
507
  table.add_row("Endpoint", caps.endpoint_url or "—")
@@ -0,0 +1,141 @@
1
+ """
2
+ pygeofetch monitor — real CLI entry point for Feature 4 (Automated
3
+ Monitoring).
4
+
5
+ HONEST SCOPE, stated plainly rather than implied: this command wires
6
+ together the real, tested pieces already built --
7
+ pygeofetch.core.state.ProjectState and
8
+ pygeofetch.core.monitor.plan_monitoring_run -- to do real search and
9
+ real incremental planning (what's new, what pairs are needed). It
10
+ does NOT itself call download, coregistration, interferogram
11
+ formation, or invert_weighted() end to end here; those are the
12
+ existing, separately-tested real pipeline stages, and wiring all of
13
+ them into one CLI command is a real, substantial integration task of
14
+ its own that deserves the same direct verification already given to
15
+ each individual piece, not a rushed final assembly. What this command
16
+ does today: real search, real state-aware new-date detection, real
17
+ incremental pair planning, real persistence of results — genuinely
18
+ useful on its own for seeing what a scheduled run would actually do,
19
+ and the concrete foundation the remaining wiring builds on next.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+
26
+ import click
27
+ from rich.console import Console
28
+
29
+ from pygeofetch.core.monitor import plan_monitoring_run
30
+ from pygeofetch.core.state import ProjectState, RunSummary
31
+
32
+ console = Console()
33
+ logger = logging.getLogger("pygeofetch.cli.monitor")
34
+
35
+
36
+ @click.group(
37
+ name="monitor", help="Automated, incremental InSAR monitoring (Feature 4)."
38
+ )
39
+ def monitor():
40
+ pass
41
+
42
+
43
+ @monitor.command(
44
+ name="run",
45
+ help="Run one real monitoring cycle: search for new scenes, plan incremental processing.",
46
+ )
47
+ @click.option(
48
+ "--state-db",
49
+ required=True,
50
+ type=click.Path(),
51
+ help="Path to this project's real state database.",
52
+ )
53
+ @click.option(
54
+ "--bbox",
55
+ required=True,
56
+ type=str,
57
+ help="Real AOI bounding box, 'minlon,minlat,maxlon,maxlat'.",
58
+ )
59
+ @click.option(
60
+ "--satellites",
61
+ default="Sentinel-1A,Sentinel-1B",
62
+ help="Comma-separated real satellite names.",
63
+ )
64
+ @click.option(
65
+ "--n-neighbors",
66
+ default=3,
67
+ type=int,
68
+ help="Real neighbors each new date connects to (spec default: 3).",
69
+ )
70
+ @click.option(
71
+ "--cron-daily",
72
+ is_flag=True,
73
+ help="Marker flag for scheduled invocation (real cron job); does not change behavior, documents intent in logs.",
74
+ )
75
+ @click.pass_context
76
+ def monitor_run(ctx, state_db, bbox, satellites, n_neighbors, cron_daily):
77
+ client = ctx.obj.get("client") if ctx.obj else None
78
+ if client is None:
79
+ from pygeofetch import PyGeoFetch
80
+
81
+ client = PyGeoFetch()
82
+
83
+ state = ProjectState(state_db)
84
+
85
+ minlon, minlat, maxlon, maxlat = (float(x) for x in bbox.split(","))
86
+ sat_list = [s.strip() for s in satellites.split(",")]
87
+
88
+ from pygeofetch.models.search_query import SearchQuery
89
+
90
+ query = SearchQuery(
91
+ bbox=(minlon, minlat, maxlon, maxlat),
92
+ satellites=sat_list,
93
+ product_type="SLC",
94
+ )
95
+ if state.last_download_timestamp:
96
+ query.start_date = state.last_download_timestamp
97
+
98
+ console.print(
99
+ f"[bold]pygeofetch monitor[/bold] — {'scheduled' if cron_daily else 'manual'} run"
100
+ )
101
+ console.print(f"State: {state_db}")
102
+
103
+ try:
104
+ results = client.search(query)
105
+ except Exception as exc:
106
+ state.record_run(RunSummary(status="failed", detail=f"search failed: {exc}"))
107
+ console.print(f"[red]Search failed:[/red] {exc}")
108
+ raise
109
+
110
+ available_dates = sorted(
111
+ {str(r.datetime)[:10] for r in results if getattr(r, "datetime", None)}
112
+ )
113
+ plan = plan_monitoring_run(state, available_dates, n_neighbors=n_neighbors)
114
+
115
+ console.print(plan.message)
116
+ if plan.new_dates:
117
+ console.print(f" New dates: {plan.new_dates}")
118
+ console.print(f" New pairs needed: {len(plan.new_pairs)}")
119
+
120
+ state.record_run(
121
+ RunSummary(
122
+ status="success" if plan.network_changed else "no_new_scenes",
123
+ n_new_scenes=len(plan.new_dates),
124
+ detail=plan.message,
125
+ )
126
+ )
127
+
128
+ return plan
129
+
130
+
131
+ @monitor.command(name="history", help="Show real run history for a monitoring project.")
132
+ @click.option("--state-db", required=True, type=click.Path(exists=True))
133
+ @click.option("--limit", default=20, type=int)
134
+ def monitor_history(state_db, limit):
135
+ state = ProjectState(state_db)
136
+ runs = state.run_history(limit=limit)
137
+ if not runs:
138
+ console.print("No real runs recorded yet.")
139
+ return
140
+ for run in runs:
141
+ console.print(f"[{run['status']}] {run['started_at']} — {run['detail']}")
@@ -67,7 +67,7 @@ def search() -> None:
67
67
  @click.option(
68
68
  "--polarisation",
69
69
  default=None,
70
- help='SAR polarisation e.g. VV, VH, HH, HV. Same Copernicus-only caveat as --product-type.',
70
+ help="SAR polarisation e.g. VV, VH, HH, HV. Same Copernicus-only caveat as --product-type.",
71
71
  )
72
72
  @click.option("--max-results", "-n", default=100, show_default=True)
73
73
  @click.option(
@@ -447,9 +447,11 @@ class AuthManager:
447
447
  {
448
448
  "provider": provider,
449
449
  "has_session": session is not None and session.is_valid,
450
- "session_expires": session.expires_at.isoformat()
451
- if session and session.expires_at
452
- else None,
450
+ "session_expires": (
451
+ session.expires_at.isoformat()
452
+ if session and session.expires_at
453
+ else None
454
+ ),
453
455
  }
454
456
  )
455
457
  return result
@@ -25,7 +25,7 @@ import hashlib
25
25
  import time
26
26
  from concurrent.futures import ThreadPoolExecutor, as_completed
27
27
  from pathlib import Path
28
- from typing import TYPE_CHECKING, Any, Callable, Dict
28
+ from typing import TYPE_CHECKING, Any, Callable, Dict, List, cast
29
29
 
30
30
  from pygeofetch.core.logging import DownloadProgress, get_logger
31
31
  from pygeofetch.models.download_task import (
@@ -365,16 +365,19 @@ class AdaptiveDownloader:
365
365
  sum(1 for r in results if r and r.success)
366
366
  sum(1 for r in results if r and not r.success)
367
367
  dp.__exit__(None, None, None)
368
- assert len(results) == len(data_list), (
369
- f"BUG: result count {len(results)} != input count {len(data_list)}"
370
- )
368
+ assert len(results) == len(
369
+ data_list
370
+ ), f"BUG: result count {len(results)} != input count {len(data_list)}"
371
371
  sum(
372
372
  r.bytes_downloaded / (1024 * 1024)
373
373
  for r in results
374
374
  if r and r.success and r.bytes_downloaded
375
375
  )
376
376
  # summary printed by DownloadProgress footer
377
- return results
377
+ # The loop above has already replaced every None slot with a
378
+ # real DownloadResult (failure placeholder), so this is
379
+ # genuinely list[DownloadResult] at this point.
380
+ return cast(List[DownloadResult], results)
378
381
 
379
382
  def _get_provider(self, provider_id: str) -> Any:
380
383
  """Get a provider instance with a fresh authenticated session."""
@@ -889,7 +892,7 @@ class AdaptiveDownloader:
889
892
  from pygeofetch.processor import SpectralIndex
890
893
  except ImportError as exc:
891
894
  raise ImportError(
892
- f'{index_name} requires the processor extra: '
895
+ f"{index_name} requires the processor extra: "
893
896
  f'pip install "pygeofetch[processor]"'
894
897
  ) from exc
895
898
 
@@ -904,7 +907,10 @@ class AdaptiveDownloader:
904
907
  (
905
908
  p
906
909
  for p in result.output_paths
907
- if any(pat in p.name or pat.lower() in p.name.lower() for pat in patterns)
910
+ if any(
911
+ pat in p.name or pat.lower() in p.name.lower()
912
+ for pat in patterns
913
+ )
908
914
  ),
909
915
  None,
910
916
  )
@@ -914,8 +920,15 @@ class AdaptiveDownloader:
914
920
  f"{band_name} (looked for {patterns} in filenames)"
915
921
  )
916
922
  with rasterio.open(path) as src:
917
- arr = src.read(1, out_shape=ref_shape, resampling=rasterio.enums.Resampling.bilinear) \
918
- if ref_shape else src.read(1)
923
+ arr = (
924
+ src.read(
925
+ 1,
926
+ out_shape=ref_shape,
927
+ resampling=rasterio.enums.Resampling.bilinear,
928
+ )
929
+ if ref_shape
930
+ else src.read(1)
931
+ )
919
932
  if ref_shape is None:
920
933
  ref_shape = arr.shape
921
934
  profile = src.profile.copy()
@@ -929,6 +942,12 @@ class AdaptiveDownloader:
929
942
  arr = np.where(arr == src.nodata, np.nan, arr)
930
943
  band_arrays[band_name] = arr
931
944
 
945
+ assert ref_shape is not None and profile is not None, (
946
+ f"{index_name}: required_bands was empty or the loop never "
947
+ f"ran -- ref_shape/profile must be set by the first band "
948
+ f"processed above."
949
+ )
950
+
932
951
  si = SpectralIndex(prefer_spyndex=False)
933
952
  index_arr = np.asarray(si.compute(index_name, **band_arrays), dtype=np.float32)
934
953
  # Any band being NaN at a pixel must make the index NaN there too,
@@ -950,7 +969,6 @@ class AdaptiveDownloader:
950
969
  logger.info(f"{index_name} computed → {out_path}")
951
970
  return result
952
971
 
953
-
954
972
  def _find_existing_download(
955
973
  self, data: "SatelliteData", destination: Path
956
974
  ) -> Path | None:
@@ -1230,7 +1248,8 @@ class AdaptiveDownloader:
1230
1248
  "%s has no direct CRS/transform, using its %d "
1231
1249
  "ground control points for reprojection instead "
1232
1250
  "(common for raw SAR/GRD delivery).",
1233
- source_path.name, len(src_gcps),
1251
+ source_path.name,
1252
+ len(src_gcps),
1234
1253
  )
1235
1254
  elif src_crs is None:
1236
1255
  raise RuntimeError(
@@ -1301,7 +1320,6 @@ class AdaptiveDownloader:
1301
1320
  )
1302
1321
  raise RuntimeError(msg)
1303
1322
 
1304
-
1305
1323
  def _backoff(self, attempt: int, options: DownloadOptions) -> float:
1306
1324
  """Calculate retry delay based on strategy."""
1307
1325
  import random
@@ -1334,4 +1352,4 @@ class AdaptiveDownloader:
1334
1352
  @property
1335
1353
  def _result_total_size_mb(self) -> float:
1336
1354
  """Property used by result objects."""
1337
- return 0.0
1355
+ return 0.0
@@ -484,4 +484,4 @@ class PyGeoFetch:
484
484
 
485
485
  def __repr__(self) -> str:
486
486
  authed = [item["provider"] for item in self.auth.list()]
487
- return f"PyGeoFetch(authenticated={authed}, cache_entries={len(self.searcher.cache._cache)})"
487
+ return f"PyGeoFetch(authenticated={authed}, cache_entries={len(self.searcher.cache._cache)})"