Skip to content

Commit f67b90d

Browse files
authored
Merge pull request #11 from spiraldb/mp/release-0.1.2
release 0.1.2: uv sync --inexact + TUI auto-installs upstream extras
2 parents 1575c06 + b233502 commit f67b90d

10 files changed

Lines changed: 122 additions & 35 deletions

File tree

.agents/skills/raincloud-add-kaggle-tos/SKILL.md

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

2626
Use the [Python load-edit-dump pattern](../../context/AGENTS.md#safe-ways-to-edit-sourcesjson) — never `sed`.
2727

28-
2. **Confirm Kaggle creds are set up:** `~/.kaggle/kaggle.json` with `chmod 600`, and the project synced via `uv sync --extra kaggle`.
28+
2. **Confirm Kaggle creds are set up:** `~/.kaggle/kaggle.json` with `chmod 600`, and the project synced via `uv sync --extra kaggle --inexact`.
2929

3030
3. **Try the first build via `/raincloud-build <slug> --loose`.** Pre-flight will print `kaggle (ToS-gated): ...`. Expect a 403 on the first try. The error message will point at the exact Kaggle URL the user must visit.
3131

.agents/skills/raincloud-build/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Modifiers:
2424
Before running:
2525
- **Confirm with the user** before triggering anything non-trivial. JSONBench 100M ≈ 6 h, Wikipedia Structured Contents → 34 GB parquet, OSM Germany ~45 min per kind. Small (<100 MB) parquets are fine without asking. (See [AGENTS.md "Rebuilding is expensive"](../../context/AGENTS.md).)
2626
- For large builds, set `RAINCLOUD_DUCKDB_MEMORY_LIMIT` and `RAINCLOUD_DUCKDB_TEMP_DIRECTORY` — see `/raincloud-large-build` for the full pattern.
27-
- For Kaggle/HF datasets, ensure `uv sync --extra kaggle` (or `--extra huggingface`) was run.
27+
- For Kaggle/HF datasets, ensure `uv sync --extra kaggle --inexact` (or `--extra huggingface --inexact`) was run. The `--inexact` flag is important: without it, syncing one extra removes the others.
2828

2929
After a successful build, suggest running `/raincloud-docs` to regenerate derived docs.
3030

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ On a fresh clone `outputs/` is empty — that's expected. The `outputs/v1/<slug>
1212
python -m scripts.pipeline.status --fast --missing-only
1313
```
1414

15-
It loads `sources.json`, walks the manifest, and prints per-slug filesystem state in seconds with no side effects. If it errors, fix the env (`uv sync`) before running any build.
15+
It loads `sources.json`, walks the manifest, and prints per-slug filesystem state in seconds with no side effects. If it errors, fix the env (`uv sync --inexact`) before running any build. Always pass `--inexact` to `uv sync`: without it, syncing one extra (e.g. `--extra dev`) silently uninstalls the others (kaggle, huggingface, tui), so a subsequent build of an HF/Kaggle slug will fail.
1616

1717
For a manifest sanity check that doesn't touch the filesystem at all:
1818

@@ -33,12 +33,12 @@ python -m scripts.pipeline.list_datasets --grep '\bgeo' --long
3333

3434
Filters compose with AND across `--family`, `--handler`, `--license`, `--fetch-type`, `--reader`, `--vortex` / `--no-vortex`, `--kaggle-tos`, `--grep`. Output modes: default (one slug per line), `--long` (wide table), `--json` (jq-friendly), `--count`.
3535

36-
If the user wants to *browse* interactively rather than query, point them at `python -m scripts.pipeline.browse` (read-only Textual TUI over the same data; requires `uv sync --extra tui`). It's a human-facing tool — don't try to run it from an agent context, since it won't render and will hang waiting for keystrokes.
36+
If the user wants to *browse* interactively rather than query, point them at `python -m scripts.pipeline.browse` (read-only Textual TUI over the same data; requires `uv sync --extra tui --inexact`). It's a human-facing tool — don't try to run it from an agent context, since it won't render and will hang waiting for keystrokes.
3737

3838
For a slightly broader regression net, the `tests/` directory carries a sub-second pytest smoke suite (manifest shape, schema self-consistency, handler registry, example template). Run it after any change to the manifest, the schema, or the handler registry:
3939

4040
```bash
41-
uv sync --extra dev # one-time — installs pytest
41+
uv sync --extra dev --inexact # one-time — installs pytest, preserves other extras
4242
pytest
4343
```
4444

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.2] - 2026-05-10
9+
10+
### Fixed
11+
12+
- All `uv sync` instructions across the docs (README, AGENTS, CONTRIBUTING,
13+
SKILLS, in-code install hints, and skill files) now pass `--inexact` so
14+
installing one extra no longer uninstalls the others. Without this, the
15+
documented sequential setup (`uv sync --extra tui` → bare `uv sync`
16+
`uv sync --extra huggingface`) silently left the user with only the last
17+
extra installed, and subsequent builds of HF/Kaggle slugs failed with
18+
`ImportError`. uv has no project-level toggle for this — `--inexact` is
19+
per-command — so the fix is documentation-wide.
20+
21+
### Changed
22+
23+
- TUI build action (`python -m scripts.pipeline.browse`, then `b` on a row)
24+
now runs `uv sync --extra <kaggle|huggingface> --inexact` automatically
25+
before the build subprocess when the dataset's `fetch.type` requires an
26+
upstream-fetch backend. Sync output streams into the same RichLog as the
27+
build; sync failure aborts the build with a visible exit code. Pure-HTTP
28+
and custom-fetch slugs see the same flow as before (no extra sync).
29+
`BuildConfirmModal` surfaces the sync command line above the build command
30+
line so the user sees both before confirming.
31+
832
## [0.1.1] - 2026-05-07
933

1034
### Added
@@ -82,5 +106,6 @@ This release bundles:
82106
this repository" button in the repo sidebar with BibTeX / APA / Chicago
83107
exports.
84108

109+
[0.1.2]: https://github.com/spiraldb/raincloud/releases/tag/v0.1.2
85110
[0.1.1]: https://github.com/spiraldb/raincloud/releases/tag/v0.1.1
86111
[0.1.0]: https://github.com/spiraldb/raincloud/releases/tag/v0.1.0

CONTRIBUTING.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ pipeline itself, see [`README.md`](README.md), [`AGENTS.md`](AGENTS.md), and
1010
```bash
1111
git clone git@github.com:spiraldb/raincloud.git
1212
cd raincloud
13-
uv sync --extra dev
13+
uv sync --extra dev --inexact
1414
```
1515

1616
`--extra dev` pulls in `pytest`. Add `--extra kaggle` or `--extra huggingface`
1717
if your work touches those upstream types, or `--extra all` for everything.
18+
Always pass `--inexact` — without it, each `uv sync --extra X` removes the
19+
extras from the previous one (e.g. syncing `--extra dev` after `--extra
20+
huggingface` uninstalls `huggingface_hub`).
1821

1922
## Before you open a PR
2023

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Nothing downstream of `sources.json` is hand-maintained; `docs/datasets.md` and
2424
**Browse the catalog at a glance** — sortable columns, parquet/vortex presence per slug, no builds required:
2525

2626
```bash
27-
uv sync --extra tui
27+
uv sync --extra tui --inexact
2828
python -m scripts.pipeline.browse
2929
```
3030

@@ -33,7 +33,7 @@ A read-only Textual TUI over `sources.json`. Click any column header to sort; ri
3333
**Tell Raincloud which dataset you want; get back a Parquet + Vortex file on disk.**
3434

3535
```bash
36-
uv sync
36+
uv sync --inexact
3737
python -m scripts.pipeline.status --fast --missing-only # read-only env check
3838
python -m scripts.pipeline.build countries-of-the-world
3939
```
@@ -59,23 +59,25 @@ python -m scripts.pipeline.build --family public-bi # all 46 Public BI w
5959

6060
```bash
6161
# Kaggle-hosted (33 slugs). One-time credential setup:
62-
uv sync --extra kaggle
62+
uv sync --extra kaggle --inexact
6363
mkdir -p ~/.kaggle && mv /path/to/kaggle.json ~/.kaggle/ && chmod 600 ~/.kaggle/kaggle.json
6464

6565
# Hugging Face-hosted (59 slugs):
66-
uv sync --extra huggingface
66+
uv sync --extra huggingface --inexact
6767

6868
# Everything:
69-
uv sync --extra all
69+
uv sync --extra all --inexact
7070
```
7171

72+
The `--inexact` flag matters: by default `uv sync` removes any extras you installed previously. Pass it on every `uv sync` so the tui / kaggle / huggingface / dev extras accumulate instead of overwriting each other.
73+
7274
## For AI coding agents
7375

7476
If you're an AI coding agent landing in this repo:
7577

7678
1. Read [`AGENTS.md`](AGENTS.md) (auto-loaded from `CLAUDE.md → AGENTS.md`) for the invariants and architecture.
7779
2. Run `python -m scripts.pipeline.status --fast --missing-only` to verify the env, then `python -m scripts.pipeline.validate_manifest` to confirm `sources.json` is well-formed. Both are sub-second and side-effect-free.
78-
3. Run `pytest` (after `uv sync --extra dev`) for a regression net before any non-trivial change to the manifest, schema, or handler registry.
80+
3. Run `pytest` (after `uv sync --extra dev --inexact`) for a regression net before any non-trivial change to the manifest, schema, or handler registry.
7981
4. For catalog questions ("which slugs use handler X", "what's CC0-licensed"), use `python -m scripts.pipeline.list_datasets` rather than greping `sources.json` or scrolling [`docs/v1/datasets.md`](docs/v1/datasets.md).
8082
5. Copy-pasteable templates for new manifest entries and streaming handlers live in [`examples/`](examples/).
8183
6. Harnesses that follow the [Agent Skills](https://agentskills.io) standard get 16 invokable skills under [`.agents/skills/`](.agents/skills/) (the `.claude → .agents` symlink means Claude Code sees the same files). Tracked safe-default permissions in [`.agents/settings.json`](.agents/settings.json) — see [`.agents/README.md`](.agents/README.md) for the full layout.
@@ -175,7 +177,7 @@ python -m scripts.pipeline.list_datasets --grep '\bgeo' --long
175177
python -m scripts.pipeline.browse # interactive TUI (requires --extra tui)
176178

177179
# Run the test suite (sub-second, no fetch / no build)
178-
uv sync --extra dev && pytest
180+
uv sync --extra dev --inexact && pytest
179181
```
180182

181183
Each stage is independently invokable — e.g. `python -m scripts.pipeline.fetch <slug>` to download raw bytes without running the rest. Stages are idempotent: fetch skips when `expected_bytes`/`expected_sha256` already matches on disk, write skips when the output parquet is already current.

SKILLS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Playbooks for common operations in this repo. Each section is a self-contained recipe — copy and adapt.
44

5-
Prereqs: Python 3.11+ and [uv](https://docs.astral.sh/uv/). Run `uv sync` in the repo root to install the pinned core deps (`pyarrow`, `duckdb`, `vortex-data`, `zstandard`, `py7zr`, `unlzw3`, `pandas`, `openpyxl`, `pyreadstat`, `osmium`, `jsonschema`). Add `--extra kaggle` for Kaggle-hosted datasets, `--extra huggingface` for Hugging Face ones, or `--extra dev` for `pytest` — see [`README.md`](README.md#upstream-specific-extras). Invoke Python as `.venv/bin/python` (or activate the venv).
5+
Prereqs: Python 3.11+ and [uv](https://docs.astral.sh/uv/). Run `uv sync --inexact` in the repo root to install the pinned core deps (`pyarrow`, `duckdb`, `vortex-data`, `zstandard`, `py7zr`, `unlzw3`, `pandas`, `openpyxl`, `pyreadstat`, `osmium`, `jsonschema`). Add `--extra kaggle` for Kaggle-hosted datasets, `--extra huggingface` for Hugging Face ones, or `--extra dev` for `pytest` — see [`README.md`](README.md#upstream-specific-extras). Always pass `--inexact` so subsequent extras accumulate instead of overwriting prior ones (uv's default is "exact" sync, which removes anything not requested by the current invocation). Invoke Python as `.venv/bin/python` (or activate the venv).
66

77
## Index
88

@@ -51,8 +51,8 @@ Env vars the helper honours (see [`README.md`](README.md#duckdb-resource-limits)
5151
## Running the test suite
5252

5353
```bash
54-
uv sync --extra dev # one-time — installs pytest
55-
pytest # ~0.5 s on the full suite
54+
uv sync --extra dev --inexact # one-time — installs pytest, preserves other extras
55+
pytest # ~0.5 s on the full suite
5656
```
5757

5858
`tests/` carries a sub-second smoke suite for the manifest, the schema, the handler registry, and the example templates. No fetch, no build, no filesystem writes. Run after any change to `sources.json`, `sources.schema.json`, `scripts/pipeline/handlers/__init__.py`, or `examples/`. Tests exercise the same `validate_manifest` codepath the `/raincloud-validate-manifest` skill runs.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "raincloud"
3-
version = "0.1.1"
3+
version = "0.1.2"
44
description = "Client-reproducible pipeline for building a curated catalog of public datasets as Parquet + Vortex files."
55
readme = "README.md"
66
requires-python = ">=3.11"

scripts/pipeline/browse.py

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
streams output into a modal log; cancellation kills the subprocess.
88
99
Run: `python -m scripts.pipeline.browse`
10-
Install: `uv sync --extra tui`
10+
Install: `uv sync --extra tui --inexact`
1111
1212
Keybindings:
1313
q — quit (orphans any in-flight build subprocesses)
@@ -37,7 +37,7 @@
3737
from textual.widgets import DataTable, Footer, Header, RichLog, Static
3838
except ImportError:
3939
print(
40-
"textual is not installed. Install with: uv sync --extra tui",
40+
"textual is not installed. Install with: uv sync --extra tui --inexact",
4141
file=sys.stderr,
4242
)
4343
raise SystemExit(2)
@@ -216,6 +216,30 @@ def _read_column_stats(parquet: Path) -> list[dict] | None:
216216
return read_column_stats(parquet)
217217

218218

219+
def _required_extras(spec: dict) -> list[str]:
220+
"""Optional pyproject extras the build needs based on fetch.type.
221+
222+
Handler-specific format deps (pandas, openpyxl, pyreadstat, osmium,
223+
zstandard, py7zr, unlzw3) all live in core deps, so the only extras
224+
we ever need to pull in on demand are the upstream-fetch backends:
225+
`kaggle` for fetch.type=kaggle, `huggingface` for fetch.type=huggingface.
226+
Returns [] for http / custom — no sync needed before build.
227+
"""
228+
ftype = (spec.get("fetch") or {}).get("type")
229+
if ftype == "kaggle": return ["kaggle"]
230+
if ftype == "huggingface": return ["huggingface"]
231+
return []
232+
233+
234+
def _uv_sync_command(extras: list[str]) -> list[str]:
235+
"""Argv for `uv sync --extra X [--extra Y...] --inexact`."""
236+
cmd = ["uv", "sync"]
237+
for e in extras:
238+
cmd += ["--extra", e]
239+
cmd.append("--inexact")
240+
return cmd
241+
242+
219243
def _build_time_estimate(spec: dict, snapshot: dict | None = None) -> str:
220244
"""Heuristic build-time bracket. Prefers fetch.expected_bytes when set
221245
(most accurate, but rare in the manifest); falls back to expect.rows,
@@ -549,6 +573,18 @@ def compose(self) -> ComposeResult:
549573
if advisory else ""
550574
)
551575

576+
extras = _required_extras(spec)
577+
sync_line = (
578+
f" [reverse] {' '.join(_uv_sync_command(extras))} [/reverse]\n"
579+
if extras else ""
580+
)
581+
sync_note = (
582+
f"[dim]First syncs the {'/'.join(extras)} extra (preserving any "
583+
f"others installed) so the {(spec.get('fetch') or {}).get('type')} "
584+
f"backend is available; then runs the build.[/dim]\n\n"
585+
if extras else ""
586+
)
587+
552588
body = (
553589
f"[dim]{full_name}[/dim]\n\n"
554590
f"{description}\n\n"
@@ -557,7 +593,9 @@ def compose(self) -> ComposeResult:
557593
f"[b]est. time[/b] {est}\n"
558594
f"{advisory_block}\n"
559595
f"Will run from the repo root:\n"
596+
f"{sync_line}"
560597
f" [reverse] python -m scripts.pipeline.build {self.slug} [/reverse]\n\n"
598+
f"{sync_note}"
561599
f"[dim]The TUI will stream the subprocess output. Cancelling the "
562600
f"build modal terminates the subprocess. Quitting the TUI "
563601
f"orphans any in-flight builds — use the CLI for hours-long "
@@ -622,9 +660,10 @@ class BuildLogModal(ModalScreen):
622660
Binding("q", "request_close", "cancel + close"),
623661
]
624662

625-
def __init__(self, slug: str) -> None:
663+
def __init__(self, slug: str, spec: dict | None = None) -> None:
626664
super().__init__()
627665
self.slug = slug
666+
self.spec = spec or {}
628667
self._process: asyncio.subprocess.Process | None = None
629668
self._task: asyncio.Task | None = None
630669

@@ -641,23 +680,41 @@ def compose(self) -> ComposeResult:
641680
def on_mount(self) -> None:
642681
self._task = asyncio.create_task(self._run_build())
643682

683+
async def _stream_subprocess(self, argv: list[str], log: "RichLog") -> int:
684+
"""Spawn argv, stream stdout/stderr line-by-line into `log`, return exit code.
685+
Stores the process on self so cancellation can SIGTERM/SIGKILL it."""
686+
self._process = await asyncio.create_subprocess_exec(
687+
*argv,
688+
stdout=asyncio.subprocess.PIPE,
689+
stderr=asyncio.subprocess.STDOUT,
690+
cwd=str(REPO_ROOT),
691+
)
692+
assert self._process.stdout is not None
693+
while True:
694+
line = await self._process.stdout.readline()
695+
if not line:
696+
break
697+
log.write(line.decode("utf-8", errors="replace").rstrip("\n"))
698+
return await self._process.wait()
699+
644700
async def _run_build(self) -> None:
645701
log = self.query_one("#build-log", RichLog)
646702
status = self.query_one("#status", Static)
647703
try:
648-
self._process = await asyncio.create_subprocess_exec(
649-
sys.executable, "-u", "-m", "scripts.pipeline.build", self.slug,
650-
stdout=asyncio.subprocess.PIPE,
651-
stderr=asyncio.subprocess.STDOUT,
652-
cwd=str(REPO_ROOT),
653-
)
654-
assert self._process.stdout is not None
655-
while True:
656-
line = await self._process.stdout.readline()
657-
if not line:
658-
break
659-
log.write(line.decode("utf-8", errors="replace").rstrip("\n"))
660-
rc = await self._process.wait()
704+
extras = _required_extras(self.spec)
705+
if extras:
706+
sync_cmd = _uv_sync_command(extras)
707+
status.update(f"[yellow]syncing {'/'.join(extras)}…[/yellow]")
708+
log.write(f"$ {' '.join(sync_cmd)}")
709+
rc = await self._stream_subprocess(sync_cmd, log)
710+
if rc != 0:
711+
status.update(f"[red]✗ uv sync failed (exit {rc}) — build skipped[/red]")
712+
return
713+
log.write("") # blank line between sync and build output
714+
status.update("[yellow]running…[/yellow]")
715+
build_cmd = [sys.executable, "-u", "-m", "scripts.pipeline.build", self.slug]
716+
log.write(f"$ {' '.join(build_cmd)}")
717+
rc = await self._stream_subprocess(build_cmd, log)
661718
if rc == 0:
662719
status.update("[green]✓ build succeeded[/green]")
663720
else:
@@ -887,7 +944,7 @@ def _on_confirm(confirmed: bool | None) -> None:
887944
_vortex_cell(spec, parquet, vortex),
888945
_hydrate_cell(spec, hydrated),
889946
)
890-
self.push_screen(BuildLogModal(slug))
947+
self.push_screen(BuildLogModal(slug, spec))
891948

892949
self.push_screen(BuildConfirmModal(slug, spec, snapshot=self._snapshot), _on_confirm)
893950

tests/test_manifest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
build, no filesystem writes. They're the regression net for changes to
77
the manifest, the schema, or the handler registry.
88
9-
Run: `uv sync --extra dev && pytest`.
9+
Run: `uv sync --extra dev --inexact && pytest`.
1010
"""
1111
from __future__ import annotations
1212

0 commit comments

Comments
 (0)