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,597 @@
|
|
|
1
|
+
"""A bounded, fail-closed reader for the YAML subset a person writes a Recipe in.
|
|
2
|
+
|
|
3
|
+
Nobody hand-maintains ten kilobytes of JSON twice. YAML is the authoring surface; JSON stays the
|
|
4
|
+
contract. This module reads exactly enough YAML for that job and refuses everything else **by
|
|
5
|
+
name**, because a reader that guesses at a Recipe is worse than one that stops: whatever comes out
|
|
6
|
+
of here is what `mr-data approve` binds a fingerprint to.
|
|
7
|
+
|
|
8
|
+
What it reads:
|
|
9
|
+
|
|
10
|
+
* names with values under them, at any consistent indentation
|
|
11
|
+
* lists, including lists of names-with-values and names with lists under them
|
|
12
|
+
* plain values -- text, whole numbers, decimals, ``true``, ``false``, ``null`` and ``~``
|
|
13
|
+
* text in double quotes (with ``\\"``, ``\\\\``, ``\\n`` and ``\\t``) or single quotes (with ``''``)
|
|
14
|
+
* ``#`` notes, at the end of a line or on a line of their own
|
|
15
|
+
* one optional ``---`` before the first line of content
|
|
16
|
+
* ``[]`` and ``{}`` for a list or a group with nothing in it -- the only bracketed forms accepted
|
|
17
|
+
|
|
18
|
+
Everything else raises :class:`AuthorError` naming the construct and the line it is on: anchors,
|
|
19
|
+
aliases, merge keys, tags, bracketed lists and groups with contents in them, a second document,
|
|
20
|
+
tabs used for indentation, block text markers, an indentation step that never opened, a repeated
|
|
21
|
+
name, and anything the line scan does not positively recognise.
|
|
22
|
+
|
|
23
|
+
Three properties this file is responsible for:
|
|
24
|
+
|
|
25
|
+
* **No third-party loader exists here.** Nothing can call one by mistake or construct an object
|
|
26
|
+
from a tag, and this boundary adds no parser dependency.
|
|
27
|
+
* **No expansion vector.** Anchors and aliases are refused outright, so the repeated-expansion
|
|
28
|
+
denial of service has nothing to work with. Depth and input size are capped besides.
|
|
29
|
+
* **When in doubt, refuse.** Every accepted form is written down below. A line that matches none of
|
|
30
|
+
them is ``AUTHOR_YAML_UNSUPPORTED`` with its own text quoted back, never a best-effort guess.
|
|
31
|
+
|
|
32
|
+
The value this returns is still only a value. :func:`mostlyright.data_harness.recipe
|
|
33
|
+
.parse_frozen_recipe` is the second gate, and the plain-language summary `mr-data approve` prints
|
|
34
|
+
is the third.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import re
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import Any
|
|
43
|
+
|
|
44
|
+
from mostlyright.data_harness.recipe import RecipeError
|
|
45
|
+
from mostlyright.data_harness.ux.path_kind import UNKNOWN_KIND, name_of_kind, presence_at
|
|
46
|
+
from mostlyright.data_harness.ux.plain_file import (
|
|
47
|
+
DANGLING,
|
|
48
|
+
MISSING,
|
|
49
|
+
NOT_PLAIN,
|
|
50
|
+
PlainFileRefusal,
|
|
51
|
+
read_plain_file,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# One megabyte of hand-written YAML is far more Recipe than anyone writes; above it the input is
|
|
55
|
+
# refused before a single byte is decoded.
|
|
56
|
+
MAX_INPUT_BYTES = 1 << 20
|
|
57
|
+
|
|
58
|
+
# The checked-in example Recipe nests six deep. Thirty-two leaves room for anything a person means
|
|
59
|
+
# and stops a file whose only purpose is to nest.
|
|
60
|
+
MAX_DEPTH = 32
|
|
61
|
+
|
|
62
|
+
_DOCUMENT_MARKER = "---"
|
|
63
|
+
_DOCUMENT_END = "..."
|
|
64
|
+
|
|
65
|
+
_ESCAPES = {'"': '"', "\\": "\\", "n": "\n", "t": "\t"}
|
|
66
|
+
|
|
67
|
+
_INTEGER = re.compile(r"[-+]?[0-9]+")
|
|
68
|
+
_DECIMAL = re.compile(
|
|
69
|
+
r"(?:[-+]?(?:[0-9]+\.[0-9]*|\.[0-9]+)(?:[eE][-+]?[0-9]+)?)|(?:[-+]?[0-9]+[eE][-+]?[0-9]+)"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Spellings that are almost certainly meant as one of the three constants. Reading them as text
|
|
73
|
+
# would be a silent surprise, so they are refused and the writer is told which spelling to use.
|
|
74
|
+
# The lower-case words `on`, `off`, `yes`, `no` and `none` are ordinary text here and stay text --
|
|
75
|
+
# the checked-in example Recipe uses `none` as a value and `on` as a name.
|
|
76
|
+
_AMBIGUOUS = frozenset({"True", "TRUE", "False", "FALSE", "Null", "NULL", "None"})
|
|
77
|
+
|
|
78
|
+
_SEQUENCE = "sequence"
|
|
79
|
+
_MAPPING = "mapping"
|
|
80
|
+
_VALUE = "value"
|
|
81
|
+
|
|
82
|
+
_PLAIN_FORMS = (
|
|
83
|
+
"write text, a whole number, a decimal, true, false, or null, or put the text in quotes"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AuthorError(RecipeError):
|
|
88
|
+
"""One YAML line the authoring reader will not read, named and located.
|
|
89
|
+
|
|
90
|
+
It rides on :class:`RecipeError` on purpose: the command line already catches that class, so
|
|
91
|
+
an authoring refusal reaches a person through the same path a Recipe refusal does without a
|
|
92
|
+
second command-boundary translation.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, code: str, line: int, detail: str) -> None:
|
|
96
|
+
self.line = line
|
|
97
|
+
super().__init__(code, f"line {line}" if line else "recipe", detail)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class _Token:
|
|
102
|
+
"""One thing found on one line: a list dash, a name, or a value standing alone.
|
|
103
|
+
|
|
104
|
+
``indent`` is the column the thing starts at, so ``- name: value`` yields a dash at the line's
|
|
105
|
+
own indentation and a name two columns further in. That is what lets one rule -- deeper column
|
|
106
|
+
means underneath -- build the whole tree.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
kind: str
|
|
110
|
+
indent: int
|
|
111
|
+
line: int
|
|
112
|
+
key: str | None = None
|
|
113
|
+
text: str | None = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def read_author_source(path: Path) -> bytes:
|
|
117
|
+
"""Read one authoring file, refusing an oversized one before any of it is read.
|
|
118
|
+
|
|
119
|
+
The rule itself -- open once, ask the descriptor what it is, read that descriptor under the
|
|
120
|
+
bound -- lives in :mod:`ux.plain_file`, because ``peek`` needs the identical rule and the
|
|
121
|
+
second copy of it is where this defect came back. What stays here is the wording: a Recipe is
|
|
122
|
+
written in a file, and the three sentences below say so in the words this command uses.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
return read_plain_file(path, max_bytes=MAX_INPUT_BYTES)
|
|
127
|
+
except PlainFileRefusal as refusal:
|
|
128
|
+
# Raised here rather than returned from a helper: the sweep that proves every typed code
|
|
129
|
+
# names its fix reads `raise` statements out of the live source, so a code that reaches a
|
|
130
|
+
# person only through a returned object is a code that sweep cannot see.
|
|
131
|
+
if refusal.reason == DANGLING:
|
|
132
|
+
# A dangling link is present even though its target is absent, so missing-path advice
|
|
133
|
+
# would misstate the observation. The noun comes from the mode already held by the
|
|
134
|
+
# rule, not a second filesystem lookup.
|
|
135
|
+
raise AuthorError(
|
|
136
|
+
"AUTHOR_YAML_DANGLING",
|
|
137
|
+
0,
|
|
138
|
+
f"there is {name_of_kind(refusal.mode) or UNKNOWN_KIND} at {refusal.path}, "
|
|
139
|
+
"and it leads "
|
|
140
|
+
"nowhere; repoint it at your Recipe, or remove it and write one there",
|
|
141
|
+
) from None
|
|
142
|
+
if refusal.reason == MISSING:
|
|
143
|
+
raise AuthorError(
|
|
144
|
+
"AUTHOR_YAML_MISSING",
|
|
145
|
+
0,
|
|
146
|
+
f"there is {presence_at(path) or UNKNOWN_KIND} at {path}; check the spelling and "
|
|
147
|
+
"the folder you are in",
|
|
148
|
+
) from None
|
|
149
|
+
if refusal.reason == NOT_PLAIN:
|
|
150
|
+
raise AuthorError(
|
|
151
|
+
"AUTHOR_YAML_TARGET",
|
|
152
|
+
0,
|
|
153
|
+
f"{path} is not a plain file; a Recipe is written in a file, not in a folder, a "
|
|
154
|
+
"pipe, or a device",
|
|
155
|
+
) from None
|
|
156
|
+
raise AuthorError(
|
|
157
|
+
"AUTHOR_YAML_SIZE",
|
|
158
|
+
0,
|
|
159
|
+
f"{path} is larger than {MAX_INPUT_BYTES} bytes; write the Recipe in a smaller file",
|
|
160
|
+
) from None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_author_yaml(raw: bytes | str) -> Any:
|
|
164
|
+
"""Read one authoring file and return the plain value it describes.
|
|
165
|
+
|
|
166
|
+
Returns ``None`` for a file holding nothing but notes; naming what a Recipe is missing is
|
|
167
|
+
``parse_frozen_recipe``'s job, not this one's.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
tokens = _tokenise(_decode(raw))
|
|
171
|
+
if not tokens:
|
|
172
|
+
return None
|
|
173
|
+
first = tokens[0]
|
|
174
|
+
if first.indent != 0:
|
|
175
|
+
raise AuthorError(
|
|
176
|
+
"AUTHOR_YAML_INDENT",
|
|
177
|
+
first.line,
|
|
178
|
+
"the first line of content is indented; start it at the left margin",
|
|
179
|
+
)
|
|
180
|
+
if first.kind == _VALUE:
|
|
181
|
+
raise AuthorError(
|
|
182
|
+
"AUTHOR_YAML_UNSUPPORTED",
|
|
183
|
+
first.line,
|
|
184
|
+
f"a Recipe is a set of names with values under them; write name: value, not "
|
|
185
|
+
f"{_quote_back(first.text)}",
|
|
186
|
+
)
|
|
187
|
+
value, index = _build(tokens, 0, 0, 1)
|
|
188
|
+
if index < len(tokens):
|
|
189
|
+
raise AuthorError(
|
|
190
|
+
"AUTHOR_YAML_INDENT",
|
|
191
|
+
tokens[index].line,
|
|
192
|
+
"this line does not line up with anything above it; check its indentation",
|
|
193
|
+
)
|
|
194
|
+
return value
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------------------------------------------------
|
|
198
|
+
# Bytes to lines
|
|
199
|
+
# ------------------------------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _decode(raw: bytes | str) -> str:
|
|
203
|
+
if isinstance(raw, str):
|
|
204
|
+
data = raw.encode("utf-8", errors="surrogatepass")
|
|
205
|
+
else:
|
|
206
|
+
data = raw
|
|
207
|
+
if len(data) > MAX_INPUT_BYTES:
|
|
208
|
+
raise AuthorError(
|
|
209
|
+
"AUTHOR_YAML_SIZE",
|
|
210
|
+
0,
|
|
211
|
+
f"the Recipe file is larger than {MAX_INPUT_BYTES} bytes; write a smaller one",
|
|
212
|
+
)
|
|
213
|
+
if isinstance(raw, str):
|
|
214
|
+
return raw
|
|
215
|
+
try:
|
|
216
|
+
return data.decode("utf-8")
|
|
217
|
+
except UnicodeDecodeError as exc:
|
|
218
|
+
raise AuthorError(
|
|
219
|
+
"AUTHOR_YAML_ENCODING",
|
|
220
|
+
0,
|
|
221
|
+
"the Recipe file is not plain UTF-8 text; save it as UTF-8 and try again",
|
|
222
|
+
) from exc
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _tokenise(text: str) -> list[_Token]:
|
|
226
|
+
"""Turn every line of content into the things standing on it, left to right."""
|
|
227
|
+
|
|
228
|
+
tokens: list[_Token] = []
|
|
229
|
+
marker_seen = False
|
|
230
|
+
content_seen = False
|
|
231
|
+
for number, source in enumerate(text.split("\n"), start=1):
|
|
232
|
+
line = source[:-1] if source.endswith("\r") else source
|
|
233
|
+
body_start = len(line) - len(line.lstrip(" \t"))
|
|
234
|
+
if "\t" in line[:body_start]:
|
|
235
|
+
raise AuthorError(
|
|
236
|
+
"AUTHOR_YAML_TAB",
|
|
237
|
+
number,
|
|
238
|
+
"this line is indented with a tab; use spaces",
|
|
239
|
+
)
|
|
240
|
+
stripped = line[body_start:].rstrip()
|
|
241
|
+
if not stripped or stripped.startswith("#"):
|
|
242
|
+
continue
|
|
243
|
+
if stripped == _DOCUMENT_MARKER:
|
|
244
|
+
if content_seen or marker_seen:
|
|
245
|
+
raise AuthorError(
|
|
246
|
+
"AUTHOR_YAML_MULTIDOC",
|
|
247
|
+
number,
|
|
248
|
+
"one Recipe per file; --- may appear once, before the first line of content",
|
|
249
|
+
)
|
|
250
|
+
marker_seen = True
|
|
251
|
+
continue
|
|
252
|
+
if stripped == _DOCUMENT_END or stripped.startswith(f"{_DOCUMENT_END} "):
|
|
253
|
+
raise AuthorError(
|
|
254
|
+
"AUTHOR_YAML_MULTIDOC",
|
|
255
|
+
number,
|
|
256
|
+
"one Recipe per file; write no ... end marker",
|
|
257
|
+
)
|
|
258
|
+
content_seen = True
|
|
259
|
+
_tokenise_line(tokens, line, body_start, number)
|
|
260
|
+
return tokens
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _tokenise_line(tokens: list[_Token], line: str, indent: int, number: int) -> None:
|
|
264
|
+
column = indent
|
|
265
|
+
rest = line[indent:].rstrip()
|
|
266
|
+
while rest.startswith("-") and (len(rest) == 1 or rest[1] in " \t"):
|
|
267
|
+
if len(rest) > 1 and rest[1] == "\t":
|
|
268
|
+
raise AuthorError(
|
|
269
|
+
"AUTHOR_YAML_TAB",
|
|
270
|
+
number,
|
|
271
|
+
"there is a tab after this list dash; use spaces",
|
|
272
|
+
)
|
|
273
|
+
tokens.append(_Token(_SEQUENCE, column, number))
|
|
274
|
+
step = 1
|
|
275
|
+
while step < len(rest) and rest[step] == " ":
|
|
276
|
+
step += 1
|
|
277
|
+
column += step
|
|
278
|
+
rest = rest[step:]
|
|
279
|
+
if not rest:
|
|
280
|
+
return
|
|
281
|
+
if not rest or rest.startswith("#"):
|
|
282
|
+
return
|
|
283
|
+
if rest.startswith("\t"):
|
|
284
|
+
raise AuthorError("AUTHOR_YAML_TAB", number, "there is a tab here; use spaces")
|
|
285
|
+
if rest == "?" or rest.startswith("? "):
|
|
286
|
+
# The marker for a name written on its own line, with its value on the next. Caught here
|
|
287
|
+
# rather than left to the build so the refusal names the line the writer typed it on.
|
|
288
|
+
raise AuthorError(
|
|
289
|
+
"AUTHOR_YAML_UNSUPPORTED",
|
|
290
|
+
number,
|
|
291
|
+
"? starts a name written on a line of its own, which is not read here; write "
|
|
292
|
+
"name: value on one line",
|
|
293
|
+
)
|
|
294
|
+
key, value = _split_name(rest, number)
|
|
295
|
+
if key is None:
|
|
296
|
+
tokens.append(_Token(_VALUE, column, number, text=rest))
|
|
297
|
+
return
|
|
298
|
+
_check_name(key, number)
|
|
299
|
+
tokens.append(_Token(_MAPPING, column, number, key=key, text=value))
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _split_name(rest: str, number: int) -> tuple[str | None, str | None]:
|
|
303
|
+
"""Split ``name: value`` at the colon that ends the name, or report there is no name here.
|
|
304
|
+
|
|
305
|
+
The colon that ends a name is the first one followed by a space or by the end of the line, so
|
|
306
|
+
a value holding a time or an address keeps its own colons.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
if rest[0] in "\"'":
|
|
310
|
+
name, end = _read_quoted(rest, number)
|
|
311
|
+
tail = rest[end:]
|
|
312
|
+
if tail.startswith(":") and (len(tail) == 1 or tail[1] == " "):
|
|
313
|
+
return name, _inline_value(tail[1:])
|
|
314
|
+
return None, None
|
|
315
|
+
for index, character in enumerate(rest):
|
|
316
|
+
if character == ":" and (index + 1 == len(rest) or rest[index + 1] == " "):
|
|
317
|
+
return rest[:index].rstrip(), _inline_value(rest[index + 1 :])
|
|
318
|
+
return None, None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _inline_value(text: str) -> str | None:
|
|
322
|
+
"""What follows the colon, or nothing when only a note follows it."""
|
|
323
|
+
|
|
324
|
+
value = text.strip()
|
|
325
|
+
if not value or value.startswith("#"):
|
|
326
|
+
return None
|
|
327
|
+
return value
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _check_name(key: str, number: int) -> None:
|
|
331
|
+
if not key:
|
|
332
|
+
raise AuthorError("AUTHOR_YAML_UNSUPPORTED", number, "this line has a colon but no name")
|
|
333
|
+
if key == "<<":
|
|
334
|
+
raise AuthorError(
|
|
335
|
+
"AUTHOR_YAML_MERGE_KEY",
|
|
336
|
+
number,
|
|
337
|
+
"<< copies one group of names into another, which is not read here; write the names "
|
|
338
|
+
"out in full",
|
|
339
|
+
)
|
|
340
|
+
_refuse_construct(key[0], key, number)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ------------------------------------------------------------------------------------------------
|
|
344
|
+
# Values
|
|
345
|
+
# ------------------------------------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _scalar(text: str | None, number: int) -> Any:
|
|
349
|
+
"""One plain value, or a typed refusal naming what was written instead."""
|
|
350
|
+
|
|
351
|
+
if not text:
|
|
352
|
+
return None
|
|
353
|
+
if text[0] in "\"'":
|
|
354
|
+
value, end = _read_quoted(text, number)
|
|
355
|
+
_require_note_only(text[end:], number)
|
|
356
|
+
return value
|
|
357
|
+
bare = _strip_note(text)
|
|
358
|
+
if not bare:
|
|
359
|
+
return None
|
|
360
|
+
if bare == "[]":
|
|
361
|
+
return []
|
|
362
|
+
if bare == "{}":
|
|
363
|
+
return {}
|
|
364
|
+
_refuse_construct(bare[0], bare, number)
|
|
365
|
+
if bare in ("null", "~"):
|
|
366
|
+
return None
|
|
367
|
+
if bare == "true":
|
|
368
|
+
return True
|
|
369
|
+
if bare == "false":
|
|
370
|
+
return False
|
|
371
|
+
if bare in _AMBIGUOUS:
|
|
372
|
+
raise AuthorError(
|
|
373
|
+
"AUTHOR_YAML_AMBIGUOUS",
|
|
374
|
+
number,
|
|
375
|
+
f"{bare} could be a value or the word itself; write true, false or null in lower "
|
|
376
|
+
f"case, or put {bare} in quotes to mean the word",
|
|
377
|
+
)
|
|
378
|
+
if _INTEGER.fullmatch(bare):
|
|
379
|
+
return _whole_number(bare, number)
|
|
380
|
+
if _DECIMAL.fullmatch(bare):
|
|
381
|
+
return float(bare)
|
|
382
|
+
return bare
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _whole_number(bare: str, number: int) -> int:
|
|
386
|
+
"""One whole number, or the same refusal by name that every other unreadable value gets.
|
|
387
|
+
|
|
388
|
+
The only conversion in this reader that the interpreter can refuse: CPython will not turn a
|
|
389
|
+
decimal string longer than ``sys.get_int_max_str_digits()`` digits into an int, and says so
|
|
390
|
+
with a bare ``ValueError``. That class is not an ``AuthorError``, is not caught at the command
|
|
391
|
+
line, and reached the terminal as a Python traceback with no code and no fix -- out of the one
|
|
392
|
+
module whose whole contract is refusing by name with the line it is on.
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
return int(bare)
|
|
397
|
+
except ValueError as exc:
|
|
398
|
+
raise AuthorError(
|
|
399
|
+
"AUTHOR_YAML_NUMBER",
|
|
400
|
+
number,
|
|
401
|
+
f"a whole number written with {len(bare.lstrip('+-'))} digits is longer than this "
|
|
402
|
+
"reader converts; put it in quotes to keep it as text, or write a shorter number",
|
|
403
|
+
) from exc
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _refuse_construct(head: str, written: str, number: int) -> None:
|
|
407
|
+
"""Refuse, by name, every construct this reader does not read."""
|
|
408
|
+
|
|
409
|
+
if head == "&":
|
|
410
|
+
raise AuthorError(
|
|
411
|
+
"AUTHOR_YAML_ANCHOR",
|
|
412
|
+
number,
|
|
413
|
+
f"& marks a spot to copy from later, which is not read here; write the value out in "
|
|
414
|
+
f"full instead of {_quote_back(written)}",
|
|
415
|
+
)
|
|
416
|
+
if head == "*":
|
|
417
|
+
raise AuthorError(
|
|
418
|
+
"AUTHOR_YAML_ALIAS",
|
|
419
|
+
number,
|
|
420
|
+
f"* copies a value marked earlier, which is not read here; write the value out in "
|
|
421
|
+
f"full instead of {_quote_back(written)}",
|
|
422
|
+
)
|
|
423
|
+
if head == "!":
|
|
424
|
+
raise AuthorError(
|
|
425
|
+
"AUTHOR_YAML_TAG",
|
|
426
|
+
number,
|
|
427
|
+
f"! names a type to build, which is never read here; write the plain value instead of "
|
|
428
|
+
f"{_quote_back(written)}",
|
|
429
|
+
)
|
|
430
|
+
if head in "{[":
|
|
431
|
+
raise AuthorError(
|
|
432
|
+
"AUTHOR_YAML_FLOW",
|
|
433
|
+
number,
|
|
434
|
+
f"a list or group written inside brackets is not read here; write one entry per line "
|
|
435
|
+
f"under its name instead of {_quote_back(written)}",
|
|
436
|
+
)
|
|
437
|
+
if head in "|>":
|
|
438
|
+
raise AuthorError(
|
|
439
|
+
"AUTHOR_YAML_BLOCK_SCALAR",
|
|
440
|
+
number,
|
|
441
|
+
f"{head} starts text carried over several lines, which is not read here; write the "
|
|
442
|
+
f"text on one line in quotes, using \\n where you want a line break, instead of "
|
|
443
|
+
f"{_quote_back(written)}",
|
|
444
|
+
)
|
|
445
|
+
if head in "?%@`":
|
|
446
|
+
raise AuthorError(
|
|
447
|
+
"AUTHOR_YAML_UNSUPPORTED",
|
|
448
|
+
number,
|
|
449
|
+
f"{_quote_back(written)} starts with {head}, which is not read here; {_PLAIN_FORMS}",
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _read_quoted(text: str, number: int) -> tuple[str, int]:
|
|
454
|
+
"""Read one quoted value, returning it and the column just past its closing quote."""
|
|
455
|
+
|
|
456
|
+
quote = text[0]
|
|
457
|
+
out: list[str] = []
|
|
458
|
+
index = 1
|
|
459
|
+
while index < len(text):
|
|
460
|
+
character = text[index]
|
|
461
|
+
if quote == '"' and character == "\\":
|
|
462
|
+
if index + 1 >= len(text):
|
|
463
|
+
break
|
|
464
|
+
following = text[index + 1]
|
|
465
|
+
mapped = _ESCAPES.get(following)
|
|
466
|
+
if mapped is None:
|
|
467
|
+
raise AuthorError(
|
|
468
|
+
"AUTHOR_YAML_QUOTE",
|
|
469
|
+
number,
|
|
470
|
+
f"\\{following} is not one of the four escapes read here; "
|
|
471
|
+
f'write \\" \\\\ \\n or \\t',
|
|
472
|
+
)
|
|
473
|
+
out.append(mapped)
|
|
474
|
+
index += 2
|
|
475
|
+
continue
|
|
476
|
+
if character == quote:
|
|
477
|
+
if quote == "'" and index + 1 < len(text) and text[index + 1] == "'":
|
|
478
|
+
out.append("'")
|
|
479
|
+
index += 2
|
|
480
|
+
continue
|
|
481
|
+
return "".join(out), index + 1
|
|
482
|
+
out.append(character)
|
|
483
|
+
index += 1
|
|
484
|
+
raise AuthorError(
|
|
485
|
+
"AUTHOR_YAML_QUOTE",
|
|
486
|
+
number,
|
|
487
|
+
f"this {quote} is never closed; close it on the same line",
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _require_note_only(tail: str, number: int) -> None:
|
|
492
|
+
remainder = tail.strip()
|
|
493
|
+
if remainder and not remainder.startswith("#"):
|
|
494
|
+
raise AuthorError(
|
|
495
|
+
"AUTHOR_YAML_UNSUPPORTED",
|
|
496
|
+
number,
|
|
497
|
+
f"{_quote_back(remainder)} follows the closing quote; put the whole value inside the "
|
|
498
|
+
f"quotes, or start a note with #",
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _strip_note(text: str) -> str:
|
|
503
|
+
"""Drop an end-of-line note. A # with no space before it is part of the word."""
|
|
504
|
+
|
|
505
|
+
for index in range(1, len(text)):
|
|
506
|
+
if text[index] == "#" and text[index - 1] in " \t":
|
|
507
|
+
return text[:index].rstrip()
|
|
508
|
+
return text
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _quote_back(written: str | None) -> str:
|
|
512
|
+
return repr(written if written is not None else "")
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
# ------------------------------------------------------------------------------------------------
|
|
516
|
+
# Lines to a value
|
|
517
|
+
# ------------------------------------------------------------------------------------------------
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _build(tokens: list[_Token], index: int, column: int, depth: int) -> tuple[Any, int]:
|
|
521
|
+
if depth > MAX_DEPTH:
|
|
522
|
+
raise AuthorError(
|
|
523
|
+
"AUTHOR_YAML_DEPTH",
|
|
524
|
+
tokens[index].line,
|
|
525
|
+
f"this is nested more than {MAX_DEPTH} deep; a Recipe never needs that",
|
|
526
|
+
)
|
|
527
|
+
token = tokens[index]
|
|
528
|
+
if token.kind == _SEQUENCE:
|
|
529
|
+
return _build_sequence(tokens, index, column, depth)
|
|
530
|
+
if token.kind == _MAPPING:
|
|
531
|
+
return _build_mapping(tokens, index, column, depth)
|
|
532
|
+
return _scalar(token.text, token.line), index + 1
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _build_mapping(
|
|
536
|
+
tokens: list[_Token], index: int, column: int, depth: int
|
|
537
|
+
) -> tuple[dict[str, Any], int]:
|
|
538
|
+
result: dict[str, Any] = {}
|
|
539
|
+
while index < len(tokens):
|
|
540
|
+
token = tokens[index]
|
|
541
|
+
if token.indent < column:
|
|
542
|
+
break
|
|
543
|
+
if token.indent > column:
|
|
544
|
+
raise AuthorError(
|
|
545
|
+
"AUTHOR_YAML_INDENT",
|
|
546
|
+
token.line,
|
|
547
|
+
"this line is indented further in than the line above it opened; line it up with "
|
|
548
|
+
"the names it belongs to",
|
|
549
|
+
)
|
|
550
|
+
if token.kind != _MAPPING:
|
|
551
|
+
raise AuthorError(
|
|
552
|
+
"AUTHOR_YAML_INDENT",
|
|
553
|
+
token.line,
|
|
554
|
+
"a list entry stands where a name was expected; give the list a name, or indent "
|
|
555
|
+
"it under the one it belongs to",
|
|
556
|
+
)
|
|
557
|
+
name = token.key or ""
|
|
558
|
+
if name in result:
|
|
559
|
+
raise AuthorError(
|
|
560
|
+
"AUTHOR_YAML_DUPLICATE_KEY",
|
|
561
|
+
token.line,
|
|
562
|
+
f"{name} is named twice in the same group; every name is written once",
|
|
563
|
+
)
|
|
564
|
+
index += 1
|
|
565
|
+
if token.text is not None:
|
|
566
|
+
result[name] = _scalar(token.text, token.line)
|
|
567
|
+
continue
|
|
568
|
+
result[name], index = _under(tokens, index, column, depth)
|
|
569
|
+
return result, index
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _under(tokens: list[_Token], index: int, column: int, depth: int) -> tuple[Any, int]:
|
|
573
|
+
"""What sits under a name that carries no value of its own."""
|
|
574
|
+
|
|
575
|
+
if index < len(tokens) and tokens[index].indent > column:
|
|
576
|
+
return _build(tokens, index, tokens[index].indent, depth + 1)
|
|
577
|
+
# A list may sit flush with the name it belongs to; that is the form most people type, and it
|
|
578
|
+
# means what the indented form means.
|
|
579
|
+
if index < len(tokens) and tokens[index].indent == column and tokens[index].kind == _SEQUENCE:
|
|
580
|
+
return _build_sequence(tokens, index, column, depth + 1)
|
|
581
|
+
return None, index
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _build_sequence(
|
|
585
|
+
tokens: list[_Token], index: int, column: int, depth: int
|
|
586
|
+
) -> tuple[list[Any], int]:
|
|
587
|
+
items: list[Any] = []
|
|
588
|
+
while (
|
|
589
|
+
index < len(tokens) and tokens[index].kind == _SEQUENCE and tokens[index].indent == column
|
|
590
|
+
):
|
|
591
|
+
index += 1
|
|
592
|
+
if index < len(tokens) and tokens[index].indent > column:
|
|
593
|
+
item, index = _build(tokens, index, tokens[index].indent, depth + 1)
|
|
594
|
+
else:
|
|
595
|
+
item = None
|
|
596
|
+
items.append(item)
|
|
597
|
+
return items, index
|