Skip to content

project_init

tit.project_init

Project initialization helpers for TI-Toolbox.

Provides utilities for detecting new projects, scaffolding BIDS-compliant directory structures, and reading/writing project_status.json. The example data is :func:tit.examples.fetch.

initialize_project_structure

initialize_project_structure(project_dir: Path) -> None

Scaffold a full BIDS-compliant directory structure for project_dir.

Idempotent, and honest about it: the "New project detected" banner and the per-item ✓ … created lines are printed only for work this call actually did. An established project -- one :func:has_project_data_or_markers recognises -- gets a single Project structure verified line, because that is all that happened. (Before 2026-09-15 the banner and every ✓ printed unconditionally, so asking an established project for anything that re-ran this looked like it was being re-initialised.)

Parameters

project_dir : Path Root directory of the project.

Source code in tit/project_init/initializer.py
def initialize_project_structure(project_dir: Path) -> None:
    """Scaffold a full BIDS-compliant directory structure for *project_dir*.

    Idempotent, and **honest about it**: the "New project detected" banner and the per-item
    ``✓ … created`` lines are printed only for work this call actually did. An established
    project -- one :func:`has_project_data_or_markers` recognises -- gets a single
    ``Project structure verified`` line, because that is all that happened. (Before
    2026-09-15 the banner and every ✓ printed unconditionally, so asking an established
    project for anything that re-ran this looked like it was being re-initialised.)

    Parameters
    ----------
    project_dir : Path
        Root directory of the project.
    """
    # Decided *before* anything is created, or the first mkdir would answer the question.
    is_new = not has_project_data_or_markers(project_dir)
    created: list[str] = []

    def note(label: str, did: bool) -> None:
        if did:
            created.append(label)

    if is_new:
        print("")
        print("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
        print(f"  New project detected: {project_dir.name}")
        print("  Initializing BIDS-compliant structure...")
        print("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
        print("")

    for rel in (
        Path("code") / "ti-toolbox" / "config",
        Path("derivatives") / "freesurfer",
        Path("derivatives") / "SimNIBS",
        Path("sourcedata"),
    ):
        target = project_dir / rel
        existed = target.is_dir()
        target.mkdir(parents=True, exist_ok=True)
        note(f"{rel}/", not existed)

    note("README", initialize_readme(project_dir))
    note("dataset_description.json", initialize_dataset_description(project_dir))
    for derivative in ("ti-toolbox", "freesurfer", "SimNIBS"):
        note(
            f"derivatives/{derivative}/dataset_description.json",
            initialize_derivative_dataset_description(project_dir, derivative),
        )
    note("project_status.json", initialize_project_status(project_dir))

    marker = project_dir / "code" / "ti-toolbox" / "config" / ".initialized"
    existed = marker.exists()
    marker.touch()
    note("initialization marker", not existed)

    if not is_new and not created:
        print(f"Project structure verified: {project_dir}", flush=True)
        return

    for label in created:
        print(f"  ✓ {label} created")
    if not created:
        print("  (everything was already in place)")

    if is_new:
        print("")
        print("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
        print("  ✓ Project initialization complete!")
        print("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
        print("")
    else:
        print(f"Project structure verified: {project_dir}", flush=True)

is_new_project

is_new_project(project_dir: Path) -> bool

Return True if project_dir exists and contains no project data.

Parameters

project_dir : Path Root directory of the project.

Returns

bool True when the directory is empty of project data and markers.

Source code in tit/project_init/initializer.py
def is_new_project(project_dir: Path) -> bool:
    """Return ``True`` if *project_dir* exists and contains no project data.

    Parameters
    ----------
    project_dir : Path
        Root directory of the project.

    Returns
    -------
    bool
        ``True`` when the directory is empty of project data and markers.
    """
    return (
        project_dir.exists()
        and project_dir.is_dir()
        and not has_project_data_or_markers(project_dir)
    )

load_project_status

load_project_status(project_dir: Path) -> dict[str, Any]

Read project_status.json and return its contents.

Returns an empty dict when the file is missing or unreadable. This function never writes to disk.

Parameters

project_dir : Path Root directory of the project.

Source code in tit/project_init/initializer.py
def load_project_status(project_dir: Path) -> dict[str, Any]:
    """Read ``project_status.json`` and return its contents.

    Returns an **empty dict** when the file is missing or unreadable.
    This function never writes to disk.

    Parameters
    ----------
    project_dir : Path
        Root directory of the project.
    """
    status_file = _status_file_path(project_dir)
    if not status_file.exists():
        return {}
    try:
        return json.loads(status_file.read_text())
    except Exception as exc:
        logger.warning("Could not read %s: %s", status_file, exc)
        return {}

update_project_status

update_project_status(project_dir: Path, updates: dict[str, Any]) -> bool

Merge updates into project_status.json and write back.

Performs a recursive (deep) merge so that nested keys such as user_preferences.show_welcome can be updated without clobbering sibling keys. Automatically sets last_updated.

If the file does not yet exist a warning is logged and the function returns False — the file should have been created by :func:initialize_project_status.

Parameters

project_dir : Path Root directory of the project. updates : dict Fields to merge into the existing status.

Returns

bool True on success.

Source code in tit/project_init/initializer.py
def update_project_status(project_dir: Path, updates: dict[str, Any]) -> bool:
    """Merge *updates* into ``project_status.json`` and write back.

    Performs a recursive (deep) merge so that nested keys such as
    ``user_preferences.show_welcome`` can be updated without clobbering
    sibling keys.  Automatically sets ``last_updated``.

    If the file does not yet exist a warning is logged and the function
    returns ``False`` — the file should have been created by
    :func:`initialize_project_status`.

    Parameters
    ----------
    project_dir : Path
        Root directory of the project.
    updates : dict
        Fields to merge into the existing status.

    Returns
    -------
    bool
        ``True`` on success.
    """
    status_file = _status_file_path(project_dir)
    current = load_project_status(project_dir)
    if not current:
        logger.warning(
            "project_status.json does not exist at %s — skipping update",
            status_file,
        )
        return False

    _deep_merge(current, updates)
    current["last_updated"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")

    try:
        status_file.write_text(json.dumps(current, indent=2))
        return True
    except Exception as exc:
        logger.error("Failed to write %s: %s", status_file, exc)
        return False