mostlyright-data 0.25.8__tar.gz → 0.25.9__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 (113) hide show
  1. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/PKG-INFO +1 -1
  2. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/pyproject.toml +1 -1
  3. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/canonical.py +19 -5
  4. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/runs.py +5 -2
  5. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_artifacts.py +13 -6
  6. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_query.py +28 -3
  7. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_tables.py +8 -0
  8. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/.gitignore +0 -0
  9. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/README.md +0 -0
  10. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/scripts/hatch_build.py +0 -0
  11. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/SKILL.md +0 -0
  12. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/agents/openai.yaml +0 -0
  13. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/1-open-the-page-and-the-link-to-it-in-the-first-message.md +0 -0
  14. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
  15. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
  16. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
  17. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
  18. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
  19. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
  20. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
  21. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
  22. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/agent-protocol.md +0 -0
  23. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
  24. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
  25. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/boundaries.md +0 -0
  26. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
  27. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/commands.md +0 -0
  28. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
  29. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/installation-parity.md +0 -0
  30. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/live-run.md +0 -0
  31. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/narrating-the-run.md +0 -0
  32. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
  33. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/one-install.md +0 -0
  34. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/prediction-labels.md +0 -0
  35. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/promote.md +0 -0
  36. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/readers.md +0 -0
  37. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/receipts.md +0 -0
  38. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
  39. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
  40. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/reference-pages.md +0 -0
  41. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/required-protocol.md +0 -0
  42. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/source-credentials.md +0 -0
  43. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/sources.md +0 -0
  44. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
  45. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/transforms.md +0 -0
  46. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/user-communication-contract.md +0 -0
  47. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
  48. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  49. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/__init__.py +0 -0
  50. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/agent_protocol.py +0 -0
  51. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/formats.py +0 -0
  52. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  53. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/key_seam.py +0 -0
  54. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/page_coverage.py +0 -0
  55. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  56. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/session_probes.py +0 -0
  57. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/skill_assets.py +0 -0
  58. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/table_manifest.py +0 -0
  59. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/__init__.py +0 -0
  60. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  61. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  62. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/activity.py +0 -0
  63. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  64. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/categories.py +0 -0
  65. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/commands.py +0 -0
  66. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  67. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/download.py +0 -0
  68. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  69. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/parity.py +0 -0
  70. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/probe.py +0 -0
  71. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  72. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/propose.py +0 -0
  73. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/recipe.py +0 -0
  74. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
  75. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
  76. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/research.py +0 -0
  77. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/router.py +0 -0
  78. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/session.py +0 -0
  79. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/stream.py +0 -0
  80. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  81. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/transport.py +0 -0
  82. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
  83. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4.py +0 -0
  84. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  85. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
  86. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  87. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  88. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
  89. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  90. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  91. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_public.py +0 -0
  92. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
  93. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
  94. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  95. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  96. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
  97. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  98. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/attendance.py +0 -0
  99. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  100. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
  101. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  102. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  103. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
  104. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  105. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  106. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  107. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
  108. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/credentials.py +0 -0
  109. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/login.py +0 -0
  110. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  111. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  112. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/remediation.py +0 -0
  113. {mostlyright_data-0.25.8 → mostlyright_data-0.25.9}/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.25.8
3
+ Version: 0.25.9
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.25.8"
3
+ version = "0.25.9"
4
4
  description = "Mostly Right hosted CLI for reviewed datasets"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -34,7 +34,9 @@ def parse_json(value: str | bytes | bytearray | memoryview) -> Any:
34
34
  return _parse(value, fractions="reject")
35
35
 
36
36
 
37
- def parse_foreign_json(value: str | bytes | bytearray | memoryview) -> Any:
37
+ def parse_foreign_json(
38
+ value: str | bytes | bytearray | memoryview, *, max_members: int = MAX_CANONICAL_MEMBERS
39
+ ) -> Any:
38
40
  """Parse a third-party document with the same hardening, but accept fractional numbers.
39
41
 
40
42
  The Harness data model is integer-only, and :func:`parse_json` enforces that because every
@@ -54,12 +56,20 @@ def parse_foreign_json(value: str | bytes | bytearray | memoryview) -> Any:
54
56
  a parser but can never be sealed, digested or written into a canonical document. That is the
55
57
  boundary this pair of functions draws: read what the world publishes, write only what the model
56
58
  admits.
59
+
60
+ ``max_members`` raises the member ceiling for one document whose size a route already bounds,
61
+ such as a table manifest listing thousands of parts.
57
62
  """
58
63
 
59
- return _parse(value, fractions="accept")
64
+ return _parse(value, fractions="accept", max_members=max_members)
60
65
 
61
66
 
62
- def _parse(value: str | bytes | bytearray | memoryview, *, fractions: str) -> Any:
67
+ def _parse(
68
+ value: str | bytes | bytearray | memoryview,
69
+ *,
70
+ fractions: str,
71
+ max_members: int = MAX_CANONICAL_MEMBERS,
72
+ ) -> Any:
63
73
  if isinstance(value, str):
64
74
  text = value
65
75
  elif isinstance(value, (bytes, bytearray, memoryview)):
@@ -106,6 +116,7 @@ def _parse(value: str | bytes | bytearray | memoryview, *, fractions: str) -> An
106
116
  depth=0,
107
117
  members=[0],
108
118
  accept_fractions=accept_fractions,
119
+ max_members=max_members,
109
120
  )
110
121
 
111
122
 
@@ -171,6 +182,7 @@ def _normalize_json(
171
182
  depth: int,
172
183
  members: list[int],
173
184
  accept_fractions: bool = False,
185
+ max_members: int = MAX_CANONICAL_MEMBERS,
174
186
  ) -> Any:
175
187
  if depth > MAX_CANONICAL_DEPTH:
176
188
  raise CanonicalJSONError(
@@ -179,11 +191,11 @@ def _normalize_json(
179
191
  f"nesting exceeds {MAX_CANONICAL_DEPTH} levels",
180
192
  )
181
193
  members[0] += 1
182
- if members[0] > MAX_CANONICAL_MEMBERS:
194
+ if members[0] > max_members:
183
195
  raise CanonicalJSONError(
184
196
  "JSON_MEMBERS",
185
197
  path,
186
- f"value exceeds {MAX_CANONICAL_MEMBERS} members",
198
+ f"value exceeds {max_members} members",
187
199
  )
188
200
 
189
201
  if value is None or type(value) is bool:
@@ -236,6 +248,7 @@ def _normalize_json(
236
248
  depth=depth + 1,
237
249
  members=members,
238
250
  accept_fractions=accept_fractions,
251
+ max_members=max_members,
239
252
  )
240
253
  return result
241
254
  if isinstance(value, (list, tuple)):
@@ -246,6 +259,7 @@ def _normalize_json(
246
259
  depth=depth + 1,
247
260
  members=members,
248
261
  accept_fractions=accept_fractions,
262
+ max_members=max_members,
249
263
  )
250
264
  for index, item in enumerate(value)
251
265
  ]
@@ -28,6 +28,7 @@ from urllib.parse import urlencode
28
28
  from uuid import UUID
29
29
 
30
30
  from mostlyright.data_harness.canonical import (
31
+ MAX_CANONICAL_MEMBERS,
31
32
  CanonicalJSONError,
32
33
  canonical_json_bytes,
33
34
  parse_foreign_json,
@@ -235,6 +236,8 @@ class StudioApiClient:
235
236
  expected: tuple[int, ...],
236
237
  shape: type = dict,
237
238
  response_headers: dict[str, str] | None = None,
239
+ maximum: int = MAX_JSON_RESPONSE_BYTES,
240
+ max_members: int = MAX_CANONICAL_MEMBERS,
238
241
  ) -> Any:
239
242
  headers = {
240
243
  "Accept": "application/json",
@@ -250,7 +253,7 @@ class StudioApiClient:
250
253
  url = f"{self._session.studio_base_url}{path}{_query_string(query)}"
251
254
  try:
252
255
  status, raw, answered_headers = self._transport.request(
253
- method, url, headers, raw_body, MAX_JSON_RESPONSE_BYTES
256
+ method, url, headers, raw_body, maximum
254
257
  )
255
258
  if response_headers is not None:
256
259
  response_headers.update(answered_headers)
@@ -270,7 +273,7 @@ class StudioApiClient:
270
273
  # "Studio did not answer with an object". Nothing read here can be sealed either way:
271
274
  # `canonical_json_bytes` still refuses a float, so a value parsed here can be shown
272
275
  # and can never reach a digest.
273
- parsed = parse_foreign_json(raw)
276
+ parsed = parse_foreign_json(raw, max_members=max_members)
274
277
  except CanonicalJSONError:
275
278
  parsed = None
276
279
  if status not in expected:
@@ -155,10 +155,10 @@ CHECK_SCOPE_VERSION = "version"
155
155
  #: arrives before a byte is fetched.
156
156
  MAX_READ_ARTIFACT_BYTES = 8 * 1024 * 1024
157
157
 
158
- #: Studio caps an artifact page at two hundred and pages with ``X-Next-Cursor``. A run produces at
159
- #: most a handful of artifacts, so a second page is already a surprise; the ceiling is here so a
160
- #: server answering with an endless cursor is a refusal rather than an endless loop.
161
- MAX_ARTIFACT_PAGES = 8
158
+ #: Studio caps an artifact page at two hundred and pages with ``X-Next-Cursor``. A version may name
159
+ #: 4,096 parts beside its manifest, receipt and profile, which is twenty-one pages; the ceiling
160
+ #: leaves room above that so an endless cursor is a refusal rather than an endless loop.
161
+ MAX_ARTIFACT_PAGES = 32
162
162
 
163
163
  #: How many signed part reads one page of the version route is asked for. The command's own
164
164
  #: maximum, so the fewest pages are spent on the widest version.
@@ -394,9 +394,16 @@ def _listing(client: StudioV4ArtifactClient, run_id: str) -> list[dict[str, Any]
394
394
  for _page in range(MAX_ARTIFACT_PAGES):
395
395
  headers: dict[str, str] = {}
396
396
  rows.extend(client.artifacts(run_id, cursor=cursor, response_headers=headers))
397
- cursor = headers.get("X-Next-Cursor") or headers.get("x-next-cursor")
398
- if not cursor:
397
+ following = headers.get("X-Next-Cursor") or headers.get("x-next-cursor")
398
+ if not following:
399
399
  return rows
400
+ # The cursor is the last artifact id of the page and the listing is ordered by it.
401
+ if cursor is not None and following <= cursor:
402
+ raise ThinLaneError(
403
+ "THIN_RESPONSE_INVALID",
404
+ "this run's artifact listing answered with a cursor that did not advance",
405
+ )
406
+ cursor = following
400
407
  raise ThinLaneError(
401
408
  "THIN_RESPONSE_INVALID",
402
409
  f"this run's artifact listing did not end within {MAX_ARTIFACT_PAGES} pages",
@@ -52,7 +52,7 @@ from mostlyright.data_harness.thin.session import (
52
52
  open_studio_session,
53
53
  team_dashboard_url,
54
54
  )
55
- from mostlyright.data_harness.thin.transport import ThinLaneError
55
+ from mostlyright.data_harness.thin.transport import ThinLaneError, UrlLibTransport
56
56
  from mostlyright.data_harness.thin.v4 import (
57
57
  GET_RECIPE_PATH,
58
58
  GET_RUN_PATH,
@@ -103,6 +103,12 @@ QUERY_POLL_MAX_SECONDS = 2.0
103
103
  #: has been minted -- the reason every early refusal in this repository exists.
104
104
  MAX_STATEMENT_BYTES = 64 * 1024
105
105
 
106
+ #: The per-request clock for this command. Studio signs a read for every selected part before the
107
+ #: submit answers, which has taken over a minute on a wide version. A submit that outlives the
108
+ #: shared thirty-second clock still lands, queued behind a session this command has already
109
+ #: closed, and times out there; every identical submit after it replays that failure.
110
+ QUERY_REQUEST_TIMEOUT_SECONDS = 150.0
111
+
106
112
  #: The header Studio stamps on a repeated submit. Without reading it, two identical invocations
107
113
  #: are indistinguishable from two executions, and an agent would believe it had spent twice.
108
114
  IDEMPOTENT_REPLAY_HEADER = "Idempotent-Replay"
@@ -371,7 +377,9 @@ def _session(args: argparse.Namespace) -> StudioSession:
371
377
 
372
378
 
373
379
  def _client(args: argparse.Namespace) -> StudioV4QueryClient:
374
- return StudioV4QueryClient(_session(args))
380
+ return StudioV4QueryClient(
381
+ _session(args), transport=UrlLibTransport(timeout=QUERY_REQUEST_TIMEOUT_SECONDS)
382
+ )
375
383
 
376
384
 
377
385
  # --------------------------------------------------------------------------------------------
@@ -725,7 +733,12 @@ def _settled(
725
733
  }
726
734
  state = record.get("state")
727
735
  if state == "failed":
728
- raise _settled_refusal(record, query_id)
736
+ refusal = _settled_refusal(record, query_id)
737
+ if replayed:
738
+ code = refusal.studio_code # type: ignore[attr-defined]
739
+ refusal = ThinLaneError(refusal.code, f"{refusal.detail}. {_replayed_failure(record)}")
740
+ refusal.studio_code = code # type: ignore[attr-defined]
741
+ raise refusal
729
742
  if state != "succeeded":
730
743
  return {
731
744
  **head,
@@ -761,6 +774,18 @@ def _settled(
761
774
  return payload
762
775
 
763
776
 
777
+ def _replayed_failure(record: Mapping[str, Any]) -> str:
778
+ """Why an identical question keeps failing, and how to get it executed again."""
779
+
780
+ settled = record.get("settled_at")
781
+ when = f" at {settled}" if isinstance(settled, str) and settled else ""
782
+ return (
783
+ f"This is the answer Studio recorded{when} for an earlier identical query, replayed "
784
+ "rather than executed again. Studio answers this statement, row limit and range with "
785
+ "that record every time, so change one of them to have it run again"
786
+ )
787
+
788
+
764
789
  class _WarmSession:
765
790
  """The research session this command opened so a worker would exist to ask, and its close.
766
791
 
@@ -182,6 +182,12 @@ PARTS_SCHEMA = f"{THIN_SCHEMA_PREFIX}-v4-table-parts.v1"
182
182
  #: re-spelled, so a listing is never shorter than a version the format admits.
183
183
  MAX_LISTED_PARTS = MAX_PARTS_PER_VERSION
184
184
 
185
+ #: The manifest route answers with the whole part list, so its ceilings are sized for the widest
186
+ #: version the format admits rather than for an ordinary control-plane answer. A part with bounds
187
+ #: on sixteen fields is about two kilobytes and a hundred and thirty members.
188
+ MAX_MANIFEST_RESPONSE_BYTES = 32 * 1024 * 1024
189
+ MAX_MANIFEST_RESPONSE_MEMBERS = MAX_LISTED_PARTS * 160
190
+
185
191
  #: The two acts ``mr-data table`` carries beside the state read, as words a person types in front
186
192
  #: of the table identifier: ``mr-data table rename TABLE --name NAME``.
187
193
  #:
@@ -502,6 +508,8 @@ class StudioV4TableClient(StudioV4Client):
502
508
  "GET",
503
509
  TABLE_VERSION_MANIFEST_PATH.format(table_id=table_id, table_version_id=version),
504
510
  expected=(200,),
511
+ maximum=MAX_MANIFEST_RESPONSE_BYTES,
512
+ max_members=MAX_MANIFEST_RESPONSE_MEMBERS,
505
513
  )
506
514
 
507
515
  def reschedule_table(self, table_id: str, body: Mapping[str, Any]) -> dict[str, Any]: