Source code for bioimageflow.dataframe_tool
"""DataFrameTool base class — main process only."""
from typing import Any
from bioimageflow_core.tool import BaseTool, IOModel
[docs]
class Passthrough(IOModel):
"""Marker base class for DataFrameTool Outputs that preserve input columns."""
pass
[docs]
class DataFrameTool(BaseTool):
"""Tool that transforms DataFrames in the main process."""
accepts_upstream: bool = True
"""Whether this tool accepts positional upstream Nodes.
Set to ``False`` on source tools (e.g. ``Files``, ``Generate``) that
produce a DataFrame from constants alone. A source tool raises
:class:`SourceToolUpstreamError` if any positional argument is passed.
"""
def __call__(
self,
*upstream_nodes: Any,
name: str | None = None,
output_templates: dict[str, str] | None = None,
**kwargs: Any,
) -> Any:
"""Create a graph node. No computation occurs."""
if not self.accepts_upstream and upstream_nodes:
from bioimageflow.node import SourceToolUpstreamError
raise SourceToolUpstreamError(
f"Tool '{type(self).__name__}' is a source tool "
f"(accepts_upstream=False) and does not accept upstream "
f"DataFrames. Got {len(upstream_nodes)} positional argument(s)."
)
try:
from bioimageflow.node import Node
except ImportError:
raise RuntimeError(
f"{type(self).__name__}.__call__() requires the bioimageflow "
f"orchestrator package."
)
return Node(
tool=self,
args=list(upstream_nodes),
kwargs=kwargs,
name=name,
output_templates=output_templates,
)
[docs]
def merge_dataframes(self, dfs: list[Any], arguments: Any) -> Any:
"""Default: inner join on index."""
if not dfs:
import pandas as pd
return pd.DataFrame()
if len(dfs) == 1:
return dfs[0].copy()
result = dfs[0]
for df in dfs[1:]:
result = result.join(df, how="inner", rsuffix="__bif_dup")
result = result[[c for c in result.columns if not c.endswith("__bif_dup")]]
return result
[docs]
def transform(self, df: Any, arguments: Any) -> Any:
"""Default: identity (passthrough)."""
return df
[docs]
@classmethod
def resolve_outputs(
cls,
inputs: dict[str, Any] | None = None,
) -> dict[str, dict[str, Any]] | None:
"""Return the output column schema for the given (possibly partial) inputs.
Each value has the per-field shape produced by
:func:`~bioimageflow.validation.serialize_output_schema`:
``{"type": str, "default": Any | None, "image_spec": dict | None}``.
Default implementation:
- ``Outputs`` declared as :class:`~bioimageflow_core.IOModel` →
returns the static schema.
- ``Outputs`` is a :class:`Passthrough` subclass → returns the marker
dict ``{"_passthrough": True, **declared_extra_fields}``.
- Otherwise → returns ``None`` (schema unknown without input information).
Tools whose output column names are derived from their inputs (e.g.
``Generate``, where ``column_name`` is a runtime parameter) override
this method. Merge tools whose output schema depends on upstream
schemas (rather than just inputs) instead override
:meth:`resolve_merge_schema`.
Must be pure (no I/O, no side effects). Returning ``None`` or a
partial dict is allowed when ``inputs`` is incomplete.
"""
from bioimageflow.validation import serialize_output_schema
outputs_cls = getattr(cls, "Outputs", None)
if outputs_cls is None:
return None
# Delegates to the canonical wire-format helper. Passthrough classes
# produce ``{"_passthrough": True, ...}`` markers identically to
# ``serialize_output_schema``.
return serialize_output_schema(cls)
[docs]
@classmethod
def resolve_merge_schema(
cls,
upstream_schemas: list[dict[str, dict[str, Any]] | None],
inputs: dict[str, Any] | None = None,
) -> dict[str, dict[str, Any]] | None:
"""Return the merged output schema given upstream schemas.
Override this on merge tools whose output columns are computed from
upstream column sets (``InnerJoin``, ``CrossJoin``, ``JoinOnColumn``,
``Concat``, ``Collect``).
Returns ``None`` when any required upstream schema is ``None`` (caller
should treat that as "schema unresolvable"). The default implementation
returns ``None`` so :meth:`Node.get_output_schema` falls back to
:meth:`resolve_outputs` for non-merge tools.
Each entry has the same shape as the values returned by
:meth:`resolve_outputs`.
"""
return None