Ranges

Categorize your data with ranges.

Ranges identify specific periods of time with a name. We can use the Python client to read from, write to, and attach metadata to these ranges.

Creating Ranges

To create a range, we can use the client.ranges.create method:

import synnax as sy
from datetime import datetime

time_format = "%Y-%m-%d %H:%M:%S"

my_range = client.ranges.create(
    # This name does not need to be unique, but it's a good idea to 
    # pick something that will be easy to identify later.
    name="My Range",
    time_range=sy.TimeRange(
        start = datetime.strptime("2023-2-12 12:30:00", time_format),
        end = datetime.strptime("2023-2-12 14:30:00", time_format),
    ),
)

Synnax will automatically generate a unique identifier for the range.

Only Create a Range if it Doesn’t Exist

If we only want to create a range if one with the same name doesn’t already exist, we can pass in the retrieve_if_name_exists parameter:

my_range = client.ranges.create(
    name="My Range",
    time_range=sy.TimeRange(
        start = datetime.strptime("2023-2-12 12:30:00", time_format),
        end = datetime.strptime("2023-2-12 14:30:00", time_format),
    ),
    retrieve_if_name_exists=True,
)

In the event the range already exists, Synnax will return the existing range instead of creating a new one.

Retrieving Ranges

We can fetch a range using the client.ranges.retrieve method.

Retrieving a Single Range

We can retrieve a range by its name or key:

# By name
my_range = client.ranges.retrieve("My Range")

# By key
my_range = client.ranges.retrieve(my_range.key)

Synnax will raise a NotFoundError if the range does not exist, and a MultipleFoundError if multiple ranges with the given name exist. If you’d like to accept multiple or no results, provide a list to the retrieve method as shown below.

Retrieving Multiple Ranges

We can retrieve multiple ranges by passing a list of names or keys to the retrieve method:

# By name
my_ranges = client.ranges.retrieve(["My Range", "My Other Range"])

# By key
my_ranges = client.ranges.retrieve([my_range.key, my_other_range.key])

# This won't work!
my_ranges = client.ranges.retrieve(["My Range", my_other_range.key])

In these examples, Synnax will not raise an error if a range cannot be found. Instead, the missing range will be omitted from the returned list.

Working with Channels

Accessing Channels

We can access the channels on a range as if they were class properties or dictionary keys:

my_range = client.ranges.retrieve("My Range")

# Using a property accessor
my_pressure_channel = my_range.pressure_2
# Using a dictionary accessor
my_pressure_channel = my_range["pressure_2"]

Accessing Multiple Channels

We can also access multiple channels on the range by passing a regular expression to our property accessor:

my_range = client.ranges.retrieve("My Range")

# Returns an iterable object containing matching channels
my_pressure_channels = my_range["^pressure"]

If we try to access channel-specific methods on the returned object, such as a name or data, Synnax will raise MultipleFoundError. Instead, we should iterate over the returned list. Here’s a simple example where we plot the data from all of our pressure channels:

import matplotlib.pyplot as plt

for ch in my_range["^pressure"]:
    plt.plot(my_range.timestamps, ch, label=ch.name)

This iteration pattern is valid even if we only have one channel that matches our regular expression.

Aliasing Channels

Channels must maintain their original names, but situations arise where we’d like to give a channel a more descriptive name in the context of a particular range. Ranges allow us to do just that.

Imagine we have a channel named daq_analog_input_1 that we’d like to refer to as tank_pressure for a tank burst test. We can do this by aliasing the channel:

burst_test = client.ranges.retrieve("Oct 10 Burst Test")

# Set our alias
burst_test.daq_analog_input_1.set_alias("tank_pressure")

# We can also set an alias like this
burst_test.set_alias("daq_analog_input_1", "tank_pressure")

We can now access the channel using its alias:

burst_test.tank_pressure

Subsequent calls to set_alias will overwrite the previous alias.

Aliases are only valid within the context of a particular range. If you try to access an aliased channel outside of the range, Synnax will not be able to find it.

Attaching Metadata

Setting Metadata

It’s common to have non-data information we’d like to attach to a particular range, such as test configuration parameters, numeric results, part numbers, etc. We can attach this metadata to a range using the meta_data property:

burst_test = client.ranges.retrieve("Oct 10 Burst Test")

# Set a single key/value pair
burst_test.meta_data.set("part_number", "12345")

# Another way to set a single key/value pair
burst_test.meta_data["part_number"] = "12345"

# Set multiple key/value pairs
burst_test.meta_data.set({
    "part_number": "12345",
    "test_configuration": "Test 1",
    "test_result": "123.45",
})

All metadata values are stored as strings. It’s up to you to correctly cast the values to the appropriate type.

Getting Metadata

Getting metadata is as easy as setting it:

burst_test = client.ranges.retrieve("Oct 10 Burst Test")

# Retrieve a single key
part_number = burst_test.meta_data.get("part_number")

# Another way to retrieve a single key
part_number = burst_test.meta_data["part_number"]

Deleting Metadata

We can delete metadata using the delete method:

burst_test = client.ranges.retrieve("Oct 10 Burst Test")

# Delete a single key
burst_test.meta_data.delete("part_number")

# Another way to delete a single key
del burst_test.meta_data["part_number"]

# Delete multiple keys
burst_test.meta_data.delete(["part_number", "test_configuration"])

Deleting Ranges

Deleting a range is as simple as passing in its name or key to the delete method:

client.ranges.delete("My Range")
client.ranges.delete(my_range.key)

Deleting a range by name will delete all ranges with that name. Be careful!

We can delete multiple ranges by passing a list of names or keys to the delete method:

client.ranges.delete(["My Range", "My Other Range"])
client.ranges.delete([my_range.key, my_other_range.key])