Source code for osi_utilities.filename
# SPDX-License-Identifier: MPL-2.0
# SPDX-FileCopyrightText: Copyright (c) 2026, Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
import logging
import re
from datetime import datetime
from pathlib import Path
from osi_utilities._types import MessageType
logger = logging.getLogger(__name__)
# Short code to message type (for OSI naming convention parser)
_SHORT_CODE_TO_MESSAGE_TYPE: dict[str, MessageType] = {
"sv": MessageType.SENSOR_VIEW,
"svc": MessageType.SENSOR_VIEW_CONFIGURATION,
"gt": MessageType.GROUND_TRUTH,
"hvd": MessageType.HOST_VEHICLE_DATA,
"sd": MessageType.SENSOR_DATA,
"tc": MessageType.TRAFFIC_COMMAND,
"tcu": MessageType.TRAFFIC_COMMAND_UPDATE,
"tu": MessageType.TRAFFIC_UPDATE,
"mr": MessageType.MOTION_REQUEST,
"su": MessageType.STREAMING_UPDATE,
}
# Longest-first to avoid prefix ambiguity (e.g. svc before sv)
_SHORT_CODE_PATTERN = "|".join(sorted(_SHORT_CODE_TO_MESSAGE_TYPE.keys(), key=len, reverse=True))
# Maps filename pattern substrings to MessageType
_FILENAME_MESSAGE_TYPE_MAP: dict[str, MessageType] = {
f"_{short_code}_": message_type for short_code, message_type in _SHORT_CODE_TO_MESSAGE_TYPE.items()
}
[docs]
def infer_message_type_from_filename(filename: str) -> MessageType:
"""Infer message type from filename patterns.
Matches the underscore-delimited patterns (e.g. ``_gt_``) and also
supports the common convention where the type code appears at the end of
the stem before the extension (e.g. ``trace_gt.osi``, ``output_sv.mcap``).
"""
lower_name = filename.lower()
# First try the strict patterns (e.g. "_gt_")
for pattern, msg_type in _FILENAME_MESSAGE_TYPE_MAP.items():
if pattern in lower_name:
return msg_type
# Fallback: match type code at end of stem before extension (e.g. "_gt.osi")
stem = Path(filename).stem.lower()
for pattern, msg_type in _FILENAME_MESSAGE_TYPE_MAP.items():
code = pattern.strip("_") # "_gt_" -> "gt"
if stem.endswith(f"_{code}") or stem == code:
return msg_type
return MessageType.UNKNOWN
[docs]
def parse_osi_trace_filename(filename: str) -> dict:
"""Parse an OSI trace filename according to OSI naming convention 2.2.6.2.
Format: <timestamp>_<type>_<osi-version>_<protobuf-version>_<number-of-frames>_<custom-trace-name>.osi
Returns a dict with parsed fields, or empty dict if parsing fails.
"""
pattern = re.compile(
r"^(?P<timestamp>\d{8}T\d{6}Z)"
rf"_(?P<message_type>{_SHORT_CODE_PATTERN})"
r"_(?P<osi_version>[^_]+)"
r"_(?P<protobuf_version>[^_]+)"
r"_(?P<number_of_frames>\d+)"
r"_(?P<custom_trace_name>[^.]+)"
r"\.osi$",
)
match = pattern.match(filename)
if not match:
return {}
try:
timestamp = datetime.strptime(match.group("timestamp"), "%Y%m%dT%H%M%SZ")
return {
"timestamp": timestamp,
"message_type": _SHORT_CODE_TO_MESSAGE_TYPE.get(match.group("message_type")),
"osi_version": match.group("osi_version"),
"protobuf_version": match.group("protobuf_version"),
"number_of_frames": int(match.group("number_of_frames")),
"custom_trace_name": match.group("custom_trace_name"),
}
except (ValueError, IndexError) as e:
logger.warning("Error parsing filename %s: %s", filename, e)
return {}