blob: 03e64a8209b5adf320f23ea9e4a1c15886be9014 [file]
# 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.
"""Public ROSBag ingestion API."""
import os
import sys
from dataclasses import dataclass
from typing import Callable, Mapping, Optional
from pypaimon.common.options import Options
from pypaimon.multimodal.rosbag.loader import _load_rosbag_manifests
from pypaimon.multimodal.rosbag.source import _discover_rosbag_sources
from pypaimon.multimodal.source_utils import (
_SourceFileIO,
_validated_source_options,
_validate_source_kerberos,
)
@dataclass(frozen=True)
class RosbagStagingConfig:
"""Local temporary-space controls for ROSBag ingestion.
In serial mode, ``max_bytes`` covers remote source copies and validated
Arrow IPC output. Ray applies it to each worker's remote source copy;
Ray object-store capacity is configured separately.
"""
directory: Optional[str] = None
max_bytes: Optional[int] = None
min_free_bytes: int = 1 << 30
copy_buffer_bytes: int = 8 << 20
@dataclass(frozen=True)
class RosbagLoadResult:
"""Counts and committed snapshot for one ROSBag load.
``batch_count`` is the number of transform outputs in serial mode and
``None`` in Ray mode, where Ray may reorganize output blocks.
"""
source_count: int
batch_count: Optional[int]
row_count: int
snapshot_id: Optional[int]
def load_from_rosbag(
table,
paths,
*,
transform: Callable,
default_typestore=None,
typestore_factory=None,
source_options: Optional[Mapping[str, object]] = None,
staging: Optional[RosbagStagingConfig] = None,
allow_storage_fragment: bool = False):
"""Validate and append ROS1/ROS2 transforms in one commit.
``transform(reader, source)`` returns Arrow tables or record batches.
Every source and transform result is validated before a Paimon writer is
created. The operation is append-only and a commit exception can have an
unknown result, so callers must inspect table state before retrying.
"""
if sys.version_info < (3, 10):
raise RuntimeError(
"load_from_rosbag requires Python 3.10 or newer; the rosbag "
"extra is not available on older Python versions.")
if not callable(transform):
raise ValueError("transform must be callable.")
if default_typestore is not None and typestore_factory is not None:
raise ValueError(
"default_typestore and typestore_factory are mutually exclusive.")
if typestore_factory is not None and not callable(typestore_factory):
raise ValueError("typestore_factory must be callable.")
if staging is None:
staging = RosbagStagingConfig()
if not isinstance(staging, RosbagStagingConfig):
raise ValueError("staging must be a RosbagStagingConfig.")
_validate_staging_config(staging)
path_values = _path_values(paths)
options = _validated_source_options(source_options)
_validate_source_kerberos(path_values, options, source_name="ROSBag")
source_file_io = _SourceFileIO(Options(options))
try:
manifests = _discover_rosbag_sources(
path_values,
source_file_io,
allow_storage_fragment=allow_storage_fragment,
)
if not manifests:
return RosbagLoadResult(0, 0, 0, None)
try:
from rosbags.highlevel import AnyReader
except ImportError as error:
raise ImportError(
"load_from_rosbag requires rosbags; install "
"'pypaimon[rosbag]'.") from error
return _load_rosbag_manifests(
table,
manifests,
transform,
source_file_io,
AnyReader,
default_typestore=default_typestore,
typestore_factory=typestore_factory,
staging=staging,
)
finally:
source_file_io.close()
def _path_values(paths):
if isinstance(paths, (str, os.PathLike)):
return [paths]
if isinstance(paths, bytes):
raise ValueError("paths must be a path or an iterable of paths.")
try:
return list(paths)
except TypeError as error:
raise ValueError(
"paths must be a path or an iterable of paths.") from error
def _validate_staging_config(staging):
if (
staging.max_bytes is not None
and (
isinstance(staging.max_bytes, bool)
or not isinstance(staging.max_bytes, int)
or staging.max_bytes < 0)):
raise ValueError("staging.max_bytes must be a non-negative integer.")
if (
isinstance(staging.min_free_bytes, bool)
or not isinstance(staging.min_free_bytes, int)
or staging.min_free_bytes < 0):
raise ValueError(
"staging.min_free_bytes must be a non-negative integer.")
if (
isinstance(staging.copy_buffer_bytes, bool)
or not isinstance(staging.copy_buffer_bytes, int)
or staging.copy_buffer_bytes <= 0):
raise ValueError(
"staging.copy_buffer_bytes must be a positive integer.")