Skip to content

feat(build)!: build one prepared release in the built-in backend - #653

Merged
karhu merged 8 commits into
tnet-157/5-build-releasefrom
tnet-157/6-build-prepared
Nov 10, 2026
Merged

karhu merged 8 commits into
tnet-157/5-build-releasefrom
tnet-157/6-build-prepared

Conversation

@karhu

@karhu karhu commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

The Stack

This stack covers refactoring the internal logic of how releases are resolved during read, download & build, in particular also in the context of parent releases / parent datasets. On top of that it refactors the public TimeNet APIs and CLI interfaces around these actions, to streamline the behavior across them.

This PR

Adds ConnectorBuilder.build_prepared(), so that the coordinator can build one release of a connector dataset.

Details

#652 adds the build coordinator and the build_prepared() method of the builder protocol (TNET-157). The built-in backend, ConnectorBuilder, does not implement that method yet. So the coordinator cannot build a dataset of a connector.

  • ConnectorBuilder.build_prepared(release, store, *, force, pinned_by, options) builds exactly one release into a local store. The store already holds each parent of the release, and each payload source of such a parent.
  • If the card declares another version than the release, it raises TimeNetVersionUnavailableError before anything runs. For an ID that the backend does not know, it raises TimeNetDatasetNotFoundError.
  • TIMENET_ISOLATION decides where the build runs, as for build().
  • With isolation on, run_isolated_build() in env.py runs the hidden command timenet-build build-prepared-release org/name@version --store PATH in the environment of the connector. It forwards --force, --keep-cache and the pins in a file. If options.quiet is set, it also forwards --quiet. Otherwise the child prints its shards and downloads to the inherited stderr. If the child fails, it raises TimeNetBuildError with the tail of the stderr of the child.
  • With isolation off, it calls run_pipeline(), and the connector decides the values backend. It sends each download as a BytesMoved event and each finished shard as a ShardWritten event.
  • This PR adds BytesMoved and ShardWritten to timenet.progress.
  • progress_renderer() in timenet.cli.ui shows every progress event on the console. Each copy or build step and each written shard gets a line. In a terminal, each transfer gets a bar. Elsewhere, a transfer gets a line at about every 20%. The build-prepared-release command uses it.
  • Unless --quiet is set, the build-prepared-release command shows the shards and the downloads on the console. It prints nothing on stdout.
  • timenet_connectors.download.ProgressCallback is now DownloadCallback, so its name differs from timenet.progress.ProgressCallback.

Design decisions

  • The child does not plan. The parent process plans once, and the child builds only the release that it gets. A test makes sure of that: with a parent missing from the store, the child fails instead of building the parent.
  • The parent process does not import the connector on the isolated path. So the parent never needs the dependencies of the connector.
  • The old in-process build path and the new command share progress_renderer(). The old path only turns each DownloadProgress of a connector into a BytesMoved event.

Breaking changes

  • timenet_connectors.download.ProgressCallback is now DownloadCallback.

Stack

Changes later in the stack:

Merge message

feat(build)!: build one prepared release in the built-in backend

ConnectorBuilder.build_prepared() builds exactly one release whose
parents are already in the local registry, so that the coordinator can
build a connector dataset.

- With isolation on, the hidden command build-prepared-release builds
  the release in the environment of the connector. The parent process
  never imports the connector.
- The child does not plan. If a parent is missing from the local
  registry, the build fails.
- BytesMoved and ShardWritten report downloads and shards.
  progress_renderer() shows every progress event on the console.

BREAKING CHANGE: timenet_connectors.download.ProgressCallback becomes
DownloadCallback.

@karhu
karhu force-pushed the tnet-157/6-build-prepared branch from 171c7e2 to 593d9f5 Compare November 6, 2026 19:20
@karhu
karhu force-pushed the tnet-157/6-build-prepared branch from 593d9f5 to 42dcabe Compare November 7, 2026 13:26
@karhu
karhu force-pushed the tnet-157/6-build-prepared branch from 42dcabe to bf51e42 Compare November 7, 2026 15:20
@karhu karhu changed the title feat(build): build one prepared release in the built-in backend feat(build)!: build one prepared release in the built-in backend Nov 10, 2026
@karhu
karhu force-pushed the tnet-157/6-build-prepared branch from bf51e42 to 65aab49 Compare November 10, 2026 14:39

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟡 packages/timenet-connectors/src/timenet_connectors/builder/backend.py — With isolation on (the default), callers of build_prepared() never get the TimeFFormatError the backend protocol promises for a pin mismatch. The same failure raises TimeFFormatError with isolation off. The isolated branch sends every child failure through _run_in_env (env.py:221-226), which turns it into TimeNetBuildError. Fix: raise the same exception types on both paths. For example, the child could write its error type and message to stdout, which build-prepared-release no longer uses, and the parent could raise that type again. At minimum, document the difference. Pre-existing pattern: run_isolated does the same.

    Why this was flagged

    BuilderBackend.build_prepared (packages/timenet/src/timenet/builders.py:160-164) documents TimeFFormatError: If the build omits or changes a pinned file, and build_release (coordinator/build.py:57) repeats it. The trigger is a rebuild with non-empty pinned_by whose new files differ from the pins. TimeFWriter._check_pinned_by (writer.py:433) raises TimeFFormatError in the child build-prepared-release. The child exits non-zero, and _run_in_env (env.py:221-226) raises TimeNetBuildError("building org/name@ ver failed with exit code 1 ...") with only the stderr tail. TIMENET_ISOLATION defaults to "on" (config.py:27), so this is the normal path. With isolation off, _build_in_process lets the real TimeFFormatError reach the caller. So the error type depends on an environment variable. The base run_isolated already collapses errors this way for build(). The new protocol method repeats that and so breaks its own documented contract.

    Verification: nit. The trigger is a build_prepared() call with isolation on (the default) and a non-empty pinned_by whose rebuilt files differ from the pins. On a non-zero exit _run_in_env always raises TimeNetBuildError(f"building {subject} failed with exit code ...") (env.py ~221-226). An except TimeFFormatError therefore never matches on the isolated path, while it does match with TIMENET_ISOLATION=off.

@karhu
karhu force-pushed the tnet-157/6-build-prepared branch from 65aab49 to 5e8afa7 Compare November 10, 2026 15:50

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@karhu
karhu removed this pull request from stack #699 November 10, 2026 15:58
@karhu
karhu force-pushed the tnet-157/6-build-prepared branch from 5e8afa7 to 344d43c Compare November 10, 2026 16:24
@karhu
karhu added this pull request to stack #772 November 10, 2026 16:24

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

ConnectorBuilder now implements build_prepared(). It builds exactly one
release into a local store that already holds each parent of the
release, and each payload source of such a parent. It does not plan. If
the card of the connector declares another version than the release, it
raises TimeFValidationError before anything runs. TIMENET_ISOLATION
decides where the build runs, as for build().

With isolation on, run_isolated_build() in env.py runs the hidden
command timenet-build build-release org/name@version --store PATH in the
environment of the connector. It forwards --force, --keep-cache and the
pins in a file. If the options have no progress callback, it also
forwards --quiet. The child runs with TIMENET_ISOLATION=off. If the
child fails, run_isolated_build() raises TimeNetBuildError with the tail
of the stderr of the child.

With isolation off, build_prepared() calls run_pipeline() with the
values_backend of the connector and builds into the root of the store.
It sends each download as a BytesMoved event and each finished shard as
a ShardWritten event to the progress callback of the options. Without a
progress callback, it installs no download sink.

The build-release command calls build_prepared() of ConnectorBuilder.
Unless --quiet is set, it shows the build lines, the shards and the
downloads on the console. It prints nothing on stdout. A test makes sure
that the command does not plan: with a parent missing from the store,
it fails instead of building the parent. The bar display of the
downloads moves into _download_bars(), so that the old download display
and the new renderer share it.

Nothing outside the tests calls build_prepared() yet. The build command,
build() and run_isolated() work as before.

Part of TNET-157.
ConnectorBuilder.build_prepared() now passes options.quiet to the
isolated child. Before, it passed whether options.progress was None. So
an SDK build, which sends no progress callback, ran the child with
--quiet, and the user saw nothing of a long download. The hidden
build-release command passes its own --quiet on in the options too.

Another version than the card declares now raises
TimeNetVersionUnavailableError, like build_release() does. It is a
TimeFValidationError too, so a handler of the old error still catches
it.

The docstring of build_prepared() wraps near 100 columns again.

Part of TNET-157.
The docstring now says what the method builds and when it needs an isolated environment. Part of TNET-157.
…plain the card-only read

The hidden child command now has the same name as build_prepared(), as the review asked. The comment in build_prepared() now says why the parent reads only the card: the connector and its dependencies exist only in the environment of the child.

Part of TNET-157.
Say only what a caller of the prepared build needs to know.

Part of TNET-157.
The build-prepared-release child no longer sends StepStarted and
StepFinished. The coordinator in the parent sends them around
build_prepared(), so the child showed each step line a second time. The
renderer loses its two step branches, and the command function is now
build_prepared_release_command, after its command name.

run_isolated() and run_isolated_build() now run the child through one
private helper. run_isolated_build() takes force, keep_cache, quiet and
pinned_by without defaults.

Tests that repeated another test go away: the second stderr-tail test,
the in-process backend build that the CLI test covers, a progress case
that the child never sees, and the isolation case of the version check,
which runs before isolation matters. The child-command test no longer
patches the planner, because its missing-parent failure proves that the
child does not plan.

Part of TNET-157.
Shorter docstrings for the code that this change adds or changes.

Part of TNET-157.
The build CLI drew its own download bars and status lines, and turned
each BytesMoved event back into a DownloadProgress event to reuse its
old reporters. timenet.cli.ui.progress_renderer() now shows every
progress event: step lines, a bar for each transfer on a terminal, and
throttled lines elsewhere. The build CLI uses it, and the conversion
back goes. Only the old in-process build still turns the downloads of a
connector into BytesMoved events.

timenet_connectors.download.ProgressCallback had the name of the
callback type in timenet.progress, but takes another event. It is now
DownloadCallback.

BREAKING CHANGE: timenet_connectors.download.ProgressCallback is now
DownloadCallback.

Part of TNET-157.
@karhu
karhu force-pushed the tnet-157/6-build-prepared branch from 344d43c to 38a10b6 Compare November 10, 2026 17:41
@karhu
karhu merged commit 987c7ce into main Nov 10, 2026
4 of 8 checks passed
@karhu
karhu deleted the tnet-157/6-build-prepared branch November 10, 2026 18:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants