Source code for bioimageflow_core.arguments
"""Arguments namespace, execution context, and index lineage helpers."""
from dataclasses import dataclass
from difflib import get_close_matches as _get_close_matches
from pathlib import Path
from typing import Any, Optional
[docs]
@dataclass(frozen=True)
class ExecutionContext:
"""Per-execution filesystem context for ProcessingTool runtime scratch."""
run_dir: Path
assets_dir: Path
work_dir: Path
rows_dir: Path
row_dir: Optional[Path] = None
batch_dir: Optional[Path] = None
row_index: Optional[str] = None
def __post_init__(self) -> None:
expected_work_dir = self.run_dir / "work"
if self.work_dir != expected_work_dir:
raise ValueError(
f"ExecutionContext.work_dir must be {expected_work_dir}, "
f"got {self.work_dir}."
)
expected_rows_dir = self.work_dir / "rows"
if self.rows_dir != expected_rows_dir:
raise ValueError(
f"ExecutionContext.rows_dir must be {expected_rows_dir}, "
f"got {self.rows_dir}."
)
if (self.row_dir is None) == (self.batch_dir is None):
raise ValueError("ExecutionContext requires exactly one of row_dir or batch_dir.")
if self.row_dir is not None:
if self.row_dir.parent != self.rows_dir:
raise ValueError(
f"ExecutionContext.row_dir must be a child of {self.rows_dir}, "
f"got {self.row_dir}."
)
if self.row_index is None:
raise ValueError("ExecutionContext.row_index is required when row_dir is set.")
if self.batch_dir is not None:
expected_batch_dir = self.work_dir / "batch"
if self.batch_dir != expected_batch_dir:
raise ValueError(
f"ExecutionContext.batch_dir must be {expected_batch_dir}, "
f"got {self.batch_dir}."
)
if self.row_index is not None:
raise ValueError("ExecutionContext.row_index must be None when batch_dir is set.")
[docs]
def to_dict(self) -> dict[str, Optional[str]]:
"""Return a picklable representation for worker dispatch."""
return {
"run_dir": str(self.run_dir),
"assets_dir": str(self.assets_dir),
"work_dir": str(self.work_dir),
"rows_dir": str(self.rows_dir),
"row_dir": str(self.row_dir) if self.row_dir is not None else None,
"batch_dir": str(self.batch_dir) if self.batch_dir is not None else None,
"row_index": self.row_index,
}
[docs]
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "ExecutionContext":
"""Reconstruct an execution context from a worker payload."""
return cls(
run_dir=Path(data["run_dir"]),
assets_dir=Path(data["assets_dir"]),
work_dir=Path(data["work_dir"]),
rows_dir=Path(data["rows_dir"]),
row_dir=Path(data["row_dir"]) if data["row_dir"] is not None else None,
batch_dir=Path(data["batch_dir"]) if data["batch_dir"] is not None else None,
row_index=data.get("row_index"),
)
[docs]
class Arguments:
"""
Lightweight namespace for passing resolved values to tool methods.
Supports attribute access with helpful error messages on typos.
"""
[docs]
def __init__(self, **kwargs: Any) -> None:
self.__dict__.update(kwargs)
def __getattr__(self, name: str) -> Any:
if name.startswith('_'):
raise AttributeError(name)
available = [k for k in self.__dict__ if not k.startswith('_')]
close = _get_close_matches(name, available, n=3, cutoff=0.6)
msg = f"Arguments has no field '{name}'."
if close:
msg += f" Did you mean: {', '.join(close)}?"
else:
msg += f" Available fields: {', '.join(sorted(available))}"
raise AttributeError(msg)
[docs]
def parse_index_lineage(index: str) -> list[str]:
"""Split an exploded index into its lineage components."""
return index.split("::")
[docs]
def parent_index(index: str) -> str:
"""Return the parent index (strip last explosion level)."""
parts = index.split("::")
return "::".join(parts[:-1]) if len(parts) > 1 else index