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,1124 @@
1
+ """Reading the builds you already have: one build in full, and every build under a folder.
2
+
3
+ Two readings live here, and the difference between them is the whole point of the module.
4
+
5
+ The full reading goes through :func:`pipeline.inspect_candidate` -- the descriptor-relative,
6
+ ancestor-race-checked, lease-validated reader that replays the sealed tree before it reports a
7
+ single fact. Nothing on that path opens a receipt with a plain read, because a plain read looks
8
+ identical and silently drops every guarantee this product sells.
9
+
10
+ The listing is deliberately cheap. It reads the receipt header off each folder without replaying
11
+ anything, so a folder full of builds lists in the time one verification would take. That is a real
12
+ trade and it is stated out loud rather than hidden: every listed row, in both output modes, ends
13
+ with ``listed, not verified`` until someone asks for the re-check.
14
+
15
+ Nothing here prints. Every function returns facts or text, so the same reading can be rendered
16
+ into a browser page later without re-deriving it.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import errno
22
+ import os
23
+ import shutil
24
+ import stat
25
+ import tempfile
26
+ from collections.abc import Mapping, Sequence
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ try:
32
+ import fcntl
33
+ except ImportError: # pragma: no cover - descriptor workspaces are POSIX-only
34
+ fcntl = None # type: ignore[assignment]
35
+
36
+ from mostlyright.data_harness.canonical import CanonicalJSONError, parse_json
37
+ from mostlyright.data_harness.pipeline import (
38
+ MAX_CANDIDATE_BYTES,
39
+ MAX_CANDIDATE_MEMBER_BYTES,
40
+ RECIPE_MANIFEST_VERSION,
41
+ BuildError,
42
+ inspect_candidate,
43
+ )
44
+ from mostlyright.data_harness.recipe import version_bindings, version_summary
45
+ from mostlyright.data_harness.ux.headline import naming, plain_headline
46
+ from mostlyright.data_harness.ux.path_kind import (
47
+ A_FILE,
48
+ A_FOLDER,
49
+ UNKNOWN_KIND,
50
+ entry_at,
51
+ kind_at,
52
+ presence_at,
53
+ )
54
+
55
+ # Where builds land when nobody says otherwise, and how far down to look for them. Six levels
56
+ # covers `.work/<anything>/<a few nested folders>/<build>` without ever walking a whole home
57
+ # directory by accident.
58
+ DEFAULT_LIST_ROOT = Path(".work")
59
+ DEFAULT_LIST_DEPTH = 6
60
+ MAX_LIST_DEPTH = 32
61
+
62
+ # What a listing says about itself. These are the exact phrases the honesty test pins, and they
63
+ # are the same strings in the plain lines and in the machine-readable object.
64
+ LISTED_ONLY = "listed, not verified"
65
+ VERIFIED = "verified"
66
+ UNREADABLE = "could not be read"
67
+ # What the listing as a whole says about itself once `--verify` has run. The listing-level word is
68
+ # derived from the entries and never from the flag: asking for a re-check is not the same fact as
69
+ # every build passing one, and a listing that reports the mode it was asked for under the same key
70
+ # an entry uses for its outcome tells a machine that a tampered tree verified.
71
+ NOT_ALL_VERIFIED = "not all verified"
72
+ NOTHING_TO_VERIFY = "nothing to verify"
73
+ # A folder the walk stopped at that holds no Build at all -- a Workbench folder made but not run
74
+ # yet, most often. It is not a build that could not be read, and saying so is the difference
75
+ # between "you have not run this" and "something is wrong with this".
76
+ NOTHING_BUILT = "nothing built here yet"
77
+
78
+ # The two plain sentences an entry can carry instead of a build's facts. They are sentences rather
79
+ # than the exception's own text on purpose: an errno string names an internal path nobody typed,
80
+ # and a raise-site message is written in the words the module around it uses. The exact wording is
81
+ # never lost -- it is what `mr-data show` and `mr-data verify` print for that same folder, and the
82
+ # listing's note says so.
83
+ NOTHING_BUILT_REASON = "Nothing has been built in this folder yet."
84
+ UNREADABLE_REASON = "Its Receipt is there but could not be read."
85
+ # A Workbench folder whose durable record could not be read at all. The Build underneath may be
86
+ # perfectly good -- `mr-data show` on it often still answers -- but the listing does not know where
87
+ # it is, and saying "nothing has been built here" about a folder that holds a Receipt is the one
88
+ # answer that is affirmatively false.
89
+ UNRESOLVED_REASON = "Its Workbench record could not be read, so its Build was not opened."
90
+ # A descendant the descriptor-rooted walk could not even classify. It cannot truthfully claim a
91
+ # Receipt or Workbench record exists, but it must still make the incomplete part of the catalog a
92
+ # typed unreadable row instead of making that subtree indistinguishable from an empty one.
93
+ DESCENDANT_UNREADABLE_REASON = "This folder could not be read, so any Build in it was not opened."
94
+ # The commonest reason a Workbench record cannot be read right now, and the only one that resolves
95
+ # itself: a build is holding the folder. The listing says so and moves on rather than joining the
96
+ # queue behind a build that may run for minutes.
97
+ BUSY_REASON = "A build is running in this folder, so its record was not read."
98
+
99
+ # The sealed tree's own folder name. It is a contract identifier -- it is spelled this way inside
100
+ # every receipt this product has ever written -- so it is written once here and referred to by
101
+ # name everywhere else, rather than repeated as a bare word the vocabulary sweep has to argue for.
102
+ _SEALED_TREE = Path("candidate")
103
+ _RECEIPT = _SEALED_TREE / "manifest.json"
104
+ _PROFILE = _SEALED_TREE / "evidence" / "profile.json"
105
+ _CONTROL_DIRECTORY = ".mr-data"
106
+ # The child a Workbench folder keeps its run under. This is not a guess the listing makes: the run
107
+ # state records this path and the coordinator validates the recorded value to be exactly this, so
108
+ # it is where the run is whenever the record can be read at all. It is used as the fallback when
109
+ # the record cannot be read, so a Build under a damaged Workbench folder is still pointed at rather
110
+ # than dropped from the listing.
111
+ _RUN_CHILD = Path("result")
112
+ _STATE_FILE = "state.json"
113
+ _LOCK_FILE = ".lock"
114
+ _MAX_WORKBENCH_STATE_BYTES = 2 * 1024 * 1024
115
+
116
+
117
+ # ------------------------------------------------------------------------------------------------
118
+ # One build, in full
119
+ # ------------------------------------------------------------------------------------------------
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class BuildFacts:
124
+ """Everything a reader asks about one sealed build, from one verified reading of it."""
125
+
126
+ run_dir: Path
127
+ candidate_digest: str
128
+ table_sha256: str
129
+ manifest_version: str
130
+ validation_policy_digest: str
131
+ producer_principal: str
132
+ producer_attempt: str
133
+ question: str
134
+ feasibility: str | None
135
+ grain: tuple[str, ...]
136
+ columns: tuple[str, ...]
137
+ column_types: tuple[tuple[str, str], ...]
138
+ row_count: int
139
+ sources: tuple[dict[str, Any], ...]
140
+ join: dict[str, Any]
141
+ quality: dict[str, Any]
142
+ lineage: dict[str, Any]
143
+ lineage_digest: str
144
+ member_paths: tuple[str, ...]
145
+ recipe_id: str | None = None
146
+ recipe_version: int | None = None
147
+ recipe_digest: str | None = None
148
+ approval_id: str | None = None
149
+ approval_digest: str | None = None
150
+ execution_digest: str | None = None
151
+ execution_mode: str | None = None
152
+
153
+ def type_of(self, column: str) -> str | None:
154
+ return dict(self.column_types).get(column)
155
+
156
+ def to_dict(self, *, members: bool = False) -> dict[str, Any]:
157
+ """The one payload both renderings are built from.
158
+
159
+ Facts taken from the data -- column names, check names, source names -- go in values, never
160
+ in keys. A key is given a plain label when it is shown to a person, and a column named
161
+ ``max_temp_c`` used as a key would be shown as ``Max temp c``: a name the data does not
162
+ have.
163
+ """
164
+
165
+ payload: dict[str, Any] = {
166
+ "status": "build_shown",
167
+ "run_dir": str(self.run_dir),
168
+ "candidate_digest": self.candidate_digest,
169
+ "table_sha256": self.table_sha256,
170
+ "manifest_version": self.manifest_version,
171
+ "row_count": self.row_count,
172
+ "columns": list(self.columns),
173
+ "grain": list(self.grain),
174
+ "question": self.question,
175
+ "built_by": self.producer_principal,
176
+ "build_attempt": self.producer_attempt,
177
+ "validation_policy_digest": self.validation_policy_digest,
178
+ "lineage_digest": self.lineage_digest,
179
+ "schema": self._schema_payload(),
180
+ "quality": self._quality_payload(),
181
+ "sources": self._sources_payload(),
182
+ "join": self._join_payload(),
183
+ }
184
+ if self.feasibility is not None:
185
+ payload["feasibility"] = self.feasibility
186
+ if self.recipe_digest is not None:
187
+ payload["recipe_id"] = self.recipe_id
188
+ payload["recipe_version"] = self.recipe_version
189
+ payload["recipe_digest"] = self.recipe_digest
190
+ payload["approval_id"] = self.approval_id
191
+ payload["approval_digest"] = self.approval_digest
192
+ payload["execution_digest"] = self.execution_digest
193
+ payload["execution_mode"] = self.execution_mode
194
+ if members:
195
+ payload["member_paths"] = list(self.member_paths)
196
+ return payload
197
+
198
+ def _schema_payload(self) -> dict[str, dict[str, Any]]:
199
+ width = len(str(max(len(self.columns), 1)))
200
+ return {
201
+ f"column {index:0{width}d}": {"name": name, "type": self.type_of(name)}
202
+ for index, name in enumerate(self.columns, start=1)
203
+ }
204
+
205
+ def _quality_payload(self) -> dict[str, Any]:
206
+ checks = _checks(self.quality)
207
+ width = len(str(max(len(checks), 1)))
208
+ payload: dict[str, Any] = {"result": self.quality.get("status")}
209
+ for index, check in enumerate(checks, start=1):
210
+ payload[f"check {index:0{width}d}"] = {
211
+ "name": check.get("check_id"),
212
+ "result": "passed" if check.get("passed") else "failed",
213
+ "wanted": check.get("expected"),
214
+ "found": check.get("observed"),
215
+ }
216
+ return payload
217
+
218
+ def _sources_payload(self) -> dict[str, Any]:
219
+ width = len(str(max(len(self.sources), 1)))
220
+ return {
221
+ f"source {index:0{width}d}": {
222
+ "name": source.get("source_id"),
223
+ "file": source.get("relative_path"),
224
+ "size in bytes": source.get("bytes"),
225
+ "content fingerprint": source.get("sha256"),
226
+ "came from": source.get("origin"),
227
+ }
228
+ for index, source in enumerate(self.sources, start=1)
229
+ }
230
+
231
+ def _join_payload(self) -> dict[str, Any]:
232
+ if not self.join:
233
+ return {}
234
+ return {
235
+ "left": self.join.get("left_source"),
236
+ "right": self.join.get("right_source"),
237
+ "keys": list(self.join.get("keys", ())),
238
+ "relationship": self.join.get("cardinality"),
239
+ "rows in": self.join.get("left_rows"),
240
+ "rows out": self.join.get("output_rows"),
241
+ "rows that did not match": self.join.get("unmatched_left_rows"),
242
+ }
243
+
244
+
245
+ def build_facts(run_dir: Path | str) -> BuildFacts:
246
+ """Read one sealed build, verifying it on the way.
247
+
248
+ Everything reported here comes from :func:`inspect_candidate`, which replays the whole sealed
249
+ tree, plus the two recipe-side readers that go through the same verified snapshot. A folder
250
+ with no build in it is named as such before any of that runs, because the reader's own refusal
251
+ for a missing tree reads like a break-in rather than a typo.
252
+ """
253
+
254
+ target = Path(run_dir)
255
+ require_a_build(target)
256
+ inspection = inspect_candidate(target)
257
+ summary = version_summary(target)
258
+ bindings = (
259
+ version_bindings(target) if inspection.manifest_version == RECIPE_MANIFEST_VERSION else None
260
+ )
261
+ return BuildFacts(
262
+ run_dir=target,
263
+ candidate_digest=inspection.candidate_digest,
264
+ table_sha256=inspection.table_sha256,
265
+ manifest_version=inspection.manifest_version,
266
+ validation_policy_digest=inspection.validation_policy_digest,
267
+ producer_principal=inspection.producer_principal,
268
+ producer_attempt=inspection.producer_attempt,
269
+ question=inspection.question,
270
+ feasibility=inspection.feasibility,
271
+ grain=inspection.grain,
272
+ columns=inspection.columns,
273
+ column_types=summary.column_types,
274
+ row_count=inspection.row_count,
275
+ sources=inspection.sources,
276
+ join=inspection.join,
277
+ quality=inspection.quality,
278
+ lineage=inspection.lineage,
279
+ lineage_digest=summary.lineage_digest,
280
+ member_paths=inspection.member_paths,
281
+ recipe_id=None if bindings is None else bindings.recipe.recipe_id,
282
+ recipe_version=None if bindings is None else bindings.recipe.recipe_version,
283
+ recipe_digest=None if bindings is None else bindings.recipe.digest,
284
+ approval_id=None if bindings is None else bindings.approval.approval_id,
285
+ approval_digest=None if bindings is None else bindings.approval.digest,
286
+ execution_digest=None if bindings is None else bindings.execution.digest,
287
+ execution_mode=None if bindings is None else bindings.execution.mode,
288
+ )
289
+
290
+
291
+ def plain_card(facts: BuildFacts) -> str:
292
+ """The plain-language card for one build, written from the facts above.
293
+
294
+ It does not read, and must not read, the sealed card member of the build. Those bytes sit
295
+ inside seven recorded digests, so rewording them moves every one of those digests, and no
296
+ command shows them to a person today. Everything a card would say is already in the facts a
297
+ verified reading returns, so the card is written from those instead.
298
+ """
299
+
300
+ lines = [
301
+ f"What this answers: {facts.question}",
302
+ f"One row is one {_grain_phrase(facts.grain)}.",
303
+ f"{facts.row_count} rows, {len(facts.columns)} columns:",
304
+ ]
305
+ lines.extend(f" {name} ({facts.type_of(name) or 'unknown'})" for name in facts.columns)
306
+ if facts.sources:
307
+ lines.append("Where it came from:")
308
+ for source in facts.sources:
309
+ name = source.get("source_id", "a source")
310
+ where = source.get("relative_path", "an unnamed file")
311
+ lines.append(f" {name}: {where}")
312
+ lines.append(_checks_sentence(facts.quality))
313
+ if facts.recipe_id is not None:
314
+ lines.append(f"Made by the Recipe {facts.recipe_id}, version {facts.recipe_version}.")
315
+ lines.append(f"Build fingerprint: {facts.candidate_digest}")
316
+ lines.append(f"Data fingerprint: {facts.table_sha256}")
317
+ return "\n".join(lines)
318
+
319
+
320
+ def _grain_phrase(grain: Sequence[str]) -> str:
321
+ """What one row is, said the way a person would say it."""
322
+
323
+ names = [str(name) for name in grain]
324
+ if not names:
325
+ return "record"
326
+ if len(names) == 1:
327
+ return names[0]
328
+ return f"{', '.join(names[:-1])} and {names[-1]}"
329
+
330
+
331
+ def _checks_sentence(quality: Mapping[str, Any]) -> str:
332
+ """One honest sentence about the checks, naming the ones that did not pass."""
333
+
334
+ checks = _checks(quality)
335
+ if not checks:
336
+ return "No checks were recorded for this build."
337
+ failed = [str(check.get("check_id")) for check in checks if not check.get("passed")]
338
+ if not failed:
339
+ return f"Checks: all {len(checks)} passed."
340
+ passed = len(checks) - len(failed)
341
+ return f"Checks: {passed} of {len(checks)} passed. Did not pass: {', '.join(failed)}"
342
+
343
+
344
+ def _checks(quality: Mapping[str, Any]) -> tuple[dict[str, Any], ...]:
345
+ raw = quality.get("checks") if isinstance(quality, Mapping) else None
346
+ if not isinstance(raw, list):
347
+ return ()
348
+ return tuple(item for item in raw if isinstance(item, dict))
349
+
350
+
351
+ def require_a_build(run_dir: Path) -> None:
352
+ """Refuse a folder with no build in it by name, before the verified reader is asked.
353
+
354
+ This is a signpost, not a check that anything is trusted: the verified reading still runs and
355
+ still decides. Its only job is that pointing a command at the wrong folder reads like the typo
356
+ it usually is.
357
+
358
+ Public because the comparison in :mod:`mostlyright.data_harness.ux.diffing` needs the same
359
+ signpost, and two copies of one sentence are two sentences that can drift apart.
360
+ """
361
+
362
+ if not (run_dir / _RECEIPT).is_file():
363
+ raise BuildError("CANDIDATE_ABSENT", f"there is no build at {run_dir}")
364
+
365
+
366
+ def holds_a_build(directory: Path) -> bool:
367
+ """Whether this folder is, or leads to, a Build -- the question the plain sentences need.
368
+
369
+ A Workbench folder keeps its run underneath, so it counts when that run holds a Receipt; any
370
+ other folder counts when it holds one itself. Public because the command line's error boundary
371
+ has to tell "you have not built here yet" apart from "this build cannot be opened safely", and
372
+ those two answers name completely different fixes.
373
+
374
+ The answer is the Receipt, never the readability of the durable record. A Workbench folder
375
+ whose record cannot be read is one of the two cases the boundary is here to tell apart, and
376
+ answering ``False`` for it would hand the class rule -- no answer about a Build where there is
377
+ no Build -- a false premise about a folder that holds one.
378
+
379
+ The question is asked of the entry, not of what the name leads to. A resolving
380
+ ``(stop.run_dir / manifest.json).is_file()`` check would treat an ordinary
381
+ ``~/builds/latest -> real-build`` link as "a Build is there" even though the harness refuses
382
+ to open that path: ``mr-data build --output ~/builds/latest`` says the path
383
+ "already holds something ... Run: mr-data show
384
+ ~/builds/latest", and that command exits 1 with ``CANDIDATE_ANCESTOR_SYMLINK``. So the question
385
+ is now the one every reader here actually asks: is the entry a folder, and is the Receipt in it
386
+ a plain file. A link is not a Build this harness will open, and saying it is was the whole
387
+ defect.
388
+ """
389
+
390
+ if kind_at(directory) != A_FOLDER:
391
+ return False
392
+ if _receipt_entry(directory):
393
+ return True
394
+ return kind_at(directory / _CONTROL_DIRECTORY) == A_FOLDER and _receipt_entry(
395
+ directory / _RUN_CHILD
396
+ )
397
+
398
+
399
+ def _receipt_entry(run_dir: Path) -> bool:
400
+ """Whether the Receipt in this folder is a plain file, asked of the entry and never followed.
401
+
402
+ A resolving ``(run_dir / _RECEIPT).is_file()`` check is wrong in two ways. It **follows the
403
+ name**, so a link where the Receipt should be answers "a Build is here" about a file no reader
404
+ in this package will open. And ``Path.is_file`` *raises* when the account may not look -- which
405
+ is load-bearing: a listing that answered ``False`` there would
406
+ report a folder it cannot read as a folder with nothing built in it, which is a second silent
407
+ answer of exactly the kind this module exists to end.
408
+
409
+ So this is one ``lstat``, it follows nothing, and the two refusals that mean "no Receipt is
410
+ there" are the only ones it turns into an answer. Every other refusal is raised, the way it
411
+ always was, and the callers that have somewhere to put it -- ``cli._subject_holds_a_build``,
412
+ ``preflight._holds_a_build`` -- still turn it into "I cannot tell". That is
413
+ :func:`ux.path_kind.entry_at`, which exists because this is the question, asked once, in the
414
+ module that owns it.
415
+ """
416
+
417
+ return entry_at(run_dir / _RECEIPT) == A_FILE
418
+
419
+
420
+ # ------------------------------------------------------------------------------------------------
421
+ # Every build under a folder
422
+ # ------------------------------------------------------------------------------------------------
423
+
424
+
425
+ @dataclass(frozen=True)
426
+ class BuildEntry:
427
+ """One folder a listing stopped at, and how much was actually read to describe it.
428
+
429
+ ``reason`` is one plain sentence, present exactly when the facts above it are not. It is the
430
+ same string in both output modes, because a listing has one set of facts and renders them
431
+ twice; the listing does not carry a second, engineering wording for a machine to read.
432
+ """
433
+
434
+ run_dir: Path
435
+ verified: bool
436
+ manifest_version: str | None = None
437
+ candidate_digest: str | None = None
438
+ table_sha256: str | None = None
439
+ row_count: int | None = None
440
+ reason: str | None = None
441
+ nothing_built: bool = False
442
+
443
+ @property
444
+ def verification(self) -> str:
445
+ if self.nothing_built:
446
+ return NOTHING_BUILT
447
+ if self.reason is not None:
448
+ return UNREADABLE
449
+ return VERIFIED if self.verified else LISTED_ONLY
450
+
451
+ def to_dict(self) -> dict[str, Any]:
452
+ payload: dict[str, Any] = {
453
+ "run_dir": str(self.run_dir),
454
+ "manifest_version": self.manifest_version,
455
+ "candidate_digest": self.candidate_digest,
456
+ "table_sha256": self.table_sha256,
457
+ "row_count": self.row_count,
458
+ "verification": self.verification,
459
+ }
460
+ if self.reason is not None:
461
+ payload["why"] = self.reason
462
+ return payload
463
+
464
+
465
+ def listing_verification(entries: Sequence[BuildEntry], *, verify: bool) -> str:
466
+ """What the listing as a whole may claim, taken from the entries rather than from the flag.
467
+
468
+ ``--verify`` is a request. Whether every build under the root answered it is a fact, and the
469
+ two are not the same sentence. The listing-level word shares its key and its vocabulary with
470
+ the per-entry one, so a reader that branches on either gets the same answer about the same
471
+ tree: ``verified`` only when every entry verified, and a word that says otherwise when one did
472
+ not. A root with nothing under it verified nothing, and says that rather than claiming a clean
473
+ result over an empty listing.
474
+ """
475
+
476
+ if not verify:
477
+ return LISTED_ONLY
478
+ if not entries:
479
+ return NOTHING_TO_VERIFY
480
+ if all(entry.verification == VERIFIED for entry in entries):
481
+ return VERIFIED
482
+ return NOT_ALL_VERIFIED
483
+
484
+
485
+ def list_builds(
486
+ root: Path | str,
487
+ *,
488
+ verify: bool = False,
489
+ max_depth: int = DEFAULT_LIST_DEPTH,
490
+ ) -> tuple[BuildEntry, ...]:
491
+ """Every build under ``root``, in folder order.
492
+
493
+ Without ``verify`` this reads each receipt and nothing else, and every entry says so. With
494
+ ``verify`` each build is replayed through the same reader ``show`` uses, and only then does an
495
+ entry say it was verified.
496
+
497
+ A folder that cannot be read comes back as an entry carrying the reason. A build you cannot
498
+ read is exactly the thing you want a listing to put in front of you, so it is never dropped.
499
+ """
500
+
501
+ start = Path(root)
502
+ try:
503
+ root_before = _open_listing_root(start)
504
+ except OSError as error:
505
+ found_at_root = presence_at(start)
506
+ if found_at_root == A_FOLDER:
507
+ # The entry is the required kind, but this account could not open it for traversal.
508
+ # Preserve that operating-system refusal so the command's shared remediation says the
509
+ # access was refused; calling an unreadable folder absent would erase the distinction.
510
+ raise
511
+ # `is_dir` follows links and cannot distinguish a dangling link or plain file from an absent
512
+ # listing root. Use the noun from `ux.path_kind`'s non-following lookup instead.
513
+ raise BuildError(
514
+ "LIST_ROOT_ABSENT",
515
+ f"there is {found_at_root or UNKNOWN_KIND} at {start}, and a listing reads a folder",
516
+ ) from error
517
+ try:
518
+ depth = _bounded_depth(max_depth)
519
+ found = sorted(
520
+ _build_directories(root_before.descriptor, start, depth),
521
+ key=lambda stop: stop.run_dir,
522
+ )
523
+ entries = tuple(
524
+ _entry(stop, root_descriptor=root_before.descriptor, verify=verify) for stop in found
525
+ )
526
+ try:
527
+ root_after = _open_listing_root(start)
528
+ except OSError:
529
+ root_after = None
530
+ try:
531
+ if root_after is None or root_after.identity != root_before.identity:
532
+ # No row collected through a different namespace is allowed to leave this
533
+ # function. The first observation used to be followed by an unbound pathname walk,
534
+ # so replacing the checked folder with a link in that gap made outside Receipts
535
+ # look like children of the original spelling. The no-follow directory opens bind
536
+ # both observations to concrete inodes; a changed name becomes a typed refusal
537
+ # before either renderer gets the tuple above.
538
+ raise BuildError(
539
+ "LIST_ROOT_ABSENT",
540
+ f"the listing root changed while it was being read: {start}",
541
+ )
542
+ finally:
543
+ if root_after is not None:
544
+ os.close(root_after.descriptor)
545
+ return entries
546
+ finally:
547
+ os.close(root_before.descriptor)
548
+
549
+
550
+ @dataclass(frozen=True)
551
+ class _ListingRootHandle:
552
+ """An opened directory and the filesystem identity bound by that same operation."""
553
+
554
+ descriptor: int
555
+ identity: tuple[int, int]
556
+
557
+
558
+ def _open_listing_root(root: Path) -> _ListingRootHandle:
559
+ """Open the named root as the directory itself, never a link target."""
560
+
561
+ descriptor = os.open(
562
+ root,
563
+ os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0),
564
+ )
565
+ try:
566
+ observed = os.fstat(descriptor)
567
+ except BaseException:
568
+ os.close(descriptor)
569
+ raise
570
+ return _ListingRootHandle(descriptor, (observed.st_dev, observed.st_ino))
571
+
572
+
573
+ def _bounded_depth(max_depth: int) -> int:
574
+ if type(max_depth) is not int or not 0 <= max_depth <= MAX_LIST_DEPTH:
575
+ raise BuildError(
576
+ "LIST_DEPTH",
577
+ f"how deep to look must be a whole number from 0 to {MAX_LIST_DEPTH}",
578
+ )
579
+ return max_depth
580
+
581
+
582
+ @dataclass(frozen=True)
583
+ class _Stop:
584
+ """One folder the walk stopped at, before anything in it was read.
585
+
586
+ ``run_dir`` is where the Build would be. ``reason`` is set only when resolving the folder was
587
+ itself refused -- the record could not be read, or a build is holding it -- and it is carried
588
+ into the entry rather than thrown away, because a folder whose record cannot be read is not a
589
+ folder nothing has been built in. ``nothing_built`` is the one narrow case where that second
590
+ sentence is true: there is no run recorded here at all.
591
+ """
592
+
593
+ run_dir: Path
594
+ relative: tuple[str, ...]
595
+ reason: str | None = None
596
+ nothing_built: bool = False
597
+
598
+
599
+ def _open_directory_beneath(root_descriptor: int, relative: tuple[str, ...]) -> int:
600
+ """Open one descendant from the retained root, refusing every linked component."""
601
+
602
+ descriptor = os.dup(root_descriptor)
603
+ try:
604
+ for name in relative:
605
+ child = os.open(
606
+ name,
607
+ os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0),
608
+ dir_fd=descriptor,
609
+ )
610
+ os.close(descriptor)
611
+ descriptor = child
612
+ return descriptor
613
+ except BaseException:
614
+ os.close(descriptor)
615
+ raise
616
+
617
+
618
+ def _read_regular_at(
619
+ directory_descriptor: int,
620
+ relative: tuple[str, ...],
621
+ *,
622
+ maximum: int | None,
623
+ required_mode: int | None = None,
624
+ ) -> bytes:
625
+ """Read one bounded regular descendant through directory descriptors only."""
626
+
627
+ if not relative:
628
+ raise FileNotFoundError("a file name is required")
629
+ parent = _open_directory_beneath(directory_descriptor, relative[:-1])
630
+ descriptor: int | None = None
631
+ try:
632
+ descriptor = os.open(
633
+ relative[-1],
634
+ os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0),
635
+ dir_fd=parent,
636
+ )
637
+ observed = os.fstat(descriptor)
638
+ if not stat.S_ISREG(observed.st_mode):
639
+ raise OSError(errno.EINVAL, "listed member is not a regular file")
640
+ if required_mode is not None and stat.S_IMODE(observed.st_mode) != required_mode:
641
+ raise OSError(errno.EPERM, "listed member mode changed")
642
+ if maximum is not None and observed.st_size > maximum:
643
+ raise OSError(errno.EFBIG, "listed member exceeds its read bound")
644
+ chunks: list[bytes] = []
645
+ remaining = maximum + 1 if maximum is not None else None
646
+ while remaining is None or remaining:
647
+ chunk = os.read(
648
+ descriptor, min(remaining, 1 << 16) if remaining is not None else 1 << 16
649
+ )
650
+ if not chunk:
651
+ break
652
+ chunks.append(chunk)
653
+ if remaining is not None:
654
+ remaining -= len(chunk)
655
+ raw = b"".join(chunks)
656
+ if maximum is not None and len(raw) > maximum:
657
+ raise OSError(errno.EFBIG, "listed member grew beyond its read bound")
658
+ return raw
659
+ finally:
660
+ if descriptor is not None:
661
+ os.close(descriptor)
662
+ os.close(parent)
663
+
664
+
665
+ def _read_unbounded_regular_at(
666
+ directory_descriptor: int,
667
+ relative: tuple[str, ...],
668
+ ) -> bytes:
669
+ """Read a rooted regular listing member without a byte limit."""
670
+
671
+ return _read_regular_at(directory_descriptor, relative, maximum=None)
672
+
673
+
674
+ def _plain_file_at(directory_descriptor: int, relative: tuple[str, ...]) -> bool:
675
+ """Whether one descendant entry is a regular file, without following any component."""
676
+
677
+ if not relative:
678
+ return False
679
+ try:
680
+ parent = _open_directory_beneath(directory_descriptor, relative[:-1])
681
+ except (FileNotFoundError, NotADirectoryError):
682
+ return False
683
+ try:
684
+ observed = os.stat(relative[-1], dir_fd=parent, follow_symlinks=False)
685
+ except (FileNotFoundError, NotADirectoryError):
686
+ return False
687
+ finally:
688
+ os.close(parent)
689
+ return stat.S_ISREG(observed.st_mode)
690
+
691
+
692
+ def _folder_at(directory_descriptor: int, name: str) -> bool:
693
+ try:
694
+ observed = os.stat(name, dir_fd=directory_descriptor, follow_symlinks=False)
695
+ except (FileNotFoundError, NotADirectoryError):
696
+ return False
697
+ return stat.S_ISDIR(observed.st_mode)
698
+
699
+
700
+ def _build_directories(root_descriptor: int, root: Path, max_depth: int) -> list[_Stop]:
701
+ """Walk below the retained root descriptor, never reopening the root pathname."""
702
+
703
+ found: list[_Stop] = []
704
+ pending: list[tuple[tuple[str, ...], int]] = [((), 0)]
705
+ while pending:
706
+ relative, depth = pending.pop()
707
+ try:
708
+ directory = _open_directory_beneath(root_descriptor, relative)
709
+ except OSError:
710
+ if not relative:
711
+ raise
712
+ found.append(
713
+ _Stop(
714
+ root.joinpath(*relative),
715
+ relative,
716
+ reason=DESCENDANT_UNREADABLE_REASON,
717
+ )
718
+ )
719
+ continue
720
+ try:
721
+ try:
722
+ stop = _build_directory_at(directory, root, relative)
723
+ except OSError:
724
+ found.append(
725
+ _Stop(
726
+ root.joinpath(*relative),
727
+ relative,
728
+ reason=DESCENDANT_UNREADABLE_REASON,
729
+ )
730
+ )
731
+ continue
732
+ if stop is not None:
733
+ found.append(stop)
734
+ continue
735
+ if depth >= max_depth:
736
+ continue
737
+ try:
738
+ children, unreadable, incomplete = _child_directories(directory)
739
+ except OSError:
740
+ found.append(
741
+ _Stop(
742
+ root.joinpath(*relative),
743
+ relative,
744
+ reason=DESCENDANT_UNREADABLE_REASON,
745
+ )
746
+ )
747
+ continue
748
+ if incomplete:
749
+ found.append(
750
+ _Stop(
751
+ root.joinpath(*relative),
752
+ relative,
753
+ reason=DESCENDANT_UNREADABLE_REASON,
754
+ )
755
+ )
756
+ found.extend(
757
+ _Stop(
758
+ root.joinpath(*relative, name),
759
+ (*relative, name),
760
+ reason=DESCENDANT_UNREADABLE_REASON,
761
+ )
762
+ for name in unreadable
763
+ )
764
+ pending.extend(((*relative, name), depth + 1) for name in children)
765
+ finally:
766
+ os.close(directory)
767
+ return found
768
+
769
+
770
+ def _build_directory_at(
771
+ directory_descriptor: int,
772
+ root: Path,
773
+ relative: tuple[str, ...],
774
+ ) -> _Stop | None:
775
+ """The run represented by an already opened descendant, if it holds one."""
776
+
777
+ display = root.joinpath(*relative)
778
+ if _folder_at(directory_descriptor, _CONTROL_DIRECTORY):
779
+ return _workbench_run_directory(directory_descriptor, display, relative)
780
+ if _plain_file_at(directory_descriptor, _RECEIPT.parts):
781
+ return _Stop(display, relative)
782
+ return None
783
+
784
+
785
+ def _workbench_run_directory(
786
+ directory_descriptor: int,
787
+ display: Path,
788
+ relative: tuple[str, ...],
789
+ ) -> _Stop:
790
+ """Read a Workbench's fixed result child through its retained directory descriptor."""
791
+
792
+ child_relative = (*relative, _RUN_CHILD.name)
793
+ child_display = display / _RUN_CHILD
794
+ control: int | None = None
795
+ lock: int | None = None
796
+ try:
797
+ control = os.open(
798
+ _CONTROL_DIRECTORY,
799
+ os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0),
800
+ dir_fd=directory_descriptor,
801
+ )
802
+ if fcntl is None:
803
+ raise OSError(errno.ENOTSUP, "workbench locks are unavailable")
804
+ try:
805
+ lock = os.open(
806
+ _LOCK_FILE,
807
+ os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0),
808
+ dir_fd=control,
809
+ )
810
+ if not stat.S_ISREG(os.fstat(lock).st_mode):
811
+ raise OSError(errno.EINVAL, "workbench lock is not a regular file")
812
+ except FileNotFoundError:
813
+ return _Stop(display, relative, nothing_built=True)
814
+ try:
815
+ fcntl.flock(lock, fcntl.LOCK_SH | fcntl.LOCK_NB)
816
+ except OSError as error:
817
+ if error.errno in (errno.EWOULDBLOCK, errno.EAGAIN, errno.EACCES):
818
+ return _Stop(child_display, child_relative, reason=BUSY_REASON)
819
+ raise
820
+ try:
821
+ raw = _read_regular_at(
822
+ control,
823
+ (_STATE_FILE,),
824
+ maximum=_MAX_WORKBENCH_STATE_BYTES,
825
+ required_mode=0o600,
826
+ )
827
+ except FileNotFoundError:
828
+ return _Stop(display, relative, nothing_built=True)
829
+ state = parse_json(raw)
830
+ if not isinstance(state, dict) or state.get("paths") != {
831
+ "input_root": "inputs",
832
+ "result": _RUN_CHILD.name,
833
+ }:
834
+ raise ValueError("workbench state does not name the fixed result child")
835
+ return _Stop(child_display, child_relative)
836
+ except (OSError, CanonicalJSONError, ValueError):
837
+ if _plain_file_at(directory_descriptor, _RUN_CHILD.parts + _RECEIPT.parts):
838
+ return _Stop(child_display, child_relative, reason=UNRESOLVED_REASON)
839
+ return _Stop(display, relative, reason=UNRESOLVED_REASON)
840
+ finally:
841
+ if lock is not None:
842
+ os.close(lock)
843
+ if control is not None:
844
+ os.close(control)
845
+
846
+
847
+ def _child_directories(directory_descriptor: int) -> tuple[list[str], list[str], bool]:
848
+ children: list[str] = []
849
+ unreadable: list[str] = []
850
+ incomplete = False
851
+ with os.scandir(directory_descriptor) as entries:
852
+ iterator = iter(entries)
853
+ while True:
854
+ try:
855
+ entry = next(iterator)
856
+ except StopIteration:
857
+ break
858
+ except OSError:
859
+ incomplete = True
860
+ break
861
+ try:
862
+ if entry.is_dir(follow_symlinks=False):
863
+ children.append(entry.name)
864
+ except OSError:
865
+ unreadable.append(entry.name)
866
+ return sorted(children), sorted(unreadable), incomplete
867
+
868
+
869
+ def _entry(stop: _Stop, *, root_descriptor: int, verify: bool) -> BuildEntry:
870
+ """One row, from a folder the walk stopped at.
871
+
872
+ A stop that already carries a reason is answered with that reason in both readings. Asking for
873
+ a verified reading of a folder whose record could not be read cannot make it readable, and
874
+ replaying it anyway would report the folder under whichever wording the replay happened to
875
+ fail with rather than the one true thing known about it.
876
+ """
877
+
878
+ if stop.reason is not None:
879
+ return BuildEntry(run_dir=stop.run_dir, verified=False, reason=stop.reason)
880
+ if stop.nothing_built:
881
+ return BuildEntry(
882
+ run_dir=stop.run_dir,
883
+ verified=False,
884
+ reason=NOTHING_BUILT_REASON,
885
+ nothing_built=True,
886
+ )
887
+ if verify:
888
+ return _verified_entry(stop, root_descriptor)
889
+ return _listed_entry(stop, root_descriptor)
890
+
891
+
892
+ def _copy_directory_snapshot(
893
+ source_descriptor: int,
894
+ destination: Path,
895
+ remaining: list[int],
896
+ ) -> None:
897
+ """Copy one descriptor-bound tree into a private path for the existing full verifier."""
898
+
899
+ with os.scandir(source_descriptor) as entries:
900
+ for entry in entries:
901
+ observed = entry.stat(follow_symlinks=False)
902
+ target = destination / entry.name
903
+ if stat.S_ISDIR(observed.st_mode):
904
+ child = os.open(
905
+ entry.name,
906
+ os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0),
907
+ dir_fd=source_descriptor,
908
+ )
909
+ try:
910
+ target.mkdir(mode=0o700)
911
+ _copy_directory_snapshot(child, target, remaining)
912
+ target.chmod(stat.S_IMODE(observed.st_mode))
913
+ finally:
914
+ os.close(child)
915
+ continue
916
+ if stat.S_ISREG(observed.st_mode):
917
+ source = os.open(
918
+ entry.name,
919
+ os.O_RDONLY | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0),
920
+ dir_fd=source_descriptor,
921
+ )
922
+ destination_fd: int | None = None
923
+ try:
924
+ opened = os.fstat(source)
925
+ if not stat.S_ISREG(opened.st_mode) or (
926
+ opened.st_dev,
927
+ opened.st_ino,
928
+ ) != (observed.st_dev, observed.st_ino):
929
+ raise OSError(errno.ESTALE, "listed member changed during snapshot")
930
+ if opened.st_size > MAX_CANDIDATE_MEMBER_BYTES or opened.st_size > remaining[0]:
931
+ raise OSError(errno.EFBIG, "listed Build exceeds its verification bound")
932
+ destination_fd = os.open(
933
+ target,
934
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL,
935
+ 0o600,
936
+ )
937
+ member_bytes = 0
938
+ while chunk := os.read(source, 1 << 20):
939
+ member_bytes += len(chunk)
940
+ remaining[0] -= len(chunk)
941
+ if member_bytes > MAX_CANDIDATE_MEMBER_BYTES or remaining[0] < 0:
942
+ raise OSError(
943
+ errno.EFBIG,
944
+ "listed Build grew beyond its verification bound",
945
+ )
946
+ view = memoryview(chunk)
947
+ while view:
948
+ written = os.write(destination_fd, view)
949
+ view = view[written:]
950
+ target.chmod(stat.S_IMODE(opened.st_mode))
951
+ finally:
952
+ if destination_fd is not None:
953
+ os.close(destination_fd)
954
+ os.close(source)
955
+ continue
956
+ if stat.S_ISLNK(observed.st_mode):
957
+ target.symlink_to(os.readlink(entry.name, dir_fd=source_descriptor))
958
+ continue
959
+ raise OSError(errno.EINVAL, "listed tree contains an unsupported entry kind")
960
+
961
+
962
+ def _snapshot_for_verification(root_descriptor: int, relative: tuple[str, ...]) -> Path:
963
+ """Materialise only bytes reachable below the retained listing root descriptor."""
964
+
965
+ temporary = Path(os.path.realpath(tempfile.mkdtemp(prefix="mr-data-list-verify-")))
966
+ run = temporary / "run"
967
+ run.mkdir()
968
+ source: int | None = None
969
+ candidate: int | None = None
970
+ try:
971
+ source = _open_directory_beneath(root_descriptor, relative)
972
+ candidate = os.open(
973
+ _SEALED_TREE.name,
974
+ os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0),
975
+ dir_fd=source,
976
+ )
977
+ candidate_info = os.fstat(candidate)
978
+ candidate_copy = run / _SEALED_TREE
979
+ candidate_copy.mkdir(mode=0o700)
980
+ _copy_directory_snapshot(candidate, candidate_copy, [MAX_CANDIDATE_BYTES])
981
+ candidate_copy.chmod(stat.S_IMODE(candidate_info.st_mode))
982
+ return run
983
+ except BaseException:
984
+ shutil.rmtree(temporary, ignore_errors=True)
985
+ raise
986
+ finally:
987
+ if candidate is not None:
988
+ os.close(candidate)
989
+ if source is not None:
990
+ os.close(source)
991
+
992
+
993
+ def _discard_snapshot(snapshot: Path) -> None:
994
+ """Make the private sealed copy removable, then remove it."""
995
+
996
+ for directory, _children, files in os.walk(snapshot.parent):
997
+ folder = Path(directory)
998
+ try:
999
+ folder.chmod(0o700)
1000
+ except OSError:
1001
+ pass
1002
+ for name in files:
1003
+ try:
1004
+ (folder / name).chmod(0o600, follow_symlinks=False)
1005
+ except OSError:
1006
+ pass
1007
+ shutil.rmtree(snapshot.parent, ignore_errors=True)
1008
+
1009
+
1010
+ def _verified_entry(stop: _Stop, root_descriptor: int) -> BuildEntry:
1011
+ snapshot: Path | None = None
1012
+ try:
1013
+ snapshot = _snapshot_for_verification(root_descriptor, stop.relative)
1014
+ facts = build_facts(snapshot)
1015
+ except BuildError as error:
1016
+ if error.finding_id == "CANDIDATE_ABSENT":
1017
+ return BuildEntry(
1018
+ run_dir=stop.run_dir,
1019
+ verified=False,
1020
+ reason=NOTHING_BUILT_REASON,
1021
+ nothing_built=True,
1022
+ )
1023
+ # A typed refusal from the replay. Its raise-site wording is translated through the same
1024
+ # boundary the command line uses, so a listing cannot put an internal word on a line a
1025
+ # person reads; a message that is already plain comes through untouched.
1026
+ plain = plain_headline(error.finding_id, str(error), raised_as=naming(error))
1027
+ return BuildEntry(run_dir=stop.run_dir, verified=False, reason=plain or f"{error}.")
1028
+ except Exception:
1029
+ return BuildEntry(run_dir=stop.run_dir, verified=False, reason=UNREADABLE_REASON)
1030
+ finally:
1031
+ if snapshot is not None:
1032
+ _discard_snapshot(snapshot)
1033
+ return BuildEntry(
1034
+ run_dir=stop.run_dir,
1035
+ verified=True,
1036
+ manifest_version=facts.manifest_version,
1037
+ candidate_digest=facts.candidate_digest,
1038
+ table_sha256=facts.table_sha256,
1039
+ row_count=facts.row_count,
1040
+ )
1041
+
1042
+
1043
+ def _listed_entry(stop: _Stop, root_descriptor: int) -> BuildEntry:
1044
+ """Read the receipt header and the row count, and nothing else.
1045
+
1046
+ This is the one place in the module that opens a sealed file directly, and it buys speed at
1047
+ the cost of every guarantee the verified reading gives. That is why the entry it returns says
1048
+ ``listed, not verified`` in both output modes, and why nothing else in this module does it.
1049
+ """
1050
+
1051
+ try:
1052
+ directory = _open_directory_beneath(root_descriptor, stop.relative)
1053
+ try:
1054
+ receipt = parse_json(_read_unbounded_regular_at(directory, _RECEIPT.parts))
1055
+ finally:
1056
+ os.close(directory)
1057
+ except FileNotFoundError:
1058
+ # Not a build that could not be read: a folder the walk stopped at -- almost always a
1059
+ # Workbench folder that has been made but not run -- which holds no Receipt at all.
1060
+ return BuildEntry(
1061
+ run_dir=stop.run_dir,
1062
+ verified=False,
1063
+ reason=NOTHING_BUILT_REASON,
1064
+ nothing_built=True,
1065
+ )
1066
+ except (OSError, CanonicalJSONError, ValueError):
1067
+ return BuildEntry(run_dir=stop.run_dir, verified=False, reason=UNREADABLE_REASON)
1068
+ if not isinstance(receipt, dict):
1069
+ return BuildEntry(run_dir=stop.run_dir, verified=False, reason=UNREADABLE_REASON)
1070
+ return BuildEntry(
1071
+ run_dir=stop.run_dir,
1072
+ verified=False,
1073
+ manifest_version=_text(receipt.get("manifest_version")),
1074
+ candidate_digest=_text(receipt.get("candidate_digest")),
1075
+ table_sha256=_text(receipt.get("table_sha256")),
1076
+ row_count=_listed_row_count(root_descriptor, stop.relative),
1077
+ )
1078
+
1079
+
1080
+ def _listed_row_count(root_descriptor: int, relative: tuple[str, ...]) -> int | None:
1081
+ """The row count off the same unverified reading; the receipt itself does not carry one."""
1082
+
1083
+ try:
1084
+ directory = _open_directory_beneath(root_descriptor, relative)
1085
+ try:
1086
+ profile = parse_json(_read_unbounded_regular_at(directory, _PROFILE.parts))
1087
+ finally:
1088
+ os.close(directory)
1089
+ except (OSError, CanonicalJSONError, ValueError):
1090
+ return None
1091
+ if not isinstance(profile, dict):
1092
+ return None
1093
+ count = profile.get("row_count")
1094
+ return count if isinstance(count, int) and not isinstance(count, bool) else None
1095
+
1096
+
1097
+ def _text(value: Any) -> str | None:
1098
+ return value if isinstance(value, str) else None
1099
+
1100
+
1101
+ __all__ = [
1102
+ "BUSY_REASON",
1103
+ "DEFAULT_LIST_DEPTH",
1104
+ "DEFAULT_LIST_ROOT",
1105
+ "DESCENDANT_UNREADABLE_REASON",
1106
+ "LISTED_ONLY",
1107
+ "MAX_LIST_DEPTH",
1108
+ "NOTHING_BUILT",
1109
+ "NOTHING_BUILT_REASON",
1110
+ "NOTHING_TO_VERIFY",
1111
+ "NOT_ALL_VERIFIED",
1112
+ "UNREADABLE",
1113
+ "UNREADABLE_REASON",
1114
+ "UNRESOLVED_REASON",
1115
+ "VERIFIED",
1116
+ "BuildEntry",
1117
+ "BuildFacts",
1118
+ "build_facts",
1119
+ "holds_a_build",
1120
+ "list_builds",
1121
+ "listing_verification",
1122
+ "plain_card",
1123
+ "require_a_build",
1124
+ ]