pygeofetch 2.3.0__tar.gz → 2.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/CHANGELOG.md +6 -6
  2. {pygeofetch-2.3.0/pygeofetch.egg-info → pygeofetch-2.5.0}/PKG-INFO +3 -3
  3. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/__init__.py +2 -2
  4. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/main.py +2 -2
  5. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/preflight.py +113 -49
  6. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/unwrap.py +95 -8
  7. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/meta.yaml +1 -1
  8. {pygeofetch-2.3.0 → pygeofetch-2.5.0/pygeofetch.egg-info}/PKG-INFO +3 -3
  9. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch.egg-info/requires.txt +2 -2
  10. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pyproject.toml +3 -3
  11. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_pgv_integration_contract.py +1 -1
  12. pygeofetch-2.5.0/tests/test_preflight_gate.py +588 -0
  13. pygeofetch-2.3.0/tests/test_preflight_gate.py +0 -176
  14. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/LICENSE +0 -0
  15. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/MANIFEST.in +0 -0
  16. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/README.md +0 -0
  17. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/__init__.py +0 -0
  18. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/auth_commands.py +0 -0
  19. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/config_commands.py +0 -0
  20. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/download_commands.py +0 -0
  21. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/index_commands.py +0 -0
  22. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/pipeline_process_commands.py +0 -0
  23. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/postprocess_commands.py +0 -0
  24. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/preprocess_commands.py +0 -0
  25. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/sar_commands.py +0 -0
  26. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/cli/search_commands.py +0 -0
  27. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/config/__init__.py +0 -0
  28. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/config/defaults.yaml +0 -0
  29. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/config/settings.py +0 -0
  30. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/__init__.py +0 -0
  31. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/authenticator.py +0 -0
  32. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/cache_manager.py +0 -0
  33. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/downloader.py +0 -0
  34. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/engine.py +0 -0
  35. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/logging.py +0 -0
  36. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/orbits.py +0 -0
  37. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/scheduler.py +0 -0
  38. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/core/searcher.py +0 -0
  39. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/__init__.py +0 -0
  40. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/analysis.py +0 -0
  41. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/annotation.py +0 -0
  42. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/atmosphere.py +0 -0
  43. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/coregister.py +0 -0
  44. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/deburst.py +0 -0
  45. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/esd.py +0 -0
  46. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/extraction.py +0 -0
  47. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/flatearth.py +0 -0
  48. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/geolocation.py +0 -0
  49. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/gpu.py +0 -0
  50. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/interferogram.py +0 -0
  51. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/ionosphere.py +0 -0
  52. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/provenance.py +0 -0
  53. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/splitspectrum_diagnostic.py +0 -0
  54. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/stack_selection.py +0 -0
  55. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/synthetic.py +0 -0
  56. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/timeseries.py +0 -0
  57. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/validate.py +0 -0
  58. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/visualize.py +0 -0
  59. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/insar/workflow.py +0 -0
  60. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/models/__init__.py +0 -0
  61. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/models/download_task.py +0 -0
  62. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/models/satellite_data.py +0 -0
  63. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/models/search_query.py +0 -0
  64. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/models/user_auth.py +0 -0
  65. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/__init__.py +0 -0
  66. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/base.py +0 -0
  67. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/batch.py +0 -0
  68. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/indices.py +0 -0
  69. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/pipeline.py +0 -0
  70. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/postprocessor.py +0 -0
  71. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/preprocessor.py +0 -0
  72. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processing/sar.py +0 -0
  73. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processor/__init__.py +0 -0
  74. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processor/indices.py +0 -0
  75. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processor/landsat.py +0 -0
  76. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processor/loader.py +0 -0
  77. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processor/stacker.py +0 -0
  78. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/processor/timeseries.py +0 -0
  79. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/__init__.py +0 -0
  80. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/airbus_oneatlas.py +0 -0
  81. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/alaska_satellite_facility.py +0 -0
  82. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/aws_earth.py +0 -0
  83. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/base.py +0 -0
  84. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/copernicus.py +0 -0
  85. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/copernicus_nodes.py +0 -0
  86. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/digitalglobe.py +0 -0
  87. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/earth_explorer_additional.py +0 -0
  88. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/element84.py +0 -0
  89. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/eodag_provider.py +0 -0
  90. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/esa_scihub.py +0 -0
  91. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/geoserver_generic.py +0 -0
  92. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/google_earth_engine.py +0 -0
  93. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/inpe_cbers.py +0 -0
  94. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/isro_bhuvan.py +0 -0
  95. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/jaxa_earth.py +0 -0
  96. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/maxar_gbdx.py +0 -0
  97. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/nasa_earthdata.py +0 -0
  98. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/nasa_earthdata_cloud.py +0 -0
  99. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/noaa_big_data.py +0 -0
  100. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/opentopography.py +0 -0
  101. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/planet.py +0 -0
  102. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/planetary_computer.py +0 -0
  103. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/sentinel_hub.py +0 -0
  104. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/terrabotics.py +0 -0
  105. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/providers/usgs.py +0 -0
  106. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/sar/__init__.py +0 -0
  107. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/sar/_native.py +0 -0
  108. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/sar/_ost.py +0 -0
  109. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/sar/_sarxarray.py +0 -0
  110. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/sar/extraction.py +0 -0
  111. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/sar/processor.py +0 -0
  112. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/utils/__init__.py +0 -0
  113. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/utils/file_utils.py +0 -0
  114. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/utils/gcp_recovery.py +0 -0
  115. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/utils/geo_utils.py +0 -0
  116. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/utils/logging_setup.py +0 -0
  117. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/utils/retry_handler.py +0 -0
  118. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/utils/validators.py +0 -0
  119. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/viz/__init__.py +0 -0
  120. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/viz/map.py +0 -0
  121. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch/viz/plot.py +0 -0
  122. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch.egg-info/SOURCES.txt +0 -0
  123. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch.egg-info/dependency_links.txt +0 -0
  124. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch.egg-info/entry_points.txt +0 -0
  125. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch.egg-info/not-zip-safe +0 -0
  126. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/pygeofetch.egg-info/top_level.txt +0 -0
  127. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/setup.cfg +0 -0
  128. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_atmospheric_circular_regression (1).py +0 -0
  129. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_atmospheric_circular_regression.py +0 -0
  130. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_build_sbas_network.py +0 -0
  131. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_burst_metadata.py +0 -0
  132. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_burst_processing_integration.py +0 -0
  133. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_burst_sync_large_gap.py +0 -0
  134. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_burst_sync_prescreening.py +0 -0
  135. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_burst_synchronization.py +0 -0
  136. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_copernicus_nodes.py +0 -0
  137. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_coregister_integration.py +0 -0
  138. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_coregister_upgrades.py +0 -0
  139. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_era5_atmospheric_correction.py +0 -0
  140. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_extraction.py +0 -0
  141. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_flat_earth_phase.py +0 -0
  142. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_gcp_reprojection.py +0 -0
  143. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_insar.py +0 -0
  144. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_ionosphere.py +0 -0
  145. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_map_preview_consolidation.py +0 -0
  146. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_models.py +0 -0
  147. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_preflight_gate (1).py +0 -0
  148. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_provider_geometry_audit (1).py +0 -0
  149. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_provider_geometry_audit.py +0 -0
  150. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_provider_scene_disambiguation.py +0 -0
  151. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_providers.py +0 -0
  152. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_raster_collocation_integration.py +0 -0
  153. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_reference_pixel_and_despike.py +0 -0
  154. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_risk_mapping.py +0 -0
  155. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_screen_stack_orchestration.py +0 -0
  156. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_search_download.py +0 -0
  157. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_splitspectrum_diagnostic.py +0 -0
  158. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_stack_selection_consolidation.py +0 -0
  159. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_topo_r2_bug.py +0 -0
  160. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/tests/test_utils.py +0 -0
  161. {pygeofetch-2.3.0 → pygeofetch-2.5.0}/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.3.0/).
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/2.5.0/).
5
5
 
6
- ## [2.3.0] — 2026-07-29
6
+ ## [2.5.0] — 2026-07-29
7
7
 
8
8
  ### Added
9
9
  - Major SNAPHU unwrapping improvements for urban InSAR applications
@@ -51,7 +51,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/2.3.0/).
51
51
 
52
52
  ### Migration Guide
53
53
  ```python
54
- # Before (v2.3.0)
54
+ # Before (v2.4.0)
55
55
  unwrapper = PhaseUnwrapper(cost_mode="defo", init_method="mcf")
56
56
  unwrapped = unwrapper.unwrap(phase, coherence, nlooks=2.0)
57
57
 
@@ -72,7 +72,7 @@ unwrapped = unwrapper.unwrap(
72
72
 
73
73
  ---
74
74
 
75
- ## [2.3.0] — 2026-07-12
75
+ ## [2.5.0] — 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.3.0** | 2026-07-29 | SNAPHU unwrapping fixes, 87.1% reliable pixels |
192
- | **2.3.0** | 2026-07-12 | 22 providers, 17 indices, Sentinel-1C/D |
191
+ | **2.5.0** | 2026-07-29 | SNAPHU unwrapping fixes, 87.1% reliable pixels |
192
+ | **2.5.0** | 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.3.0
3
+ Version: 2.5.0
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
@@ -91,8 +91,8 @@ Requires-Dist: sidecar>=0.6.0; extra == "viz"
91
91
  Requires-Dist: bokeh>=3.0.0; extra == "viz"
92
92
  Requires-Dist: seaborn>=0.12.0; extra == "viz"
93
93
  Requires-Dist: plotly>=5.14.0; extra == "viz"
94
- Requires-Dist: whitebox>=2.3.0; extra == "viz"
95
- Requires-Dist: whiteboxgui>=2.3.0; extra == "viz"
94
+ Requires-Dist: whitebox>=2.4.0; extra == "viz"
95
+ Requires-Dist: whiteboxgui>=2.4.0; extra == "viz"
96
96
  Requires-Dist: geemap>=0.28.0; extra == "viz"
97
97
  Requires-Dist: osmnx>=1.9.4; extra == "viz"
98
98
  Requires-Dist: contextily>=1.9.4; extra == "viz"
@@ -1,5 +1,5 @@
1
1
  """
2
- PyGeoFetch v2.3.0 — Universal Satellite Data Pipeline.
2
+ PyGeoFetch v2.4.0 — Universal Satellite Data Pipeline.
3
3
 
4
4
  Unified access to 22+ satellite data providers with a consistent
5
5
  Python API and CLI. Supports authentication management, federated search,
@@ -30,7 +30,7 @@ CLI::
30
30
  pygeofetch doctor
31
31
  """
32
32
 
33
- __version__ = "2.3.0"
33
+ __version__ = "2.5.0"
34
34
  __author__ = "PyGeoFetch Contributors"
35
35
  __license__ = "MIT"
36
36
 
@@ -1,5 +1,5 @@
1
1
  """
2
- PyGeoFetch CLI v2.3.0 main entry point.
2
+ PyGeoFetch CLI v2.4.0 main entry point.
3
3
 
4
4
  Full command reference:
5
5
  pygeofetch auth — credential management
@@ -71,7 +71,7 @@ def cli(
71
71
  ) -> None:
72
72
  """
73
73
  \b
74
- PyGeoFetch v2.3.0 — Universal Satellite Data Pipeline
74
+ PyGeoFetch v2.4.0 — Universal Satellite Data Pipeline
75
75
  ===========================================================
76
76
  Unified access to 22+ satellite data providers.
77
77
 
@@ -1,38 +1,34 @@
1
1
  """
2
2
  Pre-download data-quality gate for InSAR stack selection.
3
-
4
3
  Runs AFTER search, BEFORE download. Catches — and where possible
5
4
  auto-corrects — real, confirmed data-selection failures this project
6
5
  has hit, so bandwidth and hours are never spent on a stack that was
7
6
  guaranteed to fail:
8
-
9
- 1. Search truncation -> auto-widen max_results & re-search
10
- 2. Mixed relative orbits -> confirmed (already enforced by
11
- select_consistent_geometry(), which
12
- should already have run before this
13
- gate; re-checked here defensively)
14
- 3. Multi-candidate per date -> coverage-aware dedup (already
15
- handled by search_and_select_
16
- consistent_stack(), which this gate
17
- calls)
18
- 4. 0%/low AOI coverage -> drop + report genuine gaps
19
- 5. Sparse temporal sampling -> predict SBAS connectivity, warn
20
- 6. Burst-timing family risk -> honest advisory, NOT a detector --
21
- see _screen_burst_families()'s own
22
- docstring for why a real pre-
23
- download detector isn't achievable
24
- with what's available before
25
- download, and for a real, confirmed
26
- bug found in an earlier version
27
- that claimed to be one anyway.
28
-
7
+ Search truncation -> auto-widen max_results & re-search
8
+ Mixed relative orbits -> confirmed (already enforced by
9
+ select_consistent_geometry(), which
10
+ should already have run before this
11
+ gate; re-checked here defensively)
12
+ Multi-candidate per date -> coverage-aware dedup (already
13
+ handled by search_and_select_
14
+ consistent_stack(), which this gate
15
+ calls)
16
+ 0%/low AOI coverage -> drop + report genuine gaps
17
+ Sparse temporal sampling -> predict SBAS connectivity, warn
18
+ Burst-timing family risk -> honest advisory, NOT a detector --
19
+ see _screen_burst_families()'s own
20
+ docstring for why a real pre-
21
+ download detector isn't achievable
22
+ with what's available before
23
+ download, and for a real, confirmed
24
+ bug found in an earlier version
25
+ that claimed to be one anyway.
29
26
  The definitive burst-family check still happens post-download via
30
27
  select_burst_synchronized_dates() (it needs SwathTiming parsed from
31
28
  each product's real annotation XML, which isn't available before
32
29
  download). This gate handles what real pre-download signals actually
33
30
  allow, and is explicit about the one real risk it cannot resolve early.
34
31
  """
35
-
36
32
  from __future__ import annotations
37
33
 
38
34
  import logging
@@ -44,16 +40,14 @@ from shapely.geometry import shape, box
44
40
 
45
41
  logger = logging.getLogger("pygeofetch.insar.preflight")
46
42
 
47
-
48
43
  # ─────────────────────────────────────────────────────────────────────────────
49
44
  # Report data model
50
- # ─────────────────────────────────────────────────────────────────────────────
45
+ # ────────────────────────────────────────────────────────────────────────────
51
46
 
52
47
  SEVERITY_GREEN = "GREEN" # safe to proceed
53
48
  SEVERITY_YELLOW = "YELLOW" # proceed, but with logged caveats
54
49
  SEVERITY_RED = "RED" # do not download; fix first
55
50
 
56
-
57
51
  @dataclass
58
52
  class PreflightIssue:
59
53
  """A single problem found during preflight, with its fix."""
@@ -63,12 +57,10 @@ class PreflightIssue:
63
57
  auto_fixed: bool = False # did the gate heal this automatically?
64
58
  fix_detail: str = "" # what the auto-fix did, if applied
65
59
 
66
-
67
60
  @dataclass
68
61
  class PreflightReport:
69
62
  """
70
63
  Full outcome of the pre-download gate.
71
-
72
64
  Carries the go/no-go decision, every issue found, every auto-fix
73
65
  applied, and — critically — the provenance manifest that makes the
74
66
  whole run reproducible and defensible.
@@ -78,16 +70,45 @@ class PreflightReport:
78
70
  issues: List[PreflightIssue] = field(default_factory=list)
79
71
  selected: List[Any] = field(default_factory=list) # final scene objects
80
72
  manifest: Dict[str, Any] = field(default_factory=dict)
73
+ original_count: int = 0 # how many scenes came IN, before any filtering -- see summary()'s own real-bug note
81
74
 
82
75
  def summary(self) -> str:
83
76
  lines = [f"PREFLIGHT {self.severity} — {'PROCEED' if self.go else 'BLOCKED'}"]
77
+
78
+ # Print Bandwidth Manifest if available
79
+ if "bandwidth_and_manifest" in self.manifest:
80
+ bw = self.manifest["bandwidth_and_manifest"]
81
+ lines.append(f" [INFO] Estimated download size: {bw['estimated_bandwidth_gb']} GB for {len(bw['download_manifest'])} scenes.")
82
+
84
83
  for issue in self.issues:
85
84
  fixed = " [AUTO-FIXED]" if issue.auto_fixed else ""
86
85
  lines.append(f" [{issue.severity}] {issue.code}{fixed}: {issue.message}")
87
86
  if issue.auto_fixed and issue.fix_detail:
88
87
  lines.append(f" -> {issue.fix_detail}")
89
- return "\n".join(lines)
90
88
 
89
+ # REAL BUG FOUND AND GUARDED AGAINST HERE: a real notebook
90
+ # called gate.run(selected, search_report) and printed
91
+ # report.summary() -- which correctly showed real dates being
92
+ # excluded -- but never reassigned `selected = report.selected`
93
+ # afterward, so every downstream step (the very next cell,
94
+ # literally titled "Download the real, filtered scenes") kept
95
+ # using the original, unfiltered list. The filtering logic was
96
+ # completely correct; the scenes it said to exclude got
97
+ # downloaded anyway, because nothing after this function call
98
+ # actually used its return value. This can't be fixed from
99
+ # inside this class (a function can't force a caller to use
100
+ # its return value) but it CAN be made loud and impossible to
101
+ # miss in the one place every real run already prints.
102
+ if self.original_count and len(self.selected) < self.original_count:
103
+ dropped = self.original_count - len(self.selected)
104
+ lines.append(
105
+ f" [REMINDER] {dropped} of {self.original_count} scene(s) were "
106
+ f"excluded by this gate. Use report.selected -- not your original "
107
+ f"list -- for every downstream step (download, extraction, "
108
+ f"processing). Passing the original list silently re-includes "
109
+ f"exactly what this gate just excluded."
110
+ )
111
+ return "\n".join(lines)
91
112
 
92
113
  # ─────────────────────────────────────────────────────────────────────────────
93
114
  # The gate itself
@@ -96,7 +117,6 @@ class PreflightReport:
96
117
  class PreflightGate:
97
118
  """
98
119
  Orchestrates every pre-download check and auto-heals what it can.
99
-
100
120
  Usage:
101
121
  gate = PreflightGate(client, aoi_bbox, start_date, end_date)
102
122
  report = gate.run(selected, search_report)
@@ -105,7 +125,6 @@ class PreflightGate:
105
125
  raise RuntimeError("Preflight blocked the stack. See report.")
106
126
  download_results = client.download(report.selected, ...)
107
127
  """
108
-
109
128
  def __init__(
110
129
  self,
111
130
  client: Any,
@@ -162,7 +181,7 @@ class PreflightGate:
162
181
  # authenticated provider these real annotation fetches need.
163
182
  self.attempt_real_burst_check = attempt_real_burst_check
164
183
 
165
- # ── public entry point ────────────────────────────────────────────────
184
+ # ── public entry point ───────────────────────────────────────────────
166
185
  def run(self, selected: List[Any], search_report: Dict[str, Any]) -> PreflightReport:
167
186
  issues: List[PreflightIssue] = []
168
187
  selected = list(selected) # work on a copy
@@ -172,6 +191,15 @@ class PreflightGate:
172
191
  if trunc_issue:
173
192
  issues.append(trunc_issue)
174
193
 
194
+ # Baseline for the "did anything get silently dropped downstream"
195
+ # reminder in summary() -- captured HERE, after truncation
196
+ # resolves, not at the very top: truncation can legitimately
197
+ # GROW the candidate pool (a wider re-search finding more real
198
+ # scenes), which isn't a "drop" and shouldn't suppress a real
199
+ # warning about coverage/burst-family filtering that happens
200
+ # after this point.
201
+ original_count = len(selected)
202
+
175
203
  # Check 2: AOI coverage per scene (drop genuine gaps)
176
204
  selected, cov_issues = self._check_coverage(selected)
177
205
  issues.extend(cov_issues)
@@ -180,7 +208,6 @@ class PreflightGate:
180
208
  issues.extend(self._check_temporal_network(selected))
181
209
 
182
210
  # Check 4: burst-timing family proxy screen
183
-
184
211
  geometry_report = search_report.get("geometry_report", {})
185
212
  selected, burst_issues = self._screen_burst_families(selected, geometry_report)
186
213
  issues.extend(burst_issues)
@@ -195,6 +222,7 @@ class PreflightGate:
195
222
  report = PreflightReport(
196
223
  go=go, severity=severity, issues=issues,
197
224
  selected=selected, manifest=manifest,
225
+ original_count=original_count,
198
226
  )
199
227
  logger.info("\n%s", report.summary())
200
228
  return report
@@ -294,7 +322,6 @@ class PreflightGate:
294
322
  candidate_pairs = generate_candidate_pairs(
295
323
  dates, max_temporal_baseline_days=self.max_temporal_baseline_days,
296
324
  )
297
-
298
325
  adjacency: Dict[str, set] = {d: set() for d in dates}
299
326
  for d1, d2 in candidate_pairs:
300
327
  adjacency[d1].add(d2)
@@ -309,8 +336,8 @@ class PreflightGate:
309
336
  for nb in adjacency[d] - visited:
310
337
  visited.add(nb)
311
338
  stack.append(nb)
312
- unconnected = set(dates) - visited
313
339
 
340
+ unconnected = set(dates) - visited
314
341
  issues = []
315
342
  if unconnected:
316
343
  issues.append(PreflightIssue(
@@ -351,7 +378,6 @@ class PreflightGate:
351
378
  full products), falling back to an honest "unassessed" advisory
352
379
  only if that isn't available or doesn't succeed. Never silently
353
380
  claims risk is absent just because it couldn't be checked.
354
-
355
381
  REAL BUG FIXED: earlier versions returned only `issues`, never
356
382
  `selected` -- so a successful real detection's own
357
383
  "BURST_FAMILY_DETECTED" issue was marked auto_fixed=True with a
@@ -366,7 +392,6 @@ class PreflightGate:
366
392
  report claimed the exclusion had already happened. Now returns
367
393
  the real, actually-filtered selection, matching the pattern
368
394
  _check_coverage() already used correctly.
369
-
370
395
  HISTORY, kept because it explains why this function is careful
371
396
  about false passes: an earlier version grouped scenes by
372
397
  (Track, Satellite) and treated agreement within a group as
@@ -381,7 +406,6 @@ class PreflightGate:
381
406
  check needs to catch. A second, independent bug in that version
382
407
  also made the (Track, Satellite) grouping non-functional in
383
408
  practice (wrong dict keys against the real scene object).
384
-
385
409
  What this function guarantees regardless of which path fires:
386
410
  every selected scene really is on a single, consistent track
387
411
  (already enforced by select_consistent_geometry(); re-checked
@@ -401,7 +425,6 @@ class PreflightGate:
401
425
  tracks.add(track)
402
426
 
403
427
  issues: List[PreflightIssue] = []
404
-
405
428
  if len(tracks) > 1:
406
429
  # Should be unreachable if select_consistent_geometry() already
407
430
  # ran correctly -- a real defensive check, not the expected path.
@@ -416,7 +439,6 @@ class PreflightGate:
416
439
  ))
417
440
 
418
441
  satellites_present = geometry_report.get("satellites", set())
419
-
420
442
  real_result = self._try_real_burst_family_check(selected) if self.attempt_real_burst_check else None
421
443
 
422
444
  nodes_version = "unknown"
@@ -496,6 +518,7 @@ class PreflightGate:
496
518
  f"specific pairs to correlate poorly too."
497
519
  if internal_bridges else ""
498
520
  )
521
+
499
522
  issues.append(PreflightIssue(
500
523
  code="BURST_FAMILY_DETECTED",
501
524
  severity=SEVERITY_GREEN if resolved else SEVERITY_YELLOW,
@@ -531,6 +554,7 @@ class PreflightGate:
531
554
  f"its own (see message). Full stack kept: {chosen_dates}."
532
555
  ),
533
556
  ))
557
+
534
558
  # ACTUALLY apply the recommendation -- not just report it.
535
559
  chosen_set = set(chosen_dates)
536
560
  filtered_selected = [s for s in selected if str(s.datetime)[:10] in chosen_set]
@@ -561,7 +585,6 @@ class PreflightGate:
561
585
  "absent."
562
586
  ),
563
587
  ))
564
-
565
588
  return selected, issues
566
589
 
567
590
  def _try_real_burst_family_check(
@@ -569,7 +592,6 @@ class PreflightGate:
569
592
  ) -> Optional[Tuple[List[str], Dict[str, Any]]]:
570
593
  """
571
594
  Best-effort REAL pre-download burst-family classification.
572
-
573
595
  Uses providers.copernicus_nodes to fetch only each candidate
574
596
  date's lightweight annotation XML (not the full, multi-GB
575
597
  product) plus its real orbit file, then runs the SAME, already-
@@ -580,7 +602,6 @@ class PreflightGate:
580
602
  parse_slc_geometry() read only the annotation XML either way,
581
603
  confirmed directly with an end-to-end test where the real,
582
604
  unmodified parser reads a mini-zip built this way correctly.
583
-
584
605
  Returns None -- never raises -- if anything real goes wrong:
585
606
  missing "copernicus" credentials, a network failure, an
586
607
  unexpected real API response shape, too few dates with both
@@ -589,7 +610,6 @@ class PreflightGate:
589
610
  advisory," never "risk confirmed absent."
590
611
  """
591
612
  import tempfile
592
-
593
613
  try:
594
614
  from pygeofetch.providers.copernicus_nodes import fetch_annotation_zip
595
615
  from pygeofetch.core.orbits import fetch_orbit_file
@@ -615,16 +635,40 @@ class PreflightGate:
615
635
  except Exception as exc:
616
636
  logger.info("Real pre-download burst check: no annotation for %s (%s).", date, exc)
617
637
  continue
638
+
618
639
  try:
619
640
  name = s.properties.get("name") if hasattr(s, "properties") else None
620
- orbit_file = fetch_orbit_file(
621
- product_name=name, output_dir=str(work_dir), orbit_type="precise",
622
- ) if name else None
641
+ if not name:
642
+ continue
643
+
644
+ # ── FIX: ORBIT CASCADING ──
645
+ # Previously, this only tried "precise". If precise wasn't
646
+ # available (e.g., recent acquisitions), the date was dropped.
647
+ # Now it cascades: Precise -> Restituted -> Predicted.
648
+ orbit_file = None
649
+ for orbit_type in ["precise", "restituted", "predicted"]:
650
+ try:
651
+ orbit_file = fetch_orbit_file(
652
+ product_name=name, output_dir=str(work_dir), orbit_type=orbit_type,
653
+ )
654
+ if orbit_file is not None:
655
+ if orbit_type != "precise":
656
+ logger.warning(
657
+ "Orbit cascading: fell back to '%s' orbit for %s "
658
+ "(precise not available).", orbit_type, date
659
+ )
660
+ break
661
+ except Exception as exc:
662
+ logger.debug("Orbit cascade: '%s' failed for %s (%s).", orbit_type, date, exc)
663
+ continue
664
+
623
665
  if orbit_file is None:
666
+ logger.info("Real pre-download burst check: no orbit file (any type) for %s.", date)
624
667
  continue
668
+
625
669
  orbit_files[date] = orbit_file
626
670
  except Exception as exc:
627
- logger.info("Real pre-download burst check: no orbit file for %s (%s).", date, exc)
671
+ logger.info("Real pre-download burst check: orbit cascade failed for %s (%s).", date, exc)
628
672
  continue
629
673
 
630
674
  usable_dates = sorted(set(safe_zips) & set(orbit_files))
@@ -640,7 +684,6 @@ class PreflightGate:
640
684
  aoi_center_lat = (self.aoi_bbox.min_lat + self.aoi_bbox.max_lat) / 2
641
685
  aoi_center_lon = (self.aoi_bbox.min_lon + self.aoi_bbox.max_lon) / 2
642
686
  ground_point = geodetic_to_ecef(aoi_center_lat, aoi_center_lon, 0.0)
643
-
644
687
  chosen_dates, family_report = select_burst_synchronized_dates(
645
688
  usable_dates, safe_zips, orbit_files, ground_point,
646
689
  min_majority_dates=self.min_majority_family_dates,
@@ -660,6 +703,22 @@ class PreflightGate:
660
703
  self, selected: List[Any], search_report: Dict[str, Any],
661
704
  issues: List[PreflightIssue],
662
705
  ) -> Dict[str, Any]:
706
+ # ── FIX: BANDWIDTH & DOWNLOAD MANIFEST ──
707
+ # Calculate estimated bandwidth and build a concrete manifest of
708
+ # what will actually be downloaded, preventing surprise disk fills.
709
+ ESTIMATED_GB_PER_SLC_SCENE = 7.5 # Sentinel-1 IW SLC average size
710
+ estimated_bandwidth_gb = len(selected) * ESTIMATED_GB_PER_SLC_SCENE
711
+
712
+ download_manifest = []
713
+ for s in selected:
714
+ props = s.properties if hasattr(s, "properties") else {}
715
+ download_manifest.append({
716
+ "date": str(s.datetime)[:10],
717
+ "scene_id": props.get("name"),
718
+ "satellite": props.get("platform"),
719
+ "polarisation": props.get("polarisation"),
720
+ })
721
+
663
722
  return {
664
723
  "search": {
665
724
  "start_date": self.start_date,
@@ -681,6 +740,11 @@ class PreflightGate:
681
740
  search_report.get("geometry_report", {}).get("satellites", set())
682
741
  ),
683
742
  },
743
+ # NEW: Bandwidth & Manifest section
744
+ "bandwidth_and_manifest": {
745
+ "estimated_bandwidth_gb": round(estimated_bandwidth_gb, 2),
746
+ "download_manifest": download_manifest,
747
+ },
684
748
  "preflight": {
685
749
  "severity": self._overall_severity(issues),
686
750
  "issues": [
@@ -356,6 +356,7 @@ class PhaseUnwrapper:
356
356
 
357
357
  def _unwrap_cli(self, igram, corr, nlooks, min_conncomp_frac, min_region_size):
358
358
  import numpy as np
359
+ import os
359
360
  cli_path = _ensure_snaphu_cli()
360
361
  height, width = igram.shape
361
362
  bin_dir = Path(cli_path).parent
@@ -371,10 +372,6 @@ class PhaseUnwrapper:
371
372
  corr.tofile(str(corr_path))
372
373
  nlooks_int = max(1, int(nlooks))
373
374
 
374
- # FIX: Removed 'CONNCOMPOUTFORMAT UINT_DATA'. It is not a valid
375
- # SNAPHU parameter. SNAPHU always writes connected components as
376
- # 32-bit unsigned integers by default. The strict Windows port
377
- # we use rejects the config file if it sees unknown keys.
378
375
  conf_text = f"""INFILE igram.cpx
379
376
  INFILEFORMAT COMPLEX_DATA
380
377
  CORRFILE corr.cor
@@ -396,7 +393,6 @@ NLOOKSAZ {nlooks_int}
396
393
  with tempfile.TemporaryDirectory() as tmp_dir:
397
394
  result, unw_path, cc_path = _run_snaphu(tmp_dir)
398
395
 
399
- # AUTO-REPAIR: Catch Windows STATUS_DLL_NOT_FOUND (0xC0000135)
400
396
  if result.returncode in (3221225781, -1073741515):
401
397
  logger.warning("SNAPHU crashed with STATUS_DLL_NOT_FOUND (0xC0000135). Auto-bundling runtime DLLs...")
402
398
  self._ensure_runtime_dlls(bin_dir)
@@ -406,8 +402,44 @@ NLOOKSAZ {nlooks_int}
406
402
  if result.returncode != 0:
407
403
  raise RuntimeError(f"SNAPHU CLI failed with code {result.returncode}: {result.stderr}")
408
404
 
409
- unw = np.fromfile(str(unw_path), dtype=np.float32).reshape((height, width))
410
- conncomp = np.fromfile(str(cc_path), dtype=np.uint32).reshape((height, width))
405
+ # ═══════════════════════════════════════════════════════════════
406
+ # SMART FILE READER (Handles Windows Port Quirks)
407
+ # ═══════════════════════════════════════════════════════════════
408
+ expected_pixels = height * width
409
+
410
+ # 1. Read Unwrapped Phase (Usually FLOAT_DATA = 4 bytes)
411
+ unw_size = os.path.getsize(str(unw_path))
412
+ unw_dtype = np.float32 if unw_size <= expected_pixels * 4 else np.float64
413
+ unw = np.fromfile(str(unw_path), dtype=unw_dtype)
414
+
415
+ # 2. Read Connected Components (Adapts to 1-byte, 2-byte, or 4-byte formats)
416
+ cc_size = os.path.getsize(str(cc_path))
417
+ if cc_size >= expected_pixels * 4:
418
+ cc_dtype = np.uint32
419
+ elif cc_size >= expected_pixels * 2:
420
+ cc_dtype = np.uint16
421
+ else:
422
+ cc_dtype = np.uint8 # The Windows port default
423
+
424
+ conncomp = np.fromfile(str(cc_path), dtype=cc_dtype)
425
+
426
+ # 3. Auto-heal the Windows "off-by-one" truncation bug
427
+ if conncomp.size < expected_pixels:
428
+ logger.warning(
429
+ "SNAPHU conncomp file is %d elements, expected %d. Padding missing bytes with 0 "
430
+ "(known Windows port truncation quirk).", conncomp.size, expected_pixels
431
+ )
432
+ conncomp = np.pad(conncomp, (0, expected_pixels - conncomp.size), mode='constant', constant_values=0)
433
+ elif conncomp.size > expected_pixels:
434
+ conncomp = conncomp[:expected_pixels]
435
+
436
+ if unw.size < expected_pixels:
437
+ unw = np.pad(unw, (0, expected_pixels - unw.size), mode='constant', constant_values=0)
438
+ elif unw.size > expected_pixels:
439
+ unw = unw[:expected_pixels]
440
+
441
+ unw = unw.reshape((height, width))
442
+ conncomp = conncomp.reshape((height, width))
411
443
 
412
444
  total_pixels = height * width
413
445
  labels, counts = np.unique(conncomp, return_counts=True)
@@ -591,4 +623,59 @@ NLOOKSAZ {nlooks_int}
591
623
 
592
624
  def _np(self):
593
625
  import numpy as np
594
- return np
626
+ return np
627
+
628
+
629
+ import shutil
630
+ import subprocess
631
+ import logging
632
+ import sys
633
+
634
+ logger = logging.getLogger(__name__)
635
+
636
+ def verify_snaphu_ready():
637
+ """
638
+ Proactively checks for snaphu availability to assure the user
639
+ that no manual installation is required.
640
+ """
641
+ # 1. Check for the standard SNAPHU binary in the system PATH
642
+ snaphu_bin = shutil.which("snaphu")
643
+ if snaphu_bin:
644
+ # Optional: Verify the binary actually executes and isn't just a broken alias
645
+ try:
646
+ subprocess.run(
647
+ ["snaphu"],
648
+ stdout=subprocess.DEVNULL,
649
+ stderr=subprocess.DEVNULL,
650
+ timeout=3
651
+ )
652
+ except Exception:
653
+ pass # Snaphu without args prints usage to stderr and exits; this is expected.
654
+
655
+ msg = f"✅ Success: snaphu binary detected at '{snaphu_bin}'. No manual installation needed."
656
+ print(msg)
657
+ logger.info(msg)
658
+ return True
659
+
660
+ # 2. Fallback: Check if it's available as a Python package/module
661
+ try:
662
+ import snaphu # or pysnaphu, depending on your wrapper
663
+ msg = "✅ Success: snaphu Python module detected. No manual installation needed."
664
+ print(msg)
665
+ logger.info(msg)
666
+ return True
667
+ except ImportError:
668
+ pass
669
+
670
+ # 3. If neither is found, handle the failure gracefully
671
+ msg = "❌ snaphu not found in PATH or Python environment. Phase unwrapping will fail."
672
+ print(msg)
673
+ logger.warning(msg)
674
+ return False
675
+
676
+ # --- Embed this early in your notebook or pipeline ---
677
+ snaphu_is_ready = verify_snaphu_ready()
678
+
679
+ if not snaphu_is_ready:
680
+ # Decide how to handle the missing dependency (see questions below)
681
+ pass
@@ -1,4 +1,4 @@
1
- {% set version = "2.3.0" %}
1
+ {% set version = "2.5.0" %}
2
2
 
3
3
  package:
4
4
  name: pygeofetch
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygeofetch
3
- Version: 2.3.0
3
+ Version: 2.5.0
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
@@ -91,8 +91,8 @@ Requires-Dist: sidecar>=0.6.0; extra == "viz"
91
91
  Requires-Dist: bokeh>=3.0.0; extra == "viz"
92
92
  Requires-Dist: seaborn>=0.12.0; extra == "viz"
93
93
  Requires-Dist: plotly>=5.14.0; extra == "viz"
94
- Requires-Dist: whitebox>=2.3.0; extra == "viz"
95
- Requires-Dist: whiteboxgui>=2.3.0; extra == "viz"
94
+ Requires-Dist: whitebox>=2.4.0; extra == "viz"
95
+ Requires-Dist: whiteboxgui>=2.4.0; extra == "viz"
96
96
  Requires-Dist: geemap>=0.28.0; extra == "viz"
97
97
  Requires-Dist: osmnx>=1.9.4; extra == "viz"
98
98
  Requires-Dist: contextily>=1.9.4; extra == "viz"
@@ -85,8 +85,8 @@ sidecar>=0.6.0
85
85
  bokeh>=3.0.0
86
86
  seaborn>=0.12.0
87
87
  plotly>=5.14.0
88
- whitebox>=2.3.0
89
- whiteboxgui>=2.3.0
88
+ whitebox>=2.4.0
89
+ whiteboxgui>=2.4.0
90
90
  geemap>=0.28.0
91
91
  osmnx>=1.9.4
92
92
  contextily>=1.9.4
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pygeofetch"
7
- version = "2.3.0"
7
+ version = "2.5.0"
8
8
  description = "Universal satellite data pipeline + geospatial processing platform"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -94,8 +94,8 @@ viz = [
94
94
  "bokeh>=3.0.0",
95
95
  "seaborn>=0.12.0",
96
96
  "plotly>=5.14.0",
97
- "whitebox>=2.3.0",
98
- "whiteboxgui>=2.3.0",
97
+ "whitebox>=2.4.0",
98
+ "whiteboxgui>=2.4.0",
99
99
  "geemap>=0.28.0",
100
100
  "osmnx>=1.9.4",
101
101
  "contextily>=1.9.4",