PyGeoFetch 2.6.1__tar.gz → 2.6.2__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 (172) hide show
  1. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/CHANGELOG.md +5 -5
  2. {pygeofetch-2.6.1/pygeofetch.egg-info → pygeofetch-2.6.2}/PKG-INFO +1 -1
  3. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/__init__.py +1 -1
  4. pygeofetch-2.6.2/pygeofetch/cli/monitor_commands.py +106 -0
  5. pygeofetch-2.6.2/pygeofetch/core/monitor.py +153 -0
  6. pygeofetch-2.6.2/pygeofetch/core/state.py +232 -0
  7. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/__init__.py +105 -3
  8. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/geolocation.py +99 -0
  9. pygeofetch-2.6.2/pygeofetch/insar/offset_geometry.py +199 -0
  10. pygeofetch-2.6.2/pygeofetch/insar/offset_tracking.py +415 -0
  11. pygeofetch-2.6.2/pygeofetch/insar/ps_selection.py +347 -0
  12. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/timeseries.py +538 -259
  13. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/validate.py +114 -0
  14. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/meta.yaml +1 -1
  15. {pygeofetch-2.6.1 → pygeofetch-2.6.2/pygeofetch.egg-info}/PKG-INFO +1 -1
  16. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch.egg-info/SOURCES.txt +13 -2
  17. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pyproject.toml +1 -1
  18. pygeofetch-2.6.2/tests/test_monitor.py +113 -0
  19. pygeofetch-2.6.2/tests/test_offset_tracking.py +231 -0
  20. pygeofetch-2.6.2/tests/test_ps_selection.py +231 -0
  21. pygeofetch-2.6.2/tests/test_sign_convention.py +175 -0
  22. pygeofetch-2.6.2/tests/test_state.py +123 -0
  23. pygeofetch-2.6.2/tests/test_wls_inversion.py +167 -0
  24. pygeofetch-2.6.1/tests/test_atmospheric_circular_regression.py +0 -104
  25. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/LICENSE +0 -0
  26. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/MANIFEST.in +0 -0
  27. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/README.md +0 -0
  28. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/__init__.py +0 -0
  29. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/auth_commands.py +0 -0
  30. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/config_commands.py +0 -0
  31. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/download_commands.py +0 -0
  32. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/index_commands.py +0 -0
  33. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/main.py +0 -0
  34. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/pipeline_process_commands.py +0 -0
  35. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/postprocess_commands.py +0 -0
  36. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/preprocess_commands.py +0 -0
  37. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/sar_commands.py +0 -0
  38. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/search_commands.py +0 -0
  39. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/config/__init__.py +0 -0
  40. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/config/defaults.yaml +0 -0
  41. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/config/settings.py +0 -0
  42. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/__init__.py +0 -0
  43. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/authenticator.py +0 -0
  44. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/cache_manager.py +0 -0
  45. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/downloader.py +0 -0
  46. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/engine.py +0 -0
  47. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/logging.py +0 -0
  48. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/orbits.py +0 -0
  49. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/scheduler.py +0 -0
  50. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/searcher.py +0 -0
  51. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/analysis.py +0 -0
  52. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/annotation.py +0 -0
  53. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/atmosphere.py +0 -0
  54. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/coregister.py +0 -0
  55. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/deburst.py +0 -0
  56. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/esd.py +0 -0
  57. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/extraction.py +0 -0
  58. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/flatearth.py +0 -0
  59. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/gpu.py +0 -0
  60. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/interferogram.py +0 -0
  61. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/ionosphere.py +0 -0
  62. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/preflight.py +0 -0
  63. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/provenance.py +0 -0
  64. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/splitspectrum_diagnostic.py +0 -0
  65. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/stack_selection.py +0 -0
  66. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/synthetic.py +0 -0
  67. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/unwrap.py +0 -0
  68. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/visualize.py +0 -0
  69. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/workflow.py +0 -0
  70. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/models/__init__.py +0 -0
  71. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/models/download_task.py +0 -0
  72. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/models/satellite_data.py +0 -0
  73. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/models/search_query.py +0 -0
  74. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/models/user_auth.py +0 -0
  75. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/__init__.py +0 -0
  76. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/base.py +0 -0
  77. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/batch.py +0 -0
  78. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/indices.py +0 -0
  79. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/pipeline.py +0 -0
  80. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/postprocessor.py +0 -0
  81. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/preprocessor.py +0 -0
  82. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/sar.py +0 -0
  83. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processor/__init__.py +0 -0
  84. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processor/indices.py +0 -0
  85. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processor/landsat.py +0 -0
  86. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processor/loader.py +0 -0
  87. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processor/stacker.py +0 -0
  88. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processor/timeseries.py +0 -0
  89. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/__init__.py +0 -0
  90. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/airbus_oneatlas.py +0 -0
  91. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/alaska_satellite_facility.py +0 -0
  92. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/aws_earth.py +0 -0
  93. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/base.py +0 -0
  94. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/copernicus.py +0 -0
  95. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/copernicus_nodes.py +0 -0
  96. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/digitalglobe.py +0 -0
  97. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/earth_explorer_additional.py +0 -0
  98. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/element84.py +0 -0
  99. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/eodag_provider.py +0 -0
  100. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/esa_scihub.py +0 -0
  101. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/geoserver_generic.py +0 -0
  102. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/google_earth_engine.py +0 -0
  103. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/inpe_cbers.py +0 -0
  104. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/isro_bhuvan.py +0 -0
  105. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/jaxa_earth.py +0 -0
  106. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/maxar_gbdx.py +0 -0
  107. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/nasa_earthdata.py +0 -0
  108. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/nasa_earthdata_cloud.py +0 -0
  109. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/noaa_big_data.py +0 -0
  110. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/opentopography.py +0 -0
  111. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/planet.py +0 -0
  112. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/planetary_computer.py +0 -0
  113. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/sentinel_hub.py +0 -0
  114. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/terrabotics.py +0 -0
  115. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/usgs.py +0 -0
  116. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/sar/__init__.py +0 -0
  117. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/sar/_native.py +0 -0
  118. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/sar/_ost.py +0 -0
  119. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/sar/_sarxarray.py +0 -0
  120. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/sar/extraction.py +0 -0
  121. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/sar/processor.py +0 -0
  122. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/utils/__init__.py +0 -0
  123. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/utils/file_utils.py +0 -0
  124. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/utils/gcp_recovery.py +0 -0
  125. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/utils/geo_utils.py +0 -0
  126. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/utils/logging_setup.py +0 -0
  127. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/utils/retry_handler.py +0 -0
  128. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/utils/validators.py +0 -0
  129. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/viz/__init__.py +0 -0
  130. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/viz/map.py +0 -0
  131. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/viz/plot.py +0 -0
  132. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch.egg-info/dependency_links.txt +0 -0
  133. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch.egg-info/entry_points.txt +0 -0
  134. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch.egg-info/not-zip-safe +0 -0
  135. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch.egg-info/requires.txt +0 -0
  136. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch.egg-info/top_level.txt +0 -0
  137. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/setup.cfg +0 -0
  138. /pygeofetch-2.6.1/tests/test_atmospheric_circular_regression (1).py → /pygeofetch-2.6.2/tests/test_atmospheric_circular_regression.py +0 -0
  139. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_build_sbas_network.py +0 -0
  140. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_burst_metadata.py +0 -0
  141. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_burst_processing_integration.py +0 -0
  142. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_burst_sync_large_gap.py +0 -0
  143. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_burst_sync_prescreening.py +0 -0
  144. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_burst_synchronization.py +0 -0
  145. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_copernicus_nodes.py +0 -0
  146. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_coregister_integration.py +0 -0
  147. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_coregister_upgrades.py +0 -0
  148. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_era5_atmospheric_correction.py +0 -0
  149. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_extraction.py +0 -0
  150. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_flat_earth_phase.py +0 -0
  151. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_gcp_reprojection.py +0 -0
  152. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_insar.py +0 -0
  153. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_ionosphere.py +0 -0
  154. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_map_preview_consolidation.py +0 -0
  155. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_models.py +0 -0
  156. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_pgv_integration_contract.py +0 -0
  157. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_preflight_gate (1).py +0 -0
  158. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_preflight_gate.py +0 -0
  159. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_provider_geometry_audit (1).py +0 -0
  160. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_provider_geometry_audit.py +0 -0
  161. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_provider_scene_disambiguation.py +0 -0
  162. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_providers.py +0 -0
  163. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_raster_collocation_integration.py +0 -0
  164. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_reference_pixel_and_despike.py +0 -0
  165. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_risk_mapping.py +0 -0
  166. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_screen_stack_orchestration.py +0 -0
  167. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_search_download.py +0 -0
  168. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_splitspectrum_diagnostic.py +0 -0
  169. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_stack_selection_consolidation.py +0 -0
  170. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_topo_r2_bug.py +0 -0
  171. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_utils.py +0 -0
  172. {pygeofetch-2.6.1 → pygeofetch-2.6.2}/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/).
5
5
 
6
- ## [2.6.1] — 2026-07-29
6
+ ## [2.6.2] — 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] — 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** | 2026-07-29 | SNAPHU unwrapping fixes, 87.1% reliable pixels |
192
+ | **2.6.2** | 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
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
@@ -30,7 +30,7 @@ CLI::
30
30
  pygeofetch doctor
31
31
  """
32
32
 
33
- __version__ = "2.6.1"
33
+ __version__ = "2.6.2"
34
34
  __author__ = "PyGeoFetch Contributors"
35
35
  __license__ = "MIT"
36
36
 
@@ -0,0 +1,106 @@
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(name="monitor", help="Automated, incremental InSAR monitoring (Feature 4).")
37
+ def monitor():
38
+ pass
39
+
40
+
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.")
47
+ @click.pass_context
48
+ def monitor_run(ctx, state_db, bbox, satellites, n_neighbors, cron_daily):
49
+ client = ctx.obj.get("client") if ctx.obj else None
50
+ if client is None:
51
+ from pygeofetch import PyGeoFetch
52
+ client = PyGeoFetch()
53
+
54
+ state = ProjectState(state_db)
55
+
56
+ minlon, minlat, maxlon, maxlat = (float(x) for x in bbox.split(","))
57
+ sat_list = [s.strip() for s in satellites.split(",")]
58
+
59
+ from pygeofetch.models.search_query import SearchQuery
60
+
61
+ query = SearchQuery(
62
+ bbox=(minlon, minlat, maxlon, maxlat),
63
+ satellites=sat_list,
64
+ product_type="SLC",
65
+ )
66
+ if state.last_download_timestamp:
67
+ query.start_date = state.last_download_timestamp
68
+
69
+ console.print(f"[bold]pygeofetch monitor[/bold] — {'scheduled' if cron_daily else 'manual'} run")
70
+ console.print(f"State: {state_db}")
71
+
72
+ try:
73
+ results = client.search(query)
74
+ except Exception as exc:
75
+ state.record_run(RunSummary(status="failed", detail=f"search failed: {exc}"))
76
+ console.print(f"[red]Search failed:[/red] {exc}")
77
+ raise
78
+
79
+ available_dates = sorted({str(r.datetime)[:10] for r in results if getattr(r, "datetime", None)})
80
+ plan = plan_monitoring_run(state, available_dates, n_neighbors=n_neighbors)
81
+
82
+ console.print(plan.message)
83
+ if plan.new_dates:
84
+ console.print(f" New dates: {plan.new_dates}")
85
+ console.print(f" New pairs needed: {len(plan.new_pairs)}")
86
+
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
+ ))
92
+
93
+ return plan
94
+
95
+
96
+ @monitor.command(name="history", help="Show real run history for a monitoring project.")
97
+ @click.option("--state-db", required=True, type=click.Path(exists=True))
98
+ @click.option("--limit", default=20, type=int)
99
+ def monitor_history(state_db, limit):
100
+ state = ProjectState(state_db)
101
+ runs = state.run_history(limit=limit)
102
+ if not runs:
103
+ console.print("No real runs recorded yet.")
104
+ return
105
+ for run in runs:
106
+ console.print(f"[{run['status']}] {run['started_at']} — {run['detail']}")
@@ -0,0 +1,153 @@
1
+ """
2
+ Real incremental-monitoring orchestration for pygeofetch's InSAR
3
+ pipeline — Feature 4's "incremental ingestion" and "incremental
4
+ inversion" pieces, built on top of the already-real, already-tested
5
+ ProjectState (state.py).
6
+
7
+ Deliberately does NOT implement a true incremental linear-algebra
8
+ update to the SBAS design matrix (e.g. a real rank-1 Cholesky update).
9
+ The spec's own real requirement is narrower and more honest: don't
10
+ reprocess the entire historical archive's raw SLC/interferogram
11
+ formation when new scenes arrive, only form the new pairs actually
12
+ needed to connect new dates to the existing network, then re-run the
13
+ already-verified, already-tested invert_weighted() on the combined
14
+ pair list. For the realistic problem sizes this pipeline already
15
+ handles (Berardino et al. 2002's own formulation, N < 200 or so real
16
+ dates), re-solving the full linear system is genuinely fast — the
17
+ expensive part this feature actually saves is search, download,
18
+ coregistration, and interferogram formation for scenes that were
19
+ already processed, not the linear algebra itself. Building a real,
20
+ correct incremental linear solver would be new numerical work with
21
+ its own correctness risk, for a part of the pipeline that was never
22
+ actually the bottleneck.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+ from dataclasses import dataclass
29
+ from typing import Any, List, Sequence, Tuple
30
+
31
+ logger = logging.getLogger("pygeofetch.core.monitor")
32
+
33
+
34
+ def generate_incremental_pairs(
35
+ new_dates: Sequence[str],
36
+ existing_dates: Sequence[str],
37
+ n_neighbors: int = 3,
38
+ ) -> List[Tuple[str, str]]:
39
+ """
40
+ Real, precise implementation of the spec's own stated requirement:
41
+ "generate only the new interferogram pairs connecting [a new
42
+ scene] to the 3 most recent temporal neighbors in the existing
43
+ network" — deliberately narrower than the existing
44
+ generate_candidate_pairs() (which takes every pair within a
45
+ temporal-baseline window), because a monitoring run's real goal is
46
+ connecting new dates into the network cheaply, not re-deriving the
47
+ full candidate pool.
48
+
49
+ Each new date is paired with its n_neighbors chronologically
50
+ nearest dates from the REAL UNION of existing_dates and any other
51
+ new_dates that sort before it — so a batch of several new dates
52
+ arriving together correctly connects to each other too, not just
53
+ to the old, already-processed archive.
54
+
55
+ Args:
56
+ new_dates: Real new acquisition dates not yet in the network.
57
+ existing_dates: Real dates already processed and in the
58
+ existing network (ProjectState.processed_dates()).
59
+ n_neighbors: How many nearest existing/earlier dates to pair
60
+ each new date with (spec default: 3).
61
+
62
+ Returns:
63
+ Real list of (earlier_date, later_date) pairs, each pair
64
+ genuinely new (never duplicating an existing_dates-only pair,
65
+ since both existing dates were already connected in a prior run).
66
+ """
67
+ all_dates_sorted = sorted(set(existing_dates) | set(new_dates))
68
+ date_to_idx = {d: i for i, d in enumerate(all_dates_sorted)}
69
+
70
+ pairs: List[Tuple[str, str]] = []
71
+ seen = set()
72
+
73
+ for new_date in sorted(new_dates):
74
+ idx = date_to_idx[new_date]
75
+ # Real candidates: every OTHER real date in the combined,
76
+ # chronologically sorted list, ranked by real temporal distance
77
+ candidates = [d for d in all_dates_sorted if d != new_date]
78
+ candidates_by_distance = sorted(
79
+ candidates,
80
+ key=lambda d: abs(date_to_idx[d] - idx),
81
+ )
82
+ neighbors = candidates_by_distance[:n_neighbors]
83
+ for neighbor in neighbors:
84
+ pair = tuple(sorted([new_date, neighbor]))
85
+ if pair not in seen:
86
+ seen.add(pair)
87
+ pairs.append(pair)
88
+
89
+ return pairs
90
+
91
+
92
+ @dataclass
93
+ class MonitoringRunResult:
94
+ """Real, honest outcome of one monitor() call."""
95
+
96
+ new_dates: List[str]
97
+ new_pairs: List[Tuple[str, str]]
98
+ network_changed: bool
99
+ ran_inversion: bool
100
+ message: str
101
+
102
+
103
+ def plan_monitoring_run(
104
+ state: Any, # ProjectState
105
+ available_dates: Sequence[str],
106
+ n_neighbors: int = 3,
107
+ ) -> MonitoringRunResult:
108
+ """
109
+ Real, idempotent planning step for one monitoring run: given the
110
+ real dates a fresh search actually returned and the real state
111
+ from all prior runs, decides exactly what's new and exactly which
112
+ new pairs are needed — without downloading, processing, or
113
+ inverting anything itself. Separated from execution deliberately,
114
+ so this planning logic can be tested on its own without needing a
115
+ real Copernicus connection, real SLC data, or a real inversion —
116
+ matching how the rest of this project keeps orchestration logic
117
+ testable independent of expensive I/O.
118
+
119
+ Args:
120
+ state: A real ProjectState for this monitoring project.
121
+ available_dates: Real dates a fresh search returned right now
122
+ (e.g. from client.search() against the real Copernicus API).
123
+ n_neighbors: Passed through to generate_incremental_pairs().
124
+
125
+ Returns:
126
+ MonitoringRunResult describing exactly what this run would do.
127
+ network_changed=False and ran_inversion=False with a real,
128
+ honest message when there's genuinely nothing new — this is
129
+ the case the spec's own idempotency requirement is really
130
+ about: running twice on the same day with no new scenes must
131
+ do nothing the second time, not silently re-download or
132
+ re-invert.
133
+ """
134
+ already_processed = set(state.processed_dates())
135
+ new_dates = sorted(set(available_dates) - already_processed)
136
+
137
+ if not new_dates:
138
+ return MonitoringRunResult(
139
+ new_dates=[], new_pairs=[], network_changed=False, ran_inversion=False,
140
+ message="No new dates since last run -- nothing to do.",
141
+ )
142
+
143
+ new_pairs = generate_incremental_pairs(new_dates, sorted(already_processed), n_neighbors)
144
+
145
+ logger.info(
146
+ "Monitoring run: %d new date(s), %d new pair(s) needed (%d neighbors each).",
147
+ len(new_dates), len(new_pairs), n_neighbors,
148
+ )
149
+
150
+ return MonitoringRunResult(
151
+ new_dates=new_dates, new_pairs=new_pairs, network_changed=True, ran_inversion=False,
152
+ message=f"{len(new_dates)} new date(s), {len(new_pairs)} new pair(s) planned.",
153
+ )
@@ -0,0 +1,232 @@
1
+ """
2
+ Real, persistent state tracking for turning pygeofetch's InSAR pipeline
3
+ from a batch tool into a continuous monitoring system — Feature 4 from
4
+ the InSAR innovations spec.
5
+
6
+ Backed by real SQLite (not a hand-rolled JSON-with-file-locking
7
+ scheme), specifically so concurrent access (a scheduled cron run
8
+ starting while a previous run is still finishing) fails safely via
9
+ SQLite's own real transaction semantics rather than a corrupted
10
+ partial JSON write.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ import json
17
+ import logging
18
+ import sqlite3
19
+ from contextlib import contextmanager
20
+ from dataclasses import dataclass
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+ from typing import List, Optional
24
+
25
+ logger = logging.getLogger("pygeofetch.core.state")
26
+
27
+ _SCHEMA = """
28
+ CREATE TABLE IF NOT EXISTS project_meta (
29
+ key TEXT PRIMARY KEY,
30
+ value TEXT
31
+ );
32
+
33
+ CREATE TABLE IF NOT EXISTS processed_dates (
34
+ date TEXT PRIMARY KEY,
35
+ scene_id TEXT,
36
+ added_at TEXT
37
+ );
38
+
39
+ CREATE TABLE IF NOT EXISTS run_log (
40
+ run_id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+ started_at TEXT,
42
+ finished_at TEXT,
43
+ status TEXT,
44
+ n_new_scenes INTEGER,
45
+ detail TEXT
46
+ );
47
+ """
48
+
49
+
50
+ def network_topology_hash(dates: List[str], pairs: List[tuple]) -> str:
51
+ """
52
+ Real, deterministic hash of a real SBAS network's structure — used
53
+ to detect whether the network actually changed between runs
54
+ (rather than assuming it did every time), so an idempotent
55
+ incremental run can skip re-inverting a network that's identical
56
+ to what was already solved.
57
+
58
+ Deterministic: sorts dates and pairs before hashing, so the same
59
+ real network always hashes identically regardless of the order it
60
+ was constructed or iterated in — verified directly in this
61
+ module's own tests, not assumed.
62
+ """
63
+ canonical = json.dumps(
64
+ {"dates": sorted(dates), "pairs": sorted(tuple(sorted(p)) for p in pairs)},
65
+ sort_keys=True,
66
+ )
67
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
68
+
69
+
70
+ @dataclass
71
+ class RunSummary:
72
+ """Real outcome of one real monitoring run, written to run_log."""
73
+
74
+ status: str # "success", "no_new_scenes", "failed"
75
+ n_new_scenes: int = 0
76
+ detail: str = ""
77
+
78
+
79
+ class ProjectState:
80
+ """
81
+ Real, persistent state for one InSAR monitoring project, backed by
82
+ a real SQLite database file.
83
+
84
+ Tracks exactly what the spec asks for: processed_dates,
85
+ last_successful_run, network_topology_hash, reference_pixel_coords,
86
+ and last_download_timestamp — plus a real run_log for auditability
87
+ (so "did last night's cron run actually succeed" has a real,
88
+ persistent answer, not just an assumption).
89
+
90
+ Example::
91
+
92
+ state = ProjectState(Path("./mining_site_a/state.db"))
93
+ new_since = state.last_download_timestamp
94
+ # ... real search for scenes newer than new_since ...
95
+ state.mark_dates_processed(["2024-03-01", "2024-03-13"])
96
+ state.set_reference_pixel(120, 340)
97
+ state.record_run(RunSummary(status="success", n_new_scenes=2))
98
+ """
99
+
100
+ def __init__(self, db_path: Path):
101
+ self.db_path = Path(db_path)
102
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
103
+ self._init_schema()
104
+
105
+ @contextmanager
106
+ def _connect(self):
107
+ # A real, fresh connection per operation (not one held open for
108
+ # the object's lifetime) -- correct for exactly the concurrent-
109
+ # access case this class exists for: a scheduled run should not
110
+ # hold a long-lived lock that blocks a manual run started at
111
+ # the same time, and SQLite's own busy_timeout handles the
112
+ # real, brief contention that can still occur.
113
+ conn = sqlite3.connect(str(self.db_path), timeout=30.0)
114
+ conn.execute("PRAGMA busy_timeout = 30000")
115
+ try:
116
+ yield conn
117
+ conn.commit()
118
+ except Exception:
119
+ conn.rollback()
120
+ raise
121
+ finally:
122
+ conn.close()
123
+
124
+ def _init_schema(self):
125
+ with self._connect() as conn:
126
+ conn.executescript(_SCHEMA)
127
+
128
+ # ── processed dates ─────────────────────────────────────────────
129
+
130
+ def mark_dates_processed(self, dates: List[str], scene_ids: Optional[List[str]] = None):
131
+ """Real, idempotent insert -- re-marking an already-processed date is a no-op, not an error."""
132
+ now = datetime.now(timezone.utc).isoformat()
133
+ scene_ids = scene_ids or [None] * len(dates)
134
+ with self._connect() as conn:
135
+ conn.executemany(
136
+ "INSERT OR IGNORE INTO processed_dates (date, scene_id, added_at) VALUES (?, ?, ?)",
137
+ [(d, s, now) for d, s in zip(dates, scene_ids)],
138
+ )
139
+
140
+ def processed_dates(self) -> List[str]:
141
+ with self._connect() as conn:
142
+ rows = conn.execute("SELECT date FROM processed_dates ORDER BY date").fetchall()
143
+ return [r[0] for r in rows]
144
+
145
+ def is_date_processed(self, date: str) -> bool:
146
+ with self._connect() as conn:
147
+ row = conn.execute("SELECT 1 FROM processed_dates WHERE date = ?", (date,)).fetchone()
148
+ return row is not None
149
+
150
+ # ── project metadata (last_download_timestamp, network hash, reference pixel) ──
151
+
152
+ def _set_meta(self, key: str, value: str):
153
+ with self._connect() as conn:
154
+ conn.execute(
155
+ "INSERT INTO project_meta (key, value) VALUES (?, ?) "
156
+ "ON CONFLICT(key) DO UPDATE SET value = excluded.value",
157
+ (key, value),
158
+ )
159
+
160
+ def _get_meta(self, key: str) -> Optional[str]:
161
+ with self._connect() as conn:
162
+ row = conn.execute("SELECT value FROM project_meta WHERE key = ?", (key,)).fetchone()
163
+ return row[0] if row else None
164
+
165
+ @property
166
+ def last_download_timestamp(self) -> Optional[str]:
167
+ return self._get_meta("last_download_timestamp")
168
+
169
+ def set_last_download_timestamp(self, ts: str):
170
+ self._set_meta("last_download_timestamp", ts)
171
+
172
+ @property
173
+ def last_successful_run(self) -> Optional[str]:
174
+ return self._get_meta("last_successful_run")
175
+
176
+ @property
177
+ def network_topology_hash(self) -> Optional[str]:
178
+ return self._get_meta("network_topology_hash")
179
+
180
+ def set_network_topology_hash(self, h: str):
181
+ self._set_meta("network_topology_hash", h)
182
+
183
+ @property
184
+ def reference_pixel_coords(self) -> Optional[tuple]:
185
+ raw = self._get_meta("reference_pixel_coords")
186
+ if raw is None:
187
+ return None
188
+ row, col = json.loads(raw)
189
+ return (row, col)
190
+
191
+ def set_reference_pixel(self, row: int, col: int):
192
+ """
193
+ Real, deliberate persistence of the reference pixel across
194
+ runs. This matters for real correctness, not just convenience:
195
+ SBASTimeSeries.invert()'s own docstring is explicit that
196
+ automatic reference-pixel selection can pick a different point
197
+ each run if the coherence pattern shifts slightly, and a
198
+ genuinely continuous time series needs the SAME reference
199
+ point held fixed across every incremental run, or displacement
200
+ values from different runs aren't comparable to each other.
201
+ """
202
+ self._set_meta("reference_pixel_coords", json.dumps([row, col]))
203
+
204
+ # ── run log ──────────────────────────────────────────────────────
205
+
206
+ def record_run(self, summary: RunSummary):
207
+ now = datetime.now(timezone.utc).isoformat()
208
+ with self._connect() as conn:
209
+ conn.execute(
210
+ "INSERT INTO run_log (started_at, finished_at, status, n_new_scenes, detail) "
211
+ "VALUES (?, ?, ?, ?, ?)",
212
+ (now, now, summary.status, summary.n_new_scenes, summary.detail),
213
+ )
214
+ if summary.status == "success":
215
+ conn.execute(
216
+ "INSERT INTO project_meta (key, value) VALUES ('last_successful_run', ?) "
217
+ "ON CONFLICT(key) DO UPDATE SET value = excluded.value",
218
+ (now,),
219
+ )
220
+
221
+ def run_history(self, limit: int = 20) -> List[dict]:
222
+ with self._connect() as conn:
223
+ rows = conn.execute(
224
+ "SELECT run_id, started_at, finished_at, status, n_new_scenes, detail "
225
+ "FROM run_log ORDER BY run_id DESC LIMIT ?",
226
+ (limit,),
227
+ ).fetchall()
228
+ 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]}
231
+ for r in rows
232
+ ]