| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| """Distributed HDF5 ingestion using the multimodal transform contract.""" |
| |
| from typing import Any, Dict, Mapping, Optional |
| |
| |
| def load_from_hdf5( |
| table_identifier: str, |
| paths, |
| catalog_options: Dict[str, str], |
| *, |
| transform, |
| source_options: Optional[Mapping[str, object]] = None, |
| concurrency: Optional[int] = None, |
| ray_remote_args: Optional[Dict[str, Any]] = None): |
| """Transform complete HDF5 files on Ray and append them in one commit. |
| |
| The transform has the same ``(h5py.File, Hdf5File)`` contract as |
| :meth:`MultimodalConnection.load_from_hdf5`. Discovery runs on the driver; |
| workers open and transform complete files, and the Paimon Ray sink commits |
| all worker messages once. The result reports logical output rows and the |
| exact committed snapshot; its batch count is ``None`` because Ray does not |
| expose that count without re-executing the lazy transform. |
| """ |
| if not callable(transform): |
| raise ValueError("transform must be callable.") |
| |
| from pypaimon.catalog.catalog_factory import CatalogFactory |
| from pypaimon.common.options import Options |
| from pypaimon.multimodal.hdf5 import ( |
| Hdf5LoadResult, |
| _Hdf5SourceFileIO, |
| _discover_hdf5_files, |
| _path_values, |
| _validate_source_kerberos, |
| _validated_source_options, |
| ) |
| from pypaimon.multimodal.table import _target_schema |
| from pypaimon.ray.ray_paimon import _require_ray_data, write_paimon |
| |
| ray_data = _require_ray_data() |
| validated_options = _validated_source_options(source_options) |
| path_values = _path_values(paths) |
| _validate_source_kerberos(path_values, validated_options) |
| source_file_io = _Hdf5SourceFileIO(Options(validated_options)) |
| try: |
| files = _discover_hdf5_files(path_values, source_file_io) |
| finally: |
| source_file_io.close() |
| if not files: |
| return Hdf5LoadResult( |
| file_count=0, |
| batch_count=None, |
| row_count=0, |
| snapshot_id=None, |
| ) |
| |
| table = CatalogFactory.create(catalog_options).get_table(table_identifier) |
| target_schema = _target_schema(table) |
| inputs = ray_data.from_items( |
| [{"path": source.path} for source in files], |
| override_num_blocks=len(files), |
| ) |
| transformed = inputs.map_batches( |
| _TransformHdf5File, |
| fn_constructor_kwargs={ |
| "transform": transform, |
| "source_options": validated_options, |
| "target_schema": target_schema, |
| }, |
| batch_format="pyarrow", |
| batch_size=1, |
| concurrency=concurrency, |
| **dict(ray_remote_args or {}), |
| ) |
| write_result = write_paimon( |
| transformed, |
| table_identifier, |
| catalog_options, |
| concurrency=concurrency, |
| ray_remote_args=ray_remote_args, |
| ) |
| return Hdf5LoadResult( |
| file_count=len(files), |
| batch_count=None, |
| row_count=0 if write_result is None else write_result.row_count, |
| snapshot_id=( |
| None if write_result is None else write_result.snapshot_id |
| ), |
| ) |
| |
| |
| class _TransformHdf5File: |
| |
| def __init__(self, *, transform, source_options, target_schema): |
| self.transform = transform |
| self.source_options = source_options |
| self.target_schema = target_schema |
| |
| def __call__(self, batch): |
| if batch.num_rows != 1: |
| raise ValueError("Ray HDF5 transform requires one source per batch.") |
| |
| from pypaimon.common.options import Options |
| from pypaimon.multimodal.hdf5 import ( |
| Hdf5File, |
| _Hdf5SourceFileIO, |
| _transform_hdf5_file, |
| ) |
| |
| try: |
| import h5py |
| except ImportError as error: |
| raise ImportError( |
| "load_from_hdf5 requires h5py; install 'pypaimon[ray,hdf5]'." |
| ) from error |
| |
| source = Hdf5File(path=batch["path"][0].as_py()) |
| source_file_io = _Hdf5SourceFileIO(Options(self.source_options)) |
| try: |
| for table in _transform_hdf5_file( |
| source, |
| self.transform, |
| source_file_io, |
| h5py, |
| self.target_schema): |
| if table.num_rows: |
| yield table |
| finally: |
| source_file_io.close() |