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,1277 @@
1
+ """One-shot Cloud Run wrapper for broker bootstrap, trusted crawling, and signed GCS PUT."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import http.client
7
+ import os
8
+ import re
9
+ import shutil
10
+ import ssl
11
+ import subprocess
12
+ import sys
13
+ from dataclasses import asdict, dataclass
14
+ from datetime import UTC, datetime
15
+ from pathlib import Path
16
+ from typing import Any, NoReturn, Protocol
17
+ from urllib.error import HTTPError
18
+ from urllib.parse import parse_qsl, urlencode, urlsplit
19
+ from urllib.request import (
20
+ HTTPRedirectHandler,
21
+ HTTPSHandler,
22
+ ProxyHandler,
23
+ Request,
24
+ build_opener,
25
+ )
26
+ from uuid import UUID
27
+
28
+ from mostlyright.data_harness.canonical import (
29
+ CanonicalJSONError,
30
+ canonical_json_bytes,
31
+ parse_canonical_json,
32
+ parse_json,
33
+ sha256_bytes,
34
+ )
35
+ from mostlyright.data_harness.hosted_crawler import (
36
+ CRAWLER_EGRESS_POLICY_ENV,
37
+ WARM_UP_RESULT_SCHEMA,
38
+ encode_normalization_input,
39
+ )
40
+ from mostlyright.data_harness.hosted_crawler_fetch import fetch_crawler_source
41
+ from mostlyright.data_harness.hosted_crawler_protocol import (
42
+ CRAWLER_REQUEST_SCHEMA_V3,
43
+ CRAWLER_RESULT_MEDIA_TYPE,
44
+ CRAWLER_RESULT_SCHEMA_V1,
45
+ CRAWLER_RESULT_SCHEMA_V3,
46
+ HOSTED_CRAWLER_FAILURE_CODES,
47
+ MAX_CRAWLER_REQUEST_BYTES,
48
+ MAX_CRAWLER_RESULT_BYTES,
49
+ HostedCrawlerExchange,
50
+ HostedCrawlerFailure,
51
+ HostedCrawlerProtocolError,
52
+ HostedCrawlerRequest,
53
+ parse_crawler_request,
54
+ parse_crawler_result,
55
+ )
56
+ from mostlyright.data_harness.linux_process_boundary import (
57
+ _disable_process_dumpability,
58
+ _install_networkless_seccomp,
59
+ )
60
+
61
+ UPLOAD_CAPABILITY_SCHEMA = "mostlyright-crawler-upload-capability.v1"
62
+ BOOTSTRAP_RESPONSE_SCHEMA = "mostlyright-crawler-bootstrap.v1"
63
+ UPLOAD_MEDIA_TYPE = CRAWLER_RESULT_MEDIA_TYPE
64
+ RESULTS_BUCKET_ENV = "MOSTLYRIGHT_CRAWLER_RESULTS_BUCKET"
65
+ BOOTSTRAP_AUDIENCE_BASE_ENV = "MOSTLYRIGHT_CRAWLER_BOOTSTRAP_AUDIENCE_BASE"
66
+ BOOTSTRAP_URL_BASE_ENV = "MOSTLYRIGHT_CRAWLER_BOOTSTRAP_URL_BASE"
67
+ CLOUD_RUN_EXECUTION_ENV = "CLOUD_RUN_EXECUTION"
68
+ RESULT_OBJECT_PREFIX = "crawler-quarantine/v2/"
69
+ MAX_UPLOAD_CAPABILITY_BYTES = 32 * 1024
70
+ MAX_BOOTSTRAP_RESPONSE_BYTES = MAX_CRAWLER_REQUEST_BYTES + MAX_UPLOAD_CAPABILITY_BYTES + 16 * 1024
71
+ MAX_METADATA_IDENTITY_TOKEN_BYTES = 16 * 1024
72
+ # The only failures a wrapper can know before Studio has supplied the sealed
73
+ # request and single-use upload capability. Keep this closed in lock-step with
74
+ # Studio's ``PRE_BOOTSTRAP_FAILURE_CODES``; ordinary Courier failures travel in
75
+ # the signed result bundle and must never acquire this control-plane path.
76
+ PRE_BOOTSTRAP_FAILURE_CODES = frozenset(
77
+ {
78
+ "CRAWLER_BOOTSTRAP_REDEMPTION_FAILED",
79
+ "CRAWLER_BOOTSTRAP_RESPONSE_INVALID",
80
+ "CRAWLER_BOOTSTRAP_RESPONSE_TOO_LARGE",
81
+ }
82
+ )
83
+ # Sized for an EPA-scale year: the streamed decode, the streamed verification pass, and
84
+ # the chunked result encode are each one pure-Python walk over ~200M cells, which a live
85
+ # run spends 12-15 minutes on. The old 600 was calibrated to decode alone and killed a
86
+ # healthy child mid-verification the first time one survived that far.
87
+ CHILD_TIMEOUT_SECONDS = 1800.0
88
+ # Sized for EPA-scale archive decode under the raised Courier caps: the pure-Python
89
+ # row materialization of a ~200M-cell year file peaks at a measured 24.5 GiB, so the
90
+ # boundary is 32 GiB, equal to the Studio job's container limit by the exact-value check.
91
+ READER_MEMORY_CGROUP_MAX_BYTES = 34_359_738_368
92
+ UPLOAD_TIMEOUT_SECONDS = 600.0
93
+ # The networkless child runs under a seccomp filter that denies clone/clone3 as
94
+ # process-escape syscalls, so native libraries must not spawn worker threads.
95
+ # numpy's BLAS spawns its pool at import and would be killed mid-import without
96
+ # these pins; a single-threaded child is also the deterministic one. Arrow's
97
+ # prefixed jemalloc additionally announces its denied background thread on
98
+ # stderr, and the wrapper's contract treats any child stderr as failure, so
99
+ # its background thread is disabled outright.
100
+ CRAWLER_CHILD_THREAD_PINS = (
101
+ ("JE_ARROW_MALLOC_CONF", "background_thread:false"),
102
+ ("MKL_NUM_THREADS", "1"),
103
+ ("NUMEXPR_NUM_THREADS", "1"),
104
+ ("OMP_NUM_THREADS", "1"),
105
+ ("OPENBLAS_NUM_THREADS", "1"),
106
+ )
107
+ BOOTSTRAP_TIMEOUT_SECONDS = 10.0
108
+ METADATA_IDENTITY_URL = (
109
+ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity"
110
+ )
111
+ _DIGEST = re.compile(r"^[0-9a-f]{64}$")
112
+ _BUCKET = re.compile(r"^[a-z0-9][a-z0-9._-]{1,61}[a-z0-9]$")
113
+ _OBJECT_SUFFIX = re.compile(r"^[0-9a-f]{64}$")
114
+ _JWT = re.compile(rb"^[A-Za-z0-9_-]{1,8192}\.[A-Za-z0-9_-]{1,8192}\.[A-Za-z0-9_-]{1,8192}$")
115
+ _CLOUD_RUN_EXECUTION = re.compile(r"^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$")
116
+ _TIMESTAMP = re.compile(
117
+ r"^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])"
118
+ r"T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\.[0-9]{1,6})?Z$"
119
+ )
120
+ _SIGNED_QUERY_FIELDS = {
121
+ "X-Goog-Algorithm",
122
+ "X-Goog-Credential",
123
+ "X-Goog-Date",
124
+ "X-Goog-Expires",
125
+ "X-Goog-Signature",
126
+ "X-Goog-SignedHeaders",
127
+ }
128
+ _SIGNED_HEADERS = ";".join(
129
+ sorted(
130
+ (
131
+ "content-type",
132
+ "host",
133
+ "x-goog-content-length-range",
134
+ "x-goog-content-sha256",
135
+ "x-goog-if-generation-match",
136
+ "x-goog-meta-mostlyright-crawler-session",
137
+ "x-goog-meta-mostlyright-egress-policy",
138
+ "x-goog-meta-mostlyright-request-digest",
139
+ )
140
+ )
141
+ )
142
+
143
+
144
+ class HostedCrawlerJobError(RuntimeError):
145
+ """Fail-closed wrapper error that never includes signed URLs or argv values."""
146
+
147
+ def __init__(self, code: str, detail: str) -> None:
148
+ self.code = code
149
+ self.detail = detail
150
+ super().__init__(f"{detail} [{code}]")
151
+
152
+
153
+ @dataclass(frozen=True)
154
+ class UploadCapability:
155
+ crawler_session_id: str
156
+ request_digest: str
157
+ upload_url: str
158
+ required_headers: tuple[tuple[str, str], ...]
159
+ expires_at: str
160
+
161
+
162
+ @dataclass(frozen=True)
163
+ class CrawlerBootstrap:
164
+ request_bytes: bytes
165
+ upload_capability: UploadCapability
166
+
167
+
168
+ class CrawlerBootstrapTransport(Protocol):
169
+ """Minimal no-proxy, no-redirect transport used for one bootstrap redemption."""
170
+
171
+ def request(
172
+ self,
173
+ *,
174
+ method: str,
175
+ url: str,
176
+ headers: dict[str, str],
177
+ body: bytes | None,
178
+ maximum_response_bytes: int,
179
+ ) -> tuple[int, bytearray]: ...
180
+
181
+
182
+ class _NoRedirect(HTTPRedirectHandler):
183
+ def redirect_request( # type: ignore[override]
184
+ self,
185
+ req: Request,
186
+ fp: Any,
187
+ code: int,
188
+ msg: str,
189
+ headers: Any,
190
+ newurl: str,
191
+ ) -> None:
192
+ return None
193
+
194
+
195
+ class UrlLibCrawlerBootstrapTransport:
196
+ """Bounded transport with ambient proxy use and redirects disabled."""
197
+
198
+ def __init__(self, *, timeout: float = BOOTSTRAP_TIMEOUT_SECONDS) -> None:
199
+ self._timeout = timeout
200
+ self._opener = build_opener(
201
+ ProxyHandler({}),
202
+ HTTPSHandler(context=ssl.create_default_context()),
203
+ _NoRedirect(),
204
+ )
205
+
206
+ def request(
207
+ self,
208
+ *,
209
+ method: str,
210
+ url: str,
211
+ headers: dict[str, str],
212
+ body: bytes | None,
213
+ maximum_response_bytes: int,
214
+ ) -> tuple[int, bytearray]:
215
+ request = Request(url, data=body, headers=headers, method=method)
216
+ try:
217
+ response = self._opener.open(request, timeout=self._timeout)
218
+ except HTTPError as error:
219
+ response = error
220
+ try:
221
+ content_length = response.headers.get("Content-Length")
222
+ if (
223
+ content_length is not None
224
+ and content_length.isdigit()
225
+ and int(content_length) > maximum_response_bytes
226
+ ):
227
+ _fail("CRAWLER_BOOTSTRAP_RESPONSE_TOO_LARGE", "bootstrap response is too large")
228
+ content = bytearray(response.read(maximum_response_bytes + 1))
229
+ if len(content) > maximum_response_bytes:
230
+ _zero(content)
231
+ _fail("CRAWLER_BOOTSTRAP_RESPONSE_TOO_LARGE", "bootstrap response is too large")
232
+ return int(response.status), content
233
+ finally:
234
+ response.close()
235
+
236
+
237
+ def _fail(code: str, detail: str) -> NoReturn:
238
+ raise HostedCrawlerJobError(code, detail)
239
+
240
+
241
+ def _zero(value: bytearray | None) -> None:
242
+ if value is not None:
243
+ value[:] = b"\x00" * len(value)
244
+
245
+
246
+ def _canonical_timestamp(value: Any, label: str) -> str:
247
+ if not isinstance(value, str) or _TIMESTAMP.fullmatch(value) is None:
248
+ _fail("CRAWLER_UPLOAD_CAPABILITY_INVALID", f"{label} is not canonical UTC")
249
+ try:
250
+ parsed = datetime.fromisoformat(value.removesuffix("Z") + "+00:00")
251
+ except ValueError:
252
+ _fail("CRAWLER_UPLOAD_CAPABILITY_INVALID", f"{label} is not a real UTC time")
253
+ if parsed.tzinfo is None or parsed.utcoffset() is None:
254
+ _fail("CRAWLER_UPLOAD_CAPABILITY_INVALID", f"{label} is not canonical UTC")
255
+ return value
256
+
257
+
258
+ def _canonical_uuid(value: Any, label: str) -> str:
259
+ try:
260
+ parsed = UUID(value)
261
+ except (TypeError, ValueError, AttributeError):
262
+ _fail("CRAWLER_UPLOAD_CAPABILITY_INVALID", f"{label} is not a canonical UUID")
263
+ if str(parsed) != value:
264
+ _fail("CRAWLER_UPLOAD_CAPABILITY_INVALID", f"{label} is not a canonical UUID")
265
+ return value
266
+
267
+
268
+ def _bootstrap_base(value: Any, *, label: str) -> str:
269
+ if not isinstance(value, str) or not 1 <= len(value) <= 2048:
270
+ _fail("CRAWLER_BOOTSTRAP_CONFIGURATION_INVALID", f"{label} is unavailable")
271
+ parsed = urlsplit(value)
272
+ try:
273
+ port = parsed.port
274
+ except ValueError:
275
+ _fail("CRAWLER_BOOTSTRAP_CONFIGURATION_INVALID", f"{label} is invalid")
276
+ if (
277
+ parsed.scheme != "https"
278
+ or not parsed.hostname
279
+ or parsed.username is not None
280
+ or parsed.password is not None
281
+ or port is not None
282
+ or parsed.query
283
+ or parsed.fragment
284
+ or not parsed.path.startswith("/")
285
+ or parsed.path.endswith("/")
286
+ or "%" in parsed.path
287
+ or "\\" in parsed.path
288
+ or "//" in parsed.path
289
+ or "/./" in parsed.path
290
+ or "/../" in parsed.path
291
+ or value != f"https://{parsed.hostname}{parsed.path}"
292
+ ):
293
+ _fail("CRAWLER_BOOTSTRAP_CONFIGURATION_INVALID", f"{label} is invalid")
294
+ return value
295
+
296
+
297
+ def _crawler_session_id(value: Any) -> str:
298
+ try:
299
+ parsed = UUID(value)
300
+ except (TypeError, ValueError, AttributeError):
301
+ _fail("CRAWLER_JOB_ARGUMENT_INVALID", "crawler session identifier is invalid")
302
+ if str(parsed) != value:
303
+ _fail("CRAWLER_JOB_ARGUMENT_INVALID", "crawler session identifier is invalid")
304
+ return value
305
+
306
+
307
+ def _cloud_run_execution(value: Any) -> str:
308
+ """Accept only Cloud Run's canonical injected execution-name token."""
309
+
310
+ if not isinstance(value, str) or _CLOUD_RUN_EXECUTION.fullmatch(value) is None:
311
+ _fail("CRAWLER_BOOTSTRAP_CONFIGURATION_INVALID", "crawler execution coordinate is invalid")
312
+ return value
313
+
314
+
315
+ def _metadata_identity_token(
316
+ *,
317
+ audience: str,
318
+ transport: CrawlerBootstrapTransport,
319
+ ) -> bytearray:
320
+ metadata_url = f"{METADATA_IDENTITY_URL}?{urlencode({'audience': audience, 'format': 'full'})}"
321
+ try:
322
+ status, token = transport.request(
323
+ method="GET",
324
+ url=metadata_url,
325
+ headers={"Metadata-Flavor": "Google"},
326
+ body=None,
327
+ maximum_response_bytes=MAX_METADATA_IDENTITY_TOKEN_BYTES,
328
+ )
329
+ except HostedCrawlerJobError:
330
+ raise
331
+ except Exception:
332
+ _fail(
333
+ "CRAWLER_WORKLOAD_IDENTITY_UNAVAILABLE",
334
+ "crawler workload identity is unavailable",
335
+ )
336
+ if not isinstance(token, bytearray):
337
+ token = bytearray(token)
338
+ if status != 200 or _JWT.fullmatch(token) is None:
339
+ _zero(token)
340
+ _fail(
341
+ "CRAWLER_WORKLOAD_IDENTITY_UNAVAILABLE",
342
+ "crawler workload identity is unavailable",
343
+ )
344
+ return token
345
+
346
+
347
+ def _redeem_crawler_bootstrap(
348
+ *,
349
+ crawler_session_id: str,
350
+ bucket: str,
351
+ transport: CrawlerBootstrapTransport | None = None,
352
+ now: datetime | None = None,
353
+ ) -> CrawlerBootstrap:
354
+ """Redeem one workload-bound bootstrap without putting authority on argv or env."""
355
+
356
+ try:
357
+ _disable_process_dumpability()
358
+ except OSError:
359
+ _fail(
360
+ "CRAWLER_PROCESS_HARDENING_UNAVAILABLE",
361
+ "crawler process hardening is unavailable",
362
+ )
363
+ session_id = _crawler_session_id(crawler_session_id)
364
+ audience_base = _bootstrap_base(
365
+ os.environ.get(BOOTSTRAP_AUDIENCE_BASE_ENV),
366
+ label="bootstrap audience base",
367
+ )
368
+ url_base = _bootstrap_base(
369
+ os.environ.get(BOOTSTRAP_URL_BASE_ENV),
370
+ label="bootstrap URL base",
371
+ )
372
+ audience = f"{audience_base}/{session_id}:redeem"
373
+ redemption_url = f"{url_base}/{session_id}:redeem"
374
+ cloud_run_execution = _cloud_run_execution(os.environ.get(CLOUD_RUN_EXECUTION_ENV))
375
+ client = transport or UrlLibCrawlerBootstrapTransport()
376
+ token: bytearray | None = None
377
+ response: bytearray | None = None
378
+ authorization_headers: dict[str, str] = {}
379
+ token_text = ""
380
+ try:
381
+ token = _metadata_identity_token(audience=audience, transport=client)
382
+ token_text = token.decode("ascii", errors="strict")
383
+ authorization_headers = {
384
+ "Accept": "application/json",
385
+ "Authorization": f"Bearer {token_text}",
386
+ "X-Mostlyright-Cloud-Run-Execution": cloud_run_execution,
387
+ }
388
+ try:
389
+ status, response = client.request(
390
+ method="POST",
391
+ url=redemption_url,
392
+ headers=authorization_headers,
393
+ body=None,
394
+ maximum_response_bytes=MAX_BOOTSTRAP_RESPONSE_BYTES,
395
+ )
396
+ except HostedCrawlerJobError:
397
+ raise
398
+ except Exception:
399
+ _fail(
400
+ "CRAWLER_BOOTSTRAP_REDEMPTION_FAILED",
401
+ "crawler bootstrap redemption failed",
402
+ )
403
+ if not isinstance(response, bytearray):
404
+ response = bytearray(response)
405
+ if status != 200 or not 1 <= len(response) <= MAX_BOOTSTRAP_RESPONSE_BYTES:
406
+ _fail(
407
+ "CRAWLER_BOOTSTRAP_REDEMPTION_FAILED",
408
+ "crawler bootstrap redemption failed",
409
+ )
410
+ value = parse_json(response)
411
+ expected_fields = {
412
+ "schema_version",
413
+ "crawler_session_id",
414
+ "request",
415
+ "upload_capability",
416
+ }
417
+ if (
418
+ not isinstance(value, dict)
419
+ or set(value) != expected_fields
420
+ or value["schema_version"] != BOOTSTRAP_RESPONSE_SCHEMA
421
+ or value["crawler_session_id"] != session_id
422
+ ):
423
+ _fail(
424
+ "CRAWLER_BOOTSTRAP_RESPONSE_INVALID",
425
+ "crawler bootstrap response fields or session binding are invalid",
426
+ )
427
+ request = parse_crawler_request(value["request"])
428
+ request_bytes = canonical_json_bytes(request.to_dict())
429
+ if not 1 <= len(request_bytes) <= MAX_CRAWLER_REQUEST_BYTES:
430
+ _fail(
431
+ "CRAWLER_BOOTSTRAP_RESPONSE_INVALID",
432
+ "crawler request exceeds its fixed bound",
433
+ )
434
+ capability = parse_upload_capability(
435
+ value["upload_capability"],
436
+ request_digest=request.digest,
437
+ egress_policy_attestation=request.egress_policy_attestation,
438
+ bucket=bucket,
439
+ now=now,
440
+ )
441
+ if capability.crawler_session_id != session_id:
442
+ _fail(
443
+ "CRAWLER_BOOTSTRAP_RESPONSE_INVALID",
444
+ "crawler upload capability session binding is invalid",
445
+ )
446
+ return CrawlerBootstrap(
447
+ request_bytes=request_bytes,
448
+ upload_capability=capability,
449
+ )
450
+ except (UnicodeDecodeError, CanonicalJSONError, HostedCrawlerProtocolError):
451
+ _fail(
452
+ "CRAWLER_BOOTSTRAP_RESPONSE_INVALID",
453
+ "crawler bootstrap response is invalid",
454
+ )
455
+ finally:
456
+ authorization_headers.clear()
457
+ token_text = ""
458
+ _zero(token)
459
+ _zero(response)
460
+
461
+
462
+ def _report_crawler_bootstrap_failure(
463
+ *,
464
+ crawler_session_id: str,
465
+ failure_code: str,
466
+ transport: CrawlerBootstrapTransport | None = None,
467
+ ) -> bool:
468
+ """Best-effort one-way report for a failure before upload authority exists.
469
+
470
+ The failed job still exits non-zero if this report cannot be delivered. It
471
+ obtains an independent workload token for the exact ``:fail`` route, sends
472
+ only an allowlisted code in canonical bytes, and accepts only an empty 204
473
+ acknowledgement. No response diagnostic, request, or storage capability
474
+ can cross back into the crawler through this path.
475
+ """
476
+
477
+ if failure_code not in PRE_BOOTSTRAP_FAILURE_CODES:
478
+ return False
479
+ token: bytearray | None = None
480
+ response: bytearray | None = None
481
+ authorization_headers: dict[str, str] = {}
482
+ token_text = ""
483
+ try:
484
+ session_id = _crawler_session_id(crawler_session_id)
485
+ audience_base = _bootstrap_base(
486
+ os.environ.get(BOOTSTRAP_AUDIENCE_BASE_ENV),
487
+ label="bootstrap audience base",
488
+ )
489
+ url_base = _bootstrap_base(
490
+ os.environ.get(BOOTSTRAP_URL_BASE_ENV),
491
+ label="bootstrap URL base",
492
+ )
493
+ audience = f"{audience_base}/{session_id}:fail"
494
+ failure_url = f"{url_base}/{session_id}:fail"
495
+ report = canonical_json_bytes({"failure_code": failure_code})
496
+ client = transport or UrlLibCrawlerBootstrapTransport()
497
+ token = _metadata_identity_token(audience=audience, transport=client)
498
+ token_text = token.decode("ascii", errors="strict")
499
+ authorization_headers = {
500
+ "Accept": "application/json",
501
+ "Authorization": f"Bearer {token_text}",
502
+ "Content-Type": "application/json",
503
+ }
504
+ status, response = client.request(
505
+ method="POST",
506
+ url=failure_url,
507
+ headers=authorization_headers,
508
+ body=report,
509
+ maximum_response_bytes=0,
510
+ )
511
+ if not isinstance(response, bytearray):
512
+ response = bytearray(response)
513
+ return status == 204 and not response
514
+ except (HostedCrawlerJobError, UnicodeDecodeError, CanonicalJSONError, ValueError):
515
+ return False
516
+ except Exception:
517
+ return False
518
+ finally:
519
+ authorization_headers.clear()
520
+ token_text = ""
521
+ _zero(token)
522
+ _zero(response)
523
+
524
+
525
+ def _required_headers(
526
+ value: Any,
527
+ *,
528
+ crawler_session_id: str,
529
+ request_digest: str,
530
+ egress_policy_attestation: str,
531
+ ) -> tuple[tuple[str, str], ...]:
532
+ expected = (
533
+ ("Content-Type", UPLOAD_MEDIA_TYPE),
534
+ ("x-goog-content-length-range", f"1,{MAX_CRAWLER_RESULT_BYTES}"),
535
+ ("x-goog-content-sha256", "UNSIGNED-PAYLOAD"),
536
+ ("x-goog-if-generation-match", "0"),
537
+ ("x-goog-meta-mostlyright-crawler-session", crawler_session_id),
538
+ ("x-goog-meta-mostlyright-request-digest", request_digest),
539
+ ("x-goog-meta-mostlyright-egress-policy", egress_policy_attestation),
540
+ )
541
+ if not isinstance(value, list) or len(value) != len(expected):
542
+ _fail(
543
+ "CRAWLER_UPLOAD_CAPABILITY_INVALID",
544
+ "required upload header inventory is not exact",
545
+ )
546
+ parsed: list[tuple[str, str]] = []
547
+ for item in value:
548
+ if not isinstance(item, dict) or set(item) != {"name", "value"}:
549
+ _fail(
550
+ "CRAWLER_UPLOAD_CAPABILITY_INVALID",
551
+ "required upload header entry is not exact",
552
+ )
553
+ name = item["name"]
554
+ header_value = item["value"]
555
+ if (
556
+ not isinstance(name, str)
557
+ or not isinstance(header_value, str)
558
+ or "\r" in name
559
+ or "\n" in name
560
+ or "\r" in header_value
561
+ or "\n" in header_value
562
+ ):
563
+ _fail(
564
+ "CRAWLER_UPLOAD_CAPABILITY_INVALID",
565
+ "required upload header entry is invalid",
566
+ )
567
+ parsed.append((name, header_value))
568
+ if tuple(parsed) != expected:
569
+ _fail(
570
+ "CRAWLER_UPLOAD_CAPABILITY_INVALID",
571
+ "required upload headers differ from the fixed authority binding",
572
+ )
573
+ return tuple(parsed)
574
+
575
+
576
+ def _validate_upload_url(value: Any, *, bucket: str) -> str:
577
+ if not isinstance(value, str) or not 1 <= len(value) <= 16 * 1024:
578
+ _fail("CRAWLER_UPLOAD_CAPABILITY_INVALID", "upload URL is invalid")
579
+ parsed = urlsplit(value)
580
+ try:
581
+ port = parsed.port
582
+ except ValueError:
583
+ _fail("CRAWLER_UPLOAD_CAPABILITY_INVALID", "upload URL port is invalid")
584
+ if (
585
+ parsed.scheme != "https"
586
+ or parsed.hostname != "storage.googleapis.com"
587
+ or parsed.username is not None
588
+ or parsed.password is not None
589
+ or port is not None
590
+ or parsed.fragment
591
+ ):
592
+ _fail(
593
+ "CRAWLER_UPLOAD_CAPABILITY_INVALID",
594
+ "upload URL is outside the exact GCS authority",
595
+ )
596
+ path_prefix = f"/{bucket}/{RESULT_OBJECT_PREFIX}"
597
+ if (
598
+ not parsed.path.startswith(path_prefix)
599
+ or parsed.path.count("/") != path_prefix.count("/")
600
+ or _OBJECT_SUFFIX.fullmatch(parsed.path.removeprefix(path_prefix)) is None
601
+ or "%" in parsed.path
602
+ or "\\" in parsed.path
603
+ or "//" in parsed.path
604
+ or "/./" in parsed.path
605
+ or "/../" in parsed.path
606
+ ):
607
+ _fail(
608
+ "CRAWLER_UPLOAD_CAPABILITY_INVALID",
609
+ "upload URL object path is outside the quarantine prefix",
610
+ )
611
+ try:
612
+ query = parse_qsl(parsed.query, keep_blank_values=True, strict_parsing=True)
613
+ except ValueError:
614
+ _fail(
615
+ "CRAWLER_UPLOAD_CAPABILITY_INVALID",
616
+ "upload URL signed query inventory is invalid",
617
+ )
618
+ if (
619
+ len(query) != len(_SIGNED_QUERY_FIELDS)
620
+ or {name for name, _ in query} != _SIGNED_QUERY_FIELDS
621
+ or any(not item for _, item in query)
622
+ ):
623
+ _fail(
624
+ "CRAWLER_UPLOAD_CAPABILITY_INVALID",
625
+ "upload URL signed query inventory is not exact",
626
+ )
627
+ values = dict(query)
628
+ if (
629
+ values["X-Goog-Algorithm"] != "GOOG4-RSA-SHA256"
630
+ or values["X-Goog-SignedHeaders"] != _SIGNED_HEADERS
631
+ or not values["X-Goog-Expires"].isdigit()
632
+ # Bounded by the Studio upload window, which covers the child's whole execution
633
+ # budget rather than an interactive session: the capability has to stay
634
+ # presentable through a 15-20 minute year-scale crawl.
635
+ or not 1 <= int(values["X-Goog-Expires"]) <= 2100
636
+ or re.fullmatch(
637
+ r"(?:[0-9a-f]{2}){64,512}",
638
+ values["X-Goog-Signature"],
639
+ )
640
+ is None
641
+ ):
642
+ _fail(
643
+ "CRAWLER_UPLOAD_CAPABILITY_INVALID",
644
+ "upload URL signature parameters are outside the fixed policy",
645
+ )
646
+ return value
647
+
648
+
649
+ def parse_upload_capability(
650
+ value: Any,
651
+ *,
652
+ request_digest: str,
653
+ egress_policy_attestation: str,
654
+ bucket: str,
655
+ now: datetime | None = None,
656
+ ) -> UploadCapability:
657
+ fields = {
658
+ "schema_version",
659
+ "crawler_session_id",
660
+ "request_digest",
661
+ "method",
662
+ "upload_url",
663
+ "required_headers",
664
+ "max_result_bytes",
665
+ "expires_at",
666
+ "redirect_policy",
667
+ "single_use",
668
+ }
669
+ if not isinstance(value, dict) or set(value) != fields:
670
+ _fail("CRAWLER_UPLOAD_CAPABILITY_INVALID", "upload capability fields are not exact")
671
+ if (
672
+ value["schema_version"] != UPLOAD_CAPABILITY_SCHEMA
673
+ or value["method"] != "PUT"
674
+ or type(value["max_result_bytes"]) is not int
675
+ or value["max_result_bytes"] != MAX_CRAWLER_RESULT_BYTES
676
+ or value["redirect_policy"] != "deny"
677
+ or value["single_use"] is not True
678
+ or not isinstance(value["request_digest"], str)
679
+ or _DIGEST.fullmatch(value["request_digest"]) is None
680
+ or value["request_digest"] != request_digest
681
+ ):
682
+ _fail(
683
+ "CRAWLER_UPLOAD_CAPABILITY_INVALID",
684
+ "upload capability constants or request binding mismatch",
685
+ )
686
+ crawler_session_id = _canonical_uuid(
687
+ value["crawler_session_id"],
688
+ "crawler_session_id",
689
+ )
690
+ expires_at = _canonical_timestamp(value["expires_at"], "expires_at")
691
+ expiry = datetime.fromisoformat(expires_at.removesuffix("Z") + "+00:00")
692
+ current = now or datetime.now(UTC)
693
+ if current.tzinfo is None or current.astimezone(UTC) >= expiry:
694
+ _fail("CRAWLER_UPLOAD_CAPABILITY_EXPIRED", "upload capability has expired")
695
+ upload_url = _validate_upload_url(value["upload_url"], bucket=bucket)
696
+ headers = _required_headers(
697
+ value["required_headers"],
698
+ crawler_session_id=crawler_session_id,
699
+ request_digest=request_digest,
700
+ egress_policy_attestation=egress_policy_attestation,
701
+ )
702
+ return UploadCapability(
703
+ crawler_session_id=crawler_session_id,
704
+ request_digest=request_digest,
705
+ upload_url=upload_url,
706
+ required_headers=headers,
707
+ expires_at=expires_at,
708
+ )
709
+
710
+
711
+ def _run_crawler_child(
712
+ request: HostedCrawlerRequest,
713
+ normalization_input: bytes,
714
+ ) -> bytes:
715
+ crawler = shutil.which("mr-data-crawler")
716
+ if crawler is None or not os.path.isabs(crawler):
717
+ _fail("CRAWLER_EXECUTABLE_UNAVAILABLE", "fixed crawler executable is unavailable")
718
+ completed = subprocess.run(
719
+ [crawler, "--input-fd", "0", "--result-fd", "1"],
720
+ input=normalization_input,
721
+ capture_output=True,
722
+ env={
723
+ CRAWLER_EGRESS_POLICY_ENV: request.egress_policy_attestation,
724
+ **dict(CRAWLER_CHILD_THREAD_PINS),
725
+ },
726
+ close_fds=True,
727
+ preexec_fn=_install_networkless_seccomp,
728
+ check=False,
729
+ timeout=CHILD_TIMEOUT_SECONDS,
730
+ )
731
+ if completed.returncode != 0:
732
+ _fail(_child_failure_code(completed.stderr), "fixed crawler execution failed")
733
+ if completed.stderr or not 1 <= len(completed.stdout) <= MAX_CRAWLER_RESULT_BYTES:
734
+ _fail("CRAWLER_CHILD_INVALID", "fixed crawler output is invalid")
735
+ return completed.stdout
736
+
737
+
738
+ def _run_crawler_warm_up(request: HostedCrawlerRequest) -> None:
739
+ """Run the mandatory known-sample check before any public source byte is fetched."""
740
+
741
+ crawler = shutil.which("mr-data-crawler")
742
+ if crawler is None or not os.path.isabs(crawler):
743
+ _fail("CRAWLER_EXECUTABLE_UNAVAILABLE", "fixed crawler executable is unavailable")
744
+ completed = subprocess.run(
745
+ [crawler, "--warm-up", "--input-fd", "0", "--result-fd", "1"],
746
+ input=canonical_json_bytes(request.to_dict()),
747
+ capture_output=True,
748
+ env={
749
+ CRAWLER_EGRESS_POLICY_ENV: request.egress_policy_attestation,
750
+ **dict(CRAWLER_CHILD_THREAD_PINS),
751
+ },
752
+ close_fds=True,
753
+ preexec_fn=_install_networkless_seccomp,
754
+ check=False,
755
+ timeout=CHILD_TIMEOUT_SECONDS,
756
+ )
757
+ if completed.returncode != 0:
758
+ code = _child_failure_code(completed.stderr)
759
+ _fail(code if code == "READER_WARMUP" else "READER_WARMUP", "hosted Reader warm-up failed")
760
+ try:
761
+ result = parse_canonical_json(completed.stdout)
762
+ except (CanonicalJSONError, TypeError, ValueError):
763
+ _fail("READER_WARMUP", "hosted Reader warm-up result is invalid")
764
+ if completed.stderr or result != {
765
+ "schema_version": WARM_UP_RESULT_SCHEMA,
766
+ "request_digest": request.digest,
767
+ "status": "passed",
768
+ }:
769
+ _fail("READER_WARMUP", "hosted Reader warm-up result is invalid")
770
+
771
+
772
+ def _require_reader_memory_boundary() -> None:
773
+ """Bind hosted Reader execution to the immutable Cloud Run cgroup ceiling.
774
+
775
+ Cloud Run's job sandbox mounts a hybrid cgroup layout: the kernel enforces the
776
+ container memory limit through the v1 controller at memory/memory.limit_in_bytes,
777
+ and the v2 unified file memory.max is absent. Both spellings name the same
778
+ kernel-enforced ceiling, so either satisfies the boundary; the required value is
779
+ exact in both cases.
780
+ """
781
+
782
+ value = None
783
+ for candidate in (
784
+ Path("/sys/fs/cgroup/memory.max"),
785
+ Path("/sys/fs/cgroup/memory/memory.limit_in_bytes"),
786
+ ):
787
+ try:
788
+ value = candidate.read_text(encoding="ascii").strip()
789
+ except (OSError, UnicodeError):
790
+ continue
791
+ break
792
+ if value is None:
793
+ _fail("CRAWLER_MEMORY_BOUNDARY", "hosted Reader memory cgroup is unavailable")
794
+ if value != str(READER_MEMORY_CGROUP_MAX_BYTES):
795
+ _fail("CRAWLER_MEMORY_BOUNDARY", "hosted Reader memory cgroup is not exact")
796
+
797
+
798
+ def _child_failure_code(stderr: bytes) -> str:
799
+ """Admit only a closed stable child code; never propagate child detail or source text."""
800
+
801
+ if not 1 <= len(stderr) <= 512:
802
+ return "CRAWLER_CHILD_FAILED"
803
+ try:
804
+ value = parse_canonical_json(stderr.strip())
805
+ except (CanonicalJSONError, TypeError, ValueError):
806
+ return "CRAWLER_CHILD_FAILED"
807
+ if (
808
+ isinstance(value, dict)
809
+ and set(value) == {"status", "code"}
810
+ and value.get("status") == "failed"
811
+ and value.get("code") in HOSTED_CRAWLER_FAILURE_CODES
812
+ ):
813
+ return str(value["code"])
814
+ return "CRAWLER_CHILD_FAILED"
815
+
816
+
817
+ def _validate_result_against_retrieval(
818
+ *,
819
+ request: HostedCrawlerRequest,
820
+ retrieved: Any,
821
+ result: Any,
822
+ ) -> None:
823
+ """Bind the crawler child's result to the exact bytes and evidence fetched by this job."""
824
+
825
+ if (request.adapter_id, request.adapter_version) == ("public.https", "1.0.0"):
826
+ _validate_public_https_result(request=request, retrieved=retrieved, result=result)
827
+ return
828
+ if (request.adapter_id, request.adapter_version) != ("external.openligadb", "1.0.0"):
829
+ _fail(
830
+ "CRAWLER_CHILD_INVALID",
831
+ "crawler result uses an unsupported adapter coordinate",
832
+ )
833
+
834
+ receipt = result.acquisition_receipt
835
+ observation = result.observation
836
+ if not isinstance(receipt, dict) or not isinstance(observation, dict):
837
+ _fail(
838
+ "CRAWLER_CHILD_INVALID",
839
+ "crawler result does not bind the trusted retrieval evidence",
840
+ )
841
+ receipt_fields = {
842
+ "schema_version",
843
+ "source_id",
844
+ "adapter_id",
845
+ "adapter_version",
846
+ "request_digest",
847
+ "query",
848
+ "source_url",
849
+ "final_url",
850
+ "media_type",
851
+ "acquired_at",
852
+ "raw_content_sha256",
853
+ "raw_size_bytes",
854
+ "normalized_content_sha256",
855
+ "normalized_size_bytes",
856
+ "row_count",
857
+ "column_names",
858
+ "transport_evidence_digest",
859
+ "egress_policy_attestation",
860
+ }
861
+ observation_fields = {
862
+ "schema_version",
863
+ "source_id",
864
+ "adapter_id",
865
+ "adapter_version",
866
+ "request_digest",
867
+ "observed_at",
868
+ "available_at",
869
+ "historical_start",
870
+ "historical_end",
871
+ "event_time_field",
872
+ "live_status",
873
+ "raw_content_sha256",
874
+ "normalized_content_sha256",
875
+ "row_count",
876
+ }
877
+ normalized_digest = sha256_bytes(result.normalized_content)
878
+ identity = (
879
+ receipt.get("source_id"),
880
+ receipt.get("adapter_id"),
881
+ receipt.get("adapter_version"),
882
+ receipt.get("request_digest"),
883
+ )
884
+ expected_identity = (
885
+ request.source_id,
886
+ request.adapter_id,
887
+ request.adapter_version,
888
+ request.digest,
889
+ )
890
+ if (
891
+ set(receipt) != receipt_fields
892
+ or set(observation) != observation_fields
893
+ or identity != expected_identity
894
+ or (
895
+ observation.get("source_id"),
896
+ observation.get("adapter_id"),
897
+ observation.get("adapter_version"),
898
+ observation.get("request_digest"),
899
+ )
900
+ != expected_identity
901
+ or receipt.get("query") != request.query
902
+ or receipt.get("source_url") != retrieved.source_url
903
+ or receipt.get("final_url") != retrieved.final_url
904
+ or receipt.get("media_type") != retrieved.media_type
905
+ or receipt.get("acquired_at") != request.requested_at
906
+ or receipt.get("raw_content_sha256") != retrieved.content_sha256
907
+ or receipt.get("raw_size_bytes") != len(retrieved.content)
908
+ or receipt.get("transport_evidence_digest") != retrieved.transport_evidence_digest
909
+ or receipt.get("egress_policy_attestation") != request.egress_policy_attestation
910
+ or receipt.get("normalized_content_sha256") != normalized_digest
911
+ or receipt.get("normalized_size_bytes") != len(result.normalized_content)
912
+ or observation.get("observed_at") != request.requested_at
913
+ or observation.get("available_at") != request.requested_at
914
+ or observation.get("event_time_field") != "match_datetime_utc"
915
+ or observation.get("live_status") != "live"
916
+ or observation.get("raw_content_sha256") != retrieved.content_sha256
917
+ or observation.get("normalized_content_sha256") != normalized_digest
918
+ or observation.get("row_count") != receipt.get("row_count")
919
+ ):
920
+ _fail(
921
+ "CRAWLER_CHILD_INVALID",
922
+ "crawler result does not bind the trusted retrieval evidence",
923
+ )
924
+
925
+
926
+ def _validate_public_https_result(
927
+ *,
928
+ request: HostedCrawlerRequest,
929
+ retrieved: Any,
930
+ result: Any,
931
+ ) -> None:
932
+ """Bind the public-HTTPS normalizer result to the trusted wrapper retrieval."""
933
+
934
+ receipt = result.acquisition_receipt
935
+ observation = result.observation
936
+ enhanced_cadence_evidence = request.schema_version == CRAWLER_REQUEST_SCHEMA_V3
937
+ receipt_fields = {
938
+ "schema_version",
939
+ "source_id",
940
+ "adapter_id",
941
+ "adapter_version",
942
+ "request_digest",
943
+ "source_url",
944
+ "final_url",
945
+ "fetched_media_type",
946
+ "normalized_media_type",
947
+ "normalized_data_format",
948
+ "normalized_filename",
949
+ "acquired_at",
950
+ "raw_content_sha256",
951
+ "raw_size_bytes",
952
+ "normalized_content_sha256",
953
+ "normalized_size_bytes",
954
+ "row_count",
955
+ "column_names",
956
+ "parsed_schema_digest",
957
+ "family_id",
958
+ "family_version",
959
+ "decode_options_digest",
960
+ "decode_flags",
961
+ "resource_caps",
962
+ "reader_budgets",
963
+ "transport_evidence_digest",
964
+ "egress_policy_attestation",
965
+ "clean_room",
966
+ }
967
+ if enhanced_cadence_evidence:
968
+ receipt_fields |= {
969
+ "row_digest",
970
+ "http_status_code",
971
+ "etag_digest",
972
+ "last_modified_digest",
973
+ }
974
+ observation_fields = {
975
+ "schema_version",
976
+ "source_id",
977
+ "request_digest",
978
+ "observed_at",
979
+ "raw_content_sha256",
980
+ "normalized_content_sha256",
981
+ "row_count",
982
+ }
983
+ if enhanced_cadence_evidence:
984
+ observation_fields |= {
985
+ "row_digest",
986
+ "http_status_code",
987
+ "etag_digest",
988
+ "last_modified_digest",
989
+ }
990
+ if (
991
+ not isinstance(receipt, dict)
992
+ or set(receipt) != receipt_fields
993
+ or not isinstance(observation, dict)
994
+ or set(observation) != observation_fields
995
+ ):
996
+ _fail(
997
+ "CRAWLER_CHILD_INVALID",
998
+ "public HTTPS result evidence fields are not exact",
999
+ )
1000
+
1001
+ query = request.query
1002
+ reader = query.get("reader_pin")
1003
+ if reader is None:
1004
+ reader_identity = (None, None, None)
1005
+ expected_resource_caps = None
1006
+ expected_reader_budgets = None
1007
+ direct_identity = (
1008
+ query.get("data_format"),
1009
+ query.get("filename"),
1010
+ retrieved.media_type,
1011
+ )
1012
+ else:
1013
+ from mostlyright.data_harness.readers.contracts import ReaderPin
1014
+
1015
+ try:
1016
+ pin = ReaderPin(
1017
+ reader["family_id"],
1018
+ reader["family_version"],
1019
+ reader["decode_options"],
1020
+ )
1021
+ except (KeyError, TypeError, ValueError):
1022
+ _fail("CRAWLER_CHILD_INVALID", "public HTTPS Reader identity is invalid")
1023
+ reader_identity = (pin.family_id, pin.family_version, pin.options_digest)
1024
+ resource_caps = query.get("resource_caps")
1025
+ try:
1026
+ from mostlyright.data_harness.readers.registry import TOOLBOX
1027
+
1028
+ family = TOOLBOX.resolve(pin.family_id, pin.family_version)
1029
+ expected_reader_budgets = asdict(
1030
+ family.default_budgets.narrowed_by(
1031
+ {
1032
+ "max_input_bytes": query["limits"]["max_source_bytes"],
1033
+ "max_output_bytes": query["limits"]["max_normalized_bytes"],
1034
+ "max_rows": query["limits"]["max_rows"],
1035
+ "max_columns": query["limits"]["max_columns"],
1036
+ **(resource_caps or {}),
1037
+ }
1038
+ )
1039
+ )
1040
+ except (KeyError, TypeError, ValueError):
1041
+ _fail("CRAWLER_CHILD_INVALID", "public HTTPS Reader budgets are invalid")
1042
+ expected_resource_caps = resource_caps
1043
+ direct_identity = (
1044
+ receipt.get("normalized_data_format"),
1045
+ receipt.get("normalized_filename"),
1046
+ receipt.get("normalized_media_type"),
1047
+ )
1048
+
1049
+ normalized_digest = sha256_bytes(result.normalized_content)
1050
+ terminal = retrieved.hops[-1]
1051
+ expected_etag_digest = (
1052
+ None if terminal.etag is None else sha256_bytes(terminal.etag.encode("utf-8"))
1053
+ )
1054
+ expected_last_modified_digest = (
1055
+ None
1056
+ if terminal.last_modified is None
1057
+ else sha256_bytes(terminal.last_modified.encode("utf-8"))
1058
+ )
1059
+ row_count = receipt.get("row_count")
1060
+ column_names = receipt.get("column_names")
1061
+ limits = query.get("limits", {})
1062
+ expected_identity = (
1063
+ request.source_id,
1064
+ request.adapter_id,
1065
+ request.adapter_version,
1066
+ request.digest,
1067
+ )
1068
+ if (
1069
+ (
1070
+ receipt.get("source_id"),
1071
+ receipt.get("adapter_id"),
1072
+ receipt.get("adapter_version"),
1073
+ receipt.get("request_digest"),
1074
+ )
1075
+ != expected_identity
1076
+ or receipt.get("schema_version")
1077
+ != (
1078
+ "hosted-public-https-acquisition-receipt.v3"
1079
+ if enhanced_cadence_evidence
1080
+ else "hosted-public-https-acquisition-receipt.v1"
1081
+ )
1082
+ or observation.get("schema_version")
1083
+ != (
1084
+ "hosted-public-https-source-observation.v3"
1085
+ if enhanced_cadence_evidence
1086
+ else "hosted-public-https-source-observation.v1"
1087
+ )
1088
+ or result.schema_version
1089
+ != (CRAWLER_RESULT_SCHEMA_V3 if enhanced_cadence_evidence else CRAWLER_RESULT_SCHEMA_V1)
1090
+ or observation.get("source_id") != request.source_id
1091
+ or observation.get("request_digest") != request.digest
1092
+ or receipt.get("source_url") != query.get("url")
1093
+ or receipt.get("source_url") != retrieved.source_url
1094
+ or receipt.get("final_url") != retrieved.final_url
1095
+ or receipt.get("fetched_media_type") != retrieved.media_type
1096
+ or receipt.get("acquired_at") != request.requested_at
1097
+ or receipt.get("raw_content_sha256") != retrieved.content_sha256
1098
+ or receipt.get("raw_size_bytes") != len(retrieved.content)
1099
+ or receipt.get("transport_evidence_digest") != retrieved.transport_evidence_digest
1100
+ or receipt.get("egress_policy_attestation") != request.egress_policy_attestation
1101
+ or receipt.get("normalized_content_sha256") != normalized_digest
1102
+ or receipt.get("normalized_size_bytes") != len(result.normalized_content)
1103
+ or (
1104
+ receipt.get("family_id"),
1105
+ receipt.get("family_version"),
1106
+ receipt.get("decode_options_digest"),
1107
+ )
1108
+ != reader_identity
1109
+ or (
1110
+ receipt.get("normalized_data_format"),
1111
+ receipt.get("normalized_filename"),
1112
+ receipt.get("normalized_media_type"),
1113
+ )
1114
+ != direct_identity
1115
+ or not isinstance(receipt.get("decode_flags"), list)
1116
+ or receipt.get("resource_caps") != expected_resource_caps
1117
+ or receipt.get("reader_budgets") != expected_reader_budgets
1118
+ or (reader is None and receipt.get("decode_flags") != [])
1119
+ or type(row_count) is not int
1120
+ or not 0 <= row_count <= limits.get("max_rows", -1)
1121
+ or not isinstance(column_names, list)
1122
+ or not 1 <= len(column_names) <= limits.get("max_columns", -1)
1123
+ or any(not isinstance(name, str) or not name for name in column_names)
1124
+ or not isinstance(receipt.get("parsed_schema_digest"), str)
1125
+ or _DIGEST.fullmatch(receipt["parsed_schema_digest"]) is None
1126
+ or receipt.get("clean_room")
1127
+ != {
1128
+ "host_platform": "linux-amd64",
1129
+ "parser_network_syscalls": "denied",
1130
+ "parser_process_creation": "denied",
1131
+ "no_new_privs": True,
1132
+ }
1133
+ or observation.get("observed_at") != request.requested_at
1134
+ or observation.get("raw_content_sha256") != retrieved.content_sha256
1135
+ or observation.get("normalized_content_sha256") != normalized_digest
1136
+ or observation.get("row_count") != row_count
1137
+ or (
1138
+ enhanced_cadence_evidence
1139
+ and (
1140
+ receipt.get("http_status_code") != terminal.status
1141
+ or receipt.get("etag_digest") != expected_etag_digest
1142
+ or receipt.get("last_modified_digest") != expected_last_modified_digest
1143
+ or not isinstance(receipt.get("row_digest"), str)
1144
+ or _DIGEST.fullmatch(receipt["row_digest"]) is None
1145
+ or observation.get("row_digest") != receipt["row_digest"]
1146
+ or observation.get("http_status_code") != terminal.status
1147
+ or observation.get("etag_digest") != expected_etag_digest
1148
+ or observation.get("last_modified_digest") != expected_last_modified_digest
1149
+ )
1150
+ )
1151
+ or result.next_watermark is not None
1152
+ ):
1153
+ _fail(
1154
+ "CRAWLER_CHILD_INVALID",
1155
+ "public HTTPS result does not bind the trusted retrieval evidence",
1156
+ )
1157
+
1158
+
1159
+ def _upload_result(capability: UploadCapability, content: bytes) -> None:
1160
+ parsed = urlsplit(capability.upload_url)
1161
+ target = parsed.path + "?" + parsed.query
1162
+ connection = http.client.HTTPSConnection(
1163
+ "storage.googleapis.com",
1164
+ 443,
1165
+ timeout=UPLOAD_TIMEOUT_SECONDS,
1166
+ context=ssl.create_default_context(),
1167
+ )
1168
+ try:
1169
+ connection.putrequest("PUT", target, skip_host=False, skip_accept_encoding=True)
1170
+ connection.putheader("Content-Length", str(len(content)))
1171
+ for name, value in capability.required_headers:
1172
+ connection.putheader(name, value)
1173
+ connection.endheaders(content)
1174
+ response = connection.getresponse()
1175
+ response_body = response.read(64 * 1024 + 1)
1176
+ if response.status not in {200, 201} or len(response_body) > 64 * 1024:
1177
+ _fail("CRAWLER_UPLOAD_FAILED", "quarantine result upload failed")
1178
+ except (OSError, ssl.SSLError, http.client.HTTPException):
1179
+ _fail("CRAWLER_UPLOAD_FAILED", "quarantine result upload failed")
1180
+ finally:
1181
+ connection.close()
1182
+
1183
+
1184
+ def _scrub_argv(argv: list[str] | None) -> None:
1185
+ target = sys.argv if argv is None else argv
1186
+ for index in range(1, len(target)):
1187
+ target[index] = "[redacted]"
1188
+
1189
+
1190
+ def crawler_job_main(argv: list[str] | None = None) -> int:
1191
+ parser = argparse.ArgumentParser(prog="mr-data-crawler-job")
1192
+ parser.add_argument("--crawler-session-id", required=True)
1193
+ arguments = parser.parse_args(argv)
1194
+ crawler_session_id = arguments.crawler_session_id
1195
+ arguments.crawler_session_id = "[redacted]"
1196
+ _scrub_argv(argv)
1197
+ bootstrap: CrawlerBootstrap | None = None
1198
+ request: HostedCrawlerRequest | None = None
1199
+ try:
1200
+ bucket = os.environ.get(RESULTS_BUCKET_ENV)
1201
+ if not isinstance(bucket, str) or _BUCKET.fullmatch(bucket) is None:
1202
+ _fail(
1203
+ "CRAWLER_RESULTS_BUCKET_REQUIRED",
1204
+ "crawler quarantine bucket pin is unavailable",
1205
+ )
1206
+ bootstrap = _redeem_crawler_bootstrap(
1207
+ crawler_session_id=crawler_session_id,
1208
+ bucket=bucket,
1209
+ )
1210
+ request = parse_crawler_request(parse_canonical_json(bootstrap.request_bytes))
1211
+ if request.adapter_id == "public.https" and request.query.get("reader_pin") is not None:
1212
+ _require_reader_memory_boundary()
1213
+ _run_crawler_warm_up(request)
1214
+ retrieved = fetch_crawler_source(
1215
+ request,
1216
+ expected_egress_policy_attestation=request.egress_policy_attestation,
1217
+ )
1218
+ normalization_input = encode_normalization_input(request, retrieved)
1219
+ result_bytes = _run_crawler_child(request, normalization_input)
1220
+ result = parse_crawler_result(parse_canonical_json(result_bytes))
1221
+ # Constructed for its __post_init__ check, not for the value: the result must
1222
+ # carry this request's digest, source id, adapter id, and adapter version.
1223
+ HostedCrawlerExchange(request=request, result=result)
1224
+ _validate_result_against_retrieval(
1225
+ request=request,
1226
+ retrieved=retrieved,
1227
+ result=result,
1228
+ )
1229
+ if canonical_json_bytes(result.to_dict()) != result_bytes:
1230
+ _fail("CRAWLER_CHILD_INVALID", "crawler result bytes are not exact canonical JSON")
1231
+ _upload_result(bootstrap.upload_capability, result_bytes)
1232
+ return 0
1233
+ except (
1234
+ CanonicalJSONError,
1235
+ HostedCrawlerJobError,
1236
+ HostedCrawlerProtocolError,
1237
+ TypeError,
1238
+ ValueError,
1239
+ subprocess.SubprocessError,
1240
+ ) as error:
1241
+ code = getattr(error, "code", "CRAWLER_JOB_FAILED")
1242
+ if bootstrap is None and code in PRE_BOOTSTRAP_FAILURE_CODES:
1243
+ # There is no upload capability before a successful bootstrap, so
1244
+ # this narrow redacted report is the only way Studio can release
1245
+ # capacity and expose the terminal cause before clients time out.
1246
+ _report_crawler_bootstrap_failure(
1247
+ crawler_session_id=crawler_session_id,
1248
+ failure_code=code,
1249
+ )
1250
+ if bootstrap is not None and request is not None and code in HOSTED_CRAWLER_FAILURE_CODES:
1251
+ failure = HostedCrawlerFailure(
1252
+ request_digest=request.digest,
1253
+ source_id=request.source_id,
1254
+ adapter_id=request.adapter_id,
1255
+ adapter_version=request.adapter_version,
1256
+ failure_code=code,
1257
+ )
1258
+ try:
1259
+ _upload_result(
1260
+ bootstrap.upload_capability,
1261
+ canonical_json_bytes(failure.to_dict()),
1262
+ )
1263
+ except HostedCrawlerJobError:
1264
+ pass
1265
+ else:
1266
+ sys.stderr.write(
1267
+ canonical_json_bytes({"status": "reported", "code": code}).decode()
1268
+ )
1269
+ sys.stderr.write("\n")
1270
+ return 0
1271
+ sys.stderr.write(canonical_json_bytes({"status": "failed", "code": code}).decode())
1272
+ sys.stderr.write("\n")
1273
+ return 1
1274
+
1275
+
1276
+ if __name__ == "__main__": # pragma: no cover
1277
+ raise SystemExit(crawler_job_main())