Source code for haddock.gear.postprocessing

"""Tools for post-processing haddock3 runs."""

import os
import shutil
import tarfile
import time

from haddock import log
from haddock.clis.cli_analyse import ANA_FOLDER
from haddock.core.typing import Optional


def _robust_rmtree(path: str, retries: int = 3, delay: float = 0.5) -> None:
    """Delete a directory tree, tolerant of non-local filesystem quirks.

    On non-local filesystems (NFS, FUSE object-store mounts such as gcsfuse or
    s3fs, overlayfs in containers, CIFS/SMB) deleting a still-open file does not
    remove it immediately, and directory listings can be eventually inconsistent.
    As a result ``shutil.rmtree`` may fail with
    ``OSError: [Errno 39] Directory not empty`` even when nothing should remain.

    This retries the removal a few times to absorb transient inconsistency. If
    the tree still cannot be removed, a warning is logged and the leftover
    directory is left in place rather than crashing the run — the ``.tgz``
    archive has already been written, so no data is lost.

    The primary safeguard against this situation is closing any open file
    handles (e.g. the run ``log`` file) *before* calling this function; the
    retries here are a defensive fallback for whatever filesystem the run
    happens to live on.
    """
    for attempt in range(1, retries + 1):
        try:
            shutil.rmtree(path)
            return
        except OSError as err:
            if attempt == retries:
                log.warning(
                    f"Could not fully remove '{path}' after archiving "
                    f"({err}). The archive is intact; leaving the directory "
                    "in place. This is typically caused by a non-local "
                    "filesystem (NFS, gcsfuse/s3fs, overlayfs) holding a file "
                    "open or lagging on deletion."
                )
                return
            time.sleep(delay)


[docs] def archive_run(run_dir: str, delete: bool = True) -> tuple[str, Optional[str]]: """Create an archive of the haddock3 run directory and analysis. Parameters ---------- run_dir : str Path to the run directory delete : bool, optional Should the un-archived directory be deleted?, by default False Returns ------- tuple[str, Optional[str]] run_archive_fname : str Path to the run archive analysis_archive_fname : Optional[str] Path to the run analysis archive """ log.info("Creating an archive of the run") # Start by archiving the run_directory run_archive_fname = f"{run_dir}.tgz" with tarfile.open(run_archive_fname, "w:gz") as tar: tar.add(run_dir, arcname=os.path.basename(run_dir)) # Archive the analysis directory analysis_archive_fname = None if os.path.exists(f"{run_dir}/{ANA_FOLDER}"): analysis_archive_fname = f"{run_dir}_{ANA_FOLDER}.tgz" with tarfile.open(analysis_archive_fname, "w:gz") as tar: tar.add(f"{run_dir}/{ANA_FOLDER}", arcname=f"{run_dir}_{ANA_FOLDER}") if delete: _robust_rmtree(run_dir) return run_archive_fname, analysis_archive_fname