rainspout 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rainspout-1.0.0/.github/workflows/ci.yml +26 -0
- rainspout-1.0.0/.github/workflows/publish.yml +60 -0
- rainspout-1.0.0/.gitignore +8 -0
- rainspout-1.0.0/LICENSE +21 -0
- rainspout-1.0.0/PKG-INFO +52 -0
- rainspout-1.0.0/RAINSPOUT_DESIGN.md +463 -0
- rainspout-1.0.0/README.md +26 -0
- rainspout-1.0.0/docs/CONFIG_AUTHORING.md +346 -0
- rainspout-1.0.0/docs/HANDLER_AUTHORING.md +505 -0
- rainspout-1.0.0/docs/PACKAGE_AUTHORING.md +228 -0
- rainspout-1.0.0/docs/README.md +141 -0
- rainspout-1.0.0/docs/STAGE_AUTHORING.md +499 -0
- rainspout-1.0.0/docs/templates/package_ci.yml +31 -0
- rainspout-1.0.0/docs/tutorials/01_add_a_handler.md +169 -0
- rainspout-1.0.0/docs/tutorials/02_add_a_stage.md +141 -0
- rainspout-1.0.0/docs/tutorials/03_create_a_run.md +186 -0
- rainspout-1.0.0/pyproject.toml +73 -0
- rainspout-1.0.0/src/rainspout/__init__.py +9 -0
- rainspout-1.0.0/src/rainspout/cli/__init__.py +1 -0
- rainspout-1.0.0/src/rainspout/cli/_mount.py +40 -0
- rainspout-1.0.0/src/rainspout/cli/build_image.py +46 -0
- rainspout-1.0.0/src/rainspout/cli/main.py +260 -0
- rainspout-1.0.0/src/rainspout/config.py +235 -0
- rainspout-1.0.0/src/rainspout/conformance.py +267 -0
- rainspout-1.0.0/src/rainspout/contracts/__init__.py +45 -0
- rainspout-1.0.0/src/rainspout/contracts/_enforcement.py +97 -0
- rainspout-1.0.0/src/rainspout/contracts/dimension.py +37 -0
- rainspout-1.0.0/src/rainspout/contracts/handler.py +361 -0
- rainspout-1.0.0/src/rainspout/contracts/metadata.py +90 -0
- rainspout-1.0.0/src/rainspout/contracts/models.py +37 -0
- rainspout-1.0.0/src/rainspout/contracts/reference.py +78 -0
- rainspout-1.0.0/src/rainspout/contracts/stage.py +129 -0
- rainspout-1.0.0/src/rainspout/dag.py +73 -0
- rainspout-1.0.0/src/rainspout/devtools/__init__.py +1 -0
- rainspout-1.0.0/src/rainspout/devtools/version_bump.py +97 -0
- rainspout-1.0.0/src/rainspout/discovery.py +43 -0
- rainspout-1.0.0/src/rainspout/driver.py +297 -0
- rainspout-1.0.0/src/rainspout/errors.py +77 -0
- rainspout-1.0.0/src/rainspout/oplog.py +96 -0
- rainspout-1.0.0/src/rainspout/provenance.py +76 -0
- rainspout-1.0.0/src/rainspout/registry.py +45 -0
- rainspout-1.0.0/src/rainspout/runner.py +171 -0
- rainspout-1.0.0/src/rainspout/testing.py +188 -0
- rainspout-1.0.0/src/rainspout/validation.py +258 -0
- rainspout-1.0.0/tests/conftest.py +13 -0
- rainspout-1.0.0/tests/fake_package.py +143 -0
- rainspout-1.0.0/tests/reference_content/__init__.py +0 -0
- rainspout-1.0.0/tests/reference_content/components.py +11 -0
- rainspout-1.0.0/tests/reference_content/handlers/__init__.py +0 -0
- rainspout-1.0.0/tests/reference_content/handlers/ref_grid_json/__init__.py +0 -0
- rainspout-1.0.0/tests/reference_content/handlers/ref_grid_json/example_data/7_alpha.json +1 -0
- rainspout-1.0.0/tests/reference_content/handlers/ref_grid_json/handler.py +39 -0
- rainspout-1.0.0/tests/reference_content/handlers/ref_grid_json/test_roundtrip.py +13 -0
- rainspout-1.0.0/tests/reference_content/handlers/ref_lines_txt/__init__.py +0 -0
- rainspout-1.0.0/tests/reference_content/handlers/ref_lines_txt/example_data/alpha/7.txt +2 -0
- rainspout-1.0.0/tests/reference_content/handlers/ref_lines_txt/handler.py +39 -0
- rainspout-1.0.0/tests/reference_content/handlers/ref_lines_txt/test_roundtrip.py +15 -0
- rainspout-1.0.0/tests/reference_content/handlers/ref_table_json/__init__.py +0 -0
- rainspout-1.0.0/tests/reference_content/handlers/ref_table_json/example_data/alpha.json +1 -0
- rainspout-1.0.0/tests/reference_content/handlers/ref_table_json/handler.py +41 -0
- rainspout-1.0.0/tests/reference_content/handlers/ref_table_json/test_roundtrip.py +13 -0
- rainspout-1.0.0/tests/reference_content/stages/__init__.py +0 -0
- rainspout-1.0.0/tests/reference_content/stages/ref_enrich/__init__.py +0 -0
- rainspout-1.0.0/tests/reference_content/stages/ref_enrich/stage.py +51 -0
- rainspout-1.0.0/tests/reference_content/stages/ref_enrich/test_ref_enrich.py +31 -0
- rainspout-1.0.0/tests/reference_content/stages/ref_snip/__init__.py +0 -0
- rainspout-1.0.0/tests/reference_content/stages/ref_snip/stage.py +41 -0
- rainspout-1.0.0/tests/reference_content/stages/ref_snip/test_ref_snip.py +20 -0
- rainspout-1.0.0/tests/roundtrip_handlers.py +130 -0
- rainspout-1.0.0/tests/runner_components.py +104 -0
- rainspout-1.0.0/tests/sample_components.py +93 -0
- rainspout-1.0.0/tests/test_cli.py +120 -0
- rainspout-1.0.0/tests/test_cli_verbs.py +208 -0
- rainspout-1.0.0/tests/test_config_models.py +123 -0
- rainspout-1.0.0/tests/test_conformance.py +154 -0
- rainspout-1.0.0/tests/test_data_plane.py +283 -0
- rainspout-1.0.0/tests/test_dimensions.py +132 -0
- rainspout-1.0.0/tests/test_discovery.py +63 -0
- rainspout-1.0.0/tests/test_driver.py +289 -0
- rainspout-1.0.0/tests/test_handler_contract.py +219 -0
- rainspout-1.0.0/tests/test_provenance.py +56 -0
- rainspout-1.0.0/tests/test_reference.py +46 -0
- rainspout-1.0.0/tests/test_reference_content.py +141 -0
- rainspout-1.0.0/tests/test_registry.py +54 -0
- rainspout-1.0.0/tests/test_runner.py +274 -0
- rainspout-1.0.0/tests/test_stage_contract.py +214 -0
- rainspout-1.0.0/tests/test_testing_helpers.py +179 -0
- rainspout-1.0.0/tests/test_validation.py +320 -0
- rainspout-1.0.0/tests/test_version_bump.py +84 -0
- rainspout-1.0.0/uv.lock +749 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
checks:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
with:
|
|
14
|
+
fetch-depth: 0 # full history: the version-bump check diffs against main
|
|
15
|
+
- uses: astral-sh/setup-uv@v5
|
|
16
|
+
- name: Sync environment
|
|
17
|
+
run: uv sync
|
|
18
|
+
- name: Lint
|
|
19
|
+
run: uv run ruff check src tests
|
|
20
|
+
- name: Type-check
|
|
21
|
+
run: uv run mypy
|
|
22
|
+
- name: Tests + coverage floor
|
|
23
|
+
run: uv run pytest # --cov-fail-under=90 enforced via pyproject addopts
|
|
24
|
+
- name: Stage version-bump check
|
|
25
|
+
if: github.event_name == 'pull_request'
|
|
26
|
+
run: uv run python -m rainspout.devtools.version_bump --base origin/${{ github.base_ref }}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# Publishing is tag-driven: pushing a tag `vX.Y.Z` runs the full gate suite,
|
|
4
|
+
# checks the tag matches the package version, builds sdist + wheel, and
|
|
5
|
+
# publishes to PyPI via trusted publishing (OIDC — no long-lived token).
|
|
6
|
+
# One-time setup on pypi.org: add this repo as a trusted publisher for the
|
|
7
|
+
# `rainspout` project (owner: strongmats, repo: rainspout,
|
|
8
|
+
# workflow: publish.yml, environment: pypi).
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
tags: ["v*"]
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
checks:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: astral-sh/setup-uv@v5
|
|
20
|
+
- name: Sync environment
|
|
21
|
+
run: uv sync
|
|
22
|
+
- name: Lint
|
|
23
|
+
run: uv run ruff check src tests
|
|
24
|
+
- name: Type-check
|
|
25
|
+
run: uv run mypy
|
|
26
|
+
- name: Tests + coverage floor
|
|
27
|
+
run: uv run pytest
|
|
28
|
+
- name: Tag must match the package version
|
|
29
|
+
run: |
|
|
30
|
+
version=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
31
|
+
if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then
|
|
32
|
+
echo "tag ${GITHUB_REF_NAME} does not match pyproject version ${version}" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
build:
|
|
37
|
+
needs: checks
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
- uses: astral-sh/setup-uv@v5
|
|
42
|
+
- name: Build sdist and wheel
|
|
43
|
+
run: uv build
|
|
44
|
+
- uses: actions/upload-artifact@v4
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
48
|
+
|
|
49
|
+
publish:
|
|
50
|
+
needs: build
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
environment: pypi
|
|
53
|
+
permissions:
|
|
54
|
+
id-token: write # trusted publishing (OIDC)
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/download-artifact@v4
|
|
57
|
+
with:
|
|
58
|
+
name: dist
|
|
59
|
+
path: dist/
|
|
60
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
rainspout-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Strong
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
rainspout-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rainspout
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A domain-agnostic frame for building scientific data-processing pipelines
|
|
5
|
+
Project-URL: Homepage, https://github.com/strongmats/rainspout
|
|
6
|
+
Project-URL: Documentation, https://github.com/strongmats/rainspout/tree/main/docs
|
|
7
|
+
Project-URL: Issues, https://github.com/strongmats/rainspout/issues
|
|
8
|
+
Author-email: Matthew Strong <strongmats@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: data-processing,pipeline,provenance,scientific-computing,workflow
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: pydantic<3,>=2
|
|
23
|
+
Requires-Dist: pyyaml>=6
|
|
24
|
+
Requires-Dist: typer>=0.12
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Rainspout
|
|
28
|
+
|
|
29
|
+
A domain-agnostic frame for building scientific data-processing pipelines.
|
|
30
|
+
Rainspout contains no science of its own: content packages ship the stages
|
|
31
|
+
(processing steps) and handlers (storage adapters); one YAML config describes
|
|
32
|
+
a run; the `spout` command executes it, tracking what has already been done.
|
|
33
|
+
|
|
34
|
+
**Start with [`docs/README.md`](docs/README.md)** — the documentation is the
|
|
35
|
+
API: you can build a content package from the authoring guides alone, without
|
|
36
|
+
reading this repository's source.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
pip install rainspout # or: uv add rainspout
|
|
42
|
+
spout --help
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
uv sync # create the environment
|
|
49
|
+
uv run pytest # tests (coverage floor enforced)
|
|
50
|
+
uv run ruff check # lint
|
|
51
|
+
uv run mypy # types
|
|
52
|
+
```
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# Rainspout — Consolidated Design Document (v2)
|
|
2
|
+
|
|
3
|
+
> **Rainspout** is a general-purpose scientific data-processing pipeline *framework*
|
|
4
|
+
> (the skeleton). It is domain-agnostic. **SkyCT** — the VLF ionospheric-tomography
|
|
5
|
+
> work — is one *content package* built on top of Rainspout, not the framework itself.
|
|
6
|
+
> The distribution/import name is `rainspout`; the CLI command is `spout`.
|
|
7
|
+
|
|
8
|
+
> This document is the **single consolidated source of truth**: the original design
|
|
9
|
+
> document with every decision from the Gate 1 review, the Gate 2 review, the Gate 2b
|
|
10
|
+
> question round, the Gate 2c corrections, and the Phase 1 review folded in. It
|
|
11
|
+
> supersedes the layered prompt-2 document + response stack. The authoring documents
|
|
12
|
+
> under `docs/` are the *normative developer-facing contract*; this document is the
|
|
13
|
+
> design rationale and decision record behind them. Where they could ever disagree,
|
|
14
|
+
> `docs/` governs component authors and this document governs the skeleton's builders —
|
|
15
|
+
> and the disagreement is a bug to be reported.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## A. Foundational philosophy
|
|
20
|
+
|
|
21
|
+
- A reusable **skeleton/shell** that scientific (or other) processing pipelines are
|
|
22
|
+
built on top of. The skeleton contains **no science**.
|
|
23
|
+
- **Separate "what / where / how."** Stages (what computation), handlers (where data
|
|
24
|
+
lives and how it is read/written), dimensions/driver (over what the run sweeps) —
|
|
25
|
+
each independent, none leaking into the others.
|
|
26
|
+
- **Break loudly, early.** Every misconfiguration fails at startup with a specific,
|
|
27
|
+
actionable message naming the offender — never a silent wrong result or a deep
|
|
28
|
+
runtime crash.
|
|
29
|
+
- **Declared-in-code, validated-against-config.** Everything a component needs is
|
|
30
|
+
declared in code and checked against config before any data moves.
|
|
31
|
+
- **Blind-authorable contracts.** Someone with only an authoring doc (no skeleton
|
|
32
|
+
access) can build a conforming component. The docs are the literal API boundary.
|
|
33
|
+
- **Documentation-driven design.** The authoring standards were written FIRST (Gate 2)
|
|
34
|
+
and define the interface; the machinery is built to satisfy them.
|
|
35
|
+
- **Deep-validate cheap things (settings); cheap-check expensive things (data).**
|
|
36
|
+
- **The skeleton is dimension-agnostic; stages are coordinate-aware.** No dimension
|
|
37
|
+
name is hardcoded anywhere in Rainspout — names come from the run config. Stages
|
|
38
|
+
*read* their work-item coordinate freely (position-dependent science is legitimate)
|
|
39
|
+
but never forge or alter it. *(Gate 2 Part B, replacing the earlier
|
|
40
|
+
"stages are dimension-blind" over-application.)*
|
|
41
|
+
- **Presence-and-wiring is the guarantee boundary for handler dependencies.** The
|
|
42
|
+
skeleton guarantees a stage that its declared dependencies are satisfied and
|
|
43
|
+
reference real, validly-configured components — exactly parallel to what it
|
|
44
|
+
guarantees for settings. How the stage *uses* a handler it was handed is private
|
|
45
|
+
and trust-based; misuse is an isolated runtime failure. *(Gate 2c.)*
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## B. Core / content split (plugin architecture)
|
|
50
|
+
|
|
51
|
+
- **`rainspout` (the skeleton):** the brain (registry, config, validation, runner,
|
|
52
|
+
driver, two logging systems, CLI), the base-class contracts, and the authoring
|
|
53
|
+
standards. No science, no real handlers, no real stages.
|
|
54
|
+
- **Content packages** (e.g. `skyct`): concrete stages + handlers + example configs
|
|
55
|
+
(+ optional package CLI verbs + lifecycle/artifact commands), obeying the contracts
|
|
56
|
+
and registering via **entry points**. Multiple packages coexist without knowing
|
|
57
|
+
about each other.
|
|
58
|
+
- **The authoring docs are the inter-repo contract** and a first-class deliverable;
|
|
59
|
+
they live in `docs/` at the repo root (canonical home), written to be readable by a
|
|
60
|
+
newcomer (plain-language overview + glossary in `docs/README.md`, a "short version"
|
|
61
|
+
preamble on each standard).
|
|
62
|
+
- **Public API surface = `rainspout.contracts` + `rainspout.testing`, nothing else.**
|
|
63
|
+
Both carry a v1 **stability commitment** (packages pin `rainspout>=1,<2`); private
|
|
64
|
+
modules may change without notice. *(Gate 2 #9.)*
|
|
65
|
+
- **Installable packages** via entry points (not drag-drop folders); editable installs
|
|
66
|
+
(`uv add --editable ./pkg`) for live development; a package can move to its own repo
|
|
67
|
+
unchanged once its contracts stabilize.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## C. Handlers (the collapsed IO axis)
|
|
72
|
+
|
|
73
|
+
- **Combined handlers** replace freely-mixed Storage×Codec: each handler is ONE fixed
|
|
74
|
+
combination of {file type, in-file structure, filename convention, folder structure,
|
|
75
|
+
access channel}, killing the N×M testing burden.
|
|
76
|
+
- **Naming:** `datatype_channel_type` — **underscores only, no dots** (e.g.
|
|
77
|
+
`broadband_local_mat`) *(Gate 1 C1)*. Purely conventional, never parsed; the
|
|
78
|
+
registry enforces `^[a-z][a-z0-9_]*$` on every axis.
|
|
79
|
+
- **Author surface = private hooks; public verbs are final.** *(Gate 2 #1.)* Authors
|
|
80
|
+
implement `_load_cell` / `_save_cell` / `_catalog_cells` (+ optional `_probe`,
|
|
81
|
+
`_check_structure`); the base class owns final `load`, `load_one`, `save`,
|
|
82
|
+
`catalog`, `preflight`, whose docstrings document exactly what they do around the
|
|
83
|
+
hooks (range expansion, lazy per-cell iteration, single-cell-save enforcement,
|
|
84
|
+
error wrapping). Finality is enforced at class definition *and* against
|
|
85
|
+
post-definition monkey-patching *(Phase 1 review)*.
|
|
86
|
+
- **One input shape.** Every verb takes a dimension spec `{role: (values…)}`; a
|
|
87
|
+
single value is a tuple of one. **The driver expands config ranges before any
|
|
88
|
+
handler call** — handlers never parse range syntax *(Gate 2 #2)*. `load(spec)`
|
|
89
|
+
returns a lazy per-cell iterator of `Cell(coords, data, meta)`. **`load_one(coords)`**
|
|
90
|
+
is a final base convenience — exactly `load` with a one-value spec, returning
|
|
91
|
+
`(data, meta)` directly — and is what stages typically call on an auxiliary handler.
|
|
92
|
+
- **Dimension roles.** A handler declares `dimension_roles` + `dimension_types` — the
|
|
93
|
+
whole dimension vocabulary it knows, named before any config exists. Configs map
|
|
94
|
+
their dimension names onto roles; everything the handler receives is keyed by role.
|
|
95
|
+
- **Two positions, one contract.** *(Gate 2 Part B4; presentation confirmed in the
|
|
96
|
+
Phase 1 review: one concept, two entry points, not two species.)*
|
|
97
|
+
- **Seed handler** — named in the config's `seed:` block; the driver coordinates
|
|
98
|
+
with it per work item. Rigorously validated at startup: mapped roles ↔ iterated
|
|
99
|
+
dimensions exactly, types coercible, then the pre-flight probe.
|
|
100
|
+
- **Auxiliary handler** — handed to a stage as a `handler:`-wired dependency; the
|
|
101
|
+
**stage** computes the coordinates it asks for (via `load_one`/`load`), along
|
|
102
|
+
axes that by default should be assumed **unrelated** to the run's dimensions.
|
|
103
|
+
Not dimension-validated at startup; fitness proves out at runtime, isolated per
|
|
104
|
+
work item. *(Gate 2c #4.)*
|
|
105
|
+
- **Two INDEPENDENT range concepts**, both default-off, declared honestly:
|
|
106
|
+
1. **Dimension-grid range** (`supports_grid_range`) — multi-cell `load` specs;
|
|
107
|
+
always served lazily per cell. A range asked of a non-range handler fails at
|
|
108
|
+
startup when the ask comes from the run's own wiring, or at call time (that work
|
|
109
|
+
item only) when a stage asks; `load_one` in a loop works against every handler.
|
|
110
|
+
2. **Within-file windowing** (`supports_windowed_read`) — serve a slice inside one
|
|
111
|
+
stored file without materializing it (HDF5/memmap territory); window-argument
|
|
112
|
+
semantics are handler-defined and documented. Kept visibly distinct from
|
|
113
|
+
`LazyReference.window()` (inter-stage, in-flight) — both docs carry "not to be
|
|
114
|
+
confused with" notes *(Gate 1 Q8)*.
|
|
115
|
+
- **`catalog` surveys only the asked window** *(Gate 1 Q2)* — real-time polls make
|
|
116
|
+
unbounded surveys unaffordable; report a cell only if a load would plausibly
|
|
117
|
+
succeed (cheapest test, no deep validation); yield lazily; optional catalog file
|
|
118
|
+
(validated JSON: handler, roles, entries `{coords, extras}`, generated-at) written
|
|
119
|
+
by the base class.
|
|
120
|
+
- **All structural knowledge is handler-private.** Base directory down — folders,
|
|
121
|
+
filenames, in-file layout, connections — opaque to the skeleton. **Lifecycle is
|
|
122
|
+
private**: per-transaction by default; sessions managed internally; no external
|
|
123
|
+
setup ordering.
|
|
124
|
+
- **Metadata: one file, no sidecars.** *(Gate 2 #6, replacing the sidecar fallback.)*
|
|
125
|
+
The shared metadata block (schema version, `run_id`, coords, provenance chain) is
|
|
126
|
+
embedded **in the data file itself**; the skeleton defines no sidecar convention
|
|
127
|
+
and auxiliary metadata files are discouraged. Metadata handling is **optional**:
|
|
128
|
+
a handler may deliberately ignore it (conforming, provenance-severing, must be
|
|
129
|
+
stated in its docstring). Plain-text formats embed responsibly via one clearly
|
|
130
|
+
delimited, strippable section (e.g. a single `# rainspout-meta: {…}` comment line —
|
|
131
|
+
taught in Tutorial 1). Foreign/metadata-less data always loads with a fresh
|
|
132
|
+
empty-provenance block.
|
|
133
|
+
- **Compression on save** where the format supports it.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## D. Stages
|
|
138
|
+
|
|
139
|
+
- **"Stage"** = one processing step (never "module", reserved for `.py` files).
|
|
140
|
+
- **Thin orchestrator.** Declarations + a short `run` calling module-level science
|
|
141
|
+
functions (testable without the skeleton). More than ~a screen of `run` means
|
|
142
|
+
science is leaking into the orchestrator.
|
|
143
|
+
- **Coordinate-aware, not dimension-blind.** *(Gate 2 Part B1/B2.)* Every reference a
|
|
144
|
+
stage receives exposes `ref.coords`: the work item's read-only `{dimension: value}`
|
|
145
|
+
mapping, stamped by the driver at seed time, flowing downstream automatically.
|
|
146
|
+
Stages read it freely; they never forge or alter it (trustworthy provenance). Keys
|
|
147
|
+
are the config author's dimension names, so a stage needing a specific axis takes
|
|
148
|
+
the dimension name as a **bounded setting** (e.g. `time_dim: str = "time"`) rather
|
|
149
|
+
than hardcoding it *(Gate 2b Q3)*.
|
|
150
|
+
- **Three strictly separated input kinds:**
|
|
151
|
+
- **Settings** — static config, deep-validated (`settings_model`, `extra="forbid"`,
|
|
152
|
+
frozen). **Every field bounded** (Field ranges / Literal / Enum / constrained
|
|
153
|
+
strings); unbounded only as a justified, commented exception drawing a lint-style
|
|
154
|
+
conformance warning. *(Gate 1 review addition.)*
|
|
155
|
+
- **Dependencies** — named data inputs (`dependencies_model`); the field's **type
|
|
156
|
+
annotation declares its wiring kind** *(Gate 2 #5)*: `LazyReference` ↔ `from:`
|
|
157
|
+
(an upstream stage or the seed entry), `Handler` ↔ `handler:` (an instance from
|
|
158
|
+
the `handlers:` block). Mismatches fail at startup naming stage + field.
|
|
159
|
+
- **Resources** — a handler's fetch config; never a stage's concern.
|
|
160
|
+
- **Construction & injection.** *(Gate 2c #3.)* Stages construct nothing: the driver
|
|
161
|
+
constructs each stage at validation time (settings through the un-bypassable base
|
|
162
|
+
`__init__`), and per work item resolves and injects dependencies into `run` —
|
|
163
|
+
`from:` fields as coordinate-stamped references, `handler:` fields as ready-to-call
|
|
164
|
+
handler instances built from configured resources. This is also what makes stages
|
|
165
|
+
testable (fakes injected through the same door).
|
|
166
|
+
- **No seed-loader stage.** *(Gate 2 Part B3.)* Data enters through the config's
|
|
167
|
+
`seed:` block; the first stage wires `from: <seed_name>`. No package writes a
|
|
168
|
+
pass-through loader.
|
|
169
|
+
- **Auxiliary handler use is private to the stage.** *(Gate 2c #4.)* The config says
|
|
170
|
+
which handler; the stage decides what to load, deriving coordinates from
|
|
171
|
+
`ref.coords`/settings as it sees fit — a window of cells is simply a loop over
|
|
172
|
+
`load_one`. No skeleton binding, projection, or coordinate validation; loading is
|
|
173
|
+
the only legitimate stage-side use (saving is config-designated). The formerly
|
|
174
|
+
flagged "windowed loader" v1 limitation is dissolved by this model.
|
|
175
|
+
- **Setup hook** — `setup()`, idempotent, after validation, before any work item;
|
|
176
|
+
`spout setup` runs all of them.
|
|
177
|
+
- **Status / progress / warnings / errors:** mandatory `set_status` (≥once per run);
|
|
178
|
+
optional-but-recommended `progress()` (None where totals are unknowable); warnings
|
|
179
|
+
via `add_warning` (output still valid; recorded; still counts as succeeded);
|
|
180
|
+
errors are exceptions (prefer `StageError`) — fail this (stage, work item),
|
|
181
|
+
skip its downstream for this work item only, continue the run. Cheap shape/type
|
|
182
|
+
checks only; never deep per-element validation.
|
|
183
|
+
- **Versioning:** required `version`, bumped on ANY stage-code edit. Enforced by a
|
|
184
|
+
CI diff check; backstopped by a code hash recorded in every provenance entry.
|
|
185
|
+
**Code/test hash boundary** *(Gate 1 Q4 + Gate 2 #7)*: `test_*.py`, `*_test.py`,
|
|
186
|
+
`fixtures/`, `example_data/` are excluded from the hash and the bump requirement.
|
|
187
|
+
- **One stage = one self-contained directory** *(Gate 1 Q4)* — class, science
|
|
188
|
+
modules, fixtures, and its mandated test together; interior structure free;
|
|
189
|
+
copying the directory into another package (plus a registration import) works
|
|
190
|
+
unchanged.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## E. Config, DAG, driver, dimensions
|
|
195
|
+
|
|
196
|
+
- **One `.yml` file per run; six top-level keys** (`run`, `dimensions`, `iteration`,
|
|
197
|
+
`seed`, `handlers`, `stages`); unknown keys rejected; examples written in YAML
|
|
198
|
+
block form *(Gate 2c #2)*.
|
|
199
|
+
- **`seed:` — named entries.** *(Gate 2 Part B3 + Gate 2b Q1.)* Each entry gives a
|
|
200
|
+
registry handler, resources, and a role map; the driver loads the seed cell per
|
|
201
|
+
work item, stamps the coordinate, and offers it under the entry's name (`from:
|
|
202
|
+
raw`). Seed names share the upstream namespace with stage instance names.
|
|
203
|
+
**Exactly one entry in v1** — a second fails loudly — but the plural shape makes
|
|
204
|
+
multiple seeds (future branching: sferic + transmitter → tomography) a non-breaking
|
|
205
|
+
addition.
|
|
206
|
+
- **`handlers:` — named instances for the two other storage touchpoints:**
|
|
207
|
+
stage-callable inputs (`handler:`-wired dependencies; **no role map** — the stage
|
|
208
|
+
computes its own coordinates) and save targets (**role map required**, held to the
|
|
209
|
+
seed's standard: complete, onto the iterated dimensions, startup-checked, because
|
|
210
|
+
the driver writes at work-item granularity). A `dimensions:` map anywhere may only
|
|
211
|
+
name declared dimensions — a pure dangling-reference check that **never** compares
|
|
212
|
+
config names to handler-internal names *(Gate 2b Q4)*.
|
|
213
|
+
- **The same `handler:` key wires inputs and saves** — direction comes from
|
|
214
|
+
`dependencies:` vs `save:`, never from the key name *(Gate 2c #1; `loader:` is
|
|
215
|
+
dead vocabulary)*.
|
|
216
|
+
- **Dimensions**: named axes, list form (order preserved, duplicates rejected) or
|
|
217
|
+
inclusive `start/stop/step` ranges (numeric, or date/datetime with `1d`/`6h`/`30m`/
|
|
218
|
+
`15s` steps), expanded by the driver. Dimension names are the config author's
|
|
219
|
+
bookkeeping *(Gate 2 Part B6)* — enforced only at the seed edge and save targets,
|
|
220
|
+
via mappings; no stage-declared "expected dimensions" (deliberately rejected).
|
|
221
|
+
- **A work item** = one point in the dimension cross-product; the whole DAG runs once
|
|
222
|
+
per work item at one granularity. **No mid-DAG collapsing** — aggregation across a
|
|
223
|
+
dimension is a separate run with a different config reading what this one saved.
|
|
224
|
+
- **Linear chains in v1, general machinery underneath.** *(Gate 2 Part B5.)* The
|
|
225
|
+
runner/validator use general graph machinery (topological sort, dependency
|
|
226
|
+
resolution); the v1 single-chain restriction (seed → stage → … → stage) is a
|
|
227
|
+
distinct, loud validation rule, not an assumption baked into the code — branching
|
|
228
|
+
later is non-breaking.
|
|
229
|
+
- **Retrograde and real-time are one mechanism.** The **delta** =
|
|
230
|
+
`exists − attempted` *(Gate 1 Q1)*: the seed's `catalog` minus every work item in
|
|
231
|
+
the operational log, success **or** failure — poison inputs are not hammered.
|
|
232
|
+
Retrograde drains the delta once; realtime drains, sleeps `poll_frequency`
|
|
233
|
+
(required iff realtime, forbidden otherwise), recomputes, forever; never polls
|
|
234
|
+
mid-run. Clean stop *(Gate 1 Q9)*: Ctrl-C/SIGTERM between work items finishes the
|
|
235
|
+
current item, flushes the log, exits 0; a mid-item interrupt just never records
|
|
236
|
+
success, so resume redoes it.
|
|
237
|
+
- **Re-run controls** *(Gate 2 #11 + Gate 2b Q2)*:
|
|
238
|
+
- `--retry-failed` re-queues **failed** cells only; never touches succeeded ones.
|
|
239
|
+
- `--select dim=value` narrows the space first, then the log is subtracted as
|
|
240
|
+
usual (an attempted selected cell is still skipped); composes with
|
|
241
|
+
`--retry-failed` to re-queue exactly the selected failures.
|
|
242
|
+
- `--force-rewrite` — the separate, bluntly named flag that re-runs **succeeded**
|
|
243
|
+
cells (overwriting good output), usually with `--select`. Never overloaded onto
|
|
244
|
+
`--retry-failed`.
|
|
245
|
+
- **Failure isolation per work item**; **sequential in v1** (work items independent,
|
|
246
|
+
future parallelism preserved).
|
|
247
|
+
- **Config-designated saving**: any stage, anywhere in the DAG, may carry
|
|
248
|
+
`save: {handler: <instance>}`; only saved outputs persist; everything else stays
|
|
249
|
+
lazy/in-memory and is discarded per work item. A save is the output-symmetric
|
|
250
|
+
mirror of the seed (both driver-operated, both role-mapped).
|
|
251
|
+
- **Lazy inter-stage passing**: stages exchange `LazyReference`s; `get()` universal,
|
|
252
|
+
`window()` an advertised capability; materialization on demand; contract first
|
|
253
|
+
even where v1 passes in-memory objects underneath.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## F. Logging, provenance, resume
|
|
258
|
+
|
|
259
|
+
- **Two systems, never merged:**
|
|
260
|
+
- **Operational log** — keyed per (stage, work item): ran/succeeded/failed,
|
|
261
|
+
warnings/errors, timestamps. Source of truth for resume.
|
|
262
|
+
- **Provenance chain** — travels WITH the data: ordered
|
|
263
|
+
`{stage_name, stage_version, code_hash, settings_used, timestamp}` entries,
|
|
264
|
+
persisted inside the metadata block on every save. Scientific reproducibility.
|
|
265
|
+
- **Identity:** `run_id` per run (`<name>-<UTC stamp>-<suffix>`) + a per-work-item
|
|
266
|
+
sub-key (`cell_id`, the canonical serialized coordinate,
|
|
267
|
+
`day=2026-01-01|sensor=s1`) threading through log records, provenance entries,
|
|
268
|
+
and saved metadata. *(Gate 1 §3.5; confirmed in the Phase 1 review.)*
|
|
269
|
+
- **The log follows the run definition** *(Phase 4 review → Phase 5 decision)*:
|
|
270
|
+
default location `.rainspout/<run.name>.oplog.jsonl` **next to the config
|
|
271
|
+
file**, overridable with the optional `run.oplog:` key (relative paths resolve
|
|
272
|
+
against the config's directory). Never derived from the working directory —
|
|
273
|
+
the resume/delta story requires that a later run always finds the history it
|
|
274
|
+
must subtract; anchoring to the run definition makes that automatic, and a
|
|
275
|
+
deliberate `run.name` change starts a fresh history.
|
|
276
|
+
- Warnings ⇒ succeeded (skipped on resume). Failures ⇒ not re-run by default
|
|
277
|
+
(`--retry-failed` is the escape hatch). Definition errors ⇒ fatal before any data.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## G. Settings & validation (cross-cutting)
|
|
282
|
+
|
|
283
|
+
- **Pydantic v2 everywhere**, `extra="forbid"`, frozen after validation.
|
|
284
|
+
- **Un-bypassable validation:** the base `__init__` validates; subclasses may not
|
|
285
|
+
define `__init__` (class-definition-time check; mixins with `__init__` rejected);
|
|
286
|
+
post-definition monkey-patching of `__init__` or final methods is blocked by the
|
|
287
|
+
contract metaclass and covered by negative tests *(Phase 1 review)*. Post-validation
|
|
288
|
+
init goes in `setup()`.
|
|
289
|
+
- **`run` = `validate` + execution; no skip path.** Gates in order: (1) config parse,
|
|
290
|
+
(2) registry resolution, (3) seed rule, (4) DAG validation (wiring kinds, dangling
|
|
291
|
+
references, acyclicity, v1 linearity), (5) per-component settings/resources
|
|
292
|
+
validation. All definition-time, instant, touching no data.
|
|
293
|
+
- **Startup pre-flight probe — seed only.** *(Gate 1 review + Gate 2 #4.)* Steps:
|
|
294
|
+
(1) role-map check, (2) type check from `dimension_types`, (3) structural probe on
|
|
295
|
+
the first cataloged cell in the run window — default `_probe` = full load +
|
|
296
|
+
`_check_structure`, overridable where loads are expensive, (4) empty catalog ⇒
|
|
297
|
+
probe skipped with a loud logged notice (legitimate in realtime). Auxiliary
|
|
298
|
+
instances get resources validation only. **Deliberately NO inter-stage structural
|
|
299
|
+
checks** — a wrong structure between stages is an isolated per-work-item runtime
|
|
300
|
+
failure; validate the storage edges strictly, never burden the internal seams.
|
|
301
|
+
- **Every failure mode has a named-offender message:** bad/out-of-range setting;
|
|
302
|
+
missing/extra/misspelled dependency; wrong wiring kind; `from:` → nonexistent
|
|
303
|
+
upstream; DAG cycle; branching in v1; bad handler resources; malformed config;
|
|
304
|
+
unknown registry key (listing known keys); seed role-map miss; uncoercible
|
|
305
|
+
dimension values; save target without/with-incomplete role map; multiple seeds;
|
|
306
|
+
seed/stage name collision; `poll_frequency` misuse; range on a non-range handler;
|
|
307
|
+
unbumped version.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## H. Registration & discovery
|
|
312
|
+
|
|
313
|
+
- **One uniform gesture on every axis:** subclass the contract base with a `name` —
|
|
314
|
+
`__init_subclass__` validates the whole class-level contract and registers it.
|
|
315
|
+
Missing/invalid/duplicate names fail at import naming the class (duplicates name
|
|
316
|
+
both parties). The registry maps name → class per axis and is never hand-edited.
|
|
317
|
+
- **Entry points:** `rainspout.components` (one collector module per package;
|
|
318
|
+
import = registration; collisions fail loudly naming both packages) and
|
|
319
|
+
`rainspout.verbs` (a Typer app; mount name = entry-point name). *(Gate 2 #10.)*
|
|
320
|
+
Two documented discovery facts: entry-point changes need `uv sync` (collector-module
|
|
321
|
+
additions are live), and a component missing from the collector fails **silently** —
|
|
322
|
+
verify with `spout catalog`.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## I. CLI
|
|
327
|
+
|
|
328
|
+
- **Philosophy:** clean, composable commands; the user's cron/sbatch/Docker
|
|
329
|
+
orchestrates. Orchestration is out of scope.
|
|
330
|
+
- **Verbs:** `run` (with `--dry-run` planning, `--select`, `--retry-failed`,
|
|
331
|
+
`--force-rewrite`), `validate` (definition-only, instant), `catalog`, `setup`,
|
|
332
|
+
`build-image` (v1-minimal: Dockerfile from `uv.lock` + installed entry-point
|
|
333
|
+
packages, nothing cleverer — *Gate 1 Q6*), `test-package`, plus
|
|
334
|
+
**package-contributed verbs** (`spout <package> <verb>`).
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## J. Lifecycle commands & artifacts (the dissolved "sims")
|
|
339
|
+
|
|
340
|
+
- Packages may ship lifecycle verbs (`train`, `simulate`, `build`, `calibrate`)
|
|
341
|
+
producing **artifacts** — versioned inputs written **through a handler**, loaded by
|
|
342
|
+
a consuming stage as an ordinary `handler:` dependency (artifact version via a
|
|
343
|
+
bounded setting), its version landing in provenance. No separate machinery.
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## K. Testing & coverage
|
|
348
|
+
|
|
349
|
+
- **Skeleton:** 90% coverage floor (aim 95%+), `--cov-fail-under=90`, hard-failing
|
|
350
|
+
CI; `# pragma: no cover` only with justification — and enforcement paths are
|
|
351
|
+
covered by **negative tests**, never exempted *(Phase 1 review)*.
|
|
352
|
+
- **`rainspout.testing` is public, stable API** *(Gate 2 #9)*: `run_stage` (real
|
|
353
|
+
validation path; wraps deps; optional `coords=`), `from_handler_data` (fake handler
|
|
354
|
+
whose `load_one` returns the object for any coordinates), `assert_roundtrip`
|
|
355
|
+
(load → save → load → equal → catalog; float-tolerant; `equal=` escape hatch;
|
|
356
|
+
checks **preservation of whatever exists** — data always, metadata iff the handler
|
|
357
|
+
claims it; never fails a handler merely for ignoring metadata).
|
|
358
|
+
- **Mandated test shapes**, statically shape-checked at package load (cheap; no
|
|
359
|
+
coverage measurement at load): stages ship `STAGE` + `EXAMPLE_SETTINGS` + a
|
|
360
|
+
known-output test + a failure-path test; handlers ship `HANDLER` +
|
|
361
|
+
`EXAMPLE_RESOURCES` + `EXAMPLE_COORDS` + the round-trip test + tiny committed
|
|
362
|
+
example data. Tests live **inside the component's directory** *(Gate 2 #8)*;
|
|
363
|
+
package-level `tests/` is integration-only. Package coverage is enforced in the
|
|
364
|
+
package's own CI (skeleton CI template; `spout test-package` on demand).
|
|
365
|
+
- **Reference content vs example package:** skeleton `tests/` reference content is
|
|
366
|
+
adversarial and machinery-proving — including a mid-DAG extra `handler:` dependency
|
|
367
|
+
and a trivial, observable setup-hook exercise (**no Cython/C toolchain in CI** —
|
|
368
|
+
*Gate 1 Q7*). The clean example package teaches a user and is the yardstick for
|
|
369
|
+
**blind-agent verification**: a separate agent with only `docs/` + scientific code
|
|
370
|
+
must produce a conforming, runnable package.
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## L. Tooling
|
|
375
|
+
|
|
376
|
+
uv (lock committed) · Typer · Pydantic v2 · structlog (`run_id`-threaded) · pytest +
|
|
377
|
+
pytest-cov · ruff + mypy · GitHub Actions (lint, type, test, coverage, version-bump).
|
|
378
|
+
Content-package territory: h5py/scipy, ONNX Runtime, Docker (`build-image` target).
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## M. Build status (the gated process, as executed)
|
|
383
|
+
|
|
384
|
+
1. **Gate 1** — plan + restatement + proposed answers: delivered, approved with
|
|
385
|
+
corrections (reports/1, prompts/2).
|
|
386
|
+
2. **Gate 2** — authoring docs + tutorials: delivered (reports/2), approved with
|
|
387
|
+
corrections (prompts/3), revised through 2b (question round) and 2c (config
|
|
388
|
+
naming + auxiliary model); `docs/` at the repo root is the canonical set.
|
|
389
|
+
3. **Phase 1 — contracts & registry: DONE** (approved with one required change,
|
|
390
|
+
satisfied: negative tests + metaclass guard for the `__init__` invariant).
|
|
391
|
+
4. **Phase 2 — config & DAG validation, `spout validate`: DONE** (this document
|
|
392
|
+
accompanies its delivery).
|
|
393
|
+
5. **Phase 3 — handlers & data plane: DONE** (verb bodies, metadata/provenance
|
|
394
|
+
models, `rainspout.testing`).
|
|
395
|
+
6. **Phase 4 — runner: DONE** (approved; oplog-location question answered in
|
|
396
|
+
Phase 5: the log follows the run definition, §F).
|
|
397
|
+
7. **Phase 5 — driver + `spout run`: DONE**.
|
|
398
|
+
8. **Phase 6 — CLI complete (catalog/setup/test-package/build-image, package
|
|
399
|
+
verbs): DONE**.
|
|
400
|
+
9. **Phase 7 — reference content + example package + CI: DONE** (system
|
|
401
|
+
complete to v1 scope; review gate passed).
|
|
402
|
+
10. **Phase 8 — literal tutorial verification: DONE.** All three tutorials
|
|
403
|
+
executed step-for-step in a clean environment against the built system;
|
|
404
|
+
divergences fixed on whichever side was wrong (see the decision record).
|
|
405
|
+
Skeleton version set to **1.0.0** — the docs' `rainspout>=1,<2` pin and
|
|
406
|
+
stability commitment now resolve against the real package.
|
|
407
|
+
11. **Phase 9 — blind-authorability verification: PASSED** (run by an
|
|
408
|
+
independent agent chosen by the project owner to avoid biasing; a
|
|
409
|
+
docs-only build of a conforming package succeeded). The gated build is
|
|
410
|
+
complete. Released under the MIT license; published to PyPI as
|
|
411
|
+
`rainspout` from github.com/strongmats/rainspout (tag-driven trusted
|
|
412
|
+
publishing).
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## N. Decision record (what changed at each review, in one place)
|
|
417
|
+
|
|
418
|
+
| Decision | Where settled |
|
|
419
|
+
|---|---|
|
|
420
|
+
| Handler names underscore-only, no dots | Gate 1 C1 |
|
|
421
|
+
| Delta = exists − **attempted**; `--retry-failed` escape hatch | Gate 1 Q1 |
|
|
422
|
+
| Window-bounded `catalog` | Gate 1 Q2 |
|
|
423
|
+
| Dimension roles + startup pre-flight probe (4 steps, empty-catalog notice) | Gate 1 Q3 / Gate 2 #4 |
|
|
424
|
+
| One stage = one self-contained **directory**; tests outside the code hash | Gate 1 Q4 / Gate 2 #7 |
|
|
425
|
+
| `__init__` ban incl. mixins, definition-time | Gate 1 Q5 (+ Phase 1 review: metaclass + negative tests) |
|
|
426
|
+
| `build-image` v1-minimal | Gate 1 Q6 |
|
|
427
|
+
| No Cython/C toolchain in CI; observable setup exercise instead | Gate 1 Q7 |
|
|
428
|
+
| Two windowing layers kept visibly distinct | Gate 1 Q8 |
|
|
429
|
+
| Clean realtime stop semantics | Gate 1 Q9 |
|
|
430
|
+
| Bounded settings mandatory | Gate 1 review |
|
|
431
|
+
| No inter-stage structural checks | Gate 1 review |
|
|
432
|
+
| Private hooks under final public verbs | Gate 2 #1 |
|
|
433
|
+
| Driver expands ranges; handlers see explicit values | Gate 2 #2 |
|
|
434
|
+
| Role-map syntax + seed exactness | Gate 2 #3/#4, Part B4 |
|
|
435
|
+
| Wiring kinds as type annotations | Gate 2 #5 |
|
|
436
|
+
| Single-file metadata, no sidecars; optional posture; preserves-what-exists | Gate 2 #6 |
|
|
437
|
+
| Tests inside the component directory | Gate 2 #8 |
|
|
438
|
+
| `rainspout.testing` = stable public API | Gate 2 #9 |
|
|
439
|
+
| Entry-point groups + discovery gotchas documented | Gate 2 #10 |
|
|
440
|
+
| `--select` × delta semantics | Gate 2 #11 |
|
|
441
|
+
| Stages coordinate-aware; skeleton name-agnostic | Gate 2 B1/B2 |
|
|
442
|
+
| `seed:` config block; no seed-loader stage | Gate 2 B3 |
|
|
443
|
+
| Seed vs auxiliary validation split | Gate 2 B4 |
|
|
444
|
+
| Linear v1 on general graph machinery | Gate 2 B5 |
|
|
445
|
+
| Dimension names = config bookkeeping; no stage-declared dims | Gate 2 B6 |
|
|
446
|
+
| **Named** seed entries (`from: raw`); one in v1 | Gate 2b Q1 |
|
|
447
|
+
| `--force-rewrite` as a separate flag | Gate 2b Q2 |
|
|
448
|
+
| Coordinate keys read via bounded settings | Gate 2b Q3 |
|
|
449
|
+
| Aux dangling-reference typo check (config-internal only) | Gate 2b Q4 |
|
|
450
|
+
| `handler:` key for both dependencies and saves (`loader:` retired) | Gate 2c #1 |
|
|
451
|
+
| Block-form YAML in all examples | Gate 2c #2 |
|
|
452
|
+
| Driver constructs stages, injects resolved dependencies | Gate 2c #3 |
|
|
453
|
+
| Auxiliary handlers stage-callable (`load_one(coords)`); presence-and-wiring guarantee; save targets alone carry role maps; windowed-loader limitation dissolved | Gate 2c #4 |
|
|
454
|
+
| `run_id` per-run + per-work-item sub-key | Gate 1 §3.5, confirmed Phase 1 review |
|
|
455
|
+
| Oplog follows the run definition (`.rainspout/<name>.oplog.jsonl` next to the config; `run.oplog:` override resolves against the config dir, never CWD) | Phase 4 review / Phase 5 |
|
|
456
|
+
| Collector imports carry `# noqa: F401` (linters auto-remove them otherwise) | Phase 7 |
|
|
457
|
+
| Skeleton version 1.0.0: the documented `rainspout>=1,<2` pin must resolve | Phase 8 |
|
|
458
|
+
| Emptied YAML mapping keys (`dependencies:` with nothing under it) read as empty, so validation reaches the real named-offender error | Phase 8 |
|
|
459
|
+
| Registry lookups name their owner (`seed 'raw': unknown handler …`) | Phase 8 |
|
|
460
|
+
| Handler runtime errors print canonical coords + the underlying exception type | Phase 8 |
|
|
461
|
+
| Tutorial 3 uses relative `./data` paths; resource values are handler-interpreted (relative paths resolve against the invocation directory, unlike `run.oplog:`) | Phase 8 |
|
|
462
|
+
| Conditional settings = Pydantic discriminated unions (supported natively); bounded rule applies inside every arm — the conformance lint recurses into nested models; nested settings models declared frozen | Phase 8 addendum |
|
|
463
|
+
| Docs code-display convention: annotated templates (`contract:`/`yours:` per line) + worked examples in one fixed made-up domain; framework imports are only `rainspout.contracts`/`rainspout.testing`, stated once in the docs README | Phase 8 addendum 2 |
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Rainspout
|
|
2
|
+
|
|
3
|
+
A domain-agnostic frame for building scientific data-processing pipelines.
|
|
4
|
+
Rainspout contains no science of its own: content packages ship the stages
|
|
5
|
+
(processing steps) and handlers (storage adapters); one YAML config describes
|
|
6
|
+
a run; the `spout` command executes it, tracking what has already been done.
|
|
7
|
+
|
|
8
|
+
**Start with [`docs/README.md`](docs/README.md)** — the documentation is the
|
|
9
|
+
API: you can build a content package from the authoring guides alone, without
|
|
10
|
+
reading this repository's source.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
pip install rainspout # or: uv add rainspout
|
|
16
|
+
spout --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Development
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
uv sync # create the environment
|
|
23
|
+
uv run pytest # tests (coverage floor enforced)
|
|
24
|
+
uv run ruff check # lint
|
|
25
|
+
uv run mypy # types
|
|
26
|
+
```
|