gea-program 0.17.1__tar.gz → 0.19.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 (247) hide show
  1. {gea_program-0.17.1 → gea_program-0.19.0}/PKG-INFO +28 -2
  2. {gea_program-0.17.1 → gea_program-0.19.0}/README.md +27 -1
  3. {gea_program-0.17.1 → gea_program-0.19.0}/gea/__init__.py +1 -1
  4. {gea_program-0.17.1 → gea_program-0.19.0}/gea/__main__.py +36 -2
  5. {gea_program-0.17.1 → gea_program-0.19.0}/gea/acceptance_tests.py +279 -0
  6. gea_program-0.19.0/gea/backup.py +262 -0
  7. {gea_program-0.17.1 → gea_program-0.19.0}/gea/doctor.py +16 -1
  8. gea_program-0.19.0/gea/help/backup.md +11 -0
  9. gea_program-0.19.0/gea/help/month-end.md +11 -0
  10. {gea_program-0.17.1 → gea_program-0.19.0}/gea/helplib.py +2 -0
  11. gea_program-0.19.0/gea/month_end.py +290 -0
  12. {gea_program-0.17.1 → gea_program-0.19.0}/gea/service.py +87 -1
  13. {gea_program-0.17.1 → gea_program-0.19.0}/gea/web/app.html +18 -0
  14. {gea_program-0.17.1 → gea_program-0.19.0}/gea/workspace.py +8 -0
  15. {gea_program-0.17.1 → gea_program-0.19.0}/gea_program.egg-info/PKG-INFO +28 -2
  16. {gea_program-0.17.1 → gea_program-0.19.0}/gea_program.egg-info/SOURCES.txt +4 -0
  17. {gea_program-0.17.1 → gea_program-0.19.0}/pyproject.toml +1 -1
  18. {gea_program-0.17.1 → gea_program-0.19.0}/LICENSE +0 -0
  19. {gea_program-0.17.1 → gea_program-0.19.0}/gea/BENCH_TEST_PROTOCOL.md +0 -0
  20. {gea_program-0.17.1 → gea_program-0.19.0}/gea/accuracy_statement.py +0 -0
  21. {gea_program-0.17.1 → gea_program-0.19.0}/gea/alarm_engine.py +0 -0
  22. {gea_program-0.17.1 → gea_program-0.19.0}/gea/bench.py +0 -0
  23. {gea_program-0.17.1 → gea_program-0.19.0}/gea/blind_harness.py +0 -0
  24. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/acex_lomonosov_age_depth_model.provenance.json +0 -0
  25. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/acex_lomonosov_age_depth_model.txt +0 -0
  26. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/agassiz77_canada_temperature.csv +0 -0
  27. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/agassiz77_canada_temperature.provenance.json +0 -0
  28. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/barbados_110_consolidation.provenance.json +0 -0
  29. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/barbados_110_consolidation.txt +0 -0
  30. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/bengal_u1452_grain_size.provenance.json +0 -0
  31. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/bengal_u1452_grain_size.txt +0 -0
  32. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/blake_164_methane_isotopes.provenance.json +0 -0
  33. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/blake_164_methane_isotopes.txt +0 -0
  34. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +0 -0
  35. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.txt +0 -0
  36. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/collingwood_1_28_ks_complete.las +0 -0
  37. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/collingwood_1_28_ks_complete.provenance.json +0 -0
  38. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/costa_rica_odp_friction_envelope.provenance.json +0 -0
  39. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/costa_rica_odp_friction_envelope.txt +0 -0
  40. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +0 -0
  41. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +0 -0
  42. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/dsdp_504b_physical_properties.provenance.json +0 -0
  43. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/dsdp_504b_physical_properties.txt +0 -0
  44. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/dsdp_504b_sound_velocity.provenance.json +0 -0
  45. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/dsdp_504b_sound_velocity.txt +0 -0
  46. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/elgygytgyn_5011_turbidites.provenance.json +0 -0
  47. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/elgygytgyn_5011_turbidites.txt +0 -0
  48. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/epica_domec_co2_800kyr.provenance.json +0 -0
  49. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/epica_domec_co2_800kyr.txt +0 -0
  50. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/fram_909_organic_petrography.provenance.json +0 -0
  51. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/fram_909_organic_petrography.txt +0 -0
  52. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/gbr_325_coral_uth_ages.provenance.json +0 -0
  53. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/gbr_325_coral_uth_ages.txt +0 -0
  54. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/gisp2_greenland_temperature.csv +0 -0
  55. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/gisp2_greenland_temperature.provenance.json +0 -0
  56. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/gom_308_t2p_insitu.provenance.json +0 -0
  57. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/gom_308_t2p_insitu.txt +0 -0
  58. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/guaymas_385_dom_d13c.provenance.json +0 -0
  59. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/guaymas_385_dom_d13c.txt +0 -0
  60. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +0 -0
  61. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/hikurangi_u1520_friction_insitu.txt +0 -0
  62. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +0 -0
  63. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +0 -0
  64. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/iodp_u1324_pore_pressure.provenance.json +0 -0
  65. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/iodp_u1324_pore_pressure.txt +0 -0
  66. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/jfast_c0019_slow_slip_events.provenance.json +0 -0
  67. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/jfast_c0019_slow_slip_events.txt +0 -0
  68. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/kennetcook_2_p129_excerpt.las +0 -0
  69. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/kennetcook_2_p129_excerpt.provenance.json +0 -0
  70. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ktb_hb_bhgm_density.dat +0 -0
  71. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ktb_hb_bhgm_density.provenance.json +0 -0
  72. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +0 -0
  73. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ktb_hb_complog_6020_excerpt.txt +0 -0
  74. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ktb_hb_hlog246_temperature.dat +0 -0
  75. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ktb_hb_hlog246_temperature.provenance.json +0 -0
  76. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ktb_hb_rockmech_compress.dat +0 -0
  77. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ktb_hb_rockmech_compress.provenance.json +0 -0
  78. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +0 -0
  79. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +0 -0
  80. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ktb_vb_rockmech_compress.dat +0 -0
  81. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ktb_vb_rockmech_compress.provenance.json +0 -0
  82. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ktb_vb_vlog251_temperature.dat +0 -0
  83. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ktb_vb_vlog251_temperature.provenance.json +0 -0
  84. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/l06_06_nl_survey.csv +0 -0
  85. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/l06_06_nl_survey.provenance.json +0 -0
  86. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/l07_01_nl_excerpt.las +0 -0
  87. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/l07_01_nl_excerpt.provenance.json +0 -0
  88. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +0 -0
  89. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/mariana_1200_serpentinite_geochem.txt +0 -0
  90. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/med_160_sapropels.provenance.json +0 -0
  91. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/med_160_sapropels.txt +0 -0
  92. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/nankai_megasplay_shear_strength.provenance.json +0 -0
  93. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/nankai_megasplay_shear_strength.txt +0 -0
  94. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/odp_1027b_thermal_conductivity.provenance.json +0 -0
  95. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/odp_1027b_thermal_conductivity.txt +0 -0
  96. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/odp_1027c_cork_temperature.provenance.json +0 -0
  97. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/odp_1027c_cork_temperature.txt +0 -0
  98. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/odp_1165b_thermal_conductivity.provenance.json +0 -0
  99. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/odp_1165b_thermal_conductivity.txt +0 -0
  100. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +0 -0
  101. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.txt +0 -0
  102. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +0 -0
  103. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/odp_504b_dike_elastic_moduli.txt +0 -0
  104. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +0 -0
  105. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/odp_504b_leg137_borehole_fluids.txt +0 -0
  106. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +0 -0
  107. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.txt +0 -0
  108. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/peru_201_sulfate_reduction.provenance.json +0 -0
  109. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/peru_201_sulfate_reduction.txt +0 -0
  110. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/scorpio_e1_sa_excerpt.las +0 -0
  111. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/scorpio_e1_sa_excerpt.provenance.json +0 -0
  112. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/sumatra_362_cohesion.provenance.json +0 -0
  113. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/sumatra_362_cohesion.txt +0 -0
  114. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/university_6_17_no1_tx_excerpt.las +0 -0
  115. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +0 -0
  116. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ursa_308_xrd_mineralogy.provenance.json +0 -0
  117. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/ursa_308_xrd_mineralogy.txt +0 -0
  118. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/volve_15_9_19_sr_excerpt.las +0 -0
  119. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +0 -0
  120. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/volve_15_9_19a_core_excerpt.csv +0 -0
  121. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +0 -0
  122. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/volve_f12_f14_production_excerpt.csv +0 -0
  123. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/volve_f12_f14_production_excerpt.provenance.json +0 -0
  124. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/walvis_208_petm_carbonate.provenance.json +0 -0
  125. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/walvis_208_petm_carbonate.txt +0 -0
  126. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/woodlark_1109_rock_eval.provenance.json +0 -0
  127. {gea_program-0.17.1 → gea_program-0.19.0}/gea/catalog/woodlark_1109_rock_eval.txt +0 -0
  128. {gea_program-0.17.1 → gea_program-0.19.0}/gea/certificates.py +0 -0
  129. {gea_program-0.17.1 → gea_program-0.19.0}/gea/cli.py +0 -0
  130. {gea_program-0.17.1 → gea_program-0.19.0}/gea/client_reports.py +0 -0
  131. {gea_program-0.17.1 → gea_program-0.19.0}/gea/config_versioning.py +0 -0
  132. {gea_program-0.17.1 → gea_program-0.19.0}/gea/conformance.py +0 -0
  133. {gea_program-0.17.1 → gea_program-0.19.0}/gea/correlation.py +0 -0
  134. {gea_program-0.17.1 → gea_program-0.19.0}/gea/dashboard.py +0 -0
  135. {gea_program-0.17.1 → gea_program-0.19.0}/gea/deviation.py +0 -0
  136. {gea_program-0.17.1 → gea_program-0.19.0}/gea/downhole_engine.py +0 -0
  137. {gea_program-0.17.1 → gea_program-0.19.0}/gea/drift_monitor.py +0 -0
  138. {gea_program-0.17.1 → gea_program-0.19.0}/gea/earth_model.py +0 -0
  139. {gea_program-0.17.1 → gea_program-0.19.0}/gea/etp.py +0 -0
  140. {gea_program-0.17.1 → gea_program-0.19.0}/gea/etp12_schemas.json +0 -0
  141. {gea_program-0.17.1 → gea_program-0.19.0}/gea/example_register_map.json +0 -0
  142. {gea_program-0.17.1 → gea_program-0.19.0}/gea/fat_sat.py +0 -0
  143. {gea_program-0.17.1 → gea_program-0.19.0}/gea/files.py +0 -0
  144. {gea_program-0.17.1 → gea_program-0.19.0}/gea/follower.py +0 -0
  145. {gea_program-0.17.1 → gea_program-0.19.0}/gea/forward_model.py +0 -0
  146. {gea_program-0.17.1 → gea_program-0.19.0}/gea/gamma.py +0 -0
  147. {gea_program-0.17.1 → gea_program-0.19.0}/gea/gauge_aging.py +0 -0
  148. {gea_program-0.17.1 → gea_program-0.19.0}/gea/gauge_specs.py +0 -0
  149. {gea_program-0.17.1 → gea_program-0.19.0}/gea/geodesy.py +0 -0
  150. {gea_program-0.17.1 → gea_program-0.19.0}/gea/gravity_reference.py +0 -0
  151. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/TESTER_GUIDE.md +0 -0
  152. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/alarms.md +0 -0
  153. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/audit-update.md +0 -0
  154. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/conformance.md +0 -0
  155. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/data-in.md +0 -0
  156. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/doctor.md +0 -0
  157. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/drift.md +0 -0
  158. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/etp.md +0 -0
  159. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/files.md +0 -0
  160. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/instruments.md +0 -0
  161. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/notifications.md +0 -0
  162. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/osdu.md +0 -0
  163. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/patches.md +0 -0
  164. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/ppdm.md +0 -0
  165. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/quakeml.md +0 -0
  166. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/quality.md +0 -0
  167. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/seedlink.md +0 -0
  168. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/seismic.md +0 -0
  169. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/site.md +0 -0
  170. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/sites.md +0 -0
  171. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/sra.md +0 -0
  172. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/start.md +0 -0
  173. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/transients.md +0 -0
  174. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/upkeep.md +0 -0
  175. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/vibration.md +0 -0
  176. {gea_program-0.17.1 → gea_program-0.19.0}/gea/help/well-tests.md +0 -0
  177. {gea_program-0.17.1 → gea_program-0.19.0}/gea/housekeeping.py +0 -0
  178. {gea_program-0.17.1 → gea_program-0.19.0}/gea/inverse_engine.py +0 -0
  179. {gea_program-0.17.1 → gea_program-0.19.0}/gea/jobs.py +0 -0
  180. {gea_program-0.17.1 → gea_program-0.19.0}/gea/live_ports.py +0 -0
  181. {gea_program-0.17.1 → gea_program-0.19.0}/gea/loadtest.py +0 -0
  182. {gea_program-0.17.1 → gea_program-0.19.0}/gea/matplotlib_demo.py +0 -0
  183. {gea_program-0.17.1 → gea_program-0.19.0}/gea/modbus.py +0 -0
  184. {gea_program-0.17.1 → gea_program-0.19.0}/gea/model_card.py +0 -0
  185. {gea_program-0.17.1 → gea_program-0.19.0}/gea/mqtt_port.py +0 -0
  186. {gea_program-0.17.1 → gea_program-0.19.0}/gea/notify.py +0 -0
  187. {gea_program-0.17.1 → gea_program-0.19.0}/gea/opcua_port.py +0 -0
  188. {gea_program-0.17.1 → gea_program-0.19.0}/gea/operator_app.py +0 -0
  189. {gea_program-0.17.1 → gea_program-0.19.0}/gea/osdu.py +0 -0
  190. {gea_program-0.17.1 → gea_program-0.19.0}/gea/patches.py +0 -0
  191. {gea_program-0.17.1 → gea_program-0.19.0}/gea/permits.py +0 -0
  192. {gea_program-0.17.1 → gea_program-0.19.0}/gea/ports.py +0 -0
  193. {gea_program-0.17.1 → gea_program-0.19.0}/gea/ppdm.py +0 -0
  194. {gea_program-0.17.1 → gea_program-0.19.0}/gea/profile_catalog.py +0 -0
  195. {gea_program-0.17.1 → gea_program-0.19.0}/gea/project.py +0 -0
  196. {gea_program-0.17.1 → gea_program-0.19.0}/gea/qt6_downhole_app.py +0 -0
  197. {gea_program-0.17.1 → gea_program-0.19.0}/gea/quakeml.py +0 -0
  198. {gea_program-0.17.1 → gea_program-0.19.0}/gea/reconciler.py +0 -0
  199. {gea_program-0.17.1 → gea_program-0.19.0}/gea/reference/IU_ANMO_10_BHZ_response.provenance.json +0 -0
  200. {gea_program-0.17.1 → gea_program-0.19.0}/gea/reference/IU_ANMO_10_BHZ_response.xml +0 -0
  201. {gea_program-0.17.1 → gea_program-0.19.0}/gea/reference/libmseed_steim.provenance.json +0 -0
  202. {gea_program-0.17.1 → gea_program-0.19.0}/gea/reference/libmseed_steim1_4096.mseed +0 -0
  203. {gea_program-0.17.1 → gea_program-0.19.0}/gea/reference/libmseed_steim2_4096.mseed +0 -0
  204. {gea_program-0.17.1 → gea_program-0.19.0}/gea/reference/libmseed_steim2_512.mseed +0 -0
  205. {gea_program-0.17.1 → gea_program-0.19.0}/gea/rock_inventory.py +0 -0
  206. {gea_program-0.17.1 → gea_program-0.19.0}/gea/sample_record.py +0 -0
  207. {gea_program-0.17.1 → gea_program-0.19.0}/gea/sample_well_profile.csv +0 -0
  208. {gea_program-0.17.1 → gea_program-0.19.0}/gea/sbom.py +0 -0
  209. {gea_program-0.17.1 → gea_program-0.19.0}/gea/seedlink.py +0 -0
  210. {gea_program-0.17.1 → gea_program-0.19.0}/gea/segy.py +0 -0
  211. {gea_program-0.17.1 → gea_program-0.19.0}/gea/seismic.py +0 -0
  212. {gea_program-0.17.1 → gea_program-0.19.0}/gea/seismic_array.py +0 -0
  213. {gea_program-0.17.1 → gea_program-0.19.0}/gea/seismic_assoc.py +0 -0
  214. {gea_program-0.17.1 → gea_program-0.19.0}/gea/seismic_detect.py +0 -0
  215. {gea_program-0.17.1 → gea_program-0.19.0}/gea/seismic_film.py +0 -0
  216. {gea_program-0.17.1 → gea_program-0.19.0}/gea/seismic_harmonic.py +0 -0
  217. {gea_program-0.17.1 → gea_program-0.19.0}/gea/seismic_qc.py +0 -0
  218. {gea_program-0.17.1 → gea_program-0.19.0}/gea/seismic_response.py +0 -0
  219. {gea_program-0.17.1 → gea_program-0.19.0}/gea/seismic_signature.py +0 -0
  220. {gea_program-0.17.1 → gea_program-0.19.0}/gea/seismic_track.py +0 -0
  221. {gea_program-0.17.1 → gea_program-0.19.0}/gea/seismic_uncertainty.py +0 -0
  222. {gea_program-0.17.1 → gea_program-0.19.0}/gea/seismic_unlisted.py +0 -0
  223. {gea_program-0.17.1 → gea_program-0.19.0}/gea/sensor_swap.py +0 -0
  224. {gea_program-0.17.1 → gea_program-0.19.0}/gea/service_life.py +0 -0
  225. {gea_program-0.17.1 → gea_program-0.19.0}/gea/shell.py +0 -0
  226. {gea_program-0.17.1 → gea_program-0.19.0}/gea/shut_in.py +0 -0
  227. {gea_program-0.17.1 → gea_program-0.19.0}/gea/sla_report.py +0 -0
  228. {gea_program-0.17.1 → gea_program-0.19.0}/gea/sra.py +0 -0
  229. {gea_program-0.17.1 → gea_program-0.19.0}/gea/store_forward.py +0 -0
  230. {gea_program-0.17.1 → gea_program-0.19.0}/gea/strata_join.py +0 -0
  231. {gea_program-0.17.1 → gea_program-0.19.0}/gea/supervisor.py +0 -0
  232. {gea_program-0.17.1 → gea_program-0.19.0}/gea/survey_cmd.py +0 -0
  233. {gea_program-0.17.1 → gea_program-0.19.0}/gea/survey_view.py +0 -0
  234. {gea_program-0.17.1 → gea_program-0.19.0}/gea/telemetry.py +0 -0
  235. {gea_program-0.17.1 → gea_program-0.19.0}/gea/tool_library.py +0 -0
  236. {gea_program-0.17.1 → gea_program-0.19.0}/gea/transient.py +0 -0
  237. {gea_program-0.17.1 → gea_program-0.19.0}/gea/vibration.py +0 -0
  238. {gea_program-0.17.1 → gea_program-0.19.0}/gea/well_assembler.py +0 -0
  239. {gea_program-0.17.1 → gea_program-0.19.0}/gea/well_test_validation.py +0 -0
  240. {gea_program-0.17.1 → gea_program-0.19.0}/gea/wits0.py +0 -0
  241. {gea_program-0.17.1 → gea_program-0.19.0}/gea/witsml.py +0 -0
  242. {gea_program-0.17.1 → gea_program-0.19.0}/gea_program.egg-info/dependency_links.txt +0 -0
  243. {gea_program-0.17.1 → gea_program-0.19.0}/gea_program.egg-info/entry_points.txt +0 -0
  244. {gea_program-0.17.1 → gea_program-0.19.0}/gea_program.egg-info/requires.txt +0 -0
  245. {gea_program-0.17.1 → gea_program-0.19.0}/gea_program.egg-info/top_level.txt +0 -0
  246. {gea_program-0.17.1 → gea_program-0.19.0}/setup.cfg +0 -0
  247. {gea_program-0.17.1 → gea_program-0.19.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.1
3
+ Version: 0.19.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 (344 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]")
@@ -710,6 +710,24 @@ after the report was written - and "Update every report from its source"
710
710
  dashboard for every well, then every seismic station, errors collected and
711
711
  listed, never hidden.
712
712
 
713
+ ### The month-end packet
714
+
715
+ A client's month was five or six commands and a walk through the reports
716
+ folder. `gea workspace --path C:\site --action month-end --period 2026-09`
717
+ (or `--period previous`) runs the refresh for the period and collects every
718
+ report the program writes - each well's drift report, alarm month, well-test
719
+ validation, shut-in report, data resilience, machine vibration and the
720
+ certificate register's ISO/IEC 17025 7.8 conformity; each site's SRA packet
721
+ for the period and site report; every seismic station and track; the
722
+ accuracy statement, the SLA month, the FAT/SAT protocol, the model cards and
723
+ the SBOM - into `reports/month_end/<period>/` with `INDEX` (html, md, json:
724
+ every item INCLUDED with its files or NOT AVAILABLE with the reason that
725
+ names what the site lacks; every failed step; OPEN for a month that has not
726
+ ended) and `MANIFEST.json` (every file's SHA-256), zipped beside it. Nothing
727
+ is recomputed in the index. On the Reports page: the packets on record,
728
+ Assemble as a job, and Schedule monthly (day and hour, the previous month).
729
+ `gea help month-end`.
730
+
713
731
  ### Before a site goes live
714
732
 
715
733
  `deploy/README.md` is the checklist: keep the service on the loopback
@@ -722,6 +740,14 @@ headers on every response, and `gea housekeeping --workspace C:\site --apply`
722
740
  than truncating them and prunes finished job folders and old recordings. `gea
723
741
  loadtest --patches 16 --seconds 60 --with-service` tells you whether the site
724
742
  machine carries the patches it will be given, with the page still answering.
743
+ `gea backup --workspace C:\site --out D:\gea-backups --keep 14` copies the
744
+ whole site into one dated archive with a manifest of every file's hash, onto
745
+ another disk or a share; schedule it daily from Audit / Update, verify an
746
+ archive with `--verify`, and restore one onto an empty folder with
747
+ `--restore ... --to` and open it once before the first well is live - the
748
+ records are the product, and a backup nobody has restored is a hope. The
749
+ doctor warns when there has never been a backup or the last is more than a
750
+ day old. `gea help backup`.
725
751
 
726
752
  ## The standalone install kit
727
753
 
@@ -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 (344 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]")
@@ -671,6 +671,24 @@ after the report was written - and "Update every report from its source"
671
671
  dashboard for every well, then every seismic station, errors collected and
672
672
  listed, never hidden.
673
673
 
674
+ ### The month-end packet
675
+
676
+ A client's month was five or six commands and a walk through the reports
677
+ folder. `gea workspace --path C:\site --action month-end --period 2026-09`
678
+ (or `--period previous`) runs the refresh for the period and collects every
679
+ report the program writes - each well's drift report, alarm month, well-test
680
+ validation, shut-in report, data resilience, machine vibration and the
681
+ certificate register's ISO/IEC 17025 7.8 conformity; each site's SRA packet
682
+ for the period and site report; every seismic station and track; the
683
+ accuracy statement, the SLA month, the FAT/SAT protocol, the model cards and
684
+ the SBOM - into `reports/month_end/<period>/` with `INDEX` (html, md, json:
685
+ every item INCLUDED with its files or NOT AVAILABLE with the reason that
686
+ names what the site lacks; every failed step; OPEN for a month that has not
687
+ ended) and `MANIFEST.json` (every file's SHA-256), zipped beside it. Nothing
688
+ is recomputed in the index. On the Reports page: the packets on record,
689
+ Assemble as a job, and Schedule monthly (day and hour, the previous month).
690
+ `gea help month-end`.
691
+
674
692
  ### Before a site goes live
675
693
 
676
694
  `deploy/README.md` is the checklist: keep the service on the loopback
@@ -683,6 +701,14 @@ headers on every response, and `gea housekeeping --workspace C:\site --apply`
683
701
  than truncating them and prunes finished job folders and old recordings. `gea
684
702
  loadtest --patches 16 --seconds 60 --with-service` tells you whether the site
685
703
  machine carries the patches it will be given, with the page still answering.
704
+ `gea backup --workspace C:\site --out D:\gea-backups --keep 14` copies the
705
+ whole site into one dated archive with a manifest of every file's hash, onto
706
+ another disk or a share; schedule it daily from Audit / Update, verify an
707
+ archive with `--verify`, and restore one onto an empty folder with
708
+ `--restore ... --to` and open it once before the first well is live - the
709
+ records are the product, and a backup nobody has restored is a hope. The
710
+ doctor warns when there has never been a backup or the last is more than a
711
+ day old. `gea help backup`.
686
712
 
687
713
  ## The standalone install kit
688
714
 
@@ -123,7 +123,7 @@ from .profile_catalog import (
123
123
  read_pangaea_txt,
124
124
  )
125
125
 
126
- __version__ = "0.17.1"
126
+ __version__ = "0.19.0"
127
127
  __all__ = [
128
128
  "aging_rate", "rate_psi_yr", "accuracy_psi",
129
129
  "Sensor", "SimulatorConfig", "DownholeEngine",
@@ -285,7 +285,7 @@ def main(argv=None) -> int:
285
285
  "add-seismic", "refresh-seismic", "remove-seismic", "sar-film", "refresh-all", "add-track", "refresh-track", "remove-track",
286
286
  "seismic-dataset", "seismic-field", "add-site", "sites", "site-report", "remove-site", "rename",
287
287
  "set-well", "sra-define", "sra-report", "associate", "osdu-export", "quakeml-export", "well-identity", "vibration-report",
288
- "add-live-station", "live-fold", "live-run"])
288
+ "add-live-station", "live-fold", "live-run", "month-end"])
289
289
  p_ws.add_argument("--stations", type=str, nargs="+", default=None, help="add-track: two or more array station ids of this workspace")
290
290
  p_ws.add_argument("--site", type=str, default=None, help="site-report/remove-site: the site id")
291
291
  p_ws.add_argument("--api", type=str, default=None, help="set-well: the API number")
@@ -307,7 +307,7 @@ def main(argv=None) -> int:
307
307
  p_ws.add_argument("--checkpoint-months", type=int, default=3, help="sra-define: months between checkpoints with Commission staff (3)")
308
308
  p_ws.add_argument("--formation-boundary", type=str, default="base of the Wolfcamp", help="sra-define: the stratigraphic boundary between the two tiers")
309
309
  p_ws.add_argument("--triggering-event", type=str, default="", help="sra-define: the catalogue id of the event the plan answers")
310
- p_ws.add_argument("--catalog", type=str, default=None, help="sra-report: a TexNet earthquake-catalogue export (CSV)")
310
+ p_ws.add_argument("--catalog", type=str, default=None, help="sra-report / month-end: a TexNet earthquake-catalogue export (CSV)")
311
311
  p_ws.add_argument("--aftershocks", type=str, nargs="*", default=None, help="sra-report: catalogue ids the operator declares as aftershocks (exempt)")
312
312
  p_ws.add_argument("--start", type=str, default=None, help="sra-report: first day of the record, YYYY-MM-DD")
313
313
  p_ws.add_argument("--end", type=str, default=None, help="sra-report: last day of the record, YYYY-MM-DD")
@@ -359,6 +359,8 @@ def main(argv=None) -> int:
359
359
  p_ws.add_argument("--seedlink-config", type=str, default=None, help="add-live-station: the SeedLink config JSON (gea seedlink --write-example-config)")
360
360
  p_ws.add_argument("--include-open", action="store_true", help="live-fold: fold the current day's files too (they are still being written)")
361
361
  p_ws.add_argument("--seconds", type=float, default=None, help="live-run: how long to run (default: until Ctrl+C)")
362
+ p_ws.add_argument("--period", type=str, default="previous", help="month-end: the month, YYYY-MM, or 'previous' (the last closed month)")
363
+ p_ws.add_argument("--no-refresh", action="store_true", help="month-end: collect the reports as they stand without refreshing first")
362
364
  p_ws.add_argument("--name", type=str, default=None, help="init: site name; add-*: display name")
363
365
  p_ws.add_argument("--file", type=str, default=None, help="add-file: the client's data file; add-live: the port configuration JSON")
364
366
  p_ws.add_argument("--entry", type=str, default=None, help="add-catalog: catalogue entry")
@@ -411,6 +413,16 @@ def main(argv=None) -> int:
411
413
  p_es.add_argument("--frames", type=int, default=600)
412
414
  p_es.add_argument("--interval", type=float, default=1.0, help="seconds between samples")
413
415
  p_es.add_argument("--seed", type=int, default=1)
416
+ 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")
417
+ p_bk.add_argument("--workspace", type=str, default=None, help="the site folder (for a backup, or --status)")
418
+ 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")
419
+ p_bk.add_argument("--keep", type=int, default=None, help="keep only the newest N archives of this site in --out")
420
+ p_bk.add_argument("--label", type=str, default="", help="a note carried in the manifest (e.g. 'before the gauge swap')")
421
+ p_bk.add_argument("--verify", type=str, default=None, help="check this archive against its manifest and its .sha256")
422
+ p_bk.add_argument("--restore", type=str, default=None, help="restore this archive ...")
423
+ p_bk.add_argument("--to", type=str, default=None, help="... onto this empty folder")
424
+ p_bk.add_argument("--status", action="store_true", help="when the last backup of --workspace was taken, and whether that is too long ago")
425
+ p_bk.add_argument("--actor", type=str, default="cli")
414
426
  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
427
  p_sl.add_argument("--config", type=str, default=None, help="the SeedLink config JSON (host, port, streams)")
416
428
  p_sl.add_argument("--write-example-config", type=str, default=None, help="write an example config here and exit")
@@ -1934,6 +1946,10 @@ def main(argv=None) -> int:
1934
1946
  raise SystemExit("well-identity needs --well <id>")
1935
1947
  r = ws.well_identity(a.well)
1936
1948
  print(_ppdm_text(r)); return 0
1949
+ elif a.action == "month-end":
1950
+ from .month_end import report_text as _me_text
1951
+ r = ws.month_end(a.period, actor=a.actor, refresh=not a.no_refresh, catalog_csv=a.catalog)
1952
+ print(_me_text(r)); return 0 if not r["errors"] else 1
1937
1953
  elif a.action == "add-live-station":
1938
1954
  if not a.seedlink_config or not a.name:
1939
1955
  raise SystemExit("add-live-station needs --name <display> and --seedlink-config <json>")
@@ -2017,6 +2033,24 @@ def main(argv=None) -> int:
2017
2033
  stop.set()
2018
2034
  print("wits0-sim: stopped")
2019
2035
  return 0
2036
+ elif a.cmd == "backup":
2037
+ from . import backup as _B
2038
+ try:
2039
+ if a.verify:
2040
+ r = _B.verify(a.verify); print(_B.report_text(r, "verify")); return 0 if r["status"] == "OK" else 1
2041
+ if a.restore:
2042
+ if not a.to:
2043
+ raise SystemExit("backup --restore needs --to <empty folder>")
2044
+ print(_B.report_text(_B.restore(a.restore, a.to, a.actor), "restore")); return 0
2045
+ if not a.workspace:
2046
+ raise SystemExit("backup needs --workspace <site folder> (with --out <another disk>), or --verify / --restore an archive")
2047
+ if a.status:
2048
+ r = _B.status(a.workspace); print(_B.report_text(r, "status")); return 0 if not (r["never"] or r["stale"]) else 1
2049
+ if not a.out:
2050
+ raise SystemExit("backup needs --out <a folder on another disk or a share>")
2051
+ print(_B.report_text(_B.make(a.workspace, a.out, a.actor, keep=a.keep, label=a.label), "make")); return 0
2052
+ except (ValueError, OSError) as e:
2053
+ raise SystemExit(f"backup: {e}")
2020
2054
  elif a.cmd == "seedlink":
2021
2055
  import json as _json
2022
2056
  from . import seedlink as _L
@@ -5719,6 +5719,283 @@ 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
+
5875
+
5876
+ def section_bm_month_end(tmp: str) -> None:
5877
+ """Section BM - the month-end packet: the refresh for the period, every report collected with INCLUDED or NOT
5878
+ AVAILABLE and the reason, the index and the manifest, the archive, the records; the page, the job and the
5879
+ monthly schedule."""
5880
+ import http.cookiejar
5881
+ import subprocess as _sp
5882
+ import urllib.request
5883
+ import urllib.error
5884
+ import zipfile
5885
+ from . import month_end as ME
5886
+ from . import helplib as H
5887
+ from .telemetry import TelemetryRecorder, TelemetryConfig
5888
+ from .downhole_engine import DownholeEngine, SimulatorConfig
5889
+ from .workspace import Workspace
5890
+ from .service import Service, Users
5891
+ pkg = Path(__file__).parent
5892
+ d = Path(tmp, "bm"); d.mkdir()
5893
+ hist = str(d / "hist.csv")
5894
+ TelemetryRecorder(engine=DownholeEngine(SimulatorConfig()), config=TelemetryConfig(duration_hours=2.0, seed=5)).run().export_csv(hist)
5895
+ wsp = str(d / "ws"); ws = Workspace.create(wsp, "Pad 3", actor="tester")
5896
+ w = ws.add_well_file(hist, display="SWD 1", actor="tester")
5897
+ site = ws.add_site("Pad 3 site", wells=[w["id"]], actor="tester")
5898
+ bad_period = None
5899
+ try:
5900
+ ME.period_bounds("2026-9")
5901
+ except ValueError as e:
5902
+ bad_period = str(e)
5903
+ pb = ME.period_bounds("2026-02")
5904
+ rec = ME.assemble(ws, "2026-09", actor="tester")
5905
+ idx = json.loads((Path(rec["folder"]) / "INDEX.json").read_text(encoding="utf-8"))
5906
+ man = json.loads((Path(rec["folder"]) / "MANIFEST.json").read_text(encoding="utf-8"))
5907
+ by_name = {i["name"]: i for i in idx["items"]}
5908
+ v_folder = ME.verify(rec["folder"]); v_zip = ME.verify(rec["archive"])
5909
+ with zipfile.ZipFile(rec["archive"]) as z:
5910
+ znames = set(z.namelist())
5911
+ # a second assembly of the same period rebuilds whole; a future month is OPEN
5912
+ rec2 = ME.assemble(ws, "2026-09", actor="tester", refresh=False)
5913
+ from datetime import datetime as _dt, timezone as _tz
5914
+ now = _dt.now(_tz.utc)
5915
+ cur = f"{now.year:04d}-{now.month:02d}"
5916
+ rec3 = ME.assemble(ws, cur, actor="tester", refresh=False)
5917
+ pk = ME.packets(ws)
5918
+ audit = [a["action"] for a in ws.audit_log()]
5919
+ c1 = {
5920
+ "period_checked": bool(bad_period) and "YYYY-MM" in bad_period and pb["days"] == 28 and pb["start"] == "2026-02-01T00:00:00Z" and pb["end"] == "2026-02-28T23:59:59Z" and pb["closed"] is True,
5921
+ "included": by_name["SWD 1: gauge drift report"]["status"] == "INCLUDED" and by_name["SWD 1: alarm event report"]["status"] == "INCLUDED"
5922
+ and by_name["SLA month 2026-09"]["status"] == "INCLUDED" and by_name["Accuracy statement"]["status"] == "INCLUDED"
5923
+ and by_name["Pad 3 site: site report"]["status"] == "INCLUDED" and by_name["Model cards and SBOM"]["status"] == "INCLUDED",
5924
+ "not_available_with_reason": by_name["SWD 1: machine vibration"]["status"] == "NOT AVAILABLE" and "no machine record" in by_name["SWD 1: machine vibration"]["reason"]
5925
+ and by_name["Pad 3 site: SRA packet for 2026-09"]["status"] == "NOT AVAILABLE" and "no Seismicity Response Area" in by_name["Pad 3 site: SRA packet for 2026-09"]["reason"]
5926
+ and "no certificate register" in by_name["SWD 1: certificate conformity (ISO/IEC 17025 7.8)"]["reason"],
5927
+ "files_hashed": all(f["sha256"] and f["bytes"] > 0 for i in idx["items"] for f in i["files"]) and man["n_files"] == len(man["files"]) and man["n_files"] >= 20
5928
+ and "wells/SWD-1/gauge_drift_report.html" in man["files"] and "site/sla_report_2026-09.html" in man["files"] and "INDEX.html" in man["files"],
5929
+ "record": rec["included"] == len([i for i in idx["items"] if i["status"] == "INCLUDED"]) and rec["closed"] is True and rec["errors"] == [] and os.path.isfile(rec["archive"])
5930
+ and os.path.isfile(rec["archive"] + "") and rec["archive"].endswith("month_end_Pad-3_2026-09.zip"),
5931
+ "verify": v_folder["status"] == "OK" and v_zip["status"] == "OK" and v_zip["n_files"] == man["n_files"] and "MANIFEST.json" in znames and "INDEX.html" in znames,
5932
+ "rebuilt_whole_and_open": rec2["generated_utc"] >= rec["generated_utc"] and rec2["refresh"] is False and rec3["closed"] is False and "OPEN" in (Path(rec3["folder"]) / "INDEX.md").read_text(encoding="utf-8")
5933
+ and len(pk) == 3 and all(p["present"] for p in pk) and pk[-1]["period"] == cur,
5934
+ "audited": audit.count("month_end.assemble") == 3 and "dashboard.refresh" in audit and "site.report" in audit,
5935
+ "not_a_measurement": "does not call a measurement" in (Path(rec["folder"]) / "INDEX.md").read_text(encoding="utf-8"),
5936
+ }
5937
+ failed1 = [k for k, v in c1.items() if not v]
5938
+ ok(not failed1,
5939
+ "BM1 the month-end packet: the period checked (YYYY-MM; February's 28 days; a past month closed), the refresh run for it, every report "
5940
+ "collected - the well's drift and alarm reports, the SLA month, the accuracy statement, the site report, the model cards - INCLUDED with "
5941
+ "hashed files, and the vibration report, the SRA packet and the certificate conformity NOT AVAILABLE with the reason that names what the "
5942
+ "site lacks; INDEX and MANIFEST written, the archive verifies against the manifest, a second assembly rebuilds the period whole, the "
5943
+ "current month is marked OPEN, every packet on record, every assembly in the audit"
5944
+ + (f" [failed: {', '.join(failed1)}]" if failed1 else ""))
5945
+
5946
+ # -- the page, the job, the schedule, the CLI, the help --------------------------------------------------------
5947
+ users = Users(str(Path(wsp, "users.json"))); users.add("op", "operator-pass-1", "operator"); users.add("adm", "admin-pass-1", "admin")
5948
+ svc = Service(wsp, port=0, scheduler=False).start()
5949
+ base = svc.url.rstrip("/")
5950
+ opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
5951
+
5952
+ def call(method, path, body=None):
5953
+ req = urllib.request.Request(base + path, method=method, data=(json.dumps(body).encode() if body is not None else None))
5954
+ req.add_header("Content-Type", "application/json"); req.add_header("X-GEA-Action", "1")
5955
+ try:
5956
+ with opener.open(req, timeout=300) as rr_:
5957
+ return rr_.status, json.loads(rr_.read())
5958
+ except urllib.error.HTTPError as e:
5959
+ return e.code, json.loads(e.read())
5960
+ try:
5961
+ call("POST", "/api/login", {"name": "op", "password": "operator-pass-1"})
5962
+ st_l, lst = call("GET", "/api/month-end")
5963
+ st_bad = call("POST", "/api/month-end/run", {"period": "Sept 2026"})[0]
5964
+ st_sched_op = call("POST", "/api/month-end/schedule", {"day": 1, "at": "03:00"})[0]
5965
+ st_run, job = call("POST", "/api/month-end/run", {"period": "2026-08", "no_refresh": True})
5966
+ done = svc.app.runner.wait(job["id"], 300)
5967
+ st_l2, lst2 = call("GET", "/api/month-end")
5968
+ req = urllib.request.Request(base + "/reports/month_end/2026-08/INDEX.html"); req.add_header("X-GEA-Action", "1")
5969
+ with opener.open(req, timeout=60) as rr_:
5970
+ html_ok = rr_.status == 200 and b"Month-End Packet" in rr_.read()
5971
+ call("POST", "/api/login", {"name": "adm", "password": "admin-pass-1"})
5972
+ st_sched_bad = call("POST", "/api/month-end/schedule", {"day": 31, "at": "03:00"})[0]
5973
+ st_sched, sch = call("POST", "/api/month-end/schedule", {"day": 2, "at": "03:15"})
5974
+ st_l3, lst3 = call("GET", "/api/month-end")
5975
+ finally:
5976
+ svc.stop()
5977
+ page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
5978
+ help_md = (pkg / "help" / "month-end.md").read_text(encoding="utf-8")
5979
+ r_cli = _sp.run([sys.executable, "-m", "gea", "workspace", "--path", wsp, "--action", "month-end", "--period", "2026-07", "--no-refresh"], capture_output=True, text=True)
5980
+ r_prev = _sp.run([sys.executable, "-m", "gea", "workspace", "--path", wsp, "--action", "month-end", "--no-refresh"], capture_output=True, text=True)
5981
+ c2 = {
5982
+ "list": st_l == 200 and len(lst["packets"]) == 3 and lst["previous"] == ME.previous_period() and lst["schedule"] is None,
5983
+ "refusals": st_bad == 400 and st_sched_op == 403 and st_sched_bad == 400,
5984
+ "job": st_run == 200 and done["status"] == "DONE" and st_l2 == 200 and any(p["period"] == "2026-08" and p["present"] for p in lst2["packets"]) and html_ok,
5985
+ "schedule": st_sched == 200 and sch["name"] == "month-end-monthly" and sch["monthly_day"] == 2 and sch["daily_at"] == "03:15" and "previous" in sch["args"]
5986
+ and st_l3 == 200 and lst3["schedule"]["name"] == "month-end-monthly",
5987
+ "page": "Month-end packet" in page_src and "/api/month-end/run" in page_src and "/api/month-end/schedule" in page_src,
5988
+ "cli": r_cli.returncode == 0 and "month-end 2026-07 (closed)" in r_cli.stdout and r_prev.returncode == 0 and f"month-end {ME.previous_period()} (closed)" in r_prev.stdout,
5989
+ "help": any(t["topic"] == "month-end" for t in H.topics()) and not H.check("month-end") and "does not recompute" in help_md.split("will not call a measurement")[1],
5990
+ }
5991
+ failed2 = [k for k, v in c2.items() if not v]
5992
+ ok(not failed2,
5993
+ "BM2 the Reports page carries the packet card: the packets on record with their index and archive, the period defaulting to the last closed "
5994
+ "month, Assemble as a job (a malformed period refused) whose INDEX the page then serves, Schedule monthly for an administrator only (day 1..28) "
5995
+ "as the `month-end-monthly` entry for the previous month; `gea workspace --action month-end` with a period or with 'previous'; the help page "
5996
+ "says the index collects and does not recompute"
5997
+ + (f" [failed: {', '.join(failed2)}; cli={r_cli.stdout[-200:]!r} {r_cli.stderr[-200:]!r}]" if failed2 else ""))
5998
+
5722
5999
  def main() -> int:
5723
6000
  print("GEA-Program - ACCEPTANCE SUITE (the product gate)")
5724
6001
  # a handle a job subprocess still holds at the end must not turn a finished gate into a traceback on Windows:
@@ -5785,6 +6062,8 @@ def main() -> int:
5785
6062
  section_bi_conformance(tmp)
5786
6063
  section_bj_etp(tmp)
5787
6064
  section_bk_seedlink(tmp)
6065
+ section_bl_backup(tmp)
6066
+ section_bm_month_end(tmp)
5788
6067
  if _FAILS:
5789
6068
  print(f"[ACCEPTANCE] {len(_FAILS)} FAILURES ({_PASS} passed):")
5790
6069
  for f in _FAILS: