Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 26 additions & 2 deletions python/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,29 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

## [v0.21.0] - September 4, 2026

### What's New

#### MCAP imports

`client.data_import` now supports MCAP (`.mcap`) files with ROS 2 (`ros2msg`/`cdr`) topics.

```python
job = client.data_import.import_from_path("recording.mcap", asset=my_asset)
```

Importing without a config ingests every supported channel. Topics that cannot be decoded fail the import unless `McapParseErrorPolicy.IGNORE_ERROR` is set.

`detect_config` reads the file's channels locally without decoding messages, returning an `McapImportConfig` whose `data` holds one entry per field. Edit it to select, rename, or retype channels before importing.

```python
config = client.data_import.detect_config("recording.mcap")
config.complex_types_import_mode = McapComplexTypesImportMode.STRING
```

Variable-cardinality fields (dynamic and bounded arrays) are typed `BYTES`. As with Parquet, `complex_types_import_mode` on the config decides what each becomes: Arrow IPC bytes, a JSON string under `<name>.json`, both (the default), or neither.

Reading a file locally needs the new `mcap` extra (`pip install sift-stack-py[mcap]`), so both `detect_config` and importing without a config require it.
=======
#### Credentials from sift-cli profiles

`SiftClient` now reads the same `sift.toml` profiles that `sift-cli --profile` uses. An environment that you configure once for the CLI works from Python with no arguments.
Expand All @@ -35,6 +54,11 @@ An `http://` URL now connects without TLS instead of failing, because transport

In the config file, the canonical spelling of the API key is `api_key`, which matches the rest of the Sift API. The older `apikey` spelling is still valid, so you do not need to migrate. If a profile holds both keys, the client uses `api_key`.


## [v0.21.0] - September 4, 2026

### What's New

#### List and get data imports

New in `client.data_import`: `list_` and `get` (plus `find`), and a `run.data_imports` property.
Expand Down
79 changes: 79 additions & 0 deletions python/examples/data_import/mcap/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Import an MCAP (.mcap) file into Sift.

MCAP files are self-describing, so the import needs no column mapping: every
channel of every supported topic (ros2msg schemas with cdr messages) is
imported. Channel names combine the topic and flattened field path; the
bundled sample_data.mcap produces "/imu/data.angular_velocity.x",
"/imu/data.linear_acceleration.x", "/battery.voltage", and their siblings.

Swap sample_data.mcap for your own .mcap recording.
"""

import os

from dotenv import load_dotenv
from sift_client import SiftClient

if __name__ == "__main__":
load_dotenv()

grpc_uri = os.getenv("SIFT_GRPC_URI")
assert grpc_uri, "expected 'SIFT_GRPC_URI' environment variable to be set"

rest_uri = os.getenv("SIFT_REST_URI")
assert rest_uri, "expected 'SIFT_REST_URI' environment variable to be set"

apikey = os.getenv("SIFT_API_KEY")
assert apikey, "expected 'SIFT_API_KEY' environment variable to be set"

asset_name = os.getenv("ASSET_NAME")
assert asset_name, "expected 'ASSET_NAME' environment variable to be set"

client = SiftClient(api_key=apikey, grpc_url=grpc_uri, rest_url=rest_uri)

# Auto-detect the config and import the file.
import_job = client.data_import.import_from_path(
"sample_data.mcap",
asset=asset_name,
)

import_job.wait_until_complete()

# If auto-detect doesn't quite match your file, inspect the config and patch
# it before importing. Common fixes: drop channels you don't need, rename or
# retype a channel, skip undecodable topics, set a start time for logs on a
# non-Unix epoch, or pick metadata records to import.
#
# from datetime import datetime, timezone
#
# from sift_client.sift_types.data_import import (
# McapComplexTypesImportMode,
# McapParseErrorPolicy,
# )
#
# config = client.data_import.detect_config("sample_data.mcap")
# print(config) # inspect every detected channel
#
# # Example: import array fields only as JSON strings, instead of the
# # default of both JSON and Arrow IPC bytes
# config.complex_types_import_mode = McapComplexTypesImportMode.STRING
#
# # Example: import only the IMU topic
# config.data = [d for d in config.data if d.topic == "/imu/data"]
#
# # Example: skip undecodable topics and records instead of failing
# config.parse_error_policy = McapParseErrorPolicy.IGNORE_ERROR
#
# # Example: reinterpret log_time as elapsed nanoseconds from an explicit
# # start; only for recorders whose clock did not track Unix time
# config.relative_start_time = datetime(2026, 1, 1, tzinfo=timezone.utc)
#
# # Example: import every key of a named metadata record as run metadata
# config.metadata_records = ["calibration"]
#
# import_job = client.data_import.import_from_path(
# "sample_data.mcap",
# asset=asset_name,
# config=config,
# )
# import_job.wait_until_complete()
2 changes: 2 additions & 0 deletions python/examples/data_import/mcap/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
python-dotenv
sift-stack-py[mcap]
Binary file added python/examples/data_import/mcap/sample_data.mcap
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
DataImport,
Hdf5ImportConfig,
ImportConfig,
McapImportConfig,
ParquetFlatDatasetImportConfig,
ParquetSingleChannelPerRowImportConfig,
TdmsImportConfig,
Expand Down Expand Up @@ -50,6 +51,8 @@ def _set_config_on_request(
request.hdf5_config.CopyFrom(config._to_proto())
elif isinstance(config, UlogImportConfig):
request.ulog_config.CopyFrom(config._to_proto())
elif isinstance(config, McapImportConfig):
request.mcap_config.CopyFrom(config._to_proto())
else:
raise TypeError(f"Unsupported import config type: {type(config).__name__}")

Expand Down
Loading
Loading