Change Log¶
All notable changes to this project will be documented in this file.
This project adheres to Semantic Versioning.
[v0.23.1] - October 8, 2026¶
- Test-results steps updated out of order now have their statuses corrected before the report is finalized, with a
SiftReportLifecycleWarning. (#821)
[v0.23.0] - October 1, 2026¶
- Fix issue where a test-results log containing an update that named no fields could never finish importing. Re-run
import-test-result-logto finish a log written by an earlier version. (#809) - A key that is not a field of a create or update model now warns with
SiftIgnoredInputWarning, and suggests a near match. (#809) - The pytest plugin can archive a report as soon as it creates it, so the run stays out of the default Test Results views. Set
archive_on_createunder[tool.sift.pytest.report], or the matching CLI flag, ini key, or environment variable. Off by default. See Archiving a run at creation. (#808) - Data reads cap concurrent channel requests at 500, so a query that spans many channels no longer trips backend rate limits. (#788)
[v0.22.1] - September 29, 2026¶
- Fix streaming ingestion processes crashing with SIGSEGV or SIGABRT at interpreter exit. The
sift-stream-bindingstokio runtime was never shut down, so a runtime thread that finished work while Python was finalizing re-entered the interpreter and killed the process after the program had completed. The bindings now stop the runtime from anatexithook and exposeshutdown(timeout=5.0)for callers that exit withos._exit(). If runtime threads outlive the timeout,shutdown()issues aRuntimeWarning. Requiressift-stream-bindings0.5.2. (#804)
[v0.22.0] - September 25, 2026¶
- Added
client.user_defined_functionsfor reusable expressions. - Added
client.annotationsfor data reviews, phases, and review history. - Added
client.campaignsfor grouping report results across runs. - Added
client.webhooks, andRuleAction.webhookto call one when a rule is violated. RuleCreate.actionandRuleUpdate.actionaccept aWebhookdirectly.RuleCreate.asset_idsandRuleUpdate.asset_idsnow acceptAssetobjects as well as IDs.WebhookTest.textdecodes the response body, andWebhook.created_by/modified_byresolve users.
What's New¶
MCAP imports¶
client.data_import now supports MCAP (.mcap) files with ROS 2 (ros2msg/cdr) topics.
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.
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.
client = SiftClient() # the CLI's default profile
client = SiftClient(profile="staging") # a named profile
client = SiftClient.from_profile("staging")
Precedence, highest first:
- Arguments.
- A profile that you name in code.
SIFT_API_KEY,SIFT_GRPC_URI,SIFT_REST_URI, andSIFT_APP_URL.- The profile that
SIFT_PROFILEnames. - The config file's default profile.
client.credential_sources reports the layer that supplied each value. See Credentials and profiles.
The pytest plugin gains --sift-profile, the sift_profile ini key, and SIFT_PROFILE. In the plugin, the existing settings surfaces outrank the profile. The profile supplies only the values that they leave unset, so a key that CI injects stays in effect.
An http:// URL now connects without TLS instead of failing, because transport security follows the scheme of the gRPC URL. An https:// URL and a bare host name behave as before.
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.
# Imports for a run, e.g. to check on a batch
imports = client.data_import.list_(runs=[run])
imports = run.data_imports # same query, keyed off the run
# One import by ID
data_import = client.data_import.get("data-import-id")
Each DataImport carries the import's status, error_message, and warning_messages. job.get_data_import() returns it for the Job that import_from_path hands back.
Import enum channels from any file format¶
Data columns on all import configs now accept enum_types, a mapping of enum name to key.
CsvDataColumn(
column=2,
name="state",
data_type=ChannelDataType.ENUM,
enum_types={"IDLE": 0, "ARMED": 1},
)
Setting enum_types on a non-enum data type raises a validation error. Multi-channel Parquet single-channel-per-row imports don't support enums, since channel configs there are derived per row.
Bugfixes¶
- Fix ingesting
ChannelDataType.BYTESchannels. Registering a flow with a bytes channel raisedValueError: Unknown data type: bytes, and sending a value on one raisedValueError: Invalid data type: bytes. Both paths now map to the bytes type insift-stream-bindings. Values may bebytes,bytearray,memoryview, or an iterable of ints. Requiressift-stream-bindings0.5.1. - Fix
channels.get_dataprinting aUserWarning: Discarding nonzero nanoseconds in conversiononce per channel on each run-scoped fetch. The channel data cache stored segment bounds asdatetime, which truncated the nanosecond timestamps the data already carries; they are nowpd.Timestamp.start_timeandend_timeonget_dataandget_data_as_arrowalso accept apd.Timestampnow, and its nanoseconds survive to the wire, where the service filters on them.
[v0.20.0] - August 25, 2026¶
What's New¶
Interrupted test-result uploads resume¶
import-test-result-log <log> now finishes an upload that was cut short instead of creating a second report. Every upload records what reached the server in a tracking sidecar beside the log (<log>.jsonl.tracking), so a re-run reuses the report the earlier attempt created and sends only the entries that are missing. A log that is already fully uploaded is a no-op.
# Resume, or start fresh if there is nothing to resume
import-test-result-log ./offline-runs/a1b2c3/a1b2c3.jsonl
# Abandon the partial upload and create a new report
import-test-result-log --new-report ./offline-runs/a1b2c3/a1b2c3.jsonl
--new-report moves the existing sidecar to <log>.jsonl.tracking.bak rather than overwriting it, so the abandoned report's ID stays recoverable. Resuming requires that report to still exist; if it was deleted, or the sidecar came from another environment, the command fails and names --new-report as the way forward.
client.test_results.import_log_file(...) takes the matching new_report argument. import-test-result-log no longer accepts --incremental: resuming is the default now, and the plugin's background worker moved to its own entry point. (#745)
New generated protos¶
- Added the public
sift.automation.v1package and refreshed the generated protobufs across common, data, data imports, external sync, families, filters, ingestion configs, metadata, notifications, panel configurations, remote files, reports, rule evaluation, runs, and saved searches. (#753)
Bugfixes¶
- Fix
assets.archiveraisingAttributeErrorby reading the correct response field,archived_run_ids. (#747) - Fix streaming ingestion failing to start when an ingestion config's flow schema exceeded the streaming client's 4 MiB decode limit.
sift-stream-bindings0.5.0 decodes up to 50 MiB. (#751) - Fix the pytest plugin's upload worker failing to spawn when the venv's
bin/was not onPATH, such as undersudoorpython -m pytestagainst a non-activated venv. It now runs through the same interpreter as pytest. (#748) - Fix
client.runs.list(is_stopped=False)returning only stopped runs.Falsenow applies no filter;is_stopped=Trueis unchanged. (#728)
Full Changelog¶
- Replace bash dev script with justfile for task automation
- Update company name and year in licenses
- Use a normal truthy check for the is_stopped run filter
- Resume incomplete test report upload
- Fix assets.archive AttributeError
- Spawn import process in same interpreter as pytest
- Decode large flow schemas on stream build
- Update public protos
[v0.19.1] - July 27, 2026¶
What's New¶
page_size on get_data¶
client.channels.get_data(...) and get_data_as_arrow(...) now take a page_size, the number of points fetched per request. It defaults to 10,000; the server caps it at 1,000,000 and coerces higher values down. Raise it to cut round-trips on large windows.
New generated protos¶
- Added the public
sift.canvas.v1package. (#683) - Refreshed the generated protobufs, adding
sift.families.v1and updating common, data imports, exports, reports, rules, rule evaluation, and saved searches. (#689)
Bugfixes¶
- Fix
get_datafailing to build its request:start_timeandend_timewere passed asdatetimeobjects where protobuf timestamps were required. (#688) - Upload a teardown failure on the final test of a pytest session. The session-scoped
report_contextfixture tore down before pytest reported the outcome, so a run whose last test failed in teardown was recorded as fully passing. Late failures now also correct already-closed ancestor steps from passed to failed. (#691) - Record combined
@pytest.mark.parametrize("a,b,c", ...)axes as one step instead of one per argument name, and honorids=. Step names and tree depth change, so reports from before and after this release will not group together. (#691)
Full Changelog¶
- Add public sift.canvas.v1 proto package
- Fix time type in get_data requests
- Update protos
- Surface get_data page_size parameter
- Fix teardown failure on the last test and combined parametrize axis nesting
[v0.19.0] - July 10, 2026¶
What's New¶
Faster get_data pagination¶
Up to a ~80x speedup for some get_data calls. get_data now also displays a progress bar while paging through results.
Shared on-disk cache (opt-out, on by default)¶
client.channels.get_data(...) now caches the channel windows it returns to disk by default. Subsequent calls covering the same channel/time range — including from a fresh process — read straight out of the cache instead of going to the wire. This also bounds memory: nothing is held in process after the call returns.
The cache lives on the SiftClient as a single shared store: every cache-aware resource writes to one global byte budget at one path, with one LRU policy. The default location is <tempfile.gettempdir()>/sift-data-cache, capped at 4 GiB with LRU eviction. If the default path can't be opened (read-only filesystem, restricted container, etc.), the client logs a warning and continues with caching disabled — get_data still works, it just always goes to the wire.
ignore_cache=True on client.channels.get_data(...) now skips writing into the cache as well as reading from it. Previously a "non-caching" workload still appended to the shared cache on every call.
Configuration lives on the new client.cache namespace — knobs are global because the store is shared:
# Opt out — no data persisted to disk; every get_data call goes to the wire.
client.cache.disable()
# Reconfigure the location or byte cap.
client.cache.enable(path="/data/sift-cache", max_bytes=2 * 1024 ** 3)
# Remove a stale or corrupted cache directory.
client.cache.clear() # default tmp path
client.cache.clear("/data/sift-cache") # custom path
enable is also the way to turn the cache back on after a prior disable call.
The cache is powered by diskcache (pure-Python, SQLite-backed) with LRU eviction.
Report templates¶
Added a full report template API, available as a nested resource of reports at client.reports.templates (and asynchronously via client.async_.reports.templates). Report templates can now be created, fetched, listed, updated, archived, and unarchived directly from sift_client:
from sift_client.sift_types import ReportTemplateCreate
template = client.reports.templates.create(
ReportTemplateCreate(
name="Motor Checkout",
client_key="motor-checkout",
rule_ids=rules, # Rule objects or IDs; or rule_client_keys=[...]
tags=["motor"],
)
)
# Run the template against a run
job = template.create_report(run=run)
Templates can be fetched by ID or client key, and rules can be attached by rule ID or rule client key (rule_ids also accepts Rule objects, and tags accepts Tag objects or names). Updating tags, rule_ids, or rule_client_keys replaces the full list on the template.
Breaking change: client.reports.create_from_template now takes report_template (a ReportTemplate or ID string) and run (a Run or ID string) instead of report_template_id and run_id, matching the other create_from_* methods.
ULog imports¶
Added PX4 ULog (.ulg) as a supported data import format. ULog files are self-describing, so client.data_import.import_from_path("flight.ulg", asset=asset) imports every channel in the file with no column mapping. Detection runs client-side and requires the ulog extra (pip install sift-stack-py[ulog]).
Call detect_config to inspect and patch the import before uploading, the same as the other formats:
config = client.data_import.detect_config("flight.ulg")
# Import only the accelerometer channels
config.data = [d for d in config.data if d.message_name == "sensor_accel"]
# Anchor the timeline at an explicit log start time; this takes precedence
# over the log's GPS fix and is required for logs without one
config.relative_start_time = datetime(2026, 1, 1, tzinfo=timezone.utc)
# Import firmware version and a parameter as run metadata
config.info_keys = ["ver_sw"]
config.param_keys = ["BAT1_CAPACITY"]
client.data_import.import_from_path("flight.ulg", asset=asset, config=config)
Resource and principal attributes (ABAC)¶
Added a public API for attribute-based access control (ABAC) attributes under client.access_control. Resource attributes describe the Sift objects an access decision applies to, such as assets, channels, and runs. Principal attributes describe the users or groups an access decision applies to. Async APIs are available under client.async_.access_control.
An attribute key is the entry point. Create or fetch a key, define its enum values, then assign a value to a set of resources. Resource assignments accept supported resource objects, or a typed ResourceAttributeEntity when you only have an ID:
from sift_client.sift_types import ResourceAttributeEntity, ResourceAttributeValueType
key = client.access_control.resource_attributes.keys.get_or_create(
"licenses",
ResourceAttributeValueType.SET_OF_ENUM,
)
licenses = key.get_or_create_enum_values(["LICENSE_A", "LICENSE_B"])
key.assign_to([ResourceAttributeEntity.for_channel("channel-id")], value=licenses)
Principal attributes accept User objects, typed PrincipalRef references, or user email addresses (resolved to user IDs automatically). Use PrincipalRef.user_group(...) for user groups:
from sift_client.sift_types import PrincipalAttributeValueType
key = client.access_control.principal_attributes.keys.get_or_create(
"licenses",
PrincipalAttributeValueType.SET_OF_ENUM,
)
licenses = key.get_or_create_enum_values(["LICENSE_A"])
key.assign_to(["user@example.com"], value=licenses)
Keys, enum values, and assignments are managed through the nested keys, enum_values, and assignments APIs on each side (for example client.access_control.resource_attributes.assignments.list_()), each supporting the relevant create, get, list, update, and archive operations. For SET_OF_ENUM keys, an assignment replaces the full value set on each target, and each enum value in the set is returned as its own assignment.
Users¶
Added a users API at client.users (async: client.async_.users). A user's name is their login name, typically their email address:
user = client.users.find(name="alice@example.com")
users = client.users.list_(name_contains="@example.com")
ids = client.users.resolve_ids(["alice@example.com", "bob@example.com"])
Bugfixes¶
- Fix
get_datapagination returning incorrect results in some windows. (#678) - Strip docstrings captured by the pytest plugin so they no longer leak into report step names and descriptions. (#658)
Full Changelog¶
- Add ABAC resource and principal attributes
- Get data cache improvements
- Clean docstrings captured in pytest
- ULog data imports
- Group access control APIs under client.access_control
- Report templates API resource
- Let download_file forward extra headers to rest_client.get
- Fix get_data pagination and add progress bars
[v0.18.0] - June 19, 2026¶
This release is a major overhaul of the pytest plugin. One import enables it, with a hierarchical report tree, offline and disabled run modes, pass/fail and abort handling that rolls up correctly, a terminal report summary, project-level configuration, and an audit log for diagnosing field failures.
What's New¶
Single-import plugin¶
The plugin is now sift_client.pytest_plugin, wired in with one line:
This replaces the previous setup that required conftest.py boilerplate and a from sift_client.util.test_results import * activation. A default sift_client fixture reads SIFT_* env vars with sift_grpc_uri / sift_rest_uri ini fallback, so per-project config lives in pyproject.toml. sift_include / sift_exclude markers and the sift_autouse ini key scope which tests produce Sift reports, and autouse fixtures are gated per test so mixed unit and integration suites share one pytest invocation without unit tests paying a backend round-trip.
Hierarchical report tree¶
Python packages, test modules, test classes (including nested), and @pytest.mark.parametrize axes each open a nested parent step automatically, so the report mirrors the test layout. Parents are keyed by identity rather than held on a shared stack, so the tree is order-independent: it works with pytest-randomly, pytest-ordering, and fixture-scope reordering. Each parent closes as soon as the last test in its subtree finishes, so containers resolve progressively in incrementally uploaded reports instead of all at session end.
Parametrize placement is scope-aware: function-scoped params nest under the test, while a class, module, or session-scoped parametrized fixture sits at that level, matching where pytest re-runs the work. Four ini keys gate the layers individually: sift_package_step, sift_module_step, sift_class_step, and sift_parametrize_nesting (all default true).
Offline and disabled modes¶
Two named connection modes replace the previous check-connection toggle:
--sift-offlineskips the connectivity ping and routes create/update calls to a JSONL log for later replay viaimport-test-result-log, tolerating missingSIFT_*env vars.--sift-disabled(alsoSIFT_DISABLED=1) contacts nothing and writes no log, butstep.measure(...)still evaluates bounds and returns a real pass/fail result. No credentials required.
is_simulated is exposed on TestReport, TestStep, TestMeasurement, and ReportContext so callers can branch on provenance. In online mode a failed ping now aborts the session once via pytest.exit, before any test body runs and with the full error untruncated.
Pass/fail reporting¶
Assertion failures record the concise assertion message on error_info (the step stays FAILED with no traceback) so the detail surfaces in the report UI. step.pytest_fail_if_step_failed() fails the pytest test when the step or any descendant failed, closing the gap where a failed substep or report_outcome() marked a step FAILED in the report but left pytest green. The failure message names each out-of-bounds measurement by name, value, and bounds, plus any failed substep.
Status now rolls up correctly on a session abort. Parent steps (class, module, package) resolve from their descendants after pytest's fixture teardown, so a Ctrl-C or KeyboardInterrupt records the report and any open parents as ABORTED instead of letting them close PASSED before the interrupted leaf resolved. sift_client.pytest_plugin.abort(reason) triggers the same path from test code; pytest.exit() and ordinary test failures roll up as FAILED, and an open substep interrupted by pytest.exit() resolves ABORTED.
Terminal summary¶
A session-start header shows the SDK version and active mode (e.g. Sift: sift-stack-py 0.18.0 — online mode). An end-of-run Sift report panel reports status, step and measurement counts (color-coded), test case, system, log file, and a clickable report link (online) or upload command (offline). New --sift-open-report / sift_open_report opens the report in a browser at session end on interactive terminals. sift_app_url / SIFT_APP_URL sets the web-app origin for on-prem and custom deployments; SiftClient.app_url exposes the resolved origin to non-plugin consumers.
Configurable report content¶
Report fields are configurable from [tool.sift.pytest.report] in pyproject.toml (name, test_case, test_system_name, system_operator, serial_number, part_number, and a metadata table) or from SIFT_REPORT_* env vars for per-run values, with env winning over TOML. name and test_case accept placeholders including {target}, {command}, {timestamp}, {count}, {git_repo}, {git_branch}, and {git_commit}. When unset, name defaults to {target} {timestamp} and test_case to {target}, where target is derived from the collected tests so it stays stable across flag order and filters. The full invocation is always recorded under the pytest_command metadata key.
Unknown SIFT_* env vars and [tool.sift.pytest.*] keys produce a warning with a closest-match suggestion, so typos surface immediately.
Output directory and audit logging¶
--sift-output-dir / sift_output_dir sets the directory for a run's artifacts; each run nests a random subfolder and defaults to a temp dir. Two outputs land there and are on by default: the JSONL test-result log (replayable via import-test-result-log) and a DEBUG audit trace that lets field failures be diagnosed from one file without a rerun. Disable either with --no-sift-log-file / --no-sift-audit-log; warnings are still echoed to stdout. The audit trace records config typos, connection state, the resolved-settings snapshot with provenance, status transitions, step open/close, the replay lifecycle, and step.outcome / step.propagate / step.resolve events that show why each step reached its status (own measurement or outcome failure, inherited child failure, or abort). Values for sensitive field names such as token, api_key, and secret are redacted as [REDACTED]. The terminal summary gains an audit-log section.
Bugfixes¶
- Fix incremental upload of test-result logs failing on any resume tick after the first, which left the report stuck in progress. Resume ticks now read the report ID from the tracking map instead of requiring an in-memory report, and the recovery hint points at
import-test-result-log --incremental <log>so a manual retry resumes from the cursor rather than creating a duplicate report. (#611)
Full Changelog¶
- pytest plugin improvements
- pytest pass/fail behavior improvements
- Graceful handling of a missing connection
- Hierarchical pytest report tree
- Report assertion message as error info
- pytest summary output
- Flexible report naming and consolidated options
- Exit instead of raise on connection failure
- Fix incremental upload bug and add unit tests
- Order-independent report tree with early parent close
- Broaden step failure check to cover substeps
- Add audit logging to the pytest plugin
- Scope-aware parametrize support in the report tree
- Abort propagation and output-directory settings
[v0.17.1] - June 10, 2026¶
Bugfixes¶
- Bound the test-results offline log lock wait to 60 seconds so a held lock no longer hangs synchronous API calls; a stuck lock now raises
TimeoutError. Raises the minimumfilelockversion to 3.15. (#623) - Skip progress bars when no console is attached (PyInstaller
--noconsole,pythonw.exe, detached processes), so uploads, downloads, and job polling no longer crash. (#622)
[v0.17.0] - June 3, 2026¶
What's New¶
Update, Archive, and Unarchive Channels¶
Channels can now be edited from the Python client. Use client.channels.update(channel, ChannelUpdate(...)) to change a channel's description, unit, metadata, or archived state, or call update(), archive(), and unarchive() directly on a Channel. Units are read and written by name, and Channel now exposes metadata and is_archived.
channel = client.channels.update(channel, ChannelUpdate(unit="volts", metadata={"calibrated": True}))
channel.archive()
HDF5 Schema Detection¶
Data import now detects HDF5 files under three layouts, selected through the data_type argument with the new DataTypeKey.HDF5_ONE_D, DataTypeKey.HDF5_TWO_D, and DataTypeKey.HDF5_COMPOUND keys. detect_config and import_from_path also accept a new time_format argument; when omitted, HDF5 imports use TimeFormat.ABSOLUTE_UNIX_NANOSECONDS. HDF5 support remains an optional extra (pip install sift-stack-py[hdf5]).
Configurable Request Timeouts¶
gRPC and REST calls now accept a configurable default request timeout. Set GrpcConfig(request_timeout=...) for unary gRPC calls and RestConfig(request_timeout=...) for REST, both defaulting to 60 seconds. A per-call timeout overrides the default, None disables it, and long-running operations such as wait_until_complete and wait_and_download honor their own timeout_secs.
Bugfixes¶
- Bound synchronous API calls so they fail fast instead of hanging the calling thread indefinitely when a request stalls or the client has already been closed. (#613)
- Release the underlying gRPC channels on
close()andclose_sync()and make teardown idempotent, eliminating a shutdown warning, stall, and double-close hang at process exit. (#595)
Full Changelog¶
- Add update, archive, and unarchive to Channel
- Add HDF5 schema detection
- Add a default gRPC request deadline
- Bound sync calls at the transport layer
- Fix gRPC client lifecycle so channels are released promptly
[v0.16.2] - May 21, 2026¶
Bugfixes¶
- Resolve nested calculated channel references when fetching calculated channels so expressions that depend on other calculated channels evaluate correctly. (#580)
- Infer the parquet time column for integer timestamp columns in data imports so files with integer epoch time columns no longer fail detection. (#582)
[v0.16.1] - May 19, 2026¶
Bugfixes¶
- Replace the POSIX-only
fcntl.flockcall in the test-results log with a cross-platformfilelock.FileLockso importingsift_clientno longer fails on Windows. (#574)
[v0.16.0] - May 14, 2026¶
What's New¶
Description, Metadata, and Associated Channels on Test Measurements¶
TestMeasurementCreate and TestMeasurementUpdate now accept description, metadata, and channel_names to attach contextual information and link measurements to the channels they were derived from. channel_names accepts either channel name strings or Channel objects. Descriptions longer than 2000 characters are truncated with a warning to match the server-side limit.
Metadata on Test Steps¶
TestStepCreate and TestStepUpdate now accept a metadata dict so test steps can carry structured key/value context the same way reports and measurements do. The ReportContext.new_step / create_step and NewStep.substep helpers in the test-results context manager forward a new metadata kwarg through to the created step.
Bugfixes¶
- Pass the configured log file through to test result update calls so update events are written to the
.jsonllog alongside create events. (#560)
Full Changelog¶
- Update protos
- Fix log not being passed with update calls
- Add metadata, description, and associated channel support to measurements
- Add step metadata support
[v0.15.0] - May 7, 2026¶
What's New¶
v0.15.0 updates the streaming ingestion client to match the new sift-stream-bindings 0.3.0
API. The RecoveryStrategyConfig class and recovery_strategy parameter have been replaced
with an explicit StreamingMode enum and discrete per-mode configuration kwargs. The
non-blocking send method has been renamed for consistency with the Rust library, and a new
try_send method is available for single-flow non-blocking sends. This release contains
breaking changes to the ingestion API — see below for details and a migration prompt.
Breaking Changes¶
1. RecoveryStrategyConfig Removed — Replaced by StreamingMode + Per-Mode Kwargs¶
RecoveryStrategyConfig and the recovery_strategy parameter on IngestionConfigStreamingClient
and IngestionAPIAsync have been removed. Transport mode is now selected via a StreamingMode
enum, with separate retry_policy and disk_backup_policy kwargs for per-mode configuration.
The default mode is StreamingMode.LIVE_WITH_BACKUPS, which matches the previous default
behavior of RecoveryStrategyConfig.retry_with_backups().
Removed from the public API:
- RecoveryStrategyConfig class (and its retry_only() / retry_with_backups() factory methods)
- recovery_strategy parameter on IngestionConfigStreamingClient and IngestionAPIAsync
Added:
- StreamingMode enum — LIVE_ONLY, LIVE_WITH_BACKUPS, FILE_BACKUP
- streaming_mode parameter (default: StreamingMode.LIVE_WITH_BACKUPS)
- retry_policy parameter — applies to LIVE_WITH_BACKUPS mode
- disk_backup_policy parameter — applies to LIVE_WITH_BACKUPS and FILE_BACKUP modes
- checkpoint_interval_seconds parameter — applies to LIVE_WITH_BACKUPS mode
Before:
from sift_client.resources.ingestion import (
IngestionConfigStreamingClient,
RecoveryStrategyConfig,
)
# Default: live streaming with backups
client = await IngestionConfigStreamingClient.create(
ingestion_config=my_config,
recovery_strategy=RecoveryStrategyConfig.retry_with_backups(),
)
# Retry only (no disk backups)
client = await IngestionConfigStreamingClient.create(
ingestion_config=my_config,
recovery_strategy=RecoveryStrategyConfig.retry_only(),
)
After:
from sift_client.resources.ingestion import (
IngestionConfigStreamingClient,
StreamingMode,
)
# Default: live streaming with backups (no change needed if you were using the default)
client = await IngestionConfigStreamingClient.create(
ingestion_config=my_config,
streaming_mode=StreamingMode.LIVE_WITH_BACKUPS,
)
# Live only — no disk backups, lowest overhead
client = await IngestionConfigStreamingClient.create(
ingestion_config=my_config,
streaming_mode=StreamingMode.LIVE_ONLY,
)
# File backup only
client = await IngestionConfigStreamingClient.create(
ingestion_config=my_config,
streaming_mode=StreamingMode.FILE_BACKUP,
)
To pass a custom retry or disk backup policy:
from sift_stream_bindings import DiskBackupPolicyPy, RetryPolicyPy
client = await IngestionConfigStreamingClient.create(
ingestion_config=my_config,
streaming_mode=StreamingMode.LIVE_WITH_BACKUPS,
retry_policy=RetryPolicyPy.default(),
disk_backup_policy=DiskBackupPolicyPy.default(),
checkpoint_interval_seconds=30,
)
2. send_requests_nonblocking Renamed to try_send_requests¶
IngestionConfigStreamingClient.send_requests_nonblocking has been renamed to try_send_requests
to align with the Rust sift-stream naming convention where try_ methods return immediately
without awaiting channel capacity.
Before:
After:
New Features¶
try_send — Non-Blocking Single-Flow Send¶
A new try_send(flow) method is available on IngestionConfigStreamingClient for non-blocking
single-flow sends. It accepts either a Flow or a raw FlowPy object.
Use try_send in real-time loops where blocking on channel capacity is unacceptable. For
most use cases, the async send(flow) method (which applies backpressure) is preferred.
sift-stream-bindings 0.3.0¶
The sift-stream-bindings dependency has been bumped to 0.3.0, which reflects the
sift-stream 0.9.0 breaking API changes (stepped builder, send rename, removed types).
AI-Assisted Migration Prompt (v0.14.x → v0.15.0)¶
Copy and paste the following prompt to an AI coding agent to automate the upgrade:
You are upgrading a Python project from sift_client v0.14.x to v0.15.0. The streaming ingestion
API has breaking changes. Apply ALL of the following changes precisely. Do not make any other
modifications.
---
## 1. Update `pyproject.toml`
Find every occurrence of `sift-stream-bindings==0.2.2` and replace it with
`sift-stream-bindings==0.3.0`. This may appear under multiple dependency groups (e.g. `all`,
`dev-all`, `sift-stream`, `sift-stream-bindings`).
---
## 2. Remove all imports of `RecoveryStrategyConfig`
Delete any line that imports `RecoveryStrategyConfig`, for example:
from sift_client.resources.ingestion import RecoveryStrategyConfig
from sift_client.resources.ingestion import IngestionConfigStreamingClient, RecoveryStrategyConfig
Remove only `RecoveryStrategyConfig` from those imports; keep any other names on the same line.
---
## 3. Add `StreamingMode` to imports where needed
Wherever `IngestionConfigStreamingClient` or `IngestionAPIAsync` is imported and a streaming
mode needs to be specified, add `StreamingMode` to the import:
from sift_client.resources.ingestion import IngestionConfigStreamingClient, StreamingMode
---
## 4. Replace all `recovery_strategy` call sites
Search for every call to `IngestionConfigStreamingClient.create(...)` and
`IngestionAPIAsync.create(...)` that contains a `recovery_strategy` keyword argument.
### Case A — `RecoveryStrategyConfig.retry_with_backups()` (or no recovery_strategy at all)
This was (and remains) the default. Replace the kwarg:
# BEFORE
recovery_strategy=RecoveryStrategyConfig.retry_with_backups()
# AFTER
streaming_mode=StreamingMode.LIVE_WITH_BACKUPS
If the call had no `recovery_strategy` argument, no change is needed — `LIVE_WITH_BACKUPS`
is the default.
If the old call passed explicit `retry_policy` or `disk_backup_policy` arguments inside
`RecoveryStrategyConfig.retry_with_backups(...)`, move them to top-level kwargs:
# BEFORE
recovery_strategy=RecoveryStrategyConfig.retry_with_backups(
retry_policy=my_retry_policy,
disk_backup_policy=my_disk_policy,
)
# AFTER
streaming_mode=StreamingMode.LIVE_WITH_BACKUPS,
retry_policy=my_retry_policy,
disk_backup_policy=my_disk_policy,
### Case B — `RecoveryStrategyConfig.retry_only()`
Replace with `streaming_mode=StreamingMode.LIVE_ONLY`. If a `retry_policy` was passed,
keep it as a top-level kwarg (it is ignored for `LIVE_ONLY` in this version, but preserving
it avoids a TypeError):
# BEFORE
recovery_strategy=RecoveryStrategyConfig.retry_only(retry_policy=my_policy)
# AFTER
streaming_mode=StreamingMode.LIVE_ONLY
### Case C — Raw `RecoveryStrategyPy` object passed directly
If any call passes a raw `RecoveryStrategyPy` instance as `recovery_strategy`, determine
which mode it was configured for and replace accordingly:
- `RecoveryStrategyPy.retry_only(...)` → `streaming_mode=StreamingMode.LIVE_ONLY`
- `RecoveryStrategyPy.retry_with_backups(...)` → `streaming_mode=StreamingMode.LIVE_WITH_BACKUPS`
with `retry_policy` and `disk_backup_policy` promoted to top-level kwargs.
---
## 5. Rename `send_requests_nonblocking` → `try_send_requests`
Find every call to `.send_requests_nonblocking(...)` on any ingestion client instance and
rename it to `.try_send_requests(...)`. The signature is unchanged.
# BEFORE
client.send_requests_nonblocking(requests)
# AFTER
client.try_send_requests(requests)
---
## 6. Verify
After applying the above changes:
1. Run `grep -r "RecoveryStrategyConfig" .` — expect zero results.
2. Run `grep -r "send_requests_nonblocking" .` — expect zero results.
3. Run `grep -r "recovery_strategy" .` — expect zero results.
4. Run your test suite to confirm no remaining references.
[v0.14.1] - April 30, 2026¶
Bugfixes¶
- Lazy-import
h5pyandnptdmsso the base install doesn't require them. (#547) - Expose
page_sizeon resource list methods so callers can shrink pages when responses hit gRPC message-size limits. (#548)
[v0.14.0] - April 28, 2026¶
What's New¶
Data Import API in SiftClient¶
The sift_client module now exposes a data import API supporting CSV, Parquet, TDMS, and HDF5. With this addition, all features previously available only in sift_py are now available in sift_client, which remains the recommended interface for new development. sift_py (deprecated since v0.10.0) continues to work and ship in this release.
Migrating from sift_py: the per-format upload services (CsvUploadService, ParquetUploadService, Hdf5UploadService, TdmsUploadService) collapse into a single client.data_import.import_from_path method. sift_py only auto-detected for CSV via simple_upload; other formats required more setup. sift_client unifies all four with auto-detection built into import_from_path itself: the config argument is optional, so the common call takes just a file path and target asset. Call client.data_import.detect_config(...) first if you want to inspect or patch the config before importing. import_from_path returns a Job you can optionally wait on.
# sift_py (deprecated)
from sift_py.data_import.csv import CsvUploadService
from sift_py.rest import SiftRestConfig
rest_conf: SiftRestConfig = {"uri": sift_uri, "apikey": apikey}
csv_service = CsvUploadService(rest_conf)
import_service = csv_service.simple_upload(asset_name="my_asset", path="data.csv")
import_service.wait_until_complete()
# sift_client
from sift_client import SiftClient
client = SiftClient(api_key=apikey, grpc_url=grpc_url, rest_url=rest_url)
job = client.data_import.import_from_path("data.csv", asset="my_asset")
job.wait_until_complete()
Format-by-format support:
- CSV: auto-detected from .csv. Supports an optional JSON metadata row (row 1 or row 2) for specifying channel names, units, data types, and the time column format.
- Parquet: requires an explicit data_type (PARQUET_FLATDATASET or PARQUET_SINGLE_CHANNEL_PER_ROW) since .parquet alone doesn't disambiguate the layout. Detection only reads the file footer, so it stays fast on large files.
- HDF5: new in this release. Auto-detected from .h5 / .hdf5. Detection works out-of-the-box for files with a compound-dataset layout (first field = time) or a shared root-level time dataset; other layouts may need a hand-built config.
- TDMS: new in this release. Auto-detected from .tdms and recognizes the common timing conventions (group-level xchannel, first-channel TimeStamp, or per-channel waveform properties). TdmsImportConfig controls handling of untimed channels (fallback_method), complex values (complex_component), scaled vs. raw data, and waveform start-time overrides.
Parquet as an Export Output Format¶
client.data_export.export(...) now accepts ExportOutputFormat.PARQUET alongside the existing CSV and Sun/WinPlot options. Unlike the sift_py DataService + DataFrame.to_parquet() pattern (async-only, buffers everything in memory, name-strings only), the new export API runs as a server-side job, works sync or async, accepts Asset/Channel objects or IDs, and scales to large exports.
# sift_py (deprecated): no dedicated export API, so query in-memory and write yourself
import pandas as pd
from sift_py.data.query import ChannelQuery, DataQuery
from sift_py.data.service import DataService
from sift_py.grpc.transport import use_sift_async_channel
async with use_sift_async_channel({"uri": sift_uri, "apikey": apikey}) as channel:
result = await DataService(channel).execute(DataQuery(
asset_name="my_asset",
start_time=start,
end_time=stop,
channels=[ChannelQuery(channel_name="my_channel")],
))
pd.DataFrame(result.all_channels()[0].columns()).to_parquet("out.parquet")
# sift_client
from sift_client import SiftClient
from sift_client.sift_types.export import ExportOutputFormat
client = SiftClient(api_key=apikey, grpc_url=grpc_url, rest_url=rest_url)
job = client.data_export.export(
assets=["my_asset"], # accepts Asset objects or IDs
channels=["my_channel"], # accepts Channel objects or IDs
start_time=start,
stop_time=stop,
output_format=ExportOutputFormat.PARQUET,
)
files = job.wait_and_download(output_dir="./exports")
wait_and_download polls the job to completion, downloads the resulting archive, and returns the list of downloaded file paths. Both arguments shown are optional: if output_dir is omitted, a temporary directory is created and used. By default the downloaded zip is extracted (and the archive deleted) so you get the Parquet files directly; pass extract=False to keep the zip instead, which is useful if you want to hand the whole bundle off to another system without unpacking it client-side.
Test Result Logging¶
Test result create and update events can now be optionally written to a .jsonl log file during a test run, then replayed against the Sift API later via the new import-test-result-log CLI command (installed with the package).
Progress Indicators¶
Adds progress indicators for job polling and file downloads for better visibility during long-running operations.
Stop Bundling google.api, protoc_gen_openapiv2, and buf.validate¶
Previously the package shipped its own bundled copies of the generated Python bindings for google.api, protoc_gen_openapiv2, and buf.validate. With this release:
- google.api and protoc_gen_openapiv2 are now pulled in via the googleapis-common-protos and protoc-gen-openapiv2 runtime dependencies, so pip installs the upstream-maintained versions.
- buf.validate has been removed entirely. The protovalidate field annotations were stripped from the affected .proto files, so the generated _pb2.py files no longer reference buf.validate.
Upgrading from a pre-v0.14.0 install¶
Upgrading in place can leave behind __init__.py files and the buf/ directory from the previously-bundled copies. When that happens, the leftover google/__init__.py and google/api/__init__.py turn google and google.api into regular packages and collapse the PEP 420 namespace, hiding the upstream googleapis-common-protos contribution. The symptom is an ImportError such as:
A fresh virtualenv resolves it cleanly:
To repair an existing environment in place:
SITE=$(python -c "import sysconfig; print(sysconfig.get_paths()['purelib'])")
rm -f "$SITE/google/__init__.py" "$SITE/google/api/__init__.py"
rm -rf "$SITE/buf"
pip install --force-reinstall --no-deps \
googleapis-common-protos protoc-gen-openapiv2 sift-stack-py
Verify with:
Bugfixes¶
- Add
py.typedto the generated proto directory so type checkers pick up protobuf types correctly.
Full Changelog¶
- Add data import API to sift_client
- Add HDF5 client-side detect_config
- Add TDMS client-side detect_config
- Add optional logging of test result create and update events
- Add progress indicators for job polling and file downloads
- Refactor files using run_in_executor
- Add Parquet as an export output format
- Add py.typed file to proto dir
- Remove vendored google.api and protoc_gen_openapiv2 modules from buf
- Remove protovalidate from client-side protos
[v0.13.0] - March 24, 2026¶
What's New¶
SiftClient Export API¶
The sift_client module now exposes a public export interface and wrapper, making it easier to integrate with downstream tools and workflows.
Module Migration to sift_client¶
The grpc, rest, and time modules have been migrated from sift_py to sift_client. All imports from sift_py for these modules have been removed. This continues the transition toward sift_client as the primary interface ahead of the v1.0.0 release.
Arbitrary File Attachment Uploads¶
File attachment uploads now support arbitrary file types (e.g., Parquet, pcapng) by falling back to application/octet-stream for unknown MIME types.
Improved SiftChannelConfig URL Handling¶
SiftChannelConfig now handles URLs more robustly, fixing gRPC connection errors caused by missing URL schemes.
Streaming Ingestion Performance¶
The streaming ingestion module has been refactored to use sift_stream_bindings types for improved performance.
Full Changelog¶
- Add export API for SiftClient
- Port grpc, rest, and time modules into sift_client
- Fall back to application/octet-stream for unknown MIME types
- Parquet file attachment support
- Fix gRPC connection errors with missing scheme
- Migrate to sift_stream_bindings types for improved ingestion performance
- Add Python streaming ingestion tutorial example
- Update pandas and pandas-stubs version bounds
- Pin doc build versions
[v0.12.0] - February 26, 2026¶
What's New¶
Channel Archiving/Unarchiving Support in SiftClient¶
SiftClient has been updated with the ability to archive and unarchive channels, allowing users to cleanup old channel definitions that are no longer desired.
Full Changelog¶
[v0.11.0] - February 24, 2026¶
What's New¶
Better Rule and Report Support in SiftClient¶
SiftClient has been updated to provide better support for RuleVersions and better ergonomics for Reports
- Additional methods have been added to the SiftClient Rules resource to allow easy access to RuleVersions
- Reports can now be generated from RuleVersions
- The Jobs and Reports resources now have a wait_until_complete sync/async method, which provides a convenient way to wait until a job or report is complete, with configurable polling and timeout settings.
- When creating reports, SiftClient now returns a Job object, to signify that the report is still in progress. Waiting on the job with wait_until_complete will return the completed report once finished. The report_id can also still be accessed directly from the job object if needed.
Full Changelog¶
- Add more support for rule versions to SiftClient
- Add better report creation and wait_until_complete to SiftClient
[v0.10.1] - February 11, 2026¶
What's New¶
- Add pagination for list ingestion config/flows
- Increase page size, fix list ingestion config API
- Regenerate remote file upload proto to include TestSteps
[v0.10.0] - January 30, 2026¶
DEPRECATION NOTICE¶
The sift_py module is deprecated as of v0.10.0 and will be removed in v1.0.0.
Please use sift_client for all new development. Several minor releases will follow
before the major release to add features and give users time to migrate.
What's New¶
- Stabilizes the sift_client module.
- Add FileAttachmentsMixin to TestStep
- Add support for live rule field in sift-client
- Adds batch rule update/create support to sift_client
- Sift client jobs resource
[v0.9.6] - December 22, 2025¶
[v0.9.5] - December 12, 2025¶
[v0.9.4] - December 10, 2025¶
[v0.9.3] - December 4, 2025¶
[v0.9.2] - December 3, 2025¶
- Set Content-Disposition on data imports
- Show validation errors in External Rules
- Use faster yaml loading for telemtery configs
[v0.9.1] - August 18, 2025¶
[v0.9.0] - September 15, 2025¶
What's New¶
Parquet Upload Service¶
Adds support for uploading Parquet files to Sift for ingestion through the addition of the ParquetUploadService. See examples/data_import/parquet for an example of how to upload Parquet files.
TDMS File Metadata Support¶
Adds a flag to automatically import TDMS file metdadata into Sift Runs.
Lazy Flow Creation¶
Adds the ability to create flows lazily which reduces initialization time of the IngestService.
Full Changelog¶
- Lazy flow creation
- Adds support for bytes
- Add ParquetUploadService
- Import TDMS file metadata to Sift Run
[v0.8.4] - August 18, 2025¶
[v0.8.3] - August 11, 2025¶
[v0.8.2] - August 1, 2025¶
[v0.8.1] - July 31, 2025¶
- Catch PermissionError when removing temp files
- Add support for start and end times to rule evaluation
[v0.8.0] - July 29, 2025¶
What's New¶
HDF5 Upload Service¶
Adds support for uploading HDF5 files to Sift for ingestion through the addition of the Hdf5UploadService and Hdf5Config. See examples/data_import/hdf5 for an example of how to upload HDF5 files.
Full Changelog¶
- Add HDF5 upload service
- Fixes bug when updating rules where the rule_id is not always passed to Sift
[v0.7.0] - June 24, 2025¶
What's New¶
AssetService and Metadata Support¶
Support for attaching metadata to a run or asset has been introduced through the addition of the AssetService and updates to the attach_run method of the IngestionService. See examples/assets for an example of updating metadata for an asset.
Full Changelog¶
- Make ingestion client key optional
- Add support for metadata definition through the python client
- Add public method for creating new run
[v0.6.2] - June 9, 2025¶
[v0.6.1] - June 2, 2025¶
[v0.6.0] - May 23, 2025¶
What's New¶
Rule Evaluation Service¶
The RuleEvaluationService has been introduced to provide functionality for evaluating rules and reports against runs or assets. This includes support for dry-run evaluations and previews, as well as evaluating rules defined in YAML configurations.
External Rules¶
The RuleService has been updated to support External Rules. External Rules are ideal for automated workflows such as CI/CD pipelines, where external Rules are managed and evaluated programmatically.
Read more about External Rules here here.
Full Changelog¶
- Add support for rule evaluation service and external rules
- Allow creating multiple new flows at the same time
[v0.5.3] - May 20, 2025¶
- Contextual channels support
- Updates for user defined functions and external rules
- Fix bugs within the IngestService
[v0.5.2] - April 23, 2025¶
[v0.5.1] - April 18, 2025¶
[v0.5.0] - March 31, 2025¶
What's New¶
Rosbag2 Uploads¶
A new RosbagsUploadService has been added to sift_py.data_import.rosbags, which provides the functionality for uploading
rosbags, including video frames from a rosbag. See examples/data_import/rosbags for examples on how to use the new service.
Full Changelog¶
[v0.4.2] - March 4, 2025¶
[v0.4.1] - February 27, 2025¶
[v0.4.0] - February 21, 2025¶
What's New¶
Calculated Channels Service¶
A new CalculatedChannelService, in sift_py.calculated_channels.service, provides functionality for creating, updating,
searching, and listing Calculated Channels. It also provides a YAML interface for defining Calculated Channels using a
YAML file. Examples can be found in examples/calculated_channels.
Deprecation Notices¶
Channel Component field is being deprecated and will be removed in version 1.0.0. Please see https://docs.siftstack.com/docs/glossary#component¶
for more information.
Full Changelog¶
- Added automatic retry for REST API calls
- Deprecated
componentfield for Channels - Fix for
from_annotation_typeinRuleConfig - Added support for Calculated Channels
- Remove previously deprecated
overwrite_rulesargument from ingestion service - Fix
ReportTemplateupdating - Remove breakpoint from internal function
[v0.3.3] - January 24, 2025¶
[v0.3.2] - January 17, 2025¶
[v0.3.1] - January 8, 2025¶
- Moves npTDMS dependency to optional
Specify
sift-stack-py[tdms]in your project dependencies if you need to use the TDMS upload service.
[v0.3.0] - January 7, 2025¶
What's New¶
Report Template Service¶
There is now a ReportTemplateService that is available from sift_py.report_templates.service. This service allows for creating,
updating, and retrieving report templates with a client key, where client keys are unique user defined strings associated with a given report
template. Report templates may either by defined via a python config (using the ReportTemplateConfig class) or a YAML file, which
follows the same schema.
Please see examples/report_templates for some examples of how to manage report templates via python and YAML using the ReportTemplateService.
- Adds ReportTemplateService and example usage
Rule Service¶
There is now a RuleService that is available from sift_py.rule.service. This service allows for creating, updating, and retrieving rules.
Rules created through this service should now include a rule client key, a unique user defined string associated with a given rule. As before,
rules may be defined in python (via the RuleConfigClass) or via YAML (following the RuleYamlSpec class).
Please see examples/ingestion_with_yaml_config for an example of how to create rules from YAML using the RuleService.
- Updates most examples to use RuleService to create rules
Chapter 10 Support¶
There is now a base class, BaseCh10File, and an upload service, Ch10UploadService that allows for easily parsing and uploading IRIG Chapter 10 files.
Please see examples/data_import/ch10 for an example of how to use these classes.
- Allow uploading compressed chapter 10 files
Other Improvements¶
- Enables keepalive by default and configures user-agent
- Ping Example added
- Adds optional openSSL certificate fetching
Bugfixes¶
Deprecation Notices¶
The overwrite_rules option of IngestionService is going to be deprecated in the next release, and will emit a warning if set.
Full Changelog¶
- Updates most examples to use RuleService to create rules
- Adds ReportTemplateService and example usage
- Enables keepalive by default and configures user-agent
- Fix status bug with CSV uploads
- Support Windows mimetypes for CSV uploads
- Allow uploading compressed chapter 10 files
- Fix data download bug for channels with '.' delimited name.
- Improvements to CSV importing
- Ping Example added
- Adds optional openSSL certificate fetching
- Loosen various dependency requirements
[v0.2.2] - October 9, 2024¶
Summary of changes: - Loosen various dependency requirements
[v0.2.1] - September 26, 2024¶
Summary of changes: - add missing double channel computation - update protos - update protos - csv example ingestion - update example - Add multiple CSV ingestion example - Add metadata arg to use_sift_channel
[v0.2.0] - September 3, 2024¶
Summary of changes: - Add pydantic models for the sift grafana plugin queries - Lowered minimum version requirements for various dependencies and dependency cleanup - Added py.typed to modules that utilize type annotations - Added services derived from protobuf for the following APIs * Remote files * Rule versions * Saved searches - Added helpful validations when processing Sift API URLs - Added support for gRPC keep-alive - Added service to upload and download file attachments - Added timer-based flushing for buffered ingestion and custom error-handling - Added functionality to generate a channel value from a channel config
The following section will cover some of the more notable features in depth. Please refer to the documentation for even more detail.
Table of Contents¶
- Keepalive
- File attachments
- Timer based flushing for buffered ingestion
- Custom error handling for buffered ingestion
Keepalive¶
Long-lived connections are generally are at risk of being closed due to idle timeouts if they are idle for a particular duration. In other words, if there is no data being exchanged on the connection for a duration specified by a load balancer's idle timeout, the connection will be closed. In order to ensure that this does not happen you can opt into enabling the HTTP/2 PING-based keepalive mechanism either on your own gRPC channel or the one that Sift provides. To configure keep-alive using the Sift-provided gRPC channel:
from sift_py.grpc.transport import use_sift_channel, SiftChannelConfig
sift_channel_config = SiftChannelConfig(
uri=uri,
apikey=apikey,
enable_keepalive=True,
)
with use_sift_channel(sift_channel_config) as channel:
...
This uses default values set in the sift_py.grpc.keepalive module. If you'd like to configure your own keepalive parameters you could
also do the following:
from sift_py.grpc.transport import use_sift_channel, SiftChannelConfig
from sift_py.grpc.keepalive import KeepaliveConfig
sift_channel_config = SiftChannelConfig(
uri=uri,
apikey=apikey,
enable_keepalive=KeepaliveConfig(
keepalive_time_ms=keepalive_time_ms,
keepalive_timeout_ms=keepalive_timeout_ms,
keepalive_permit_without_calls=keepalive_permit_without_calls,
max_pings_without_data=max_pings_without_data,
),
)
with use_sift_channel(sift_channel_config) as channel:
...
File attachments¶
There is now a sift_py.file_attachment module that contains utilities to programmatically upload and download file attachments.
Files currently can be attached to various entities such as runs, annotations, and annotation logs. Various video and image formats
are currently supported. Below is an example demonstrating how to upload a file attachment and programmatically downloading it.
from sift_py.grpc.transport import SiftChannelConfig, use_sift_channel
from sift_py.file_attachment.service import FileAttachmentService
from sift_py.file_attachment.entity import Entity, EntityType
from sift_py.file_attachment.metadata import VideoMetadata
from sift_py.rest import SiftRestConfig
from sift.remote_files.v1.remote_files_pb2 import GetRemoteFileRequest
from sift.remote_files.v1.remote_files_pb2_grpc import RemoteFileServiceStub
# Get API credentials setup
...
with use_sift_channel(sift_channel_config) as channel:
file_attachment_service = FileAttachmentService(channel, rest_config)
run = entity=Entity(
entity_id=run_id, # some arbitrary run ID that refers to an existing run
entity_type=EntityType.RUN,
)
# uploading the file attachment and attaching it to a run of `run_id`
remote_file = file_attachment_service.upload_attachment(
path="path/to/foo.mp4",
entity=run,
# Metatadata.. optional but recommended for optimal viewing in the application
metadata=VideoMetadata(height=2160, width=3840, duration_seconds=5.5),
description="thrusters getting too hot" ,
)
# retrieving all of the file attachments for our run
all_file_attachments = file_attachment_service.retrieve_attachments(run)
# downloading our file_attachment and saving it to our current working dir
file_attachment_service.download_attachment(remote_file)
# downloading our file_attachment and saving it somewhere else with a different name
file_attachment_service.download_attachment(remote_file, "somewhere/else/foo.mp4")
# deleting out file attachment from Sift
file_attachment_service.delete_file_attachments(remote_file_1, remote_file_2, remote_file_etc)
Timer based flushing for buffered ingestion¶
Previously the buffered ingestion API flushes a buffer whenever any one
of the following conditions are met:
- The caller manually calls flush
- The buffer gets filled
- The with-block associated with the buffered ingestion service as a context-manager goes out of scope.
- An exception is raised inside of the aforementioned with-block.
Note that the last two points only apply if the buffered ingestion service is used as a context manager like so:
Now there is support to periodically flush the buffer regardless of whether or not the buffer is filled. To configure a timer to flush periodically:
This will configure the buffered ingestion service to flush its buffer every 3.2 seconds. If the buffer happens to be filled before the timer elapses,
then the timer will reset.
Custom error handling for buffered ingestion¶
Previously when using the buffered ingestion service as a context manager, any exception that gets raised within the with-block will result
in the service attempting to flush one more time. The caller can now customize error-handling behavior by passing in a function handler that takes
in three arguments: the error that's raised, the buffer containing the remaining requests that weren't ingested, and a function that when called will
attempt to flush the buffer.
# Custom code to run when error
def on_error_calback(err, buffer, flush):
# Maybe try to save contents of buffer to disk
...
# Try once more to flush the buffer
flush()
with ingestion_service.buffered_ingestion(on_error=on_error_calback) as buffered_ingestion:
...
[v0.1.1] - July 17, 2024¶
Summary of changes: - Extend support for Python 3.8
[v0.1.0] - July 12, 2024¶
Summary of changes:
- Promote to v0.1.0 from release candidate state.
- Add module to download telemetry
In addition to the changes above, documentation is now also available online: - Online documentation - Section on how to download telemetry
[v0.1.0-rc.3] - July 3, 2024¶
Summary of changes: - Automated gRPC retries if an unexpected gRPC status code is returned and if a connection unexpectedly terminates
The following are some gRPC error codes that can happen due to external factors that Sift doesn't directly control:
- UNKNOWN
- UNAVAILABLE
- ABORTED
- DEADLINE_EXCEEDED
They are the source of common disruptions, particularly during ingestion, and so this mechanism will automatically retry failed RPCs over an existing connection or will establish a new one if necessary.
[v0.1.0-rc.2] - July 1, 2024¶
Summary of changes: - Introduced automated ingestion request buffering to improve performance - Added support for multi-config ingestion and creating new flows at run-time - Added methods that combine request creation and ingestion into a single-step. - Updates to documentation.
For in-depth documentation please see the documentation section of the README for instructions on how to build the documentation locally.
Table of Contents¶
- Combining Request Generation and Ingestion into a Single Step
- Request Buffering
- Creating New Flows on the Fly
- Multi-config Ingestion
Combining Request Generation and Ingestion into a Single Step¶
Previously to ingest a single request you needed to do the following:
request = ingestion_service.try_create_ingestion_request(
flow_name="logs",
timestamp=timestamp,
channel_values=[
{
"channel_name": "log",
"value": string_value("some log"),
},
],
)
ingestion_service.ingest(request)
Now you can combine both steps using either of the following APIs: - try_ingest_flows - ingest_flows
ingestion_service.try_ingest_flows({
"flow_name": "log",
"timestamp": timestamp,
"channel_values": [
{
"channel_name": "log",
"value": string_value("some string")
},
],
})
You can also send multiple flows:
# Send data for both logs and readings flows
ingestion_service.try_ingest_flows(
{
"flow_name": "readings",
"timestamp": datetime.now(timezone.utc),
"channel_values": [
{
"channel_name": "velocity",
"component": "mainmotor",
"value": double_value(10),
},
{
"channel_name": "voltage",
"value": int32_value(5),
},
{
"channel_name": "vehicle_state",
"value": enum_value(2),
},
],
},
{
"flow_name": "logs",
"timestamp": datetime.now(timezone.utc),
"channel_values": [
{
"channel_name": "logs",
"value": string_value("INFO: some message")
},
],
},
)
Request Buffering¶
Requests are now automatically buffered using the buffered ingestion API. Using this may significantly improve performance as it allows serialization and ingestion to occur in batches:
# Defaults to a buffer size of `sift_py.ingestion.buffer.DEFAULT_BUFFER_SIZE` requests.
with ingestion_service.buffered_ingestion() as buffered_ingestion:
buffered_ingestion.try_ingest_flows(*lots_of_flows)
buffered_ingestion.try_ingest_flows(*lots_more_flows)
# Custom buffer size of 750 requests
with ingestion_service.buffered_ingestion(750) as buffered_ingestion:
buffered_ingestion.try_ingest_flows(*lots_of_flows)
buffered_ingestion.try_ingest_flows(*lots_more_flows)
Once the with-block ends the remaining requests will be automatically flushed and ingested, but flushing may also be done manually:
with ingestion_service.buffered_ingestion() as buffered_ingestion:
buffered_ingestion.try_ingest_flows(*lots_of_flows)
buffered_ingestion.flush()
Contrast this with regular ingestion:
ingestion_service.try_ingest_flows(*lots_of_flows)
ingestion_service.try_ingest_flows(*lots_more_flows)
Creating New Flows on the Fly¶
If there is a flow you need to create on the fly which wasn't declared in your initial telemetry config,
you may use either of the following APIs:
- try_create_flow
- create_flow
new_flow_config = FlowConfig(
name="my_new_flow", channels=[ChannelConfig("new_channel", ChannelDataType.DOUBLE)]
)
ingestion_service.try_create_flow(new_flow_config)
Multi-config Ingestion¶
There is now an ergonomic utility class, IngestionServicesManager, that allows users to manage telemetry for multiple configs: