PyGeoFetch 2.6.2__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 (173) hide show
  1. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/CHANGELOG.md +5 -5
  2. {pygeofetch-2.6.2/pygeofetch.egg-info → pygeofetch-2.6.2.1}/PKG-INFO +25 -28
  3. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/README.md +23 -27
  4. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/__init__.py +1 -1
  5. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/cli/auth_commands.py +6 -3
  6. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/cli/main.py +10 -6
  7. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/cli/monitor_commands.py +49 -14
  8. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/cli/search_commands.py +1 -1
  9. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/core/authenticator.py +5 -3
  10. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/core/downloader.py +31 -13
  11. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/core/engine.py +1 -1
  12. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/core/monitor.py +16 -5
  13. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/core/state.py +20 -6
  14. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/__init__.py +96 -71
  15. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/analysis.py +62 -40
  16. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/annotation.py +116 -70
  17. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/atmosphere.py +168 -70
  18. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/coregister.py +126 -58
  19. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/deburst.py +39 -12
  20. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/esd.py +65 -28
  21. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/extraction.py +126 -67
  22. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/flatearth.py +45 -18
  23. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/geolocation.py +79 -29
  24. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/gpu.py +3 -1
  25. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/interferogram.py +502 -193
  26. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/ionosphere.py +118 -45
  27. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/offset_geometry.py +47 -43
  28. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/offset_tracking.py +45 -20
  29. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/preflight.py +288 -169
  30. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/provenance.py +2 -1
  31. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/ps_selection.py +21 -9
  32. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/splitspectrum_diagnostic.py +12 -4
  33. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/stack_selection.py +179 -77
  34. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/synthetic.py +53 -17
  35. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/timeseries.py +268 -152
  36. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/unwrap.py +408 -156
  37. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/validate.py +36 -13
  38. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/visualize.py +15 -5
  39. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/insar/workflow.py +102 -29
  40. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/meta.yaml +1 -1
  41. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/models/satellite_data.py +5 -3
  42. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/processing/indices.py +5 -0
  43. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/processing/preprocessor.py +55 -25
  44. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/processing/sar.py +13 -4
  45. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/processor/indices.py +4 -0
  46. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/processor/stacker.py +4 -0
  47. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/processor/timeseries.py +41 -13
  48. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/airbus_oneatlas.py +17 -4
  49. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/alaska_satellite_facility.py +5 -2
  50. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/aws_earth.py +1 -1
  51. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/copernicus.py +19 -8
  52. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/copernicus_nodes.py +73 -23
  53. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/digitalglobe.py +5 -2
  54. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/earth_explorer_additional.py +5 -2
  55. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/element84.py +1 -1
  56. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/eodag_provider.py +9 -5
  57. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/esa_scihub.py +17 -4
  58. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/geoserver_generic.py +5 -2
  59. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/google_earth_engine.py +17 -4
  60. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/inpe_cbers.py +5 -2
  61. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/isro_bhuvan.py +5 -2
  62. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/jaxa_earth.py +5 -2
  63. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/maxar_gbdx.py +5 -2
  64. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/nasa_earthdata.py +6 -4
  65. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/nasa_earthdata_cloud.py +2 -2
  66. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/noaa_big_data.py +5 -2
  67. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/opentopography.py +6 -2
  68. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/planet.py +4 -3
  69. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/planetary_computer.py +1 -1
  70. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/sentinel_hub.py +11 -7
  71. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/terrabotics.py +4 -3
  72. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/usgs.py +20 -5
  73. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/sar/__init__.py +1 -1
  74. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/sar/_native.py +1 -1
  75. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/sar/extraction.py +8 -4
  76. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/utils/gcp_recovery.py +1 -3
  77. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/viz/map.py +82 -33
  78. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/viz/plot.py +91 -38
  79. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1/pygeofetch.egg-info}/PKG-INFO +25 -28
  80. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch.egg-info/SOURCES.txt +0 -2
  81. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch.egg-info/requires.txt +1 -0
  82. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pyproject.toml +9 -2
  83. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_atmospheric_circular_regression.py +21 -6
  84. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_build_sbas_network.py +68 -38
  85. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_burst_metadata.py +20 -6
  86. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_burst_processing_integration.py +110 -31
  87. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_burst_sync_large_gap.py +49 -35
  88. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_burst_sync_prescreening.py +68 -43
  89. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_burst_synchronization.py +56 -36
  90. {pygeofetch-2.6.2 → 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.2 → pygeofetch-2.6.2.1}/tests/test_coregister_upgrades.py +110 -47
  93. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_era5_atmospheric_correction.py +17 -6
  94. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_extraction.py +46 -12
  95. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_flat_earth_phase.py +72 -24
  96. {pygeofetch-2.6.2 → 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.2 → pygeofetch-2.6.2.1}/tests/test_ionosphere.py +102 -33
  99. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_map_preview_consolidation.py +41 -24
  100. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_models.py +10 -3
  101. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_monitor.py +51 -30
  102. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_offset_tracking.py +73 -31
  103. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_pgv_integration_contract.py +42 -42
  104. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_preflight_gate.py +398 -107
  105. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_provider_geometry_audit.py +26 -12
  106. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_provider_scene_disambiguation.py +31 -12
  107. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_providers.py +1 -0
  108. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_ps_selection.py +122 -54
  109. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_raster_collocation_integration.py +75 -36
  110. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_reference_pixel_and_despike.py +64 -38
  111. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_risk_mapping.py +99 -43
  112. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_screen_stack_orchestration.py +68 -41
  113. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_search_download.py +3 -1
  114. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_sign_convention.py +71 -37
  115. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_splitspectrum_diagnostic.py +7 -2
  116. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_stack_selection_consolidation.py +104 -45
  117. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_state.py +33 -24
  118. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_topo_r2_bug.py +42 -21
  119. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_utils.py +2 -1
  120. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/tests/test_wls_inversion.py +124 -54
  121. pygeofetch-2.6.2/tests/test_coregister_integration.py +0 -354
  122. pygeofetch-2.6.2/tests/test_insar.py +0 -578
  123. pygeofetch-2.6.2/tests/test_preflight_gate (1).py +0 -319
  124. pygeofetch-2.6.2/tests/test_provider_geometry_audit (1).py +0 -200
  125. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/LICENSE +0 -0
  126. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/MANIFEST.in +0 -0
  127. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/cli/__init__.py +0 -0
  128. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/cli/config_commands.py +0 -0
  129. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/cli/download_commands.py +0 -0
  130. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/cli/index_commands.py +0 -0
  131. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/cli/pipeline_process_commands.py +0 -0
  132. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/cli/postprocess_commands.py +0 -0
  133. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/cli/preprocess_commands.py +0 -0
  134. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/cli/sar_commands.py +0 -0
  135. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/config/__init__.py +0 -0
  136. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/config/defaults.yaml +0 -0
  137. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/config/settings.py +0 -0
  138. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/core/__init__.py +0 -0
  139. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/core/cache_manager.py +0 -0
  140. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/core/logging.py +0 -0
  141. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/core/orbits.py +0 -0
  142. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/core/scheduler.py +0 -0
  143. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/core/searcher.py +0 -0
  144. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/models/__init__.py +0 -0
  145. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/models/download_task.py +0 -0
  146. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/models/search_query.py +0 -0
  147. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/models/user_auth.py +0 -0
  148. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/processing/__init__.py +0 -0
  149. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/processing/base.py +0 -0
  150. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/processing/batch.py +0 -0
  151. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/processing/pipeline.py +0 -0
  152. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/processing/postprocessor.py +0 -0
  153. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/processor/__init__.py +0 -0
  154. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/processor/landsat.py +0 -0
  155. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/processor/loader.py +0 -0
  156. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/__init__.py +0 -0
  157. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/providers/base.py +0 -0
  158. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/sar/_ost.py +0 -0
  159. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/sar/_sarxarray.py +0 -0
  160. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/sar/processor.py +0 -0
  161. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/utils/__init__.py +0 -0
  162. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/utils/file_utils.py +0 -0
  163. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/utils/geo_utils.py +0 -0
  164. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/utils/logging_setup.py +0 -0
  165. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/utils/retry_handler.py +0 -0
  166. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/utils/validators.py +0 -0
  167. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch/viz/__init__.py +0 -0
  168. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch.egg-info/dependency_links.txt +0 -0
  169. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch.egg-info/entry_points.txt +0 -0
  170. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch.egg-info/not-zip-safe +0 -0
  171. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/pygeofetch.egg-info/top_level.txt +0 -0
  172. {pygeofetch-2.6.2 → pygeofetch-2.6.2.1}/setup.cfg +0 -0
  173. {pygeofetch-2.6.2 → 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.2/).
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/2.6.2.1/).
5
5
 
6
- ## [2.6.2] — 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.2] — 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.2** | 2026-07-29 | SNAPHU unwrapping fixes, 87.1% reliable pixels |
192
- | **2.6.2** | 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.2
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.2"
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 "—")
@@ -33,22 +33,51 @@ console = Console()
33
33
  logger = logging.getLogger("pygeofetch.cli.monitor")
34
34
 
35
35
 
36
- @click.group(name="monitor", help="Automated, incremental InSAR monitoring (Feature 4).")
36
+ @click.group(
37
+ name="monitor", help="Automated, incremental InSAR monitoring (Feature 4)."
38
+ )
37
39
  def monitor():
38
40
  pass
39
41
 
40
42
 
41
- @monitor.command(name="run", help="Run one real monitoring cycle: search for new scenes, plan incremental processing.")
42
- @click.option("--state-db", required=True, type=click.Path(), help="Path to this project's real state database.")
43
- @click.option("--bbox", required=True, type=str, help="Real AOI bounding box, 'minlon,minlat,maxlon,maxlat'.")
44
- @click.option("--satellites", default="Sentinel-1A,Sentinel-1B", help="Comma-separated real satellite names.")
45
- @click.option("--n-neighbors", default=3, type=int, help="Real neighbors each new date connects to (spec default: 3).")
46
- @click.option("--cron-daily", is_flag=True, help="Marker flag for scheduled invocation (real cron job); does not change behavior, documents intent in logs.")
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
+ )
47
75
  @click.pass_context
48
76
  def monitor_run(ctx, state_db, bbox, satellites, n_neighbors, cron_daily):
49
77
  client = ctx.obj.get("client") if ctx.obj else None
50
78
  if client is None:
51
79
  from pygeofetch import PyGeoFetch
80
+
52
81
  client = PyGeoFetch()
53
82
 
54
83
  state = ProjectState(state_db)
@@ -66,7 +95,9 @@ def monitor_run(ctx, state_db, bbox, satellites, n_neighbors, cron_daily):
66
95
  if state.last_download_timestamp:
67
96
  query.start_date = state.last_download_timestamp
68
97
 
69
- console.print(f"[bold]pygeofetch monitor[/bold] — {'scheduled' if cron_daily else 'manual'} run")
98
+ console.print(
99
+ f"[bold]pygeofetch monitor[/bold] — {'scheduled' if cron_daily else 'manual'} run"
100
+ )
70
101
  console.print(f"State: {state_db}")
71
102
 
72
103
  try:
@@ -76,7 +107,9 @@ def monitor_run(ctx, state_db, bbox, satellites, n_neighbors, cron_daily):
76
107
  console.print(f"[red]Search failed:[/red] {exc}")
77
108
  raise
78
109
 
79
- available_dates = sorted({str(r.datetime)[:10] for r in results if getattr(r, "datetime", None)})
110
+ available_dates = sorted(
111
+ {str(r.datetime)[:10] for r in results if getattr(r, "datetime", None)}
112
+ )
80
113
  plan = plan_monitoring_run(state, available_dates, n_neighbors=n_neighbors)
81
114
 
82
115
  console.print(plan.message)
@@ -84,11 +117,13 @@ def monitor_run(ctx, state_db, bbox, satellites, n_neighbors, cron_daily):
84
117
  console.print(f" New dates: {plan.new_dates}")
85
118
  console.print(f" New pairs needed: {len(plan.new_pairs)}")
86
119
 
87
- state.record_run(RunSummary(
88
- status="success" if plan.network_changed else "no_new_scenes",
89
- n_new_scenes=len(plan.new_dates),
90
- detail=plan.message,
91
- ))
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
+ )
92
127
 
93
128
  return plan
94
129
 
@@ -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)})"
@@ -81,7 +81,8 @@ def generate_incremental_pairs(
81
81
  )
82
82
  neighbors = candidates_by_distance[:n_neighbors]
83
83
  for neighbor in neighbors:
84
- pair = tuple(sorted([new_date, neighbor]))
84
+ a, b = sorted([new_date, neighbor])
85
+ pair: Tuple[str, str] = (a, b)
85
86
  if pair not in seen:
86
87
  seen.add(pair)
87
88
  pairs.append(pair)
@@ -136,18 +137,28 @@ def plan_monitoring_run(
136
137
 
137
138
  if not new_dates:
138
139
  return MonitoringRunResult(
139
- new_dates=[], new_pairs=[], network_changed=False, ran_inversion=False,
140
+ new_dates=[],
141
+ new_pairs=[],
142
+ network_changed=False,
143
+ ran_inversion=False,
140
144
  message="No new dates since last run -- nothing to do.",
141
145
  )
142
146
 
143
- new_pairs = generate_incremental_pairs(new_dates, sorted(already_processed), n_neighbors)
147
+ new_pairs = generate_incremental_pairs(
148
+ new_dates, sorted(already_processed), n_neighbors
149
+ )
144
150
 
145
151
  logger.info(
146
152
  "Monitoring run: %d new date(s), %d new pair(s) needed (%d neighbors each).",
147
- len(new_dates), len(new_pairs), n_neighbors,
153
+ len(new_dates),
154
+ len(new_pairs),
155
+ n_neighbors,
148
156
  )
149
157
 
150
158
  return MonitoringRunResult(
151
- new_dates=new_dates, new_pairs=new_pairs, network_changed=True, ran_inversion=False,
159
+ new_dates=new_dates,
160
+ new_pairs=new_pairs,
161
+ network_changed=True,
162
+ ran_inversion=False,
152
163
  message=f"{len(new_dates)} new date(s), {len(new_pairs)} new pair(s) planned.",
153
164
  )
@@ -127,7 +127,9 @@ class ProjectState:
127
127
 
128
128
  # ── processed dates ─────────────────────────────────────────────
129
129
 
130
- def mark_dates_processed(self, dates: List[str], scene_ids: Optional[List[str]] = None):
130
+ def mark_dates_processed(
131
+ self, dates: List[str], scene_ids: Optional[List[Optional[str]]] = None
132
+ ):
131
133
  """Real, idempotent insert -- re-marking an already-processed date is a no-op, not an error."""
132
134
  now = datetime.now(timezone.utc).isoformat()
133
135
  scene_ids = scene_ids or [None] * len(dates)
@@ -139,12 +141,16 @@ class ProjectState:
139
141
 
140
142
  def processed_dates(self) -> List[str]:
141
143
  with self._connect() as conn:
142
- rows = conn.execute("SELECT date FROM processed_dates ORDER BY date").fetchall()
144
+ rows = conn.execute(
145
+ "SELECT date FROM processed_dates ORDER BY date"
146
+ ).fetchall()
143
147
  return [r[0] for r in rows]
144
148
 
145
149
  def is_date_processed(self, date: str) -> bool:
146
150
  with self._connect() as conn:
147
- row = conn.execute("SELECT 1 FROM processed_dates WHERE date = ?", (date,)).fetchone()
151
+ row = conn.execute(
152
+ "SELECT 1 FROM processed_dates WHERE date = ?", (date,)
153
+ ).fetchone()
148
154
  return row is not None
149
155
 
150
156
  # ── project metadata (last_download_timestamp, network hash, reference pixel) ──
@@ -159,7 +165,9 @@ class ProjectState:
159
165
 
160
166
  def _get_meta(self, key: str) -> Optional[str]:
161
167
  with self._connect() as conn:
162
- row = conn.execute("SELECT value FROM project_meta WHERE key = ?", (key,)).fetchone()
168
+ row = conn.execute(
169
+ "SELECT value FROM project_meta WHERE key = ?", (key,)
170
+ ).fetchone()
163
171
  return row[0] if row else None
164
172
 
165
173
  @property
@@ -226,7 +234,13 @@ class ProjectState:
226
234
  (limit,),
227
235
  ).fetchall()
228
236
  return [
229
- {"run_id": r[0], "started_at": r[1], "finished_at": r[2],
230
- "status": r[3], "n_new_scenes": r[4], "detail": r[5]}
237
+ {
238
+ "run_id": r[0],
239
+ "started_at": r[1],
240
+ "finished_at": r[2],
241
+ "status": r[3],
242
+ "n_new_scenes": r[4],
243
+ "detail": r[5],
244
+ }
231
245
  for r in rows
232
246
  ]