mostlyright-data 0.31.9__tar.gz → 0.31.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.
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/PKG-INFO +3 -2
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/README.md +2 -1
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/scripts/hatch_build.py +1 -1
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/making-a-table-incremental.md +12 -4
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/readers.md +3 -2
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/source-credentials.md +2 -1
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/sources.md +72 -5
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/recipe.py +10 -6
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/recipe_explain.py +94 -20
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/recipe_lint.py +90 -14
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/studio_capabilities.py +8 -1
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/table_search_contract.json +87 -18
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4.py +2 -2
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/.gitignore +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/pyproject.toml +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/SKILL.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/agents/openai.yaml +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/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.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/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.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/agent-protocol.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/boundaries.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/commands.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/enriching-with-small-models.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/installation-parity.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/live-run.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/narrating-the-run.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/one-install.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/patterns.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/prediction-labels.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/preparing-table-search.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/promote.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/receipts.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/recipe-test.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/reference-pages.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/required-protocol.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/transforms.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/user-communication-contract.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/__init__.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/agent_protocol.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/canonical.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/formats.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/key_seam.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/page_coverage.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/session_probes.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/skill_assets.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/table_manifest.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/__init__.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/acquire.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/activity.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/approvals.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/backoff.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/categories.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/commands.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/delivery.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/description_shape.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/download.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/enrichment.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/fast_features.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/matching_benchmark.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/narrative.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/parity.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/probe.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/progress_rate.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/propose.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/recipe_forecast.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/recipe_test.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/refresh_rehearsal.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/release_check.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/research.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/router.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/run_diagnose.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/run_prefix.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/runs.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/session.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/stream.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/support.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/table_view.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/transport.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_matching.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_public.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_query.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_search.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_search_contract.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/__init__.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/attendance.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/clarification.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/credentials.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/login.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/remediation.py +0 -0
- {mostlyright_data-0.31.9 → mostlyright_data-0.31.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.31.
|
|
3
|
+
Version: 0.31.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/
|
|
@@ -216,10 +216,11 @@ There is no `dev` extra to ask for -- `uv run --extra dev` answers
|
|
|
216
216
|
tooling is the default dependency group, which `uv sync` installs.
|
|
217
217
|
|
|
218
218
|
`make lint` is the repository's one lint set (`uv run ruff check .` plus `uv lock --check`); it is
|
|
219
|
-
the first thing `make verify` runs and the same target the
|
|
219
|
+
the first thing `make verify` runs and the same target the checkout leg of `thin-client-smoke.yml`
|
|
220
220
|
runs, so the two cannot disagree about what is linted.
|
|
221
221
|
|
|
222
222
|
The test suite checks client behavior, contract compatibility and thin package ownership. Backend
|
|
223
223
|
execution and worker-boundary tests run in Studio. The client supports Linux, macOS and Windows.
|
|
224
|
+
CI runs on Linux only, on the AWS runner pool, so macOS and Windows are not tested there.
|
|
224
225
|
|
|
225
226
|
To update the public Studio contract pin, run `make repin STUDIO_GIT=/path/to/studio STUDIO_REF=<commit>`. It reads committed OpenAPI and JSON Schema bytes, records their hashes and source commit in `vendor/studio-contracts/client-v4-pin.json`, and updates the thin client. Run the cross-repository tests against that Studio revision before release. Worker/client generation and backend job-contract repins now belong to Studio.
|
|
@@ -201,10 +201,11 @@ There is no `dev` extra to ask for -- `uv run --extra dev` answers
|
|
|
201
201
|
tooling is the default dependency group, which `uv sync` installs.
|
|
202
202
|
|
|
203
203
|
`make lint` is the repository's one lint set (`uv run ruff check .` plus `uv lock --check`); it is
|
|
204
|
-
the first thing `make verify` runs and the same target the
|
|
204
|
+
the first thing `make verify` runs and the same target the checkout leg of `thin-client-smoke.yml`
|
|
205
205
|
runs, so the two cannot disagree about what is linted.
|
|
206
206
|
|
|
207
207
|
The test suite checks client behavior, contract compatibility and thin package ownership. Backend
|
|
208
208
|
execution and worker-boundary tests run in Studio. The client supports Linux, macOS and Windows.
|
|
209
|
+
CI runs on Linux only, on the AWS runner pool, so macOS and Windows are not tested there.
|
|
209
210
|
|
|
210
211
|
To update the public Studio contract pin, run `make repin STUDIO_GIT=/path/to/studio STUDIO_REF=<commit>`. It reads committed OpenAPI and JSON Schema bytes, records their hashes and source commit in `vendor/studio-contracts/client-v4-pin.json`, and updates the thin client. Run the cross-repository tests against that Studio revision before release. Worker/client generation and backend job-contract repins now belong to Studio.
|
|
@@ -15,7 +15,7 @@ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
|
15
15
|
#
|
|
16
16
|
# A compiled member would not, which is why the tag is not the only thing this hook does. It also
|
|
17
17
|
# REFUSES TO BUILD when it finds one, rather than falling back to a platform tag: a fallback is
|
|
18
|
-
# the worst outcome on the list, because the
|
|
18
|
+
# the worst outcome on the list, because the thin-client smoke would keep passing while quietly
|
|
19
19
|
# testing a different artifact. A universal tag over a compiled payload is worse still -- it
|
|
20
20
|
# installs everywhere and fails at import on all but one platform, on somebody else's machine.
|
|
21
21
|
#
|
|
@@ -603,16 +603,24 @@ again. Ask the user before passing either flag. Never pass one to clear a refusa
|
|
|
603
603
|
|
|
604
604
|
Check `studio_capabilities.keyless_window` is literal `true` before using this grammar.
|
|
605
605
|
An absent or false capability means "not available on this Studio yet". Registration and
|
|
606
|
-
file explanation refuse with `THIN_GRAMMAR_NOT_SERVED` before local lint or registration
|
|
607
|
-
|
|
606
|
+
file explanation refuse with `THIN_GRAMMAR_NOT_SERVED` before local lint or registration.
|
|
607
|
+
An older API may answer the explanation with a schema 422 before it reports capabilities. The
|
|
608
|
+
client then runs its local checks and lint. A local fault is reported as itself. Only a clean
|
|
609
|
+
document is refused as not served. That refusal marks each member with the capability it needs,
|
|
610
|
+
`keyless_post` members first. No dataset delegation is written.
|
|
608
611
|
The capability covers `public.https.api@1.0.0`, `generic_api.auth.kind: none`, endpoint
|
|
609
612
|
`epoch_millis`, the `epoch_millis_value` partition key, and the two request controls below. Existing credential-free file and collection
|
|
610
613
|
adapters do not require this capability.
|
|
611
614
|
|
|
612
615
|
For a public JSON/CSV API use `credential_mode: none`, an HTTPS `origin`, and `generic_api`
|
|
613
|
-
with exact `auth: {"kind":"none"}`.
|
|
616
|
+
with exact `auth: {"kind":"none"}`. A request is a GET, or a POST with a `body_template` on
|
|
617
|
+
Studio 4.49. It states no credential reference. A POST needs `studio_capabilities.keyless_post`
|
|
618
|
+
literal `true`. Otherwise registration refuses with `THIN_GRAMMAR_NOT_SERVED` and names the
|
|
619
|
+
request `method` first, marked `(keyless_post)`. Send a GET in its place, or wait for the Studio
|
|
620
|
+
deploy.
|
|
614
621
|
Authenticated adapters retain their existing credential requirements. Declare both window
|
|
615
|
-
endpoints with `location: parameter` and corresponding required request parameters.
|
|
622
|
+
endpoints with `location: parameter` and corresponding required request parameters. A
|
|
623
|
+
parameter fills its placeholder in the query, or in the body of a POST. Origin,
|
|
616
624
|
host allowlists and cursor next-URL restrictions still apply.
|
|
617
625
|
|
|
618
626
|
For Binance BTCUSDT one-minute klines, start the backfill at `2017-08-17T00:00:00Z` and request
|
{mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/readers.md
RENAMED
|
@@ -173,8 +173,9 @@ response matches the family's exact media-type, settings, and budget contract.
|
|
|
173
173
|
### Needs Studio 4.26
|
|
174
174
|
|
|
175
175
|
Check first: run `mr-data recipe explain RECIPE.json --json` and read `studio_capabilities`.
|
|
176
|
-
Use `public.https.api@1.0.0` only when `keyless_window` is literal `true
|
|
177
|
-
"not available on this Studio yet".
|
|
176
|
+
Use `public.https.api@1.0.0` only when `keyless_window` is literal `true`. Otherwise report
|
|
177
|
+
"not available on this Studio yet". A POST request needs Studio 4.49 and `keyless_post` literal
|
|
178
|
+
`true` as well. Registration and file explanation refuse with
|
|
178
179
|
`THIN_GRAMMAR_NOT_SERVED`, including an older API's schema 422 before capabilities.
|
|
179
180
|
|
|
180
181
|
The new adapter uses the existing `generic_api.response` Reader contract. For Binance klines,
|
|
@@ -33,7 +33,8 @@ the user — the source needs a credential and which one — and nothing about t
|
|
|
33
33
|
A [per-row request](sources.md#authoring-a-per-row-request) presents a key by its source's
|
|
34
34
|
`connector.credential_mode`.
|
|
35
35
|
|
|
36
|
-
- `none` presents nothing
|
|
36
|
+
- `none` presents nothing. The request is a `GET`. Needs Studio 4.49: it may be a `POST` with a
|
|
37
|
+
`body_template` when `studio_capabilities.keyless_post` is literal `true`.
|
|
37
38
|
- `opaque_reference` presents the secret named in `credential.secret_name`, the way
|
|
38
39
|
`request.auth` says. A saved connection cannot stand in for it.
|
|
39
40
|
- `platform` presents a key Mostly Right holds, so the user enrols nothing.
|
{mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/sources.md
RENAMED
|
@@ -254,10 +254,15 @@ breaks a rule above.
|
|
|
254
254
|
|
|
255
255
|
| `credential_mode` | State | Use it for |
|
|
256
256
|
| --- | --- | --- |
|
|
257
|
-
| `none` | no `credential
|
|
257
|
+
| `none` | no `credential` and no `auth`. A `GET`, or a `POST` with `body_template` (Needs Studio 4.49) | a public API that takes no key |
|
|
258
258
|
| `opaque_reference` | `credential.secret_name` and `request.auth` | a key the user enrolled with `mr-data keys set` |
|
|
259
259
|
| `platform` | no `credential` and no `auth` | an endpoint Mostly Right holds a key for |
|
|
260
260
|
|
|
261
|
+
A keyless per-row `POST` needs Studio 4.49. Use it only when `keyless_post` is literal `true` in
|
|
262
|
+
`studio_capabilities`. Otherwise the client refuses with `THIN_GRAMMAR_NOT_SERVED` and names the
|
|
263
|
+
request `method` first, marked `(keyless_post)`. Send a GET in its place, or wait for the Studio
|
|
264
|
+
deploy.
|
|
265
|
+
|
|
261
266
|
The platform credential needs no key from anyone, and any subscribed workspace may use it. Each
|
|
262
267
|
binding is one route and one pinned model, and `mr-data models --json` lists the ones this
|
|
263
268
|
deployment serves: Clef for classification (`classify`), TypeSafe's Jev through OpenRouter
|
|
@@ -378,18 +383,80 @@ full grammar.
|
|
|
378
383
|
### Needs Studio 4.26
|
|
379
384
|
|
|
380
385
|
Check first: run `mr-data recipe explain RECIPE.json --json` and read `studio_capabilities`.
|
|
381
|
-
Use the following only when `keyless_window` is literal `true
|
|
382
|
-
on this Studio yet". The client refuses with `THIN_GRAMMAR_NOT_SERVED` before registration
|
|
383
|
-
|
|
386
|
+
Use the following only when `keyless_window` is literal `true`. Otherwise report "not available
|
|
387
|
+
on this Studio yet". The client refuses with `THIN_GRAMMAR_NOT_SERVED` before registration.
|
|
388
|
+
An older API may reject the explanation document with a schema 422 before it reports
|
|
389
|
+
capabilities. The client then runs its local checks and lint. A local fault is reported as
|
|
390
|
+
itself. Only a clean document is refused as not served. That refusal marks each member with the
|
|
391
|
+
capability it needs, `keyless_post` members first.
|
|
384
392
|
|
|
385
393
|
`public.https.api@1.0.0` is the credential-free generic HTTPS API adapter. State
|
|
386
394
|
`credential_mode: none`, `origin`, and `generic_api` with exact `auth: {"kind":"none"}`.
|
|
387
|
-
|
|
395
|
+
A request is a GET, or a POST with a `body_template` on Studio 4.49. It presents no credential:
|
|
396
|
+
no credential reference, token, secret header or secret query value.
|
|
388
397
|
This adapter does not relax authentication on the keyed adapters. The owned HTTPS origin,
|
|
389
398
|
host allowlist, bounded request budgets and cursor next-URL restrictions remain in force.
|
|
390
399
|
The existing generic response mapping and JSON/CSV pagination apply; a cursor needs the
|
|
391
400
|
publisher's actual next-token pointer. JSON-pointer pagination does not extract XML OAI-PMH tokens.
|
|
392
401
|
|
|
402
|
+
Needs Studio 4.49. Some APIs take the date range in a JSON body. Send a POST and state the body
|
|
403
|
+
in `body_template`. Use it only when `keyless_post` is literal `true` in `studio_capabilities`.
|
|
404
|
+
Otherwise the client refuses with `THIN_GRAMMAR_NOT_SERVED` and names
|
|
405
|
+
`generic_api/request/method` first, marked `(keyless_post)`. Send a GET in its place, or wait for
|
|
406
|
+
the Studio deploy.
|
|
407
|
+
Declare both window endpoints with `location: parameter`. Each endpoint fills the body
|
|
408
|
+
placeholder of the parameter it names. One request covers one window unit, so state
|
|
409
|
+
`pagination: {"kind": "none", "max_pages": 1}`. The values frozen in `connector.parameters` are
|
|
410
|
+
the first day of the window, the day `start_at` names. This response is a root array, so
|
|
411
|
+
`records_pointer` is `""`. Write `response.reader` the way [Readers](readers.md#needs-studio-426)
|
|
412
|
+
shows. The publisher here is invented:
|
|
413
|
+
|
|
414
|
+
```json
|
|
415
|
+
{
|
|
416
|
+
"connector": {
|
|
417
|
+
"adapter_id": "public.https.api@1.0.0",
|
|
418
|
+
"credential_mode": "none",
|
|
419
|
+
"origin": "https://api.prices.example",
|
|
420
|
+
"parameters": [
|
|
421
|
+
{"name": "from_date", "value": "2026-01-01"},
|
|
422
|
+
{"name": "to_date", "value": "2026-01-01"}
|
|
423
|
+
],
|
|
424
|
+
"generic_api": {
|
|
425
|
+
"request": {
|
|
426
|
+
"method": "POST",
|
|
427
|
+
"path_template": "/v1/prices/query",
|
|
428
|
+
"static_query": [],
|
|
429
|
+
"static_headers": [],
|
|
430
|
+
"body_template": {"market": "day_ahead", "from": "{from_date}", "to": "{to_date}"},
|
|
431
|
+
"parameters": [
|
|
432
|
+
{"name": "from_date", "type": "string", "required": true},
|
|
433
|
+
{"name": "to_date", "type": "string", "required": true}
|
|
434
|
+
]
|
|
435
|
+
},
|
|
436
|
+
"auth": {"kind": "none"},
|
|
437
|
+
"response": {"format": "json", "records_pointer": ""},
|
|
438
|
+
"pagination": {"kind": "none", "max_pages": 1}
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
"window": {
|
|
442
|
+
"start_at": "2026-01-01T00:00:00Z",
|
|
443
|
+
"granularity": "day",
|
|
444
|
+
"timezone": "UTC",
|
|
445
|
+
"lookback_seconds": 86400,
|
|
446
|
+
"max_span_seconds": 31622400,
|
|
447
|
+
"request": {
|
|
448
|
+
"start": {"encoding": "iso_date", "bound": "inclusive", "location": "parameter", "parameter": "from_date"},
|
|
449
|
+
"end": {"encoding": "iso_date", "bound": "inclusive", "location": "parameter", "parameter": "to_date"}
|
|
450
|
+
},
|
|
451
|
+
"merge": {
|
|
452
|
+
"materialization": "partition_replace",
|
|
453
|
+
"partition": {"column": "date", "key": "iso_date_value"},
|
|
454
|
+
"row_identity": ["date", "hour"]
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
393
460
|
For Binance klines use parameter-windowed `startTime`/`endTime`, millisecond endpoints and
|
|
394
461
|
12-hour slices at one request per second. Follow the backfill and hourly-refresh example in
|
|
395
462
|
[making a table incremental](making-a-table-incremental.md#keyless-api-windows-studio-426)
|
{mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/recipe.py
RENAMED
|
@@ -168,7 +168,7 @@ _BOUNDED_STREAMING_POLICY = "bounded-streaming.v1"
|
|
|
168
168
|
FILE_DECODER_COORDINATES: frozenset[tuple[str, str]] = frozenset(
|
|
169
169
|
(family, version)
|
|
170
170
|
for family, versions in {
|
|
171
|
-
"archive.gzip": ("1.0.0", "1.1.0", "2.0.0"),
|
|
171
|
+
"archive.gzip": ("1.0.0", "1.1.0", "2.0.0", "2.1.0"),
|
|
172
172
|
"archive.tar": ("1.0.0", "1.1.0", "3.0.0", "4.0.0"),
|
|
173
173
|
"archive.zip": ("1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0", "1.5.0", "3.0.0"),
|
|
174
174
|
"delimited_text": ("1.0.0", "1.1.0", "2.0.0"),
|
|
@@ -1431,9 +1431,10 @@ def register(
|
|
|
1431
1431
|
) -> dict[str, Any]:
|
|
1432
1432
|
"""``mr-data recipe FILE``: register one document and print what the server derived from it.
|
|
1433
1433
|
|
|
1434
|
-
Existing documents are linted before a credential is resolved. A keyless
|
|
1435
|
-
asks what Studio serves before local lint,
|
|
1436
|
-
before reporting capabilities. The brief and
|
|
1434
|
+
Existing documents are linted before a credential is resolved. A document using keyless
|
|
1435
|
+
grammar (``keyless_window`` or ``keyless_post``) asks what Studio serves before local lint,
|
|
1436
|
+
because an older schema may reject the grammar before reporting capabilities. The brief and
|
|
1437
|
+
delegation gates follow, and registration is last.
|
|
1437
1438
|
"""
|
|
1438
1439
|
|
|
1439
1440
|
document = read_document(Path(args.target))
|
|
@@ -1442,13 +1443,16 @@ def register(
|
|
|
1442
1443
|
v4.recipe_registration_body(document, "00000000-0000-0000-0000-000000000000")
|
|
1443
1444
|
# New grammar must be refused as unserved before local lint can diagnose its shape.
|
|
1444
1445
|
keyless = any(
|
|
1445
|
-
capability
|
|
1446
|
+
capability in recipe_explain.KEYLESS_CAPABILITIES
|
|
1447
|
+
for _, capability in recipe_explain.gated_grammar(document)
|
|
1446
1448
|
)
|
|
1447
1449
|
selected = client
|
|
1448
1450
|
served = []
|
|
1449
1451
|
if keyless:
|
|
1450
1452
|
selected = selected or _client(args)
|
|
1451
|
-
served = recipe_explain.registration_gate(
|
|
1453
|
+
served = recipe_explain.registration_gate(
|
|
1454
|
+
selected, document, lint=not getattr(args, "no_lint", False)
|
|
1455
|
+
)
|
|
1452
1456
|
check_document(document)
|
|
1453
1457
|
if not getattr(args, "no_lint", False):
|
|
1454
1458
|
refusal = lint_refusal(
|
|
@@ -35,6 +35,13 @@ from urllib.parse import parse_qsl, urlsplit
|
|
|
35
35
|
from uuid import UUID
|
|
36
36
|
|
|
37
37
|
from mostlyright.data_harness.thin import THIN_SCHEMA_PREFIX, numbered
|
|
38
|
+
from mostlyright.data_harness.thin.recipe_lint import (
|
|
39
|
+
KEYLESS_API_ADAPTER_ID,
|
|
40
|
+
ROW_REQUEST_ADAPTER_ID,
|
|
41
|
+
lint_document,
|
|
42
|
+
lint_refusal,
|
|
43
|
+
)
|
|
44
|
+
from mostlyright.data_harness.thin.studio_capabilities import KEYLESS_CAPABILITIES, KEYLESS_POST
|
|
38
45
|
from mostlyright.data_harness.thin.transport import ThinLaneError
|
|
39
46
|
from mostlyright.data_harness.thin.v4 import (
|
|
40
47
|
EXPLAIN_AND_FORECAST_REQUEST_VERSION,
|
|
@@ -131,6 +138,7 @@ CAPABILITY_NAMES: tuple[str, ...] = (
|
|
|
131
138
|
"snapshot_refresh",
|
|
132
139
|
"row_request_batch",
|
|
133
140
|
"keyless_window",
|
|
141
|
+
"keyless_post",
|
|
134
142
|
)
|
|
135
143
|
|
|
136
144
|
#: What a Studio that answers 404 or 405 from the explanation routes serves (README D27).
|
|
@@ -1228,12 +1236,17 @@ def _explanation(answer: Any, path: str) -> dict[str, Any]:
|
|
|
1228
1236
|
|
|
1229
1237
|
|
|
1230
1238
|
def explain_document_at_studio(
|
|
1231
|
-
client: StudioV4Client, document: Mapping[str, Any]
|
|
1239
|
+
client: StudioV4Client, document: Mapping[str, Any], *, lint: bool = True
|
|
1232
1240
|
) -> dict[str, Any] | None:
|
|
1233
1241
|
"""``POST /v4/recipes:explain-refresh``, or ``None`` when this Studio does not serve it.
|
|
1234
1242
|
|
|
1235
1243
|
Studio judges the document alone: it reads no predecessor, registers nothing and writes
|
|
1236
1244
|
nothing, which is what makes it safe to ask before a registration.
|
|
1245
|
+
|
|
1246
|
+
An older API's schema answers 422 to a keyless document, and also to a document that is
|
|
1247
|
+
simply wrong. So that 422 is checked against the local document rules and, unless ``lint`` is
|
|
1248
|
+
false, the local lint. A local fault is raised as itself. Only a document that passes both is
|
|
1249
|
+
reported as not served.
|
|
1237
1250
|
"""
|
|
1238
1251
|
|
|
1239
1252
|
body = {
|
|
@@ -1254,23 +1267,66 @@ def explain_document_at_studio(
|
|
|
1254
1267
|
if route_unavailable(refusal):
|
|
1255
1268
|
return None
|
|
1256
1269
|
keyless = [
|
|
1257
|
-
pointer
|
|
1270
|
+
(pointer, capability)
|
|
1258
1271
|
for pointer, capability in gated_grammar(document)
|
|
1259
|
-
if capability
|
|
1272
|
+
if capability in KEYLESS_CAPABILITIES
|
|
1260
1273
|
]
|
|
1261
1274
|
if keyless and getattr(refusal, "http_status", None) == 422:
|
|
1275
|
+
_raise_local_fault(document, lint=lint, cause=refusal)
|
|
1262
1276
|
# Older API schemas reject the document before they can answer capabilities.
|
|
1263
1277
|
# A schema rejection is never evidence that this grammar is served.
|
|
1264
1278
|
raise ThinLaneError(
|
|
1265
1279
|
GRAMMAR_NOT_SERVED_CODE,
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1280
|
+
not_served_detail(
|
|
1281
|
+
keyless,
|
|
1282
|
+
"Studio rejected the explanation document before reporting capabilities, "
|
|
1283
|
+
"and the document passes the local checks",
|
|
1284
|
+
known=False,
|
|
1285
|
+
),
|
|
1269
1286
|
) from refusal
|
|
1270
1287
|
raise
|
|
1271
1288
|
return _explanation(answer, EXPLAIN_REFRESH_PATH)
|
|
1272
1289
|
|
|
1273
1290
|
|
|
1291
|
+
def _raise_local_fault(document: Mapping[str, Any], *, lint: bool, cause: ThinLaneError) -> None:
|
|
1292
|
+
"""Raise the local finding a schema 422 may stand for, or return when there is none."""
|
|
1293
|
+
|
|
1294
|
+
from mostlyright.data_harness.thin import recipe as recipe_module
|
|
1295
|
+
|
|
1296
|
+
try:
|
|
1297
|
+
recipe_module.check_document(dict(document))
|
|
1298
|
+
except ThinLaneError as fault:
|
|
1299
|
+
raise fault from cause
|
|
1300
|
+
if lint:
|
|
1301
|
+
fault = lint_refusal(lint_document(document), subject="the recipe document")
|
|
1302
|
+
if fault is not None:
|
|
1303
|
+
raise fault from cause
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
def not_served_detail(missing: Sequence[tuple[str, str]], why: str, *, known: bool) -> str:
|
|
1307
|
+
"""The not-served sentence: each pointer with the capability it needs, ``keyless_post`` first.
|
|
1308
|
+
|
|
1309
|
+
A keyless POST is the newest gated grammar, so where one is listed its method is named first
|
|
1310
|
+
and its repair is a GET in its place. ``known`` says whether Studio stated that the other
|
|
1311
|
+
members are unserved. After a schema 422 it did not, so they are not told to be removed.
|
|
1312
|
+
"""
|
|
1313
|
+
|
|
1314
|
+
ordered = sorted(missing, key=lambda item: item[1] != KEYLESS_POST)
|
|
1315
|
+
listed = ", ".join(f"{pointer} ({capability})" for pointer, capability in ordered)
|
|
1316
|
+
post = any(capability == KEYLESS_POST for _, capability in missing)
|
|
1317
|
+
others = any(capability != KEYLESS_POST for _, capability in missing)
|
|
1318
|
+
if post and others and known:
|
|
1319
|
+
repair = "send each keyless_post request as a GET, and remove the other members"
|
|
1320
|
+
elif post:
|
|
1321
|
+
repair = "send each keyless_post request as a GET"
|
|
1322
|
+
else:
|
|
1323
|
+
repair = "remove the member"
|
|
1324
|
+
return (
|
|
1325
|
+
f"not available on this Studio yet: {listed}. {why}. Nothing was registered. Wait for "
|
|
1326
|
+
f"the Studio deploy, or {repair}"
|
|
1327
|
+
)
|
|
1328
|
+
|
|
1329
|
+
|
|
1274
1330
|
def explain_table_at_studio(client: StudioV4Client, table_id: str) -> dict[str, Any] | None:
|
|
1275
1331
|
"""``GET /v4/tables/{table_id}/refresh-explanation``, or ``None`` when it is not served."""
|
|
1276
1332
|
|
|
@@ -1308,7 +1364,9 @@ def gated_grammar(document: Mapping[str, Any]) -> list[tuple[str, str]]:
|
|
|
1308
1364
|
``row_filter`` on any collection discovery, and a per-row request collection whose
|
|
1309
1365
|
``pages.revisit`` states ``interval`` (4.14.0). A revisit on any other collection is old
|
|
1310
1366
|
grammar. ``batch`` on a per-row request (4.15.0). The public generic API, no-auth kind,
|
|
1311
|
-
millisecond endpoints and slice/pacing members (4.26.0) each need ``keyless_window``.
|
|
1367
|
+
millisecond endpoints and slice/pacing members (4.26.0) each need ``keyless_window``. A POST
|
|
1368
|
+
that presents no credential (4.49.0) needs ``keyless_post``: a ``public.https.api@1.0.0``
|
|
1369
|
+
request, or a per-row request under ``credential_mode: none``. Its pointer is the method.
|
|
1312
1370
|
"""
|
|
1313
1371
|
|
|
1314
1372
|
gated: list[tuple[str, str]] = []
|
|
@@ -1323,12 +1381,16 @@ def gated_grammar(document: Mapping[str, Any]) -> list[tuple[str, str]]:
|
|
|
1323
1381
|
prefix = f"/sources/{index}"
|
|
1324
1382
|
connector = source.get("connector")
|
|
1325
1383
|
if isinstance(connector, Mapping):
|
|
1326
|
-
|
|
1384
|
+
keyless_api = connector.get("adapter_id") == KEYLESS_API_ADAPTER_ID
|
|
1385
|
+
if keyless_api:
|
|
1327
1386
|
gated.append((f"{prefix}/connector/adapter_id", "keyless_window"))
|
|
1328
1387
|
generic = connector.get("generic_api")
|
|
1329
1388
|
auth = generic.get("auth") if isinstance(generic, Mapping) else None
|
|
1330
1389
|
if isinstance(auth, Mapping) and auth.get("kind") == "none":
|
|
1331
1390
|
gated.append((f"{prefix}/connector/generic_api/auth/kind", "keyless_window"))
|
|
1391
|
+
request = generic.get("request") if isinstance(generic, Mapping) else None
|
|
1392
|
+
if keyless_api and isinstance(request, Mapping) and request.get("method") == "POST":
|
|
1393
|
+
gated.append((f"{prefix}/connector/generic_api/request/method", "keyless_post"))
|
|
1332
1394
|
window = source.get("window")
|
|
1333
1395
|
merge = window.get("merge") if isinstance(window, Mapping) else None
|
|
1334
1396
|
partition = merge.get("partition") if isinstance(merge, Mapping) else None
|
|
@@ -1363,6 +1425,14 @@ def gated_grammar(document: Mapping[str, Any]) -> list[tuple[str, str]]:
|
|
|
1363
1425
|
gated.append(
|
|
1364
1426
|
(f"/sources/{index}/collection/discovery/request/batch", "row_request_batch")
|
|
1365
1427
|
)
|
|
1428
|
+
if (
|
|
1429
|
+
isinstance(connector, Mapping)
|
|
1430
|
+
and connector.get("adapter_id") == ROW_REQUEST_ADAPTER_ID
|
|
1431
|
+
and connector.get("credential_mode") == "none"
|
|
1432
|
+
and isinstance(request, Mapping)
|
|
1433
|
+
and request.get("method") == "POST"
|
|
1434
|
+
):
|
|
1435
|
+
gated.append((f"/sources/{index}/collection/discovery/request/method", "keyless_post"))
|
|
1366
1436
|
return gated
|
|
1367
1437
|
|
|
1368
1438
|
|
|
@@ -1381,7 +1451,9 @@ def snapshot_sources(document: Mapping[str, Any]) -> list[str]:
|
|
|
1381
1451
|
return named
|
|
1382
1452
|
|
|
1383
1453
|
|
|
1384
|
-
def registration_gate(
|
|
1454
|
+
def registration_gate(
|
|
1455
|
+
client: StudioV4Client, document: Mapping[str, Any], *, lint: bool = True
|
|
1456
|
+
) -> list[str]:
|
|
1385
1457
|
"""Refuse grammar this Studio does not serve, and return the snapshot warning when it applies.
|
|
1386
1458
|
|
|
1387
1459
|
Asks the document route only when the answer can change what happens: the document uses
|
|
@@ -1389,14 +1461,15 @@ def registration_gate(client: StudioV4Client, document: Mapping[str, Any]) -> li
|
|
|
1389
1461
|
other document registers exactly as before, with no extra request.
|
|
1390
1462
|
|
|
1391
1463
|
⚠ REFUSED BEFORE ANYTHING IS WRITTEN. The caller runs this before the brief gate's delegation
|
|
1392
|
-
append and before the registration, so a refusal here leaves nothing behind.
|
|
1464
|
+
append and before the registration, so a refusal here leaves nothing behind. ``lint`` is
|
|
1465
|
+
false under ``--no-lint``, so an older schema's 422 is then not checked against local lint.
|
|
1393
1466
|
"""
|
|
1394
1467
|
|
|
1395
1468
|
gated = gated_grammar(document)
|
|
1396
1469
|
snapshots = snapshot_sources(document)
|
|
1397
1470
|
if not gated and not snapshots:
|
|
1398
1471
|
return []
|
|
1399
|
-
answer = explain_document_at_studio(client, document)
|
|
1472
|
+
answer = explain_document_at_studio(client, document, lint=lint)
|
|
1400
1473
|
return grammar_gate(document, answer)
|
|
1401
1474
|
|
|
1402
1475
|
|
|
@@ -1406,18 +1479,16 @@ def grammar_gate(document: Mapping[str, Any], answer: Mapping[str, Any] | None)
|
|
|
1406
1479
|
gated = gated_grammar(document)
|
|
1407
1480
|
snapshots = snapshot_sources(document)
|
|
1408
1481
|
capabilities = served_capabilities(answer)
|
|
1409
|
-
missing = [
|
|
1482
|
+
missing = [
|
|
1483
|
+
(pointer, capability) for pointer, capability in gated if not capabilities[capability]
|
|
1484
|
+
]
|
|
1410
1485
|
if missing:
|
|
1411
1486
|
why = (
|
|
1412
|
-
"
|
|
1487
|
+
"This Studio does not serve the refresh explanation, so it predates 4.14.0"
|
|
1413
1488
|
if answer is None
|
|
1414
|
-
else "
|
|
1415
|
-
)
|
|
1416
|
-
raise ThinLaneError(
|
|
1417
|
-
GRAMMAR_NOT_SERVED_CODE,
|
|
1418
|
-
f"not available on this Studio yet: {', '.join(missing)} ({why}). Nothing was "
|
|
1419
|
-
"registered. Wait for the Studio deploy, or remove the member",
|
|
1489
|
+
else "This Studio's capabilities say it does not serve them"
|
|
1420
1490
|
)
|
|
1491
|
+
raise ThinLaneError(GRAMMAR_NOT_SERVED_CODE, not_served_detail(missing, why, known=True))
|
|
1421
1492
|
if snapshots and not capabilities["snapshot_refresh"]:
|
|
1422
1493
|
return [
|
|
1423
1494
|
f"{SNAPSHOT_WILL_NOT_REFRESH_YET}: {', '.join(repr(name) for name in snapshots)} "
|
|
@@ -1513,7 +1584,9 @@ def explain(args: argparse.Namespace, *, client: StudioV4Client | None = None) -
|
|
|
1513
1584
|
else:
|
|
1514
1585
|
document = recipe_module.read_document(Path(target))
|
|
1515
1586
|
selected = client or recipe_module._client(args)
|
|
1516
|
-
keyless = any(
|
|
1587
|
+
keyless = any(
|
|
1588
|
+
capability in KEYLESS_CAPABILITIES for _, capability in gated_grammar(document)
|
|
1589
|
+
)
|
|
1517
1590
|
if keyless:
|
|
1518
1591
|
answer = explain_document_at_studio(selected, document)
|
|
1519
1592
|
grammar_gate(document, answer)
|
|
@@ -1625,6 +1698,7 @@ __all__ = [
|
|
|
1625
1698
|
"EXPLAIN_REFRESH_PATH",
|
|
1626
1699
|
"EXPLAIN_SCHEMA",
|
|
1627
1700
|
"GRAMMAR_NOT_SERVED_CODE",
|
|
1701
|
+
"KEYLESS_CAPABILITIES",
|
|
1628
1702
|
"MIRRORED_RULE_CODES",
|
|
1629
1703
|
"NO_CAPABILITIES",
|
|
1630
1704
|
"ROUTES_REQUEST_VERSION",
|
|
@@ -161,6 +161,9 @@ COHERENCE_CREDENTIAL_MODES: frozenset[str] = frozenset({"none", "opaque_referenc
|
|
|
161
161
|
#: ``request`` instead of ``url_column``, on a source declared through this adapter.
|
|
162
162
|
ROW_REQUEST_ADAPTER_ID = "public.https.collection@2.0.0"
|
|
163
163
|
|
|
164
|
+
#: The generic API adapter that presents no credential (4.26.0).
|
|
165
|
+
KEYLESS_API_ADAPTER_ID = "public.https.api@1.0.0"
|
|
166
|
+
|
|
164
167
|
#: One per-row template token, read left to right: an escaped brace, a ``{name}`` placeholder, a
|
|
165
168
|
#: lone brace, or a run of text with no brace. The registration route and the worker read every
|
|
166
169
|
#: per-row template with this same expression, and no per-row template pattern of
|
|
@@ -237,6 +240,7 @@ def lint_document(document: Mapping[str, Any]) -> list[Finding]:
|
|
|
237
240
|
findings.extend(_refresh_trigger(document))
|
|
238
241
|
findings.extend(_row_filters(document.get("sources")))
|
|
239
242
|
findings.extend(_row_request_credentials(document.get("sources")))
|
|
243
|
+
findings.extend(_keyless_api_requests(document.get("sources")))
|
|
240
244
|
findings.extend(_row_request_templates(document.get("sources")))
|
|
241
245
|
findings.extend(_units(document.get("units")))
|
|
242
246
|
findings.extend(_coherence(document, declared=declared))
|
|
@@ -908,7 +912,10 @@ def _row_request_credentials(sources: Any) -> list[Finding]:
|
|
|
908
912
|
request's origin, method and path says which credential is presented and how.
|
|
909
913
|
- ``opaque_reference`` names a workspace secret in ``credential.secret_name`` and states
|
|
910
914
|
``auth``, which says how that secret is presented.
|
|
911
|
-
- ``none`` states no ``auth
|
|
915
|
+
- ``none`` states no ``auth``.
|
|
916
|
+
|
|
917
|
+
Whatever the mode, a request sends a GET with no ``body_template``, or a POST with one.
|
|
918
|
+
``batch`` stays GET-only, which :func:`_row_request_batch_coherence` holds.
|
|
912
919
|
|
|
913
920
|
Each is a schema rule, so each finding carries :data:`DOCUMENT_INVALID`. Which binding admits
|
|
914
921
|
a ``platform`` request is deployment configuration that no document carries, so that question
|
|
@@ -1006,16 +1013,87 @@ def _row_request_credentials(sources: Any) -> list[Finding]:
|
|
|
1006
1013
|
"name one with 'opaque_reference', or drop it",
|
|
1007
1014
|
)
|
|
1008
1015
|
)
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1016
|
+
if request is not None:
|
|
1017
|
+
findings.extend(_request_method(request, at=at, subject="A per-row request"))
|
|
1018
|
+
return findings
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def _request_method(request: Mapping[str, Any], *, at: str, subject: str) -> list[Finding]:
|
|
1022
|
+
"""The generic request rule: a GET states no ``body_template``, and a POST states one.
|
|
1023
|
+
|
|
1024
|
+
``subject`` begins the sentence a wrong method earns. A request that states no method is the
|
|
1025
|
+
schema's missing-member fault, and nothing is read off it here.
|
|
1026
|
+
"""
|
|
1027
|
+
|
|
1028
|
+
if "method" not in request:
|
|
1029
|
+
return []
|
|
1030
|
+
method = request.get("method")
|
|
1031
|
+
if _outside(method, frozenset({"GET", "POST"})):
|
|
1032
|
+
return [
|
|
1033
|
+
Finding(
|
|
1034
|
+
DOCUMENT_INVALID,
|
|
1035
|
+
f"{at}/method",
|
|
1036
|
+
f"is {method!r}. {subject} is a GET, or a POST with a body_template",
|
|
1037
|
+
)
|
|
1038
|
+
]
|
|
1039
|
+
if method == "POST" and "body_template" not in request:
|
|
1040
|
+
return [
|
|
1041
|
+
Finding(
|
|
1042
|
+
DOCUMENT_INVALID,
|
|
1043
|
+
f"{at}/body_template",
|
|
1044
|
+
"is absent, and a POST sends the body this member renders",
|
|
1045
|
+
)
|
|
1046
|
+
]
|
|
1047
|
+
if method == "GET" and "body_template" in request:
|
|
1048
|
+
return [
|
|
1049
|
+
Finding(
|
|
1050
|
+
DOCUMENT_INVALID,
|
|
1051
|
+
f"{at}/body_template",
|
|
1052
|
+
"is stated on a GET, which sends no body. Make the request a POST, or drop it",
|
|
1053
|
+
)
|
|
1054
|
+
]
|
|
1055
|
+
return []
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def _keyless_api_requests(sources: Any) -> list[Finding]:
|
|
1059
|
+
"""A ``public.https.api@1.0.0`` request, held to the rules Studio's parser holds it to.
|
|
1060
|
+
|
|
1061
|
+
The adapter presents no credential, so ``generic_api.auth`` is exactly ``{"kind": "none"}``.
|
|
1062
|
+
Its request is a GET with no ``body_template``, or a POST with one (4.49.0). Each is a schema
|
|
1063
|
+
rule, so each finding carries :data:`DOCUMENT_INVALID`.
|
|
1064
|
+
"""
|
|
1065
|
+
|
|
1066
|
+
if not isinstance(sources, list):
|
|
1067
|
+
return []
|
|
1068
|
+
findings: list[Finding] = []
|
|
1069
|
+
for index, source in enumerate(sources):
|
|
1070
|
+
if not isinstance(source, Mapping):
|
|
1071
|
+
continue
|
|
1072
|
+
connector = source.get("connector")
|
|
1073
|
+
if not isinstance(connector, Mapping):
|
|
1074
|
+
continue
|
|
1075
|
+
generic = connector.get("generic_api")
|
|
1076
|
+
if connector.get("adapter_id") != KEYLESS_API_ADAPTER_ID or not isinstance(
|
|
1077
|
+
generic, Mapping
|
|
1078
|
+
):
|
|
1079
|
+
continue
|
|
1080
|
+
at = f"/sources/{index}/connector/generic_api"
|
|
1081
|
+
if "auth" in generic and generic.get("auth") != {"kind": "none"}:
|
|
1082
|
+
findings.append(
|
|
1083
|
+
Finding(
|
|
1084
|
+
DOCUMENT_INVALID,
|
|
1085
|
+
f"{at}/auth",
|
|
1086
|
+
f"is {generic.get('auth')!r}. A {KEYLESS_API_ADAPTER_ID} source presents no "
|
|
1087
|
+
'credential, so its auth is exactly {"kind": "none"}',
|
|
1088
|
+
)
|
|
1089
|
+
)
|
|
1090
|
+
request = generic.get("request")
|
|
1091
|
+
if isinstance(request, Mapping):
|
|
1092
|
+
findings.extend(
|
|
1093
|
+
_request_method(
|
|
1094
|
+
request, at=f"{at}/request", subject=f"A {KEYLESS_API_ADAPTER_ID} request"
|
|
1018
1095
|
)
|
|
1096
|
+
)
|
|
1019
1097
|
return findings
|
|
1020
1098
|
|
|
1021
1099
|
|
|
@@ -1292,8 +1370,7 @@ def _row_request_batch_coherence(sources: Any) -> list[Finding]:
|
|
|
1292
1370
|
Finding(
|
|
1293
1371
|
DOCUMENT_INCOHERENT,
|
|
1294
1372
|
f"{at}/batch/parameter",
|
|
1295
|
-
f"names {name!r}, which must be declared with required: true to be "
|
|
1296
|
-
"batched",
|
|
1373
|
+
f"names {name!r}, which must be declared with required: true to be batched",
|
|
1297
1374
|
)
|
|
1298
1375
|
)
|
|
1299
1376
|
sites = [
|
|
@@ -1354,8 +1431,7 @@ def _table_partition_coherence(document: Mapping[str, Any], *, known: set[str])
|
|
|
1354
1431
|
sources = document.get("sources")
|
|
1355
1432
|
sources = sources if isinstance(sources, list) else []
|
|
1356
1433
|
if any(
|
|
1357
|
-
isinstance(source, Mapping) and source.get("source_class") == "stream"
|
|
1358
|
-
for source in sources
|
|
1434
|
+
isinstance(source, Mapping) and source.get("source_class") == "stream" for source in sources
|
|
1359
1435
|
):
|
|
1360
1436
|
findings.append(
|
|
1361
1437
|
Finding(
|