v0.2.0 — lightweight loader, publish CLI, wheel-installable build pipeline
Added
raincloudloader package. A new importable package
(separate from thescripts/build pipeline) for loading
already-prepared artefacts.raincloud.load("<slug>")(alias
load_dataset) returns a lazyDatasethandle — nothing is fetched
until you call.path()/.to_arrow()/.scan()/.to_pandas().
Resolution order is local cache → mirror → local build: a cache
hit short-circuits, otherwise it pulls from the configured mirror,
and only on a cache+mirror miss does it shell out to
scripts.pipeline.build. Configured via env vars:RAINCLOUD_MIRROR
(anfsspecbase such ass3://bucket/prefixorfile:///path—
a private/internal artefact store, not a public Raincloud endpoint),
RAINCLOUD_CACHE(cache dir override),RAINCLOUD_OFFLINE
(cache-only; mirror/build misses raise),RAINCLOUD_STRICT_CHECKSUM
(opt-in hard integrity gate; see below). When the snapshot records a
checksum, a drift from it warns-and-adopts by default (see "Drift is an
alert"); where no checksum is recorded yet — most of the catalog today —
the pinned byte size is used as a cheap corruption check instead.scripts.pipeline.publishmirror-sync CLI.
python -m scripts.pipeline.publish <slugs|--all> --mirror <url>
uploads builtoutputs/v1/...artefacts to a mirror, gated on each
artefact's on-disk sha256 matchingdocs/v1/snapshot.json(slugs with
no recorded sha are uploaded ungated). Each upload streams to a
<key>.<uuid>.parttemp key and renames into place, so a mid-stream
crash never leaves a truncated object at the canonical key. The
snapshot is resolved via the sameRAINCLOUD_SNAPSHOT→ checkout →
wheel precedence the loader uses, not a hardcoded checkout path.
--dry-runpreviews the upload plan.parquet_sha256/vortex_sha256indocs/v1/snapshot.json—
per-slug artefact checksums, used by both the loader (download
integrity) andpublish(the snapshot-match gate).examples/use_loader.py— runnable walkthrough of the loader API
(metadata access,.to_arrow/.scan/.to_pandasmaterialization,
format override, env-var configuration, the full
RaincloudErrorhierarchy). Runs against the packaged catalog with
no network;--materializeexercises the full resolution path.- Code-path example scripts in
examples/. Single-file demos that
load()a real catalog dataset and run a query:nyc_taxi_tip_rate.py
(DuckDB over.scan()on 48.7M yellow-cab trips — what share left no
recorded tip, bypayment_type),kepler_exoplanets.py(pandas
disposition counts + smallest confirmed planet),wine_quality_correlations.py
(feature↔quality correlations), andolympic_medals.py(medals by NOC /
decade).tests/test_examples.pybyte-compiles every example and (under
--run-network) runs the kepler one end-to-end. - New agent skills:
raincloud-loadandraincloud-publish. Wrap
the loader API and the publish CLI respectively, matching the
existingraincloud-*skill conventions (name-only,
disable-model-invocation: true).
Changed
-
examples/is now runnable demos; authoring templates moved to
templates/.minimal_spec.jsonandstreaming_handler.py.tmpl(config
templates for adding a source) live under the new top-leveltemplates/;
examples/is reserved for code-path scripts that use theraincloud.load
API. Doc and skill references updated accordingly. -
Packaging: the project is now a hatchling-built, installable
package (installed from GitHub:pip install "raincloud @ git+https://github.com/spiraldb/raincloud", not PyPI). The wheel force-includes
docs/v1/snapshot.jsonandsources.jsonas packaged data under
raincloud/_data/, so the catalog resolves with no repo checkout. -
BREAKING (install): the heavy build toolchain moved out of the base
dependency set into the[build]extra. A bareuv sync --inexact
(or apip installfrom the GitHub repo) now installs only the lightweight loader
(pyarrow,numpy,vortex-data,fsspec); building datasets
requiresuv sync --extra build --inexact(duckdb, pandas, osmium,
pyreadstat, openpyxl, py7zr, unlzw3, zstandard, jsonschema). Transport
backends are per-scheme extras ([s3]→ s3fs,[http]→ aiohttp;
file://needs neither);[duckdb]/[pandas]back
Dataset.scan()/.to_pandas(). This does not change the
no-redistribution posture inDISCLAIMER.md. -
Drift is an alert, not a blocker. When a slug's sha256 is pinned
in the snapshot and the mirror or local build produces different
bytes, the loader now prints[raincloud] WARN: <slug> from <origin> sha256 drifted ...to stderr and adopts the new bytes anyway.
Upstream content changes are common and benign; the build should
still work, with the user informed. The loader's mirror-fetch path is
the strict-capable caller — underRAINCLOUD_STRICT_CHECKSUMit passes
_cache.adopt(..., strict=True)so a mirror mismatch raises
ChecksumMismatch. (scripts.pipeline.publishis a separate gate: it
refuses to upload via its ownPublishMismatch, not throughadopt.)
Adopted bytes are recorded in a.<name>.pinsidecar — the snapshot sha
reconciled against, the on-disk size, and the origin (mirror/build)
— so later loads serve them straight from cache; a genuine snapshot
revision (the pinned sha changed) still re-fetches, and a post-adoption
size change still falls through to a fresh fetch. In the default
(non-strict) mode a sha-present cache hit is served on a byte-size match
without rehashing (the multi-GB rehash-avoidance fast path), so a
same-size on-disk content swap isn't caught until strict mode forces a
rehash.Set
RAINCLOUD_STRICT_CHECKSUM=1to opt the loader into a hard gate:
for a sha-pinned slug from the mirror, a mismatch on download AND on a
cache hit raisesChecksumMismatch(the cached file is rehashed each
load, catching even same-size tampering). Sha-less slugs have nothing to
rehash against, so strict leaves their size/pin corruption check
unchanged. The local build path is never strict-gated against the
maintainer's sha: a client's rebuild legitimately differs (columnar
output is rarely bit-reproducible), so the built artefact is tagged
origin=buildin its pin and served from cache by that provenance —
even under strict — instead of being rebuilt every load. It is rebuilt
only when the snapshot pin it was built against changes (the source of
truth moved) or the cached file is corrupted. For full cryptographic
integrity, point strict deployments at a mirror.
Fixed
- Wheel-install path crashes after long-running work.
scripts/pipeline/hydrate.py(success-log + the FileNotFoundError
message),
scripts/pipeline/tighten_variant.py(workdir +
three log lines),
scripts/pipeline/overnight_profile.py
(LOG_PATH,STATE_PATH,_slug_already_built,_wipe_slug,
manifest loader),
scripts/pipeline/list_datasets.py, and
scripts/pipeline/browse.pyall routed through
REPO_ROOT / "outputs/..."or.relative_to(REPO_ROOT)— fragile
under wheel installs and anyRAINCLOUD_HOME/RAINCLOUD_OUTPUTS/
RAINCLOUD_WORKDIRredirect, where it raisedValueErrorat the
tail of a multi-hour build. All call sites now use the env-aware
display_path()/outputs_root()/raw_downloads_root()/
workdir_root()helpers. New
tests/test_pipeline_path_hermeticity.pygreps the pipeline package
on every test run and fails on regressions. - Build-availability now distinguishes a missing extra from a broken
install._resolvecaptures whyscripts.pipeline.buildcan't be
imported: a plainImportError/ModuleNotFoundError(the[build]
extra isn't installed) still yields the "installraincloud[build]"
hint, but any other module-init failure (a handler raising at top
level, a malformed packaged manifest) now surfaces the real exception
in theBuildToolingMissingmessage instead of misdirecting the user
to apip installthey've already done. - Local build failures are typed. A non-zero
scripts.pipeline.buildsubprocess now raisesBuildFailed
(aRaincloudError) instead of leaking a raw
subprocess.CalledProcessErrorpast the loader's typed-error contract. read_pinrejects non-object JSON. A torn/partial or tampered
.pinsidecar containing valid-but-non-dict JSON (42,[...]) now
returnsNonerather than a value whose later.get(...)would raise
AttributeErrorinsideresolve().- Sha-less cache files are size-checked, not trusted on existence.
For a slug with no pinned sha (most of the catalog), a cache hit serves
via the adoption pin or a snapshot-byte-size match; a cached file whose
size diverges from the snapshot with no pin vouching for it is treated
as corruption and re-fetched, rather than served on mere existence. - Catalog parquet visibility for snapshot-only slugs. When a slug
is indocs/v1/snapshot.jsonbut absent fromsources.json
(legacy / deprecated entry still on a mirror),Catalog.entry()now
exposes the parquet format via the same
snap.get('parquet_bytes') is not Noneclause that already covered
vortex. Loadable now; previously raisedFormatUnavailable. Dataset.scan()stderr note. When a slug was loaded as vortex
butscan()needs the parquet sibling (DuckDB has no Vortex
reader), the loader prints[raincloud] scan() needs parquet but <slug> was loaded as 'vortex'; resolving parquet sibling ...before
the resolve, so an implicit mirror fetch isn't a surprise..parttmp race fixed._resolve.resolvenow writes to
f".<name>.<pid>-<uuid8>.part"(per-process unique) and sweeps
stale.partsiblings older than six hours on each resolve, so
concurrent loaders no longer clobber each other's in-flight writes
and SIGKILL leftovers don't accumulate.
Performance
- Cache-hit skips sha256 rehash when artifact size matches the
snapshot._resolve.resolveuses
dest.stat().st_size == FormatInfo.nbytesas the fast path — a full
sha256 over multi-GB artifacts on every load defeated the cache.
Cuts repeat-load cost on the 34 GB Wikipedia parquet from minutes
to milliseconds. The full rehash only runs when size disagrees and
the pin sidecar doesn't already vouch for the file; a same-size,
different-content snapshot revision is the one case the size fast-path
can't distinguish (an accepted blind spot, same as before). - Snapshot regen reuses prior sha when size unchanged.
docs.py
snapshot regen (_sha256_or_reuse) skips re-streaming an artifact
when its bytes-on-disk match the prior snapshot's recorded size and
the prior sha is known. A full-catalog regen on 250 slugs
(including the multi-GB heavyweights) drops from hours to seconds
when nothing's changed. Pass--rehash(python -m scripts.pipeline.docs snapshot --rehash) to force a full recompute —
needed only in the rare case a rebuild changed an artifact's content
without changing its byte length, which would otherwise wedge
publish's checksum gate; unlike--overwrite-missingit preserves
prior data for slugs not built this run.