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,7 @@
1
+ """Harvesters that read open catalogue endpoints into structured public-source facts.
2
+
3
+ This package carries no re-exports. Consumers import the concrete module -- ``harvest.protocol``
4
+ for the transport, record shape and registry, ``harvest.ckan`` and the rest for one endpoint
5
+ family each. Keeping ``__init__`` empty means adding a harvester never contends with an existing
6
+ file, and it stops the transport and a parser from sharing a namespace.
7
+ """
@@ -0,0 +1,384 @@
1
+ """The CKAN harvester: read one ``package_search`` response into public-source records.
2
+
3
+ CKAN is the most common open data-catalogue software, and its API is close to uniform across
4
+ instances -- but only close. The base path is per-instance configuration rather than a constant:
5
+ ``data.gov.uk`` serves ``/api/action/`` and 301s on ``/api/3/action/``, while ``open.canada.ca``
6
+ and ``data.humdata.org`` serve ``/api/3/action/``. This module therefore never constructs an
7
+ endpoint URL; the caller supplies one and the transport validates it.
8
+
9
+ Three field-level judgements are worth stating because all three are deliberate losses of
10
+ information.
11
+
12
+ **An unrecognised payload format is dropped, not carried.** ``DATA_FORMATS`` is a closed
13
+ vocabulary of things the harness can actually read. An entry claiming a format the harness cannot
14
+ open is worse than one claiming none, because the claim is what a later reader would trust.
15
+
16
+ **An unrecognised licence maps to unclear.** Unclear escalates to a human; it never admits. The
17
+ alternative -- guessing that an unfamiliar licence id probably means attribution -- would put the
18
+ harness in the position of having granted permission nobody granted.
19
+
20
+ **An out-of-bounds field degrades that field or that record, never the response.** CKAN's ``notes``
21
+ has no server-side length limit, and a multi-thousand-character description is ordinary on
22
+ data.gov.uk and open.canada.ca. Passing one to ``HarvestedRecord`` raised, the raise escaped
23
+ ``parse``, and one publisher's prose refused every other package on the page -- so free text is cut
24
+ to the record bound and an unusable id, landing page or licence id degrades to ``skipped`` or to
25
+ ``None``. The rule and the reasoning live in ``protocol.bounded_record_text`` and
26
+ ``protocol.usable_record_id``, next to the timestamp case that first stated it.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from typing import Any
32
+ from urllib.parse import parse_qs, urlsplit
33
+
34
+ from mostlyright.data_harness.canonical import CanonicalJSONError, parse_foreign_json
35
+ from mostlyright.data_harness.formats import FORMAT_MEDIA_TYPES
36
+ from mostlyright.data_harness.sources.catalog.harvest.protocol import (
37
+ MAX_RECORD_PUBLISHER,
38
+ MAX_RECORD_TEXT,
39
+ MAX_RECORD_TITLE,
40
+ CatalogHarvestError,
41
+ HarvestCursor,
42
+ HarvestedRecord,
43
+ HarvesterDescriptor,
44
+ HarvestPage,
45
+ HarvestResponseEvidence,
46
+ bounded_record_text,
47
+ carries_a_zone_offset,
48
+ declared_timestamp,
49
+ harvest_limits,
50
+ usable_https_uri,
51
+ usable_license_id,
52
+ usable_record_id,
53
+ )
54
+ from mostlyright.data_harness.sources.contracts import DATA_FORMATS, EvidenceReference
55
+
56
+ CKAN_LIMITS = harvest_limits("application/json", max_response_bytes=2 * 1024 * 1024)
57
+
58
+ MAX_HARVEST_RECORDS = 1_000
59
+
60
+ # Publisher spellings a CKAN instance uses that the format table does not carry. Each is a
61
+ # non-canonical synonym rather than a format's real media type: ``application/csv`` and
62
+ # ``application/json-lines`` are vendor spellings no registry lists, and ``jsonl`` is a bare
63
+ # suffix published in the ``format`` field. They belong here because they are facts about how
64
+ # publishers write, not about what the harness can read.
65
+ _PUBLISHER_FORMAT_SYNONYMS = {
66
+ "application/csv": "csv",
67
+ "application/json-lines": "ndjson",
68
+ "jsonl": "ndjson",
69
+ }
70
+
71
+ # Every canonical media type is derived from ``FORMAT_MEDIA_TYPES`` rather than re-spelled. A
72
+ # format's media types are the format table's to declare; restating them here is a second
73
+ # declaration of the table, and one that drifts silently the moment a Reader family lands.
74
+ _FORMAT_ALIASES: dict[str, str] = {
75
+ **{
76
+ media_type: name
77
+ for name, media_types in FORMAT_MEDIA_TYPES.items()
78
+ for media_type in media_types
79
+ },
80
+ **_PUBLISHER_FORMAT_SYNONYMS,
81
+ }
82
+
83
+ # One licence id, one rights disposition. `conditional` requires at least one obligation by
84
+ # contract, which is exactly right for an attribution licence: the obligation is the attribution.
85
+ ATTRIBUTION = "attribution"
86
+ CKAN_LICENCE_RIGHTS: dict[str, tuple[str, tuple[str, ...], str | None]] = {
87
+ "cc-zero": ("approved", ("analysis", "publication"), None),
88
+ "cc0-1.0": ("approved", ("analysis", "publication"), None),
89
+ "odc-pddl": ("approved", ("analysis", "publication"), None),
90
+ "cc-by": ("conditional", ("analysis", "publication"), ATTRIBUTION),
91
+ "cc-by-4.0": ("conditional", ("analysis", "publication"), ATTRIBUTION),
92
+ "cc-by-sa": ("conditional", ("analysis", "publication"), ATTRIBUTION),
93
+ "uk-ogl": ("conditional", ("analysis", "publication"), ATTRIBUTION),
94
+ "odc-by": ("conditional", ("analysis", "publication"), ATTRIBUTION),
95
+ }
96
+
97
+ UNCLEAR_LICENCE_IDS = frozenset({"", "notspecified", "other-open", "other-closed", "unknown"})
98
+
99
+
100
+ def ckan_licence_rights(license_id: str | None) -> tuple[str, tuple[str, ...], str | None]:
101
+ """Map a CKAN licence id onto a rights status, allowed uses and an obligation kind.
102
+
103
+ An absent, empty, ``notspecified`` or unrecognised licence maps to ``unclear``. That is the
104
+ status the facts gate escalates rather than guesses at.
105
+ """
106
+
107
+ if license_id is None:
108
+ return ("unclear", (), None)
109
+ normalized = license_id.strip().lower()
110
+ if normalized in UNCLEAR_LICENCE_IDS:
111
+ return ("unclear", (), None)
112
+ return CKAN_LICENCE_RIGHTS.get(normalized, ("unclear", (), None))
113
+
114
+
115
+ def normalize_data_format(value: str) -> str | None:
116
+ """Return the closed-vocabulary format this publisher spelling means, or ``None``."""
117
+
118
+ if not isinstance(value, str):
119
+ return None
120
+ candidate = value.strip().lower().lstrip(".")
121
+ candidate = _FORMAT_ALIASES.get(candidate, candidate)
122
+ return candidate if candidate in DATA_FORMATS else None
123
+
124
+
125
+ class CkanHarvester:
126
+ """Read a CKAN ``package_search`` response. Takes bytes; never fetches."""
127
+
128
+ _DESCRIPTOR = HarvesterDescriptor(
129
+ protocol="ckan",
130
+ harvester_id="ckan.package_search",
131
+ harvester_version="1.0.0",
132
+ )
133
+
134
+ @property
135
+ def descriptor(self) -> HarvesterDescriptor:
136
+ return self._DESCRIPTOR
137
+
138
+ def parse(
139
+ self,
140
+ payload: bytes,
141
+ *,
142
+ uri: str,
143
+ observed_at: str,
144
+ evidence: EvidenceReference,
145
+ ) -> tuple[HarvestedRecord, ...]:
146
+ page = self.parse_page(
147
+ payload,
148
+ uri=uri,
149
+ observed_at=observed_at,
150
+ evidence=evidence,
151
+ response_evidence=None,
152
+ cursor=None,
153
+ _require_provider_count=False,
154
+ )
155
+ if not page.records:
156
+ raise CatalogHarvestError(
157
+ "HARVEST_SHAPE",
158
+ "ckan.result.results",
159
+ "no package carried a usable public landing uri; "
160
+ f"skipped={list(page.skipped_record_ids)}",
161
+ )
162
+ return page.records
163
+
164
+ def parse_page(
165
+ self,
166
+ payload: bytes,
167
+ *,
168
+ uri: str,
169
+ observed_at: str,
170
+ evidence: EvidenceReference,
171
+ response_evidence: HarvestResponseEvidence | None,
172
+ cursor: HarvestCursor | None,
173
+ _require_provider_count: bool = True,
174
+ ) -> HarvestPage:
175
+ document = _document(payload)
176
+ if document.get("success") is not True:
177
+ raise CatalogHarvestError(
178
+ "HARVEST_UNSUCCESSFUL",
179
+ "ckan.success",
180
+ "the CKAN instance reported an unsuccessful response",
181
+ )
182
+ result = document.get("result")
183
+ if not isinstance(result, dict) or not isinstance(result.get("results"), list):
184
+ raise CatalogHarvestError(
185
+ "HARVEST_SHAPE",
186
+ "ckan.result.results",
187
+ "a CKAN package_search response must carry result.results",
188
+ )
189
+ packages = result["results"]
190
+ provider_count = result.get("count")
191
+ if provider_count is None and not _require_provider_count:
192
+ provider_count = len(packages)
193
+ if type(provider_count) is not int or provider_count < 0:
194
+ raise CatalogHarvestError(
195
+ "HARVEST_COUNT",
196
+ "ckan.result.count",
197
+ "a paged CKAN response must carry a nonnegative integer count",
198
+ )
199
+ if len(packages) > MAX_HARVEST_RECORDS:
200
+ raise CatalogHarvestError(
201
+ "HARVEST_RECORD_LIMIT",
202
+ "ckan.result.results",
203
+ f"a single response may not carry more than {MAX_HARVEST_RECORDS} packages",
204
+ )
205
+ skipped: list[str] = []
206
+ records: list[HarvestedRecord] = []
207
+ for position, package in enumerate(packages):
208
+ if not isinstance(package, dict):
209
+ raise CatalogHarvestError(
210
+ "HARVEST_SHAPE",
211
+ f"ckan.result.results[{position}]",
212
+ "each package must be an object",
213
+ )
214
+ record = _record(package, observed_at=observed_at, evidence=evidence, skipped=skipped)
215
+ if record is not None:
216
+ records.append(record)
217
+ start, rows = _page_coordinate(uri, cursor, len(packages))
218
+ next_start = start + len(packages)
219
+ next_cursor = (
220
+ HarvestCursor(protocol="ckan", kind="offset", value=f"{next_start}:{rows}")
221
+ if packages and next_start < provider_count
222
+ else None
223
+ )
224
+ if not packages and start < provider_count:
225
+ raise CatalogHarvestError(
226
+ "HARVEST_EMPTY_PAGE",
227
+ "ckan.result.results",
228
+ "the provider count says records remain but the page is empty",
229
+ )
230
+ return HarvestPage(
231
+ protocol="ckan",
232
+ records=tuple(records),
233
+ next_cursor=next_cursor,
234
+ provider_count=provider_count,
235
+ count_basis="reported_total",
236
+ skipped_record_ids=tuple(sorted(skipped)),
237
+ evidence=evidence,
238
+ response_evidence=response_evidence,
239
+ )
240
+
241
+
242
+ def _page_coordinate(
243
+ uri: str, cursor: HarvestCursor | None, returned_records: int
244
+ ) -> tuple[int, int]:
245
+ if cursor is not None:
246
+ if cursor.protocol != "ckan" or cursor.kind != "offset":
247
+ raise CatalogHarvestError("HARVEST_CURSOR", "ckan.cursor", "cursor mismatch")
248
+ start_text, rows_text = cursor.value.split(":", 1)
249
+ start, rows = int(start_text), int(rows_text)
250
+ else:
251
+ query = parse_qs(urlsplit(uri).query, keep_blank_values=True)
252
+ try:
253
+ start = int(query.get("start", ["0"])[0])
254
+ rows = int(query.get("rows", [str(max(1, returned_records))])[0])
255
+ except (TypeError, ValueError):
256
+ raise CatalogHarvestError(
257
+ "HARVEST_CURSOR", "ckan.uri", "start and rows must be canonical integers"
258
+ ) from None
259
+ if start < 0 or not 1 <= rows <= MAX_HARVEST_RECORDS:
260
+ raise CatalogHarvestError(
261
+ "HARVEST_CURSOR", "ckan.cursor", "start/rows are outside the page bounds"
262
+ )
263
+ if returned_records > rows:
264
+ raise CatalogHarvestError(
265
+ "HARVEST_CURSOR", "ckan.result.results", "response exceeds its requested rows"
266
+ )
267
+ return start, rows
268
+
269
+
270
+ def _document(payload: bytes) -> dict[str, Any]:
271
+ # `parse_foreign_json` is the repository's one parsing authority in its third-party mode. It
272
+ # refuses duplicate keys, byte-order marks, non-strict UTF-8, NaN/Infinity and out-of-range
273
+ # integers exactly as the canonical parser does, and differs only in admitting finite fractional
274
+ # numbers -- which a publisher's document legitimately carries (a resource size, a search score,
275
+ # a coordinate) and which the strict parser would refuse, taking the whole response with it. No
276
+ # float reaches a `HarvestedRecord`: every field below is read through `_string` or the closed
277
+ # format vocabulary, and `canonical_json_bytes` still refuses a float, so nothing read here can
278
+ # be digested. Never `json.loads` here.
279
+ try:
280
+ document = parse_foreign_json(payload)
281
+ except CanonicalJSONError as error:
282
+ raise CatalogHarvestError(
283
+ "HARVEST_SHAPE",
284
+ "ckan.response",
285
+ f"the response is not strict JSON: {error.detail}",
286
+ ) from None
287
+ if not isinstance(document, dict):
288
+ raise CatalogHarvestError(
289
+ "HARVEST_SHAPE",
290
+ "ckan.response",
291
+ "a CKAN response must be a JSON object",
292
+ )
293
+ return document
294
+
295
+
296
+ def _record(
297
+ package: dict[str, Any],
298
+ *,
299
+ observed_at: str,
300
+ evidence: EvidenceReference,
301
+ skipped: list[str],
302
+ ) -> HarvestedRecord | None:
303
+ # Every field is absent-by-default. A CKAN instance may omit any of them, and a KeyError
304
+ # escaping this parser would be a crash where a typed refusal belongs. Every field is also
305
+ # bounded-by-default: an id or a uri that is not one degrades this record, and free text is cut
306
+ # to the record bound. Neither may reach `HarvestedRecord` unchecked, because a raise there
307
+ # escapes `parse` and takes every sibling package in the response with it.
308
+ record_id = usable_record_id(package.get("id")) or usable_record_id(package.get("name"))
309
+ if record_id is None:
310
+ skipped.append("<package without a usable id>")
311
+ return None
312
+ landing = usable_https_uri(package.get("url"))
313
+ if landing is None:
314
+ # Recorded, never silently included: a non-HTTPS landing page is not a public citation.
315
+ skipped.append(record_id)
316
+ return None
317
+ formats: list[str] = []
318
+ resources = package.get("resources")
319
+ if isinstance(resources, list):
320
+ for resource in resources:
321
+ if not isinstance(resource, dict):
322
+ continue
323
+ normalized = normalize_data_format(_string(resource.get("format")) or "")
324
+ if normalized is not None and normalized not in formats:
325
+ formats.append(normalized)
326
+ organization = package.get("organization")
327
+ publisher = None
328
+ if isinstance(organization, dict):
329
+ publisher = _string(organization.get("title")) or _string(organization.get("name"))
330
+ title = _string(package.get("title"))
331
+ description = _string(package.get("notes"))
332
+ return HarvestedRecord(
333
+ protocol="ckan",
334
+ record_id=record_id,
335
+ title=bounded_record_text(title, maximum=MAX_RECORD_TITLE) if title else record_id,
336
+ description=(
337
+ bounded_record_text(description, maximum=MAX_RECORD_TEXT)
338
+ if description
339
+ else "No description published."
340
+ ),
341
+ publisher=(
342
+ bounded_record_text(publisher, maximum=MAX_RECORD_PUBLISHER)
343
+ if publisher
344
+ else "Unnamed publisher"
345
+ ),
346
+ landing_uri=landing,
347
+ data_formats=tuple(sorted(formats)),
348
+ evidence=evidence,
349
+ license_id=usable_license_id(package.get("license_id")),
350
+ license_uri=usable_https_uri(package.get("license_url")),
351
+ declared_updated_at=_ckan_timestamp(_string(package.get("metadata_modified"))),
352
+ )
353
+
354
+
355
+ def _string(value: Any) -> str | None:
356
+ if not isinstance(value, str):
357
+ return None
358
+ text = value.strip()
359
+ return text or None
360
+
361
+
362
+ def _ckan_timestamp(value: str | None) -> str | None:
363
+ """CKAN emits ``2025-12-31T11:33:28.123456`` with no zone. Read it as UTC or not at all.
364
+
365
+ A CKAN instance is free to put anything in ``metadata_modified``, so the normalised result goes
366
+ through ``declared_timestamp`` and becomes an undeclared update time when it is not a canonical
367
+ UTC timestamp. Without that, one package's bad field refused every other package in the same
368
+ response -- see that function.
369
+
370
+ The offset check runs before the fractional second is cut, because the offset rides on the
371
+ fraction and was being cut away with it -- see ``carries_a_zone_offset``.
372
+ """
373
+
374
+ if value is None:
375
+ return None
376
+ if carries_a_zone_offset(value):
377
+ return None
378
+ text = value.rstrip("Z")
379
+ if "." in text:
380
+ head, _, fraction = text.partition(".")
381
+ text = f"{head}.{fraction[:6]}" if fraction[:6].isdigit() else head
382
+ if len(text) < 19:
383
+ return None
384
+ return declared_timestamp(f"{text}Z")