gea-program 0.46.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.46.0 → gea_program-0.48.0}/PKG-INFO +26 -1
  2. {gea_program-0.46.0 → gea_program-0.48.0}/README.md +25 -0
  3. {gea_program-0.46.0 → gea_program-0.48.0}/gea/__init__.py +1 -1
  4. {gea_program-0.46.0 → gea_program-0.48.0}/gea/__main__.py +51 -3
  5. {gea_program-0.46.0 → gea_program-0.48.0}/gea/acceptance_tests.py +354 -0
  6. {gea_program-0.46.0 → gea_program-0.48.0}/gea/alerts.py +20 -2
  7. {gea_program-0.46.0 → gea_program-0.48.0}/gea/declarations.py +136 -2
  8. {gea_program-0.46.0 → gea_program-0.48.0}/gea/dossier.py +36 -2
  9. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/alerts.md +1 -1
  10. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/declare.md +2 -2
  11. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/dossier.md +2 -0
  12. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/invoice-check.md +1 -1
  13. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/pad-summary.md +1 -1
  14. gea_program-0.48.0/gea/help/rig-time.md +17 -0
  15. {gea_program-0.46.0 → gea_program-0.48.0}/gea/helplib.py +2 -1
  16. {gea_program-0.46.0 → gea_program-0.48.0}/gea/invoice_check.py +37 -9
  17. {gea_program-0.46.0 → gea_program-0.48.0}/gea/notify.py +3 -1
  18. {gea_program-0.46.0 → gea_program-0.48.0}/gea/pad_summary.py +41 -4
  19. gea_program-0.48.0/gea/rig_state.py +787 -0
  20. {gea_program-0.46.0 → gea_program-0.48.0}/gea/service.py +31 -1
  21. {gea_program-0.46.0 → gea_program-0.48.0}/gea/web/app.html +73 -8
  22. gea_program-0.48.0/gea/xlsx_read.py +205 -0
  23. {gea_program-0.46.0 → gea_program-0.48.0}/gea_program.egg-info/PKG-INFO +26 -1
  24. {gea_program-0.46.0 → gea_program-0.48.0}/gea_program.egg-info/SOURCES.txt +3 -0
  25. {gea_program-0.46.0 → gea_program-0.48.0}/pyproject.toml +1 -1
  26. {gea_program-0.46.0 → gea_program-0.48.0}/LICENSE +0 -0
  27. {gea_program-0.46.0 → gea_program-0.48.0}/gea/BENCH_TEST_PROTOCOL.md +0 -0
  28. {gea_program-0.46.0 → gea_program-0.48.0}/gea/accuracy_statement.py +0 -0
  29. {gea_program-0.46.0 → gea_program-0.48.0}/gea/alarm_engine.py +0 -0
  30. {gea_program-0.46.0 → gea_program-0.48.0}/gea/backup.py +0 -0
  31. {gea_program-0.46.0 → gea_program-0.48.0}/gea/bench.py +0 -0
  32. {gea_program-0.46.0 → gea_program-0.48.0}/gea/blind_harness.py +0 -0
  33. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/acex_lomonosov_age_depth_model.provenance.json +0 -0
  34. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/acex_lomonosov_age_depth_model.txt +0 -0
  35. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/agassiz77_canada_temperature.csv +0 -0
  36. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/agassiz77_canada_temperature.provenance.json +0 -0
  37. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/barbados_110_consolidation.provenance.json +0 -0
  38. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/barbados_110_consolidation.txt +0 -0
  39. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/bengal_u1452_grain_size.provenance.json +0 -0
  40. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/bengal_u1452_grain_size.txt +0 -0
  41. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/blake_164_methane_isotopes.provenance.json +0 -0
  42. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/blake_164_methane_isotopes.txt +0 -0
  43. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +0 -0
  44. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.txt +0 -0
  45. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/collingwood_1_28_ks_complete.las +0 -0
  46. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/collingwood_1_28_ks_complete.provenance.json +0 -0
  47. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/costa_rica_odp_friction_envelope.provenance.json +0 -0
  48. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/costa_rica_odp_friction_envelope.txt +0 -0
  49. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +0 -0
  50. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +0 -0
  51. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/dsdp_504b_physical_properties.provenance.json +0 -0
  52. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/dsdp_504b_physical_properties.txt +0 -0
  53. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/dsdp_504b_sound_velocity.provenance.json +0 -0
  54. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/dsdp_504b_sound_velocity.txt +0 -0
  55. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/elgygytgyn_5011_turbidites.provenance.json +0 -0
  56. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/elgygytgyn_5011_turbidites.txt +0 -0
  57. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/epica_domec_co2_800kyr.provenance.json +0 -0
  58. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/epica_domec_co2_800kyr.txt +0 -0
  59. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/fram_909_organic_petrography.provenance.json +0 -0
  60. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/fram_909_organic_petrography.txt +0 -0
  61. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/gbr_325_coral_uth_ages.provenance.json +0 -0
  62. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/gbr_325_coral_uth_ages.txt +0 -0
  63. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/gisp2_greenland_temperature.csv +0 -0
  64. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/gisp2_greenland_temperature.provenance.json +0 -0
  65. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/gom_308_t2p_insitu.provenance.json +0 -0
  66. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/gom_308_t2p_insitu.txt +0 -0
  67. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/guaymas_385_dom_d13c.provenance.json +0 -0
  68. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/guaymas_385_dom_d13c.txt +0 -0
  69. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +0 -0
  70. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/hikurangi_u1520_friction_insitu.txt +0 -0
  71. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +0 -0
  72. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +0 -0
  73. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/iodp_u1324_pore_pressure.provenance.json +0 -0
  74. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/iodp_u1324_pore_pressure.txt +0 -0
  75. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/jfast_c0019_slow_slip_events.provenance.json +0 -0
  76. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/jfast_c0019_slow_slip_events.txt +0 -0
  77. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/kennetcook_2_p129_excerpt.las +0 -0
  78. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/kennetcook_2_p129_excerpt.provenance.json +0 -0
  79. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_bhgm_density.dat +0 -0
  80. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_bhgm_density.provenance.json +0 -0
  81. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +0 -0
  82. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_complog_6020_excerpt.txt +0 -0
  83. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_hlog246_temperature.dat +0 -0
  84. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_hlog246_temperature.provenance.json +0 -0
  85. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_rockmech_compress.dat +0 -0
  86. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_rockmech_compress.provenance.json +0 -0
  87. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +0 -0
  88. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +0 -0
  89. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ktb_vb_rockmech_compress.dat +0 -0
  90. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ktb_vb_rockmech_compress.provenance.json +0 -0
  91. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ktb_vb_vlog251_temperature.dat +0 -0
  92. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ktb_vb_vlog251_temperature.provenance.json +0 -0
  93. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/l06_06_nl_survey.csv +0 -0
  94. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/l06_06_nl_survey.provenance.json +0 -0
  95. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/l07_01_nl_excerpt.las +0 -0
  96. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/l07_01_nl_excerpt.provenance.json +0 -0
  97. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +0 -0
  98. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/mariana_1200_serpentinite_geochem.txt +0 -0
  99. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/med_160_sapropels.provenance.json +0 -0
  100. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/med_160_sapropels.txt +0 -0
  101. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/nankai_megasplay_shear_strength.provenance.json +0 -0
  102. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/nankai_megasplay_shear_strength.txt +0 -0
  103. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/odp_1027b_thermal_conductivity.provenance.json +0 -0
  104. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/odp_1027b_thermal_conductivity.txt +0 -0
  105. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/odp_1027c_cork_temperature.provenance.json +0 -0
  106. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/odp_1027c_cork_temperature.txt +0 -0
  107. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/odp_1165b_thermal_conductivity.provenance.json +0 -0
  108. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/odp_1165b_thermal_conductivity.txt +0 -0
  109. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +0 -0
  110. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.txt +0 -0
  111. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +0 -0
  112. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/odp_504b_dike_elastic_moduli.txt +0 -0
  113. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +0 -0
  114. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/odp_504b_leg137_borehole_fluids.txt +0 -0
  115. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +0 -0
  116. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.txt +0 -0
  117. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/peru_201_sulfate_reduction.provenance.json +0 -0
  118. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/peru_201_sulfate_reduction.txt +0 -0
  119. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/scorpio_e1_sa_excerpt.las +0 -0
  120. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/scorpio_e1_sa_excerpt.provenance.json +0 -0
  121. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/sumatra_362_cohesion.provenance.json +0 -0
  122. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/sumatra_362_cohesion.txt +0 -0
  123. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/university_6_17_no1_tx_excerpt.las +0 -0
  124. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +0 -0
  125. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ursa_308_xrd_mineralogy.provenance.json +0 -0
  126. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/ursa_308_xrd_mineralogy.txt +0 -0
  127. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/volve_15_9_19_sr_excerpt.las +0 -0
  128. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +0 -0
  129. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/volve_15_9_19a_core_excerpt.csv +0 -0
  130. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +0 -0
  131. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/volve_f12_f14_production_excerpt.csv +0 -0
  132. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/volve_f12_f14_production_excerpt.provenance.json +0 -0
  133. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/walvis_208_petm_carbonate.provenance.json +0 -0
  134. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/walvis_208_petm_carbonate.txt +0 -0
  135. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/woodlark_1109_rock_eval.provenance.json +0 -0
  136. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog/woodlark_1109_rock_eval.txt +0 -0
  137. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog_map.py +0 -0
  138. {gea_program-0.46.0 → gea_program-0.48.0}/gea/catalog_register.py +0 -0
  139. {gea_program-0.46.0 → gea_program-0.48.0}/gea/certificates.py +0 -0
  140. {gea_program-0.46.0 → gea_program-0.48.0}/gea/cli.py +0 -0
  141. {gea_program-0.46.0 → gea_program-0.48.0}/gea/client_reports.py +0 -0
  142. {gea_program-0.46.0 → gea_program-0.48.0}/gea/config_versioning.py +0 -0
  143. {gea_program-0.46.0 → gea_program-0.48.0}/gea/conformance.py +0 -0
  144. {gea_program-0.46.0 → gea_program-0.48.0}/gea/control_room.py +0 -0
  145. {gea_program-0.46.0 → gea_program-0.48.0}/gea/correlation.py +0 -0
  146. {gea_program-0.46.0 → gea_program-0.48.0}/gea/dashboard.py +0 -0
  147. {gea_program-0.46.0 → gea_program-0.48.0}/gea/deviation.py +0 -0
  148. {gea_program-0.46.0 → gea_program-0.48.0}/gea/dlis.py +0 -0
  149. {gea_program-0.46.0 → gea_program-0.48.0}/gea/dnp3.py +0 -0
  150. {gea_program-0.46.0 → gea_program-0.48.0}/gea/doctor.py +0 -0
  151. {gea_program-0.46.0 → gea_program-0.48.0}/gea/dossier_verify.py +0 -0
  152. {gea_program-0.46.0 → gea_program-0.48.0}/gea/downhole_engine.py +0 -0
  153. {gea_program-0.46.0 → gea_program-0.48.0}/gea/drift_monitor.py +0 -0
  154. {gea_program-0.46.0 → gea_program-0.48.0}/gea/drill_string.py +0 -0
  155. {gea_program-0.46.0 → gea_program-0.48.0}/gea/earth_model.py +0 -0
  156. {gea_program-0.46.0 → gea_program-0.48.0}/gea/economics.py +0 -0
  157. {gea_program-0.46.0 → gea_program-0.48.0}/gea/ed25519.py +0 -0
  158. {gea_program-0.46.0 → gea_program-0.48.0}/gea/edr.py +0 -0
  159. {gea_program-0.46.0 → gea_program-0.48.0}/gea/elevation.py +0 -0
  160. {gea_program-0.46.0 → gea_program-0.48.0}/gea/etp.py +0 -0
  161. {gea_program-0.46.0 → gea_program-0.48.0}/gea/etp12_schemas.json +0 -0
  162. {gea_program-0.46.0 → gea_program-0.48.0}/gea/example_register_map.json +0 -0
  163. {gea_program-0.46.0 → gea_program-0.48.0}/gea/fat_sat.py +0 -0
  164. {gea_program-0.46.0 → gea_program-0.48.0}/gea/files.py +0 -0
  165. {gea_program-0.46.0 → gea_program-0.48.0}/gea/fixtures/README.md +0 -0
  166. {gea_program-0.46.0 → gea_program-0.48.0}/gea/fixtures/comcat_delaware_basin_2026-09.txt +0 -0
  167. {gea_program-0.46.0 → gea_program-0.48.0}/gea/fixtures/comcat_mahoning_1990-2026.txt +0 -0
  168. {gea_program-0.46.0 → gea_program-0.48.0}/gea/fixtures/comcat_youngstown_2011-2012.txt +0 -0
  169. {gea_program-0.46.0 → gea_program-0.48.0}/gea/fixtures/ne_110m_coastline.json +0 -0
  170. {gea_program-0.46.0 → gea_program-0.48.0}/gea/follower.py +0 -0
  171. {gea_program-0.46.0 → gea_program-0.48.0}/gea/forward_model.py +0 -0
  172. {gea_program-0.46.0 → gea_program-0.48.0}/gea/friction_priors.py +0 -0
  173. {gea_program-0.46.0 → gea_program-0.48.0}/gea/gamma.py +0 -0
  174. {gea_program-0.46.0 → gea_program-0.48.0}/gea/gauge_aging.py +0 -0
  175. {gea_program-0.46.0 → gea_program-0.48.0}/gea/gauge_specs.py +0 -0
  176. {gea_program-0.46.0 → gea_program-0.48.0}/gea/geodesy.py +0 -0
  177. {gea_program-0.46.0 → gea_program-0.48.0}/gea/gravity_reference.py +0 -0
  178. {gea_program-0.46.0 → gea_program-0.48.0}/gea/hartip.py +0 -0
  179. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/TESTER_GUIDE.md +0 -0
  180. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/alarms.md +0 -0
  181. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/audit-update.md +0 -0
  182. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/backup.md +0 -0
  183. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/catalog.md +0 -0
  184. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/charts.md +0 -0
  185. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/conformance.md +0 -0
  186. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/control-room.md +0 -0
  187. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/data-in.md +0 -0
  188. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/dlis.md +0 -0
  189. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/doctor.md +0 -0
  190. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/drift.md +0 -0
  191. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/drillstring.md +0 -0
  192. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/economics.md +0 -0
  193. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/edr.md +0 -0
  194. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/etp.md +0 -0
  195. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/field-ports.md +0 -0
  196. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/files.md +0 -0
  197. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/historian.md +0 -0
  198. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/instruments.md +0 -0
  199. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/interval.md +0 -0
  200. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/map.md +0 -0
  201. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/month-end.md +0 -0
  202. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/notifications.md +0 -0
  203. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/opseis.md +0 -0
  204. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/osdu.md +0 -0
  205. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/patches.md +0 -0
  206. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/penetrator.md +0 -0
  207. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/ppdm.md +0 -0
  208. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/quakeml.md +0 -0
  209. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/quality.md +0 -0
  210. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/report-history.md +0 -0
  211. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/seedlink.md +0 -0
  212. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/seismic.md +0 -0
  213. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/setup.md +0 -0
  214. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/simulation.md +0 -0
  215. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/site.md +0 -0
  216. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/sites.md +0 -0
  217. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/slip.md +0 -0
  218. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/soak.md +0 -0
  219. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/sra.md +0 -0
  220. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/start.md +0 -0
  221. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/structure.md +0 -0
  222. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/tools.md +0 -0
  223. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/transients.md +0 -0
  224. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/upkeep.md +0 -0
  225. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/vibration.md +0 -0
  226. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/well-report.md +0 -0
  227. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/well-tests.md +0 -0
  228. {gea_program-0.46.0 → gea_program-0.48.0}/gea/help/well3d.md +0 -0
  229. {gea_program-0.46.0 → gea_program-0.48.0}/gea/housekeeping.py +0 -0
  230. {gea_program-0.46.0 → gea_program-0.48.0}/gea/interval.py +0 -0
  231. {gea_program-0.46.0 → gea_program-0.48.0}/gea/inverse_engine.py +0 -0
  232. {gea_program-0.46.0 → gea_program-0.48.0}/gea/jobs.py +0 -0
  233. {gea_program-0.46.0 → gea_program-0.48.0}/gea/live_ports.py +0 -0
  234. {gea_program-0.46.0 → gea_program-0.48.0}/gea/loadtest.py +0 -0
  235. {gea_program-0.46.0 → gea_program-0.48.0}/gea/matplotlib_demo.py +0 -0
  236. {gea_program-0.46.0 → gea_program-0.48.0}/gea/modbus.py +0 -0
  237. {gea_program-0.46.0 → gea_program-0.48.0}/gea/modbus_rtu.py +0 -0
  238. {gea_program-0.46.0 → gea_program-0.48.0}/gea/model_card.py +0 -0
  239. {gea_program-0.46.0 → gea_program-0.48.0}/gea/month_end.py +0 -0
  240. {gea_program-0.46.0 → gea_program-0.48.0}/gea/mqtt_port.py +0 -0
  241. {gea_program-0.46.0 → gea_program-0.48.0}/gea/op_seismicity.py +0 -0
  242. {gea_program-0.46.0 → gea_program-0.48.0}/gea/opcua_port.py +0 -0
  243. {gea_program-0.46.0 → gea_program-0.48.0}/gea/operator_app.py +0 -0
  244. {gea_program-0.46.0 → gea_program-0.48.0}/gea/osdu.py +0 -0
  245. {gea_program-0.46.0 → gea_program-0.48.0}/gea/patches.py +0 -0
  246. {gea_program-0.46.0 → gea_program-0.48.0}/gea/pdf_report.py +0 -0
  247. {gea_program-0.46.0 → gea_program-0.48.0}/gea/penetrator.py +0 -0
  248. {gea_program-0.46.0 → gea_program-0.48.0}/gea/permits.py +0 -0
  249. {gea_program-0.46.0 → gea_program-0.48.0}/gea/piwebapi.py +0 -0
  250. {gea_program-0.46.0 → gea_program-0.48.0}/gea/ports.py +0 -0
  251. {gea_program-0.46.0 → gea_program-0.48.0}/gea/ppdm.py +0 -0
  252. {gea_program-0.46.0 → gea_program-0.48.0}/gea/profile_catalog.py +0 -0
  253. {gea_program-0.46.0 → gea_program-0.48.0}/gea/project.py +0 -0
  254. {gea_program-0.46.0 → gea_program-0.48.0}/gea/qt6_downhole_app.py +0 -0
  255. {gea_program-0.46.0 → gea_program-0.48.0}/gea/quakeml.py +0 -0
  256. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reconciler.py +0 -0
  257. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/IU_ANMO_10_BHZ_response.provenance.json +0 -0
  258. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/IU_ANMO_10_BHZ_response.xml +0 -0
  259. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/eia_henry_hub_daily.csv.gz +0 -0
  260. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/eia_prices.provenance.json +0 -0
  261. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/eia_wti_daily.csv.gz +0 -0
  262. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/libmseed_steim.provenance.json +0 -0
  263. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/libmseed_steim1_4096.mseed +0 -0
  264. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/libmseed_steim2_4096.mseed +0 -0
  265. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/libmseed_steim2_512.mseed +0 -0
  266. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/mahoning_example.json +0 -0
  267. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/mud_motor_specs.json +0 -0
  268. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/penetrator_ratings.json +0 -0
  269. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/seismicity_protocols.json +0 -0
  270. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/tool_sources.json +0 -0
  271. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/us_outline.json +0 -0
  272. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/us_outline.provenance.json +0 -0
  273. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/volve_3d.provenance.json +0 -0
  274. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/volve_f4_interpretation.las.gz +0 -0
  275. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/volve_f4_logs.provenance.json +0 -0
  276. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/volve_f4_lwd.las.gz +0 -0
  277. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/volve_surfaces.json.gz +0 -0
  278. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/volve_traj_F-10.xml.gz +0 -0
  279. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/volve_traj_F-4.xml.gz +0 -0
  280. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/volve_traj_F-5_12.25in.xml.gz +0 -0
  281. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/volve_traj_F-5_17.5in.xml.gz +0 -0
  282. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/volve_traj_F-5_36in.xml.gz +0 -0
  283. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/volve_traj_F-5_8.5in.xml.gz +0 -0
  284. {gea_program-0.46.0 → gea_program-0.48.0}/gea/reference/volve_well_picks.csv.gz +0 -0
  285. {gea_program-0.46.0 → gea_program-0.48.0}/gea/report_history.py +0 -0
  286. {gea_program-0.46.0 → gea_program-0.48.0}/gea/rock_inventory.py +0 -0
  287. {gea_program-0.46.0 → gea_program-0.48.0}/gea/sample_record.py +0 -0
  288. {gea_program-0.46.0 → gea_program-0.48.0}/gea/sample_well_profile.csv +0 -0
  289. {gea_program-0.46.0 → gea_program-0.48.0}/gea/sbom.py +0 -0
  290. {gea_program-0.46.0 → gea_program-0.48.0}/gea/seedlink.py +0 -0
  291. {gea_program-0.46.0 → gea_program-0.48.0}/gea/segy.py +0 -0
  292. {gea_program-0.46.0 → gea_program-0.48.0}/gea/seismic.py +0 -0
  293. {gea_program-0.46.0 → gea_program-0.48.0}/gea/seismic_array.py +0 -0
  294. {gea_program-0.46.0 → gea_program-0.48.0}/gea/seismic_assoc.py +0 -0
  295. {gea_program-0.46.0 → gea_program-0.48.0}/gea/seismic_detect.py +0 -0
  296. {gea_program-0.46.0 → gea_program-0.48.0}/gea/seismic_film.py +0 -0
  297. {gea_program-0.46.0 → gea_program-0.48.0}/gea/seismic_harmonic.py +0 -0
  298. {gea_program-0.46.0 → gea_program-0.48.0}/gea/seismic_qc.py +0 -0
  299. {gea_program-0.46.0 → gea_program-0.48.0}/gea/seismic_response.py +0 -0
  300. {gea_program-0.46.0 → gea_program-0.48.0}/gea/seismic_signature.py +0 -0
  301. {gea_program-0.46.0 → gea_program-0.48.0}/gea/seismic_track.py +0 -0
  302. {gea_program-0.46.0 → gea_program-0.48.0}/gea/seismic_uncertainty.py +0 -0
  303. {gea_program-0.46.0 → gea_program-0.48.0}/gea/seismic_unlisted.py +0 -0
  304. {gea_program-0.46.0 → gea_program-0.48.0}/gea/seismic_watch.py +0 -0
  305. {gea_program-0.46.0 → gea_program-0.48.0}/gea/sensor_swap.py +0 -0
  306. {gea_program-0.46.0 → gea_program-0.48.0}/gea/series_cache.py +0 -0
  307. {gea_program-0.46.0 → gea_program-0.48.0}/gea/service_life.py +0 -0
  308. {gea_program-0.46.0 → gea_program-0.48.0}/gea/setup_checklist.py +0 -0
  309. {gea_program-0.46.0 → gea_program-0.48.0}/gea/shapefile.py +0 -0
  310. {gea_program-0.46.0 → gea_program-0.48.0}/gea/shell.py +0 -0
  311. {gea_program-0.46.0 → gea_program-0.48.0}/gea/shut_in.py +0 -0
  312. {gea_program-0.46.0 → gea_program-0.48.0}/gea/simulation.py +0 -0
  313. {gea_program-0.46.0 → gea_program-0.48.0}/gea/site_map.py +0 -0
  314. {gea_program-0.46.0 → gea_program-0.48.0}/gea/sla_report.py +0 -0
  315. {gea_program-0.46.0 → gea_program-0.48.0}/gea/slip.py +0 -0
  316. {gea_program-0.46.0 → gea_program-0.48.0}/gea/soak.py +0 -0
  317. {gea_program-0.46.0 → gea_program-0.48.0}/gea/sra.py +0 -0
  318. {gea_program-0.46.0 → gea_program-0.48.0}/gea/store_forward.py +0 -0
  319. {gea_program-0.46.0 → gea_program-0.48.0}/gea/strata_join.py +0 -0
  320. {gea_program-0.46.0 → gea_program-0.48.0}/gea/structure.py +0 -0
  321. {gea_program-0.46.0 → gea_program-0.48.0}/gea/supervisor.py +0 -0
  322. {gea_program-0.46.0 → gea_program-0.48.0}/gea/survey_cmd.py +0 -0
  323. {gea_program-0.46.0 → gea_program-0.48.0}/gea/survey_view.py +0 -0
  324. {gea_program-0.46.0 → gea_program-0.48.0}/gea/telemetry.py +0 -0
  325. {gea_program-0.46.0 → gea_program-0.48.0}/gea/tool_catalog.py +0 -0
  326. {gea_program-0.46.0 → gea_program-0.48.0}/gea/tool_library.py +0 -0
  327. {gea_program-0.46.0 → gea_program-0.48.0}/gea/tool_taxonomy.py +0 -0
  328. {gea_program-0.46.0 → gea_program-0.48.0}/gea/transient.py +0 -0
  329. {gea_program-0.46.0 → gea_program-0.48.0}/gea/vibration.py +0 -0
  330. {gea_program-0.46.0 → gea_program-0.48.0}/gea/well3d.py +0 -0
  331. {gea_program-0.46.0 → gea_program-0.48.0}/gea/well_assembler.py +0 -0
  332. {gea_program-0.46.0 → gea_program-0.48.0}/gea/well_report.py +0 -0
  333. {gea_program-0.46.0 → gea_program-0.48.0}/gea/well_test_validation.py +0 -0
  334. {gea_program-0.46.0 → gea_program-0.48.0}/gea/wits0.py +0 -0
  335. {gea_program-0.46.0 → gea_program-0.48.0}/gea/witsml.py +0 -0
  336. {gea_program-0.46.0 → gea_program-0.48.0}/gea/workspace.py +0 -0
  337. {gea_program-0.46.0 → gea_program-0.48.0}/gea_program.egg-info/dependency_links.txt +0 -0
  338. {gea_program-0.46.0 → gea_program-0.48.0}/gea_program.egg-info/entry_points.txt +0 -0
  339. {gea_program-0.46.0 → gea_program-0.48.0}/gea_program.egg-info/requires.txt +0 -0
  340. {gea_program-0.46.0 → gea_program-0.48.0}/gea_program.egg-info/top_level.txt +0 -0
  341. {gea_program-0.46.0 → gea_program-0.48.0}/setup.cfg +0 -0
  342. {gea_program-0.46.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.46.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,31 @@ 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
+
664
+ ### Rig time
665
+
666
+ What the rig did each second of the drilling record, read from the hook
667
+ load, the block, the bit, the pumps and the rotary: drilling, in the slips,
668
+ tripping, circulating, reaming, off the hook. Every drilling connection is
669
+ timed weight to weight with its three parts, every trip by its speed over
670
+ the pipe-handling hours with its breaks beside it, and the time each loses
671
+ against a target - the drilling program's, or this pad's own best quarter -
672
+ is summed per report day and priced at the contract's operating rate. The
673
+ **Rig time** tab, `gea rig-time`, `gea help rig-time`; the invoice check
674
+ shows each day's lost time beside the bill.
675
+
651
676
  ### The invoice cross-check
652
677
 
653
678
  The drilling contractor's daywork invoice, day by day, against the daily
@@ -609,6 +609,31 @@ 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
+
625
+ ### Rig time
626
+
627
+ What the rig did each second of the drilling record, read from the hook
628
+ load, the block, the bit, the pumps and the rotary: drilling, in the slips,
629
+ tripping, circulating, reaming, off the hook. Every drilling connection is
630
+ timed weight to weight with its three parts, every trip by its speed over
631
+ the pipe-handling hours with its breaks beside it, and the time each loses
632
+ against a target - the drilling program's, or this pad's own best quarter -
633
+ is summed per report day and priced at the contract's operating rate. The
634
+ **Rig time** tab, `gea rig-time`, `gea help rig-time`; the invoice check
635
+ shows each day's lost time beside the bill.
636
+
612
637
  ### The invoice cross-check
613
638
 
614
639
  The drilling contractor's daywork invoice, day by day, against the daily
@@ -123,7 +123,7 @@ from .profile_catalog import (
123
123
  read_pangaea_txt,
124
124
  )
125
125
 
126
- __version__ = "0.46.0"
126
+ __version__ = "0.48.0"
127
127
  __all__ = [
128
128
  "aging_rate", "rate_psi_yr", "accuracy_psi",
129
129
  "Sensor", "SimulatorConfig", "DownholeEngine",
@@ -590,6 +590,21 @@ def main(argv=None) -> int:
590
590
  p_dz.add_argument("--actor", type=str, default="cli")
591
591
  p_dz.add_argument("--json", action="store_true")
592
592
 
593
+ p_rt = sub.add_parser("rig-time", help="what the rig did each second of the drilling record - drilling, connections, trips, the slips - and the time lost against the pad's own best")
594
+ p_rt.add_argument("--workspace", type=str, required=True)
595
+ p_rt.add_argument("--well", type=str, default=None, help="with --write: one well's report (without it, the pad's)")
596
+ p_rt.add_argument("--write", action="store_true", help="write the client report")
597
+ p_rt.add_argument("--w2w-min", type=str, default=None, help="declare the weight-to-weight connection target, minutes ('' removes it)")
598
+ p_rt.add_argument("--trip-in", type=str, default=None, help="declare the tripping-in target, ft/h")
599
+ p_rt.add_argument("--trip-out", type=str, default=None, help="declare the tripping-out target, ft/h")
600
+ p_rt.add_argument("--source", type=str, default="", help="where the targets came from")
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")
605
+ p_rt.add_argument("--actor", type=str, default="cli")
606
+ p_rt.add_argument("--json", action="store_true")
607
+
593
608
  p_iv = sub.add_parser("invoice-check", help="the drilling contractor's daywork invoice, day by day, against the daily drilling reports and the drilling record, at the contract's terms")
594
609
  p_iv.add_argument("--workspace", type=str, default=None)
595
610
  p_iv.add_argument("--well", type=str, default=None)
@@ -2193,9 +2208,10 @@ def main(argv=None) -> int:
2193
2208
  if not a.file:
2194
2209
  raise SystemExit("declare: give --file <sheet.csv> (or --template <out.csv> to start one)")
2195
2210
  try:
2196
- with open(a.file, encoding="utf-8-sig") as f:
2197
- text_ = f.read()
2198
- 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:
2199
2215
  raise SystemExit(f"declare: {e}")
2200
2216
  p = _DC.apply(ws, text_, a.actor, os.path.dirname(os.path.abspath(a.file)), write=a.apply)
2201
2217
  print(_json.dumps(p, default=str) if a.json else _DC.text(p))
@@ -2356,6 +2372,38 @@ def main(argv=None) -> int:
2356
2372
  print(f" {os.path.join(ws.reports_dir, e['path'])}")
2357
2373
  print(f" key fingerprint {e['fingerprint']}")
2358
2374
  return 0
2375
+ elif a.cmd == "rig-time":
2376
+ import json as _json
2377
+ from . import rig_state as _RS
2378
+ from .cli import _safe_stdout
2379
+ from .workspace import Workspace, WorkspaceError
2380
+ _safe_stdout()
2381
+ try:
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
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:
2390
+ if a.block_weight is not None and not a.well:
2391
+ raise ValueError("--block-weight needs --well")
2392
+ s_ = _RS.set_settings(ws, a.actor, a.source, a.w2w_min, a.trip_in, a.trip_out, (a.well, a.block_weight) if a.block_weight is not None else None)
2393
+ print("targets: " + (", ".join(f"{k} {v:g}" for k, v in s_.get("targets", {}).items()) or "none declared")
2394
+ + "; block weights: " + (", ".join(f"{k} {v:g} klbf" for k, v in s_.get("block_weight_klbf", {}).items()) or "estimated"))
2395
+ return 0
2396
+ if a.write:
2397
+ r_ = _RS.write_report(ws, __version__, a.actor, a.well)
2398
+ print(f"wrote {r_['report_id']}: {r_['paths']['html']}")
2399
+ if r_['paths'].get('pdf'):
2400
+ print(f" printable: {r_['paths']['pdf']}")
2401
+ return 0
2402
+ p_ = _RS.pad(ws)
2403
+ except (KeyError, ValueError, OSError, WorkspaceError) as e:
2404
+ raise SystemExit(f"rig-time: {e.args[0] if e.args else e}")
2405
+ print(_json.dumps(p_, default=str) if a.json else _RS.text(p_))
2406
+ return 0
2359
2407
  elif a.cmd == "invoice-check":
2360
2408
  import json as _json
2361
2409
  from . import invoice_check as _IC
@@ -10761,6 +10761,358 @@ def section_co_invoice_check(tmp: str) -> None:
10761
10761
  + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
10762
10762
 
10763
10763
 
10764
+ def section_cp_rig_time(tmp: str) -> None:
10765
+ """Section CP - rig time: a recorder export with known answers - three drilling connections of 6, 9 and 15 minutes
10766
+ weight to weight, a 3,000 ft trip out at 2,449 ft/h and in at 3,333 ft/h over their pipe-handling hours, the block's
10767
+ 40 klbf - read back to the second; the slips not taken from a light string pushed into a lateral, a depth re-set
10768
+ not taken for pipe moving, the weight off the hook for half an hour not a connection, the slips found from the block
10769
+ when there is no hook load; the targets as the pad's best quarter or as declared, each connection's and trip's time
10770
+ past them, priced at the contract's operating rate and beside each day of the invoice check; the report with its
10771
+ PDF; the API, the CLI, the tab and the help page."""
10772
+ import http.cookiejar
10773
+ import subprocess as _sp
10774
+ import urllib.request
10775
+ import urllib.error
10776
+ from datetime import datetime as _dtm
10777
+ from . import __version__
10778
+ from . import helplib as H
10779
+ from . import invoice_check as IC
10780
+ from . import pdf_report as PDF
10781
+ from . import rig_state as RS
10782
+ from .workspace import Workspace
10783
+ from .service import Service, Users
10784
+ pkg = Path(__file__).parent
10785
+ d = Path(tmp, "cp"); d.mkdir()
10786
+ F = RS.selftest_edr(str(d))
10787
+ E_ = F["expect"]
10788
+ wsp = str(d / "ws"); ws = Workspace.create(wsp, "Pad CP", actor="tester")
10789
+ w1 = ws.add_well_file(F["path"], display="RIG-1", actor="tester")
10790
+ r = RS.well(ws, w1["id"])
10791
+
10792
+ # -- CP1 the states, the connections, the trips ------------------------------------------------------------------
10793
+ cs = r["connections"]
10794
+ tr = {x["direction"]: x for x in r["trips"]}
10795
+ near = lambda a, b, tol: a is not None and abs(a - b) <= tol
10796
+ t = np.arange(0.0, 7200.0)
10797
+ n = len(t)
10798
+ zeros = np.zeros(n)
10799
+ lateral = RS.classify(t, {"hookload": np.full(n, 45.0), "block": 100.0 - t * 0.01, "bit": 15000.0 + t * 1.0, "hole": np.full(n, 25000.0),
10800
+ "flow": zeros, "spp": None, "rpm": zeros}, np.zeros(n, bool), block_weight=40.0)
10801
+ jump_bit = np.where(t < 3600, 9000.0, 9600.0)
10802
+ jump = RS.classify(t, {"hookload": np.full(n, 200.0), "block": np.full(n, 50.0), "bit": jump_bit, "hole": np.full(n, 9700.0), "flow": zeros,
10803
+ "spp": None, "rpm": zeros}, np.zeros(n, bool), block_weight=40.0)
10804
+ hang = RS.classify(t, {"hookload": np.full(n, 40.0), "block": np.full(n, 50.0), "bit": np.full(n, 9000.0), "hole": np.full(n, 9700.0), "flow": zeros,
10805
+ "spp": None, "rpm": zeros}, np.zeros(n, bool), block_weight=40.0)
10806
+ blk = 10.0 + (t % 120.0)
10807
+ nohl = RS.classify(t, {"hookload": None, "block": blk, "bit": np.full(n, 9000.0), "hole": np.full(n, 9700.0), "flow": zeros, "spp": None, "rpm": zeros},
10808
+ np.zeros(n, bool))
10809
+ c1 = {
10810
+ "connections": [round(c["w2w_min"]) for c in cs] == [6, 9, 15] and all(near(c["w2w_min"], e, 0.1) for c, e in zip(cs, E_["connections_w2w_min"]))
10811
+ and all(near(c["slips_min"], e, 0.1) for c, e in zip(cs, E_["slips_min"])) and all(near(c["pre_min"], 1.0, 0.1) and near(c["post_min"], 1.0, 0.1) for c in cs),
10812
+ "trips": set(tr) == {"IN", "OUT"} and near(tr["OUT"]["speed_ft_h"], E_["trip_out_ft_h"], 0.02 * E_["trip_out_ft_h"])
10813
+ and near(tr["IN"]["speed_ft_h"], E_["trip_in_ft_h"], 0.02 * E_["trip_in_ft_h"]) and near(tr["OUT"]["feet"], 3000, 30) and tr["OUT"]["connections"] >= 29
10814
+ and near(tr["OUT"]["moving_speed_ft_h"], 6000, 600),
10815
+ "states": near(r["hours"]["ROTARY DRILLING"], 2.0, 0.02) and r["hours"]["SLIDE DRILLING"] == 0 and r["hours"]["OFF THE HOOK"] == 0
10816
+ and near(r["footage"]["rotary_ft"], 380, 2) and near(r["footage"]["rotary_rop_ft_h"], 190, 2),
10817
+ "block": near(r["block_weight_klbf"], E_["block_weight_klbf"], 0.01) and r["block_weight_estimated"] and near(r["slips_threshold_klbf"], 60.0, 0.01),
10818
+ "lateral": int((lateral["state"] == RS.SLIPS).sum()) == 0 and int((lateral["state"] == RS.TIN).sum()) > 0.9 * n,
10819
+ "jump": int((jump["state"] == RS.TIN).sum()) == 0,
10820
+ "off_hook": int((hang["state"] == RS.OFFHOOK).sum()) == n and int((hang["state"] == RS.SLIPS).sum()) == 0,
10821
+ "no_hookload": nohl["slips"].sum() > 0.5 * n and "no hook load" in nohl["how"][0],
10822
+ }
10823
+ failed1 = [k for k, v in c1.items() if not v]
10824
+ ok(not failed1,
10825
+ "CP1 the record read to the second: three connections of 6, 9 and 15 min weight to weight with 4, 7 and 13 min in the slips; a 3,000 ft "
10826
+ "trip out at 2,449 ft/h and in at 3,333 ft/h over their pipe-handling hours; 2.0 h drilling 380 ft; the block's 40 klbf estimated; a light "
10827
+ "string pushed into a lateral tripping, not in the slips; a depth re-set not pipe moving; the weight off the hook for two hours OFF THE "
10828
+ "HOOK; the slips from the block when there is no hook load" + (f" [failed: {', '.join(failed1)}]" if failed1 else ""))
10829
+
10830
+ # -- CP2 the targets and the time lost ----------------------------------------------------------------------------
10831
+ w2 = ws.add_well_file(F["path"], display="RIG-2", actor="tester")
10832
+ p0 = RS.pad(ws)
10833
+ tg0 = p0["targets"]
10834
+ w2w = sorted(c["w2w_min"] for w_ in p0["wells"] for c in w_["connection_list"])
10835
+ lost0 = sum(max(0.0, x - tg0["w2w_min"]["value"]) for x in w2w) / 60.0
10836
+ IC.set_terms(ws, "tester", "test contract", "06:00", "start", {"operating": 30000}, None, None)
10837
+ try:
10838
+ RS.set_settings(ws, "tester", "", 5)
10839
+ no_source = False
10840
+ except ValueError:
10841
+ no_source = True
10842
+ RS.set_settings(ws, "tester", "drilling program 2026", 5, 2800, None)
10843
+ p1 = RS.pad(ws)
10844
+ W1 = p1["wells"][0]
10845
+ ti = next(x for x in W1["trips"] if x["direction"] == "IN")
10846
+ IC.attach(ws, "tester", w1["id"], "invoice", "inv.csv", b"Date,Rate Type,Quantity,Unit,Unit Price,Amount\n2026-08-01,Operating day rate,1,day,30000,30000\n", "test")
10847
+ ic = IC.check(ws, w1["id"])
10848
+ c2 = {
10849
+ "best_quarter": tg0["w2w_min"]["value"] == round(float(np.percentile(w2w, 25)), 2) and "best quarter" in tg0["w2w_min"]["how"] and tg0["trip_in_ft_h"] is None
10850
+ and abs(sum(w_["lost_connections_h"] for w_ in p0["wells"]) - lost0) < 0.02,
10851
+ "declared": no_source and p1["targets"]["w2w_min"]["value"] == 5 and "drilling program 2026" in p1["targets"]["w2w_min"]["how"]
10852
+ and abs(W1["lost_connections_h"] - sum(c["w2w_min"] - 5 for c in W1["connection_list"]) / 60.0) < 0.01
10853
+ and abs(ti["lost_h"] - max(0.0, ti["hours"] - ti["feet"] / 2800.0)) < 0.001 and next(x for x in W1["trips"] if x["direction"] == "OUT")["lost_h"] is None,
10854
+ "priced": p1["operating_usd_per_day"] == 30000 and abs(W1["lost_usd"] - W1["lost_h"] / 24.0 * 30000) < 0.006 * 30000 / 24.0,
10855
+ "by_day": len(W1["days"]) == 1 and W1["days"][0]["date"] == "2026-08-01" and W1["days"][0]["connections"] == 3 and abs(W1["days"][0]["lost_h"] - W1["lost_h"]) < 0.02,
10856
+ "invoice": ic["days"][0]["lost_time"] and abs(ic["days"][0]["lost_time"]["lost_h"] - W1["lost_h"]) < 0.02 and ic["days"][0]["status"] == "QUESTION",
10857
+ }
10858
+ failed2 = [k for k, v in c2.items() if not v]
10859
+ ok(not failed2,
10860
+ "CP2 the targets: the pad's best quarter (the 25th percentile of its connections) when none is declared, a declared target needing its "
10861
+ "source and then used; each connection's and trip's time past its target summed per well and per report day, priced at the contract's "
10862
+ "operating rate, and shown beside the invoice check's day" + (f" [failed: {', '.join(failed2)}]" if failed2 else ""))
10863
+
10864
+ # -- CP3 the report, the API, the CLI, the tab, the help ------------------------------------------------------------
10865
+ rp = RS.write_report(ws, __version__, "tester")
10866
+ rw = RS.write_report(ws, __version__, "tester", w1["id"])
10867
+ rd = PDF.read(Path(rp["paths"]["pdf"]).read_bytes())
10868
+ Users(str(Path(wsp, "users.json"))).add("v", "viewer-pass-1", "viewer"); Users(str(Path(wsp, "users.json"))).add("op", "op-pass-123", "operator")
10869
+ svc = Service(wsp, port=0, scheduler=False).start()
10870
+ base_url = svc.url.rstrip("/")
10871
+
10872
+ def opener(name, pw_):
10873
+ o = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
10874
+ rq = urllib.request.Request(base_url + "/api/login", method="POST", data=json.dumps({"name": name, "password": pw_}).encode())
10875
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1"); o.open(rq, timeout=30)
10876
+ return o
10877
+
10878
+ def call(o, method, path, body=None):
10879
+ rq = urllib.request.Request(base_url + path, method=method, data=(json.dumps(body).encode() if body is not None else None))
10880
+ if method == "POST":
10881
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1")
10882
+ try:
10883
+ with o.open(rq, timeout=120) as rr_:
10884
+ return rr_.status, json.loads(rr_.read())
10885
+ except urllib.error.HTTPError as e:
10886
+ return e.code, json.loads(e.read())
10887
+ try:
10888
+ V, O = opener("v", "viewer-pass-1"), opener("op", "op-pass-123")
10889
+ s_g, gj = call(V, "GET", "/api/rig-time")
10890
+ s_vs, _ = call(V, "POST", "/api/rig-time/settings", {"w2w_min": 4, "source": "x"})
10891
+ s_os, osj = call(O, "POST", "/api/rig-time/settings", {"w2w_min": 4.5, "source": "program B"})
10892
+ s_bad, _ = call(O, "POST", "/api/rig-time/settings", {"w2w_min": -1, "source": "program B"})
10893
+ s_bw, bwj = call(O, "POST", "/api/rig-time/settings", {"well_id": w2["id"], "block_weight_klbf": 42})
10894
+ s_w, wj = call(O, "POST", "/api/rig-time/write", {"well_id": w2["id"]})
10895
+ finally:
10896
+ svc.stop()
10897
+ cli_t = _sp.run([sys.executable, "-m", "gea", "rig-time", "--workspace", wsp], capture_output=True, text=True, cwd=str(pkg.parent))
10898
+ cli_w = _sp.run([sys.executable, "-m", "gea", "rig-time", "--workspace", wsp, "--write"], capture_output=True, text=True, cwd=str(pkg.parent))
10899
+ page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
10900
+ c3 = {
10901
+ "report": rd["ok"] and "Rig time - Pad CP" in rd["text"][0] and Path(rw["paths"]["pdf"]).is_file() and "weight to weight" in Path(rp["paths"]["html"]).read_text(encoding="utf-8").lower(),
10902
+ "api": s_g == 200 and len(gj["wells"]) == 2 and gj["targets"]["w2w_min"]["value"] == 5 and s_vs == 403 and s_os == 200 and osj["targets"]["w2w_min"] == 4.5
10903
+ and s_bad == 400 and s_bw == 200 and bwj["block_weight_klbf"][w2["id"]] == 42 and s_w == 200 and wj["report_id"].startswith("RIG-"),
10904
+ "cli": cli_t.returncode == 0 and "drilling connections: weight to weight median 9.0 min" in cli_t.stdout and "the block weight declared, 42 klbf" in cli_t.stdout
10905
+ and cli_w.returncode == 0 and "printable:" in cli_w.stdout,
10906
+ "page": all(x in page_src for x in ("VIEWS.rigtime", "['#/rigtime', 'Rig time']", "/api/rig-time/settings", "rtBars", "Lost vs targets")),
10907
+ "help": any(t_[0] == "rig-time" for t_ in H.INDEX) and not H.check("rig-time") and H.VIEW_TOPIC.get("rigtime") == "rig-time",
10908
+ }
10909
+ failed3 = [k for k, v in c3.items() if not v]
10910
+ ok(not failed3,
10911
+ "CP3 the pad's report and a well's, with their PDFs; the API (anyone signed in reads it; only an operator sets a target or a block "
10912
+ "weight, or writes a report; a target below zero refused); the CLI (the rig time, the report); the tab and the help page"
10913
+ + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
10914
+
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
+
10764
11116
  def main() -> int:
10765
11117
  print("GEA-Program - ACCEPTANCE SUITE (the product gate)")
10766
11118
  # a handle a job subprocess still holds at the end must not turn a finished gate into a traceback on Windows:
@@ -10857,6 +11209,8 @@ def main() -> int:
10857
11209
  section_cm_watch_and_alerts(tmp)
10858
11210
  section_cn_four(tmp)
10859
11211
  section_co_invoice_check(tmp)
11212
+ section_cp_rig_time(tmp)
11213
+ section_cq_tied_together(tmp)
10860
11214
  if _FAILS:
10861
11215
  print(f"[ACCEPTANCE] {len(_FAILS)} FAILURES ({_PASS} passed):")
10862
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