Skip to content

job_outputs

tit.server.job_outputs

The one folder a job's results live in (JobDetail.output_dir).

The desktop's folder buttons (the run pages' Terminal pane, the Jobs page) open this folder. It used to be guessed from the job's registered artifacts, which a runner reports only when it finishes: while a job ran, the button fell back to code/ti-toolbox/jobs/<id>/ (the job's own bookkeeping record), and a finished simulation or search, whose first artifact is the run folder itself, opened that folder's parent.

The answer, in order:

  1. the folder the runner itself registered, as a dir artifact (a simulation registers its Simulations/<montage> folder, a flex-search its run folder);
  2. the folder POST /api/plan/{kind} names for the job's own spec (:func:tit.server.routes.plan.plan_jobs, the resolver the run pages' plan uses), so a job that is still running or queued is answered too. A flex, ex or mEx search whose run folder the config leaves to the runner (a timestamp the plan can only preview) has no planned answer;
  3. the directory of the first file the runner registered (a search's results.csv).

None when there is no single folder: a job that registered nothing and plans nothing (a pre-processing group), a simulation of several montages, a job whose folder does not exist yet or is outside the project. The caller then falls back to the job's own folder.

job_output_dir

job_output_dir(kind: str, config: dict[str, Any], subject_ids: list[str], artifacts: list[dict[str, Any]]) -> str | None

The folder holding the job's results, or None (see the module docstring).

Only a folder that exists inside the project is returned, so a button built on it never opens a path outside the project or one a run has not created yet.

Source code in tit/server/job_outputs.py
def job_output_dir(
    kind: str,
    config: dict[str, Any],
    subject_ids: list[str],
    artifacts: list[dict[str, Any]],
) -> str | None:
    """The folder holding the job's results, or ``None`` (see the module docstring).

    Only a folder that exists inside the project is returned, so a button built on it never
    opens a path outside the project or one a run has not created yet.
    """
    claimed, folder = _registered_folder(artifacts)
    if not claimed:
        folder = _planned_folder(kind, config, subject_ids)
    if not claimed and folder is None:
        first = next((a["path"] for a in artifacts if a.get("path")), None)
        folder = os.path.dirname(first) if first else None
    if not folder:
        return None
    root = get_path_manager().project_dir
    if root and not is_within(root, folder):
        return None
    return folder if os.path.isdir(folder) else None