mostlyright-data 0.30.1__tar.gz → 0.30.3__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.1 → mostlyright_data-0.30.3}/PKG-INFO +1 -1
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/SKILL.md +3 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/page_coverage.py +62 -2
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/progress_rate.py +145 -29
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_runs.py +108 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_stream.py +10 -2
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/.gitignore +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/README.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/pyproject.toml +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/scripts/hatch_build.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/agents/openai.yaml +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/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.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/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.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/agent-protocol.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/boundaries.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/commands.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/installation-parity.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/live-run.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/making-a-table-incremental.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/narrating-the-run.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/one-install.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/patterns.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/prediction-labels.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/preparing-table-search.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/promote.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/readers.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/receipts.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/recipe-test.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/reference-pages.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/required-protocol.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/source-credentials.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/sources.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/transforms.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/user-communication-contract.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/__init__.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/agent_protocol.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/canonical.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/formats.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/key_seam.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/session_probes.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/skill_assets.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/table_manifest.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/__init__.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/acquire.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/activity.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/approvals.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/backoff.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/categories.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/commands.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/download.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/fast_features.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/narrative.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/parity.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/probe.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/propose.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/recipe.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/recipe_explain.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/recipe_forecast.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/recipe_test.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/refresh_rehearsal.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/release_check.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/research.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/router.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/run_diagnose.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/run_prefix.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/runs.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/session.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/stream.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/studio_capabilities.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/support.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/table_search_contract.json +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/transport.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_public.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_query.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_search.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_search_contract.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/__init__.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/attendance.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/clarification.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/credentials.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/login.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/remediation.py +0 -0
- {mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/render.py +0 -0
|
@@ -152,6 +152,9 @@ step 6. The stage 3 reference holds its rule. Gate 4 is step 6.
|
|
|
152
152
|
quality checks.
|
|
153
153
|
7. Follow the returned run with `mr-data watch RUN_ID --json`. Its `source_rates` and
|
|
154
154
|
`mr-data status RUN_ID --json` on a live run give each collection source's rate and time left.
|
|
155
|
+
On a queued or running run, `status` also gives `progress`: the current attempt, how many of
|
|
156
|
+
its sources it fetched of how many, the source it is on with its pages, and how the attempt
|
|
157
|
+
before it ended. A handed-back run's new attempt counts from zero.
|
|
155
158
|
Quote those numbers rather than doing the arithmetic. Read `run.status`: returning an existing
|
|
156
159
|
run does not mean new work was queued. Resume the known run after an interruption, using the
|
|
157
160
|
saved cursor where available. Read [run lifecycle](references/live-run.md) for holds and
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/page_coverage.py
RENAMED
|
@@ -20,6 +20,7 @@ __all__ = [
|
|
|
20
20
|
"PAGE_BUDGET_KINDS",
|
|
21
21
|
"PAGE_COVERAGE_COUNT_FIELDS",
|
|
22
22
|
"PAGE_COVERAGE_FIELDS",
|
|
23
|
+
"SOURCE_ROWS_INCOMPLETE",
|
|
23
24
|
"page_coverage_sentence",
|
|
24
25
|
]
|
|
25
26
|
|
|
@@ -42,6 +43,9 @@ __all__ = [
|
|
|
42
43
|
#: predecessor rows, but current collection refresh is refused before acquisition, except a
|
|
43
44
|
#: per-row request collection's, which refreshes with the direct request window it inherits and
|
|
44
45
|
#: asks again for a failed row whenever a refresh covers that row's partition.
|
|
46
|
+
#: A ``source_rows`` collection also states it on a full run, as ``SOURCE_ROWS_INCOMPLETE``, when
|
|
47
|
+
#: the upstream relation it lists from is incomplete: that is not a listing anybody failed to read
|
|
48
|
+
#: but a list that may be short by what the upstream source is missing, and the sentence says so.
|
|
45
49
|
PAGE_COVERAGE_FIELDS: tuple[str, ...] = (
|
|
46
50
|
"discovered",
|
|
47
51
|
"discovery_requests",
|
|
@@ -84,6 +88,46 @@ PAGE_COVERAGE_COUNT_FIELDS: tuple[str, ...] = (
|
|
|
84
88
|
)
|
|
85
89
|
|
|
86
90
|
|
|
91
|
+
#: The discovery failure a ``source_rows`` collection states when the upstream relation it lists
|
|
92
|
+
#: from is itself incomplete (failed, pending or unlisted members upstream). Nothing failed to be
|
|
93
|
+
#: LISTED: the list is built from rows another source did not finish delivering, so it may be
|
|
94
|
+
#: short by exactly what that source is missing. Said as what it is, never as an unreadable
|
|
95
|
+
#: listing, because a reader told "the listing could not be read" goes looking for an outage.
|
|
96
|
+
SOURCE_ROWS_INCOMPLETE = "SOURCE_ROWS_INCOMPLETE"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _count(block: Mapping[str, Any], member: str) -> int | None:
|
|
100
|
+
value = block.get(member)
|
|
101
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
102
|
+
return None
|
|
103
|
+
return value
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _pages_held(block: Mapping[str, Any]) -> int | None:
|
|
107
|
+
"""How many of the collection's members hold an answer, or ``None`` when the block cannot say.
|
|
108
|
+
|
|
109
|
+
MEMBERS, NOT CALLS. ``fetched_this_run`` counts the requests this run made: a per-row request
|
|
110
|
+
collection asks once for every member that renders the same request, a batch answers up to a
|
|
111
|
+
hundred members in one call, and a page kept from an earlier capture is not fetched again. So
|
|
112
|
+
``fetched_this_run`` over ``discovered`` divides calls by members, and a run that answered
|
|
113
|
+
every member it listed reads as half done (food run 23161446: "459,156 of 1,005,223" for a
|
|
114
|
+
run with 1,004,768 members answered and 455 failed). The members that hold an answer are the
|
|
115
|
+
ones the ledger knows that are neither still owed, nor failed, nor skipped -- the same count
|
|
116
|
+
the dataset page's sources card prints as fetched.
|
|
117
|
+
|
|
118
|
+
Read with ``get``: a record written before these members existed carries only the four the
|
|
119
|
+
sentence always needed, and says what it always said.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
known = _count(block, "known")
|
|
123
|
+
pending = _count(block, "pending")
|
|
124
|
+
if known is None or pending is None:
|
|
125
|
+
return None
|
|
126
|
+
failed = _count(block, "failed") or 0
|
|
127
|
+
skipped = _count(block, "skipped") or 0
|
|
128
|
+
return max(known - pending - failed - skipped, 0)
|
|
129
|
+
|
|
130
|
+
|
|
87
131
|
def page_coverage_sentence(block: Mapping[str, Any]) -> str:
|
|
88
132
|
"""The one sentence a person reads off a page collection's coverage.
|
|
89
133
|
|
|
@@ -96,13 +140,29 @@ def page_coverage_sentence(block: Mapping[str, Any]) -> str:
|
|
|
96
140
|
failed = int(block["failed"])
|
|
97
141
|
if block["complete"]:
|
|
98
142
|
return f"all {discovered:,} pages fetched"
|
|
99
|
-
|
|
143
|
+
held = _pages_held(block)
|
|
144
|
+
if held is None:
|
|
145
|
+
parts = [f"{fetched:,} of {discovered:,} pages fetched"]
|
|
146
|
+
else:
|
|
147
|
+
# A ledger can know pages its latest listing no longer names (a refresh keeps them), so
|
|
148
|
+
# the whole is the larger of the two and the sentence never says "more of fewer".
|
|
149
|
+
whole = max(discovered, _count(block, "known") or 0)
|
|
150
|
+
lead = f"{held:,} of {whole:,} pages fetched"
|
|
151
|
+
if fetched != held:
|
|
152
|
+
lead += f" ({fetched:,} requests this run)"
|
|
153
|
+
parts = [lead]
|
|
100
154
|
# READ WITH ``get``, AND ONLY THIS MEMBER. The block a run produces always carries it, and the
|
|
101
155
|
# block Studio hands back may not: it is optional on the run record, so a record written before
|
|
102
156
|
# the member existed is still one sentence rather than a KeyError this lane swallows whole.
|
|
103
157
|
listing = block.get("discovery_failure")
|
|
104
158
|
if isinstance(listing, Mapping):
|
|
105
|
-
|
|
159
|
+
if listing.get("code") == SOURCE_ROWS_INCOMPLETE:
|
|
160
|
+
parts.append(
|
|
161
|
+
"a page list built from another source's rows may be short, because that source "
|
|
162
|
+
f"is incomplete ({SOURCE_ROWS_INCOMPLETE})"
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
parts.append(f"the page listing could not be read ({listing['code']})")
|
|
106
166
|
if failed:
|
|
107
167
|
parts.append(f"{failed:,} failed")
|
|
108
168
|
# The block does not say which kind of collection it counts, so the remedy names both. A
|
|
@@ -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__ = [
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_runs.py
RENAMED
|
@@ -2355,6 +2355,8 @@ def status(args: argparse.Namespace, *, client: StudioV4RunClient | None = None)
|
|
|
2355
2355
|
held = {member: run[member] for member in _PROJECTION_MEMBERS[1:] if member in run}
|
|
2356
2356
|
if held:
|
|
2357
2357
|
payload["projection"] = held
|
|
2358
|
+
if state in ATTEMPTING_RUN_STATUSES:
|
|
2359
|
+
payload.update(attempt_progress_members(selected, run_id))
|
|
2358
2360
|
if getattr(args, "rate", False) and state not in TERMINAL_RUN_STATUSES:
|
|
2359
2361
|
# Imported here: progress_rate reads the event stream, whose module imports this one.
|
|
2360
2362
|
from mostlyright.data_harness.thin import progress_rate
|
|
@@ -2378,6 +2380,112 @@ def status(args: argparse.Namespace, *, client: StudioV4RunClient | None = None)
|
|
|
2378
2380
|
return payload
|
|
2379
2381
|
|
|
2380
2382
|
|
|
2383
|
+
#: The states a run holds while it has, or is between, producer attempts. A run held at a spend
|
|
2384
|
+
#: or sample gate has never been claimed, so there is no attempt to read for it.
|
|
2385
|
+
ATTEMPTING_RUN_STATUSES: frozenset[str] = frozenset({"queued", "running"})
|
|
2386
|
+
|
|
2387
|
+
#: How each way an attempt can end reads in the progress sentence. The keys are
|
|
2388
|
+
#: ``run-attempts.schema.json``'s closed ``end`` vocabulary.
|
|
2389
|
+
_ATTEMPT_ENDS: dict[str, str] = {
|
|
2390
|
+
"running": "is still running",
|
|
2391
|
+
"completed": "completed",
|
|
2392
|
+
"requeued_by_worker": "was handed back by its own worker",
|
|
2393
|
+
"handed_back": "was handed back after its worker went silent",
|
|
2394
|
+
"failed": "failed",
|
|
2395
|
+
"lost": "went silent",
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
|
|
2399
|
+
def attempt_progress_members(client: StudioV4RunClient, run_id: str) -> dict[str, Any]:
|
|
2400
|
+
"""The ``status`` members for a live run's current attempt and its sources, or why not.
|
|
2401
|
+
|
|
2402
|
+
Read off the attempt ledger (``GET /v4/runs/{run_id}/attempts``), which Studio folds from
|
|
2403
|
+
each progress tick as it arrives and never from the run log. So, unlike the rate, this does
|
|
2404
|
+
not depend on replaying the log in time, and an attempt that has run for hours still says
|
|
2405
|
+
how many of its sources it fetched. Counts are the current attempt's own: a run handed back
|
|
2406
|
+
walks its sources again from the first.
|
|
2407
|
+
|
|
2408
|
+
A Studio before 4.37.0 serves no per-source member, and then the attempt and how the one
|
|
2409
|
+
before it ended are still stated. A refused read is reported, never fatal: the run record
|
|
2410
|
+
has already been read.
|
|
2411
|
+
"""
|
|
2412
|
+
|
|
2413
|
+
try:
|
|
2414
|
+
answer = client.run_attempts(run_id)
|
|
2415
|
+
except ThinLaneError as refusal:
|
|
2416
|
+
return {"progress_unavailable": f"{refusal.code}: {refusal.detail}"}
|
|
2417
|
+
rows = answer.get("attempts") if isinstance(answer, Mapping) else None
|
|
2418
|
+
rows = [
|
|
2419
|
+
row for row in rows or () if isinstance(row, Mapping) and type(row.get("generation")) is int
|
|
2420
|
+
]
|
|
2421
|
+
if not rows:
|
|
2422
|
+
return {"progress_unavailable": "Studio lists no attempt of this run yet"}
|
|
2423
|
+
rows.sort(key=lambda row: row["generation"])
|
|
2424
|
+
current = rows[-1]
|
|
2425
|
+
sources = current.get("sources") if isinstance(current.get("sources"), Mapping) else None
|
|
2426
|
+
named = sources.get("current") if sources is not None else None
|
|
2427
|
+
progress: dict[str, Any] = {
|
|
2428
|
+
"attempt": current["generation"],
|
|
2429
|
+
"attempts": len(rows),
|
|
2430
|
+
"source_count": sources.get("source_count") if sources is not None else None,
|
|
2431
|
+
"sources_fetched": sources.get("fetched") if sources is not None else None,
|
|
2432
|
+
"sources_refused": sources.get("refused") if sources is not None else None,
|
|
2433
|
+
"counted_from_first_tick": (
|
|
2434
|
+
sources.get("counted_from_first_tick") if sources is not None else None
|
|
2435
|
+
),
|
|
2436
|
+
"current_source": dict(named) if isinstance(named, Mapping) else None,
|
|
2437
|
+
"previous_attempt": None,
|
|
2438
|
+
}
|
|
2439
|
+
if len(rows) > 1:
|
|
2440
|
+
previous = rows[-2]
|
|
2441
|
+
loss = previous.get("loss") if isinstance(previous.get("loss"), Mapping) else {}
|
|
2442
|
+
progress["previous_attempt"] = {
|
|
2443
|
+
"attempt": previous["generation"],
|
|
2444
|
+
"end": previous.get("end"),
|
|
2445
|
+
"ended_at": previous.get("ended_at"),
|
|
2446
|
+
"cause": loss.get("cause"),
|
|
2447
|
+
"cause_inferred": loss.get("inferred"),
|
|
2448
|
+
}
|
|
2449
|
+
return {"progress": progress, "progress_sentence": attempt_progress_sentence(progress)}
|
|
2450
|
+
|
|
2451
|
+
|
|
2452
|
+
def attempt_progress_sentence(progress: Mapping[str, Any]) -> str:
|
|
2453
|
+
"""``Attempt 4 of 4 has fetched 27 of 86 sources and is on ...``, from the members above."""
|
|
2454
|
+
|
|
2455
|
+
said = f"Attempt {progress['attempt']} of {progress['attempts']}"
|
|
2456
|
+
fetched, count = progress.get("sources_fetched"), progress.get("source_count")
|
|
2457
|
+
if fetched is None:
|
|
2458
|
+
said += "; this Studio reports no per-source progress for it."
|
|
2459
|
+
else:
|
|
2460
|
+
said += f" has fetched {fetched:,}"
|
|
2461
|
+
said += f" of {count:,} sources" if count is not None else " sources"
|
|
2462
|
+
if progress.get("counted_from_first_tick") is False:
|
|
2463
|
+
said += " (at least: counting began after the attempt started)"
|
|
2464
|
+
named = progress.get("current_source")
|
|
2465
|
+
if isinstance(named, Mapping):
|
|
2466
|
+
said += f" and is on {named.get('source_name')}"
|
|
2467
|
+
if named.get("source_index") is not None and count is not None:
|
|
2468
|
+
said += f", source {named['source_index']:,} of {count:,}"
|
|
2469
|
+
done, planned = named.get("pages_processed"), named.get("pages_planned")
|
|
2470
|
+
unit = "members" if named.get("pages_unit") == "member" else "pages"
|
|
2471
|
+
if done is not None and planned is not None and named.get("discovery_complete"):
|
|
2472
|
+
said += f": {done:,} of {planned:,} {unit}"
|
|
2473
|
+
elif done is not None:
|
|
2474
|
+
said += f": {done:,} {unit} so far"
|
|
2475
|
+
said += "."
|
|
2476
|
+
previous = progress.get("previous_attempt")
|
|
2477
|
+
if isinstance(previous, Mapping):
|
|
2478
|
+
end = _ATTEMPT_ENDS.get(str(previous.get("end")), f"ended {previous.get('end')}")
|
|
2479
|
+
said += f" Attempt {previous['attempt']} {end}"
|
|
2480
|
+
if previous.get("cause") is not None:
|
|
2481
|
+
said += (
|
|
2482
|
+
f"; {'inferred ' if previous.get('cause_inferred') else ''}cause: "
|
|
2483
|
+
f"{previous['cause']}"
|
|
2484
|
+
)
|
|
2485
|
+
said += "."
|
|
2486
|
+
return said
|
|
2487
|
+
|
|
2488
|
+
|
|
2381
2489
|
def status_exit_code(payload: Mapping[str, Any]) -> int:
|
|
2382
2490
|
"""A failed run exits non-zero, so a script gating on a build cannot read it as a success."""
|
|
2383
2491
|
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/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
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/boundaries.md
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/commands.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/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.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/one-install.md
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/patterns.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/promote.md
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/readers.md
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/receipts.md
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/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.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/sources.md
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/skills/mr-data-build/references/transforms.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/__init__.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/agent_protocol.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/canonical.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/key_seam.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/session_probes.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/skill_assets.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/table_manifest.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/__init__.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/acquire.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/activity.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/approvals.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/backoff.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/categories.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/commands.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/download.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/narrative.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/parity.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/probe.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/propose.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/recipe.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/recipe_lint.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/recipe_test.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/research.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/router.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/run_prefix.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/runs.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/session.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/stream.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/support.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/transport.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/user_agent.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/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.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_datasets.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_handoff.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_public.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_query.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_reader.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_search.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_secrets.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/v4_tables.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/thin/vocabulary.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/__init__.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/attendance.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/clarification.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/cloud_auth.py
RENAMED
|
File without changes
|
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/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.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/credentials.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/login.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/path_kind.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/plain_file.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/remediation.py
RENAMED
|
File without changes
|
{mostlyright_data-0.30.1 → mostlyright_data-0.30.3}/src/mostlyright/data_harness/ux/render.py
RENAMED
|
File without changes
|