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,312 @@
|
|
|
1
|
+
"""Go-live target resolution: explicit-only, read-and-refuse.
|
|
2
|
+
|
|
3
|
+
A deploy target says where a live dataset would be published: which cloud project, which region,
|
|
4
|
+
which Studio service account and worker job, and which cloud endpoint verifies API keys. Getting
|
|
5
|
+
that answer wrong spends real money in a real project, so this module is deliberately unable to
|
|
6
|
+
guess it.
|
|
7
|
+
|
|
8
|
+
Two rules hold everywhere in this module:
|
|
9
|
+
|
|
10
|
+
* Nothing is inferred. Every field comes from a mapping a human named explicitly. The module
|
|
11
|
+
never consults ambient ``gcloud`` configuration, never reads an environment variable for target
|
|
12
|
+
identity, and never shells out.
|
|
13
|
+
* Nothing is created. The module never creates a project, never enables billing, and never
|
|
14
|
+
touches IAM. It reads a descriptor and either resolves it or refuses it.
|
|
15
|
+
|
|
16
|
+
A refusal is a typed, quiet outcome that names its reasons, not a crash.
|
|
17
|
+
|
|
18
|
+
Why the corroboration step exists
|
|
19
|
+
---------------------------------
|
|
20
|
+
|
|
21
|
+
The rule for this project is that live staging happens only against an unambiguously verified
|
|
22
|
+
target, and that ambiguity means skip with a reason. On the machine this was written for, the
|
|
23
|
+
target is ambiguous, measured rather than assumed:
|
|
24
|
+
|
|
25
|
+
* ``gcloud config`` reports ``core/project=mostly-light``.
|
|
26
|
+
* ``gcloud config`` reports ``billing/quota_project=mostlyright-backend``.
|
|
27
|
+
* ``gcloud projects list`` returns PERMISSION_DENIED, so no third signal can break the tie.
|
|
28
|
+
* The active account is a personal one, with an inactive company account also credentialed.
|
|
29
|
+
|
|
30
|
+
Two config keys naming two different projects is exactly the case where a helpful default would
|
|
31
|
+
be a costly one. So :func:`preflight` compares the explicit descriptor against a corroborating
|
|
32
|
+
signal the caller supplies, and returns a refusal naming both values when they disagree. It never
|
|
33
|
+
picks a winner.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import re
|
|
39
|
+
from collections.abc import Mapping
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
from typing import Any
|
|
42
|
+
from urllib.parse import urlsplit
|
|
43
|
+
|
|
44
|
+
REQUIRED_FIELDS: tuple[str, ...] = (
|
|
45
|
+
"gcp_project_id",
|
|
46
|
+
"region",
|
|
47
|
+
"studio_service_account",
|
|
48
|
+
"studio_worker_job",
|
|
49
|
+
"cloud_verify_url",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
CORROBORATION_FIELDS: tuple[str, ...] = ("source", "gcp_project_id", "unreadable_reason")
|
|
53
|
+
|
|
54
|
+
MAX_FIELD_LENGTH = 2048
|
|
55
|
+
|
|
56
|
+
# GCP's documented project-id shape: 6 to 30 characters, a lowercase letter first, then lowercase
|
|
57
|
+
# letters, digits, or hyphens, and never a trailing hyphen.
|
|
58
|
+
_PROJECT_ID = re.compile(r"^[a-z][a-z0-9-]{4,28}[a-z0-9]$")
|
|
59
|
+
|
|
60
|
+
# Cloud Run region shape, for example us-central1, europe-west4, asia-northeast1.
|
|
61
|
+
_REGION = re.compile(r"^[a-z]{2,}-[a-z]{2,}[1-9][0-9]?$")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DeployTargetError(RuntimeError):
|
|
65
|
+
"""A typed go-live target failure."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TargetAmbiguityError(DeployTargetError, ValueError):
|
|
69
|
+
"""The target could not be established beyond doubt, so it is refused."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _https_url(value: str, label: str) -> str:
|
|
73
|
+
"""Return ``value`` when it is a plain absolute https URL, otherwise refuse it.
|
|
74
|
+
|
|
75
|
+
This mirrors ``hosted_bootstrap._https_url`` (hosted_bootstrap.py:147) instead of importing
|
|
76
|
+
it. Importing that module pulls the entire hosted-worker stack (pyarrow, pandas, numpy,
|
|
77
|
+
cryptography) into a module whose whole job is to refuse quickly, and it raises
|
|
78
|
+
``HostedWorkerError`` with a bootstrap-specific code that means nothing here. The URL policy
|
|
79
|
+
itself must stay identical to the bootstrap one: https only, an absolute URL, no user
|
|
80
|
+
information, no fragment, no trailing slash.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
if not isinstance(value, str) or not 1 <= len(value) <= MAX_FIELD_LENGTH:
|
|
84
|
+
raise TargetAmbiguityError(f"{label} is not a usable URL")
|
|
85
|
+
if value.endswith("/"):
|
|
86
|
+
raise TargetAmbiguityError(f"{label} must not end with a slash")
|
|
87
|
+
parsed = urlsplit(value)
|
|
88
|
+
if (
|
|
89
|
+
parsed.scheme != "https"
|
|
90
|
+
or not parsed.hostname
|
|
91
|
+
or parsed.username is not None
|
|
92
|
+
or parsed.password is not None
|
|
93
|
+
or parsed.fragment
|
|
94
|
+
):
|
|
95
|
+
raise TargetAmbiguityError(
|
|
96
|
+
f"{label} must be an absolute https URL without user information or fragment"
|
|
97
|
+
)
|
|
98
|
+
return value
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class DeployTarget:
|
|
103
|
+
"""One fully named go-live target; callers may narrow a target but never widen it.
|
|
104
|
+
|
|
105
|
+
Every field was named explicitly by a human. No field was inferred from ambient state, and no
|
|
106
|
+
field may be filled in later from a default, because a partially known target is exactly the
|
|
107
|
+
condition this module exists to refuse.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
gcp_project_id: str
|
|
111
|
+
region: str
|
|
112
|
+
studio_service_account: str
|
|
113
|
+
studio_worker_job: str
|
|
114
|
+
cloud_verify_url: str
|
|
115
|
+
|
|
116
|
+
def __post_init__(self) -> None:
|
|
117
|
+
for name in REQUIRED_FIELDS:
|
|
118
|
+
value = getattr(self, name)
|
|
119
|
+
if type(value) is not str:
|
|
120
|
+
raise TargetAmbiguityError(f"{name} must be text that names one value")
|
|
121
|
+
if not value or value != value.strip():
|
|
122
|
+
raise TargetAmbiguityError(f"{name} is missing")
|
|
123
|
+
if len(value) > MAX_FIELD_LENGTH:
|
|
124
|
+
raise TargetAmbiguityError(f"{name} is longer than {MAX_FIELD_LENGTH} characters")
|
|
125
|
+
if not _PROJECT_ID.fullmatch(self.gcp_project_id):
|
|
126
|
+
raise TargetAmbiguityError(
|
|
127
|
+
"gcp_project_id is not a valid cloud project id: it must be 6 to 30 characters, "
|
|
128
|
+
"start with a lowercase letter, use only lowercase letters, digits, or hyphens, "
|
|
129
|
+
"and not end with a hyphen"
|
|
130
|
+
)
|
|
131
|
+
if not _REGION.fullmatch(self.region):
|
|
132
|
+
raise TargetAmbiguityError(
|
|
133
|
+
"region is not a valid cloud region, for example us-central1"
|
|
134
|
+
)
|
|
135
|
+
_https_url(self.cloud_verify_url, "cloud_verify_url")
|
|
136
|
+
|
|
137
|
+
def to_dict(self) -> dict[str, Any]:
|
|
138
|
+
"""Return a plain JSON-safe mapping of the named target."""
|
|
139
|
+
|
|
140
|
+
return {name: getattr(self, name) for name in REQUIRED_FIELDS}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def resolve_deploy_target(descriptor: Mapping[str, object]) -> DeployTarget:
|
|
144
|
+
"""Resolve an explicit descriptor into a :class:`DeployTarget`, or refuse it by name.
|
|
145
|
+
|
|
146
|
+
Load-bearing rule: this function reads only the mapping it is handed. It never consults
|
|
147
|
+
ambient ``gcloud`` configuration, never reads an environment variable for target identity,
|
|
148
|
+
never shells out, and never creates a project, enables billing, or touches IAM. It is
|
|
149
|
+
read-and-refuse only.
|
|
150
|
+
|
|
151
|
+
Unknown keys are refused rather than ignored, because a typo in a target descriptor is the
|
|
152
|
+
cheapest way to deploy into the wrong project.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
TargetAmbiguityError: when any field is absent, blank, malformed, or unrecognized.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
if not isinstance(descriptor, Mapping):
|
|
159
|
+
raise TargetAmbiguityError(
|
|
160
|
+
"the deployment descriptor must be a mapping of field names to named values"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
unknown = sorted(str(key) for key in descriptor if key not in REQUIRED_FIELDS)
|
|
164
|
+
if unknown:
|
|
165
|
+
raise TargetAmbiguityError(
|
|
166
|
+
"the deployment descriptor names fields this resolver does not know: "
|
|
167
|
+
+ ", ".join(unknown)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
values: dict[str, str] = {}
|
|
171
|
+
for name in REQUIRED_FIELDS:
|
|
172
|
+
if name not in descriptor:
|
|
173
|
+
raise TargetAmbiguityError(f"the deployment descriptor does not name {name}")
|
|
174
|
+
raw = descriptor[name]
|
|
175
|
+
if type(raw) is not str:
|
|
176
|
+
raise TargetAmbiguityError(f"{name} must be text that names one value")
|
|
177
|
+
value = raw.strip()
|
|
178
|
+
if not value:
|
|
179
|
+
raise TargetAmbiguityError(f"the deployment descriptor does not name {name}")
|
|
180
|
+
values[name] = value
|
|
181
|
+
|
|
182
|
+
return DeployTarget(**values)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass(frozen=True)
|
|
186
|
+
class DeployPreflight:
|
|
187
|
+
"""The verdict on whether a go-live target is known beyond doubt.
|
|
188
|
+
|
|
189
|
+
Invariant: ``ok`` is true if and only if ``refusals`` is empty and ``target`` is not None.
|
|
190
|
+
Only two shapes exist. An ok verdict carries a resolved target and no reasons; a refusal
|
|
191
|
+
carries at least one named reason and no target at all. A refused verdict never hands back a
|
|
192
|
+
target, because a target in hand is the thing a caller acts on.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
ok: bool
|
|
196
|
+
refusals: tuple[str, ...]
|
|
197
|
+
target: DeployTarget | None
|
|
198
|
+
|
|
199
|
+
def __post_init__(self) -> None:
|
|
200
|
+
if type(self.ok) is not bool:
|
|
201
|
+
raise DeployTargetError("ok must be a boolean verdict")
|
|
202
|
+
if type(self.refusals) is not tuple or any(
|
|
203
|
+
type(item) is not str or not item.strip() for item in self.refusals
|
|
204
|
+
):
|
|
205
|
+
raise DeployTargetError("refusals must be a tuple of non-empty reasons")
|
|
206
|
+
if self.target is not None and not isinstance(self.target, DeployTarget):
|
|
207
|
+
raise DeployTargetError("target must be a resolved DeployTarget or None")
|
|
208
|
+
clean = not self.refusals and self.target is not None
|
|
209
|
+
if self.ok is not clean:
|
|
210
|
+
raise DeployTargetError(
|
|
211
|
+
"a preflight is ok only when there are no refusals and a target was resolved"
|
|
212
|
+
)
|
|
213
|
+
if not self.ok and (not self.refusals or self.target is not None):
|
|
214
|
+
raise DeployTargetError(
|
|
215
|
+
"a refused preflight must name at least one reason and carry no target"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def to_dict(self) -> dict[str, Any]:
|
|
219
|
+
"""Return a plain JSON-safe mapping of the verdict.
|
|
220
|
+
|
|
221
|
+
The verdict carries the named target values and the refusal reasons only. This module
|
|
222
|
+
accepts no credential of any kind, so a verdict cannot leak one.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
"ok": self.ok,
|
|
227
|
+
"refusals": list(self.refusals),
|
|
228
|
+
"target": None if self.target is None else self.target.to_dict(),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _corroborating_project(corroboration: Mapping[str, object]) -> tuple[str | None, str, str]:
|
|
233
|
+
"""Return the corroborated project id (or None), its source, and why it was unreadable."""
|
|
234
|
+
|
|
235
|
+
if not isinstance(corroboration, Mapping):
|
|
236
|
+
raise TargetAmbiguityError(
|
|
237
|
+
"corroboration must be a mapping describing what an independent signal reports"
|
|
238
|
+
)
|
|
239
|
+
unknown = sorted(str(key) for key in corroboration if key not in CORROBORATION_FIELDS)
|
|
240
|
+
if unknown:
|
|
241
|
+
raise TargetAmbiguityError(
|
|
242
|
+
"corroboration names fields this resolver does not know: " + ", ".join(unknown)
|
|
243
|
+
)
|
|
244
|
+
for name in ("source", "unreadable_reason"):
|
|
245
|
+
value = corroboration.get(name)
|
|
246
|
+
if value is not None and type(value) is not str:
|
|
247
|
+
raise TargetAmbiguityError(f"corroboration {name} must be text")
|
|
248
|
+
project = corroboration.get("gcp_project_id")
|
|
249
|
+
if project is not None and type(project) is not str:
|
|
250
|
+
raise TargetAmbiguityError("corroboration gcp_project_id must be text or absent")
|
|
251
|
+
|
|
252
|
+
source = str(corroboration.get("source") or "").strip() or "no corroborating signal"
|
|
253
|
+
reason = str(corroboration.get("unreadable_reason") or "").strip() or "nothing readable"
|
|
254
|
+
named = None if project is None else project.strip() or None
|
|
255
|
+
return named, source, reason
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def preflight(
|
|
259
|
+
descriptor: Mapping[str, object],
|
|
260
|
+
*,
|
|
261
|
+
corroboration: Mapping[str, object],
|
|
262
|
+
) -> DeployPreflight:
|
|
263
|
+
"""Judge whether a go-live target is known beyond doubt, and refuse quietly when it is not.
|
|
264
|
+
|
|
265
|
+
``corroboration`` describes what an independent signal reports about the target: a
|
|
266
|
+
``gcp_project_id`` read from a deployment config, plus an optional ``source`` naming where
|
|
267
|
+
it came
|
|
268
|
+
from and an optional ``unreadable_reason`` when no signal could be read at all.
|
|
269
|
+
|
|
270
|
+
The caller supplies that signal. This module refuses to be the thing that learns the answer,
|
|
271
|
+
because a component that can discover a target can also silently pick the wrong one. It runs
|
|
272
|
+
no ``gcloud`` command, creates no project, enables no billing, and touches no IAM.
|
|
273
|
+
|
|
274
|
+
An ambiguous input is never an exception; it is a verdict with named reasons. Only a
|
|
275
|
+
structurally invalid call raises.
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
TargetAmbiguityError: when ``descriptor`` or ``corroboration`` is not a well-formed
|
|
279
|
+
mapping of the fields this resolver knows.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
if not isinstance(descriptor, Mapping):
|
|
283
|
+
raise TargetAmbiguityError(
|
|
284
|
+
"the deployment descriptor must be a mapping of field names to named values"
|
|
285
|
+
)
|
|
286
|
+
corroborated, source, reason = _corroborating_project(corroboration)
|
|
287
|
+
|
|
288
|
+
refusals: list[str] = []
|
|
289
|
+
target: DeployTarget | None
|
|
290
|
+
try:
|
|
291
|
+
target = resolve_deploy_target(descriptor)
|
|
292
|
+
except TargetAmbiguityError as error:
|
|
293
|
+
target = None
|
|
294
|
+
refusals.append(str(error))
|
|
295
|
+
|
|
296
|
+
if corroborated is None:
|
|
297
|
+
refusals.append(
|
|
298
|
+
f"the target project could not be corroborated: {source} reported {reason}; "
|
|
299
|
+
"go-live is refused until an independent signal names the target project"
|
|
300
|
+
)
|
|
301
|
+
elif target is not None and corroborated != target.gcp_project_id:
|
|
302
|
+
refusals.append(
|
|
303
|
+
"target project is ambiguous: the deployment descriptor names "
|
|
304
|
+
f"{target.gcp_project_id}, "
|
|
305
|
+
f"the corroborating signal ({source}) names {corroborated}; "
|
|
306
|
+
"name one explicitly before going live"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if refusals:
|
|
310
|
+
# A refused verdict never carries a target: the caller must have nothing to act on.
|
|
311
|
+
return DeployPreflight(ok=False, refusals=tuple(refusals), target=None)
|
|
312
|
+
return DeployPreflight(ok=True, refusals=(), target=target)
|