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