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.
Files changed (314) hide show
  1. mostlyright/data_harness/__init__.py +158 -0
  2. mostlyright/data_harness/acquisition/__init__.py +55 -0
  3. mostlyright/data_harness/acquisition/http.py +2773 -0
  4. mostlyright/data_harness/acquisition/parsing.py +809 -0
  5. mostlyright/data_harness/acquisition/ranges.py +495 -0
  6. mostlyright/data_harness/acquisition/result_download.py +360 -0
  7. mostlyright/data_harness/acquisition/retention_admission.py +248 -0
  8. mostlyright/data_harness/acquisition/sandbox.py +4888 -0
  9. mostlyright/data_harness/acquisition/url_policy.py +530 -0
  10. mostlyright/data_harness/agent_runtime.py +2743 -0
  11. mostlyright/data_harness/assets/logo-ink.svg +31 -0
  12. mostlyright/data_harness/backends/__init__.py +28 -0
  13. mostlyright/data_harness/backends/pandas_backend.py +350 -0
  14. mostlyright/data_harness/backends/polars_backend.py +366 -0
  15. mostlyright/data_harness/backends/protocol.py +124 -0
  16. mostlyright/data_harness/backends/reference.py +83 -0
  17. mostlyright/data_harness/backends/registry.py +55 -0
  18. mostlyright/data_harness/backends/restrictions.py +126 -0
  19. mostlyright/data_harness/canonical.py +333 -0
  20. mostlyright/data_harness/catalog_job.py +625 -0
  21. mostlyright/data_harness/cli.py +5398 -0
  22. mostlyright/data_harness/contracts.py +53 -0
  23. mostlyright/data_harness/coordinator.py +1307 -0
  24. mostlyright/data_harness/deploy.py +924 -0
  25. mostlyright/data_harness/deploy_target.py +312 -0
  26. mostlyright/data_harness/deployment_evidence.py +1067 -0
  27. mostlyright/data_harness/event_presentation.py +576 -0
  28. mostlyright/data_harness/events.py +2152 -0
  29. mostlyright/data_harness/fast_delimited.py +239 -0
  30. mostlyright/data_harness/fleet.py +237 -0
  31. mostlyright/data_harness/formats.py +236 -0
  32. mostlyright/data_harness/governors.py +1163 -0
  33. mostlyright/data_harness/hosted_bootstrap.py +972 -0
  34. mostlyright/data_harness/hosted_crawler.py +1115 -0
  35. mostlyright/data_harness/hosted_crawler_container_smoke.py +351 -0
  36. mostlyright/data_harness/hosted_crawler_fetch.py +423 -0
  37. mostlyright/data_harness/hosted_crawler_job.py +1277 -0
  38. mostlyright/data_harness/hosted_crawler_protocol.py +676 -0
  39. mostlyright/data_harness/hosted_dataset.py +1500 -0
  40. mostlyright/data_harness/hosted_deploy.py +3037 -0
  41. mostlyright/data_harness/hosted_handoff.py +62 -0
  42. mostlyright/data_harness/hosted_ingestion_contract.py +504 -0
  43. mostlyright/data_harness/hosted_ingestion_job.py +356 -0
  44. mostlyright/data_harness/hosted_ingestion_job_smoke.py +40 -0
  45. mostlyright/data_harness/hosted_session_container_smoke.py +194 -0
  46. mostlyright/data_harness/hosted_session_worker.py +3554 -0
  47. mostlyright/data_harness/hosted_session_worker_job_smoke.py +46 -0
  48. mostlyright/data_harness/hosted_worker.py +6784 -0
  49. mostlyright/data_harness/ingestion/__init__.py +56 -0
  50. mostlyright/data_harness/ingestion/contracts.py +461 -0
  51. mostlyright/data_harness/ingestion/faults.py +42 -0
  52. mostlyright/data_harness/ingestion/gcs_store.py +1162 -0
  53. mostlyright/data_harness/ingestion/spool.py +130 -0
  54. mostlyright/data_harness/ingestion/store.py +885 -0
  55. mostlyright/data_harness/key_seam.py +434 -0
  56. mostlyright/data_harness/linux_process_boundary.py +262 -0
  57. mostlyright/data_harness/local_contracts.py +2880 -0
  58. mostlyright/data_harness/local_search/__init__.py +5 -0
  59. mostlyright/data_harness/local_search/build_index.py +1087 -0
  60. mostlyright/data_harness/local_search/contracts.py +920 -0
  61. mostlyright/data_harness/local_search/query_trace.py +266 -0
  62. mostlyright/data_harness/local_search/retrieval.py +700 -0
  63. mostlyright/data_harness/local_search/sealed.py +474 -0
  64. mostlyright/data_harness/local_search/service.py +784 -0
  65. mostlyright/data_harness/nbrender/CONTRACT.md +212 -0
  66. mostlyright/data_harness/nbrender/__init__.py +12 -0
  67. mostlyright/data_harness/nbrender/chrome.py +359 -0
  68. mostlyright/data_harness/nbrender/code_body.py +266 -0
  69. mostlyright/data_harness/nbrender/document.py +407 -0
  70. mostlyright/data_harness/nbrender/frame.py +275 -0
  71. mostlyright/data_harness/nbrender/interactive.py +337 -0
  72. mostlyright/data_harness/nbrender/markdown_body.py +477 -0
  73. mostlyright/data_harness/nbrender/mr_components.py +134 -0
  74. mostlyright/data_harness/nbrender/outputs_data.py +595 -0
  75. mostlyright/data_harness/nbrender/outputs_rich.py +906 -0
  76. mostlyright/data_harness/nbrender/outputs_source.py +260 -0
  77. mostlyright/data_harness/nbrender/outputs_stage.py +176 -0
  78. mostlyright/data_harness/nbrender/outputs_text.py +400 -0
  79. mostlyright/data_harness/nbrender/parse.py +394 -0
  80. mostlyright/data_harness/nbrender/status.py +40 -0
  81. mostlyright/data_harness/nbrender/tokens.py +1295 -0
  82. mostlyright/data_harness/notebook.py +1710 -0
  83. mostlyright/data_harness/offline.py +2049 -0
  84. mostlyright/data_harness/operation_registry.py +1007 -0
  85. mostlyright/data_harness/operator_setup.py +239 -0
  86. mostlyright/data_harness/pipeline.py +6428 -0
  87. mostlyright/data_harness/plan_graph.py +2026 -0
  88. mostlyright/data_harness/preparation/__init__.py +104 -0
  89. mostlyright/data_harness/preparation/contracts.py +1017 -0
  90. mostlyright/data_harness/preparation/engine.py +221 -0
  91. mostlyright/data_harness/preparation/errors.py +14 -0
  92. mostlyright/data_harness/preparation/gates.py +751 -0
  93. mostlyright/data_harness/preparation/joins.py +574 -0
  94. mostlyright/data_harness/preparation/profile.py +384 -0
  95. mostlyright/data_harness/preparation/table.py +217 -0
  96. mostlyright/data_harness/preparation/transforms.py +568 -0
  97. mostlyright/data_harness/progress_events.py +534 -0
  98. mostlyright/data_harness/readers/__init__.py +46 -0
  99. mostlyright/data_harness/readers/containers.py +963 -0
  100. mostlyright/data_harness/readers/contracts.py +542 -0
  101. mostlyright/data_harness/readers/delimited.py +257 -0
  102. mostlyright/data_harness/readers/grib2/__init__.py +33 -0
  103. mostlyright/data_harness/readers/grib2/admission.py +722 -0
  104. mostlyright/data_harness/readers/grib2/decode.py +1009 -0
  105. mostlyright/data_harness/readers/grib2/geometry.py +1133 -0
  106. mostlyright/data_harness/readers/grib2/portable_math.py +501 -0
  107. mostlyright/data_harness/readers/json_tabular.py +485 -0
  108. mostlyright/data_harness/readers/registry.py +514 -0
  109. mostlyright/data_harness/readers/samples/README.md +110 -0
  110. mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/cities.csv.gz +0 -0
  111. mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/expected.json +24 -0
  112. mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/cities.csv.gz +0 -0
  113. mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/expected.json +24 -0
  114. mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/cities.tar +0 -0
  115. mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/expected.json +24 -0
  116. mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/cities.tar +0 -0
  117. mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/expected.json +24 -0
  118. mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/cities.zip +0 -0
  119. mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/expected.json +25 -0
  120. mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
  121. mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/expected.json +25 -0
  122. mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
  123. mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/expected.json +25 -0
  124. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/cities.csv +3 -0
  125. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/expected.json +23 -0
  126. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/cities.tsv +5 -0
  127. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/expected.json +25 -0
  128. mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/expected.json +30 -0
  129. mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/observations.csv +5 -0
  130. mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/expected.json +44 -0
  131. mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/stations.json +1 -0
  132. mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/expected.json +48 -0
  133. mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/observations.ndjson +4 -0
  134. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/cities.xlsx +0 -0
  135. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/expected.json +24 -0
  136. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/expected.json +27 -0
  137. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
  138. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/expected.json +27 -0
  139. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
  140. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/README.md +20 -0
  141. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/expected.json +55 -0
  142. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/gfs-2m-temperature.grib2 +0 -0
  143. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/expected.json +54 -0
  144. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
  145. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/expected.json +54 -0
  146. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/hrrr-categorical-rain.grib2 +0 -0
  147. mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json +54 -0
  148. mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
  149. mostlyright/data_harness/readers/samples.py +582 -0
  150. mostlyright/data_harness/readers/spreadsheet.py +803 -0
  151. mostlyright/data_harness/readers/tabular.py +510 -0
  152. mostlyright/data_harness/recipe.py +5321 -0
  153. mostlyright/data_harness/repair/__init__.py +78 -0
  154. mostlyright/data_harness/repair/adapters.py +274 -0
  155. mostlyright/data_harness/repair/contracts.py +872 -0
  156. mostlyright/data_harness/repair/coordinator.py +1099 -0
  157. mostlyright/data_harness/repair/errors.py +16 -0
  158. mostlyright/data_harness/review.py +2533 -0
  159. mostlyright/data_harness/rowset.py +283 -0
  160. mostlyright/data_harness/serving.py +1975 -0
  161. mostlyright/data_harness/serving_edge.py +590 -0
  162. mostlyright/data_harness/serving_http.py +1031 -0
  163. mostlyright/data_harness/session_probes.py +759 -0
  164. mostlyright/data_harness/signing.py +101 -0
  165. mostlyright/data_harness/source_discovery.py +898 -0
  166. mostlyright/data_harness/sources/__init__.py +209 -0
  167. mostlyright/data_harness/sources/_adapter_steps.py +213 -0
  168. mostlyright/data_harness/sources/adapters.py +1214 -0
  169. mostlyright/data_harness/sources/cadence.py +1428 -0
  170. mostlyright/data_harness/sources/cadence_emission.py +453 -0
  171. mostlyright/data_harness/sources/cadence_history.py +546 -0
  172. mostlyright/data_harness/sources/catalog/__init__.py +17 -0
  173. mostlyright/data_harness/sources/catalog/admission.py +477 -0
  174. mostlyright/data_harness/sources/catalog/authoring.py +1701 -0
  175. mostlyright/data_harness/sources/catalog/authoring_policy.py +701 -0
  176. mostlyright/data_harness/sources/catalog/authoring_shards.py +1217 -0
  177. mostlyright/data_harness/sources/catalog/bounded_io.py +231 -0
  178. mostlyright/data_harness/sources/catalog/channel.py +523 -0
  179. mostlyright/data_harness/sources/catalog/channel_client.py +296 -0
  180. mostlyright/data_harness/sources/catalog/contracts.py +825 -0
  181. mostlyright/data_harness/sources/catalog/coverage.py +137 -0
  182. mostlyright/data_harness/sources/catalog/delta.py +1340 -0
  183. mostlyright/data_harness/sources/catalog/embedding.py +532 -0
  184. mostlyright/data_harness/sources/catalog/entry_v2.py +1182 -0
  185. mostlyright/data_harness/sources/catalog/fill.py +3889 -0
  186. mostlyright/data_harness/sources/catalog/fill_partitions.py +459 -0
  187. mostlyright/data_harness/sources/catalog/fill_staging.py +1105 -0
  188. mostlyright/data_harness/sources/catalog/gating.py +374 -0
  189. mostlyright/data_harness/sources/catalog/generation_receipt.py +1607 -0
  190. mostlyright/data_harness/sources/catalog/harvest/__init__.py +7 -0
  191. mostlyright/data_harness/sources/catalog/harvest/ckan.py +384 -0
  192. mostlyright/data_harness/sources/catalog/harvest/datagov_v4.py +798 -0
  193. mostlyright/data_harness/sources/catalog/harvest/protocol.py +964 -0
  194. mostlyright/data_harness/sources/catalog/harvest/sdmx.py +445 -0
  195. mostlyright/data_harness/sources/catalog/harvest/stac.py +384 -0
  196. mostlyright/data_harness/sources/catalog/health.py +447 -0
  197. mostlyright/data_harness/sources/catalog/hosted_catalog.py +105 -0
  198. mostlyright/data_harness/sources/catalog/identity_history.py +1549 -0
  199. mostlyright/data_harness/sources/catalog/neural.py +1618 -0
  200. mostlyright/data_harness/sources/catalog/packed_catalog.py +2345 -0
  201. mostlyright/data_harness/sources/catalog/packed_retrieval.py +1517 -0
  202. mostlyright/data_harness/sources/catalog/packed_writer.py +2802 -0
  203. mostlyright/data_harness/sources/catalog/query_trace.py +1037 -0
  204. mostlyright/data_harness/sources/catalog/recommend.py +171 -0
  205. mostlyright/data_harness/sources/catalog/retrieval.py +230 -0
  206. mostlyright/data_harness/sources/catalog/retrieval_manifest.py +995 -0
  207. mostlyright/data_harness/sources/catalog/rights_decisions.py +254 -0
  208. mostlyright/data_harness/sources/catalog/sealed.py +560 -0
  209. mostlyright/data_harness/sources/catalog/search.py +230 -0
  210. mostlyright/data_harness/sources/catalog/streaming_delta.py +1097 -0
  211. mostlyright/data_harness/sources/catalog/update.py +891 -0
  212. mostlyright/data_harness/sources/collections.py +815 -0
  213. mostlyright/data_harness/sources/contracts.py +2223 -0
  214. mostlyright/data_harness/sources/deletion.py +761 -0
  215. mostlyright/data_harness/sources/fitness.py +162 -0
  216. mostlyright/data_harness/sources/governance.py +163 -0
  217. mostlyright/data_harness/sources/hosted.py +173 -0
  218. mostlyright/data_harness/sources/integration.py +218 -0
  219. mostlyright/data_harness/sources/range_reader.py +418 -0
  220. mostlyright/data_harness/sources/registry.py +514 -0
  221. mostlyright/data_harness/sources/rights_rule.py +59 -0
  222. mostlyright/data_harness/sources/source_cadence_vectors.v1.json +1 -0
  223. mostlyright/data_harness/sources/sports.py +521 -0
  224. mostlyright/data_harness/sources/stream.py +524 -0
  225. mostlyright/data_harness/sources/stream_connector.py +418 -0
  226. mostlyright/data_harness/sources/stream_recorder.py +1404 -0
  227. mostlyright/data_harness/studio_boundary.py +2019 -0
  228. mostlyright/data_harness/thin/__init__.py +37 -0
  229. mostlyright/data_harness/thin/acquire.py +1137 -0
  230. mostlyright/data_harness/thin/acquire_cancel.py +579 -0
  231. mostlyright/data_harness/thin/approvals.py +617 -0
  232. mostlyright/data_harness/thin/commands.py +406 -0
  233. mostlyright/data_harness/thin/download.py +194 -0
  234. mostlyright/data_harness/thin/narrative.py +589 -0
  235. mostlyright/data_harness/thin/parity.py +1070 -0
  236. mostlyright/data_harness/thin/propose.py +2759 -0
  237. mostlyright/data_harness/thin/research.py +1663 -0
  238. mostlyright/data_harness/thin/router.py +924 -0
  239. mostlyright/data_harness/thin/runs.py +519 -0
  240. mostlyright/data_harness/thin/session.py +281 -0
  241. mostlyright/data_harness/thin/stream.py +501 -0
  242. mostlyright/data_harness/thin/transport.py +187 -0
  243. mostlyright/data_harness/thin/vocabulary.py +368 -0
  244. mostlyright/data_harness/thin/workers.py +164 -0
  245. mostlyright/data_harness/ucum/TABLE-PIN.json +40 -0
  246. mostlyright/data_harness/ucum/ucum-subset.v1.json +632 -0
  247. mostlyright/data_harness/unit_flow.py +927 -0
  248. mostlyright/data_harness/units.py +572 -0
  249. mostlyright/data_harness/ux/__init__.py +9 -0
  250. mostlyright/data_harness/ux/approve.py +485 -0
  251. mostlyright/data_harness/ux/author_yaml.py +597 -0
  252. mostlyright/data_harness/ux/cloud_auth.py +447 -0
  253. mostlyright/data_harness/ux/commands/__init__.py +260 -0
  254. mostlyright/data_harness/ux/commands/approve.py +136 -0
  255. mostlyright/data_harness/ux/commands/auth.py +744 -0
  256. mostlyright/data_harness/ux/commands/author.py +79 -0
  257. mostlyright/data_harness/ux/commands/catalog_author.py +403 -0
  258. mostlyright/data_harness/ux/commands/catalog_fill.py +523 -0
  259. mostlyright/data_harness/ux/commands/catalog_harvest.py +545 -0
  260. mostlyright/data_harness/ux/commands/catalog_publish.py +1838 -0
  261. mostlyright/data_harness/ux/commands/catalog_search.py +71 -0
  262. mostlyright/data_harness/ux/commands/catalog_update.py +437 -0
  263. mostlyright/data_harness/ux/commands/deploy.py +134 -0
  264. mostlyright/data_harness/ux/commands/deploy_dataset.py +98 -0
  265. mostlyright/data_harness/ux/commands/deploy_plan.py +105 -0
  266. mostlyright/data_harness/ux/commands/deploy_status.py +104 -0
  267. mostlyright/data_harness/ux/commands/diff.py +74 -0
  268. mostlyright/data_harness/ux/commands/index.py +84 -0
  269. mostlyright/data_harness/ux/commands/inventory.py +47 -0
  270. mostlyright/data_harness/ux/commands/list_builds.py +143 -0
  271. mostlyright/data_harness/ux/commands/login.py +63 -0
  272. mostlyright/data_harness/ux/commands/peek.py +236 -0
  273. mostlyright/data_harness/ux/commands/plan_check.py +90 -0
  274. mostlyright/data_harness/ux/commands/preflight.py +97 -0
  275. mostlyright/data_harness/ux/commands/record.py +107 -0
  276. mostlyright/data_harness/ux/commands/review_setup.py +47 -0
  277. mostlyright/data_harness/ux/commands/search.py +440 -0
  278. mostlyright/data_harness/ux/commands/show.py +61 -0
  279. mostlyright/data_harness/ux/commands/whoami.py +37 -0
  280. mostlyright/data_harness/ux/credential_native.py +551 -0
  281. mostlyright/data_harness/ux/credential_store.py +1055 -0
  282. mostlyright/data_harness/ux/credentials.py +631 -0
  283. mostlyright/data_harness/ux/diffing.py +444 -0
  284. mostlyright/data_harness/ux/headline.py +671 -0
  285. mostlyright/data_harness/ux/hosted_acquisition.py +974 -0
  286. mostlyright/data_harness/ux/hosted_run_status.py +619 -0
  287. mostlyright/data_harness/ux/inventory.py +427 -0
  288. mostlyright/data_harness/ux/local_review.py +375 -0
  289. mostlyright/data_harness/ux/login.py +691 -0
  290. mostlyright/data_harness/ux/path_kind.py +147 -0
  291. mostlyright/data_harness/ux/peek.py +1000 -0
  292. mostlyright/data_harness/ux/plain_file.py +178 -0
  293. mostlyright/data_harness/ux/plan_check.py +311 -0
  294. mostlyright/data_harness/ux/preflight.py +918 -0
  295. mostlyright/data_harness/ux/readers.py +1124 -0
  296. mostlyright/data_harness/ux/remediation.py +2195 -0
  297. mostlyright/data_harness/ux/render.py +657 -0
  298. mostlyright/data_harness/ux/workload.py +1077 -0
  299. mostlyright/data_harness/viewer.py +3713 -0
  300. mostlyright/data_harness/visual_run/__init__.py +83 -0
  301. mostlyright/data_harness/visual_run/authoring.py +235 -0
  302. mostlyright/data_harness/visual_run/contracts.py +673 -0
  303. mostlyright/data_harness/visual_run/materialize.py +486 -0
  304. mostlyright/data_harness/visual_run/observations.py +874 -0
  305. mostlyright/data_harness/visual_run/query.py +259 -0
  306. mostlyright/data_harness/visual_run/reducer.py +280 -0
  307. mostlyright/data_harness/visual_run/sdk.py +892 -0
  308. mostlyright/data_harness/visual_run/store.py +584 -0
  309. mostlyright/data_harness/visual_run/transport.py +239 -0
  310. mostlyright/data_harness/watch.py +2999 -0
  311. mostlyright_data-0.9.0.dist-info/METADATA +607 -0
  312. mostlyright_data-0.9.0.dist-info/RECORD +314 -0
  313. mostlyright_data-0.9.0.dist-info/WHEEL +4 -0
  314. mostlyright_data-0.9.0.dist-info/entry_points.txt +12 -0
@@ -0,0 +1,759 @@
1
+ """The probe vocabulary a research session speaks (ADR 0021).
2
+
3
+ Studio owns the probe *kind* enum and nothing else about a probe. Its contract says so in as many
4
+ words: a probe request and a probe result are "opaque to Studio", size-bounded, digested, and
5
+ stored uninterpreted, "so the harness can add a probe parameter or a result field without a Studio
6
+ contract change". This module is the other half of that arrangement -- the place where the bodies
7
+ under those four kinds are actually specified, so that "opaque to Studio" does not degrade into
8
+ "unspecified anywhere".
9
+
10
+ **Why the kind enum is not restated as a choice.** ``research-session.schema.json`` closes
11
+ ``probe_kind`` to four values. Restating them here as a *harness* decision would invite the two
12
+ spellings to drift; :data:`PROBE_KINDS` is therefore the same four, and
13
+ ``tests/test_session_probes.py`` asserts it equals the generated ``ContractProbeKind`` enum. What
14
+ the harness decides is the body, and only the body.
15
+
16
+ **Why the schemas are data and the validators are code.** Every schema below is a JSON Schema
17
+ object, and every one of them is enforced at runtime by a hand-written validator rather than by a
18
+ schema library. That is not duplication for its own sake: ``jsonschema`` is a development and
19
+ release dependency and is deliberately absent from ``[local]``, which is the only extra the worker
20
+ image installs, so a runtime schema check would be a runtime import the image cannot satisfy. The
21
+ schemas are the statement, the validators are the enforcement, and
22
+ ``test_session_probes.py`` runs both over the same corpus and refuses any input the two disagree
23
+ about. A drifted validator is a red test rather than a silently widened probe.
24
+
25
+ **Why every request names a source.** All four kinds carry ``source_id``, including
26
+ ``evaluate_expression``. It costs one field and buys two things. Probe progress can then be emitted
27
+ as the ``source_probe_started`` / ``source_probe_settled`` pair -- which travel as Studio's
28
+ ``attempt_progress`` and ``source_probe_completed`` event types, exactly the two the
29
+ research-session design names -- without mislabelling a probe id as a source id. And the session's
30
+ ``context.source_ids`` becomes an enforceable allowlist: a probe that names a source the session
31
+ was not opened against is refused before anything is read, which is the whole of a probe's
32
+ authority check.
33
+
34
+ **Why expressions are an AST and never a string.** ``evaluate_expression`` is the one kind a caller
35
+ could reasonably expect to accept a little language. It does not. The request carries a closed,
36
+ bounded, non-recursive-by-construction operator tree; there is no parser, no ``eval``, no
37
+ comprehension over caller text, and no operator whose cost is superlinear in anything the caller
38
+ controls. A probe body arrives from Studio having been stored uninterpreted, which means it arrives
39
+ with exactly as much trust as the client that submitted it -- that is to say, none.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import math
45
+ from collections.abc import Mapping, Sequence
46
+ from typing import Any, Final
47
+
48
+ from mostlyright.data_harness.canonical import canonical_json_bytes
49
+
50
+ #: The vocabulary's own version, carried on every result body. It is not Studio's
51
+ #: ``schema_version``: Studio versions the envelope, this versions what the harness puts inside it,
52
+ #: and the point of the arrangement is that this number can move without Studio moving.
53
+ PROBE_SCHEMA_VERSION: Final = "mr-session-probes.v1"
54
+
55
+ #: Studio's closed enum, restated so this module loads without the generated client (the
56
+ #: ``progress_events`` precedent) and asserted equal to it by the test suite.
57
+ PROBE_KINDS: Final[tuple[str, ...]] = (
58
+ "source_inspect",
59
+ "sample_rows",
60
+ "profile_columns",
61
+ "evaluate_expression",
62
+ )
63
+
64
+ #: Studio's own bounds on the two opaque bodies, restated because the worker must refuse an
65
+ #: oversized result *before* spending a request on it. Studio enforces these too; a worker that
66
+ #: relied on that alone would learn about the ceiling by being rejected, after the work.
67
+ PROBE_REQUEST_MAX_BYTES: Final = 64 * 1024
68
+ PROBE_RESULT_MAX_BYTES: Final = 256 * 1024
69
+
70
+ #: How many rows any probe may pull back, and how many any probe may look at. The first bounds the
71
+ #: answer, the second bounds the work: a profile over a million rows is not an interactive probe.
72
+ MAX_SAMPLE_ROWS: Final = 200
73
+ MAX_SCAN_ROWS: Final = 50_000
74
+ DEFAULT_SCAN_ROWS: Final = 10_000
75
+ #: A cell is rendered as a short token or a number. A probe answer is a preview, not an export, so
76
+ #: a long string is truncated in the answer rather than the answer being refused.
77
+ MAX_CELL_CHARS: Final = 512
78
+ MAX_PROFILE_COLUMNS: Final = 256
79
+ #: Expression trees are bounded twice: by depth, so nesting cannot be used to build a large tree
80
+ #: from a small body, and by operand count at each node.
81
+ MAX_EXPRESSION_DEPTH: Final = 8
82
+ MAX_EXPRESSION_OPERANDS: Final = 16
83
+
84
+ #: The aggregate an ``evaluate_expression`` probe may compute. Closed, and every member is one
85
+ #: pass over at most ``MAX_SCAN_ROWS`` values.
86
+ AGGREGATES: Final[tuple[str, ...]] = ("count", "sum", "min", "max", "avg")
87
+
88
+ #: The predicate operators a filter may use. Closed, and every member is one comparison between one
89
+ #: cell and one caller literal. Nothing here matches, searches, or concatenates.
90
+ COMPARISONS: Final[tuple[str, ...]] = ("eq", "ne", "lt", "le", "gt", "ge")
91
+ UNARY_TESTS: Final[tuple[str, ...]] = ("is_null", "is_not_null")
92
+ CONNECTIVES: Final[tuple[str, ...]] = ("and", "or", "not")
93
+
94
+
95
+ class ProbeVocabularyError(ValueError):
96
+ """A probe body that is not in the vocabulary. Carries the code the probe fails with."""
97
+
98
+ def __init__(self, code: str, detail: str) -> None:
99
+ super().__init__(f"{code}: {detail}")
100
+ self.code = code
101
+ self.detail = detail
102
+
103
+
104
+ # ------------------------------------------------------------------------------------------
105
+ # The declared schemas. Documentation and test oracle; never the runtime check -- see the module
106
+ # docstring for why the worker image cannot afford a schema library.
107
+ # ------------------------------------------------------------------------------------------
108
+
109
+ _UUID_PATTERN: Final = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
110
+ _COLUMN: Final = {"type": "string", "minLength": 1, "maxLength": 256}
111
+ _SCALAR: Final = {
112
+ "description": "One caller literal. Numbers, booleans, null and short strings only.",
113
+ "type": ["string", "number", "boolean", "null"],
114
+ "maxLength": MAX_CELL_CHARS,
115
+ }
116
+
117
+
118
+ def _predicate_schema(depth: int) -> dict[str, Any]:
119
+ """The predicate grammar, unrolled to :data:`MAX_EXPRESSION_DEPTH`.
120
+
121
+ Unrolled rather than expressed with ``$ref`` back to itself, because a self-referential schema
122
+ states a grammar with no depth bound and this grammar's depth bound is a security property.
123
+ The unrolled form makes the ceiling visible in the schema instead of only in the validator.
124
+ """
125
+
126
+ branches: list[dict[str, Any]] = [
127
+ {
128
+ "type": "object",
129
+ "additionalProperties": False,
130
+ "required": ["op", "column", "value"],
131
+ "properties": {
132
+ "op": {"enum": list(COMPARISONS)},
133
+ "column": _COLUMN,
134
+ "value": _SCALAR,
135
+ },
136
+ },
137
+ {
138
+ "type": "object",
139
+ "additionalProperties": False,
140
+ "required": ["op", "column"],
141
+ "properties": {"op": {"enum": list(UNARY_TESTS)}, "column": _COLUMN},
142
+ },
143
+ ]
144
+ if depth > 1:
145
+ inner = _predicate_schema(depth - 1)
146
+ branches.append(
147
+ {
148
+ "type": "object",
149
+ "additionalProperties": False,
150
+ "required": ["op", "operands"],
151
+ "properties": {
152
+ "op": {"enum": ["and", "or"]},
153
+ "operands": {
154
+ "type": "array",
155
+ "minItems": 1,
156
+ "maxItems": MAX_EXPRESSION_OPERANDS,
157
+ "items": inner,
158
+ },
159
+ },
160
+ }
161
+ )
162
+ branches.append(
163
+ {
164
+ "type": "object",
165
+ "additionalProperties": False,
166
+ "required": ["op", "operand"],
167
+ "properties": {"op": {"const": "not"}, "operand": inner},
168
+ }
169
+ )
170
+ return {"oneOf": branches}
171
+
172
+
173
+ PREDICATE_SCHEMA: Final[dict[str, Any]] = _predicate_schema(MAX_EXPRESSION_DEPTH)
174
+
175
+ PROBE_REQUEST_SCHEMA: Final[Mapping[str, dict[str, Any]]] = {
176
+ "source_inspect": {
177
+ "$schema": "http://json-schema.org/draft-07/schema#",
178
+ "title": "source_inspect request",
179
+ "description": (
180
+ "What shape is this source? Reports the detected format, the column names and their "
181
+ "inferred types, and the schema digest -- and no cell values at all, which is what "
182
+ "separates it from sample_rows."
183
+ ),
184
+ "type": "object",
185
+ "additionalProperties": False,
186
+ "required": ["source_id"],
187
+ "properties": {
188
+ "source_id": {"type": "string", "pattern": _UUID_PATTERN},
189
+ "scan_rows": {"type": "integer", "minimum": 1, "maximum": MAX_SCAN_ROWS},
190
+ },
191
+ },
192
+ "sample_rows": {
193
+ "$schema": "http://json-schema.org/draft-07/schema#",
194
+ "title": "sample_rows request",
195
+ "description": "The first `limit` rows after `offset`, rendered as preview cells.",
196
+ "type": "object",
197
+ "additionalProperties": False,
198
+ "required": ["source_id", "limit"],
199
+ "properties": {
200
+ "source_id": {"type": "string", "pattern": _UUID_PATTERN},
201
+ "limit": {"type": "integer", "minimum": 1, "maximum": MAX_SAMPLE_ROWS},
202
+ "offset": {"type": "integer", "minimum": 0, "maximum": MAX_SCAN_ROWS},
203
+ "columns": {
204
+ "description": "Project to these columns. Absent means every column.",
205
+ "type": "array",
206
+ "minItems": 1,
207
+ "maxItems": MAX_PROFILE_COLUMNS,
208
+ "uniqueItems": True,
209
+ "items": _COLUMN,
210
+ },
211
+ },
212
+ },
213
+ "profile_columns": {
214
+ "$schema": "http://json-schema.org/draft-07/schema#",
215
+ "title": "profile_columns request",
216
+ "description": (
217
+ "Per-column null counts, distinct counts and extremes over a bounded scan. Distinct "
218
+ "is exact over the scanned rows and is reported with the number scanned, because a "
219
+ "distinct count without its denominator is a number nobody can use."
220
+ ),
221
+ "type": "object",
222
+ "additionalProperties": False,
223
+ "required": ["source_id"],
224
+ "properties": {
225
+ "source_id": {"type": "string", "pattern": _UUID_PATTERN},
226
+ "scan_rows": {"type": "integer", "minimum": 1, "maximum": MAX_SCAN_ROWS},
227
+ "columns": {
228
+ "type": "array",
229
+ "minItems": 1,
230
+ "maxItems": MAX_PROFILE_COLUMNS,
231
+ "uniqueItems": True,
232
+ "items": _COLUMN,
233
+ },
234
+ },
235
+ },
236
+ "evaluate_expression": {
237
+ "$schema": "http://json-schema.org/draft-07/schema#",
238
+ "title": "evaluate_expression request",
239
+ "description": (
240
+ "One aggregate over an optionally filtered bounded scan. The expression is an "
241
+ "operator tree, never a string: there is no parser here and nothing a caller sends "
242
+ "is ever compiled, evaluated, or matched as a pattern."
243
+ ),
244
+ "type": "object",
245
+ "additionalProperties": False,
246
+ "required": ["source_id", "expression"],
247
+ "properties": {
248
+ "source_id": {"type": "string", "pattern": _UUID_PATTERN},
249
+ "scan_rows": {"type": "integer", "minimum": 1, "maximum": MAX_SCAN_ROWS},
250
+ # Two arms, not one with an optional column: `count` counts matched ROWS and takes no
251
+ # column, because `count(column)` would silently mean either rows or non-null values
252
+ # and an exploratory caller has no way to tell which they got. Every other aggregate
253
+ # needs a column and says so here rather than only in the validator.
254
+ "expression": {
255
+ "oneOf": [
256
+ {
257
+ "type": "object",
258
+ "additionalProperties": False,
259
+ "required": ["aggregate"],
260
+ "properties": {
261
+ "aggregate": {"const": "count"},
262
+ "where": PREDICATE_SCHEMA,
263
+ },
264
+ },
265
+ {
266
+ "type": "object",
267
+ "additionalProperties": False,
268
+ "required": ["aggregate", "column"],
269
+ "properties": {
270
+ "aggregate": {"enum": [name for name in AGGREGATES if name != "count"]},
271
+ "column": _COLUMN,
272
+ "where": PREDICATE_SCHEMA,
273
+ },
274
+ },
275
+ ]
276
+ },
277
+ },
278
+ },
279
+ }
280
+
281
+ _RESULT_ENVELOPE: Final[dict[str, Any]] = {
282
+ "schema_version": {"const": PROBE_SCHEMA_VERSION},
283
+ "probe_kind": {"enum": list(PROBE_KINDS)},
284
+ "source_id": {"type": "string", "pattern": _UUID_PATTERN},
285
+ "content_sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
286
+ "content_bytes": {"type": "integer", "minimum": 0},
287
+ "data_format": {"type": "string", "minLength": 1, "maxLength": 64},
288
+ "sandbox_policy_digest": {"type": "string", "minLength": 1, "maxLength": 256},
289
+ "rows_scanned": {"type": "integer", "minimum": 0},
290
+ "truncated": {"type": "boolean"},
291
+ }
292
+ _ENVELOPE_REQUIRED: Final = (
293
+ "schema_version",
294
+ "probe_kind",
295
+ "source_id",
296
+ "content_sha256",
297
+ "content_bytes",
298
+ "data_format",
299
+ "sandbox_policy_digest",
300
+ "rows_scanned",
301
+ "truncated",
302
+ )
303
+
304
+ _COLUMN_SHAPE: Final[dict[str, Any]] = {
305
+ "type": "object",
306
+ "additionalProperties": False,
307
+ "required": ["name", "inferred_type"],
308
+ "properties": {
309
+ "name": _COLUMN,
310
+ "inferred_type": {"enum": ["string", "integer", "number", "boolean", "null", "mixed"]},
311
+ },
312
+ }
313
+
314
+ PROBE_RESULT_SCHEMA: Final[Mapping[str, dict[str, Any]]] = {
315
+ "source_inspect": {
316
+ "$schema": "http://json-schema.org/draft-07/schema#",
317
+ "type": "object",
318
+ "additionalProperties": False,
319
+ "required": [*_ENVELOPE_REQUIRED, "columns", "schema_digest"],
320
+ "properties": {
321
+ **_RESULT_ENVELOPE,
322
+ "media_type": {"type": "string", "minLength": 1, "maxLength": 256},
323
+ "schema_digest": {"type": "string", "minLength": 1, "maxLength": 256},
324
+ "columns": {
325
+ "type": "array",
326
+ "maxItems": MAX_PROFILE_COLUMNS,
327
+ "items": _COLUMN_SHAPE,
328
+ },
329
+ },
330
+ },
331
+ "sample_rows": {
332
+ "$schema": "http://json-schema.org/draft-07/schema#",
333
+ "type": "object",
334
+ "additionalProperties": False,
335
+ "required": [*_ENVELOPE_REQUIRED, "columns", "rows"],
336
+ "properties": {
337
+ **_RESULT_ENVELOPE,
338
+ "columns": {
339
+ "type": "array",
340
+ "maxItems": MAX_PROFILE_COLUMNS,
341
+ "items": _COLUMN,
342
+ },
343
+ "rows": {
344
+ "type": "array",
345
+ "maxItems": MAX_SAMPLE_ROWS,
346
+ "items": {
347
+ "type": "array",
348
+ "maxItems": MAX_PROFILE_COLUMNS,
349
+ "items": {"type": ["string", "number", "boolean", "null"]},
350
+ },
351
+ },
352
+ },
353
+ },
354
+ "profile_columns": {
355
+ "$schema": "http://json-schema.org/draft-07/schema#",
356
+ "type": "object",
357
+ "additionalProperties": False,
358
+ "required": [*_ENVELOPE_REQUIRED, "columns"],
359
+ "properties": {
360
+ **_RESULT_ENVELOPE,
361
+ "columns": {
362
+ "type": "array",
363
+ "maxItems": MAX_PROFILE_COLUMNS,
364
+ "items": {
365
+ "type": "object",
366
+ "additionalProperties": False,
367
+ "required": ["name", "inferred_type", "non_null", "nulls", "distinct"],
368
+ "properties": {
369
+ **_COLUMN_SHAPE["properties"],
370
+ "non_null": {"type": "integer", "minimum": 0},
371
+ "nulls": {"type": "integer", "minimum": 0},
372
+ "distinct": {"type": "integer", "minimum": 0},
373
+ "minimum": {"type": ["string", "number", "boolean", "null"]},
374
+ "maximum": {"type": ["string", "number", "boolean", "null"]},
375
+ },
376
+ },
377
+ },
378
+ },
379
+ },
380
+ "evaluate_expression": {
381
+ "$schema": "http://json-schema.org/draft-07/schema#",
382
+ "type": "object",
383
+ "additionalProperties": False,
384
+ "required": [*_ENVELOPE_REQUIRED, "aggregate", "value", "rows_matched"],
385
+ "properties": {
386
+ **_RESULT_ENVELOPE,
387
+ "aggregate": {"enum": list(AGGREGATES)},
388
+ "column": _COLUMN,
389
+ "value": {"type": ["string", "number", "boolean", "null"]},
390
+ "rows_matched": {"type": "integer", "minimum": 0},
391
+ },
392
+ },
393
+ }
394
+
395
+
396
+ # ------------------------------------------------------------------------------------------
397
+ # Runtime validation. Hand written, total, and refusing rather than coercing.
398
+ # ------------------------------------------------------------------------------------------
399
+
400
+
401
+ def _require(condition: bool, code: str, detail: str) -> None:
402
+ if not condition:
403
+ raise ProbeVocabularyError(code, detail)
404
+
405
+
406
+ def _object(value: Any, label: str) -> dict[str, Any]:
407
+ _require(
408
+ isinstance(value, dict) and all(isinstance(key, str) for key in value),
409
+ "PROBE_REQUEST_INVALID",
410
+ f"{label} must be an object with string keys",
411
+ )
412
+ return dict(value)
413
+
414
+
415
+ def _exact_keys(value: Mapping[str, Any], required: set[str], optional: set[str]) -> None:
416
+ present = set(value)
417
+ missing = sorted(required - present)
418
+ _require(not missing, "PROBE_REQUEST_INVALID", f"missing field(s): {', '.join(missing)}")
419
+ unknown = sorted(present - required - optional)
420
+ _require(not unknown, "PROBE_REQUEST_INVALID", f"unknown field(s): {', '.join(unknown)}")
421
+
422
+
423
+ def _uuid_text(value: Any, label: str) -> str:
424
+ _require(isinstance(value, str), "PROBE_REQUEST_INVALID", f"{label} must be a string")
425
+ text = str(value)
426
+ parts = text.split("-")
427
+ shape = [8, 4, 4, 4, 12]
428
+ ok = len(parts) == 5 and all(
429
+ len(part) == width and all(character in "0123456789abcdef" for character in part)
430
+ for part, width in zip(parts, shape, strict=True)
431
+ )
432
+ _require(ok, "PROBE_REQUEST_INVALID", f"{label} must be a lowercase UUID")
433
+ return text
434
+
435
+
436
+ def _bounded_int(value: Any, label: str, *, minimum: int, maximum: int) -> int:
437
+ # `type(...) is int` rather than isinstance: bool is a subclass of int, and a caller that sent
438
+ # `true` for a row limit made a mistake that must surface as one.
439
+ _require(type(value) is int, "PROBE_REQUEST_INVALID", f"{label} must be an integer")
440
+ _require(
441
+ minimum <= value <= maximum,
442
+ "PROBE_REQUEST_INVALID",
443
+ f"{label} must be in [{minimum}, {maximum}]",
444
+ )
445
+ return int(value)
446
+
447
+
448
+ def _column_name(value: Any, label: str) -> str:
449
+ _require(
450
+ isinstance(value, str) and 1 <= len(value) <= 256,
451
+ "PROBE_REQUEST_INVALID",
452
+ f"{label} must be a string of 1..256 characters",
453
+ )
454
+ return str(value)
455
+
456
+
457
+ def _column_list(value: Any, label: str) -> tuple[str, ...]:
458
+ _require(isinstance(value, list), "PROBE_REQUEST_INVALID", f"{label} must be an array")
459
+ _require(
460
+ 1 <= len(value) <= MAX_PROFILE_COLUMNS,
461
+ "PROBE_REQUEST_INVALID",
462
+ f"{label} must name between 1 and {MAX_PROFILE_COLUMNS} columns",
463
+ )
464
+ names = tuple(_column_name(item, f"{label} entry") for item in value)
465
+ _require(len(set(names)) == len(names), "PROBE_REQUEST_INVALID", f"{label} repeats a column")
466
+ return names
467
+
468
+
469
+ def _scalar(value: Any, label: str) -> Any:
470
+ if value is None or isinstance(value, bool):
471
+ return value
472
+ if isinstance(value, str):
473
+ _require(
474
+ len(value) <= MAX_CELL_CHARS,
475
+ "PROBE_REQUEST_INVALID",
476
+ f"{label} exceeds {MAX_CELL_CHARS} characters",
477
+ )
478
+ return value
479
+ if type(value) in {int, float}:
480
+ _require(
481
+ not isinstance(value, float) or math.isfinite(value),
482
+ "PROBE_REQUEST_INVALID",
483
+ f"{label} must be a finite number",
484
+ )
485
+ return value
486
+ raise ProbeVocabularyError("PROBE_REQUEST_INVALID", f"{label} is not an admitted literal")
487
+
488
+
489
+ def _predicate(value: Any, *, depth: int) -> dict[str, Any]:
490
+ _require(
491
+ depth <= MAX_EXPRESSION_DEPTH,
492
+ "PROBE_EXPRESSION_TOO_DEEP",
493
+ f"predicate nests deeper than {MAX_EXPRESSION_DEPTH}",
494
+ )
495
+ node = _object(value, "predicate")
496
+ operator = node.get("op")
497
+ _require(isinstance(operator, str), "PROBE_REQUEST_INVALID", "predicate op must be a string")
498
+ if operator in COMPARISONS:
499
+ _exact_keys(node, {"op", "column", "value"}, set())
500
+ return {
501
+ "op": operator,
502
+ "column": _column_name(node["column"], "predicate column"),
503
+ "value": _scalar(node["value"], "predicate value"),
504
+ }
505
+ if operator in UNARY_TESTS:
506
+ _exact_keys(node, {"op", "column"}, set())
507
+ return {"op": operator, "column": _column_name(node["column"], "predicate column")}
508
+ if operator in {"and", "or"}:
509
+ _exact_keys(node, {"op", "operands"}, set())
510
+ operands = node["operands"]
511
+ _require(
512
+ isinstance(operands, list) and 1 <= len(operands) <= MAX_EXPRESSION_OPERANDS,
513
+ "PROBE_REQUEST_INVALID",
514
+ f"{operator} takes 1..{MAX_EXPRESSION_OPERANDS} operands",
515
+ )
516
+ return {
517
+ "op": operator,
518
+ "operands": [_predicate(item, depth=depth + 1) for item in operands],
519
+ }
520
+ if operator == "not":
521
+ _exact_keys(node, {"op", "operand"}, set())
522
+ return {"op": "not", "operand": _predicate(node["operand"], depth=depth + 1)}
523
+ raise ProbeVocabularyError(
524
+ "PROBE_REQUEST_INVALID", f"predicate op {operator!r} is not admitted"
525
+ )
526
+
527
+
528
+ def probe_kind(value: Any) -> str:
529
+ """Admit one Studio probe kind, or refuse with the code the probe fails under."""
530
+
531
+ _require(
532
+ isinstance(value, str) and value in PROBE_KINDS,
533
+ "PROBE_KIND_UNSUPPORTED",
534
+ f"probe kind {value!r} is not in the vocabulary",
535
+ )
536
+ return str(value)
537
+
538
+
539
+ def validate_probe_request(kind: str, request: Any) -> dict[str, Any]:
540
+ """Admit one probe request body and return it normalized, with defaults filled in.
541
+
542
+ Normalizing here rather than at each use is deliberate: the executor should never see an
543
+ absent ``scan_rows`` and choose a default of its own, because two executors choosing two
544
+ defaults is how a probe stops being reproducible.
545
+ """
546
+
547
+ kind = probe_kind(kind)
548
+ body = _object(request, "probe request")
549
+ encoded = canonical_json_bytes(body)
550
+ _require(
551
+ len(encoded) <= PROBE_REQUEST_MAX_BYTES,
552
+ "PROBE_REQUEST_TOO_LARGE",
553
+ f"probe request is {len(encoded)} canonical bytes, over {PROBE_REQUEST_MAX_BYTES}",
554
+ )
555
+ if kind == "source_inspect":
556
+ _exact_keys(body, {"source_id"}, {"scan_rows"})
557
+ return {
558
+ "source_id": _uuid_text(body["source_id"], "source_id"),
559
+ "scan_rows": _bounded_int(
560
+ body.get("scan_rows", DEFAULT_SCAN_ROWS),
561
+ "scan_rows",
562
+ minimum=1,
563
+ maximum=MAX_SCAN_ROWS,
564
+ ),
565
+ }
566
+ if kind == "sample_rows":
567
+ _exact_keys(body, {"source_id", "limit"}, {"offset", "columns"})
568
+ normalized: dict[str, Any] = {
569
+ "source_id": _uuid_text(body["source_id"], "source_id"),
570
+ "limit": _bounded_int(body["limit"], "limit", minimum=1, maximum=MAX_SAMPLE_ROWS),
571
+ "offset": _bounded_int(
572
+ body.get("offset", 0), "offset", minimum=0, maximum=MAX_SCAN_ROWS
573
+ ),
574
+ }
575
+ if "columns" in body:
576
+ normalized["columns"] = list(_column_list(body["columns"], "columns"))
577
+ return normalized
578
+ if kind == "profile_columns":
579
+ _exact_keys(body, {"source_id"}, {"scan_rows", "columns"})
580
+ profiled: dict[str, Any] = {
581
+ "source_id": _uuid_text(body["source_id"], "source_id"),
582
+ "scan_rows": _bounded_int(
583
+ body.get("scan_rows", DEFAULT_SCAN_ROWS),
584
+ "scan_rows",
585
+ minimum=1,
586
+ maximum=MAX_SCAN_ROWS,
587
+ ),
588
+ }
589
+ if "columns" in body:
590
+ profiled["columns"] = list(_column_list(body["columns"], "columns"))
591
+ return profiled
592
+ _exact_keys(body, {"source_id", "expression"}, {"scan_rows"})
593
+ expression = _object(body["expression"], "expression")
594
+ _exact_keys(expression, {"aggregate"}, {"column", "where"})
595
+ aggregate = expression["aggregate"]
596
+ _require(
597
+ isinstance(aggregate, str) and aggregate in AGGREGATES,
598
+ "PROBE_REQUEST_INVALID",
599
+ f"aggregate {aggregate!r} is not admitted",
600
+ )
601
+ normalized_expression: dict[str, Any] = {"aggregate": str(aggregate)}
602
+ if aggregate == "count":
603
+ # `count` over a column would silently mean one of two different things -- rows, or
604
+ # non-null values in that column -- so it takes no column at all and always means rows.
605
+ _require(
606
+ "column" not in expression,
607
+ "PROBE_REQUEST_INVALID",
608
+ "count takes no column; it counts matched rows",
609
+ )
610
+ else:
611
+ _require(
612
+ "column" in expression,
613
+ "PROBE_REQUEST_INVALID",
614
+ f"{aggregate} requires a column",
615
+ )
616
+ normalized_expression["column"] = _column_name(expression["column"], "expression column")
617
+ if "where" in expression:
618
+ normalized_expression["where"] = _predicate(expression["where"], depth=1)
619
+ return {
620
+ "source_id": _uuid_text(body["source_id"], "source_id"),
621
+ "scan_rows": _bounded_int(
622
+ body.get("scan_rows", DEFAULT_SCAN_ROWS), "scan_rows", minimum=1, maximum=MAX_SCAN_ROWS
623
+ ),
624
+ "expression": normalized_expression,
625
+ }
626
+
627
+
628
+ def validate_probe_result(kind: str, result: Any) -> dict[str, Any]:
629
+ """Admit one probe result body before it is sent, so an over-budget answer is ours to refuse.
630
+
631
+ Studio bounds the result too. Checking here as well is not belt-and-braces: an answer refused
632
+ by Studio has already cost the probe its one settlement, and the probe cannot be answered
633
+ again. Refusing locally converts that into an ordinary ``failed`` outcome with a code.
634
+ """
635
+
636
+ kind = probe_kind(kind)
637
+ body = _object(result, "probe result")
638
+ encoded = canonical_json_bytes(body)
639
+ _require(
640
+ len(encoded) <= PROBE_RESULT_MAX_BYTES,
641
+ "PROBE_RESULT_TOO_LARGE",
642
+ f"probe result is {len(encoded)} canonical bytes, over {PROBE_RESULT_MAX_BYTES}",
643
+ )
644
+ schema = PROBE_RESULT_SCHEMA[kind]
645
+ required = set(schema["required"])
646
+ optional = set(schema["properties"]) - required
647
+ _exact_keys(body, required, optional)
648
+ _require(
649
+ body["schema_version"] == PROBE_SCHEMA_VERSION,
650
+ "PROBE_RESULT_INVALID",
651
+ "probe result schema_version is not this vocabulary",
652
+ )
653
+ _require(
654
+ body["probe_kind"] == kind,
655
+ "PROBE_RESULT_INVALID",
656
+ "probe result names a different kind than the probe",
657
+ )
658
+ return body
659
+
660
+
661
+ def preview_cell(value: Any) -> Any:
662
+ """Render one parsed cell as a JSON-safe preview value.
663
+
664
+ A parsed cell can be a decimal, a date, or a very long string; a probe answer is a preview and
665
+ must be small, canonical-JSON encodable, and finite. Anything that is not already an admitted
666
+ literal is rendered as its ``str`` and truncated, and truncation is visible in the string
667
+ rather than silent, because a preview that lies about being complete is worse than no preview.
668
+ """
669
+
670
+ if value is None or isinstance(value, bool):
671
+ return value
672
+ if type(value) is int:
673
+ # Canonical JSON refuses integers outside the exactly representable range; a count that
674
+ # cannot survive the wire is reported as text rather than rounded.
675
+ return value if abs(value) <= 2**53 - 1 else f"{value}"[:MAX_CELL_CHARS]
676
+ if type(value) is float:
677
+ return value if math.isfinite(value) else None
678
+ text = value if isinstance(value, str) else str(value)
679
+ if len(text) > MAX_CELL_CHARS:
680
+ return text[: MAX_CELL_CHARS - 1] + "…"
681
+ return text
682
+
683
+
684
+ def inferred_type(values: Sequence[Any]) -> str:
685
+ """Name the one type a column's scanned values share, or ``mixed``/``null``."""
686
+
687
+ seen: set[str] = set()
688
+ for value in values:
689
+ if value is None:
690
+ continue
691
+ if isinstance(value, bool):
692
+ seen.add("boolean")
693
+ elif type(value) is int:
694
+ seen.add("integer")
695
+ elif type(value) is float:
696
+ seen.add("number")
697
+ elif isinstance(value, str):
698
+ seen.add("string")
699
+ else:
700
+ seen.add("string")
701
+ if len(seen) > 1:
702
+ return "mixed"
703
+ if not seen:
704
+ return "null"
705
+ return seen.pop()
706
+
707
+
708
+ def _compare(cell: Any, literal: Any) -> int | None:
709
+ """Order one cell against one literal, or ``None`` when the two are not comparable.
710
+
711
+ Incomparable is a third answer rather than an exception or a coerced ``False``: a predicate
712
+ over a mixed column must exclude the rows it cannot judge, and must not decide that a string
713
+ is less than a number on the strength of Python's willingness to say so (it is not, and in
714
+ Python 3 it raises).
715
+ """
716
+
717
+ if cell is None or literal is None:
718
+ return None
719
+ if isinstance(cell, bool) != isinstance(literal, bool):
720
+ return None
721
+ if isinstance(cell, bool):
722
+ return (cell > literal) - (cell < literal)
723
+ numeric = (int, float)
724
+ if type(cell) in numeric and type(literal) in numeric:
725
+ return (cell > literal) - (cell < literal)
726
+ if isinstance(cell, str) and isinstance(literal, str):
727
+ return (cell > literal) - (cell < literal)
728
+ return None
729
+
730
+
731
+ def evaluate_predicate(predicate: Mapping[str, Any], row: Mapping[str, Any]) -> bool:
732
+ """Decide one validated predicate against one row. Total, and never raises on data."""
733
+
734
+ operator = predicate["op"]
735
+ if operator in {"and", "or"}:
736
+ results = (evaluate_predicate(item, row) for item in predicate["operands"])
737
+ return all(results) if operator == "and" else any(results)
738
+ if operator == "not":
739
+ return not evaluate_predicate(predicate["operand"], row)
740
+ # A predicate naming a column the source does not have is False for every row rather than an
741
+ # error. The column list is already in the source_inspect answer, so a caller that wants the
742
+ # difference between "no such column" and "no matching row" can see it there; failing the
743
+ # whole probe instead would make an exploratory typo cost a round trip.
744
+ cell = row.get(predicate["column"])
745
+ if operator == "is_null":
746
+ return cell is None
747
+ if operator == "is_not_null":
748
+ return cell is not None
749
+ order = _compare(cell, predicate["value"])
750
+ if order is None:
751
+ return False
752
+ return {
753
+ "eq": order == 0,
754
+ "ne": order != 0,
755
+ "lt": order < 0,
756
+ "le": order <= 0,
757
+ "gt": order > 0,
758
+ "ge": order >= 0,
759
+ }[operator]