Skip to content

Commit f3b4136

Browse files
mprammerclaude
andauthored
Add lightweight raincloud loader, publish CLI, and wheel-installable build pipeline (#15)
Adds an importable `raincloud` loader (lazy `Dataset` over prepared Parquet/Vortex, resolving cache → mirror → local build), a `scripts.pipeline.publish` mirror-sync CLI gated on the snapshot sha256, and a configurable data-area root so the build pipeline runs from a wheel install. The install is layered: a bare install is loader-only; the heavy build toolchain moved behind the `[build]` extra. See the CHANGELOG 0.2.0 entry for env vars, resolution order, and integrity semantics. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: mprammer <martin@spiraldb.com>
1 parent 854b09b commit f3b4136

74 files changed

Lines changed: 4769 additions & 202 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Everything an AI coding agent needs to operate inside this repo. The `.claude
88
|---|---|
99
| [`settings.json`](settings.json) | **Tracked** allow-list of safe, read-only Bash / git / WebFetch / pipeline commands. A fresh-clone agent gets these pre-approved so it doesn't burn turns on permission prompts. Side-effecting stages (build / fetch / extract / convert / tighten_variant) are intentionally *not* pre-approved here. |
1010
| `settings.local.json` | **Gitignored** per-machine override — additional permissions specific to the local agent's session. Don't commit. |
11-
| [`skills/`](skills/) | 16 invokable skills following the [Agent Skills](https://agentskills.io) standard — wrappers around pipeline entrypoints (`/raincloud-build`, `/raincloud-fetch`, `/raincloud-status`, `/raincloud-validate-manifest`, `/raincloud-list-datasets`, …) and procedural playbooks (`/raincloud-add-dataset`, `/raincloud-add-handler`, `/raincloud-debug-build`, …). See [`skills/README.md`](skills/README.md). |
11+
| [`skills/`](skills/) | 21 invokable skills following the [Agent Skills](https://agentskills.io) standard — wrappers around pipeline entrypoints (`/raincloud-build`, `/raincloud-fetch`, `/raincloud-status`, `/raincloud-validate-manifest`, `/raincloud-list-datasets`, `/raincloud-load`, `/raincloud-publish`, …) and procedural playbooks (`/raincloud-add-dataset`, `/raincloud-add-handler`, `/raincloud-debug-build`, …). See [`skills/README.md`](skills/README.md). |
1212
| [`context/`](context/) | Symlinks back to the repo-root canonical docs (`AGENTS.md`, `SKILLS.md`, `README.md`, `sources.schema.md`) so each `SKILL.md` can pull authoritative guidance via a stable relative path without copying. |
1313
| `scheduled_tasks.lock` | Gitignored — agent-runtime state. |
1414

.agents/skills/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ Wrappers around `python -m scripts.pipeline.<module>`. Side-effecting ones set `
2020
| `/raincloud-status` | `scripts.pipeline.status` | Per-slug filesystem state (raw / workdir / parquet / vortex / variant-pending). *(read-only, model-invocable.)* |
2121
| `/raincloud-validate-manifest` | `scripts.pipeline.validate_manifest` | Static checks for `sources.json` — JSON Schema + handler-registry / slug-uniqueness / fetch-auth cross-checks. *(read-only, model-invocable.)* |
2222
| `/raincloud-list-datasets` | `scripts.pipeline.list_datasets` | Filter/list slugs by handler / license / fetch-type / reader / vortex / tag / showcase / size / regex. *(read-only, model-invocable.)* |
23+
| `/raincloud-discover` | `scripts.pipeline.list_datasets` | Find "interesting" datasets via the discoverability flags — tag / showcase / size / trait / view. *(read-only, model-invocable.)* |
24+
| `/raincloud-profile` | `scripts.pipeline.profile` | Compute per-column statistics → `outputs/v1/<slug>/profile.json` (opt-in; feeds the TUI detail pane + `list_datasets --inspect`). *(writes `profile.json`; model-invocable.)* |
25+
| `/raincloud-load` | `raincloud.load` (loader API) | Load a prepared dataset (cache → mirror → local build) as a lazy `Dataset`; inspect metadata or materialize. *(`disable-model-invocation: true`.)* |
26+
| `/raincloud-publish` | `scripts.pipeline.publish` | Sync built `outputs/v1/...` artefacts to a mirror, gated on the snapshot sha256. *(side-effecting — `disable-model-invocation: true`.)* |
2327

2428
## Procedural playbooks (model-invocable)
2529

.agents/skills/raincloud-add-dataset/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Steps:
1313
- The license — must permit redistribution-of-derivatives. Check SPDX ID and `source_url`.
1414
- Approximate row count (used for `expect.rows`; can be `null` on first build).
1515

16-
2. **Append a `DatasetSpec` to `sources.json`** using the Python load-edit-dump pattern from [AGENTS.md](../../context/AGENTS.md#safe-ways-to-edit-sourcesjson) — never `sed`. Start from [`examples/minimal_spec.json`](../../../examples/minimal_spec.json) (every field present with placeholder values) rather than typing one from scratch. Minimal direct-HTTP shape:
16+
2. **Append a `DatasetSpec` to `sources.json`** using the Python load-edit-dump pattern from [AGENTS.md](../../context/AGENTS.md#safe-ways-to-edit-sourcesjson) — never `sed`. Start from [`templates/minimal_spec.json`](../../../templates/minimal_spec.json) (every field present with placeholder values) rather than typing one from scratch. Minimal direct-HTTP shape:
1717

1818
```jsonc
1919
{

.agents/skills/raincloud-add-handler/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Steps:
2020

2121
- `parsed` contains one `(path, table)` tuple per parsed file; `table` is `None` when `parse.reader = "custom"`.
2222
- Return `[(output_slug, table), ...]` — one tuple per output parquet. Multi-output handlers emit several slugs from one source (see `glove_split`, `osm_pbf_split`, `stack_exchange_split`).
23-
- **Streaming handlers** (write direct to parquet, bypass the write stage) return `[]`. Copy [`examples/streaming_handler.py.tmpl`](../../../examples/streaming_handler.py.tmpl) as the starting point — it has the `outputs_root()` / `duckdb_connect()` / cleanup wiring already shaped — and study `factbook_variant_parse`, `wikipedia_variant_parse`, `lichess_pgn_parse` for upstream-shape variations.
23+
- **Streaming handlers** (write direct to parquet, bypass the write stage) return `[]`. Copy [`templates/streaming_handler.py.tmpl`](../../../templates/streaming_handler.py.tmpl) as the starting point — it has the `outputs_root()` / `duckdb_connect()` / cleanup wiring already shaped — and study `factbook_variant_parse`, `wikipedia_variant_parse`, `lichess_pgn_parse` for upstream-shape variations.
2424

2525
2. **Register** in `scripts/pipeline/handlers/__init__.py`:
2626

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
name: raincloud-load
3+
description: Load a Raincloud dataset via the lightweight Python loader API (`raincloud.load(slug)`). Use when the user wants to read a prepared parquet/vortex artifact, inspect a slug's metadata, or pull a dataset for downstream analysis from cache → mirror → local build.
4+
argument-hint: <slug> [--format vortex|parquet] [--materialize to_arrow|scan|to_pandas]
5+
disable-model-invocation: true
6+
allowed-tools: Bash(python -c *), Bash(python -m raincloud *), Bash(python examples/use_loader.py *)
7+
---
8+
9+
The Raincloud loader is a separate, lightweight Python package (`raincloud`) for reading **already-prepared** artifacts. Resolution order is `local cache → mirror → local build`; nothing is fetched until you call an accessor.
10+
11+
## Most common shape
12+
13+
```bash
14+
python -c "
15+
import raincloud
16+
ds = raincloud.load('$ARGUMENTS') # default format='vortex' with parquet fallback
17+
print('rows :', ds.num_rows)
18+
print('cols :', ds.column_names[:5])
19+
print('source :', ds.info.get('source_url'))
20+
print('path :', ds.path()) # triggers cache/mirror/build resolution
21+
"
22+
```
23+
24+
## Materialization
25+
26+
| Accessor | Returns | Notes |
27+
|---|---|---|
28+
| `ds.path()` | `pathlib.Path` to the on-disk artifact | First call resolves; subsequent calls are cache hits. |
29+
| `ds.to_arrow()` | `pyarrow.Table` | Materializes the whole table; expensive on multi-GB slugs. |
30+
| `ds.to_vortex()` | `vortex.VortexFile` (lazy) | Vortex-native handle. |
31+
| `ds.scan()` | `duckdb.DuckDBPyRelation` | Requires `raincloud[duckdb]`. Always reads the parquet sibling — if the slug was loaded as vortex, you'll see a `[raincloud]` stderr note before the parquet is resolved. |
32+
| `ds.to_pandas()` | `pandas.DataFrame` | Requires `raincloud[pandas]`. |
33+
| `ds.schema` | `pyarrow.Schema` | Footer-only read for parquet; opens the file for vortex. |
34+
35+
## Config via env vars
36+
37+
| Env var | Effect |
38+
|---|---|
39+
| `RAINCLOUD_CACHE` | Local artifact cache dir (default `~/.cache/raincloud`). |
40+
| `RAINCLOUD_MIRROR` | Mirror URL (`s3://…`, `https://…`, `file://…`). Tried before the local build. |
41+
| `RAINCLOUD_OFFLINE=1` | Block mirror + build; raise `OfflineMiss` on cache miss. |
42+
| `RAINCLOUD_SNAPSHOT` | Override `docs/v1/snapshot.json` (catalog). |
43+
| `RAINCLOUD_MANIFEST` | Override `sources.json`. |
44+
45+
## Drift semantics
46+
47+
When a slug has a `sha256` pinned in the snapshot and the mirror or local build produces bytes that disagree, the loader prints a `[raincloud] WARN: <slug> from <mirror|build> sha256 drifted ...` to stderr and adopts the new bytes anyway. Upstream content drifts; that's not a panic case. Catch + escalate via `raincloud.ChecksumMismatch` only if you want a hard gate (e.g. `_cache.adopt(..., strict=True)` — used by `python -m scripts.pipeline.publish`).
48+
49+
## Errors (all subclass `raincloud.RaincloudError`)
50+
51+
`UnknownSlug`, `FormatUnavailable`, `ArtifactNotFound`, `OfflineMiss`, `BuildToolingMissing`, `MissingDependency`, `ChecksumMismatch`.
52+
53+
## Worked example
54+
55+
```bash
56+
python examples/use_loader.py --slug $ARGUMENTS # metadata only
57+
python examples/use_loader.py --slug $ARGUMENTS --materialize # full path
58+
```
59+
60+
[`examples/use_loader.py`](../../../examples/use_loader.py) walks the full API end-to-end against the packaged catalog with no network.
61+
62+
## Install tiers
63+
64+
```bash
65+
pip install raincloud # lightweight loader
66+
pip install 'raincloud[duckdb]' # adds .scan()
67+
pip install 'raincloud[pandas]' # adds .to_pandas()
68+
pip install 'raincloud[s3]' # s3:// mirror transport
69+
pip install 'raincloud[http]' # https:// mirror transport
70+
pip install 'raincloud[build]' # adds local-build fallback (heavyweight)
71+
```
72+
73+
Context: [AGENTS.md "The loader package"](../../../AGENTS.md), [`examples/use_loader.py`](../../../examples/use_loader.py).
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
name: raincloud-publish
3+
description: Push locally-built Raincloud artifacts to a mirror (`scripts.pipeline.publish`). Use when the maintainer wants to upload one or more slugs' parquet/vortex bytes to a configured mirror after a successful build, gated on the snapshot's recorded sha256.
4+
argument-hint: <slug>... | --all --mirror <s3://… | https://… | file://…> [--dry-run]
5+
disable-model-invocation: true
6+
allowed-tools: Bash(python -m scripts.pipeline.publish *)
7+
---
8+
9+
Sync locally-built artifacts to a mirror so downstream `raincloud.load()` users can fetch them. The CLI verifies each artifact's sha256 against `docs/v1/snapshot.json` before upload — mismatches block the push (publish is the one place an integrity gate is correct: corrupted bytes should never reach a shared mirror).
10+
11+
## Most common shape
12+
13+
```bash
14+
python -m scripts.pipeline.publish $ARGUMENTS
15+
```
16+
17+
Selection (one required):
18+
- `<slug>...` — positional dataset slugs (any number).
19+
- `--all` — every slug whose parquet OR vortex artifact exists locally under `outputs/v{n}/<slug>/`.
20+
21+
Required:
22+
- `--mirror <url>` — destination root. Examples:
23+
- `s3://my-bucket/raincloud` (needs `pip install raincloud[s3]`)
24+
- `https://artifacts.example.com/raincloud` (needs `raincloud[http]`)
25+
- `file:///mnt/shared/raincloud-mirror` (built-in)
26+
- `RAINCLOUD_MIRROR=<url>` env var works as an alternative to the flag.
27+
28+
Modifiers:
29+
- `--dry-run` — print the upload plan (paths + keys) without writing. Always preview large publishes this way first.
30+
31+
## Before publishing
32+
33+
1. **Build the slug locally first** (`/raincloud-build <slug>` or `python -m scripts.pipeline.build <slug>`). Publish does not build; it only pushes what's already in `outputs/v{n}/<slug>/`.
34+
2. **Regenerate the snapshot** (`/raincloud-docs` or `python -m scripts.pipeline.docs`). The publish gate compares local bytes against `parquet_sha256` / `vortex_sha256` in `docs/v1/snapshot.json` — a stale snapshot causes false `PublishMismatch` failures.
35+
3. **Verify the mirror URL** with a dry-run first.
36+
37+
## What gets uploaded
38+
39+
For each slug × format pair (`parquet`, `vortex`) where a local artifact exists:
40+
- Key: `v1/<slug>/<fmt>/<slug>.<ext>`
41+
- Body: the raw artifact bytes
42+
- Gate: `sha256(local) == snapshot[<fmt>_sha256]` (raises `PublishMismatch` on disagree; an unpinned slug — `sha256` is `null` — uploads without verification, so prefer to regen the snapshot first).
43+
44+
## Failure modes
45+
46+
| Error | Meaning | Fix |
47+
|---|---|---|
48+
| `PublishMismatch: ...` | Local bytes diverge from the snapshot's recorded sha. | Re-run `/raincloud-docs` to refresh the snapshot, OR confirm the local artifact is correct and commit the new snapshot. |
49+
| `FileNotFoundError: outputs/v1/<slug>/...` | Slug isn't built locally. | Run `/raincloud-build <slug>` first. |
50+
| `ImportError: Install s3fs ...` | `s3://` mirror without `[s3]` extra. | `pip install 'raincloud[s3]'`. |
51+
52+
## After publishing
53+
54+
Downstream users can now `raincloud.load(<slug>)` and the loader will pull from the configured mirror (cache → mirror → build).
55+
56+
Context: [AGENTS.md "The loader package"](../../../AGENTS.md), [`scripts/pipeline/publish.py`](../../../scripts/pipeline/publish.py).

.github/workflows/ci.yml

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ on:
66
pull_request:
77
branches: [develop]
88

9+
# Minimal token scope: every job is read-only (checkout + deps + lint/tests);
10+
# none posts comments, uploads artifacts, or writes to the repo. Without this,
11+
# the GITHUB_TOKEN defaults to broad write scope (flagged by CodeQL).
12+
permissions:
13+
contents: read
14+
915
# Cancel in-progress runs when a new commit lands on the same branch.
1016
concurrency:
1117
group: ci-${{ github.ref }}
@@ -26,11 +32,14 @@ jobs:
2632
run: uv python install 3.11
2733

2834
# `--extra dev` brings in pytest + ruff. `--extra tui` brings in
29-
# textual so the test_browse suite runs instead of skipping. Skip
30-
# kaggle/huggingface — those deps are only needed at fetch time and
31-
# neither test path imports them.
35+
# textual so the test_browse suite runs instead of skipping. `--extra
36+
# build` brings in the heavy pipeline deps (duckdb, osmium, pyreadstat,
37+
# zstandard, jsonschema, …) — required because validate_manifest +
38+
# pytest collection import the handler registry, which transitively
39+
# pulls those. Skip kaggle/huggingface — those deps are only needed at
40+
# fetch time and neither test path imports them.
3241
- name: Install dependencies
33-
run: uv sync --extra dev --extra tui
42+
run: uv sync --extra dev --extra tui --extra build
3443

3544
- name: Lint (ruff)
3645
run: uv run ruff check
@@ -40,3 +49,38 @@ jobs:
4049

4150
- name: Test (pytest)
4251
run: uv run pytest -q
52+
53+
wheel:
54+
runs-on: ubuntu-latest
55+
steps:
56+
- uses: actions/checkout@v4
57+
- name: Install uv
58+
uses: astral-sh/setup-uv@v5
59+
with:
60+
enable-cache: true
61+
- name: Pin Python
62+
run: uv python install 3.11
63+
- name: Install dev deps (for pytest + vortex base dep used by fixtures)
64+
run: uv sync --extra dev
65+
# Scope collection to test_wheel.py: this env intentionally omits the
66+
# [build] extra, but pytest imports every collected module before -m
67+
# filtering, and test_manifest/test_profile import jsonschema (a [build]
68+
# dep) at module top. All wheel-marked tests live in test_wheel.py.
69+
- name: Run wheel tests
70+
run: uv run pytest --run-wheel -m wheel -v tests/test_wheel.py
71+
72+
realbuild:
73+
runs-on: ubuntu-latest
74+
continue-on-error: true # non-blocking: upstream flakiness never reds the build
75+
steps:
76+
- uses: actions/checkout@v4
77+
- name: Install uv
78+
uses: astral-sh/setup-uv@v5
79+
with:
80+
enable-cache: true
81+
- name: Pin Python
82+
run: uv python install 3.11
83+
- name: Install dev + build deps
84+
run: uv sync --extra dev --extra build
85+
- name: Run real-build network tests
86+
run: uv run pytest --run-network -m network -v

0 commit comments

Comments
 (0)