Skip to content

feat(sources)!: resolve a release through an ordered list of sources - #657

Merged
karhu merged 4 commits into
tnet-157/8-build-clientfrom
tnet-157/9-sources
Nov 10, 2026
Merged

karhu merged 4 commits into
tnet-157/8-build-clientfrom
tnet-157/9-sources

Conversation

@karhu

@karhu karhu commented Nov 4, 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 timenet.sources, which resolves a release through an ordered list of sources. The build and the client do not use it yet.

Details

A build and a read must find each release in one ordered list of sources: registries and the installed connectors (TNET-157). Today each path asks its own places in its own order. This PR adds the parts that answer the question "which source provides this release?". The build and the client do not use them yet.

  • The new module timenet.sources defines ReleaseSource, the union BaseRegistry | InstalledConnectors. Each source has one method: resolve(dataset_id, version), where version is a Version, or None for the latest version.
  • A source gives one of four answers, a ReleaseLookup. FoundInRegistry names the version and the registry that serves the release. FoundInConnector keeps the InstalledBuilder that the lookup read, so a caller does not read the card again. Found is the union of the two. NotFound means that the source does not know the release. NotServed means that the source lists the release but does not serve its files, for example a credentialed release on a non-local registry. NotServed.access_error() turns it into a TimeNetAccessError that names the access page of each card that is not open.
  • resolve_release() parses the version once and asks the sources in order.
  • Every registry is now a source. BaseRegistry.resolve() reads the manifest. If the registry requires open access (requires_open_access), it answers NotServed for a release whose effective access is not open. LocalRegistry.resolve() serves every release that it holds, and looks only for the manifest file of that release.
  • InstalledConnectors() answers with the version that the card of the connector declares. For another version, it answers NotFound. A caller gets the declared version from builders.declared(), which raises TimeNetVersionUnavailableError.
  • A remote registry that cannot be reached raises TimeNetRegistryError, which names the registry or its storage, not an httpx error. This includes a "latest" lookup, which asks every source. It also includes a network drop during the parallel download of a release. An S3 registry that cannot reach its bucket raises TimeNetRegistryError, which names the bucket, not a botocore error.
  • FoundInRegistry and NotServed keep the manifest that the source read, so a caller does not fetch it again.

Design decisions

  • An exact version comes from the first source that serves it. Without a version, the release is the newest version of the first source that serves the dataset. "latest" is the highest version that any source serves, and the order of the sources breaks a tie. So with @latest, a store that holds an old version does not hide a newer one.
  • NotFound and NotServed fall through to the next source. Any other error stops the resolution, so a broken source is never skipped in silence.
  • None and an empty string mean no version, and "latest" asks every source. split_ref() returns "latest" for org/name@latest, so the two requests stay apart. resolve_release() parses the version once, so no source parses it again.
  • ReleaseSource is a union, not a protocol. So code that holds an answer narrows it with isinstance, and the types rule out any other kind of source. A new kind of source extends the union.
  • A local miss does not read every manifest in the registry, because a read of a dataset that the store lacks must stay cheap.
  • Limit: a source that answers "latest" with NotServed for its newest version hides its older open versions, because no registry lists the versions that it serves.
  • Both packages now have a module named sources: timenet.sources and timenet_connectors.sources. For such a pair, the API docs generator names each page after its full module path. The URL of the connectors page changes.

Breaking changes

  • A registry that cannot be reached raises TimeNetRegistryError, not an httpx error.
  • An S3 registry that cannot reach its bucket raises TimeNetRegistryError, not a botocore error.
  • split_ref() returns "latest" for org/name@latest, not None.
  • The API page of timenet_connectors.sources moves from api/sources.md to api/timenet_connectors.sources.md.

Stack

Changes later in the stack:

Merge message

feat(sources)!: resolve a release through an ordered list of sources

timenet.sources resolves a release through an ordered list of
registries and installed connectors, so that builds and reads can find
each release in one way. Nothing uses it yet.

- Each source answers resolve() with FoundInRegistry,
  FoundInConnector, NotFound or NotServed. NotServed means that the
  source lists the release but does not serve its files.
- An exact version comes from the first source that serves it. No
  version means the newest version of the first source that serves
  the dataset. "latest" is the highest version of any source.
- NotFound and NotServed fall through to the next source. Any other
  error stops the lookup, so a broken source is never skipped.

BREAKING CHANGE: A registry or S3 bucket that cannot be reached raises
TimeNetRegistryError instead of an httpx or botocore error.

BREAKING CHANGE: split_ref() returns "latest" for org/name@latest
instead of None.

BREAKING CHANGE: The API page of timenet_connectors.sources moves from
api/sources.md to api/timenet_connectors.sources.md.

@karhu
karhu added this pull request to stack #650 November 4, 2026 12:02
@karhu
karhu force-pushed the tnet-157/9-sources branch 2 times, most recently from 06647d6 to 4a28aae Compare November 4, 2026 13:08
@karhu
karhu force-pushed the tnet-157/9-sources branch from 4a28aae to 504d5ea Compare November 4, 2026 13:16
@karhu
karhu force-pushed the tnet-157/9-sources branch from 504d5ea to 76e9b97 Compare November 4, 2026 13:34
@karhu
karhu force-pushed the tnet-157/9-sources branch from 76e9b97 to 6284ca1 Compare November 4, 2026 14:58
@karhu
karhu marked this pull request as ready for review November 4, 2026 15:21
Comment thread docs/registry.md Resolved
Comment thread docs/registry.md Resolved
Comment thread docs/registry.md Resolved
Comment thread docs/registry.md Resolved
Comment thread docs/registry.md Resolved
Comment thread docs/registry.md Resolved
Comment thread docs/registry.md Resolved
@karhu
karhu marked this pull request as draft November 4, 2026 15:22
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

✅ CI checks passed

Check Status
Quick checks ✅ Passed
Tests (Python 3.13) ✅ Passed
Tests (Python 3.11) ✅ Passed

Updated for 3117782 · run #8357

@karhu
karhu force-pushed the tnet-157/9-sources branch 2 times, most recently from f54ac3d to 8ae7797 Compare November 4, 2026 20:02
Comment thread packages/timenet/tests/test_sources.py Resolved
Comment thread packages/timenet/tests/test_sources.py Resolved
Comment thread packages/timenet/tests/test_sources.py Resolved
Comment thread packages/timenet/tests/test_sources.py Resolved
Comment thread packages/timenet/tests/test_sources.py 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.

some comments

@karhu
karhu force-pushed the tnet-157/9-sources branch from 16b591e to d549cd2 Compare November 6, 2026 19:20
@karhu
karhu force-pushed the tnet-157/9-sources branch from d549cd2 to 272bdcc Compare November 7, 2026 13:26
@karhu
karhu force-pushed the tnet-157/9-sources branch 2 times, most recently from 08de933 to bd32dd9 Compare November 7, 2026 16:27

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

Actually…..lgtm

@karhu
karhu force-pushed the tnet-157/9-sources branch from bd32dd9 to a73fc50 Compare November 7, 2026 17:40
@karhu karhu changed the title feat(sources): resolve a release through an ordered list of sources feat(sources)!: resolve a release through an ordered list of sources Nov 10, 2026
@karhu
karhu force-pushed the tnet-157/9-sources branch from a73fc50 to c1d1c26 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.

Beyond the inline finding, I checked whether the HTTP registry has the same body-streaming gap. It does not: in _http.py, stream_to keeps _reaching around the whole iter_bytes loop, and RemoteRegistry.download_version wraps all of download_version_files.

Extended reasoning...

I compared the HTTP path with the S3 finding. In RegistryHttpClient.stream_to, _reaching is entered in the same with statement as self._client.stream(...), so a transport error while reading the body is still wrapped. RemoteRegistry.download_version also wraps the parallel downloader in _reaching.

@karhu
karhu force-pushed the tnet-157/9-sources branch from c1d1c26 to 5199c96 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 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.

@karhu
karhu force-pushed the tnet-157/9-sources branch from 5199c96 to 0775580 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.

A build and a read must find each release in one ordered list of
sources: registries and the installed connectors. This change adds the
parts that answer that question. The build and the client do not use
them yet.

The new module timenet.sources defines ReleaseSource, the union of
BaseRegistry and InstalledConnectors. A source has the method
resolve(dataset_id, version), where version is a Version, or None for
the newest version of that source. A source gives one of four answers, a
ReleaseLookup. FoundInRegistry names the version and the registry that
serves the release. FoundInConnector keeps the InstalledBuilder that the
lookup read: the backend and the card that build the release. Found is
the union of the two. NotFound means that the source does not know the
release. NotServed means that the source lists the release but does not
serve its files. NotServed keeps the access page of each card that is
not open, and access_error() turns it into a TimeNetAccessError.

resolve_release() parses the version once and asks the sources in order.
An exact version comes from the first source that serves it. Without a
version (None or an empty string), the first source that serves the
dataset gives its newest version. So a copy in the first source answers,
and no later source is asked. "latest" is the highest version that any
source provides, and the order decides only which source serves it.
NotFound and NotServed fall through, and any other error stops the
resolution. A remote registry that cannot be reached raises
TimeNetRegistryError, which names the registry, not a raw httpx error.
This also holds for the parallel download of a release. An S3 registry
that cannot reach its bucket raises TimeNetRegistryError, which names
the bucket.

Every registry is now a source. BaseRegistry.resolve() reads the
manifest. If the registry requires open access, it answers NotServed for
a release whose effective access is not open. LocalRegistry.resolve()
serves every release that it holds. It looks only for the manifest of
the release, so a miss does not read every manifest in the registry.
InstalledConnectors() answers with the version that the card of the
connector declares.

FoundInRegistry and NotServed keep the manifest that the source read to
answer, in an optional manifest field. A caller can use it and does not
fetch the same manifest again. The field takes no part in equality or in
the repr. In every registry method that takes a version string, None, an
empty string and "latest" all mean the newest version of that registry.

split_ref() now keeps "latest", so org/x@latest and org/x are two
different requests. A build reads both as the version of the card,
because a connector builds only that version.

The API docs generator now names a page after its full module path if
both packages have a module with that name, because timenet.sources and
timenet_connectors.sources both mapped to sources.md.

BREAKING CHANGE: A remote registry that cannot be reached raises
TimeNetRegistryError, not an httpx error. An S3 registry that cannot
reach its bucket raises TimeNetRegistryError, not a botocore error.
split_ref() returns "latest" for a reference that ends with @latest, not
None. So a TimeNet method that gets such a reference and also a version
argument raises TimeFValidationError.

Part of TNET-157.
Say only what a caller of a release source and of resolve_release() 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/9-sources branch from 0775580 to 3117782 Compare November 10, 2026 17:41
@karhu
karhu merged commit 0fee37c into main Nov 10, 2026
4 of 8 checks passed
@karhu
karhu deleted the tnet-157/9-sources 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