Skip to content

feat(registry)!: copy a release between registries - #649

Merged
karhu merged 5 commits into
mainfrom
tnet-157/3-copy-release
Nov 10, 2026
Merged

karhu merged 5 commits into
mainfrom
tnet-157/3-copy-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 timenet.registry.copy_release(), which copies a release between registries and never changes a published version.

Details

Builds, downloads and publishing all copy releases between registries. Other releases with inheritance pin the files of a published version, so a copy must not change it.

  • timenet.registry.copy_release(reference, source, dest, *, with_payload_sources=True, progress=None) copies a release and the payload sources that its manifest lists into another registry. With with_payload_sources=False, it copies the release alone.
  • BaseRegistry.download_release(reference, directory, ...) is public now. It takes the release as a DatasetRef. A failed download keeps the old copy.
  • BaseRegistry.find_manifest() returns the manifest of a release. For a release that the registry does not hold, it returns None. LocalRegistry.damage() returns the error for the first missing or resized file of a stored release, or None.
  • TimeNetVersionConflictError reports another build of the same version, with the release, the registries and the difference ("files" or "card") as attributes.
  • The typed progress events StepStarted and StepFinished live in timenet.progress. copy_release() is their first sender.

Design decisions

  • A published version cannot change. copy_release() compares the whole manifest of each release with timenet.manifest.same_release(), without the build environment. The file records (path, size and checksum) come first, then the card and the rest of the manifest. If the manifests are the same, nothing happens, or a local destination gets the release again from the source. If they differ, TimeNetVersionConflictError stops the copy before anything is written, and its message asks for a new version. An overwrite breaks every release that pins the old files.
  • A release needs exactly the releases that its manifest lists as payload sources, never the payload sources of a payload source. A payload source is a release whose payload files the copied release reads, and the manifest lists every such release, at any depth. So a read in dest needs no more. Sources come first, and each release is copied once and alone.
  • One walker, find_payload_sources() in timenet.registry.dependencies, applies this rule and the one access check. The planner and the download plan later in the stack use the same walker. A walk of one level cannot loop, so the copy needs no cycle check.
  • A source can lack a payload source that the destination holds complete and with the pinned files. Then the copy uses the copy in the destination. A build can then copy a release from a registry that lacks a payload source that the store already holds or just built.
  • Every check runs before the first copy. This includes access: BaseRegistry.requires_open_access is true by default and false on LocalRegistry. The copy refuses a release that is not open when dest, or the source that it fetches from, requires open access.
  • Between two non-local registries, only the release that is missing is downloaded.
  • The step events carry the release and the action, not a step object. Their shape stays the same while the step type of the coordinator grows in later PRs.

Breaking changes

  • BaseRegistry._download_release(dataset_id, version, dest_dir, ...) is now download_release(reference, directory, ...). A registry of another package that overrides it must rename the override.
  • TimeNetMissingParentError takes (missing, needed_by, searched, *, payload_source). Before, payload_source was the third positional argument.

Stack

Changes later in the stack:

Merge message

feat(registry)!: copy a release between registries

copy_release() copies a release between registries and keeps published
versions unchanged, because child releases pin their files.

- A release needs exactly the payload sources that its manifest lists.
  The copy takes each one once, before the release, and a complete copy
  in the destination can supply a missing one. find_payload_sources()
  applies this rule.
- The copy compares the whole manifest without the build environment,
  and it checks access before it copies anything. Another build of the
  same version raises TimeNetVersionConflictError.
- A complete copy is reused, a damaged local copy is fetched again, and
  a failed download keeps the old copy.
- Each copy sends StepStarted and StepFinished events.

BREAKING CHANGE: BaseRegistry._download_release() becomes
download_release(reference, directory, ...), with a DatasetRef in place
of dataset_id and version. Registry overrides use the new signature.

BREAKING CHANGE: TimeNetMissingParentError takes searched as its third
argument and payload_source as a keyword-only argument.

Comment thread docs/registry.md Resolved
Comment thread docs/registry.md Resolved
Comment thread docs/types.md Resolved
Comment thread packages/timenet/src/timenet/errors.py Resolved
@karhu
karhu marked this pull request as ready for review November 4, 2026 12:25
@karhu
karhu requested a review from TimoStoff November 4, 2026 12:25
@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 3e30447 · run #8360

@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! Minor nits, but please fix them docstrings!

@karhu
karhu force-pushed the tnet-157/3-copy-release branch from a4ec699 to b22699a Compare November 5, 2026 20:07
@karhu
karhu force-pushed the tnet-157/3-copy-release branch from b22699a to 2c9667e Compare November 6, 2026 07:54
@karhu
karhu removed this pull request from stack #650 November 6, 2026 08:09
@karhu
karhu added this pull request to stack #699 November 6, 2026 08:10
@karhu
karhu force-pushed the tnet-157/3-copy-release branch 2 times, most recently from ade7cd5 to ebe933c Compare November 7, 2026 15:20
@karhu karhu changed the title feat(registry): copy a release without changing a published version feat(registry)!: copy a release between registries Nov 9, 2026

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

Good direction, thank you! But please let's give it a bit more solid and declarative design; we are building a release system here and can have classes and well-defined interfaces to realize this

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

As discussed, accepting to unblock : ) We can clean up later

@karhu
karhu force-pushed the tnet-157/3-copy-release branch from ebe933c to 83d5205 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/3-copy-release branch from 83d5205 to 908925d 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 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 removed this pull request from stack #699 November 10, 2026 15:58
@karhu
karhu changed the base branch from tnet-157/2-put-release to main November 10, 2026 16:07
@karhu
karhu force-pushed the tnet-157/3-copy-release branch from 908925d to 1225d48 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 skipped: your organization has reached its API usage limit, so Code Review can't run right now.

An organization admin can review the limit in your organization's usage and billing settings.

Once the limit resets or is raised, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

copy_release(reference, source, dest) copies a release and each payload
source that its manifest lists into another registry. A release needs
exactly those releases, never the payload sources of a payload source.
With with_payload_sources=False, the copy takes the release alone.
Sources come first, and each release is copied once. A published
version cannot change: if the destination holds the same file records,
nothing happens, or a local destination gets the release again from the
source. If it holds other files, TimeNetVersionConflictError stops the
copy before anything is written. Every check runs before the first copy,
access included: a registry that is not local serves files only for
releases whose card and lineage declare access open. Between two
registries that are not local, only the missing release is downloaded.

find_payload_sources() in timenet.registry.dependencies applies this
rule and the access check. It finds each payload source in the registry
to copy from, else as a complete copy in the destination.

The typed progress events live in timenet.progress. The step events
carry the release and the action, so their shape stays the same while
the coordinator's step type grows.

Part of TNET-157.
…or users

copy_release() is now part of the public names of timenet.registry, next to LocalRegistry. Callers
import it with `from timenet.registry import copy_release`. Its module is now the private
timenet/registry/_copy.py, like _paths.py.

The module docstring describes the copy in plain words and no longer lists the internal steps.
The docstrings of this change say "non-local" for a registry that is not local, and the TimeNetVersionConflictError text says that a registry "already holds" the
release.

Part of TNET-157.
…he copy docstrings

BaseRegistry.requires_open_access tells whether a registry holds and
serves only releases with open access. The access checks use it instead
of a test for LocalRegistry.
BaseRegistry.download_release() is public.
TimeNetMissingParentError takes payload_source as a keyword argument.
timenet.progress lists only the events that a copy sends.
The docstrings of copy_release() are shorter.

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

Part of TNET-157.
A copy compared only the file records with the copy in the destination.
So a rebuild with the same files and another card or lineage counted as
the same release, and the destination kept the old card. Now the copy
compares the whole manifest without its build environment.
TimeNetVersionConflictError names the difference: "files" or "card".

A forced download deleted the old copy before it renamed the new one in
place. If the rename failed, no copy was left. Both download paths now
stage the files with staged_directory(), which keeps the old copy until
the new one is in place.

download_release() takes a DatasetRef, as copy_release() does. The
registries get find_manifest(), which returns None for a release that a
registry does not hold, and LocalRegistry.damage(), which names the
first missing or resized file. The copy uses both instead of its own
checks.

Part of TNET-157.
@karhu
karhu force-pushed the tnet-157/3-copy-release branch from 1225d48 to 3e30447 Compare November 10, 2026 17:41
@karhu
karhu merged commit a27670e into main Nov 10, 2026
4 checks passed
@karhu
karhu deleted the tnet-157/3-copy-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.

3 participants