mostlyright-data 0.9.0__py3-none-any.whl
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.
- mostlyright/data_harness/__init__.py +158 -0
- mostlyright/data_harness/acquisition/__init__.py +55 -0
- mostlyright/data_harness/acquisition/http.py +2773 -0
- mostlyright/data_harness/acquisition/parsing.py +809 -0
- mostlyright/data_harness/acquisition/ranges.py +495 -0
- mostlyright/data_harness/acquisition/result_download.py +360 -0
- mostlyright/data_harness/acquisition/retention_admission.py +248 -0
- mostlyright/data_harness/acquisition/sandbox.py +4888 -0
- mostlyright/data_harness/acquisition/url_policy.py +530 -0
- mostlyright/data_harness/agent_runtime.py +2743 -0
- mostlyright/data_harness/assets/logo-ink.svg +31 -0
- mostlyright/data_harness/backends/__init__.py +28 -0
- mostlyright/data_harness/backends/pandas_backend.py +350 -0
- mostlyright/data_harness/backends/polars_backend.py +366 -0
- mostlyright/data_harness/backends/protocol.py +124 -0
- mostlyright/data_harness/backends/reference.py +83 -0
- mostlyright/data_harness/backends/registry.py +55 -0
- mostlyright/data_harness/backends/restrictions.py +126 -0
- mostlyright/data_harness/canonical.py +333 -0
- mostlyright/data_harness/catalog_job.py +625 -0
- mostlyright/data_harness/cli.py +5398 -0
- mostlyright/data_harness/contracts.py +53 -0
- mostlyright/data_harness/coordinator.py +1307 -0
- mostlyright/data_harness/deploy.py +924 -0
- mostlyright/data_harness/deploy_target.py +312 -0
- mostlyright/data_harness/deployment_evidence.py +1067 -0
- mostlyright/data_harness/event_presentation.py +576 -0
- mostlyright/data_harness/events.py +2152 -0
- mostlyright/data_harness/fast_delimited.py +239 -0
- mostlyright/data_harness/fleet.py +237 -0
- mostlyright/data_harness/formats.py +236 -0
- mostlyright/data_harness/governors.py +1163 -0
- mostlyright/data_harness/hosted_bootstrap.py +972 -0
- mostlyright/data_harness/hosted_crawler.py +1115 -0
- mostlyright/data_harness/hosted_crawler_container_smoke.py +351 -0
- mostlyright/data_harness/hosted_crawler_fetch.py +423 -0
- mostlyright/data_harness/hosted_crawler_job.py +1277 -0
- mostlyright/data_harness/hosted_crawler_protocol.py +676 -0
- mostlyright/data_harness/hosted_dataset.py +1500 -0
- mostlyright/data_harness/hosted_deploy.py +3037 -0
- mostlyright/data_harness/hosted_handoff.py +62 -0
- mostlyright/data_harness/hosted_ingestion_contract.py +504 -0
- mostlyright/data_harness/hosted_ingestion_job.py +356 -0
- mostlyright/data_harness/hosted_ingestion_job_smoke.py +40 -0
- mostlyright/data_harness/hosted_session_container_smoke.py +194 -0
- mostlyright/data_harness/hosted_session_worker.py +3554 -0
- mostlyright/data_harness/hosted_session_worker_job_smoke.py +46 -0
- mostlyright/data_harness/hosted_worker.py +6784 -0
- mostlyright/data_harness/ingestion/__init__.py +56 -0
- mostlyright/data_harness/ingestion/contracts.py +461 -0
- mostlyright/data_harness/ingestion/faults.py +42 -0
- mostlyright/data_harness/ingestion/gcs_store.py +1162 -0
- mostlyright/data_harness/ingestion/spool.py +130 -0
- mostlyright/data_harness/ingestion/store.py +885 -0
- mostlyright/data_harness/key_seam.py +434 -0
- mostlyright/data_harness/linux_process_boundary.py +262 -0
- mostlyright/data_harness/local_contracts.py +2880 -0
- mostlyright/data_harness/local_search/__init__.py +5 -0
- mostlyright/data_harness/local_search/build_index.py +1087 -0
- mostlyright/data_harness/local_search/contracts.py +920 -0
- mostlyright/data_harness/local_search/query_trace.py +266 -0
- mostlyright/data_harness/local_search/retrieval.py +700 -0
- mostlyright/data_harness/local_search/sealed.py +474 -0
- mostlyright/data_harness/local_search/service.py +784 -0
- mostlyright/data_harness/nbrender/CONTRACT.md +212 -0
- mostlyright/data_harness/nbrender/__init__.py +12 -0
- mostlyright/data_harness/nbrender/chrome.py +359 -0
- mostlyright/data_harness/nbrender/code_body.py +266 -0
- mostlyright/data_harness/nbrender/document.py +407 -0
- mostlyright/data_harness/nbrender/frame.py +275 -0
- mostlyright/data_harness/nbrender/interactive.py +337 -0
- mostlyright/data_harness/nbrender/markdown_body.py +477 -0
- mostlyright/data_harness/nbrender/mr_components.py +134 -0
- mostlyright/data_harness/nbrender/outputs_data.py +595 -0
- mostlyright/data_harness/nbrender/outputs_rich.py +906 -0
- mostlyright/data_harness/nbrender/outputs_source.py +260 -0
- mostlyright/data_harness/nbrender/outputs_stage.py +176 -0
- mostlyright/data_harness/nbrender/outputs_text.py +400 -0
- mostlyright/data_harness/nbrender/parse.py +394 -0
- mostlyright/data_harness/nbrender/status.py +40 -0
- mostlyright/data_harness/nbrender/tokens.py +1295 -0
- mostlyright/data_harness/notebook.py +1710 -0
- mostlyright/data_harness/offline.py +2049 -0
- mostlyright/data_harness/operation_registry.py +1007 -0
- mostlyright/data_harness/operator_setup.py +239 -0
- mostlyright/data_harness/pipeline.py +6428 -0
- mostlyright/data_harness/plan_graph.py +2026 -0
- mostlyright/data_harness/preparation/__init__.py +104 -0
- mostlyright/data_harness/preparation/contracts.py +1017 -0
- mostlyright/data_harness/preparation/engine.py +221 -0
- mostlyright/data_harness/preparation/errors.py +14 -0
- mostlyright/data_harness/preparation/gates.py +751 -0
- mostlyright/data_harness/preparation/joins.py +574 -0
- mostlyright/data_harness/preparation/profile.py +384 -0
- mostlyright/data_harness/preparation/table.py +217 -0
- mostlyright/data_harness/preparation/transforms.py +568 -0
- mostlyright/data_harness/progress_events.py +534 -0
- mostlyright/data_harness/readers/__init__.py +46 -0
- mostlyright/data_harness/readers/containers.py +963 -0
- mostlyright/data_harness/readers/contracts.py +542 -0
- mostlyright/data_harness/readers/delimited.py +257 -0
- mostlyright/data_harness/readers/grib2/__init__.py +33 -0
- mostlyright/data_harness/readers/grib2/admission.py +722 -0
- mostlyright/data_harness/readers/grib2/decode.py +1009 -0
- mostlyright/data_harness/readers/grib2/geometry.py +1133 -0
- mostlyright/data_harness/readers/grib2/portable_math.py +501 -0
- mostlyright/data_harness/readers/json_tabular.py +485 -0
- mostlyright/data_harness/readers/registry.py +514 -0
- mostlyright/data_harness/readers/samples/README.md +110 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/cities.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/cities.csv +3 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/expected.json +23 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/cities.tsv +5 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/expected.json +30 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/observations.csv +5 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/expected.json +44 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/stations.json +1 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/expected.json +48 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/observations.ndjson +4 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/cities.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/expected.json +24 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/README.md +20 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/expected.json +55 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/gfs-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/hrrr-categorical-rain.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples.py +582 -0
- mostlyright/data_harness/readers/spreadsheet.py +803 -0
- mostlyright/data_harness/readers/tabular.py +510 -0
- mostlyright/data_harness/recipe.py +5321 -0
- mostlyright/data_harness/repair/__init__.py +78 -0
- mostlyright/data_harness/repair/adapters.py +274 -0
- mostlyright/data_harness/repair/contracts.py +872 -0
- mostlyright/data_harness/repair/coordinator.py +1099 -0
- mostlyright/data_harness/repair/errors.py +16 -0
- mostlyright/data_harness/review.py +2533 -0
- mostlyright/data_harness/rowset.py +283 -0
- mostlyright/data_harness/serving.py +1975 -0
- mostlyright/data_harness/serving_edge.py +590 -0
- mostlyright/data_harness/serving_http.py +1031 -0
- mostlyright/data_harness/session_probes.py +759 -0
- mostlyright/data_harness/signing.py +101 -0
- mostlyright/data_harness/source_discovery.py +898 -0
- mostlyright/data_harness/sources/__init__.py +209 -0
- mostlyright/data_harness/sources/_adapter_steps.py +213 -0
- mostlyright/data_harness/sources/adapters.py +1214 -0
- mostlyright/data_harness/sources/cadence.py +1428 -0
- mostlyright/data_harness/sources/cadence_emission.py +453 -0
- mostlyright/data_harness/sources/cadence_history.py +546 -0
- mostlyright/data_harness/sources/catalog/__init__.py +17 -0
- mostlyright/data_harness/sources/catalog/admission.py +477 -0
- mostlyright/data_harness/sources/catalog/authoring.py +1701 -0
- mostlyright/data_harness/sources/catalog/authoring_policy.py +701 -0
- mostlyright/data_harness/sources/catalog/authoring_shards.py +1217 -0
- mostlyright/data_harness/sources/catalog/bounded_io.py +231 -0
- mostlyright/data_harness/sources/catalog/channel.py +523 -0
- mostlyright/data_harness/sources/catalog/channel_client.py +296 -0
- mostlyright/data_harness/sources/catalog/contracts.py +825 -0
- mostlyright/data_harness/sources/catalog/coverage.py +137 -0
- mostlyright/data_harness/sources/catalog/delta.py +1340 -0
- mostlyright/data_harness/sources/catalog/embedding.py +532 -0
- mostlyright/data_harness/sources/catalog/entry_v2.py +1182 -0
- mostlyright/data_harness/sources/catalog/fill.py +3889 -0
- mostlyright/data_harness/sources/catalog/fill_partitions.py +459 -0
- mostlyright/data_harness/sources/catalog/fill_staging.py +1105 -0
- mostlyright/data_harness/sources/catalog/gating.py +374 -0
- mostlyright/data_harness/sources/catalog/generation_receipt.py +1607 -0
- mostlyright/data_harness/sources/catalog/harvest/__init__.py +7 -0
- mostlyright/data_harness/sources/catalog/harvest/ckan.py +384 -0
- mostlyright/data_harness/sources/catalog/harvest/datagov_v4.py +798 -0
- mostlyright/data_harness/sources/catalog/harvest/protocol.py +964 -0
- mostlyright/data_harness/sources/catalog/harvest/sdmx.py +445 -0
- mostlyright/data_harness/sources/catalog/harvest/stac.py +384 -0
- mostlyright/data_harness/sources/catalog/health.py +447 -0
- mostlyright/data_harness/sources/catalog/hosted_catalog.py +105 -0
- mostlyright/data_harness/sources/catalog/identity_history.py +1549 -0
- mostlyright/data_harness/sources/catalog/neural.py +1618 -0
- mostlyright/data_harness/sources/catalog/packed_catalog.py +2345 -0
- mostlyright/data_harness/sources/catalog/packed_retrieval.py +1517 -0
- mostlyright/data_harness/sources/catalog/packed_writer.py +2802 -0
- mostlyright/data_harness/sources/catalog/query_trace.py +1037 -0
- mostlyright/data_harness/sources/catalog/recommend.py +171 -0
- mostlyright/data_harness/sources/catalog/retrieval.py +230 -0
- mostlyright/data_harness/sources/catalog/retrieval_manifest.py +995 -0
- mostlyright/data_harness/sources/catalog/rights_decisions.py +254 -0
- mostlyright/data_harness/sources/catalog/sealed.py +560 -0
- mostlyright/data_harness/sources/catalog/search.py +230 -0
- mostlyright/data_harness/sources/catalog/streaming_delta.py +1097 -0
- mostlyright/data_harness/sources/catalog/update.py +891 -0
- mostlyright/data_harness/sources/collections.py +815 -0
- mostlyright/data_harness/sources/contracts.py +2223 -0
- mostlyright/data_harness/sources/deletion.py +761 -0
- mostlyright/data_harness/sources/fitness.py +162 -0
- mostlyright/data_harness/sources/governance.py +163 -0
- mostlyright/data_harness/sources/hosted.py +173 -0
- mostlyright/data_harness/sources/integration.py +218 -0
- mostlyright/data_harness/sources/range_reader.py +418 -0
- mostlyright/data_harness/sources/registry.py +514 -0
- mostlyright/data_harness/sources/rights_rule.py +59 -0
- mostlyright/data_harness/sources/source_cadence_vectors.v1.json +1 -0
- mostlyright/data_harness/sources/sports.py +521 -0
- mostlyright/data_harness/sources/stream.py +524 -0
- mostlyright/data_harness/sources/stream_connector.py +418 -0
- mostlyright/data_harness/sources/stream_recorder.py +1404 -0
- mostlyright/data_harness/studio_boundary.py +2019 -0
- mostlyright/data_harness/thin/__init__.py +37 -0
- mostlyright/data_harness/thin/acquire.py +1137 -0
- mostlyright/data_harness/thin/acquire_cancel.py +579 -0
- mostlyright/data_harness/thin/approvals.py +617 -0
- mostlyright/data_harness/thin/commands.py +406 -0
- mostlyright/data_harness/thin/download.py +194 -0
- mostlyright/data_harness/thin/narrative.py +589 -0
- mostlyright/data_harness/thin/parity.py +1070 -0
- mostlyright/data_harness/thin/propose.py +2759 -0
- mostlyright/data_harness/thin/research.py +1663 -0
- mostlyright/data_harness/thin/router.py +924 -0
- mostlyright/data_harness/thin/runs.py +519 -0
- mostlyright/data_harness/thin/session.py +281 -0
- mostlyright/data_harness/thin/stream.py +501 -0
- mostlyright/data_harness/thin/transport.py +187 -0
- mostlyright/data_harness/thin/vocabulary.py +368 -0
- mostlyright/data_harness/thin/workers.py +164 -0
- mostlyright/data_harness/ucum/TABLE-PIN.json +40 -0
- mostlyright/data_harness/ucum/ucum-subset.v1.json +632 -0
- mostlyright/data_harness/unit_flow.py +927 -0
- mostlyright/data_harness/units.py +572 -0
- mostlyright/data_harness/ux/__init__.py +9 -0
- mostlyright/data_harness/ux/approve.py +485 -0
- mostlyright/data_harness/ux/author_yaml.py +597 -0
- mostlyright/data_harness/ux/cloud_auth.py +447 -0
- mostlyright/data_harness/ux/commands/__init__.py +260 -0
- mostlyright/data_harness/ux/commands/approve.py +136 -0
- mostlyright/data_harness/ux/commands/auth.py +744 -0
- mostlyright/data_harness/ux/commands/author.py +79 -0
- mostlyright/data_harness/ux/commands/catalog_author.py +403 -0
- mostlyright/data_harness/ux/commands/catalog_fill.py +523 -0
- mostlyright/data_harness/ux/commands/catalog_harvest.py +545 -0
- mostlyright/data_harness/ux/commands/catalog_publish.py +1838 -0
- mostlyright/data_harness/ux/commands/catalog_search.py +71 -0
- mostlyright/data_harness/ux/commands/catalog_update.py +437 -0
- mostlyright/data_harness/ux/commands/deploy.py +134 -0
- mostlyright/data_harness/ux/commands/deploy_dataset.py +98 -0
- mostlyright/data_harness/ux/commands/deploy_plan.py +105 -0
- mostlyright/data_harness/ux/commands/deploy_status.py +104 -0
- mostlyright/data_harness/ux/commands/diff.py +74 -0
- mostlyright/data_harness/ux/commands/index.py +84 -0
- mostlyright/data_harness/ux/commands/inventory.py +47 -0
- mostlyright/data_harness/ux/commands/list_builds.py +143 -0
- mostlyright/data_harness/ux/commands/login.py +63 -0
- mostlyright/data_harness/ux/commands/peek.py +236 -0
- mostlyright/data_harness/ux/commands/plan_check.py +90 -0
- mostlyright/data_harness/ux/commands/preflight.py +97 -0
- mostlyright/data_harness/ux/commands/record.py +107 -0
- mostlyright/data_harness/ux/commands/review_setup.py +47 -0
- mostlyright/data_harness/ux/commands/search.py +440 -0
- mostlyright/data_harness/ux/commands/show.py +61 -0
- mostlyright/data_harness/ux/commands/whoami.py +37 -0
- mostlyright/data_harness/ux/credential_native.py +551 -0
- mostlyright/data_harness/ux/credential_store.py +1055 -0
- mostlyright/data_harness/ux/credentials.py +631 -0
- mostlyright/data_harness/ux/diffing.py +444 -0
- mostlyright/data_harness/ux/headline.py +671 -0
- mostlyright/data_harness/ux/hosted_acquisition.py +974 -0
- mostlyright/data_harness/ux/hosted_run_status.py +619 -0
- mostlyright/data_harness/ux/inventory.py +427 -0
- mostlyright/data_harness/ux/local_review.py +375 -0
- mostlyright/data_harness/ux/login.py +691 -0
- mostlyright/data_harness/ux/path_kind.py +147 -0
- mostlyright/data_harness/ux/peek.py +1000 -0
- mostlyright/data_harness/ux/plain_file.py +178 -0
- mostlyright/data_harness/ux/plan_check.py +311 -0
- mostlyright/data_harness/ux/preflight.py +918 -0
- mostlyright/data_harness/ux/readers.py +1124 -0
- mostlyright/data_harness/ux/remediation.py +2195 -0
- mostlyright/data_harness/ux/render.py +657 -0
- mostlyright/data_harness/ux/workload.py +1077 -0
- mostlyright/data_harness/viewer.py +3713 -0
- mostlyright/data_harness/visual_run/__init__.py +83 -0
- mostlyright/data_harness/visual_run/authoring.py +235 -0
- mostlyright/data_harness/visual_run/contracts.py +673 -0
- mostlyright/data_harness/visual_run/materialize.py +486 -0
- mostlyright/data_harness/visual_run/observations.py +874 -0
- mostlyright/data_harness/visual_run/query.py +259 -0
- mostlyright/data_harness/visual_run/reducer.py +280 -0
- mostlyright/data_harness/visual_run/sdk.py +892 -0
- mostlyright/data_harness/visual_run/store.py +584 -0
- mostlyright/data_harness/visual_run/transport.py +239 -0
- mostlyright/data_harness/watch.py +2999 -0
- mostlyright_data-0.9.0.dist-info/METADATA +607 -0
- mostlyright_data-0.9.0.dist-info/RECORD +314 -0
- mostlyright_data-0.9.0.dist-info/WHEEL +4 -0
- mostlyright_data-0.9.0.dist-info/entry_points.txt +12 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
"""Durable local storage for visual-run events and immutable artifacts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fcntl
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import secrets
|
|
10
|
+
import stat
|
|
11
|
+
from collections.abc import Callable, Iterator
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import BinaryIO
|
|
16
|
+
|
|
17
|
+
from mostlyright.data_harness.visual_run.contracts import (
|
|
18
|
+
DOCUMENT_HEADER_CELL_ID,
|
|
19
|
+
LEGACY_DOCUMENT_HEADER_CELL_ID_PREFIX,
|
|
20
|
+
ArtifactReference,
|
|
21
|
+
VisualRunDraft,
|
|
22
|
+
VisualRunError,
|
|
23
|
+
VisualRunEvent,
|
|
24
|
+
canonical_draft_bytes,
|
|
25
|
+
canonical_event_bytes,
|
|
26
|
+
parse_event,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
MAX_LOG_BYTES = 64 * 1024 * 1024
|
|
30
|
+
MAX_EVENTS = 100_000
|
|
31
|
+
LOG_NAME = "events.jsonl"
|
|
32
|
+
LOCK_NAME = ".append.lock"
|
|
33
|
+
ARTIFACT_DIR = "artifacts"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _command_bytes(draft: VisualRunDraft) -> bytes:
|
|
37
|
+
"""Canonical command identity, excluding only the generated observation timestamp."""
|
|
38
|
+
|
|
39
|
+
value = draft.to_mapping()
|
|
40
|
+
value.pop("observed_at")
|
|
41
|
+
return (
|
|
42
|
+
json.dumps(
|
|
43
|
+
value, ensure_ascii=False, allow_nan=False, sort_keys=True, separators=(",", ":")
|
|
44
|
+
)
|
|
45
|
+
+ "\n"
|
|
46
|
+
).encode("utf-8")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class EventPage:
|
|
51
|
+
events: tuple[VisualRunEvent, ...]
|
|
52
|
+
cursor: int
|
|
53
|
+
truncated: bool
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AuthorityLease:
|
|
57
|
+
"""A held authority lock with one commit staged after final identity validation."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, validate: Callable[[], None]) -> None:
|
|
60
|
+
self._validate = validate
|
|
61
|
+
self._commit: Callable[[], None] | None = None
|
|
62
|
+
|
|
63
|
+
def __call__(self) -> None:
|
|
64
|
+
self._validate()
|
|
65
|
+
|
|
66
|
+
def commit_after_validation(self, commit: Callable[[], None]) -> None:
|
|
67
|
+
if self._commit is not None:
|
|
68
|
+
raise RuntimeError("authority lease accepts one staged commit")
|
|
69
|
+
self._commit = commit
|
|
70
|
+
|
|
71
|
+
def _finish(self) -> None:
|
|
72
|
+
self._validate()
|
|
73
|
+
if self._commit is not None:
|
|
74
|
+
self._commit()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _error(code: str, message: str) -> VisualRunError:
|
|
78
|
+
return VisualRunError(code, message)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _regular_descriptor(descriptor: int, name: str) -> os.stat_result:
|
|
82
|
+
info = os.fstat(descriptor)
|
|
83
|
+
if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
|
|
84
|
+
raise _error("VISUAL_RUN_STORE_IDENTITY", f"{name} must be one regular file")
|
|
85
|
+
return info
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _read_descriptor(descriptor: int, *, maximum: int, code: str, message: str) -> bytes:
|
|
89
|
+
os.lseek(descriptor, 0, os.SEEK_SET)
|
|
90
|
+
chunks: list[bytes] = []
|
|
91
|
+
total = 0
|
|
92
|
+
while True:
|
|
93
|
+
chunk = os.read(descriptor, min(64 * 1024, maximum + 1 - total))
|
|
94
|
+
if not chunk:
|
|
95
|
+
return b"".join(chunks)
|
|
96
|
+
chunks.append(chunk)
|
|
97
|
+
total += len(chunk)
|
|
98
|
+
if total > maximum:
|
|
99
|
+
raise _error(code, message)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@contextmanager
|
|
103
|
+
def _locked(directory_fd: int) -> Iterator[BinaryIO]:
|
|
104
|
+
descriptor = os.open(
|
|
105
|
+
LOCK_NAME,
|
|
106
|
+
os.O_RDWR | os.O_CREAT | os.O_CLOEXEC | os.O_NOFOLLOW,
|
|
107
|
+
0o600,
|
|
108
|
+
dir_fd=directory_fd,
|
|
109
|
+
)
|
|
110
|
+
_regular_descriptor(descriptor, LOCK_NAME)
|
|
111
|
+
stream = os.fdopen(descriptor, "r+b", closefd=True)
|
|
112
|
+
try:
|
|
113
|
+
fcntl.flock(stream.fileno(), fcntl.LOCK_EX)
|
|
114
|
+
yield stream
|
|
115
|
+
finally:
|
|
116
|
+
fcntl.flock(stream.fileno(), fcntl.LOCK_UN)
|
|
117
|
+
stream.close()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@contextmanager
|
|
121
|
+
def visual_authority_lock(directory_fd: int, *, exclusive: bool) -> Iterator[AuthorityLease]:
|
|
122
|
+
"""Serialize first visual authority installation with legacy viewer admission.
|
|
123
|
+
|
|
124
|
+
The already-pinned research directory inode is the lock object. Unlike a child lock pathname,
|
|
125
|
+
that descriptor-relative synchronization boundary cannot be substituted between validation
|
|
126
|
+
and commit. Visual writers take it exclusively while installing ``visual-run``; viewers take
|
|
127
|
+
it shared across the final absent observation and one staged legacy journal commit.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
descriptor = os.open(
|
|
131
|
+
".",
|
|
132
|
+
os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW,
|
|
133
|
+
dir_fd=directory_fd,
|
|
134
|
+
)
|
|
135
|
+
try:
|
|
136
|
+
info = os.fstat(descriptor)
|
|
137
|
+
pinned = os.fstat(directory_fd)
|
|
138
|
+
if not stat.S_ISDIR(info.st_mode) or (info.st_dev, info.st_ino) != (
|
|
139
|
+
pinned.st_dev,
|
|
140
|
+
pinned.st_ino,
|
|
141
|
+
):
|
|
142
|
+
raise _error(
|
|
143
|
+
"VISUAL_RUN_STORE_IDENTITY",
|
|
144
|
+
"visual authority must lock the pinned research directory",
|
|
145
|
+
)
|
|
146
|
+
fcntl.flock(descriptor, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
|
|
147
|
+
|
|
148
|
+
def validate() -> None:
|
|
149
|
+
try:
|
|
150
|
+
current = os.fstat(descriptor)
|
|
151
|
+
pinned_now = os.fstat(directory_fd)
|
|
152
|
+
except OSError as exc:
|
|
153
|
+
raise _error(
|
|
154
|
+
"VISUAL_RUN_STORE_IDENTITY",
|
|
155
|
+
"pinned research authority became unavailable during the locked operation",
|
|
156
|
+
) from exc
|
|
157
|
+
if (
|
|
158
|
+
(current.st_dev, current.st_ino) != (info.st_dev, info.st_ino)
|
|
159
|
+
or (pinned_now.st_dev, pinned_now.st_ino) != (info.st_dev, info.st_ino)
|
|
160
|
+
or not stat.S_ISDIR(current.st_mode)
|
|
161
|
+
):
|
|
162
|
+
raise _error(
|
|
163
|
+
"VISUAL_RUN_STORE_IDENTITY",
|
|
164
|
+
"pinned research authority changed during the locked operation",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
lease = AuthorityLease(validate)
|
|
169
|
+
lease()
|
|
170
|
+
yield lease
|
|
171
|
+
lease._finish()
|
|
172
|
+
finally:
|
|
173
|
+
fcntl.flock(descriptor, fcntl.LOCK_UN)
|
|
174
|
+
finally:
|
|
175
|
+
os.close(descriptor)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class VisualRunStore:
|
|
179
|
+
"""One run-bound append-only event log.
|
|
180
|
+
|
|
181
|
+
The root is supplied by the trusted viewer/coordinator. The store never accepts a path from an
|
|
182
|
+
event. Sequence allocation and idempotency checks happen under one kernel file lock.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
def __init__(
|
|
186
|
+
self,
|
|
187
|
+
root: Path,
|
|
188
|
+
run_id: str,
|
|
189
|
+
*,
|
|
190
|
+
parent_fd: int | None = None,
|
|
191
|
+
root_fd: int | None = None,
|
|
192
|
+
) -> None:
|
|
193
|
+
self.root = Path(root)
|
|
194
|
+
self.run_id = run_id
|
|
195
|
+
self._root_fd = -1
|
|
196
|
+
self._artifact_fd = -1
|
|
197
|
+
if parent_fd is not None and root_fd is not None:
|
|
198
|
+
raise ValueError("visual store accepts one pinned directory boundary")
|
|
199
|
+
if root_fd is not None:
|
|
200
|
+
self._root_fd = os.dup(root_fd)
|
|
201
|
+
elif parent_fd is not None:
|
|
202
|
+
if self.root.name != str(self.root) or self.root.name in {"", ".", ".."}:
|
|
203
|
+
raise ValueError("descriptor-relative visual store root must be one name")
|
|
204
|
+
with visual_authority_lock(parent_fd, exclusive=True) as validate_authority:
|
|
205
|
+
validate_authority()
|
|
206
|
+
try:
|
|
207
|
+
os.mkdir(self.root.name, mode=0o700, dir_fd=parent_fd)
|
|
208
|
+
os.fsync(parent_fd)
|
|
209
|
+
except FileExistsError:
|
|
210
|
+
pass
|
|
211
|
+
validate_authority()
|
|
212
|
+
self._root_fd = os.open(
|
|
213
|
+
self.root.name,
|
|
214
|
+
os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW,
|
|
215
|
+
dir_fd=parent_fd,
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
parent = self.root.parent
|
|
219
|
+
parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
220
|
+
parent_fd = os.open(
|
|
221
|
+
parent,
|
|
222
|
+
os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW,
|
|
223
|
+
)
|
|
224
|
+
try:
|
|
225
|
+
with visual_authority_lock(parent_fd, exclusive=True) as validate_authority:
|
|
226
|
+
validate_authority()
|
|
227
|
+
try:
|
|
228
|
+
os.mkdir(self.root.name, mode=0o700, dir_fd=parent_fd)
|
|
229
|
+
os.fsync(parent_fd)
|
|
230
|
+
except FileExistsError:
|
|
231
|
+
pass
|
|
232
|
+
validate_authority()
|
|
233
|
+
self._root_fd = os.open(
|
|
234
|
+
self.root.name,
|
|
235
|
+
os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW,
|
|
236
|
+
dir_fd=parent_fd,
|
|
237
|
+
)
|
|
238
|
+
finally:
|
|
239
|
+
os.close(parent_fd)
|
|
240
|
+
root_info = os.fstat(self._root_fd)
|
|
241
|
+
path_info = self.root.stat(follow_symlinks=False)
|
|
242
|
+
if (root_info.st_dev, root_info.st_ino) != (path_info.st_dev, path_info.st_ino):
|
|
243
|
+
root_descriptor, self._root_fd = self._root_fd, -1
|
|
244
|
+
os.close(root_descriptor)
|
|
245
|
+
raise _error("VISUAL_RUN_STORE_IDENTITY", "store root changed while it was opened")
|
|
246
|
+
if not stat.S_ISDIR(os.fstat(self._root_fd).st_mode):
|
|
247
|
+
root_descriptor, self._root_fd = self._root_fd, -1
|
|
248
|
+
os.close(root_descriptor)
|
|
249
|
+
raise _error("VISUAL_RUN_STORE_IDENTITY", "store root must be a real directory")
|
|
250
|
+
self.artifact_root = self.root / ARTIFACT_DIR
|
|
251
|
+
try:
|
|
252
|
+
os.mkdir(ARTIFACT_DIR, mode=0o700, dir_fd=self._root_fd)
|
|
253
|
+
os.fsync(self._root_fd)
|
|
254
|
+
except FileExistsError:
|
|
255
|
+
pass
|
|
256
|
+
try:
|
|
257
|
+
self._artifact_fd = os.open(
|
|
258
|
+
ARTIFACT_DIR,
|
|
259
|
+
os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW,
|
|
260
|
+
dir_fd=self._root_fd,
|
|
261
|
+
)
|
|
262
|
+
except OSError as exc:
|
|
263
|
+
root_descriptor, self._root_fd = self._root_fd, -1
|
|
264
|
+
os.close(root_descriptor)
|
|
265
|
+
raise _error(
|
|
266
|
+
"VISUAL_RUN_STORE_IDENTITY", "artifact root must be a real directory"
|
|
267
|
+
) from exc
|
|
268
|
+
if not stat.S_ISDIR(os.fstat(self._artifact_fd).st_mode):
|
|
269
|
+
artifact_descriptor, self._artifact_fd = self._artifact_fd, -1
|
|
270
|
+
root_descriptor, self._root_fd = self._root_fd, -1
|
|
271
|
+
os.close(artifact_descriptor)
|
|
272
|
+
os.close(root_descriptor)
|
|
273
|
+
raise _error("VISUAL_RUN_STORE_IDENTITY", "artifact root must be a real directory")
|
|
274
|
+
self.log_path = self.root / LOG_NAME
|
|
275
|
+
self.lock_path = self.root / LOCK_NAME
|
|
276
|
+
|
|
277
|
+
def close(self) -> None:
|
|
278
|
+
"""Release the pinned directory descriptors; repeated calls are harmless."""
|
|
279
|
+
|
|
280
|
+
artifact_fd = getattr(self, "_artifact_fd", -1)
|
|
281
|
+
root_fd = getattr(self, "_root_fd", -1)
|
|
282
|
+
self._artifact_fd = -1
|
|
283
|
+
self._root_fd = -1
|
|
284
|
+
if artifact_fd >= 0:
|
|
285
|
+
os.close(artifact_fd)
|
|
286
|
+
if root_fd >= 0:
|
|
287
|
+
os.close(root_fd)
|
|
288
|
+
|
|
289
|
+
def __del__(self) -> None:
|
|
290
|
+
try:
|
|
291
|
+
self.close()
|
|
292
|
+
except OSError:
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
def _open_log(self, flags: int, *, create: bool = False) -> tuple[int, bool]:
|
|
296
|
+
common = flags | os.O_CLOEXEC | os.O_NOFOLLOW
|
|
297
|
+
if not create:
|
|
298
|
+
descriptor = os.open(LOG_NAME, common, dir_fd=self._root_fd)
|
|
299
|
+
try:
|
|
300
|
+
_regular_descriptor(descriptor, LOG_NAME)
|
|
301
|
+
except BaseException:
|
|
302
|
+
os.close(descriptor)
|
|
303
|
+
raise
|
|
304
|
+
return descriptor, False
|
|
305
|
+
try:
|
|
306
|
+
descriptor = os.open(
|
|
307
|
+
LOG_NAME,
|
|
308
|
+
common | os.O_CREAT | os.O_EXCL,
|
|
309
|
+
0o600,
|
|
310
|
+
dir_fd=self._root_fd,
|
|
311
|
+
)
|
|
312
|
+
try:
|
|
313
|
+
_regular_descriptor(descriptor, LOG_NAME)
|
|
314
|
+
except BaseException:
|
|
315
|
+
os.close(descriptor)
|
|
316
|
+
raise
|
|
317
|
+
return descriptor, True
|
|
318
|
+
except FileExistsError:
|
|
319
|
+
descriptor = os.open(LOG_NAME, common, dir_fd=self._root_fd)
|
|
320
|
+
try:
|
|
321
|
+
_regular_descriptor(descriptor, LOG_NAME)
|
|
322
|
+
except BaseException:
|
|
323
|
+
os.close(descriptor)
|
|
324
|
+
raise
|
|
325
|
+
return descriptor, False
|
|
326
|
+
|
|
327
|
+
def _parse_events(self, raw: bytes) -> tuple[VisualRunEvent, ...]:
|
|
328
|
+
if raw and not raw.endswith(b"\n"):
|
|
329
|
+
raise _error("VISUAL_RUN_LOG_TRUNCATED", "visual run log has a partial final record")
|
|
330
|
+
lines = raw.splitlines()
|
|
331
|
+
if len(lines) > MAX_EVENTS:
|
|
332
|
+
raise _error("VISUAL_RUN_LOG_LIMIT", "visual run log exceeds its event ceiling")
|
|
333
|
+
events: list[VisualRunEvent] = []
|
|
334
|
+
seen_keys: dict[str, bytes] = {}
|
|
335
|
+
for index, line in enumerate(lines, 1):
|
|
336
|
+
event = parse_event(line)
|
|
337
|
+
if event.run_id != self.run_id:
|
|
338
|
+
raise _error("VISUAL_RUN_RUN_MISMATCH", "event belongs to another run")
|
|
339
|
+
if event.sequence != index:
|
|
340
|
+
raise _error("VISUAL_RUN_SEQUENCE_GAP", "event sequence is not contiguous")
|
|
341
|
+
encoded = canonical_draft_bytes(VisualRunDraft.from_event(event))
|
|
342
|
+
previous = seen_keys.setdefault(event.idempotency_key, encoded)
|
|
343
|
+
if previous != encoded:
|
|
344
|
+
raise _error("VISUAL_RUN_IDEMPOTENCY_CONFLICT", "idempotency key was reused")
|
|
345
|
+
events.append(event)
|
|
346
|
+
return tuple(events)
|
|
347
|
+
|
|
348
|
+
def _events_from_descriptor(self, descriptor: int) -> tuple[VisualRunEvent, ...]:
|
|
349
|
+
_regular_descriptor(descriptor, LOG_NAME)
|
|
350
|
+
raw = _read_descriptor(
|
|
351
|
+
descriptor,
|
|
352
|
+
maximum=MAX_LOG_BYTES,
|
|
353
|
+
code="VISUAL_RUN_LOG_LIMIT",
|
|
354
|
+
message="visual run log exceeds its byte ceiling",
|
|
355
|
+
)
|
|
356
|
+
return self._parse_events(raw)
|
|
357
|
+
|
|
358
|
+
def _read_events(self) -> tuple[VisualRunEvent, ...]:
|
|
359
|
+
try:
|
|
360
|
+
descriptor, _ = self._open_log(os.O_RDONLY)
|
|
361
|
+
except FileNotFoundError:
|
|
362
|
+
return ()
|
|
363
|
+
try:
|
|
364
|
+
return self._events_from_descriptor(descriptor)
|
|
365
|
+
finally:
|
|
366
|
+
os.close(descriptor)
|
|
367
|
+
|
|
368
|
+
def events(self) -> tuple[VisualRunEvent, ...]:
|
|
369
|
+
with _locked(self._root_fd):
|
|
370
|
+
return self._read_events()
|
|
371
|
+
|
|
372
|
+
def event_for_key(self, key: str) -> VisualRunEvent | None:
|
|
373
|
+
"""Return the durable result for an idempotent command key, when present."""
|
|
374
|
+
|
|
375
|
+
with _locked(self._root_fd):
|
|
376
|
+
return next(
|
|
377
|
+
(event for event in self._read_events() if event.idempotency_key == key), None
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def append(self, draft: VisualRunDraft) -> VisualRunEvent:
|
|
381
|
+
if draft.run_id != self.run_id:
|
|
382
|
+
raise _error("VISUAL_RUN_RUN_MISMATCH", "draft belongs to another run")
|
|
383
|
+
# Validate the complete draft byte bound before comparing its command identity.
|
|
384
|
+
canonical_draft_bytes(draft)
|
|
385
|
+
command_bytes = _command_bytes(draft)
|
|
386
|
+
with _locked(self._root_fd):
|
|
387
|
+
descriptor, created = self._open_log(os.O_RDWR | os.O_APPEND, create=True)
|
|
388
|
+
try:
|
|
389
|
+
existing = self._events_from_descriptor(descriptor)
|
|
390
|
+
for event in existing:
|
|
391
|
+
if event.idempotency_key != draft.idempotency_key:
|
|
392
|
+
continue
|
|
393
|
+
if _command_bytes(VisualRunDraft.from_event(event)) == command_bytes:
|
|
394
|
+
return event
|
|
395
|
+
raise _error("VISUAL_RUN_IDEMPOTENCY_CONFLICT", "idempotency key was reused")
|
|
396
|
+
projection_reserved = draft.object_id == DOCUMENT_HEADER_CELL_ID or (
|
|
397
|
+
draft.object_id == LEGACY_DOCUMENT_HEADER_CELL_ID_PREFIX
|
|
398
|
+
or draft.object_id.startswith(f"{LEGACY_DOCUMENT_HEADER_CELL_ID_PREFIX}-")
|
|
399
|
+
)
|
|
400
|
+
if draft.event_type == "cell.created" and projection_reserved:
|
|
401
|
+
raise _error(
|
|
402
|
+
"VISUAL_RUN_RESERVED_CELL_ID",
|
|
403
|
+
"cell id is reserved for projected document structure",
|
|
404
|
+
)
|
|
405
|
+
if len(existing) >= MAX_EVENTS:
|
|
406
|
+
raise _error("VISUAL_RUN_LOG_LIMIT", "visual run log exceeds its event ceiling")
|
|
407
|
+
event = draft.with_sequence(len(existing) + 1)
|
|
408
|
+
# Syntax-valid is not enough: invalid transitions never enter the durable log.
|
|
409
|
+
from mostlyright.data_harness.visual_run.materialize import notebook_bytes
|
|
410
|
+
from mostlyright.data_harness.visual_run.reducer import apply_event, reduce_events
|
|
411
|
+
|
|
412
|
+
before = reduce_events(existing)
|
|
413
|
+
prospective = apply_event(before, event)
|
|
414
|
+
prospective_cell = prospective.cells.get(event.object_id)
|
|
415
|
+
if (
|
|
416
|
+
event.actor == "agent"
|
|
417
|
+
and event.event_type
|
|
418
|
+
in {"cell.source_extended", "cell.source_revised", "cell.completed"}
|
|
419
|
+
and prospective_cell is not None
|
|
420
|
+
and prospective_cell.cell_type == "markdown"
|
|
421
|
+
):
|
|
422
|
+
from mostlyright.data_harness.visual_run.authoring import (
|
|
423
|
+
validate_agent_authoring_event,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
validate_agent_authoring_event(before, event, prospective, self.read_artifact)
|
|
427
|
+
try:
|
|
428
|
+
notebook_bytes(prospective, self.read_artifact)
|
|
429
|
+
except ValueError as exc:
|
|
430
|
+
raise _error(
|
|
431
|
+
"VISUAL_RUN_PROJECTION_LIMIT",
|
|
432
|
+
"event would exceed the materialized research notebook limit",
|
|
433
|
+
) from exc
|
|
434
|
+
encoded = canonical_event_bytes(event)
|
|
435
|
+
current_size = os.fstat(descriptor).st_size
|
|
436
|
+
if current_size + len(encoded) > MAX_LOG_BYTES:
|
|
437
|
+
raise _error("VISUAL_RUN_LOG_LIMIT", "visual run log exceeds its byte ceiling")
|
|
438
|
+
written = 0
|
|
439
|
+
while written < len(encoded):
|
|
440
|
+
progress = os.write(descriptor, encoded[written:])
|
|
441
|
+
if progress <= 0:
|
|
442
|
+
raise _error("VISUAL_RUN_LOG_WRITE", "event log write made no progress")
|
|
443
|
+
written += progress
|
|
444
|
+
os.fsync(descriptor)
|
|
445
|
+
if created:
|
|
446
|
+
os.fsync(self._root_fd)
|
|
447
|
+
return event
|
|
448
|
+
finally:
|
|
449
|
+
os.close(descriptor)
|
|
450
|
+
|
|
451
|
+
def page_after(self, cursor: int, *, limit: int = 256) -> EventPage:
|
|
452
|
+
if isinstance(cursor, bool) or not isinstance(cursor, int) or cursor < 0:
|
|
453
|
+
raise _error("VISUAL_RUN_CURSOR_INVALID", "cursor must be a non-negative integer")
|
|
454
|
+
if isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= 1024:
|
|
455
|
+
raise _error("VISUAL_RUN_CURSOR_INVALID", "limit is outside the closed range")
|
|
456
|
+
events = self.events()
|
|
457
|
+
if cursor > len(events):
|
|
458
|
+
raise _error("VISUAL_RUN_CURSOR_FUTURE", "cursor is beyond the durable log")
|
|
459
|
+
selected = events[cursor : cursor + limit]
|
|
460
|
+
next_cursor = cursor + len(selected)
|
|
461
|
+
return EventPage(selected, next_cursor, next_cursor < len(events))
|
|
462
|
+
|
|
463
|
+
def install_artifact(self, reference: ArtifactReference, content: bytes) -> Path:
|
|
464
|
+
if not isinstance(content, bytes):
|
|
465
|
+
raise _error("VISUAL_RUN_ARTIFACT_INVALID", "artifact content must be bytes")
|
|
466
|
+
if len(content) != reference.byte_size:
|
|
467
|
+
raise _error("VISUAL_RUN_ARTIFACT_SIZE", "artifact byte size differs from reference")
|
|
468
|
+
if hashlib.sha256(content).hexdigest() != reference.sha256:
|
|
469
|
+
raise _error("VISUAL_RUN_ARTIFACT_DIGEST", "artifact digest differs from reference")
|
|
470
|
+
target = self.artifact_root / reference.sha256
|
|
471
|
+
with _locked(self._root_fd):
|
|
472
|
+
try:
|
|
473
|
+
existing_fd = os.open(
|
|
474
|
+
reference.sha256,
|
|
475
|
+
os.O_RDONLY | os.O_CLOEXEC | os.O_NOFOLLOW,
|
|
476
|
+
dir_fd=self._artifact_fd,
|
|
477
|
+
)
|
|
478
|
+
except FileNotFoundError:
|
|
479
|
+
existing_fd = -1
|
|
480
|
+
if existing_fd >= 0:
|
|
481
|
+
try:
|
|
482
|
+
_regular_descriptor(existing_fd, reference.sha256)
|
|
483
|
+
existing = _read_descriptor(
|
|
484
|
+
existing_fd,
|
|
485
|
+
maximum=reference.byte_size,
|
|
486
|
+
code="VISUAL_RUN_ARTIFACT_SIZE",
|
|
487
|
+
message="installed artifact size differs",
|
|
488
|
+
)
|
|
489
|
+
finally:
|
|
490
|
+
os.close(existing_fd)
|
|
491
|
+
if existing != content:
|
|
492
|
+
raise _error("VISUAL_RUN_ARTIFACT_CONFLICT", "artifact digest path conflicts")
|
|
493
|
+
return target
|
|
494
|
+
temporary = f".{reference.sha256}.{secrets.token_hex(12)}.tmp"
|
|
495
|
+
descriptor = os.open(
|
|
496
|
+
temporary,
|
|
497
|
+
os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | os.O_NOFOLLOW,
|
|
498
|
+
0o400,
|
|
499
|
+
dir_fd=self._artifact_fd,
|
|
500
|
+
)
|
|
501
|
+
try:
|
|
502
|
+
try:
|
|
503
|
+
written = 0
|
|
504
|
+
while written < len(content):
|
|
505
|
+
progress = os.write(descriptor, content[written:])
|
|
506
|
+
if progress <= 0:
|
|
507
|
+
raise _error(
|
|
508
|
+
"VISUAL_RUN_ARTIFACT_WRITE", "artifact write made no progress"
|
|
509
|
+
)
|
|
510
|
+
written += progress
|
|
511
|
+
os.fsync(descriptor)
|
|
512
|
+
installed = _read_descriptor(
|
|
513
|
+
descriptor,
|
|
514
|
+
maximum=reference.byte_size,
|
|
515
|
+
code="VISUAL_RUN_ARTIFACT_SIZE",
|
|
516
|
+
message="staged artifact size differs",
|
|
517
|
+
)
|
|
518
|
+
if installed != content:
|
|
519
|
+
raise _error("VISUAL_RUN_ARTIFACT_DIGEST", "staged artifact differs")
|
|
520
|
+
finally:
|
|
521
|
+
os.close(descriptor)
|
|
522
|
+
except BaseException:
|
|
523
|
+
try:
|
|
524
|
+
os.unlink(temporary, dir_fd=self._artifact_fd)
|
|
525
|
+
except FileNotFoundError:
|
|
526
|
+
pass
|
|
527
|
+
raise
|
|
528
|
+
try:
|
|
529
|
+
os.replace(
|
|
530
|
+
temporary,
|
|
531
|
+
reference.sha256,
|
|
532
|
+
src_dir_fd=self._artifact_fd,
|
|
533
|
+
dst_dir_fd=self._artifact_fd,
|
|
534
|
+
)
|
|
535
|
+
os.fsync(self._artifact_fd)
|
|
536
|
+
except BaseException:
|
|
537
|
+
try:
|
|
538
|
+
os.unlink(temporary, dir_fd=self._artifact_fd)
|
|
539
|
+
except FileNotFoundError:
|
|
540
|
+
pass
|
|
541
|
+
raise
|
|
542
|
+
return target
|
|
543
|
+
|
|
544
|
+
def read_artifact(self, reference: ArtifactReference) -> bytes:
|
|
545
|
+
try:
|
|
546
|
+
descriptor = os.open(
|
|
547
|
+
reference.sha256,
|
|
548
|
+
os.O_RDONLY | os.O_CLOEXEC | os.O_NOFOLLOW,
|
|
549
|
+
dir_fd=self._artifact_fd,
|
|
550
|
+
)
|
|
551
|
+
except FileNotFoundError as exc:
|
|
552
|
+
raise _error("VISUAL_RUN_ARTIFACT_MISSING", "artifact is not installed") from exc
|
|
553
|
+
try:
|
|
554
|
+
_regular_descriptor(descriptor, reference.sha256)
|
|
555
|
+
content = _read_descriptor(
|
|
556
|
+
descriptor,
|
|
557
|
+
maximum=reference.byte_size,
|
|
558
|
+
code="VISUAL_RUN_ARTIFACT_SIZE",
|
|
559
|
+
message="installed artifact size differs",
|
|
560
|
+
)
|
|
561
|
+
finally:
|
|
562
|
+
os.close(descriptor)
|
|
563
|
+
if len(content) != reference.byte_size:
|
|
564
|
+
raise _error("VISUAL_RUN_ARTIFACT_SIZE", "installed artifact size differs")
|
|
565
|
+
if hashlib.sha256(content).hexdigest() != reference.sha256:
|
|
566
|
+
raise _error("VISUAL_RUN_ARTIFACT_DIGEST", "installed artifact digest differs")
|
|
567
|
+
return content
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def snapshot_json(page: EventPage) -> bytes:
|
|
571
|
+
return (
|
|
572
|
+
json.dumps(
|
|
573
|
+
{
|
|
574
|
+
"cursor": page.cursor,
|
|
575
|
+
"events": [event.to_mapping() for event in page.events],
|
|
576
|
+
"truncated": page.truncated,
|
|
577
|
+
},
|
|
578
|
+
ensure_ascii=False,
|
|
579
|
+
allow_nan=False,
|
|
580
|
+
sort_keys=True,
|
|
581
|
+
separators=(",", ":"),
|
|
582
|
+
)
|
|
583
|
+
+ "\n"
|
|
584
|
+
).encode("utf-8")
|