Source code for osi_utilities.tracefile.configure
# SPDX-License-Identifier: MPL-2.0
# SPDX-FileCopyrightText: Copyright (c) 2026, Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
import logging
from pathlib import Path
from osi_utilities.api.types import ChannelSpecification, MessageType
from osi_utilities.message_types import require_message_type
from osi_utilities.tracefile.readers.base import TraceReader
logger = logging.getLogger(__name__)
[docs]
def create_reader(path: str | Path) -> TraceReader:
"""Create a trace file reader appropriate for the given file.
Args:
path: Path to the trace file. Extension determines the reader type.
Returns:
An unopened TraceReader instance.
Raises:
ValueError: If the file extension is not supported.
"""
path = Path(path)
suffix = path.suffix.lower()
if suffix == ".mcap":
from osi_utilities.tracefile.readers.multi import MultiTraceReader
reader = MultiTraceReader()
elif suffix == ".osi":
from osi_utilities.tracefile.readers.single import SingleTraceReader
reader = SingleTraceReader()
elif suffix == ".txth":
logger.warning("The .txth format is not reliably deserializable. Use .osi or .mcap instead.")
from osi_utilities.tracefile.readers.textformat import ProtobufTextFormatTraceReader
reader = ProtobufTextFormatTraceReader()
else:
raise ValueError(f"Unsupported trace file extension: '{suffix}'")
return reader
def configure_reader(reader: TraceReader, channel_spec: ChannelSpecification) -> None:
"""Configure a concrete reader from a channel specification."""
# Single channel reader configuration
if channel_spec.message_type is not None and hasattr(reader, "set_message_type"):
reader.set_message_type(require_message_type(channel_spec.message_type))
# Multi channel reader configuration
if channel_spec.topic is not None and hasattr(reader, "set_topics"):
reader.set_topics([channel_spec.topic])
if (
channel_spec.topic is not None
and channel_spec.message_type is not None
and hasattr(reader, "set_topic_message_types")
):
reader.set_topic_message_types({channel_spec.topic: require_message_type(channel_spec.message_type)})
def configure_reader_for_channels(reader: TraceReader, channel_specs: list[ChannelSpecification]) -> None:
"""Configure a reader from multiple channel specifications.
Intended for multi-channel use-cases where several logical channels from the
same source file should be read through one configured reader.
Raises:
ValueError: If ``channel_specs`` reference different paths or contain
conflicting message type expectations.
"""
if not channel_specs:
return
paths = {Path(spec.path) for spec in channel_specs}
if len(paths) > 1:
raise ValueError("configure_reader_for_channels requires all channel specs to reference the same path.")
# Configure topics and per-topic message type expectations (for multi readers).
topics: list[str] = []
topic_message_types: dict[str, MessageType] = {}
for spec in channel_specs:
if spec.topic is not None and spec.topic not in topics:
topics.append(spec.topic)
if spec.topic is not None and spec.message_type is not None:
msg_type = require_message_type(spec.message_type)
existing = topic_message_types.get(spec.topic)
if existing is not None and existing != msg_type:
raise ValueError(
f"Conflicting message type expectations for topic '{spec.topic}': {existing} vs {msg_type}."
)
topic_message_types[spec.topic] = msg_type
if topics and hasattr(reader, "set_topics"):
reader.set_topics(topics)
if topic_message_types and hasattr(reader, "set_topic_message_types"):
reader.set_topic_message_types(topic_message_types)
# Configure single-channel readers when a single consistent type is specified.
if hasattr(reader, "set_message_type"):
single_types = {
require_message_type(spec.message_type) for spec in channel_specs if spec.message_type is not None
}
if len(single_types) > 1:
raise ValueError(f"Conflicting message type expectations for single reader: {single_types}.")
if len(single_types) == 1:
reader.set_message_type(next(iter(single_types)))