Sift Client Basic Usage¶
This notebook demonstrates the core features of the Sift Python client:
- Initializing the Sift client
- Finding, creating, and updating resources
- Searching channels
- Pulling data
- Creating calculated channels
Running this notebook¶
This notebook is written in Jupyter Notebook format and can be run in any Jupyter environment.
Some additional package prerequisites are required to run this notebook
notebookfor running Jupyter Notebookspython-dotenvfor loading environment variablesrichfor pretty-printing outputpandasfor data manipulation and analysismatplotlibfor data visualization
You can install these packages using pip install notebook rich python-dotenv pandas matplotlib.
Setup and Initialization¶
First, import the necessary modules and initialize the Sift client with your credentials.
Best practice is to access credentials using environment variables or a .env file with python-dotenv. Avoid hardcoding your API in any code you write.
import os
from datetime import datetime, timedelta
from dotenv import load_dotenv
from rich import print
from sift_client import SiftClient
# Get our environment variables
load_dotenv() # Load environment variables from .env file
api_key = os.getenv("SIFT_API_KEY")
grpc_url = os.getenv("SIFT_GRPC_URI")
rest_url = os.getenv("SIFT_REST_URI")
client = SiftClient(api_key=api_key, grpc_url=grpc_url, rest_url=rest_url)
print("✓ Sift client initialized successfully")
✓ Sift client initialized successfully
Sift Resources¶
Sift objects, such as Assets, Runs, etc. are all accessed via their API resources.
The SiftClient class provides these resources as properties
assetsruns- etc.
Asynchronous versions are also available by accessing the async_ property of the client. For example
client.async_.assetsclient.async_.runs- etc.
For example, the Ping resource can be used for a basic health check.
client.ping.ping()
'Hello from Sift!'
Assets and Runs¶
Assets represent physical or logical entities in your system (e.g., vehicles, machines, devices). Runs represent time-bounded operational periods for an asset (e.g., a flight, a test, a mission).
Resources generally offer similar interaction patterns and methods. For example, the AssetsAPI has
getlist_findupdatearchiveunarchive
Other resources may offer additional methods such as create.
These resource methods operate on and will return Sift object types. More on tehse can be found here: sift_types
Listing, Finding, and Getting¶
list_ can be used to retrieve objects that match a specific set of criteria:
# List all assets (limited to 10 for this example)
assets = client.assets.list_(name_contains="Mars", limit=5)
for asset in assets:
print(f"Name: {asset.name}, ID: {asset.id_}")
Name: MarsRoverIngestPusher, ID: 429b864b-0911-4e23-b9d1-2a1fba4d441c
Name: Mars Rover [Jonno export test], ID: 5d86ed46-dec6-41c8-8680-a0ba02d9e546
Name: MarsRover_pb4, ID: 9bb3bfec-840d-40b7-a5ab-25591f9a1b38
Name: MarsRover, ID: 611914d3-ffb1-402e-ae1e-5eb3e66dea7c
Name: MarsRover42NaN, ID: 0ad88099-1aea-461c-91b5-0a91c7811f74
find can be used to find a single matching object. It will return an error if multiple are found. It takes the same arguments and filters as list_.
# Find a specific asset by name
asset_name = "MarsRover0"
asset = client.assets.find(name=asset_name)
print(asset)
Asset( id_='61d6e4f0-8287-4678-b071-18a95fcd9db6', name='MarsRover0', organization_id='dd9f82ef-7805-4b02-9572-ec61b71edde6', created_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), created_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d', modified_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), modified_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d', tags=['simulator', 'speed-test'], metadata={'vehicle_type': 'rover', 'vehicle_version': 123123123.0, 'version_active': True}, is_archived=False, archived_date=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc) )
When we know exactly what we are looking for, we can use get.
# Get the exact asset by ID
asset = client.assets.get(asset_id=asset.id_)
print(asset)
Asset( id_='61d6e4f0-8287-4678-b071-18a95fcd9db6', name='MarsRover0', organization_id='dd9f82ef-7805-4b02-9572-ec61b71edde6', created_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), created_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d', modified_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), modified_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d', tags=['simulator', 'speed-test'], metadata={'vehicle_type': 'rover', 'vehicle_version': 123123123.0, 'version_active': True}, is_archived=False, archived_date=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc) )
Creating, Updating, and Archiving¶
Most resources offer create, update, and archive methods.
Since create returns a Run, we can chain update and archive on it and the Sift object will update in-place.
# Run creation
run = client.runs.create(
dict(
name="Test Run", description="A test run", asset_ids=[asset.id_], start_time=datetime.now()
)
)
print(run)
Run( id_='523c6f2a-181c-4ee7-8876-421901daf97f', name='Test Run', description='A test run', created_date=datetime.datetime(2025, 10, 10, 21, 43, 39, 913134, tzinfo=datetime.timezone.utc), modified_date=datetime.datetime(2025, 10, 10, 21, 43, 39, 915093, tzinfo=datetime.timezone.utc), created_by_user_id='ae1f6c7c-3e93-40a9-8796-d227a725662c', modified_by_user_id='ae1f6c7c-3e93-40a9-8796-d227a725662c', organization_id='dd9f82ef-7805-4b02-9572-ec61b71edde6', metadata={}, tags=[], asset_ids=[], is_adhoc=False, is_archived=False, start_time=datetime.datetime(2025, 10, 10, 14, 43, 39, 861550, tzinfo=datetime.timezone.utc), stop_time=None, duration=datetime.timedelta(seconds=25200, microseconds=54929), default_report_id='854787b6-4e38-4c81-bdf0-8d143c791d01', client_key=None, archived_date=None )
# Run update
run.update(
dict(
name="Updated Test Run",
description="An updated test run",
)
)
print(run)
Run( id_='523c6f2a-181c-4ee7-8876-421901daf97f', name='Updated Test Run', description='An updated test run', created_date=datetime.datetime(2025, 10, 10, 21, 43, 39, 913134, tzinfo=datetime.timezone.utc), modified_date=datetime.datetime(2025, 10, 10, 21, 43, 39, 981793, tzinfo=datetime.timezone.utc), created_by_user_id='ae1f6c7c-3e93-40a9-8796-d227a725662c', modified_by_user_id='ae1f6c7c-3e93-40a9-8796-d227a725662c', organization_id='dd9f82ef-7805-4b02-9572-ec61b71edde6', metadata={}, tags=[], asset_ids=[], is_adhoc=False, is_archived=False, start_time=datetime.datetime(2025, 10, 10, 14, 43, 39, 861550, tzinfo=datetime.timezone.utc), stop_time=None, duration=datetime.timedelta(seconds=25200, microseconds=123957), default_report_id='854787b6-4e38-4c81-bdf0-8d143c791d01', client_key=None, archived_date=None )
# Run archive
run.archive()
print(run)
Run( id_='523c6f2a-181c-4ee7-8876-421901daf97f', name='Updated Test Run', description='An updated test run', created_date=datetime.datetime(2025, 10, 10, 21, 43, 39, 913134, tzinfo=datetime.timezone.utc), modified_date=datetime.datetime(2025, 10, 10, 21, 43, 40, 52001, tzinfo=datetime.timezone.utc), created_by_user_id='ae1f6c7c-3e93-40a9-8796-d227a725662c', modified_by_user_id='ae1f6c7c-3e93-40a9-8796-d227a725662c', organization_id='dd9f82ef-7805-4b02-9572-ec61b71edde6', metadata={}, tags=[], asset_ids=[], is_adhoc=False, is_archived=True, start_time=datetime.datetime(2025, 10, 10, 14, 43, 39, 861550, tzinfo=datetime.timezone.utc), stop_time=None, duration=datetime.timedelta(seconds=25200, microseconds=196350), default_report_id='854787b6-4e38-4c81-bdf0-8d143c791d01', client_key=None, archived_date=datetime.datetime(2025, 10, 10, 21, 43, 40, 48186, tzinfo=datetime.timezone.utc) )
Searching Channels¶
Channels represent time-series data streams (e.g., sensor readings, telemetry).
# List channels for the selected asset
channels = client.channels.list_(asset=asset.id_, limit=5)
print(f"Found {len(channels)} channels for asset '{asset.name}':")
for channel in channels: # Show first 10
print(channel)
Found 5 channels for asset 'MarsRover0':
Channel( id_='0f1a32d4-0f97-494f-82f8-2ed7f3983e74', name='is_even', data_type=<ChannelDataType.BOOL: 5>, description='is the value of milliseconds even', unit='0c359676-c6c9-47d3-acca-d8e938311b2a', bit_field_elements=[], enum_types={}, asset_id='61d6e4f0-8287-4678-b071-18a95fcd9db6', created_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), modified_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), created_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d', modified_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d' )
Channel( id_='1c2dd815-2c9e-4801-92dc-1f65cad2114b', name='voltage', data_type=<ChannelDataType.INT_32: 7>, description='voltage at the source', unit='09f1005b-df6a-4f82-838f-c57e77c587ef', bit_field_elements=[], enum_types={}, asset_id='61d6e4f0-8287-4678-b071-18a95fcd9db6', created_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), modified_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), created_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d', modified_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d' )
Channel( id_='606817a0-396e-4575-9ffc-45a4ef9ca66a', name='log', data_type=<ChannelDataType.STRING: 2>, description='this simulates log files', unit='0c359676-c6c9-47d3-acca-d8e938311b2a', bit_field_elements=[], enum_types={}, asset_id='61d6e4f0-8287-4678-b071-18a95fcd9db6', created_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), modified_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), created_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d', modified_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d' )
Channel( id_='a59c4ec4-5e8c-4457-8a41-ecbc06b0dca7', name='mainmotor.velocity', data_type=<ChannelDataType.DOUBLE: 1>, description='speed', unit='b840c8e8-33fb-433c-9448-07577f04e990', bit_field_elements=[], enum_types={}, asset_id='61d6e4f0-8287-4678-b071-18a95fcd9db6', created_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), modified_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), created_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d', modified_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d' )
Channel( id_='c052cef4-d3eb-486e-b3cb-63b59504e913', name='gpio', data_type=<ChannelDataType.BIT_FIELD: 4>, description='on/off values for pins on gpio', unit='0c359676-c6c9-47d3-acca-d8e938311b2a', bit_field_elements=[ {'name': '12v', 'index': 0, 'bit_count': 1}, {'name': 'charge', 'index': 1, 'bit_count': 2}, {'name': 'led', 'index': 3, 'bit_count': 4}, {'name': 'heater', 'index': 7, 'bit_count': 1} ], enum_types={}, asset_id='61d6e4f0-8287-4678-b071-18a95fcd9db6', created_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), modified_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), created_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d', modified_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d' )
# Search for specific channels by name pattern
# Replace with a pattern that matches your channel names
velocity_channels = client.channels.list_(asset=asset.id_, name_contains="velocity", limit=10)
print(f"Channels containing 'velocity': {len(velocity_channels)}")
for ch in velocity_channels:
print(ch)
Channels containing 'velocity': 1
Channel( id_='a59c4ec4-5e8c-4457-8a41-ecbc06b0dca7', name='mainmotor.velocity', data_type=<ChannelDataType.DOUBLE: 1>, description='speed', unit='b840c8e8-33fb-433c-9448-07577f04e990', bit_field_elements=[], enum_types={}, asset_id='61d6e4f0-8287-4678-b071-18a95fcd9db6', created_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), modified_date=datetime.datetime(2025, 3, 4, 19, 51, 15, 89746, tzinfo=datetime.timezone.utc), created_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d', modified_by_user_id='f47e4854-234b-421c-badb-7f8bb757cd9d' )
# Get channels for a specific run. There should be none since we just created it.
if run:
run_channels = client.channels.list_(run=run.id_, limit=10)
print(f"Channels in run '{run.name}': {len(run_channels)}")
for ch in run_channels:
print(f" - {ch.name}")
Channels in run 'Updated Test Run': 0
Pulling Data¶
Retrieve time-series data from channels as pandas DataFrames.
run = client.runs.list_(
duration_greater_than=timedelta(seconds=30),
description_contains="simulated run: 1 flows, 8 total channels, 5hz sampling rate",
)[0]
# Get data as a dictionary of pandas DataFrames
data = client.channels.get_data(
channels=client.channels.list_(run=run, name_contains="v"),
run=run,
limit=1000, # Limit to 1000 data points per channel
)
print(f"\n✓ Retrieved data for {len(data)} channels:")
for channel_name, df in data.items():
print(f"\n Channel: {channel_name}")
print(f" Data points: {len(df)}")
print(df.head())
✓ Retrieved data for 4 channels:
Channel: is_even
Data points: 1000
is_even 2025-06-04 17:21:13.180486958+00:00 True 2025-06-04 17:21:13.380486958+00:00 True 2025-06-04 17:21:13.580486958+00:00 True 2025-06-04 17:21:13.780486958+00:00 True 2025-06-04 17:21:13.980486958+00:00 True
Channel: mainmotor.velocity
Data points: 1000
mainmotor.velocity 2025-06-04 17:21:13.180486958+00:00 21.352492 2025-06-04 17:21:13.380486958+00:00 32.011998 2025-06-04 17:21:13.580486958+00:00 34.370449 2025-06-04 17:21:13.780486958+00:00 25.635827 2025-06-04 17:21:13.980486958+00:00 16.148490
Channel: vehicle_state
Data points: 1000
vehicle_state 2025-06-04 17:21:13.180486958+00:00 1 2025-06-04 17:21:13.380486958+00:00 1 2025-06-04 17:21:13.580486958+00:00 1 2025-06-04 17:21:13.780486958+00:00 1 2025-06-04 17:21:13.980486958+00:00 1
Channel: voltage
Data points: 1000
voltage 2025-06-04 17:21:13.180486958+00:00 12 2025-06-04 17:21:13.380486958+00:00 12 2025-06-04 17:21:13.580486958+00:00 12 2025-06-04 17:21:13.780486958+00:00 2 2025-06-04 17:21:13.980486958+00:00 2
Creating Calculated Channels¶
Calculated channels allow you to create derived metrics from existing channels using mathematical expressions.
# Create a calculated channel
# This example creates a channel that divides two existing channels
# Replace channel names with actual channels from your system
# Use first two channels for this example
channel1 = channels[0]
channel2 = channels[1]
calc_channel_name = f"{channel1.name}_per_{channel2.name}"
# Check if calculated channel already exists
existing = client.calculated_channels.find(
name=calc_channel_name, asset=asset.id_, include_archived=True
)
if existing:
print(f"Calculated channel '{calc_channel_name}' already exists")
calc_channel = existing
else:
print(f"Creating calculated channel: {calc_channel_name}")
calc_channel = client.calculated_channels.create(
dict(
name=calc_channel_name,
description=f"Ratio of {channel1.name} to {channel2.name}",
expression="$1 / $2", # $1 and $2 refer to the channel references below
expression_channel_references=[
dict(channel_reference="$1", channel_identifier=channel1.name),
dict(channel_reference="$2", channel_identifier=channel2.name),
],
asset_ids=[asset.id_],
)
)
print(calc_channel)
Calculated channel 'is_even_per_voltage' already exists
CalculatedChannel( id_='d283b5fe-fed5-4260-93d1-8bd65bec4bfe', name='is_even_per_voltage', description='Ratio of is_even to voltage', expression='$1 / $2', channel_references=[ {'channel_reference': '$1', 'channel_identifier': 'is_even'}, {'channel_reference': '$2', 'channel_identifier': 'voltage'} ], is_archived=True, units='', asset_ids=['61d6e4f0-8287-4678-b071-18a95fcd9db6'], tag_ids=[], all_assets=False, organization_id='dd9f82ef-7805-4b02-9572-ec61b71edde6', client_key='', archived_date=datetime.datetime(2025, 10, 10, 21, 36, 19, 947790, tzinfo=datetime.timezone.utc), version_id='0923ec64-9799-4e73-8038-71496227a846', version=1, change_message='Created calculated channel', user_notes='', created_date=datetime.datetime(2025, 10, 10, 21, 34, 3, 533939, tzinfo=datetime.timezone.utc), modified_date=datetime.datetime(2025, 10, 10, 21, 34, 3, 533939, tzinfo=datetime.timezone.utc), created_by_user_id='ae1f6c7c-3e93-40a9-8796-d227a725662c', modified_by_user_id='ae1f6c7c-3e93-40a9-8796-d227a725662c' )
# List all calculated channels for the asset
calc_channels = client.calculated_channels.list_(asset=asset.id_, name=calc_channel_name, limit=10)
print(f"Calculated channels for asset '{asset.name}': {len(calc_channels)}")
for cc in calc_channels:
print(f" - {cc.name}")
print(f" Expression: {cc.expression}")
print(f" Version: {cc.version}")
Calculated channels for asset 'MarsRover0': 1
- is_even_per_voltage
Expression: $1 / $2
Version: 1
# Optional: Clean up resources
# Uncomment to archive the created calculated channel and rule
if calc_channel:
calc_channel.archive()
print(f"Archived calculated channel: {calc_channel.name}")
print("\n✓ Example complete!")
Archived calculated channel: is_even_per_voltage
✓ Example complete!