mostlyright-data 0.30.0__tar.gz → 0.30.2__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.30.0 → mostlyright_data-0.30.2}/PKG-INFO +1 -1
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/preparing-table-search.md +12 -8
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/progress_rate.py +145 -29
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/table_search_contract.json +1 -1
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4.py +2 -2
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_search.py +144 -75
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_stream.py +10 -2
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/.gitignore +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/README.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/pyproject.toml +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/scripts/hatch_build.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/SKILL.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/agents/openai.yaml +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/1-open-the-page-and-the-link-to-it-in-the-first-message.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/agent-protocol.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/boundaries.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/commands.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/installation-parity.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/live-run.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/making-a-table-incremental.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/narrating-the-run.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/one-install.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/patterns.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/prediction-labels.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/promote.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/readers.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/receipts.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/recipe-test.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/reference-pages.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/required-protocol.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/source-credentials.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/sources.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/transforms.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/user-communication-contract.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/__init__.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/agent_protocol.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/canonical.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/formats.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/key_seam.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/page_coverage.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/session_probes.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/skill_assets.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/table_manifest.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/__init__.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/acquire.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/activity.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/approvals.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/backoff.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/categories.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/commands.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/download.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/fast_features.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/narrative.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/parity.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/probe.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/propose.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/recipe.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/recipe_explain.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/recipe_forecast.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/recipe_test.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/refresh_rehearsal.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/release_check.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/research.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/router.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/run_diagnose.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/run_prefix.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/runs.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/session.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/stream.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/studio_capabilities.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/support.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/transport.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_public.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_query.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_search_contract.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/__init__.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/attendance.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/clarification.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/credentials.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/login.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/remediation.py +0 -0
- {mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/render.py +0 -0
|
@@ -199,8 +199,8 @@ The `local` block lists each refusal with a JSON pointer. Repair every diagnosti
|
|
|
199
199
|
|
|
200
200
|
Register and build the table as usual. A finished build of a table with `table.search` prepares
|
|
201
201
|
search in the background. Data publication never waits for it. The live search package that
|
|
202
|
-
`search try`
|
|
203
|
-
|
|
202
|
+
`search try` uses exists only for a table published publicly. Read the version ID from the run
|
|
203
|
+
receipt, then check readiness:
|
|
204
204
|
|
|
205
205
|
```sh
|
|
206
206
|
mr-data search status TABLE_ID --version VERSION_ID --json
|
|
@@ -280,12 +280,14 @@ mr-data search locate TABLE_ID --public-table PUBLIC_TABLE_ID --title "Dune" --y
|
|
|
280
280
|
mr-data search locate TABLE_ID --public-table PUBLIC_TABLE_ID --id 27205 --json
|
|
281
281
|
```
|
|
282
282
|
|
|
283
|
-
`locate`
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
283
|
+
`locate` reads the public table's rows and maps each matching row to its locator. It needs no
|
|
284
|
+
search package, so write the cases before the version has one. The table must be published
|
|
285
|
+
publicly with its preview on. It reads the title, date and identifier columns from the field
|
|
286
|
+
roles. Override them with `--title-column`, `--date-column` and `--id-column`. A title matches
|
|
287
|
+
exactly, with the same case and spacing as the cell. A year matches the start of the date
|
|
288
|
+
column's value. A title without a year can match several rows. `locate` then lists them all and
|
|
289
|
+
exits 4. Add the year or use the id. It writes `--out` only when every target resolved and the
|
|
290
|
+
cases meet the contract.
|
|
289
291
|
|
|
290
292
|
Locators belong to one sealed version. `locate` resolves rows of the live version only. Configure
|
|
291
293
|
the cases on that same version. A later version has a different row layout, so locate again after
|
|
@@ -313,6 +315,8 @@ Leave out `--spec` to declare the `table.search` spec of the recipe that sealed
|
|
|
313
315
|
Use that to add evaluation cases without retyping the spec. Without `--spec` the client cannot
|
|
314
316
|
check locally that semantic cases include a paraphrase and a hard negative. Studio checks them
|
|
315
317
|
against the recipe's spec. A recipe with no `table.search` answers 409 `SEARCH_NOT_CONFIGURED`.
|
|
318
|
+
`search status --job` reads any search job: the build, its package and embed steps, the
|
|
319
|
+
evaluator and a title job. A job still `queued`, `dispatched` or `running` exits 3.
|
|
316
320
|
`--expected-revision` is the current draft from `search status` (`selected_draft_revision_id`),
|
|
317
321
|
or `none`. Reuse `--idempotency-key` only to retry the same configuration. When the job succeeds,
|
|
318
322
|
`search status TABLE_ID --version VERSION_ID --revision REVISION_ID` reports `not_activated` and
|
|
@@ -6,10 +6,10 @@ Each tick also carries the time it happened. Two ticks of one source give a rate
|
|
|
6
6
|
count gives what is left. Nothing here is a new Studio fact: it is the arithmetic an agent used to
|
|
7
7
|
do by hand while polling.
|
|
8
8
|
|
|
9
|
-
``pages_processed``
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
``pages_processed`` combines saved answers replayed and requests sent. Its rate is therefore
|
|
10
|
+
source-member throughput, never publisher throughput or whole-run completion. Live status rates
|
|
11
|
+
require reaching the exact snapshot event, matching its attempt, and a fresh source observation.
|
|
12
|
+
Reaching that cursor does not establish complete source inventory or complete event history.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
@@ -19,7 +19,7 @@ import time
|
|
|
19
19
|
from collections import deque
|
|
20
20
|
from collections.abc import Callable, Mapping
|
|
21
21
|
from dataclasses import dataclass, field
|
|
22
|
-
from datetime import datetime
|
|
22
|
+
from datetime import UTC, datetime
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
25
|
from mostlyright.data_harness.thin.stream import RunEventStream
|
|
@@ -32,6 +32,13 @@ TRAILING_WINDOW_SECONDS = 600.0
|
|
|
32
32
|
#: How often ``watch`` prints a rate line for one source, in seconds of event time.
|
|
33
33
|
RATE_LINE_EVERY_SECONDS = 60.0
|
|
34
34
|
|
|
35
|
+
#: A rate is a live estimate only while its own latest observation is this recent.
|
|
36
|
+
LIVE_FRESHNESS_SECONDS = 30.0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def utc_now() -> datetime:
|
|
40
|
+
return datetime.now(UTC)
|
|
41
|
+
|
|
35
42
|
|
|
36
43
|
def parse_instant(value: Any) -> datetime | None:
|
|
37
44
|
"""An RFC 3339 instant as Studio stamps it, or ``None`` when it is absent or unreadable."""
|
|
@@ -74,16 +81,16 @@ class SourceRate:
|
|
|
74
81
|
planned: int | None = None
|
|
75
82
|
discovery_complete: bool = False
|
|
76
83
|
printed_at: datetime | None = None
|
|
84
|
+
unavailable_reason: str | None = None
|
|
77
85
|
|
|
78
|
-
def observe(self, at: datetime, done: int) ->
|
|
79
|
-
if self.ticks and done < self.ticks[-1][1]:
|
|
80
|
-
# The count went back, so this is a different acquisition. Start again.
|
|
81
|
-
self.ticks.clear()
|
|
82
|
-
self.first = None
|
|
86
|
+
def observe(self, at: datetime, done: int) -> bool:
|
|
83
87
|
if self.ticks and at <= self.ticks[-1][0]:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
return False
|
|
89
|
+
if self.ticks and done < self.ticks[-1][1]:
|
|
90
|
+
# There is no batch identity on these counters. A decrease cannot establish one.
|
|
91
|
+
self.unavailable_reason = "counter_regressed_without_scope_change"
|
|
92
|
+
self.planned = None
|
|
93
|
+
self.discovery_complete = False
|
|
87
94
|
self.ticks.append((at, done))
|
|
88
95
|
if self.first is None:
|
|
89
96
|
self.first = (at, done)
|
|
@@ -92,6 +99,7 @@ class SourceRate:
|
|
|
92
99
|
and (at - self.ticks[1][0]).total_seconds() >= TRAILING_WINDOW_SECONDS
|
|
93
100
|
):
|
|
94
101
|
self.ticks.popleft()
|
|
102
|
+
return True
|
|
95
103
|
|
|
96
104
|
@property
|
|
97
105
|
def done(self) -> int | None:
|
|
@@ -100,7 +108,7 @@ class SourceRate:
|
|
|
100
108
|
def per_second(self) -> float | None:
|
|
101
109
|
"""The trailing rate, or ``None`` before two ticks with time between them."""
|
|
102
110
|
|
|
103
|
-
if len(self.ticks) < 2:
|
|
111
|
+
if self.unavailable_reason is not None or len(self.ticks) < 2:
|
|
104
112
|
return None
|
|
105
113
|
(start, start_done), (end, end_done) = self.ticks[0], self.ticks[-1]
|
|
106
114
|
elapsed = (end - start).total_seconds()
|
|
@@ -109,7 +117,7 @@ class SourceRate:
|
|
|
109
117
|
def average_per_second(self) -> float | None:
|
|
110
118
|
"""The rate from the first tick to the last, or ``None`` before two ticks."""
|
|
111
119
|
|
|
112
|
-
if self.first is None or not self.ticks:
|
|
120
|
+
if self.unavailable_reason is not None or self.first is None or not self.ticks:
|
|
113
121
|
return None
|
|
114
122
|
elapsed = (self.ticks[-1][0] - self.first[0]).total_seconds()
|
|
115
123
|
return (self.ticks[-1][1] - self.first[1]) / elapsed if elapsed > 0 else None
|
|
@@ -127,6 +135,7 @@ class SourceRate:
|
|
|
127
135
|
or done is None
|
|
128
136
|
or rate is None
|
|
129
137
|
or rate <= 0
|
|
138
|
+
or done > self.planned
|
|
130
139
|
):
|
|
131
140
|
return None
|
|
132
141
|
return max(0, self.planned - done) / rate
|
|
@@ -171,21 +180,46 @@ class RateTracker:
|
|
|
171
180
|
|
|
172
181
|
def __init__(self) -> None:
|
|
173
182
|
self.sources: dict[str, SourceRate] = {}
|
|
183
|
+
self.attempt_id: str | None = None
|
|
184
|
+
self.retired_attempts: set[str] = set()
|
|
185
|
+
self.last_sequence = 0
|
|
186
|
+
self.live_observation: dict[str, Any] | None = None
|
|
174
187
|
|
|
175
188
|
def observe(self, body: Mapping[str, Any]) -> SourceRate | None:
|
|
176
189
|
"""Fold one run event record, and answer the source it moved, or ``None``."""
|
|
177
190
|
|
|
191
|
+
sequence = _count(body.get("sequence"))
|
|
192
|
+
if sequence is None or sequence <= self.last_sequence:
|
|
193
|
+
return None
|
|
194
|
+
self.last_sequence = sequence
|
|
178
195
|
event_type = body.get("event_type")
|
|
179
|
-
if isinstance(event_type, str)
|
|
196
|
+
if not isinstance(event_type, str) or not event_type.startswith("run_progress_"):
|
|
180
197
|
return None
|
|
198
|
+
attempt = body.get("attempt_id")
|
|
199
|
+
if isinstance(attempt, str) and attempt:
|
|
200
|
+
if attempt in self.retired_attempts:
|
|
201
|
+
return None
|
|
202
|
+
if attempt != self.attempt_id:
|
|
203
|
+
if self.attempt_id is not None:
|
|
204
|
+
self.retired_attempts.add(self.attempt_id)
|
|
205
|
+
self.sources.clear()
|
|
206
|
+
self.attempt_id = attempt
|
|
181
207
|
facts = tick_facts(body)
|
|
182
208
|
at = parse_instant(body.get("occurred_at"))
|
|
183
209
|
done = _count(facts.get("pages_processed"))
|
|
184
210
|
name = facts.get("source_id") or facts.get("source_name")
|
|
185
|
-
if
|
|
211
|
+
if (
|
|
212
|
+
at is None
|
|
213
|
+
or done is None
|
|
214
|
+
or not isinstance(name, str)
|
|
215
|
+
or not name
|
|
216
|
+
or not isinstance(attempt, str)
|
|
217
|
+
or not attempt
|
|
218
|
+
):
|
|
186
219
|
return None
|
|
187
220
|
source = self.sources.setdefault(name, SourceRate(name))
|
|
188
|
-
source.observe(at, done)
|
|
221
|
+
if not source.observe(at, done) or source.unavailable_reason is not None:
|
|
222
|
+
return None
|
|
189
223
|
planned = _count(facts.get("pages_planned"))
|
|
190
224
|
if planned is not None:
|
|
191
225
|
source.planned = planned
|
|
@@ -193,12 +227,17 @@ class RateTracker:
|
|
|
193
227
|
source.discovery_complete = facts["discovery_complete"]
|
|
194
228
|
return source
|
|
195
229
|
|
|
196
|
-
def due_line(
|
|
230
|
+
def due_line(
|
|
231
|
+
self, source: SourceRate, *, wall_clock: Callable[[], datetime] = utc_now
|
|
232
|
+
) -> str | None:
|
|
197
233
|
"""The source's line when one is due, at most once a minute of event time per source."""
|
|
198
234
|
|
|
199
235
|
if not source.ticks:
|
|
200
236
|
return None
|
|
201
237
|
now = source.ticks[-1][0]
|
|
238
|
+
age = (wall_clock() - now).total_seconds()
|
|
239
|
+
if age < 0 or age > LIVE_FRESHNESS_SECONDS:
|
|
240
|
+
return None
|
|
202
241
|
if (
|
|
203
242
|
source.printed_at is not None
|
|
204
243
|
and (now - source.printed_at).total_seconds() < RATE_LINE_EVERY_SECONDS
|
|
@@ -265,7 +304,8 @@ def live_rates(
|
|
|
265
304
|
"""The rates of a live run, from the last ``tail`` records of its event log.
|
|
266
305
|
|
|
267
306
|
Reads the progress snapshot for the newest sequence, then replays the log from ``tail``
|
|
268
|
-
records before it and stops at that sequence.
|
|
307
|
+
records before it and stops at or beyond that sequence. Only observing the exact target
|
|
308
|
+
certifies completion of this bounded replay. Answers ``None`` when the run has reported no
|
|
269
309
|
progress yet. A refusal on the way is raised, and the caller decides what it costs.
|
|
270
310
|
"""
|
|
271
311
|
|
|
@@ -274,14 +314,51 @@ def live_rates(
|
|
|
274
314
|
if type(sequence) is not int or sequence < 1:
|
|
275
315
|
return None
|
|
276
316
|
tracker = RateTracker()
|
|
277
|
-
|
|
317
|
+
target_seen = False
|
|
318
|
+
target_attempt: str | None = None
|
|
319
|
+
observed_at: str | None = None
|
|
320
|
+
from_sequence = max(0, sequence - tail)
|
|
321
|
+
expected_sequence = from_sequence + 1
|
|
322
|
+
sequence_gap = False
|
|
323
|
+
|
|
324
|
+
def observe(body: Mapping[str, Any], _frame_name: str) -> bool:
|
|
325
|
+
nonlocal target_seen, target_attempt, observed_at, expected_sequence, sequence_gap
|
|
326
|
+
observed = _count(body.get("sequence"))
|
|
327
|
+
if observed is None:
|
|
328
|
+
return False
|
|
329
|
+
if observed != expected_sequence:
|
|
330
|
+
sequence_gap = True
|
|
331
|
+
expected_sequence = observed + 1
|
|
332
|
+
instant = parse_instant(body.get("occurred_at"))
|
|
333
|
+
observed_at = instant.isoformat() if instant is not None else None
|
|
334
|
+
if observed <= sequence:
|
|
335
|
+
tracker.observe(body)
|
|
336
|
+
if observed == sequence:
|
|
337
|
+
target_seen = True
|
|
338
|
+
attempt = body.get("attempt_id")
|
|
339
|
+
target_attempt = attempt if isinstance(attempt, str) and attempt else None
|
|
340
|
+
return observed >= sequence
|
|
341
|
+
|
|
342
|
+
followed = RunEventStream(
|
|
278
343
|
client.session, run_id, transport=client.transport, sleep=sleep, path=RUN_EVENTS_PATH
|
|
279
344
|
).follow(
|
|
280
|
-
from_seq=
|
|
281
|
-
observe=
|
|
345
|
+
from_seq=from_sequence,
|
|
346
|
+
observe=observe,
|
|
282
347
|
deadline=clock() + deadline_seconds,
|
|
283
348
|
clock=clock,
|
|
284
349
|
)
|
|
350
|
+
tracker.live_observation = {
|
|
351
|
+
"scope": "bounded_event_tail",
|
|
352
|
+
"from_sequence_exclusive": from_sequence,
|
|
353
|
+
"target_sequence": sequence,
|
|
354
|
+
"observed_sequence": followed.last_sequence,
|
|
355
|
+
"replay_complete": target_seen and not sequence_gap,
|
|
356
|
+
"sequence_gap": sequence_gap,
|
|
357
|
+
"observed_at": observed_at,
|
|
358
|
+
"attempt_id": target_attempt,
|
|
359
|
+
"deadline_expired": followed.expired,
|
|
360
|
+
"end_reason": followed.end_reason,
|
|
361
|
+
}
|
|
285
362
|
return tracker
|
|
286
363
|
|
|
287
364
|
|
|
@@ -314,19 +391,58 @@ def history_rates(
|
|
|
314
391
|
return tracker
|
|
315
392
|
|
|
316
393
|
|
|
317
|
-
def live_rate_members(
|
|
394
|
+
def live_rate_members(
|
|
395
|
+
client: Any, run_id: str, *, wall_clock: Callable[[], datetime] = utc_now, **kwargs: Any
|
|
396
|
+
) -> dict[str, Any]:
|
|
318
397
|
"""The ``status`` receipt members for a live run's rates, or why there are none."""
|
|
319
398
|
|
|
320
399
|
try:
|
|
321
400
|
tracker = live_rates(client, run_id, **kwargs)
|
|
322
401
|
except ThinLaneError as refusal:
|
|
323
402
|
return {"source_rates_unavailable": f"{refusal.code}: {refusal.detail}"}
|
|
324
|
-
if tracker is None
|
|
403
|
+
if tracker is None:
|
|
325
404
|
return {"source_rates_unavailable": "no collection source has reported two ticks yet"}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
405
|
+
observation = dict(tracker.live_observation or {})
|
|
406
|
+
now = wall_clock()
|
|
407
|
+
observation["checked_at"] = now.isoformat()
|
|
408
|
+
result: dict[str, Any] = {"source_rate_observation": observation}
|
|
409
|
+
if not observation.get("replay_complete"):
|
|
410
|
+
result["source_rates_unavailable"] = (
|
|
411
|
+
"incomplete replay: event gap or snapshot event missing"
|
|
412
|
+
)
|
|
413
|
+
return result
|
|
414
|
+
if not observation.get("attempt_id") or observation["attempt_id"] != tracker.attempt_id:
|
|
415
|
+
result["source_rates_unavailable"] = "snapshot attempt could not be established"
|
|
416
|
+
return result
|
|
417
|
+
receipts, lines, unavailable = [], {}, {}
|
|
418
|
+
for source in tracker.sources.values():
|
|
419
|
+
at = source.ticks[-1][0] if source.ticks else None
|
|
420
|
+
age = (now - at).total_seconds() if at is not None else None
|
|
421
|
+
reason = source.unavailable_reason
|
|
422
|
+
if reason is None and (age is None or age < 0):
|
|
423
|
+
reason = "observation_time_unavailable_or_in_future"
|
|
424
|
+
if reason is None and age is not None and age > LIVE_FRESHNESS_SECONDS:
|
|
425
|
+
reason = "stale_observation"
|
|
426
|
+
if reason is None and source.per_second() is None:
|
|
427
|
+
reason = "insufficient_observations"
|
|
428
|
+
if reason is not None:
|
|
429
|
+
unavailable[source.name] = {
|
|
430
|
+
"reason": reason,
|
|
431
|
+
"observed_at": at.isoformat() if at else None,
|
|
432
|
+
"age_seconds": age,
|
|
433
|
+
}
|
|
434
|
+
continue
|
|
435
|
+
receipt = source.to_receipt()
|
|
436
|
+
receipt.update(observed_at=at.isoformat(), age_seconds=age, attempt_id=tracker.attempt_id)
|
|
437
|
+
receipts.append(receipt)
|
|
438
|
+
lines[f"source {len(lines) + 1}"] = source.line()
|
|
439
|
+
if unavailable:
|
|
440
|
+
result["source_rates_unavailable_by_source"] = unavailable
|
|
441
|
+
if receipts:
|
|
442
|
+
result.update(source_rates=receipts, rates=lines)
|
|
443
|
+
else:
|
|
444
|
+
result["source_rates_unavailable"] = "no fresh, same-attempt source rate is available"
|
|
445
|
+
return result
|
|
330
446
|
|
|
331
447
|
|
|
332
448
|
__all__ = [
|
|
@@ -148,8 +148,8 @@ from mostlyright.data_harness.thin.transport import ThinLaneError
|
|
|
148
148
|
#: ``JOB_INVALID`` outright, which in production was every refresh a collection epoch was offered
|
|
149
149
|
#: for rather than only the collection runs. This package must not reach a Studio older than the
|
|
150
150
|
#: commit it pins; ``docs/V4-WORKER-PROTOCOL.md`` states it beside the layout's own ordering rule.
|
|
151
|
-
PINNED_V4_OPENAPI_SOURCE_SHA256 = "
|
|
152
|
-
PINNED_V4_CONTRACT_VERSION = "4.
|
|
151
|
+
PINNED_V4_OPENAPI_SOURCE_SHA256 = "001a5e132e8af9872ac61168e41df4465627f34ac062066b212f1b48a88385f4"
|
|
152
|
+
PINNED_V4_CONTRACT_VERSION = "4.35.0"
|
|
153
153
|
|
|
154
154
|
#: The ``schema_version`` every command body that predates 4.14.0 stamps: ``dataset activity``,
|
|
155
155
|
#: ``table rename``, ``table archive`` and ``dataset archive``. It is the number the 0.25 line
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_search.py
RENAMED
|
@@ -197,6 +197,7 @@ def search_exit_code(payload: dict[str, Any]) -> int:
|
|
|
197
197
|
"queued",
|
|
198
198
|
"dispatched",
|
|
199
199
|
"running",
|
|
200
|
+
"deferred",
|
|
200
201
|
"pending",
|
|
201
202
|
"cancellation_requested",
|
|
202
203
|
}:
|
|
@@ -217,6 +218,12 @@ def search_exit_code(payload: dict[str, Any]) -> int:
|
|
|
217
218
|
|
|
218
219
|
|
|
219
220
|
SEARCH_JOB_SCHEMAS = {"table-search-job.v1", "table-search-job.v2"}
|
|
221
|
+
#: Every job Studio's job read answers. The whole-version (v1) and sharded (v2) parents carry
|
|
222
|
+
#: their schema; the title job carries v3; a package, embed or evaluation child carries none.
|
|
223
|
+
JOB_STATUS_SCHEMAS = {*SEARCH_JOB_SCHEMAS, "table-search-job.v3", None}
|
|
224
|
+
JOB_STATUSES = frozenset(
|
|
225
|
+
{"queued", "dispatched", "running", "deferred", "succeeded", "failed", "cancelled"}
|
|
226
|
+
)
|
|
220
227
|
|
|
221
228
|
RECIPE_INSPECTION_PATH = "/v4/search/recipe-inspection"
|
|
222
229
|
TABLE_SEARCH_PATH = "/v4/tables/{table_id}/search"
|
|
@@ -715,26 +722,60 @@ def _try(
|
|
|
715
722
|
return _payload("try", result)
|
|
716
723
|
|
|
717
724
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
+
#: Rows per page of a locate read. Cloud returns each row's physical locator only on a compact
|
|
726
|
+
#: page, and a compact page holds at most 50 rows.
|
|
727
|
+
LOCATE_PAGE_ROWS = 50
|
|
728
|
+
#: Pages one locate target may read before it reports the rows it found as ambiguous.
|
|
729
|
+
MAX_LOCATE_PAGES = 20
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _public_table(public_table_id: str, public: Any) -> dict[str, Any]:
|
|
733
|
+
"""The public schema's table: which Studio version it serves, and whether it is queryable."""
|
|
734
|
+
|
|
735
|
+
from mostlyright.data_harness.thin.v4_public import PublicCatalogClient, resolve_public_origin
|
|
736
|
+
|
|
737
|
+
selected = public or PublicCatalogClient(resolve_public_origin())
|
|
738
|
+
body = selected.get_table_schema(public_table_id)
|
|
739
|
+
table = body.get("table") if isinstance(body, dict) else None
|
|
740
|
+
if not isinstance(table, dict):
|
|
741
|
+
raise ThinLaneError("THIN_RESPONSE_INVALID", "Cloud returned a schema without a table")
|
|
742
|
+
return table
|
|
725
743
|
|
|
726
744
|
|
|
727
745
|
class _Locator:
|
|
728
|
-
"""Resolves rows of the published live version to evaluation-case locators.
|
|
746
|
+
"""Resolves rows of the published live version to evaluation-case locators.
|
|
747
|
+
|
|
748
|
+
It reads through Cloud's public table query, filtered on the title or identifier column, so
|
|
749
|
+
it needs no search package: cases are written before the version has one. Each row comes
|
|
750
|
+
back with its physical ``[part, row]`` locator. Every read is pinned to the version the first
|
|
751
|
+
one served, and that version must be the one Studio reports live."""
|
|
729
752
|
|
|
730
753
|
def __init__(self, args, client, public, query_client) -> None:
|
|
731
754
|
self.table_id = identifier(_required(args.target, "a table identifier"), "table identifier")
|
|
732
755
|
self.public_table_id = identifier(args.public_table_id, "public table ID")
|
|
733
756
|
self.studio = _studio(client)
|
|
734
|
-
self.
|
|
735
|
-
|
|
736
|
-
)
|
|
737
|
-
|
|
757
|
+
table = _public_table(self.public_table_id, public)
|
|
758
|
+
metadata = _metadata(self.studio, self.table_id, None)
|
|
759
|
+
self.version = metadata.get("table_version_id")
|
|
760
|
+
capabilities = table.get("capabilities") or {}
|
|
761
|
+
search = capabilities.get("search") if isinstance(capabilities, dict) else None
|
|
762
|
+
served = {table.get("version_id")}
|
|
763
|
+
if isinstance(search, dict):
|
|
764
|
+
served.add(search.get("table_version_id"))
|
|
765
|
+
if not self.version or served != {self.version}:
|
|
766
|
+
raise ThinLaneError(
|
|
767
|
+
"THIN_SEARCH_VERSION_MISMATCH",
|
|
768
|
+
f"public table {self.public_table_id} serves version "
|
|
769
|
+
f"{table.get('version_id')}, but table {self.table_id} has live version "
|
|
770
|
+
f"{self.version}. Check --public-table with mr-data public tables DATASET_SLUG. "
|
|
771
|
+
"The public schema can lag a new live version by a few minutes, so retry then.",
|
|
772
|
+
)
|
|
773
|
+
if isinstance(capabilities, dict) and capabilities.get("query") is False:
|
|
774
|
+
raise ThinLaneError(
|
|
775
|
+
"THIN_SEARCH_CAPABILITY_UNAVAILABLE",
|
|
776
|
+
f"public table {self.public_table_id} does not serve queries, so locate cannot "
|
|
777
|
+
"read its rows. Turn on its preview in the dataset's publication settings.",
|
|
778
|
+
)
|
|
738
779
|
if (
|
|
739
780
|
args.table_version_id is not None
|
|
740
781
|
and identifier(args.table_version_id, "table version") != self.version
|
|
@@ -751,22 +792,84 @@ class _Locator:
|
|
|
751
792
|
self.id_column = args.id_column or roles.get("identifier")
|
|
752
793
|
self.counts = _manifest_record_counts(self.studio, self.table_id, self.version)
|
|
753
794
|
self.query_client = query_client
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
)
|
|
761
|
-
if
|
|
795
|
+
self.pin: tuple[str, str] | None = None
|
|
796
|
+
|
|
797
|
+
def _bound(self, answer: dict[str, Any]) -> None:
|
|
798
|
+
served = answer.get("table")
|
|
799
|
+
if not isinstance(served, dict):
|
|
800
|
+
raise ThinLaneError("THIN_RESPONSE_INVALID", "Cloud returned rows without a table")
|
|
801
|
+
coordinates = (served.get("version_id"), served.get("content_digest"))
|
|
802
|
+
if not all(isinstance(value, str) and value for value in coordinates):
|
|
803
|
+
raise ThinLaneError("THIN_RESPONSE_INVALID", "Cloud returned rows without a version")
|
|
804
|
+
live = served.get("live_version_id")
|
|
805
|
+
rows = served.get("row_count")
|
|
806
|
+
if (
|
|
807
|
+
coordinates[0] != self.version
|
|
808
|
+
or live not in (None, self.version)
|
|
809
|
+
or (rows is not None and rows != sum(self.counts))
|
|
810
|
+
or (self.pin is not None and coordinates != self.pin)
|
|
811
|
+
):
|
|
762
812
|
raise ThinLaneError(
|
|
763
|
-
"
|
|
764
|
-
"
|
|
765
|
-
|
|
766
|
-
+ f"). locate reads {', '.join(columns)}. Each must be in the spec's "
|
|
767
|
-
"display_columns, and the package must be ready in mr-data search status.",
|
|
813
|
+
"THIN_SEARCH_VERSION_MISMATCH",
|
|
814
|
+
f"public table {self.public_table_id} stopped serving live version "
|
|
815
|
+
f"{self.version} while locate read it. Run locate again.",
|
|
768
816
|
)
|
|
769
|
-
|
|
817
|
+
self.pin = coordinates
|
|
818
|
+
|
|
819
|
+
def _rows(self, filters: list[dict[str, Any]], columns: list[str]) -> tuple[list, bool]:
|
|
820
|
+
"""Every row matching ``filters``, each with its locator, and whether pages remain."""
|
|
821
|
+
|
|
822
|
+
found: list[dict[str, Any]] = []
|
|
823
|
+
cursor: str | None = None
|
|
824
|
+
for _ in range(MAX_LOCATE_PAGES):
|
|
825
|
+
# A cursor is honoured only beside the query it was issued for, and it carries its
|
|
826
|
+
# own version pin.
|
|
827
|
+
body: dict[str, Any] = {
|
|
828
|
+
"columns": columns,
|
|
829
|
+
"filters": filters,
|
|
830
|
+
"limit": LOCATE_PAGE_ROWS,
|
|
831
|
+
"compact_cells": True,
|
|
832
|
+
}
|
|
833
|
+
if cursor is not None:
|
|
834
|
+
body["cursor"] = cursor
|
|
835
|
+
elif self.pin is not None:
|
|
836
|
+
body.update(version_id=self.pin[0], content_digest=self.pin[1])
|
|
837
|
+
answer, _ = _cloud_query(self.public_table_id, body, self.query_client)
|
|
838
|
+
self._bound(answer)
|
|
839
|
+
rows, refs = answer.get("rows"), answer.get("row_refs")
|
|
840
|
+
if (
|
|
841
|
+
not isinstance(rows, list)
|
|
842
|
+
or not isinstance(refs, list)
|
|
843
|
+
or len(rows) != len(refs)
|
|
844
|
+
or len(rows) > LOCATE_PAGE_ROWS
|
|
845
|
+
):
|
|
846
|
+
raise ThinLaneError(
|
|
847
|
+
"THIN_RESPONSE_INVALID", "Cloud returned rows without their locators"
|
|
848
|
+
)
|
|
849
|
+
for row, ref in zip(rows, refs, strict=True):
|
|
850
|
+
part = ref.get("part_index") if isinstance(ref, dict) else None
|
|
851
|
+
index = ref.get("row_index") if isinstance(ref, dict) else None
|
|
852
|
+
if (
|
|
853
|
+
not isinstance(row, dict)
|
|
854
|
+
or type(part) is not int
|
|
855
|
+
or type(index) is not int
|
|
856
|
+
or not 0 <= part < len(self.counts)
|
|
857
|
+
or not 0 <= index < self.counts[part]
|
|
858
|
+
):
|
|
859
|
+
raise ThinLaneError(
|
|
860
|
+
"THIN_RESPONSE_INVALID", "a located row lies outside the sealed version"
|
|
861
|
+
)
|
|
862
|
+
found.append(
|
|
863
|
+
{
|
|
864
|
+
"row_id": sum(self.counts[:part]) + index,
|
|
865
|
+
"locator": [part, index],
|
|
866
|
+
**row,
|
|
867
|
+
}
|
|
868
|
+
)
|
|
869
|
+
cursor = answer.get("next_cursor")
|
|
870
|
+
if not cursor:
|
|
871
|
+
return found, False
|
|
872
|
+
return found, True
|
|
770
873
|
|
|
771
874
|
def resolve(self, target: Any) -> dict[str, Any]:
|
|
772
875
|
"""One draft target to its matches: ``{"title", "year"}``, ``{"id"}`` or a locator."""
|
|
@@ -796,23 +899,9 @@ class _Locator:
|
|
|
796
899
|
value = target["id"]
|
|
797
900
|
if self.types.get(column) == "integer" and isinstance(value, str) and value.isdigit():
|
|
798
901
|
value = int(value)
|
|
799
|
-
|
|
800
|
-
"
|
|
801
|
-
|
|
802
|
-
"text": "",
|
|
803
|
-
"predicates": [
|
|
804
|
-
{"column": column, "operator": "eq", "value": value, "origin": "explicit"}
|
|
805
|
-
],
|
|
806
|
-
"prefer": [],
|
|
807
|
-
"order": [],
|
|
808
|
-
"scope": "matching_rows",
|
|
809
|
-
"unresolved": [],
|
|
810
|
-
}
|
|
811
|
-
rows = [
|
|
812
|
-
row
|
|
813
|
-
for row in self._rows(str(target["id"]), plan, [column])
|
|
814
|
-
if str((row.get("fields") or {}).get(column)) == str(value)
|
|
815
|
-
]
|
|
902
|
+
matches, more = self._rows(
|
|
903
|
+
[{"column": column, "operator": "eq", "value": value}], [column]
|
|
904
|
+
)
|
|
816
905
|
else:
|
|
817
906
|
column = _required(self.title_column, "--title-column (no title field role)")
|
|
818
907
|
title = str(target["title"])
|
|
@@ -821,37 +910,18 @@ class _Locator:
|
|
|
821
910
|
if year is not None:
|
|
822
911
|
date = _required(self.date_column, "--date-column (no date field role)")
|
|
823
912
|
columns.append(date)
|
|
824
|
-
|
|
825
|
-
"
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
"predicates": [],
|
|
829
|
-
"prefer": [],
|
|
830
|
-
"order": [],
|
|
831
|
-
"scope": "relevance_window",
|
|
832
|
-
"unresolved": [],
|
|
833
|
-
}
|
|
834
|
-
rows = [
|
|
835
|
-
row
|
|
836
|
-
for row in self._rows(title, plan, list(dict.fromkeys(columns)))
|
|
837
|
-
if _norm((row.get("fields") or {}).get(column)) == _norm(title)
|
|
838
|
-
]
|
|
913
|
+
matches, more = self._rows(
|
|
914
|
+
[{"column": column, "operator": "eq", "value": title}],
|
|
915
|
+
list(dict.fromkeys(columns)),
|
|
916
|
+
)
|
|
839
917
|
if year is not None:
|
|
840
|
-
|
|
841
|
-
row
|
|
842
|
-
for row in rows
|
|
843
|
-
if str((row.get("fields") or {}).get(date) or "").startswith(str(year))
|
|
918
|
+
matches = [
|
|
919
|
+
row for row in matches if str(row.get(date) or "").startswith(str(year))
|
|
844
920
|
]
|
|
845
|
-
matches
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
**row["fields"],
|
|
850
|
-
}
|
|
851
|
-
for row in rows
|
|
852
|
-
if type(row.get("row_id")) is int and isinstance(row.get("fields"), dict)
|
|
853
|
-
]
|
|
854
|
-
status = "located" if len(matches) == 1 else "not_found" if not matches else "ambiguous"
|
|
921
|
+
if len(matches) == 1 and not more:
|
|
922
|
+
status = "located"
|
|
923
|
+
else:
|
|
924
|
+
status = "not_found" if not matches and not more else "ambiguous"
|
|
855
925
|
return {
|
|
856
926
|
"target": target,
|
|
857
927
|
"status": status,
|
|
@@ -1057,9 +1127,8 @@ def search(
|
|
|
1057
1127
|
if job_operation:
|
|
1058
1128
|
if (
|
|
1059
1129
|
answer.get("job_id") != job_id
|
|
1060
|
-
or answer.get("status")
|
|
1061
|
-
|
|
1062
|
-
or (verb == "status" and answer.get("schema_version") not in SEARCH_JOB_SCHEMAS)
|
|
1130
|
+
or answer.get("status") not in JOB_STATUSES
|
|
1131
|
+
or (verb == "status" and answer.get("schema_version") not in JOB_STATUS_SCHEMAS)
|
|
1063
1132
|
):
|
|
1064
1133
|
raise ThinLaneError(
|
|
1065
1134
|
"THIN_RESPONSE_INVALID", "Studio returned invalid search job coordinates"
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_stream.py
RENAMED
|
@@ -40,10 +40,11 @@ import argparse
|
|
|
40
40
|
import json
|
|
41
41
|
import time
|
|
42
42
|
from collections.abc import Callable, Mapping
|
|
43
|
+
from datetime import datetime
|
|
43
44
|
from typing import Any
|
|
44
45
|
|
|
45
46
|
from mostlyright.data_harness.thin import THIN_SCHEMA_PREFIX
|
|
46
|
-
from mostlyright.data_harness.thin.progress_rate import RateTracker
|
|
47
|
+
from mostlyright.data_harness.thin.progress_rate import RateTracker, utc_now
|
|
47
48
|
from mostlyright.data_harness.thin.session import (
|
|
48
49
|
StudioSession,
|
|
49
50
|
StudioSessionProvider,
|
|
@@ -216,6 +217,7 @@ def watch(
|
|
|
216
217
|
*,
|
|
217
218
|
client: StudioV4RunClient | None = None,
|
|
218
219
|
clock: Callable[[], float] = time.monotonic,
|
|
220
|
+
wall_clock: Callable[[], datetime] = utc_now,
|
|
219
221
|
sessions: StudioSessionProvider | None = None,
|
|
220
222
|
) -> dict[str, Any]:
|
|
221
223
|
"""``mr-data watch RUN``: the live timeline, and the record the run stopped on.
|
|
@@ -259,7 +261,7 @@ def watch(
|
|
|
259
261
|
print(render_v4_frame(body, frame_name), flush=True)
|
|
260
262
|
moved = rates.observe(body)
|
|
261
263
|
if moved is not None and announce:
|
|
262
|
-
line = rates.due_line(moved)
|
|
264
|
+
line = rates.due_line(moved, wall_clock=wall_clock)
|
|
263
265
|
if line is not None:
|
|
264
266
|
print(f" rate {line}", flush=True)
|
|
265
267
|
if body.get("event_type") in V4_TERMINAL_EVENT_TYPES:
|
|
@@ -334,6 +336,12 @@ def terminal_payload(
|
|
|
334
336
|
if measured:
|
|
335
337
|
# The last rate each collection source reached, from its own progress ticks.
|
|
336
338
|
payload["source_rates"] = measured
|
|
339
|
+
payload["source_rates_basis"] = "historical_last_observation_per_source"
|
|
340
|
+
payload["source_rate_observed_at"] = {
|
|
341
|
+
name: source.ticks[-1][0].isoformat()
|
|
342
|
+
for name, source in rates.sources.items()
|
|
343
|
+
if source.per_second() is not None
|
|
344
|
+
}
|
|
337
345
|
if event_type == "run_failed":
|
|
338
346
|
triple = failure_from(event_facts(terminal), subject="a run_failed event")
|
|
339
347
|
payload["failure"] = triple
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/boundaries.md
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/commands.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/live-run.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/one-install.md
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/patterns.md
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/promote.md
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/readers.md
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/receipts.md
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/recipe-test.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/sources.md
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/skills/mr-data-build/references/transforms.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/__init__.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/agent_protocol.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/canonical.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/key_seam.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/page_coverage.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/session_probes.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/skill_assets.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/table_manifest.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/__init__.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/acquire.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/activity.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/approvals.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/backoff.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/categories.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/commands.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/download.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/narrative.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/parity.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/probe.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/propose.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/recipe.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/recipe_lint.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/recipe_test.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/research.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/router.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/run_prefix.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/runs.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/session.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/stream.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/support.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/transport.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/user_agent.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_catalog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_datasets.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_handoff.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_public.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_query.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_reader.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_runs.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_secrets.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/v4_tables.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/thin/vocabulary.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/__init__.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/attendance.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/clarification.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/cloud_auth.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/commands/auth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/credentials.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/login.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/path_kind.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/plain_file.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/remediation.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.0 → mostlyright_data-0.30.2}/src/mostlyright/data_harness/ux/render.py
RENAMED
|
File without changes
|