Skip to content

feat(build)!: run every build through the coordinator - #655

Merged
karhu merged 8 commits into
tnet-157/7-build-clifrom
tnet-157/8-build-client
Nov 10, 2026
Merged

karhu merged 8 commits into
tnet-157/7-build-clifrom
tnet-157/8-build-client

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

Moves the client onto the coordinator, so that every build runs through build_release(), and removes the old build path.

Details

After #654, the client is the last caller of the old build path (TNET-157). It still builds through ConnectorBuilder.build() and build_one(), so the builder protocol keeps two ways to build.

  • The client builds a missing version and a missing payload source with build_release(). It maps its fetch_from and auto_build onto build_release(), with build_parents set.
  • A version that the connector does not declare raises TimeNetVersionUnavailableError before the build starts.
  • The isolated child of a client build prints its shards and downloads to stderr, as before.
  • The BuilderBackend protocol loses knows(), declared_version() and the old build(). build_prepared() is now build(), so a backend implements dataset_ids(), card() and build(). The hidden command keeps its name, build-prepared-release. ConnectorBuilder.card() returns None for a spelling that no card declares.
  • run_isolated(), build_one() and its helpers, prepare_parents() and the hidden --pinned-by option of build go away.
  • timenet.engine.publish_pipeline() goes away too. open_parents() takes the LocalRegistry that the build writes to.
  • put_dataset(force=True) on a remote or S3 registry raises TimeFValidationError before it uploads, because a published version never changes.
  • timenet_connectors.builder imports none of its modules. So loading the backend through its entry point loads neither Typer nor the console of the CLI. A test makes sure of that.
  • About 1350 of the 1730 changed lines are deletions.

Design decisions

  • Each backend builds every release that it claims, the requested release too. build_one() always built the requested release on the built-in path. So a subclass of ConnectorBuilder that overrides build() now builds all of its releases through it.
  • The client copies a parent like the build command does, with each payload source at every depth.
  • The import test does not check rich, because httpx loads rich whenever rich is installed.

Breaking changes

  • BuilderBackend no longer has knows() and declared_version(), and build() takes (release, store, *, force, pinned_by, options). A backend of another package must implement dataset_ids(), card() and the new build().
  • timenet.engine.publish_pipeline() is gone. Build with run_pipeline(), then publish with copy_release().
  • open_parents() takes a LocalRegistry. It no longer opens parents from a remote registry.
  • put_dataset(force=True) on a remote or S3 registry raises TimeFValidationError.
  • timenet_connectors.builder no longer exports app and main. Import them from timenet_connectors.builder.cli.
  • The hidden --pinned-by option of timenet-build build is gone.
  • timenet_connectors.discovery.has_connector() is gone, because nothing calls it now.
  • TimeNet.open() and TimeNet.load() raise TimeNetVersionUnavailableError for a version that the connector does not declare. It is a TimeNetDatasetNotFoundError, as before.

Stack

Changes later in the stack:

  • docs: update the docs for the build coordinator #683 holds the doc changes of this PR.
  • The mapping of fetch_from and auto_build onto build_release() in _obtain() and _prepare() is thin glue. "Client reads on the coordinator" replaces it with the entry machinery.

Merge message

feat(build)!: run every build through the coordinator

The client builds through build_release() too, so the old build path
goes away and a backend has one way to build.

- The client maps fetch_from and auto_build onto build_release(), and
  copies parents with each payload source at every depth.
- Each backend builds every release that it claims, the requested
  release too.
- put_dataset(force=True) on a remote or S3 registry refuses before it
  uploads, because a published version never changes.
- Loading the backend through its entry point loads neither Typer nor
  the console of the CLI.

BREAKING CHANGE: BuilderBackend loses knows() and declared_version(),
and build() takes (release, store, *, force, pinned_by, options).

BREAKING CHANGE: run_isolated(), build_one(), prepare_parents(),
publish_pipeline(), has_connector() and the hidden --pinned-by option
of timenet-build build are gone.

BREAKING CHANGE: open_parents() takes a LocalRegistry, and
timenet_connectors.builder no longer exports app and main.

@karhu
karhu added this pull request to stack #650 November 3, 2026 23:39
@karhu karhu changed the title tnet 157/8 build client feat(build)!: run every build through the coordinator Nov 3, 2026
@karhu
karhu force-pushed the tnet-157/8-build-client branch from f6db2f9 to 4aebc1f Compare November 4, 2026 08:10
@karhu
karhu force-pushed the tnet-157/8-build-client branch from 18ee40c to 04d10d9 Compare November 4, 2026 12:01
@karhu
karhu force-pushed the tnet-157/8-build-client branch from 04d10d9 to 57ca7c7 Compare November 4, 2026 12:42
@karhu
karhu force-pushed the tnet-157/8-build-client branch from 57ca7c7 to 122b508 Compare November 4, 2026 13:08
@karhu
karhu force-pushed the tnet-157/8-build-client branch from 122b508 to 3afdcae Compare November 4, 2026 13:16
@karhu
karhu force-pushed the tnet-157/8-build-client branch from 3afdcae to 0370b07 Compare November 4, 2026 13:34
@karhu
karhu force-pushed the tnet-157/8-build-client branch from 0370b07 to e1876cf Compare November 4, 2026 14:58
Comment thread docs/client.md Resolved

@TimoStoff TimoStoff left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why can't they all be like this 😍

@karhu
karhu force-pushed the tnet-157/8-build-client branch from 37081c7 to 69ed889 Compare November 6, 2026 19:20
@karhu
karhu force-pushed the tnet-157/8-build-client branch from 69ed889 to c8ceb49 Compare November 7, 2026 13:26
@karhu
karhu force-pushed the tnet-157/8-build-client branch 2 times, most recently from c5df932 to ea48d99 Compare November 7, 2026 17:40
@karhu
karhu force-pushed the tnet-157/8-build-client branch from ea48d99 to 3b3041a 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.

I reviewed this PR and found no bugs. It is a large breaking change to the public builder protocol, the client's build path and the remote put_dataset behaviour, so a human should still look at it. New commits were also pushed after the existing approval.

What was reviewed:

  • _obtain and _prepare in client.py now go through build_release(). A pinned version is passed as id@ version and "latest" sentinels are passed as a bare ID. The payload-source builds still get their pinned_by pins.
  • plan_parents/_locate now take only a LocalRegistry. Because the output is always local, the old remote-output guard and the requires_open_access handling are no longer needed. LazyReadRegistry.put_dataset(force=True) raises before it stages anything.
  • run_isolated_build and _run_build in env.py: with the child's stdout sent to DEVNULL, reading stderr inline cannot deadlock. I also checked the candidate concern that third-party backends still on the old build() signature would get a TypeError. I ruled it out because the PR declares that protocol break.
Extended reasoning...

The change touches 25 files across timenet and timenet-connectors, about 350 lines added and 1,280 removed. It removes knows(), declared_version(), build_one(), run_isolated(), prepare_parents(), publish_pipeline() and has_connector(), and changes the BuilderBackend.build() signature. It also routes the client's missing-version and payload-source builds through coordinator.build_release(), and makes put_dataset(force=True) refuse on remote and S3 registries. The only security-relevant surface is the subprocess call in env.py, which still runs a fixed argument vector, plus the existing open-access checks. I deferred because of the size and the breaking changes to the public and plugin API, and because commits landed after the human approval.

@karhu
karhu force-pushed the tnet-157/8-build-client branch from 3b3041a to f621061 Compare November 10, 2026 15:50
@karhu
karhu removed this pull request from stack #699 November 10, 2026 15:58

@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 force-pushed the tnet-157/8-build-client branch from f621061 to c1f6efa 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.

The TimeNet client now builds a missing version and a missing payload
source with build_release(), like the build command and
timenet_connectors.build(). So every build entry point runs through the
same coordinator, and the old build path goes away.

The client maps its fetch_from and auto_build onto build_release(), with
build_parents set. A version that the connector does not declare raises
TimeNetVersionUnavailableError before the build starts. That error is a
TimeNetDatasetNotFoundError, as before, and also a TimeFValidationError.

The BuilderBackend protocol loses knows() and declared_version(), and
its build() now builds one prepared release into a local store. A
backend now implements dataset_ids(), card() and build().
ConnectorBuilder changes the same way. Its card() returns None for a
spelling that no card declares, such as timenet/hello_world.
run_isolated(), build_one() and its helpers, prepare_parents() and the
hidden --pinned-by option of build go away.

Each backend now builds every release that it claims, the requested
release too. build_one() always built the requested release on the
built-in path. So a subclass of ConnectorBuilder that overrides build()
now builds all of its releases through it.

Every build now writes into a local store first. So
engine.publish_pipeline(), which built straight into a registry, goes
away, and open_parents() takes a LocalRegistry. put_dataset() with
force=True on a remote or S3 registry raises TimeFValidationError,
because a published version never changes. The planner writes only
into a LocalRegistry, so its checks for a remote output go away. The
isolated child writes nothing to stdout, so the parent discards it.

timenet_connectors.builder imports none of its modules, and the backend
no longer imports the CLI. So loading the backend through its entry
point loads neither Typer nor the console of the CLI.

The plan tests that ran prepare_parents() now run build_release(). When
the client builds, it now copies a parent like the build command does:
with each payload source at every depth. So such a copy fails if the
registry to copy from holds a payload source without its own payload
sources.

BREAKING CHANGE: BuilderBackend no longer has knows() and
declared_version(), and its build() takes a prepared release:
build(release, store, *, force, pinned_by, options). A backend of
another package must implement dataset_ids(), card() and build().
timenet_connectors.builder no longer exports app and main: import them
from timenet_connectors.builder.cli. has_connector() of
timenet_connectors.discovery is gone, because nothing calls it now. The
hidden --pinned-by option of timenet-build build is gone. TimeNet.open()
and TimeNet.load() raise TimeNetVersionUnavailableError for a version
that the connector does not declare. timenet.engine.publish_pipeline()
is gone: build into a local registry, then publish the release.
open_parents() takes a LocalRegistry. put_dataset(force=True) on a
remote or S3 registry raises TimeFValidationError.

Part of TNET-157.
The client builds with the default BuildOptions, so quiet stays unset. A
new test opens a missing release with the built-in backend, isolation on
and the child faked, and makes sure that the child is not quiet. Before
the backend read BuildOptions.quiet, a client build ran the child with
--quiet, because the client sends no progress callback.

Part of TNET-157.
Module docstrings now say what the module does, not who loads it.

Part of TNET-157.
The two comments on this mapping now start with "Glue:", as in later
changes. They say that it stays until the client takes a list of sources.

Part of TNET-157.
The two glue comments now start with TODO(adrian), so a search finds each one that is left.

Part of TNET-157.
Say only what a caller of the builder backend and of the client reads needs to know.

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

Part of TNET-157.
Drop tests that cannot fail or repeat other tests, and sharpen the rest.

Part of TNET-157.
@karhu
karhu force-pushed the tnet-157/8-build-client branch from c1f6efa to 7048dc7 Compare November 10, 2026 17:41
@karhu
karhu merged commit bf3e42c into main Nov 10, 2026
4 of 8 checks passed
@karhu
karhu deleted the tnet-157/8-build-client 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