mostlyright-data 0.29.9__tar.gz → 0.29.10__tar.gz

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 (122) hide show
  1. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/PKG-INFO +1 -1
  2. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/pyproject.toml +1 -1
  3. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/preparing-table-search.md +28 -2
  4. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/router.py +4 -1
  5. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/session.py +68 -5
  6. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_public.py +38 -19
  7. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/.gitignore +0 -0
  8. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/README.md +0 -0
  9. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/scripts/hatch_build.py +0 -0
  10. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/SKILL.md +0 -0
  11. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/agents/openai.yaml +0 -0
  12. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/1-open-the-page-and-the-link-to-it-in-the-first-message.md +0 -0
  13. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
  14. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
  15. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
  16. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
  17. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
  18. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
  19. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
  20. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
  21. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/agent-protocol.md +0 -0
  22. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
  23. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
  24. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/boundaries.md +0 -0
  25. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
  26. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/commands.md +0 -0
  27. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
  28. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/installation-parity.md +0 -0
  29. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/live-run.md +0 -0
  30. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/making-a-table-incremental.md +0 -0
  31. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/narrating-the-run.md +0 -0
  32. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
  33. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/one-install.md +0 -0
  34. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/prediction-labels.md +0 -0
  35. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/promote.md +0 -0
  36. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/readers.md +0 -0
  37. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/receipts.md +0 -0
  38. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
  39. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
  40. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/reference-pages.md +0 -0
  41. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/required-protocol.md +0 -0
  42. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/source-credentials.md +0 -0
  43. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/sources.md +0 -0
  44. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
  45. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/transforms.md +0 -0
  46. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/user-communication-contract.md +0 -0
  47. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
  48. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  49. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/__init__.py +0 -0
  50. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/agent_protocol.py +0 -0
  51. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/canonical.py +0 -0
  52. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/formats.py +0 -0
  53. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  54. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/key_seam.py +0 -0
  55. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/page_coverage.py +0 -0
  56. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  57. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/session_probes.py +0 -0
  58. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/skill_assets.py +0 -0
  59. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/table_manifest.py +0 -0
  60. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/__init__.py +0 -0
  61. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  62. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  63. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/activity.py +0 -0
  64. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  65. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/categories.py +0 -0
  66. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/commands.py +0 -0
  67. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  68. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/download.py +0 -0
  69. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/fast_features.py +0 -0
  70. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  71. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/parity.py +0 -0
  72. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/probe.py +0 -0
  73. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/progress_rate.py +0 -0
  74. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  75. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/propose.py +0 -0
  76. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/recipe.py +0 -0
  77. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
  78. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/recipe_explain.py +0 -0
  79. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/recipe_forecast.py +0 -0
  80. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
  81. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/research.py +0 -0
  82. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/run_diagnose.py +0 -0
  83. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/runs.py +0 -0
  84. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/stream.py +0 -0
  85. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  86. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/studio_capabilities.py +0 -0
  87. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/transport.py +0 -0
  88. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
  89. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4.py +0 -0
  90. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
  91. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  92. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
  93. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  94. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  95. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
  96. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  97. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  98. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_query.py +0 -0
  99. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
  100. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
  101. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_search.py +0 -0
  102. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  103. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  104. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
  105. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
  106. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  107. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/attendance.py +0 -0
  108. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  109. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
  110. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  111. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  112. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
  113. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  114. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  115. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  116. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
  117. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/credentials.py +0 -0
  118. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/login.py +0 -0
  119. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  120. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  121. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/remediation.py +0 -0
  122. {mostlyright_data-0.29.9 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/render.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mostlyright-data
3
- Version: 0.29.9
3
+ Version: 0.29.10
4
4
  Summary: Mostly Right hosted CLI for reviewed datasets
5
5
  Project-URL: Homepage, https://mostlyright.md/
6
6
  Project-URL: Documentation, https://mostlyright.md/docs/guides/cli/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mostlyright-data"
3
- version = "0.29.9"
3
+ version = "0.29.10"
4
4
  description = "Mostly Right hosted CLI for reviewed datasets"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -15,8 +15,9 @@ mr-data search inspect --recipe recipe.json --json
15
15
 
16
16
  Local CLI help describes installed commands; it does not prove the deployed server serves them.
17
17
  For the recipe-target probe above, read JSON refusals from stderr: `schema_version` is
18
- `mr-data-error.v1`, `code` is the typed discriminator, and `error` is the detail. There is no
19
- public `http_status` field in that envelope. Treat `code: THIN_NOT_FOUND` as absent search
18
+ `mr-data-error.v1`, `code` is the typed discriminator, and `error` is the detail. Cloud refusals
19
+ also expose `http_status`, `request_id`, `detail` and `retry_after_seconds` when supplied;
20
+ Studio inspection refusals retain their typed code. Treat `code: THIN_NOT_FOUND` as absent search
20
21
  registration support only when `error` begins exactly with
21
22
  `Studio has no such resource for /v4/search/recipe-inspection`; the CLI maps this route's HTTP 404
22
23
  that way. An explicit `code: THIN_SEARCH_CAPABILITY_UNAVAILABLE` also states a capability limit.
@@ -141,3 +142,28 @@ an exact filter.
141
142
 
142
143
  This workflow opts in newly authored recipes only. It does not migrate old datasets, reauthor users'
143
144
  existing recipes, launch a full data rebuild or imply successful indexing of every table.
145
+
146
+ ## Diagnose preparation and query a published table
147
+
148
+ `preparation_state: unavailable` can mean a never-built table, a failed or cancelled job,
149
+ or an incompatible specification. Keep `job_id` from the search build receipt and run
150
+ `mr-data search status --job JOB_ID --json` to inspect the job's status and failure.
151
+ Studio is adding `preparation_detail`; print it when present rather than inferring a reason
152
+ from `unavailable`. Table status passes this additive field through unchanged.
153
+
154
+ For a published table, first run `mr-data public schema TABLE_ID --json`.
155
+ The table schema's `capabilities.search` contains `table_version_id`, `spec_digest`,
156
+ `keyword_columns`, `semantic_columns`, and optional stored `lexical`/`semantic` preparation values.
157
+ Use `keyword_columns` for `search.columns`; top-level `columns` is the non-empty output projection.
158
+ The following lexical example uses a syntactically valid illustrative digest:
159
+
160
+ ```sh
161
+ mr-data public query TABLE_ID --body '{"columns":["title"],"search":{"text":"example","columns":["title"],"spec_digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","mode":"lexical"},"limit":10}' --json
162
+ ```
163
+
164
+ Replace the illustrative columns and digest with the actual schema values. Hybrid mode additionally
165
+ needs `generation_digest` (`sha256:` plus 64 lowercase hex digits) from readiness/explain;
166
+ it is not in `capabilities.search`. Omit it for this lexical example. Public query prints
167
+ Cloud stage durations in milliseconds under `result.timings` when Server-Timing is present,
168
+ and reports HTTP status, request id and retry timing. Keep refusal `detail` with the request id.
169
+ Missing timing metadata does not establish that a stage did no work.
@@ -888,7 +888,10 @@ def _hosted(command: str, arguments: Sequence[str], *, as_json: bool) -> int:
888
888
  as_json=as_json or parsed.json,
889
889
  retry_after_seconds=getattr(refusal, "retry_after_seconds", None),
890
890
  details=_refusal_details(refusal),
891
- extra={"sessions": sessions} if sessions else None,
891
+ extra={
892
+ **getattr(refusal, "cloud_metadata", {}),
893
+ **({"sessions": sessions} if sessions else {}),
894
+ },
892
895
  )
893
896
  except Exception as error:
894
897
  code = getattr(error, "code", None)
@@ -17,6 +17,7 @@ recorded production failures on 2026-08-20.
17
17
  from __future__ import annotations
18
18
 
19
19
  import json
20
+ import math
20
21
  import re
21
22
  import time
22
23
  from collections.abc import Callable, Mapping
@@ -523,8 +524,59 @@ def stated_sentence(value: Any) -> str | None:
523
524
  return stated
524
525
 
525
526
 
527
+ def _cloud_sentence(value: Any) -> str | None:
528
+ stated = stated_sentence(value)
529
+ if stated is None:
530
+ return None
531
+ # Refusal prose must never echo bearer/device tokens or URLs (including signed URLs).
532
+ stated = re.sub(r"https?://\S+", "[redacted URL]", stated, flags=re.IGNORECASE)
533
+ stated = re.sub(r"mr_(?:cli|use)_[A-Za-z0-9_-]+", "[redacted token]", stated)
534
+ stated = re.sub(r"(?i)bearer\s+\S+", "Bearer [redacted token]", stated)
535
+ return stated
536
+
537
+
538
+ def cloud_response_metadata(status: int, parsed: Any, headers: Mapping[str, str]) -> dict[str, Any]:
539
+ """Only bounded diagnostic fields, never arbitrary response headers or authority."""
540
+ normalized = {key.lower(): value for key, value in headers.items()}
541
+ metadata: dict[str, Any] = {"http_status": status}
542
+ for request_id in (
543
+ normalized.get("x-request-id"),
544
+ parsed.get("request_id") if isinstance(parsed, dict) else None,
545
+ ):
546
+ if (
547
+ isinstance(request_id, str)
548
+ and re.fullmatch(r"[A-Za-z0-9_.:-]{1,128}", request_id)
549
+ and not request_id.startswith("mr_")
550
+ ):
551
+ metadata["request_id"] = request_id
552
+ break
553
+ raw_retry = normalized.get("retry-after", "")
554
+ if isinstance(raw_retry, str) and re.fullmatch(r"[0-9]{1,4}", raw_retry.strip()):
555
+ seconds = int(raw_retry.strip())
556
+ if 0 <= seconds <= 3600:
557
+ metadata["retry_after_seconds"] = seconds
558
+ timings: dict[str, float] = {}
559
+ for metric in normalized.get("server-timing", "")[:8192].split(",")[:64]:
560
+ match = re.fullmatch(
561
+ r"\s*([A-Za-z][A-Za-z0-9_-]{0,63})\s*;\s*dur=([0-9]+(?:\.[0-9]+)?)(?:\s*;.*)?\s*",
562
+ metric,
563
+ )
564
+ if match:
565
+ duration = float(match[2])
566
+ if math.isfinite(duration):
567
+ timings[match[1]] = duration
568
+ if timings:
569
+ metadata["timings"] = timings
570
+ return metadata
571
+
572
+
526
573
  def cloud_refusal(
527
- status: int, parsed: Any, *, act: str, absent: str | None = None
574
+ status: int,
575
+ parsed: Any,
576
+ *,
577
+ act: str,
578
+ absent: str | None = None,
579
+ headers: Mapping[str, str] | None = None,
528
580
  ) -> ThinLaneError:
529
581
  """One Cloud refusal, folded the way ``runs._studio_refusal`` folds Studio's.
530
582
 
@@ -564,7 +616,7 @@ def cloud_refusal(
564
616
  stated = parsed.get("code") if isinstance(parsed, dict) else None
565
617
  named = stated if isinstance(stated, str) and CLOUD_REFUSAL_CODE.fullmatch(stated) else None
566
618
  said = (
567
- stated_sentence(parsed.get("error")) if named is None and isinstance(parsed, dict) else None
619
+ _cloud_sentence(parsed.get("error")) if named is None and isinstance(parsed, dict) else None
568
620
  )
569
621
  if status in {401, 403}:
570
622
  if named:
@@ -588,7 +640,16 @@ def cloud_refusal(
588
640
  refusal = ThinLaneError(
589
641
  "THIN_CLOUD_REFUSED", f"{detail}. Cloud said: {said}" if said else detail
590
642
  )
591
- refusal.http_status = status # type: ignore[attr-defined]
643
+ detail = _cloud_sentence(parsed.get("detail")) if isinstance(parsed, dict) else None
644
+ if detail:
645
+ refusal.detail += f". Cloud detail: {detail}"
646
+ refusal.args = (f"{refusal.code}: {refusal.detail}",)
647
+ refusal.cloud_metadata = cloud_response_metadata(status, parsed, headers or {})
648
+ if detail:
649
+ refusal.cloud_metadata["detail"] = detail
650
+ for field, value in refusal.cloud_metadata.items():
651
+ if field != "detail":
652
+ setattr(refusal, field, value)
592
653
  return refusal
593
654
 
594
655
 
@@ -613,6 +674,7 @@ class CloudClient:
613
674
  self._credentials = credentials
614
675
  self._origin = service_origin(credentials.cloud_url, "Cloud URL", allow_loopback_http=False)
615
676
  self._transport = transport or UrlLibTransport()
677
+ self.response_metadata: dict[str, Any] = {}
616
678
 
617
679
  @property
618
680
  def cloud_url(self) -> str:
@@ -661,7 +723,7 @@ class CloudClient:
661
723
  if body is not None:
662
724
  headers["Content-Type"] = "application/json"
663
725
  encoded = json.dumps(dict(body), separators=(",", ":")).encode("utf-8")
664
- status, raw, _headers = self._transport.request(
726
+ status, raw, response_headers = self._transport.request(
665
727
  method,
666
728
  f"{self._origin}{path}",
667
729
  headers,
@@ -676,8 +738,9 @@ class CloudClient:
676
738
  )
677
739
  except CanonicalJSONError:
678
740
  parsed = None
741
+ self.response_metadata = cloud_response_metadata(status, parsed, response_headers)
679
742
  if status not in expected:
680
- raise cloud_refusal(status, parsed, act=act, absent=absent)
743
+ raise cloud_refusal(status, parsed, act=act, absent=absent, headers=response_headers)
681
744
  if not isinstance(parsed, dict):
682
745
  raise ThinLaneError(
683
746
  "THIN_RESPONSE_INVALID",
@@ -39,6 +39,7 @@ from mostlyright.data_harness.thin.session import (
39
39
  MAX_TOKEN_RESPONSE_BYTES,
40
40
  CloudClient,
41
41
  cloud_refusal,
42
+ cloud_response_metadata,
42
43
  require_device_key,
43
44
  stated_sentence,
44
45
  )
@@ -193,7 +194,6 @@ def declare_arguments(parser: argparse.ArgumentParser) -> None:
193
194
  ),
194
195
  )
195
196
 
196
-
197
197
  parser.add_argument(
198
198
  "--body",
199
199
  default=None,
@@ -287,8 +287,10 @@ class PublicCatalogClient:
287
287
  )
288
288
  error = parsed.get("error")
289
289
  detail = parsed.get("detail") if isinstance(parsed.get("detail"), str) else None
290
- code = f"THIN_CLOUD_{error}".upper() if isinstance(error, str) and error else (
291
- "THIN_CLOUD_REFUSED"
290
+ code = (
291
+ f"THIN_CLOUD_{error}".upper()
292
+ if isinstance(error, str) and error
293
+ else ("THIN_CLOUD_REFUSED")
292
294
  )
293
295
  message = detail or f"Cloud refused to {act} (HTTP {status})"
294
296
  refusal = ThinLaneError(code, message)
@@ -471,9 +473,7 @@ def _search_hit(dataset: Mapping[str, Any]) -> dict[str, Any]:
471
473
  "id": dataset.get("id"),
472
474
  "slug": dataset.get("slug"),
473
475
  "title": dataset.get("title"),
474
- "summary": (
475
- f"{summary[: SEARCH_SUMMARY_LIMIT - 1].rstrip()}…" if cut else summary
476
- ),
476
+ "summary": (f"{summary[: SEARCH_SUMMARY_LIMIT - 1].rstrip()}…" if cut else summary),
477
477
  "topics": list(dataset.get("topics") or []),
478
478
  "publisher": _publisher(publisher),
479
479
  "published_at": dataset.get("published_at"),
@@ -861,7 +861,6 @@ def _fetch(args: argparse.Namespace, client: PublicCatalogClient) -> dict[str, A
861
861
  }
862
862
 
863
863
 
864
-
865
864
  CLOUD_PUBLIC_TABLE_QUERY_PATH = "/api/cli/public/tables/{table_id}/query"
866
865
  CLOUD_PUBLIC_TABLE_DOWNLOAD_URL_PATH = "/api/cli/public/tables/{table_id}/download-url"
867
866
  QUERY_SCHEMA = f"{THIN_SCHEMA_PREFIX}-v4-public-query.v1"
@@ -885,8 +884,15 @@ MAX_PUBLIC_QUERY_RESPONSE_MEMBERS = (
885
884
  class PublicQueryClient(CloudClient):
886
885
  """Authenticated public table query + download-url over the device-key client."""
887
886
 
887
+ def __init__(self, credentials, *, transport=None) -> None:
888
+ # Let Cloud's gateway deadline return its own refusal and request id first.
889
+ super().__init__(credentials, transport=transport)
890
+ self._query_client = CloudClient(
891
+ credentials, transport=transport or UrlLibTransport(timeout=150.0)
892
+ )
893
+
888
894
  def query_table(self, table_id: str, body: Mapping[str, Any]) -> dict[str, Any]:
889
- return self.call(
895
+ answer = self._query_client.call(
890
896
  "POST",
891
897
  CLOUD_PUBLIC_TABLE_QUERY_PATH.format(table_id=table_id),
892
898
  act="query a public table",
@@ -897,6 +903,9 @@ class PublicQueryClient(CloudClient):
897
903
  max_members=MAX_PUBLIC_QUERY_RESPONSE_MEMBERS,
898
904
  )
899
905
 
906
+ self.response_metadata = self._query_client.response_metadata
907
+ return answer
908
+
900
909
  def download_url(self, table_id: str) -> dict[str, Any]:
901
910
  """Read download instructions; keep download_not_enabled distinct from missing table.
902
911
 
@@ -911,7 +920,7 @@ class PublicQueryClient(CloudClient):
911
920
  "Accept": "application/json",
912
921
  "Authorization": f"Bearer {self._credentials.raw_key}",
913
922
  }
914
- status, raw, _headers = self._transport.request(
923
+ status, raw, response_headers = self._transport.request(
915
924
  "GET",
916
925
  f"{self.cloud_url}{path}",
917
926
  headers,
@@ -941,6 +950,7 @@ class PublicQueryClient(CloudClient):
941
950
  "THIN_CLOUD_DOWNLOAD_NOT_ENABLED",
942
951
  f"Cloud refused to {act} (download_not_enabled). Cloud said: {said}",
943
952
  )
953
+ refusal.cloud_metadata = cloud_response_metadata(404, parsed, response_headers)
944
954
  refusal.http_status = 404 # type: ignore[attr-defined]
945
955
  raise refusal
946
956
  raise cloud_refusal(
@@ -948,6 +958,7 @@ class PublicQueryClient(CloudClient):
948
958
  parsed,
949
959
  act=act,
950
960
  absent="no public table has that id",
961
+ headers=response_headers,
951
962
  )
952
963
 
953
964
 
@@ -993,12 +1004,16 @@ def _query(args: argparse.Namespace, client: Any = None) -> dict[str, Any]:
993
1004
  body = _read_query_body(args)
994
1005
  selected = _auth_query_client(client if isinstance(client, PublicQueryClient) else None)
995
1006
  answer = selected.query_table(table_id, body)
1007
+ metadata = getattr(selected, "response_metadata", {})
1008
+ if "timings" in metadata:
1009
+ answer = {**answer, "timings": metadata["timings"]}
996
1010
  return {
997
1011
  "schema_version": QUERY_SCHEMA,
998
1012
  "status": "public_table_queried",
999
1013
  "lane": "hosted",
1000
1014
  "table_id": table_id,
1001
1015
  "result": answer,
1016
+ **{key: value for key, value in metadata.items() if key != "timings"},
1002
1017
  "next_step": _NEXT["public_table_queried"],
1003
1018
  }
1004
1019
 
@@ -1061,16 +1076,20 @@ def public(
1061
1076
  "THIN_REQUEST_INVALID",
1062
1077
  f"--cursor applies to public search, not public {action}",
1063
1078
  )
1064
- if action in {
1065
- "show",
1066
- "tables",
1067
- "schema",
1068
- "sample",
1069
- "fetch",
1070
- "lookup",
1071
- "query",
1072
- "download-url",
1073
- } and not args.target:
1079
+ if (
1080
+ action
1081
+ in {
1082
+ "show",
1083
+ "tables",
1084
+ "schema",
1085
+ "sample",
1086
+ "fetch",
1087
+ "lookup",
1088
+ "query",
1089
+ "download-url",
1090
+ }
1091
+ and not args.target
1092
+ ):
1074
1093
  needed = {
1075
1094
  "show": "a dataset slug",
1076
1095
  "tables": "a dataset slug",