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,519 @@
|
|
|
1
|
+
"""The Studio V3 run calls the thin lane makes: start one, read one, and fetch what it produced.
|
|
2
|
+
|
|
3
|
+
Written against the contract rather than against the generated client, and that is a decision
|
|
4
|
+
rather than an omission. The generated client is an optional extra (``mostlyright-studio-client-v3``
|
|
5
|
+
arrives with ``[hosted]``), it pulls ``httpx`` and its whole closure, and its own
|
|
6
|
+
``stream_run_events`` buffers the entire Server-Sent-Events body into one ``str`` before returning
|
|
7
|
+
it -- unusable for a live stream by construction. A thin client that must run with no third-party
|
|
8
|
+
package at all therefore speaks the wire directly, and :data:`PINNED_CONTRACT_VERSION` plus the
|
|
9
|
+
route table below are what a reviewer diffs against ``contracts/openapi/studio-v3.yaml``.
|
|
10
|
+
|
|
11
|
+
Every route here is a READ or a start. Nothing in this module cancels, retries, approves or
|
|
12
|
+
releases: those are authority-bearing mutations whose refusals belong with the operator surfaces
|
|
13
|
+
that already own them. The three lanes that DO write --
|
|
14
|
+
:mod:`mostlyright.data_harness.thin.acquire`, which opens a hosted acquisition session,
|
|
15
|
+
:mod:`mostlyright.data_harness.thin.approvals`, which records an ask a human must still settle,
|
|
16
|
+
and :mod:`mostlyright.data_harness.thin.narrative`, which appends one unsealed cell of prose --
|
|
17
|
+
keep their own routes in their own modules and share only :class:`StudioApiClient`, so this
|
|
18
|
+
module's promise stays checkable by reading it.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
import secrets
|
|
25
|
+
from collections.abc import Mapping
|
|
26
|
+
from typing import Any
|
|
27
|
+
from urllib.parse import urlencode
|
|
28
|
+
from uuid import UUID
|
|
29
|
+
|
|
30
|
+
from mostlyright.data_harness.canonical import (
|
|
31
|
+
CanonicalJSONError,
|
|
32
|
+
canonical_json_bytes,
|
|
33
|
+
parse_foreign_json,
|
|
34
|
+
)
|
|
35
|
+
from mostlyright.data_harness.thin.session import StudioSession
|
|
36
|
+
from mostlyright.data_harness.thin.transport import (
|
|
37
|
+
MAX_JSON_RESPONSE_BYTES,
|
|
38
|
+
ThinLaneError,
|
|
39
|
+
Transport,
|
|
40
|
+
UrlLibTransport,
|
|
41
|
+
signed_transfer_url,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
#: The contract version every payload below is written against. ``studio_boundary`` pins the
|
|
45
|
+
#: generated client to the same 3.0.0 and to one OpenAPI source digest; this constant is the thin
|
|
46
|
+
#: lane's half of that pin, and ``tests/test_thin_client.py`` holds the two equal.
|
|
47
|
+
PINNED_CONTRACT_VERSION = "3.0.0"
|
|
48
|
+
|
|
49
|
+
#: The routes. Kept as one table because it is the thing a reviewer checks against the contract.
|
|
50
|
+
START_RUN_PATH = "/v3/runs"
|
|
51
|
+
#: The same collection, read rather than added to. Written out rather than aliased to
|
|
52
|
+
#: :data:`START_RUN_PATH`, because the cross-repo gate checks each name against its own generated
|
|
53
|
+
#: operation and an alias would check one of them twice.
|
|
54
|
+
LIST_RUNS_PATH = "/v3/runs"
|
|
55
|
+
GET_RUN_PATH = "/v3/runs/{run_id}"
|
|
56
|
+
RUN_EVENTS_STREAM_PATH = "/v3/runs/{run_id}/events:stream"
|
|
57
|
+
RUN_EXECUTION_SUMMARY_PATH = "/v3/runs/{run_id}/execution-summary"
|
|
58
|
+
RUN_CANDIDATE_EVIDENCE_PATH = "/v3/runs/{run_id}/candidate-evidence"
|
|
59
|
+
RUN_AGENT_EVIDENCE_PATH = "/v3/runs/{run_id}/agent-evidence"
|
|
60
|
+
RUN_FINDINGS_PATH = "/v3/runs/{run_id}/findings"
|
|
61
|
+
#: Studio renders this itself, from the run's event log and from nothing else, once the run is
|
|
62
|
+
#: terminal. It is the whole reason the vendored client moved to 46e64f1; faa7dbb after it is what
|
|
63
|
+
#: made `acquire`, `approve` and `recipe-approve` writable, and fd14830 after that is what made
|
|
64
|
+
#: `note` able to put a sentence into the log this document is rendered from.
|
|
65
|
+
RUN_NOTEBOOK_PATH = "/v3/runs/{run_id}/notebook"
|
|
66
|
+
TABLE_VERSIONS_PATH = "/v3/tables/{table_id}/versions"
|
|
67
|
+
TABLE_VERSION_PATH = "/v3/tables/{table_id}/versions/{table_version_id}"
|
|
68
|
+
TABLE_VERSION_PREVIEW_PATH = "/v3/tables/{table_id}/versions/{table_version_id}/preview"
|
|
69
|
+
TABLE_VERSION_DOWNLOAD_PATH = "/v3/tables/{table_id}/versions/{table_version_id}/download"
|
|
70
|
+
ARTIFACT_DOWNLOAD_SESSION_PATH = "/v3/artifacts/{artifact_id}/download-session"
|
|
71
|
+
#: Two reads every lane that touches a recipe proposal shares. The proposal itself, for its status
|
|
72
|
+
#: and the ETag its approval is pinned to; and the Build an approval STARTED, which Studio #124
|
|
73
|
+
#: made possible: the dashboard's confirmation can start the first hosted Build in the same
|
|
74
|
+
#: transaction, after which the proposal is `activation_started`, `POST /v3/runs` refuses it, and
|
|
75
|
+
#: this projection is the only way to recover the run that already exists. It answers 404 while
|
|
76
|
+
#: the approval is undecided or unstarted, and that 404 is an ordinary answer here.
|
|
77
|
+
GET_RECIPE_PROPOSAL_PATH = "/v3/recipe-proposals/{recipe_proposal_id}"
|
|
78
|
+
TABLE_RECIPE_APPROVAL_START_PATH = "/v3/table-recipe-approvals/{approval_request_id}/start"
|
|
79
|
+
|
|
80
|
+
#: The Run statuses this contract admits (``run.schema.json#/properties/status``). Held equal to
|
|
81
|
+
#: the schema's own enum by a cross-repo gate, so a filter can never name a status no Run holds.
|
|
82
|
+
RUN_STATUSES = (
|
|
83
|
+
"queued",
|
|
84
|
+
"producer_retry_pending",
|
|
85
|
+
"running",
|
|
86
|
+
"awaiting_candidate_selection",
|
|
87
|
+
"verifying",
|
|
88
|
+
"verifier_retry_pending",
|
|
89
|
+
"releasable",
|
|
90
|
+
"released",
|
|
91
|
+
"repair_required",
|
|
92
|
+
"failed",
|
|
93
|
+
"cancelled",
|
|
94
|
+
"abandoned",
|
|
95
|
+
"human_direction_required",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
#: Studio's own ceiling on one page of any V3 list (``components.parameters.Limit``). Asking for
|
|
99
|
+
#: more is a 422, so it is bounded here and the refusal is a sentence rather than a round trip.
|
|
100
|
+
MAX_LIST_LIMIT = 200
|
|
101
|
+
|
|
102
|
+
#: ``kind`` values a start-run request may name. The Run resource itself admits more (a successor
|
|
103
|
+
#: or a repair successor is created by Studio, never asked for from out here).
|
|
104
|
+
START_RUN_KINDS = ("initial_build", "incremental_refresh", "bounded_backfill")
|
|
105
|
+
|
|
106
|
+
#: The closed capacity vocabulary a start-run may name
|
|
107
|
+
#: (``common.schema.json#/$defs/resource_class``).
|
|
108
|
+
RESOURCE_CLASSES = ("standard", "large")
|
|
109
|
+
|
|
110
|
+
#: ``expected_recipe_digest`` is the one digest in this contract carried WITHOUT the ``sha256:``
|
|
111
|
+
#: prefix. Refusing a prefixed value here turns a silent 422 from Studio into a local sentence.
|
|
112
|
+
_RECIPE_DIGEST = re.compile(r"^[0-9a-f]{64}$")
|
|
113
|
+
_PREFIXED_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
114
|
+
|
|
115
|
+
#: ``common.schema.json#/$defs/idempotency_key``.
|
|
116
|
+
_IDEMPOTENCY_KEY = re.compile(r"^[A-Za-z0-9._:-]{16,128}$")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def new_idempotency_key(prefix: str = "mr-data") -> str:
|
|
120
|
+
"""A fresh idempotency key for one submission.
|
|
121
|
+
|
|
122
|
+
Random rather than derived from the request body: two deliberate submissions of the same
|
|
123
|
+
recipe are two runs, and a content-derived key would silently return the first one's Run and
|
|
124
|
+
look like a success.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
key = f"{prefix}-{secrets.token_hex(16)}"
|
|
128
|
+
if _IDEMPOTENCY_KEY.fullmatch(key) is None: # pragma: no cover - the format is fixed above
|
|
129
|
+
raise ThinLaneError("THIN_REQUEST_INVALID", "the generated idempotency key is malformed")
|
|
130
|
+
return key
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def start_run_body(
|
|
134
|
+
*,
|
|
135
|
+
workspace_id: UUID,
|
|
136
|
+
expected_recipe_digest: str,
|
|
137
|
+
recipe_proposal_id: UUID,
|
|
138
|
+
resource_class: str | None = None,
|
|
139
|
+
) -> dict[str, Any]:
|
|
140
|
+
"""The ``initial_build`` start-run body, with the contract's conditional applied here.
|
|
141
|
+
|
|
142
|
+
⚠ ``initial_build`` is exclusive. The contract requires ``recipe_proposal_id`` and FORBIDS
|
|
143
|
+
``dataset_id``, ``table_id`` and ``requested_range``; the other two kinds invert that exactly.
|
|
144
|
+
This builder covers the initial build only, so the forbidden trio can never be added by
|
|
145
|
+
accident, and a caller that needs a refresh or a backfill gets a builder of its own rather
|
|
146
|
+
than a flag on this one.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
if _PREFIXED_DIGEST.fullmatch(expected_recipe_digest):
|
|
150
|
+
raise ThinLaneError(
|
|
151
|
+
"THIN_REQUEST_INVALID",
|
|
152
|
+
"expected_recipe_digest is bare lowercase hex in this contract, without a sha256: "
|
|
153
|
+
"prefix",
|
|
154
|
+
)
|
|
155
|
+
if _RECIPE_DIGEST.fullmatch(expected_recipe_digest) is None:
|
|
156
|
+
raise ThinLaneError(
|
|
157
|
+
"THIN_REQUEST_INVALID", "expected_recipe_digest is not a 64-character hex digest"
|
|
158
|
+
)
|
|
159
|
+
if resource_class is not None and resource_class not in RESOURCE_CLASSES:
|
|
160
|
+
allowed = ", ".join(RESOURCE_CLASSES)
|
|
161
|
+
raise ThinLaneError("THIN_REQUEST_INVALID", f"resource_class must be one of: {allowed}")
|
|
162
|
+
body: dict[str, Any] = {
|
|
163
|
+
"workspace_id": str(workspace_id),
|
|
164
|
+
"expected_recipe_digest": expected_recipe_digest,
|
|
165
|
+
"kind": "initial_build",
|
|
166
|
+
"recipe_proposal_id": str(recipe_proposal_id),
|
|
167
|
+
}
|
|
168
|
+
if resource_class is not None:
|
|
169
|
+
body["resource_class"] = resource_class
|
|
170
|
+
return body
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class StudioApiClient:
|
|
174
|
+
"""One bearer-authenticated V3 call, and the refusal it becomes when it does not answer.
|
|
175
|
+
|
|
176
|
+
Extracted from :class:`StudioRunClient` rather than copied, because the two lanes that came
|
|
177
|
+
after it -- hosted acquisition and the approval ask -- speak the same wire under the same
|
|
178
|
+
hardening, and a second ``_call`` would be a second place for the byte ceiling, the JSON
|
|
179
|
+
hardening and the Studio-code translation to drift apart. Subclasses add routes; none of them
|
|
180
|
+
adds a transport.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def __init__(self, session: StudioSession, *, transport: Transport | None = None) -> None:
|
|
184
|
+
self._session = session
|
|
185
|
+
self._transport = transport or UrlLibTransport()
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def session(self) -> StudioSession:
|
|
189
|
+
return self._session
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def transport(self) -> Transport:
|
|
193
|
+
"""The bounded transport this client speaks through.
|
|
194
|
+
|
|
195
|
+
Exposed so a signed download made ON BEHALF of a call this client made goes out over the
|
|
196
|
+
same substituted seam -- otherwise a test that fakes Studio would still reach the network
|
|
197
|
+
for the bytes Studio pointed it at.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
return self._transport
|
|
201
|
+
|
|
202
|
+
def _call(
|
|
203
|
+
self,
|
|
204
|
+
method: str,
|
|
205
|
+
path: str,
|
|
206
|
+
*,
|
|
207
|
+
body: Mapping[str, Any] | None = None,
|
|
208
|
+
extra_headers: Mapping[str, str] | None = None,
|
|
209
|
+
query: Mapping[str, Any] | None = None,
|
|
210
|
+
expected: tuple[int, ...],
|
|
211
|
+
shape: type = dict,
|
|
212
|
+
response_headers: dict[str, str] | None = None,
|
|
213
|
+
) -> Any:
|
|
214
|
+
headers = {"Accept": "application/json", **self._session.authorization()}
|
|
215
|
+
if extra_headers:
|
|
216
|
+
headers.update(extra_headers)
|
|
217
|
+
raw_body: bytes | None = None
|
|
218
|
+
if body is not None:
|
|
219
|
+
headers["Content-Type"] = "application/json"
|
|
220
|
+
raw_body = canonical_json_bytes(body)
|
|
221
|
+
url = f"{self._session.studio_base_url}{path}{_query_string(query)}"
|
|
222
|
+
try:
|
|
223
|
+
status, raw, answered_headers = self._transport.request(
|
|
224
|
+
method, url, headers, raw_body, MAX_JSON_RESPONSE_BYTES
|
|
225
|
+
)
|
|
226
|
+
if response_headers is not None:
|
|
227
|
+
response_headers.update(answered_headers)
|
|
228
|
+
except ThinLaneError:
|
|
229
|
+
raise
|
|
230
|
+
except Exception as error:
|
|
231
|
+
raise ThinLaneError(
|
|
232
|
+
"THIN_STUDIO_UNREACHABLE", f"Studio did not answer {path}"
|
|
233
|
+
) from error
|
|
234
|
+
try:
|
|
235
|
+
# ⚠ `parse_foreign_json`, NOT `parse_json`. Every hardening stays -- duplicate keys,
|
|
236
|
+
# byte-order marks, non-strict UTF-8, NaN, out-of-range integers, depth and member
|
|
237
|
+
# ceilings are all refused identically -- and the single difference is that a finite
|
|
238
|
+
# fractional number is admissible. It has to be: a table preview's rows carry the
|
|
239
|
+
# table's own values, and a temperature column is not integers. `parse_json` refused
|
|
240
|
+
# the whole response for one `20.5`, which turned a perfectly good preview into
|
|
241
|
+
# "Studio did not answer with an object". Nothing read here can be sealed either way:
|
|
242
|
+
# `canonical_json_bytes` still refuses a float, so a value parsed here can be shown
|
|
243
|
+
# and can never reach a digest.
|
|
244
|
+
parsed = parse_foreign_json(raw)
|
|
245
|
+
except CanonicalJSONError:
|
|
246
|
+
parsed = None
|
|
247
|
+
if status not in expected:
|
|
248
|
+
raise _studio_refusal(status, parsed, path)
|
|
249
|
+
if not isinstance(parsed, shape):
|
|
250
|
+
wanted = "a list" if shape is list else "an object"
|
|
251
|
+
raise ThinLaneError("THIN_RESPONSE_INVALID", f"{path} did not answer with {wanted}")
|
|
252
|
+
return parsed
|
|
253
|
+
|
|
254
|
+
def _call_list(
|
|
255
|
+
self, path: str, *, query: Mapping[str, Any] | None = None
|
|
256
|
+
) -> list[dict[str, Any]]:
|
|
257
|
+
"""One V3 list route, which answers with a JSON array rather than with an object.
|
|
258
|
+
|
|
259
|
+
Separate from :meth:`_call` rather than folded into it with a flag: every list route in
|
|
260
|
+
this contract answers with a bare array and every single-resource route answers with an
|
|
261
|
+
object, so a reader of either method knows which shape it is looking at without checking a
|
|
262
|
+
parameter. Non-object members are dropped rather than refused -- a list is a page of
|
|
263
|
+
resources, and one malformed member is not a reason to refuse the page -- but the array
|
|
264
|
+
itself being absent is.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
parsed = self._call("GET", path, query=query, expected=(200,), shape=list)
|
|
268
|
+
return [item for item in parsed if isinstance(item, dict)]
|
|
269
|
+
|
|
270
|
+
def recipe_proposal(
|
|
271
|
+
self, recipe_proposal_id: str, *, response_headers: dict[str, str] | None = None
|
|
272
|
+
) -> dict[str, Any]:
|
|
273
|
+
"""One recipe proposal as Studio holds it, with the ETag it answered under."""
|
|
274
|
+
|
|
275
|
+
return self._call(
|
|
276
|
+
"GET",
|
|
277
|
+
GET_RECIPE_PROPOSAL_PATH.format(recipe_proposal_id=recipe_proposal_id),
|
|
278
|
+
expected=(200,),
|
|
279
|
+
response_headers=response_headers,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def approval_start(self, approval_request_id: str) -> dict[str, Any] | None:
|
|
283
|
+
"""The Build a Table recipe approval started, or ``None`` while it has started none.
|
|
284
|
+
|
|
285
|
+
404 is the ordinary answer for an approval that is undecided, unstarted, or local-build,
|
|
286
|
+
and it is returned as ``None`` rather than raised: every caller asks this question to
|
|
287
|
+
decide between "print the build command" and "print the run that already exists".
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
return self._call(
|
|
292
|
+
"GET",
|
|
293
|
+
TABLE_RECIPE_APPROVAL_START_PATH.format(approval_request_id=approval_request_id),
|
|
294
|
+
expected=(200,),
|
|
295
|
+
)
|
|
296
|
+
except ThinLaneError as refusal:
|
|
297
|
+
if refusal.code == "THIN_NOT_FOUND":
|
|
298
|
+
return None
|
|
299
|
+
raise
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class StudioRunClient(StudioApiClient):
|
|
303
|
+
"""Bearer-authenticated V3 reads and one start, over the thin transport."""
|
|
304
|
+
|
|
305
|
+
def start_initial_build(
|
|
306
|
+
self,
|
|
307
|
+
*,
|
|
308
|
+
expected_recipe_digest: str,
|
|
309
|
+
recipe_proposal_id: UUID,
|
|
310
|
+
resource_class: str | None = None,
|
|
311
|
+
idempotency_key: str | None = None,
|
|
312
|
+
) -> dict[str, Any]:
|
|
313
|
+
"""Queue one hosted initial build and return Studio's Run."""
|
|
314
|
+
|
|
315
|
+
return self._call(
|
|
316
|
+
"POST",
|
|
317
|
+
START_RUN_PATH,
|
|
318
|
+
body=start_run_body(
|
|
319
|
+
workspace_id=self._session.workspace_id,
|
|
320
|
+
expected_recipe_digest=expected_recipe_digest,
|
|
321
|
+
recipe_proposal_id=recipe_proposal_id,
|
|
322
|
+
resource_class=resource_class,
|
|
323
|
+
),
|
|
324
|
+
extra_headers={"Idempotency-Key": idempotency_key or new_idempotency_key()},
|
|
325
|
+
expected=(202,),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
def get_run(self, run_id: str) -> dict[str, Any]:
|
|
329
|
+
return self._call("GET", GET_RUN_PATH.format(run_id=run_id), expected=(200,))
|
|
330
|
+
|
|
331
|
+
def list_runs(
|
|
332
|
+
self,
|
|
333
|
+
*,
|
|
334
|
+
status: str | None = None,
|
|
335
|
+
limit: int | None = None,
|
|
336
|
+
dataset_id: str | None = None,
|
|
337
|
+
question_id: str | None = None,
|
|
338
|
+
created_after: str | None = None,
|
|
339
|
+
cursor: str | None = None,
|
|
340
|
+
) -> list[dict[str, Any]]:
|
|
341
|
+
"""One page of this workspace's runs, newest filters applied by Studio rather than here.
|
|
342
|
+
|
|
343
|
+
The workspace is the bearer token's, never a parameter: this contract scopes every list to
|
|
344
|
+
the authority that asked, and passing a workspace would be asking for one we might not
|
|
345
|
+
hold.
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
if status is not None and status not in RUN_STATUSES:
|
|
349
|
+
raise ThinLaneError(
|
|
350
|
+
"THIN_REQUEST_INVALID",
|
|
351
|
+
f"{status!r} is not a run state; this contract admits " + ", ".join(RUN_STATUSES),
|
|
352
|
+
)
|
|
353
|
+
if limit is not None and not 1 <= limit <= MAX_LIST_LIMIT:
|
|
354
|
+
raise ThinLaneError(
|
|
355
|
+
"THIN_REQUEST_INVALID", f"how many runs to list must be from 1 to {MAX_LIST_LIMIT}"
|
|
356
|
+
)
|
|
357
|
+
return self._call_list(
|
|
358
|
+
LIST_RUNS_PATH,
|
|
359
|
+
query={
|
|
360
|
+
"status": status,
|
|
361
|
+
"limit": limit,
|
|
362
|
+
"dataset_id": dataset_id,
|
|
363
|
+
"question_id": question_id,
|
|
364
|
+
"created_after": created_after,
|
|
365
|
+
"cursor": cursor,
|
|
366
|
+
},
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def run_findings(self, run_id: str) -> list[dict[str, Any]]:
|
|
370
|
+
return self._call_list(RUN_FINDINGS_PATH.format(run_id=run_id))
|
|
371
|
+
|
|
372
|
+
def run_agent_evidence(self, run_id: str) -> list[dict[str, Any]]:
|
|
373
|
+
return self._call_list(RUN_AGENT_EVIDENCE_PATH.format(run_id=run_id))
|
|
374
|
+
|
|
375
|
+
def run_notebook(self, run_id: str) -> dict[str, Any]:
|
|
376
|
+
"""The notebook Studio rendered for one terminal run, and a signed download per document.
|
|
377
|
+
|
|
378
|
+
404 here is an ordinary answer rather than a fault: a run that has not finished has no
|
|
379
|
+
notebook, because the live view is the stream. The caller turns that into the sentence.
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
return self._call("GET", RUN_NOTEBOOK_PATH.format(run_id=run_id), expected=(200,))
|
|
383
|
+
|
|
384
|
+
def table_versions(self, table_id: str) -> list[dict[str, Any]]:
|
|
385
|
+
return self._call_list(TABLE_VERSIONS_PATH.format(table_id=table_id))
|
|
386
|
+
|
|
387
|
+
def table_version(self, table_id: str, table_version_id: str) -> dict[str, Any]:
|
|
388
|
+
return self._call(
|
|
389
|
+
"GET",
|
|
390
|
+
TABLE_VERSION_PATH.format(table_id=table_id, table_version_id=table_version_id),
|
|
391
|
+
expected=(200,),
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
def table_version_preview(self, table_id: str, table_version_id: str) -> dict[str, Any]:
|
|
395
|
+
"""The bounded preview Studio publishes beside one released version.
|
|
396
|
+
|
|
397
|
+
Bounded by Studio, not by this client: the preview is an artifact the release wrote, so a
|
|
398
|
+
caller can narrow what it shows and can never widen it.
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
return self._call(
|
|
402
|
+
"GET",
|
|
403
|
+
TABLE_VERSION_PREVIEW_PATH.format(table_id=table_id, table_version_id=table_version_id),
|
|
404
|
+
expected=(200,),
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def execution_summary(self, run_id: str) -> dict[str, Any]:
|
|
408
|
+
return self._call("GET", RUN_EXECUTION_SUMMARY_PATH.format(run_id=run_id), expected=(200,))
|
|
409
|
+
|
|
410
|
+
def candidate_evidence(self, run_id: str) -> dict[str, Any]:
|
|
411
|
+
return self._call("GET", RUN_CANDIDATE_EVIDENCE_PATH.format(run_id=run_id), expected=(200,))
|
|
412
|
+
|
|
413
|
+
def table_version_download(self, table_id: str, table_version_id: str) -> dict[str, Any]:
|
|
414
|
+
return self._call(
|
|
415
|
+
"GET",
|
|
416
|
+
TABLE_VERSION_DOWNLOAD_PATH.format(
|
|
417
|
+
table_id=table_id, table_version_id=table_version_id
|
|
418
|
+
),
|
|
419
|
+
expected=(200,),
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
def artifact_download_session(self, artifact_id: str) -> dict[str, Any]:
|
|
423
|
+
"""Ask Studio to sign one short-lived download of one artifact.
|
|
424
|
+
|
|
425
|
+
The session is created, never chosen: kind, purpose and classification come from sealed
|
|
426
|
+
server metadata, so this call carries no body at all beyond its idempotency key.
|
|
427
|
+
"""
|
|
428
|
+
|
|
429
|
+
session = self._call(
|
|
430
|
+
"POST",
|
|
431
|
+
ARTIFACT_DOWNLOAD_SESSION_PATH.format(artifact_id=artifact_id),
|
|
432
|
+
body={},
|
|
433
|
+
extra_headers={"Idempotency-Key": new_idempotency_key("mr-data-dl")},
|
|
434
|
+
expected=(201,),
|
|
435
|
+
)
|
|
436
|
+
if session.get("direction") != "download" or session.get("method") != "GET":
|
|
437
|
+
raise ThinLaneError(
|
|
438
|
+
"THIN_RESPONSE_INVALID", "Studio signed a session that is not a download"
|
|
439
|
+
)
|
|
440
|
+
signed_transfer_url(session.get("signed_url", ""), "the signed download URL")
|
|
441
|
+
return session
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _query_string(query: Mapping[str, Any] | None) -> str:
|
|
445
|
+
"""Render the filters one list route was given, and nothing for the ones it was not.
|
|
446
|
+
|
|
447
|
+
``None`` means "not asked for" and is dropped rather than sent empty: this contract's list
|
|
448
|
+
parameters are all optional, and `?status=` is a different request from no `status` at all.
|
|
449
|
+
Sorted, so two identical requests produce one identical URL -- which is what makes a recorded
|
|
450
|
+
request in a test an assertion about the request rather than about dict ordering.
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
if not query:
|
|
454
|
+
return ""
|
|
455
|
+
selected = {key: value for key, value in sorted(query.items()) if value is not None}
|
|
456
|
+
if not selected:
|
|
457
|
+
return ""
|
|
458
|
+
return "?" + urlencode({key: str(value) for key, value in selected.items()})
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _studio_refusal(status: int, parsed: Any, path: str) -> ThinLaneError:
|
|
462
|
+
"""Turn one ``application/problem+json`` body into a typed refusal.
|
|
463
|
+
|
|
464
|
+
Studio's error code is a closed vocabulary and is far more useful than the status, so it is
|
|
465
|
+
carried through verbatim as the refusal detail's first token. The message is Studio's own and
|
|
466
|
+
is bounded by its contract at 2000 characters; it is not reformatted here.
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
studio_code = parsed.get("code") if isinstance(parsed, dict) else None
|
|
470
|
+
if not isinstance(studio_code, str):
|
|
471
|
+
studio_code = None
|
|
472
|
+
if status in {401, 403}:
|
|
473
|
+
refusal = ThinLaneError(
|
|
474
|
+
"THIN_AUTHORIZATION_DENIED",
|
|
475
|
+
"the Studio token does not carry the authority this call needs",
|
|
476
|
+
)
|
|
477
|
+
elif status == 404:
|
|
478
|
+
refusal = ThinLaneError("THIN_NOT_FOUND", f"Studio has no such resource for {path}")
|
|
479
|
+
elif studio_code is not None:
|
|
480
|
+
message = parsed.get("message") if isinstance(parsed, dict) else None
|
|
481
|
+
detail = message if isinstance(message, str) and message else "Studio refused the call"
|
|
482
|
+
refusal = ThinLaneError(f"THIN_STUDIO_{studio_code}", detail)
|
|
483
|
+
else:
|
|
484
|
+
refusal = ThinLaneError("THIN_STUDIO_REFUSED", f"Studio answered {path} with HTTP {status}")
|
|
485
|
+
# The status and Studio's own code ride on the refusal as facts about the answer, beside the
|
|
486
|
+
# translated code a script branches on: a write lane deciding whether a refused request was
|
|
487
|
+
# applied needs to tell a validation 422 from a gateway 504 carrying the same translated code.
|
|
488
|
+
refusal.http_status = status # type: ignore[attr-defined]
|
|
489
|
+
refusal.studio_code = studio_code # type: ignore[attr-defined]
|
|
490
|
+
return refusal
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
__all__ = [
|
|
494
|
+
"ARTIFACT_DOWNLOAD_SESSION_PATH",
|
|
495
|
+
"GET_RECIPE_PROPOSAL_PATH",
|
|
496
|
+
"GET_RUN_PATH",
|
|
497
|
+
"LIST_RUNS_PATH",
|
|
498
|
+
"MAX_LIST_LIMIT",
|
|
499
|
+
"PINNED_CONTRACT_VERSION",
|
|
500
|
+
"RESOURCE_CLASSES",
|
|
501
|
+
"RUN_AGENT_EVIDENCE_PATH",
|
|
502
|
+
"RUN_CANDIDATE_EVIDENCE_PATH",
|
|
503
|
+
"RUN_EVENTS_STREAM_PATH",
|
|
504
|
+
"RUN_EXECUTION_SUMMARY_PATH",
|
|
505
|
+
"RUN_FINDINGS_PATH",
|
|
506
|
+
"RUN_NOTEBOOK_PATH",
|
|
507
|
+
"RUN_STATUSES",
|
|
508
|
+
"START_RUN_KINDS",
|
|
509
|
+
"START_RUN_PATH",
|
|
510
|
+
"TABLE_RECIPE_APPROVAL_START_PATH",
|
|
511
|
+
"TABLE_VERSIONS_PATH",
|
|
512
|
+
"TABLE_VERSION_DOWNLOAD_PATH",
|
|
513
|
+
"TABLE_VERSION_PATH",
|
|
514
|
+
"TABLE_VERSION_PREVIEW_PATH",
|
|
515
|
+
"StudioApiClient",
|
|
516
|
+
"StudioRunClient",
|
|
517
|
+
"new_idempotency_key",
|
|
518
|
+
"start_run_body",
|
|
519
|
+
]
|