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,418 @@
|
|
|
1
|
+
"""Closed declarative connector configurations for recorded stream capture.
|
|
2
|
+
|
|
3
|
+
Recorder code is generic; a source is a closed declarative configuration that is digested and
|
|
4
|
+
bound into capture evidence. This module holds that configuration contract and the connector
|
|
5
|
+
configurations this build knows about.
|
|
6
|
+
|
|
7
|
+
Nothing here opens a connection. ``endpoint_url`` and ``subscribe_template`` are *grammar*: the
|
|
8
|
+
address a trusted Recorder would hold and the exact subscription message it would send. This
|
|
9
|
+
build records the grammar, checks it, and digests it; the socket belongs to a trusted Recorder
|
|
10
|
+
role that does not exist yet, and the deterministic path is never allowed to open one. The
|
|
11
|
+
validation below is therefore about whether a configuration is well formed and bounded, never
|
|
12
|
+
about whether an address answers.
|
|
13
|
+
|
|
14
|
+
The subscription member set is not part of the configuration. Which markets a capture
|
|
15
|
+
subscribes to is a declared bounded set pinned in the Recipe's canonical query, exactly as
|
|
16
|
+
``public.https.collection`` pins its members: source content cannot add a member.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from collections.abc import Mapping
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from types import MappingProxyType
|
|
25
|
+
from typing import Any
|
|
26
|
+
from urllib.parse import urlsplit
|
|
27
|
+
|
|
28
|
+
from mostlyright.data_harness.canonical import canonical_sha256
|
|
29
|
+
from mostlyright.data_harness.sources.contracts import SourceContractError
|
|
30
|
+
|
|
31
|
+
STREAM_CONNECTOR_VERSION = "harness-stream-connector.v1"
|
|
32
|
+
|
|
33
|
+
# Every message this build will admit is classified into exactly one of these three sets by the
|
|
34
|
+
# connector's ``message_type_field``. A message whose type is in none of them is refused rather
|
|
35
|
+
# than guessed at: an unclassified message is either a venue change or a wrong connector, and
|
|
36
|
+
# both deserve a stop.
|
|
37
|
+
MESSAGE_CLASSES = ("snapshot", "delta", "ignored")
|
|
38
|
+
|
|
39
|
+
# The three gap-repair modes cover the known universe of venue behaviour. ``none`` is not a
|
|
40
|
+
# failure to configure: for an event-only stream the gap really is permanent, and saying so is
|
|
41
|
+
# the honest answer.
|
|
42
|
+
GAP_REPAIR_MODES = frozenset({"snapshot_on_subscribe", "cursor_resume", "none"})
|
|
43
|
+
|
|
44
|
+
# Where an event's identity comes from. ``provider`` extracts a venue-assigned ID; ``derived``
|
|
45
|
+
# assigns ``{epoch}:{counter}`` because the venue offers nothing stable.
|
|
46
|
+
EVENT_ID_RULES = frozenset({"provider", "derived"})
|
|
47
|
+
|
|
48
|
+
# How to read the event-time field. The interpretation is declared, never sniffed: a field of
|
|
49
|
+
# digits is milliseconds or seconds depending on the venue, and guessing wrong moves every event
|
|
50
|
+
# by a factor of a thousand.
|
|
51
|
+
EVENT_TIME_KINDS: Mapping[str, re.Pattern[str]] = MappingProxyType(
|
|
52
|
+
{
|
|
53
|
+
"rfc3339_utc": re.compile(
|
|
54
|
+
r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,9})?Z"
|
|
55
|
+
),
|
|
56
|
+
"epoch_milliseconds_text": re.compile(r"[0-9]{1,15}"),
|
|
57
|
+
"epoch_seconds_text": re.compile(r"[0-9]{1,12}"),
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
_CONNECTOR_ID = re.compile(r"[a-z][a-z0-9]*(?:\.[a-z0-9][a-z0-9-]*)+")
|
|
62
|
+
# The stream identifier addresses one directory beneath the capture root, so it is a strict
|
|
63
|
+
# single path component: no dot, no separator, nothing that could climb out of the root.
|
|
64
|
+
_STREAM_ID = re.compile(r"[a-z][a-z0-9-]{0,63}")
|
|
65
|
+
# A recorded address names a host, never a literal one. The trailing alphabetic label is what
|
|
66
|
+
# refuses ``127.0.0.1`` and its friends: an address that is already a number answers to no naming
|
|
67
|
+
# policy, so a Recorder must not be pointed at one from a configuration.
|
|
68
|
+
_HOSTNAME = re.compile(
|
|
69
|
+
r"[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\.[a-z]{2,63}"
|
|
70
|
+
)
|
|
71
|
+
_MESSAGE_FIELD = re.compile(r"[A-Za-z_][A-Za-z0-9_]{0,63}")
|
|
72
|
+
_MESSAGE_TYPE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}")
|
|
73
|
+
# A capture root reserves this name for the durable per-source bookmark ledger, so no stream may
|
|
74
|
+
# claim it as its own directory.
|
|
75
|
+
RESERVED_STREAM_IDS = frozenset({"bookmarks", "batches"})
|
|
76
|
+
|
|
77
|
+
# The hard bound on one sealed batch, mirroring ``stream.MAX_BATCH_EVENTS``. It is restated as
|
|
78
|
+
# a bound on the *configuration* so an unbuildable connector is refused where it is written down
|
|
79
|
+
# rather than at the first rotation.
|
|
80
|
+
MAX_CONFIGURED_BATCH_EVENTS = 100_000
|
|
81
|
+
MAX_CONFIGURED_BATCH_BYTES = 64 * 1024 * 1024
|
|
82
|
+
MAX_CONFIGURED_BATCH_SECONDS = 24 * 60 * 60
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class StreamConnectorConfig:
|
|
87
|
+
"""One venue's closed capture grammar, digested and bound into capture evidence."""
|
|
88
|
+
|
|
89
|
+
connector_id: str
|
|
90
|
+
stream_id: str
|
|
91
|
+
endpoint_url: str
|
|
92
|
+
subscribe_template: Mapping[str, Any]
|
|
93
|
+
subscription_member_key: str
|
|
94
|
+
message_type_field: str
|
|
95
|
+
snapshot_message_types: tuple[str, ...]
|
|
96
|
+
delta_message_types: tuple[str, ...]
|
|
97
|
+
ignored_message_types: tuple[str, ...]
|
|
98
|
+
event_id_rule: str
|
|
99
|
+
provider_event_id_field: str | None
|
|
100
|
+
event_time_field: str
|
|
101
|
+
event_time_kind: str
|
|
102
|
+
gap_repair: str
|
|
103
|
+
heartbeat_seconds: int
|
|
104
|
+
reconnect_backoff_seconds: int
|
|
105
|
+
max_batch_events: int
|
|
106
|
+
max_batch_bytes: int
|
|
107
|
+
max_batch_seconds: int
|
|
108
|
+
data_format: str = "ndjson"
|
|
109
|
+
schema_version: str = STREAM_CONNECTOR_VERSION
|
|
110
|
+
|
|
111
|
+
def __post_init__(self) -> None:
|
|
112
|
+
if self.schema_version != STREAM_CONNECTOR_VERSION:
|
|
113
|
+
raise SourceContractError(
|
|
114
|
+
"STREAM_CONNECTOR_VERSION",
|
|
115
|
+
"connector.schema_version",
|
|
116
|
+
"is unsupported",
|
|
117
|
+
)
|
|
118
|
+
_matched(self.connector_id, _CONNECTOR_ID, "connector.connector_id")
|
|
119
|
+
_matched(self.stream_id, _STREAM_ID, "connector.stream_id")
|
|
120
|
+
if self.stream_id in RESERVED_STREAM_IDS:
|
|
121
|
+
raise SourceContractError(
|
|
122
|
+
"STREAM_CONNECTOR",
|
|
123
|
+
"connector.stream_id",
|
|
124
|
+
f"must not be one of the reserved capture names {sorted(RESERVED_STREAM_IDS)}",
|
|
125
|
+
)
|
|
126
|
+
_subscription_endpoint(self.endpoint_url)
|
|
127
|
+
_subscribe_template(self.subscribe_template, self.subscription_member_key)
|
|
128
|
+
_matched(self.message_type_field, _MESSAGE_FIELD, "connector.message_type_field")
|
|
129
|
+
_matched(self.event_time_field, _MESSAGE_FIELD, "connector.event_time_field")
|
|
130
|
+
self._validate_message_types()
|
|
131
|
+
if self.event_id_rule not in EVENT_ID_RULES:
|
|
132
|
+
raise SourceContractError(
|
|
133
|
+
"STREAM_CONNECTOR",
|
|
134
|
+
"connector.event_id_rule",
|
|
135
|
+
f"must be one of {sorted(EVENT_ID_RULES)}",
|
|
136
|
+
)
|
|
137
|
+
if (self.provider_event_id_field is None) != (self.event_id_rule == "derived"):
|
|
138
|
+
raise SourceContractError(
|
|
139
|
+
"STREAM_CONNECTOR",
|
|
140
|
+
"connector.provider_event_id_field",
|
|
141
|
+
"must name a field exactly when the event-ID rule extracts a provider ID",
|
|
142
|
+
)
|
|
143
|
+
if self.provider_event_id_field is not None:
|
|
144
|
+
_matched(
|
|
145
|
+
self.provider_event_id_field,
|
|
146
|
+
_MESSAGE_FIELD,
|
|
147
|
+
"connector.provider_event_id_field",
|
|
148
|
+
)
|
|
149
|
+
if self.event_time_kind not in EVENT_TIME_KINDS:
|
|
150
|
+
raise SourceContractError(
|
|
151
|
+
"STREAM_CONNECTOR",
|
|
152
|
+
"connector.event_time_kind",
|
|
153
|
+
f"must be one of {sorted(EVENT_TIME_KINDS)}",
|
|
154
|
+
)
|
|
155
|
+
if self.gap_repair not in GAP_REPAIR_MODES:
|
|
156
|
+
raise SourceContractError(
|
|
157
|
+
"STREAM_CONNECTOR",
|
|
158
|
+
"connector.gap_repair",
|
|
159
|
+
f"must be one of {sorted(GAP_REPAIR_MODES)}",
|
|
160
|
+
)
|
|
161
|
+
if self.gap_repair == "snapshot_on_subscribe" and not self.snapshot_message_types:
|
|
162
|
+
raise SourceContractError(
|
|
163
|
+
"STREAM_CONNECTOR",
|
|
164
|
+
"connector.gap_repair",
|
|
165
|
+
"snapshot repair requires at least one snapshot message type",
|
|
166
|
+
)
|
|
167
|
+
if self.data_format not in {"json", "ndjson"}:
|
|
168
|
+
raise SourceContractError(
|
|
169
|
+
"STREAM_CONNECTOR",
|
|
170
|
+
"connector.data_format",
|
|
171
|
+
"must be json or ndjson",
|
|
172
|
+
)
|
|
173
|
+
self._validate_bounds()
|
|
174
|
+
|
|
175
|
+
def _validate_message_types(self) -> None:
|
|
176
|
+
groups = (
|
|
177
|
+
("snapshot_message_types", self.snapshot_message_types),
|
|
178
|
+
("delta_message_types", self.delta_message_types),
|
|
179
|
+
("ignored_message_types", self.ignored_message_types),
|
|
180
|
+
)
|
|
181
|
+
seen: set[str] = set()
|
|
182
|
+
for name, values in groups:
|
|
183
|
+
if not isinstance(values, tuple) or len(values) > 64:
|
|
184
|
+
raise SourceContractError(
|
|
185
|
+
"STREAM_CONNECTOR",
|
|
186
|
+
f"connector.{name}",
|
|
187
|
+
"must be a bounded immutable tuple",
|
|
188
|
+
)
|
|
189
|
+
for value in values:
|
|
190
|
+
_matched(value, _MESSAGE_TYPE, f"connector.{name}")
|
|
191
|
+
if value in seen:
|
|
192
|
+
raise SourceContractError(
|
|
193
|
+
"STREAM_CONNECTOR",
|
|
194
|
+
f"connector.{name}",
|
|
195
|
+
f"message type {value!r} is classified more than once",
|
|
196
|
+
)
|
|
197
|
+
seen.add(value)
|
|
198
|
+
if not self.snapshot_message_types and not self.delta_message_types:
|
|
199
|
+
raise SourceContractError(
|
|
200
|
+
"STREAM_CONNECTOR",
|
|
201
|
+
"connector.delta_message_types",
|
|
202
|
+
"a connector that admits no message at all captures nothing",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def _validate_bounds(self) -> None:
|
|
206
|
+
bounds = (
|
|
207
|
+
("heartbeat_seconds", self.heartbeat_seconds, 1, 3_600),
|
|
208
|
+
("reconnect_backoff_seconds", self.reconnect_backoff_seconds, 1, 3_600),
|
|
209
|
+
("max_batch_events", self.max_batch_events, 1, MAX_CONFIGURED_BATCH_EVENTS),
|
|
210
|
+
("max_batch_bytes", self.max_batch_bytes, 1, MAX_CONFIGURED_BATCH_BYTES),
|
|
211
|
+
("max_batch_seconds", self.max_batch_seconds, 1, MAX_CONFIGURED_BATCH_SECONDS),
|
|
212
|
+
)
|
|
213
|
+
for name, value, low, high in bounds:
|
|
214
|
+
if type(value) is not int or not low <= value <= high:
|
|
215
|
+
raise SourceContractError(
|
|
216
|
+
"STREAM_CONNECTOR",
|
|
217
|
+
f"connector.{name}",
|
|
218
|
+
f"must be an integer between {low} and {high}",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def digest(self) -> str:
|
|
223
|
+
return canonical_sha256(self.to_dict())
|
|
224
|
+
|
|
225
|
+
def message_class(self, message_type: str) -> str:
|
|
226
|
+
"""Return which of the three closed classes a message type belongs to.
|
|
227
|
+
|
|
228
|
+
Raises rather than defaulting. An unclassified message type means the venue changed
|
|
229
|
+
its message set or the wrong connector was chosen, and both are stops.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
if message_type in self.snapshot_message_types:
|
|
233
|
+
return "snapshot"
|
|
234
|
+
if message_type in self.delta_message_types:
|
|
235
|
+
return "delta"
|
|
236
|
+
if message_type in self.ignored_message_types:
|
|
237
|
+
return "ignored"
|
|
238
|
+
raise SourceContractError(
|
|
239
|
+
"STREAM_MESSAGE_TYPE",
|
|
240
|
+
"recording.message",
|
|
241
|
+
f"message type {message_type!r} is not classified by connector {self.connector_id}",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def to_dict(self) -> dict[str, Any]:
|
|
245
|
+
return {
|
|
246
|
+
"schema_version": self.schema_version,
|
|
247
|
+
"connector_id": self.connector_id,
|
|
248
|
+
"stream_id": self.stream_id,
|
|
249
|
+
"endpoint_url": self.endpoint_url,
|
|
250
|
+
"subscribe_template": dict(self.subscribe_template),
|
|
251
|
+
"subscription_member_key": self.subscription_member_key,
|
|
252
|
+
"message_type_field": self.message_type_field,
|
|
253
|
+
"snapshot_message_types": list(self.snapshot_message_types),
|
|
254
|
+
"delta_message_types": list(self.delta_message_types),
|
|
255
|
+
"ignored_message_types": list(self.ignored_message_types),
|
|
256
|
+
"event_id_rule": self.event_id_rule,
|
|
257
|
+
"provider_event_id_field": self.provider_event_id_field,
|
|
258
|
+
"event_time_field": self.event_time_field,
|
|
259
|
+
"event_time_kind": self.event_time_kind,
|
|
260
|
+
"gap_repair": self.gap_repair,
|
|
261
|
+
"heartbeat_seconds": self.heartbeat_seconds,
|
|
262
|
+
"reconnect_backoff_seconds": self.reconnect_backoff_seconds,
|
|
263
|
+
"max_batch_events": self.max_batch_events,
|
|
264
|
+
"max_batch_bytes": self.max_batch_bytes,
|
|
265
|
+
"max_batch_seconds": self.max_batch_seconds,
|
|
266
|
+
"data_format": self.data_format,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _matched(value: Any, pattern: re.Pattern[str], path: str) -> None:
|
|
271
|
+
if not isinstance(value, str) or pattern.fullmatch(value) is None:
|
|
272
|
+
raise SourceContractError("STREAM_CONNECTOR", path, "does not match its exact grammar")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _subscription_endpoint(value: Any) -> None:
|
|
276
|
+
"""Check the recorded address is a well-formed credential-free secure socket address.
|
|
277
|
+
|
|
278
|
+
Grammar only. Nothing in this build resolves, connects to, or otherwise touches the
|
|
279
|
+
address; the check exists so a malformed or credential-bearing address is refused where it
|
|
280
|
+
is written down rather than carried into a Recorder that would.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
if not isinstance(value, str) or not 1 <= len(value) <= 2_048:
|
|
284
|
+
raise SourceContractError(
|
|
285
|
+
"STREAM_ENDPOINT",
|
|
286
|
+
"connector.endpoint_url",
|
|
287
|
+
"must be a bounded non-empty address",
|
|
288
|
+
)
|
|
289
|
+
parsed = urlsplit(value)
|
|
290
|
+
if parsed.scheme != "wss":
|
|
291
|
+
raise SourceContractError(
|
|
292
|
+
"STREAM_ENDPOINT",
|
|
293
|
+
"connector.endpoint_url",
|
|
294
|
+
"must name the wss scheme; a Recorder holds no other kind of connection",
|
|
295
|
+
)
|
|
296
|
+
if parsed.username is not None or parsed.password is not None:
|
|
297
|
+
raise SourceContractError(
|
|
298
|
+
"STREAM_ENDPOINT",
|
|
299
|
+
"connector.endpoint_url",
|
|
300
|
+
"must carry no inline credential",
|
|
301
|
+
)
|
|
302
|
+
if parsed.query or parsed.fragment:
|
|
303
|
+
raise SourceContractError(
|
|
304
|
+
"STREAM_ENDPOINT",
|
|
305
|
+
"connector.endpoint_url",
|
|
306
|
+
"must carry no query or fragment; subscription is a message, not an address",
|
|
307
|
+
)
|
|
308
|
+
try:
|
|
309
|
+
port = parsed.port
|
|
310
|
+
except ValueError:
|
|
311
|
+
raise SourceContractError(
|
|
312
|
+
"STREAM_ENDPOINT",
|
|
313
|
+
"connector.endpoint_url",
|
|
314
|
+
"carries an unreadable port",
|
|
315
|
+
) from None
|
|
316
|
+
if port not in {None, 443}:
|
|
317
|
+
raise SourceContractError(
|
|
318
|
+
"STREAM_ENDPOINT",
|
|
319
|
+
"connector.endpoint_url",
|
|
320
|
+
"must use the default secure port",
|
|
321
|
+
)
|
|
322
|
+
hostname = parsed.hostname
|
|
323
|
+
if hostname is None or _HOSTNAME.fullmatch(hostname) is None:
|
|
324
|
+
raise SourceContractError(
|
|
325
|
+
"STREAM_ENDPOINT",
|
|
326
|
+
"connector.endpoint_url",
|
|
327
|
+
"must name a plain lowercase dotted host, not a literal address",
|
|
328
|
+
)
|
|
329
|
+
if parsed.path and re.fullmatch(r"(/[A-Za-z0-9._~-]+)+", parsed.path) is None:
|
|
330
|
+
raise SourceContractError(
|
|
331
|
+
"STREAM_ENDPOINT",
|
|
332
|
+
"connector.endpoint_url",
|
|
333
|
+
"must carry a plain unescaped path",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _subscribe_template(value: Any, member_key: Any) -> None:
|
|
338
|
+
"""Check the subscription message is a flat closed template with one member slot."""
|
|
339
|
+
|
|
340
|
+
if not isinstance(value, Mapping) or not 1 <= len(value) <= 16:
|
|
341
|
+
raise SourceContractError(
|
|
342
|
+
"STREAM_SUBSCRIBE",
|
|
343
|
+
"connector.subscribe_template",
|
|
344
|
+
"must be a small non-empty mapping",
|
|
345
|
+
)
|
|
346
|
+
for key, item in value.items():
|
|
347
|
+
_matched(key, _MESSAGE_FIELD, "connector.subscribe_template")
|
|
348
|
+
if not isinstance(item, str | list) or (isinstance(item, list) and item):
|
|
349
|
+
raise SourceContractError(
|
|
350
|
+
"STREAM_SUBSCRIBE",
|
|
351
|
+
"connector.subscribe_template",
|
|
352
|
+
"values must be fixed text or the one empty member slot",
|
|
353
|
+
)
|
|
354
|
+
if not isinstance(member_key, str) or member_key not in value:
|
|
355
|
+
raise SourceContractError(
|
|
356
|
+
"STREAM_SUBSCRIBE",
|
|
357
|
+
"connector.subscription_member_key",
|
|
358
|
+
"must name a key of the subscription message",
|
|
359
|
+
)
|
|
360
|
+
if not isinstance(value[member_key], list):
|
|
361
|
+
raise SourceContractError(
|
|
362
|
+
"STREAM_SUBSCRIBE",
|
|
363
|
+
"connector.subscription_member_key",
|
|
364
|
+
"must name the empty member slot the Recipe's declared set fills",
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# Polymarket's public market channel. Credential-free, and the first target because no public
|
|
369
|
+
# archive of its order-book depth exists: whoever captures it from now owns history nobody else
|
|
370
|
+
# can reconstruct.
|
|
371
|
+
#
|
|
372
|
+
# The venue re-sends a full ``book`` snapshot on every subscribe, so a new connection repairs
|
|
373
|
+
# its own gap and the epoch is self-repairing. There are no venue sequence numbers, so event
|
|
374
|
+
# identity is Recorder-assigned ``{epoch}:{counter}``.
|
|
375
|
+
#
|
|
376
|
+
# Endpoint and message shapes are recorded here from the venue's published documentation and are
|
|
377
|
+
# not verified against the live venue by this build, which reaches no network at all.
|
|
378
|
+
POLYMARKET_CLOB_MARKET = StreamConnectorConfig(
|
|
379
|
+
connector_id="polymarket.clob-market",
|
|
380
|
+
stream_id="polymarket-clob-market",
|
|
381
|
+
endpoint_url="wss://ws-subscriptions-clob.polymarket.com/ws/market",
|
|
382
|
+
subscribe_template={"type": "market", "assets_ids": []},
|
|
383
|
+
subscription_member_key="assets_ids",
|
|
384
|
+
message_type_field="event_type",
|
|
385
|
+
snapshot_message_types=("book",),
|
|
386
|
+
delta_message_types=("price_change", "tick_size_change", "last_trade_price"),
|
|
387
|
+
ignored_message_types=("pong",),
|
|
388
|
+
event_id_rule="derived",
|
|
389
|
+
provider_event_id_field=None,
|
|
390
|
+
event_time_field="timestamp",
|
|
391
|
+
event_time_kind="epoch_milliseconds_text",
|
|
392
|
+
gap_repair="snapshot_on_subscribe",
|
|
393
|
+
heartbeat_seconds=10,
|
|
394
|
+
reconnect_backoff_seconds=5,
|
|
395
|
+
max_batch_events=5_000,
|
|
396
|
+
max_batch_bytes=8 * 1024 * 1024,
|
|
397
|
+
max_batch_seconds=300,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
KNOWN_STREAM_CONNECTORS: Mapping[str, StreamConnectorConfig] = MappingProxyType(
|
|
402
|
+
{POLYMARKET_CLOB_MARKET.connector_id: POLYMARKET_CLOB_MARKET}
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
STREAM_CONNECTOR_IDS: tuple[str, ...] = tuple(sorted(KNOWN_STREAM_CONNECTORS))
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def stream_connector(connector_id: str) -> StreamConnectorConfig:
|
|
409
|
+
"""Return the one connector configuration this build knows by that name."""
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
return KNOWN_STREAM_CONNECTORS[connector_id]
|
|
413
|
+
except KeyError:
|
|
414
|
+
raise SourceContractError(
|
|
415
|
+
"STREAM_CONNECTOR_UNKNOWN",
|
|
416
|
+
"connector_id",
|
|
417
|
+
f"connector {connector_id!r} is not one of {list(STREAM_CONNECTOR_IDS)}",
|
|
418
|
+
) from None
|