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,485 @@
1
+ """What ``mr-data approve`` derives, and the plain summary of what freezing a Recipe binds.
2
+
3
+ Approving a Recipe used to mean typing four identifiers correctly -- an approval name, an approver,
4
+ a timestamp, and a policy fingerprint -- and the README's own example passed sixty-four zeros for
5
+ the last one, which is how that goes in practice. Everything here exists so none of those has to be
6
+ typed, and so the person is shown what they are freezing before it is frozen.
7
+
8
+ Two rules hold this module together.
9
+
10
+ **Every derived value satisfies the contract by construction.** The validators in
11
+ :mod:`mostlyright.data_harness.recipe` are the truth about what a ``RecipeApproval`` accepts, and
12
+ the derivations below are written against them, not against a copy of their grammar.
13
+
14
+ **The approver is a person who ran a command.** ``derive_approved_by`` reads the flag the operator
15
+ passed or the owner of this process, and nothing else -- not the environment, not the Recipe, not a
16
+ file. When neither yields a usable name it refuses, because an approval naming the wrong person is
17
+ worse than an approval that did not happen.
18
+
19
+ Nothing here prints, reads a file, or writes one. :func:`approval_summary` returns structured
20
+ facts for terminal and browser rendering without going through argparse.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import os
26
+ import pwd
27
+ from collections.abc import Mapping, Sequence
28
+ from dataclasses import dataclass
29
+ from datetime import UTC, datetime
30
+ from types import MappingProxyType
31
+ from typing import Any
32
+
33
+ from mostlyright.data_harness.canonical import canonical_json_bytes, sha256_bytes
34
+ from mostlyright.data_harness.local_contracts import GraphTablePlan
35
+ from mostlyright.data_harness.recipe import (
36
+ FrozenRecipe,
37
+ RecipeError,
38
+ _identifier,
39
+ _timestamp,
40
+ )
41
+ from mostlyright.data_harness.ux.render import render_lines
42
+
43
+ # The identifier grammar `recipe._IDENT` accepts: a leading letter or digit, then up to 127 more
44
+ # characters drawn from letters, digits, and `_ . -`. The derivations below fit inside it rather
45
+ # than hand the contract a value it would refuse.
46
+ MAX_IDENTIFIER_CHARACTERS = 128
47
+ IDENTIFIER_CHARACTERS = "_.-"
48
+ APPROVAL_ID_PREFIX = "approval-"
49
+ FINGERPRINT_CHARACTERS = 12
50
+
51
+ # The canonical UTC grammar `recipe._timestamp` accepts, to the second.
52
+ TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
53
+
54
+ # The approval policy when the operator supplies none: a policy with no rules written down. Its
55
+ # fingerprint is a real sha256 of real canonical bytes, which is the whole point -- a fingerprint
56
+ # of sixty-four zeros means "I did not think about this", and that is worse than no fingerprint.
57
+ #
58
+ # Changing this value changes every fingerprint derived from it. It is a published constant, not
59
+ # an implementation detail: an approval recorded last year can only be re-checked against the same
60
+ # bytes it was hashed from.
61
+ EMPTY_APPROVAL_POLICY: Mapping[str, Any] = MappingProxyType({})
62
+
63
+
64
+ # ------------------------------------------------------------------------------------------------
65
+ # The derivations
66
+ # ------------------------------------------------------------------------------------------------
67
+
68
+
69
+ def derive_approval_id(recipe: FrozenRecipe) -> str:
70
+ """Name this approval after the exact Recipe it approves.
71
+
72
+ The name carries the Recipe's own name, its version, and the first
73
+ :data:`FINGERPRINT_CHARACTERS` characters of its fingerprint, so reading the name tells you
74
+ what was approved. It is derived from the Recipe alone and from nothing else, so approving the
75
+ same Recipe twice derives the same name: the second run is a visible refusal to overwrite
76
+ rather than a second approval of the same thing under a different name.
77
+ """
78
+
79
+ tail = f"-v{recipe.recipe_version}-{recipe.digest[:FINGERPRINT_CHARACTERS]}"
80
+ budget = MAX_IDENTIFIER_CHARACTERS - len(APPROVAL_ID_PREFIX) - len(tail)
81
+ stem = recipe.recipe_id[:budget].rstrip(IDENTIFIER_CHARACTERS) if budget > 0 else ""
82
+ derived = f"{APPROVAL_ID_PREFIX}{stem}{tail}" if stem else f"approval{tail}"
83
+ return _identifier(derived, "approval.approval_id")
84
+
85
+
86
+ def derive_approved_by(explicit: str | None = None) -> str:
87
+ """Name the person approving: the flag they passed, or the owner of this process.
88
+
89
+ There is deliberately no third source. The environment is not one of them either: the standard
90
+ ``getpass.getuser`` reads ``LOGNAME``/``USER``/``LNAME``/``USERNAME`` before it reads the
91
+ password database, so an approval built on it would name whoever last exported a variable
92
+ (T-29-26). This reads the process owner from the password database directly.
93
+
94
+ A name that cannot be established is a refusal naming ``--approved-by``. Nothing is invented:
95
+ this field is what the review trail rests on.
96
+ """
97
+
98
+ if explicit is not None:
99
+ if _as_identifier(explicit) != explicit.strip():
100
+ raise RecipeError(
101
+ "RECIPE_APPROVER",
102
+ "approval.approved_by",
103
+ "--approved-by must be letters, digits, and - . _ only, starting with a letter "
104
+ "or digit",
105
+ )
106
+ return _identifier(explicit.strip(), "approval.approved_by")
107
+ try:
108
+ owner = _process_owner()
109
+ except OSError as error:
110
+ raise RecipeError(
111
+ "RECIPE_APPROVER",
112
+ "approval.approved_by",
113
+ "this account has no name on this machine; pass --approved-by to say who is approving",
114
+ ) from error
115
+ derived = _as_identifier(owner)
116
+ if not derived:
117
+ raise RecipeError(
118
+ "RECIPE_APPROVER",
119
+ "approval.approved_by",
120
+ "the owner of this process has no usable name; pass --approved-by to say who is "
121
+ "approving",
122
+ )
123
+ return _identifier(derived, "approval.approved_by")
124
+
125
+
126
+ def derive_approved_at(explicit: str | None = None) -> str:
127
+ """The moment of approval, in the exact UTC grammar the contract accepts.
128
+
129
+ An explicit value is checked rather than trusted, so a test can pin the timestamp and a typo
130
+ is named at the point it was typed.
131
+ """
132
+
133
+ if explicit is not None:
134
+ return _timestamp(explicit, "approval.approved_at")
135
+ return _timestamp(datetime.now(UTC).strftime(TIMESTAMP_FORMAT), "approval.approved_at")
136
+
137
+
138
+ def derive_policy_digest(policy_value: Mapping[str, Any] | None = None) -> str:
139
+ """Fingerprint the approval policy the approver acted under.
140
+
141
+ With no policy supplied this is the fingerprint of :data:`EMPTY_APPROVAL_POLICY` -- a real hash
142
+ of the real canonical bytes of a policy with no rules in it. It is never sixty-four zeros.
143
+ """
144
+
145
+ value = EMPTY_APPROVAL_POLICY if policy_value is None else policy_value
146
+ if not isinstance(value, Mapping):
147
+ raise RecipeError(
148
+ "RECIPE_APPROVAL_POLICY",
149
+ "approval.approval_policy",
150
+ "an approval policy must be a JSON object",
151
+ )
152
+ try:
153
+ raw = canonical_json_bytes(dict(value))
154
+ except (TypeError, ValueError) as error:
155
+ raise RecipeError(
156
+ "RECIPE_APPROVAL_POLICY",
157
+ "approval.approval_policy",
158
+ "an approval policy must hold JSON values only",
159
+ ) from error
160
+ return sha256_bytes(raw)
161
+
162
+
163
+ def _process_owner() -> str:
164
+ """The name of the account this process runs as, read from the password database."""
165
+
166
+ try:
167
+ return pwd.getpwuid(os.getuid()).pw_name
168
+ except KeyError as error:
169
+ raise OSError("this user id has no password database entry") from error
170
+
171
+
172
+ def _as_identifier(value: str) -> str:
173
+ """``value`` reduced to the identifier grammar, or the empty string when nothing survives."""
174
+
175
+ kept = [
176
+ character
177
+ for character in value.strip()
178
+ if character.isascii() and (character.isalnum() or character in IDENTIFIER_CHARACTERS)
179
+ ]
180
+ while kept and not (kept[0].isascii() and kept[0].isalnum()):
181
+ kept.pop(0)
182
+ return "".join(kept)[:MAX_IDENTIFIER_CHARACTERS]
183
+
184
+
185
+ # ------------------------------------------------------------------------------------------------
186
+ # What you are freezing
187
+ # ------------------------------------------------------------------------------------------------
188
+
189
+ # Plain sentences for the checks the pinned engine always runs. An unknown check id falls through
190
+ # to its own name, so a new engine check appears in the summary the day it ships.
191
+ _CHECK_SENTENCES: Mapping[str, str] = MappingProxyType(
192
+ {
193
+ "EXACT_CANDIDATE_REPLAY": "the sealed result replays byte for byte",
194
+ "RECIPE_DRIFT": "the result stays inside the refresh bounds below",
195
+ "SOURCE_AUTHORITY": "every input file matches the fingerprint the run pins",
196
+ "OUTPUT_SEMANTICS": "every column keeps the type and meaning declared above",
197
+ "TEMPORAL_INTEGRITY": "dates and times stay inside the approved window",
198
+ }
199
+ )
200
+
201
+ _RELATIONSHIPS: Mapping[str, str] = MappingProxyType(
202
+ {
203
+ "one_to_one": "one row on each side matches at most one row on the other",
204
+ "many_to_one": "many rows on the left match one row on the right",
205
+ }
206
+ )
207
+
208
+ _CLEANING_SENTENCES: Mapping[str, str] = MappingProxyType(
209
+ {
210
+ "trim": "trim the spaces around {columns} in {source}",
211
+ "empty_to_null": "treat empty text as missing in {columns} of {source}",
212
+ "rename": "rename {columns} in {source}",
213
+ "cast": "read {columns} in {source}",
214
+ }
215
+ )
216
+
217
+ _CLOSING = (
218
+ "Freezing is permanent: this fingerprint is what runs from now on, it can be replayed for as "
219
+ "long as the inputs are kept, and any change at all means a new Recipe with a new fingerprint. "
220
+ "Nothing is approved until this command finishes, and nothing can approve it for you."
221
+ )
222
+
223
+
224
+ @dataclass(frozen=True)
225
+ class ApprovalSummary:
226
+ """Everything approving one Recipe binds, in the words a person reads before deciding.
227
+
228
+ Every field is derived from one parsed :class:`FrozenRecipe` and from no second source, so the
229
+ summary cannot describe something other than what the fingerprint binds (T-29-27).
230
+ """
231
+
232
+ recipe: str
233
+ recipe_digest: str
234
+ question: str
235
+ one_row_is: str
236
+ population: str
237
+ columns: tuple[str, ...]
238
+ sources: tuple[str, ...]
239
+ cleaning: tuple[str, ...]
240
+ join: str
241
+ checks: tuple[str, ...]
242
+ refresh_bounds: str
243
+ backfill_window: str
244
+ engine: str
245
+ what_it_is_for: str
246
+ plan_shape: str = "joined"
247
+ closing: str = _CLOSING
248
+
249
+ def to_dict(self) -> dict[str, Any]:
250
+ """The structured facts, in the order a person reads them.
251
+
252
+ Facts taken from the Recipe -- column names, source names, check names -- go in values,
253
+ never in keys, so nothing the data happens to be called is shown as a label.
254
+ """
255
+
256
+ steps_label = (
257
+ "the ordered graph steps" if self.plan_shape == "graph" else "the cleaning steps"
258
+ )
259
+ structure_label = "the graph output" if self.plan_shape == "graph" else "the join"
260
+ return {
261
+ "what this answers": self.question,
262
+ "one row is": self.one_row_is,
263
+ "it covers": self.population,
264
+ "what the result may be used for": self.what_it_is_for,
265
+ "the columns": _numbered("column", self.columns),
266
+ "the sources": _numbered("source", self.sources),
267
+ steps_label: _numbered("step", self.cleaning),
268
+ structure_label: self.join,
269
+ "the checks it must pass": _numbered("check", self.checks),
270
+ "the refresh bounds": self.refresh_bounds,
271
+ "the backfill window": self.backfill_window,
272
+ "the pinned versions": self.engine,
273
+ "the recipe": self.recipe,
274
+ "the fingerprint being frozen": self.recipe_digest,
275
+ "what freezing means": self.closing,
276
+ }
277
+
278
+ def lines(self) -> tuple[str, ...]:
279
+ """The terminal rendering of :meth:`to_dict`, and not a second truth.
280
+
281
+ The same renderer the command line uses for every other payload is used here, so what a
282
+ person reads on the terminal and what a caller reads under ``--json`` are the same facts
283
+ in the same order.
284
+ """
285
+
286
+ return tuple(render_lines(self.to_dict()))
287
+
288
+
289
+ def approval_summary(recipe: FrozenRecipe) -> ApprovalSummary:
290
+ """Say, in plain words, exactly what approving ``recipe`` freezes."""
291
+
292
+ plan = recipe.transform_plan
293
+ requirements = recipe.requirements
294
+ proposals = {item.source_id: item for item in recipe.source_proposals}
295
+ plan_sources = {item.source_id: item for item in plan.sources}
296
+ semantics = {item.name: item for item in recipe.output_semantics}
297
+ predecessor = (
298
+ "no earlier version"
299
+ if recipe.predecessor_recipe_digest is None
300
+ else f"replacing the version fingerprinted {recipe.predecessor_recipe_digest}"
301
+ )
302
+ covers = requirements.time_range
303
+ graph_plan = isinstance(plan, GraphTablePlan)
304
+ return ApprovalSummary(
305
+ recipe=f"{recipe.recipe_id}, version {recipe.recipe_version}, {predecessor}",
306
+ recipe_digest=recipe.digest,
307
+ question=recipe.question.text,
308
+ one_row_is=requirements.output_grain.description,
309
+ population=(
310
+ f"{requirements.population} From {covers.start_inclusive} up to {covers.end_exclusive}."
311
+ ),
312
+ columns=tuple(_column_sentence(semantics[name]) for name in plan.select),
313
+ sources=tuple(
314
+ _source_sentence(
315
+ source,
316
+ plan_sources.get(source.source_id),
317
+ proposals.get(source.source_id),
318
+ )
319
+ for source in sorted(recipe.sources, key=lambda item: item.source_id)
320
+ ),
321
+ cleaning=(
322
+ tuple(_graph_node_sentence(node) for node in plan.nodes)
323
+ if graph_plan
324
+ else tuple(_cleaning_sentence(step) for step in plan.cleaning)
325
+ ),
326
+ join=_graph_output_sentence(plan) if graph_plan else _join_sentence(plan.join),
327
+ checks=_check_sentences(recipe, plan.quality),
328
+ refresh_bounds=_refresh_sentence(recipe.refresh_policy),
329
+ backfill_window=_backfill_sentence(recipe.backfill_policy),
330
+ engine=(
331
+ f"engine {recipe.engine_version}, operations {recipe.operation_contract_version}, "
332
+ f"readers {recipe.reader_contract_version}, "
333
+ f"PyArrow {recipe.pyarrow_version}, checks {recipe.validation_policy_version} "
334
+ f"({recipe.validation_policy_digest})"
335
+ ),
336
+ what_it_is_for=_intent_sentence(plan.output_intent),
337
+ plan_shape="graph" if graph_plan else "joined",
338
+ )
339
+
340
+
341
+ # What the frozen intent means, said as a permission rather than as the identifier it is stored
342
+ # under. These are the three `local_contracts._OUTPUT_INTENTS`, and the guard below fails when that
343
+ # set grows a fourth, because an intent with no sentence here would be frozen unstated.
344
+ _INTENT_SENTENCES: Mapping[str, str] = MappingProxyType(
345
+ {
346
+ "local_use": "use on this machine only -- not published, not hosted",
347
+ "redistribute": "passing the result on to others",
348
+ "host": "serving the result from a hosted service",
349
+ }
350
+ )
351
+
352
+
353
+ def _intent_sentence(intent: str) -> str:
354
+ """What this Recipe declares the result is for, and what that costs every source.
355
+
356
+ This is not decoration beside the rights already listed per source. It is the frozen claim the
357
+ build enforces: `pipeline` refuses to build when a source's recorded permissions do not include
358
+ this intent, so freezing `redistribute` and freezing `local_use` are two different approvals of
359
+ the same data, and a summary that shows neither asked somebody to agree to whichever one it
360
+ happened to be.
361
+ """
362
+
363
+ meaning = _INTENT_SENTENCES.get(intent)
364
+ plain = f"{intent}: {meaning}" if meaning is not None else intent
365
+ return f"{plain}. Every source above has to permit it, and a build stops when one does not."
366
+
367
+
368
+ def _column_sentence(field: Any) -> str:
369
+ parts = [f"{field.name}: {field.logical_type}, {field.semantic_type}"]
370
+ if field.unit not in {"none", "count"}:
371
+ parts.append(f"in {field.unit}")
372
+ elif field.unit == "count":
373
+ parts.append("a count")
374
+ if field.temporal_role != "none":
375
+ parts.append(f"used as the {field.temporal_role.replace('_', ' ')}")
376
+ if field.timezone is not None:
377
+ parts.append(f"in {field.timezone}")
378
+ return ", ".join(parts)
379
+
380
+
381
+ def _source_sentence(source: Any, plan_source: Any, proposal: Any) -> str:
382
+ parts = [f"{source.source_id}: {source.plan_source_path} ({source.data_format})"]
383
+ if proposal is not None:
384
+ parts.append(f"chosen as {proposal.display_name}, rights {proposal.rights_status}")
385
+ if plan_source is not None:
386
+ rights = plan_source.rights
387
+ parts.append(f"from {plan_source.origin}, obtained by {plan_source.acquisition_method}")
388
+ parts.append(f"rights {rights.status}, may {', '.join(rights.permissions)}")
389
+ parts.append(f"rights evidence {rights.evidence}")
390
+ parts.append(f"opened by {source.adapter_id} {source.adapter_version}")
391
+ parts.append(f"treated as {source.declared_classification}, under {source.lawful_basis}")
392
+ parts.append(
393
+ "keeps no Bookmark"
394
+ if source.watermark_kind == "none"
395
+ else f"Bookmark from {source.watermark_field} ({source.watermark_kind})"
396
+ )
397
+ return "; ".join(parts)
398
+
399
+
400
+ def _cleaning_sentence(step: Any) -> str:
401
+ columns = step.columns
402
+ if columns and isinstance(columns[0], tuple):
403
+ written = ", ".join(f"{name} as {target}" for name, target in columns)
404
+ else:
405
+ written = ", ".join(str(name) for name in columns)
406
+ template = _CLEANING_SENTENCES.get(step.operation)
407
+ if template is None:
408
+ return f"{step.operation} {written} in {step.source_id}"
409
+ return template.format(columns=written, source=step.source_id)
410
+
411
+
412
+ def _join_sentence(join: Any) -> str:
413
+ relationship = _RELATIONSHIPS.get(join.cardinality, join.cardinality)
414
+ return (
415
+ f"{join.left} joined to {join.right} on {', '.join(join.on)} "
416
+ f"({join.kind} join): {relationship} ({join.cardinality})"
417
+ )
418
+
419
+
420
+ def _graph_node_sentence(node: Any) -> str:
421
+ inputs = ", ".join(node.inputs) if node.inputs else "none"
422
+ parameters = canonical_json_bytes(node.to_dict()["parameters"]).decode("utf-8")
423
+ return (
424
+ f"{node.node_id}: {node.operation} {node.operation_version}; "
425
+ f"inputs: {inputs}; parameters {parameters}"
426
+ )
427
+
428
+
429
+ def _graph_output_sentence(plan: GraphTablePlan) -> str:
430
+ join_nodes = tuple(node.node_id for node in plan.nodes if node.operation == "join")
431
+ joins = f"join steps {', '.join(join_nodes)}" if join_nodes else "no join step"
432
+ return (
433
+ f"the closed graph ends at {plan.terminal} after {len(plan.nodes)} ordered steps; {joins}"
434
+ )
435
+
436
+
437
+ def _check_sentences(recipe: FrozenRecipe, quality: Any) -> tuple[str, ...]:
438
+ written = [f"at least {quality.min_rows} rows"]
439
+ if quality.not_null:
440
+ written.append(f"never missing: {', '.join(quality.not_null)}")
441
+ written.append(f"each {', '.join(recipe.transform_plan.grain)} appears exactly once")
442
+ written.extend(
443
+ f"{check_id}: {_CHECK_SENTENCES.get(check_id, 'the pinned engine check of that name')}"
444
+ for check_id in recipe.required_check_ids
445
+ )
446
+ return tuple(written)
447
+
448
+
449
+ def _refresh_sentence(policy: Any) -> str:
450
+ additions = (
451
+ "new columns are allowed" if policy.allow_column_additions else "new columns are refused"
452
+ )
453
+ return (
454
+ f"a later run must return between {policy.min_row_count_percent}% and "
455
+ f"{policy.max_row_count_percent}% of the rows this version has, and {additions}"
456
+ )
457
+
458
+
459
+ def _backfill_sentence(policy: Any) -> str:
460
+ envelope = policy.coverage_envelope
461
+ scope = policy.partition_scope
462
+ fields = ", ".join(scope.fields) if scope.fields else "the whole result"
463
+ pieces = scope.max_partitions_per_run
464
+ return (
465
+ f"backfills may cover {envelope.start_inclusive} up to {envelope.end_exclusive}, "
466
+ f"at most {_duration(policy.max_window_seconds)} in one run, "
467
+ f"over {fields} in at most {pieces} {'piece' if pieces == 1 else 'pieces'} per run, "
468
+ f"overlapping released coverage: {policy.overlap_policy}"
469
+ )
470
+
471
+
472
+ def _duration(seconds: int) -> str:
473
+ """Seconds, with the plain reading of them alongside when it is an exact number of days."""
474
+
475
+ if seconds % 86_400 == 0:
476
+ days = seconds // 86_400
477
+ return f"{seconds} seconds ({days} {'day' if days == 1 else 'days'})"
478
+ return f"{seconds} seconds"
479
+
480
+
481
+ def _numbered(noun: str, items: Sequence[str]) -> dict[str, str]:
482
+ """One numbered key per item, so the order a person reads is the order the Recipe runs."""
483
+
484
+ width = len(str(max(len(items), 1)))
485
+ return {f"{noun} {index:0{width}d}": item for index, item in enumerate(items, start=1)}