Source code for tlc_plugin_sdk.shared.generic_job
# Copyright 2026 3LC Inc.
# SPDX-License-Identifier: Apache-2.0
"""Shared helper for translating training progress into the generic progress schema.
Used by training plugins inside ``run_job``: they compute an
epoch/batch progress dict and call :func:`epoch_progress` to render the generic
``{percent, label, timing}`` shape the frontend understands, which they then push
through their own ``ctx.emit`` channel. (The host owns job *listing* and *cancel*;
this module only shapes the progress payload.)
"""
from __future__ import annotations
from typing import Any
[docs]
def epoch_progress(
progress: dict[str, Any],
*,
phase_key: str = "phase",
epoch_key: str = "epoch",
total_key: str = "total_epochs",
batch_frac_key: str = "batch_frac",
step_label: str = "epoch",
) -> dict[str, Any] | None:
"""Build progress dict from epoch/batch-based training progress.
Common to epoch-based training plugins.
"""
if not progress:
return None
epoch = progress.get(epoch_key, 0)
total = progress.get(total_key, 0)
if total <= 0:
return None
batch_frac = progress.get(batch_frac_key, 0)
percent = int(((epoch - 1 + batch_frac) / total) * 100) if total > 0 else 0
percent = max(0, min(100, percent))
phase = progress.get(phase_key, "")
label = f"Epoch {epoch}/{total}" if total > 0 else ""
if phase:
label = f"{label} · {phase}" if label else phase
result: dict[str, Any] = {"percent": percent, "label": label}
timing = progress.get("timing")
if timing:
result["timing"] = {
"elapsed_s": timing.get("elapsed_s"),
"eta_s": timing.get("eta_s"),
"avg_step_s": timing.get("avg_epoch_s", timing.get("avg_step_s")),
"step_label": step_label,
}
return result