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,574 @@
1
+ """Stable equality and backward as-of left joins with exact audit evidence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from dataclasses import dataclass, replace
7
+ from datetime import datetime
8
+ from typing import Any
9
+
10
+ from mostlyright.data_harness.preparation.contracts import (
11
+ JOIN_AUDIT_VERSION,
12
+ MAX_ROWS,
13
+ AsOfJoinSpec,
14
+ ColumnLineage,
15
+ EqualityJoinSpec,
16
+ FieldSpec,
17
+ parse_utc,
18
+ )
19
+ from mostlyright.data_harness.preparation.errors import PreparationError
20
+ from mostlyright.data_harness.preparation.table import TableState, validate_columns
21
+
22
+ MAX_ASOF_COMPARISONS = 10_000_000
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class SelectedRevision:
27
+ left_ordinal: int
28
+ right_ordinal: int
29
+ event_time: str
30
+ available_at: str
31
+ revision_time: str
32
+
33
+ def __post_init__(self) -> None:
34
+ if type(self.left_ordinal) is not int or self.left_ordinal < 0:
35
+ raise PreparationError(
36
+ "JOIN_AUDIT_REVISION",
37
+ "selected_revision.left_ordinal",
38
+ "must be a nonnegative integer",
39
+ )
40
+ if type(self.right_ordinal) is not int or self.right_ordinal < 0:
41
+ raise PreparationError(
42
+ "JOIN_AUDIT_REVISION",
43
+ "selected_revision.right_ordinal",
44
+ "must be a nonnegative integer",
45
+ )
46
+ parse_utc(self.event_time, "selected_revision.event_time")
47
+ parse_utc(self.available_at, "selected_revision.available_at")
48
+ parse_utc(self.revision_time, "selected_revision.revision_time")
49
+
50
+ def to_dict(self) -> dict[str, Any]:
51
+ return {
52
+ "left_ordinal": self.left_ordinal,
53
+ "right_ordinal": self.right_ordinal,
54
+ "event_time": self.event_time,
55
+ "available_at": self.available_at,
56
+ "revision_time": self.revision_time,
57
+ }
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class JoinAudit:
62
+ schema_version: str
63
+ join_id: str
64
+ kind: str
65
+ cardinality: str
66
+ input_left_rows: int
67
+ input_right_rows: int
68
+ output_rows: int
69
+ matched_left_rows: int
70
+ unmatched_left_rows: int
71
+ matched_pairs: int
72
+ left_duplicate_keys: int
73
+ right_duplicate_keys: int
74
+ row_multiplier_numerator: int
75
+ row_multiplier_denominator: int
76
+ future_availability_rejections: int
77
+ future_revision_rejections: int
78
+ tolerance_rejections: int
79
+ lag_seconds_minimum: int | None
80
+ lag_seconds_maximum: int | None
81
+ selected_revisions: tuple[SelectedRevision, ...]
82
+
83
+ def __post_init__(self) -> None:
84
+ if self.schema_version != JOIN_AUDIT_VERSION:
85
+ raise PreparationError("JOIN_AUDIT_VERSION", "join_audit.schema_version", "unsupported")
86
+ count_fields = (
87
+ "input_left_rows",
88
+ "input_right_rows",
89
+ "output_rows",
90
+ "matched_left_rows",
91
+ "unmatched_left_rows",
92
+ "matched_pairs",
93
+ "left_duplicate_keys",
94
+ "right_duplicate_keys",
95
+ "row_multiplier_numerator",
96
+ "row_multiplier_denominator",
97
+ "future_availability_rejections",
98
+ "future_revision_rejections",
99
+ "tolerance_rejections",
100
+ )
101
+ if any(
102
+ type(getattr(self, name)) is not int or getattr(self, name) < 0 for name in count_fields
103
+ ):
104
+ raise PreparationError(
105
+ "JOIN_AUDIT_COUNT",
106
+ "join_audit",
107
+ "contains a negative or non-integer count",
108
+ )
109
+ if self.matched_left_rows + self.unmatched_left_rows != self.input_left_rows:
110
+ raise PreparationError(
111
+ "JOIN_AUDIT_COUNT",
112
+ "join_audit",
113
+ "matched and unmatched counts do not reconcile",
114
+ )
115
+ if self.row_multiplier_numerator != self.output_rows:
116
+ raise PreparationError(
117
+ "JOIN_AUDIT_COUNT",
118
+ "join_audit.row_multiplier_numerator",
119
+ "must equal output rows",
120
+ )
121
+ if self.row_multiplier_denominator != self.input_left_rows:
122
+ raise PreparationError(
123
+ "JOIN_AUDIT_COUNT",
124
+ "join_audit.row_multiplier_denominator",
125
+ "must equal input left rows",
126
+ )
127
+ if self.kind == "asof_left":
128
+ if self.matched_pairs != self.matched_left_rows:
129
+ raise PreparationError(
130
+ "JOIN_AUDIT_COUNT",
131
+ "join_audit.matched_pairs",
132
+ "as-of joins select at most one right row per matched left row",
133
+ )
134
+ if len(self.selected_revisions) != self.matched_left_rows:
135
+ raise PreparationError(
136
+ "JOIN_AUDIT_COUNT",
137
+ "join_audit.selected_revisions",
138
+ "does not reconcile matched left rows",
139
+ )
140
+ elif self.kind != "equality_left":
141
+ raise PreparationError("JOIN_AUDIT_KIND", "join_audit.kind", "is unsupported")
142
+ if (self.lag_seconds_minimum is None) != (self.lag_seconds_maximum is None):
143
+ raise PreparationError(
144
+ "JOIN_AUDIT_LAG",
145
+ "join_audit",
146
+ "lag bounds must be paired",
147
+ )
148
+ if (
149
+ self.lag_seconds_minimum is not None
150
+ and self.lag_seconds_maximum is not None
151
+ and (
152
+ self.lag_seconds_minimum < 0 or self.lag_seconds_maximum < self.lag_seconds_minimum
153
+ )
154
+ ):
155
+ raise PreparationError("JOIN_AUDIT_LAG", "join_audit", "lag bounds are invalid")
156
+
157
+ def to_dict(self) -> dict[str, Any]:
158
+ return {
159
+ "schema_version": self.schema_version,
160
+ "join_id": self.join_id,
161
+ "kind": self.kind,
162
+ "cardinality": self.cardinality,
163
+ "input_left_rows": self.input_left_rows,
164
+ "input_right_rows": self.input_right_rows,
165
+ "output_rows": self.output_rows,
166
+ "matched_left_rows": self.matched_left_rows,
167
+ "unmatched_left_rows": self.unmatched_left_rows,
168
+ "matched_pairs": self.matched_pairs,
169
+ "left_duplicate_keys": self.left_duplicate_keys,
170
+ "right_duplicate_keys": self.right_duplicate_keys,
171
+ "row_multiplier_numerator": self.row_multiplier_numerator,
172
+ "row_multiplier_denominator": self.row_multiplier_denominator,
173
+ "future_availability_rejections": self.future_availability_rejections,
174
+ "future_revision_rejections": self.future_revision_rejections,
175
+ "tolerance_rejections": self.tolerance_rejections,
176
+ "lag_seconds_minimum": self.lag_seconds_minimum,
177
+ "lag_seconds_maximum": self.lag_seconds_maximum,
178
+ "selected_revisions": [item.to_dict() for item in self.selected_revisions],
179
+ }
180
+
181
+
182
+ def equality_left_join(
183
+ left: TableState,
184
+ right: TableState,
185
+ spec: EqualityJoinSpec,
186
+ ) -> tuple[TableState, JoinAudit]:
187
+ """Perform one declared stable equality left join."""
188
+
189
+ if right.table_id != spec.right_source_id:
190
+ raise PreparationError("JOIN_SOURCE", spec.join_id, "right source does not match join")
191
+ left_indexes = validate_columns(left, spec.left_keys, f"{spec.join_id}.left_keys")
192
+ right_indexes = validate_columns(right, spec.right_keys, f"{spec.join_id}.right_keys")
193
+ _validate_key_types(left, right, left_indexes, right_indexes, spec.join_id)
194
+ left_keys = _keys(left, left_indexes, spec.join_id, "left")
195
+ right_keys = _keys(right, right_indexes, spec.join_id, "right")
196
+ left_duplicates = _duplicate_count(left_keys)
197
+ right_duplicates = _duplicate_count(right_keys)
198
+ if spec.cardinality in {"one_to_one", "one_to_many"} and left_duplicates:
199
+ raise PreparationError(
200
+ "JOIN_CARDINALITY",
201
+ spec.join_id,
202
+ "declared cardinality requires unique left keys",
203
+ severity="critical",
204
+ )
205
+ if spec.cardinality in {"one_to_one", "many_to_one"} and right_duplicates:
206
+ raise PreparationError(
207
+ "JOIN_CARDINALITY",
208
+ spec.join_id,
209
+ "declared cardinality requires unique right keys",
210
+ severity="critical",
211
+ )
212
+ right_lookup: dict[tuple[Any, ...], list[int]] = {}
213
+ for ordinal, key in enumerate(right_keys):
214
+ right_lookup.setdefault(key, []).append(ordinal)
215
+ output_fields, output_lineage, right_output_indexes = _joined_schema(
216
+ left,
217
+ right,
218
+ skipped_right=frozenset(spec.right_keys),
219
+ suffix=spec.suffix,
220
+ join_id=spec.join_id,
221
+ )
222
+ output_rows: list[tuple[Any, ...]] = []
223
+ matched_left = 0
224
+ matched_pairs = 0
225
+ for left_row, key in zip(left.rows, left_keys, strict=True):
226
+ matches = right_lookup.get(key, ())
227
+ if matches:
228
+ matched_left += 1
229
+ matched_pairs += len(matches)
230
+ if len(output_rows) + len(matches) > MAX_ROWS:
231
+ raise PreparationError(
232
+ "JOIN_ROW_LIMIT",
233
+ spec.join_id,
234
+ "join output exceeds the deterministic row budget",
235
+ severity="critical",
236
+ )
237
+ for right_ordinal in matches:
238
+ output_rows.append(
239
+ (
240
+ *left_row,
241
+ *(right.rows[right_ordinal][index] for index in right_output_indexes),
242
+ )
243
+ )
244
+ else:
245
+ output_rows.append((*left_row, *(None for _ in right_output_indexes)))
246
+ output = TableState(
247
+ table_id=f"{left.table_id}.{spec.join_id}",
248
+ snapshot_digests=tuple(sorted(set((*left.snapshot_digests, *right.snapshot_digests)))),
249
+ fields=output_fields,
250
+ rows=tuple(output_rows),
251
+ lineage=output_lineage,
252
+ )
253
+ audit = JoinAudit(
254
+ schema_version=JOIN_AUDIT_VERSION,
255
+ join_id=spec.join_id,
256
+ kind="equality_left",
257
+ cardinality=spec.cardinality,
258
+ input_left_rows=len(left.rows),
259
+ input_right_rows=len(right.rows),
260
+ output_rows=len(output.rows),
261
+ matched_left_rows=matched_left,
262
+ unmatched_left_rows=len(left.rows) - matched_left,
263
+ matched_pairs=matched_pairs,
264
+ left_duplicate_keys=left_duplicates,
265
+ right_duplicate_keys=right_duplicates,
266
+ row_multiplier_numerator=len(output.rows),
267
+ row_multiplier_denominator=len(left.rows),
268
+ future_availability_rejections=0,
269
+ future_revision_rejections=0,
270
+ tolerance_rejections=0,
271
+ lag_seconds_minimum=None,
272
+ lag_seconds_maximum=None,
273
+ selected_revisions=(),
274
+ )
275
+ return output, audit
276
+
277
+
278
+ def backward_asof_join(
279
+ left: TableState,
280
+ right: TableState,
281
+ spec: AsOfJoinSpec,
282
+ ) -> tuple[TableState, JoinAudit]:
283
+ """Perform a partitioned cutoff-safe backward as-of join."""
284
+
285
+ if right.table_id != spec.right_source_id:
286
+ raise PreparationError("JOIN_SOURCE", spec.join_id, "right source does not match join")
287
+ left_by = validate_columns(left, spec.left_by, f"{spec.join_id}.left_by")
288
+ right_by = validate_columns(right, spec.right_by, f"{spec.join_id}.right_by")
289
+ _validate_key_types(left, right, left_by, right_by, spec.join_id)
290
+ left_keys = _keys(left, left_by, spec.join_id, "left")
291
+ right_keys = _keys(right, right_by, spec.join_id, "right")
292
+ left_duplicates = _duplicate_count(
293
+ tuple(
294
+ (*key, left.rows[index][left.index(spec.left_time)])
295
+ for index, key in enumerate(left_keys)
296
+ )
297
+ )
298
+ if spec.cardinality == "one_to_one" and left_duplicates:
299
+ raise PreparationError(
300
+ "JOIN_CARDINALITY",
301
+ spec.join_id,
302
+ "one_to_one as-of join requires unique left partition/time keys",
303
+ severity="critical",
304
+ )
305
+ right_time_index = right.index(spec.right_time)
306
+ available_index = right.index(spec.right_available_at)
307
+ revision_index = right.index(spec.right_revision_time)
308
+ left_time_index = left.index(spec.left_time)
309
+ left_cutoff_index = left.index(spec.left_cutoff)
310
+ for state, indexes, label in (
311
+ (left, (left_time_index, left_cutoff_index), "left"),
312
+ (right, (right_time_index, available_index, revision_index), "right"),
313
+ ):
314
+ for index in indexes:
315
+ if state.fields[index].logical_type != "timestamp_utc":
316
+ raise PreparationError(
317
+ "JOIN_TIME_TYPE",
318
+ spec.join_id,
319
+ f"{label} time fields must be UTC timestamps",
320
+ )
321
+ right_identity = tuple(
322
+ (*right_keys[index], row[right_time_index], row[revision_index])
323
+ for index, row in enumerate(right.rows)
324
+ )
325
+ right_duplicates = _duplicate_count(right_identity)
326
+ if right_duplicates:
327
+ raise PreparationError(
328
+ "JOIN_UNSTABLE_TIE",
329
+ spec.join_id,
330
+ "right partition/event/revision identity is not unique",
331
+ severity="critical",
332
+ )
333
+ output_fields, output_lineage, right_output_indexes = _joined_schema(
334
+ left,
335
+ right,
336
+ skipped_right=frozenset(spec.right_by),
337
+ suffix=spec.suffix,
338
+ join_id=spec.join_id,
339
+ )
340
+ right_by_partition: dict[tuple[Any, ...], list[int]] = {}
341
+ for ordinal, key in enumerate(right_keys):
342
+ right_by_partition.setdefault(key, []).append(ordinal)
343
+ question_cutoff = parse_utc(spec.question_cutoff, f"{spec.join_id}.question_cutoff")
344
+ output_rows: list[tuple[Any, ...]] = []
345
+ selected: list[SelectedRevision] = []
346
+ lags: list[int] = []
347
+ matched_left = 0
348
+ future_availability = 0
349
+ future_revision = 0
350
+ tolerance_rejections = 0
351
+ comparisons = 0
352
+ for left_ordinal, (left_row, partition) in enumerate(zip(left.rows, left_keys, strict=True)):
353
+ left_time = _instant(left_row[left_time_index], spec.join_id)
354
+ row_cutoff = _instant(left_row[left_cutoff_index], spec.join_id)
355
+ if row_cutoff > question_cutoff:
356
+ raise PreparationError(
357
+ "TEMPORAL_CUTOFF",
358
+ f"{spec.join_id}.left[{left_ordinal}]",
359
+ "row cutoff exceeds the question cutoff",
360
+ )
361
+ eligible: list[tuple[datetime, datetime, int, datetime]] = []
362
+ for right_ordinal in right_by_partition.get(partition, ()):
363
+ comparisons += 1
364
+ if comparisons > MAX_ASOF_COMPARISONS:
365
+ raise PreparationError(
366
+ "JOIN_COMPARISON_LIMIT",
367
+ spec.join_id,
368
+ "as-of candidate comparisons exceed the deterministic budget",
369
+ severity="critical",
370
+ )
371
+ right_row = right.rows[right_ordinal]
372
+ event_time = _instant(right_row[right_time_index], spec.join_id)
373
+ if event_time > left_time:
374
+ continue
375
+ lag = int((left_time - event_time).total_seconds())
376
+ if lag > spec.tolerance_seconds:
377
+ tolerance_rejections += 1
378
+ continue
379
+ available_at = _instant(right_row[available_index], spec.join_id)
380
+ if available_at > row_cutoff or available_at > question_cutoff:
381
+ future_availability += 1
382
+ continue
383
+ revision_time = _instant(right_row[revision_index], spec.join_id)
384
+ if revision_time > row_cutoff or revision_time > question_cutoff:
385
+ future_revision += 1
386
+ continue
387
+ eligible.append((event_time, revision_time, right_ordinal, available_at))
388
+ chosen: tuple[datetime, datetime, int, datetime] | None = None
389
+ if eligible:
390
+ latest_event = max(item[0] for item in eligible)
391
+ revisions = [item for item in eligible if item[0] == latest_event]
392
+ if spec.revision_policy == "latest_available":
393
+ chosen = max(revisions, key=lambda item: (item[1], -item[2]))
394
+ else:
395
+ chosen = min(revisions, key=lambda item: (item[1], item[2]))
396
+ if chosen is None:
397
+ output_rows.append((*left_row, *(None for _ in right_output_indexes)))
398
+ continue
399
+ event_time, revision_time, right_ordinal, available_at = chosen
400
+ matched_left += 1
401
+ right_row = right.rows[right_ordinal]
402
+ output_rows.append((*left_row, *(right_row[index] for index in right_output_indexes)))
403
+ lag = int((left_time - event_time).total_seconds())
404
+ lags.append(lag)
405
+ selected.append(
406
+ SelectedRevision(
407
+ left_ordinal=left_ordinal,
408
+ right_ordinal=right_ordinal,
409
+ event_time=_utc_text(event_time),
410
+ available_at=_utc_text(available_at),
411
+ revision_time=_utc_text(revision_time),
412
+ )
413
+ )
414
+ output = TableState(
415
+ table_id=f"{left.table_id}.{spec.join_id}",
416
+ snapshot_digests=tuple(sorted(set((*left.snapshot_digests, *right.snapshot_digests)))),
417
+ fields=output_fields,
418
+ rows=tuple(output_rows),
419
+ lineage=output_lineage,
420
+ )
421
+ audit = JoinAudit(
422
+ schema_version=JOIN_AUDIT_VERSION,
423
+ join_id=spec.join_id,
424
+ kind="asof_left",
425
+ cardinality=spec.cardinality,
426
+ input_left_rows=len(left.rows),
427
+ input_right_rows=len(right.rows),
428
+ output_rows=len(output.rows),
429
+ matched_left_rows=matched_left,
430
+ unmatched_left_rows=len(left.rows) - matched_left,
431
+ matched_pairs=matched_left,
432
+ left_duplicate_keys=left_duplicates,
433
+ right_duplicate_keys=right_duplicates,
434
+ row_multiplier_numerator=len(output.rows),
435
+ row_multiplier_denominator=len(left.rows),
436
+ future_availability_rejections=future_availability,
437
+ future_revision_rejections=future_revision,
438
+ tolerance_rejections=tolerance_rejections,
439
+ lag_seconds_minimum=min(lags) if lags else None,
440
+ lag_seconds_maximum=max(lags) if lags else None,
441
+ selected_revisions=tuple(selected),
442
+ )
443
+ return output, audit
444
+
445
+
446
+ def _joined_schema(
447
+ left: TableState,
448
+ right: TableState,
449
+ *,
450
+ skipped_right: frozenset[str],
451
+ suffix: str,
452
+ join_id: str,
453
+ ) -> tuple[tuple[FieldSpec, ...], tuple[ColumnLineage, ...], tuple[int, ...]]:
454
+ output_names = set(left.columns)
455
+ right_indexes: list[int] = []
456
+ right_names: dict[str, str] = {}
457
+ for index, field in enumerate(right.fields):
458
+ if field.name in skipped_right:
459
+ continue
460
+ output_name = field.name if field.name not in output_names else f"{field.name}_{suffix}"
461
+ if output_name in output_names:
462
+ raise PreparationError("JOIN_COLLISION", join_id, "suffix does not resolve collision")
463
+ output_names.add(output_name)
464
+ right_indexes.append(index)
465
+ right_names[field.name] = output_name
466
+ right_fields = tuple(
467
+ replace(
468
+ right.fields[index],
469
+ name=right_names[right.fields[index].name],
470
+ nullable=True,
471
+ availability_column=(
472
+ right_names.get(
473
+ right.fields[index].availability_column,
474
+ right.fields[index].availability_column,
475
+ )
476
+ if right.fields[index].availability_column is not None
477
+ else None
478
+ ),
479
+ )
480
+ for index in right_indexes
481
+ )
482
+ right_lineage_by_name = {item.output_column: item for item in right.lineage}
483
+ right_lineage = tuple(
484
+ ColumnLineage(
485
+ output_column=right_names[right.fields[index].name],
486
+ inputs=right_lineage_by_name[right.fields[index].name].inputs,
487
+ operation_ids=(
488
+ *right_lineage_by_name[right.fields[index].name].operation_ids,
489
+ join_id,
490
+ ),
491
+ )
492
+ for index in right_indexes
493
+ )
494
+ left_lineage = tuple(
495
+ ColumnLineage(
496
+ output_column=item.output_column,
497
+ inputs=item.inputs,
498
+ operation_ids=(*item.operation_ids, join_id),
499
+ )
500
+ for item in left.lineage
501
+ )
502
+ return (*left.fields, *right_fields), (*left_lineage, *right_lineage), tuple(right_indexes)
503
+
504
+
505
+ def _validate_key_types(
506
+ left: TableState,
507
+ right: TableState,
508
+ left_indexes: tuple[int, ...],
509
+ right_indexes: tuple[int, ...],
510
+ join_id: str,
511
+ ) -> None:
512
+ for left_index, right_index in zip(left_indexes, right_indexes, strict=True):
513
+ left_field = left.fields[left_index]
514
+ right_field = right.fields[right_index]
515
+ left_type = (
516
+ left_field.logical_type,
517
+ left_field.physical_type,
518
+ left_field.semantic_type,
519
+ left_field.unit,
520
+ left_field.entity,
521
+ left_field.temporal_role,
522
+ )
523
+ right_type = (
524
+ right_field.logical_type,
525
+ right_field.physical_type,
526
+ right_field.semantic_type,
527
+ right_field.unit,
528
+ right_field.entity,
529
+ right_field.temporal_role,
530
+ )
531
+ if left_type != right_type:
532
+ raise PreparationError("JOIN_KEY_TYPE", join_id, "join key types are not exact")
533
+
534
+
535
+ def _keys(
536
+ state: TableState,
537
+ indexes: tuple[int, ...],
538
+ join_id: str,
539
+ side: str,
540
+ ) -> tuple[tuple[Any, ...], ...]:
541
+ result: list[tuple[Any, ...]] = []
542
+ for row_index, row in enumerate(state.rows):
543
+ key = tuple(row[index] for index in indexes)
544
+ if any(value is None for value in key):
545
+ raise PreparationError(
546
+ "JOIN_NULL_KEY",
547
+ f"{join_id}.{side}[{row_index}]",
548
+ "join keys cannot be null",
549
+ severity="critical",
550
+ )
551
+ result.append(key)
552
+ return tuple(result)
553
+
554
+
555
+ def _duplicate_count(keys: tuple[tuple[Any, ...], ...]) -> int:
556
+ return sum(count - 1 for count in Counter(keys).values())
557
+
558
+
559
+ def _instant(value: Any, join_id: str) -> datetime:
560
+ if not isinstance(value, str):
561
+ raise PreparationError("JOIN_TIME_TYPE", join_id, "join time value is not text")
562
+ return parse_utc(value, join_id)
563
+
564
+
565
+ def _utc_text(value: datetime) -> str:
566
+ return value.isoformat(timespec="microseconds").replace("+00:00", "Z").replace(".000000Z", "Z")
567
+
568
+
569
+ __all__ = [
570
+ "JoinAudit",
571
+ "SelectedRevision",
572
+ "backward_asof_join",
573
+ "equality_left_join",
574
+ ]