Migration to the Profile API#
There are two recent API changes that work in tandem:
Instead of calling methods directly on the
nominalmodule (often imported asnm), it is now recommended to first create aNominalClient, and to call methods on the client.Instead of creating a client from token, Nominal recommends creating the client from a profile stored on disk, that encapsulates the root URL, token, and workspace.
Migrating existing credentials#
If you used nom to store credentials before profiles existed, migrate
your old configuration file (~/.nominal.yml) to the new format
(~/.config/nominal/config.yml).
You can do this with the following command:
nom config migrate
# Or, if `nom` is missing from your path:
python -m nominal.cli config migrate
Creating credentials from scratch#
See API keys setup.
Creating a client from a profile#
All operations will happen via the NominalClient interface. You
can construct a Client from profile or from token (see
API keys setup for more detail).
from nominal.core import NominalClient
client = NominalClient.from_profile("default")
Migration patterns#
Some of the search and listing functions from the old nominal
namespace are still available on the NominalClient, but most creation functions have a slightly different API.
Here are a few common patterns:
Uploading CSVs to a dataset#
Best practice is to upload multiple CSV of the same type into one
Dataset:
# Do this once
dataset = client.create_dataset(
name='engine',
description='Engine measurements from multiple flights'
)
# Repeat this for each CSV file of the same type
dataset.add_tabular_data(
path='my_data.csv',
timestamp_column='time',
timestamp_type='epoch_seconds'
)
See also Timestamp reference.
Uploading Pandas dataframes#
If you want to upload multiple dataframes into a single dataset, do so as follows:
dataset = client.create_dataset(
name='temperature',
description='Engine temperate measurements from multiple flights'
)
for df in my_dataframes:
with tempfile.NamedTemporaryFile(suffix=".parquet") as f:
df.to_parquet(f.name)
dataset.add_tabular_data(
f.name,
timestamp_column='source_time',
timestamp_type='iso_8601'
)
Note that, if your timestamp column is the index, but the index is unnamed, you should assign a name to it, e.g.:
df.index.name = 'source_time'
Creating runs from data#
Previously, it was possible to create a run directly from a CSV dataset. Our concept of a run has since evolved: whereas before a run was seen as a container of data, now it is seen as a time bounded view onto data.
Typically, a run would therefore specify the start and end time of an experiment (a flight, e.g.), and will narrow all data to that those time bounds.
However, if you want to replicate the ability to create a run from data, you can do so as follows:
Upload data
Get the data bounds
Create a run that matches those bounds and associate the dataset
# Wait until data is fully ingested, and time bounds have been computed
for f in dataset.list_files(successful_only=False):
f.poll_until_ingestion_completed()
dataset.refresh()
run = client.create_run(
name='Experimental run',
description='First experimental run of Q3',
start=dataset.bounds.start,
end=dataset.bounds.end
)
run.add_dataset("engine", dataset)
Uploading videos#
Video is stored as a channel on a Dataset, alongside your other test data. Create the Dataset, then add video files to a named channel on it:
dataset = client.create_dataset(name='Engine Cam 6')
dataset.add_video(
path='myfile.mp4',
channel='engine_cam_6',
start=dataframe['source_time'].min()
)
Note
Standalone Video objects are deprecated in favor of video channels. client.create_video(),
Video.add_file(), Asset.add_video(), Run.add_video() and the rest of the Video API emit a
LegacyVideoDeprecationWarning. Use Dataset.add_video()
and attach the backing Dataset instead.
Writing logs#
Logs are no longer separate objects, but are now entries added to an existing Datasets.
Consider, e.g., a log with lines formatted as:
2025-04-08T14:26:28.679052Z [INFO] Sent ACTUATE_MOTOR command
You can add matching log entries to your Dataset like this:
from nominal.core import LogPoint
def parse_logs_from_file(file_path):
with open(file_path, "r") as f:
for line in f:
timestamp, message = line.removesuffix("\n").split(maxsplit=1)
yield LogPoint.create(timestamp, message, None)
dataset = client.get_dataset("dataset_rid")
logs = parse_logs_from_file("logs.txt")
dataset.write_logs(logs)