Skip to content

dqm_ml_job.outputwriter

Output writers module for DQM ML Job.

This module contains classes for writing pipeline results (features and metrics) to various storage backends.

Classes:

Name Description
OutputWriter

Protocol for output writer implementations.

ParquetOutputWriter

Writer that saves data to Parquet files.

__all__ = ['OutputWriter', 'ParquetOutputWriter', 'dqml_outputs_registry'] module-attribute

dqml_outputs_registry = {'parquet': ParquetOutputWriter} module-attribute

OutputWriter

Bases: Protocol

Protocol for Output Writers.

Defines the interface for writing pipeline results (features or metrics) to storage.

Source code in packages/dqm-ml-job/src/dqm_ml_job/outputwriter/__init__.py
@runtime_checkable
class OutputWriter(Protocol):
    """
    Protocol for Output Writers.

    Defines the interface for writing pipeline results (features or metrics) to storage.
    """

    columns: list[str]
    name: str

    def write_metrics_dict(self, metrics_dict: dict[str, dict[str, Any]]) -> None:
        """Persist computed metrics for all selections.

        Args:
            metrics_dict: Map of selection names to their metric dictionaries.
        """

    def write_table(self, name: str, table: Any, part_index: int | None = None) -> None:
        """
        Write a table (features or metrics) to the output.

        Args:
            name: Name of the dataset or metric.
            table: The data to write (usually a pyarrow Table or dict of arrays).
            part_index: Index of the data part (for chunked writing).
        """

columns: list[str] instance-attribute

name: str instance-attribute

write_metrics_dict(metrics_dict: dict[str, dict[str, Any]]) -> None

Persist computed metrics for all selections.

Parameters:

Name Type Description Default
metrics_dict dict[str, dict[str, Any]]

Map of selection names to their metric dictionaries.

required
Source code in packages/dqm-ml-job/src/dqm_ml_job/outputwriter/__init__.py
def write_metrics_dict(self, metrics_dict: dict[str, dict[str, Any]]) -> None:
    """Persist computed metrics for all selections.

    Args:
        metrics_dict: Map of selection names to their metric dictionaries.
    """

write_table(name: str, table: Any, part_index: int | None = None) -> None

Write a table (features or metrics) to the output.

Parameters:

Name Type Description Default
name str

Name of the dataset or metric.

required
table Any

The data to write (usually a pyarrow Table or dict of arrays).

required
part_index int | None

Index of the data part (for chunked writing).

None
Source code in packages/dqm-ml-job/src/dqm_ml_job/outputwriter/__init__.py
def write_table(self, name: str, table: Any, part_index: int | None = None) -> None:
    """
    Write a table (features or metrics) to the output.

    Args:
        name: Name of the dataset or metric.
        table: The data to write (usually a pyarrow Table or dict of arrays).
        part_index: Index of the data part (for chunked writing).
    """

ParquetOutputWriter

Output writer that saves processed features to a Parquet file.

Source code in packages/dqm-ml-job/src/dqm_ml_job/outputwriter/parquet.py
class ParquetOutputWriter:
    """
    Output writer that saves processed features to a Parquet file.
    """

    def __init__(self, name: str, config: dict[str, Any] | None = None):
        """
        Initialize a ParquetOutputWriter.

        Args:
            name: Unique name for this output writer.
            config: Configuration dictionary with keys:
                - path_pattern (str): Output file path format string.
                - columns (List[str]): Columns to save.
                - storage (bool or dict, optional): Storage configuration.
                  If dict with type "s3", can contain access_key, secret_key, and endpoint_override.

        Raises:
            ValueError: If required config keys are missing.
        """
        cfg = ParquetOutputConfig.model_validate(config or {})

        self.path_pattern = cfg.path_pattern
        self.columns = list(cfg.columns)
        self.exclude = list(cfg.exclude)
        self.name = name
        self.s3_filesystem = None

        self._accumulate = "{}" not in self.path_pattern
        self._accumulated_features: dict[str, list[pa.Array]] = {}
        storage_cfg = cfg.storage
        if storage_cfg:
            storage_config = StorageConfig.model_validate(storage_cfg)
            if storage_config.type == "s3":
                self.s3_filesystem = get_s3_filesystem(storage_config)

    @staticmethod
    def _collect_metric(metric_name: str, metrics_dict: dict[str, dict[str, Any]], keys: list[str]) -> pa.Array | None:
        values = []
        for key in keys:
            val = metrics_dict[key][metric_name]
            if isinstance(val, pa.FixedSizeListArray):
                return None
            if isinstance(val, pa.Array):
                values.extend(val.to_pylist())
            else:
                values.append(val)
        return pa.array(values)

    def write_metrics_dict(self, metrics_dict: dict[str, dict[str, Any]]) -> None:
        """Aggregate and write dataset-level metrics for all selections.

        Args:
            metrics_dict: Map of selection names to their computed
                metric dictionaries.
        """
        if len(metrics_dict) <= 0:
            return
        logger.debug(f"Writing metrics for the {len(metrics_dict)} data selections")
        keys = list(metrics_dict.keys())
        metric_names = list(metrics_dict[keys[0]].keys())
        metrics_table = {"selection": pa.array(keys)}
        for metric_name in metric_names:
            if metric_name.startswith("__") and metric_name.endswith("__"):
                continue
            col = self._collect_metric(metric_name, metrics_dict, keys)
            if col is not None:
                metrics_table[metric_name] = col
        self.write_table("", metrics_table)

    def _format_filename(self, path_pattern: str, part: int | None = None) -> str:
        """Format the output filename from the path pattern and optional part."""
        if part is None:
            return self.path_pattern.format(path_pattern, "")
        return self.path_pattern.format(path_pattern, part)

    def _write_local(self, table: pa.Table, filename: str) -> None:
        """Write a table to a local file, creating the parent directory if needed."""
        output_dir = Path(filename).parent
        if not Path.exists(output_dir):
            logger.info(f"Creating output directory: {output_dir}")
            Path.mkdir(output_dir, parents=True, exist_ok=True)
        pq.write_table(table, filename)
        logger.info(f"Wrote output table to {filename}")

    def write_table(
        self,
        path_pattern: str,
        features_array: dict[str, Any],
        part: int | None = None,
    ) -> None:
        """Write a table of features or metrics to a Parquet file.

        Handles directory creation if the target path doesn't exist
        (for local writes).

        Args:
            path_pattern: Identifier for the data destination.
            features_array: Map of column names to pyarrow Arrays.
            part: Optional partition index for chunked output.
        """

        for key in self.columns:
            if key not in features_array:
                logger.error(f"Missing {key} in features for output")

        # Accumulate mode: buffer features for a single flush at the end
        if self._accumulate:
            for k, v in features_array.items():
                if isinstance(v, pa.ChunkedArray):
                    v = v.combine_chunks()
                self._accumulated_features.setdefault(k, []).append(v)
            return

        table = pa.table(features_array)
        filename = self._format_filename(path_pattern, part)
        if self.s3_filesystem is not None:
            self._write_to_s3(table, filename)
        else:
            self._write_local(table, filename)

    def flush(self) -> None:
        """Write all accumulated features to the output file.

        Called after all selections have been processed in accumulate mode.
        Does nothing if there are no accumulated features or if not
        in accumulate mode.
        """
        if not self._accumulated_features:
            return

        final = {k: pa.concat_arrays(v) for k, v in self._accumulated_features.items()}
        table = pa.table(final)
        filename = self.path_pattern

        if self.s3_filesystem is not None:
            s3_path = self._get_s3_path(filename)
            pq.write_table(table, s3_path, filesystem=self.s3_filesystem)
            logger.info(f"Wrote accumulated output table to S3: {s3_path}")
        else:
            self._write_local(table, filename)

        self._accumulated_features.clear()

    def _get_s3_path(self, file_path: str) -> str:
        """Construct an S3 path by combining the bucket name with a file path.

        Args:
            file_path: The file path within the bucket.

        Returns:
            str: The full S3 path in format "bucket_name/file_path".
        """
        bucket_name = os.getenv("S3_BUCKET_NAME", "")
        return bucket_name + "/" + file_path

    def _write_to_s3(self, table: pa.Table, filename: str) -> None:
        """Write a PyArrow table to S3.

        Args:
            table: The table to write.
            filename: The file path within the bucket.
        """
        s3_path = self._get_s3_path(filename)
        try:
            pq.write_table(table, s3_path, filesystem=self.s3_filesystem)
            logger.info(f"Wrote output table to S3: {s3_path}")
        except Exception:
            logger.exception("Failed to write to S3")
            raise

columns = list(cfg.columns) instance-attribute

exclude = list(cfg.exclude) instance-attribute

name = name instance-attribute

path_pattern = cfg.path_pattern instance-attribute

s3_filesystem = None instance-attribute

__init__(name: str, config: dict[str, Any] | None = None)

Initialize a ParquetOutputWriter.

Parameters:

Name Type Description Default
name str

Unique name for this output writer.

required
config dict[str, Any] | None

Configuration dictionary with keys: - path_pattern (str): Output file path format string. - columns (List[str]): Columns to save. - storage (bool or dict, optional): Storage configuration. If dict with type "s3", can contain access_key, secret_key, and endpoint_override.

None

Raises:

Type Description
ValueError

If required config keys are missing.

Source code in packages/dqm-ml-job/src/dqm_ml_job/outputwriter/parquet.py
def __init__(self, name: str, config: dict[str, Any] | None = None):
    """
    Initialize a ParquetOutputWriter.

    Args:
        name: Unique name for this output writer.
        config: Configuration dictionary with keys:
            - path_pattern (str): Output file path format string.
            - columns (List[str]): Columns to save.
            - storage (bool or dict, optional): Storage configuration.
              If dict with type "s3", can contain access_key, secret_key, and endpoint_override.

    Raises:
        ValueError: If required config keys are missing.
    """
    cfg = ParquetOutputConfig.model_validate(config or {})

    self.path_pattern = cfg.path_pattern
    self.columns = list(cfg.columns)
    self.exclude = list(cfg.exclude)
    self.name = name
    self.s3_filesystem = None

    self._accumulate = "{}" not in self.path_pattern
    self._accumulated_features: dict[str, list[pa.Array]] = {}
    storage_cfg = cfg.storage
    if storage_cfg:
        storage_config = StorageConfig.model_validate(storage_cfg)
        if storage_config.type == "s3":
            self.s3_filesystem = get_s3_filesystem(storage_config)

flush() -> None

Write all accumulated features to the output file.

Called after all selections have been processed in accumulate mode. Does nothing if there are no accumulated features or if not in accumulate mode.

Source code in packages/dqm-ml-job/src/dqm_ml_job/outputwriter/parquet.py
def flush(self) -> None:
    """Write all accumulated features to the output file.

    Called after all selections have been processed in accumulate mode.
    Does nothing if there are no accumulated features or if not
    in accumulate mode.
    """
    if not self._accumulated_features:
        return

    final = {k: pa.concat_arrays(v) for k, v in self._accumulated_features.items()}
    table = pa.table(final)
    filename = self.path_pattern

    if self.s3_filesystem is not None:
        s3_path = self._get_s3_path(filename)
        pq.write_table(table, s3_path, filesystem=self.s3_filesystem)
        logger.info(f"Wrote accumulated output table to S3: {s3_path}")
    else:
        self._write_local(table, filename)

    self._accumulated_features.clear()

write_metrics_dict(metrics_dict: dict[str, dict[str, Any]]) -> None

Aggregate and write dataset-level metrics for all selections.

Parameters:

Name Type Description Default
metrics_dict dict[str, dict[str, Any]]

Map of selection names to their computed metric dictionaries.

required
Source code in packages/dqm-ml-job/src/dqm_ml_job/outputwriter/parquet.py
def write_metrics_dict(self, metrics_dict: dict[str, dict[str, Any]]) -> None:
    """Aggregate and write dataset-level metrics for all selections.

    Args:
        metrics_dict: Map of selection names to their computed
            metric dictionaries.
    """
    if len(metrics_dict) <= 0:
        return
    logger.debug(f"Writing metrics for the {len(metrics_dict)} data selections")
    keys = list(metrics_dict.keys())
    metric_names = list(metrics_dict[keys[0]].keys())
    metrics_table = {"selection": pa.array(keys)}
    for metric_name in metric_names:
        if metric_name.startswith("__") and metric_name.endswith("__"):
            continue
        col = self._collect_metric(metric_name, metrics_dict, keys)
        if col is not None:
            metrics_table[metric_name] = col
    self.write_table("", metrics_table)

write_table(path_pattern: str, features_array: dict[str, Any], part: int | None = None) -> None

Write a table of features or metrics to a Parquet file.

Handles directory creation if the target path doesn't exist (for local writes).

Parameters:

Name Type Description Default
path_pattern str

Identifier for the data destination.

required
features_array dict[str, Any]

Map of column names to pyarrow Arrays.

required
part int | None

Optional partition index for chunked output.

None
Source code in packages/dqm-ml-job/src/dqm_ml_job/outputwriter/parquet.py
def write_table(
    self,
    path_pattern: str,
    features_array: dict[str, Any],
    part: int | None = None,
) -> None:
    """Write a table of features or metrics to a Parquet file.

    Handles directory creation if the target path doesn't exist
    (for local writes).

    Args:
        path_pattern: Identifier for the data destination.
        features_array: Map of column names to pyarrow Arrays.
        part: Optional partition index for chunked output.
    """

    for key in self.columns:
        if key not in features_array:
            logger.error(f"Missing {key} in features for output")

    # Accumulate mode: buffer features for a single flush at the end
    if self._accumulate:
        for k, v in features_array.items():
            if isinstance(v, pa.ChunkedArray):
                v = v.combine_chunks()
            self._accumulated_features.setdefault(k, []).append(v)
        return

    table = pa.table(features_array)
    filename = self._format_filename(path_pattern, part)
    if self.s3_filesystem is not None:
        self._write_to_s3(table, filename)
    else:
        self._write_local(table, filename)