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,514 @@
1
+ """The Toolbox: the closed table of Reader families the product ships with.
2
+
3
+ Resolution is on an exact ``(family_id, family_version)`` key. A near version is refused,
4
+ never substituted, because a recipe pins the code that decoded its bytes and a substituted
5
+ decoder would silently answer a different question. ``weather.grib2@1.0.0`` and
6
+ ``weather.grib2@2.0.0`` are co-installed so each Recipe resolves its exact pin.
7
+
8
+ The table is closed and has no discovery path. Membership is exactly the frozen descriptors
9
+ written into ``_CERTIFIED_FAMILIES`` below: there is no plugin scan, entry point, environment
10
+ override, or recipe-controlled module. A descriptor imports its one literal module/factory only
11
+ after exact coordinate resolution. Adding a family is a source edit that goes through
12
+ Certification and review.
13
+
14
+ The table is deliberately not derived from ``formats``. The two answer different
15
+ questions: ``formats`` answers "what encoding may be sealed", and this table answers "what
16
+ code may run". A format may be sealable with no family that produces it, and a family may
17
+ be certified before any recipe pins it. Deriving one from the other would make each answer
18
+ the other's question.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import importlib
24
+ from dataclasses import dataclass, field
25
+
26
+ from mostlyright.data_harness.formats import DATA_FORMATS, READER_CONTRACT_VERSION
27
+ from mostlyright.data_harness.readers.contracts import (
28
+ ReaderBudgets,
29
+ ReaderError,
30
+ ReaderFamily,
31
+ bulk_default_budgets,
32
+ identifier,
33
+ semver,
34
+ )
35
+
36
+ __all__ = ["TOOLBOX", "ReaderRegistry", "certified_descriptors"]
37
+
38
+
39
+ _SPREADSHEET_DEFAULT_BUDGETS = ReaderBudgets(
40
+ max_uncompressed_bytes=32 * 1024 * 1024,
41
+ max_declared_cells=250_000,
42
+ max_rows=250_001,
43
+ max_columns=256,
44
+ )
45
+ _JSON_DEFAULT_BUDGETS = bulk_default_budgets()
46
+
47
+
48
+ def spreadsheet_default_budgets() -> ReaderBudgets:
49
+ """Return the one certified budget value shared by lazy and loaded XLSX metadata."""
50
+
51
+ return _SPREADSHEET_DEFAULT_BUDGETS
52
+
53
+
54
+ # What a family must answer before it may enter the table. Checked at registration rather
55
+ # than at first use, so a mis-shaped family fails when the process starts and not when a
56
+ # refresh is halfway through.
57
+ _REQUIRED_MEMBERS = (
58
+ "family_id",
59
+ "family_version",
60
+ "contract_version",
61
+ "output_format",
62
+ "accepted_media_types",
63
+ "default_budgets",
64
+ "validate_options",
65
+ "decode",
66
+ )
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class _FamilyDescriptor:
71
+ """Static certified metadata plus one fixed lazy factory coordinate.
72
+
73
+ ``settings_summary`` is the one sentence a recipe author reads about this coordinate. It is
74
+ carried here rather than typed into the skill's settings table because that table, the closed
75
+ coordinate tables in the suite, and this tuple were three hand-kept copies of one fact, and
76
+ adding a family meant remembering all of them. ``scripts/generate_registry_tables.py`` emits
77
+ every one of those restatements from this tuple, and a test in the ordinary collection fails
78
+ when a checked-in copy and this tuple disagree. It is inert metadata: nothing in resolution,
79
+ decoding, or admission reads it.
80
+ """
81
+
82
+ family_id: str
83
+ family_version: str
84
+ module_name: str
85
+ factory_name: str
86
+ accepted_media_types: tuple[str, ...]
87
+ settings_summary: str
88
+ contract_version: str = READER_CONTRACT_VERSION
89
+ output_format: str = "csv"
90
+ default_budgets: ReaderBudgets = field(default_factory=ReaderBudgets)
91
+ optional_dependency: str | None = None
92
+
93
+ def __post_init__(self) -> None:
94
+ # A blank summary would emit a blank table cell rather than fail, so the emptiness is
95
+ # refused where the entry is written instead of surviving into a page somebody reads.
96
+ # The value has to *be* its one line, not merely split into one. ``splitlines`` honours
97
+ # every separator that would break a table row, including the vertical tab and the
98
+ # paragraph separator, but it drops a trailing one -- so a summary ending in a newline
99
+ # splits into a single line and would push the row after it out of the table anyway.
100
+ summary = self.settings_summary
101
+ lines = summary.splitlines() if isinstance(summary, str) else []
102
+ if (
103
+ not isinstance(summary, str)
104
+ or not summary.strip()
105
+ or "|" in summary
106
+ or len(lines) != 1
107
+ or lines[0] != summary
108
+ ):
109
+ raise ReaderError(
110
+ "READER_CONTRACT",
111
+ "toolbox.family.settings_summary",
112
+ "must be one nonempty line holding no table separator",
113
+ )
114
+ # The factory coordinate is written verbatim into a checked-in Python table, so its shape
115
+ # is refused here rather than trusted. A value carrying a quote would change the emitted
116
+ # tuple's arity instead of failing, and this is the field that decides what gets imported.
117
+ if not all(part.isidentifier() for part in self.module_name.split(".")) or (
118
+ not self.factory_name.isidentifier()
119
+ ):
120
+ raise ReaderError(
121
+ "READER_CONTRACT",
122
+ "toolbox.family.module_name",
123
+ "must be a dotted identifier naming one module and one factory",
124
+ )
125
+ # The other two fields reach the same emitted tuple through the same interpolation, and
126
+ # a quote in either would change that tuple's arity rather than fail. Validating only
127
+ # the import target would have been an arbitrary half of the same rule.
128
+ quoted = [
129
+ value
130
+ for value in (self.optional_dependency, *self.accepted_media_types)
131
+ if value is not None and ('"' in value or "\\" in value or value != value.strip())
132
+ ]
133
+ if quoted:
134
+ raise ReaderError(
135
+ "READER_CONTRACT",
136
+ "toolbox.family.accepted_media_types",
137
+ "must carry no quote, backslash, or surrounding space",
138
+ )
139
+
140
+ def load(self) -> ReaderFamily:
141
+ """Import the fixed factory, translating only optional absence/incompatibility."""
142
+
143
+ try:
144
+ module = importlib.import_module(self.module_name)
145
+ factory = getattr(module, self.factory_name)
146
+ family = factory()
147
+ except (AttributeError, ImportError) as exc:
148
+ if self.optional_dependency is None:
149
+ raise
150
+ raise ReaderError(
151
+ "READER_DEPENDENCY_UNAVAILABLE",
152
+ "recipe.sources.family_id",
153
+ f"optional dependency {self.optional_dependency} is unavailable or incompatible "
154
+ f"for {self.family_id}@{self.family_version}",
155
+ ) from exc
156
+ _validate_family(family)
157
+ for name in (
158
+ "family_id",
159
+ "family_version",
160
+ "contract_version",
161
+ "output_format",
162
+ "accepted_media_types",
163
+ "default_budgets",
164
+ ):
165
+ if getattr(family, name) != getattr(self, name):
166
+ raise ReaderError(
167
+ "READER_CONTRACT",
168
+ f"toolbox.family.{name}",
169
+ "loaded family metadata differs from its certified lazy descriptor",
170
+ )
171
+ return family
172
+
173
+
174
+ def _validate_family(family: ReaderFamily | _FamilyDescriptor) -> tuple[str, str]:
175
+ """Validate one concrete family or static descriptor and return its exact key."""
176
+
177
+ missing = [name for name in _REQUIRED_MEMBERS if not hasattr(family, name)]
178
+ if isinstance(family, _FamilyDescriptor):
179
+ missing = []
180
+ if missing:
181
+ raise ReaderError(
182
+ "READER_CONTRACT",
183
+ "toolbox.family",
184
+ f"does not answer the Reader contract: missing {', '.join(sorted(missing))}",
185
+ )
186
+ identifier(family.family_id, "toolbox.family.family_id")
187
+ semver(family.family_version, "toolbox.family.family_version")
188
+ if family.contract_version != READER_CONTRACT_VERSION:
189
+ raise ReaderError(
190
+ "READER_CONTRACT",
191
+ "toolbox.family.contract_version",
192
+ f"is not {READER_CONTRACT_VERSION}: a family built against another contract "
193
+ "cannot enter the table",
194
+ )
195
+ if family.output_format not in DATA_FORMATS:
196
+ raise ReaderError(
197
+ "READER_CONTRACT",
198
+ "toolbox.family.output_format",
199
+ "is not a format the table admits",
200
+ )
201
+ media_types = family.accepted_media_types
202
+ if (
203
+ not isinstance(media_types, tuple)
204
+ or not media_types
205
+ or any(not isinstance(item, str) or not item for item in media_types)
206
+ or tuple(sorted(set(media_types))) != media_types
207
+ ):
208
+ raise ReaderError(
209
+ "READER_CONTRACT",
210
+ "toolbox.family.accepted_media_types",
211
+ "must be a nonempty sorted tuple of distinct media types",
212
+ )
213
+ if not isinstance(family.default_budgets, ReaderBudgets):
214
+ raise ReaderError(
215
+ "READER_CONTRACT",
216
+ "toolbox.family.default_budgets",
217
+ "must be a typed budget object",
218
+ )
219
+ return family.family_id, family.family_version
220
+
221
+
222
+ class ReaderRegistry:
223
+ """The closed Reader allowlist, keyed on exact identity and exact version."""
224
+
225
+ def __init__(self, families: tuple[ReaderFamily | _FamilyDescriptor, ...] = ()) -> None:
226
+ self._families: dict[tuple[str, str], ReaderFamily | _FamilyDescriptor] = {}
227
+ for family in families:
228
+ self.register(family)
229
+
230
+ def register(self, family: ReaderFamily | _FamilyDescriptor) -> None:
231
+ """Admit one certified family, refusing anything the contract does not allow."""
232
+
233
+ key = _validate_family(family)
234
+ if key in self._families:
235
+ raise ReaderError(
236
+ "READER_CONTRACT",
237
+ "toolbox",
238
+ f"family {key[0]}@{key[1]} is already registered",
239
+ )
240
+ self._families[key] = family
241
+
242
+ def resolve(self, family_id: str, family_version: str) -> ReaderFamily:
243
+ """Return the one family registered under this exact coordinate.
244
+
245
+ There is no fallback. A version this table does not hold is refused by name, and
246
+ the refusal names the versions that are held, because "the recipe pins 1.0.1 and
247
+ this build ships 1.0.0" is the fix a person needs to read.
248
+ """
249
+
250
+ try:
251
+ family = self._families[(family_id, family_version)]
252
+ except KeyError:
253
+ available = sorted(
254
+ version for identity, version in self._families if identity == family_id
255
+ )
256
+ known = f"; this build ships {', '.join(available)}" if available else ""
257
+ raise ReaderError(
258
+ "READER_FAMILY_UNKNOWN",
259
+ "recipe.sources.family_id",
260
+ f"no certified Reader {family_id}@{family_version} is in the Toolbox{known}",
261
+ ) from None
262
+ if isinstance(family, _FamilyDescriptor):
263
+ loaded = family.load()
264
+ self._families[(family_id, family_version)] = loaded
265
+ return loaded
266
+ return family
267
+
268
+ def families(self) -> tuple[tuple[str, str], ...]:
269
+ """The sorted coordinates of every certified family, as a value and not the table."""
270
+
271
+ return tuple(sorted(self._families))
272
+
273
+ def widest_version(self, family_id: str) -> str:
274
+ """The highest certified version of one family: the coordinate a new source should pin.
275
+
276
+ A weak-label widening lands as a new coordinate rather than an edit to an existing one,
277
+ so that an already-approved recipe cannot silently begin admitting responses its review
278
+ never saw. The cost of that rule is that the *base* version is the wrong default for a
279
+ source nobody has approved yet: pinning ``delimited_text@1.0.0`` for a brand-new source
280
+ refuses a publisher that merely declined to describe its file, and the author has to
281
+ know a version table to avoid it.
282
+
283
+ This is the one place that answers "which coordinate should a new pin name". Highest is
284
+ decided on the parsed numeric version and never on the string: ``families()`` is sorted
285
+ lexicographically, where ``1.10.0`` sorts *before* ``1.9.0``, so taking its last entry
286
+ would answer ``1.9.0`` for a family that had certified ``1.10.0``. No family carries a
287
+ double-digit component today, which is exactly why this would have gone unnoticed until
288
+ one did.
289
+
290
+ Choosing a coordinate is not the same as widening one. This picks among what review has
291
+ already certified; it can never admit a media type no certified family carries.
292
+
293
+ It answers the version question and not the fitness question, and the two come apart on a
294
+ major bump. A minor widening inherits ``decode`` untouched, so the highest version seals
295
+ the bytes the base version would have sealed and only admits more responses. A major one
296
+ need not: ``weather.grib2@2.0.0`` emits columns ``1.0.0`` does not, so the highest
297
+ coordinate there is a different output contract rather than a wider door onto the same
298
+ one. A caller pinning a new source still chooses deliberately for such a family; what
299
+ this removes is having to read a version table to avoid being refused for a label.
300
+ """
301
+
302
+ versions = [version for identity, version in self.families() if identity == family_id]
303
+ if not versions:
304
+ raise ReaderError(
305
+ "READER_FAMILY_UNKNOWN",
306
+ "toolbox.family",
307
+ f"{family_id} is not a certified Reader family",
308
+ )
309
+ return max(versions, key=lambda version: tuple(int(part) for part in version.split(".")))
310
+
311
+ def accepted_media_types(self) -> tuple[str, ...]:
312
+ """Every media type any certified family admits as input, sorted and de-duplicated.
313
+
314
+ A property of the Toolbox rather than of any caller, and the one derivation of it.
315
+ A fetch for a pinned source has to admit media types no wire format claims -- a zipped
316
+ source arrives as an archive -- and the answer to "which ones" is this table's to give.
317
+ Restating a media type outside the module that owns it is the drift the format table's
318
+ anti-duplication gate exists to catch, and it scans media types as well as format names
319
+ (the format table's own; a Reader-only media type such as an archive's is bound by each
320
+ family's certified descriptor and the load-time equality check instead).
321
+
322
+ The union is the outer bound. A single pinned fetch narrows further, to the one
323
+ family's own declaration, because a source pinned to a zip Reader has no business
324
+ admitting a spreadsheet.
325
+ """
326
+
327
+ return tuple(
328
+ sorted(
329
+ {
330
+ media_type
331
+ for family in self._families.values()
332
+ for media_type in family.accepted_media_types
333
+ }
334
+ )
335
+ )
336
+
337
+
338
+ # Certified Reader families. Membership requires adversarial review, an exact decoder pin, and
339
+ # known-good samples. The tuple has no provisional state.
340
+ _CERTIFIED_FAMILIES: tuple[_FamilyDescriptor, ...] = (
341
+ _FamilyDescriptor(
342
+ "archive.zip",
343
+ "1.0.0",
344
+ "mostlyright.data_harness.readers.containers",
345
+ "ZipReader",
346
+ ("application/x-zip-compressed", "application/zip"),
347
+ settings_summary="`member`; `member_format` (`csv` or `tsv`); optional `header: true`.",
348
+ default_budgets=bulk_default_budgets(),
349
+ ),
350
+ _FamilyDescriptor(
351
+ "archive.zip",
352
+ "1.1.0",
353
+ "mostlyright.data_harness.readers.containers",
354
+ "ZipReaderV1_1",
355
+ ("application/x-zip-compressed", "application/zip"),
356
+ settings_summary="Same, plus `member_format: semicolon` for publishers such as DWD.",
357
+ default_budgets=bulk_default_budgets(),
358
+ ),
359
+ _FamilyDescriptor(
360
+ "archive.zip",
361
+ "1.2.0",
362
+ "mostlyright.data_harness.readers.containers",
363
+ "ZipReaderV1_2",
364
+ ("application/octet-stream", "application/x-zip-compressed", "application/zip"),
365
+ settings_summary=(
366
+ "Same as `archive.zip@1.1.0`; also accepts a response served as "
367
+ "`application/octet-stream`."
368
+ ),
369
+ default_budgets=bulk_default_budgets(),
370
+ ),
371
+ _FamilyDescriptor(
372
+ "archive.gzip",
373
+ "1.0.0",
374
+ "mostlyright.data_harness.readers.containers",
375
+ "GzipReader",
376
+ ("application/gzip", "application/x-gzip"),
377
+ settings_summary="`member_format`; optional `header: true`. `member` is invalid.",
378
+ default_budgets=bulk_default_budgets(),
379
+ ),
380
+ _FamilyDescriptor(
381
+ "archive.gzip",
382
+ "1.1.0",
383
+ "mostlyright.data_harness.readers.containers",
384
+ "GzipReaderV1_1",
385
+ ("application/gzip", "application/octet-stream", "application/x-gzip"),
386
+ settings_summary="Same; also accepts a response served as `application/octet-stream`.",
387
+ default_budgets=bulk_default_budgets(),
388
+ ),
389
+ _FamilyDescriptor(
390
+ "archive.tar",
391
+ "1.0.0",
392
+ "mostlyright.data_harness.readers.containers",
393
+ "TarReader",
394
+ ("application/x-tar",),
395
+ settings_summary="Same as `archive.zip@1.0.0`.",
396
+ default_budgets=bulk_default_budgets(),
397
+ ),
398
+ _FamilyDescriptor(
399
+ "archive.tar",
400
+ "1.1.0",
401
+ "mostlyright.data_harness.readers.containers",
402
+ "TarReaderV1_1",
403
+ ("application/octet-stream", "application/x-tar"),
404
+ settings_summary="Same; also accepts a response served as `application/octet-stream`.",
405
+ default_budgets=bulk_default_budgets(),
406
+ ),
407
+ _FamilyDescriptor(
408
+ "spreadsheet.xlsx",
409
+ "1.0.0",
410
+ "mostlyright.data_harness.readers.spreadsheet",
411
+ "SpreadsheetReader",
412
+ ("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",),
413
+ settings_summary=(
414
+ "`sheet`, `header_row`, and `range`. The heading row must be the first row of the "
415
+ "range."
416
+ ),
417
+ default_budgets=spreadsheet_default_budgets(),
418
+ ),
419
+ _FamilyDescriptor(
420
+ "spreadsheet.xlsx",
421
+ "1.1.0",
422
+ "mostlyright.data_harness.readers.spreadsheet",
423
+ "SpreadsheetReaderV1_1",
424
+ (
425
+ "application/octet-stream",
426
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
427
+ ),
428
+ settings_summary="Same; also accepts a response served as `application/octet-stream`.",
429
+ default_budgets=spreadsheet_default_budgets(),
430
+ ),
431
+ _FamilyDescriptor(
432
+ "delimited_text",
433
+ "1.0.0",
434
+ "mostlyright.data_harness.readers.delimited",
435
+ "DelimitedTextReader",
436
+ ("text/csv", "text/tab-separated-values"),
437
+ settings_summary=(
438
+ "`delimiter` (comma or tab); optional `header: true`; optional `encoding: utf-8`."
439
+ ),
440
+ default_budgets=bulk_default_budgets(),
441
+ ),
442
+ _FamilyDescriptor(
443
+ "delimited_text",
444
+ "1.1.0",
445
+ "mostlyright.data_harness.readers.delimited",
446
+ "DelimitedTextReaderV1_1",
447
+ ("text/csv", "text/plain", "text/tab-separated-values"),
448
+ settings_summary=(
449
+ "Same; also accepts a response served as `text/plain` (common for public agency CSV "
450
+ "such as IEM)."
451
+ ),
452
+ default_budgets=bulk_default_budgets(),
453
+ ),
454
+ _FamilyDescriptor(
455
+ "json.tabular",
456
+ "1.0.0",
457
+ "mostlyright.data_harness.readers.json_tabular",
458
+ "JsonTabularReader",
459
+ ("application/json", "application/x-ndjson"),
460
+ settings_summary=(
461
+ "`columns` with output names and RFC 6901 pointers; optional `document_format` (`json` "
462
+ "or `ndjson`), `records_pointer`, and sequential `expand` pointers. Columns are "
463
+ "required unless they state `required: false`."
464
+ ),
465
+ default_budgets=_JSON_DEFAULT_BUDGETS,
466
+ ),
467
+ _FamilyDescriptor(
468
+ "json.tabular",
469
+ "1.1.0",
470
+ "mostlyright.data_harness.readers.json_tabular",
471
+ "JsonTabularReaderV1_1",
472
+ ("application/json", "application/x-ndjson", "text/plain"),
473
+ settings_summary=(
474
+ "Same; also accepts a response served as `text/plain` (common for public agency JSON "
475
+ "and NDJSON)."
476
+ ),
477
+ default_budgets=_JSON_DEFAULT_BUDGETS,
478
+ ),
479
+ _FamilyDescriptor(
480
+ "weather.grib2",
481
+ "1.0.0",
482
+ "mostlyright.data_harness.readers.grib2.decode",
483
+ "WeatherGrib2Reader",
484
+ ("application/octet-stream", "application/wmo-grib2"),
485
+ settings_summary="`variable`, `points`, and `admission_allowlist_version`.",
486
+ optional_dependency="mostlyright-grib",
487
+ ),
488
+ _FamilyDescriptor(
489
+ "weather.grib2",
490
+ "2.0.0",
491
+ "mostlyright.data_harness.readers.grib2.decode",
492
+ "WeatherGrib2ReaderV2",
493
+ ("application/octet-stream", "application/wmo-grib2"),
494
+ settings_summary=(
495
+ "Same settings as 1.0.0; output also includes time, level, matched coordinates, and "
496
+ "message digest."
497
+ ),
498
+ optional_dependency="mostlyright-grib",
499
+ ),
500
+ )
501
+
502
+ TOOLBOX = ReaderRegistry(_CERTIFIED_FAMILIES)
503
+
504
+
505
+ def certified_descriptors() -> tuple[_FamilyDescriptor, ...]:
506
+ """The frozen certified descriptors, in table order, without loading a single decoder.
507
+
508
+ ``TOOLBOX.resolve`` imports the family it answers with, which for ``weather.grib2`` needs an
509
+ optional binding that a plain install does not carry. Everything restated about a coordinate
510
+ -- its media types, its factory, the sentence a recipe author reads -- is already on the
511
+ frozen descriptor, so the table generator reads it from here and never imports anything.
512
+ """
513
+
514
+ return _CERTIFIED_FAMILIES
@@ -0,0 +1,110 @@
1
+ # Known-good samples
2
+
3
+ A known-good sample is a real file with its correct answer written down beside it. Certification
4
+ requires one: a Reader lands with samples or it does not land.
5
+
6
+ These samples have two consumers and one purpose. CI decodes every sample on every leg and fails
7
+ if any answer has moved. A refresh re-opens one designated sample before it fetches anything, and
8
+ halts if the answer has moved. Both read the bytes and the recorded answer from this tree through
9
+ one loader (`readers/samples.py`) and compare through one function, so the number CI asserts and
10
+ the number a refresh checks against cannot drift apart. Two copies of one constant is the defect
11
+ this convention exists to prevent.
12
+
13
+ The tree ships inside the package rather than under `tests/`, because the warm-up check runs in
14
+ production and its sample must be installed with the wheel.
15
+
16
+ ## Layout
17
+
18
+ ```
19
+ samples/<family_id>/<family_version>/<sample_name>/
20
+ <the sample file under its natural name>
21
+ expected.json
22
+ ```
23
+
24
+ One directory per sample. The directory names are the identity: an `expected.json` that states a
25
+ `family_id` or `family_version` its directory does not is refused rather than believed. A sample
26
+ directory with no `expected.json` is a failure that names the directory; it is never a skip.
27
+
28
+ ## expected.json
29
+
30
+ The key set is closed. Every key below is required except `warm_up`, and a key outside the set is
31
+ refused by name.
32
+
33
+ | Key | Meaning |
34
+ |---|---|
35
+ | `family_id` | The Reader that opens this file. Must equal the directory. |
36
+ | `family_version` | The exact version of that Reader. Must equal the directory. |
37
+ | `decode_options` | The settings the Reader runs with, exactly as a recipe would pin them. |
38
+ | `sample_file` | The sample's filename. A bare name in this directory; never a path. |
39
+ | `normalized_sha256` | The sha256 of the Reader's output bytes — the sealed form, not the input. |
40
+ | `row_count` | How many rows the Reader produces. |
41
+ | `column_names` | The column names it produces, in order. |
42
+ | `flags` | The facts the decode reports about itself, from the closed decode vocabulary. |
43
+ | `provenance` | Where this sample came from and what it proves. See below. |
44
+ | `warm_up` | Optional, default false. Marks the one sample a refresh re-opens. |
45
+
46
+ Exactly one sample per Reader version sets `warm_up`: it is the file a refresh re-opens before it
47
+ fetches anything. A Reader that designates none cannot be warmed up, and the warm-up check refuses
48
+ rather than passing — an unwarmed Reader is not a warmed one. A Reader that designates two is
49
+ refused when the tree loads, because "one known file" is a property of this tree and not of
50
+ whoever calls the check.
51
+
52
+ ### provenance
53
+
54
+ Four facts, no more and no fewer:
55
+
56
+ | Key | Meaning |
57
+ |---|---|
58
+ | `origin` | Where the bytes came from: a URL, or `authored for this test`. |
59
+ | `retrieved` | The date they were obtained, as `YYYY-MM-DD`. |
60
+ | `licence` | The terms the bytes may be redistributed under. |
61
+ | `why` | One sentence on what this sample proves. |
62
+
63
+ A sample with no provenance cannot pass Certification, because there is nothing to review. The
64
+ loader refuses one, so a sample cannot enter this tree without a claim a person can check.
65
+
66
+ ## Size discipline
67
+
68
+ Samples are committed bytes that ship in the wheel and are decoded before every refresh. Keep each
69
+ one under 256 KiB. A larger sample is admissible only when the Reader's own plan records why a
70
+ smaller one could not prove the same thing.
71
+
72
+ That "only when" is checkable rather than a habit. `tests/test_reader_samples.py` holds
73
+ `_OVERSIZE_BY_REVIEW`, one entry per oversize sample, each a path and the written reason. A sample
74
+ over the ceiling with no entry fails; an entry naming a file that is absent, or one that now fits,
75
+ fails too. So the ceiling still applies to every sample that is not on the list, and putting one on
76
+ it is a reviewed source edit with the reason attached — not a raised ceiling.
77
+
78
+ ## Known-good samples are not goldens
79
+
80
+ This is a rule with a reason, and the reason is the part that matters.
81
+
82
+ Golden digests in this repository are minted on one authority: the x86_64 Linux `verify` job. Every
83
+ golden-digest assertion therefore sits behind a check on the running machine's architecture, and
84
+ skips when it does not match. That is correct for goldens and wrong for samples: a sample assertion
85
+ built the same way would skip on every arm64 development host, leaving the gate live on CI alone
86
+ while still reporting green on the machine a person is actually working on. (Until ADR 0021
87
+ ADR 0021 there was a third case, the `macos-14` parity leg, which made it one live leg out of
88
+ three; that leg retired with the local lane and the argument did not need it.)
89
+
90
+ So, concretely:
91
+
92
+ - Samples live here, never under `tests/fixtures/goldens/`.
93
+ - Samples are never produced by `scripts/mint_goldens.py`, which has no discover-and-overwrite mode
94
+ by design and knows nothing about this tree.
95
+ - An `expected.json` carrying an `environment` key is refused by name. An identity stamp is how a
96
+ golden records the machine that produced it, and a sample that recorded one would be asserting a
97
+ fact about a host instead of a fact about a Reader.
98
+ - Sample assertions carry no architecture guard and no skip. They run, and can fail, everywhere.
99
+
100
+ A sample's answer is a property of the Reader and the bytes. If it depends on the machine, the
101
+ Reader is not deterministic and that is the finding, not an excuse to gate the assertion.
102
+
103
+ ## Adding a sample
104
+
105
+ 1. Put the file and its `expected.json` in a new directory under the Reader's version.
106
+ 2. Record the answer by decoding through the Reader itself — never by copying a number from a failing
107
+ test. A recorded answer that came from an observed failure records the bug.
108
+ 3. State the provenance, including what the sample proves that the existing ones do not.
109
+ 4. If it is the sample a refresh should warm up on, set `warm_up` to true and clear the flag from the
110
+ previous one.
@@ -0,0 +1,24 @@
1
+ {
2
+ "column_names": [
3
+ "city",
4
+ "population",
5
+ "note"
6
+ ],
7
+ "decode_options": {
8
+ "header": true,
9
+ "member_format": "csv"
10
+ },
11
+ "family_id": "archive.gzip",
12
+ "family_version": "1.0.0",
13
+ "flags": [],
14
+ "normalized_sha256": "09ffd82e7942d837974824bd381d76ad9fee3f916f51b9217e5df0cf034e64dd",
15
+ "provenance": {
16
+ "licence": "project-owned test fixture, redistributable with the package",
17
+ "origin": "authored for this test",
18
+ "retrieved": "2026-08-07",
19
+ "why": "proves the degenerate form of clause 2: one gzip stream with no member name, whose inner form the recipe still states, carrying a missing cell so the empty cell survives the round trip through the canonical emitter"
20
+ },
21
+ "row_count": 2,
22
+ "sample_file": "cities.csv.gz",
23
+ "warm_up": true
24
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "column_names": [
3
+ "city",
4
+ "population",
5
+ "note"
6
+ ],
7
+ "decode_options": {
8
+ "header": true,
9
+ "member_format": "csv"
10
+ },
11
+ "family_id": "archive.gzip",
12
+ "family_version": "1.1.0",
13
+ "flags": [],
14
+ "normalized_sha256": "09ffd82e7942d837974824bd381d76ad9fee3f916f51b9217e5df0cf034e64dd",
15
+ "provenance": {
16
+ "licence": "project-owned test fixture, redistributable with the package",
17
+ "origin": "authored for this test",
18
+ "retrieved": "2026-08-07",
19
+ "why": "proves the degenerate form of clause 2 \u2014 one gzip stream with no member name, whose inner form the recipe still states, carrying a missing cell so the empty cell survives the round trip through the canonical emitter \u2014 carried byte-for-byte from the archive.gzip@1.0.0 warm-up sample because this coordinate widens only the label a response may arrive under and inherits the decode unchanged."
20
+ },
21
+ "row_count": 2,
22
+ "sample_file": "cities.csv.gz",
23
+ "warm_up": true
24
+ }