Repository navigation
feat(registry)!: publish a local release with put_release - #648
Conversation
✅ CI checks passed
Updated for |
512f7e8 to
4aa5bfa
Compare
865e703 to
24e84af
Compare
TimoStoff
left a comment
There was a problem hiding this comment.
LGTM! THere are some nits, so please do look at them before landing :)
|
Mostly opportunities to delete more! SO you should be happy |
4aa5bfa to
5075df5
Compare
f5b4644 to
5507f11
Compare
06721f0 to
4a056ff
Compare
5507f11 to
26387eb
Compare
26387eb to
8f3725f
Compare
4a056ff to
25a52fd
Compare
8f3725f to
a8cf860
Compare
25a52fd to
7ca6140
Compare
7ca6140 to
3527f95
Compare
a8cf860 to
1e73239
Compare
RealLast
left a comment
There was a problem hiding this comment.
LMGTM, thanks! A few nits I'd like to see addressed please on ownership of functionality
RealLast
left a comment
There was a problem hiding this comment.
As discussed, accepting to unblock : ) We can clean up later
3527f95 to
a54a43b
Compare
1e73239 to
22f28b9
Compare
There was a problem hiding this comment.
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/src/timenet/registry/writable.py— A caller ofput_releasecan publish a release whose payload sources the destination lacks; it then fails on every open.put_datasetnever publishes such a release._read_releaseonly checksmanifest.files.all_files()sizes and never looks atmanifest.payload_sources. The local, remote and S3put_releasethen publish without the per-source checkTimeFWriter(registry=self)runs. Fix: before publishing, have_read_release(or eachput_release) check every entry ofmanifest.payload_sourcesagainstself, the way the writer does.Why this was flagged
Trigger: a release directory built in another registry whose manifest records
payload_sources(reused parent files), passed toLocalRegistry.put_release(local.py:503),S3Registry.put_release(s3.py:790) orRemoteRegistry.put_release(remote/registry.py:605).WritableRegistry._read_release(writable.py:967-971) only runscheck_file_sizesonmanifest.files.all_files(), so a missing parent does not stop the publish. The release is copied or uploaded and committed. Every later open then fails: locallyopen_local_releaseraisesTimeNetMissingParentErrororTimeNetDatasetNotFoundError(dependencies.py:101-103). Before this change the only write path wasstore(), whereTimeFWriter(..., registry=self)(writer.py:146-153) checks each payload source in the target registry before anything is published. The PR's design note only rules out comparing against an existing copy of the same release, not checking parents.Verification: Severity: nit.
WritableRegistry._read_release(writable.py) only runsread_manifest(directory / MANIFEST_FILE)andcheck_file_sizes(version, manifest.files.all_files(), owner=...). Nothing in_read_releasereadsmanifest.payload_sources. Later opens go throughattach_payload_sources(dependencies.py:134-138), which raisesTimeNetMissingParentErrorfor a missing parent. The only caller ofput_releasein the repo isLazyReadRegistry.put_dataset(lazy_read.py:72). -
🟡
packages/timenet/src/timenet/registry/local.py— A caller that replaces a release withput_releasecan keep getting the old manifest from the sameLocalRegistry, so the new files are read through stale records.put_releasecopiesmanifest.jsonwithshutil.copy2, which keeps the source file's mtime. The manifest cache at local.py:103-105 only checksst_mtime, so a new manifest with the same mtime counts as a cache hit. Fix: drop the_manifest_cacheentry for the reference afterpublish_directory, or copy the manifest without its metadata (shutil.copy), so every publish shows the new manifest.Why this was flagged
The trigger is a
LocalRegistryinstance that read a release (which cached(mtime, manifest)at local.py:113) and then getsput_release(from_directory)for the same dataset@ version. The new manifest's source has the same mtime as the old one. This is common when releases come from archives with a fixed mtime (reproducible tarballs with SOURCE_DATE_EPOCH,rsync -a,cp -p). The copy loop input_releaseat local.py:211-214 usesshutil.copy2, which keeps the source mtime. It never invalidatesself._manifest_cache. The nextget_manifest/open_versioncompares onlypath.stat().st_mtime(local.py:103-105) and returns the old cached manifest for the new release directory. Its file records (sizes, checksums, paths) then no longer match the files on disk. On the base branch the only local write path was the writer, which writes a new manifest with a current mtime, so the mtime check always caught a replacement.Verification:
get_manifest(local.py:103-106) returns the cached manifest whencached[0] == mtime. The newput_releasecopies withshutil.copy2at local.py:211-214, which keeps the source mtime, and never drops_manifest_cache[(dataset_id, version)]. On the base, every local write went throughTimeFWriterand produced a fresh mtime.
WritableRegistry.put_release(directory, manifest) publishes a complete local release on the local, remote and S3 registries. Before anything is published, it makes sure that manifest.json is there and equals the given manifest, and that every declared file has its recorded size. It publishes only the declared files. It does not compare with a copy that the registry already holds. The remote and S3 store() now compile into staging and call put_release(). The local registry and the writer share one helper that swaps a staged copy into place. If the swap fails, the helper moves the old copy back. WritableRegistry has a new abstract method, so a subclass outside this repository must implement it. Part of TNET-157.
…ut of put_release() WritableRegistry.store() is now put_dataset(), with the same parameters, on the local, remote and S3 registries and in the test fakes. It turns a TimeFDataset in memory into TimeF files and publishes them. The new name pairs it with put_release(), which publishes a release that is already on disk. Every call site, test name and docstring uses the new name. The first parameter of put_release() is now from_directory. The name and the docstring say that the registry only reads that directory and does not change it. TimeFWriter.close() no longer has a comment about what its callers pass for replace. BREAKING CHANGE: WritableRegistry.store() is now put_dataset(), and the directory parameter of put_release() is now from_directory. There is no alias. Code that calls store(), or passes directory= to put_release(), must use the new names. A subclass of WritableRegistry must rename both. Part of TNET-157.
…ctory put_release() reads manifest.json from the release directory. A missing or unreadable manifest raises TimeFFormatError. put_dataset() and put_release() return the DatasetRef of the stored release. publish_pipeline() passes pinned_by to put_dataset() directly. publish_directory() moves to the private module timenet._fs, with its own tests. The docstrings of the registry write methods are shorter. BREAKING CHANGE: put_release(from_directory) takes no manifest; put_dataset() and put_release() return a DatasetRef. Part of TNET-157.
Shorter docstrings for the code that this change adds or changes. Part of TNET-157.
A cut or invalid manifest.json made reads, copies and lists of a local registry fail with a raw pydantic ValidationError and a traceback. The new read_manifest() and parse_manifest() in timenet.manifest raise TimeFFormatError instead. The message names the file, or the release and the registry, and the first error. Every place that parses a stored manifest uses them. The remote and the S3 registry had two identical copies of put_dataset(). One copy in LazyReadRegistry now serves both. Part of TNET-157.
22f28b9 to
3719fb9
Compare
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
Splits out the upload part from the current locally-store-then-upload flow of releasing an in-memory dataset to a registry. This allows reusing it for uploads from a version already on disk.
Details
Today the remote and S3 registries can only compile a dataset and upload the result inside
store().WritableRegistry.put_release(from_directory)publishes a complete prebuilt local release on the local, remote and S3 registries.manifest.jsonfrom the directory and makes sure that every declared file has its recorded size. It publishes only the declared files.store()becomesput_dataset(), so the two write methods pair up:put_dataset()takes an in-memory dataset,put_release()a release that is already on disk.put_release()only readsfrom_directory.put_dataset()andput_release()both return theDatasetRefof the stored release.put_dataset()now compile into staging and callput_release(). The local registry and the writer share one helper,publish_directory()intimenet/_fs.py. It swaps a staged copy into place. If the swap fails, it moves the old copy back.timenet.manifestgetsread_manifest()andparse_manifest(). Every registry reads a stored manifest through them. A manifest that cannot be read or does not parse raisesTimeFFormatError, and the message names the file or the release.Design decisions
put_release()does not compare with a copy that the destination already holds. The copy function in the next PR decides whether a destination can receive a version.manifest.jsonraisesTimeFFormatError. A missing file or a wrong size raisesTimeNetFileError.Breaking changes
WritableRegistryhas a new abstract method,put_release(). A subclass outside this repository must implement it.WritableRegistry.store()is nowput_dataset()on every registry, with the same parameters. Rename each call.TimeFFormatError, not a pydanticValidationError.Stack
Changes later in the stack:
Merge message