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,542 @@
|
|
|
1
|
+
"""The contract every Reader family implements, and the typed failures it may raise.
|
|
2
|
+
|
|
3
|
+
A Reader family is the code that turns fetched bytes into rows. This module states the
|
|
4
|
+
shape of that hand-off and nothing else: it decodes nothing, registers nothing, and reaches
|
|
5
|
+
no filesystem. Four things live here.
|
|
6
|
+
|
|
7
|
+
``ReaderError``
|
|
8
|
+
The one failure type, carrying a stable machine-readable code, the subject that failed,
|
|
9
|
+
and a human detail. The codes are a closed table, ``READER_ERROR_CODES``, validated at
|
|
10
|
+
construction so a typo becomes a failure instead of an unmapped error string. The table is
|
|
11
|
+
the interface consumed by error remediation, so codes are added here and nowhere else.
|
|
12
|
+
|
|
13
|
+
``ReaderPin``
|
|
14
|
+
The three fields a recipe seals to say which code decoded its bytes and with which
|
|
15
|
+
settings. The pin is all-present or all-absent, never half-stated, and its
|
|
16
|
+
``options_digest`` is computed exactly the way the recipe computes its own
|
|
17
|
+
``decode_options_digest``: sha256 over ``canonical_json_bytes`` of the options mapping.
|
|
18
|
+
It is never computed a second way.
|
|
19
|
+
|
|
20
|
+
``ReaderBudgets``
|
|
21
|
+
The resource ceilings a family decodes under. ``narrowed_by`` takes a per-field minimum
|
|
22
|
+
against a caps mapping, so a recipe may narrow a budget and may never widen one. The
|
|
23
|
+
caps mapping is accepted as a plain mapping rather than the recipe's own typed object,
|
|
24
|
+
because importing ``recipe`` here would close an import cycle.
|
|
25
|
+
|
|
26
|
+
``ReaderResult`` and ``ReaderFamily``
|
|
27
|
+
What a decode returns and what a family must offer. A decode receives exact bytes and
|
|
28
|
+
returns exact bytes and touches no filesystem: no temporary directory, no extraction, no
|
|
29
|
+
spill.
|
|
30
|
+
|
|
31
|
+
Decode options reach this module already screened for secrets and already bounded by the
|
|
32
|
+
recipe layer. This module states that dependency rather than duplicating the screen; a
|
|
33
|
+
second, differently-worded screen would be a second declaration of the same rule.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import re
|
|
39
|
+
from collections.abc import Mapping
|
|
40
|
+
from dataclasses import dataclass, fields
|
|
41
|
+
from types import MappingProxyType
|
|
42
|
+
from typing import Any, Protocol
|
|
43
|
+
|
|
44
|
+
from mostlyright.data_harness.canonical import (
|
|
45
|
+
CanonicalJSONError,
|
|
46
|
+
canonical_json_bytes,
|
|
47
|
+
sha256_bytes,
|
|
48
|
+
)
|
|
49
|
+
from mostlyright.data_harness.formats import (
|
|
50
|
+
DATA_FORMATS,
|
|
51
|
+
FORMAT_MEDIA_TYPES,
|
|
52
|
+
READER_CONTRACT_VERSION,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"DECODE_FLAGS",
|
|
57
|
+
"READER_CONTRACT_VERSION",
|
|
58
|
+
"READER_ERROR_CODES",
|
|
59
|
+
"ReaderBudgets",
|
|
60
|
+
"ReaderError",
|
|
61
|
+
"ReaderFamily",
|
|
62
|
+
"ReaderPin",
|
|
63
|
+
"ReaderResult",
|
|
64
|
+
"identifier",
|
|
65
|
+
"semver",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
# Error remediation consumes this closed table. Add new codes here, not at individual raise sites.
|
|
69
|
+
READER_ERROR_CODES: frozenset[str] = frozenset(
|
|
70
|
+
{
|
|
71
|
+
# Resolution and contract shape.
|
|
72
|
+
"READER_FAMILY_UNKNOWN",
|
|
73
|
+
"READER_DEPENDENCY_UNAVAILABLE",
|
|
74
|
+
"READER_CONTRACT",
|
|
75
|
+
"READER_OPTIONS",
|
|
76
|
+
"READER_ADMISSION",
|
|
77
|
+
"READER_BUDGET",
|
|
78
|
+
# Container members.
|
|
79
|
+
"READER_MEMBER_MISSING",
|
|
80
|
+
"READER_MEMBER_AMBIGUOUS",
|
|
81
|
+
"READER_MEMBER_TYPE",
|
|
82
|
+
"READER_NESTED_CONTAINER",
|
|
83
|
+
# Spreadsheet coordinates.
|
|
84
|
+
"READER_SHEET_MISSING",
|
|
85
|
+
"READER_RANGE",
|
|
86
|
+
# Weather-model admission, refused before decode.
|
|
87
|
+
"READER_GRID_UNKNOWN",
|
|
88
|
+
"READER_PACKING_UNKNOWN",
|
|
89
|
+
# Bytes a family admitted and then could not decode: truncated, corrupted in transit, or
|
|
90
|
+
# damaged at the publisher. It is separate from the admission codes on purpose, because
|
|
91
|
+
# the fix is different in kind: an admission refusal is repaired by editing the recipe or
|
|
92
|
+
# by widening a reviewed allowlist, and this one is repaired by fetching the file again.
|
|
93
|
+
# A family whose decoder reports its own failures translates them into this code rather
|
|
94
|
+
# than letting the decoder's exception type escape, since an untyped failure carries no
|
|
95
|
+
# code for the remediation map to bind to.
|
|
96
|
+
"READER_DECODE",
|
|
97
|
+
# Output and the pre-refresh warm-up check.
|
|
98
|
+
"READER_OUTPUT",
|
|
99
|
+
"READER_WARMUP",
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# The closed vocabulary of facts a decode may report about itself. A flag is a fact the
|
|
104
|
+
# receipt carries forward, not a warning: "this spreadsheet held formulas, and the values
|
|
105
|
+
# read were the ones the authoring application had cached" is a provenance statement a
|
|
106
|
+
# reader of the dataset is owed. Flags are sorted and de-duplicated on the result, so two
|
|
107
|
+
# runs of the same decode produce the same tuple and the same receipt bytes.
|
|
108
|
+
#
|
|
109
|
+
# ``contains_masked_points`` is the weather family's: a message may carry a bitmap marking points
|
|
110
|
+
# that hold no value, and a point the recipe named which the message marked absent becomes an
|
|
111
|
+
# empty cell. Without the flag that empty cell is ambiguous -- it reads the same as a Reader that
|
|
112
|
+
# had a problem -- and with it the receipt states that the publisher marked the point absent.
|
|
113
|
+
DECODE_FLAGS: frozenset[str] = frozenset(
|
|
114
|
+
{"contains_formula_cells", "contains_cached_values", "contains_masked_points"}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Identity and version rules, spelled the way the recipe spells them. They are duplicated
|
|
118
|
+
# rather than imported because importing ``recipe`` here would close an import cycle; a test
|
|
119
|
+
# asserts the two patterns letter for letter so the duplication cannot drift.
|
|
120
|
+
_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$")
|
|
121
|
+
_SEMVER = re.compile(r"^[1-9][0-9]*\.[0-9]+\.[0-9]+$")
|
|
122
|
+
|
|
123
|
+
# The same ceiling the recipe applies to a sealed decode-options object.
|
|
124
|
+
_MAX_DECODE_OPTIONS_BYTES = 256 * 1024
|
|
125
|
+
|
|
126
|
+
# A sealed snapshot filename is a bare name in a coordinator-owned directory: no separator,
|
|
127
|
+
# no parent reference, no leading dot.
|
|
128
|
+
_FILENAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,254}$")
|
|
129
|
+
|
|
130
|
+
# Nesting depth is fixed rather than tunable. A budget that could ask for depth 2 would be
|
|
131
|
+
# the archive-in-archive bug class re-entering by configuration, so the only admissible
|
|
132
|
+
# value is 1 and any other value is refused where the budget is built.
|
|
133
|
+
_FIXED_NESTING_DEPTH = 1
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# The attributes the interpreter owns rather than this class. Sealing them seals nothing: an
|
|
137
|
+
# exception whose traceback cannot be set is not immutable, it is broken. ``contextlib`` and
|
|
138
|
+
# the exception-chaining machinery assign ``__traceback__`` at Python level, so a Reader
|
|
139
|
+
# refusal crossing a context manager would surface as ``AttributeError: cannot set
|
|
140
|
+
# '__traceback__'`` instead of as its own code -- the loudest failure replaced by a confusing
|
|
141
|
+
# one. What must not be rewritable is the code, the subject, and the detail, and those stay
|
|
142
|
+
# sealed exactly as they were.
|
|
143
|
+
_INTERPRETER_OWNED_ATTRIBUTES = frozenset(
|
|
144
|
+
{"__traceback__", "__cause__", "__context__", "__suppress_context__", "__notes__"}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ReaderError(ValueError):
|
|
149
|
+
"""One stable, fail-closed Reader failure: a code, the subject, and a detail.
|
|
150
|
+
|
|
151
|
+
The three-part shape mirrors the source-contract error the acquisition layer already
|
|
152
|
+
raises, so a caller handling one handles the other the same way. Instances are
|
|
153
|
+
immutable once constructed: an error that a handler could rewrite on its way up is an
|
|
154
|
+
error whose code no longer means anything.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(self, code: str, subject: str, detail: str) -> None:
|
|
158
|
+
if not isinstance(code, str) or code not in READER_ERROR_CODES:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"unknown Reader error code {code!r}; add it to READER_ERROR_CODES so "
|
|
161
|
+
"error remediation can handle it"
|
|
162
|
+
)
|
|
163
|
+
super().__init__(f"{subject}: {detail} [{code}]")
|
|
164
|
+
self.__dict__["code"] = code
|
|
165
|
+
self.__dict__["subject"] = subject
|
|
166
|
+
self.__dict__["detail"] = detail
|
|
167
|
+
self.__dict__["_sealed"] = True
|
|
168
|
+
|
|
169
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
170
|
+
if self.__dict__.get("_sealed") and name not in _INTERPRETER_OWNED_ATTRIBUTES:
|
|
171
|
+
raise AttributeError(f"ReaderError is immutable; cannot set {name!r}")
|
|
172
|
+
super().__setattr__(name, value)
|
|
173
|
+
|
|
174
|
+
def __delattr__(self, name: str) -> None:
|
|
175
|
+
raise AttributeError(f"ReaderError is immutable; cannot delete {name!r}")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def identifier(value: Any, subject: str) -> str:
|
|
179
|
+
"""Admit a canonical identifier, on the recipe's own rule.
|
|
180
|
+
|
|
181
|
+
Public because the registry validates a family's declared identity against exactly
|
|
182
|
+
this rule. One rule, one implementation, so a family that could be registered can
|
|
183
|
+
always be pinned and a family that can be pinned can always be registered.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
if not isinstance(value, str) or _IDENTIFIER.fullmatch(value) is None:
|
|
187
|
+
raise ReaderError("READER_CONTRACT", subject, "must be a canonical identifier")
|
|
188
|
+
return value
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def semver(value: Any, subject: str) -> str:
|
|
192
|
+
"""Admit a canonical positive semver, on the recipe's own rule."""
|
|
193
|
+
|
|
194
|
+
if not isinstance(value, str) or _SEMVER.fullmatch(value) is None:
|
|
195
|
+
raise ReaderError("READER_CONTRACT", subject, "must be a canonical positive semver")
|
|
196
|
+
return value
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _strict_json_object(value: Any, subject: str) -> dict[str, Any]:
|
|
200
|
+
"""Admit a bounded, canonicalisable JSON object on the recipe's own terms."""
|
|
201
|
+
|
|
202
|
+
if not isinstance(value, Mapping):
|
|
203
|
+
raise ReaderError("READER_OPTIONS", subject, "must be an object")
|
|
204
|
+
try:
|
|
205
|
+
raw = canonical_json_bytes(dict(value))
|
|
206
|
+
except CanonicalJSONError as error:
|
|
207
|
+
raise ReaderError(
|
|
208
|
+
"READER_OPTIONS",
|
|
209
|
+
subject,
|
|
210
|
+
"contains values that cannot be sealed canonically",
|
|
211
|
+
) from error
|
|
212
|
+
if len(raw) > _MAX_DECODE_OPTIONS_BYTES:
|
|
213
|
+
raise ReaderError("READER_OPTIONS", subject, "exceeds 256 KiB")
|
|
214
|
+
return dict(value)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass(frozen=True)
|
|
218
|
+
class ReaderPin:
|
|
219
|
+
"""The exact decoder a recipe sealed, and the exact settings it ran with.
|
|
220
|
+
|
|
221
|
+
``family_id`` and ``family_version`` resolve one entry of the Toolbox on an exact key.
|
|
222
|
+
``decode_options`` are the family's own settings, sealed verbatim, so replaying the
|
|
223
|
+
recipe replays the decode. Every knob is inside ``options_digest``: changing one
|
|
224
|
+
changes the recipe digest and invalidates an approval granted against the old settings.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
family_id: str
|
|
228
|
+
family_version: str
|
|
229
|
+
decode_options: Mapping[str, Any]
|
|
230
|
+
|
|
231
|
+
def __post_init__(self) -> None:
|
|
232
|
+
identifier(self.family_id, "reader.pin.family_id")
|
|
233
|
+
semver(self.family_version, "reader.pin.family_version")
|
|
234
|
+
object.__setattr__(
|
|
235
|
+
self,
|
|
236
|
+
"decode_options",
|
|
237
|
+
MappingProxyType(_strict_json_object(self.decode_options, "reader.pin.decode_options")),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
@classmethod
|
|
241
|
+
def from_recipe_fields(
|
|
242
|
+
cls,
|
|
243
|
+
family_id: str | None,
|
|
244
|
+
family_version: str | None,
|
|
245
|
+
decode_options: Mapping[str, Any] | None,
|
|
246
|
+
) -> ReaderPin | None:
|
|
247
|
+
"""Build the pin from a recipe source's three fields, or None when none are stated.
|
|
248
|
+
|
|
249
|
+
All three present or all three absent is the only admissible shape. A source that
|
|
250
|
+
names a decoder without pinning its settings, or pins settings without naming a
|
|
251
|
+
decoder, could not be replayed from its own record. Absent means today's direct
|
|
252
|
+
fetch, where no Reader family decoded anything. This is the one construction path,
|
|
253
|
+
so no caller has to re-derive the rule.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
present = tuple(item is not None for item in (family_id, family_version, decode_options))
|
|
257
|
+
if not any(present):
|
|
258
|
+
return None
|
|
259
|
+
if not all(present):
|
|
260
|
+
raise ReaderError(
|
|
261
|
+
"READER_CONTRACT",
|
|
262
|
+
"reader.pin",
|
|
263
|
+
"family_id, family_version, and decode_options are all present or all absent",
|
|
264
|
+
)
|
|
265
|
+
assert family_id is not None and family_version is not None
|
|
266
|
+
assert decode_options is not None
|
|
267
|
+
return cls(family_id, family_version, decode_options)
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def options_digest(self) -> str:
|
|
271
|
+
"""Digest over the exact decode settings, computed the recipe's one way."""
|
|
272
|
+
|
|
273
|
+
return sha256_bytes(canonical_json_bytes(dict(self.decode_options)))
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def coordinate(self) -> str:
|
|
277
|
+
"""The ``id@version`` coordinate, for a refusal message a person can act on."""
|
|
278
|
+
|
|
279
|
+
return f"{self.family_id}@{self.family_version}"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass(frozen=True)
|
|
283
|
+
class ReaderBudgets:
|
|
284
|
+
"""The ceilings one decode runs under. A recipe may narrow them; never widen them.
|
|
285
|
+
|
|
286
|
+
The defaults are the family-level defaults that apply when a recipe states no caps.
|
|
287
|
+
``narrowed_by`` is per-field minimum only, which is what makes a stated cap unable to
|
|
288
|
+
buy more room than the family already allows.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
max_input_bytes: int = 16 * 1024 * 1024
|
|
292
|
+
max_output_bytes: int = 16 * 1024 * 1024
|
|
293
|
+
max_uncompressed_bytes: int = 128 * 1024 * 1024
|
|
294
|
+
max_expansion_ratio: int = 100
|
|
295
|
+
max_declared_cells: int = 10_000_000
|
|
296
|
+
max_container_members: int = 1_024
|
|
297
|
+
max_nesting_depth: int = _FIXED_NESTING_DEPTH
|
|
298
|
+
max_rows: int = 1_000_000
|
|
299
|
+
max_columns: int = 1_024
|
|
300
|
+
|
|
301
|
+
def __post_init__(self) -> None:
|
|
302
|
+
for item in fields(self):
|
|
303
|
+
value = getattr(self, item.name)
|
|
304
|
+
if type(value) is not int or value < 1:
|
|
305
|
+
raise ReaderError(
|
|
306
|
+
"READER_BUDGET",
|
|
307
|
+
f"reader.budgets.{item.name}",
|
|
308
|
+
"must be a positive integer",
|
|
309
|
+
)
|
|
310
|
+
if self.max_nesting_depth != _FIXED_NESTING_DEPTH:
|
|
311
|
+
raise ReaderError(
|
|
312
|
+
"READER_BUDGET",
|
|
313
|
+
"reader.budgets.max_nesting_depth",
|
|
314
|
+
"must be exactly 1: nesting depth is a security invariant, not a budget",
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def narrowed_by(self, caps: Mapping[str, Any] | None) -> ReaderBudgets:
|
|
318
|
+
"""Return budgets no wider than these on any field.
|
|
319
|
+
|
|
320
|
+
A cap naming a field this contract does not have is refused by name rather than
|
|
321
|
+
ignored, because a silently ignored cap is a budget a recipe believes it set.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
if caps is None:
|
|
325
|
+
return self
|
|
326
|
+
if not isinstance(caps, Mapping):
|
|
327
|
+
raise ReaderError("READER_BUDGET", "reader.budgets.caps", "must be a mapping or absent")
|
|
328
|
+
known = {item.name for item in fields(self)}
|
|
329
|
+
unknown = sorted(str(key) for key in caps if key not in known)
|
|
330
|
+
if unknown:
|
|
331
|
+
raise ReaderError(
|
|
332
|
+
"READER_BUDGET",
|
|
333
|
+
"reader.budgets.caps",
|
|
334
|
+
f"names no such budget: {', '.join(unknown)}",
|
|
335
|
+
)
|
|
336
|
+
narrowed: dict[str, int] = {}
|
|
337
|
+
for name in known:
|
|
338
|
+
current = getattr(self, name)
|
|
339
|
+
if name not in caps:
|
|
340
|
+
narrowed[name] = current
|
|
341
|
+
continue
|
|
342
|
+
requested = caps[name]
|
|
343
|
+
if type(requested) is not int or requested < 1:
|
|
344
|
+
raise ReaderError(
|
|
345
|
+
"READER_BUDGET",
|
|
346
|
+
f"reader.budgets.caps.{name}",
|
|
347
|
+
"must be a positive integer",
|
|
348
|
+
)
|
|
349
|
+
narrowed[name] = min(current, requested)
|
|
350
|
+
return ReaderBudgets(**narrowed)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def bulk_default_budgets() -> ReaderBudgets:
|
|
354
|
+
"""Family defaults for the bulk-capable text and container families.
|
|
355
|
+
|
|
356
|
+
Sized for EPA-scale public sources under the raised hosted Courier caps: a 256 MiB
|
|
357
|
+
fetched archive whose decode output may reach the 4 GiB normalized budget. The
|
|
358
|
+
uncompressed ceiling rises with the output budget because a container's declared
|
|
359
|
+
member size is checked against it before extraction. Spreadsheet and GRIB families
|
|
360
|
+
keep their own, tighter certified defaults.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
return ReaderBudgets(
|
|
364
|
+
max_input_bytes=268_435_456,
|
|
365
|
+
max_output_bytes=4_294_967_296,
|
|
366
|
+
max_uncompressed_bytes=4_294_967_296,
|
|
367
|
+
max_declared_cells=400_000_000,
|
|
368
|
+
max_rows=10_000_000,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@dataclass(frozen=True)
|
|
373
|
+
class ReaderResult:
|
|
374
|
+
"""What one decode produced: exact output bytes and the facts about producing them.
|
|
375
|
+
|
|
376
|
+
``content`` is already the canonical output encoding. The Reader owns the byte
|
|
377
|
+
stability of the sealed artifact, so the bytes here are the bytes that get sealed.
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
content: bytes
|
|
381
|
+
data_format: str
|
|
382
|
+
media_type: str
|
|
383
|
+
filename: str
|
|
384
|
+
row_count: int
|
|
385
|
+
column_names: tuple[str, ...]
|
|
386
|
+
declared_cell_count: int
|
|
387
|
+
flags: tuple[str, ...] = ()
|
|
388
|
+
|
|
389
|
+
def __post_init__(self) -> None:
|
|
390
|
+
if not isinstance(self.content, (bytes, bytearray)) or len(self.content) == 0:
|
|
391
|
+
raise ReaderError(
|
|
392
|
+
"READER_OUTPUT",
|
|
393
|
+
"reader.result.content",
|
|
394
|
+
"must be nonempty exact bytes",
|
|
395
|
+
)
|
|
396
|
+
object.__setattr__(self, "content", bytes(self.content))
|
|
397
|
+
if self.data_format not in DATA_FORMATS:
|
|
398
|
+
raise ReaderError(
|
|
399
|
+
"READER_OUTPUT",
|
|
400
|
+
"reader.result.data_format",
|
|
401
|
+
"is not a format the table admits",
|
|
402
|
+
)
|
|
403
|
+
if self.media_type not in FORMAT_MEDIA_TYPES[self.data_format]:
|
|
404
|
+
raise ReaderError(
|
|
405
|
+
"READER_OUTPUT",
|
|
406
|
+
"reader.result.media_type",
|
|
407
|
+
"is not a media type the table maps to this format",
|
|
408
|
+
)
|
|
409
|
+
if not isinstance(self.filename, str) or _FILENAME.fullmatch(self.filename) is None:
|
|
410
|
+
raise ReaderError(
|
|
411
|
+
"READER_OUTPUT",
|
|
412
|
+
"reader.result.filename",
|
|
413
|
+
"must be a confined bare filename",
|
|
414
|
+
)
|
|
415
|
+
if type(self.row_count) is not int or self.row_count < 0:
|
|
416
|
+
raise ReaderError(
|
|
417
|
+
"READER_OUTPUT",
|
|
418
|
+
"reader.result.row_count",
|
|
419
|
+
"must be a non-negative integer",
|
|
420
|
+
)
|
|
421
|
+
if not isinstance(self.column_names, tuple) or not self.column_names:
|
|
422
|
+
raise ReaderError(
|
|
423
|
+
"READER_OUTPUT",
|
|
424
|
+
"reader.result.column_names",
|
|
425
|
+
"must be a nonempty tuple",
|
|
426
|
+
)
|
|
427
|
+
for index, name in enumerate(self.column_names):
|
|
428
|
+
if not isinstance(name, str) or not name or "\x00" in name:
|
|
429
|
+
raise ReaderError(
|
|
430
|
+
"READER_OUTPUT",
|
|
431
|
+
f"reader.result.column_names[{index}]",
|
|
432
|
+
"must be nonempty text without a NUL byte",
|
|
433
|
+
)
|
|
434
|
+
if len(set(self.column_names)) != len(self.column_names):
|
|
435
|
+
raise ReaderError(
|
|
436
|
+
"READER_OUTPUT",
|
|
437
|
+
"reader.result.column_names",
|
|
438
|
+
"must not repeat a column name",
|
|
439
|
+
)
|
|
440
|
+
if type(
|
|
441
|
+
self.declared_cell_count
|
|
442
|
+
) is not int or self.declared_cell_count < self.row_count * len(self.column_names):
|
|
443
|
+
raise ReaderError(
|
|
444
|
+
"READER_OUTPUT",
|
|
445
|
+
"reader.result.declared_cell_count",
|
|
446
|
+
"must be an integer no smaller than the emitted table geometry",
|
|
447
|
+
)
|
|
448
|
+
if not isinstance(self.flags, tuple):
|
|
449
|
+
raise ReaderError("READER_OUTPUT", "reader.result.flags", "must be a tuple")
|
|
450
|
+
unknown = sorted(str(flag) for flag in self.flags if flag not in DECODE_FLAGS)
|
|
451
|
+
if unknown:
|
|
452
|
+
raise ReaderError(
|
|
453
|
+
"READER_OUTPUT",
|
|
454
|
+
"reader.result.flags",
|
|
455
|
+
f"names no such decode flag: {', '.join(unknown)}",
|
|
456
|
+
)
|
|
457
|
+
object.__setattr__(self, "flags", tuple(sorted(set(self.flags))))
|
|
458
|
+
|
|
459
|
+
def validate_for(self, output_format: str, budgets: ReaderBudgets) -> None:
|
|
460
|
+
"""Bind the result to the family that produced it and the budgets it ran under.
|
|
461
|
+
|
|
462
|
+
Kept separate from construction because a result does not know which family built
|
|
463
|
+
it, and a family's declared output format is the thing being checked.
|
|
464
|
+
"""
|
|
465
|
+
|
|
466
|
+
if self.data_format != output_format:
|
|
467
|
+
raise ReaderError(
|
|
468
|
+
"READER_OUTPUT",
|
|
469
|
+
"reader.result.data_format",
|
|
470
|
+
"differs from the format the family declares it emits",
|
|
471
|
+
)
|
|
472
|
+
if len(self.content) > budgets.max_output_bytes:
|
|
473
|
+
raise ReaderError(
|
|
474
|
+
"READER_BUDGET",
|
|
475
|
+
"reader.result.content",
|
|
476
|
+
"exceeds the output byte budget",
|
|
477
|
+
)
|
|
478
|
+
if self.row_count > budgets.max_rows:
|
|
479
|
+
raise ReaderError("READER_BUDGET", "reader.result.row_count", "exceeds the row budget")
|
|
480
|
+
if len(self.column_names) > budgets.max_columns:
|
|
481
|
+
raise ReaderError(
|
|
482
|
+
"READER_BUDGET",
|
|
483
|
+
"reader.result.column_names",
|
|
484
|
+
"exceeds the column budget",
|
|
485
|
+
)
|
|
486
|
+
if self.declared_cell_count > budgets.max_declared_cells:
|
|
487
|
+
raise ReaderError(
|
|
488
|
+
"READER_BUDGET",
|
|
489
|
+
"reader.result.declared_cell_count",
|
|
490
|
+
"exceeds the declared-cell budget",
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class ReaderFamily(Protocol):
|
|
495
|
+
"""One certified decoder, resolved by exact identity from the closed Toolbox.
|
|
496
|
+
|
|
497
|
+
A family is deliberately not runtime-checkable. Membership of the Toolbox is decided
|
|
498
|
+
by the registry at registration time against this contract's requirements, not by an
|
|
499
|
+
isinstance test at a call site, so a partially-shaped object cannot slip through by
|
|
500
|
+
happening to answer the attributes an isinstance check samples.
|
|
501
|
+
"""
|
|
502
|
+
|
|
503
|
+
@property
|
|
504
|
+
def family_id(self) -> str:
|
|
505
|
+
"""The identifier half of the exact resolution key."""
|
|
506
|
+
|
|
507
|
+
@property
|
|
508
|
+
def family_version(self) -> str:
|
|
509
|
+
"""The version half of the exact resolution key. Never resolved approximately."""
|
|
510
|
+
|
|
511
|
+
@property
|
|
512
|
+
def contract_version(self) -> str:
|
|
513
|
+
"""The Reader-contract label this family was built against."""
|
|
514
|
+
|
|
515
|
+
@property
|
|
516
|
+
def output_format(self) -> str:
|
|
517
|
+
"""The one format this family emits, which the result must match."""
|
|
518
|
+
|
|
519
|
+
@property
|
|
520
|
+
def accepted_media_types(self) -> tuple[str, ...]:
|
|
521
|
+
"""The media types this family admits as input, sorted and de-duplicated."""
|
|
522
|
+
|
|
523
|
+
@property
|
|
524
|
+
def default_budgets(self) -> ReaderBudgets:
|
|
525
|
+
"""The ceilings that apply when a recipe states no caps."""
|
|
526
|
+
|
|
527
|
+
def validate_options(self, options: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
528
|
+
"""Admit this family's own decode settings, refusing an unknown key by name."""
|
|
529
|
+
|
|
530
|
+
def decode(
|
|
531
|
+
self,
|
|
532
|
+
content: bytes,
|
|
533
|
+
pin: ReaderPin,
|
|
534
|
+
budgets: ReaderBudgets,
|
|
535
|
+
) -> ReaderResult:
|
|
536
|
+
"""Turn exact input bytes into exact output bytes, touching no filesystem.
|
|
537
|
+
|
|
538
|
+
The decode receives the bytes it is given and returns the bytes it produced. It
|
|
539
|
+
creates no temporary directory, extracts no archive member to disk, and spills
|
|
540
|
+
nothing: a container is streamed in memory or it is refused. That is the whole
|
|
541
|
+
security argument for admitting containers at all, and it admits no exception.
|
|
542
|
+
"""
|