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,691 @@
1
+ """``mr-data login`` -- RFC 8628 device authorization against the Mostly Right cloud.
2
+
3
+ Three calls, in order, against the cloud's better-auth device-authorization plugin and its device
4
+ key mint route:
5
+
6
+ 1. ``POST {cloud}/api/auth/device/code`` starts the flow and returns a ``user_code`` to show the
7
+ operator and a ``device_code`` this machine polls with.
8
+ 2. ``POST {cloud}/api/auth/device/token`` is polled on the interval the cloud names, until the
9
+ operator approves in their browser (``authorization_pending`` keeps polling, ``slow_down`` widens
10
+ the interval by RFC 8628's required five seconds, ``access_denied``/``expired_token`` stop the
11
+ flow) or it returns a Better Auth session token.
12
+ 3. ``POST {cloud}/api/device/keys``, immediately, with that session token as a bearer credential,
13
+ mints the durable ``mr_cli_`` device key this command stores. The cloud revokes the bearer
14
+ session right after this mint, so nothing else may call the cloud between steps 2 and 3.
15
+
16
+ The transport is stdlib ``urllib`` in the shape :mod:`mostlyright.data_harness.key_seam` and
17
+ :mod:`mostlyright.data_harness.hosted_bootstrap` already use -- proxies and redirects disabled, a
18
+ byte cap and a bounded timeout on every response -- rather than a new dependency. ``login`` has to
19
+ work on the same plain install ``mr-data inventory`` does.
20
+
21
+ The raw key this mints is handed to :mod:`mostlyright.data_harness.ux.credentials` the moment it
22
+ exists and is never returned from here, logged, or put in a payload; only :class:`DeviceKey`'s
23
+ ``prefix`` and ``last_four`` are.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import contextlib
29
+ import json
30
+ import os
31
+ import socket
32
+ import ssl
33
+ import sys
34
+ import time
35
+ from collections.abc import Callable, Mapping
36
+ from dataclasses import dataclass
37
+ from datetime import UTC, datetime
38
+ from pathlib import Path
39
+ from typing import Any, Protocol
40
+ from urllib.error import HTTPError
41
+ from urllib.parse import urlsplit
42
+ from urllib.request import (
43
+ HTTPRedirectHandler,
44
+ HTTPSHandler,
45
+ ProxyHandler,
46
+ Request,
47
+ build_opener,
48
+ )
49
+
50
+ from mostlyright.data_harness.canonical import CanonicalJSONError, parse_foreign_json
51
+ from mostlyright.data_harness.key_seam import LEGACY_API_KEY_ENV
52
+ from mostlyright.data_harness.ux.cloud_auth import CloudAuthClient, CloudAuthError
53
+ from mostlyright.data_harness.ux.credential_store import (
54
+ CredentialEnvelope,
55
+ CredentialStoreError,
56
+ ProtectedCredential,
57
+ credential_lock,
58
+ )
59
+ from mostlyright.data_harness.ux.credentials import (
60
+ CLOUD_URL_ENV,
61
+ DEFAULT_CLOUD_URL,
62
+ LoginError,
63
+ StoredCredentials,
64
+ credential_transaction_store,
65
+ install_credential_manifest,
66
+ refuse_existing_credentials,
67
+ )
68
+
69
+ #: The OAuth client identity every device-flow request carries. Fixed, because this command is the
70
+ #: only issuer of it.
71
+ CLIENT_ID = "mostlyright-cli"
72
+
73
+ DEVICE_CODE_PATH = "/api/auth/device/code"
74
+ DEVICE_TOKEN_PATH = "/api/auth/device/token"
75
+ DEVICE_KEYS_PATH = "/api/device/keys"
76
+
77
+ #: RFC 8628's grant type string for the token endpoint, verbatim.
78
+ DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"
79
+
80
+ #: A verdict is a small object; anything larger than this is not one. Mirrors
81
+ #: ``key_seam.MAX_VERIFY_RESPONSE_BYTES`` and ``hosted_bootstrap.MAX_BOOTSTRAP_RESPONSE_BYTES``: a
82
+ #: cap sized for what the endpoint is documented to return, not for what it could be made to send.
83
+ MAX_RESPONSE_BYTES = 16 * 1024
84
+
85
+ #: A bounded wait on each individual request. A poll that hangs is a login that never returns.
86
+ REQUEST_TIMEOUT_SECONDS = 10.0
87
+
88
+ #: A floor under whatever interval the cloud names, so a misconfigured or hostile ``interval`` of
89
+ #: zero cannot turn polling into a busy loop.
90
+ MIN_POLL_INTERVAL_SECONDS = 1.0
91
+
92
+ #: RFC 8628 section 3.5: on ``slow_down`` the client MUST widen the polling interval by at least
93
+ #: five seconds for the rest of the session.
94
+ SLOW_DOWN_INCREMENT_SECONDS = 5.0
95
+
96
+ MIN_DEVICE_NAME_LENGTH = 1
97
+ MAX_DEVICE_NAME_LENGTH = 100
98
+
99
+ LOGIN_SCHEMA_VERSION = "mr-data-login.v1"
100
+
101
+
102
+ def _with_environment_authority(payload: dict[str, Any]) -> dict[str, Any]:
103
+ """State the surviving parent-process authority without ever reading its value."""
104
+
105
+ if LEGACY_API_KEY_ENV in os.environ:
106
+ payload["environment_authority"] = "remains_effective"
107
+ return payload
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class DeviceAuthorization:
112
+ """The device flow's starting grant: what to show the operator, and what to poll with."""
113
+
114
+ device_code: str
115
+ user_code: str
116
+ verification_uri: str
117
+ verification_uri_complete: str
118
+ expires_in: int
119
+ interval: float
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class DeviceSession:
124
+ """A Better Auth session token, good for exactly one thing: minting the device key."""
125
+
126
+ access_token: str
127
+
128
+ def __repr__(self) -> str:
129
+ return "DeviceSession(access_token='<redacted>')"
130
+
131
+
132
+ @dataclass(frozen=True)
133
+ class DeviceKey:
134
+ """The durable ``mr_cli_`` credential the mint route hands back."""
135
+
136
+ key_id: str
137
+ raw_key: str
138
+ prefix: str
139
+ last_four: str
140
+
141
+ def __repr__(self) -> str:
142
+ return (
143
+ f"DeviceKey(key_id={self.key_id!r}, prefix={self.prefix!r}, "
144
+ f"last_four={self.last_four!r}, raw_key='<redacted>')"
145
+ )
146
+
147
+
148
+ class DeviceFlowTransport(Protocol):
149
+ """Minimal redirect-free, proxy-free transport -- the same shape ``key_seam`` and
150
+ ``hosted_bootstrap`` already define, so a stub written for one reads as a stub for any."""
151
+
152
+ def request(
153
+ self,
154
+ method: str,
155
+ url: str,
156
+ headers: Mapping[str, str],
157
+ body: bytes | None,
158
+ maximum: int,
159
+ ) -> tuple[int, bytes]: ...
160
+
161
+
162
+ class _NoRedirect(HTTPRedirectHandler):
163
+ """Refuse every redirect; a followed one would re-send the bearer token to another host."""
164
+
165
+ def redirect_request(
166
+ self,
167
+ request: Request,
168
+ file_pointer: Any,
169
+ code: int,
170
+ message: str,
171
+ headers: Any,
172
+ new_url: str,
173
+ ) -> None:
174
+ return None
175
+
176
+
177
+ class UrlLibDeviceFlowTransport:
178
+ """Small stdlib transport with ambient proxies and redirects disabled."""
179
+
180
+ def __init__(self) -> None:
181
+ self._opener = build_opener(
182
+ ProxyHandler({}),
183
+ HTTPSHandler(context=ssl.create_default_context()),
184
+ _NoRedirect(),
185
+ )
186
+
187
+ def request(
188
+ self,
189
+ method: str,
190
+ url: str,
191
+ headers: Mapping[str, str],
192
+ body: bytes | None,
193
+ maximum: int,
194
+ ) -> tuple[int, bytes]:
195
+ request = Request(url, data=body, headers=dict(headers), method=method)
196
+ try:
197
+ response = self._opener.open(request, timeout=REQUEST_TIMEOUT_SECONDS)
198
+ except HTTPError as error:
199
+ response = error
200
+ with response:
201
+ status = int(response.status)
202
+ content_length = response.headers.get("Content-Length")
203
+ if content_length is not None:
204
+ try:
205
+ declared = int(content_length)
206
+ except ValueError as error:
207
+ raise LoginError(
208
+ "LOGIN_TRANSPORT_FAILED",
209
+ "the cloud sent an invalid content length",
210
+ ) from error
211
+ if declared < 0 or declared > maximum:
212
+ raise LoginError(
213
+ "LOGIN_TRANSPORT_FAILED",
214
+ "the cloud's response exceeds its byte limit",
215
+ )
216
+ raw = response.read(maximum + 1)
217
+ if len(raw) > maximum:
218
+ raise LoginError(
219
+ "LOGIN_TRANSPORT_FAILED", "the cloud's response exceeds its byte limit"
220
+ )
221
+ return status, raw
222
+
223
+
224
+ def _https_url(value: str, label: str) -> str:
225
+ """Return ``value`` when it is a plain absolute https URL, otherwise refuse it.
226
+
227
+ Copied rather than imported, the way ``key_seam._https_url`` and
228
+ ``hosted_bootstrap._https_url`` are each their own copy of the same policy: importing across
229
+ those modules would pull in code this command does not otherwise touch, for a validation that
230
+ has to stay identical without needing to be the same function.
231
+ """
232
+
233
+ if not isinstance(value, str) or not 1 <= len(value) <= 2048:
234
+ raise LoginError("LOGIN_CONFIG_INVALID", f"{label} is not a usable URL")
235
+ if value.endswith("/"):
236
+ raise LoginError("LOGIN_CONFIG_INVALID", f"{label} must not end with a slash")
237
+ parsed = urlsplit(value)
238
+ if (
239
+ parsed.scheme != "https"
240
+ or not parsed.hostname
241
+ or parsed.username is not None
242
+ or parsed.password is not None
243
+ or parsed.fragment
244
+ ):
245
+ raise LoginError(
246
+ "LOGIN_CONFIG_INVALID",
247
+ f"{label} must be an absolute https URL without user information or fragment",
248
+ )
249
+ return value
250
+
251
+
252
+ def _device_name(value: str | None) -> str:
253
+ name = value if value is not None else socket.gethostname()
254
+ if not isinstance(name, str):
255
+ raise LoginError("LOGIN_CONFIG_INVALID", "a device name must be text")
256
+ if not MIN_DEVICE_NAME_LENGTH <= len(name) <= MAX_DEVICE_NAME_LENGTH:
257
+ raise LoginError(
258
+ "LOGIN_CONFIG_INVALID",
259
+ f"a device name must be {MIN_DEVICE_NAME_LENGTH}-{MAX_DEVICE_NAME_LENGTH} characters",
260
+ )
261
+ return name
262
+
263
+
264
+ def _post_json(
265
+ transport: DeviceFlowTransport,
266
+ url: str,
267
+ payload: Mapping[str, Any],
268
+ *,
269
+ headers: Mapping[str, str] | None = None,
270
+ ) -> tuple[int, Any]:
271
+ """POST one JSON body and return ``(status, parsed_body_or_None)``.
272
+
273
+ Never raises for a non-2xx status or a body that fails to parse: the three device-flow callers
274
+ each read those differently (a poll's ``authorization_pending`` is not a failure; a mint
275
+ failure means something else again), so only what none of them can do anything useful with --
276
+ the request never reaching the cloud at all, or the byte cap being exceeded -- is collapsed
277
+ into one typed failure here.
278
+ """
279
+
280
+ body = json.dumps(dict(payload), sort_keys=True).encode("utf-8")
281
+ request_headers = {"Content-Type": "application/json", "Accept": "application/json"}
282
+ request_headers.update(headers or {})
283
+ try:
284
+ status, raw = transport.request("POST", url, request_headers, body, MAX_RESPONSE_BYTES)
285
+ except LoginError:
286
+ raise
287
+ except Exception as error:
288
+ raise LoginError(
289
+ "LOGIN_TRANSPORT_FAILED", f"the cloud could not be reached: {error}"
290
+ ) from error
291
+ if not raw:
292
+ return status, None
293
+ try:
294
+ # `parse_foreign_json`, not `parse_json`: this body is authored by a separate cloud
295
+ # codebase, not by this harness, and the Harness's own integer-only canonical form is not a
296
+ # contract that document ever signed up to.
297
+ return status, parse_foreign_json(raw)
298
+ except CanonicalJSONError:
299
+ return status, None
300
+
301
+
302
+ def _required_str(payload: Mapping[str, Any], field: str) -> str:
303
+ value = payload.get(field)
304
+ if not isinstance(value, str) or not value:
305
+ raise LoginError(
306
+ "LOGIN_TRANSPORT_FAILED", f"the cloud's response is missing a usable {field!r}"
307
+ )
308
+ return value
309
+
310
+
311
+ def _required_positive_int(payload: Mapping[str, Any], field: str) -> int:
312
+ value = payload.get(field)
313
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
314
+ raise LoginError(
315
+ "LOGIN_TRANSPORT_FAILED", f"the cloud's response is missing a usable {field!r}"
316
+ )
317
+ return value
318
+
319
+
320
+ def _positive_number(payload: Mapping[str, Any], field: str, *, default: float) -> float:
321
+ value = payload.get(field, default)
322
+ if isinstance(value, bool) or not isinstance(value, int | float) or value <= 0:
323
+ raise LoginError(
324
+ "LOGIN_TRANSPORT_FAILED", f"the cloud's response is missing a usable {field!r}"
325
+ )
326
+ return float(value)
327
+
328
+
329
+ def request_device_code(cloud_url: str, *, transport: DeviceFlowTransport) -> DeviceAuthorization:
330
+ """Start the device flow: ``POST {cloud}/api/auth/device/code``."""
331
+
332
+ url = f"{cloud_url}{DEVICE_CODE_PATH}"
333
+ status, parsed = _post_json(transport, url, {"client_id": CLIENT_ID})
334
+ if status != 200 or not isinstance(parsed, dict):
335
+ raise LoginError(
336
+ "LOGIN_TRANSPORT_FAILED",
337
+ f"the cloud could not start a device sign-in (HTTP {status})",
338
+ )
339
+ return DeviceAuthorization(
340
+ device_code=_required_str(parsed, "device_code"),
341
+ user_code=_required_str(parsed, "user_code"),
342
+ verification_uri=_required_str(parsed, "verification_uri"),
343
+ verification_uri_complete=_required_str(parsed, "verification_uri_complete"),
344
+ expires_in=_required_positive_int(parsed, "expires_in"),
345
+ interval=_positive_number(parsed, "interval", default=MIN_POLL_INTERVAL_SECONDS * 5),
346
+ )
347
+
348
+
349
+ def poll_for_token(
350
+ cloud_url: str,
351
+ authorization: DeviceAuthorization,
352
+ *,
353
+ transport: DeviceFlowTransport,
354
+ clock: Callable[[], float] = time.monotonic,
355
+ sleep: Callable[[float], None] = time.sleep,
356
+ ) -> DeviceSession:
357
+ """Poll ``POST {cloud}/api/auth/device/token`` until approval, denial, or expiry.
358
+
359
+ Waits ``interval`` seconds before every poll, including the first -- the operator cannot have
360
+ approved before they have seen the code -- and widens ``interval`` by
361
+ :data:`SLOW_DOWN_INCREMENT_SECONDS` on every ``slow_down`` for the rest of this call, per
362
+ RFC 8628 section 3.5.
363
+ """
364
+
365
+ deadline = clock() + authorization.expires_in
366
+ interval = max(authorization.interval, MIN_POLL_INTERVAL_SECONDS)
367
+ url = f"{cloud_url}{DEVICE_TOKEN_PATH}"
368
+ payload = {
369
+ "grant_type": DEVICE_CODE_GRANT_TYPE,
370
+ "device_code": authorization.device_code,
371
+ "client_id": CLIENT_ID,
372
+ }
373
+ while True:
374
+ if clock() >= deadline:
375
+ raise LoginError(
376
+ "LOGIN_EXPIRED",
377
+ "the sign-in code expired before it was approved; run mr-data login again",
378
+ )
379
+ sleep(interval)
380
+ status, parsed = _post_json(transport, url, payload)
381
+ if not isinstance(parsed, dict):
382
+ raise LoginError(
383
+ "LOGIN_TRANSPORT_FAILED",
384
+ f"the cloud's sign-in response could not be read (HTTP {status})",
385
+ )
386
+ error = parsed.get("error")
387
+ if error == "authorization_pending":
388
+ continue
389
+ if error == "slow_down":
390
+ interval += SLOW_DOWN_INCREMENT_SECONDS
391
+ continue
392
+ if error == "access_denied":
393
+ raise LoginError(
394
+ "LOGIN_ACCESS_DENIED",
395
+ "sign-in was denied in the browser; run mr-data login again to retry",
396
+ )
397
+ if error == "expired_token":
398
+ raise LoginError(
399
+ "LOGIN_EXPIRED",
400
+ "the sign-in code expired before it was approved; run mr-data login again",
401
+ )
402
+ if isinstance(error, str) and error:
403
+ raise LoginError("LOGIN_TRANSPORT_FAILED", f"the cloud refused the sign-in: {error}")
404
+ if status == 200:
405
+ return DeviceSession(access_token=_required_str(parsed, "access_token"))
406
+ raise LoginError(
407
+ "LOGIN_TRANSPORT_FAILED",
408
+ f"the cloud's sign-in response was not understood (HTTP {status})",
409
+ )
410
+
411
+
412
+ def mint_device_key(
413
+ cloud_url: str,
414
+ session: DeviceSession,
415
+ *,
416
+ device_name: str,
417
+ transport: DeviceFlowTransport,
418
+ ) -> DeviceKey:
419
+ """``POST {cloud}/api/device/keys``, the one call this session's bearer token is good for.
420
+
421
+ Must run immediately after :func:`poll_for_token` returns: the cloud revokes the bearer session
422
+ right after this mint, so no other network call may sit between the two.
423
+ """
424
+
425
+ status, parsed = _post_json(
426
+ transport,
427
+ f"{cloud_url}{DEVICE_KEYS_PATH}",
428
+ {"deviceName": device_name},
429
+ headers={"Authorization": f"Bearer {session.access_token}"},
430
+ )
431
+ if status != 201 or not isinstance(parsed, dict):
432
+ raise LoginError(
433
+ "LOGIN_MINT_FAILED",
434
+ f"signed in, but the cloud could not issue a device key (HTTP {status}); "
435
+ "run mr-data login again",
436
+ )
437
+ try:
438
+ return DeviceKey(
439
+ key_id=_required_str(parsed, "id"),
440
+ raw_key=_required_str(parsed, "rawKey"),
441
+ prefix=_required_str(parsed, "prefix"),
442
+ last_four=_required_str(parsed, "lastFour"),
443
+ )
444
+ except LoginError as error:
445
+ raise LoginError(
446
+ "LOGIN_MINT_FAILED",
447
+ "signed in, but the cloud's device key response is missing fields it must have; "
448
+ "run mr-data login again",
449
+ ) from error
450
+
451
+
452
+ def _now_iso() -> str:
453
+ return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
454
+
455
+
456
+ def announce_device_code(authorization: DeviceAuthorization) -> None:
457
+ """Print the code and address prominently, on stderr, so it survives a piped stdout.
458
+
459
+ Plain text only, and only wired up from plain-mode command dispatch: ``--json`` mode calls
460
+ :func:`run_login` without an announcer, because a caller asking for one machine-readable object
461
+ should get exactly one, on stdout, when this finishes -- not an interstitial line ahead of it.
462
+ """
463
+
464
+ print(file=sys.stderr)
465
+ print(f" {authorization.user_code}", file=sys.stderr)
466
+ print(file=sys.stderr)
467
+ print(
468
+ f"Open {authorization.verification_uri_complete} to approve this device.",
469
+ file=sys.stderr,
470
+ )
471
+ print("Waiting for approval...", file=sys.stderr, flush=True)
472
+
473
+
474
+ def run_login(
475
+ *,
476
+ cloud_url: str | None = None,
477
+ device_name: str | None = None,
478
+ force: bool = False,
479
+ config_dir: Path | None = None,
480
+ credential_store: str | None = None,
481
+ transport: DeviceFlowTransport | None = None,
482
+ clock: Callable[[], float] = time.monotonic,
483
+ sleep: Callable[[float], None] = time.sleep,
484
+ open_browser: Callable[[str], bool] | None = None,
485
+ announce: Callable[[DeviceAuthorization], None] | None = None,
486
+ cloud_client_factory: Callable[[str, str], Any] | None = None,
487
+ ) -> dict[str, Any]:
488
+ """Run the whole device flow and store the resulting credential.
489
+
490
+ ``open_browser`` defaults to :func:`webbrowser.open`, resolved lazily so importing this module
491
+ never imports ``webbrowser``'s platform-probing machinery for callers -- ``whoami`` among them
492
+ -- that never open a browser at all.
493
+ """
494
+
495
+ if open_browser is None:
496
+ import webbrowser
497
+
498
+ open_browser = webbrowser.open
499
+
500
+ resolved_cloud_url = _https_url(
501
+ cloud_url if cloud_url is not None else _configured_cloud_url(), "the cloud address"
502
+ )
503
+ resolved_device_name = _device_name(device_name)
504
+ if not force:
505
+ # Preserve the compatibility preflight for every occupied control path, including a
506
+ # dangling link. The durable lease below remains the authoritative concurrent gate.
507
+ refuse_existing_credentials(config_dir=config_dir)
508
+ try:
509
+ opened = credential_transaction_store(
510
+ config_dir=config_dir, credential_store=credential_store
511
+ )
512
+ except LoginError:
513
+ raise
514
+ if opened is None: # Defensive: the helper returns a selected empty store for first login.
515
+ raise LoginError(
516
+ "LOGIN_CREDENTIAL_STORE_UNAVAILABLE", "the credential store is unavailable"
517
+ )
518
+ store, manifest_path = opened
519
+ try:
520
+ current = store.load()
521
+ if current is not None and current.state == "active" and not force:
522
+ refuse_existing_credentials(config_dir=config_dir)
523
+ operation = "rotate" if force else "login"
524
+ pending = store.acquire_login_lease(
525
+ operation=operation,
526
+ requested_device_name=resolved_device_name,
527
+ created_at=_now_iso(),
528
+ )
529
+ if not manifest_path.exists():
530
+ install_credential_manifest(manifest_path, store.backend.name)
531
+ except CredentialStoreError as error:
532
+ raise LoginError("LOGIN_CREDENTIAL_RECOVERY_REQUIRED", error.detail) from None
533
+ live_transport = transport or UrlLibDeviceFlowTransport()
534
+
535
+ authorization = request_device_code(resolved_cloud_url, transport=live_transport)
536
+ if announce is not None:
537
+ announce(authorization)
538
+ with contextlib.suppress(Exception):
539
+ # Best-effort: a headless box or a browser that fails to launch must not stop a flow whose
540
+ # answer is printed either way.
541
+ open_browser(authorization.verification_uri_complete)
542
+
543
+ session = poll_for_token(
544
+ resolved_cloud_url, authorization, transport=live_transport, clock=clock, sleep=sleep
545
+ )
546
+ # No network call of any kind may sit between the line above and the one below -- see
547
+ # `mint_device_key`'s docstring: the cloud revokes this bearer session immediately after mint.
548
+ key = mint_device_key(
549
+ resolved_cloud_url, session, device_name=resolved_device_name, transport=live_transport
550
+ )
551
+
552
+ credentials = StoredCredentials(
553
+ cloud_url=resolved_cloud_url,
554
+ key_id=key.key_id,
555
+ prefix=key.prefix,
556
+ last_four=key.last_four,
557
+ device_name=resolved_device_name,
558
+ created_at=_now_iso(),
559
+ raw_key=key.raw_key,
560
+ )
561
+ minted = credentials._protected()
562
+ try:
563
+ installed = store.complete_login_lease(nonce=pending.login_pending.nonce, minted=minted)
564
+ except CredentialStoreError as error:
565
+ cleanup = _self_revoke_after_mint(minted, cloud_client_factory=cloud_client_factory)
566
+ raise LoginError(
567
+ "LOGIN_MINT_CLEANUP_REQUIRED",
568
+ "the minted device key could not be stored; remote cleanup is " + cleanup,
569
+ ) from error
570
+
571
+ if installed.state == "active":
572
+ if force:
573
+ return _with_environment_authority(
574
+ {
575
+ "schema_version": "mr-data-auth-rotate.v1",
576
+ "status": "rotated",
577
+ "remote_revocation": "proved",
578
+ "local_state": "active",
579
+ **credentials.to_receipt(),
580
+ }
581
+ )
582
+ return _with_environment_authority(
583
+ {
584
+ "schema_version": LOGIN_SCHEMA_VERSION,
585
+ "status": "device_authenticated",
586
+ **credentials.to_receipt(),
587
+ "credentials_path": str(manifest_path),
588
+ }
589
+ )
590
+
591
+ assert installed.active is not None and installed.prior is not None
592
+ try:
593
+ _revoke_exact(
594
+ installed.prior,
595
+ cloud_client_factory=cloud_client_factory,
596
+ )
597
+ except CloudAuthError as error:
598
+ return _with_environment_authority(
599
+ {
600
+ "schema_version": "mr-data-auth-rotate.v1",
601
+ "status": "recovery_required",
602
+ "remote_revocation": "unproved",
603
+ "local_state": "revoke_pending",
604
+ "error_code": error.code,
605
+ }
606
+ )
607
+ try:
608
+ with credential_lock(store.config_dir):
609
+ cleanup_pending = CredentialEnvelope.pending_cleanup(
610
+ generation=installed.generation + 1,
611
+ active=installed.active,
612
+ prior=installed.prior,
613
+ )
614
+ store.replace_under_held_lock(
615
+ cleanup_pending,
616
+ expected_generation=installed.generation,
617
+ expected_states=frozenset({"revoke_pending"}),
618
+ )
619
+ active = CredentialEnvelope.active_envelope(
620
+ generation=cleanup_pending.generation + 1,
621
+ active=cleanup_pending.active,
622
+ )
623
+ store.replace_under_held_lock(
624
+ active,
625
+ expected_generation=cleanup_pending.generation,
626
+ expected_states=frozenset({"cleanup_pending"}),
627
+ )
628
+ except CredentialStoreError as error:
629
+ return _with_environment_authority(
630
+ {
631
+ "schema_version": "mr-data-auth-rotate.v1",
632
+ "status": "recovery_required",
633
+ "remote_revocation": "proved",
634
+ "local_state": "cleanup_pending",
635
+ "error_code": error.code,
636
+ }
637
+ )
638
+ return _with_environment_authority(
639
+ {
640
+ "schema_version": "mr-data-auth-rotate.v1",
641
+ "status": "rotated",
642
+ "remote_revocation": "proved",
643
+ "local_state": "active",
644
+ **credentials.to_receipt(),
645
+ }
646
+ )
647
+
648
+
649
+ def _revoke_exact(
650
+ credential: ProtectedCredential,
651
+ *,
652
+ cloud_client_factory: Callable[[str, str], Any] | None,
653
+ ) -> None:
654
+ client = (CloudAuthClient if cloud_client_factory is None else cloud_client_factory)(
655
+ credential.cloud_url, credential.raw_key
656
+ )
657
+ client.revoke(credential.key_id, exact_secret_same_key_replay=True)
658
+
659
+
660
+ def _self_revoke_after_mint(
661
+ credential: ProtectedCredential,
662
+ *,
663
+ cloud_client_factory: Callable[[str, str], Any] | None,
664
+ ) -> str:
665
+ try:
666
+ _revoke_exact(credential, cloud_client_factory=cloud_client_factory)
667
+ except CloudAuthError:
668
+ return "unproved"
669
+ return "proved"
670
+
671
+
672
+ def _configured_cloud_url() -> str:
673
+ return os.environ.get(CLOUD_URL_ENV) or DEFAULT_CLOUD_URL
674
+
675
+
676
+ __all__ = [
677
+ "CLIENT_ID",
678
+ "CLOUD_URL_ENV",
679
+ "DEFAULT_CLOUD_URL",
680
+ "LOGIN_SCHEMA_VERSION",
681
+ "DeviceAuthorization",
682
+ "DeviceFlowTransport",
683
+ "DeviceKey",
684
+ "DeviceSession",
685
+ "UrlLibDeviceFlowTransport",
686
+ "announce_device_code",
687
+ "mint_device_key",
688
+ "poll_for_token",
689
+ "request_device_code",
690
+ "run_login",
691
+ ]