mostlyright-data 0.31.10__tar.gz → 0.31.11__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.
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/PKG-INFO +1 -1
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/preparing-table-search.md +27 -8
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/recipe-test.md +4 -3
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/activity.py +6 -5
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/recipe_test.py +46 -11
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/runs.py +96 -7
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/session.py +83 -29
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_search.py +31 -1
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/.gitignore +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/README.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/pyproject.toml +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/scripts/hatch_build.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/SKILL.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/agents/openai.yaml +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/1-open-the-page-and-the-link-to-it-in-the-first-message.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/agent-protocol.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/boundaries.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/commands.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/enriching-with-small-models.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/installation-parity.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/live-run.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/making-a-table-incremental.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/narrating-the-run.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/one-install.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/patterns.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/prediction-labels.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/promote.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/readers.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/receipts.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/reference-pages.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/required-protocol.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/source-credentials.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/sources.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/transforms.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/user-communication-contract.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/__init__.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/agent_protocol.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/canonical.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/formats.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/key_seam.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/page_coverage.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/session_probes.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/skill_assets.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/table_manifest.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/__init__.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/acquire.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/approvals.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/backoff.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/categories.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/commands.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/delivery.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/description_shape.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/download.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/enrichment.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/fast_features.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/matching_benchmark.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/narrative.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/parity.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/probe.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/progress_rate.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/propose.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/recipe.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/recipe_explain.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/recipe_forecast.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/refresh_rehearsal.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/release_check.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/research.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/router.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/run_diagnose.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/run_prefix.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/stream.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/studio_capabilities.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/support.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/table_search_contract.json +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/table_view.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/transport.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_matching.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_public.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_query.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_search_contract.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/__init__.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/attendance.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/clarification.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/credentials.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/login.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/remediation.py +0 -0
- {mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/render.py +0 -0
|
@@ -146,7 +146,10 @@ column description.
|
|
|
146
146
|
|
|
147
147
|
## 6. Write the vocabulary column by column
|
|
148
148
|
|
|
149
|
-
An alias maps a phrase to an exact filter, and a sort alias maps a phrase to a sort.
|
|
149
|
+
An alias maps a phrase to an exact filter, and a sort alias maps a phrase to a sort. Once
|
|
150
|
+
planning#1842 lands, the live page's search node applies the aliases and merges them with the
|
|
151
|
+
planner's plan. Until then aliases are honoured only by `search explain`, and only while the
|
|
152
|
+
planner is unavailable for that table. The live page applies none. List every
|
|
150
153
|
declared column before you write the spec. Then generate its phrases from its type:
|
|
151
154
|
|
|
152
155
|
| Column | Phrases | Spec entry |
|
|
@@ -200,12 +203,27 @@ nothing guarantees that.
|
|
|
200
203
|
## 8. Turn the planner on
|
|
201
204
|
|
|
202
205
|
The planner turns a sentence into filters, sorts and preferences. Turn it on for every table with
|
|
203
|
-
search
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
search with `"enabled": true`. A query whose every word an alias resolved skips it. Queries with
|
|
207
|
+
negation or "or" stay unresolved rather than becoming a guessed filter.
|
|
208
|
+
|
|
209
|
+
The planner model sees the query text, the column names and their descriptions, value
|
|
210
|
+
dictionaries of at most 16 columns of at most 256 short values each, the aliases and the field
|
|
211
|
+
roles. It never sees rows.
|
|
212
|
+
|
|
213
|
+
`external_context_allowed` is the owner's consent to send that context to the planner model:
|
|
214
|
+
|
|
215
|
+
- A public table whose sources are all public gets the planner on its live page whatever the flag
|
|
216
|
+
says. Set the flag to `true` on it, so `search explain` and `capabilities.planner` match the page.
|
|
217
|
+
- A private or link-shared table gets the planner only when the recipe sets the flag to `true`.
|
|
218
|
+
That is a deliberate yes from the user or the workspace policy. Data-read permission is not that
|
|
219
|
+
consent. Without that yes, keep the flag `false`.
|
|
220
|
+
- Until the Studio change in planning#1844 is live, the flag changes only `search explain` and the
|
|
221
|
+
`capabilities.planner` readout. The live page plans a public table with public sources and no
|
|
222
|
+
other table.
|
|
223
|
+
|
|
224
|
+
`search inspect`, `search status` and `search explain` say why the planner is unavailable in
|
|
225
|
+
`planner_detail.reason`. `external_context_not_allowed` means the recipe withholds consent.
|
|
226
|
+
`private_source` means a source is not public.
|
|
209
227
|
|
|
210
228
|
Write each column description as if the planner reads it. Say what the column holds, its unit,
|
|
211
229
|
and the words people use for it.
|
|
@@ -314,7 +332,8 @@ mr-data search explain TABLE_ID --version VERSION_ID --query "stocks under 10" -
|
|
|
314
332
|
|
|
315
333
|
Read `planner_used`, `filters` and `order_by`. A query that asks for a filter or a sort and plans
|
|
316
334
|
text only is a gap in the spec. Add the alias or sharpen the column description, then check again.
|
|
317
|
-
`planner_used: false` on a settled query is a fault to report, unless aliases resolved every word
|
|
335
|
+
`planner_used: false` on a settled query is a fault to report, unless aliases resolved every word
|
|
336
|
+
or `planner_detail.reason` says the planner is withheld.
|
|
318
337
|
|
|
319
338
|
Run real queries against the live search once `package_state` is `ready`:
|
|
320
339
|
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/recipe-test.md
RENAMED
|
@@ -43,7 +43,7 @@ Read `status`:
|
|
|
43
43
|
| `recipe_test_cancelled` | Cancelled before it reported. | 2 |
|
|
44
44
|
| `recipe_test_awaiting_confirmation` | Held for a size confirmation. Run `confirm_command`, then ask again. The hold is a size check, not a charge. | 2 |
|
|
45
45
|
| `recipe_test_started` | `--no-wait` returned before the report. | 0 |
|
|
46
|
-
| `recipe_test_still_running` | Not reported within the wait. Ask again with the same bounds. | 2 |
|
|
46
|
+
| `recipe_test_still_running` | Not reported within the wait. `test_status: queued` means no worker holds the test yet, so nothing is wrong with the recipe. Ask again with the same bounds. | 2 |
|
|
47
47
|
| `recipe_test_skipped` | Nothing was tested. `test.reason` says why: `not_served` (Studio predates the test), `engine_unsupported` (the recipe runs on the Keel engine) or `worker_unsupported` (the worker predates the test and refused it `JOB_INVALID`). | 0 |
|
|
48
48
|
|
|
49
49
|
Asking again with the same bounds reads a running test back, and a completed test replays its
|
|
@@ -66,8 +66,9 @@ page is fetched twice, and the receipt's `recipe_test` block names the test run.
|
|
|
66
66
|
When the test finds a blocking problem, fails, is held or is still running after those five
|
|
67
67
|
minutes, the full is not started. The status is `run_stopped_by_test`,
|
|
68
68
|
`run_test_awaiting_confirmation` or `run_test_still_running`, `run_id` names the test, and the
|
|
69
|
-
command exits 2.
|
|
70
|
-
|
|
69
|
+
command exits 2. `test.test_status: queued` on `run_test_still_running` means no worker had
|
|
70
|
+
picked the test up; `mr-data status RUN_ID` says when one has. Running the same command again
|
|
71
|
+
picks a running test back up, replays a finished one, and tests again after a failure. `next` ends with the command that starts the
|
|
71
72
|
same full without the test. Repair the recipe first. Skip the test only when the user decides the
|
|
72
73
|
problem does not matter:
|
|
73
74
|
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/activity.py
RENAMED
|
@@ -50,7 +50,7 @@ from time import monotonic
|
|
|
50
50
|
from typing import Any
|
|
51
51
|
|
|
52
52
|
from mostlyright.data_harness.thin import THIN_SCHEMA_PREFIX
|
|
53
|
-
from mostlyright.data_harness.thin.session import
|
|
53
|
+
from mostlyright.data_harness.thin.session import StudioSessionProvider, dataset_dashboard_url
|
|
54
54
|
from mostlyright.data_harness.thin.transport import (
|
|
55
55
|
REQUEST_TIMEOUT_SECONDS,
|
|
56
56
|
ThinLaneError,
|
|
@@ -931,8 +931,9 @@ def _document_counts(document: Mapping[str, Any]) -> tuple[int, int]:
|
|
|
931
931
|
#: One session per process, because two reports in one command -- ``recipe`` says one thing before
|
|
932
932
|
#: it registers and another after -- must not buy two token exchanges. It is never shared with a
|
|
933
933
|
#: command's own client: those open their own, and a cache reaching across the two would make a
|
|
934
|
-
#: progress report able to break a command by holding an expired token.
|
|
935
|
-
|
|
934
|
+
#: progress report able to break a command by holding an expired token. It is a provider, so the
|
|
935
|
+
#: clients built on it share one renewal when the token nears its expiry rather than one each.
|
|
936
|
+
_SESSION: StudioSessionProvider | None = None
|
|
936
937
|
|
|
937
938
|
#: The verb's transport, which is cached because its clock is a constant. The automatic half's is
|
|
938
939
|
#: not cached and cannot be: its clock is a deadline, and a deadline belongs to one report.
|
|
@@ -993,11 +994,11 @@ def _reporting_client(budget: _Budget) -> StudioV4ActivityClient:
|
|
|
993
994
|
|
|
994
995
|
def _client_on(transport: Transport) -> StudioV4ActivityClient:
|
|
995
996
|
global _SESSION
|
|
996
|
-
from mostlyright.data_harness.thin.session import
|
|
997
|
+
from mostlyright.data_harness.thin.session import open_renewable_session
|
|
997
998
|
from mostlyright.data_harness.ux.credentials import resolve_cloud_credentials
|
|
998
999
|
|
|
999
1000
|
if _SESSION is None:
|
|
1000
|
-
_SESSION =
|
|
1001
|
+
_SESSION = open_renewable_session(resolve_cloud_credentials(), transport=transport)
|
|
1001
1002
|
return StudioV4ActivityClient(_SESSION, transport=transport)
|
|
1002
1003
|
|
|
1003
1004
|
|
|
@@ -49,6 +49,7 @@ from mostlyright.data_harness.thin.v4_runs import (
|
|
|
49
49
|
CONFIRM_REQUIRED_CODE,
|
|
50
50
|
TERMINAL_RUN_STATUSES,
|
|
51
51
|
StudioV4RunClient,
|
|
52
|
+
attempt_progress_members,
|
|
52
53
|
confirm_held_command,
|
|
53
54
|
failure_sentence,
|
|
54
55
|
failure_triple,
|
|
@@ -515,6 +516,12 @@ def reading(run: Mapping[str, Any]) -> tuple[str, dict[str, Any]]:
|
|
|
515
516
|
#: under. It is not a status word: the status is still :data:`FAILED`, with ``room_fault: true``.
|
|
516
517
|
ROOM_FAULT = "room_fault"
|
|
517
518
|
|
|
519
|
+
#: The sentence key for a test still ``queued`` when the wait ended: no worker holds it. It is not
|
|
520
|
+
#: a status word -- the status stays ``recipe_test_still_running`` / ``run_test_still_running`` --
|
|
521
|
+
#: it is which ``next`` and headline that status gets, because "running" was untrue of a test no
|
|
522
|
+
#: worker had picked up and sent people to look for a slow source rather than a cold queue.
|
|
523
|
+
QUEUED = "queued"
|
|
524
|
+
|
|
518
525
|
#: What a person does next, by what the test came to.
|
|
519
526
|
_NEXT = {
|
|
520
527
|
PASSED: (
|
|
@@ -662,11 +669,29 @@ def _settled(
|
|
|
662
669
|
"confirm_command": confirm_held_command(str(run["run_id"])),
|
|
663
670
|
}
|
|
664
671
|
if state not in TERMINAL_RUN_STATUSES:
|
|
672
|
+
minutes = int(ceiling // 60)
|
|
673
|
+
if state == QUEUED:
|
|
674
|
+
ledger = attempt_progress_members(client, str(run["run_id"]))
|
|
675
|
+
waiting = (
|
|
676
|
+
"it is back in the queue after an earlier attempt and no worker holds it now"
|
|
677
|
+
if "progress" in ledger
|
|
678
|
+
else "no worker has picked it up yet"
|
|
679
|
+
)
|
|
680
|
+
return {
|
|
681
|
+
"status": STILL_RUNNING,
|
|
682
|
+
"headline": f"The recipe test is still queued: {waiting}, so its report is not in.",
|
|
683
|
+
**members,
|
|
684
|
+
"test_status": state,
|
|
685
|
+
**ledger,
|
|
686
|
+
"next": f"The test was still queued after {minutes} minutes: {waiting}. Ask again "
|
|
687
|
+
"with the same bounds to pick the same test back up, or check whether a worker "
|
|
688
|
+
f"has claimed it with mr-data status {run['run_id']}.",
|
|
689
|
+
}
|
|
665
690
|
return {
|
|
666
691
|
"status": STILL_RUNNING,
|
|
667
692
|
**members,
|
|
668
693
|
"test_status": state,
|
|
669
|
-
"next": f"The test had not reported after {
|
|
694
|
+
"next": f"The test had not reported after {minutes} minutes. Ask "
|
|
670
695
|
"again with the same bounds to pick it back up, or follow it with "
|
|
671
696
|
f"mr-data watch {run['run_id']}.",
|
|
672
697
|
}
|
|
@@ -786,18 +811,24 @@ def before_full(
|
|
|
786
811
|
block = {"status": "passed", **members}
|
|
787
812
|
block.pop("preflight_report", None)
|
|
788
813
|
return BeforeFull(block=block, resume_capture_run_id=str(settled["run_id"]))
|
|
814
|
+
headline = members.pop("headline", None)
|
|
789
815
|
test_block = {"status": status.removeprefix("recipe_test_"), **members}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
816
|
+
if members.get("room_fault") is True:
|
|
817
|
+
said = ROOM_FAULT
|
|
818
|
+
elif status == STILL_RUNNING and members.get("test_status") == QUEUED:
|
|
819
|
+
said = QUEUED
|
|
820
|
+
else:
|
|
821
|
+
said = status
|
|
822
|
+
sentence = _STOPPED_NEXT[said]
|
|
823
|
+
stopped: dict[str, Any] = {"run_id": str(settled["run_id"]), "test": test_block}
|
|
824
|
+
if said == QUEUED:
|
|
825
|
+
# The test's own headline, said about the full it was standing in front of.
|
|
826
|
+
sentence = sentence.format(run=settled["run_id"])
|
|
827
|
+
stopped["headline"] = str(headline).replace(
|
|
828
|
+
"so its report is not in", "so the full run was not started"
|
|
799
829
|
)
|
|
800
|
-
|
|
830
|
+
stopped["next"] = sentence + _HELD_TAIL + rerun
|
|
831
|
+
return BeforeFull(stop=_stopped(status, stopped))
|
|
801
832
|
|
|
802
833
|
|
|
803
834
|
_STOPPED_NEXT = {
|
|
@@ -814,6 +845,9 @@ _STOPPED_NEXT = {
|
|
|
814
845
|
STILL_RUNNING: f"The recipe test had not reported after {int(FULL_WAIT_SECONDS // 60)} "
|
|
815
846
|
"minutes, so the full was not started. Run this command again to pick the same test back "
|
|
816
847
|
"up.",
|
|
848
|
+
QUEUED: f"The recipe test was still queued after {int(FULL_WAIT_SECONDS // 60)} minutes, "
|
|
849
|
+
"waiting for a worker, so the full was not started. Run this command again to pick the same "
|
|
850
|
+
"test back up; mr-data status {run} says whether a worker has claimed it.",
|
|
817
851
|
}
|
|
818
852
|
|
|
819
853
|
|
|
@@ -847,6 +881,7 @@ __all__ = [
|
|
|
847
881
|
"MAX_WAIT_SECONDS",
|
|
848
882
|
"PASSED",
|
|
849
883
|
"PREFLIGHT_COMPLETE",
|
|
884
|
+
"QUEUED",
|
|
850
885
|
"ROOM_FAULT",
|
|
851
886
|
"RUN_STOPPED_STATUS",
|
|
852
887
|
"SKIPPED",
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/runs.py
RENAMED
|
@@ -34,7 +34,11 @@ from mostlyright.data_harness.canonical import (
|
|
|
34
34
|
canonical_json_bytes,
|
|
35
35
|
parse_foreign_json,
|
|
36
36
|
)
|
|
37
|
-
from mostlyright.data_harness.thin.session import
|
|
37
|
+
from mostlyright.data_harness.thin.session import (
|
|
38
|
+
StudioSession,
|
|
39
|
+
StudioSessionProvider,
|
|
40
|
+
sessions_for,
|
|
41
|
+
)
|
|
38
42
|
from mostlyright.data_harness.thin.transport import (
|
|
39
43
|
MAX_JSON_RESPONSE_BYTES,
|
|
40
44
|
ThinLaneError,
|
|
@@ -211,15 +215,39 @@ class StudioApiClient:
|
|
|
211
215
|
hardening, and a second ``_call`` would be a second place for the byte ceiling, the JSON
|
|
212
216
|
hardening and the Studio-code translation to drift apart. Subclasses add routes; none of them
|
|
213
217
|
adds a transport.
|
|
218
|
+
|
|
219
|
+
⚠ AND EVERY CALL ASKS FOR ITS TOKEN FRESH (planning#1845). The client holds a
|
|
220
|
+
:class:`~mostlyright.data_harness.thin.session.StudioSessionProvider` rather than one token, so
|
|
221
|
+
a call made after a long wait -- the recipe test's report, a search poll, a closing read --
|
|
222
|
+
goes out on a token renewed when it had under a minute left, and a 401 on a token that was not
|
|
223
|
+
just renewed renews it once and sends the same call again, once. A session built by hand has
|
|
224
|
+
no renewal, and its 401 is final as it always was.
|
|
214
225
|
"""
|
|
215
226
|
|
|
216
|
-
def __init__(
|
|
217
|
-
self
|
|
227
|
+
def __init__(
|
|
228
|
+
self,
|
|
229
|
+
session: StudioSession | StudioSessionProvider,
|
|
230
|
+
*,
|
|
231
|
+
transport: Transport | None = None,
|
|
232
|
+
) -> None:
|
|
233
|
+
self._sessions = sessions_for(session)
|
|
218
234
|
self._transport = transport or UrlLibTransport()
|
|
219
235
|
|
|
220
236
|
@property
|
|
221
237
|
def session(self) -> StudioSession:
|
|
222
|
-
|
|
238
|
+
"""The session held right now; a call renews it first when it is nearly spent."""
|
|
239
|
+
|
|
240
|
+
return self._sessions.session
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def _session(self) -> StudioSession:
|
|
244
|
+
return self._sessions.session
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def sessions(self) -> StudioSessionProvider:
|
|
248
|
+
"""The provider this client's calls renew through, for a stream that should share it."""
|
|
249
|
+
|
|
250
|
+
return self._sessions
|
|
223
251
|
|
|
224
252
|
@property
|
|
225
253
|
def transport(self) -> Transport:
|
|
@@ -251,7 +279,6 @@ class StudioApiClient:
|
|
|
251
279
|
headers = {
|
|
252
280
|
"Accept": "application/json",
|
|
253
281
|
CLIENT_HEADER: CLIENT_NAME,
|
|
254
|
-
**self._session.authorization(),
|
|
255
282
|
}
|
|
256
283
|
if extra_headers:
|
|
257
284
|
headers.update(extra_headers)
|
|
@@ -282,10 +309,72 @@ class StudioApiClient:
|
|
|
282
309
|
and len(raw_body) > max_request_bytes
|
|
283
310
|
):
|
|
284
311
|
raise ThinLaneError("THIN_REQUEST_INVALID", "request exceeds its byte limit")
|
|
285
|
-
|
|
312
|
+
# A renewal made for THIS call -- ahead of expiry -- means a 401 on it is not an expiry.
|
|
313
|
+
renewals_before = self._sessions.renewals
|
|
314
|
+
selected = self._sessions.current()
|
|
315
|
+
try:
|
|
316
|
+
return self._send(
|
|
317
|
+
selected,
|
|
318
|
+
method,
|
|
319
|
+
path,
|
|
320
|
+
query=query,
|
|
321
|
+
headers=headers,
|
|
322
|
+
raw_body=raw_body,
|
|
323
|
+
expected=expected,
|
|
324
|
+
shape=shape,
|
|
325
|
+
response_headers=response_headers,
|
|
326
|
+
maximum=maximum,
|
|
327
|
+
max_members=max_members,
|
|
328
|
+
)
|
|
329
|
+
except ThinLaneError as refusal:
|
|
330
|
+
if (
|
|
331
|
+
getattr(refusal, "http_status", None) != 401
|
|
332
|
+
or not self._sessions.renewable
|
|
333
|
+
or self._sessions.renewals != renewals_before
|
|
334
|
+
# A call that carried its own bearer was refused for THAT credential.
|
|
335
|
+
or "Authorization" in headers
|
|
336
|
+
):
|
|
337
|
+
raise
|
|
338
|
+
# The token lapsed between the check and Studio reading it, or Studio holds a shorter
|
|
339
|
+
# clock than the expiry Cloud stated. Renew once and send the same call once more; a 401
|
|
340
|
+
# never reached a handler, so the call was not applied. A second 401 is final.
|
|
341
|
+
return self._send(
|
|
342
|
+
self._sessions.renew(),
|
|
343
|
+
method,
|
|
344
|
+
path,
|
|
345
|
+
query=query,
|
|
346
|
+
headers=headers,
|
|
347
|
+
raw_body=raw_body,
|
|
348
|
+
expected=expected,
|
|
349
|
+
shape=shape,
|
|
350
|
+
response_headers=response_headers,
|
|
351
|
+
maximum=maximum,
|
|
352
|
+
max_members=max_members,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def _send(
|
|
356
|
+
self,
|
|
357
|
+
session: StudioSession,
|
|
358
|
+
method: str,
|
|
359
|
+
path: str,
|
|
360
|
+
*,
|
|
361
|
+
query: Mapping[str, Any] | None,
|
|
362
|
+
headers: Mapping[str, str],
|
|
363
|
+
raw_body: bytes | None,
|
|
364
|
+
expected: tuple[int, ...],
|
|
365
|
+
shape: type,
|
|
366
|
+
response_headers: dict[str, str] | None,
|
|
367
|
+
maximum: int,
|
|
368
|
+
max_members: int,
|
|
369
|
+
) -> Any:
|
|
370
|
+
"""One request on one session's token, and the refusal it becomes."""
|
|
371
|
+
|
|
372
|
+
url = f"{session.studio_base_url}{path}{_query_string(query)}"
|
|
373
|
+
# The session's bearer first, so a call that states its own (a deciding credential) wins.
|
|
374
|
+
sent = {**session.authorization(), **headers}
|
|
286
375
|
try:
|
|
287
376
|
status, raw, answered_headers = self._transport.request(
|
|
288
|
-
method, url,
|
|
377
|
+
method, url, sent, raw_body, maximum
|
|
289
378
|
)
|
|
290
379
|
if response_headers is not None:
|
|
291
380
|
response_headers.update(answered_headers)
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/session.py
RENAMED
|
@@ -22,6 +22,7 @@ import re
|
|
|
22
22
|
import time
|
|
23
23
|
from collections.abc import Callable, Mapping
|
|
24
24
|
from dataclasses import dataclass
|
|
25
|
+
from dataclasses import field as dataclass_field
|
|
25
26
|
from datetime import UTC, datetime, timedelta
|
|
26
27
|
from typing import Any
|
|
27
28
|
from urllib.parse import quote, urlencode
|
|
@@ -169,22 +170,43 @@ TOKEN_EXCHANGE_MAX_RETRY_SECONDS = 30.0
|
|
|
169
170
|
#: The spread is never more than the wait itself, and never subtracted from it.
|
|
170
171
|
TOKEN_EXCHANGE_MAX_JITTER_SECONDS = 5.0
|
|
171
172
|
|
|
172
|
-
#: How close to its expiry a Studio token is traded for a new one before
|
|
173
|
-
#:
|
|
173
|
+
#: How close to its expiry a Studio token is traded for a new one before the next call or the next
|
|
174
|
+
#: stream connection goes out. A token that would expire mid-call is not worth sending.
|
|
174
175
|
SESSION_RENEWAL_MARGIN_SECONDS = 60.0
|
|
175
176
|
|
|
176
177
|
_CLI_KEY = re.compile(r"^mr_cli_[A-Za-z0-9_-]{64}$")
|
|
177
178
|
|
|
178
179
|
|
|
180
|
+
@dataclass(frozen=True)
|
|
181
|
+
class SessionRenewal:
|
|
182
|
+
"""How one session is traded for a fresh one: the device-key exchange, and the clock it reads.
|
|
183
|
+
|
|
184
|
+
Held by a session :func:`open_studio_session` minted, so every client built on that session can
|
|
185
|
+
renew it without each command threading the credential through by hand. It closes over the
|
|
186
|
+
device key already in this process's memory for the first exchange; nothing here writes it.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
exchange: Callable[[], StudioSession]
|
|
190
|
+
now: Callable[[], datetime]
|
|
191
|
+
|
|
192
|
+
def __repr__(self) -> str:
|
|
193
|
+
return "SessionRenewal(<device-key exchange>)"
|
|
194
|
+
|
|
195
|
+
|
|
179
196
|
@dataclass(frozen=True)
|
|
180
197
|
class StudioSession:
|
|
181
|
-
"""One short-lived Studio authority: where to call, as whom, and until when.
|
|
198
|
+
"""One short-lived Studio authority: where to call, as whom, and until when.
|
|
199
|
+
|
|
200
|
+
``renewal`` is how to trade it for a fresh one, and ``None`` for a session that can never be
|
|
201
|
+
renewed -- one a test or a caller built by hand. It takes no part in equality or the repr.
|
|
202
|
+
"""
|
|
182
203
|
|
|
183
204
|
studio_base_url: str
|
|
184
205
|
workspace_id: UUID
|
|
185
206
|
token: str
|
|
186
207
|
expires_at: datetime
|
|
187
208
|
cloud_url: str
|
|
209
|
+
renewal: SessionRenewal | None = dataclass_field(default=None, compare=False, repr=False)
|
|
188
210
|
|
|
189
211
|
def __repr__(self) -> str:
|
|
190
212
|
return (
|
|
@@ -358,7 +380,12 @@ def open_studio_session(
|
|
|
358
380
|
now: Callable[[], datetime] = lambda: datetime.now(UTC),
|
|
359
381
|
sleep: Callable[[float], None] = time.sleep,
|
|
360
382
|
) -> StudioSession:
|
|
361
|
-
"""Exchange the stored device key for a short-lived Studio token.
|
|
383
|
+
"""Exchange the stored device key for a short-lived Studio token.
|
|
384
|
+
|
|
385
|
+
The session that comes back carries its own :class:`SessionRenewal` -- this same exchange,
|
|
386
|
+
made again -- so a client built on it renews the token ahead of its expiry and once on a 401
|
|
387
|
+
(see :class:`StudioSessionProvider`) rather than outliving it.
|
|
388
|
+
"""
|
|
362
389
|
|
|
363
390
|
require_device_key(credentials)
|
|
364
391
|
cloud_url = service_origin(credentials.cloud_url, "Cloud URL", allow_loopback_http=False)
|
|
@@ -412,6 +439,10 @@ def open_studio_session(
|
|
|
412
439
|
expires_at = _timestamp(_text(parsed, "expires_at"), "expires_at")
|
|
413
440
|
if expires_at <= now():
|
|
414
441
|
raise ThinLaneError("THIN_TOKEN_EXCHANGE_FAILED", "Cloud returned an expired Studio token")
|
|
442
|
+
|
|
443
|
+
def _again() -> StudioSession:
|
|
444
|
+
return open_studio_session(credentials, transport=transport, now=now, sleep=sleep)
|
|
445
|
+
|
|
415
446
|
return StudioSession(
|
|
416
447
|
studio_base_url=service_origin(
|
|
417
448
|
_text(parsed, "studio_base_url"), "Studio base URL", allow_loopback_http=True
|
|
@@ -420,28 +451,33 @@ def open_studio_session(
|
|
|
420
451
|
token=_text(parsed, "token"),
|
|
421
452
|
expires_at=expires_at,
|
|
422
453
|
cloud_url=cloud_url,
|
|
454
|
+
renewal=SessionRenewal(exchange=_again, now=now),
|
|
423
455
|
)
|
|
424
456
|
|
|
425
457
|
|
|
426
458
|
class StudioSessionProvider:
|
|
427
|
-
"""The Studio session a
|
|
428
|
-
|
|
429
|
-
⚠
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
459
|
+
"""The Studio session a command calls through, renewed rather than outlived.
|
|
460
|
+
|
|
461
|
+
⚠ A STUDIO TOKEN LIVES FOR MINUTES AND A WAIT CAN LAST LONGER. A ``watch`` streams for hours;
|
|
462
|
+
``run --full`` waits up to five minutes for the recipe test in front of it, and ``recipe
|
|
463
|
+
test`` twenty; the search and query polls wait on a node that is still settling. Each of those
|
|
464
|
+
used to make its follow-up call on the token minted at the start, and Studio refused it with
|
|
465
|
+
``401 AUTHENTICATION_REQUIRED`` -- which surfaced as an authorization refusal in the middle of
|
|
466
|
+
a wait that was going fine (planning#1845). So every Studio call
|
|
467
|
+
(:class:`~mostlyright.data_harness.thin.runs.StudioApiClient`) and every stream connection asks
|
|
468
|
+
a provider for its session first, and the provider re-exchanges the device key when the token
|
|
469
|
+
has less than :data:`SESSION_RENEWAL_MARGIN_SECONDS` left. A 401 on a token that was not just
|
|
470
|
+
renewed renews once more and the caller sends the same call again, once; a second 401 is final.
|
|
471
|
+
|
|
472
|
+
⚠ A PROVIDER WITH NO ``exchange`` NEVER RENEWS. A test, or a caller handed only a session it
|
|
473
|
+
built by hand, gets exactly the behaviour it had: the session it was given, and a 401 that is
|
|
474
|
+
final. A session :func:`open_studio_session` minted carries its exchange, and
|
|
475
|
+
:func:`sessions_for` hands it to the provider.
|
|
440
476
|
|
|
441
477
|
⚠ AND A RENEWAL MUST NAME THE SAME STUDIO AND THE SAME WORKSPACE. A stream resumes from a
|
|
442
|
-
``Last-Event-ID`` in one Studio's cursor space
|
|
443
|
-
|
|
444
|
-
rather than followed.
|
|
478
|
+
``Last-Event-ID`` in one Studio's cursor space, and a wait reads back a run in one workspace; a
|
|
479
|
+
fresh token for another origin or another workspace would carry that cursor or that run
|
|
480
|
+
somewhere it means nothing, so the renewal is refused by name rather than followed.
|
|
445
481
|
|
|
446
482
|
The device key the exchange closes over is the one already in this process's memory for the
|
|
447
483
|
first exchange. Nothing here writes a token anywhere.
|
|
@@ -472,10 +508,22 @@ class StudioSessionProvider:
|
|
|
472
508
|
return self._exchange is not None
|
|
473
509
|
|
|
474
510
|
def current(self) -> StudioSession:
|
|
475
|
-
"""The session to open the next connection with, renewed first when it is nearly spent.
|
|
511
|
+
"""The session to open the next connection with, renewed first when it is nearly spent.
|
|
512
|
+
|
|
513
|
+
⚠ A RENEWAL AHEAD OF EXPIRY IS A COURTESY, NOT A GATE. When Cloud cannot mint a new token
|
|
514
|
+
right now (a cold-start 503, a 429, a 500) and the token held has not expired yet, the
|
|
515
|
+
call goes out on the token held -- which is what it did before renewal existed. Only a
|
|
516
|
+
token that has already lapsed makes the failed exchange the answer. A renewal that names
|
|
517
|
+
another Studio or workspace is refused either way.
|
|
518
|
+
"""
|
|
476
519
|
|
|
477
520
|
if self._exchange is not None and self._session.expires_at - self._now() < self._margin:
|
|
478
|
-
|
|
521
|
+
try:
|
|
522
|
+
return self.renew()
|
|
523
|
+
except ThinLaneError as refusal:
|
|
524
|
+
lapsed = self._session.expires_at <= self._now()
|
|
525
|
+
if refusal.code == "THIN_SESSION_BINDING" or lapsed:
|
|
526
|
+
raise
|
|
479
527
|
return self._session
|
|
480
528
|
|
|
481
529
|
def renew(self) -> StudioSession:
|
|
@@ -508,20 +556,25 @@ def open_renewable_session(
|
|
|
508
556
|
now: Callable[[], datetime] = lambda: datetime.now(UTC),
|
|
509
557
|
sleep: Callable[[float], None] = time.sleep,
|
|
510
558
|
) -> StudioSessionProvider:
|
|
511
|
-
"""Exchange the device key once now, and keep the means to exchange it again
|
|
512
|
-
|
|
513
|
-
def _exchange() -> StudioSession:
|
|
514
|
-
return open_studio_session(credentials, transport=transport, now=now, sleep=sleep)
|
|
559
|
+
"""Exchange the device key once now, and keep the means to exchange it again."""
|
|
515
560
|
|
|
516
|
-
return
|
|
561
|
+
return sessions_for(open_studio_session(credentials, transport=transport, now=now, sleep=sleep))
|
|
517
562
|
|
|
518
563
|
|
|
519
564
|
def sessions_for(session: StudioSession | StudioSessionProvider) -> StudioSessionProvider:
|
|
520
|
-
"""A provider for what a caller holds: itself, or
|
|
565
|
+
"""A provider for what a caller holds: itself, or one over the session and its own renewal.
|
|
566
|
+
|
|
567
|
+
A session :func:`open_studio_session` minted renews through the exchange it carries; one built
|
|
568
|
+
by hand carries none and is never renewed.
|
|
569
|
+
"""
|
|
521
570
|
|
|
522
571
|
if isinstance(session, StudioSessionProvider):
|
|
523
572
|
return session
|
|
524
|
-
|
|
573
|
+
if session.renewal is None:
|
|
574
|
+
return StudioSessionProvider(session)
|
|
575
|
+
return StudioSessionProvider(
|
|
576
|
+
session, exchange=session.renewal.exchange, now=session.renewal.now
|
|
577
|
+
)
|
|
525
578
|
|
|
526
579
|
|
|
527
580
|
#: What a stated refusal code has to look like before it is interpolated into one.
|
|
@@ -960,6 +1013,7 @@ __all__ = [
|
|
|
960
1013
|
"USAGE_LIMIT_SCHEMA",
|
|
961
1014
|
"CloudClient",
|
|
962
1015
|
"CloudRouteAbsent",
|
|
1016
|
+
"SessionRenewal",
|
|
963
1017
|
"StudioSession",
|
|
964
1018
|
"StudioSessionProvider",
|
|
965
1019
|
"approval_dashboard_url",
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_search.py
RENAMED
|
@@ -325,11 +325,41 @@ SEARCH_PATHS = frozenset(
|
|
|
325
325
|
)
|
|
326
326
|
|
|
327
327
|
|
|
328
|
+
#: The human line for each ``planner_detail.reason`` Studio states (``table-search.schema.json``).
|
|
329
|
+
#: Only the headline says these; ``result.planner_detail.reason`` stays Studio's own code in
|
|
330
|
+
#: ``--json``. ``external_context_allowed`` is the recipe owner's consent to send the query and the
|
|
331
|
+
#: search context -- never rows -- to the planner model; a public table whose sources are all
|
|
332
|
+
#: public is planned either way, so the flag is what a private table's yes is written in.
|
|
333
|
+
PLANNER_REASONS: dict[str, str] = {
|
|
334
|
+
"external_context_not_allowed": (
|
|
335
|
+
"planner consent withheld by the recipe (planner.external_context_allowed is false)"
|
|
336
|
+
),
|
|
337
|
+
"private_source": "a source is not public",
|
|
338
|
+
"disabled": "the recipe turns it off (planner.enabled is false)",
|
|
339
|
+
"no_route": "this deployment has no planner route bound",
|
|
340
|
+
"provider_quota": "the planner provider's spend limit is spent",
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _planner_line(answer: Any) -> str | None:
|
|
345
|
+
"""The planner's state in one sentence, when the answer carries ``planner_detail``."""
|
|
346
|
+
|
|
347
|
+
detail = answer.get("planner_detail") if isinstance(answer, dict) else None
|
|
348
|
+
reason = detail.get("reason") if isinstance(detail, dict) else None
|
|
349
|
+
if not isinstance(reason, str) or not reason:
|
|
350
|
+
return None
|
|
351
|
+
if reason == "available":
|
|
352
|
+
return "Planner: available."
|
|
353
|
+
return f"Planner unavailable: {PLANNER_REASONS.get(reason, reason)}."
|
|
354
|
+
|
|
355
|
+
|
|
328
356
|
def _payload(verb: str, answer: dict[str, Any], **extra: Any) -> dict[str, Any]:
|
|
357
|
+
planner = _planner_line(answer)
|
|
358
|
+
headline = f"Search {verb} result." + (f" {planner}" if planner else "")
|
|
329
359
|
return {
|
|
330
360
|
"schema_version": f"{THIN_SCHEMA_PREFIX}-v4-search.v1",
|
|
331
361
|
"status": "search_result",
|
|
332
|
-
"headline":
|
|
362
|
+
"headline": headline,
|
|
333
363
|
"lane": "hosted",
|
|
334
364
|
"operation": verb,
|
|
335
365
|
"result": answer,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/agents/openai.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/boundaries.md
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/commands.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/live-run.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/one-install.md
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/patterns.md
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/promote.md
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/readers.md
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/receipts.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/sources.md
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/skills/mr-data-build/references/transforms.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/__init__.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/agent_protocol.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/canonical.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/formats.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/key_seam.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/page_coverage.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/session_probes.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/skill_assets.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/table_manifest.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/__init__.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/acquire.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/approvals.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/backoff.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/commands.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/delivery.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/download.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/narrative.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/parity.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/probe.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/propose.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/recipe.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/research.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/router.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/stream.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/support.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/transport.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_public.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_query.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_reader.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_runs.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_stream.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/thin/v4_tables.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/__init__.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/attendance.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/cloud_auth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/credentials.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/login.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/path_kind.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/plain_file.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/remediation.py
RENAMED
|
File without changes
|
{mostlyright_data-0.31.10 → mostlyright_data-0.31.11}/src/mostlyright/data_harness/ux/render.py
RENAMED
|
File without changes
|