--- myst: html_meta: description: "Write and upload Avro stream files, where each channel has its own timestamps and tags." --- # Avro streams {.lead} An Avro stream file holds records shaped like [streamed data](/guides/streaming/overview.md): each record names one channel, with its own timestamps, values and tags. Upload one with {py:meth}`~nominal.core.Dataset.add_avro_stream`. ```{include} /guides/_snippets/connect-the-client.md ``` ## When to use Avro streams Use Parquet when every channel shares one timestamp column. Use an Avro stream when they don't: | | Parquet | Avro stream | | --- | --- | --- | | Timestamps | One column for every channel | Per record, so channels can sample at different times | | Sparse channels | A cell for every channel in every row | Only the samples that exist | | Tags | One column per tag, the same for the whole file | A map on each record | | Layout | Columnar, compresses well | One record per channel batch | Because the records match the streaming format, a program that streams can also write them to disk and upload them later. ## Write a file `nominal_streaming`, installed with `nominal` on [supported platforms](/guides/install.md#extras), writes Avro streams with the same `enqueue` calls used to stream. Writing to a file needs no API key: ```python from datetime import datetime, timezone from pathlib import Path from nominal_streaming import NominalDatasetStream with NominalDatasetStream().to_file(Path("flight.avro")) as stream: stream.enqueue("altitude", datetime.now(timezone.utc), 1250.5, tags={"vehicle_id": "sn-001"}) stream.enqueue_batch("airspeed", [1_757_500_000_000_000_000, 1_757_500_100_000_000_000], [42.0, 42.5]) stream.enqueue_from_dict(1_757_500_200_000_000_000, {"altitude": 1252.0, "fault_code": "E07"}) ``` Batch timestamps are epoch nanoseconds. To keep data when a live stream loses its connection, pass `file_fallback` to {py:meth}`~nominal.core.Dataset.get_write_stream`. Batches that fail to send are written to that `.avro` file instead: ```python dataset = client.get_dataset("") with dataset.get_write_stream(implementation="rust", file_fallback="flight_backup.avro") as stream: stream.enqueue("altitude", datetime.now(timezone.utc), 1250.5) ``` `file_fallback` needs the Rust stream. The Python stream logs a warning and drops failed batches. ## Upload a file ```python dataset = client.create_dataset("Flight telemetry") file = dataset.add_avro_stream("flight.avro", tags={"vehicle_id": "sn-001"}) file.poll_until_ingestion_completed() ``` `timestamp_type` defaults to `"epoch_nanoseconds"`. It takes numeric types only: another epoch unit, or {py:class}`ts.Relative `. `tags` fill in keys that a record doesn't set. Tags in the records take precedence. {py:class}`~nominal.core.Asset` and {py:class}`~nominal.core.Run` also have `add_avro_stream`, which uploads to a dataset by data scope name or ref name. To include an Avro stream in a batch, see [Ingest jobs](/guides/ingest/ingestion-jobs.md). ## Value types Each value is a `double`, `long`, `string`, double array, string array or JSON struct. - Every value for one channel and tag set must use the same type. Mixing them fails ingestion. - The type sets the channel's data type. A `long` value for an existing `DOUBLE` channel creates a second series. [Register channels](/guides/datasets/channels.md) to avoid this. - Booleans, datetimes and decimals have no type. Convert them first, for example a boolean to `double` or `string`. - Integer arrays become double arrays. - `NaN` and infinity are valid `double` values, and `NaN` is the usual way to mark a missing sample. JSON structs can't hold them, so replace them with `null` first. ## Schema Writers in other languages need this schema. The order of the types in the `values` union must match: ```json { "type": "record", "name": "AvroStream", "namespace": "io.nominal.ingest", "fields": [ {"name": "channel", "type": "string"}, {"name": "timestamps", "type": {"type": "array", "items": "long"}}, {"name": "values", "type": {"type": "array", "items": [ "double", "long", "string", {"type": "record", "name": "DoubleArray", "fields": [{"name": "items", "type": {"type": "array", "items": "double"}}]}, {"type": "record", "name": "StringArray", "fields": [{"name": "items", "type": {"type": "array", "items": "string"}}]}, {"type": "record", "name": "JsonStruct", "fields": [{"name": "json", "type": "string"}]} ]}}, {"name": "tags", "type": {"type": "map", "values": "string"}, "default": {}} ] } ``` `timestamps` and `values` are parallel arrays of the same length. A file that doesn't match the schema fails ingestion. The earlier schema, with only `double` and `string` values, is still accepted.