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,579 @@
1
+ """Cancel one still-active hosted public acquisition, once and durably.
2
+
3
+ The acquisition receipt names the workspace and crawler-session coordinate. Cancellation is a
4
+ separate, authority-bearing act: this module owns its one V3 mutation rather than teaching the
5
+ read-only run client a cancel route. A small local journal is written *before* the request so a
6
+ lost response is retried with the same idempotency key. It deliberately stores only public
7
+ coordinates, the idempotency key, and Studio's cancellation receipt -- never a device key, bearer
8
+ token, Cloud URL, or environment variable name/value.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import os
16
+ import re
17
+ import secrets
18
+ import stat
19
+ from collections.abc import Mapping
20
+ from datetime import datetime
21
+ from pathlib import Path
22
+ from typing import Any
23
+ from uuid import UUID
24
+
25
+ from mostlyright.data_harness.canonical import CanonicalJSONError, canonical_sha256, parse_json
26
+ from mostlyright.data_harness.thin.runs import StudioApiClient
27
+ from mostlyright.data_harness.thin.session import StudioSession, open_studio_session
28
+ from mostlyright.data_harness.thin.transport import ThinLaneError
29
+ from mostlyright.data_harness.ux.credentials import (
30
+ default_credentials_dir,
31
+ resolve_cloud_credentials,
32
+ )
33
+
34
+ CANCEL_PUBLIC_ACQUISITION_PATH = (
35
+ "/v3/workspaces/{workspace_id}/public-acquisitions/{crawler_session_id}:cancel"
36
+ )
37
+ CANCELLATION_RECEIPT_VERSION = "mostlyright-public-acquisition-cancellation-receipt.v1"
38
+ CANCELLATION_JOURNAL_VERSION = "mostlyright-thin-client-public-acquisition-cancellation-journal.v1"
39
+ MAX_JOURNAL_BYTES = 64 * 1024
40
+ MAX_RETRY_AFTER_SECONDS = 900
41
+ _JOURNAL_STATE_DIRECTORY = "mr-data/public-acquisition-cancellations"
42
+
43
+ RECEIPT_FIELDS = frozenset(
44
+ {
45
+ "schema_version",
46
+ "crawler_session_id",
47
+ "workspace_id",
48
+ "requester_principal_id",
49
+ "status",
50
+ "cancelled_at",
51
+ }
52
+ )
53
+ JOURNAL_FIELDS = frozenset(
54
+ {
55
+ "schema_version",
56
+ "workspace_id",
57
+ "crawler_session_id",
58
+ "idempotency_key",
59
+ "state",
60
+ "receipt",
61
+ }
62
+ )
63
+
64
+ _UUID = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
65
+ _IDEMPOTENCY = re.compile(r"^mr-data\.acquire-cancel\.v1:[0-9a-f]{64}$")
66
+ _SECONDS_Z = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$")
67
+
68
+
69
+ class CancellationRefusal(ThinLaneError):
70
+ """A cancellation refusal whose retry facts the caller must be able to retain."""
71
+
72
+ def __init__(
73
+ self,
74
+ code: str,
75
+ detail: str,
76
+ *,
77
+ workspace_id: str | None = None,
78
+ crawler_session_id: str | None = None,
79
+ retry_after_seconds: int | None = None,
80
+ ) -> None:
81
+ super().__init__(code, detail)
82
+ self.workspace_id = workspace_id
83
+ self.crawler_session_id = crawler_session_id
84
+ self.retry_after_seconds = retry_after_seconds
85
+
86
+ def to_payload(self) -> dict[str, Any]:
87
+ payload: dict[str, Any] = {
88
+ "schema_version": "mr-data-error.v1",
89
+ "code": self.code,
90
+ "error": self.detail,
91
+ "headline": self.detail,
92
+ }
93
+ if self.retry_after_seconds is not None:
94
+ payload["retry_after_seconds"] = self.retry_after_seconds
95
+ if self.workspace_id is not None and self.crawler_session_id is not None:
96
+ payload["active_public_acquisition"] = {
97
+ "workspace_id": self.workspace_id,
98
+ "crawler_session_id": self.crawler_session_id,
99
+ }
100
+ return payload
101
+
102
+
103
+ class StudioAcquisitionCancellationClient(StudioApiClient):
104
+ """The one public-acquisition cancellation route and no hidden follow-up mutation."""
105
+
106
+ def cancel(
107
+ self,
108
+ *,
109
+ workspace_id: str,
110
+ crawler_session_id: str,
111
+ idempotency_key: str,
112
+ ) -> tuple[dict[str, Any], bool]:
113
+ headers: dict[str, str] = {}
114
+ try:
115
+ receipt = self._call(
116
+ "POST",
117
+ CANCEL_PUBLIC_ACQUISITION_PATH.format(
118
+ workspace_id=workspace_id,
119
+ crawler_session_id=crawler_session_id,
120
+ ),
121
+ body={
122
+ "schema_version": "3.0.0",
123
+ "workspace_id": workspace_id,
124
+ "crawler_session_id": crawler_session_id,
125
+ },
126
+ extra_headers={"Idempotency-Key": idempotency_key},
127
+ expected=(200,),
128
+ response_headers=headers,
129
+ )
130
+ except ThinLaneError as refusal:
131
+ status = getattr(refusal, "http_status", None)
132
+ if status == 429:
133
+ window = _retry_after(headers)
134
+ if window is None:
135
+ raise ThinLaneError(
136
+ "THIN_CANCELLATION_RETRY_AFTER_INVALID",
137
+ "Studio rate-limited cancellation without a retry window from 1 to 900 "
138
+ "seconds",
139
+ ) from refusal
140
+ raise CancellationRefusal(
141
+ refusal.code,
142
+ f"{refusal.detail}; nothing was cancelled and Studio asked for a retry after "
143
+ f"{window} seconds",
144
+ workspace_id=workspace_id,
145
+ crawler_session_id=crawler_session_id,
146
+ retry_after_seconds=window,
147
+ ) from refusal
148
+ if status == 409:
149
+ raise CancellationRefusal(
150
+ "THIN_ACQUISITION_CANCEL_TERMINAL",
151
+ "Studio cannot cancel this public acquisition because it already completed "
152
+ "or failed",
153
+ ) from refusal
154
+ raise
155
+ replay = headers.get("idempotent-replay")
156
+ if replay not in {"true", "false"}:
157
+ raise ThinLaneError(
158
+ "THIN_CANCELLATION_REPLAY_INVALID",
159
+ "Studio answered cancellation without Idempotent-Replay true or false",
160
+ )
161
+ return receipt, replay == "true"
162
+
163
+
164
+ class CancellationJournal:
165
+ """A narrowly shaped, atomically written receipt journal for one coordinate."""
166
+
167
+ def __init__(self, path: Path) -> None:
168
+ self.path = path
169
+ self.state: dict[str, Any] = {}
170
+
171
+ def load_or_start(self, *, workspace_id: str, crawler_session_id: str) -> None:
172
+ _journal_location(self.path)
173
+ if self.path.exists():
174
+ self.state = _read_journal(self.path)
175
+ if (
176
+ self.state["workspace_id"] != workspace_id
177
+ or self.state["crawler_session_id"] != crawler_session_id
178
+ ):
179
+ raise ThinLaneError(
180
+ "THIN_CANCELLATION_JOURNAL_SCOPE",
181
+ "the cancellation journal belongs to a different public acquisition coordinate",
182
+ )
183
+ expected = _idempotency_key(workspace_id, crawler_session_id)
184
+ if self.state["idempotency_key"] != expected:
185
+ raise ThinLaneError(
186
+ "THIN_CANCELLATION_JOURNAL_INVALID",
187
+ "the cancellation journal has an idempotency key this client would not use",
188
+ )
189
+ return
190
+ self.state = {
191
+ "schema_version": CANCELLATION_JOURNAL_VERSION,
192
+ "workspace_id": workspace_id,
193
+ "crawler_session_id": crawler_session_id,
194
+ "idempotency_key": _idempotency_key(workspace_id, crawler_session_id),
195
+ "state": "prepared",
196
+ "receipt": None,
197
+ }
198
+ self.save()
199
+
200
+ def save(self) -> None:
201
+ rendered = json.dumps(self.state, ensure_ascii=False, sort_keys=True).encode("utf-8")
202
+ if len(rendered) > MAX_JOURNAL_BYTES:
203
+ raise ThinLaneError(
204
+ "THIN_CANCELLATION_JOURNAL_INVALID",
205
+ "the cancellation journal exceeds its byte limit",
206
+ )
207
+ parent = _open_private_parent(self.path.parent)
208
+ staged = f".{self.path.name}.{secrets.token_hex(16)}.partial"
209
+ descriptor: int | None = None
210
+ try:
211
+ try:
212
+ flags = (
213
+ os.O_WRONLY
214
+ | os.O_CREAT
215
+ | os.O_EXCL
216
+ | getattr(os, "O_NOFOLLOW", 0)
217
+ | getattr(os, "O_CLOEXEC", 0)
218
+ | getattr(os, "O_BINARY", 0)
219
+ )
220
+ descriptor = os.open(staged, flags, 0o600, dir_fd=parent)
221
+ _validate_private_file(os.fstat(descriptor))
222
+ offset = 0
223
+ while offset < len(rendered):
224
+ written = os.write(descriptor, rendered[offset:])
225
+ if written <= 0:
226
+ raise OSError("could not finish writing cancellation journal")
227
+ offset += written
228
+ os.fsync(descriptor)
229
+ finally:
230
+ if descriptor is not None:
231
+ os.close(descriptor)
232
+ os.replace(staged, self.path.name, src_dir_fd=parent, dst_dir_fd=parent)
233
+ os.fsync(parent)
234
+ except BaseException:
235
+ _remove_stage(parent, staged)
236
+ raise
237
+ finally:
238
+ os.close(parent)
239
+
240
+ def cancelled(self, receipt: Mapping[str, Any]) -> None:
241
+ self.state["state"] = "cancelled"
242
+ self.state["receipt"] = dict(receipt)
243
+ self.save()
244
+
245
+
246
+ def declare_arguments(parser: argparse.ArgumentParser) -> None:
247
+ parser.add_argument(
248
+ "--workspace-id",
249
+ required=True,
250
+ help="the workspace_id printed by mr-data acquire for this public acquisition",
251
+ )
252
+ parser.add_argument(
253
+ "--crawler-session-id",
254
+ required=True,
255
+ help="the crawler_session_id printed by mr-data acquire for this public acquisition",
256
+ )
257
+ parser.add_argument(
258
+ "--journal",
259
+ metavar="FILE",
260
+ help=(
261
+ "override the protected Receipt journal for this address; retries reuse its "
262
+ "idempotency key and it stores no credential"
263
+ ),
264
+ )
265
+
266
+
267
+ def acquire_cancel(
268
+ args: argparse.Namespace,
269
+ *,
270
+ client: StudioAcquisitionCancellationClient | None = None,
271
+ ) -> dict[str, Any]:
272
+ """Cancel an active acquisition, or return its locally confirmed terminal receipt."""
273
+
274
+ workspace_id = _coordinate(getattr(args, "workspace_id", None), "--workspace-id")
275
+ crawler_session_id = _coordinate(
276
+ getattr(args, "crawler_session_id", None), "--crawler-session-id"
277
+ )
278
+ supplied_journal = getattr(args, "journal", None)
279
+ journal = CancellationJournal(
280
+ Path(str(supplied_journal)).expanduser()
281
+ if supplied_journal
282
+ else default_cancellation_journal_path(workspace_id, crawler_session_id)
283
+ )
284
+ # A bad coordinate or journal path is refused before Cloud is contacted; creating the durable
285
+ # record itself waits until the effective hosted credential exists, so an unauthenticated typo
286
+ # does not leave a file behind.
287
+ _journal_location(journal.path, create_parent=supplied_journal is None)
288
+ selected = client or StudioAcquisitionCancellationClient(_session(args))
289
+ journal.load_or_start(workspace_id=workspace_id, crawler_session_id=crawler_session_id)
290
+ receipt = journal.state.get("receipt")
291
+ if journal.state.get("state") == "cancelled":
292
+ if not isinstance(receipt, Mapping):
293
+ raise ThinLaneError(
294
+ "THIN_CANCELLATION_JOURNAL_INVALID",
295
+ "the cancellation journal says cancelled but holds no receipt",
296
+ )
297
+ _validate_receipt(receipt, workspace_id=workspace_id, crawler_session_id=crawler_session_id)
298
+ return dict(receipt)
299
+ returned, _replayed = selected.cancel(
300
+ workspace_id=workspace_id,
301
+ crawler_session_id=crawler_session_id,
302
+ idempotency_key=str(journal.state["idempotency_key"]),
303
+ )
304
+ _validate_receipt(returned, workspace_id=workspace_id, crawler_session_id=crawler_session_id)
305
+ journal.cancelled(returned)
306
+ return dict(returned)
307
+
308
+
309
+ def _session(args: argparse.Namespace) -> StudioSession:
310
+ return open_studio_session(resolve_cloud_credentials())
311
+
312
+
313
+ def _coordinate(value: Any, flag: str) -> str:
314
+ if not isinstance(value, str) or _UUID.fullmatch(value) is None:
315
+ raise ThinLaneError(
316
+ "THIN_CANCELLATION_COORDINATE_INVALID",
317
+ f"{flag} must be one lowercase RFC 4122 UUID",
318
+ )
319
+ try:
320
+ return str(UUID(value))
321
+ except ValueError as error: # pragma: no cover - the expression excludes malformed UUIDs
322
+ raise ThinLaneError(
323
+ "THIN_CANCELLATION_COORDINATE_INVALID", f"{flag} is not a UUID"
324
+ ) from error
325
+
326
+
327
+ def _idempotency_key(workspace_id: str, crawler_session_id: str) -> str:
328
+ return "mr-data.acquire-cancel.v1:" + canonical_sha256(
329
+ {
330
+ "schema_version": "hosted-public-acquisition-cancellation-idempotency.v1",
331
+ "workspace_id": workspace_id,
332
+ "crawler_session_id": crawler_session_id,
333
+ }
334
+ )
335
+
336
+
337
+ def _retry_after(headers: Mapping[str, str]) -> int | None:
338
+ value = headers.get("retry-after", "")
339
+ if not value.isdecimal():
340
+ return None
341
+ seconds = int(value)
342
+ return seconds if 1 <= seconds <= MAX_RETRY_AFTER_SECONDS else None
343
+
344
+
345
+ def _validate_receipt(
346
+ receipt: Mapping[str, Any], *, workspace_id: str, crawler_session_id: str
347
+ ) -> None:
348
+ if set(receipt) != RECEIPT_FIELDS:
349
+ raise ThinLaneError(
350
+ "THIN_CANCELLATION_RECEIPT_INVALID",
351
+ "Studio's cancellation receipt has missing, malformed, or extra fields",
352
+ )
353
+ cancelled_at = receipt.get("cancelled_at")
354
+ if (
355
+ receipt.get("schema_version") != CANCELLATION_RECEIPT_VERSION
356
+ or receipt.get("workspace_id") != workspace_id
357
+ or receipt.get("crawler_session_id") != crawler_session_id
358
+ or receipt.get("status") != "cancelled"
359
+ or not isinstance(receipt.get("requester_principal_id"), str)
360
+ or _UUID.fullmatch(str(receipt.get("requester_principal_id"))) is None
361
+ or not isinstance(cancelled_at, str)
362
+ or _SECONDS_Z.fullmatch(cancelled_at) is None
363
+ ):
364
+ raise ThinLaneError(
365
+ "THIN_CANCELLATION_RECEIPT_INVALID",
366
+ "Studio's cancellation receipt is not for this coordinate or is not terminally "
367
+ "cancelled",
368
+ )
369
+ try:
370
+ datetime.strptime(cancelled_at, "%Y-%m-%dT%H:%M:%SZ")
371
+ except ValueError as error:
372
+ raise ThinLaneError(
373
+ "THIN_CANCELLATION_RECEIPT_INVALID",
374
+ "Studio's cancellation receipt has no RFC3339 seconds timestamp",
375
+ ) from error
376
+
377
+
378
+ def default_cancellation_journal_path(workspace_id: str, crawler_session_id: str) -> Path:
379
+ """The private, deterministic receipt location for one acquisition coordinate."""
380
+
381
+ return (
382
+ default_credentials_dir()
383
+ / _JOURNAL_STATE_DIRECTORY
384
+ / (f"{workspace_id}-{crawler_session_id}.json")
385
+ )
386
+
387
+
388
+ def _journal_location(path: Path, *, create_parent: bool = False) -> None:
389
+ _require_secure_journal_primitives()
390
+ if path.name in {"", ".", ".."}:
391
+ raise ThinLaneError(
392
+ "THIN_CANCELLATION_JOURNAL_INVALID",
393
+ "--journal must name one ordinary file",
394
+ )
395
+ parent = path.parent
396
+ if create_parent:
397
+ _create_private_state_parent(parent)
398
+ try:
399
+ descriptor = _open_private_parent(parent)
400
+ except OSError as error:
401
+ raise ThinLaneError(
402
+ "THIN_CANCELLATION_JOURNAL_INVALID",
403
+ "--journal's parent must be one private directory owned by this user",
404
+ ) from error
405
+ else:
406
+ os.close(descriptor)
407
+
408
+
409
+ def _create_private_state_parent(parent: Path) -> None:
410
+ """Create only our two state directories, then verify them through no-follow opens."""
411
+
412
+ root = default_credentials_dir()
413
+ try:
414
+ root.mkdir(mode=0o700, parents=True, exist_ok=True)
415
+ _assert_private_directory(root)
416
+ current = root
417
+ for part in Path(_JOURNAL_STATE_DIRECTORY).parts:
418
+ current = current / part
419
+ current.mkdir(mode=0o700, exist_ok=True)
420
+ _assert_private_directory(current)
421
+ except OSError as error:
422
+ raise ThinLaneError(
423
+ "THIN_CANCELLATION_JOURNAL_INVALID",
424
+ "the default cancellation journal directory could not be created securely",
425
+ ) from error
426
+ if parent != current:
427
+ raise ThinLaneError(
428
+ "THIN_CANCELLATION_JOURNAL_INVALID", "the default cancellation journal path is invalid"
429
+ )
430
+
431
+
432
+ def _assert_private_directory(path: Path) -> None:
433
+ descriptor = _open_private_parent(path)
434
+ os.close(descriptor)
435
+
436
+
437
+ def _open_private_parent(path: Path) -> int:
438
+ _require_secure_journal_primitives()
439
+ flags = (
440
+ os.O_RDONLY
441
+ | getattr(os, "O_DIRECTORY", 0)
442
+ | getattr(os, "O_NOFOLLOW", 0)
443
+ | getattr(os, "O_CLOEXEC", 0)
444
+ )
445
+ descriptor = os.open(path, flags)
446
+ try:
447
+ _validate_private_directory(os.fstat(descriptor))
448
+ except BaseException:
449
+ os.close(descriptor)
450
+ raise
451
+ return descriptor
452
+
453
+
454
+ def _require_secure_journal_primitives() -> None:
455
+ """Refuse platforms without the descriptor operations that make journal writes safe.
456
+
457
+ Windows does not expose the POSIX ``dir_fd``/``O_NOFOLLOW`` contract needed to protect this
458
+ state from reparse-point replacement. A path-based fallback would make a cancellation retry
459
+ less safe than the first cancellation, so portability here means an explicit, pre-network
460
+ refusal until an equally strong native primitive is available.
461
+ """
462
+
463
+ supported = getattr(os, "supports_dir_fd", frozenset())
464
+ if (
465
+ os.name != "posix"
466
+ or not all(operation in supported for operation in (os.open, os.rename, os.unlink))
467
+ or not hasattr(os, "O_NOFOLLOW")
468
+ or not hasattr(os, "O_DIRECTORY")
469
+ ):
470
+ raise ThinLaneError(
471
+ "THIN_CANCELLATION_JOURNAL_UNSUPPORTED",
472
+ "this platform lacks the secure filesystem primitives required for cancellation "
473
+ "receipt journaling",
474
+ )
475
+
476
+
477
+ def _validate_private_directory(info: os.stat_result) -> None:
478
+ if not stat.S_ISDIR(info.st_mode):
479
+ raise OSError("journal parent is not a directory")
480
+ if os.name == "posix" and (info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) & 0o022):
481
+ raise OSError("journal parent is not private to this user")
482
+
483
+
484
+ def _validate_private_file(info: os.stat_result) -> None:
485
+ if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
486
+ raise OSError("journal is not a single-link regular file")
487
+ if os.name == "posix" and (info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o600):
488
+ raise OSError("journal is not a private file")
489
+
490
+
491
+ def _remove_stage(parent: int, staged: str) -> None:
492
+ """Best-effort cleanup of our unguessable staging name; unlink never follows links."""
493
+
494
+ try:
495
+ os.unlink(staged, dir_fd=parent)
496
+ except FileNotFoundError:
497
+ pass
498
+
499
+
500
+ def _read_journal(path: Path) -> dict[str, Any]:
501
+ parent: int | None = None
502
+ descriptor: int | None = None
503
+ try:
504
+ parent = _open_private_parent(path.parent)
505
+ flags = (
506
+ os.O_RDONLY
507
+ | os.O_NONBLOCK
508
+ | getattr(os, "O_NOFOLLOW", 0)
509
+ | getattr(os, "O_CLOEXEC", 0)
510
+ | getattr(os, "O_BINARY", 0)
511
+ )
512
+ descriptor = os.open(path.name, flags, dir_fd=parent)
513
+ info = os.fstat(descriptor)
514
+ _validate_private_file(info)
515
+ if info.st_size > MAX_JOURNAL_BYTES:
516
+ raise OSError("journal exceeds its byte limit")
517
+ pieces: list[bytes] = []
518
+ remaining = MAX_JOURNAL_BYTES + 1
519
+ while remaining:
520
+ piece = os.read(descriptor, min(remaining, 1 << 16))
521
+ if not piece:
522
+ break
523
+ pieces.append(piece)
524
+ remaining -= len(piece)
525
+ raw = b"".join(pieces)
526
+ if len(raw) > MAX_JOURNAL_BYTES:
527
+ raise OSError("journal exceeds its byte limit")
528
+ value = parse_json(raw)
529
+ except (OSError, CanonicalJSONError) as error:
530
+ raise ThinLaneError(
531
+ "THIN_CANCELLATION_JOURNAL_INVALID",
532
+ "the cancellation journal could not be read as one canonical journal this client wrote",
533
+ ) from error
534
+ finally:
535
+ if descriptor is not None:
536
+ os.close(descriptor)
537
+ if parent is not None:
538
+ os.close(parent)
539
+ if not isinstance(value, dict) or set(value) != JOURNAL_FIELDS:
540
+ raise ThinLaneError(
541
+ "THIN_CANCELLATION_JOURNAL_INVALID",
542
+ "the cancellation journal has missing, malformed, or extra fields",
543
+ )
544
+ if (
545
+ value.get("schema_version") != CANCELLATION_JOURNAL_VERSION
546
+ or value.get("state") not in {"prepared", "cancelled"}
547
+ or not isinstance(value.get("workspace_id"), str)
548
+ or not isinstance(value.get("crawler_session_id"), str)
549
+ or not isinstance(value.get("idempotency_key"), str)
550
+ or _IDEMPOTENCY.fullmatch(str(value.get("idempotency_key"))) is None
551
+ ):
552
+ raise ThinLaneError(
553
+ "THIN_CANCELLATION_JOURNAL_INVALID", "the cancellation journal is invalid"
554
+ )
555
+ if value["state"] == "prepared" and value.get("receipt") is not None:
556
+ raise ThinLaneError(
557
+ "THIN_CANCELLATION_JOURNAL_INVALID",
558
+ "the prepared cancellation journal already holds a receipt",
559
+ )
560
+ if value["state"] == "cancelled" and not isinstance(value.get("receipt"), dict):
561
+ raise ThinLaneError(
562
+ "THIN_CANCELLATION_JOURNAL_INVALID",
563
+ "the cancelled cancellation journal holds no receipt",
564
+ )
565
+ return value
566
+
567
+
568
+ __all__ = [
569
+ "CANCELLATION_JOURNAL_VERSION",
570
+ "CANCELLATION_RECEIPT_VERSION",
571
+ "CANCEL_PUBLIC_ACQUISITION_PATH",
572
+ "JOURNAL_FIELDS",
573
+ "CancellationJournal",
574
+ "CancellationRefusal",
575
+ "StudioAcquisitionCancellationClient",
576
+ "acquire_cancel",
577
+ "declare_arguments",
578
+ "default_cancellation_journal_path",
579
+ ]