gea-program 0.43.0__tar.gz → 0.44.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.
- {gea_program-0.43.0 → gea_program-0.44.0}/PKG-INFO +15 -1
- {gea_program-0.43.0 → gea_program-0.44.0}/README.md +14 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/__init__.py +1 -1
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/__main__.py +54 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/acceptance_tests.py +216 -0
- gea_program-0.44.0/gea/alerts.py +177 -0
- gea_program-0.44.0/gea/help/alerts.md +17 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/notifications.md +1 -1
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/opseis.md +2 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/helplib.py +1 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/notify.py +22 -2
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/op_seismicity.py +7 -0
- gea_program-0.44.0/gea/seismic_watch.py +336 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/service.py +18 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/web/app.html +22 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea_program.egg-info/PKG-INFO +15 -1
- {gea_program-0.43.0 → gea_program-0.44.0}/gea_program.egg-info/SOURCES.txt +3 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/pyproject.toml +1 -1
- {gea_program-0.43.0 → gea_program-0.44.0}/LICENSE +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/BENCH_TEST_PROTOCOL.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/accuracy_statement.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/alarm_engine.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/backup.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/bench.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/blind_harness.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/acex_lomonosov_age_depth_model.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/acex_lomonosov_age_depth_model.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/agassiz77_canada_temperature.csv +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/agassiz77_canada_temperature.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/barbados_110_consolidation.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/barbados_110_consolidation.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/bengal_u1452_grain_size.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/bengal_u1452_grain_size.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/blake_164_methane_isotopes.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/blake_164_methane_isotopes.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/collingwood_1_28_ks_complete.las +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/collingwood_1_28_ks_complete.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/costa_rica_odp_friction_envelope.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/costa_rica_odp_friction_envelope.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/dsdp_504b_physical_properties.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/dsdp_504b_physical_properties.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/dsdp_504b_sound_velocity.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/dsdp_504b_sound_velocity.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/elgygytgyn_5011_turbidites.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/elgygytgyn_5011_turbidites.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/epica_domec_co2_800kyr.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/epica_domec_co2_800kyr.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/fram_909_organic_petrography.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/fram_909_organic_petrography.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/gbr_325_coral_uth_ages.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/gbr_325_coral_uth_ages.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/gisp2_greenland_temperature.csv +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/gisp2_greenland_temperature.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/gom_308_t2p_insitu.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/gom_308_t2p_insitu.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/guaymas_385_dom_d13c.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/guaymas_385_dom_d13c.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/hikurangi_u1520_friction_insitu.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/iodp_u1324_pore_pressure.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/iodp_u1324_pore_pressure.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/jfast_c0019_slow_slip_events.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/jfast_c0019_slow_slip_events.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/kennetcook_2_p129_excerpt.las +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/kennetcook_2_p129_excerpt.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_bhgm_density.dat +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_bhgm_density.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_complog_6020_excerpt.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_hlog246_temperature.dat +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_hlog246_temperature.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_rockmech_compress.dat +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_rockmech_compress.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_vb_rockmech_compress.dat +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_vb_rockmech_compress.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_vb_vlog251_temperature.dat +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_vb_vlog251_temperature.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/l06_06_nl_survey.csv +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/l06_06_nl_survey.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/l07_01_nl_excerpt.las +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/l07_01_nl_excerpt.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/mariana_1200_serpentinite_geochem.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/med_160_sapropels.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/med_160_sapropels.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/nankai_megasplay_shear_strength.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/nankai_megasplay_shear_strength.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1027b_thermal_conductivity.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1027b_thermal_conductivity.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1027c_cork_temperature.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1027c_cork_temperature.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1165b_thermal_conductivity.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1165b_thermal_conductivity.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_504b_dike_elastic_moduli.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_504b_leg137_borehole_fluids.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/peru_201_sulfate_reduction.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/peru_201_sulfate_reduction.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/scorpio_e1_sa_excerpt.las +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/scorpio_e1_sa_excerpt.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/sumatra_362_cohesion.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/sumatra_362_cohesion.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/university_6_17_no1_tx_excerpt.las +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ursa_308_xrd_mineralogy.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ursa_308_xrd_mineralogy.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/volve_15_9_19_sr_excerpt.las +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/volve_15_9_19a_core_excerpt.csv +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/volve_f12_f14_production_excerpt.csv +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/volve_f12_f14_production_excerpt.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/walvis_208_petm_carbonate.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/walvis_208_petm_carbonate.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/woodlark_1109_rock_eval.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/woodlark_1109_rock_eval.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog_map.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog_register.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/certificates.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/cli.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/client_reports.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/config_versioning.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/conformance.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/control_room.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/correlation.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/dashboard.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/declarations.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/deviation.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/dlis.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/dnp3.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/doctor.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/dossier.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/dossier_verify.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/downhole_engine.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/drift_monitor.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/drill_string.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/earth_model.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/economics.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/ed25519.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/edr.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/elevation.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/etp.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/etp12_schemas.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/example_register_map.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/fat_sat.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/files.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/fixtures/README.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/fixtures/comcat_delaware_basin_2026-09.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/fixtures/comcat_mahoning_1990-2026.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/fixtures/comcat_youngstown_2011-2012.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/fixtures/ne_110m_coastline.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/follower.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/forward_model.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/friction_priors.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/gamma.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/gauge_aging.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/gauge_specs.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/geodesy.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/gravity_reference.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/hartip.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/TESTER_GUIDE.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/alarms.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/audit-update.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/backup.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/catalog.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/charts.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/conformance.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/control-room.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/data-in.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/declare.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/dlis.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/doctor.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/dossier.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/drift.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/drillstring.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/economics.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/edr.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/etp.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/field-ports.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/files.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/historian.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/instruments.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/interval.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/map.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/month-end.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/osdu.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/patches.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/penetrator.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/ppdm.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/quakeml.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/quality.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/report-history.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/seedlink.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/seismic.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/setup.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/simulation.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/site.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/sites.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/slip.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/soak.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/sra.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/start.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/structure.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/tools.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/transients.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/upkeep.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/vibration.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/well-report.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/well-tests.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/well3d.md +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/housekeeping.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/interval.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/inverse_engine.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/jobs.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/live_ports.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/loadtest.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/matplotlib_demo.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/modbus.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/modbus_rtu.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/model_card.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/month_end.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/mqtt_port.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/opcua_port.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/operator_app.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/osdu.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/patches.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/penetrator.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/permits.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/piwebapi.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/ports.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/ppdm.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/profile_catalog.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/project.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/qt6_downhole_app.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/quakeml.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reconciler.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/IU_ANMO_10_BHZ_response.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/IU_ANMO_10_BHZ_response.xml +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/eia_henry_hub_daily.csv.gz +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/eia_prices.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/eia_wti_daily.csv.gz +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/libmseed_steim.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/libmseed_steim1_4096.mseed +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/libmseed_steim2_4096.mseed +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/libmseed_steim2_512.mseed +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/mahoning_example.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/mud_motor_specs.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/penetrator_ratings.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/seismicity_protocols.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/tool_sources.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/us_outline.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/us_outline.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_3d.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_f4_interpretation.las.gz +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_f4_logs.provenance.json +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_f4_lwd.las.gz +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_surfaces.json.gz +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_traj_F-10.xml.gz +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_traj_F-4.xml.gz +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_traj_F-5_12.25in.xml.gz +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_traj_F-5_17.5in.xml.gz +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_traj_F-5_36in.xml.gz +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_traj_F-5_8.5in.xml.gz +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_well_picks.csv.gz +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/report_history.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/rock_inventory.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/sample_record.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/sample_well_profile.csv +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/sbom.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/seedlink.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/segy.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_array.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_assoc.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_detect.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_film.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_harmonic.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_qc.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_response.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_signature.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_track.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_uncertainty.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_unlisted.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/sensor_swap.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/series_cache.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/service_life.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/setup_checklist.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/shapefile.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/shell.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/shut_in.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/simulation.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/site_map.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/sla_report.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/slip.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/soak.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/sra.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/store_forward.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/strata_join.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/structure.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/supervisor.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/survey_cmd.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/survey_view.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/telemetry.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/tool_catalog.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/tool_library.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/tool_taxonomy.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/transient.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/vibration.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/well3d.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/well_assembler.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/well_report.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/well_test_validation.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/wits0.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/witsml.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea/workspace.py +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea_program.egg-info/dependency_links.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea_program.egg-info/entry_points.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea_program.egg-info/requires.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/gea_program.egg-info/top_level.txt +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/setup.cfg +0 -0
- {gea_program-0.43.0 → gea_program-0.44.0}/tests/test_acceptance.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gea-program
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.44.0
|
|
4
4
|
Summary: GEA-Program: downhole gauge monitoring - canonical measurement records, quality rules, drift evaluation with a re-fit change log, well-test validation, alarm management, model cards, store-and-forward, monthly SLA measurement, FAT/SAT protocols and a dashboard, generated from live objects in the client's reporting outline.
|
|
5
5
|
Author: Daniel T. Murphy
|
|
6
6
|
License: MPL-2.0
|
|
@@ -648,6 +648,20 @@ against it; `gea seedlink --config ... --hello` prints what a server says of
|
|
|
648
648
|
itself; `gea help seedlink`. A TexNet station is served by EarthScope at
|
|
649
649
|
`rtserve.iris.washington.edu:18000`, network `TX`.
|
|
650
650
|
|
|
651
|
+
### The earthquake watch and the alerts
|
|
652
|
+
|
|
653
|
+
While a well operates, the program reads the USGS earthquake catalogue
|
|
654
|
+
every few minutes from the site's machine - the real-time feed USGS asks
|
|
655
|
+
automated programs to use, and the catalogue service for any gap - and
|
|
656
|
+
raises an alert when an earthquake near the well reaches the chosen
|
|
657
|
+
regulator's protocol level; a revision to a higher level is a new alert,
|
|
658
|
+
and a watch that cannot reach the catalogue says so. The same alerts cover
|
|
659
|
+
what nobody watches by hand: a component's service interval used up, a
|
|
660
|
+
penetrator check outside its rating, a signed dossier the workspace no
|
|
661
|
+
longer matches. Every alert is listed on the Alarms tab and sent through
|
|
662
|
+
the notification rules (email or a webhook). `gea seismicity --watch-once`,
|
|
663
|
+
`gea alerts`, `gea help alerts`.
|
|
664
|
+
|
|
651
665
|
### Report history and the signed dossier
|
|
652
666
|
|
|
653
667
|
Every client report a workspace writes is kept, version by version, under
|
|
@@ -609,6 +609,20 @@ against it; `gea seedlink --config ... --hello` prints what a server says of
|
|
|
609
609
|
itself; `gea help seedlink`. A TexNet station is served by EarthScope at
|
|
610
610
|
`rtserve.iris.washington.edu:18000`, network `TX`.
|
|
611
611
|
|
|
612
|
+
### The earthquake watch and the alerts
|
|
613
|
+
|
|
614
|
+
While a well operates, the program reads the USGS earthquake catalogue
|
|
615
|
+
every few minutes from the site's machine - the real-time feed USGS asks
|
|
616
|
+
automated programs to use, and the catalogue service for any gap - and
|
|
617
|
+
raises an alert when an earthquake near the well reaches the chosen
|
|
618
|
+
regulator's protocol level; a revision to a higher level is a new alert,
|
|
619
|
+
and a watch that cannot reach the catalogue says so. The same alerts cover
|
|
620
|
+
what nobody watches by hand: a component's service interval used up, a
|
|
621
|
+
penetrator check outside its rating, a signed dossier the workspace no
|
|
622
|
+
longer matches. Every alert is listed on the Alarms tab and sent through
|
|
623
|
+
the notification rules (email or a webhook). `gea seismicity --watch-once`,
|
|
624
|
+
`gea alerts`, `gea help alerts`.
|
|
625
|
+
|
|
612
626
|
### Report history and the signed dossier
|
|
613
627
|
|
|
614
628
|
Every client report a workspace writes is kept, version by version, under
|
|
@@ -563,9 +563,19 @@ def main(argv=None) -> int:
|
|
|
563
563
|
p_os.add_argument("--kind", type=str, default=None, choices=("drilling", "completion", "disposal", "injection", "other"))
|
|
564
564
|
p_os.add_argument("--remove-op", type=str, default=None)
|
|
565
565
|
p_os.add_argument("--remove-catalog", type=str, default=None)
|
|
566
|
+
p_os.add_argument("--watch", type=str, default=None, choices=("on", "off"), help="turn the live earthquake watch on or off")
|
|
567
|
+
p_os.add_argument("--watch-every", type=float, default=None, help="the watch's interval in minutes (default 5)")
|
|
568
|
+
p_os.add_argument("--watch-mode", type=str, default=None, choices=("operations", "always"), help="poll only while an operation is under way (default), or always")
|
|
569
|
+
p_os.add_argument("--watch-once", action="store_true", help="poll the USGS catalogue now, from this machine, and say what came back")
|
|
566
570
|
p_os.add_argument("--actor", type=str, default="cli")
|
|
567
571
|
p_os.add_argument("--json", action="store_true")
|
|
568
572
|
|
|
573
|
+
p_al = sub.add_parser("alerts", help="the program's alerts: earthquakes near operating wells and the protocol levels they reach, the earthquake watch going quiet, components due for service, penetrator checks outside their ratings, dossiers the workspace no longer matches")
|
|
574
|
+
p_al.add_argument("--workspace", type=str, required=True)
|
|
575
|
+
p_al.add_argument("--check", action="store_true", help="check the motors, penetrators and dossiers now, sending what began or cleared through the notification rules")
|
|
576
|
+
p_al.add_argument("-n", type=int, default=30)
|
|
577
|
+
p_al.add_argument("--json", action="store_true")
|
|
578
|
+
|
|
569
579
|
p_dz = sub.add_parser("dossier", help="the signed dossier: a well's (or the pad's) reports, declarations, audit log and source hashes in one zip, signed with the site's Ed25519 key, with a checker anyone can run without this program")
|
|
570
580
|
p_dz.add_argument("--workspace", type=str, default=None)
|
|
571
581
|
p_dz.add_argument("--well", type=str, default=None, help="a well; without it, the pad (every well and the site reports)")
|
|
@@ -2225,6 +2235,26 @@ def main(argv=None) -> int:
|
|
|
2225
2235
|
return 0
|
|
2226
2236
|
if a.remove_op:
|
|
2227
2237
|
_OS.remove_operation(ws, a.actor, a.remove_op); print(f"removed {a.remove_op}"); return 0
|
|
2238
|
+
if a.watch or a.watch_every is not None or a.watch_mode or a.watch_once:
|
|
2239
|
+
from . import seismic_watch as _SW
|
|
2240
|
+
if ws is None:
|
|
2241
|
+
raise SystemExit("seismicity: the watch needs --workspace")
|
|
2242
|
+
if a.watch or a.watch_every is not None or a.watch_mode:
|
|
2243
|
+
_SW.set_settings(ws, a.actor, None if a.watch is None else a.watch == "on", a.watch_mode, a.watch_every)
|
|
2244
|
+
if a.watch_once:
|
|
2245
|
+
from .notify import Notifier
|
|
2246
|
+
n_ = Notifier(ws.path); n_.reload()
|
|
2247
|
+
r_ = _SW.poll(ws, n_, force=True)
|
|
2248
|
+
if r_.get("ok"):
|
|
2249
|
+
print(f"reached the catalogue ({r_['how']}): {r_['in_circle']} earthquake(s) in the watch circle, {r_['new']} new; {len(r_['alerts'])} alert(s)")
|
|
2250
|
+
for al_ in r_["alerts"]:
|
|
2251
|
+
print(f" {al_['event']}: {al_.get('detail', '')}")
|
|
2252
|
+
else:
|
|
2253
|
+
print(f"did not reach the catalogue: {r_.get('error') or r_.get('why')}")
|
|
2254
|
+
st_ = _SW.status(ws)
|
|
2255
|
+
print(f"watch: {st_['state']} - every {st_['settings']['every_min']:g} min, {st_['settings']['mode']}; last good poll {st_['last_ok_utc'] or 'never'}"
|
|
2256
|
+
+ (f"; last error {st_['last_error']}" if st_.get('last_error') else ""))
|
|
2257
|
+
return 0 if (not a.watch_once or r_.get("ok")) else 1
|
|
2228
2258
|
if a.remove_catalog:
|
|
2229
2259
|
_OS.remove_catalog(ws, a.actor, a.remove_catalog); print(f"removed {a.remove_catalog}"); return 0
|
|
2230
2260
|
over = {k: v for k, v in (("protocol", a.protocol), ("radius_km", a.radius_km), ("screen_km", a.screen_km), ("post_days", a.post_days), ("mc", a.mc)) if v is not None}
|
|
@@ -2306,6 +2336,30 @@ def main(argv=None) -> int:
|
|
|
2306
2336
|
for v in f_["versions"][: (50 if a.family else 3)]:
|
|
2307
2337
|
print(f" {v['utc']} {v.get('report_id') or ''} reports/{v['dir']}")
|
|
2308
2338
|
return 0
|
|
2339
|
+
elif a.cmd == "alerts":
|
|
2340
|
+
import json as _json
|
|
2341
|
+
from . import alerts as _AL
|
|
2342
|
+
from .cli import _safe_stdout
|
|
2343
|
+
from .notify import Notifier
|
|
2344
|
+
from .workspace import Workspace, WorkspaceError
|
|
2345
|
+
_safe_stdout()
|
|
2346
|
+
try:
|
|
2347
|
+
ws = Workspace(a.workspace)
|
|
2348
|
+
except WorkspaceError as e:
|
|
2349
|
+
raise SystemExit(f"alerts: {e}")
|
|
2350
|
+
if a.check:
|
|
2351
|
+
n_ = Notifier(ws.path); n_.reload()
|
|
2352
|
+
r_ = _AL.check_conditions(ws, n_, force=True)
|
|
2353
|
+
print(f"checked: {len(r_)} alert(s) raised or cleared")
|
|
2354
|
+
rows = _AL.recent(ws, a.n)
|
|
2355
|
+
if a.json:
|
|
2356
|
+
print(_json.dumps(rows, default=str)); return 0
|
|
2357
|
+
for r in rows:
|
|
2358
|
+
sent = ", ".join(f"{d['channel']}{'' if d['ok'] else ' (failed)'}" for d in r.get("delivered") or []) or "no channel set up"
|
|
2359
|
+
print(f"{r['utc']} {r['event']:<20} {r.get('well') or '':<16} {r.get('detail', '')} [{sent}]")
|
|
2360
|
+
if not rows:
|
|
2361
|
+
print("no alerts yet")
|
|
2362
|
+
return 0
|
|
2309
2363
|
elif a.cmd == "simulation":
|
|
2310
2364
|
import json as _json
|
|
2311
2365
|
from . import simulation as _SIM
|
|
@@ -10116,6 +10116,221 @@ def section_cl_history_and_dossier(tmp: str) -> None:
|
|
|
10116
10116
|
+ (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
|
|
10117
10117
|
|
|
10118
10118
|
|
|
10119
|
+
def section_cm_watch_and_alerts(tmp: str) -> None:
|
|
10120
|
+
"""Section CM - the live earthquake watch and the program's alerts: the watch idle without an operation, its first poll
|
|
10121
|
+
a catalogue query over the gap and the next ones the real-time feed, the circle kept, a revision raising a higher
|
|
10122
|
+
level once, the watch going quiet and coming back - all through a stand-in catalogue served over HTTP; an alert
|
|
10123
|
+
delivered through a webhook rule; a service interval used up, a penetrator outside its rating and a dossier the
|
|
10124
|
+
workspace no longer matches each raised once and cleared; the API, the CLI, the tabs and the help page."""
|
|
10125
|
+
import http.cookiejar
|
|
10126
|
+
import http.server
|
|
10127
|
+
import subprocess as _sp
|
|
10128
|
+
import threading as _th
|
|
10129
|
+
import urllib.request
|
|
10130
|
+
import urllib.error
|
|
10131
|
+
from datetime import datetime, timedelta, timezone
|
|
10132
|
+
from . import __version__
|
|
10133
|
+
from . import alerts as AL
|
|
10134
|
+
from . import dossier as DZ
|
|
10135
|
+
from . import drill_string as DS
|
|
10136
|
+
from . import helplib as H
|
|
10137
|
+
from . import op_seismicity as OS
|
|
10138
|
+
from . import penetrator as PN
|
|
10139
|
+
from . import seismic_watch as SW
|
|
10140
|
+
from .notify import Notifier
|
|
10141
|
+
from .workspace import Workspace
|
|
10142
|
+
from .service import Service, Users
|
|
10143
|
+
pkg = Path(__file__).parent
|
|
10144
|
+
d = Path(tmp, "cm"); d.mkdir()
|
|
10145
|
+
now = datetime.now(timezone.utc).replace(microsecond=0)
|
|
10146
|
+
|
|
10147
|
+
def feat(eid, t, lat, lon, m, upd=1, typ="earthquake"):
|
|
10148
|
+
return {"type": "Feature", "id": eid, "properties": {"mag": m, "magType": "ml", "time": int(t.timestamp() * 1000), "updated": upd, "place": "SYNTHETIC",
|
|
10149
|
+
"status": "automatic", "net": "tx", "type": typ},
|
|
10150
|
+
"geometry": {"type": "Point", "coordinates": [lon, lat, 5.0]}}
|
|
10151
|
+
served = {"query": [], "feed": [], "hook": [], "fail": False}
|
|
10152
|
+
FEEDS = {"feed": [feat("tx1", now - timedelta(hours=20), 32.01, -102.0, 1.7), feat("tx2", now - timedelta(minutes=3), 32.005, -102.01, 2.4),
|
|
10153
|
+
feat("qb", now - timedelta(hours=2), 32.0, -102.02, 1.9, typ="quarry blast")],
|
|
10154
|
+
"query": [feat("tx1", now - timedelta(hours=20), 32.01, -102.0, 1.7), feat("far", now - timedelta(hours=5), 33.5, -102.0, 3.5)]}
|
|
10155
|
+
|
|
10156
|
+
class Stand(http.server.BaseHTTPRequestHandler):
|
|
10157
|
+
def log_message(self, *a):
|
|
10158
|
+
pass
|
|
10159
|
+
|
|
10160
|
+
def do_GET(self):
|
|
10161
|
+
if served["fail"]:
|
|
10162
|
+
self.send_response(503); self.end_headers(); return
|
|
10163
|
+
kind = "query" if "/fdsnws/" in self.path else "feed"
|
|
10164
|
+
served[kind].append(self.path)
|
|
10165
|
+
b = json.dumps({"type": "FeatureCollection", "features": FEEDS[kind]}).encode()
|
|
10166
|
+
self.send_response(200); self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(b))); self.end_headers()
|
|
10167
|
+
self.wfile.write(b)
|
|
10168
|
+
|
|
10169
|
+
def do_POST(self):
|
|
10170
|
+
n = int(self.headers.get("Content-Length") or 0)
|
|
10171
|
+
served["hook"].append(json.loads(self.rfile.read(n) or b"{}"))
|
|
10172
|
+
self.send_response(200); self.end_headers()
|
|
10173
|
+
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Stand)
|
|
10174
|
+
_th.Thread(target=srv.serve_forever, daemon=True).start()
|
|
10175
|
+
base = f"http://127.0.0.1:{srv.server_address[1]}"
|
|
10176
|
+
|
|
10177
|
+
wsp = str(d / "ws"); ws = Workspace.create(wsp, "Pad CM", actor="tester")
|
|
10178
|
+
w = ws.add_well_file(str(pkg / "sample_well_profile.csv"), display="FRAC-1", actor="tester")
|
|
10179
|
+
Path(wsp, "seismicity").mkdir(exist_ok=True)
|
|
10180
|
+
Path(wsp, "seismicity", "watch.json").write_text(json.dumps({"enabled": True, "mode": "operations", "every_min": 5,
|
|
10181
|
+
"feed_url": base + "/earthquakes/feed/v1.0/summary/all_day.geojson",
|
|
10182
|
+
"query_url": base + "/fdsnws/event/1/query"}), encoding="utf-8")
|
|
10183
|
+
Path(wsp, "config", "notifications.json").write_text(json.dumps({"channels": [{"name": "hook", "kind": "webhook", "url": base + "/hook"}],
|
|
10184
|
+
"rules": [{"event": "seismic.protocol", "channels": ["hook"]}, {"event": "motor.due", "channels": ["hook"]}],
|
|
10185
|
+
"quiet_s": 0}), encoding="utf-8")
|
|
10186
|
+
nt = Notifier(wsp); nt.reload()
|
|
10187
|
+
|
|
10188
|
+
# -- CM1 the watch -----------------------------------------------------------------------------------------------
|
|
10189
|
+
r_nopos = SW.poll(ws, nt, now)
|
|
10190
|
+
ws.set_well(w["id"], surface={"lat": 32.0, "lon": -102.0, "datum": "NAD83"}, actor="tester")
|
|
10191
|
+
r_idle = SW.poll(ws, nt, now)
|
|
10192
|
+
st_idle = SW.status(ws, now)["state"]
|
|
10193
|
+
OS.save_operation(ws, "tester", w["id"], "completion", (now - timedelta(days=2)).strftime("%Y-%m-%d"), None, "stage log")
|
|
10194
|
+
OS.set_settings(ws, "tester", protocol_id="bc-ksmma-2021")
|
|
10195
|
+
r1 = SW.poll(ws, nt, now)
|
|
10196
|
+
r_soon = SW.poll(ws, nt, now + timedelta(minutes=2))
|
|
10197
|
+
r2 = SW.poll(ws, nt, now + timedelta(minutes=5))
|
|
10198
|
+
n_feed_r2 = len(served["feed"])
|
|
10199
|
+
FEEDS["feed"][1] = feat("tx2", now - timedelta(minutes=3), 32.005, -102.01, 3.1, upd=2)
|
|
10200
|
+
r3 = SW.poll(ws, nt, now + timedelta(minutes=10))
|
|
10201
|
+
r3b = SW.poll(ws, nt, now + timedelta(minutes=15))
|
|
10202
|
+
served["fail"] = True
|
|
10203
|
+
fails = [SW.poll(ws, nt, now + timedelta(minutes=m)) for m in (20, 25, 30, 35)]
|
|
10204
|
+
st_down = SW.status(ws, now + timedelta(minutes=36))["state"]
|
|
10205
|
+
served["fail"] = False
|
|
10206
|
+
r_back = SW.poll(ws, nt, now + timedelta(minutes=40))
|
|
10207
|
+
q = served["query"][0] if served["query"] else ""
|
|
10208
|
+
pg = OS.page(ws)
|
|
10209
|
+
evs = {e["event_id"]: e for e in pg["wells"][0]["events"]}
|
|
10210
|
+
al = AL.recent(ws, 100)
|
|
10211
|
+
c1 = {
|
|
10212
|
+
"idle": r_nopos["polled"] is False and r_idle["polled"] is False and "no operation under way" in r_idle["why"] and st_idle == "IDLE",
|
|
10213
|
+
"query_first": r1["ok"] and r1["how"] == "query" and len(served["query"]) == 1 and "format=geojson" in q and "maxradiuskm=20.0" in q
|
|
10214
|
+
and (now - timedelta(days=2)).strftime("%Y-%m-%d") in q and r1["in_circle"] == 1,
|
|
10215
|
+
"feed_next": r_soon["polled"] is False and r_soon["why"] == "not due" and r2["how"] == "feed" and r2["new"] == 2 and n_feed_r2 == 1,
|
|
10216
|
+
"alerts": [a["event"] for a in r1["alerts"]] == ["seismic.event", "seismic.protocol"] and [(a["event"], a.get("level_m")) for a in r2["alerts"]] == [("seismic.event", None), ("seismic.protocol", 2.0)]
|
|
10217
|
+
and [(a["event"], a.get("level_m")) for a in r3["alerts"]] == [("seismic.protocol", 3.0)] and r3b["alerts"] == [],
|
|
10218
|
+
"down_up": [len(f["alerts"]) for f in fails] == [0, 0, 1, 0] and fails[2]["alerts"][0]["event"] == "seismic.watch_down" and st_down == "NOT REACHING THE CATALOGUE"
|
|
10219
|
+
and [a["event"] for a in r_back["alerts"]] == ["seismic.watch_up"],
|
|
10220
|
+
"catalogue": "live_usgs.txt" in [c["file"] for c in OS.catalogs(ws)] and set(evs) == {"tx1", "tx2"} and evs["tx2"]["magnitude"] == 3.1
|
|
10221
|
+
and evs["tx2"]["protocol"]["highest"]["m"] == 3.0 and "qb" not in evs and pg["watch"]["state"] == "WATCHING",
|
|
10222
|
+
"delivered": len(served["hook"]) == 3 and all(h_.get("event") == "seismic.protocol" for h_ in served["hook"]) and "subject" in served["hook"][0]
|
|
10223
|
+
and any(a["event"] == "seismic.protocol" and a["delivered"] and a["delivered"][0]["ok"] for a in al)
|
|
10224
|
+
and any(a["event"] == "seismic.event" and not a["delivered"] for a in al),
|
|
10225
|
+
}
|
|
10226
|
+
SW.set_settings(ws, "tester", enabled=False)
|
|
10227
|
+
c1["off"] = SW.poll(ws, nt, now + timedelta(hours=1), force=True)["why"] == "the watch is off" and SW.status(ws)["state"] == "OFF"
|
|
10228
|
+
SW.set_settings(ws, "tester", enabled=True, every_min=2)
|
|
10229
|
+
try:
|
|
10230
|
+
SW.set_settings(ws, "tester", every_min=0.1); c1["bad_interval"] = False
|
|
10231
|
+
except ValueError:
|
|
10232
|
+
c1["bad_interval"] = True
|
|
10233
|
+
failed1 = [k for k, v in c1.items() if not v]
|
|
10234
|
+
ok(not failed1,
|
|
10235
|
+
"CM1 the live earthquake watch, against a stand-in catalogue served over HTTP: idle with no well placed or no operation under way; "
|
|
10236
|
+
"its first poll a catalogue query over the circle from the operation's start, the next ones the real-time feed, not before the "
|
|
10237
|
+
"interval; an earthquake 170 km away and a quarry blast left out; every earthquake near the operation an alert once, each protocol "
|
|
10238
|
+
"level once, a revision to M 3.1 the higher level once; three intervals without an answer an alert, and the answer again another; the "
|
|
10239
|
+
"live catalogue read by the tab; the protocol alerts delivered through a webhook rule; off when turned off"
|
|
10240
|
+
+ (f" [failed: {', '.join(failed1)}]" if failed1 else ""))
|
|
10241
|
+
|
|
10242
|
+
# -- CM2 the conditions ------------------------------------------------------------------------------------------
|
|
10243
|
+
syn = DS.synthetic_motor_record(str(d))
|
|
10244
|
+
we = ws.add_well_file(syn["path"], display="SYNTHETIC EDR well", actor="tester")
|
|
10245
|
+
comps = [{"class_id": "pdc_bit", "maker": "Acme", "model": "X616", "serial": "B1"},
|
|
10246
|
+
{"class_id": "pdm_motor", "motor_id": "msd-6-3-4-78-6.4", "serial": "M100", "service_limit": 10, "service_basis": "circulating_hours", "service_source": "rental contract"}]
|
|
10247
|
+
run = DS.save_run(ws, "tester", we["id"], "Run 1", comps)
|
|
10248
|
+
k1 = AL.check_conditions(ws, nt, now, force=True)
|
|
10249
|
+
k2 = AL.check_conditions(ws, nt, now, force=True)
|
|
10250
|
+
comps[1]["service_limit"] = 100
|
|
10251
|
+
DS.save_run(ws, "tester", we["id"], "Run 1", comps, run_id=run["id"])
|
|
10252
|
+
k3 = AL.check_conditions(ws, nt, now, force=True)
|
|
10253
|
+
PN.save_install(ws, "tester", w["id"], "genco-gps-5", void_test_psi=6000, void_held="yes")
|
|
10254
|
+
k4 = AL.check_conditions(ws, nt, now, force=True)
|
|
10255
|
+
PN.save_install(ws, "tester", w["id"], "genco-gps-5", void_test_psi=4000, void_held="yes")
|
|
10256
|
+
k5 = AL.check_conditions(ws, nt, now, force=True)
|
|
10257
|
+
ws.refresh_dashboard(actor="tester")
|
|
10258
|
+
DZ.build(ws, "tester", __version__, we["id"])
|
|
10259
|
+
k6 = AL.check_conditions(ws, nt, now + timedelta(minutes=1)) # the daily dossier check ran with k1: not due again yet
|
|
10260
|
+
src = next(Path(wsp, "wells", we["id"], "source").iterdir())
|
|
10261
|
+
raw = src.read_bytes(); src.write_bytes(raw + b"\n")
|
|
10262
|
+
k7 = AL.check_conditions(ws, nt, now + timedelta(days=1, minutes=1))
|
|
10263
|
+
src.write_bytes(raw)
|
|
10264
|
+
k8 = AL.check_conditions(ws, nt, now + timedelta(days=2, minutes=2))
|
|
10265
|
+
ev = lambda ks: [a["event"] for a in ks]
|
|
10266
|
+
c2 = {
|
|
10267
|
+
"motor": ev(k1) == ["motor.due"] and "M100" in k1[0]["what"] and "rental contract" in k1[0]["detail"] and ev(k2) == []
|
|
10268
|
+
and ev(k3) == ["condition.cleared"] and k3[0]["cleared"] == "motor.due" and any(h_.get("event") == "motor.due" for h_ in served["hook"]),
|
|
10269
|
+
"penetrator": ev(k4) == ["penetrator.outside"] and "void test" in k4[0]["detail"] and ev(k5) == ["condition.cleared"],
|
|
10270
|
+
"dossier": ev(k6) == [] and ev(k7) == ["dossier.changed"] and "source records" in k7[0]["detail"] and ev(k8) == ["condition.cleared"],
|
|
10271
|
+
"state": AL.state(ws)["conditions"] == {},
|
|
10272
|
+
}
|
|
10273
|
+
failed2 = [k for k, v in c2.items() if not v]
|
|
10274
|
+
ok(not failed2,
|
|
10275
|
+
"CM2 the conditions: a motor past its declared service interval an alert once, delivered by its rule, and cleared when the interval "
|
|
10276
|
+
"is raised; a penetrator's void test above its rating an alert and cleared when corrected; a signed dossier whose source record "
|
|
10277
|
+
"changes an alert at the next daily check, and cleared when the record is restored"
|
|
10278
|
+
+ (f" [failed: {', '.join(failed2)}]" if failed2 else ""))
|
|
10279
|
+
|
|
10280
|
+
# -- CM3 the API, the CLI, the tabs ------------------------------------------------------------------------------
|
|
10281
|
+
Users(str(Path(wsp, "users.json"))).add("v", "viewer-pass-1", "viewer"); Users(str(Path(wsp, "users.json"))).add("op", "op-pass-123", "operator")
|
|
10282
|
+
svc = Service(wsp, port=0, scheduler=False).start()
|
|
10283
|
+
base_url = svc.url.rstrip("/")
|
|
10284
|
+
|
|
10285
|
+
def opener(name, pw_):
|
|
10286
|
+
o = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
|
|
10287
|
+
rq = urllib.request.Request(base_url + "/api/login", method="POST", data=json.dumps({"name": name, "password": pw_}).encode())
|
|
10288
|
+
rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1"); o.open(rq, timeout=30)
|
|
10289
|
+
return o
|
|
10290
|
+
|
|
10291
|
+
def call(o, method, path, body=None):
|
|
10292
|
+
rq = urllib.request.Request(base_url + path, method=method, data=(json.dumps(body).encode() if body is not None else None))
|
|
10293
|
+
if method == "POST":
|
|
10294
|
+
rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1")
|
|
10295
|
+
try:
|
|
10296
|
+
with o.open(rq, timeout=120) as rr_:
|
|
10297
|
+
return rr_.status, json.loads(rr_.read())
|
|
10298
|
+
except urllib.error.HTTPError as e:
|
|
10299
|
+
return e.code, json.loads(e.read())
|
|
10300
|
+
try:
|
|
10301
|
+
V, O = opener("v", "viewer-pass-1"), opener("op", "op-pass-123")
|
|
10302
|
+
s_a, aj = call(V, "GET", "/api/alerts")
|
|
10303
|
+
s_p, pj = call(V, "GET", "/api/opseis")
|
|
10304
|
+
s_vw, _ = call(V, "POST", "/api/opseis/watch", {"every_min": 3})
|
|
10305
|
+
s_ow, oj = call(O, "POST", "/api/opseis/watch", {"every_min": 3, "mode": "always", "enabled": True})
|
|
10306
|
+
s_bw, _ = call(O, "POST", "/api/opseis/watch", {"mode": "sometimes"})
|
|
10307
|
+
s_pp, ppj = call(O, "POST", "/api/opseis/watch/poll", {})
|
|
10308
|
+
finally:
|
|
10309
|
+
svc.stop()
|
|
10310
|
+
cli_once = _sp.run([sys.executable, "-m", "gea", "seismicity", "--workspace", wsp, "--watch-once"], capture_output=True, text=True, cwd=str(pkg.parent))
|
|
10311
|
+
cli_off = _sp.run([sys.executable, "-m", "gea", "seismicity", "--workspace", wsp, "--watch", "off"], capture_output=True, text=True, cwd=str(pkg.parent))
|
|
10312
|
+
cli_al = _sp.run([sys.executable, "-m", "gea", "alerts", "--workspace", wsp, "-n", "50"], capture_output=True, text=True, cwd=str(pkg.parent))
|
|
10313
|
+
srv.shutdown()
|
|
10314
|
+
page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
|
|
10315
|
+
service_src = (pkg / "service.py").read_text(encoding="utf-8")
|
|
10316
|
+
c3 = {
|
|
10317
|
+
"api": s_a == 200 and aj["notifications_configured"] and len(aj["alerts"]) >= 10 and "seismic.protocol" in aj["events"] and s_p == 200 and pj["watch"]["settings"]["every_min"] == 2
|
|
10318
|
+
and s_vw == 403 and s_ow == 200 and oj["settings"]["mode"] == "always" and oj["settings"]["every_min"] == 3 and s_bw == 400
|
|
10319
|
+
and s_pp == 200 and ppj["result"]["ok"] and ppj["status"]["state"] == "WATCHING",
|
|
10320
|
+
"cli": cli_once.returncode == 0 and "reached the catalogue" in cli_once.stdout and "watch: WATCHING" in cli_once.stdout
|
|
10321
|
+
and cli_off.returncode == 0 and "watch: OFF" in cli_off.stdout and cli_al.returncode == 0 and "seismic.protocol" in cli_al.stdout and "hook" in cli_al.stdout,
|
|
10322
|
+
"poller": "_SW.tick(self)" in service_src,
|
|
10323
|
+
"page": all(x in page_src for x in ("swCard", "Live earthquake watch", "/api/opseis/watch/poll", "alCard", "Program alerts", "/api/alerts")),
|
|
10324
|
+
"help": any(t[0] == "alerts" for t in H.INDEX) and not H.check("alerts") and "seismic.protocol" in (pkg / "help" / "notifications.md").read_text(encoding="utf-8"),
|
|
10325
|
+
}
|
|
10326
|
+
failed3 = [k for k, v in c3.items() if not v]
|
|
10327
|
+
ok(not failed3,
|
|
10328
|
+
"CM3 the alerts on the API (anyone signed in reads them and the watch; only an operator sets the watch or polls it; a mode that is "
|
|
10329
|
+
"not one refused), the CLI (a poll now through the catalogue, the watch off, the alerts with where each was sent), the service's "
|
|
10330
|
+
"poller running the watch, the watch card and the Program alerts card, and the help page"
|
|
10331
|
+
+ (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
|
|
10332
|
+
|
|
10333
|
+
|
|
10119
10334
|
def main() -> int:
|
|
10120
10335
|
print("GEA-Program - ACCEPTANCE SUITE (the product gate)")
|
|
10121
10336
|
# a handle a job subprocess still holds at the end must not turn a finished gate into a traceback on Windows:
|
|
@@ -10209,6 +10424,7 @@ def main() -> int:
|
|
|
10209
10424
|
section_cj_declarations(tmp)
|
|
10210
10425
|
section_ck_operations_seismicity(tmp)
|
|
10211
10426
|
section_cl_history_and_dossier(tmp)
|
|
10427
|
+
section_cm_watch_and_alerts(tmp)
|
|
10212
10428
|
if _FAILS:
|
|
10213
10429
|
print(f"[ACCEPTANCE] {len(_FAILS)} FAILURES ({_PASS} passed):")
|
|
10214
10430
|
for f in _FAILS:
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Program alerts: the conditions this program watches for, raised once when they begin and once when they clear.
|
|
2
|
+
|
|
3
|
+
The notification rules (`notify`) deliver an event to email or a webhook. This module decides when a condition is
|
|
4
|
+
worth an event, for the conditions the operator does not watch by hand:
|
|
5
|
+
|
|
6
|
+
- `seismic.protocol` - an earthquake near a well during (or in the association window after) one of its
|
|
7
|
+
operations reaches a level of the chosen regulator's protocol; raised again only if a later revision of its
|
|
8
|
+
magnitude reaches a higher level (from the live earthquake watch, `seismic_watch`);
|
|
9
|
+
- `seismic.event` - any new earthquake within the association radius during or after an operation;
|
|
10
|
+
- `seismic.watch_down` / `seismic.watch_up` - the earthquake watch has not reached the catalogue for three of its
|
|
11
|
+
intervals while an operation runs, and when it does again;
|
|
12
|
+
- `motor.due` - a drill-string component's declared service interval is used up (the Drill String tab's DUE);
|
|
13
|
+
- `penetrator.outside` - a line of an ESP well's penetrator check is OUTSIDE its rating (the Completion tab);
|
|
14
|
+
- `dossier.changed` - the newest signed dossier of a well or the pad no longer matches the workspace: a source
|
|
15
|
+
record or an earlier audit line has changed since it was signed (checked once a day);
|
|
16
|
+
- `condition.cleared` - one of the last three is no longer so.
|
|
17
|
+
|
|
18
|
+
Every alert is written to `records/alerts.jsonl` whether or not any notification channel is set up - the Alarms
|
|
19
|
+
tab lists them - and is handed to the notification rules, which send it where the site said (a rule per event
|
|
20
|
+
name in `notifications.json`). The state that keeps an alert from repeating is `records/alerts_state.json`.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import threading
|
|
27
|
+
from datetime import datetime, timezone
|
|
28
|
+
from typing import Dict, List, Optional
|
|
29
|
+
|
|
30
|
+
EVENTS = ('seismic.protocol', 'seismic.event', 'seismic.watch_down', 'seismic.watch_up', 'motor.due', 'penetrator.outside',
|
|
31
|
+
'dossier.changed', 'condition.cleared')
|
|
32
|
+
CONDITIONS_EVERY_S = 900 # motors and penetrators: every 15 minutes
|
|
33
|
+
DOSSIERS_EVERY_S = 86400 # dossiers: once a day
|
|
34
|
+
_LOCK = threading.Lock()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _utc(now: Optional[datetime] = None) -> str:
|
|
38
|
+
return (now or datetime.now(timezone.utc)).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _path(ws, name: str) -> str:
|
|
42
|
+
return os.path.join(ws.path, 'records', name)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def state(ws) -> dict:
|
|
46
|
+
p = _path(ws, 'alerts_state.json')
|
|
47
|
+
if not os.path.isfile(p):
|
|
48
|
+
return {'conditions': {}, 'last': {}}
|
|
49
|
+
try:
|
|
50
|
+
with open(p, encoding='utf-8') as f:
|
|
51
|
+
return json.load(f)
|
|
52
|
+
except (OSError, ValueError):
|
|
53
|
+
return {'conditions': {}, 'last': {}}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def save_state(ws, s: dict) -> None:
|
|
57
|
+
p = _path(ws, 'alerts_state.json')
|
|
58
|
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
|
59
|
+
with open(p + '.tmp', 'w', encoding='utf-8') as f:
|
|
60
|
+
json.dump(s, f, indent=1)
|
|
61
|
+
os.replace(p + '.tmp', p)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def raise_alert(ws, notifier, ev: dict, now: Optional[datetime] = None) -> dict:
|
|
65
|
+
"""Record an alert and hand it to the notification rules. Returns the record with the deliveries."""
|
|
66
|
+
rec = {'utc': _utc(now), **ev}
|
|
67
|
+
deliveries: List[dict] = []
|
|
68
|
+
if notifier is not None:
|
|
69
|
+
try:
|
|
70
|
+
deliveries = notifier.emit(dict(ev))
|
|
71
|
+
except Exception as e: # a channel never stops the record
|
|
72
|
+
deliveries = [{'ok': False, 'error': f'{type(e).__name__}: {e}'}]
|
|
73
|
+
rec['delivered'] = [{'channel': d.get('channel'), 'ok': bool(d.get('ok'))} for d in deliveries]
|
|
74
|
+
p = _path(ws, 'alerts.jsonl')
|
|
75
|
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
|
76
|
+
with _LOCK:
|
|
77
|
+
with open(p, 'a', encoding='utf-8') as f:
|
|
78
|
+
f.write(json.dumps(rec, default=str, sort_keys=True) + '\n')
|
|
79
|
+
return rec
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def recent(ws, n: int = 100) -> List[dict]:
|
|
83
|
+
p = _path(ws, 'alerts.jsonl')
|
|
84
|
+
if not os.path.isfile(p):
|
|
85
|
+
return []
|
|
86
|
+
rows = []
|
|
87
|
+
with open(p, encoding='utf-8') as f:
|
|
88
|
+
for line in f:
|
|
89
|
+
try:
|
|
90
|
+
rows.append(json.loads(line))
|
|
91
|
+
except ValueError:
|
|
92
|
+
pass
|
|
93
|
+
return list(reversed(rows[-n:]))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _transition(ws, notifier, st: dict, key: str, bad: bool, ev: dict, now: datetime, raised: List[dict]) -> None:
|
|
97
|
+
was = st['conditions'].get(key)
|
|
98
|
+
if bad and not was:
|
|
99
|
+
st['conditions'][key] = {'since': _utc(now), 'event': ev['event']}
|
|
100
|
+
raised.append(raise_alert(ws, notifier, {**ev, 'key': key}, now))
|
|
101
|
+
elif not bad and was:
|
|
102
|
+
st['conditions'].pop(key, None)
|
|
103
|
+
raised.append(raise_alert(ws, notifier, {'event': 'condition.cleared', 'key': key + ':cleared', 'cleared': was['event'],
|
|
104
|
+
'since': was['since'], 'well_id': ev.get('well_id'), 'well': ev.get('well'),
|
|
105
|
+
'detail': f"no longer so: {ev.get('what', key)}"}, now))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def check_conditions(ws, notifier, now: Optional[datetime] = None, force: bool = False) -> List[dict]:
|
|
109
|
+
"""Motors, penetrators (every 15 minutes) and dossiers (daily): raise what began, clear what ended."""
|
|
110
|
+
from . import drill_string as DS
|
|
111
|
+
from . import penetrator as PN
|
|
112
|
+
now = now or datetime.now(timezone.utc)
|
|
113
|
+
st = state(ws)
|
|
114
|
+
raised: List[dict] = []
|
|
115
|
+
last = st.setdefault('last', {})
|
|
116
|
+
|
|
117
|
+
def due(name, every):
|
|
118
|
+
t = last.get(name)
|
|
119
|
+
if force or not t:
|
|
120
|
+
return True
|
|
121
|
+
return (now - datetime.strptime(t, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)).total_seconds() >= every
|
|
122
|
+
wells = {w['id']: (w.get('display') or w['id']) for w in ws.wells()}
|
|
123
|
+
if due('conditions', CONDITIONS_EVERY_S):
|
|
124
|
+
last['conditions'] = _utc(now)
|
|
125
|
+
seen = set()
|
|
126
|
+
if DS.runs(ws):
|
|
127
|
+
try:
|
|
128
|
+
for e in DS.tracking(ws, build=True):
|
|
129
|
+
life = e.get('life')
|
|
130
|
+
if not life:
|
|
131
|
+
continue
|
|
132
|
+
key = f"motor.due:{e['key']}"
|
|
133
|
+
seen.add(key)
|
|
134
|
+
wid = e['runs'][-1]['well_id'] if e['runs'] else None
|
|
135
|
+
what = f"{e['class_name']} {e['maker']} {e['model']} {e['serial']}".strip()
|
|
136
|
+
_transition(ws, notifier, st, key, life['status'] == 'DUE',
|
|
137
|
+
{'event': 'motor.due', 'well_id': wid, 'well': wells.get(wid, wid), 'what': what,
|
|
138
|
+
'detail': f"{life['used']:g} of {life['limit']:g} {life['basis'].replace('_', ' ')} used ({life['used_pct']:g} %); interval: {life['basis']}, {(e['service'] or {}).get('source', '')}"},
|
|
139
|
+
now, raised)
|
|
140
|
+
except Exception as ex:
|
|
141
|
+
last['conditions_error'] = f'{type(ex).__name__}: {ex}'
|
|
142
|
+
for wid in list(PN.records(ws)):
|
|
143
|
+
if wid not in wells:
|
|
144
|
+
continue
|
|
145
|
+
try:
|
|
146
|
+
c = PN.check(ws, wid)
|
|
147
|
+
except Exception:
|
|
148
|
+
continue
|
|
149
|
+
bad = [r for r in c['rows'] if r['status'] == 'OUTSIDE']
|
|
150
|
+
key = f'penetrator.outside:{wid}'
|
|
151
|
+
seen.add(key)
|
|
152
|
+
_transition(ws, notifier, st, key, bool(bad),
|
|
153
|
+
{'event': 'penetrator.outside', 'well_id': wid, 'well': wells[wid], 'what': f"the penetrator check of {wells[wid]}",
|
|
154
|
+
'detail': '; '.join(f"{r['item']}: {r['why']}" for r in bad)}, now, raised)
|
|
155
|
+
for key in [k for k in st['conditions'] if k.split(':')[0] in ('motor.due', 'penetrator.outside') and k not in seen]:
|
|
156
|
+
st['conditions'].pop(key, None) # the run or the record was removed: nothing left to clear
|
|
157
|
+
if due('dossiers', DOSSIERS_EVERY_S):
|
|
158
|
+
last['dossiers'] = _utc(now)
|
|
159
|
+
try:
|
|
160
|
+
from . import dossier as DZ
|
|
161
|
+
newest: Dict[str, dict] = {}
|
|
162
|
+
for e in DZ.ledger(ws):
|
|
163
|
+
newest[e.get('well_id') or 'pad'] = e
|
|
164
|
+
for scope, e in newest.items():
|
|
165
|
+
p = os.path.join(ws.reports_dir, e['path'])
|
|
166
|
+
key = f'dossier.changed:{scope}'
|
|
167
|
+
if not os.path.isfile(p):
|
|
168
|
+
continue
|
|
169
|
+
r = DZ.verify(p, ws)
|
|
170
|
+
bad = [c for c in (r.get('workspace_checks') or []) + r['checks'] if not c['ok']]
|
|
171
|
+
_transition(ws, notifier, st, key, bool(bad),
|
|
172
|
+
{'event': 'dossier.changed', 'well_id': e.get('well_id'), 'well': e.get('display'), 'what': f"the dossier {e['dossier_id']}",
|
|
173
|
+
'dossier_id': e['dossier_id'], 'detail': '; '.join(f"{c['check']}: {c['detail']}" for c in bad)}, now, raised)
|
|
174
|
+
except Exception as ex:
|
|
175
|
+
last['dossiers_error'] = f'{type(ex).__name__}: {ex}'
|
|
176
|
+
save_state(ws, st)
|
|
177
|
+
return raised
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# The earthquake watch and the program's alerts
|
|
2
|
+
|
|
3
|
+
> While a well operates, the program reads the USGS earthquake catalogue every few minutes from this machine and raises an alert when an earthquake near the well reaches the chosen regulator's protocol level. It also raises alerts for the conditions nobody watches by hand: the watch itself going quiet, a component's service interval used up, a penetrator check outside its rating, and a signed dossier the workspace no longer matches.
|
|
4
|
+
|
|
5
|
+
**The command**: the Operations & Seismicity tab's **Live earthquake watch** card - on or off, the interval (5 minutes by default), whether it polls only while an operation is under way (the default) or always, and **Poll now** to try the connection. Every alert is listed on the Alarms tab under **Program alerts**. From a terminal: `gea seismicity --workspace C:\site --watch-once` polls now and says what came back; `--watch on|off`, `--watch-every 5`, `--watch-mode always`; `gea alerts --workspace C:\site` lists the alerts, `--check` checks the motors, penetrators and dossiers now.
|
|
6
|
+
|
|
7
|
+
**How the watch reads the catalogue**: it runs in the service's background poller, so only while `gea serve` (the control panel) is running. An operation is under way when a declared operation (Operations & Seismicity, or the declarations sheet's `[operations]`) has started and has not ended, at a well with a surface position. A poll reads the USGS real-time summary feed of the past day, all magnitudes, updated every minute - the feed USGS asks automated programs to use - and keeps the earthquakes inside the circle around the operating wells (out to the screening distance, 20 km by default, beyond the farthest). On the first poll, or after a gap of a day or more, it asks the FDSN event service for the whole gap instead (from the operation's start, at most 30 days back). USGS revises magnitudes and locations; the newest version wins, and a revision that reaches a higher protocol level is a new alert.
|
|
8
|
+
|
|
9
|
+
**The alerts**: `seismic.protocol` (a protocol level reached, once per level), `seismic.event` (any earthquake within the association radius during or after an operation, once), `seismic.watch_down` (no answer from the catalogue for three intervals while an operation runs) and `seismic.watch_up`; `motor.due` (checked every 15 minutes), `penetrator.outside` (every 15 minutes), `dossier.changed` (the newest dossier of each well and of the pad, re-checked daily), and `condition.cleared` when one of the last three ends.
|
|
10
|
+
|
|
11
|
+
**Getting them to a person**: every alert is written to `records/alerts.jsonl` and shown on the Alarms tab. To have it sent, set up notifications (Administration, Notifications: load the example, edit, validate and commit) with an email (SMTP) or webhook channel and a rule naming the alert, for example `{"event": "seismic.protocol", "channels": ["mail"]}`; the password stays in an environment variable named by `password_env`. A text message: most carriers deliver email sent to the phone's email-to-text address, or a webhook to a messaging service. The `quiet_s` window keeps one alert from repeating.
|
|
12
|
+
|
|
13
|
+
**What it writes**: `seismicity/watch.json` (the settings), `seismicity/live/events.json` (each earthquake's latest version) and `state.json`, the catalogue the tab reads (`seismicity/catalogs/live_usgs.txt`, listed with the window and circle the watch covered), `records/alerts.jsonl`, `records/alerts_state.json`; the audit log records each change of settings and each manual poll.
|
|
14
|
+
|
|
15
|
+
**The number to check**: an alert's magnitude, time and distance against the earthquake's USGS event page (its id is in the alert); the watch's last good poll on the tab against the clock.
|
|
16
|
+
|
|
17
|
+
**What this page will not call a measurement**: the earthquake - its magnitude and position are the catalogue's, automatic at first and revised later; a cause - an alert says an earthquake reached a level near an operation, not that the operation caused it; the regulator's decision - the protocol's magnitude of record may be another network's. A watch that cannot reach the catalogue says so; it never reports quiet it did not see.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Rules route events to a webhook or a mailbox; every attempt is logged; secrets stay out of the file.
|
|
4
4
|
|
|
5
|
-
**The command**: Administration -> Notifications: "Load the example", edit, "Validate and commit" (it is versioned as `notifications`), "Send a test" per channel. `gea notify --example`, `gea notify --workspace C:\site --test mail`, `gea notify --workspace C:\site --log 20`. Events: alarm.activated (by priority), alarm.shelved, job.failed, patch.down, patch.up, approval.pending, file.imported.
|
|
5
|
+
**The command**: Administration -> Notifications: "Load the example", edit, "Validate and commit" (it is versioned as `notifications`), "Send a test" per channel. `gea notify --example`, `gea notify --workspace C:\site --test mail`, `gea notify --workspace C:\site --log 20`. Events: alarm.activated (by priority), alarm.shelved, job.failed, patch.down, patch.up, approval.pending, file.imported; and the program's alerts - seismic.protocol, seismic.event, seismic.watch_down, seismic.watch_up, motor.due, penetrator.outside, dossier.changed, condition.cleared (see the help page on the earthquake watch and the alerts).
|
|
6
6
|
|
|
7
7
|
**What it writes**: `records/notifications.jsonl` - one line per delivery attempt and per suppression (the quiet window stops repeats of the same key); the service's poller reads the alarm logs, the jobs, the patch states and the approval queue every 10 seconds and starts from a baseline at start-up, so history is never re-announced.
|
|
8
8
|
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
**The command**: the Operations & Seismicity tab. Choose the scene - your wells, or the worked example - and the protocol (British Columbia's Kiskatinaw order, Alberta's Subsurface Order No. 2, Oklahoma's completion notice, Ohio's 2014 permit conditions, Texas's response plan), the association radius (5 km by default), the days after an operation that still count (90) and the completeness magnitude. Each well shows its operations as bands on a time line, every event within the screening distance (20 km) as a circle by its distance from the well and sized by magnitude, the events table with the rig's state at an event during a drilling record, the protocol level each reaches, and the rate comparison. The operator declares operation windows (or the declarations sheet's `[operations]`) and attaches catalogue exports with the window, minimum magnitude and circle they were queried for (or `[catalogs]`); the program cannot reach a catalogue service itself, so the tab gives the ComCat query to open in a browser. From a terminal: `gea seismicity --workspace C:\site`; `--scene mahoning` for the example; `--protocols`; `--query`; `--attach FILE --start --end --min-magnitude --lat --lon --catalog-radius-km --source`; `--declare-op --well W --kind completion --start --end --source`; `--set` keeps the settings.
|
|
6
6
|
|
|
7
|
+
**The live watch**: while an operation is under way the program can read the USGS catalogue itself every few minutes and raise an alert when an earthquake reaches the protocol's level - see the help page on the earthquake watch and the alerts.
|
|
8
|
+
|
|
7
9
|
**What it writes**: `seismicity/operations.json`, `seismicity/catalogs.json` with each catalogue file kept verbatim beside it (its SHA-256 recorded), and `seismicity/settings.json` in the workspace; each declaration, attachment, removal and change of settings a line in the audit log. The protocols ship with the program (`reference/seismicity_protocols.json`), each with its document; the worked example's catalogue (`fixtures/comcat_mahoning_1990-2026.txt`) and its wells and dates (`reference/mahoning_example.json`), each with its source.
|
|
8
10
|
|
|
9
11
|
**The arithmetic**: an event's distance is the great-circle distance from the well's declared surface position (converted to WGS84). It is DURING an operation inside its window, AFTER one within the association days that follow, and OUTSIDE otherwise; an event near two wells' operations is named on both, each with the other beside it. The rate comparison counts the events at or above the completeness within the radius over the catalogue's window: n inside the operation windows (with their association days) and m outside. If one constant rate held, n would be binomial in n + m with the share of time inside; the probability of n or more is the exact conditional test for two Poisson rates. HIGHER DURING OPERATIONS means that probability is under 1 % and the rate inside is the higher; otherwise NOT DISTINGUISHED. With no event outside, the background is given as its 95 % upper bound, 3 events over its time.
|
|
@@ -50,6 +50,7 @@ INDEX: List[tuple] = [
|
|
|
50
50
|
('declare', 'The declarations sheet', 'everything the operator declares about a pad in one spreadsheet - identity, position, elevation, surveys, rates, AFE, fleet, runs, penetrators and drives - checked row by row before anything is written'),
|
|
51
51
|
('penetrator', 'Completion - the ESP penetrator', "the wellhead penetrator's ratings from its maker's documents, each well's installation record, and the ESP drive, motor and site checked against them; the permanent magnet motor safety note"),
|
|
52
52
|
('opseis', 'Operations and seismicity', "each well's drilling, completion and disposal windows against the earthquake catalogue around it: the events during and after each, the levels a regulator's protocol would reach, the rate during operations against the rest of the record; the Mahoning Valley worked example"),
|
|
53
|
+
('alerts', 'The earthquake watch and the alerts', "the USGS catalogue read every few minutes while a well operates, an alert when a protocol level is reached; and alerts for the watch going quiet, service intervals used up, penetrators outside their ratings and dossiers the workspace no longer matches"),
|
|
53
54
|
('report-history', 'Report history', 'every version of every client report kept in a dated folder with its SHA-256, when the next one replaces it'),
|
|
54
55
|
('dossier', 'The signed dossier', "a well's or the pad's reports, declarations, audit log and source hashes in one zip signed with the site's Ed25519 key, with a checker anyone can run without GEA"),
|
|
55
56
|
('economics', 'Economics', 'each well\'s days, footage and cost by the operator\'s own declared rates, against the EIA oil and gas spot prices while it was drilled, and any series the site imports'),
|