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,572 @@
1
+ """The deterministic unit grammar: a documented UCUM subset, read from a pinned table.
2
+
3
+ A declared unit is a code in the subset of UCUM that ``docs/UNITS.md`` states, and validation is
4
+ the question "does it resolve against the pinned table" rather than "is it in a hand-grown list".
5
+ The table under ``ucum/`` carries the atoms, the prefixes and their exact values; nothing here
6
+ guesses, converts through floating point, or reaches a network.
7
+
8
+ Two things this module deliberately does not do.
9
+
10
+ It does not rewrite a declared code. ``semantics.columns.unit`` is sealed as the author wrote it,
11
+ so a code is resolved for comparison and never canonicalised in place: rewriting ``ug/m3`` into a
12
+ canonical spelling would move the digest of every Build that carried it.
13
+
14
+ And it does not treat a temperature as a ratio. ``Cel`` and ``[degF]`` are affine: zero degrees
15
+ Celsius is not zero of anything, so no multiplier turns one into the other. They resolve with an
16
+ exact scale *and* an exact offset, and every comparison here uses the pair. A caller that reduces
17
+ a unit to one number is refused rather than quietly answered.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import re
24
+ from dataclasses import dataclass
25
+ from fractions import Fraction
26
+ from functools import lru_cache
27
+ from pathlib import Path
28
+ from types import MappingProxyType
29
+ from typing import Any, Final
30
+
31
+ UCUM_SUBSET_VERSION: Final = "ucum-subset.v1"
32
+
33
+ #: The sentinel a column carries when it declares no physical unit at all. It is not a UCUM code
34
+ #: and never resolves to one: ``unit_state`` uses it to mean "there is nothing here to convert",
35
+ #: which is a different claim from the dimensionless ``1``.
36
+ NO_UNIT: Final = "none"
37
+
38
+ _TABLE_DIRECTORY: Final = Path(__file__).resolve().parent / "ucum"
39
+ _TABLE_FILE: Final = "ucum-subset.v1.json"
40
+
41
+ # Bounds. A unit code is a label on a column, not a document: these are wide enough for every
42
+ # code the subset can spell and narrow enough that no input turns resolution into work.
43
+ MAX_UNIT_LENGTH: Final = 64
44
+ MAX_UNIT_TERMS: Final = 16
45
+ MAX_UNIT_EXPONENT: Final = 9
46
+ MAX_ANNOTATION_LENGTH: Final = 32
47
+ # Magnitude, which the length and term bounds do not constrain: sixteen terms of a large atom
48
+ # raised to the ninth power stay inside every bound above and resolve to a number with thousands of
49
+ # digits, which is past what the interpreter will render and therefore past what this module can
50
+ # report. Nothing anybody measures in lives outside this window -- a yottamole is 6 times 10 to the
51
+ # 47 -- so a code that leaves it is refused here rather than at some later `str()`.
52
+ MAX_UNIT_MAGNITUDE_EXPONENT: Final = 120
53
+
54
+ _ANNOTATION = re.compile(r"^[A-Za-z0-9_]+$")
55
+ _EXPONENT = re.compile(r"^[+-]?[0-9]{1,2}$")
56
+
57
+ # Constructs UCUM has and this subset does not. Each is named in the refusal so the author is told
58
+ # what was rejected rather than that something was.
59
+ _UNSUPPORTED_CONSTRUCTS: Final = (
60
+ ("(", "a parenthesised sub-expression"),
61
+ (")", "a parenthesised sub-expression"),
62
+ ("*", "a numeric factor such as 10*3"),
63
+ ("^", "a numeric factor such as 10^3"),
64
+ (" ", "a space"),
65
+ )
66
+
67
+
68
+ class UnitError(ValueError):
69
+ """A typed failure to resolve a unit code, with the exact construct that was refused."""
70
+
71
+ def __init__(self, code: str, detail: str, *, unit: str) -> None:
72
+ self.code = code
73
+ self.detail = detail
74
+ self.unit = unit
75
+ super().__init__(f"{unit!r}: {detail} [{code}]")
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class Unit:
80
+ """One resolved unit: its dimension, its exact scale, and its exact offset.
81
+
82
+ ``factor`` and ``offset`` are the affine map into the table's base units: a value ``x`` in
83
+ this unit is ``x * factor + offset`` in base units. ``offset`` is zero for every ratio-scale
84
+ unit, which is every unit in the subset except ``Cel`` and ``[degF]``.
85
+ """
86
+
87
+ code: str
88
+ exponents: tuple[int, ...]
89
+ factor: Fraction
90
+ offset: Fraction
91
+ annotations: tuple[str, ...]
92
+
93
+ @property
94
+ def affine(self) -> bool:
95
+ """Report whether this unit's zero is somewhere other than the base unit's zero."""
96
+
97
+ return self.offset != 0
98
+
99
+ @property
100
+ def dimensionless(self) -> bool:
101
+ return not any(self.exponents)
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class UnitTable:
106
+ """The pinned atom and prefix table, loaded once and never mutated."""
107
+
108
+ version: str
109
+ standard_version: str
110
+ standard_release: str
111
+ dimensions: tuple[str, ...]
112
+ prefixes: Any
113
+ atoms: Any
114
+ legacy_aliases: Any
115
+
116
+
117
+ def _fraction(value: str) -> Fraction:
118
+ return Fraction(value)
119
+
120
+
121
+ _MAGNITUDE_CEILING: Final = Fraction(10) ** MAX_UNIT_MAGNITUDE_EXPONENT
122
+ _MAGNITUDE_FLOOR: Final = Fraction(1) / _MAGNITUDE_CEILING
123
+
124
+
125
+ def _require_bounded(factor: Fraction, unit: str) -> None:
126
+ if not _MAGNITUDE_FLOOR <= factor <= _MAGNITUDE_CEILING:
127
+ raise UnitError(
128
+ "UCUM_MAGNITUDE_RANGE",
129
+ "the resolved magnitude is outside 10 to the plus or minus "
130
+ f"{MAX_UNIT_MAGNITUDE_EXPONENT}, which no measured quantity reaches",
131
+ unit=unit,
132
+ )
133
+
134
+
135
+ @lru_cache(maxsize=1)
136
+ def unit_table() -> UnitTable:
137
+ """Load the pinned subset table.
138
+
139
+ The read happens once per process. The table is data rather than code precisely so that the
140
+ values it carries can be pinned by digest and recomputed from their own definitions, which is
141
+ what ``tests/test_units_table.py`` does.
142
+ """
143
+
144
+ document = json.loads((_TABLE_DIRECTORY / _TABLE_FILE).read_text(encoding="utf-8"))
145
+ if document.get("schema") != UCUM_SUBSET_VERSION:
146
+ raise UnitError(
147
+ "UCUM_TABLE_VERSION",
148
+ f"the unit table declares {document.get('schema')!r}, not {UCUM_SUBSET_VERSION!r}",
149
+ unit="",
150
+ )
151
+ dimensions = tuple(item["symbol"] for item in document["dimensions"])
152
+ width = len(dimensions)
153
+ atoms: dict[str, dict[str, Any]] = {}
154
+ for symbol, entry in document["atoms"].items():
155
+ exponents = tuple(int(value) for value in entry["exponents"])
156
+ if len(exponents) != width:
157
+ raise UnitError(
158
+ "UCUM_TABLE_SHAPE",
159
+ f"atom {symbol!r} records {len(exponents)} exponents for {width} dimensions",
160
+ unit=symbol,
161
+ )
162
+ atoms[symbol] = {
163
+ "name": entry["name"],
164
+ "kind": entry["kind"],
165
+ "metric": bool(entry["metric"]),
166
+ "exponents": exponents,
167
+ "factor": _fraction(entry["factor"]),
168
+ "offset": _fraction(entry["offset"]) if "offset" in entry else Fraction(0),
169
+ "definition": entry.get("definition"),
170
+ }
171
+ prefixes = {
172
+ symbol: {"name": entry["name"], "factor": _fraction(entry["factor"])}
173
+ for symbol, entry in document["prefixes"].items()
174
+ }
175
+ return UnitTable(
176
+ version=document["schema"],
177
+ standard_version=document["standard_version"],
178
+ standard_release=document["standard_release"],
179
+ dimensions=dimensions,
180
+ prefixes=MappingProxyType(prefixes),
181
+ atoms=MappingProxyType(atoms),
182
+ legacy_aliases=MappingProxyType(dict(document["legacy_aliases"])),
183
+ )
184
+
185
+
186
+ def legacy_aliases() -> Any:
187
+ """The fifteen tokens the hand-grown list held, mapped to the codes they always meant.
188
+
189
+ ``none`` is absent on purpose: it was never a unit and does not become one here.
190
+ """
191
+
192
+ return unit_table().legacy_aliases
193
+
194
+
195
+ def _split_terms(text: str, unit: str) -> list[tuple[int, str]]:
196
+ """Split an expression into signed terms without splitting inside a bracketed atom."""
197
+
198
+ terms: list[tuple[int, str]] = []
199
+ sign = 1
200
+ start = 0
201
+ depth = 0
202
+ braces = 0
203
+ if text.startswith("/"):
204
+ sign = -1
205
+ start = 1
206
+ position = start
207
+ while position < len(text):
208
+ character = text[position]
209
+ if character == "[":
210
+ depth += 1
211
+ elif character == "]":
212
+ depth -= 1
213
+ if depth < 0:
214
+ raise UnitError("UCUM_SYNTAX", "a ']' closes nothing", unit=unit)
215
+ elif character == "{":
216
+ braces += 1
217
+ elif character == "}":
218
+ braces -= 1
219
+ if braces < 0:
220
+ raise UnitError("UCUM_SYNTAX", "a '}' closes nothing", unit=unit)
221
+ elif character in "./" and depth == 0 and braces == 0:
222
+ terms.append((sign, text[start:position]))
223
+ sign = 1 if character == "." else -1
224
+ start = position + 1
225
+ position += 1
226
+ if depth or braces:
227
+ raise UnitError("UCUM_SYNTAX", "a bracket or brace is left open", unit=unit)
228
+ terms.append((sign, text[start:]))
229
+ return terms
230
+
231
+
232
+ def _annotation(term: str, unit: str) -> tuple[str, str | None]:
233
+ """Split one trailing ``{...}`` annotation off a term."""
234
+
235
+ if not term.endswith("}"):
236
+ if "{" in term or "}" in term:
237
+ raise UnitError(
238
+ "UCUM_ANNOTATION",
239
+ "an annotation must be a '{...}' suffix on one term",
240
+ unit=unit,
241
+ )
242
+ return term, None
243
+ opened = term.find("{")
244
+ if opened < 0:
245
+ raise UnitError("UCUM_SYNTAX", "a '}' closes nothing", unit=unit)
246
+ text = term[opened + 1 : -1]
247
+ if not 1 <= len(text) <= MAX_ANNOTATION_LENGTH or not _ANNOTATION.fullmatch(text):
248
+ raise UnitError(
249
+ "UCUM_ANNOTATION",
250
+ f"an annotation must be 1..{MAX_ANNOTATION_LENGTH} letters, digits or underscores",
251
+ unit=unit,
252
+ )
253
+ return term[:opened], text
254
+
255
+
256
+ def _exponent(body: str, unit: str) -> tuple[str, int]:
257
+ """Split a trailing integer power off a term body."""
258
+
259
+ if body != "1" and body.lstrip("+-").isdigit():
260
+ raise UnitError(
261
+ "UCUM_UNSUPPORTED_CONSTRUCT",
262
+ f"a numeric factor such as {body} is outside {UCUM_SUBSET_VERSION}",
263
+ unit=unit,
264
+ )
265
+ digits = ""
266
+ while len(body) > 1 and body[-1].isdigit():
267
+ digits = body[-1] + digits
268
+ body = body[:-1]
269
+ if len(body) > 1 and body[-1] in "+-" and digits:
270
+ digits = body[-1] + digits
271
+ body = body[:-1]
272
+ if not digits:
273
+ return body, 1
274
+ if not _EXPONENT.fullmatch(digits):
275
+ raise UnitError("UCUM_SYNTAX", f"{digits!r} is not an integer power", unit=unit)
276
+ exponent = int(digits)
277
+ if not -MAX_UNIT_EXPONENT <= exponent <= MAX_UNIT_EXPONENT or exponent == 0:
278
+ raise UnitError(
279
+ "UCUM_EXPONENT_RANGE",
280
+ f"an integer power must be between -{MAX_UNIT_EXPONENT} and {MAX_UNIT_EXPONENT} "
281
+ "and may not be zero",
282
+ unit=unit,
283
+ )
284
+ return body, exponent
285
+
286
+
287
+ def _resolve_atom(symbol: str, unit: str) -> tuple[dict[str, Any], Fraction]:
288
+ """Resolve one atom, taking the whole symbol first and a prefix split only if it is not one.
289
+
290
+ The order is the rule, not a preference. ``cd`` is the candela and not a centi-day, and ``Pa``
291
+ is the pascal and not a peta-year, because a symbol the table holds outright is never read as
292
+ something else. Where the whole symbol is unknown, exactly one prefix split may resolve it; a
293
+ symbol that two splits could explain is refused rather than silently decided.
294
+ """
295
+
296
+ table = unit_table()
297
+ atom = table.atoms.get(symbol)
298
+ if atom is not None:
299
+ return atom, Fraction(1)
300
+ matches: list[tuple[str, dict[str, Any], Fraction]] = []
301
+ special: list[str] = []
302
+ non_metric: list[str] = []
303
+ for prefix, entry in table.prefixes.items():
304
+ if not symbol.startswith(prefix):
305
+ continue
306
+ remainder = symbol[len(prefix) :]
307
+ candidate = table.atoms.get(remainder)
308
+ if candidate is None:
309
+ continue
310
+ if candidate["kind"] == "special":
311
+ special.append(remainder)
312
+ continue
313
+ if not candidate["metric"]:
314
+ non_metric.append(remainder)
315
+ continue
316
+ matches.append((prefix, candidate, entry["factor"]))
317
+ if not matches and special:
318
+ raise UnitError(
319
+ "UCUM_PREFIX_ON_SPECIAL",
320
+ f"{special[0]!r} is an offset unit and this subset accepts no prefix on one",
321
+ unit=unit,
322
+ )
323
+ if not matches and non_metric:
324
+ raise UnitError(
325
+ "UCUM_PREFIX_ON_NON_METRIC",
326
+ f"{non_metric[0]!r} takes no prefix, so {symbol!r} is not a unit",
327
+ unit=unit,
328
+ )
329
+ if not matches:
330
+ raise UnitError(
331
+ "UCUM_UNKNOWN_ATOM",
332
+ f"{symbol!r} is not an atom in {UCUM_SUBSET_VERSION}",
333
+ unit=unit,
334
+ )
335
+ if len(matches) > 1:
336
+ spellings = ", ".join(f"{prefix}+{symbol[len(prefix) :]}" for prefix, _, _ in matches)
337
+ raise UnitError(
338
+ "UCUM_AMBIGUOUS_ATOM",
339
+ f"{symbol!r} reads as more than one prefixed atom ({spellings})",
340
+ unit=unit,
341
+ )
342
+ _prefix, candidate, factor = matches[0]
343
+ return candidate, factor
344
+
345
+
346
+ def parse_unit(unit: str) -> Unit:
347
+ """Resolve one UCUM code from the documented subset, or refuse it by name.
348
+
349
+ This is the whole of the acceptance test for ``semantics.columns.unit``. It is total: every
350
+ input either resolves to a :class:`Unit` or raises a :class:`UnitError` whose code says which
351
+ rule refused it.
352
+ """
353
+
354
+ if not isinstance(unit, str):
355
+ raise UnitError("UCUM_SYNTAX", "a unit code must be text", unit=str(unit))
356
+ if not unit:
357
+ raise UnitError("UCUM_SYNTAX", "a unit code must not be empty", unit=unit)
358
+ if len(unit) > MAX_UNIT_LENGTH:
359
+ raise UnitError(
360
+ "UCUM_LENGTH", f"a unit code may not exceed {MAX_UNIT_LENGTH} characters", unit=unit
361
+ )
362
+ for character, description in _UNSUPPORTED_CONSTRUCTS:
363
+ if character in unit:
364
+ raise UnitError(
365
+ "UCUM_UNSUPPORTED_CONSTRUCT",
366
+ f"{description} is outside {UCUM_SUBSET_VERSION}",
367
+ unit=unit,
368
+ )
369
+ if unit != unit.strip() or any(ord(character) < 0x20 for character in unit):
370
+ raise UnitError("UCUM_SYNTAX", "a unit code is one plain line of text", unit=unit)
371
+
372
+ terms = _split_terms(unit, unit)
373
+ if len(terms) > MAX_UNIT_TERMS:
374
+ raise UnitError(
375
+ "UCUM_LENGTH", f"a unit code may not exceed {MAX_UNIT_TERMS} terms", unit=unit
376
+ )
377
+
378
+ table = unit_table()
379
+ exponents = [0] * len(table.dimensions)
380
+ factor = Fraction(1)
381
+ offset = Fraction(0)
382
+ annotations: list[str] = []
383
+ special_terms = 0
384
+ for sign, term in terms:
385
+ if not term:
386
+ raise UnitError("UCUM_SYNTAX", "a term is empty", unit=unit)
387
+ body, annotation = _annotation(term, unit)
388
+ if annotation is not None:
389
+ annotations.append(annotation)
390
+ if not body:
391
+ # A bare annotation is unity carrying a label, which is what ``{count}`` is.
392
+ if sign < 0:
393
+ raise UnitError(
394
+ "UCUM_SYNTAX", "a bare annotation cannot be a denominator", unit=unit
395
+ )
396
+ continue
397
+ body, exponent = _exponent(body, unit)
398
+ atom, prefix_factor = _resolve_atom(body, unit)
399
+ if atom["kind"] == "special":
400
+ special_terms += 1
401
+ if annotation is not None or exponent != 1 or len(terms) > 1 or sign < 0:
402
+ raise UnitError(
403
+ "UCUM_SPECIAL_UNIT_NOT_COMPOSABLE",
404
+ f"{body!r} is an offset unit: it stands alone, with no power, no "
405
+ "annotation and nothing multiplied by it",
406
+ unit=unit,
407
+ )
408
+ offset = atom["offset"]
409
+ power = sign * exponent
410
+ factor *= (prefix_factor * atom["factor"]) ** power
411
+ for index, value in enumerate(atom["exponents"]):
412
+ exponents[index] += value * power
413
+ # The resolved magnitude, once, rather than the running product: ``mol9/mol9`` comes to one and
414
+ # is a unit, and bounding the partial product would have refused it for a number it never has.
415
+ # The intermediate is bounded by the term and exponent caps rather than by this, and stays
416
+ # small enough to hold; what it is not always small enough to do is be written out.
417
+ _require_bounded(factor, unit)
418
+ if special_terms > 1: # pragma: no cover - the single-term rule above already refuses this.
419
+ raise UnitError(
420
+ "UCUM_SPECIAL_UNIT_NOT_COMPOSABLE", "two offset units cannot be combined", unit=unit
421
+ )
422
+ return Unit(
423
+ code=unit,
424
+ exponents=tuple(exponents),
425
+ factor=factor,
426
+ offset=offset,
427
+ annotations=tuple(sorted(set(annotations))),
428
+ )
429
+
430
+
431
+ def resolve_declared_unit(declared: str) -> Unit:
432
+ """Resolve a declared unit, reading a legacy token as the code it always meant.
433
+
434
+ The fifteen tokens the hand-grown list held keep resolving forever. They are aliases rather
435
+ than a second vocabulary: ``celsius`` and ``Cel`` resolve to the same unit, so a Build sealed
436
+ with one and a plan authored with the other are talking about the same thing.
437
+ """
438
+
439
+ if not isinstance(declared, str):
440
+ # Before the alias lookup, which hashes what it is given: a declaration that is not text
441
+ # would otherwise leave here as a TypeError, one call short of the boundary this module
442
+ # promises to be total at.
443
+ raise UnitError("UCUM_SYNTAX", "a unit code must be text", unit=str(declared))
444
+ if declared == NO_UNIT:
445
+ raise UnitError(
446
+ "UCUM_NOT_A_UNIT",
447
+ f"{NO_UNIT!r} declares the absence of a unit and resolves to no code",
448
+ unit=declared,
449
+ )
450
+ return parse_unit(legacy_aliases().get(declared, declared))
451
+
452
+
453
+ def is_declarable_unit(declared: str) -> bool:
454
+ """Report whether a declared string resolves, without raising."""
455
+
456
+ try:
457
+ resolve_declared_unit(declared)
458
+ except UnitError:
459
+ return False
460
+ return True
461
+
462
+
463
+ def commensurable(left: Unit, right: Unit) -> bool:
464
+ """Report whether two units measure the same kind of thing."""
465
+
466
+ return left.exponents == right.exponents
467
+
468
+
469
+ def same_unit(left: Unit, right: Unit) -> bool:
470
+ """Report whether two codes name one unit, annotations included.
471
+
472
+ ``{count}`` and ``1`` are both dimensionless with factor one and are still not the same unit:
473
+ a count of hospitals is not a ratio, and the annotation is the only thing that says so.
474
+ """
475
+
476
+ return (
477
+ left.exponents == right.exponents
478
+ and left.factor == right.factor
479
+ and left.offset == right.offset
480
+ and left.annotations == right.annotations
481
+ )
482
+
483
+
484
+ def conversion(source: Unit, target: Unit) -> tuple[Fraction, Fraction]:
485
+ """Return the exact affine map ``(scale, offset)`` taking a value in ``source`` to ``target``.
486
+
487
+ ``value_target == value_source * scale + offset``. For every ratio-scale pair the offset is
488
+ zero and the scale is the factor a conversion has to multiply by. For the two offset units it
489
+ is not: Celsius to Fahrenheit is ``9/5`` and ``32``, and a check that looked only at the scale
490
+ would accept a conversion that is wrong by thirty-two degrees at every temperature.
491
+ """
492
+
493
+ if not commensurable(source, target):
494
+ raise UnitError(
495
+ "UCUM_NOT_COMMENSURABLE",
496
+ f"{source.code!r} and {target.code!r} do not measure the same kind of thing",
497
+ unit=source.code,
498
+ )
499
+ scale = source.factor / target.factor
500
+ offset = (source.offset - target.offset) / target.factor
501
+ return scale, offset
502
+
503
+
504
+ def ratio_scale_factor(unit: Unit) -> Fraction:
505
+ """The single multiplier that takes this unit into base units, for a ratio-scale unit only.
506
+
507
+ An offset unit has no such number. Twenty degrees Celsius is not twenty of anything, and a
508
+ caller that multiplied by this unit's scale and stopped would be wrong by the offset at every
509
+ value -- which is the whole reason the two temperature scales are carried as a pair. So the
510
+ question is refused rather than answered with half of it.
511
+ """
512
+
513
+ if unit.affine:
514
+ raise UnitError(
515
+ "UCUM_AFFINE_UNIT",
516
+ f"{unit.code!r} is an offset unit: it has a scale and an offset, and no single "
517
+ "multiplier stands for both. Use conversion() instead",
518
+ unit=unit.code,
519
+ )
520
+ return unit.factor
521
+
522
+
523
+ def dimension_signature(unit: Unit) -> str:
524
+ """A stable, readable spelling of a unit's dimension for a refusal message."""
525
+
526
+ table = unit_table()
527
+ parts = [
528
+ symbol if exponent == 1 else f"{symbol}{exponent}"
529
+ for symbol, exponent in zip(table.dimensions, unit.exponents, strict=True)
530
+ if exponent
531
+ ]
532
+ return ".".join(parts) if parts else "1"
533
+
534
+
535
+ COUNT_UNIT_CODE: Final = "{count}"
536
+
537
+
538
+ def is_count_unit(unit: Unit) -> bool:
539
+ """Report whether a unit is the dimensionless count, however it was spelled."""
540
+
541
+ return (
542
+ unit.dimensionless
543
+ and unit.factor == 1
544
+ and unit.offset == 0
545
+ and unit.annotations == ("count",)
546
+ )
547
+
548
+
549
+ __all__ = [
550
+ "COUNT_UNIT_CODE",
551
+ "MAX_ANNOTATION_LENGTH",
552
+ "MAX_UNIT_EXPONENT",
553
+ "MAX_UNIT_LENGTH",
554
+ "MAX_UNIT_MAGNITUDE_EXPONENT",
555
+ "MAX_UNIT_TERMS",
556
+ "NO_UNIT",
557
+ "UCUM_SUBSET_VERSION",
558
+ "Unit",
559
+ "UnitError",
560
+ "UnitTable",
561
+ "commensurable",
562
+ "conversion",
563
+ "dimension_signature",
564
+ "is_count_unit",
565
+ "is_declarable_unit",
566
+ "legacy_aliases",
567
+ "parse_unit",
568
+ "ratio_scale_factor",
569
+ "resolve_declared_unit",
570
+ "same_unit",
571
+ "unit_table",
572
+ ]
@@ -0,0 +1,9 @@
1
+ """Human-facing surfaces for ``mr-data``.
2
+
3
+ Every command emits the same facts twice: as plain labelled lines by default and as one JSON
4
+ object under ``--json``. Both renderings are built from a single payload mapping, so a fact that
5
+ reaches one rendering reaches the other.
6
+
7
+ This package holds the rendering and the command registry only. It deliberately re-exports
8
+ nothing, so importing it stays cheap for the CLI startup path.
9
+ """