Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
35d9e86
test
alden-sift Oct 7, 2026
80448b0
python(feat): detect flat dataset Parquet config client-side
alden-sift Oct 9, 2026
63ec9f2
python(fix): lint pyproject.toml and update uv.lock
alden-sift Oct 9, 2026
bdde4a0
python(test): make Parquet detection tests pass on Python 3.8
alden-sift Oct 9, 2026
27dc582
Update python/lib/sift_client/sift_types/data_import.py
alden-sift Oct 12, 2026
9a4b196
python(fix): scope relative_start_time to flat dataset Parquet configs
alden-sift Oct 12, 2026
c54c883
python(refactor): move infer_time_column into _internal.util
alden-sift Oct 12, 2026
0b773e1
python(refactor): make optional Parquet detection args keyword-only
alden-sift Oct 12, 2026
a0cf8a4
python(build): include parquet in the file-imports extra
alden-sift Oct 12, 2026
bca2990
python(test): drop redundant single channel per row relative_start_ti…
alden-sift Oct 12, 2026
a6c19e3
python(feat): support nanosecond precision for relative_start_time
alden-sift Oct 12, 2026
11c7d84
python(fix): fall back when the time format is unspecified
alden-sift Oct 12, 2026
9dbd258
python(refactor): added is_relative property to TimeFormat for easier…
alden-sift Oct 12, 2026
906b4ad
python(refactor): represent an undetected time format as None
alden-sift Oct 12, 2026
f731566
python(feat): support relative_start_time for all import formats
alden-sift Oct 12, 2026
69dda85
python(fix): raise when CSV detection finds no time column
alden-sift Oct 12, 2026
f8ed1c6
python(docs): clarify relative_start_time semantics for MCAP
alden-sift Oct 12, 2026
efe86ef
python(refactor): move time format and relative_start_time handling o…
alden-sift Oct 13, 2026
c456562
python(build): define data-review extra as an alias of parquet
alden-sift Oct 14, 2026
a41a330
python(docs): add changelog fragment
alden-sift Oct 14, 2026
ad49884
Merge remote-tracking branch 'origin/main' into eng-12274-implement-p…
alden-sift Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions python/changelog.d/819.feat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
`detect_config` and `import_from_path` detect flat dataset Parquet configs on the client, so wide Parquet files no longer fail detection with `RESOURCE_EXHAUSTED` when their footer exceeds the server's 20 MiB request limit.

Flat dataset Parquet detection now requires pyarrow. Install it with `pip install sift-stack-py[parquet]`. The `file-imports` extra includes it.

`detect_config` and `import_from_path` also accept `relative_start_time`, so files with a relative time format can be detected and imported without editing the config.
131 changes: 131 additions & 0 deletions python/lib/sift_client/_internal/util/data_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Helpers for detecting and preparing data import configs."""

from __future__ import annotations

from typing import TYPE_CHECKING

from sift_client._internal.time import to_timestamp_pb
from sift_client._internal.util.executor import run_sync_function
from sift_client._internal.util.file import extract_parquet_footer
from sift_client._internal.util.time_column import infer_time_column
from sift_client.sift_types.channel import ChannelDataType
from sift_client.sift_types.data_import import (
EXTENSION_TO_DATA_TYPE_KEY,
CsvImportConfig,
ParquetFlatDatasetImportConfig,
ParquetSingleChannelPerRowImportConfig,
ParquetTimeColumn,
TimeFormat,
)

if TYPE_CHECKING:
from datetime import datetime
from pathlib import Path

from sift_client.sift_types.data_import import DataTypeKey


_RELATIVE_FORMAT_VALUES = {f.value for f in TimeFormat if f.is_relative}


def set_detected_start_time(time_column, relative_start_time: datetime | None) -> None:
"""Set the start time on a detected relative time column proto.

Without it, a relative format detected by the server fails config
validation before ``relative_start_time`` can be applied.
"""
if relative_start_time is not None and time_column.format in _RELATIVE_FORMAT_VALUES:
time_column.relative_start_time.CopyFrom(to_timestamp_pb(relative_start_time))


def resolve_data_type_key(ext: str, data_type: DataTypeKey | None) -> DataTypeKey:
"""Resolve the data type key from file extension and explicit override."""
if data_type is not None:
return data_type
if ext in (".parquet", ".pqt"):
raise ValueError(
"Parquet files require 'data_type' to be specified. "
"Use DataTypeKey.PARQUET_FLATDATASET or DataTypeKey.PARQUET_SINGLE_CHANNEL_PER_ROW."
)
if ext in (".h5", ".hdf5"):
raise ValueError(
"HDF5 files require 'data_type' to be specified. "
"Use DataTypeKey.HDF5_ONE_D, DataTypeKey.HDF5_TWO_D, "
"or DataTypeKey.HDF5_COMPOUND."
)
if ext not in EXTENSION_TO_DATA_TYPE_KEY:
raise ValueError(
f"Unsupported file extension '{ext}'. "
f"Supported: {', '.join(sorted(EXTENSION_TO_DATA_TYPE_KEY))}. "
"You can also specify 'data_type' explicitly using a DataTypeKey value."
)
return EXTENSION_TO_DATA_TYPE_KEY[ext]


def parse_csv_detect_response(
proto, filename: str, relative_start_time: datetime | None = None
) -> CsvImportConfig:
"""Parse a CSV DetectConfig response into a config."""
if proto.time_column.column_number < 1:
raise ValueError(
f"No time column detected in '{filename}'. Build a CsvImportConfig with "
"time_column set and pass it to import_from_path as config."
)
set_detected_start_time(proto.time_column, relative_start_time)
csv_config = CsvImportConfig._from_proto(proto)
time_col = csv_config.time_column.column
csv_config.data_columns = [dc for dc in csv_config.data_columns if dc.column != time_col]
return csv_config


def parse_parquet_detect_response(
proto,
filename: str,
footer_offset: int,
footer_length: int,
*,
relative_start_time: datetime | None = None,
) -> ParquetFlatDatasetImportConfig | ParquetSingleChannelPerRowImportConfig:
"""Parse a Parquet DetectConfig response into a config."""
if proto.HasField("flat_dataset"):
set_detected_start_time(proto.flat_dataset.time_column, relative_start_time)
parquet_config = ParquetFlatDatasetImportConfig._from_proto(
proto, footer_offset=footer_offset, footer_length=footer_length
)
time_path: str | None = parquet_config.time_column.path
if not time_path:
time_path = infer_time_column(
(dc.name, dc.data_type, dc.path) for dc in parquet_config.data_columns
)
if time_path:
parquet_config.time_column = ParquetTimeColumn(path=time_path)
if time_path:
parquet_config.data_columns = [
dc for dc in parquet_config.data_columns if dc.path != time_path
]
return parquet_config
elif proto.HasField("single_channel_per_row"):
set_detected_start_time(proto.single_channel_per_row.time_column, relative_start_time)
scpr_config = ParquetSingleChannelPerRowImportConfig._from_proto(
proto, footer_offset=footer_offset, footer_length=footer_length
)
if not scpr_config.time_column.path:
time_path = infer_time_column(
(col.column_config.name, ChannelDataType(col.column_config.data_type), col.path)
for col in proto.single_channel_per_row.columns
)
if time_path is not None:
scpr_config.time_column = ParquetTimeColumn(path=time_path)
return scpr_config
raise ValueError(f"Unsupported parquet layout in DetectConfig response for '{filename}'.")


async def prepare_parquet_config(
config: ParquetFlatDatasetImportConfig | ParquetSingleChannelPerRowImportConfig,
path: Path,
) -> None:
"""Populate parquet footer fields on the config if not already set."""
if config.footer_offset == 0 and config.footer_length == 0:
footer_bytes, footer_offset = await run_sync_function(lambda: extract_parquet_footer(path))
config.footer_offset = footer_offset
config.footer_length = len(footer_bytes)
Loading
Loading