gea-program 0.17.0__tar.gz → 0.18.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 (245) hide show
  1. {gea_program-0.17.0/gea_program.egg-info → gea_program-0.18.0}/PKG-INFO +10 -2
  2. {gea_program-0.17.0 → gea_program-0.18.0}/README.md +9 -1
  3. {gea_program-0.17.0 → gea_program-0.18.0}/gea/__init__.py +1 -1
  4. {gea_program-0.17.0 → gea_program-0.18.0}/gea/__main__.py +28 -0
  5. {gea_program-0.17.0 → gea_program-0.18.0}/gea/acceptance_tests.py +154 -0
  6. gea_program-0.18.0/gea/backup.py +262 -0
  7. {gea_program-0.17.0 → gea_program-0.18.0}/gea/doctor.py +16 -1
  8. gea_program-0.18.0/gea/help/backup.md +11 -0
  9. {gea_program-0.17.0 → gea_program-0.18.0}/gea/helplib.py +1 -0
  10. {gea_program-0.17.0 → gea_program-0.18.0}/gea/service.py +60 -1
  11. {gea_program-0.17.0 → gea_program-0.18.0}/gea/web/app.html +12 -0
  12. {gea_program-0.17.0 → gea_program-0.18.0/gea_program.egg-info}/PKG-INFO +10 -2
  13. {gea_program-0.17.0 → gea_program-0.18.0}/gea_program.egg-info/SOURCES.txt +2 -0
  14. {gea_program-0.17.0 → gea_program-0.18.0}/pyproject.toml +1 -1
  15. {gea_program-0.17.0 → gea_program-0.18.0}/LICENSE +0 -0
  16. {gea_program-0.17.0 → gea_program-0.18.0}/gea/BENCH_TEST_PROTOCOL.md +0 -0
  17. {gea_program-0.17.0 → gea_program-0.18.0}/gea/accuracy_statement.py +0 -0
  18. {gea_program-0.17.0 → gea_program-0.18.0}/gea/alarm_engine.py +0 -0
  19. {gea_program-0.17.0 → gea_program-0.18.0}/gea/bench.py +0 -0
  20. {gea_program-0.17.0 → gea_program-0.18.0}/gea/blind_harness.py +0 -0
  21. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/acex_lomonosov_age_depth_model.provenance.json +0 -0
  22. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/acex_lomonosov_age_depth_model.txt +0 -0
  23. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/agassiz77_canada_temperature.csv +0 -0
  24. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/agassiz77_canada_temperature.provenance.json +0 -0
  25. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/barbados_110_consolidation.provenance.json +0 -0
  26. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/barbados_110_consolidation.txt +0 -0
  27. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/bengal_u1452_grain_size.provenance.json +0 -0
  28. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/bengal_u1452_grain_size.txt +0 -0
  29. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/blake_164_methane_isotopes.provenance.json +0 -0
  30. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/blake_164_methane_isotopes.txt +0 -0
  31. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +0 -0
  32. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.txt +0 -0
  33. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/collingwood_1_28_ks_complete.las +0 -0
  34. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/collingwood_1_28_ks_complete.provenance.json +0 -0
  35. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/costa_rica_odp_friction_envelope.provenance.json +0 -0
  36. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/costa_rica_odp_friction_envelope.txt +0 -0
  37. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +0 -0
  38. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +0 -0
  39. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/dsdp_504b_physical_properties.provenance.json +0 -0
  40. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/dsdp_504b_physical_properties.txt +0 -0
  41. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/dsdp_504b_sound_velocity.provenance.json +0 -0
  42. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/dsdp_504b_sound_velocity.txt +0 -0
  43. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/elgygytgyn_5011_turbidites.provenance.json +0 -0
  44. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/elgygytgyn_5011_turbidites.txt +0 -0
  45. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/epica_domec_co2_800kyr.provenance.json +0 -0
  46. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/epica_domec_co2_800kyr.txt +0 -0
  47. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/fram_909_organic_petrography.provenance.json +0 -0
  48. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/fram_909_organic_petrography.txt +0 -0
  49. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/gbr_325_coral_uth_ages.provenance.json +0 -0
  50. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/gbr_325_coral_uth_ages.txt +0 -0
  51. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/gisp2_greenland_temperature.csv +0 -0
  52. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/gisp2_greenland_temperature.provenance.json +0 -0
  53. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/gom_308_t2p_insitu.provenance.json +0 -0
  54. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/gom_308_t2p_insitu.txt +0 -0
  55. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/guaymas_385_dom_d13c.provenance.json +0 -0
  56. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/guaymas_385_dom_d13c.txt +0 -0
  57. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +0 -0
  58. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/hikurangi_u1520_friction_insitu.txt +0 -0
  59. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +0 -0
  60. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +0 -0
  61. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/iodp_u1324_pore_pressure.provenance.json +0 -0
  62. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/iodp_u1324_pore_pressure.txt +0 -0
  63. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/jfast_c0019_slow_slip_events.provenance.json +0 -0
  64. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/jfast_c0019_slow_slip_events.txt +0 -0
  65. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/kennetcook_2_p129_excerpt.las +0 -0
  66. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/kennetcook_2_p129_excerpt.provenance.json +0 -0
  67. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ktb_hb_bhgm_density.dat +0 -0
  68. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ktb_hb_bhgm_density.provenance.json +0 -0
  69. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +0 -0
  70. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ktb_hb_complog_6020_excerpt.txt +0 -0
  71. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ktb_hb_hlog246_temperature.dat +0 -0
  72. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ktb_hb_hlog246_temperature.provenance.json +0 -0
  73. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ktb_hb_rockmech_compress.dat +0 -0
  74. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ktb_hb_rockmech_compress.provenance.json +0 -0
  75. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +0 -0
  76. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +0 -0
  77. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ktb_vb_rockmech_compress.dat +0 -0
  78. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ktb_vb_rockmech_compress.provenance.json +0 -0
  79. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ktb_vb_vlog251_temperature.dat +0 -0
  80. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ktb_vb_vlog251_temperature.provenance.json +0 -0
  81. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/l06_06_nl_survey.csv +0 -0
  82. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/l06_06_nl_survey.provenance.json +0 -0
  83. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/l07_01_nl_excerpt.las +0 -0
  84. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/l07_01_nl_excerpt.provenance.json +0 -0
  85. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +0 -0
  86. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/mariana_1200_serpentinite_geochem.txt +0 -0
  87. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/med_160_sapropels.provenance.json +0 -0
  88. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/med_160_sapropels.txt +0 -0
  89. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/nankai_megasplay_shear_strength.provenance.json +0 -0
  90. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/nankai_megasplay_shear_strength.txt +0 -0
  91. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/odp_1027b_thermal_conductivity.provenance.json +0 -0
  92. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/odp_1027b_thermal_conductivity.txt +0 -0
  93. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/odp_1027c_cork_temperature.provenance.json +0 -0
  94. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/odp_1027c_cork_temperature.txt +0 -0
  95. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/odp_1165b_thermal_conductivity.provenance.json +0 -0
  96. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/odp_1165b_thermal_conductivity.txt +0 -0
  97. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +0 -0
  98. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.txt +0 -0
  99. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +0 -0
  100. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/odp_504b_dike_elastic_moduli.txt +0 -0
  101. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +0 -0
  102. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/odp_504b_leg137_borehole_fluids.txt +0 -0
  103. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +0 -0
  104. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.txt +0 -0
  105. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/peru_201_sulfate_reduction.provenance.json +0 -0
  106. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/peru_201_sulfate_reduction.txt +0 -0
  107. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/scorpio_e1_sa_excerpt.las +0 -0
  108. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/scorpio_e1_sa_excerpt.provenance.json +0 -0
  109. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/sumatra_362_cohesion.provenance.json +0 -0
  110. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/sumatra_362_cohesion.txt +0 -0
  111. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/university_6_17_no1_tx_excerpt.las +0 -0
  112. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +0 -0
  113. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ursa_308_xrd_mineralogy.provenance.json +0 -0
  114. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/ursa_308_xrd_mineralogy.txt +0 -0
  115. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/volve_15_9_19_sr_excerpt.las +0 -0
  116. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +0 -0
  117. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/volve_15_9_19a_core_excerpt.csv +0 -0
  118. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +0 -0
  119. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/volve_f12_f14_production_excerpt.csv +0 -0
  120. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/volve_f12_f14_production_excerpt.provenance.json +0 -0
  121. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/walvis_208_petm_carbonate.provenance.json +0 -0
  122. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/walvis_208_petm_carbonate.txt +0 -0
  123. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/woodlark_1109_rock_eval.provenance.json +0 -0
  124. {gea_program-0.17.0 → gea_program-0.18.0}/gea/catalog/woodlark_1109_rock_eval.txt +0 -0
  125. {gea_program-0.17.0 → gea_program-0.18.0}/gea/certificates.py +0 -0
  126. {gea_program-0.17.0 → gea_program-0.18.0}/gea/cli.py +0 -0
  127. {gea_program-0.17.0 → gea_program-0.18.0}/gea/client_reports.py +0 -0
  128. {gea_program-0.17.0 → gea_program-0.18.0}/gea/config_versioning.py +0 -0
  129. {gea_program-0.17.0 → gea_program-0.18.0}/gea/conformance.py +0 -0
  130. {gea_program-0.17.0 → gea_program-0.18.0}/gea/correlation.py +0 -0
  131. {gea_program-0.17.0 → gea_program-0.18.0}/gea/dashboard.py +0 -0
  132. {gea_program-0.17.0 → gea_program-0.18.0}/gea/deviation.py +0 -0
  133. {gea_program-0.17.0 → gea_program-0.18.0}/gea/downhole_engine.py +0 -0
  134. {gea_program-0.17.0 → gea_program-0.18.0}/gea/drift_monitor.py +0 -0
  135. {gea_program-0.17.0 → gea_program-0.18.0}/gea/earth_model.py +0 -0
  136. {gea_program-0.17.0 → gea_program-0.18.0}/gea/etp.py +0 -0
  137. {gea_program-0.17.0 → gea_program-0.18.0}/gea/etp12_schemas.json +0 -0
  138. {gea_program-0.17.0 → gea_program-0.18.0}/gea/example_register_map.json +0 -0
  139. {gea_program-0.17.0 → gea_program-0.18.0}/gea/fat_sat.py +0 -0
  140. {gea_program-0.17.0 → gea_program-0.18.0}/gea/files.py +0 -0
  141. {gea_program-0.17.0 → gea_program-0.18.0}/gea/follower.py +0 -0
  142. {gea_program-0.17.0 → gea_program-0.18.0}/gea/forward_model.py +0 -0
  143. {gea_program-0.17.0 → gea_program-0.18.0}/gea/gamma.py +0 -0
  144. {gea_program-0.17.0 → gea_program-0.18.0}/gea/gauge_aging.py +0 -0
  145. {gea_program-0.17.0 → gea_program-0.18.0}/gea/gauge_specs.py +0 -0
  146. {gea_program-0.17.0 → gea_program-0.18.0}/gea/geodesy.py +0 -0
  147. {gea_program-0.17.0 → gea_program-0.18.0}/gea/gravity_reference.py +0 -0
  148. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/TESTER_GUIDE.md +0 -0
  149. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/alarms.md +0 -0
  150. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/audit-update.md +0 -0
  151. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/conformance.md +0 -0
  152. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/data-in.md +0 -0
  153. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/doctor.md +0 -0
  154. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/drift.md +0 -0
  155. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/etp.md +0 -0
  156. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/files.md +0 -0
  157. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/instruments.md +0 -0
  158. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/notifications.md +0 -0
  159. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/osdu.md +0 -0
  160. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/patches.md +0 -0
  161. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/ppdm.md +0 -0
  162. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/quakeml.md +0 -0
  163. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/quality.md +0 -0
  164. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/seedlink.md +0 -0
  165. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/seismic.md +0 -0
  166. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/site.md +0 -0
  167. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/sites.md +0 -0
  168. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/sra.md +0 -0
  169. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/start.md +0 -0
  170. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/transients.md +0 -0
  171. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/upkeep.md +0 -0
  172. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/vibration.md +0 -0
  173. {gea_program-0.17.0 → gea_program-0.18.0}/gea/help/well-tests.md +0 -0
  174. {gea_program-0.17.0 → gea_program-0.18.0}/gea/housekeeping.py +0 -0
  175. {gea_program-0.17.0 → gea_program-0.18.0}/gea/inverse_engine.py +0 -0
  176. {gea_program-0.17.0 → gea_program-0.18.0}/gea/jobs.py +0 -0
  177. {gea_program-0.17.0 → gea_program-0.18.0}/gea/live_ports.py +0 -0
  178. {gea_program-0.17.0 → gea_program-0.18.0}/gea/loadtest.py +0 -0
  179. {gea_program-0.17.0 → gea_program-0.18.0}/gea/matplotlib_demo.py +0 -0
  180. {gea_program-0.17.0 → gea_program-0.18.0}/gea/modbus.py +0 -0
  181. {gea_program-0.17.0 → gea_program-0.18.0}/gea/model_card.py +0 -0
  182. {gea_program-0.17.0 → gea_program-0.18.0}/gea/mqtt_port.py +0 -0
  183. {gea_program-0.17.0 → gea_program-0.18.0}/gea/notify.py +0 -0
  184. {gea_program-0.17.0 → gea_program-0.18.0}/gea/opcua_port.py +0 -0
  185. {gea_program-0.17.0 → gea_program-0.18.0}/gea/operator_app.py +0 -0
  186. {gea_program-0.17.0 → gea_program-0.18.0}/gea/osdu.py +0 -0
  187. {gea_program-0.17.0 → gea_program-0.18.0}/gea/patches.py +0 -0
  188. {gea_program-0.17.0 → gea_program-0.18.0}/gea/permits.py +0 -0
  189. {gea_program-0.17.0 → gea_program-0.18.0}/gea/ports.py +0 -0
  190. {gea_program-0.17.0 → gea_program-0.18.0}/gea/ppdm.py +0 -0
  191. {gea_program-0.17.0 → gea_program-0.18.0}/gea/profile_catalog.py +0 -0
  192. {gea_program-0.17.0 → gea_program-0.18.0}/gea/project.py +0 -0
  193. {gea_program-0.17.0 → gea_program-0.18.0}/gea/qt6_downhole_app.py +0 -0
  194. {gea_program-0.17.0 → gea_program-0.18.0}/gea/quakeml.py +0 -0
  195. {gea_program-0.17.0 → gea_program-0.18.0}/gea/reconciler.py +0 -0
  196. {gea_program-0.17.0 → gea_program-0.18.0}/gea/reference/IU_ANMO_10_BHZ_response.provenance.json +0 -0
  197. {gea_program-0.17.0 → gea_program-0.18.0}/gea/reference/IU_ANMO_10_BHZ_response.xml +0 -0
  198. {gea_program-0.17.0 → gea_program-0.18.0}/gea/reference/libmseed_steim.provenance.json +0 -0
  199. {gea_program-0.17.0 → gea_program-0.18.0}/gea/reference/libmseed_steim1_4096.mseed +0 -0
  200. {gea_program-0.17.0 → gea_program-0.18.0}/gea/reference/libmseed_steim2_4096.mseed +0 -0
  201. {gea_program-0.17.0 → gea_program-0.18.0}/gea/reference/libmseed_steim2_512.mseed +0 -0
  202. {gea_program-0.17.0 → gea_program-0.18.0}/gea/rock_inventory.py +0 -0
  203. {gea_program-0.17.0 → gea_program-0.18.0}/gea/sample_record.py +0 -0
  204. {gea_program-0.17.0 → gea_program-0.18.0}/gea/sample_well_profile.csv +0 -0
  205. {gea_program-0.17.0 → gea_program-0.18.0}/gea/sbom.py +0 -0
  206. {gea_program-0.17.0 → gea_program-0.18.0}/gea/seedlink.py +0 -0
  207. {gea_program-0.17.0 → gea_program-0.18.0}/gea/segy.py +0 -0
  208. {gea_program-0.17.0 → gea_program-0.18.0}/gea/seismic.py +0 -0
  209. {gea_program-0.17.0 → gea_program-0.18.0}/gea/seismic_array.py +0 -0
  210. {gea_program-0.17.0 → gea_program-0.18.0}/gea/seismic_assoc.py +0 -0
  211. {gea_program-0.17.0 → gea_program-0.18.0}/gea/seismic_detect.py +0 -0
  212. {gea_program-0.17.0 → gea_program-0.18.0}/gea/seismic_film.py +0 -0
  213. {gea_program-0.17.0 → gea_program-0.18.0}/gea/seismic_harmonic.py +0 -0
  214. {gea_program-0.17.0 → gea_program-0.18.0}/gea/seismic_qc.py +0 -0
  215. {gea_program-0.17.0 → gea_program-0.18.0}/gea/seismic_response.py +0 -0
  216. {gea_program-0.17.0 → gea_program-0.18.0}/gea/seismic_signature.py +0 -0
  217. {gea_program-0.17.0 → gea_program-0.18.0}/gea/seismic_track.py +0 -0
  218. {gea_program-0.17.0 → gea_program-0.18.0}/gea/seismic_uncertainty.py +0 -0
  219. {gea_program-0.17.0 → gea_program-0.18.0}/gea/seismic_unlisted.py +0 -0
  220. {gea_program-0.17.0 → gea_program-0.18.0}/gea/sensor_swap.py +0 -0
  221. {gea_program-0.17.0 → gea_program-0.18.0}/gea/service_life.py +0 -0
  222. {gea_program-0.17.0 → gea_program-0.18.0}/gea/shell.py +0 -0
  223. {gea_program-0.17.0 → gea_program-0.18.0}/gea/shut_in.py +0 -0
  224. {gea_program-0.17.0 → gea_program-0.18.0}/gea/sla_report.py +0 -0
  225. {gea_program-0.17.0 → gea_program-0.18.0}/gea/sra.py +0 -0
  226. {gea_program-0.17.0 → gea_program-0.18.0}/gea/store_forward.py +0 -0
  227. {gea_program-0.17.0 → gea_program-0.18.0}/gea/strata_join.py +0 -0
  228. {gea_program-0.17.0 → gea_program-0.18.0}/gea/supervisor.py +0 -0
  229. {gea_program-0.17.0 → gea_program-0.18.0}/gea/survey_cmd.py +0 -0
  230. {gea_program-0.17.0 → gea_program-0.18.0}/gea/survey_view.py +0 -0
  231. {gea_program-0.17.0 → gea_program-0.18.0}/gea/telemetry.py +0 -0
  232. {gea_program-0.17.0 → gea_program-0.18.0}/gea/tool_library.py +0 -0
  233. {gea_program-0.17.0 → gea_program-0.18.0}/gea/transient.py +0 -0
  234. {gea_program-0.17.0 → gea_program-0.18.0}/gea/vibration.py +0 -0
  235. {gea_program-0.17.0 → gea_program-0.18.0}/gea/well_assembler.py +0 -0
  236. {gea_program-0.17.0 → gea_program-0.18.0}/gea/well_test_validation.py +0 -0
  237. {gea_program-0.17.0 → gea_program-0.18.0}/gea/wits0.py +0 -0
  238. {gea_program-0.17.0 → gea_program-0.18.0}/gea/witsml.py +0 -0
  239. {gea_program-0.17.0 → gea_program-0.18.0}/gea/workspace.py +0 -0
  240. {gea_program-0.17.0 → gea_program-0.18.0}/gea_program.egg-info/dependency_links.txt +0 -0
  241. {gea_program-0.17.0 → gea_program-0.18.0}/gea_program.egg-info/entry_points.txt +0 -0
  242. {gea_program-0.17.0 → gea_program-0.18.0}/gea_program.egg-info/requires.txt +0 -0
  243. {gea_program-0.17.0 → gea_program-0.18.0}/gea_program.egg-info/top_level.txt +0 -0
  244. {gea_program-0.17.0 → gea_program-0.18.0}/setup.cfg +0 -0
  245. {gea_program-0.17.0 → gea_program-0.18.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.17.0
3
+ Version: 0.18.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
@@ -125,7 +125,7 @@ gea dashboard --catalog-well volve_f12_f14_production_excerpt:15/9-F-12:10000 --
125
125
  gea client-report --report accuracy --out client_report
126
126
  gea model-cards --out model_cards
127
127
  gea sbom --out sbom
128
- gea accept # the product gate (340 checks)
128
+ gea accept # the product gate (342 checks)
129
129
  gea help drift # the help library, by the job (16 pages; the same text is on every dashboard page)
130
130
  gea guide # the click-by-click tester guide (docs/TESTER_GUIDE.md)
131
131
  gea gui # the desktop window (pip install "gea-program[desktop]")
@@ -722,6 +722,14 @@ headers on every response, and `gea housekeeping --workspace C:\site --apply`
722
722
  than truncating them and prunes finished job folders and old recordings. `gea
723
723
  loadtest --patches 16 --seconds 60 --with-service` tells you whether the site
724
724
  machine carries the patches it will be given, with the page still answering.
725
+ `gea backup --workspace C:\site --out D:\gea-backups --keep 14` copies the
726
+ whole site into one dated archive with a manifest of every file's hash, onto
727
+ another disk or a share; schedule it daily from Audit / Update, verify an
728
+ archive with `--verify`, and restore one onto an empty folder with
729
+ `--restore ... --to` and open it once before the first well is live - the
730
+ records are the product, and a backup nobody has restored is a hope. The
731
+ doctor warns when there has never been a backup or the last is more than a
732
+ day old. `gea help backup`.
725
733
 
726
734
  ## The standalone install kit
727
735
 
@@ -86,7 +86,7 @@ gea dashboard --catalog-well volve_f12_f14_production_excerpt:15/9-F-12:10000 --
86
86
  gea client-report --report accuracy --out client_report
87
87
  gea model-cards --out model_cards
88
88
  gea sbom --out sbom
89
- gea accept # the product gate (340 checks)
89
+ gea accept # the product gate (342 checks)
90
90
  gea help drift # the help library, by the job (16 pages; the same text is on every dashboard page)
91
91
  gea guide # the click-by-click tester guide (docs/TESTER_GUIDE.md)
92
92
  gea gui # the desktop window (pip install "gea-program[desktop]")
@@ -683,6 +683,14 @@ headers on every response, and `gea housekeeping --workspace C:\site --apply`
683
683
  than truncating them and prunes finished job folders and old recordings. `gea
684
684
  loadtest --patches 16 --seconds 60 --with-service` tells you whether the site
685
685
  machine carries the patches it will be given, with the page still answering.
686
+ `gea backup --workspace C:\site --out D:\gea-backups --keep 14` copies the
687
+ whole site into one dated archive with a manifest of every file's hash, onto
688
+ another disk or a share; schedule it daily from Audit / Update, verify an
689
+ archive with `--verify`, and restore one onto an empty folder with
690
+ `--restore ... --to` and open it once before the first well is live - the
691
+ records are the product, and a backup nobody has restored is a hope. The
692
+ doctor warns when there has never been a backup or the last is more than a
693
+ day old. `gea help backup`.
686
694
 
687
695
  ## The standalone install kit
688
696
 
@@ -123,7 +123,7 @@ from .profile_catalog import (
123
123
  read_pangaea_txt,
124
124
  )
125
125
 
126
- __version__ = "0.17.0"
126
+ __version__ = "0.18.0"
127
127
  __all__ = [
128
128
  "aging_rate", "rate_psi_yr", "accuracy_psi",
129
129
  "Sensor", "SimulatorConfig", "DownholeEngine",
@@ -411,6 +411,16 @@ def main(argv=None) -> int:
411
411
  p_es.add_argument("--frames", type=int, default=600)
412
412
  p_es.add_argument("--interval", type=float, default=1.0, help="seconds between samples")
413
413
  p_es.add_argument("--seed", type=int, default=1)
414
+ p_bk = sub.add_parser("backup", help="the site copied off the machine: one dated archive with a manifest; --verify an archive; --restore one onto an empty folder; --status")
415
+ p_bk.add_argument("--workspace", type=str, default=None, help="the site folder (for a backup, or --status)")
416
+ p_bk.add_argument("--out", type=str, default=None, help="the folder the archive goes to - another disk or a share, never inside the workspace")
417
+ p_bk.add_argument("--keep", type=int, default=None, help="keep only the newest N archives of this site in --out")
418
+ p_bk.add_argument("--label", type=str, default="", help="a note carried in the manifest (e.g. 'before the gauge swap')")
419
+ p_bk.add_argument("--verify", type=str, default=None, help="check this archive against its manifest and its .sha256")
420
+ p_bk.add_argument("--restore", type=str, default=None, help="restore this archive ...")
421
+ p_bk.add_argument("--to", type=str, default=None, help="... onto this empty folder")
422
+ p_bk.add_argument("--status", action="store_true", help="when the last backup of --workspace was taken, and whether that is too long ago")
423
+ p_bk.add_argument("--actor", type=str, default="cli")
414
424
  p_sl = sub.add_parser("seedlink", help="the seismic leg's live port: a SeedLink (3 / 4.0) station's records as it writes them, byte for byte into day files; --selftest against the simulator")
415
425
  p_sl.add_argument("--config", type=str, default=None, help="the SeedLink config JSON (host, port, streams)")
416
426
  p_sl.add_argument("--write-example-config", type=str, default=None, help="write an example config here and exit")
@@ -2017,6 +2027,24 @@ def main(argv=None) -> int:
2017
2027
  stop.set()
2018
2028
  print("wits0-sim: stopped")
2019
2029
  return 0
2030
+ elif a.cmd == "backup":
2031
+ from . import backup as _B
2032
+ try:
2033
+ if a.verify:
2034
+ r = _B.verify(a.verify); print(_B.report_text(r, "verify")); return 0 if r["status"] == "OK" else 1
2035
+ if a.restore:
2036
+ if not a.to:
2037
+ raise SystemExit("backup --restore needs --to <empty folder>")
2038
+ print(_B.report_text(_B.restore(a.restore, a.to, a.actor), "restore")); return 0
2039
+ if not a.workspace:
2040
+ raise SystemExit("backup needs --workspace <site folder> (with --out <another disk>), or --verify / --restore an archive")
2041
+ if a.status:
2042
+ r = _B.status(a.workspace); print(_B.report_text(r, "status")); return 0 if not (r["never"] or r["stale"]) else 1
2043
+ if not a.out:
2044
+ raise SystemExit("backup needs --out <a folder on another disk or a share>")
2045
+ print(_B.report_text(_B.make(a.workspace, a.out, a.actor, keep=a.keep, label=a.label), "make")); return 0
2046
+ except (ValueError, OSError) as e:
2047
+ raise SystemExit(f"backup: {e}")
2020
2048
  elif a.cmd == "seedlink":
2021
2049
  import json as _json
2022
2050
  from . import seedlink as _L
@@ -5719,6 +5719,159 @@ def section_bk_seedlink(tmp: str) -> None:
5719
5719
  "names the simulator to start; the help page says a gap is counted, never bridged"
5720
5720
  + (f" [failed: {', '.join(failed)}; row={json.dumps({k: row.get(k) for k in ('status', 'protocol', 'records', 'latency_p95_s', 'last_error')})}]" if failed else ""))
5721
5721
 
5722
+
5723
+ def section_bl_backup(tmp: str) -> None:
5724
+ """Section BL - the site copied off the machine: the archive and its manifest, verify, a tampered archive named,
5725
+ restore onto an empty folder and the workspace that comes back, the refusals, the doctor's line, the page's
5726
+ card, the job and the daily schedule."""
5727
+ import http.cookiejar
5728
+ import subprocess as _sp
5729
+ import urllib.request
5730
+ import urllib.error
5731
+ import zipfile
5732
+ from . import backup as B
5733
+ from . import doctor as D
5734
+ from . import helplib as H
5735
+ from .telemetry import TelemetryRecorder, TelemetryConfig
5736
+ from .downhole_engine import DownholeEngine, SimulatorConfig
5737
+ from .workspace import Workspace
5738
+ from .service import Service, Users
5739
+ pkg = Path(__file__).parent
5740
+ d = Path(tmp, "bl"); d.mkdir()
5741
+ hist = str(d / "hist.csv")
5742
+ TelemetryRecorder(engine=DownholeEngine(SimulatorConfig()), config=TelemetryConfig(duration_hours=0.5, seed=5)).run().export_csv(hist)
5743
+ wsp = str(d / "ws"); ws = Workspace.create(wsp, "Pad 3", actor="tester")
5744
+ w = ws.add_well_file(hist, display="SWD 1", actor="tester")
5745
+ users = Users(str(Path(wsp, "users.json"))); users.add("op", "operator-pass-1", "operator"); users.add("adm", "admin-pass-1", "admin")
5746
+ (Path(wsp) / "jobs" / "run-0001").mkdir(parents=True); (Path(wsp) / "jobs" / "run-0001" / "log.txt").write_text("a finished run")
5747
+ (Path(wsp) / "jobs" / "_uploads").mkdir(); (Path(wsp) / "jobs" / "_uploads" / "x.bin").write_bytes(b"scratch")
5748
+ (Path(wsp) / "jobs" / "schedule.json").write_text(json.dumps({"entries": []}))
5749
+
5750
+ # -- make, verify, status, restore; the refusals; the pruning ---------------------------------------------------
5751
+ never = B.status(wsp)
5752
+ out = str(d / "off-disk")
5753
+ r1 = B.make(wsp, out, "tester", keep=2, label="first")
5754
+ v1 = B.verify(r1["path"])
5755
+ side1 = os.path.isfile(r1["path"] + ".sha256") # before the later --keep prunes this first archive and its hash file
5756
+ st1 = B.status(wsp)
5757
+ with zipfile.ZipFile(r1["path"]) as z:
5758
+ names = z.namelist(); man = json.loads(z.read("MANIFEST.json"))
5759
+ inside_refused = None
5760
+ try:
5761
+ B.make(wsp, os.path.join(wsp, "records", "bk"), "tester")
5762
+ except ValueError as e:
5763
+ inside_refused = str(e)
5764
+ rr = B.restore(r1["path"], str(d / "restored"), "tester")
5765
+ ws2 = Workspace(str(d / "restored"))
5766
+ nonempty_refused = None
5767
+ try:
5768
+ B.restore(r1["path"], str(d / "restored"), "tester")
5769
+ except ValueError as e:
5770
+ nonempty_refused = str(e)
5771
+ # tamper: rewrite one member in a copy; verify names it and restore refuses it
5772
+ bad = str(d / "tampered.zip")
5773
+ with zipfile.ZipFile(r1["path"]) as zin, zipfile.ZipFile(bad, "w") as zout:
5774
+ for info in zin.infolist():
5775
+ data = zin.read(info.filename)
5776
+ if info.filename == "workspace.json":
5777
+ data = data.replace(b"Pad 3", b"Pad 4")
5778
+ zout.writestr(info, data)
5779
+ vbad = B.verify(bad)
5780
+ tampered_refused = None
5781
+ try:
5782
+ B.restore(bad, str(d / "restored2"), "tester")
5783
+ except ValueError as e:
5784
+ tampered_refused = str(e)
5785
+ r2 = B.make(wsp, out, "tester", keep=2); r3 = B.make(wsp, out, "tester", keep=2)
5786
+ left = sorted(n for n in os.listdir(out) if n.endswith(".zip"))
5787
+ rec_files = [n for n in names if n.startswith(f"wells/{w['id']}/")]
5788
+ c1 = {
5789
+ "never_then_current": never["never"] and never["stale"] and st1["never"] is False and st1["stale"] is False and st1["present"] is True and st1["count"] == 1,
5790
+ "manifest": r1["n_files"] == man["n_files"] and man["protocol"] == "gea.backup/1" and man["site"] == "Pad 3" and man["wells"] == [w["id"]] and man["users_json"] is True,
5791
+ "members": "users.json" in names and "records/audit.jsonl" in names and "workspace.json" in names and "jobs/schedule.json" in names and bool(rec_files)
5792
+ and not any(n.startswith("jobs/run-") or "_uploads" in n for n in names),
5793
+ "verify_ok": v1["status"] == "OK" and v1["n_files"] == r1["n_files"] and v1["archive_sha256_matches"] is True and side1,
5794
+ "inside_refused": bool(inside_refused) and "inside the workspace" in inside_refused,
5795
+ "restored_site": rr["site"] == "Pad 3" and rr["wells"] == [w["id"]] and rr["users_json"] is True and ws2.manifest["name"] == "Pad 3" and ws2.wells()[0]["id"] == w["id"],
5796
+ "restored_note_audit": os.path.isfile(d / "restored" / "restored_from.json") and any(a["action"] == "backup.restore" for a in ws2.audit_log()),
5797
+ "restored_accounts": Users(str(d / "restored" / "users.json")).check("op", "operator-pass-1") is not None,
5798
+ "nonempty_refused": bool(nonempty_refused) and "empty folder" in nonempty_refused,
5799
+ "tampered": vbad["status"] == "FAIL" and vbad["mismatched"] == ["workspace.json"] and bool(tampered_refused) and "does not verify" in tampered_refused,
5800
+ "pruned": len(left) == 2 and os.path.basename(r3["path"]) in left and os.path.basename(r1["path"]) not in left and r3["pruned"] == [os.path.basename(r1["path"])],
5801
+ "audited": any(a["action"] == "backup.make" for a in ws.audit_log()),
5802
+ }
5803
+ failed1 = [k for k, v in c1.items() if not v]
5804
+ ok(not failed1,
5805
+ "BL1 the site copied off the machine: one dated archive carrying every well's source and records, users.json, the audit log, the manifest "
5806
+ "and the schedule - never the job run folders or the upload scratch - with MANIFEST.json (site, version, every file's SHA-256) inside and the "
5807
+ "archive's own hash beside it; verify reads OK; status turns from NEVER to current; a folder inside the workspace is refused; the archive "
5808
+ "restored onto an empty folder opens as the same site with the same well, the same accounts and a restored_from note, and a second restore "
5809
+ "onto it is refused; a tampered archive fails verify naming the file and is refused by restore; --keep prunes the oldest; both in the audit"
5810
+ + (f" [failed: {', '.join(failed1)}]" if failed1 else ""))
5811
+
5812
+ # -- the doctor, the service, the page, the CLI ------------------------------------------------------------------
5813
+ wsp2 = str(d / "ws2"); ws3 = Workspace.create(wsp2, "Pad 3", actor="tester")
5814
+ users3 = Users(str(Path(wsp2, "users.json"))); users3.add("adm", "admin-pass-1", "admin"); users3.add("op", "operator-pass-1", "operator")
5815
+ f_never = [x for x in D.check_workspace(wsp2, port=0) if "backup" in x["what"]]
5816
+ svc = Service(wsp2, port=0, scheduler=False).start()
5817
+ base = svc.url.rstrip("/")
5818
+ opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
5819
+
5820
+ def call(method, path, body=None):
5821
+ req = urllib.request.Request(base + path, method=method, data=(json.dumps(body).encode() if body is not None else None))
5822
+ req.add_header("Content-Type", "application/json"); req.add_header("X-GEA-Action", "1")
5823
+ try:
5824
+ with opener.open(req, timeout=120) as rr_:
5825
+ return rr_.status, json.loads(rr_.read())
5826
+ except urllib.error.HTTPError as e:
5827
+ return e.code, json.loads(e.read())
5828
+ try:
5829
+ call("POST", "/api/login", {"name": "op", "password": "operator-pass-1"})
5830
+ st_op = call("GET", "/api/backup")[0]
5831
+ call("POST", "/api/login", {"name": "adm", "password": "admin-pass-1"})
5832
+ st_v0, v0 = call("GET", "/api/backup")
5833
+ st_run0 = call("POST", "/api/backup/run", {})[0]
5834
+ st_in = call("POST", "/api/backup/config", {"out_dir": os.path.join(wsp2, "records", "bk")})[0]
5835
+ st_bad_t = call("POST", "/api/backup/config", {"out_dir": str(d / "off2"), "daily_at": "2:30"})[0]
5836
+ st_cfg, cfg = call("POST", "/api/backup/config", {"out_dir": str(d / "off2"), "keep": 7, "daily_at": "02:30"})
5837
+ st_run, job = call("POST", "/api/backup/run", {})
5838
+ done = svc.app.runner.wait(job["id"], 120)
5839
+ st_v1, v1s = call("GET", "/api/backup")
5840
+ st_sch, sch = call("POST", "/api/backup/schedule", {})
5841
+ st_v2, v2s = call("GET", "/api/backup")
5842
+ finally:
5843
+ svc.stop()
5844
+ f_after = [x for x in D.check_workspace(wsp2, port=0) if "backup" in x["what"]]
5845
+ page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
5846
+ help_md = (pkg / "help" / "backup.md").read_text(encoding="utf-8")
5847
+ r_st0 = _sp.run([sys.executable, "-m", "gea", "backup", "--workspace", str(d / "ws3-none"), "--status"], capture_output=True, text=True)
5848
+ r_st = _sp.run([sys.executable, "-m", "gea", "backup", "--workspace", wsp2, "--status"], capture_output=True, text=True)
5849
+ r_v = _sp.run([sys.executable, "-m", "gea", "backup", "--verify", v1s["status"]["last"]["path"]], capture_output=True, text=True)
5850
+ r_vbad = _sp.run([sys.executable, "-m", "gea", "backup", "--verify", bad], capture_output=True, text=True)
5851
+ r_rs = _sp.run([sys.executable, "-m", "gea", "backup", "--restore", v1s["status"]["last"]["path"], "--to", str(d / "restored3")], capture_output=True, text=True)
5852
+ c2 = {
5853
+ "doctor_never": len(f_never) == 1 and f_never[0]["level"] == "warn" and "no backup has ever been taken" in f_never[0]["what"] and "gea backup" in f_never[0]["fix"],
5854
+ "api_admin_only_and_empty": st_op == 403 and st_v0 == 200 and v0["status"]["never"] is True and v0["schedule"] is None and v0["config"]["out_dir"] == "",
5855
+ "config_refusals": st_run0 == 400 and st_in == 400 and st_bad_t == 400 and st_cfg == 200 and cfg["keep"] == 7,
5856
+ "job_ran": st_run == 200 and done["status"] == "DONE" and st_v1 == 200 and v1s["status"]["never"] is False and v1s["status"]["present"] is True
5857
+ and os.path.isfile(v1s["status"]["last"]["path"]) and v1s["status"]["last"]["actor"] == "adm",
5858
+ "scheduled": st_sch == 200 and sch["name"] == "backup-daily" and sch["daily_at"] == "02:30" and sch["args"][0] == "backup" and "--keep" in sch["args"]
5859
+ and v2s["schedule"] is not None and v2s["schedule"]["name"] == "backup-daily",
5860
+ "doctor_after": len(f_after) == 1 and f_after[0]["level"] == "ok" and "last backup" in f_after[0]["what"],
5861
+ "page": "The backup" in page_src and "/api/backup/run" in page_src and "/api/backup/schedule" in page_src,
5862
+ "cli_status": r_st0.returncode == 1 and "NEVER" in r_st0.stdout and r_st.returncode == 0 and "current" in r_st.stdout,
5863
+ "cli_verify": r_v.returncode == 0 and "OK" in r_v.stdout and r_vbad.returncode == 1 and "workspace.json" in r_vbad.stdout,
5864
+ "cli_restore": r_rs.returncode == 0 and "users.json present" in r_rs.stdout,
5865
+ "help": any(t["topic"] == "backup" for t in H.topics()) and not H.check("backup") and "never restored" in help_md.split("will not call a measurement")[1],
5866
+ }
5867
+ failed2 = [k for k, v in c2.items() if not v]
5868
+ ok(not failed2,
5869
+ "BL2 the doctor warns NEVER until a backup exists and says when the last one was afterwards; the Audit / Update page's backup card (admin only) "
5870
+ "refuses a folder inside the workspace and a malformed hour, saves the folder and the keep count, runs the backup as a job that the status then "
5871
+ "shows with its actor, and schedules it daily as the `backup-daily` entry; `gea backup --status` exits 1 on NEVER and 0 when current, "
5872
+ "`--verify` says OK or names the tampered file, `--restore` brings the accounts back; the help page says a backup never restored is a hope"
5873
+ + (f" [failed: {', '.join(failed2)}; r_st0={r_st0.stdout[-120:]!r} {r_st0.stderr[-120:]!r}]" if failed2 else ""))
5874
+
5722
5875
  def main() -> int:
5723
5876
  print("GEA-Program - ACCEPTANCE SUITE (the product gate)")
5724
5877
  # a handle a job subprocess still holds at the end must not turn a finished gate into a traceback on Windows:
@@ -5785,6 +5938,7 @@ def main() -> int:
5785
5938
  section_bi_conformance(tmp)
5786
5939
  section_bj_etp(tmp)
5787
5940
  section_bk_seedlink(tmp)
5941
+ section_bl_backup(tmp)
5788
5942
  if _FAILS:
5789
5943
  print(f"[ACCEPTANCE] {len(_FAILS)} FAILURES ({_PASS} passed):")
5790
5944
  for f in _FAILS:
@@ -0,0 +1,262 @@
1
+ """backup - the site, copied off the machine, and proven to come back.
2
+
3
+ The records are the product. Nothing in housekeeping, the workspace or the service copied the site anywhere;
4
+ the doctor told a person to restore users.json "from a backup" that no command wrote. This module writes it.
5
+
6
+ `make` zips the workspace - every well's source and records, the seismic stations with their source and live
7
+ day files, the configuration store, the monitor, the reports, users.json, the audit log, the patches, the
8
+ schedule - into one dated archive in a folder that should be on another disk or a share, with a manifest
9
+ (program version, site name, every file's SHA-256 and size) and the manifest's own hash beside it. The job
10
+ folders of finished runs and the upload scratch are left out: they are reproducible, and they are what grows.
11
+ `verify` opens an archive and checks every file against its manifest. `restore` extracts one into an empty
12
+ folder, verifies, opens it as a workspace and says what came back - a backup nobody has restored is a hope.
13
+ `status` says when the last backup was taken and whether that is too long ago; the doctor prints it.
14
+
15
+ What it will not do: back up into the workspace itself (refused - a lost disk loses both); restore over a
16
+ folder that has anything in it; call an archive good whose manifest does not match its files.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import json
22
+ import os
23
+ import re
24
+ import shutil
25
+ import time
26
+ import zipfile
27
+ from datetime import datetime, timezone
28
+ from typing import Dict, List, Optional
29
+
30
+ STALE_AFTER_H = 26.0 # a daily backup that is more than a day and change old
31
+ EXCLUDE_TOP = ('jobs',) # taken selectively: schedule.json only
32
+ EXCLUDE_NAMES = ('_uploads', '__pycache__')
33
+ LOG_NAME = 'backups.jsonl' # records/backups.jsonl: one line per backup taken from this workspace
34
+
35
+
36
+ def _utc() -> str:
37
+ return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
38
+
39
+
40
+ def _sha256(path: str) -> str:
41
+ h = hashlib.sha256()
42
+ with open(path, 'rb') as f:
43
+ for chunk in iter(lambda: f.read(1 << 20), b''):
44
+ h.update(chunk)
45
+ return h.hexdigest()
46
+
47
+
48
+ def _sha256_bytes(b: bytes) -> str:
49
+ return hashlib.sha256(b).hexdigest()
50
+
51
+
52
+ def _inside(child: str, parent: str) -> bool:
53
+ c, p = os.path.realpath(child), os.path.realpath(parent)
54
+ return c == p or c.startswith(p + os.sep)
55
+
56
+
57
+ def site_files(workspace_path: str) -> List[str]:
58
+ """The relative paths a backup carries, sorted: everything but the job run folders and the upload scratch."""
59
+ out: List[str] = []
60
+ for root, dirs, files in os.walk(workspace_path):
61
+ rel_root = os.path.relpath(root, workspace_path)
62
+ top = rel_root.split(os.sep)[0] if rel_root != '.' else ''
63
+ dirs[:] = sorted(d for d in dirs if d not in EXCLUDE_NAMES and not (rel_root == '.' and d in EXCLUDE_TOP))
64
+ for name in sorted(files):
65
+ if name.endswith(('.tmp', '.lock')) or name in EXCLUDE_NAMES:
66
+ continue
67
+ rel = os.path.normpath(os.path.join(rel_root, name)) if rel_root != '.' else name
68
+ out.append(rel.replace(os.sep, '/'))
69
+ # the schedule is the one thing under jobs/ worth carrying: it is what a person set, not what a run left
70
+ sched = os.path.join(workspace_path, 'jobs', 'schedule.json')
71
+ if os.path.isfile(sched):
72
+ out.append('jobs/schedule.json')
73
+ return sorted(set(out))
74
+
75
+
76
+ def make(workspace_path: str, out_dir: str, actor: str = 'backup', keep: Optional[int] = None, label: str = '') -> dict:
77
+ """Write `gea-site-<slug>-<UTC>.zip` into out_dir with MANIFEST.json inside and <zip>.sha256 beside it; prune to
78
+ the newest `keep` archives of this site when asked; log the backup in records/backups.jsonl and the audit."""
79
+ from . import __version__
80
+ from .workspace import Workspace
81
+ ws = Workspace(workspace_path)
82
+ if _inside(out_dir, ws.path):
83
+ raise ValueError(f'the backup folder {out_dir} is inside the workspace - a lost disk would lose both; use another disk or a share')
84
+ os.makedirs(out_dir, exist_ok=True)
85
+ slug = ''.join(c if c.isalnum() or c in '-_' else '-' for c in ws.manifest['name']).strip('-') or 'site'
86
+ stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
87
+ name = f'gea-site-{slug}-{stamp}.zip'
88
+ k = 2
89
+ while os.path.exists(os.path.join(out_dir, name)): # two in one second: the second is numbered, never overwrites
90
+ name = f'gea-site-{slug}-{stamp}-{k}.zip'; k += 1
91
+ path = os.path.join(out_dir, name)
92
+ files = site_files(ws.path)
93
+ entries: Dict[str, dict] = {}
94
+ total = 0
95
+ t0 = time.time()
96
+ with zipfile.ZipFile(path + '.part', 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=6) as z:
97
+ for rel in files:
98
+ src = os.path.join(ws.path, *rel.split('/'))
99
+ try:
100
+ data = open(src, 'rb').read()
101
+ except OSError as e: # a file that vanished between the walk and the read (a rotated log)
102
+ entries[rel] = {'skipped': str(e)}
103
+ continue
104
+ z.writestr(zipfile.ZipInfo.from_file(src, arcname=rel), data)
105
+ entries[rel] = {'sha256': _sha256_bytes(data), 'bytes': len(data)}
106
+ total += len(data)
107
+ manifest = {'protocol': 'gea.backup/1', 'program_version': __version__, 'site': ws.manifest['name'], 'workspace': ws.path,
108
+ 'generated_utc': _utc(), 'actor': actor, 'label': label, 'files': entries,
109
+ 'n_files': sum(1 for v in entries.values() if 'sha256' in v), 'bytes': total,
110
+ 'excluded': ['jobs/<run folders>', 'jobs/_uploads', '*.tmp', '*.lock'],
111
+ 'wells': list(ws.manifest.get('wells', [])), 'seismic': list(ws.manifest.get('seismic', [])),
112
+ 'users_json': 'users.json' in entries}
113
+ z.writestr('MANIFEST.json', json.dumps(manifest, indent=1))
114
+ os.replace(path + '.part', path)
115
+ digest = _sha256(path)
116
+ with open(path + '.sha256', 'w', encoding='utf-8') as f:
117
+ f.write(f'{digest} {name}\n')
118
+ pruned = []
119
+ if keep is not None and keep > 0:
120
+ def _order(n: str): # by the stamp, then the same-second number: a lexical sort puts '-2' before '.zip'
121
+ m = re.match(rf'gea-site-{re.escape(slug)}-(\d{{8}}T\d{{6}}Z)(?:-(\d+))?\.zip$', n)
122
+ return (m.group(1), int(m.group(2) or 1)) if m else ('', 0)
123
+ mine = sorted((n for n in os.listdir(out_dir) if n.startswith(f'gea-site-{slug}-') and n.endswith('.zip')), key=_order)
124
+ for old in mine[:-keep]:
125
+ for p in (os.path.join(out_dir, old), os.path.join(out_dir, old + '.sha256')):
126
+ if os.path.isfile(p):
127
+ os.remove(p)
128
+ pruned.append(old)
129
+ rec = {'utc': manifest['generated_utc'], 'path': path, 'sha256': digest, 'bytes_archive': os.path.getsize(path), 'bytes_files': total,
130
+ 'n_files': manifest['n_files'], 'seconds': round(time.time() - t0, 2), 'pruned': pruned, 'actor': actor, 'program_version': __version__}
131
+ with open(os.path.join(ws.dir('records'), LOG_NAME), 'a', encoding='utf-8') as f:
132
+ f.write(json.dumps(rec) + '\n')
133
+ ws.audit(actor, 'backup.make', {'path': path, 'n_files': manifest['n_files'], 'bytes': total, 'sha256': digest, 'pruned': pruned})
134
+ return rec
135
+
136
+
137
+ def verify(zip_path: str) -> dict:
138
+ """Every file in the archive against MANIFEST.json, the archive against its .sha256 when that is beside it."""
139
+ out = {'path': zip_path, 'status': 'OK', 'n_files': 0, 'mismatched': [], 'missing': [], 'extra': [], 'archive_sha256': None, 'archive_sha256_matches': None,
140
+ 'manifest': None}
141
+ if not os.path.isfile(zip_path):
142
+ out.update({'status': 'FAIL', 'error': 'no such file'})
143
+ return out
144
+ try:
145
+ with zipfile.ZipFile(zip_path) as z:
146
+ bad = z.testzip()
147
+ if bad is not None:
148
+ out.update({'status': 'FAIL', 'error': f'corrupt member {bad}'})
149
+ return out
150
+ if 'MANIFEST.json' not in z.namelist():
151
+ out.update({'status': 'FAIL', 'error': 'no MANIFEST.json - not a gea backup'})
152
+ return out
153
+ man = json.loads(z.read('MANIFEST.json'))
154
+ out['manifest'] = {k: man.get(k) for k in ('protocol', 'program_version', 'site', 'generated_utc', 'n_files', 'bytes', 'wells', 'seismic', 'users_json')}
155
+ names = set(n for n in z.namelist() if n != 'MANIFEST.json' and not n.endswith('/'))
156
+ for rel, meta in man['files'].items():
157
+ if 'sha256' not in meta:
158
+ continue
159
+ if rel not in names:
160
+ out['missing'].append(rel)
161
+ continue
162
+ if _sha256_bytes(z.read(rel)) != meta['sha256']:
163
+ out['mismatched'].append(rel)
164
+ out['n_files'] += 1
165
+ out['extra'] = sorted(names - set(man['files']))
166
+ except (zipfile.BadZipFile, KeyError, ValueError) as e:
167
+ out.update({'status': 'FAIL', 'error': f'unreadable archive: {e}'})
168
+ return out
169
+ side = zip_path + '.sha256'
170
+ out['archive_sha256'] = _sha256(zip_path)
171
+ if os.path.isfile(side):
172
+ with open(side, encoding='utf-8') as f:
173
+ out['archive_sha256_matches'] = f.read().split()[0] == out['archive_sha256']
174
+ if out['mismatched'] or out['missing'] or out['extra'] or out['archive_sha256_matches'] is False:
175
+ out['status'] = 'FAIL'
176
+ return out
177
+
178
+
179
+ def restore(zip_path: str, target: str, actor: str = 'restore') -> dict:
180
+ """Into an empty (or absent) folder only; verified first; opened as a workspace afterwards and described."""
181
+ from .workspace import Workspace
182
+ v = verify(zip_path)
183
+ if v['status'] != 'OK':
184
+ raise ValueError(f"the archive does not verify: {v.get('error') or (v['mismatched'] + v['missing'] + v['extra'])}")
185
+ if os.path.exists(target) and (not os.path.isdir(target) or os.listdir(target)):
186
+ raise ValueError(f'the restore target must be an empty folder: {target}')
187
+ os.makedirs(target, exist_ok=True)
188
+ with zipfile.ZipFile(zip_path) as z:
189
+ for info in z.infolist():
190
+ if info.filename == 'MANIFEST.json' or info.filename.endswith('/'):
191
+ continue
192
+ rel = os.path.normpath(info.filename)
193
+ if rel.startswith('..') or os.path.isabs(rel):
194
+ raise ValueError(f'refusing a path outside the target: {info.filename}')
195
+ dst = os.path.join(target, rel)
196
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
197
+ with z.open(info) as src, open(dst, 'wb') as out:
198
+ shutil.copyfileobj(src, out)
199
+ man = json.loads(z.read('MANIFEST.json'))
200
+ back = [rel for rel, meta in man['files'].items() if 'sha256' in meta and _sha256(os.path.join(target, *rel.split('/'))) != meta['sha256']]
201
+ if back:
202
+ raise ValueError(f'{len(back)} file(s) differ after extraction: {back[:5]}')
203
+ ws = Workspace(target)
204
+ note = {'protocol': 'gea.restore/1', 'restored_utc': _utc(), 'from': zip_path, 'archive_sha256': v['archive_sha256'], 'backup_generated_utc': man['generated_utc'],
205
+ 'backup_program_version': man['program_version'], 'actor': actor}
206
+ with open(os.path.join(target, 'restored_from.json'), 'w', encoding='utf-8') as f:
207
+ json.dump(note, f, indent=1)
208
+ ws.audit(actor, 'backup.restore', {'from': zip_path, 'archive_sha256': v['archive_sha256'], 'n_files': v['n_files']})
209
+ s = ws.summary()
210
+ return {'target': target, 'site': ws.manifest['name'], 'n_files': v['n_files'], 'wells': list(ws.manifest.get('wells', [])), 'seismic': list(ws.manifest.get('seismic', [])),
211
+ 'users_json': os.path.isfile(os.path.join(target, 'users.json')), 'backup_generated_utc': man['generated_utc'], 'summary': s}
212
+
213
+
214
+ def status(workspace_path: str, stale_after_h: float = STALE_AFTER_H) -> dict:
215
+ """The last backup logged from this workspace, its age, whether it is still where the log says, and the verdict."""
216
+ p = os.path.join(workspace_path, 'records', LOG_NAME)
217
+ out = {'last': None, 'age_h': None, 'stale': True, 'never': True, 'present': None, 'count': 0, 'stale_after_h': stale_after_h}
218
+ if not os.path.isfile(p):
219
+ return out
220
+ last = None
221
+ n = 0
222
+ with open(p, encoding='utf-8') as f:
223
+ for line in f:
224
+ if line.strip():
225
+ try:
226
+ last = json.loads(line); n += 1
227
+ except ValueError:
228
+ continue
229
+ out['count'] = n
230
+ if last is None:
231
+ return out
232
+ t = datetime.strptime(last['utc'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)
233
+ age_h = (datetime.now(timezone.utc) - t).total_seconds() / 3600.0
234
+ out.update({'last': last, 'age_h': round(age_h, 2), 'stale': age_h > stale_after_h, 'never': False, 'present': os.path.isfile(last['path'])})
235
+ return out
236
+
237
+
238
+ def report_text(r: dict, kind: str) -> str:
239
+ if kind == 'make':
240
+ return (f"backup: {r['path']}\n {r['n_files']} files, {r['bytes_files'] / 1048576:.1f} MB of site in a {r['bytes_archive'] / 1048576:.1f} MB archive, "
241
+ f"sha256 {r['sha256'][:16]}..., {r['seconds']} s" + (f"\n pruned: {', '.join(r['pruned'])}" if r['pruned'] else '')
242
+ + "\n verify it once with --verify, and restore it once onto an empty folder before trusting it")
243
+ if kind == 'verify':
244
+ m = r.get('manifest') or {}
245
+ head = f"verify: {r['path']} - {r['status']}"
246
+ if r.get('error'):
247
+ return head + f"\n {r['error']}"
248
+ return (head + f"\n site {m.get('site')!r}, taken {m.get('generated_utc')} by gea-program {m.get('program_version')}: {r['n_files']} files checked"
249
+ + (f", archive hash {'matches' if r['archive_sha256_matches'] else 'DOES NOT MATCH'} its .sha256" if r['archive_sha256_matches'] is not None else ', no .sha256 beside it')
250
+ + (f"\n mismatched: {r['mismatched']}" if r['mismatched'] else '') + (f"\n missing: {r['missing']}" if r['missing'] else '')
251
+ + (f"\n extra: {r['extra']}" if r['extra'] else ''))
252
+ if kind == 'restore':
253
+ return (f"restored: {r['target']}\n site {r['site']!r} from the backup of {r['backup_generated_utc']}: {r['n_files']} files, "
254
+ f"{len(r['wells'])} well(s) {r['wells']}, {len(r['seismic'])} station(s), users.json {'present' if r['users_json'] else 'ABSENT'}"
255
+ "\n open it: gea serve --workspace <that folder> --port 8766, and look before you trust the copy")
256
+ if kind == 'status':
257
+ if r['never']:
258
+ return 'backup: NEVER - no backup has been taken from this workspace (gea backup --workspace ... --out <another disk>)'
259
+ l = r['last']
260
+ return (f"backup: last {l['utc']} ({r['age_h']} h ago) -> {l['path']} ({'present' if r['present'] else 'NOT FOUND where the log says'}); "
261
+ f"{r['count']} on record; {'STALE' if r['stale'] else 'current'} (stale after {r['stale_after_h']:g} h)")
262
+ return json.dumps(r, indent=1)
@@ -225,8 +225,23 @@ def check_workspace(path: str, host: str = '127.0.0.1', port: int = 8765) -> Lis
225
225
  try:
226
226
  n_users = len(json.load(open(users, encoding='utf-8')).get('users', []))
227
227
  except (OSError, json.JSONDecodeError):
228
- f.append(_finding('block', 'users.json is not valid JSON', 'restore it from a backup, or move it aside to start with a fresh administrator'))
228
+ f.append(_finding('block', 'users.json is not valid JSON', 'restore it from a backup (gea backup --restore <archive> --to <empty folder>), or move it aside to start with a fresh administrator'))
229
229
  f.append(_finding('info', f'{n_users} account(s)' + ('' if n_users else ' - the first visit to the page creates the administrator')))
230
+ try:
231
+ from . import backup as BK
232
+ bs = BK.status(ws.path)
233
+ if bs['never']:
234
+ f.append(_finding('warn', 'no backup has ever been taken from this workspace - a lost disk is a lost month, and the records are the product',
235
+ 'gea backup --workspace <this> --out <a folder on another disk or a share>; then schedule it daily on the Audit / Update page'))
236
+ elif bs['stale']:
237
+ f.append(_finding('warn', f"the last backup is {bs['age_h']:.0f} h old ({bs['last']['utc']}); daily means under {bs['stale_after_h']:g} h",
238
+ 'run the backup, and check the daily schedule entry is enabled'))
239
+ elif bs['present'] is False:
240
+ f.append(_finding('warn', f"the last backup ({bs['last']['utc']}) is not where the log says: {bs['last']['path']}", 'the backup disk or share is not mounted, or the file was moved'))
241
+ else:
242
+ f.append(_finding('ok', f"last backup {bs['last']['utc']} ({bs['age_h']:.1f} h ago), present at {bs['last']['path']}"))
243
+ except Exception as e:
244
+ f.append(_finding('warn', f'backup status could not be read: {e}'))
230
245
  try:
231
246
  from .patches import PatchStore
232
247
  ps = PatchStore(ws).list()
@@ -0,0 +1,11 @@
1
+ # The backup
2
+
3
+ > The records are the product. A lost disk is a lost month unless the site is copied off the machine every day - and a copy nobody has restored is a hope, not a backup. This page is the command that copies, the one that checks, and the one that brings it back.
4
+
5
+ **The command**: `gea backup --workspace C:\site --out D:\gea-backups --keep 14` writes one dated archive, `gea-site-<name>-<UTC>.zip`, into a folder that must be on another disk or a share (a folder inside the workspace is refused). On the dashboard, Audit / Update -> The backup: set the folder, Back up now (a job), Schedule daily (a schedule entry, `backup-daily`, at the hour given, UTC). `gea backup --verify <archive>` checks every file in it against its manifest and the archive against its `.sha256`. `gea backup --restore <archive> --to C:\site-restored` extracts onto an empty folder, verifies every file again, opens the result as a workspace and says what came back; then `gea serve --workspace C:\site-restored --port 8766` and look. `gea backup --workspace C:\site --status` prints when the last one was taken; the doctor prints the same line and warns when there has never been one or the last is more than 26 hours old.
6
+
7
+ **What it writes**: the whole site - every well's source files and records, every seismic station's source and live day files, `config/`, `monitor/`, `reports/`, `users.json`, `records/` (the audit log, the notifications, the patches, the certificates, the swaps), `workspace.json` and `jobs/schedule.json` - with `MANIFEST.json` inside (the program version, the site name, the time, every file's SHA-256 and size, the wells and stations by id) and `<archive>.sha256` beside it. Left out: the job run folders and the upload scratch, which are reproducible and are what grows. Each backup is logged in `records/backups.jsonl` and in the audit log (`backup.make`); a restore writes `restored_from.json` in the restored folder and `backup.restore` in its audit log. `--keep N` prunes the older archives of this site in that folder.
8
+
9
+ **The number to check**: on the card and in the doctor, `current` with the hours since the last backup, or `NEVER` / `STALE`; on `--verify`, `OK` with the file count and "archive hash matches"; on `--restore`, the well and station counts and `users.json present`.
10
+
11
+ **What this page will not call a measurement**: a backup that was never restored - run the restore once onto an empty folder and open the page. An archive whose manifest does not match its files - `--verify` says FAIL and names them, and `--restore` refuses it. A copy on the same disk - the folder inside the workspace is refused; a second folder on the same drive is not refused, and is not a backup either. The job run folders - they are not in the archive, by design.
@@ -46,6 +46,7 @@ INDEX: List[tuple] = [
46
46
  ('conformance', 'Conformity, in the standards\' words', 'certificates against ISO/IEC 17025 7.8 and the bias against its class under a named ILAC-G8 decision rule'),
47
47
  ('etp', 'WITSML 2.x over ETP', 'the live port on Energistics ETP v1.2: subscribe to the store\'s channels and it pushes'),
48
48
  ('seedlink', 'Live stations (SeedLink)', 'the seismic leg\'s live port: a station\'s records as it writes them, resumed by sequence number, folded into its record list'),
49
+ ('backup', 'The backup', 'the site copied off the machine, verified, and restored once before it is trusted'),
49
50
  ('doctor', 'Which code is running', 'gea doctor and the serve start-up gate'),
50
51
  ('audit-update', 'Audit / Update', 'the whole audit log with filters, the program against PyPI, every report against its source'),
51
52
  ('upkeep', 'Upkeep', 'housekeeping, sessions, the TLS proxy, the load test'),