gea-program 0.18.0__tar.gz → 0.19.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gea_program-0.18.0 → gea_program-0.19.0}/PKG-INFO +20 -2
- {gea_program-0.18.0 → gea_program-0.19.0}/README.md +19 -1
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/__init__.py +1 -1
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/__main__.py +8 -2
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/acceptance_tests.py +125 -0
- gea_program-0.19.0/gea/help/month-end.md +11 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/helplib.py +1 -0
- gea_program-0.19.0/gea/month_end.py +290 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/service.py +27 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/web/app.html +6 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/workspace.py +8 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea_program.egg-info/PKG-INFO +20 -2
- {gea_program-0.18.0 → gea_program-0.19.0}/gea_program.egg-info/SOURCES.txt +2 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/pyproject.toml +1 -1
- {gea_program-0.18.0 → gea_program-0.19.0}/LICENSE +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/BENCH_TEST_PROTOCOL.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/accuracy_statement.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/alarm_engine.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/backup.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/bench.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/blind_harness.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/acex_lomonosov_age_depth_model.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/acex_lomonosov_age_depth_model.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/agassiz77_canada_temperature.csv +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/agassiz77_canada_temperature.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/barbados_110_consolidation.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/barbados_110_consolidation.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/bengal_u1452_grain_size.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/bengal_u1452_grain_size.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/blake_164_methane_isotopes.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/blake_164_methane_isotopes.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/collingwood_1_28_ks_complete.las +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/collingwood_1_28_ks_complete.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/costa_rica_odp_friction_envelope.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/costa_rica_odp_friction_envelope.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/dsdp_504b_physical_properties.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/dsdp_504b_physical_properties.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/dsdp_504b_sound_velocity.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/dsdp_504b_sound_velocity.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/elgygytgyn_5011_turbidites.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/elgygytgyn_5011_turbidites.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/epica_domec_co2_800kyr.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/epica_domec_co2_800kyr.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/fram_909_organic_petrography.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/fram_909_organic_petrography.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/gbr_325_coral_uth_ages.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/gbr_325_coral_uth_ages.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/gisp2_greenland_temperature.csv +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/gisp2_greenland_temperature.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/gom_308_t2p_insitu.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/gom_308_t2p_insitu.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/guaymas_385_dom_d13c.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/guaymas_385_dom_d13c.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/hikurangi_u1520_friction_insitu.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/iodp_u1324_pore_pressure.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/iodp_u1324_pore_pressure.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/jfast_c0019_slow_slip_events.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/jfast_c0019_slow_slip_events.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/kennetcook_2_p129_excerpt.las +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/kennetcook_2_p129_excerpt.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ktb_hb_bhgm_density.dat +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ktb_hb_bhgm_density.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ktb_hb_complog_6020_excerpt.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ktb_hb_hlog246_temperature.dat +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ktb_hb_hlog246_temperature.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ktb_hb_rockmech_compress.dat +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ktb_hb_rockmech_compress.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ktb_vb_rockmech_compress.dat +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ktb_vb_rockmech_compress.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ktb_vb_vlog251_temperature.dat +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ktb_vb_vlog251_temperature.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/l06_06_nl_survey.csv +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/l06_06_nl_survey.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/l07_01_nl_excerpt.las +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/l07_01_nl_excerpt.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/mariana_1200_serpentinite_geochem.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/med_160_sapropels.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/med_160_sapropels.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/nankai_megasplay_shear_strength.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/nankai_megasplay_shear_strength.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/odp_1027b_thermal_conductivity.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/odp_1027b_thermal_conductivity.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/odp_1027c_cork_temperature.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/odp_1027c_cork_temperature.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/odp_1165b_thermal_conductivity.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/odp_1165b_thermal_conductivity.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/odp_504b_dike_elastic_moduli.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/odp_504b_leg137_borehole_fluids.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/peru_201_sulfate_reduction.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/peru_201_sulfate_reduction.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/scorpio_e1_sa_excerpt.las +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/scorpio_e1_sa_excerpt.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/sumatra_362_cohesion.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/sumatra_362_cohesion.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/university_6_17_no1_tx_excerpt.las +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ursa_308_xrd_mineralogy.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/ursa_308_xrd_mineralogy.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/volve_15_9_19_sr_excerpt.las +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/volve_15_9_19a_core_excerpt.csv +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/volve_f12_f14_production_excerpt.csv +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/volve_f12_f14_production_excerpt.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/walvis_208_petm_carbonate.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/walvis_208_petm_carbonate.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/woodlark_1109_rock_eval.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/catalog/woodlark_1109_rock_eval.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/certificates.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/cli.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/client_reports.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/config_versioning.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/conformance.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/correlation.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/dashboard.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/deviation.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/doctor.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/downhole_engine.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/drift_monitor.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/earth_model.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/etp.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/etp12_schemas.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/example_register_map.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/fat_sat.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/files.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/follower.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/forward_model.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/gamma.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/gauge_aging.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/gauge_specs.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/geodesy.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/gravity_reference.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/TESTER_GUIDE.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/alarms.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/audit-update.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/backup.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/conformance.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/data-in.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/doctor.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/drift.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/etp.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/files.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/instruments.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/notifications.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/osdu.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/patches.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/ppdm.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/quakeml.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/quality.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/seedlink.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/seismic.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/site.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/sites.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/sra.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/start.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/transients.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/upkeep.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/vibration.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/help/well-tests.md +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/housekeeping.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/inverse_engine.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/jobs.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/live_ports.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/loadtest.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/matplotlib_demo.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/modbus.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/model_card.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/mqtt_port.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/notify.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/opcua_port.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/operator_app.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/osdu.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/patches.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/permits.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/ports.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/ppdm.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/profile_catalog.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/project.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/qt6_downhole_app.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/quakeml.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/reconciler.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/reference/IU_ANMO_10_BHZ_response.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/reference/IU_ANMO_10_BHZ_response.xml +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/reference/libmseed_steim.provenance.json +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/reference/libmseed_steim1_4096.mseed +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/reference/libmseed_steim2_4096.mseed +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/reference/libmseed_steim2_512.mseed +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/rock_inventory.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/sample_record.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/sample_well_profile.csv +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/sbom.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/seedlink.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/segy.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/seismic.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/seismic_array.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/seismic_assoc.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/seismic_detect.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/seismic_film.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/seismic_harmonic.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/seismic_qc.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/seismic_response.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/seismic_signature.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/seismic_track.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/seismic_uncertainty.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/seismic_unlisted.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/sensor_swap.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/service_life.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/shell.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/shut_in.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/sla_report.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/sra.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/store_forward.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/strata_join.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/supervisor.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/survey_cmd.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/survey_view.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/telemetry.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/tool_library.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/transient.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/vibration.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/well_assembler.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/well_test_validation.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/wits0.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea/witsml.py +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea_program.egg-info/dependency_links.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea_program.egg-info/entry_points.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea_program.egg-info/requires.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/gea_program.egg-info/top_level.txt +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/setup.cfg +0 -0
- {gea_program-0.18.0 → gea_program-0.19.0}/tests/test_acceptance.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gea-program
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0
|
|
4
4
|
Summary: GEA-Program: downhole gauge monitoring - canonical measurement records, quality rules, drift evaluation with a re-fit change log, well-test validation, alarm management, model cards, store-and-forward, monthly SLA measurement, FAT/SAT protocols and a dashboard, generated from live objects in the client's reporting outline.
|
|
5
5
|
Author: Daniel T. Murphy
|
|
6
6
|
License: MPL-2.0
|
|
@@ -125,7 +125,7 @@ gea dashboard --catalog-well volve_f12_f14_production_excerpt:15/9-F-12:10000 --
|
|
|
125
125
|
gea client-report --report accuracy --out client_report
|
|
126
126
|
gea model-cards --out model_cards
|
|
127
127
|
gea sbom --out sbom
|
|
128
|
-
gea accept # the product gate (
|
|
128
|
+
gea accept # the product gate (344 checks)
|
|
129
129
|
gea help drift # the help library, by the job (16 pages; the same text is on every dashboard page)
|
|
130
130
|
gea guide # the click-by-click tester guide (docs/TESTER_GUIDE.md)
|
|
131
131
|
gea gui # the desktop window (pip install "gea-program[desktop]")
|
|
@@ -710,6 +710,24 @@ after the report was written - and "Update every report from its source"
|
|
|
710
710
|
dashboard for every well, then every seismic station, errors collected and
|
|
711
711
|
listed, never hidden.
|
|
712
712
|
|
|
713
|
+
### The month-end packet
|
|
714
|
+
|
|
715
|
+
A client's month was five or six commands and a walk through the reports
|
|
716
|
+
folder. `gea workspace --path C:\site --action month-end --period 2026-09`
|
|
717
|
+
(or `--period previous`) runs the refresh for the period and collects every
|
|
718
|
+
report the program writes - each well's drift report, alarm month, well-test
|
|
719
|
+
validation, shut-in report, data resilience, machine vibration and the
|
|
720
|
+
certificate register's ISO/IEC 17025 7.8 conformity; each site's SRA packet
|
|
721
|
+
for the period and site report; every seismic station and track; the
|
|
722
|
+
accuracy statement, the SLA month, the FAT/SAT protocol, the model cards and
|
|
723
|
+
the SBOM - into `reports/month_end/<period>/` with `INDEX` (html, md, json:
|
|
724
|
+
every item INCLUDED with its files or NOT AVAILABLE with the reason that
|
|
725
|
+
names what the site lacks; every failed step; OPEN for a month that has not
|
|
726
|
+
ended) and `MANIFEST.json` (every file's SHA-256), zipped beside it. Nothing
|
|
727
|
+
is recomputed in the index. On the Reports page: the packets on record,
|
|
728
|
+
Assemble as a job, and Schedule monthly (day and hour, the previous month).
|
|
729
|
+
`gea help month-end`.
|
|
730
|
+
|
|
713
731
|
### Before a site goes live
|
|
714
732
|
|
|
715
733
|
`deploy/README.md` is the checklist: keep the service on the loopback
|
|
@@ -86,7 +86,7 @@ gea dashboard --catalog-well volve_f12_f14_production_excerpt:15/9-F-12:10000 --
|
|
|
86
86
|
gea client-report --report accuracy --out client_report
|
|
87
87
|
gea model-cards --out model_cards
|
|
88
88
|
gea sbom --out sbom
|
|
89
|
-
gea accept # the product gate (
|
|
89
|
+
gea accept # the product gate (344 checks)
|
|
90
90
|
gea help drift # the help library, by the job (16 pages; the same text is on every dashboard page)
|
|
91
91
|
gea guide # the click-by-click tester guide (docs/TESTER_GUIDE.md)
|
|
92
92
|
gea gui # the desktop window (pip install "gea-program[desktop]")
|
|
@@ -671,6 +671,24 @@ after the report was written - and "Update every report from its source"
|
|
|
671
671
|
dashboard for every well, then every seismic station, errors collected and
|
|
672
672
|
listed, never hidden.
|
|
673
673
|
|
|
674
|
+
### The month-end packet
|
|
675
|
+
|
|
676
|
+
A client's month was five or six commands and a walk through the reports
|
|
677
|
+
folder. `gea workspace --path C:\site --action month-end --period 2026-09`
|
|
678
|
+
(or `--period previous`) runs the refresh for the period and collects every
|
|
679
|
+
report the program writes - each well's drift report, alarm month, well-test
|
|
680
|
+
validation, shut-in report, data resilience, machine vibration and the
|
|
681
|
+
certificate register's ISO/IEC 17025 7.8 conformity; each site's SRA packet
|
|
682
|
+
for the period and site report; every seismic station and track; the
|
|
683
|
+
accuracy statement, the SLA month, the FAT/SAT protocol, the model cards and
|
|
684
|
+
the SBOM - into `reports/month_end/<period>/` with `INDEX` (html, md, json:
|
|
685
|
+
every item INCLUDED with its files or NOT AVAILABLE with the reason that
|
|
686
|
+
names what the site lacks; every failed step; OPEN for a month that has not
|
|
687
|
+
ended) and `MANIFEST.json` (every file's SHA-256), zipped beside it. Nothing
|
|
688
|
+
is recomputed in the index. On the Reports page: the packets on record,
|
|
689
|
+
Assemble as a job, and Schedule monthly (day and hour, the previous month).
|
|
690
|
+
`gea help month-end`.
|
|
691
|
+
|
|
674
692
|
### Before a site goes live
|
|
675
693
|
|
|
676
694
|
`deploy/README.md` is the checklist: keep the service on the loopback
|
|
@@ -285,7 +285,7 @@ def main(argv=None) -> int:
|
|
|
285
285
|
"add-seismic", "refresh-seismic", "remove-seismic", "sar-film", "refresh-all", "add-track", "refresh-track", "remove-track",
|
|
286
286
|
"seismic-dataset", "seismic-field", "add-site", "sites", "site-report", "remove-site", "rename",
|
|
287
287
|
"set-well", "sra-define", "sra-report", "associate", "osdu-export", "quakeml-export", "well-identity", "vibration-report",
|
|
288
|
-
"add-live-station", "live-fold", "live-run"])
|
|
288
|
+
"add-live-station", "live-fold", "live-run", "month-end"])
|
|
289
289
|
p_ws.add_argument("--stations", type=str, nargs="+", default=None, help="add-track: two or more array station ids of this workspace")
|
|
290
290
|
p_ws.add_argument("--site", type=str, default=None, help="site-report/remove-site: the site id")
|
|
291
291
|
p_ws.add_argument("--api", type=str, default=None, help="set-well: the API number")
|
|
@@ -307,7 +307,7 @@ def main(argv=None) -> int:
|
|
|
307
307
|
p_ws.add_argument("--checkpoint-months", type=int, default=3, help="sra-define: months between checkpoints with Commission staff (3)")
|
|
308
308
|
p_ws.add_argument("--formation-boundary", type=str, default="base of the Wolfcamp", help="sra-define: the stratigraphic boundary between the two tiers")
|
|
309
309
|
p_ws.add_argument("--triggering-event", type=str, default="", help="sra-define: the catalogue id of the event the plan answers")
|
|
310
|
-
p_ws.add_argument("--catalog", type=str, default=None, help="sra-report: a TexNet earthquake-catalogue export (CSV)")
|
|
310
|
+
p_ws.add_argument("--catalog", type=str, default=None, help="sra-report / month-end: a TexNet earthquake-catalogue export (CSV)")
|
|
311
311
|
p_ws.add_argument("--aftershocks", type=str, nargs="*", default=None, help="sra-report: catalogue ids the operator declares as aftershocks (exempt)")
|
|
312
312
|
p_ws.add_argument("--start", type=str, default=None, help="sra-report: first day of the record, YYYY-MM-DD")
|
|
313
313
|
p_ws.add_argument("--end", type=str, default=None, help="sra-report: last day of the record, YYYY-MM-DD")
|
|
@@ -359,6 +359,8 @@ def main(argv=None) -> int:
|
|
|
359
359
|
p_ws.add_argument("--seedlink-config", type=str, default=None, help="add-live-station: the SeedLink config JSON (gea seedlink --write-example-config)")
|
|
360
360
|
p_ws.add_argument("--include-open", action="store_true", help="live-fold: fold the current day's files too (they are still being written)")
|
|
361
361
|
p_ws.add_argument("--seconds", type=float, default=None, help="live-run: how long to run (default: until Ctrl+C)")
|
|
362
|
+
p_ws.add_argument("--period", type=str, default="previous", help="month-end: the month, YYYY-MM, or 'previous' (the last closed month)")
|
|
363
|
+
p_ws.add_argument("--no-refresh", action="store_true", help="month-end: collect the reports as they stand without refreshing first")
|
|
362
364
|
p_ws.add_argument("--name", type=str, default=None, help="init: site name; add-*: display name")
|
|
363
365
|
p_ws.add_argument("--file", type=str, default=None, help="add-file: the client's data file; add-live: the port configuration JSON")
|
|
364
366
|
p_ws.add_argument("--entry", type=str, default=None, help="add-catalog: catalogue entry")
|
|
@@ -1944,6 +1946,10 @@ def main(argv=None) -> int:
|
|
|
1944
1946
|
raise SystemExit("well-identity needs --well <id>")
|
|
1945
1947
|
r = ws.well_identity(a.well)
|
|
1946
1948
|
print(_ppdm_text(r)); return 0
|
|
1949
|
+
elif a.action == "month-end":
|
|
1950
|
+
from .month_end import report_text as _me_text
|
|
1951
|
+
r = ws.month_end(a.period, actor=a.actor, refresh=not a.no_refresh, catalog_csv=a.catalog)
|
|
1952
|
+
print(_me_text(r)); return 0 if not r["errors"] else 1
|
|
1947
1953
|
elif a.action == "add-live-station":
|
|
1948
1954
|
if not a.seedlink_config or not a.name:
|
|
1949
1955
|
raise SystemExit("add-live-station needs --name <display> and --seedlink-config <json>")
|
|
@@ -5872,6 +5872,130 @@ def section_bl_backup(tmp: str) -> None:
|
|
|
5872
5872
|
"`--verify` says OK or names the tampered file, `--restore` brings the accounts back; the help page says a backup never restored is a hope"
|
|
5873
5873
|
+ (f" [failed: {', '.join(failed2)}; r_st0={r_st0.stdout[-120:]!r} {r_st0.stderr[-120:]!r}]" if failed2 else ""))
|
|
5874
5874
|
|
|
5875
|
+
|
|
5876
|
+
def section_bm_month_end(tmp: str) -> None:
|
|
5877
|
+
"""Section BM - the month-end packet: the refresh for the period, every report collected with INCLUDED or NOT
|
|
5878
|
+
AVAILABLE and the reason, the index and the manifest, the archive, the records; the page, the job and the
|
|
5879
|
+
monthly schedule."""
|
|
5880
|
+
import http.cookiejar
|
|
5881
|
+
import subprocess as _sp
|
|
5882
|
+
import urllib.request
|
|
5883
|
+
import urllib.error
|
|
5884
|
+
import zipfile
|
|
5885
|
+
from . import month_end as ME
|
|
5886
|
+
from . import helplib as H
|
|
5887
|
+
from .telemetry import TelemetryRecorder, TelemetryConfig
|
|
5888
|
+
from .downhole_engine import DownholeEngine, SimulatorConfig
|
|
5889
|
+
from .workspace import Workspace
|
|
5890
|
+
from .service import Service, Users
|
|
5891
|
+
pkg = Path(__file__).parent
|
|
5892
|
+
d = Path(tmp, "bm"); d.mkdir()
|
|
5893
|
+
hist = str(d / "hist.csv")
|
|
5894
|
+
TelemetryRecorder(engine=DownholeEngine(SimulatorConfig()), config=TelemetryConfig(duration_hours=2.0, seed=5)).run().export_csv(hist)
|
|
5895
|
+
wsp = str(d / "ws"); ws = Workspace.create(wsp, "Pad 3", actor="tester")
|
|
5896
|
+
w = ws.add_well_file(hist, display="SWD 1", actor="tester")
|
|
5897
|
+
site = ws.add_site("Pad 3 site", wells=[w["id"]], actor="tester")
|
|
5898
|
+
bad_period = None
|
|
5899
|
+
try:
|
|
5900
|
+
ME.period_bounds("2026-9")
|
|
5901
|
+
except ValueError as e:
|
|
5902
|
+
bad_period = str(e)
|
|
5903
|
+
pb = ME.period_bounds("2026-02")
|
|
5904
|
+
rec = ME.assemble(ws, "2026-09", actor="tester")
|
|
5905
|
+
idx = json.loads((Path(rec["folder"]) / "INDEX.json").read_text(encoding="utf-8"))
|
|
5906
|
+
man = json.loads((Path(rec["folder"]) / "MANIFEST.json").read_text(encoding="utf-8"))
|
|
5907
|
+
by_name = {i["name"]: i for i in idx["items"]}
|
|
5908
|
+
v_folder = ME.verify(rec["folder"]); v_zip = ME.verify(rec["archive"])
|
|
5909
|
+
with zipfile.ZipFile(rec["archive"]) as z:
|
|
5910
|
+
znames = set(z.namelist())
|
|
5911
|
+
# a second assembly of the same period rebuilds whole; a future month is OPEN
|
|
5912
|
+
rec2 = ME.assemble(ws, "2026-09", actor="tester", refresh=False)
|
|
5913
|
+
from datetime import datetime as _dt, timezone as _tz
|
|
5914
|
+
now = _dt.now(_tz.utc)
|
|
5915
|
+
cur = f"{now.year:04d}-{now.month:02d}"
|
|
5916
|
+
rec3 = ME.assemble(ws, cur, actor="tester", refresh=False)
|
|
5917
|
+
pk = ME.packets(ws)
|
|
5918
|
+
audit = [a["action"] for a in ws.audit_log()]
|
|
5919
|
+
c1 = {
|
|
5920
|
+
"period_checked": bool(bad_period) and "YYYY-MM" in bad_period and pb["days"] == 28 and pb["start"] == "2026-02-01T00:00:00Z" and pb["end"] == "2026-02-28T23:59:59Z" and pb["closed"] is True,
|
|
5921
|
+
"included": by_name["SWD 1: gauge drift report"]["status"] == "INCLUDED" and by_name["SWD 1: alarm event report"]["status"] == "INCLUDED"
|
|
5922
|
+
and by_name["SLA month 2026-09"]["status"] == "INCLUDED" and by_name["Accuracy statement"]["status"] == "INCLUDED"
|
|
5923
|
+
and by_name["Pad 3 site: site report"]["status"] == "INCLUDED" and by_name["Model cards and SBOM"]["status"] == "INCLUDED",
|
|
5924
|
+
"not_available_with_reason": by_name["SWD 1: machine vibration"]["status"] == "NOT AVAILABLE" and "no machine record" in by_name["SWD 1: machine vibration"]["reason"]
|
|
5925
|
+
and by_name["Pad 3 site: SRA packet for 2026-09"]["status"] == "NOT AVAILABLE" and "no Seismicity Response Area" in by_name["Pad 3 site: SRA packet for 2026-09"]["reason"]
|
|
5926
|
+
and "no certificate register" in by_name["SWD 1: certificate conformity (ISO/IEC 17025 7.8)"]["reason"],
|
|
5927
|
+
"files_hashed": all(f["sha256"] and f["bytes"] > 0 for i in idx["items"] for f in i["files"]) and man["n_files"] == len(man["files"]) and man["n_files"] >= 20
|
|
5928
|
+
and "wells/SWD-1/gauge_drift_report.html" in man["files"] and "site/sla_report_2026-09.html" in man["files"] and "INDEX.html" in man["files"],
|
|
5929
|
+
"record": rec["included"] == len([i for i in idx["items"] if i["status"] == "INCLUDED"]) and rec["closed"] is True and rec["errors"] == [] and os.path.isfile(rec["archive"])
|
|
5930
|
+
and os.path.isfile(rec["archive"] + "") and rec["archive"].endswith("month_end_Pad-3_2026-09.zip"),
|
|
5931
|
+
"verify": v_folder["status"] == "OK" and v_zip["status"] == "OK" and v_zip["n_files"] == man["n_files"] and "MANIFEST.json" in znames and "INDEX.html" in znames,
|
|
5932
|
+
"rebuilt_whole_and_open": rec2["generated_utc"] >= rec["generated_utc"] and rec2["refresh"] is False and rec3["closed"] is False and "OPEN" in (Path(rec3["folder"]) / "INDEX.md").read_text(encoding="utf-8")
|
|
5933
|
+
and len(pk) == 3 and all(p["present"] for p in pk) and pk[-1]["period"] == cur,
|
|
5934
|
+
"audited": audit.count("month_end.assemble") == 3 and "dashboard.refresh" in audit and "site.report" in audit,
|
|
5935
|
+
"not_a_measurement": "does not call a measurement" in (Path(rec["folder"]) / "INDEX.md").read_text(encoding="utf-8"),
|
|
5936
|
+
}
|
|
5937
|
+
failed1 = [k for k, v in c1.items() if not v]
|
|
5938
|
+
ok(not failed1,
|
|
5939
|
+
"BM1 the month-end packet: the period checked (YYYY-MM; February's 28 days; a past month closed), the refresh run for it, every report "
|
|
5940
|
+
"collected - the well's drift and alarm reports, the SLA month, the accuracy statement, the site report, the model cards - INCLUDED with "
|
|
5941
|
+
"hashed files, and the vibration report, the SRA packet and the certificate conformity NOT AVAILABLE with the reason that names what the "
|
|
5942
|
+
"site lacks; INDEX and MANIFEST written, the archive verifies against the manifest, a second assembly rebuilds the period whole, the "
|
|
5943
|
+
"current month is marked OPEN, every packet on record, every assembly in the audit"
|
|
5944
|
+
+ (f" [failed: {', '.join(failed1)}]" if failed1 else ""))
|
|
5945
|
+
|
|
5946
|
+
# -- the page, the job, the schedule, the CLI, the help --------------------------------------------------------
|
|
5947
|
+
users = Users(str(Path(wsp, "users.json"))); users.add("op", "operator-pass-1", "operator"); users.add("adm", "admin-pass-1", "admin")
|
|
5948
|
+
svc = Service(wsp, port=0, scheduler=False).start()
|
|
5949
|
+
base = svc.url.rstrip("/")
|
|
5950
|
+
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
|
|
5951
|
+
|
|
5952
|
+
def call(method, path, body=None):
|
|
5953
|
+
req = urllib.request.Request(base + path, method=method, data=(json.dumps(body).encode() if body is not None else None))
|
|
5954
|
+
req.add_header("Content-Type", "application/json"); req.add_header("X-GEA-Action", "1")
|
|
5955
|
+
try:
|
|
5956
|
+
with opener.open(req, timeout=300) as rr_:
|
|
5957
|
+
return rr_.status, json.loads(rr_.read())
|
|
5958
|
+
except urllib.error.HTTPError as e:
|
|
5959
|
+
return e.code, json.loads(e.read())
|
|
5960
|
+
try:
|
|
5961
|
+
call("POST", "/api/login", {"name": "op", "password": "operator-pass-1"})
|
|
5962
|
+
st_l, lst = call("GET", "/api/month-end")
|
|
5963
|
+
st_bad = call("POST", "/api/month-end/run", {"period": "Sept 2026"})[0]
|
|
5964
|
+
st_sched_op = call("POST", "/api/month-end/schedule", {"day": 1, "at": "03:00"})[0]
|
|
5965
|
+
st_run, job = call("POST", "/api/month-end/run", {"period": "2026-08", "no_refresh": True})
|
|
5966
|
+
done = svc.app.runner.wait(job["id"], 300)
|
|
5967
|
+
st_l2, lst2 = call("GET", "/api/month-end")
|
|
5968
|
+
req = urllib.request.Request(base + "/reports/month_end/2026-08/INDEX.html"); req.add_header("X-GEA-Action", "1")
|
|
5969
|
+
with opener.open(req, timeout=60) as rr_:
|
|
5970
|
+
html_ok = rr_.status == 200 and b"Month-End Packet" in rr_.read()
|
|
5971
|
+
call("POST", "/api/login", {"name": "adm", "password": "admin-pass-1"})
|
|
5972
|
+
st_sched_bad = call("POST", "/api/month-end/schedule", {"day": 31, "at": "03:00"})[0]
|
|
5973
|
+
st_sched, sch = call("POST", "/api/month-end/schedule", {"day": 2, "at": "03:15"})
|
|
5974
|
+
st_l3, lst3 = call("GET", "/api/month-end")
|
|
5975
|
+
finally:
|
|
5976
|
+
svc.stop()
|
|
5977
|
+
page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
|
|
5978
|
+
help_md = (pkg / "help" / "month-end.md").read_text(encoding="utf-8")
|
|
5979
|
+
r_cli = _sp.run([sys.executable, "-m", "gea", "workspace", "--path", wsp, "--action", "month-end", "--period", "2026-07", "--no-refresh"], capture_output=True, text=True)
|
|
5980
|
+
r_prev = _sp.run([sys.executable, "-m", "gea", "workspace", "--path", wsp, "--action", "month-end", "--no-refresh"], capture_output=True, text=True)
|
|
5981
|
+
c2 = {
|
|
5982
|
+
"list": st_l == 200 and len(lst["packets"]) == 3 and lst["previous"] == ME.previous_period() and lst["schedule"] is None,
|
|
5983
|
+
"refusals": st_bad == 400 and st_sched_op == 403 and st_sched_bad == 400,
|
|
5984
|
+
"job": st_run == 200 and done["status"] == "DONE" and st_l2 == 200 and any(p["period"] == "2026-08" and p["present"] for p in lst2["packets"]) and html_ok,
|
|
5985
|
+
"schedule": st_sched == 200 and sch["name"] == "month-end-monthly" and sch["monthly_day"] == 2 and sch["daily_at"] == "03:15" and "previous" in sch["args"]
|
|
5986
|
+
and st_l3 == 200 and lst3["schedule"]["name"] == "month-end-monthly",
|
|
5987
|
+
"page": "Month-end packet" in page_src and "/api/month-end/run" in page_src and "/api/month-end/schedule" in page_src,
|
|
5988
|
+
"cli": r_cli.returncode == 0 and "month-end 2026-07 (closed)" in r_cli.stdout and r_prev.returncode == 0 and f"month-end {ME.previous_period()} (closed)" in r_prev.stdout,
|
|
5989
|
+
"help": any(t["topic"] == "month-end" for t in H.topics()) and not H.check("month-end") and "does not recompute" in help_md.split("will not call a measurement")[1],
|
|
5990
|
+
}
|
|
5991
|
+
failed2 = [k for k, v in c2.items() if not v]
|
|
5992
|
+
ok(not failed2,
|
|
5993
|
+
"BM2 the Reports page carries the packet card: the packets on record with their index and archive, the period defaulting to the last closed "
|
|
5994
|
+
"month, Assemble as a job (a malformed period refused) whose INDEX the page then serves, Schedule monthly for an administrator only (day 1..28) "
|
|
5995
|
+
"as the `month-end-monthly` entry for the previous month; `gea workspace --action month-end` with a period or with 'previous'; the help page "
|
|
5996
|
+
"says the index collects and does not recompute"
|
|
5997
|
+
+ (f" [failed: {', '.join(failed2)}; cli={r_cli.stdout[-200:]!r} {r_cli.stderr[-200:]!r}]" if failed2 else ""))
|
|
5998
|
+
|
|
5875
5999
|
def main() -> int:
|
|
5876
6000
|
print("GEA-Program - ACCEPTANCE SUITE (the product gate)")
|
|
5877
6001
|
# a handle a job subprocess still holds at the end must not turn a finished gate into a traceback on Windows:
|
|
@@ -5939,6 +6063,7 @@ def main() -> int:
|
|
|
5939
6063
|
section_bj_etp(tmp)
|
|
5940
6064
|
section_bk_seedlink(tmp)
|
|
5941
6065
|
section_bl_backup(tmp)
|
|
6066
|
+
section_bm_month_end(tmp)
|
|
5942
6067
|
if _FAILS:
|
|
5943
6068
|
print(f"[ACCEPTANCE] {len(_FAILS)} FAILURES ({_PASS} passed):")
|
|
5944
6069
|
for f in _FAILS:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# The month-end packet
|
|
2
|
+
|
|
3
|
+
> A client's month was five or six commands and a walk through the reports folder. The packet is that month assembled once: the refresh for the period, then every report the program writes for every well, site, station and track, collected into one dated folder and one archive, with an index that says item by item what is there and what is not, and a manifest of every file's hash.
|
|
4
|
+
|
|
5
|
+
**The command**: `gea workspace --path C:\site --action month-end --period 2026-09` (or `--period previous`, the last closed month; `--no-refresh` collects the reports as they stand; `--catalog` hands the regulator's catalogue CSV to the SRA packet). On the dashboard: Reports -> Month-end packet -> the period and Assemble (a job), and for an administrator Schedule monthly (a schedule entry, `month-end-monthly`, on the day and hour given, for the previous month). The result is `reports/month_end/<period>/` with `INDEX.html` / `.md` / `.json` and `MANIFEST.json`, and `reports/month_end/month_end_<site>_<period>.zip`; each packet is logged in `records/month_end.jsonl` and in the audit (`month_end.assemble`).
|
|
6
|
+
|
|
7
|
+
**What it writes**: for the site - the dashboard index, the accuracy statement, the SLA month for that period, the FAT/SAT protocol, the model cards and the SBOM; for every well - the gauge drift report, the alarm event report, the well-test validation, the shut-in / build-up report, the data resilience report, the machine vibration report, and the certificate register's conformity against ISO/IEC 17025 7.8 as it stands; for every site - the SRA packet for the period (when a Seismicity Response Area is defined) and the site report; for every seismic station and track - their reports. Each item is INCLUDED with its files or NOT AVAILABLE with the reason (no machine record, no SRA defined, no certificate register, no well test detected, never refreshed); every step that failed is listed; a month that has not ended is marked OPEN.
|
|
8
|
+
|
|
9
|
+
**The number to check**: `N INCLUDED, M NOT AVAILABLE, 0 FAILED` on the index's front page, and `closed: yes` for a month that has ended.
|
|
10
|
+
|
|
11
|
+
**What this page will not call a measurement**: an item that is NOT AVAILABLE - the reason names what the site lacks, and the packet never stands in for it. A number in any of the reports - each carries its own method and declarations; the index collects and does not recompute. A month that has not ended - the packet says OPEN and the one assembled after the month closes supersedes it. A packet whose files no longer match its manifest - `MANIFEST.json` is the check, and a packet is rebuilt whole, never merged.
|
|
@@ -46,6 +46,7 @@ INDEX: List[tuple] = [
|
|
|
46
46
|
('conformance', 'Conformity, in the standards\' words', 'certificates against ISO/IEC 17025 7.8 and the bias against its class under a named ILAC-G8 decision rule'),
|
|
47
47
|
('etp', 'WITSML 2.x over ETP', 'the live port on Energistics ETP v1.2: subscribe to the store\'s channels and it pushes'),
|
|
48
48
|
('seedlink', 'Live stations (SeedLink)', 'the seismic leg\'s live port: a station\'s records as it writes them, resumed by sequence number, folded into its record list'),
|
|
49
|
+
('month-end', 'The month-end packet', 'the month\'s deliverable assembled once: every report collected, INCLUDED or NOT AVAILABLE with the reason, hashed'),
|
|
49
50
|
('backup', 'The backup', 'the site copied off the machine, verified, and restored once before it is trusted'),
|
|
50
51
|
('doctor', 'Which code is running', 'gea doctor and the serve start-up gate'),
|
|
51
52
|
('audit-update', 'Audit / Update', 'the whole audit log with filters, the program against PyPI, every report against its source'),
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""month_end - the month's deliverable, assembled once, from the reports the program already writes.
|
|
2
|
+
|
|
3
|
+
A client's month has, until now, been five or six commands and a walk through reports/: the drift report of
|
|
4
|
+
each well, the alarm month, the well-test validations, the vibration report where a machine record exists,
|
|
5
|
+
the certificate conformity, the SRA packet of each site that has one, the site report, the seismic station
|
|
6
|
+
and track reports, the accuracy statement and the SLA month. This module runs the refresh for the period,
|
|
7
|
+
then collects every one of those into one dated folder and one archive - with an index that says, item by
|
|
8
|
+
item, INCLUDED (with the file's hash) or NOT AVAILABLE (with the reason: no machine record, no SRA defined,
|
|
9
|
+
no certificate register, never refreshed) - and a manifest of every file. Nothing is recomputed here beyond
|
|
10
|
+
the refresh the program would run anyway; the packet is what the reports said, collected, so it can be
|
|
11
|
+
handed over and later verified against the files it came from.
|
|
12
|
+
|
|
13
|
+
What it will not do: make up an item that has no source (it is listed as NOT AVAILABLE with the reason);
|
|
14
|
+
include a report older than the refresh that produced the packet unless told to skip the refresh (and then
|
|
15
|
+
the index says every report's own generated time); name a month that has not ended as closed (it is marked
|
|
16
|
+
OPEN).
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import calendar
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import shutil
|
|
25
|
+
import zipfile
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from typing import Dict, List, Optional
|
|
28
|
+
|
|
29
|
+
PROTOCOL = 'gea.month_end/1'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _utc() -> str:
|
|
33
|
+
return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _sha256(path: str) -> str:
|
|
37
|
+
h = hashlib.sha256()
|
|
38
|
+
with open(path, 'rb') as f:
|
|
39
|
+
for chunk in iter(lambda: f.read(1 << 20), b''):
|
|
40
|
+
h.update(chunk)
|
|
41
|
+
return h.hexdigest()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def period_bounds(period: str) -> Dict[str, str]:
|
|
45
|
+
"""'2026-09' -> the first and last instants of that month (UTC) and whether the month has ended."""
|
|
46
|
+
try:
|
|
47
|
+
y, m = int(period[:4]), int(period[5:7])
|
|
48
|
+
if period[4] != '-' or not (1 <= m <= 12) or len(period) != 7:
|
|
49
|
+
raise ValueError
|
|
50
|
+
except (ValueError, IndexError):
|
|
51
|
+
raise ValueError(f'period must be YYYY-MM, got {period!r}')
|
|
52
|
+
last = calendar.monthrange(y, m)[1]
|
|
53
|
+
start = f'{y:04d}-{m:02d}-01T00:00:00Z'
|
|
54
|
+
end = f'{y:04d}-{m:02d}-{last:02d}T23:59:59Z'
|
|
55
|
+
now = datetime.now(timezone.utc)
|
|
56
|
+
closed = (y, m) < (now.year, now.month)
|
|
57
|
+
return {'period': period, 'start': start, 'end': end, 'closed': closed, 'days': last}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def previous_period(now: Optional[datetime] = None) -> str:
|
|
61
|
+
"""The last closed month as YYYY-MM - what a monthly schedule entry asks for."""
|
|
62
|
+
now = now or datetime.now(timezone.utc)
|
|
63
|
+
y, m = (now.year, now.month - 1) if now.month > 1 else (now.year - 1, 12)
|
|
64
|
+
return f'{y:04d}-{m:02d}'
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _copy_family(src_dir: str, names: List[str], dst_dir: str) -> List[dict]:
|
|
68
|
+
"""Copy the files of one report family (any extension of each basename) and return their entries."""
|
|
69
|
+
out = []
|
|
70
|
+
if not os.path.isdir(src_dir):
|
|
71
|
+
return out
|
|
72
|
+
for fn in sorted(os.listdir(src_dir)):
|
|
73
|
+
stem = fn.split('.')[0]
|
|
74
|
+
if any(fn == n or fn.startswith(n + '.') or fn.startswith(n + '_') for n in names) or stem in names:
|
|
75
|
+
s = os.path.join(src_dir, fn)
|
|
76
|
+
if os.path.isfile(s):
|
|
77
|
+
os.makedirs(dst_dir, exist_ok=True)
|
|
78
|
+
d = os.path.join(dst_dir, fn)
|
|
79
|
+
shutil.copyfile(s, d)
|
|
80
|
+
out.append({'file': fn, 'sha256': _sha256(d), 'bytes': os.path.getsize(d),
|
|
81
|
+
'generated_utc': datetime.fromtimestamp(os.path.getmtime(s), timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')})
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def assemble(ws, period: str, actor: str = 'system', refresh: bool = True, catalog_csv: Optional[str] = None) -> dict:
|
|
86
|
+
"""Run the refresh for the period (unless refresh=False), collect every report into reports/month_end/<period>/,
|
|
87
|
+
write INDEX (markdown, html, json) and MANIFEST.json, zip it, audit it. Returns the packet record."""
|
|
88
|
+
from . import __version__
|
|
89
|
+
from .client_reports import Document, Section, Table, write as _write, PROGRAM_NAME
|
|
90
|
+
pb = period_bounds(period)
|
|
91
|
+
items: List[dict] = []
|
|
92
|
+
errors: List[str] = []
|
|
93
|
+
if refresh:
|
|
94
|
+
try:
|
|
95
|
+
ws.refresh_dashboard(actor=actor, month=period)
|
|
96
|
+
except Exception as e: # the refresh's own failure is an item, never a silent gap
|
|
97
|
+
errors.append(f'dashboard refresh: {e}')
|
|
98
|
+
for st in ws.seismic_stations():
|
|
99
|
+
try:
|
|
100
|
+
ws.refresh_seismic(st['id'], actor=actor)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
errors.append(f"seismic {st['id']}: {e}")
|
|
103
|
+
for tk in ws.tracks():
|
|
104
|
+
try:
|
|
105
|
+
ws.refresh_track(tk['id'], actor=actor)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
errors.append(f"track {tk['id']}: {e}")
|
|
108
|
+
out = ws.dir('reports', 'month_end', period)
|
|
109
|
+
for old in os.listdir(out): # a packet for a period is rebuilt whole, never merged
|
|
110
|
+
p = os.path.join(out, old)
|
|
111
|
+
shutil.rmtree(p) if os.path.isdir(p) else os.remove(p)
|
|
112
|
+
rep = ws.reports_dir
|
|
113
|
+
|
|
114
|
+
def item(kind: str, ident: str, name: str, files: List[dict], reason: str = '') -> None:
|
|
115
|
+
items.append({'kind': kind, 'id': ident, 'name': name, 'status': 'INCLUDED' if files else 'NOT AVAILABLE',
|
|
116
|
+
'reason': '' if files else (reason or 'no report written'), 'files': files})
|
|
117
|
+
|
|
118
|
+
# -- the site as a whole ---------------------------------------------------------------------------------------
|
|
119
|
+
item('site', '-', 'Dashboard index', _copy_family(rep, ['index', 'dashboard'], os.path.join(out, 'site')), 'never refreshed')
|
|
120
|
+
item('site', '-', 'Accuracy statement', _copy_family(rep, ['accuracy_statement'], os.path.join(out, 'site')), 'no accuracy statement written')
|
|
121
|
+
item('site', '-', f'SLA month {period}', _copy_family(rep, [f'sla_report_{period}'], os.path.join(out, 'site')),
|
|
122
|
+
'the SLA month is written by the refresh with --month; none for this period')
|
|
123
|
+
item('site', '-', 'FAT/SAT protocol', _copy_family(rep, ['fat_protocol', 'sat_protocol'], os.path.join(out, 'site')), 'no protocol written (gea fat-sat, or the refresh with --sat)')
|
|
124
|
+
item('site', '-', 'Model cards and SBOM', _copy_family(rep, ['model_cards_index', 'sbom'], os.path.join(out, 'site')), 'never refreshed')
|
|
125
|
+
# -- every well -------------------------------------------------------------------------------------------------
|
|
126
|
+
for w in ws.wells():
|
|
127
|
+
wd = os.path.join(rep, 'wells', w['id'])
|
|
128
|
+
dst = os.path.join(out, 'wells', w['id'])
|
|
129
|
+
item('well', w['id'], f"{w['display']}: gauge drift report", _copy_family(wd, ['gauge_drift_report'], dst), 'never refreshed')
|
|
130
|
+
item('well', w['id'], f"{w['display']}: alarm event report", _copy_family(wd, ['alarm_event_report'], dst), 'no alarm report (no alarm config, or never refreshed)')
|
|
131
|
+
item('well', w['id'], f"{w['display']}: well-test validation", _copy_family(wd, ['well_test_validation'], dst), 'no well test detected in the record, or never refreshed')
|
|
132
|
+
item('well', w['id'], f"{w['display']}: shut-in / build-up", _copy_family(wd, ['pressure_transient_report'], dst), 'no shut-in found in the record, or never refreshed')
|
|
133
|
+
item('well', w['id'], f"{w['display']}: data resilience", _copy_family(wd, ['data_resilience_report'], dst), 'no store-and-forward outage was given to the refresh')
|
|
134
|
+
item('well', w['id'], f"{w['display']}: machine vibration", _copy_family(wd, ['vibration_report'], dst),
|
|
135
|
+
'no machine record for this well (gea workspace --action vibration-report)')
|
|
136
|
+
# certificate conformity: the register's standing, written here from the register as it stands
|
|
137
|
+
reg = os.path.join(ws.path, 'wells', w['id'], 'records', 'certificates.jsonl')
|
|
138
|
+
if os.path.isfile(reg):
|
|
139
|
+
try:
|
|
140
|
+
from .certificates import CertificateRegister
|
|
141
|
+
conf = CertificateRegister(reg).conformance()
|
|
142
|
+
os.makedirs(dst, exist_ok=True)
|
|
143
|
+
p = os.path.join(dst, 'certificate_conformance.json')
|
|
144
|
+
with open(p, 'w', encoding='utf-8') as f:
|
|
145
|
+
json.dump(conf, f, indent=1, default=str)
|
|
146
|
+
item('well', w['id'], f"{w['display']}: certificate conformity (ISO/IEC 17025 7.8)",
|
|
147
|
+
[{'file': 'certificate_conformance.json', 'sha256': _sha256(p), 'bytes': os.path.getsize(p), 'generated_utc': _utc()}])
|
|
148
|
+
except Exception as e:
|
|
149
|
+
item('well', w['id'], f"{w['display']}: certificate conformity (ISO/IEC 17025 7.8)", [], f'register could not be read: {e}')
|
|
150
|
+
else:
|
|
151
|
+
item('well', w['id'], f"{w['display']}: certificate conformity (ISO/IEC 17025 7.8)", [], 'no certificate register for this well')
|
|
152
|
+
# -- every site: the SRA packet and the site report ---------------------------------------------------------------
|
|
153
|
+
for st in ws.sites():
|
|
154
|
+
sd = os.path.join(rep, 'sites', st['id'])
|
|
155
|
+
dst = os.path.join(out, 'sites', st['id'])
|
|
156
|
+
if st.get('sra'):
|
|
157
|
+
try:
|
|
158
|
+
ws.write_sra_packet(st['id'], catalog_csv=catalog_csv, start=pb['start'], end=pb['end'], actor=actor)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
errors.append(f"sra {st['id']}: {e}")
|
|
161
|
+
item('site-sra', st['id'], f"{st['display']}: SRA packet for {period}", _copy_family(sd, ['sra_packet'], dst), 'the SRA packet failed to write (see errors)')
|
|
162
|
+
else:
|
|
163
|
+
item('site-sra', st['id'], f"{st['display']}: SRA packet for {period}", [], 'no Seismicity Response Area defined for this site (gea workspace --action sra-define)')
|
|
164
|
+
try:
|
|
165
|
+
ws.write_site_report(st['id'], actor=actor)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
errors.append(f"site report {st['id']}: {e}")
|
|
168
|
+
item('site-report', st['id'], f"{st['display']}: site report", _copy_family(sd, ['site_report', 'association', 'quakeml'], dst), 'the site report failed to write')
|
|
169
|
+
# -- seismic stations and tracks --------------------------------------------------------------------------------
|
|
170
|
+
for s in ws.seismic_stations():
|
|
171
|
+
item('seismic', s['id'], f"{s['display']}: seismic station report",
|
|
172
|
+
_copy_family(os.path.join(rep, 'seismic', s['id']), ['seismic_station_report', 'summary', 'detect', 'lines'], os.path.join(out, 'seismic', s['id'])), 'never refreshed')
|
|
173
|
+
for t in ws.tracks():
|
|
174
|
+
item('track', t['id'], f"{t['display']}: seismic track report",
|
|
175
|
+
_copy_family(os.path.join(rep, 'tracks', t['id']), ['seismic_track_report', 'summary'], os.path.join(out, 'tracks', t['id'])), 'never refreshed')
|
|
176
|
+
# -- the index and the manifest ----------------------------------------------------------------------------------
|
|
177
|
+
inc = [i for i in items if i['status'] == 'INCLUDED']
|
|
178
|
+
na = [i for i in items if i['status'] != 'INCLUDED']
|
|
179
|
+
now = _utc()
|
|
180
|
+
report_id = f"MONTH-{period}-{now.replace('-', '').replace(':', '')}"
|
|
181
|
+
rows = [[i['kind'], i['id'], i['name'], i['status'], i['reason'] or ', '.join(f['file'] for f in i['files'])] for i in items]
|
|
182
|
+
secs = [Section('1', 'The month', [
|
|
183
|
+
f"{ws.manifest['name']}: the deliverable for {period} ({pb['start'][:10]} to {pb['end'][:10]}, {pb['days']} days), "
|
|
184
|
+
f"{'a closed month' if pb['closed'] else 'an OPEN month - the period has not ended, and this packet will be superseded'}. "
|
|
185
|
+
f"{len(inc)} item(s) included, {len(na)} not available. "
|
|
186
|
+
+ ('The refresh ran before collection, so every report is from the sources as they stand now. ' if refresh else
|
|
187
|
+
'The refresh was skipped: each report carries its own generated time below, which may predate this packet. ')
|
|
188
|
+
+ (f"{len(errors)} step(s) failed and are listed in section 3." if errors else 'No step failed.')]),
|
|
189
|
+
Section('2', 'What is in the packet', ['Every report the program writes for a well, a site, a station or a track, collected as written - '
|
|
190
|
+
'INCLUDED with the file names, or NOT AVAILABLE with the reason. Hashes are in MANIFEST.json.'],
|
|
191
|
+
[Table(['Kind', 'Id', 'Item', 'Status', 'Files / reason'], rows)]),
|
|
192
|
+
Section('3', 'What failed', errors or ['Nothing.']),
|
|
193
|
+
Section('4', 'What this packet does not call a measurement',
|
|
194
|
+
['An item that is NOT AVAILABLE - the reason names what the site lacks, and the packet does not stand in for it.',
|
|
195
|
+
'A number in any report - each report carries its own method and its own declarations; this index collects, it does not recompute.',
|
|
196
|
+
'A month that has not ended - the packet says OPEN and is superseded by the one made after the month closes.'])]
|
|
197
|
+
front = [['Report ID', report_id], ['Site', ws.manifest['name']], ['Period', period], ['Closed', 'yes' if pb['closed'] else 'NO - open month'],
|
|
198
|
+
['Generated (UTC)', now], ['Program', f'{PROGRAM_NAME} build {__version__}'], ['Result', f'{len(inc)} INCLUDED, {len(na)} NOT AVAILABLE, {len(errors)} FAILED']]
|
|
199
|
+
doc = Document(title=f'{PROGRAM_NAME} - Month-End Packet', report_id=report_id, front=front, sections=secs,
|
|
200
|
+
footer='Collected from the reports as written; nothing is recomputed in this index.',
|
|
201
|
+
data={'protocol': PROTOCOL, 'report_id': report_id, 'site': ws.manifest['name'], 'period': pb, 'generated_utc': now,
|
|
202
|
+
'program_version': __version__, 'refresh': refresh, 'items': items, 'errors': errors, '_records': [], '_catalogue_obj': None})
|
|
203
|
+
paths = _write(doc, out, 'INDEX')
|
|
204
|
+
# the manifest: every file in the packet folder
|
|
205
|
+
files = {}
|
|
206
|
+
for root, _, fns in os.walk(out):
|
|
207
|
+
for fn in sorted(fns):
|
|
208
|
+
if fn == 'MANIFEST.json':
|
|
209
|
+
continue
|
|
210
|
+
p = os.path.join(root, fn)
|
|
211
|
+
rel = os.path.relpath(p, out).replace(os.sep, '/')
|
|
212
|
+
files[rel] = {'sha256': _sha256(p), 'bytes': os.path.getsize(p)}
|
|
213
|
+
manifest = {'protocol': PROTOCOL, 'site': ws.manifest['name'], 'period': period, 'closed': pb['closed'], 'generated_utc': now, 'program_version': __version__,
|
|
214
|
+
'report_id': report_id, 'n_files': len(files), 'files': files, 'included': len(inc), 'not_available': len(na), 'errors': errors}
|
|
215
|
+
with open(os.path.join(out, 'MANIFEST.json'), 'w', encoding='utf-8') as f:
|
|
216
|
+
json.dump(manifest, f, indent=1)
|
|
217
|
+
slug = ''.join(c if c.isalnum() or c in '-_' else '-' for c in ws.manifest['name']).strip('-') or 'site'
|
|
218
|
+
zpath = os.path.join(ws.dir('reports', 'month_end'), f'month_end_{slug}_{period}.zip')
|
|
219
|
+
with zipfile.ZipFile(zpath + '.part', 'w', compression=zipfile.ZIP_DEFLATED) as z:
|
|
220
|
+
for root, _, fns in os.walk(out):
|
|
221
|
+
for fn in sorted(fns):
|
|
222
|
+
p = os.path.join(root, fn)
|
|
223
|
+
z.write(p, os.path.relpath(p, out).replace(os.sep, '/'))
|
|
224
|
+
os.replace(zpath + '.part', zpath)
|
|
225
|
+
rec = {'period': period, 'closed': pb['closed'], 'folder': out, 'archive': zpath, 'archive_sha256': _sha256(zpath), 'index': paths['html'], 'report_id': report_id,
|
|
226
|
+
'included': len(inc), 'not_available': len(na), 'errors': errors, 'n_files': len(files), 'generated_utc': now, 'refresh': refresh}
|
|
227
|
+
with open(os.path.join(ws.dir('records'), 'month_end.jsonl'), 'a', encoding='utf-8') as f:
|
|
228
|
+
f.write(json.dumps(rec) + '\n')
|
|
229
|
+
ws.audit(actor, 'month_end.assemble', {k: rec[k] for k in ('period', 'closed', 'archive', 'archive_sha256', 'included', 'not_available', 'n_files')}
|
|
230
|
+
| ({'errors': errors} if errors else {}))
|
|
231
|
+
return rec
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def packets(ws) -> List[dict]:
|
|
235
|
+
"""Every packet assembled from this workspace, newest last, with whether its archive is still there."""
|
|
236
|
+
p = os.path.join(ws.path, 'records', 'month_end.jsonl')
|
|
237
|
+
out = []
|
|
238
|
+
if os.path.isfile(p):
|
|
239
|
+
with open(p, encoding='utf-8') as f:
|
|
240
|
+
for line in f:
|
|
241
|
+
if line.strip():
|
|
242
|
+
try:
|
|
243
|
+
r = json.loads(line)
|
|
244
|
+
except ValueError:
|
|
245
|
+
continue
|
|
246
|
+
r['present'] = os.path.isfile(r.get('archive', ''))
|
|
247
|
+
out.append(r)
|
|
248
|
+
return out
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def verify(folder_or_zip: str) -> dict:
|
|
252
|
+
"""A packet folder or its archive against its MANIFEST.json."""
|
|
253
|
+
out = {'path': folder_or_zip, 'status': 'OK', 'n_files': 0, 'mismatched': [], 'missing': []}
|
|
254
|
+
if zipfile.is_zipfile(folder_or_zip):
|
|
255
|
+
with zipfile.ZipFile(folder_or_zip) as z:
|
|
256
|
+
if 'MANIFEST.json' not in z.namelist():
|
|
257
|
+
return {**out, 'status': 'FAIL', 'error': 'no MANIFEST.json'}
|
|
258
|
+
man = json.loads(z.read('MANIFEST.json'))
|
|
259
|
+
names = set(z.namelist())
|
|
260
|
+
for rel, meta in man['files'].items():
|
|
261
|
+
if rel not in names:
|
|
262
|
+
out['missing'].append(rel); continue
|
|
263
|
+
if hashlib.sha256(z.read(rel)).hexdigest() != meta['sha256']:
|
|
264
|
+
out['mismatched'].append(rel)
|
|
265
|
+
out['n_files'] += 1
|
|
266
|
+
else:
|
|
267
|
+
mp = os.path.join(folder_or_zip, 'MANIFEST.json')
|
|
268
|
+
if not os.path.isfile(mp):
|
|
269
|
+
return {**out, 'status': 'FAIL', 'error': 'no MANIFEST.json'}
|
|
270
|
+
man = json.load(open(mp, encoding='utf-8'))
|
|
271
|
+
for rel, meta in man['files'].items():
|
|
272
|
+
p = os.path.join(folder_or_zip, *rel.split('/'))
|
|
273
|
+
if not os.path.isfile(p):
|
|
274
|
+
out['missing'].append(rel); continue
|
|
275
|
+
if _sha256(p) != meta['sha256']:
|
|
276
|
+
out['mismatched'].append(rel)
|
|
277
|
+
out['n_files'] += 1
|
|
278
|
+
out['period'] = man.get('period'); out['site'] = man.get('site'); out['generated_utc'] = man.get('generated_utc')
|
|
279
|
+
if out['mismatched'] or out['missing']:
|
|
280
|
+
out['status'] = 'FAIL'
|
|
281
|
+
return out
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def report_text(rec: dict) -> str:
|
|
285
|
+
lines = [f"month-end {rec['period']} ({'closed' if rec['closed'] else 'OPEN month'}): {rec['included']} included, {rec['not_available']} not available, "
|
|
286
|
+
f"{len(rec['errors'])} failed; {rec['n_files']} files",
|
|
287
|
+
f" folder: {rec['folder']}", f" archive: {rec['archive']} sha256 {rec['archive_sha256'][:16]}...", f" index: {rec['index']}"]
|
|
288
|
+
for e in rec['errors']:
|
|
289
|
+
lines.append(f" FAILED: {e}")
|
|
290
|
+
return '\n'.join(lines)
|
|
@@ -1602,6 +1602,10 @@ def make_handler(app: App):
|
|
|
1602
1602
|
return self._json(200, {'entries': app.catalog()})
|
|
1603
1603
|
if path == '/api/reports':
|
|
1604
1604
|
return self._json(200, app.reports_index())
|
|
1605
|
+
if path == '/api/month-end':
|
|
1606
|
+
from . import month_end as ME
|
|
1607
|
+
sched = next((e for e in app.scheduler.entries() if e['name'] == 'month-end-monthly'), None)
|
|
1608
|
+
return self._json(200, {'packets': ME.packets(app.ws), 'previous': ME.previous_period(), 'schedule': sched})
|
|
1605
1609
|
if path == '/api/jobs':
|
|
1606
1610
|
return self._json(200, {'jobs': app.runner.list(int(qs.get('limit', ['50'])[0]))})
|
|
1607
1611
|
if path.startswith('/api/jobs/') and path.endswith('/log'):
|
|
@@ -1799,6 +1803,29 @@ def make_handler(app: App):
|
|
|
1799
1803
|
'detail': ('the launcher will start the control panel again by itself if it stops unexpectedly'
|
|
1800
1804
|
if on else
|
|
1801
1805
|
'the launcher will hand the window to a PowerShell prompt when the control panel stops')})
|
|
1806
|
+
if path == '/api/month-end/run':
|
|
1807
|
+
App.require(user, 'operator')
|
|
1808
|
+
from . import month_end as ME
|
|
1809
|
+
period = str(body.get('period') or 'previous')
|
|
1810
|
+
if period != 'previous':
|
|
1811
|
+
try:
|
|
1812
|
+
ME.period_bounds(period)
|
|
1813
|
+
except ValueError as e:
|
|
1814
|
+
raise ApiError(400, str(e))
|
|
1815
|
+
args = ['workspace', '--path', app.ws.path, '--action', 'month-end', '--period', period, '--actor', user['name']]
|
|
1816
|
+
if body.get('no_refresh'):
|
|
1817
|
+
args.append('--no-refresh')
|
|
1818
|
+
return self._json(200, app._job(args, user, f'month-end packet {period}'))
|
|
1819
|
+
if path == '/api/month-end/schedule':
|
|
1820
|
+
App.require(user, 'admin')
|
|
1821
|
+
try:
|
|
1822
|
+
day = int(body.get('day', 1)); at = str(body.get('at') or '03:00')
|
|
1823
|
+
except (TypeError, ValueError):
|
|
1824
|
+
raise ApiError(400, 'day is a whole number 1..28')
|
|
1825
|
+
if not (1 <= day <= 28) or len(at) != 5 or at[2] != ':' or not (at[:2] + at[3:]).isdigit():
|
|
1826
|
+
raise ApiError(400, 'day is 1..28 and at is HH:MM')
|
|
1827
|
+
args = ['workspace', '--path', app.ws.path, '--action', 'month-end', '--period', 'previous', '--actor', 'schedule']
|
|
1828
|
+
return self._json(200, app.scheduler.add('month-end-monthly', args, user['name'], daily_at=at, monthly_day=day))
|
|
1802
1829
|
if path == '/api/backup/config':
|
|
1803
1830
|
App.require(user, 'admin')
|
|
1804
1831
|
return self._json(200, app.backup_set_config(user, body))
|