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.
Files changed (334) hide show
  1. {gea_program-0.43.0 → gea_program-0.44.0}/PKG-INFO +15 -1
  2. {gea_program-0.43.0 → gea_program-0.44.0}/README.md +14 -0
  3. {gea_program-0.43.0 → gea_program-0.44.0}/gea/__init__.py +1 -1
  4. {gea_program-0.43.0 → gea_program-0.44.0}/gea/__main__.py +54 -0
  5. {gea_program-0.43.0 → gea_program-0.44.0}/gea/acceptance_tests.py +216 -0
  6. gea_program-0.44.0/gea/alerts.py +177 -0
  7. gea_program-0.44.0/gea/help/alerts.md +17 -0
  8. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/notifications.md +1 -1
  9. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/opseis.md +2 -0
  10. {gea_program-0.43.0 → gea_program-0.44.0}/gea/helplib.py +1 -0
  11. {gea_program-0.43.0 → gea_program-0.44.0}/gea/notify.py +22 -2
  12. {gea_program-0.43.0 → gea_program-0.44.0}/gea/op_seismicity.py +7 -0
  13. gea_program-0.44.0/gea/seismic_watch.py +336 -0
  14. {gea_program-0.43.0 → gea_program-0.44.0}/gea/service.py +18 -0
  15. {gea_program-0.43.0 → gea_program-0.44.0}/gea/web/app.html +22 -0
  16. {gea_program-0.43.0 → gea_program-0.44.0}/gea_program.egg-info/PKG-INFO +15 -1
  17. {gea_program-0.43.0 → gea_program-0.44.0}/gea_program.egg-info/SOURCES.txt +3 -0
  18. {gea_program-0.43.0 → gea_program-0.44.0}/pyproject.toml +1 -1
  19. {gea_program-0.43.0 → gea_program-0.44.0}/LICENSE +0 -0
  20. {gea_program-0.43.0 → gea_program-0.44.0}/gea/BENCH_TEST_PROTOCOL.md +0 -0
  21. {gea_program-0.43.0 → gea_program-0.44.0}/gea/accuracy_statement.py +0 -0
  22. {gea_program-0.43.0 → gea_program-0.44.0}/gea/alarm_engine.py +0 -0
  23. {gea_program-0.43.0 → gea_program-0.44.0}/gea/backup.py +0 -0
  24. {gea_program-0.43.0 → gea_program-0.44.0}/gea/bench.py +0 -0
  25. {gea_program-0.43.0 → gea_program-0.44.0}/gea/blind_harness.py +0 -0
  26. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/acex_lomonosov_age_depth_model.provenance.json +0 -0
  27. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/acex_lomonosov_age_depth_model.txt +0 -0
  28. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/agassiz77_canada_temperature.csv +0 -0
  29. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/agassiz77_canada_temperature.provenance.json +0 -0
  30. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/barbados_110_consolidation.provenance.json +0 -0
  31. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/barbados_110_consolidation.txt +0 -0
  32. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/bengal_u1452_grain_size.provenance.json +0 -0
  33. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/bengal_u1452_grain_size.txt +0 -0
  34. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/blake_164_methane_isotopes.provenance.json +0 -0
  35. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/blake_164_methane_isotopes.txt +0 -0
  36. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +0 -0
  37. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.txt +0 -0
  38. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/collingwood_1_28_ks_complete.las +0 -0
  39. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/collingwood_1_28_ks_complete.provenance.json +0 -0
  40. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/costa_rica_odp_friction_envelope.provenance.json +0 -0
  41. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/costa_rica_odp_friction_envelope.txt +0 -0
  42. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +0 -0
  43. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +0 -0
  44. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/dsdp_504b_physical_properties.provenance.json +0 -0
  45. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/dsdp_504b_physical_properties.txt +0 -0
  46. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/dsdp_504b_sound_velocity.provenance.json +0 -0
  47. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/dsdp_504b_sound_velocity.txt +0 -0
  48. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/elgygytgyn_5011_turbidites.provenance.json +0 -0
  49. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/elgygytgyn_5011_turbidites.txt +0 -0
  50. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/epica_domec_co2_800kyr.provenance.json +0 -0
  51. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/epica_domec_co2_800kyr.txt +0 -0
  52. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/fram_909_organic_petrography.provenance.json +0 -0
  53. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/fram_909_organic_petrography.txt +0 -0
  54. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/gbr_325_coral_uth_ages.provenance.json +0 -0
  55. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/gbr_325_coral_uth_ages.txt +0 -0
  56. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/gisp2_greenland_temperature.csv +0 -0
  57. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/gisp2_greenland_temperature.provenance.json +0 -0
  58. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/gom_308_t2p_insitu.provenance.json +0 -0
  59. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/gom_308_t2p_insitu.txt +0 -0
  60. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/guaymas_385_dom_d13c.provenance.json +0 -0
  61. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/guaymas_385_dom_d13c.txt +0 -0
  62. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +0 -0
  63. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/hikurangi_u1520_friction_insitu.txt +0 -0
  64. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +0 -0
  65. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +0 -0
  66. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/iodp_u1324_pore_pressure.provenance.json +0 -0
  67. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/iodp_u1324_pore_pressure.txt +0 -0
  68. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/jfast_c0019_slow_slip_events.provenance.json +0 -0
  69. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/jfast_c0019_slow_slip_events.txt +0 -0
  70. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/kennetcook_2_p129_excerpt.las +0 -0
  71. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/kennetcook_2_p129_excerpt.provenance.json +0 -0
  72. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_bhgm_density.dat +0 -0
  73. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_bhgm_density.provenance.json +0 -0
  74. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +0 -0
  75. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_complog_6020_excerpt.txt +0 -0
  76. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_hlog246_temperature.dat +0 -0
  77. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_hlog246_temperature.provenance.json +0 -0
  78. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_rockmech_compress.dat +0 -0
  79. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_rockmech_compress.provenance.json +0 -0
  80. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +0 -0
  81. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +0 -0
  82. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_vb_rockmech_compress.dat +0 -0
  83. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_vb_rockmech_compress.provenance.json +0 -0
  84. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_vb_vlog251_temperature.dat +0 -0
  85. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ktb_vb_vlog251_temperature.provenance.json +0 -0
  86. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/l06_06_nl_survey.csv +0 -0
  87. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/l06_06_nl_survey.provenance.json +0 -0
  88. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/l07_01_nl_excerpt.las +0 -0
  89. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/l07_01_nl_excerpt.provenance.json +0 -0
  90. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +0 -0
  91. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/mariana_1200_serpentinite_geochem.txt +0 -0
  92. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/med_160_sapropels.provenance.json +0 -0
  93. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/med_160_sapropels.txt +0 -0
  94. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/nankai_megasplay_shear_strength.provenance.json +0 -0
  95. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/nankai_megasplay_shear_strength.txt +0 -0
  96. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1027b_thermal_conductivity.provenance.json +0 -0
  97. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1027b_thermal_conductivity.txt +0 -0
  98. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1027c_cork_temperature.provenance.json +0 -0
  99. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1027c_cork_temperature.txt +0 -0
  100. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1165b_thermal_conductivity.provenance.json +0 -0
  101. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1165b_thermal_conductivity.txt +0 -0
  102. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +0 -0
  103. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.txt +0 -0
  104. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +0 -0
  105. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_504b_dike_elastic_moduli.txt +0 -0
  106. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +0 -0
  107. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_504b_leg137_borehole_fluids.txt +0 -0
  108. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +0 -0
  109. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.txt +0 -0
  110. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/peru_201_sulfate_reduction.provenance.json +0 -0
  111. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/peru_201_sulfate_reduction.txt +0 -0
  112. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/scorpio_e1_sa_excerpt.las +0 -0
  113. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/scorpio_e1_sa_excerpt.provenance.json +0 -0
  114. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/sumatra_362_cohesion.provenance.json +0 -0
  115. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/sumatra_362_cohesion.txt +0 -0
  116. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/university_6_17_no1_tx_excerpt.las +0 -0
  117. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +0 -0
  118. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ursa_308_xrd_mineralogy.provenance.json +0 -0
  119. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/ursa_308_xrd_mineralogy.txt +0 -0
  120. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/volve_15_9_19_sr_excerpt.las +0 -0
  121. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +0 -0
  122. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/volve_15_9_19a_core_excerpt.csv +0 -0
  123. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +0 -0
  124. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/volve_f12_f14_production_excerpt.csv +0 -0
  125. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/volve_f12_f14_production_excerpt.provenance.json +0 -0
  126. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/walvis_208_petm_carbonate.provenance.json +0 -0
  127. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/walvis_208_petm_carbonate.txt +0 -0
  128. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/woodlark_1109_rock_eval.provenance.json +0 -0
  129. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog/woodlark_1109_rock_eval.txt +0 -0
  130. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog_map.py +0 -0
  131. {gea_program-0.43.0 → gea_program-0.44.0}/gea/catalog_register.py +0 -0
  132. {gea_program-0.43.0 → gea_program-0.44.0}/gea/certificates.py +0 -0
  133. {gea_program-0.43.0 → gea_program-0.44.0}/gea/cli.py +0 -0
  134. {gea_program-0.43.0 → gea_program-0.44.0}/gea/client_reports.py +0 -0
  135. {gea_program-0.43.0 → gea_program-0.44.0}/gea/config_versioning.py +0 -0
  136. {gea_program-0.43.0 → gea_program-0.44.0}/gea/conformance.py +0 -0
  137. {gea_program-0.43.0 → gea_program-0.44.0}/gea/control_room.py +0 -0
  138. {gea_program-0.43.0 → gea_program-0.44.0}/gea/correlation.py +0 -0
  139. {gea_program-0.43.0 → gea_program-0.44.0}/gea/dashboard.py +0 -0
  140. {gea_program-0.43.0 → gea_program-0.44.0}/gea/declarations.py +0 -0
  141. {gea_program-0.43.0 → gea_program-0.44.0}/gea/deviation.py +0 -0
  142. {gea_program-0.43.0 → gea_program-0.44.0}/gea/dlis.py +0 -0
  143. {gea_program-0.43.0 → gea_program-0.44.0}/gea/dnp3.py +0 -0
  144. {gea_program-0.43.0 → gea_program-0.44.0}/gea/doctor.py +0 -0
  145. {gea_program-0.43.0 → gea_program-0.44.0}/gea/dossier.py +0 -0
  146. {gea_program-0.43.0 → gea_program-0.44.0}/gea/dossier_verify.py +0 -0
  147. {gea_program-0.43.0 → gea_program-0.44.0}/gea/downhole_engine.py +0 -0
  148. {gea_program-0.43.0 → gea_program-0.44.0}/gea/drift_monitor.py +0 -0
  149. {gea_program-0.43.0 → gea_program-0.44.0}/gea/drill_string.py +0 -0
  150. {gea_program-0.43.0 → gea_program-0.44.0}/gea/earth_model.py +0 -0
  151. {gea_program-0.43.0 → gea_program-0.44.0}/gea/economics.py +0 -0
  152. {gea_program-0.43.0 → gea_program-0.44.0}/gea/ed25519.py +0 -0
  153. {gea_program-0.43.0 → gea_program-0.44.0}/gea/edr.py +0 -0
  154. {gea_program-0.43.0 → gea_program-0.44.0}/gea/elevation.py +0 -0
  155. {gea_program-0.43.0 → gea_program-0.44.0}/gea/etp.py +0 -0
  156. {gea_program-0.43.0 → gea_program-0.44.0}/gea/etp12_schemas.json +0 -0
  157. {gea_program-0.43.0 → gea_program-0.44.0}/gea/example_register_map.json +0 -0
  158. {gea_program-0.43.0 → gea_program-0.44.0}/gea/fat_sat.py +0 -0
  159. {gea_program-0.43.0 → gea_program-0.44.0}/gea/files.py +0 -0
  160. {gea_program-0.43.0 → gea_program-0.44.0}/gea/fixtures/README.md +0 -0
  161. {gea_program-0.43.0 → gea_program-0.44.0}/gea/fixtures/comcat_delaware_basin_2026-09.txt +0 -0
  162. {gea_program-0.43.0 → gea_program-0.44.0}/gea/fixtures/comcat_mahoning_1990-2026.txt +0 -0
  163. {gea_program-0.43.0 → gea_program-0.44.0}/gea/fixtures/comcat_youngstown_2011-2012.txt +0 -0
  164. {gea_program-0.43.0 → gea_program-0.44.0}/gea/fixtures/ne_110m_coastline.json +0 -0
  165. {gea_program-0.43.0 → gea_program-0.44.0}/gea/follower.py +0 -0
  166. {gea_program-0.43.0 → gea_program-0.44.0}/gea/forward_model.py +0 -0
  167. {gea_program-0.43.0 → gea_program-0.44.0}/gea/friction_priors.py +0 -0
  168. {gea_program-0.43.0 → gea_program-0.44.0}/gea/gamma.py +0 -0
  169. {gea_program-0.43.0 → gea_program-0.44.0}/gea/gauge_aging.py +0 -0
  170. {gea_program-0.43.0 → gea_program-0.44.0}/gea/gauge_specs.py +0 -0
  171. {gea_program-0.43.0 → gea_program-0.44.0}/gea/geodesy.py +0 -0
  172. {gea_program-0.43.0 → gea_program-0.44.0}/gea/gravity_reference.py +0 -0
  173. {gea_program-0.43.0 → gea_program-0.44.0}/gea/hartip.py +0 -0
  174. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/TESTER_GUIDE.md +0 -0
  175. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/alarms.md +0 -0
  176. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/audit-update.md +0 -0
  177. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/backup.md +0 -0
  178. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/catalog.md +0 -0
  179. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/charts.md +0 -0
  180. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/conformance.md +0 -0
  181. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/control-room.md +0 -0
  182. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/data-in.md +0 -0
  183. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/declare.md +0 -0
  184. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/dlis.md +0 -0
  185. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/doctor.md +0 -0
  186. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/dossier.md +0 -0
  187. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/drift.md +0 -0
  188. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/drillstring.md +0 -0
  189. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/economics.md +0 -0
  190. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/edr.md +0 -0
  191. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/etp.md +0 -0
  192. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/field-ports.md +0 -0
  193. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/files.md +0 -0
  194. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/historian.md +0 -0
  195. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/instruments.md +0 -0
  196. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/interval.md +0 -0
  197. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/map.md +0 -0
  198. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/month-end.md +0 -0
  199. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/osdu.md +0 -0
  200. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/patches.md +0 -0
  201. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/penetrator.md +0 -0
  202. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/ppdm.md +0 -0
  203. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/quakeml.md +0 -0
  204. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/quality.md +0 -0
  205. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/report-history.md +0 -0
  206. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/seedlink.md +0 -0
  207. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/seismic.md +0 -0
  208. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/setup.md +0 -0
  209. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/simulation.md +0 -0
  210. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/site.md +0 -0
  211. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/sites.md +0 -0
  212. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/slip.md +0 -0
  213. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/soak.md +0 -0
  214. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/sra.md +0 -0
  215. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/start.md +0 -0
  216. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/structure.md +0 -0
  217. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/tools.md +0 -0
  218. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/transients.md +0 -0
  219. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/upkeep.md +0 -0
  220. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/vibration.md +0 -0
  221. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/well-report.md +0 -0
  222. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/well-tests.md +0 -0
  223. {gea_program-0.43.0 → gea_program-0.44.0}/gea/help/well3d.md +0 -0
  224. {gea_program-0.43.0 → gea_program-0.44.0}/gea/housekeeping.py +0 -0
  225. {gea_program-0.43.0 → gea_program-0.44.0}/gea/interval.py +0 -0
  226. {gea_program-0.43.0 → gea_program-0.44.0}/gea/inverse_engine.py +0 -0
  227. {gea_program-0.43.0 → gea_program-0.44.0}/gea/jobs.py +0 -0
  228. {gea_program-0.43.0 → gea_program-0.44.0}/gea/live_ports.py +0 -0
  229. {gea_program-0.43.0 → gea_program-0.44.0}/gea/loadtest.py +0 -0
  230. {gea_program-0.43.0 → gea_program-0.44.0}/gea/matplotlib_demo.py +0 -0
  231. {gea_program-0.43.0 → gea_program-0.44.0}/gea/modbus.py +0 -0
  232. {gea_program-0.43.0 → gea_program-0.44.0}/gea/modbus_rtu.py +0 -0
  233. {gea_program-0.43.0 → gea_program-0.44.0}/gea/model_card.py +0 -0
  234. {gea_program-0.43.0 → gea_program-0.44.0}/gea/month_end.py +0 -0
  235. {gea_program-0.43.0 → gea_program-0.44.0}/gea/mqtt_port.py +0 -0
  236. {gea_program-0.43.0 → gea_program-0.44.0}/gea/opcua_port.py +0 -0
  237. {gea_program-0.43.0 → gea_program-0.44.0}/gea/operator_app.py +0 -0
  238. {gea_program-0.43.0 → gea_program-0.44.0}/gea/osdu.py +0 -0
  239. {gea_program-0.43.0 → gea_program-0.44.0}/gea/patches.py +0 -0
  240. {gea_program-0.43.0 → gea_program-0.44.0}/gea/penetrator.py +0 -0
  241. {gea_program-0.43.0 → gea_program-0.44.0}/gea/permits.py +0 -0
  242. {gea_program-0.43.0 → gea_program-0.44.0}/gea/piwebapi.py +0 -0
  243. {gea_program-0.43.0 → gea_program-0.44.0}/gea/ports.py +0 -0
  244. {gea_program-0.43.0 → gea_program-0.44.0}/gea/ppdm.py +0 -0
  245. {gea_program-0.43.0 → gea_program-0.44.0}/gea/profile_catalog.py +0 -0
  246. {gea_program-0.43.0 → gea_program-0.44.0}/gea/project.py +0 -0
  247. {gea_program-0.43.0 → gea_program-0.44.0}/gea/qt6_downhole_app.py +0 -0
  248. {gea_program-0.43.0 → gea_program-0.44.0}/gea/quakeml.py +0 -0
  249. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reconciler.py +0 -0
  250. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/IU_ANMO_10_BHZ_response.provenance.json +0 -0
  251. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/IU_ANMO_10_BHZ_response.xml +0 -0
  252. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/eia_henry_hub_daily.csv.gz +0 -0
  253. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/eia_prices.provenance.json +0 -0
  254. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/eia_wti_daily.csv.gz +0 -0
  255. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/libmseed_steim.provenance.json +0 -0
  256. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/libmseed_steim1_4096.mseed +0 -0
  257. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/libmseed_steim2_4096.mseed +0 -0
  258. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/libmseed_steim2_512.mseed +0 -0
  259. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/mahoning_example.json +0 -0
  260. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/mud_motor_specs.json +0 -0
  261. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/penetrator_ratings.json +0 -0
  262. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/seismicity_protocols.json +0 -0
  263. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/tool_sources.json +0 -0
  264. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/us_outline.json +0 -0
  265. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/us_outline.provenance.json +0 -0
  266. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_3d.provenance.json +0 -0
  267. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_f4_interpretation.las.gz +0 -0
  268. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_f4_logs.provenance.json +0 -0
  269. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_f4_lwd.las.gz +0 -0
  270. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_surfaces.json.gz +0 -0
  271. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_traj_F-10.xml.gz +0 -0
  272. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_traj_F-4.xml.gz +0 -0
  273. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_traj_F-5_12.25in.xml.gz +0 -0
  274. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_traj_F-5_17.5in.xml.gz +0 -0
  275. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_traj_F-5_36in.xml.gz +0 -0
  276. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_traj_F-5_8.5in.xml.gz +0 -0
  277. {gea_program-0.43.0 → gea_program-0.44.0}/gea/reference/volve_well_picks.csv.gz +0 -0
  278. {gea_program-0.43.0 → gea_program-0.44.0}/gea/report_history.py +0 -0
  279. {gea_program-0.43.0 → gea_program-0.44.0}/gea/rock_inventory.py +0 -0
  280. {gea_program-0.43.0 → gea_program-0.44.0}/gea/sample_record.py +0 -0
  281. {gea_program-0.43.0 → gea_program-0.44.0}/gea/sample_well_profile.csv +0 -0
  282. {gea_program-0.43.0 → gea_program-0.44.0}/gea/sbom.py +0 -0
  283. {gea_program-0.43.0 → gea_program-0.44.0}/gea/seedlink.py +0 -0
  284. {gea_program-0.43.0 → gea_program-0.44.0}/gea/segy.py +0 -0
  285. {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic.py +0 -0
  286. {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_array.py +0 -0
  287. {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_assoc.py +0 -0
  288. {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_detect.py +0 -0
  289. {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_film.py +0 -0
  290. {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_harmonic.py +0 -0
  291. {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_qc.py +0 -0
  292. {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_response.py +0 -0
  293. {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_signature.py +0 -0
  294. {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_track.py +0 -0
  295. {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_uncertainty.py +0 -0
  296. {gea_program-0.43.0 → gea_program-0.44.0}/gea/seismic_unlisted.py +0 -0
  297. {gea_program-0.43.0 → gea_program-0.44.0}/gea/sensor_swap.py +0 -0
  298. {gea_program-0.43.0 → gea_program-0.44.0}/gea/series_cache.py +0 -0
  299. {gea_program-0.43.0 → gea_program-0.44.0}/gea/service_life.py +0 -0
  300. {gea_program-0.43.0 → gea_program-0.44.0}/gea/setup_checklist.py +0 -0
  301. {gea_program-0.43.0 → gea_program-0.44.0}/gea/shapefile.py +0 -0
  302. {gea_program-0.43.0 → gea_program-0.44.0}/gea/shell.py +0 -0
  303. {gea_program-0.43.0 → gea_program-0.44.0}/gea/shut_in.py +0 -0
  304. {gea_program-0.43.0 → gea_program-0.44.0}/gea/simulation.py +0 -0
  305. {gea_program-0.43.0 → gea_program-0.44.0}/gea/site_map.py +0 -0
  306. {gea_program-0.43.0 → gea_program-0.44.0}/gea/sla_report.py +0 -0
  307. {gea_program-0.43.0 → gea_program-0.44.0}/gea/slip.py +0 -0
  308. {gea_program-0.43.0 → gea_program-0.44.0}/gea/soak.py +0 -0
  309. {gea_program-0.43.0 → gea_program-0.44.0}/gea/sra.py +0 -0
  310. {gea_program-0.43.0 → gea_program-0.44.0}/gea/store_forward.py +0 -0
  311. {gea_program-0.43.0 → gea_program-0.44.0}/gea/strata_join.py +0 -0
  312. {gea_program-0.43.0 → gea_program-0.44.0}/gea/structure.py +0 -0
  313. {gea_program-0.43.0 → gea_program-0.44.0}/gea/supervisor.py +0 -0
  314. {gea_program-0.43.0 → gea_program-0.44.0}/gea/survey_cmd.py +0 -0
  315. {gea_program-0.43.0 → gea_program-0.44.0}/gea/survey_view.py +0 -0
  316. {gea_program-0.43.0 → gea_program-0.44.0}/gea/telemetry.py +0 -0
  317. {gea_program-0.43.0 → gea_program-0.44.0}/gea/tool_catalog.py +0 -0
  318. {gea_program-0.43.0 → gea_program-0.44.0}/gea/tool_library.py +0 -0
  319. {gea_program-0.43.0 → gea_program-0.44.0}/gea/tool_taxonomy.py +0 -0
  320. {gea_program-0.43.0 → gea_program-0.44.0}/gea/transient.py +0 -0
  321. {gea_program-0.43.0 → gea_program-0.44.0}/gea/vibration.py +0 -0
  322. {gea_program-0.43.0 → gea_program-0.44.0}/gea/well3d.py +0 -0
  323. {gea_program-0.43.0 → gea_program-0.44.0}/gea/well_assembler.py +0 -0
  324. {gea_program-0.43.0 → gea_program-0.44.0}/gea/well_report.py +0 -0
  325. {gea_program-0.43.0 → gea_program-0.44.0}/gea/well_test_validation.py +0 -0
  326. {gea_program-0.43.0 → gea_program-0.44.0}/gea/wits0.py +0 -0
  327. {gea_program-0.43.0 → gea_program-0.44.0}/gea/witsml.py +0 -0
  328. {gea_program-0.43.0 → gea_program-0.44.0}/gea/workspace.py +0 -0
  329. {gea_program-0.43.0 → gea_program-0.44.0}/gea_program.egg-info/dependency_links.txt +0 -0
  330. {gea_program-0.43.0 → gea_program-0.44.0}/gea_program.egg-info/entry_points.txt +0 -0
  331. {gea_program-0.43.0 → gea_program-0.44.0}/gea_program.egg-info/requires.txt +0 -0
  332. {gea_program-0.43.0 → gea_program-0.44.0}/gea_program.egg-info/top_level.txt +0 -0
  333. {gea_program-0.43.0 → gea_program-0.44.0}/setup.cfg +0 -0
  334. {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.43.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
@@ -123,7 +123,7 @@ from .profile_catalog import (
123
123
  read_pangaea_txt,
124
124
  )
125
125
 
126
- __version__ = "0.43.0"
126
+ __version__ = "0.44.0"
127
127
  __all__ = [
128
128
  "aging_rate", "rate_psi_yr", "accuracy_psi",
129
129
  "Sensor", "SimulatorConfig", "DownholeEngine",
@@ -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'),