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.
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/CHANGELOG.md +5 -5
- {pygeofetch-2.6.1/pygeofetch.egg-info → pygeofetch-2.6.2}/PKG-INFO +1 -1
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/__init__.py +1 -1
- pygeofetch-2.6.2/pygeofetch/cli/monitor_commands.py +106 -0
- pygeofetch-2.6.2/pygeofetch/core/monitor.py +153 -0
- pygeofetch-2.6.2/pygeofetch/core/state.py +232 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/__init__.py +105 -3
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/geolocation.py +99 -0
- pygeofetch-2.6.2/pygeofetch/insar/offset_geometry.py +199 -0
- pygeofetch-2.6.2/pygeofetch/insar/offset_tracking.py +415 -0
- pygeofetch-2.6.2/pygeofetch/insar/ps_selection.py +347 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/timeseries.py +538 -259
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/validate.py +114 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/meta.yaml +1 -1
- {pygeofetch-2.6.1 → pygeofetch-2.6.2/pygeofetch.egg-info}/PKG-INFO +1 -1
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch.egg-info/SOURCES.txt +13 -2
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pyproject.toml +1 -1
- pygeofetch-2.6.2/tests/test_monitor.py +113 -0
- pygeofetch-2.6.2/tests/test_offset_tracking.py +231 -0
- pygeofetch-2.6.2/tests/test_ps_selection.py +231 -0
- pygeofetch-2.6.2/tests/test_sign_convention.py +175 -0
- pygeofetch-2.6.2/tests/test_state.py +123 -0
- pygeofetch-2.6.2/tests/test_wls_inversion.py +167 -0
- pygeofetch-2.6.1/tests/test_atmospheric_circular_regression.py +0 -104
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/LICENSE +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/MANIFEST.in +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/README.md +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/__init__.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/auth_commands.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/config_commands.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/download_commands.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/index_commands.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/main.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/pipeline_process_commands.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/postprocess_commands.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/preprocess_commands.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/sar_commands.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/cli/search_commands.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/config/__init__.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/config/defaults.yaml +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/config/settings.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/__init__.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/authenticator.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/cache_manager.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/downloader.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/engine.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/logging.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/orbits.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/scheduler.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/core/searcher.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/analysis.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/annotation.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/atmosphere.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/coregister.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/deburst.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/esd.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/extraction.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/flatearth.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/gpu.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/interferogram.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/ionosphere.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/preflight.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/provenance.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/splitspectrum_diagnostic.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/stack_selection.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/synthetic.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/unwrap.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/visualize.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/insar/workflow.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/models/__init__.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/models/download_task.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/models/satellite_data.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/models/search_query.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/models/user_auth.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/__init__.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/base.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/batch.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/indices.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/pipeline.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/postprocessor.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/preprocessor.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processing/sar.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processor/__init__.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processor/indices.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processor/landsat.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processor/loader.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processor/stacker.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/processor/timeseries.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/__init__.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/airbus_oneatlas.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/alaska_satellite_facility.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/aws_earth.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/base.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/copernicus.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/copernicus_nodes.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/digitalglobe.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/earth_explorer_additional.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/element84.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/eodag_provider.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/esa_scihub.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/geoserver_generic.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/google_earth_engine.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/inpe_cbers.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/isro_bhuvan.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/jaxa_earth.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/maxar_gbdx.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/nasa_earthdata.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/nasa_earthdata_cloud.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/noaa_big_data.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/opentopography.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/planet.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/planetary_computer.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/sentinel_hub.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/terrabotics.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/providers/usgs.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/sar/__init__.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/sar/_native.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/sar/_ost.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/sar/_sarxarray.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/sar/extraction.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/sar/processor.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/utils/__init__.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/utils/file_utils.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/utils/gcp_recovery.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/utils/geo_utils.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/utils/logging_setup.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/utils/retry_handler.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/utils/validators.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/viz/__init__.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/viz/map.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch/viz/plot.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch.egg-info/dependency_links.txt +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch.egg-info/entry_points.txt +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch.egg-info/not-zip-safe +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch.egg-info/requires.txt +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/pygeofetch.egg-info/top_level.txt +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/setup.cfg +0 -0
- /pygeofetch-2.6.1/tests/test_atmospheric_circular_regression (1).py → /pygeofetch-2.6.2/tests/test_atmospheric_circular_regression.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_build_sbas_network.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_burst_metadata.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_burst_processing_integration.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_burst_sync_large_gap.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_burst_sync_prescreening.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_burst_synchronization.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_copernicus_nodes.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_coregister_integration.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_coregister_upgrades.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_era5_atmospheric_correction.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_extraction.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_flat_earth_phase.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_gcp_reprojection.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_insar.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_ionosphere.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_map_preview_consolidation.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_models.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_pgv_integration_contract.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_preflight_gate (1).py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_preflight_gate.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_provider_geometry_audit (1).py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_provider_geometry_audit.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_provider_scene_disambiguation.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_providers.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_raster_collocation_integration.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_reference_pixel_and_despike.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_risk_mapping.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_screen_stack_orchestration.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_search_download.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_splitspectrum_diagnostic.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_stack_selection_consolidation.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_topo_r2_bug.py +0 -0
- {pygeofetch-2.6.1 → pygeofetch-2.6.2}/tests/test_utils.py +0 -0
- {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.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/2.6.2/).
|
|
5
5
|
|
|
6
|
-
## [2.6.
|
|
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.
|
|
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.
|
|
192
|
-
| **2.6.
|
|
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 |
|
|
@@ -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
|
+
]
|