Skip to content

feat(coordinator)!: plan and run builds through build_release - #652

Merged
karhu merged 9 commits into
tnet-157/4-plan-stepsfrom
tnet-157/5-build-release
Nov 10, 2026
Merged

karhu merged 9 commits into
tnet-157/4-plan-stepsfrom
tnet-157/5-build-release

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 the build coordinator, build_release(), next to the old build path. Later PRs in the stack move the callers onto it.

Details

TimeNet starts a build on five paths, and each path plans and runs the build in its own way (TNET-157). The coordinator is the one place that plans a build and runs its steps.

  • timenet.coordinator.build.build_release(dataset, *, store, fetch_from, build_parents, force, pinned_by, open_access_only, options) builds one release into a local store, after each release that its build needs. It returns the Plan that ran.
  • It finds the backend that claims the dataset. If the caller asks for another version than the card declares, it raises TimeNetVersionUnavailableError before anything runs.
  • It plans with plan_parents(), adds the build of the requested release, and runs each step in order. A copy step uses copy_release(). A build step calls build_prepared() of the backend for that exact release. Each step sends StepStarted and StepFinished.
  • The BuilderBackend protocol gets dataset_ids() and build_prepared(release, store, *, force, pinned_by, options). BuildOptions holds keep_cache, the progress callback and quiet. Unless quiet is set, an isolated child prints its shards and downloads to the inherited stderr. builders.installed_builder() returns an InstalledBuilder with the first backend whose dataset_ids() holds the ID, and the card of the dataset. builders.declared() also checks the version. It raises TimeNetDatasetNotFoundError for an unknown ID and TimeNetVersionUnavailableError for another version.
  • BuildOptions.keep_cache and quiet are not read in this PR. ConnectorBuilder.build_prepared() in feat(build)!: build one prepared release in the built-in backend #653 reads them.
  • ConnectorBuilder.dataset_ids() lists the IDs of the cards on disk and imports no connector. It wraps discovery.known_ids(), which the build command calls to check an ID.
  • timenet-build build takes a dataset ID or an exact release, org/name@version. For another version than the card declares, it raises TimeNetVersionUnavailableError before anything runs.
  • plan_parents() takes open_access_only. With it, a local store gets the same access checks as a registry that is not local. The checks cover the card and the lineage of a requested release that the store already holds.

Design decisions

  • The coordinator lives in timenet and imports nothing from timenet_connectors, so any package can provide a backend. A ruff rule that bans the import makes sure of that.
  • There is one version check, builders.declared(), which the coordinator and the build command call. TimeNetVersionUnavailableError is a TimeNetDatasetNotFoundError and a TimeFValidationError, so the error handlers of both current callers keep working.
  • Only the requested release gets force. The coordinator does not build again a parent that the store holds intact.
  • If the store holds the requested release intact, its copy step copies only each payload source that the store lacks or holds damaged.
  • A dataset ID matches only the ID that a card declares. DatasetId is case-sensitive and allows _, so timenet/hello_world is a different ID from timenet/hello-world. For an unknown ID, timenet-build build still shows the usage error with the list of known IDs.
  • discovery.known_ids() skips a card that cannot be read, or that has no dataset_id, with a ConnectorCardWarning that names the file. So a broken card fails only its own dataset. The card tests read every card, so CI finds such a card first.
  • The coordinator is the planner that the front ends share. It is not part of the user API, so the API docs leave out timenet.coordinator.

Breaking changes

  • A BuilderBackend must implement dataset_ids() and build_prepared().
  • builders.find_builder() and builders.exact_builder() are gone. Use builders.installed_builder(), or builders.declared() to check the version too.
  • timenet-build build and timenet_connectors.build() no longer accept the module spelling of an ID, such as timenet/hello_world. Use the ID from the card, timenet/hello-world.

Stack

Changes later in the stack:

Merge message

feat(coordinator)!: plan and run builds through build_release

build_release() plans one build and runs its steps, so that the five
build paths can share one planner. It runs next to the old build path
for now.

- It copies each missing parent with copy_release() and builds each
  release with build_prepared() of the backend that claims it.
- Only the requested release gets force. An intact parent in the
  local registry does not build again.
- builders.declared() is the one version check. Another version than
  the card declares raises TimeNetVersionUnavailableError before
  anything runs.
- The coordinator imports nothing from timenet_connectors, so any
  package can provide a backend.

BREAKING CHANGE: A BuilderBackend must implement dataset_ids() and
build_prepared().

BREAKING CHANGE: builders.find_builder() and builders.exact_builder()
become builders.installed_builder() and builders.declared().

BREAKING CHANGE: timenet-build build and timenet_connectors.build() no
longer accept the module spelling of an ID, such as
timenet/hello_world.

@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.

LGTM! Thank you. The main issue here again is sloptests and really hard to read Claude comments

Comment thread packages/timenet/tests/test_errors.py Resolved
@karhu
karhu added this pull request to stack #699 November 6, 2026 08:10
@karhu
karhu force-pushed the tnet-157/5-build-release branch 4 times, most recently from eea11c2 to 9fe4442 Compare November 7, 2026 15:20
@karhu
karhu force-pushed the tnet-157/5-build-release branch from 9fe4442 to c6d0a03 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.

@karhu
karhu force-pushed the tnet-157/5-build-release branch from c6d0a03 to f55c7a0 Compare November 10, 2026 15:50
@karhu
karhu removed this pull request from stack #699 November 10, 2026 15:58
@karhu
karhu force-pushed the tnet-157/5-build-release branch from f55c7a0 to fd2d755 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 completed

Nothing new to post: everything this review found is already covered by existing comments on this pull request or didn't merit a separate one.

Still open from earlier reviews (1):

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

build_release() in timenet.coordinator.build builds one release into a
local store, after each release that its build needs. It finds the
backend that claims the dataset. If the caller asks for another version
than the card declares, it raises TimeNetVersionUnavailableError before
anything runs. Then it plans with plan_parents(), adds the build of the
requested release, and runs each step in order. A copy step uses
copy_release(). If the store holds the release intact, the step copies
only each payload source that the store lacks or holds damaged. A build
step calls build_prepared() of the backend for that exact release. Each
step sends StepStarted and StepFinished. Only the requested release gets
force. build_release() returns the Plan that ran.

The BuilderBackend protocol gets two methods. dataset_ids() lists the
IDs of the datasets that a backend builds. build_prepared(release,
store, *, force, pinned_by, options) builds exactly one release into a
local store that holds each parent of the release, and each payload
source of such a parent. BuildOptions holds keep_cache and the progress
callback. find_builder() returns the first backend whose dataset_ids()
holds the ID. knows(), declared_version() and build() stay for now.

ConnectorBuilder lists its dataset IDs with known_ids() of
timenet_connectors.discovery. known_ids() reads the cards on disk and
imports no connector. A card that cannot be read raises
TimeNetInvalidCardError, which names the file. A dataset ID matches only
the ID that a card declares, so the module spelling, such as
timenet/hello_world, no longer resolves. The build command checks the ID
against known_ids() and shows the usage error with the known IDs.

TimeNetVersionUnavailableError is a TimeNetDatasetNotFoundError and a
TimeFValidationError. It carries the requested release and the declared
version.

plan_parents() takes open_access_only. With it, a local store gets the
same access checks as a registry that is not local, also for the card
and the lineage of a requested release that the store already holds.
If the output is not local or open_access_only is set, the planner
checks the access of the requested card first, also for a dataset
without parents.

BREAKING CHANGE: a BuilderBackend must implement dataset_ids() and
build_prepared(). find_builder(), timenet-build build and
timenet_connectors.build() no longer accept the module spelling of a
dataset ID.

Part of TNET-157.
BuildOptions gets quiet. It asks the backend to print nothing of its
own. An isolated child otherwise prints its build lines and downloads to
the inherited stderr. Before, a backend could only guess this from a
missing progress callback, so an SDK build with no callback silenced the
child too. The protocol docstring of build_prepared() names quiet and
the progress callback, and names TimeNetVersionUnavailableError for
another version.

TimeNetVersionUnavailableError now says what to do: ask for the version
that the connector builds, or leave out the version. The hint attribute
stays for front ends.

Part of TNET-157.
The docstring now says this backend is not ConnectorBuilder and the plan never builds through it.

Part of TNET-157.
Say that the coordinator is internal to TimeNet and point users to the build command and the client.

Part of TNET-157.
…protocol docstrings

The build command now asks ConnectorBuilder().dataset_ids() for the known dataset IDs, as any other caller of the builder protocol does. So known_ids() is now private and is called _known_ids().

The BuilderBackend docstring now states the contract for an implementer, and the card() docstring and the summary of the coordinator build module say less about the planner.

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

Part of TNET-157.
…once

find_builder(), find_card() and exact_builder() now share one private
lookup. A registered backend without dataset_ids() fails with a
TimeFValidationError that names its entry point, not an AttributeError.

build_release() parses the requested reference once. Before any step
runs, it makes sure that a plan with a copy step has a registry to copy
from. Its docstring names each error in one line.

known_ids() in timenet_connectors.discovery is public again. The build
command calls it, and ConnectorBuilder.dataset_ids() wraps it. A card
without a dataset_id now fails the list with TimeNetInvalidCardError, as
a card that is not a mapping does.

The ruff rule against an import of timenet_connectors replaces the
subprocess test of the build coordinator. Tests that only checked a
dataclass default, a step report that another test covers, or that a
lookup ignores knows() go away. The check that the version error is a
not-found error and a validation error moves into the parametrized
error test.

Two client tests now install their builder through the entry points,
because find_card() no longer goes through find_builder().

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

Part of TNET-157.
One connector card that could not be read made known_ids() raise. So
every lookup through the installed connectors failed, also for other
datasets. Now known_ids() skips that card with a ConnectorCardWarning,
and only its own dataset fails.

timenet-build build refused org/name@version. It now takes the version
and checks it against the card of the connector.

builders.installed_builder() walks the entry points once and returns
an InstalledBuilder: the backend with its card. builders.declared()
also checks a requested version, and it raises the one "no installed
connector builds" error. Both replace find_builder() and
exact_builder(). The check for a backend without dataset_ids() goes,
because the builder protocol requires the method.

The coordinator docstring says that the front ends share the planner.

BREAKING CHANGE: builders.find_builder() is gone. Use
builders.installed_builder().

Part of TNET-157.
@karhu
karhu force-pushed the tnet-157/5-build-release branch from fd2d755 to 5a98da8 Compare November 10, 2026 17:41
@karhu
karhu merged commit 2216df7 into main Nov 10, 2026
4 of 8 checks passed
@karhu
karhu deleted the tnet-157/5-build-release 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