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,825 @@
1
+ """Versioned catalog facts about one public source.
2
+
3
+ A ``CatalogEntry`` names one public source and carries the facts an agent needs to choose it:
4
+ coverage and cadence (through ``SourceObservation``), payload formats, access shape, rights
5
+ evidence, published column and vocabulary names, and the quirks a reader will trip over. Its
6
+ identity reads ``faostat_qcl@v3`` -- an entry id plus a version that is chained to its
7
+ predecessor's canonical **facts** digest, so the version cannot drift from the evidence and the
8
+ chain reads the same before and after the catalog is sealed.
9
+
10
+ Three properties of this module are load-bearing and must not be relaxed.
11
+
12
+ **No float, anywhere.** ``canonical_json_bytes`` refuses every non-integer JSON number, and
13
+ ``_CanonicalContract.digest`` routes through it. Embedding vectors are therefore integer-quantized
14
+ and referenced by digest through ``LayerVectorReference`` rather than inlined; that also keeps an
15
+ entry far below the canonical member cap.
16
+
17
+ **No tenant data, by construction.** There is deliberately no field able to hold a dataset, a
18
+ recipe, a Reader, a row sample, or a profile drawn from a tenant's private source. Adding one
19
+ would break the tenancy rule at the type level. ``declared_columns`` and ``declared_vocabulary``
20
+ hold *published names only* -- the headers a source documents and the item, category, or unit
21
+ terms it publishes -- never values read out of anyone's data. Both are bounded, and the bound is
22
+ part of the defence: a name list capped at 256 and 512 entries of 128 characters cannot smuggle a
23
+ dataset. The ``profiles`` embedding *layer* describes the public source's shape and is sealed as
24
+ an integer vector reference, never as data.
25
+
26
+ **No public citation, no entry.** An entry cannot be constructed without at least one
27
+ ``EvidenceReference``, whose ``uri`` must already be HTTPS. A fact nobody can cite is not a
28
+ catalog fact.
29
+
30
+ Rights are never adjudicated here. ``sources.rights_rule.rights_disposition`` is the one authority
31
+ that decides what a rights status means.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import re
37
+ from collections.abc import Mapping
38
+ from dataclasses import dataclass, replace
39
+ from typing import Any
40
+
41
+ from mostlyright.data_harness.canonical import canonical_sha256
42
+ from mostlyright.data_harness.sources.contracts import (
43
+ DATA_FORMATS,
44
+ MAX_EVIDENCE,
45
+ MAX_SAFE_INTEGER,
46
+ EvidenceReference,
47
+ LicenseObligation,
48
+ RightsEvidence,
49
+ SourceContractError,
50
+ SourceObservation,
51
+ _CanonicalContract,
52
+ )
53
+
54
+ CATALOG_ENTRY_CONTRACT_VERSION = "harness-source-catalog-entry.v1"
55
+ CATALOG_INDEX_CONTRACT_VERSION = "harness-source-catalog-index.v1"
56
+
57
+ # The four retrieval layers, in order. Retrieval finds; facts gate.
58
+ EMBEDDING_LAYERS = ("description", "metadata", "columns", "profiles")
59
+
60
+ # Every access kind is an open-access shape by construction. There is deliberately no
61
+ # authenticated member: harvesters cover open endpoints only, and a 401/403 is a typed refusal
62
+ # naming the parked authenticated-adapter item rather than a credential prompt.
63
+ ACCESS_KINDS = frozenset({"bulk_download", "open_api", "open_catalog"})
64
+
65
+ # One sealed quantization scheme. A changed scale silently changes retrieval, so the scheme is
66
+ # named in the entry and verified when a sealed catalog is loaded.
67
+ QUANTIZATIONS = frozenset({"int32_fixed_point_10000"})
68
+
69
+ MAX_QUIRKS = 32
70
+ MAX_QUIRK_DETAIL = 500
71
+ MAX_SPATIAL_SCOPE = 64
72
+ MAX_ENTRY_VERSION = 10_000
73
+ MAX_DECLARED_COLUMNS = 256
74
+ MAX_DECLARED_VOCABULARY = 512
75
+ MAX_DECLARED_NAME = 128
76
+ MAX_DESCRIPTION = 4_000
77
+ MAX_DIMENSIONS = 4_096
78
+
79
+ _IDENTIFIER = re.compile(r"^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$")
80
+ _SEMVER = re.compile(r"^[1-9][0-9]*\.[0-9]+\.[0-9]+$")
81
+ _SHA256 = re.compile(r"^[0-9a-f]{64}$")
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class SourceQuirk(_CanonicalContract):
86
+ """One published-data trap a reader of this source will hit.
87
+
88
+ The worked examples are real: FAOSTAT publishes code columns with a leading apostrophe (an
89
+ Excel text-guard artifact) and ships its CSV with a UTF-8 byte-order mark. Both silently
90
+ corrupt a naive read, and both are facts about the public source rather than about anyone's
91
+ data.
92
+ """
93
+
94
+ code: str
95
+ detail: str
96
+
97
+ def __post_init__(self) -> None:
98
+ _identifier(self.code, "quirk.code")
99
+ _text(self.detail, "quirk.detail", maximum=MAX_QUIRK_DETAIL)
100
+
101
+ def to_dict(self) -> dict[str, str]:
102
+ return {"code": self.code, "detail": self.detail}
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class LayerVectorReference(_CanonicalContract):
107
+ """A sealed integer embedding vector, referenced by digest and never inlined.
108
+
109
+ Carrying the vector itself would put thousands of numbers inside a digested contract. Instead
110
+ the entry names the layer, the backend that produced the vector, the quantization scheme, the
111
+ dimension count, and the SHA-256 of the sealed bytes -- the same shape ``SnapshotReference``
112
+ already uses for sealed payloads.
113
+
114
+ ``entry_facts_digest`` is what binds the vector to the facts it was computed from. Without it a
115
+ vector is just four self-consistent numbers: a sealed catalog assembled from entries carrying
116
+ some *other* entry's vectors passes every integrity check -- four layers present, one backend
117
+ identity, files present and digest-matching -- and then ranks the wrong source on the wrong
118
+ text, with no error at any point. That is the one failure mode of this module that is silent,
119
+ so the binding is a field rather than a convention. Checking it costs a digest comparison and
120
+ computes no embedding, which is what keeps it legal on the query path.
121
+ """
122
+
123
+ layer: str
124
+ backend_id: str
125
+ backend_version: str
126
+ dimensions: int
127
+ quantization: str
128
+ vector_sha256: str
129
+ entry_facts_digest: str
130
+
131
+ def __post_init__(self) -> None:
132
+ _choice(self.layer, frozenset(EMBEDDING_LAYERS), "vector.layer")
133
+ _identifier(self.backend_id, "vector.backend_id")
134
+ _semver(self.backend_version, "vector.backend_version")
135
+ _integer(self.dimensions, "vector.dimensions", minimum=1, maximum=MAX_DIMENSIONS)
136
+ _choice(self.quantization, QUANTIZATIONS, "vector.quantization")
137
+ _digest(self.vector_sha256, "vector.vector_sha256")
138
+ _digest(self.entry_facts_digest, "vector.entry_facts_digest")
139
+
140
+ def to_dict(self) -> dict[str, Any]:
141
+ return {
142
+ "layer": self.layer,
143
+ "backend_id": self.backend_id,
144
+ "backend_version": self.backend_version,
145
+ "dimensions": self.dimensions,
146
+ "quantization": self.quantization,
147
+ "vector_sha256": self.vector_sha256,
148
+ "entry_facts_digest": self.entry_facts_digest,
149
+ }
150
+
151
+
152
+ @dataclass(frozen=True)
153
+ class CatalogEntry(_CanonicalContract):
154
+ """One versioned public source, described entirely by citable facts."""
155
+
156
+ entry_id: str
157
+ entry_version: int
158
+ publisher: str
159
+ title: str
160
+ description: str
161
+ spatial_scope: tuple[str, ...]
162
+ data_formats: tuple[str, ...]
163
+ access_kind: str
164
+ authentication_required: bool
165
+ observation: SourceObservation
166
+ rights: RightsEvidence
167
+ quirks: tuple[SourceQuirk, ...] = ()
168
+ declared_columns: tuple[str, ...] = ()
169
+ declared_vocabulary: tuple[str, ...] = ()
170
+ declared_row_count: int | None = None
171
+ evidence: tuple[EvidenceReference, ...] = ()
172
+ vectors: tuple[LayerVectorReference, ...] = ()
173
+ previous_digest: str | None = None
174
+ schema_version: str = CATALOG_ENTRY_CONTRACT_VERSION
175
+
176
+ def __post_init__(self) -> None:
177
+ if self.schema_version != CATALOG_ENTRY_CONTRACT_VERSION:
178
+ raise SourceContractError(
179
+ "VERSION",
180
+ "catalog_entry.schema_version",
181
+ f"must equal {CATALOG_ENTRY_CONTRACT_VERSION!r}",
182
+ )
183
+ _identifier(self.entry_id, "catalog_entry.entry_id")
184
+ if type(self.entry_version) is not int or not 1 <= self.entry_version <= MAX_ENTRY_VERSION:
185
+ raise SourceContractError(
186
+ "CATALOG_VERSION",
187
+ "catalog_entry.entry_version",
188
+ f"must be an integer in [1, {MAX_ENTRY_VERSION}]",
189
+ )
190
+ _text(self.publisher, "catalog_entry.publisher", maximum=200)
191
+ _text(self.title, "catalog_entry.title", maximum=200)
192
+ _text(self.description, "catalog_entry.description", maximum=MAX_DESCRIPTION)
193
+ _texts(
194
+ self.spatial_scope,
195
+ "catalog_entry.spatial_scope",
196
+ nonempty=True,
197
+ maximum=MAX_SPATIAL_SCOPE,
198
+ item_maximum=MAX_DECLARED_NAME,
199
+ )
200
+ _choices(
201
+ self.data_formats,
202
+ DATA_FORMATS,
203
+ "catalog_entry.data_formats",
204
+ nonempty=True,
205
+ maximum=len(DATA_FORMATS),
206
+ )
207
+ _choice(self.access_kind, ACCESS_KINDS, "catalog_entry.access_kind")
208
+ _boolean(self.authentication_required, "catalog_entry.authentication_required")
209
+ if not isinstance(self.observation, SourceObservation):
210
+ raise SourceContractError(
211
+ "TYPE",
212
+ "catalog_entry.observation",
213
+ "must be a SourceObservation",
214
+ )
215
+ if not isinstance(self.rights, RightsEvidence):
216
+ raise SourceContractError(
217
+ "TYPE",
218
+ "catalog_entry.rights",
219
+ "must be a RightsEvidence",
220
+ )
221
+ if self.observation.source_id != self.entry_id:
222
+ raise SourceContractError(
223
+ "CATALOG_SOURCE_BINDING",
224
+ "catalog_entry.observation",
225
+ "the observation and the entry must describe the same source",
226
+ )
227
+ _typed_tuple(
228
+ self.quirks,
229
+ SourceQuirk,
230
+ "catalog_entry.quirks",
231
+ maximum=MAX_QUIRKS,
232
+ )
233
+ if len({item.code for item in self.quirks}) != len(self.quirks):
234
+ raise SourceContractError(
235
+ "DUPLICATE",
236
+ "catalog_entry.quirks",
237
+ "quirk codes must be unique",
238
+ )
239
+ _texts(
240
+ self.declared_columns,
241
+ "catalog_entry.declared_columns",
242
+ nonempty=False,
243
+ maximum=MAX_DECLARED_COLUMNS,
244
+ item_maximum=MAX_DECLARED_NAME,
245
+ )
246
+ _texts(
247
+ self.declared_vocabulary,
248
+ "catalog_entry.declared_vocabulary",
249
+ nonempty=False,
250
+ maximum=MAX_DECLARED_VOCABULARY,
251
+ item_maximum=MAX_DECLARED_NAME,
252
+ )
253
+ if self.declared_row_count is not None:
254
+ _integer(
255
+ self.declared_row_count,
256
+ "catalog_entry.declared_row_count",
257
+ minimum=0,
258
+ maximum=MAX_SAFE_INTEGER,
259
+ )
260
+ _typed_tuple(
261
+ self.evidence,
262
+ EvidenceReference,
263
+ "catalog_entry.evidence",
264
+ maximum=MAX_EVIDENCE,
265
+ )
266
+ if not self.evidence:
267
+ raise SourceContractError(
268
+ "CATALOG_EVIDENCE",
269
+ "catalog_entry.evidence",
270
+ "a catalog entry requires at least one public citation",
271
+ )
272
+ _typed_tuple(
273
+ self.vectors,
274
+ LayerVectorReference,
275
+ "catalog_entry.vectors",
276
+ maximum=len(EMBEDDING_LAYERS),
277
+ )
278
+ if self.vectors:
279
+ layers = tuple(item.layer for item in self.vectors)
280
+ if sorted(layers) != sorted(EMBEDDING_LAYERS):
281
+ raise SourceContractError(
282
+ "CATALOG_VECTOR_LAYERS",
283
+ "catalog_entry.vectors",
284
+ f"must carry exactly one vector per layer {list(EMBEDDING_LAYERS)}, "
285
+ "or none at all",
286
+ )
287
+ if self.previous_digest is not None:
288
+ _digest(self.previous_digest, "catalog_entry.previous_digest")
289
+ if self.entry_version == 1 and self.previous_digest is not None:
290
+ raise SourceContractError(
291
+ "CATALOG_VERSION_CHAIN",
292
+ "catalog_entry.previous_digest",
293
+ "a first version has no predecessor to link to",
294
+ )
295
+ if self.entry_version > 1 and self.previous_digest is None:
296
+ raise SourceContractError(
297
+ "CATALOG_VERSION_CHAIN",
298
+ "catalog_entry.previous_digest",
299
+ "a successor version must link to its predecessor's digest",
300
+ )
301
+
302
+ @property
303
+ def coordinate(self) -> str:
304
+ """Return the entry identity, for example ``faostat_qcl@v3``."""
305
+
306
+ return f"{self.entry_id}@v{self.entry_version}"
307
+
308
+ @property
309
+ def facts_digest(self) -> str:
310
+ """The digest of everything this entry *claims*, with its sealed vectors excluded.
311
+
312
+ The version chain runs on this rather than on ``digest``, and it has to. Sealing replaces
313
+ an entry's ``vectors``, which changes ``digest``; a chain linked on ``digest`` would
314
+ therefore verify before sealing and break after it, so the entries a sealed catalog holds
315
+ and the authored entry files they came from would be two permanently incompatible spellings
316
+ of one history. Excluding the vectors makes the link stable across sealing while leaving
317
+ every fact -- including ``previous_digest`` itself -- inside the digest, so a tampered
318
+ history still cannot re-link.
319
+
320
+ This is also the value a ``LayerVectorReference`` carries, which is what binds a sealed
321
+ vector to the facts it was computed from.
322
+ """
323
+
324
+ payload = self.to_dict()
325
+ del payload["vectors"]
326
+ return canonical_sha256(payload)
327
+
328
+ def to_dict(self) -> dict[str, Any]:
329
+ return {
330
+ "schema_version": self.schema_version,
331
+ "entry_id": self.entry_id,
332
+ "entry_version": self.entry_version,
333
+ "publisher": self.publisher,
334
+ "title": self.title,
335
+ "description": self.description,
336
+ "spatial_scope": list(self.spatial_scope),
337
+ "data_formats": list(self.data_formats),
338
+ "access_kind": self.access_kind,
339
+ "authentication_required": self.authentication_required,
340
+ "observation": self.observation.to_dict(),
341
+ "rights": self.rights.to_dict(),
342
+ "quirks": [item.to_dict() for item in self.quirks],
343
+ "declared_columns": list(self.declared_columns),
344
+ "declared_vocabulary": list(self.declared_vocabulary),
345
+ "declared_row_count": self.declared_row_count,
346
+ "evidence": [item.to_dict() for item in self.evidence],
347
+ "vectors": [item.to_dict() for item in self.vectors],
348
+ "previous_digest": self.previous_digest,
349
+ }
350
+
351
+
352
+ _MANAGED_VERSION_FIELDS = ("entry_version", "previous_digest", "vectors")
353
+
354
+
355
+ def next_entry_version(previous: CatalogEntry, **changes: object) -> CatalogEntry:
356
+ """Mint the successor of ``previous``, linked to its predecessor's facts digest.
357
+
358
+ The caller supplies the changed facts and nothing else. ``entry_version``,
359
+ ``previous_digest`` and ``vectors`` are computed here and refused as arguments -- that refusal
360
+ is precisely what stops a version from drifting away from the evidence it claims to describe.
361
+
362
+ Two of those three deserve their reason stated.
363
+
364
+ ``previous_digest`` is ``previous.facts_digest`` and not ``previous.digest``: sealing rewrites
365
+ an entry's ``vectors``, so a link taken over the whole entry would depend on whether the author
366
+ happened to hold the sealed or the unsealed spelling of the predecessor, and only one of the
367
+ two would ever verify. Linking on the facts makes the answer the same either way.
368
+
369
+ ``vectors`` is cleared unconditionally. Carrying a predecessor's sealed vectors onto a
370
+ successor is the one way to manufacture an entry that ranks on facts it no longer states, and
371
+ it is exactly what a bare ``dataclasses.replace`` would do. A successor is re-sealed, never
372
+ inherited.
373
+ """
374
+
375
+ if not isinstance(previous, CatalogEntry):
376
+ raise SourceContractError("TYPE", "previous", "must be a CatalogEntry")
377
+ managed = sorted(name for name in _MANAGED_VERSION_FIELDS if name in changes)
378
+ if managed:
379
+ raise SourceContractError(
380
+ "CATALOG_VERSION_MANAGED",
381
+ f"changes.{managed[0]}",
382
+ f"{sorted(_MANAGED_VERSION_FIELDS)} are derived from the predecessor and cannot be "
383
+ "supplied",
384
+ )
385
+ return replace(
386
+ previous,
387
+ entry_version=previous.entry_version + 1,
388
+ previous_digest=previous.facts_digest,
389
+ vectors=(),
390
+ **changes,
391
+ )
392
+
393
+
394
+ def verify_version_chain(chain: tuple[CatalogEntry, ...]) -> str:
395
+ """Verify a whole entry history and return its head coordinate, e.g. ``faostat_qcl@v3``.
396
+
397
+ A chain verifies when every member names the same source, the versions run ``1..n`` with no
398
+ gap, the first member claims no predecessor, and every later member's ``previous_digest`` is
399
+ the exact ``facts_digest`` of the member before it. Changing any fact of any member breaks
400
+ every link after it, so a tampered history cannot re-link itself.
401
+
402
+ The link is over ``facts_digest`` rather than ``digest`` so that one history verifies in both
403
+ of its spellings: the authored entries an author holds and the sealed entries a catalog holds
404
+ differ only in their vectors, and a chain that verified in one and not the other would be a
405
+ chain nobody could actually check.
406
+ """
407
+
408
+ if not isinstance(chain, tuple):
409
+ raise SourceContractError("TYPE", "chain", "must be an immutable tuple")
410
+ if not chain:
411
+ raise SourceContractError(
412
+ "CATALOG_VERSION_CHAIN",
413
+ "chain",
414
+ "a version chain must contain at least one entry",
415
+ )
416
+ for index, entry in enumerate(chain):
417
+ if not isinstance(entry, CatalogEntry):
418
+ raise SourceContractError(
419
+ "CATALOG_VERSION_CHAIN",
420
+ f"chain[{index}]",
421
+ "must be a CatalogEntry",
422
+ )
423
+ if entry.entry_id != chain[0].entry_id:
424
+ raise SourceContractError(
425
+ "CATALOG_VERSION_CHAIN",
426
+ f"chain[{index}].entry_id",
427
+ f"every member must describe {chain[0].entry_id!r}, found {entry.entry_id!r}",
428
+ )
429
+ if entry.entry_version != index + 1:
430
+ raise SourceContractError(
431
+ "CATALOG_VERSION_CHAIN",
432
+ f"chain[{index}].entry_version",
433
+ f"versions must run 1..n in order with no gap; expected {index + 1}, "
434
+ f"found {entry.entry_version}",
435
+ )
436
+ if index == 0:
437
+ if entry.previous_digest is not None:
438
+ raise SourceContractError(
439
+ "CATALOG_VERSION_CHAIN",
440
+ "chain[0].previous_digest",
441
+ "the first version has no predecessor",
442
+ )
443
+ continue
444
+ expected = chain[index - 1].facts_digest
445
+ if entry.previous_digest != expected:
446
+ raise SourceContractError(
447
+ "CATALOG_VERSION_CHAIN",
448
+ f"chain[{index}].previous_digest",
449
+ "does not match the canonical facts digest of the preceding version",
450
+ )
451
+ return chain[-1].coordinate
452
+
453
+
454
+ def catalog_entry_from_dict(data: Mapping[str, Any]) -> CatalogEntry:
455
+ """Rebuild an entry from its exact ``to_dict()`` shape, refusing anything else.
456
+
457
+ Reconstruction is strict on both sides: a missing key and an unknown key are equally a typed
458
+ failure. A parser that silently drops what it does not recognise turns a tampered artifact
459
+ into a valid-looking one, which is the failure mode the digest chain exists to prevent.
460
+ """
461
+
462
+ payload = _mapping(data, "catalog_entry")
463
+ _exact(
464
+ payload,
465
+ {
466
+ "schema_version",
467
+ "entry_id",
468
+ "entry_version",
469
+ "publisher",
470
+ "title",
471
+ "description",
472
+ "spatial_scope",
473
+ "data_formats",
474
+ "access_kind",
475
+ "authentication_required",
476
+ "observation",
477
+ "rights",
478
+ "quirks",
479
+ "declared_columns",
480
+ "declared_vocabulary",
481
+ "declared_row_count",
482
+ "evidence",
483
+ "vectors",
484
+ "previous_digest",
485
+ },
486
+ "catalog_entry",
487
+ )
488
+ return CatalogEntry(
489
+ schema_version=payload["schema_version"],
490
+ entry_id=payload["entry_id"],
491
+ entry_version=payload["entry_version"],
492
+ publisher=payload["publisher"],
493
+ title=payload["title"],
494
+ description=payload["description"],
495
+ spatial_scope=tuple(_sequence(payload["spatial_scope"], "catalog_entry.spatial_scope")),
496
+ data_formats=tuple(_sequence(payload["data_formats"], "catalog_entry.data_formats")),
497
+ access_kind=payload["access_kind"],
498
+ authentication_required=payload["authentication_required"],
499
+ observation=_observation_from_dict(payload["observation"]),
500
+ rights=_rights_from_dict(payload["rights"]),
501
+ quirks=tuple(
502
+ _quirk_from_dict(item) for item in _sequence(payload["quirks"], "catalog_entry.quirks")
503
+ ),
504
+ declared_columns=tuple(
505
+ _sequence(payload["declared_columns"], "catalog_entry.declared_columns")
506
+ ),
507
+ declared_vocabulary=tuple(
508
+ _sequence(payload["declared_vocabulary"], "catalog_entry.declared_vocabulary")
509
+ ),
510
+ declared_row_count=payload["declared_row_count"],
511
+ evidence=tuple(
512
+ _evidence_from_dict(item, f"catalog_entry.evidence[{index}]")
513
+ for index, item in enumerate(_sequence(payload["evidence"], "catalog_entry.evidence"))
514
+ ),
515
+ vectors=tuple(
516
+ _vector_from_dict(item)
517
+ for item in _sequence(payload["vectors"], "catalog_entry.vectors")
518
+ ),
519
+ previous_digest=payload["previous_digest"],
520
+ )
521
+
522
+
523
+ def _quirk_from_dict(data: Any) -> SourceQuirk:
524
+ payload = _mapping(data, "catalog_entry.quirks[]")
525
+ _exact(payload, {"code", "detail"}, "catalog_entry.quirks[]")
526
+ return SourceQuirk(code=payload["code"], detail=payload["detail"])
527
+
528
+
529
+ def _vector_from_dict(data: Any) -> LayerVectorReference:
530
+ payload = _mapping(data, "catalog_entry.vectors[]")
531
+ _exact(
532
+ payload,
533
+ {
534
+ "layer",
535
+ "backend_id",
536
+ "backend_version",
537
+ "dimensions",
538
+ "quantization",
539
+ "vector_sha256",
540
+ "entry_facts_digest",
541
+ },
542
+ "catalog_entry.vectors[]",
543
+ )
544
+ return LayerVectorReference(
545
+ layer=payload["layer"],
546
+ backend_id=payload["backend_id"],
547
+ backend_version=payload["backend_version"],
548
+ dimensions=payload["dimensions"],
549
+ quantization=payload["quantization"],
550
+ vector_sha256=payload["vector_sha256"],
551
+ entry_facts_digest=payload["entry_facts_digest"],
552
+ )
553
+
554
+
555
+ def _evidence_from_dict(data: Any, path: str) -> EvidenceReference:
556
+ payload = _mapping(data, path)
557
+ _exact(payload, {"uri", "observed_at", "content_sha256", "media_type"}, path)
558
+ return EvidenceReference(
559
+ uri=payload["uri"],
560
+ observed_at=payload["observed_at"],
561
+ content_sha256=payload["content_sha256"],
562
+ media_type=payload["media_type"],
563
+ )
564
+
565
+
566
+ def _obligation_from_dict(data: Any) -> LicenseObligation:
567
+ path = "catalog_entry.rights.obligations[]"
568
+ payload = _mapping(data, path)
569
+ _exact(
570
+ payload,
571
+ {
572
+ "obligation_id",
573
+ "applies_at",
574
+ "permitted_uses",
575
+ "prohibited_uses",
576
+ "required_artifact_digests",
577
+ "escalation_when_ambiguous",
578
+ },
579
+ path,
580
+ )
581
+ return LicenseObligation(
582
+ obligation_id=payload["obligation_id"],
583
+ applies_at=tuple(_sequence(payload["applies_at"], f"{path}.applies_at")),
584
+ permitted_uses=tuple(_sequence(payload["permitted_uses"], f"{path}.permitted_uses")),
585
+ prohibited_uses=tuple(_sequence(payload["prohibited_uses"], f"{path}.prohibited_uses")),
586
+ required_artifact_digests=tuple(
587
+ _sequence(payload["required_artifact_digests"], f"{path}.required_artifact_digests")
588
+ ),
589
+ escalation_when_ambiguous=payload["escalation_when_ambiguous"],
590
+ )
591
+
592
+
593
+ def rights_evidence_from_dict(data: Any, *, path: str = "catalog_entry.rights") -> RightsEvidence:
594
+ """Rebuild one authored rights claim from its exact ``to_dict()`` shape.
595
+
596
+ The public spelling of the parser ``catalog_entry_from_dict`` already uses for the
597
+ nested ``rights`` field, exported so a rights decision document can carry the same
598
+ shape without a second parser drifting from this one. ``path`` names the document the
599
+ claim actually sits in, so a refusal sends the person to the right file.
600
+ """
601
+
602
+ return _rights_from_dict(data, path=path)
603
+
604
+
605
+ def _rights_from_dict(data: Any, *, path: str = "catalog_entry.rights") -> RightsEvidence:
606
+ payload = _mapping(data, path)
607
+ _exact(
608
+ payload,
609
+ {
610
+ "status",
611
+ "lawful_basis",
612
+ "allowed_uses",
613
+ "evidence",
614
+ "obligations",
615
+ "human_decision_reference",
616
+ },
617
+ path,
618
+ )
619
+ return RightsEvidence(
620
+ status=payload["status"],
621
+ lawful_basis=payload["lawful_basis"],
622
+ allowed_uses=tuple(_sequence(payload["allowed_uses"], f"{path}.allowed_uses")),
623
+ evidence=tuple(
624
+ _evidence_from_dict(item, f"{path}.evidence[{index}]")
625
+ for index, item in enumerate(_sequence(payload["evidence"], f"{path}.evidence"))
626
+ ),
627
+ obligations=tuple(
628
+ _obligation_from_dict(item)
629
+ for item in _sequence(payload["obligations"], f"{path}.obligations")
630
+ ),
631
+ human_decision_reference=payload["human_decision_reference"],
632
+ )
633
+
634
+
635
+ def _observation_from_dict(data: Any) -> SourceObservation:
636
+ path = "catalog_entry.observation"
637
+ payload = _mapping(data, path)
638
+ _exact(
639
+ payload,
640
+ {
641
+ "schema_version",
642
+ "source_id",
643
+ "observed_at",
644
+ "event_time_field",
645
+ "available_at_field",
646
+ "historical_start",
647
+ "historical_end",
648
+ "live_status",
649
+ "publication_delay_seconds",
650
+ "update_frequency_seconds",
651
+ "response_status",
652
+ "evidence",
653
+ },
654
+ path,
655
+ )
656
+ return SourceObservation(
657
+ schema_version=payload["schema_version"],
658
+ source_id=payload["source_id"],
659
+ observed_at=payload["observed_at"],
660
+ event_time_field=payload["event_time_field"],
661
+ available_at_field=payload["available_at_field"],
662
+ historical_start=payload["historical_start"],
663
+ historical_end=payload["historical_end"],
664
+ live_status=payload["live_status"],
665
+ publication_delay_seconds=payload["publication_delay_seconds"],
666
+ update_frequency_seconds=payload["update_frequency_seconds"],
667
+ response_status=payload["response_status"],
668
+ evidence=tuple(
669
+ _evidence_from_dict(item, f"{path}.evidence[{index}]")
670
+ for index, item in enumerate(_sequence(payload["evidence"], f"{path}.evidence"))
671
+ ),
672
+ )
673
+
674
+
675
+ def _mapping(value: Any, path: str) -> Mapping[str, Any]:
676
+ if not isinstance(value, Mapping):
677
+ raise SourceContractError("TYPE", path, "must be an object")
678
+ for key in value:
679
+ if not isinstance(key, str):
680
+ raise SourceContractError("TYPE", path, "object keys must be strings")
681
+ return value
682
+
683
+
684
+ def _sequence(value: Any, path: str) -> list[Any]:
685
+ if not isinstance(value, list):
686
+ raise SourceContractError("TYPE", path, "must be an array")
687
+ return value
688
+
689
+
690
+ def _exact(data: Mapping[str, Any], expected: set[str], path: str) -> None:
691
+ actual = set(data)
692
+ if actual != expected:
693
+ raise SourceContractError(
694
+ "FIELDS",
695
+ path,
696
+ f"must contain exactly {sorted(expected)}; missing={sorted(expected - actual)}, "
697
+ f"extra={sorted(actual - expected)}",
698
+ )
699
+
700
+
701
+ def _text(value: Any, path: str, *, maximum: int) -> str:
702
+ if not isinstance(value, str) or not value or len(value) > maximum:
703
+ raise SourceContractError(
704
+ "TEXT",
705
+ path,
706
+ f"must be a non-empty string no longer than {maximum} characters",
707
+ )
708
+ for index, char in enumerate(value):
709
+ if 0xD800 <= ord(char) <= 0xDFFF:
710
+ raise SourceContractError(
711
+ "UNICODE",
712
+ path,
713
+ f"contains an unpaired surrogate at character {index}",
714
+ )
715
+ return value
716
+
717
+
718
+ def _texts(
719
+ values: Any,
720
+ path: str,
721
+ *,
722
+ nonempty: bool,
723
+ maximum: int,
724
+ item_maximum: int,
725
+ ) -> None:
726
+ if not isinstance(values, tuple):
727
+ raise SourceContractError("TYPE", path, "must be an immutable tuple")
728
+ if nonempty and not values:
729
+ raise SourceContractError("EMPTY", path, "must not be empty")
730
+ if len(values) > maximum:
731
+ raise SourceContractError("LIMIT", path, f"must contain at most {maximum} values")
732
+ for index, value in enumerate(values):
733
+ _text(value, f"{path}[{index}]", maximum=item_maximum)
734
+ if len(set(values)) != len(values):
735
+ raise SourceContractError("DUPLICATE", path, "values must be unique")
736
+
737
+
738
+ def _identifier(value: Any, path: str) -> str:
739
+ text = _text(value, path, maximum=MAX_DECLARED_NAME)
740
+ if not _IDENTIFIER.fullmatch(text):
741
+ raise SourceContractError(
742
+ "IDENTIFIER",
743
+ path,
744
+ "must be a lowercase dotted/dashed/underscored identifier",
745
+ )
746
+ return text
747
+
748
+
749
+ def _semver(value: Any, path: str) -> str:
750
+ text = _text(value, path, maximum=64)
751
+ if not _SEMVER.fullmatch(text):
752
+ raise SourceContractError("SEMVER", path, "must be a positive-major semantic version")
753
+ return text
754
+
755
+
756
+ def _digest(value: Any, path: str) -> str:
757
+ text = _text(value, path, maximum=64)
758
+ if not _SHA256.fullmatch(text):
759
+ raise SourceContractError("SHA256", path, "must be a lowercase 64-character SHA-256")
760
+ return text
761
+
762
+
763
+ def _choice(value: Any, allowed: frozenset[str], path: str) -> str:
764
+ text = _text(value, path, maximum=MAX_DECLARED_NAME)
765
+ if text not in allowed:
766
+ raise SourceContractError("ENUM", path, f"must be one of {sorted(allowed)}")
767
+ return text
768
+
769
+
770
+ def _choices(
771
+ values: Any,
772
+ allowed: frozenset[str],
773
+ path: str,
774
+ *,
775
+ nonempty: bool,
776
+ maximum: int,
777
+ ) -> None:
778
+ if not isinstance(values, tuple):
779
+ raise SourceContractError("TYPE", path, "must be an immutable tuple")
780
+ if nonempty and not values:
781
+ raise SourceContractError("EMPTY", path, "must not be empty")
782
+ if len(values) > maximum:
783
+ raise SourceContractError("LIMIT", path, f"must contain at most {maximum} values")
784
+ for index, value in enumerate(values):
785
+ _choice(value, allowed, f"{path}[{index}]")
786
+ if len(set(values)) != len(values):
787
+ raise SourceContractError("DUPLICATE", path, "values must be unique")
788
+
789
+
790
+ def _typed_tuple(values: Any, expected_type: type[Any], path: str, *, maximum: int) -> None:
791
+ if not isinstance(values, tuple):
792
+ raise SourceContractError("TYPE", path, "must be an immutable tuple")
793
+ if len(values) > maximum:
794
+ raise SourceContractError("LIMIT", path, f"must contain at most {maximum} values")
795
+ for index, value in enumerate(values):
796
+ if not isinstance(value, expected_type):
797
+ raise SourceContractError(
798
+ "TYPE",
799
+ f"{path}[{index}]",
800
+ f"must be a {expected_type.__name__}",
801
+ )
802
+
803
+
804
+ def _integer(value: Any, path: str, *, minimum: int, maximum: int) -> int:
805
+ # ``type(value) is not int`` rather than ``isinstance``: it refuses both bool and float, which
806
+ # is what keeps a float out of the digest before ``canonical_json_bytes`` ever sees it.
807
+ if type(value) is not int or not minimum <= value <= maximum:
808
+ raise SourceContractError(
809
+ "INTEGER",
810
+ path,
811
+ f"must be an integer in [{minimum}, {maximum}]",
812
+ )
813
+ if abs(value) > MAX_SAFE_INTEGER:
814
+ raise SourceContractError(
815
+ "SAFE_INTEGER",
816
+ path,
817
+ "integer exceeds the exact Python/JavaScript JSON domain",
818
+ )
819
+ return value
820
+
821
+
822
+ def _boolean(value: Any, path: str) -> bool:
823
+ if type(value) is not bool:
824
+ raise SourceContractError("BOOLEAN", path, "must be a boolean")
825
+ return value