headroomlabs-ai/headroom · error · FileNotFoundError

Spreadsheet not found: {p}

Error message

Spreadsheet not found: {p}

What it means

The spreadsheet ingest entry point checks `Path(path).exists()` before dispatching on suffix and raises FileNotFoundError with the exact offending path when the file is missing. This is a straightforward precondition failure: the caller referenced a spreadsheet that does not exist at that location (or is not readable at that path, e.g. inside a container).

Source

Thrown at headroom/transforms/spreadsheet_ingest.py:89


def load_spreadsheet(path: str | Path) -> dict[str, str]:
    """Load a spreadsheet file into ``{sheet_name: csv_text}``.

    Args:
        path: Path to a ``.xlsx`` or ``.xls`` file.

    Returns:
        Mapping of sheet name to CSV-rendered text (empty sheets omitted).

    Raises:
        FileNotFoundError: If the path does not exist.
        ValueError: If the file extension is unsupported.
        ImportError: If the required parser dependency is not installed.
    """
    p = Path(path)
    if not p.exists():
        raise FileNotFoundError(f"Spreadsheet not found: {p}")

    suffix = p.suffix.lower()
    if suffix == ".xlsx":
        return _load_xlsx(p)
    if suffix == ".xls":
        return _load_xls(p)  # pragma: no cover - legacy .xls path, see _load_xls
    raise ValueError(f"Unsupported spreadsheet format '{suffix}'. Supported: .xlsx, .xls")

View on GitHub (pinned to 322425c43b)

Solutions

  1. Check the path exists before calling (see validation code) and log/resolve it early with `Path.resolve()`
  2. Use absolute paths built from a configured base directory instead of cwd-relative strings
  3. If the file may legitimately disappear (async/queued ingestion), treat FileNotFoundError as a retryable condition in the caller

Example fix

# before
path = Path("uploads/q3.xlsx")
sheets = ingest_spreadsheet(path)

# after
path = (UPLOAD_DIR / "q3.xlsx").resolve()
if not path.exists():
    raise FileNotFoundError(f"upload vanished: {path}")
sheets = ingest_spreadsheet(path)
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def existing_spreadsheet(path: str | Path) -> Path:
    p = Path(path).resolve()
    if not p.is_file():
        raise FileNotFoundError(f"no spreadsheet at {p}")
    return p

sheets = ingest_spreadsheet(existing_spreadsheet(user_path))

Type guard

def is_existing_file(path: str | Path) -> bool:
    return Path(path).is_file()

Try / catch

try:
    sheets = ingest_spreadsheet(path)
except FileNotFoundError:
    log.warning("spreadsheet vanished before ingest: %s", path)
    mark_job_retryable(job_id)  # queued-upload race

Prevention

When it happens

Trigger: Calling `ingest_spreadsheet(Path("data/report.xlsx"))` when the file was never created, was moved/renamed, or when the process's working directory differs from where the caller assumes the relative path resolves.

Common situations: Relative paths in scripts run from a different cwd (cron, CI, Docker WORKDIR); user-uploaded file already cleaned up by the time a background worker processes it; typos or stale paths in config; file present locally but absent in the container image.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/03e1b4cb9014c6f2. Report an issue: GitHub.