gea-program 0.47.0__tar.gz → 0.48.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 (342) hide show
  1. {gea_program-0.47.0 → gea_program-0.48.0}/PKG-INFO +14 -1
  2. {gea_program-0.47.0 → gea_program-0.48.0}/README.md +13 -0
  3. {gea_program-0.47.0 → gea_program-0.48.0}/gea/__init__.py +1 -1
  4. {gea_program-0.47.0 → gea_program-0.48.0}/gea/__main__.py +13 -3
  5. {gea_program-0.47.0 → gea_program-0.48.0}/gea/acceptance_tests.py +201 -0
  6. {gea_program-0.47.0 → gea_program-0.48.0}/gea/alerts.py +20 -2
  7. {gea_program-0.47.0 → gea_program-0.48.0}/gea/declarations.py +136 -2
  8. {gea_program-0.47.0 → gea_program-0.48.0}/gea/dossier.py +36 -2
  9. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/alerts.md +1 -1
  10. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/declare.md +2 -2
  11. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/dossier.md +2 -0
  12. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/invoice-check.md +1 -1
  13. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/pad-summary.md +1 -1
  14. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/rig-time.md +3 -1
  15. {gea_program-0.47.0 → gea_program-0.48.0}/gea/invoice_check.py +25 -7
  16. {gea_program-0.47.0 → gea_program-0.48.0}/gea/notify.py +3 -1
  17. {gea_program-0.47.0 → gea_program-0.48.0}/gea/pad_summary.py +41 -4
  18. {gea_program-0.47.0 → gea_program-0.48.0}/gea/rig_state.py +144 -8
  19. {gea_program-0.47.0 → gea_program-0.48.0}/gea/service.py +17 -1
  20. {gea_program-0.47.0 → gea_program-0.48.0}/gea/web/app.html +15 -7
  21. gea_program-0.48.0/gea/xlsx_read.py +205 -0
  22. {gea_program-0.47.0 → gea_program-0.48.0}/gea_program.egg-info/PKG-INFO +14 -1
  23. {gea_program-0.47.0 → gea_program-0.48.0}/gea_program.egg-info/SOURCES.txt +1 -0
  24. {gea_program-0.47.0 → gea_program-0.48.0}/pyproject.toml +1 -1
  25. {gea_program-0.47.0 → gea_program-0.48.0}/LICENSE +0 -0
  26. {gea_program-0.47.0 → gea_program-0.48.0}/gea/BENCH_TEST_PROTOCOL.md +0 -0
  27. {gea_program-0.47.0 → gea_program-0.48.0}/gea/accuracy_statement.py +0 -0
  28. {gea_program-0.47.0 → gea_program-0.48.0}/gea/alarm_engine.py +0 -0
  29. {gea_program-0.47.0 → gea_program-0.48.0}/gea/backup.py +0 -0
  30. {gea_program-0.47.0 → gea_program-0.48.0}/gea/bench.py +0 -0
  31. {gea_program-0.47.0 → gea_program-0.48.0}/gea/blind_harness.py +0 -0
  32. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/acex_lomonosov_age_depth_model.provenance.json +0 -0
  33. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/acex_lomonosov_age_depth_model.txt +0 -0
  34. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/agassiz77_canada_temperature.csv +0 -0
  35. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/agassiz77_canada_temperature.provenance.json +0 -0
  36. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/barbados_110_consolidation.provenance.json +0 -0
  37. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/barbados_110_consolidation.txt +0 -0
  38. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/bengal_u1452_grain_size.provenance.json +0 -0
  39. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/bengal_u1452_grain_size.txt +0 -0
  40. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/blake_164_methane_isotopes.provenance.json +0 -0
  41. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/blake_164_methane_isotopes.txt +0 -0
  42. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +0 -0
  43. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.txt +0 -0
  44. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/collingwood_1_28_ks_complete.las +0 -0
  45. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/collingwood_1_28_ks_complete.provenance.json +0 -0
  46. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/costa_rica_odp_friction_envelope.provenance.json +0 -0
  47. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/costa_rica_odp_friction_envelope.txt +0 -0
  48. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +0 -0
  49. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +0 -0
  50. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/dsdp_504b_physical_properties.provenance.json +0 -0
  51. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/dsdp_504b_physical_properties.txt +0 -0
  52. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/dsdp_504b_sound_velocity.provenance.json +0 -0
  53. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/dsdp_504b_sound_velocity.txt +0 -0
  54. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/elgygytgyn_5011_turbidites.provenance.json +0 -0
  55. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/elgygytgyn_5011_turbidites.txt +0 -0
  56. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/epica_domec_co2_800kyr.provenance.json +0 -0
  57. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/epica_domec_co2_800kyr.txt +0 -0
  58. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/fram_909_organic_petrography.provenance.json +0 -0
  59. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/fram_909_organic_petrography.txt +0 -0
  60. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/gbr_325_coral_uth_ages.provenance.json +0 -0
  61. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/gbr_325_coral_uth_ages.txt +0 -0
  62. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/gisp2_greenland_temperature.csv +0 -0
  63. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/gisp2_greenland_temperature.provenance.json +0 -0
  64. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/gom_308_t2p_insitu.provenance.json +0 -0
  65. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/gom_308_t2p_insitu.txt +0 -0
  66. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/guaymas_385_dom_d13c.provenance.json +0 -0
  67. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/guaymas_385_dom_d13c.txt +0 -0
  68. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +0 -0
  69. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/hikurangi_u1520_friction_insitu.txt +0 -0
  70. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +0 -0
  71. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +0 -0
  72. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/iodp_u1324_pore_pressure.provenance.json +0 -0
  73. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/iodp_u1324_pore_pressure.txt +0 -0
  74. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/jfast_c0019_slow_slip_events.provenance.json +0 -0
  75. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/jfast_c0019_slow_slip_events.txt +0 -0
  76. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/kennetcook_2_p129_excerpt.las +0 -0
  77. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/kennetcook_2_p129_excerpt.provenance.json +0 -0
  78. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_bhgm_density.dat +0 -0
  79. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_bhgm_density.provenance.json +0 -0
  80. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +0 -0
  81. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_complog_6020_excerpt.txt +0 -0
  82. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_hlog246_temperature.dat +0 -0
  83. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_hlog246_temperature.provenance.json +0 -0
  84. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_rockmech_compress.dat +0 -0
  85. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_rockmech_compress.provenance.json +0 -0
  86. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +0 -0
  87. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +0 -0
  88. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ktb_vb_rockmech_compress.dat +0 -0
  89. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ktb_vb_rockmech_compress.provenance.json +0 -0
  90. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ktb_vb_vlog251_temperature.dat +0 -0
  91. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ktb_vb_vlog251_temperature.provenance.json +0 -0
  92. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/l06_06_nl_survey.csv +0 -0
  93. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/l06_06_nl_survey.provenance.json +0 -0
  94. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/l07_01_nl_excerpt.las +0 -0
  95. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/l07_01_nl_excerpt.provenance.json +0 -0
  96. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +0 -0
  97. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/mariana_1200_serpentinite_geochem.txt +0 -0
  98. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/med_160_sapropels.provenance.json +0 -0
  99. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/med_160_sapropels.txt +0 -0
  100. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/nankai_megasplay_shear_strength.provenance.json +0 -0
  101. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/nankai_megasplay_shear_strength.txt +0 -0
  102. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/odp_1027b_thermal_conductivity.provenance.json +0 -0
  103. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/odp_1027b_thermal_conductivity.txt +0 -0
  104. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/odp_1027c_cork_temperature.provenance.json +0 -0
  105. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/odp_1027c_cork_temperature.txt +0 -0
  106. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/odp_1165b_thermal_conductivity.provenance.json +0 -0
  107. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/odp_1165b_thermal_conductivity.txt +0 -0
  108. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +0 -0
  109. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.txt +0 -0
  110. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +0 -0
  111. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/odp_504b_dike_elastic_moduli.txt +0 -0
  112. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +0 -0
  113. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/odp_504b_leg137_borehole_fluids.txt +0 -0
  114. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +0 -0
  115. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.txt +0 -0
  116. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/peru_201_sulfate_reduction.provenance.json +0 -0
  117. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/peru_201_sulfate_reduction.txt +0 -0
  118. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/scorpio_e1_sa_excerpt.las +0 -0
  119. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/scorpio_e1_sa_excerpt.provenance.json +0 -0
  120. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/sumatra_362_cohesion.provenance.json +0 -0
  121. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/sumatra_362_cohesion.txt +0 -0
  122. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/university_6_17_no1_tx_excerpt.las +0 -0
  123. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +0 -0
  124. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ursa_308_xrd_mineralogy.provenance.json +0 -0
  125. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/ursa_308_xrd_mineralogy.txt +0 -0
  126. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/volve_15_9_19_sr_excerpt.las +0 -0
  127. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +0 -0
  128. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/volve_15_9_19a_core_excerpt.csv +0 -0
  129. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +0 -0
  130. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/volve_f12_f14_production_excerpt.csv +0 -0
  131. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/volve_f12_f14_production_excerpt.provenance.json +0 -0
  132. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/walvis_208_petm_carbonate.provenance.json +0 -0
  133. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/walvis_208_petm_carbonate.txt +0 -0
  134. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/woodlark_1109_rock_eval.provenance.json +0 -0
  135. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog/woodlark_1109_rock_eval.txt +0 -0
  136. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog_map.py +0 -0
  137. {gea_program-0.47.0 → gea_program-0.48.0}/gea/catalog_register.py +0 -0
  138. {gea_program-0.47.0 → gea_program-0.48.0}/gea/certificates.py +0 -0
  139. {gea_program-0.47.0 → gea_program-0.48.0}/gea/cli.py +0 -0
  140. {gea_program-0.47.0 → gea_program-0.48.0}/gea/client_reports.py +0 -0
  141. {gea_program-0.47.0 → gea_program-0.48.0}/gea/config_versioning.py +0 -0
  142. {gea_program-0.47.0 → gea_program-0.48.0}/gea/conformance.py +0 -0
  143. {gea_program-0.47.0 → gea_program-0.48.0}/gea/control_room.py +0 -0
  144. {gea_program-0.47.0 → gea_program-0.48.0}/gea/correlation.py +0 -0
  145. {gea_program-0.47.0 → gea_program-0.48.0}/gea/dashboard.py +0 -0
  146. {gea_program-0.47.0 → gea_program-0.48.0}/gea/deviation.py +0 -0
  147. {gea_program-0.47.0 → gea_program-0.48.0}/gea/dlis.py +0 -0
  148. {gea_program-0.47.0 → gea_program-0.48.0}/gea/dnp3.py +0 -0
  149. {gea_program-0.47.0 → gea_program-0.48.0}/gea/doctor.py +0 -0
  150. {gea_program-0.47.0 → gea_program-0.48.0}/gea/dossier_verify.py +0 -0
  151. {gea_program-0.47.0 → gea_program-0.48.0}/gea/downhole_engine.py +0 -0
  152. {gea_program-0.47.0 → gea_program-0.48.0}/gea/drift_monitor.py +0 -0
  153. {gea_program-0.47.0 → gea_program-0.48.0}/gea/drill_string.py +0 -0
  154. {gea_program-0.47.0 → gea_program-0.48.0}/gea/earth_model.py +0 -0
  155. {gea_program-0.47.0 → gea_program-0.48.0}/gea/economics.py +0 -0
  156. {gea_program-0.47.0 → gea_program-0.48.0}/gea/ed25519.py +0 -0
  157. {gea_program-0.47.0 → gea_program-0.48.0}/gea/edr.py +0 -0
  158. {gea_program-0.47.0 → gea_program-0.48.0}/gea/elevation.py +0 -0
  159. {gea_program-0.47.0 → gea_program-0.48.0}/gea/etp.py +0 -0
  160. {gea_program-0.47.0 → gea_program-0.48.0}/gea/etp12_schemas.json +0 -0
  161. {gea_program-0.47.0 → gea_program-0.48.0}/gea/example_register_map.json +0 -0
  162. {gea_program-0.47.0 → gea_program-0.48.0}/gea/fat_sat.py +0 -0
  163. {gea_program-0.47.0 → gea_program-0.48.0}/gea/files.py +0 -0
  164. {gea_program-0.47.0 → gea_program-0.48.0}/gea/fixtures/README.md +0 -0
  165. {gea_program-0.47.0 → gea_program-0.48.0}/gea/fixtures/comcat_delaware_basin_2026-09.txt +0 -0
  166. {gea_program-0.47.0 → gea_program-0.48.0}/gea/fixtures/comcat_mahoning_1990-2026.txt +0 -0
  167. {gea_program-0.47.0 → gea_program-0.48.0}/gea/fixtures/comcat_youngstown_2011-2012.txt +0 -0
  168. {gea_program-0.47.0 → gea_program-0.48.0}/gea/fixtures/ne_110m_coastline.json +0 -0
  169. {gea_program-0.47.0 → gea_program-0.48.0}/gea/follower.py +0 -0
  170. {gea_program-0.47.0 → gea_program-0.48.0}/gea/forward_model.py +0 -0
  171. {gea_program-0.47.0 → gea_program-0.48.0}/gea/friction_priors.py +0 -0
  172. {gea_program-0.47.0 → gea_program-0.48.0}/gea/gamma.py +0 -0
  173. {gea_program-0.47.0 → gea_program-0.48.0}/gea/gauge_aging.py +0 -0
  174. {gea_program-0.47.0 → gea_program-0.48.0}/gea/gauge_specs.py +0 -0
  175. {gea_program-0.47.0 → gea_program-0.48.0}/gea/geodesy.py +0 -0
  176. {gea_program-0.47.0 → gea_program-0.48.0}/gea/gravity_reference.py +0 -0
  177. {gea_program-0.47.0 → gea_program-0.48.0}/gea/hartip.py +0 -0
  178. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/TESTER_GUIDE.md +0 -0
  179. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/alarms.md +0 -0
  180. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/audit-update.md +0 -0
  181. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/backup.md +0 -0
  182. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/catalog.md +0 -0
  183. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/charts.md +0 -0
  184. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/conformance.md +0 -0
  185. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/control-room.md +0 -0
  186. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/data-in.md +0 -0
  187. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/dlis.md +0 -0
  188. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/doctor.md +0 -0
  189. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/drift.md +0 -0
  190. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/drillstring.md +0 -0
  191. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/economics.md +0 -0
  192. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/edr.md +0 -0
  193. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/etp.md +0 -0
  194. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/field-ports.md +0 -0
  195. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/files.md +0 -0
  196. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/historian.md +0 -0
  197. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/instruments.md +0 -0
  198. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/interval.md +0 -0
  199. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/map.md +0 -0
  200. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/month-end.md +0 -0
  201. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/notifications.md +0 -0
  202. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/opseis.md +0 -0
  203. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/osdu.md +0 -0
  204. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/patches.md +0 -0
  205. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/penetrator.md +0 -0
  206. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/ppdm.md +0 -0
  207. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/quakeml.md +0 -0
  208. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/quality.md +0 -0
  209. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/report-history.md +0 -0
  210. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/seedlink.md +0 -0
  211. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/seismic.md +0 -0
  212. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/setup.md +0 -0
  213. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/simulation.md +0 -0
  214. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/site.md +0 -0
  215. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/sites.md +0 -0
  216. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/slip.md +0 -0
  217. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/soak.md +0 -0
  218. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/sra.md +0 -0
  219. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/start.md +0 -0
  220. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/structure.md +0 -0
  221. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/tools.md +0 -0
  222. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/transients.md +0 -0
  223. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/upkeep.md +0 -0
  224. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/vibration.md +0 -0
  225. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/well-report.md +0 -0
  226. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/well-tests.md +0 -0
  227. {gea_program-0.47.0 → gea_program-0.48.0}/gea/help/well3d.md +0 -0
  228. {gea_program-0.47.0 → gea_program-0.48.0}/gea/helplib.py +0 -0
  229. {gea_program-0.47.0 → gea_program-0.48.0}/gea/housekeeping.py +0 -0
  230. {gea_program-0.47.0 → gea_program-0.48.0}/gea/interval.py +0 -0
  231. {gea_program-0.47.0 → gea_program-0.48.0}/gea/inverse_engine.py +0 -0
  232. {gea_program-0.47.0 → gea_program-0.48.0}/gea/jobs.py +0 -0
  233. {gea_program-0.47.0 → gea_program-0.48.0}/gea/live_ports.py +0 -0
  234. {gea_program-0.47.0 → gea_program-0.48.0}/gea/loadtest.py +0 -0
  235. {gea_program-0.47.0 → gea_program-0.48.0}/gea/matplotlib_demo.py +0 -0
  236. {gea_program-0.47.0 → gea_program-0.48.0}/gea/modbus.py +0 -0
  237. {gea_program-0.47.0 → gea_program-0.48.0}/gea/modbus_rtu.py +0 -0
  238. {gea_program-0.47.0 → gea_program-0.48.0}/gea/model_card.py +0 -0
  239. {gea_program-0.47.0 → gea_program-0.48.0}/gea/month_end.py +0 -0
  240. {gea_program-0.47.0 → gea_program-0.48.0}/gea/mqtt_port.py +0 -0
  241. {gea_program-0.47.0 → gea_program-0.48.0}/gea/op_seismicity.py +0 -0
  242. {gea_program-0.47.0 → gea_program-0.48.0}/gea/opcua_port.py +0 -0
  243. {gea_program-0.47.0 → gea_program-0.48.0}/gea/operator_app.py +0 -0
  244. {gea_program-0.47.0 → gea_program-0.48.0}/gea/osdu.py +0 -0
  245. {gea_program-0.47.0 → gea_program-0.48.0}/gea/patches.py +0 -0
  246. {gea_program-0.47.0 → gea_program-0.48.0}/gea/pdf_report.py +0 -0
  247. {gea_program-0.47.0 → gea_program-0.48.0}/gea/penetrator.py +0 -0
  248. {gea_program-0.47.0 → gea_program-0.48.0}/gea/permits.py +0 -0
  249. {gea_program-0.47.0 → gea_program-0.48.0}/gea/piwebapi.py +0 -0
  250. {gea_program-0.47.0 → gea_program-0.48.0}/gea/ports.py +0 -0
  251. {gea_program-0.47.0 → gea_program-0.48.0}/gea/ppdm.py +0 -0
  252. {gea_program-0.47.0 → gea_program-0.48.0}/gea/profile_catalog.py +0 -0
  253. {gea_program-0.47.0 → gea_program-0.48.0}/gea/project.py +0 -0
  254. {gea_program-0.47.0 → gea_program-0.48.0}/gea/qt6_downhole_app.py +0 -0
  255. {gea_program-0.47.0 → gea_program-0.48.0}/gea/quakeml.py +0 -0
  256. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reconciler.py +0 -0
  257. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/IU_ANMO_10_BHZ_response.provenance.json +0 -0
  258. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/IU_ANMO_10_BHZ_response.xml +0 -0
  259. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/eia_henry_hub_daily.csv.gz +0 -0
  260. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/eia_prices.provenance.json +0 -0
  261. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/eia_wti_daily.csv.gz +0 -0
  262. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/libmseed_steim.provenance.json +0 -0
  263. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/libmseed_steim1_4096.mseed +0 -0
  264. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/libmseed_steim2_4096.mseed +0 -0
  265. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/libmseed_steim2_512.mseed +0 -0
  266. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/mahoning_example.json +0 -0
  267. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/mud_motor_specs.json +0 -0
  268. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/penetrator_ratings.json +0 -0
  269. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/seismicity_protocols.json +0 -0
  270. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/tool_sources.json +0 -0
  271. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/us_outline.json +0 -0
  272. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/us_outline.provenance.json +0 -0
  273. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/volve_3d.provenance.json +0 -0
  274. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/volve_f4_interpretation.las.gz +0 -0
  275. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/volve_f4_logs.provenance.json +0 -0
  276. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/volve_f4_lwd.las.gz +0 -0
  277. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/volve_surfaces.json.gz +0 -0
  278. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/volve_traj_F-10.xml.gz +0 -0
  279. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/volve_traj_F-4.xml.gz +0 -0
  280. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/volve_traj_F-5_12.25in.xml.gz +0 -0
  281. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/volve_traj_F-5_17.5in.xml.gz +0 -0
  282. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/volve_traj_F-5_36in.xml.gz +0 -0
  283. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/volve_traj_F-5_8.5in.xml.gz +0 -0
  284. {gea_program-0.47.0 → gea_program-0.48.0}/gea/reference/volve_well_picks.csv.gz +0 -0
  285. {gea_program-0.47.0 → gea_program-0.48.0}/gea/report_history.py +0 -0
  286. {gea_program-0.47.0 → gea_program-0.48.0}/gea/rock_inventory.py +0 -0
  287. {gea_program-0.47.0 → gea_program-0.48.0}/gea/sample_record.py +0 -0
  288. {gea_program-0.47.0 → gea_program-0.48.0}/gea/sample_well_profile.csv +0 -0
  289. {gea_program-0.47.0 → gea_program-0.48.0}/gea/sbom.py +0 -0
  290. {gea_program-0.47.0 → gea_program-0.48.0}/gea/seedlink.py +0 -0
  291. {gea_program-0.47.0 → gea_program-0.48.0}/gea/segy.py +0 -0
  292. {gea_program-0.47.0 → gea_program-0.48.0}/gea/seismic.py +0 -0
  293. {gea_program-0.47.0 → gea_program-0.48.0}/gea/seismic_array.py +0 -0
  294. {gea_program-0.47.0 → gea_program-0.48.0}/gea/seismic_assoc.py +0 -0
  295. {gea_program-0.47.0 → gea_program-0.48.0}/gea/seismic_detect.py +0 -0
  296. {gea_program-0.47.0 → gea_program-0.48.0}/gea/seismic_film.py +0 -0
  297. {gea_program-0.47.0 → gea_program-0.48.0}/gea/seismic_harmonic.py +0 -0
  298. {gea_program-0.47.0 → gea_program-0.48.0}/gea/seismic_qc.py +0 -0
  299. {gea_program-0.47.0 → gea_program-0.48.0}/gea/seismic_response.py +0 -0
  300. {gea_program-0.47.0 → gea_program-0.48.0}/gea/seismic_signature.py +0 -0
  301. {gea_program-0.47.0 → gea_program-0.48.0}/gea/seismic_track.py +0 -0
  302. {gea_program-0.47.0 → gea_program-0.48.0}/gea/seismic_uncertainty.py +0 -0
  303. {gea_program-0.47.0 → gea_program-0.48.0}/gea/seismic_unlisted.py +0 -0
  304. {gea_program-0.47.0 → gea_program-0.48.0}/gea/seismic_watch.py +0 -0
  305. {gea_program-0.47.0 → gea_program-0.48.0}/gea/sensor_swap.py +0 -0
  306. {gea_program-0.47.0 → gea_program-0.48.0}/gea/series_cache.py +0 -0
  307. {gea_program-0.47.0 → gea_program-0.48.0}/gea/service_life.py +0 -0
  308. {gea_program-0.47.0 → gea_program-0.48.0}/gea/setup_checklist.py +0 -0
  309. {gea_program-0.47.0 → gea_program-0.48.0}/gea/shapefile.py +0 -0
  310. {gea_program-0.47.0 → gea_program-0.48.0}/gea/shell.py +0 -0
  311. {gea_program-0.47.0 → gea_program-0.48.0}/gea/shut_in.py +0 -0
  312. {gea_program-0.47.0 → gea_program-0.48.0}/gea/simulation.py +0 -0
  313. {gea_program-0.47.0 → gea_program-0.48.0}/gea/site_map.py +0 -0
  314. {gea_program-0.47.0 → gea_program-0.48.0}/gea/sla_report.py +0 -0
  315. {gea_program-0.47.0 → gea_program-0.48.0}/gea/slip.py +0 -0
  316. {gea_program-0.47.0 → gea_program-0.48.0}/gea/soak.py +0 -0
  317. {gea_program-0.47.0 → gea_program-0.48.0}/gea/sra.py +0 -0
  318. {gea_program-0.47.0 → gea_program-0.48.0}/gea/store_forward.py +0 -0
  319. {gea_program-0.47.0 → gea_program-0.48.0}/gea/strata_join.py +0 -0
  320. {gea_program-0.47.0 → gea_program-0.48.0}/gea/structure.py +0 -0
  321. {gea_program-0.47.0 → gea_program-0.48.0}/gea/supervisor.py +0 -0
  322. {gea_program-0.47.0 → gea_program-0.48.0}/gea/survey_cmd.py +0 -0
  323. {gea_program-0.47.0 → gea_program-0.48.0}/gea/survey_view.py +0 -0
  324. {gea_program-0.47.0 → gea_program-0.48.0}/gea/telemetry.py +0 -0
  325. {gea_program-0.47.0 → gea_program-0.48.0}/gea/tool_catalog.py +0 -0
  326. {gea_program-0.47.0 → gea_program-0.48.0}/gea/tool_library.py +0 -0
  327. {gea_program-0.47.0 → gea_program-0.48.0}/gea/tool_taxonomy.py +0 -0
  328. {gea_program-0.47.0 → gea_program-0.48.0}/gea/transient.py +0 -0
  329. {gea_program-0.47.0 → gea_program-0.48.0}/gea/vibration.py +0 -0
  330. {gea_program-0.47.0 → gea_program-0.48.0}/gea/well3d.py +0 -0
  331. {gea_program-0.47.0 → gea_program-0.48.0}/gea/well_assembler.py +0 -0
  332. {gea_program-0.47.0 → gea_program-0.48.0}/gea/well_report.py +0 -0
  333. {gea_program-0.47.0 → gea_program-0.48.0}/gea/well_test_validation.py +0 -0
  334. {gea_program-0.47.0 → gea_program-0.48.0}/gea/wits0.py +0 -0
  335. {gea_program-0.47.0 → gea_program-0.48.0}/gea/witsml.py +0 -0
  336. {gea_program-0.47.0 → gea_program-0.48.0}/gea/workspace.py +0 -0
  337. {gea_program-0.47.0 → gea_program-0.48.0}/gea_program.egg-info/dependency_links.txt +0 -0
  338. {gea_program-0.47.0 → gea_program-0.48.0}/gea_program.egg-info/entry_points.txt +0 -0
  339. {gea_program-0.47.0 → gea_program-0.48.0}/gea_program.egg-info/requires.txt +0 -0
  340. {gea_program-0.47.0 → gea_program-0.48.0}/gea_program.egg-info/top_level.txt +0 -0
  341. {gea_program-0.47.0 → gea_program-0.48.0}/setup.cfg +0 -0
  342. {gea_program-0.47.0 → gea_program-0.48.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.47.0
3
+ Version: 0.48.0
4
4
  Summary: GEA-Program: downhole gauge monitoring - canonical measurement records, quality rules, drift evaluation with a re-fit change log, well-test validation, alarm management, model cards, store-and-forward, monthly SLA measurement, FAT/SAT protocols and a dashboard, generated from live objects in the client's reporting outline.
5
5
  Author: Daniel T. Murphy
6
6
  License: MPL-2.0
@@ -648,6 +648,19 @@ against it; `gea seedlink --config ... --hello` prints what a server says of
648
648
  itself; `gea help seedlink`. A TexNet station is served by EarthScope at
649
649
  `rtserve.iris.washington.edu:18000`, network `TX`.
650
650
 
651
+ ### One record, tied together
652
+
653
+ The invoice check, rig time, the daily summary, the alerts, the signed
654
+ dossier and the declarations sheet now read each other: an invoice that
655
+ differs is an alert; the morning summary carries each well's invoice check
656
+ and yesterday's rig time; a signed dossier carries the contract, the
657
+ invoices and daily reports as attached, the rig-time targets and casing,
658
+ and both checks as they read at signing. The declarations sheet declares
659
+ the contract, the rig-time targets, block weights, casing shoes and the
660
+ watch and summary settings, and every file the program reads as a table -
661
+ the sheet, an invoice, a daily report - may be an Excel workbook. Casing
662
+ shoes split rig time by hole section.
663
+
651
664
  ### Rig time
652
665
 
653
666
  What the rig did each second of the drilling record, read from the hook
@@ -609,6 +609,19 @@ against it; `gea seedlink --config ... --hello` prints what a server says of
609
609
  itself; `gea help seedlink`. A TexNet station is served by EarthScope at
610
610
  `rtserve.iris.washington.edu:18000`, network `TX`.
611
611
 
612
+ ### One record, tied together
613
+
614
+ The invoice check, rig time, the daily summary, the alerts, the signed
615
+ dossier and the declarations sheet now read each other: an invoice that
616
+ differs is an alert; the morning summary carries each well's invoice check
617
+ and yesterday's rig time; a signed dossier carries the contract, the
618
+ invoices and daily reports as attached, the rig-time targets and casing,
619
+ and both checks as they read at signing. The declarations sheet declares
620
+ the contract, the rig-time targets, block weights, casing shoes and the
621
+ watch and summary settings, and every file the program reads as a table -
622
+ the sheet, an invoice, a daily report - may be an Excel workbook. Casing
623
+ shoes split rig time by hole section.
624
+
612
625
  ### Rig time
613
626
 
614
627
  What the rig did each second of the drilling record, read from the hook
@@ -123,7 +123,7 @@ from .profile_catalog import (
123
123
  read_pangaea_txt,
124
124
  )
125
125
 
126
- __version__ = "0.47.0"
126
+ __version__ = "0.48.0"
127
127
  __all__ = [
128
128
  "aging_rate", "rate_psi_yr", "accuracy_psi",
129
129
  "Sensor", "SimulatorConfig", "DownholeEngine",
@@ -599,6 +599,9 @@ def main(argv=None) -> int:
599
599
  p_rt.add_argument("--trip-out", type=str, default=None, help="declare the tripping-out target, ft/h")
600
600
  p_rt.add_argument("--source", type=str, default="", help="where the targets came from")
601
601
  p_rt.add_argument("--block-weight", type=str, default=None, help="with --well: the block weight in klbf where the estimate is wrong ('' removes it)")
602
+ p_rt.add_argument("--casing", type=str, default=None, help="with --well, --shoe-md and --source: declare a casing shoe, its size in inches as written (13-3/8)")
603
+ p_rt.add_argument("--shoe-md", type=str, default=None, help="the shoe's measured depth, ft")
604
+ p_rt.add_argument("--set-time", type=str, default=None, help="when the casing was set (the recorder's clock); otherwise when the hole is 50 ft past the shoe")
602
605
  p_rt.add_argument("--actor", type=str, default="cli")
603
606
  p_rt.add_argument("--json", action="store_true")
604
607
 
@@ -2205,9 +2208,10 @@ def main(argv=None) -> int:
2205
2208
  if not a.file:
2206
2209
  raise SystemExit("declare: give --file <sheet.csv> (or --template <out.csv> to start one)")
2207
2210
  try:
2208
- with open(a.file, encoding="utf-8-sig") as f:
2209
- text_ = f.read()
2210
- except OSError as e:
2211
+ from . import xlsx_read as _XR
2212
+ with open(a.file, "rb") as f:
2213
+ text_ = _XR.text_of(f.read())
2214
+ except (OSError, ValueError, KeyError) as e:
2211
2215
  raise SystemExit(f"declare: {e}")
2212
2216
  p = _DC.apply(ws, text_, a.actor, os.path.dirname(os.path.abspath(a.file)), write=a.apply)
2213
2217
  print(_json.dumps(p, default=str) if a.json else _DC.text(p))
@@ -2376,6 +2380,12 @@ def main(argv=None) -> int:
2376
2380
  _safe_stdout()
2377
2381
  try:
2378
2382
  ws = Workspace(a.workspace)
2383
+ if a.casing:
2384
+ if not a.well:
2385
+ raise ValueError("--casing needs --well")
2386
+ c_ = _RS.set_casing(ws, a.actor, a.well, a.casing, a.shoe_md, a.source, a.set_time)
2387
+ print(f"casing {c_['size']} in, shoe at {c_['shoe_md_ft']:,.0f} ft MD ({c_['source']})")
2388
+ return 0
2379
2389
  if a.w2w_min is not None or a.trip_in is not None or a.trip_out is not None or a.block_weight is not None:
2380
2390
  if a.block_weight is not None and not a.well:
2381
2391
  raise ValueError("--block-weight needs --well")
@@ -10913,6 +10913,206 @@ def section_cp_rig_time(tmp: str) -> None:
10913
10913
  + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
10914
10914
 
10915
10915
 
10916
+ def section_cq_tied_together(tmp: str) -> None:
10917
+ """Section CQ - the recent parts tied together: an Excel workbook read without Excel (shared and inline strings,
10918
+ numbers, booleans, dates and times from their cell formats, the 1904 epoch, a named sheet) and taken wherever a
10919
+ CSV is - the invoice check's files and the declarations sheet; the declarations sheet's new tables (the contract,
10920
+ the rig-time targets, block weights, casing, the watch and summary settings) planned, written, round-tripped and
10921
+ refused when wrong; rig time by hole section from declared casing; the invoice differing raised as an alert and
10922
+ cleared; the daily summary carrying each invoice check and yesterday's rig time; a signed dossier carrying the
10923
+ contract, the files, the targets, the casing and both checks as they read at signing; the API, the CLI, the tabs
10924
+ and the help pages."""
10925
+ import http.cookiejar
10926
+ import subprocess as _sp
10927
+ import urllib.request
10928
+ import urllib.error
10929
+ import base64 as _b64
10930
+ import zipfile as _zf
10931
+ from datetime import datetime as _dtm, timedelta as _td, timezone as _tz
10932
+ from . import __version__
10933
+ from . import alerts as AL
10934
+ from . import declarations as DC
10935
+ from . import dossier as DZ
10936
+ from . import helplib as H
10937
+ from . import invoice_check as IC
10938
+ from . import pad_summary as PS
10939
+ from . import rig_state as RS
10940
+ from . import seismic_watch as SW
10941
+ from . import xlsx_read as XR
10942
+ from .workspace import Workspace
10943
+ from .service import Service, Users
10944
+ pkg = Path(__file__).parent
10945
+ d = Path(tmp, "cq"); d.mkdir()
10946
+
10947
+ # -- CQ1 the workbook -------------------------------------------------------------------------------------------------
10948
+ XR.write_simple(str(d / "a.xlsx"), [["Date", "Rate Type", "Quantity", "Note"], [_dtm(2026, 7, 1), "Operating day rate", 1, 'with, a "comma"'],
10949
+ [_dtm(2026, 7, 2, 6, 30), "Standby", 12.5, None]])
10950
+ a = XR.rows((d / "a.xlsx").read_bytes())
10951
+ ns = 'xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"'
10952
+ with _zf.ZipFile(d / "b.xlsx", "w") as z: # a hand-made workbook: two sheets, inline strings, booleans, the 1904 epoch, a custom date format
10953
+ z.writestr("xl/workbook.xml", f'<workbook {ns}><workbookPr date1904="1"/><sheets><sheet name="Notes" sheetId="1" r:id="rId1"/><sheet name="Invoice" sheetId="2" r:id="rId2"/></sheets></workbook>')
10954
+ z.writestr("xl/_rels/workbook.xml.rels", '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Target="worksheets/sheet1.xml" Type="x"/><Relationship Id="rId2" Target="/xl/worksheets/sheet2.xml" Type="x"/></Relationships>')
10955
+ z.writestr("xl/styles.xml", f'<styleSheet {ns}><numFmts count="1"><numFmt numFmtId="164" formatCode="dd/mm/yyyy\\ hh:mm"/><numFmt numFmtId="165" formatCode="#,##0.00"/></numFmts>'
10956
+ '<cellXfs count="3"><xf numFmtId="0"/><xf numFmtId="164"/><xf numFmtId="165"/></cellXfs></styleSheet>')
10957
+ z.writestr("xl/worksheets/sheet1.xml", f'<worksheet {ns}><sheetData><row r="1"><c r="A1" t="inlineStr"><is><t>notes</t></is></c></row></sheetData></worksheet>')
10958
+ z.writestr("xl/worksheets/sheet2.xml", f'<worksheet {ns}><sheetData><row r="1"><c r="A1" t="inlineStr"><is><t>When</t></is></c><c r="C1" t="inlineStr"><is><t>Paid</t></is></c></row>'
10959
+ '<row r="3"><c r="A3" s="1"><v>44743.25</v></c><c r="B3" s="2"><v>1234.5</v></c><c r="C3" t="b"><v>1</v></c></row></sheetData></worksheet>')
10960
+ bx = (d / "b.xlsx").read_bytes()
10961
+ b2 = XR.rows(bx, "Invoice")
10962
+ c1 = {
10963
+ "simple": a == [["Date", "Rate Type", "Quantity", "Note"], ["2026-07-01", "Operating day rate", "1", 'with, a "comma"'], ["2026-07-02T06:30", "Standby", "12.5"]],
10964
+ "csv": XR.to_csv((d / "a.xlsx").read_bytes()).splitlines()[1] == '2026-07-01,Operating day rate,1,"with, a ""comma"""',
10965
+ "sheets": XR.sheets(bx) == ["Notes", "Invoice"] and XR.rows(bx) == [["notes"]],
10966
+ "hand_made": b2 == [["When", "", "Paid"], [], ["2026-07-02T06:00", "1234.5", "TRUE"]],
10967
+ "text_of": XR.text_of(b"a,b\n1,2\n") == "a,b\n1,2\n" and not XR.is_xlsx(b"PK not a zip"),
10968
+ }
10969
+ try:
10970
+ XR.rows(bx, "Nope"); c1["named"] = False
10971
+ except ValueError as e:
10972
+ c1["named"] = "Notes, Invoice" in str(e)
10973
+ failed1 = [k for k, v in c1.items() if not v]
10974
+ ok(not failed1,
10975
+ "CQ1 an Excel workbook read with the standard library: shared and inline strings, a comma and quotes kept, whole numbers without "
10976
+ "'.0', dates and date-times from built-in and custom formats, the 1904 epoch, booleans, a gap row kept as a gap, the first or a "
10977
+ "named sheet; CSV text passed through" + (f" [failed: {', '.join(failed1)}]" if failed1 else ""))
10978
+
10979
+ # -- CQ2 the declarations sheet, casing, and the invoice as a workbook --------------------------------------------------
10980
+ F = RS.selftest_edr(str(d))
10981
+ G = IC.selftest_files(str(d))
10982
+ wsp = str(d / "ws"); ws = Workspace.create(wsp, "Pad CQ", actor="tester")
10983
+ w1 = ws.add_well_file(F["path"], display="RIG-1", actor="tester"); w2 = ws.add_well_file(F["path"], display="RIG-2", actor="tester")
10984
+ wi = ws.add_well_file(G["edr"], display="INV-1", actor="tester")
10985
+ sheet = ("[contract],day_start,report_date,operating,standby,repair,repair_hours_per_month,source\n,06:00,start,30000,25000,0,2,daywork contract A\n"
10986
+ "[rig_time],w2w_min,trip_in_ft_h,source\n,,2800,program 2026\n[block_weights],well,block_weight_klbf\n,RIG-2,42\n"
10987
+ "[casing],well,size_in,shoe_md_ft,set,source\n,RIG-1,9-5/8,10150,,tally\n,RIG-2,9-5/8,10150,,tally\n,RIG-1,13-3/8,2000,,tally\n"
10988
+ "[settings],setting,value\n,watch_every_min,10\n,summary_at,10:45\n,summary_send,no\n")
10989
+ rows_ = [ln.split(",") for ln in sheet.strip().splitlines()]
10990
+ XR.write_simple(str(d / "decl.xlsx"), rows_)
10991
+ p_x = DC.plan(ws, XR.text_of((d / "decl.xlsx").read_bytes()))
10992
+ p_w = DC.apply(ws, sheet, "tester", write=True)
10993
+ p_rt = DC.plan(ws, DC.template(ws))
10994
+ p_bad = DC.plan(ws, "[contract],operating,source\n,30000,\n[settings],setting,value\n,watch_mode,sometimes\n,colour,blue\n[casing],well,size_in,shoe_md_ft,source\n,RIG-1,9 5/8,100,x\n,NOPE,7,100,x\n")
10995
+ pad = RS.pad(ws)
10996
+ W1 = next(w for w in pad["wells"] if w["display"] == "RIG-1")
10997
+ secs = [c["section"] for c in W1["connection_list"]]
10998
+ XR.write_simple(str(d / "inv.xlsx"), [ln.split(",") if i == 0 else [(_dtm.fromisoformat(x) if j == 0 else (float(x) if re.fullmatch(r"[\d.]+", x) else x)) for j, x in enumerate(ln.split(","))]
10999
+ for i, ln in enumerate(G["invoice"].strip().splitlines())])
11000
+ IC.attach(ws, "tester", wi["id"], "ddr", "ddr.csv", G["ddr"].encode(), "reports")
11001
+ IC.attach(ws, "tester", wi["id"], "invoice", "inv.xlsx", (d / "inv.xlsx").read_bytes(), "invoice as a workbook")
11002
+ ic = IC.check(ws, wi["id"])
11003
+ c2 = {
11004
+ "xlsx_sheet": p_x["counts"]["NEW"] == 6 and p_x["counts"]["CHANGE"] == 3 and not p_x["errors"],
11005
+ "written": p_w["applied"] and IC.terms(ws)["rates_usd_per_day"] == {"operating": 30000.0, "standby": 25000.0, "repair": 0.0}
11006
+ and RS.settings(ws)["targets"] == {"trip_in_ft_h": 2800.0} and RS.settings(ws)["block_weight_klbf"] == {w2["id"]: 42.0}
11007
+ and [x["size"] for x in RS.casing(ws)[w1["id"]]] == ["13-3/8", "9-5/8"] and SW.settings(ws)["every_min"] == 10
11008
+ and PS.settings(ws)["at"] == "10:45" and PS.settings(ws)["send"] is False,
11009
+ "round_trip": p_rt["counts"]["NEW"] == 0 and p_rt["counts"]["CHANGE"] == 0 and not p_rt["errors"],
11010
+ "refused": len(p_bad["errors"]) == 5 and any("watch_mode" in e for e in p_bad["errors"]) and any("no setting 'colour'" in e for e in p_bad["errors"])
11011
+ and any("13-3/8, 9-5/8 or 7" in e for e in p_bad["errors"]) and any("came from" in e for e in p_bad["errors"]),
11012
+ "sections": secs == ["below the 13-3/8 in shoe", "below the 9-5/8 in shoe", "below the 9-5/8 in shoe"]
11013
+ and all(x["hole"] == "CASED" for x in W1["trips"]) and set(pad["targets"]["by_section"]) == {"below the 9-5/8 in shoe"}
11014
+ and next(w for w in pad["wells"] if w["display"] == "RIG-2")["block_weight_klbf"] == 42.0,
11015
+ "invoice_xlsx": ic["status"] == "DIFFERS" and ic["totals"]["usd_in_question"] == 37666.67,
11016
+ }
11017
+ failed2 = [k for k, v in c2.items() if not v]
11018
+ ok(not failed2,
11019
+ "CQ2 the declarations sheet's new tables - the contract, the rig-time targets, a block weight, casing shoes, the watch and summary "
11020
+ "settings - read from a workbook, written, round-tripped to nothing to do, and refused when wrong (no source, an unknown setting, a "
11021
+ "size not in inches as written, a well not there); connections by hole section and trips inside casing; an invoice attached as a "
11022
+ "workbook checked to the same 37,666.67" + (f" [failed: {', '.join(failed2)}]" if failed2 else ""))
11023
+
11024
+ # -- CQ3 the alert, the summary, the dossier ----------------------------------------------------------------------
11025
+ now = _dtm(2026, 7, 2, 12, 0, tzinfo=_tz.utc)
11026
+ k1 = AL.check_conditions(ws, None, now, force=True)
11027
+ st1 = dict(AL.state(ws)["conditions"])
11028
+ g = PS.gather(ws, _dtm(2026, 8, 2, 7, 0, tzinfo=_tz.utc), _dtm(2026, 8, 1, 0, 0, tzinfo=_tz.utc))
11029
+ RS.write_report(ws, __version__, "tester")
11030
+ e1 = DZ.build(ws, "tester", __version__, None)
11031
+ with _zf.ZipFile(os.path.join(ws.reports_dir, e1["path"])) as z:
11032
+ names = set(z.namelist())
11033
+ rt = json.loads(z.read("results/rig_time.json"))
11034
+ icj = json.loads(z.read(f"results/invoice_check/{wi['id']}.json"))
11035
+ v1 = DZ.verify(os.path.join(ws.reports_dir, e1["path"]), ws)
11036
+ for f_ in IC.files(ws, wi["id"]): # the corrected report and invoice replace them
11037
+ IC.remove(ws, "tester", f_["file"])
11038
+ ddr_ok = G["ddr"].replace("2026-07-02,14:00,16:00,2,6,Trip out", "2026-07-02,14:00,16:00,2,7,Service rig").replace("6200,7600", "6200,7400")
11039
+ inv_ok = ("Date,Rate Type,Quantity,Unit,Unit Price,Amount\n2026-07-01,Operating day rate,1,day,30000,30000\n"
11040
+ "2026-07-02,Operating day rate,22,hour,1250,27500\n2026-07-02,Repair rate,2,hour,0,0\n"
11041
+ "2026-07-03,Standby rate,20,hour,1041.6667,20833.33\n2026-07-03,Operating day rate,4,hour,1250,5000\n2026-07-04,Operating day rate,1,day,30000,30000\n")
11042
+ IC.attach(ws, "tester", wi["id"], "ddr", "ddr_ok.csv", ddr_ok.encode(), "corrected reports")
11043
+ IC.attach(ws, "tester", wi["id"], "invoice", "inv_ok.csv", inv_ok.encode(), "corrected invoice")
11044
+ k2 = AL.check_conditions(ws, None, now + _td(minutes=1))
11045
+ c3 = {
11046
+ "alert": [a_["event"] for a_ in k1 if a_["event"] == "invoice.differs"] == ["invoice.differs"] and f"invoice.differs:{wi['id']}" in st1
11047
+ and "37,666.67" in next(a_ for a_ in k1 if a_["event"] == "invoice.differs")["detail"],
11048
+ "cleared": [a_["event"] for a_ in k2] == ["condition.cleared"] and k2[0]["cleared"] == "invoice.differs",
11049
+ "summary": any(x["item"] == "the invoice check of INV-1" and x["status"] == "DIFFERS" for x in g["due"])
11050
+ and [x["well"] for x in g["rig_time"]] == ["RIG-1", "RIG-2"] and all(x["connections"] == 3 and x["date"] == "2026-08-01" for x in g["rig_time"])
11051
+ and all(x["lost_usd"] is not None for x in g["rig_time"]),
11052
+ "dossier": v1["status"] == "VALID" and {"declarations/economics/invoices/terms.json", "declarations/economics/invoices/files.json",
11053
+ "declarations/drillstring/rig_time.json", "declarations/drillstring/casing.json"} <= names
11054
+ and any(n.startswith(f"declarations/economics/invoices/{wi['id']}/") and n.endswith("_inv.xlsx") for n in names)
11055
+ and icj["status"] == "DIFFERS" and len(rt["wells"]) == 3 and rt["targets"]["trip_in_ft_h"]["value"] == 2800,
11056
+ }
11057
+ failed3 = [k for k, v in c3.items() if not v]
11058
+ ok(not failed3,
11059
+ "CQ3 an invoice that differs raised as an alert with its dollars, and cleared when a corrected invoice replaces it; the daily summary "
11060
+ "carrying the invoice check and yesterday's rig time well by well with its cost at the contract's rate; a signed pad dossier carrying "
11061
+ "the contract, the invoice files as attached, the targets and the casing, and both checks as they read at signing - VALID"
11062
+ + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
11063
+
11064
+ # -- CQ4 the API, the CLI, the tabs, the help ------------------------------------------------------------------------
11065
+ Users(str(Path(wsp, "users.json"))).add("v", "viewer-pass-1", "viewer"); Users(str(Path(wsp, "users.json"))).add("op", "op-pass-123", "operator")
11066
+ svc = Service(wsp, port=0, scheduler=False).start()
11067
+ base_url = svc.url.rstrip("/")
11068
+
11069
+ def opener(name, pw_):
11070
+ o = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
11071
+ rq = urllib.request.Request(base_url + "/api/login", method="POST", data=json.dumps({"name": name, "password": pw_}).encode())
11072
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1"); o.open(rq, timeout=30)
11073
+ return o
11074
+
11075
+ def call(o, method, path, body=None):
11076
+ rq = urllib.request.Request(base_url + path, method=method, data=(json.dumps(body).encode() if body is not None else None))
11077
+ if method == "POST":
11078
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1")
11079
+ try:
11080
+ with o.open(rq, timeout=120) as rr_:
11081
+ return rr_.status, json.loads(rr_.read())
11082
+ except urllib.error.HTTPError as e:
11083
+ return e.code, json.loads(e.read())
11084
+ try:
11085
+ V, O = opener("v", "viewer-pass-1"), opener("op", "op-pass-123")
11086
+ s_vc, _ = call(V, "POST", "/api/rig-time/casing", {"well_id": w2["id"], "size": "7", "shoe_md_ft": 9000, "source": "x"})
11087
+ s_oc, ocj = call(O, "POST", "/api/rig-time/casing", {"well_id": w2["id"], "size": "7", "shoe_md_ft": 9000, "source": "tally"})
11088
+ s_ob, _ = call(O, "POST", "/api/rig-time/casing", {"well_id": w2["id"], "size": "seven", "shoe_md_ft": 9000, "source": "tally"})
11089
+ s_or, _ = call(O, "POST", "/api/rig-time/casing/remove", {"well_id": w2["id"], "size": "7"})
11090
+ s_dx, dxj = call(O, "POST", "/api/declare", {"content_b64": _b64.b64encode((d / "decl.xlsx").read_bytes()).decode()})
11091
+ s_ia, iaj = call(O, "POST", "/api/invoice-check/attach", {"well_id": wi["id"], "kind": "invoice", "filename": "inv2.xlsx", "source": "workbook",
11092
+ "content_b64": _b64.b64encode((d / "inv.xlsx").read_bytes()).decode()})
11093
+ s_al, alj = call(V, "GET", "/api/alerts")
11094
+ finally:
11095
+ svc.stop()
11096
+ cli_c = _sp.run([sys.executable, "-m", "gea", "rig-time", "--workspace", wsp, "--well", w2["id"], "--casing", "13-3/8", "--shoe-md", "2000", "--source", "tally"],
11097
+ capture_output=True, text=True, cwd=str(pkg.parent))
11098
+ cli_d = _sp.run([sys.executable, "-m", "gea", "declare", "--workspace", wsp, "--file", str(d / "decl.xlsx")], capture_output=True, text=True, cwd=str(pkg.parent))
11099
+ page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
11100
+ c4 = {
11101
+ "api": s_vc == 403 and s_oc == 200 and ocj["shoe_md_ft"] == 9000 and s_ob == 400 and s_or == 200 and s_dx == 200 and dxj["counts"]["SAME"] == 9
11102
+ and s_ia == 200 and iaj["lines"] == 7 and s_al == 200 and "invoice.differs" in alj["events"],
11103
+ "cli": cli_c.returncode == 0 and "13-3/8 in, shoe at 2,000 ft" in cli_c.stdout and cli_d.returncode == 0 and "already declared" in cli_d.stdout,
11104
+ "page": all(x in page_src for x in ("rtC", "/api/rig-time/casing", ".xlsx", "content_b64: DC.b64", "'invoice.differs': 'Invoice differs'")),
11105
+ "help": all(not H.check(t_) for t_ in ("declare", "rig-time", "invoice-check", "dossier", "alerts", "pad-summary"))
11106
+ and "[casing]" in (pkg / "help" / "declare.md").read_text(encoding="utf-8") and "Hole sections" in (pkg / "help" / "rig-time.md").read_text(encoding="utf-8")
11107
+ and "invoice.differs" in (pkg / "help" / "alerts.md").read_text(encoding="utf-8") and "results/" in (pkg / "help" / "dossier.md").read_text(encoding="utf-8"),
11108
+ }
11109
+ failed4 = [k for k, v in c4.items() if not v]
11110
+ ok(not failed4,
11111
+ "CQ4 on the API (only an operator declares or removes a casing shoe, a size that is not one refused; a declarations workbook and an "
11112
+ "invoice workbook sent as they are; the new alert listed), the CLI (a casing shoe, a declarations workbook), the tabs and the help pages"
11113
+ + (f" [failed: {', '.join(failed4)}]" if failed4 else ""))
11114
+
11115
+
10916
11116
  def main() -> int:
10917
11117
  print("GEA-Program - ACCEPTANCE SUITE (the product gate)")
10918
11118
  # a handle a job subprocess still holds at the end must not turn a finished gate into a traceback on Windows:
@@ -11010,6 +11210,7 @@ def main() -> int:
11010
11210
  section_cn_four(tmp)
11011
11211
  section_co_invoice_check(tmp)
11012
11212
  section_cp_rig_time(tmp)
11213
+ section_cq_tied_together(tmp)
11013
11214
  if _FAILS:
11014
11215
  print(f"[ACCEPTANCE] {len(_FAILS)} FAILURES ({_PASS} passed):")
11015
11216
  for f in _FAILS:
@@ -13,7 +13,9 @@ worth an event, for the conditions the operator does not watch by hand:
13
13
  - `penetrator.outside` - a line of an ESP well's penetrator check is OUTSIDE its rating (the Completion tab);
14
14
  - `dossier.changed` - the newest signed dossier of a well or the pad no longer matches the workspace: a source
15
15
  record or an earlier audit line has changed since it was signed (checked once a day);
16
- - `condition.cleared` - one of the last three is no longer so.
16
+ - `invoice.differs` - a well's invoice cross-check finds a day that DIFFERS (checked once a day, and when a file is
17
+ attached);
18
+ - `condition.cleared` - one of the last four is no longer so.
17
19
 
18
20
  Every alert is written to `records/alerts.jsonl` whether or not any notification channel is set up - the Alarms
19
21
  tab lists them - and is handed to the notification rules, which send it where the site said (a rule per event
@@ -28,7 +30,7 @@ from datetime import datetime, timezone
28
30
  from typing import Dict, List, Optional
29
31
 
30
32
  EVENTS = ('seismic.protocol', 'seismic.event', 'seismic.watch_down', 'seismic.watch_up', 'motor.due', 'penetrator.outside',
31
- 'dossier.changed', 'condition.cleared')
33
+ 'dossier.changed', 'invoice.differs', 'condition.cleared')
32
34
  CONDITIONS_EVERY_S = 900 # motors and penetrators: every 15 minutes
33
35
  DOSSIERS_EVERY_S = 86400 # dossiers: once a day
34
36
  _LOCK = threading.Lock()
@@ -173,5 +175,21 @@ def check_conditions(ws, notifier, now: Optional[datetime] = None, force: bool =
173
175
  'dossier_id': e['dossier_id'], 'detail': '; '.join(f"{c['check']}: {c['detail']}" for c in bad)}, now, raised)
174
176
  except Exception as ex:
175
177
  last['dossiers_error'] = f'{type(ex).__name__}: {ex}'
178
+ if due('invoices', DOSSIERS_EVERY_S):
179
+ last['invoices'] = _utc(now)
180
+ try:
181
+ from . import invoice_check as IC
182
+ if IC.terms(ws):
183
+ for wid in sorted({f['well_id'] for f in IC.files(ws)}):
184
+ if wid not in wells:
185
+ continue
186
+ r = IC.check(ws, wid)
187
+ T = r.get('totals') or {}
188
+ _transition(ws, notifier, st, f'invoice.differs:{wid}', r.get('status') == 'DIFFERS',
189
+ {'event': 'invoice.differs', 'well_id': wid, 'well': wells[wid], 'what': f'the invoice cross-check of {wells[wid]}',
190
+ 'detail': f"{T.get('differs', 0)} day(s) differ, {T.get('usd_in_question', 0):,.2f} USD in question of {T.get('billed_checked_usd', 0):,.2f} billed at day rates",
191
+ 'usd_in_question': T.get('usd_in_question')}, now, raised)
192
+ except Exception as ex:
193
+ last['invoices_error'] = f'{type(ex).__name__}: {ex}'
176
194
  save_state(ws, st)
177
195
  return raised
@@ -67,9 +67,24 @@ TABLES: Dict[str, dict] = {
67
67
  'gas_group', 'temp_class', 'ambient_min_f', 'ambient_max_f', 'source'),
68
68
  'need': ('well', 'source'),
69
69
  'about': "the ESP drive's output limits and the motor nameplate, the wellhead's hazardous-area classification (e.g. Class I, Division 1), gas group, temperature class and the site's ambient range"},
70
+ 'contract': {'cols': ('day_start', 'report_date', 'operating', 'standby', 'repair', 'moving', 'force_majeure', 'repair_hours_per_job',
71
+ 'repair_hours_per_month', 'source'),
72
+ 'need': ('operating', 'source'),
73
+ 'about': "the drilling contract's terms for the invoice check: the hour the report day starts on the rig's clock (06:00), whether a report is dated by the day it starts or ends (start/end), the day rates in USD (operating, and standby, repair, moving and force majeure where the contract has them), the repair allowance in hours a job and a month, and the contract it came from"},
74
+ 'rig_time': {'cols': ('w2w_min', 'trip_in_ft_h', 'trip_out_ft_h', 'source'), 'need': ('source',),
75
+ 'about': "the rig-time targets from the drilling program: weight-to-weight connection minutes and tripping speeds in and out (ft/h); blank keeps the pad's own best quarter"},
76
+ 'block_weights': {'cols': ('well', 'block_weight_klbf'), 'need': ('well', 'block_weight_klbf'),
77
+ 'about': "a well's travelling block weight in klbf, where the rig-time estimate from the hook load is wrong"},
78
+ 'casing': {'cols': ('well', 'size_in', 'shoe_md_ft', 'set', 'source'), 'need': ('well', 'size_in', 'shoe_md_ft', 'source'),
79
+ 'about': "casing shoes: the size in inches as written (13-3/8, 9-5/8, 7), the shoe's measured depth in ft, when it was set (the recorder's clock; blank: when the hole is 50 ft past it) and the tally or report it came from"},
80
+ 'settings': {'cols': ('setting', 'value'), 'need': ('setting', 'value'),
81
+ 'about': "the program's settings: watch_enabled (yes/no), watch_every_min, watch_mode (operations/always), summary_enabled (yes/no), summary_at (HH:MM UTC), summary_send (yes/no)"},
70
82
  }
71
- ORDER = ('wells', 'fleet', 'rates', 'items', 'afe', 'surveys', 'runs', 'components', 'penetrators', 'esp_drives', 'operations', 'catalogs')
72
- APPLY_ORDER = ('wells', 'fleet', 'rates', 'items', 'afe', 'surveys', 'runs', 'penetrators', 'esp_drives', 'operations', 'catalogs')
83
+ ORDER = ('wells', 'fleet', 'rates', 'items', 'afe', 'surveys', 'runs', 'components', 'penetrators', 'esp_drives', 'operations', 'catalogs',
84
+ 'contract', 'rig_time', 'block_weights', 'casing', 'settings')
85
+ APPLY_ORDER = ('wells', 'fleet', 'rates', 'items', 'afe', 'surveys', 'runs', 'penetrators', 'esp_drives', 'operations', 'catalogs',
86
+ 'contract', 'rig_time', 'block_weights', 'casing', 'settings')
87
+ SETTINGS = ('watch_enabled', 'watch_every_min', 'watch_mode', 'summary_enabled', 'summary_at', 'summary_send')
73
88
 
74
89
 
75
90
  class SheetError(ValueError):
@@ -525,6 +540,98 @@ def plan(ws, text: str, base_dir: str = '') -> dict:
525
540
  rec['lat'], rec['lon'], rec['radius_km'], rec['source']))
526
541
  guard('catalogs', rec, b)
527
542
 
543
+ # the contract, rig time, casing and settings
544
+ from . import invoice_check as IC
545
+ from . import rig_state as RS
546
+ from . import seismic_watch as SW
547
+ from . import pad_summary as PS
548
+ for rec in tables.get('contract', [])[:1]:
549
+ def b(rec=rec):
550
+ rates = {k: rec.get(k) for k in IC.KINDS if (rec.get(k) or '').strip()}
551
+ new = IC.terms_record(rec.get('source', ''), (rec.get('day_start') or '06:00').strip(), (rec.get('report_date') or 'start').strip(), rates,
552
+ rec.get('repair_hours_per_job'), rec.get('repair_hours_per_month'))
553
+ old = IC.terms(ws)
554
+ keys = ('source', 'day_start', 'report_date', 'rates_usd_per_day', 'repair_hours_per_job', 'repair_hours_per_month')
555
+ if old and all(old.get(k) == new[k] for k in keys):
556
+ add('contract', rec['_row'], None, 'SAME', 'the contract terms already declared')
557
+ return
558
+ add('contract', rec['_row'], None, 'CHANGE' if old else 'NEW',
559
+ f"operating {new['rates_usd_per_day']['operating']:,.2f} USD/day, report day from {new['day_start']} ({new['source']})",
560
+ lambda actor, rec=rec, rates=rates: IC.set_terms(ws, actor, rec.get('source', ''), (rec.get('day_start') or '06:00').strip(),
561
+ (rec.get('report_date') or 'start').strip(), rates, rec.get('repair_hours_per_job'),
562
+ rec.get('repair_hours_per_month')))
563
+ guard('contract', rec, b)
564
+ if len(tables.get('contract', [])) > 1:
565
+ errs.append(f"row {tables['contract'][1]['_row']}: [contract] the contract is one row")
566
+ for rec in tables.get('rig_time', [])[:1]:
567
+ def b(rec=rec):
568
+ vals = {k: _f(rec.get(k), k) for k in ('w2w_min', 'trip_in_ft_h', 'trip_out_ft_h')}
569
+ if not any(v is not None for v in vals.values()):
570
+ raise SheetError('give at least one target')
571
+ cur = RS.settings(ws).get('targets', {})
572
+ want = {k: v for k, v in vals.items() if v is not None}
573
+ if all(cur.get(k) == v for k, v in want.items()) and RS.settings(ws).get('targets_source') == rec['source'].strip():
574
+ add('rig_time', rec['_row'], None, 'SAME', 'the targets already declared')
575
+ return
576
+ add('rig_time', rec['_row'], None, 'CHANGE' if cur else 'NEW', ', '.join(f"{k.replace('_', ' ')} {v:g}" for k, v in want.items()) + f" ({rec['source'].strip()})",
577
+ lambda actor, vals=vals, rec=rec: RS.set_settings(ws, actor, rec['source'], vals['w2w_min'], vals['trip_in_ft_h'], vals['trip_out_ft_h']))
578
+ guard('rig_time', rec, b)
579
+ bw_have = RS.settings(ws).get('block_weight_klbf', {})
580
+ for rec in tables.get('block_weights', []):
581
+ def b(rec=rec):
582
+ wid = _well_id(ws, rec['well'], wells)
583
+ v = _f(rec.get('block_weight_klbf'), 'the block weight')
584
+ if bw_have.get(wid) == v:
585
+ add('block_weights', rec['_row'], wid, 'SAME', f'{v:g} klbf already declared')
586
+ return
587
+ add('block_weights', rec['_row'], wid, 'CHANGE' if wid in bw_have else 'NEW', f'block weight {v:g} klbf',
588
+ lambda actor, wid=wid, v=v: RS.set_settings(ws, actor, '', None, None, None, (wid, v)))
589
+ guard('block_weights', rec, b)
590
+ cs_have = RS.casing(ws)
591
+ for rec in tables.get('casing', []):
592
+ def b(rec=rec):
593
+ wid = _well_id(ws, rec['well'], wells)
594
+ new = RS.casing_record(ws, wid, rec.get('size_in', ''), rec.get('shoe_md_ft'), rec.get('source', ''), rec.get('set') or None)
595
+ old = next((x for x in cs_have.get(wid, []) if x['size'] == new['size']), None)
596
+ if old == new:
597
+ add('casing', rec['_row'], wid, 'SAME', f"{new['size']} in shoe already declared")
598
+ return
599
+ add('casing', rec['_row'], wid, 'CHANGE' if old else 'NEW', f"{new['size']} in shoe at {new['shoe_md_ft']:,.0f} ft ({new['source']})",
600
+ lambda actor, wid=wid, rec=rec: RS.set_casing(ws, actor, wid, rec.get('size_in', ''), rec.get('shoe_md_ft'), rec.get('source', ''), rec.get('set') or None))
601
+ guard('casing', rec, b)
602
+ sw_now, ps_now = SW.settings(ws), PS.settings(ws)
603
+ yes = lambda v: str(v).strip().lower() in ('yes', 'y', 'true', '1', 'on')
604
+ for rec in tables.get('settings', []):
605
+ def b(rec=rec):
606
+ k, v = rec['setting'].strip(), rec['value'].strip()
607
+ if k not in SETTINGS:
608
+ raise SheetError(f"no setting {k!r}: one of {', '.join(SETTINGS)}")
609
+ if k.endswith(('_enabled', '_send')) and v.lower() not in ('yes', 'no', 'y', 'n', 'true', 'false', '1', '0', 'on', 'off'):
610
+ raise SheetError(f'{k} is yes or no, not {v!r}')
611
+ if k == 'watch_every_min':
612
+ x = _f(v, k)
613
+ cur, want, fn = sw_now['every_min'], x, (lambda actor, x=x: SW.set_settings(ws, actor, every_min=x))
614
+ elif k == 'watch_mode':
615
+ if v not in ('operations', 'always'):
616
+ raise SheetError("watch_mode is 'operations' or 'always'")
617
+ cur, want, fn = sw_now['mode'], v, (lambda actor, v=v: SW.set_settings(ws, actor, mode=v))
618
+ elif k == 'watch_enabled':
619
+ cur, want, fn = sw_now['enabled'], yes(v), (lambda actor, v=v: SW.set_settings(ws, actor, enabled=yes(v)))
620
+ elif k == 'summary_enabled':
621
+ cur, want, fn = ps_now['enabled'], yes(v), (lambda actor, v=v: PS.set_settings(ws, actor, enabled=yes(v)))
622
+ elif k == 'summary_send':
623
+ cur, want, fn = ps_now['send'], yes(v), (lambda actor, v=v: PS.set_settings(ws, actor, send=yes(v)))
624
+ else:
625
+ if not re.fullmatch(r'([01]?\d|2[0-3]):[0-5]\d', v):
626
+ raise SheetError('summary_at is HH:MM, 24-hour, UTC')
627
+ want = f"{int(v.split(':')[0]):02d}:{v.split(':')[1]}"
628
+ cur, fn = ps_now['at'], (lambda actor, w_=want: PS.set_settings(ws, actor, at=w_))
629
+ if cur == want:
630
+ add('settings', rec['_row'], None, 'SAME', f'{k} already {_g(want)}')
631
+ return
632
+ add('settings', rec['_row'], None, 'CHANGE', f'{k} {_g(cur)} -> {_g(want)}', fn)
633
+ guard('settings', rec, b)
634
+
528
635
  steps.sort(key=lambda s: (APPLY_ORDER.index(s['table']), s['row']))
529
636
  counts = {k: sum(1 for s in steps if s['action'] == k) for k in ('NEW', 'CHANGE', 'SAME')}
530
637
  return {'steps': steps, 'errors': sorted(errs, key=lambda e: int(re.match(r'row (\d+)', e).group(1)) if re.match(r'row (\d+)', e) else 0),
@@ -669,4 +776,31 @@ def template(ws) -> str:
669
776
  head('catalogs')
670
777
  for c in OS.catalogs(ws):
671
778
  w.writerow(['# ' + c['file'] + ' is attached (' + str(c['n_events']) + ' events); list it here only to replace it'])
779
+ from . import invoice_check as IC
780
+ from . import rig_state as RS
781
+ from . import seismic_watch as SW
782
+ from . import pad_summary as PS
783
+ head('contract')
784
+ t = IC.terms(ws)
785
+ if t:
786
+ r = t['rates_usd_per_day']
787
+ w.writerow(['', t['day_start'], t.get('report_date', 'start')] + [_g(r.get(k)) for k in IC.KINDS]
788
+ + [_g(t.get('repair_hours_per_job')), _g(t.get('repair_hours_per_month')), t['source']])
789
+ head('rig_time')
790
+ rs_ = RS.settings(ws)
791
+ if rs_.get('targets'):
792
+ tg = rs_['targets']
793
+ w.writerow(['', _g(tg.get('w2w_min')), _g(tg.get('trip_in_ft_h')), _g(tg.get('trip_out_ft_h')), rs_.get('targets_source', '')])
794
+ head('block_weights')
795
+ for wid, v in rs_.get('block_weight_klbf', {}).items():
796
+ w.writerow(['', wid, _g(v)])
797
+ head('casing')
798
+ for wid, lst in RS.casing(ws).items():
799
+ for c in lst:
800
+ w.writerow(['', wid, c['size'], _g(c['shoe_md_ft']), c.get('set') or '', c['source']])
801
+ head('settings')
802
+ sw, ps = SW.settings(ws), PS.settings(ws)
803
+ for k, v in (('watch_enabled', _g(bool(sw['enabled']))), ('watch_every_min', _g(float(sw['every_min']))), ('watch_mode', sw['mode']),
804
+ ('summary_enabled', _g(bool(ps['enabled']))), ('summary_at', ps['at']), ('summary_send', _g(bool(ps['send'])))):
805
+ w.writerow(['', k, v])
672
806
  return buf.getvalue()
@@ -5,7 +5,8 @@ A dossier is a zip:
5
5
  - `manifest.json` - the well or pad, the time, the program and its version, and every file the dossier carries
6
6
  with its size and SHA-256: the reports as they stand (with the history version each one is), the declarations
7
7
  behind them (the well's identity and position, its survey, rates and AFE, runs, penetrator, operations,
8
- catalogues, the fleet), the audit log, and - by hash, or embedded when asked - the source records the reports
8
+ catalogues, the fleet, the contract's terms with the invoices and daily reports attached, the rig-time targets
9
+ and casing), the invoice cross-check and the rig time as they read at signing (`results/`), the audit log, and - by hash, or embedded when asked - the source records the reports
9
10
  were made from; the audit log's hash chain (every line chained to the one before, across its rotated segments)
10
11
  and its length; the previous dossier's id and manifest hash, so the dossiers of a workspace form one chain;
11
12
  - `signature.json` - an Ed25519 signature (RFC 8032) over the manifest's exact bytes, with the public key;
@@ -272,11 +273,15 @@ def _gather(ws, well_id: Optional[str]) -> Dict[str, List[tuple]]:
272
273
  out_list.append((os.path.join(wd, 'well.json'), f"declarations/wells/{w['id']}/well.json"))
273
274
  files_in(os.path.join(wd, 'trajectory'), f"declarations/wells/{w['id']}/trajectory")
274
275
  for rel in (('economics', 'costs.json'), ('drillstring', 'runs.json'), ('completion', 'esp.json'), ('seismicity', 'operations.json'),
275
- ('seismicity', 'catalogs.json'), ('seismicity', 'settings.json'), ('config', 'fleet.json'), ('workspace.json',)):
276
+ ('seismicity', 'catalogs.json'), ('seismicity', 'settings.json'), ('config', 'fleet.json'), ('workspace.json',),
277
+ ('economics', 'invoices', 'terms.json'), ('economics', 'invoices', 'files.json'), ('drillstring', 'rig_time.json'),
278
+ ('drillstring', 'casing.json')):
276
279
  p = os.path.join(ws.path, *rel)
277
280
  if os.path.isfile(p):
278
281
  out_list.append((p, 'declarations/' + '/'.join(rel)))
279
282
  files_in(os.path.join(ws.path, 'seismicity', 'catalogs'), 'declarations/seismicity/catalogs')
283
+ for w in wells: # the contractor's invoices and daily reports, as attached
284
+ files_in(os.path.join(ws.path, 'economics', 'invoices', w['id']), f"declarations/economics/invoices/{w['id']}")
280
285
  for p in audit_segments(ws):
281
286
  out['audit'].append((p, 'audit/' + os.path.basename(p)))
282
287
  for w in wells:
@@ -289,6 +294,32 @@ def _gather(ws, well_id: Optional[str]) -> Dict[str, List[tuple]]:
289
294
  return out
290
295
 
291
296
 
297
+ def _results(ws, well_id: Optional[str]) -> List[tuple]:
298
+ """The invoice cross-check of each well with invoices or reports attached, and the rig time of each well with a
299
+ drilling record, as JSON computed when the dossier is signed (a check that cannot run is left out, and says why)."""
300
+ out = []
301
+ wells = [well_id] if well_id else [w['id'] for w in ws.wells()]
302
+ try:
303
+ from . import invoice_check as IC
304
+ if IC.terms(ws):
305
+ for wid in wells:
306
+ if IC.files(ws, wid):
307
+ r = IC.check(ws, wid)
308
+ out.append((f'results/invoice_check/{wid}.json', json.dumps(r, indent=1, sort_keys=True, default=str).encode('utf-8')))
309
+ except Exception as e:
310
+ out.append(('results/invoice_check/not_run.txt', f'the invoice check did not run: {type(e).__name__}: {e}\n'.encode('utf-8')))
311
+ try:
312
+ from . import rig_state as RS
313
+ p = RS.pad(ws)
314
+ ws_ = [w for w in p['wells'] if w['well_id'] in wells]
315
+ if ws_:
316
+ p = {**p, 'wells': ws_}
317
+ out.append(('results/rig_time.json', json.dumps(p, indent=1, sort_keys=True, default=str).encode('utf-8')))
318
+ except Exception as e:
319
+ out.append(('results/rig_time_not_run.txt', f'rig time did not run: {type(e).__name__}: {e}\n'.encode('utf-8')))
320
+ return out
321
+
322
+
292
323
  def _history_versions(ws) -> Dict[str, dict]:
293
324
  """The latest history entry of each report file, by its path under reports/, with that version's folder."""
294
325
  from . import report_history as RH
@@ -354,6 +385,9 @@ def build(ws, actor: str, version: str, well_id: Optional[str] = None, include_s
354
385
  if hv and hv['sha256'] == row['sha256']:
355
386
  row['history'] = {'dir': hv['dir'], 'written_utc': hv['utc'], 'report_id': hv['report_id']}
356
387
  files.append(row); payload.append((arc, b))
388
+ for arc, b in _results(ws, well_id): # the checks as they read at signing
389
+ files.append({'path': arc, 'role': 'result', 'bytes': len(b), 'sha256': hashlib.sha256(b).hexdigest(), 'workspace_path': None})
390
+ payload.append((arc, b))
357
391
  kh = key_history(ws)
358
392
  if kh['rotations']: # the key's lineage, so a client who knows an earlier fingerprint can follow it
359
393
  b = json.dumps({'schema': 1, 'rotations': kh['rotations'],
@@ -6,7 +6,7 @@
6
6
 
7
7
  **How the watch reads the catalogue**: it runs in the service's background poller, so only while `gea serve` (the control panel) is running. An operation is under way when a declared operation (Operations & Seismicity, or the declarations sheet's `[operations]`) has started and has not ended, at a well with a surface position. A poll reads the USGS real-time summary feed of the past day, all magnitudes, updated every minute - the feed USGS asks automated programs to use - and keeps the earthquakes inside the circle around the operating wells (out to the screening distance, 20 km by default, beyond the farthest). On the first poll, or after a gap of a day or more, it asks the FDSN event service for the whole gap instead (from the operation's start, at most 30 days back). USGS revises magnitudes and locations; the newest version wins, and a revision that reaches a higher protocol level is a new alert.
8
8
 
9
- **The alerts**: `seismic.protocol` (a protocol level reached, once per level), `seismic.event` (any earthquake within the association radius during or after an operation, once), `seismic.watch_down` (no answer from the catalogue for three intervals while an operation runs) and `seismic.watch_up`; `motor.due` (checked every 15 minutes), `penetrator.outside` (every 15 minutes), `dossier.changed` (the newest dossier of each well and of the pad, re-checked daily), and `condition.cleared` when one of the last three ends.
9
+ **The alerts**: `seismic.protocol` (a protocol level reached, once per level), `seismic.event` (any earthquake within the association radius during or after an operation, once), `seismic.watch_down` (no answer from the catalogue for three intervals while an operation runs) and `seismic.watch_up`; `motor.due` (checked every 15 minutes), `penetrator.outside` (every 15 minutes), `dossier.changed` (the newest dossier of each well and of the pad, re-checked daily), `invoice.differs` (a well's invoice cross-check finds a day that differs; checked daily and when a file is attached), and `condition.cleared` when one of the last four ends.
10
10
 
11
11
  **Getting them to a person**: every alert is written to `records/alerts.jsonl` and shown on the Alarms tab. To have it sent, set up notifications (Administration, Notifications: load the example, edit, validate and commit) with an email (SMTP) or webhook channel and a rule naming the alert, for example `{"event": "seismic.protocol", "channels": ["mail"]}`; the password stays in an environment variable named by `password_env`. A text message: most carriers deliver email sent to the phone's email-to-text address, or a webhook to a messaging service. The `quiet_s` window keeps one alert from repeating.
12
12