Source code for matmelt.commands.results
"""The ``results`` command: list finished materials; export their records (5.1e).
``DONE`` materials come from the state database; each one's extracted
``record.json`` (the OPTIMADE-shaped output of the extract stage) is read
from its item directory under ``run.work_dir``. A funnel *configured*
without an extract stage legitimately has DONE materials without records --
they are listed and counted, never an error. (A mid-funnel ``stop_after``
run produces no DONE materials at all: they rest at the stop stage's
output, resumable.) A record that exists but cannot be parsed is
reported loudly and skipped, and the export stays honest about how many
records it carries.
``--export PATH`` writes the records as one JSON array (each element
``{"id": ..., "record": {...}}`` -- the database handoff), atomically.
``--json`` prints the same content plus the bookkeeping to stdout.
Exit codes: 0 = listed/exported, 1 = no run state exists yet, 2 = config
error.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from matmelt.commands import STATE_DB_FILENAME, resolve_config_path
from matmelt.config import ConfigError, load_config
from matmelt.core.stages.extract import RECORD_NAME
from matmelt.core.state import MaterialState, StateStore
from matmelt.ui.console import (
print_json_payload,
render_config_error,
render_error,
render_note,
render_results,
)
[docs]
def run_results(
config_path: str,
*,
as_json: bool = False,
export: str | None = None,
) -> int:
"""List the run's finished materials, and optionally export their records.
Parameters
----------
config_path
The config file naming the workspace to read.
as_json
Emit a JSON payload on stdout instead of a Rich table.
export
Write the collected records to this path as a JSON array, atomically.
Returns
-------
int
``0`` on success, ``1`` when there is no run state or the export could
not be written, ``2`` for a config error.
Notes
-----
A ``DONE`` material with no ``record.json`` is first-class here, not an
error: a run stopped before ``extract`` finishes materials that never
produced a record. Records that exist but cannot be read are skipped
**loudly**.
Read-only: the state database is byte-identical afterwards.
"""
config_path = resolve_config_path(config_path)
try:
config = load_config(config_path)
except ConfigError as exc:
render_config_error(str(exc))
return 2
work_dir = Path(config.run.work_dir)
db_path = work_dir / STATE_DB_FILENAME
if not db_path.is_file():
render_error(f"results: no run state at {db_path} -- start one with 'matmelt run'.")
return 1
store = StateStore(db_path)
try:
done = store.query({"state": MaterialState.DONE})
finally:
store.close()
records: list[dict[str, Any]] = []
without_record: list[str] = []
for material in done:
record_path = work_dir / material.material_id / RECORD_NAME
if not record_path.is_file():
without_record.append(material.material_id)
continue
try:
payload = json.loads(record_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
render_error(f"results: skipping unreadable {record_path}: {exc}")
without_record.append(material.material_id)
continue
records.append({"id": material.material_id, "record": payload})
if as_json:
print_json_payload(
{
"run": config.run.name,
"n_done": len(done),
"records": records,
"without_record": without_record,
}
)
else:
render_results(config.run.name, done, {entry["id"] for entry in records})
if export is not None:
export_path = Path(export)
try:
export_path.parent.mkdir(parents=True, exist_ok=True)
tmp_path = export_path.with_name(export_path.name + ".tmp")
tmp_path.write_text(json.dumps(records, indent=2) + "\n", encoding="utf-8")
tmp_path.replace(export_path)
except OSError as exc:
render_error(f"results: cannot write {export_path}: {exc}")
return 1
render_note(f"Exported {len(records)} record(s) to {export_path}.")
return 0