"""The ``status`` command: a read-only view of the run's durable state (5.1e).
Opens the state database under ``run.work_dir`` (WAL mode -- safe to read
while a ``run`` is live in another terminal) and reports the per-state
counts, every terminally-failed material with the stage and reason it died
on, and the in-flight job ledger. It changes nothing and never claims.
``--state <state>`` switches to the picking-list view: the material ids
currently sitting at that state (with their parents), i.e. exactly what a
user browses to build a ``run --materials`` selection.
Exit codes: 0 = state shown (failed materials are *content*, not a command
failure), 1 = no run state exists yet, 2 = config error / unknown state name.
"""
from __future__ import annotations
from pathlib import Path
from matmelt.commands import STATE_DB_FILENAME, resolve_config_path
from matmelt.config import ConfigError, load_config
from matmelt.core.state import MaterialState, StateStore
from matmelt.ui.console import (
print_json_payload,
render_config_error,
render_error,
render_state_materials,
render_status,
)
[docs]
def run_status(config_path: str, *, as_json: bool = False, state: str | None = None) -> int:
"""Show what the run's state database currently records.
Parameters
----------
config_path
The config file naming the workspace to read.
as_json
Emit a JSON payload on stdout instead of Rich tables.
state
List the material ids sitting at one lifecycle state, instead of the
tally. Case-insensitive. This is the picking list for
``run --materials`` and ``retry``.
Returns
-------
int
``0`` on success, ``1`` when there is no run state yet, ``2`` for a
config error or an unknown state name.
Notes
-----
Read-only: the database is byte-identical afterwards, and WAL mode makes
this safe to run while a job is in flight.
"""
config_path = resolve_config_path(config_path)
try:
config = load_config(config_path)
except ConfigError as exc:
render_config_error(str(exc))
return 2
wanted: MaterialState | None = None
if state is not None:
try:
wanted = MaterialState(state.strip().upper())
except ValueError:
valid = ", ".join(member.value for member in MaterialState)
render_error(f"status: unknown state {state!r}; valid states: {valid}")
return 2
db_path = Path(config.run.work_dir) / STATE_DB_FILENAME
if not db_path.is_file():
render_error(f"status: no run state at {db_path} -- start one with 'matmelt run'.")
return 1
if wanted is not None:
return _list_state(config.run.name, db_path, wanted, as_json=as_json)
store = StateStore(db_path)
try:
counts = store.count_by_state()
failed = store.query({"state": MaterialState.FAILED})
jobs = store.list_jobs()
finally:
store.close()
if as_json:
print_json_payload(
{
"run": config.run.name,
"counts": {state.value: count for state, count in counts.items()},
"failed": [
{
"id": record.material_id,
"stage": record.fail_stage,
"reason": record.fail_reason,
"attempts": record.attempts,
}
for record in failed
],
"in_flight": [
{
"job_key": job.job_key,
"stage": job.stage,
"backend": job.backend_name,
"materials": list(job.material_ids),
}
for job in jobs
],
}
)
else:
render_status(config.run.name, counts, failed, jobs)
return 0
def _list_state(run_name: str, db_path: Path, state: MaterialState, *, as_json: bool) -> int:
"""List the material ids at one state -- the ``--state`` leg.
Parameters
----------
run_name
Shown in the header.
db_path
The state database.
state
Which state to list.
as_json
Emit a JSON payload instead of a table.
Returns
-------
int
``0``. An empty state is a normal answer, not a failure.
"""
store = StateStore(db_path)
try:
records = store.query({"state": state})
finally:
store.close()
if as_json:
print_json_payload(
{
"run": run_name,
"state": state.value,
"materials": [
{"id": record.material_id, "parent_id": record.parent_id} for record in records
],
}
)
else:
render_state_materials(run_name, state, records)
return 0