Architecture¶
How TimeNet's packages, registries, and build fit together. This page is the map. Follow the links for per-component detail.
The big picture¶
TimeNet splits into three parts. A connector builds a raw source into a TimeF version. The
client/SDK reads its manifest from a registry and loads the data. The relational control
plane is one immutable DuckDB file. The values plane can be Parquet or Zarr. Reading never runs
connector code. Against a local registry, open and load can first build a dataset that the
registry does not have from an installed connector.
| What it is | Ships | Used by | |
|---|---|---|---|
timenet |
Python package | TimeF format, reader/writer, registry client, engine, BaseConnector, SDK, CLI |
everyone (pip install timenet) |
| registry | a served location | compiled TimeF versions | the SDK reads it and build publishes to it |
timenet-connectors |
Python package and source repository | connector recipes, cards, and the timenet-build CLI |
dataset builders and connector authors |
There can be several registries: one public, private internal ones, or a local directory.
The two flows¶
PRODUCE dataset.yaml + connector
│
▼
engine download ─► convert ─► derive_schema ─► store
│
▼
registry
│
▼
CONSUME SDK ─► open_version ─► TimeFReader ─► Arrow
The compiled manifest.json is the entry point and commit marker. It contains the card metadata,
derived schema, counts, and file list. open_version returns a handle to the manifest and version
files. TimeFReader uses the handle to read the DuckDB hierarchy and lazy Signal values. The SDK
never imports connector code.
Build roles: connector, engine, builder¶
Three producer-side pieces, each with one job:
| Role | What it is | Job |
|---|---|---|
| Connector | one BaseConnector subclass per dataset (connectors) |
the dataset-specific recipe: download() fetches raw files, convert() builds a TimeFDataset. Knows nothing about the engine or registry. |
| Engine | run_pipeline (build & publish) |
drives any connector through the fixed pipeline and owns caching, idempotency, and force / keep_cache. Knows no dataset specifics. |
| Builder | the timenet-build CLI (build) |
the entry point: resolves the id to its connector and runs the engine into a registry. |
timenet-build build org/name
│
├─ builder discovery.resolve("org/name") -> Connector class
│ datasets/<org>/<name>/ exposes CONNECTOR
│
└─ engine run_pipeline(connector, <registry>)
metadata -> download -> convert -> derive_schema -> store
-> <registry>/org/name/<version>/
metadata() reads the dataset.yaml card. The engine streams the converted dataset through
TimeFWriter. A local output directory is also a valid registry, so the client
can read it directly.
Where each component lives¶
| Component | Package | Side |
|---|---|---|
TimeF format, types, TimeFDataset, manifest |
timenet |
shared |
CLI, SDK, registry client, TimeFReader |
timenet |
consumer |
Engine, TimeFWriter, BaseConnector |
timenet |
producer |
Connector recipes + cards, timenet-build |
timenet-connectors |
producer |
Design principles¶
- The manifest describes the version. The SDK reads schema, counts, and file pointers from
manifest.json. The object hierarchy lives incontrol.duckdb. - Wire-facing types are immutable Pydantic models. Runtime objects use typed dataclasses. Specs and
annotation content are typed descriptors, so they pickle and round-trip through the
reader with no runtime class synthesis. That keeps multiprocessing
DataLoaderworkers safe. - NumPy and Arrow values share one array handle interface. A
Signalreadsto_arrow(),to_numpy(), andread_steps()through a private array handle. NumPy conversion rejects null values. Arrow reads preserve them. Its spec declares the scalar dtype and per-timestep shape. The writer stores typed scalar values in Parquet by default and uses Zarr for dtype-preserving multidimensional values. - Units go through pint. One shared registry owns every definition and conversion.
- Commits are atomic. The writer stages a version into a temp directory and publishes it with a
single atomic rename. Once
manifest.jsonis present, the writer commits the version. - Versions are immutable. A change produces a new version instead of modifying a committed one.
Lifecycle of a dataset¶
- Author a connector under
timenet_connectors/datasets/<org>/<name>/. Its__init__.pyexposesCONNECTOR, and itsdataset.yamlcard sits beside it. - Build:
timenet-build build <org>/<name>runs the engine, compiles the manifest, and writes a TimeF version. - Verify locally: point the SDK at the output directory (itself a valid local registry).
- Publish the complete TimeF version to a registry.
- Consume: use
TimeNet.open()for lazy access,TimeNet.load()for a dataset with every value in memory, ortimenet downloadfrom the CLI for a local copy.
Dataset inheritance¶
A dataset can derive from one or more parent datasets. It builds on their data. It does not store
their values a second time. For example, physionet/ecg-qa-cot adds questions to the recordings of
physionet/ptb-xl. The child records its parents and their lineage. So it credits them. Their
access requirements also apply to the child.
Each parent is an exact release, org/name@version, that the card of the child declares under
parents. While the connector builds the child, it copies Records, Signals, annotations, and tasks
from the parents. The child keeps the copies in its own DuckDB catalog, under IDs of its own. It
reads their values from the release that stores them, its payload source. See
Importing from a parent and
Compose a child dataset.
A build needs the parent releases. A read needs only the payload sources. Before the connector runs, the build prepares each parent release, as Prepare parent releases describes.
TimeNet.open(), TimeNet.load(), TimeNet.open_torch(), and TimeFReader.open() resolve the
payload sources before the first read. A read never opens the catalog of a parent. Against a local
registry, the client reuses a complete copy of each release, copies it from fetch_from, or builds
it. auto_build is on by default. A remote client never runs connector code. See
Open, load, or download.
The child keeps an internal map from the ID of each parent object to the ID of its copy. Through
this map, get(..., parent=) finds a copy. Through the same map, a later import reuses a copy. The
child also stores a read order of its Records, so that a full read follows the order of the stored
values. Its manifest
pins each file that it reads from a payload source by path, size, and checksum, as
Shared payloads describes.