gea-program 0.44.0__tar.gz → 0.45.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 (337) hide show
  1. {gea_program-0.44.0 → gea_program-0.45.0}/PKG-INFO +16 -1
  2. {gea_program-0.44.0 → gea_program-0.45.0}/README.md +15 -0
  3. {gea_program-0.44.0 → gea_program-0.45.0}/gea/__init__.py +1 -1
  4. {gea_program-0.44.0 → gea_program-0.45.0}/gea/__main__.py +59 -0
  5. {gea_program-0.44.0 → gea_program-0.45.0}/gea/acceptance_tests.py +281 -1
  6. {gea_program-0.44.0 → gea_program-0.45.0}/gea/client_reports.py +15 -1
  7. {gea_program-0.44.0 → gea_program-0.45.0}/gea/dossier.py +134 -17
  8. {gea_program-0.44.0 → gea_program-0.45.0}/gea/dossier_verify.py +49 -2
  9. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/dossier.md +3 -1
  10. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/opseis.md +2 -2
  11. gea_program-0.45.0/gea/help/pad-summary.md +13 -0
  12. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/report-history.md +2 -0
  13. {gea_program-0.44.0 → gea_program-0.45.0}/gea/helplib.py +1 -0
  14. {gea_program-0.44.0 → gea_program-0.45.0}/gea/notify.py +6 -1
  15. {gea_program-0.44.0 → gea_program-0.45.0}/gea/op_seismicity.py +109 -5
  16. gea_program-0.45.0/gea/pad_summary.py +305 -0
  17. gea_program-0.45.0/gea/pdf_report.py +532 -0
  18. {gea_program-0.44.0 → gea_program-0.45.0}/gea/seismic_watch.py +10 -5
  19. {gea_program-0.44.0 → gea_program-0.45.0}/gea/service.py +24 -0
  20. {gea_program-0.44.0 → gea_program-0.45.0}/gea/web/app.html +16 -4
  21. {gea_program-0.44.0 → gea_program-0.45.0}/gea_program.egg-info/PKG-INFO +16 -1
  22. {gea_program-0.44.0 → gea_program-0.45.0}/gea_program.egg-info/SOURCES.txt +3 -0
  23. {gea_program-0.44.0 → gea_program-0.45.0}/pyproject.toml +1 -1
  24. {gea_program-0.44.0 → gea_program-0.45.0}/LICENSE +0 -0
  25. {gea_program-0.44.0 → gea_program-0.45.0}/gea/BENCH_TEST_PROTOCOL.md +0 -0
  26. {gea_program-0.44.0 → gea_program-0.45.0}/gea/accuracy_statement.py +0 -0
  27. {gea_program-0.44.0 → gea_program-0.45.0}/gea/alarm_engine.py +0 -0
  28. {gea_program-0.44.0 → gea_program-0.45.0}/gea/alerts.py +0 -0
  29. {gea_program-0.44.0 → gea_program-0.45.0}/gea/backup.py +0 -0
  30. {gea_program-0.44.0 → gea_program-0.45.0}/gea/bench.py +0 -0
  31. {gea_program-0.44.0 → gea_program-0.45.0}/gea/blind_harness.py +0 -0
  32. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/acex_lomonosov_age_depth_model.provenance.json +0 -0
  33. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/acex_lomonosov_age_depth_model.txt +0 -0
  34. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/agassiz77_canada_temperature.csv +0 -0
  35. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/agassiz77_canada_temperature.provenance.json +0 -0
  36. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/barbados_110_consolidation.provenance.json +0 -0
  37. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/barbados_110_consolidation.txt +0 -0
  38. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/bengal_u1452_grain_size.provenance.json +0 -0
  39. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/bengal_u1452_grain_size.txt +0 -0
  40. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/blake_164_methane_isotopes.provenance.json +0 -0
  41. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/blake_164_methane_isotopes.txt +0 -0
  42. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +0 -0
  43. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.txt +0 -0
  44. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/collingwood_1_28_ks_complete.las +0 -0
  45. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/collingwood_1_28_ks_complete.provenance.json +0 -0
  46. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/costa_rica_odp_friction_envelope.provenance.json +0 -0
  47. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/costa_rica_odp_friction_envelope.txt +0 -0
  48. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +0 -0
  49. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +0 -0
  50. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/dsdp_504b_physical_properties.provenance.json +0 -0
  51. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/dsdp_504b_physical_properties.txt +0 -0
  52. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/dsdp_504b_sound_velocity.provenance.json +0 -0
  53. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/dsdp_504b_sound_velocity.txt +0 -0
  54. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/elgygytgyn_5011_turbidites.provenance.json +0 -0
  55. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/elgygytgyn_5011_turbidites.txt +0 -0
  56. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/epica_domec_co2_800kyr.provenance.json +0 -0
  57. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/epica_domec_co2_800kyr.txt +0 -0
  58. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/fram_909_organic_petrography.provenance.json +0 -0
  59. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/fram_909_organic_petrography.txt +0 -0
  60. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/gbr_325_coral_uth_ages.provenance.json +0 -0
  61. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/gbr_325_coral_uth_ages.txt +0 -0
  62. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/gisp2_greenland_temperature.csv +0 -0
  63. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/gisp2_greenland_temperature.provenance.json +0 -0
  64. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/gom_308_t2p_insitu.provenance.json +0 -0
  65. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/gom_308_t2p_insitu.txt +0 -0
  66. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/guaymas_385_dom_d13c.provenance.json +0 -0
  67. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/guaymas_385_dom_d13c.txt +0 -0
  68. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +0 -0
  69. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/hikurangi_u1520_friction_insitu.txt +0 -0
  70. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +0 -0
  71. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +0 -0
  72. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/iodp_u1324_pore_pressure.provenance.json +0 -0
  73. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/iodp_u1324_pore_pressure.txt +0 -0
  74. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/jfast_c0019_slow_slip_events.provenance.json +0 -0
  75. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/jfast_c0019_slow_slip_events.txt +0 -0
  76. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/kennetcook_2_p129_excerpt.las +0 -0
  77. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/kennetcook_2_p129_excerpt.provenance.json +0 -0
  78. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ktb_hb_bhgm_density.dat +0 -0
  79. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ktb_hb_bhgm_density.provenance.json +0 -0
  80. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +0 -0
  81. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ktb_hb_complog_6020_excerpt.txt +0 -0
  82. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ktb_hb_hlog246_temperature.dat +0 -0
  83. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ktb_hb_hlog246_temperature.provenance.json +0 -0
  84. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ktb_hb_rockmech_compress.dat +0 -0
  85. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ktb_hb_rockmech_compress.provenance.json +0 -0
  86. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +0 -0
  87. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +0 -0
  88. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ktb_vb_rockmech_compress.dat +0 -0
  89. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ktb_vb_rockmech_compress.provenance.json +0 -0
  90. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ktb_vb_vlog251_temperature.dat +0 -0
  91. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ktb_vb_vlog251_temperature.provenance.json +0 -0
  92. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/l06_06_nl_survey.csv +0 -0
  93. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/l06_06_nl_survey.provenance.json +0 -0
  94. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/l07_01_nl_excerpt.las +0 -0
  95. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/l07_01_nl_excerpt.provenance.json +0 -0
  96. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +0 -0
  97. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/mariana_1200_serpentinite_geochem.txt +0 -0
  98. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/med_160_sapropels.provenance.json +0 -0
  99. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/med_160_sapropels.txt +0 -0
  100. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/nankai_megasplay_shear_strength.provenance.json +0 -0
  101. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/nankai_megasplay_shear_strength.txt +0 -0
  102. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/odp_1027b_thermal_conductivity.provenance.json +0 -0
  103. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/odp_1027b_thermal_conductivity.txt +0 -0
  104. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/odp_1027c_cork_temperature.provenance.json +0 -0
  105. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/odp_1027c_cork_temperature.txt +0 -0
  106. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/odp_1165b_thermal_conductivity.provenance.json +0 -0
  107. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/odp_1165b_thermal_conductivity.txt +0 -0
  108. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +0 -0
  109. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.txt +0 -0
  110. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +0 -0
  111. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/odp_504b_dike_elastic_moduli.txt +0 -0
  112. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +0 -0
  113. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/odp_504b_leg137_borehole_fluids.txt +0 -0
  114. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +0 -0
  115. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.txt +0 -0
  116. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/peru_201_sulfate_reduction.provenance.json +0 -0
  117. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/peru_201_sulfate_reduction.txt +0 -0
  118. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/scorpio_e1_sa_excerpt.las +0 -0
  119. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/scorpio_e1_sa_excerpt.provenance.json +0 -0
  120. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/sumatra_362_cohesion.provenance.json +0 -0
  121. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/sumatra_362_cohesion.txt +0 -0
  122. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/university_6_17_no1_tx_excerpt.las +0 -0
  123. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +0 -0
  124. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ursa_308_xrd_mineralogy.provenance.json +0 -0
  125. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/ursa_308_xrd_mineralogy.txt +0 -0
  126. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/volve_15_9_19_sr_excerpt.las +0 -0
  127. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +0 -0
  128. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/volve_15_9_19a_core_excerpt.csv +0 -0
  129. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +0 -0
  130. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/volve_f12_f14_production_excerpt.csv +0 -0
  131. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/volve_f12_f14_production_excerpt.provenance.json +0 -0
  132. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/walvis_208_petm_carbonate.provenance.json +0 -0
  133. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/walvis_208_petm_carbonate.txt +0 -0
  134. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/woodlark_1109_rock_eval.provenance.json +0 -0
  135. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog/woodlark_1109_rock_eval.txt +0 -0
  136. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog_map.py +0 -0
  137. {gea_program-0.44.0 → gea_program-0.45.0}/gea/catalog_register.py +0 -0
  138. {gea_program-0.44.0 → gea_program-0.45.0}/gea/certificates.py +0 -0
  139. {gea_program-0.44.0 → gea_program-0.45.0}/gea/cli.py +0 -0
  140. {gea_program-0.44.0 → gea_program-0.45.0}/gea/config_versioning.py +0 -0
  141. {gea_program-0.44.0 → gea_program-0.45.0}/gea/conformance.py +0 -0
  142. {gea_program-0.44.0 → gea_program-0.45.0}/gea/control_room.py +0 -0
  143. {gea_program-0.44.0 → gea_program-0.45.0}/gea/correlation.py +0 -0
  144. {gea_program-0.44.0 → gea_program-0.45.0}/gea/dashboard.py +0 -0
  145. {gea_program-0.44.0 → gea_program-0.45.0}/gea/declarations.py +0 -0
  146. {gea_program-0.44.0 → gea_program-0.45.0}/gea/deviation.py +0 -0
  147. {gea_program-0.44.0 → gea_program-0.45.0}/gea/dlis.py +0 -0
  148. {gea_program-0.44.0 → gea_program-0.45.0}/gea/dnp3.py +0 -0
  149. {gea_program-0.44.0 → gea_program-0.45.0}/gea/doctor.py +0 -0
  150. {gea_program-0.44.0 → gea_program-0.45.0}/gea/downhole_engine.py +0 -0
  151. {gea_program-0.44.0 → gea_program-0.45.0}/gea/drift_monitor.py +0 -0
  152. {gea_program-0.44.0 → gea_program-0.45.0}/gea/drill_string.py +0 -0
  153. {gea_program-0.44.0 → gea_program-0.45.0}/gea/earth_model.py +0 -0
  154. {gea_program-0.44.0 → gea_program-0.45.0}/gea/economics.py +0 -0
  155. {gea_program-0.44.0 → gea_program-0.45.0}/gea/ed25519.py +0 -0
  156. {gea_program-0.44.0 → gea_program-0.45.0}/gea/edr.py +0 -0
  157. {gea_program-0.44.0 → gea_program-0.45.0}/gea/elevation.py +0 -0
  158. {gea_program-0.44.0 → gea_program-0.45.0}/gea/etp.py +0 -0
  159. {gea_program-0.44.0 → gea_program-0.45.0}/gea/etp12_schemas.json +0 -0
  160. {gea_program-0.44.0 → gea_program-0.45.0}/gea/example_register_map.json +0 -0
  161. {gea_program-0.44.0 → gea_program-0.45.0}/gea/fat_sat.py +0 -0
  162. {gea_program-0.44.0 → gea_program-0.45.0}/gea/files.py +0 -0
  163. {gea_program-0.44.0 → gea_program-0.45.0}/gea/fixtures/README.md +0 -0
  164. {gea_program-0.44.0 → gea_program-0.45.0}/gea/fixtures/comcat_delaware_basin_2026-09.txt +0 -0
  165. {gea_program-0.44.0 → gea_program-0.45.0}/gea/fixtures/comcat_mahoning_1990-2026.txt +0 -0
  166. {gea_program-0.44.0 → gea_program-0.45.0}/gea/fixtures/comcat_youngstown_2011-2012.txt +0 -0
  167. {gea_program-0.44.0 → gea_program-0.45.0}/gea/fixtures/ne_110m_coastline.json +0 -0
  168. {gea_program-0.44.0 → gea_program-0.45.0}/gea/follower.py +0 -0
  169. {gea_program-0.44.0 → gea_program-0.45.0}/gea/forward_model.py +0 -0
  170. {gea_program-0.44.0 → gea_program-0.45.0}/gea/friction_priors.py +0 -0
  171. {gea_program-0.44.0 → gea_program-0.45.0}/gea/gamma.py +0 -0
  172. {gea_program-0.44.0 → gea_program-0.45.0}/gea/gauge_aging.py +0 -0
  173. {gea_program-0.44.0 → gea_program-0.45.0}/gea/gauge_specs.py +0 -0
  174. {gea_program-0.44.0 → gea_program-0.45.0}/gea/geodesy.py +0 -0
  175. {gea_program-0.44.0 → gea_program-0.45.0}/gea/gravity_reference.py +0 -0
  176. {gea_program-0.44.0 → gea_program-0.45.0}/gea/hartip.py +0 -0
  177. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/TESTER_GUIDE.md +0 -0
  178. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/alarms.md +0 -0
  179. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/alerts.md +0 -0
  180. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/audit-update.md +0 -0
  181. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/backup.md +0 -0
  182. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/catalog.md +0 -0
  183. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/charts.md +0 -0
  184. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/conformance.md +0 -0
  185. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/control-room.md +0 -0
  186. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/data-in.md +0 -0
  187. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/declare.md +0 -0
  188. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/dlis.md +0 -0
  189. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/doctor.md +0 -0
  190. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/drift.md +0 -0
  191. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/drillstring.md +0 -0
  192. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/economics.md +0 -0
  193. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/edr.md +0 -0
  194. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/etp.md +0 -0
  195. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/field-ports.md +0 -0
  196. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/files.md +0 -0
  197. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/historian.md +0 -0
  198. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/instruments.md +0 -0
  199. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/interval.md +0 -0
  200. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/map.md +0 -0
  201. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/month-end.md +0 -0
  202. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/notifications.md +0 -0
  203. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/osdu.md +0 -0
  204. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/patches.md +0 -0
  205. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/penetrator.md +0 -0
  206. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/ppdm.md +0 -0
  207. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/quakeml.md +0 -0
  208. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/quality.md +0 -0
  209. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/seedlink.md +0 -0
  210. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/seismic.md +0 -0
  211. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/setup.md +0 -0
  212. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/simulation.md +0 -0
  213. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/site.md +0 -0
  214. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/sites.md +0 -0
  215. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/slip.md +0 -0
  216. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/soak.md +0 -0
  217. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/sra.md +0 -0
  218. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/start.md +0 -0
  219. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/structure.md +0 -0
  220. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/tools.md +0 -0
  221. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/transients.md +0 -0
  222. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/upkeep.md +0 -0
  223. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/vibration.md +0 -0
  224. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/well-report.md +0 -0
  225. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/well-tests.md +0 -0
  226. {gea_program-0.44.0 → gea_program-0.45.0}/gea/help/well3d.md +0 -0
  227. {gea_program-0.44.0 → gea_program-0.45.0}/gea/housekeeping.py +0 -0
  228. {gea_program-0.44.0 → gea_program-0.45.0}/gea/interval.py +0 -0
  229. {gea_program-0.44.0 → gea_program-0.45.0}/gea/inverse_engine.py +0 -0
  230. {gea_program-0.44.0 → gea_program-0.45.0}/gea/jobs.py +0 -0
  231. {gea_program-0.44.0 → gea_program-0.45.0}/gea/live_ports.py +0 -0
  232. {gea_program-0.44.0 → gea_program-0.45.0}/gea/loadtest.py +0 -0
  233. {gea_program-0.44.0 → gea_program-0.45.0}/gea/matplotlib_demo.py +0 -0
  234. {gea_program-0.44.0 → gea_program-0.45.0}/gea/modbus.py +0 -0
  235. {gea_program-0.44.0 → gea_program-0.45.0}/gea/modbus_rtu.py +0 -0
  236. {gea_program-0.44.0 → gea_program-0.45.0}/gea/model_card.py +0 -0
  237. {gea_program-0.44.0 → gea_program-0.45.0}/gea/month_end.py +0 -0
  238. {gea_program-0.44.0 → gea_program-0.45.0}/gea/mqtt_port.py +0 -0
  239. {gea_program-0.44.0 → gea_program-0.45.0}/gea/opcua_port.py +0 -0
  240. {gea_program-0.44.0 → gea_program-0.45.0}/gea/operator_app.py +0 -0
  241. {gea_program-0.44.0 → gea_program-0.45.0}/gea/osdu.py +0 -0
  242. {gea_program-0.44.0 → gea_program-0.45.0}/gea/patches.py +0 -0
  243. {gea_program-0.44.0 → gea_program-0.45.0}/gea/penetrator.py +0 -0
  244. {gea_program-0.44.0 → gea_program-0.45.0}/gea/permits.py +0 -0
  245. {gea_program-0.44.0 → gea_program-0.45.0}/gea/piwebapi.py +0 -0
  246. {gea_program-0.44.0 → gea_program-0.45.0}/gea/ports.py +0 -0
  247. {gea_program-0.44.0 → gea_program-0.45.0}/gea/ppdm.py +0 -0
  248. {gea_program-0.44.0 → gea_program-0.45.0}/gea/profile_catalog.py +0 -0
  249. {gea_program-0.44.0 → gea_program-0.45.0}/gea/project.py +0 -0
  250. {gea_program-0.44.0 → gea_program-0.45.0}/gea/qt6_downhole_app.py +0 -0
  251. {gea_program-0.44.0 → gea_program-0.45.0}/gea/quakeml.py +0 -0
  252. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reconciler.py +0 -0
  253. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/IU_ANMO_10_BHZ_response.provenance.json +0 -0
  254. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/IU_ANMO_10_BHZ_response.xml +0 -0
  255. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/eia_henry_hub_daily.csv.gz +0 -0
  256. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/eia_prices.provenance.json +0 -0
  257. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/eia_wti_daily.csv.gz +0 -0
  258. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/libmseed_steim.provenance.json +0 -0
  259. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/libmseed_steim1_4096.mseed +0 -0
  260. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/libmseed_steim2_4096.mseed +0 -0
  261. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/libmseed_steim2_512.mseed +0 -0
  262. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/mahoning_example.json +0 -0
  263. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/mud_motor_specs.json +0 -0
  264. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/penetrator_ratings.json +0 -0
  265. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/seismicity_protocols.json +0 -0
  266. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/tool_sources.json +0 -0
  267. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/us_outline.json +0 -0
  268. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/us_outline.provenance.json +0 -0
  269. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/volve_3d.provenance.json +0 -0
  270. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/volve_f4_interpretation.las.gz +0 -0
  271. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/volve_f4_logs.provenance.json +0 -0
  272. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/volve_f4_lwd.las.gz +0 -0
  273. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/volve_surfaces.json.gz +0 -0
  274. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/volve_traj_F-10.xml.gz +0 -0
  275. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/volve_traj_F-4.xml.gz +0 -0
  276. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/volve_traj_F-5_12.25in.xml.gz +0 -0
  277. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/volve_traj_F-5_17.5in.xml.gz +0 -0
  278. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/volve_traj_F-5_36in.xml.gz +0 -0
  279. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/volve_traj_F-5_8.5in.xml.gz +0 -0
  280. {gea_program-0.44.0 → gea_program-0.45.0}/gea/reference/volve_well_picks.csv.gz +0 -0
  281. {gea_program-0.44.0 → gea_program-0.45.0}/gea/report_history.py +0 -0
  282. {gea_program-0.44.0 → gea_program-0.45.0}/gea/rock_inventory.py +0 -0
  283. {gea_program-0.44.0 → gea_program-0.45.0}/gea/sample_record.py +0 -0
  284. {gea_program-0.44.0 → gea_program-0.45.0}/gea/sample_well_profile.csv +0 -0
  285. {gea_program-0.44.0 → gea_program-0.45.0}/gea/sbom.py +0 -0
  286. {gea_program-0.44.0 → gea_program-0.45.0}/gea/seedlink.py +0 -0
  287. {gea_program-0.44.0 → gea_program-0.45.0}/gea/segy.py +0 -0
  288. {gea_program-0.44.0 → gea_program-0.45.0}/gea/seismic.py +0 -0
  289. {gea_program-0.44.0 → gea_program-0.45.0}/gea/seismic_array.py +0 -0
  290. {gea_program-0.44.0 → gea_program-0.45.0}/gea/seismic_assoc.py +0 -0
  291. {gea_program-0.44.0 → gea_program-0.45.0}/gea/seismic_detect.py +0 -0
  292. {gea_program-0.44.0 → gea_program-0.45.0}/gea/seismic_film.py +0 -0
  293. {gea_program-0.44.0 → gea_program-0.45.0}/gea/seismic_harmonic.py +0 -0
  294. {gea_program-0.44.0 → gea_program-0.45.0}/gea/seismic_qc.py +0 -0
  295. {gea_program-0.44.0 → gea_program-0.45.0}/gea/seismic_response.py +0 -0
  296. {gea_program-0.44.0 → gea_program-0.45.0}/gea/seismic_signature.py +0 -0
  297. {gea_program-0.44.0 → gea_program-0.45.0}/gea/seismic_track.py +0 -0
  298. {gea_program-0.44.0 → gea_program-0.45.0}/gea/seismic_uncertainty.py +0 -0
  299. {gea_program-0.44.0 → gea_program-0.45.0}/gea/seismic_unlisted.py +0 -0
  300. {gea_program-0.44.0 → gea_program-0.45.0}/gea/sensor_swap.py +0 -0
  301. {gea_program-0.44.0 → gea_program-0.45.0}/gea/series_cache.py +0 -0
  302. {gea_program-0.44.0 → gea_program-0.45.0}/gea/service_life.py +0 -0
  303. {gea_program-0.44.0 → gea_program-0.45.0}/gea/setup_checklist.py +0 -0
  304. {gea_program-0.44.0 → gea_program-0.45.0}/gea/shapefile.py +0 -0
  305. {gea_program-0.44.0 → gea_program-0.45.0}/gea/shell.py +0 -0
  306. {gea_program-0.44.0 → gea_program-0.45.0}/gea/shut_in.py +0 -0
  307. {gea_program-0.44.0 → gea_program-0.45.0}/gea/simulation.py +0 -0
  308. {gea_program-0.44.0 → gea_program-0.45.0}/gea/site_map.py +0 -0
  309. {gea_program-0.44.0 → gea_program-0.45.0}/gea/sla_report.py +0 -0
  310. {gea_program-0.44.0 → gea_program-0.45.0}/gea/slip.py +0 -0
  311. {gea_program-0.44.0 → gea_program-0.45.0}/gea/soak.py +0 -0
  312. {gea_program-0.44.0 → gea_program-0.45.0}/gea/sra.py +0 -0
  313. {gea_program-0.44.0 → gea_program-0.45.0}/gea/store_forward.py +0 -0
  314. {gea_program-0.44.0 → gea_program-0.45.0}/gea/strata_join.py +0 -0
  315. {gea_program-0.44.0 → gea_program-0.45.0}/gea/structure.py +0 -0
  316. {gea_program-0.44.0 → gea_program-0.45.0}/gea/supervisor.py +0 -0
  317. {gea_program-0.44.0 → gea_program-0.45.0}/gea/survey_cmd.py +0 -0
  318. {gea_program-0.44.0 → gea_program-0.45.0}/gea/survey_view.py +0 -0
  319. {gea_program-0.44.0 → gea_program-0.45.0}/gea/telemetry.py +0 -0
  320. {gea_program-0.44.0 → gea_program-0.45.0}/gea/tool_catalog.py +0 -0
  321. {gea_program-0.44.0 → gea_program-0.45.0}/gea/tool_library.py +0 -0
  322. {gea_program-0.44.0 → gea_program-0.45.0}/gea/tool_taxonomy.py +0 -0
  323. {gea_program-0.44.0 → gea_program-0.45.0}/gea/transient.py +0 -0
  324. {gea_program-0.44.0 → gea_program-0.45.0}/gea/vibration.py +0 -0
  325. {gea_program-0.44.0 → gea_program-0.45.0}/gea/well3d.py +0 -0
  326. {gea_program-0.44.0 → gea_program-0.45.0}/gea/well_assembler.py +0 -0
  327. {gea_program-0.44.0 → gea_program-0.45.0}/gea/well_report.py +0 -0
  328. {gea_program-0.44.0 → gea_program-0.45.0}/gea/well_test_validation.py +0 -0
  329. {gea_program-0.44.0 → gea_program-0.45.0}/gea/wits0.py +0 -0
  330. {gea_program-0.44.0 → gea_program-0.45.0}/gea/witsml.py +0 -0
  331. {gea_program-0.44.0 → gea_program-0.45.0}/gea/workspace.py +0 -0
  332. {gea_program-0.44.0 → gea_program-0.45.0}/gea_program.egg-info/dependency_links.txt +0 -0
  333. {gea_program-0.44.0 → gea_program-0.45.0}/gea_program.egg-info/entry_points.txt +0 -0
  334. {gea_program-0.44.0 → gea_program-0.45.0}/gea_program.egg-info/requires.txt +0 -0
  335. {gea_program-0.44.0 → gea_program-0.45.0}/gea_program.egg-info/top_level.txt +0 -0
  336. {gea_program-0.44.0 → gea_program-0.45.0}/setup.cfg +0 -0
  337. {gea_program-0.44.0 → gea_program-0.45.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.44.0
3
+ Version: 0.45.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,21 @@ 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 lateral, the printable reports, the daily summary and the key
652
+
653
+ An earthquake near a well with a directional survey is measured from the
654
+ nearest point of its lateral, not from the pad - with the measured depth
655
+ there and the distance to the hypocentre - so a protocol radius is read
656
+ against the well bore a regulator means. Every client report is also
657
+ written as a PDF beside its HTML (the drawings redrawn, long tables
658
+ continued across pages) and kept in report history. Each morning the
659
+ program writes a one-page pad summary - what changed, what is due, what is
660
+ still missing - and sends it through the notification rules (`gea
661
+ pad-summary`). The dossier signing key can be retired and replaced: the old
662
+ key signs the hand-over, earlier dossiers still check out, and later ones
663
+ carry the rotation so a client can follow it (`gea dossier --rotate-key`).
664
+ `gea help pad-summary`.
665
+
651
666
  ### The earthquake watch and the alerts
652
667
 
653
668
  While a well operates, the program reads the USGS earthquake catalogue
@@ -609,6 +609,21 @@ 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 lateral, the printable reports, the daily summary and the key
613
+
614
+ An earthquake near a well with a directional survey is measured from the
615
+ nearest point of its lateral, not from the pad - with the measured depth
616
+ there and the distance to the hypocentre - so a protocol radius is read
617
+ against the well bore a regulator means. Every client report is also
618
+ written as a PDF beside its HTML (the drawings redrawn, long tables
619
+ continued across pages) and kept in report history. Each morning the
620
+ program writes a one-page pad summary - what changed, what is due, what is
621
+ still missing - and sends it through the notification rules (`gea
622
+ pad-summary`). The dossier signing key can be retired and replaced: the old
623
+ key signs the hand-over, earlier dossiers still check out, and later ones
624
+ carry the rotation so a client can follow it (`gea dossier --rotate-key`).
625
+ `gea help pad-summary`.
626
+
612
627
  ### The earthquake watch and the alerts
613
628
 
614
629
  While a well operates, the program reads the USGS earthquake catalogue
@@ -123,7 +123,7 @@ from .profile_catalog import (
123
123
  read_pangaea_txt,
124
124
  )
125
125
 
126
- __version__ = "0.44.0"
126
+ __version__ = "0.45.0"
127
127
  __all__ = [
128
128
  "aging_rate", "rate_psi_yr", "accuracy_psi",
129
129
  "Sensor", "SimulatorConfig", "DownholeEngine",
@@ -584,9 +584,21 @@ def main(argv=None) -> int:
584
584
  p_dz.add_argument("--sources", type=str, default="", help="with --verify: a folder holding the source records to check against their hashes")
585
585
  p_dz.add_argument("--key", action="store_true", help="print this site's signing key fingerprint")
586
586
  p_dz.add_argument("--list", action="store_true", help="list the dossiers this site has signed")
587
+ p_dz.add_argument("--rotate-key", action="store_true", help="retire this site's signing key and start a new one, signed over by the old (give --reason)")
588
+ p_dz.add_argument("--reason", type=str, default="", help="with --rotate-key: why the key is retired")
589
+ p_dz.add_argument("--key-history", action="store_true", help="the site's keys, current and retired, and the rotations between them")
587
590
  p_dz.add_argument("--actor", type=str, default="cli")
588
591
  p_dz.add_argument("--json", action="store_true")
589
592
 
593
+ p_ps = sub.add_parser("pad-summary", help="the daily pad summary: what changed since the last one, what is due, what is still missing - one page, kept in report history; the service writes it each day and sends it through the notification rules")
594
+ p_ps.add_argument("--workspace", type=str, required=True)
595
+ p_ps.add_argument("--send", action="store_true", help="also hand it to the notification rules (the event pad.summary)")
596
+ p_ps.add_argument("--at", type=str, default=None, help="the daily time the service writes it, HH:MM UTC")
597
+ p_ps.add_argument("--on", action="store_true", help="the service writes it each day")
598
+ p_ps.add_argument("--off", action="store_true", help="the service does not")
599
+ p_ps.add_argument("--actor", type=str, default="cli")
600
+ p_ps.add_argument("--json", action="store_true")
601
+
590
602
  p_rh = sub.add_parser("reports-history", help="every version of every client report this workspace has written, kept under reports/history; --prune keeps the newest N")
591
603
  p_rh.add_argument("--workspace", type=str, required=True)
592
604
  p_rh.add_argument("--family", type=str, default=None, help="e.g. wells/Well-1/well_report or pad_report")
@@ -2297,6 +2309,23 @@ def main(argv=None) -> int:
2297
2309
  k = _DZ.key_info(ws)
2298
2310
  print(f"Ed25519 {k['fingerprint']} (public key {k['public_key']}, made {k['created_utc']})" if k else "no signing key yet: the first dossier makes one")
2299
2311
  return 0
2312
+ if a.rotate_key:
2313
+ try:
2314
+ r_ = _DZ.rotate_key(ws, a.actor, a.reason)
2315
+ except ValueError as ex:
2316
+ raise SystemExit(f"dossier: {ex}")
2317
+ print(f"retired {r_['old']['fingerprint']}; the new key is {r_['new']['fingerprint']}")
2318
+ print(" the rotation is signed by both keys and rides in every later dossier; give the new fingerprint to your clients")
2319
+ return 0
2320
+ if a.key_history:
2321
+ h_ = _DZ.key_history(ws)
2322
+ if a.json:
2323
+ print(_json.dumps(h_, default=str)); return 0
2324
+ for k in h_["keys"]:
2325
+ print(f"{k['fingerprint']} made {k.get('created_utc') or '?'} " + (f"retired {k['retired_utc']} ({k.get('reason', '')}) -> {k.get('successor')}" if k.get("retired_utc") else "CURRENT"))
2326
+ if not h_["keys"]:
2327
+ print("no key history yet")
2328
+ return 0
2300
2329
  if a.list:
2301
2330
  for e in reversed(_DZ.ledger(ws)):
2302
2331
  print(f"{e['utc']} {e['dossier_id']} {e['display']:<24} {e['n_files']:>4} files {e['path']}")
@@ -2309,6 +2338,36 @@ def main(argv=None) -> int:
2309
2338
  print(f" {os.path.join(ws.reports_dir, e['path'])}")
2310
2339
  print(f" key fingerprint {e['fingerprint']}")
2311
2340
  return 0
2341
+ elif a.cmd == "pad-summary":
2342
+ import json as _json
2343
+ from . import pad_summary as _PS
2344
+ from .cli import _safe_stdout
2345
+ from .notify import Notifier
2346
+ from .workspace import Workspace, WorkspaceError
2347
+ _safe_stdout()
2348
+ try:
2349
+ ws = Workspace(a.workspace)
2350
+ except WorkspaceError as e:
2351
+ raise SystemExit(f"pad-summary: {e}")
2352
+ if a.at or a.on or a.off:
2353
+ try:
2354
+ s_ = _PS.set_settings(ws, a.actor, enabled=(True if a.on else False if a.off else None), at=a.at)
2355
+ except ValueError as e:
2356
+ raise SystemExit(f"pad-summary: {e}")
2357
+ print(f"daily pad summary {'on' if s_['enabled'] else 'off'} at {s_['at']} UTC, sent through the notification rules: {'yes' if s_['send'] else 'no'}")
2358
+ return 0
2359
+ n_ = None
2360
+ if a.send:
2361
+ n_ = Notifier(ws.path); n_.reload()
2362
+ r = _PS.write(ws, __version__, a.actor, n_, send=bool(a.send))
2363
+ if a.json:
2364
+ print(_json.dumps(r, default=str)); return 0
2365
+ print(f"wrote {r['report_id']} (since {r['since']}): {r['paths']['html']}")
2366
+ if r['paths'].get('pdf'):
2367
+ print(f" printable: {r['paths']['pdf']}")
2368
+ if a.send:
2369
+ print(f" sent: {', '.join(str(d['channel']) + (' ok' if d['ok'] else ' FAILED') for d in r['delivered']) or 'no notification rule for pad.summary'}")
2370
+ return 0
2312
2371
  elif a.cmd == "reports-history":
2313
2372
  import json as _json
2314
2373
  from . import report_history as _RH
@@ -7827,7 +7827,7 @@ def section_by_edr(tmp: str) -> None:
7827
7827
  "survey": r_svy.returncode == 0 and "stations: 835 depth 584.0 - 838.2 m (the log is indexed in feet; converted to metres for the survey)" in r_svy.stdout
7828
7828
  and "gamma -> GR" in r_svy.stdout and "NOT ESTIMATED: no density" in r_svy.stdout,
7829
7829
  "cli": r_self.returncode == 0 and "Total depth 2,750.0 ft MD" in r_self.stdout and r_file.returncode == 0 and Path(out, "md_tvd.csv").is_file()
7830
- and "written: drilling_record.md, drilling_record.html, drilling_record.json, gamma_log.las, md_tvd.csv" in r_file.stdout
7830
+ and "written: drilling_record.md, drilling_record.html, drilling_record.json, drilling_record.pdf, gamma_log.las, md_tvd.csv" in r_file.stdout
7831
7831
  and r_json.returncode == 0 and json.loads(r_json.stdout)["n_sections"] == 3 and r_bad.returncode != 0 and "not an EDR export" in (r_bad.stderr + r_bad.stdout)
7832
7832
  and r_none.returncode != 0 and "edr needs --file" in (r_none.stderr + r_none.stdout) and "<" not in (r_none.stderr + r_none.stdout).split("edr needs")[1],
7833
7833
  "ingest": r_hist.returncode == 1 and "Traceback" not in (r_hist.stderr + r_hist.stdout) and "which is an EDR export" in (r_hist.stderr + r_hist.stdout)
@@ -10331,6 +10331,285 @@ def section_cm_watch_and_alerts(tmp: str) -> None:
10331
10331
  + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
10332
10332
 
10333
10333
 
10334
+ def section_cn_four(tmp: str) -> None:
10335
+ """Section CN - the distance from the lateral, the printable PDF of every client report, the daily pad summary and
10336
+ the signing key's rotation: an earthquake measured from the nearest point of a surveyed lateral (in plan, with the
10337
+ measured depth there and the distance to the hypocentre) where the pad is kilometres off, a protocol level reached
10338
+ because of it, a well without a survey still measured from its pad, the watch circle reaching as far as the
10339
+ laterals; a PDF beside each report, read back page by page, kept in history, its tables continued across pages;
10340
+ the summary's three lists, its daily hour, its history and its delivery through a webhook rule; a key retired and
10341
+ a new one signed over by the old, earlier dossiers still valid, a dossier signed with the retired key afterwards
10342
+ not the site's, a lost key recorded unsigned; the API, the CLI, the tabs and the help pages."""
10343
+ import http.cookiejar
10344
+ import http.server
10345
+ import math as _m
10346
+ import subprocess as _sp
10347
+ import threading as _th
10348
+ import urllib.request
10349
+ import urllib.error
10350
+ import zipfile as _zf
10351
+ from datetime import datetime, timedelta, timezone
10352
+ from . import __version__
10353
+ from . import alerts as AL
10354
+ from . import client_reports as CR
10355
+ from . import dossier as DZ
10356
+ from . import edr as E
10357
+ from . import geodesy as GD
10358
+ from . import helplib as H
10359
+ from . import op_seismicity as OS
10360
+ from . import pad_summary as PS
10361
+ from . import pdf_report as PDF
10362
+ from . import report_history as RH
10363
+ from . import seismic_watch as SW
10364
+ from . import well3d as W
10365
+ from . import well_report as WR
10366
+ from .notify import Notifier
10367
+ from .workspace import Workspace
10368
+ from .service import Service, Users
10369
+ pkg = Path(__file__).parent
10370
+ d = Path(tmp, "cn"); d.mkdir()
10371
+ now = datetime.now(timezone.utc).replace(microsecond=0)
10372
+ FT = 0.3048
10373
+
10374
+ # -- CN1 the distance from the lateral ---------------------------------------------------------------------------
10375
+ ev = E.selftest_events(str(d))
10376
+ wsp = str(d / "ws"); ws = Workspace.create(wsp, "Pad CN", actor="tester")
10377
+ wl = ws.add_well_file(ev["path"], display="LAT-1H", actor="tester"); wv = ws.add_well_file(ev["path"], display="VERT-2", actor="tester")
10378
+ la0, lo0 = 31.40, -104.00
10379
+ ws.set_well(wl["id"], surface={"lat": la0, "lon": lo0, "datum": "WGS84", "elevation": {"ft": 3000.0, "datum": "KB", "source": "test plat"}}, actor="tester")
10380
+ ws.set_well(wv["id"], surface={"lat": 31.30, "lon": -104.00, "datum": "WGS84"}, actor="tester")
10381
+ # vertical to 8,000 ft, a 90-degree build over 1,000 ft (radius 636.62 ft), then 10,000 ft due east, true north
10382
+ W.attach_trajectory(ws, wl["id"], "s.csv", b"MD (ft),Inc (deg),Azi (deg)\n0,0,90\n8000,0,90\n9000,90,90\n19000,90,90\n", "tester", "true", "ft", None, None, None, "test survey")
10383
+ R_ = 1000.0 / (_m.pi / 2)
10384
+ fr = GD._tm(la0, lo0, 1.0, 0.0, 0.0, "WGS84")
10385
+ z_lat = (8000.0 + R_) * FT - 3000.0 * FT # the lateral's depth below sea level, m
10386
+ pA = GD.unproject(5000.0 * FT, 500.0, fr) # 500 m north of the lateral at 5,000 ft east
10387
+ pB = GD.unproject((10000.0 + R_) * FT + 300.0, 400.0, fr) # 500 m beyond the toe, north-east
10388
+ t_ev = (now - timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%S")
10389
+ OS.save_operation(ws, "tester", wl["id"], "completion", (now - timedelta(days=3)).strftime("%Y-%m-%d"), None, "stage log")
10390
+ sc = OS.workspace_scene(ws)
10391
+ sc["events"] = [{"event_id": "A", "time": t_ev, "magnitude": 2.6, "mag_type": "ml", "lat": pA[0], "lon": pA[1], "depth_km": 2.0},
10392
+ {"event_id": "B", "time": t_ev, "magnitude": 3.1, "mag_type": "ml", "lat": pB[0], "lon": pB[1], "depth_km": None},
10393
+ {"event_id": "C", "time": t_ev, "magnitude": 1.2, "mag_type": "ml", "lat": 31.305, "lon": -104.0, "depth_km": 4.0}]
10394
+ sc["catalog"] = {"start": (now - timedelta(days=3650)).strftime("%Y-%m-%d"), "end": now.strftime("%Y-%m-%d"), "min_magnitude": 1.0}
10395
+ sc["settings"] = {**sc["settings"], "protocol": "oklahoma-occ-2016", "radius_km": 2.0}
10396
+ res = OS.analyse(sc)
10397
+ rw = {w_["display"]: w_ for w_ in res["wells"]}
10398
+ eA = next(e for e in rw["LAT-1H"]["events"] if e["event_id"] == "A")
10399
+ eB = next(e for e in rw["LAT-1H"]["events"] if e["event_id"] == "B")
10400
+ eC = next(e for e in rw["VERT-2"]["events"] if e["event_id"] == "C")
10401
+ hypo = _m.hypot(500.0, 2000.0 - z_lat) / 1000.0
10402
+ act = SW.active(ws, now)
10403
+ reach = OS.lateral_of(ws, ws.well(wl["id"]), la0, lo0)["reach_km"]
10404
+ pg = OS.page(ws, "workspace")
10405
+ c1 = {
10406
+ "plan": abs(eA["distance_km"] - 0.5) < 0.005 and eA["distance_basis"] == "lateral" and abs(eA["nearest_md_ft"] - (9000 + 5000 - R_)) < 30
10407
+ and abs(eA["distance_surface_km"] - _m.hypot(1.524, 0.5)) < 0.01 and abs(eA["hypocentral_km"] - hypo) < 0.02,
10408
+ "toe": abs(eB["distance_km"] - 0.5) < 0.005 and abs(eB["nearest_md_ft"] - 19000) < 1 and eB["distance_surface_km"] > 3.4 and "hypocentral_km" not in eB,
10409
+ "protocol": eB["within_radius"] and (eB.get("protocol") or {}).get("highest", {}).get("m") == 3.0 and eA["protocol"]["highest"]["m"] == 2.5,
10410
+ "surface": eC["distance_basis"] == "surface" and rw["VERT-2"]["distance_from"]["basis"] == "surface" and abs(eC["distance_km"] - 0.556) < 0.01,
10411
+ "basis": rw["LAT-1H"]["distance_from"]["basis"] == "lateral" and "19,000 ft MD" in rw["LAT-1H"]["distance_from"]["detail"] and abs(reach - ((10000 + R_) * FT) / 1000) < 0.01,
10412
+ "circle": act["circle"] is not None and act["circle"]["radius_km"] >= reach + 20.0 - 0.01,
10413
+ "text": "lateral, nearest at" in OS.text(res) and "distances from the nearest point" in OS.text(res),
10414
+ "page": "lateral" not in [w_.get("lateral") for w_ in pg["wells"]] and all("lateral" not in w_ for w_ in pg["wells"]) and pg["query"]["radius_km"] > 20 + reach - 0.1,
10415
+ }
10416
+ failed1 = [k for k, v in c1.items() if not v]
10417
+ ok(not failed1,
10418
+ "CN1 the distance from the lateral: an earthquake 500 m off a surveyed lateral and 1.6 km from its pad is 0.50 km away, at the "
10419
+ "measured depth of the nearest point, with the distance to its hypocentre; one beyond the toe reaches a protocol level the pad "
10420
+ "distance would not; a well without a survey still measured from its pad; the watch circle and the query reach the laterals"
10421
+ + (f" [failed: {', '.join(failed1)}]" if failed1 else ""))
10422
+
10423
+ # -- CN2 the printable PDF -----------------------------------------------------------------------------------------
10424
+ rw1 = WR.write_well(ws, wl["id"], __version__, actor="tester")
10425
+ rp1 = WR.write_pad(ws, __version__, actor="tester")
10426
+ pdfs = [rw1["paths"].get("pdf"), rp1["paths"].get("pdf")]
10427
+ rd = [PDF.read(Path(p).read_bytes()) for p in pdfs if p]
10428
+ long_doc = CR.Document("A long table ≥ two pages", "TEST-1", [["Report ID", "TEST-1"], ["Result", "NO CHANGE"]],
10429
+ [CR.Section("1", "Rows", ["° ± – — ≤ x" * 3], [CR.Table(["Row", "A long cell"], [[i, "word " * (i % 40) + "unbrokenwordthatiswiderthanthecolumnitsitsin" * (1 if i == 7 else 0)] for i in range(160)], "the caption")])], footer="footer line")
10430
+ b1, b2 = PDF.render(long_doc), PDF.render(long_doc)
10431
+ rl = PDF.read(b1)
10432
+ hist_pdf = [f_["name"] for e_ in RH.entries(ws.reports_dir) for f_ in e_["files"] if f_["name"].endswith(".pdf")]
10433
+ c2 = {
10434
+ "written": len(rd) == 2 and all(r_["ok"] for r_ in rd) and CR.LAST_PDF_ERROR is None
10435
+ and all(Path(p).with_suffix(".html").is_file() for p in pdfs),
10436
+ "content": rw1["report_id"] in rd[0]["text"][0] and "page 1 of" in rd[0]["text"][0] and "LAT-1H" in "\n".join(rd[0]["text"])
10437
+ and any("plan view" in t_ for t_ in rd[0]["text"]) and rp1["report_id"] in rd[1]["text"][0],
10438
+ "fonts": set(rd[0]["fonts"]) == {"Helvetica", "Helvetica-Bold", "Helvetica-Oblique", "Helvetica-BoldOblique"},
10439
+ "pages": rl["ok"] and rl["pages"] >= 3 and all("A long cell" in t_ for t_ in rl["text"][:-1]) and f"page {rl['pages']} of {rl['pages']}" in rl["text"][-1]
10440
+ and ">=" in rl["text"][0] and "°" in rl["text"][0] and "footer line" in rl["text"][-1],
10441
+ "same": b1 == b2,
10442
+ "history": "well_report.pdf" in hist_pdf and "pad_report.pdf" in hist_pdf,
10443
+ "links": "['pdf','html','md','json','csv']" in (pkg / "web" / "app.html").read_text(encoding="utf-8"),
10444
+ }
10445
+ failed2 = [k for k, v in c2.items() if not v]
10446
+ ok(not failed2,
10447
+ "CN2 the printable PDF: the well and pad reports each written as a PDF beside the HTML and read back - the report number, "
10448
+ "'page 1 of', the plan view's own labels redrawn, the four base fonts; a 160-row table continued across pages with its "
10449
+ "header on each, a word wider than its column broken, the symbols carried; the same bytes for the same report; kept in history"
10450
+ + (f" [failed: {', '.join(failed2)}]" if failed2 else ""))
10451
+
10452
+ # -- CN3 the daily pad summary -------------------------------------------------------------------------------------
10453
+ hooks = []
10454
+
10455
+ class Hook(http.server.BaseHTTPRequestHandler):
10456
+ def log_message(self, *a):
10457
+ pass
10458
+
10459
+ def do_POST(self):
10460
+ n = int(self.headers.get("Content-Length") or 0)
10461
+ hooks.append(json.loads(self.rfile.read(n) or b"{}"))
10462
+ self.send_response(200); self.end_headers()
10463
+ srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Hook)
10464
+ _th.Thread(target=srv.serve_forever, daemon=True).start()
10465
+ Path(wsp, "config", "notifications.json").write_text(json.dumps({"channels": [{"name": "hook", "kind": "webhook", "url": f"http://127.0.0.1:{srv.server_address[1]}/hook"}],
10466
+ "rules": [{"event": "pad.summary", "channels": ["hook"]}], "quiet_s": 0}), encoding="utf-8")
10467
+ nt = Notifier(wsp); nt.reload()
10468
+ AL.raise_alert(ws, None, {"event": "seismic.event", "key": "seismic.event:A", "well": "LAT-1H", "detail": "M 2.6 0.5 km from the lateral of LAT-1H"}, now)
10469
+ OS.save_operation(ws, "tester", wv["id"], "disposal", (now - timedelta(days=1)).strftime("%Y-%m-%d"), None, "an honest permit")
10470
+ OS.save_operation(ws, "tester", wv["id"], "injection", (now + timedelta(days=2)).strftime("%Y-%m-%d"), None, "permit 2")
10471
+ g = PS.gather(ws, now + timedelta(minutes=1), now - timedelta(hours=1))
10472
+ t0 = now.replace(hour=10, minute=59, second=0) if now.hour < 23 else now.replace(hour=10, minute=59, second=0)
10473
+ due_before = PS.due_now(ws, t0)
10474
+ s1 = PS.write(ws, __version__, "tester", nt, now + timedelta(minutes=2))
10475
+ due_after = PS.due_now(ws, (now + timedelta(minutes=2)).replace(hour=23, minute=0))
10476
+ nxt = (now + timedelta(days=1)).replace(hour=11, minute=1, second=0)
10477
+ due_next = PS.due_now(ws, nxt)
10478
+ ws.audit("tester", "test.after", {})
10479
+ s2 = PS.write(ws, __version__, "tester", nt, now + timedelta(minutes=5), send=False)
10480
+ srv.shutdown()
10481
+ vers = [e_ for e_ in RH.entries(ws.reports_dir) if e_["family"] == "daily/pad_summary"]
10482
+ html_ = Path(wsp, "reports", "daily", "pad_summary.html").read_text(encoding="utf-8")
10483
+ c3 = {
10484
+ "lists": any(c_["action"] == "seismicity.operation" or c_["action"].startswith("seismicity.") for c_ in g["changed"]) and len(g["alerts"]) == 1
10485
+ and any(x["status"] == "UNDER WAY" and "completion" in x["item"] for x in g["due"]) and any(x["status"] == "STARTS SOON" for x in g["due"])
10486
+ and any(x["item"] == "the earthquake watch" for x in g["due"]) and any(x["item"] == "the backup" and x["status"] == "NEVER" for x in g["due"])
10487
+ and any(m_["level"] == "required" for m_ in g["missing"]),
10488
+ "clean": "honest" not in html_.lower() and "(see the audit log)" in html_,
10489
+ "files": all(Path(wsp, "reports", "daily", "pad_summary." + x).is_file() for x in ("pdf", "html", "md", "json")) and PDF.read(Path(wsp, "reports", "daily", "pad_summary.pdf").read_bytes())["ok"],
10490
+ "history": len(vers) == 2 and s2["since"] == PS._utc(now + timedelta(minutes=2)),
10491
+ "hour": (not due_before or now.hour >= 11) and not due_after and due_next,
10492
+ "sent": len(hooks) == 1 and hooks[0].get("event") == "pad.summary" and "What is due" in hooks[0].get("text", "") and s1["delivered"] == [{"channel": "hook", "ok": True}] and s2["delivered"] == [],
10493
+ "event": "pad.summary" in __import__("gea.notify", fromlist=["EVENTS"]).EVENTS,
10494
+ }
10495
+ failed3 = [k for k, v in c3.items() if not v]
10496
+ ok(not failed3,
10497
+ "CN3 the daily pad summary: the actions, alerts and new versions since the last, what is under way, starting soon or due, the "
10498
+ "watch and the backup, the checklist's missing items; a typed note a client report may not carry left out; PDF, HTML, markdown "
10499
+ "and JSON with each day's version kept; due once a day from its hour; delivered through a webhook rule for pad.summary"
10500
+ + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
10501
+
10502
+ # -- CN4 the key's rotation ----------------------------------------------------------------------------------------
10503
+ e1 = DZ.build(ws, "tester", __version__, wl["id"])
10504
+ k1 = DZ.key_info(ws)
10505
+ old_file = Path(DZ.key_path(ws)).read_text(encoding="utf-8")
10506
+ try:
10507
+ DZ.rotate_key(ws, "tester", " ")
10508
+ empty_refused = False
10509
+ except ValueError:
10510
+ empty_refused = True
10511
+ time.sleep(1.1)
10512
+ rot = DZ.rotate_key(ws, "tester", "scheduled change")
10513
+ new_file = Path(DZ.key_path(ws)).read_text(encoding="utf-8")
10514
+ time.sleep(1.1)
10515
+ e2 = DZ.build(ws, "tester", __version__, wl["id"])
10516
+ v1 = DZ.verify(os.path.join(ws.reports_dir, e1["path"]), ws)
10517
+ v2 = DZ.verify(os.path.join(ws.reports_dir, e2["path"]), ws)
10518
+ Path(DZ.key_path(ws)).write_text(old_file, encoding="utf-8") # someone still holding the retired key signs with it
10519
+ time.sleep(1.1)
10520
+ e_bad = DZ.build(ws, "tester", __version__, wl["id"])
10521
+ Path(DZ.key_path(ws)).write_text(new_file, encoding="utf-8")
10522
+ v_bad = DZ.verify(os.path.join(ws.reports_dir, e_bad["path"]), ws)
10523
+ x = d / "vx"; x.mkdir()
10524
+ with _zf.ZipFile(os.path.join(ws.reports_dir, e2["path"])) as z_:
10525
+ z_.extract("verify_dossier.py", x)
10526
+ rots_in = json.loads(z_.read("keys/rotations.json"))
10527
+ sa = _sp.run([sys.executable, "-I", str(x / "verify_dossier.py"), os.path.join(ws.reports_dir, e2["path"])], capture_output=True, text=True)
10528
+ hist_txt = Path(wsp, DZ.KEY_HISTORY).read_text(encoding="utf-8")
10529
+ c4 = {
10530
+ "rotated": empty_refused and rot["old"]["fingerprint"] == k1["fingerprint"] and rot["new"]["fingerprint"] != k1["fingerprint"]
10531
+ and bool(rot["rotation"]["old_signature"]) and json.loads(old_file)["seed"] not in new_file + hist_txt and '"seed"' not in hist_txt,
10532
+ "old_valid": v1["status"] == "VALID" and any("a former key of this site" in c_["detail"] and c_["ok"] for c_ in v1["workspace_checks"]),
10533
+ "new_valid": v2["status"] == "VALID" and any(c_["check"] == "the key rotations verify" and c_["ok"] for c_ in v2["checks"])
10534
+ and v2["key_chain"][0]["old"] == k1["fingerprint"] and v2["key_chain"][0]["signed_by_old"] is True and len(rots_in["rotations"]) == 1,
10535
+ "after_retired": v_bad["status"] == "INVALID" and any("not the site's" in c_["detail"] for c_ in v_bad["workspace_checks"])
10536
+ and any(c_["check"] == "the key rotations verify" and not c_["ok"] and "does not end at the key" in c_["detail"] for c_ in v_bad["checks"]),
10537
+ "standalone": sa.returncode == 0 and "signed by both keys" in sa.stdout and k1["fingerprint"] in sa.stdout,
10538
+ "page": [r_["signed_by_old"] for r_ in DZ.page(ws)["key_history"]["rotations"]] == [True] and len(DZ.page(ws)["key_history"]["keys"]) == 2,
10539
+ }
10540
+ os.remove(DZ.key_path(ws)); time.sleep(1.1)
10541
+ e3 = DZ.build(ws, "tester", __version__, wl["id"])
10542
+ v3 = DZ.verify(os.path.join(ws.reports_dir, e3["path"]), ws)
10543
+ c4["lost"] = (v3["status"] == "VALID" and len(v3["key_chain"]) == 2 and v3["key_chain"][1]["signed_by_old"] is None
10544
+ and "not signed by the retired key" in next(c_["detail"] for c_ in v3["checks"] if c_["check"] == "the key rotations verify"))
10545
+ failed4 = [k for k, v in c4.items() if not v]
10546
+ ok(not failed4,
10547
+ "CN4 the key's rotation: a reason required; the old key signs the hand-over and its seed is gone; a dossier it signed before still "
10548
+ "VALID here as a former key's, one signed after with it not the site's (its own rotations do not end at its key); the next dossier carries the rotation, which its own "
10549
+ "checker verifies with both signatures; a lost key file recorded as a rotation the old key did not sign"
10550
+ + (f" [failed: {', '.join(failed4)}]" if failed4 else ""))
10551
+
10552
+ # -- CN5 the API, the CLI, the tabs, the help ------------------------------------------------------------------------
10553
+ Users(str(Path(wsp, "users.json"))).add("op", "op-pass-123", "operator"); Users(str(Path(wsp, "users.json"))).add("ad", "admin-pass-12", "admin")
10554
+ svc = Service(wsp, port=0, scheduler=False).start()
10555
+ base_url = svc.url.rstrip("/")
10556
+
10557
+ def opener(name, pw_):
10558
+ o = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
10559
+ rq = urllib.request.Request(base_url + "/api/login", method="POST", data=json.dumps({"name": name, "password": pw_}).encode())
10560
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1"); o.open(rq, timeout=30)
10561
+ return o
10562
+
10563
+ def call(o, method, path, body=None):
10564
+ rq = urllib.request.Request(base_url + path, method=method, data=(json.dumps(body).encode() if body is not None else None))
10565
+ if method == "POST":
10566
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1")
10567
+ try:
10568
+ with o.open(rq, timeout=120) as rr_:
10569
+ ct = rr_.headers.get("Content-Type", "")
10570
+ b_ = rr_.read()
10571
+ return rr_.status, (json.loads(b_) if "json" in ct else {"_ct": ct, "_len": len(b_)})
10572
+ except urllib.error.HTTPError as e:
10573
+ return e.code, json.loads(e.read())
10574
+ try:
10575
+ O, A = opener("op", "op-pass-123"), opener("ad", "admin-pass-12")
10576
+ s_g, gj = call(O, "GET", "/api/pad-summary")
10577
+ s_w, wj = call(O, "POST", "/api/pad-summary", {})
10578
+ s_so, _ = call(O, "POST", "/api/pad-summary/settings", {"at": "12:30"})
10579
+ s_sa, sj = call(A, "POST", "/api/pad-summary/settings", {"at": "12:30", "enabled": True})
10580
+ s_sb, _ = call(A, "POST", "/api/pad-summary/settings", {"at": "25:00"})
10581
+ s_ro, _ = call(O, "POST", "/api/dossier/rotate", {"reason": "x"})
10582
+ s_re, _ = call(A, "POST", "/api/dossier/rotate", {"reason": ""})
10583
+ s_ra, raj = call(A, "POST", "/api/dossier/rotate", {"reason": "API test"})
10584
+ s_dz, dzj = call(O, "GET", "/api/dossier")
10585
+ s_pdf, pdj = call(O, "GET", "/reports/daily/pad_summary.pdf")
10586
+ finally:
10587
+ svc.stop()
10588
+ cli_ps = _sp.run([sys.executable, "-m", "gea", "pad-summary", "--workspace", wsp], capture_output=True, text=True, cwd=str(pkg.parent))
10589
+ cli_kh = _sp.run([sys.executable, "-m", "gea", "dossier", "--workspace", wsp, "--key-history"], capture_output=True, text=True, cwd=str(pkg.parent))
10590
+ cli_rk = _sp.run([sys.executable, "-m", "gea", "dossier", "--workspace", wsp, "--rotate-key"], capture_output=True, text=True, cwd=str(pkg.parent))
10591
+ page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
10592
+ service_src = (pkg / "service.py").read_text(encoding="utf-8")
10593
+ c5 = {
10594
+ "api": s_g == 200 and gj["settings"]["at"] == "11:00" and s_w == 200 and wj["report_id"].startswith("DAY-") and s_so == 403 and s_sa == 200 and sj["at"] == "12:30"
10595
+ and s_sb == 400 and s_ro == 403 and s_re == 400 and s_ra == 200 and raj["signed_by_old"] and s_dz == 200 and len(dzj["key_history"]["rotations"]) == 3
10596
+ and s_pdf == 200 and pdj.get("_ct") == "application/pdf" and pdj["_len"] > 1000,
10597
+ "cli": cli_ps.returncode == 0 and "printable:" in cli_ps.stdout and cli_kh.returncode == 0 and "CURRENT" in cli_kh.stdout and "retired" in cli_kh.stdout
10598
+ and cli_rk.returncode != 0 and "say why" in cli_rk.stderr,
10599
+ "poller": "_PS.tick(self)" in service_src,
10600
+ "page": all(x_ in page_src for x_ in ("psCard", "Daily pad summary", "/api/pad-summary", "dzRot", "/api/dossier/rotate", "Key history", "Distances from")),
10601
+ "help": all(any(t[0] == k for t in H.INDEX) and not H.check(k) for k in ("pad-summary", "dossier", "opseis", "report-history"))
10602
+ and "Changing the key" in (pkg / "help" / "dossier.md").read_text(encoding="utf-8") and "lateral" in (pkg / "help" / "opseis.md").read_text(encoding="utf-8")
10603
+ and "printable copy" in (pkg / "help" / "report-history.md").read_text(encoding="utf-8"),
10604
+ }
10605
+ failed5 = [k for k, v in c5.items() if not v]
10606
+ ok(not failed5,
10607
+ "CN5 on the API (anyone signed in reads the summary's settings; an operator writes one; only an administrator sets its hour or "
10608
+ "rotates the key, with a reason; the PDF served as a PDF), the CLI (the summary written, the key history, a rotation without a "
10609
+ "reason refused), the service's poller writing the summary, the cards on the Reports tab and the help pages"
10610
+ + (f" [failed: {', '.join(failed5)}]" if failed5 else ""))
10611
+
10612
+
10334
10613
  def main() -> int:
10335
10614
  print("GEA-Program - ACCEPTANCE SUITE (the product gate)")
10336
10615
  # a handle a job subprocess still holds at the end must not turn a finished gate into a traceback on Windows:
@@ -10425,6 +10704,7 @@ def main() -> int:
10425
10704
  section_ck_operations_seismicity(tmp)
10426
10705
  section_cl_history_and_dossier(tmp)
10427
10706
  section_cm_watch_and_alerts(tmp)
10707
+ section_cn_four(tmp)
10428
10708
  if _FAILS:
10429
10709
  print(f"[ACCEPTANCE] {len(_FAILS)} FAILURES ({_PASS} passed):")
10430
10710
  for f in _FAILS:
@@ -1889,8 +1889,11 @@ def guard_text(text: str, what: str = 'a client deliverable') -> str:
1889
1889
  return text
1890
1890
 
1891
1891
 
1892
+ LAST_PDF_ERROR: Optional[str] = None
1893
+
1894
+
1892
1895
  def write(doc: Document, out_dir: str, basename: str = 'gauge_drift_report') -> dict:
1893
- """Write markdown, HTML, the machine JSON, the records CSV and the tag
1896
+ """Write markdown, HTML, the printable PDF (`pdf_report`), the machine JSON, the records CSV and the tag
1894
1897
  catalogue CSV. Returns the paths. Raises if the rendered report contains
1895
1898
  any internal-register term."""
1896
1899
  os.makedirs(out_dir, exist_ok=True)
@@ -1908,6 +1911,17 @@ def write(doc: Document, out_dir: str, basename: str = 'gauge_drift_report') ->
1908
1911
  f.write(md)
1909
1912
  with open(paths['html'], 'w', encoding='utf-8', newline='') as f:
1910
1913
  f.write(ht)
1914
+ global LAST_PDF_ERROR
1915
+ try: # the printable copy, from the same document
1916
+ from . import pdf_report as _PDF
1917
+ pdf = _PDF.render(doc)
1918
+ paths['pdf'] = os.path.join(out_dir, basename + '.pdf')
1919
+ with open(paths['pdf'], 'wb') as f:
1920
+ f.write(pdf)
1921
+ LAST_PDF_ERROR = None
1922
+ except Exception as e: # a layout fault costs the printable copy, never the report
1923
+ paths.pop('pdf', None)
1924
+ LAST_PDF_ERROR = f'{basename}: {type(e).__name__}: {e}'
1911
1925
  for s in doc.sections:
1912
1926
  for fg in s.figures:
1913
1927
  fp = os.path.join(out_dir, fg.name + '.svg')