ab-plumber 0.1.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.
Files changed (72) hide show
  1. ab_plumber-0.1.0/.envrc +17 -0
  2. ab_plumber-0.1.0/.github/ISSUE_TEMPLATE/.gitkeep +0 -0
  3. ab_plumber-0.1.0/.github/workflows/.gitkeep +0 -0
  4. ab_plumber-0.1.0/.github/workflows/ci.yml +24 -0
  5. ab_plumber-0.1.0/.github/workflows/publish.yml +45 -0
  6. ab_plumber-0.1.0/.gitignore +13 -0
  7. ab_plumber-0.1.0/.scratch/discovery/issues/01-phase-discovery-and-ordering.md +27 -0
  8. ab_plumber-0.1.0/.scratch/discovery/issues/02-phasemodule-protocol-and-contract.md +32 -0
  9. ab_plumber-0.1.0/.scratch/discovery/issues/03-config-schema-and-format.md +31 -0
  10. ab_plumber-0.1.0/.scratch/discovery/issues/04-executionstrategy-protocol-and-partitioning.md +36 -0
  11. ab_plumber-0.1.0/.scratch/discovery/issues/05-check-validator-design.md +25 -0
  12. ab_plumber-0.1.0/.scratch/discovery/issues/06-skill-boundary.md +27 -0
  13. ab_plumber-0.1.0/.scratch/discovery/issues/07-packaging-and-repo-discovery.md +31 -0
  14. ab_plumber-0.1.0/.scratch/discovery/issues/08-research-beam-spark-partition-contract.md +50 -0
  15. ab_plumber-0.1.0/.scratch/discovery/issues/09-research-pipeline-framework-prior-art.md +59 -0
  16. ab_plumber-0.1.0/.scratch/discovery/map.md +115 -0
  17. ab_plumber-0.1.0/.scratch/discovery/research/beam-spark-partition-contract.md +397 -0
  18. ab_plumber-0.1.0/.scratch/discovery/research/pipeline-framework-prior-art.md +345 -0
  19. ab_plumber-0.1.0/CLAUDE.md +30 -0
  20. ab_plumber-0.1.0/CONTEXT.md +29 -0
  21. ab_plumber-0.1.0/LICENSE +21 -0
  22. ab_plumber-0.1.0/PKG-INFO +133 -0
  23. ab_plumber-0.1.0/README.md +84 -0
  24. ab_plumber-0.1.0/config/.gitkeep +0 -0
  25. ab_plumber-0.1.0/data/processed/.gitkeep +0 -0
  26. ab_plumber-0.1.0/data/raw/.gitkeep +0 -0
  27. ab_plumber-0.1.0/data/sample/.gitkeep +0 -0
  28. ab_plumber-0.1.0/docs/adapter-shim.md +59 -0
  29. ab_plumber-0.1.0/docs/adr/.gitkeep +0 -0
  30. ab_plumber-0.1.0/docs/agents/domain.md +51 -0
  31. ab_plumber-0.1.0/docs/agents/issue-tracker.md +45 -0
  32. ab_plumber-0.1.0/docs/diagrams/.gitkeep +0 -0
  33. ab_plumber-0.1.0/docs/diagrams/component-readiness.html +294 -0
  34. ab_plumber-0.1.0/flake.lock +27 -0
  35. ab_plumber-0.1.0/flake.nix +55 -0
  36. ab_plumber-0.1.0/plan.md +84 -0
  37. ab_plumber-0.1.0/pyproject.toml +59 -0
  38. ab_plumber-0.1.0/scripts/.gitkeep +0 -0
  39. ab_plumber-0.1.0/src/plumber/__init__.py +3 -0
  40. ab_plumber-0.1.0/src/plumber/check.py +250 -0
  41. ab_plumber-0.1.0/src/plumber/checkpoint.py +27 -0
  42. ab_plumber-0.1.0/src/plumber/cli.py +71 -0
  43. ab_plumber-0.1.0/src/plumber/entries.py +25 -0
  44. ab_plumber-0.1.0/src/plumber/examples/__init__.py +0 -0
  45. ab_plumber-0.1.0/src/plumber/examples/snap_shim.py +28 -0
  46. ab_plumber-0.1.0/src/plumber/local.py +18 -0
  47. ab_plumber-0.1.0/src/plumber/localmp.py +73 -0
  48. ab_plumber-0.1.0/src/plumber/probe.py +57 -0
  49. ab_plumber-0.1.0/src/plumber/protocols.py +48 -0
  50. ab_plumber-0.1.0/src/plumber/resolver.py +21 -0
  51. ab_plumber-0.1.0/src/plumber/run.py +133 -0
  52. ab_plumber-0.1.0/tests/fixture/myio.py +21 -0
  53. ab_plumber-0.1.0/tests/fixture/phases/__init__.py +0 -0
  54. ab_plumber-0.1.0/tests/fixture/phases/counter_state.py +10 -0
  55. ab_plumber-0.1.0/tests/fixture/phases/crash.py +5 -0
  56. ab_plumber-0.1.0/tests/fixture/phases/dedupe_zone_leader.py +9 -0
  57. ab_plumber-0.1.0/tests/fixture/phases/drop_value.py +5 -0
  58. ab_plumber-0.1.0/tests/fixture/phases/reproject.py +5 -0
  59. ab_plumber-0.1.0/tests/fixture/phases/scale_value.py +7 -0
  60. ab_plumber-0.1.0/tests/fixture/phases/strict_by.py +5 -0
  61. ab_plumber-0.1.0/tests/fixture/phases/tag_percentile.py +7 -0
  62. ab_plumber-0.1.0/tests/fixture/plumber.yaml +20 -0
  63. ab_plumber-0.1.0/tests/test_check.py +319 -0
  64. ab_plumber-0.1.0/tests/test_checkpoint.py +113 -0
  65. ab_plumber-0.1.0/tests/test_config.py +108 -0
  66. ab_plumber-0.1.0/tests/test_local_sequential.py +40 -0
  67. ab_plumber-0.1.0/tests/test_localmp.py +132 -0
  68. ab_plumber-0.1.0/tests/test_partial_run.py +134 -0
  69. ab_plumber-0.1.0/tests/test_resolver.py +21 -0
  70. ab_plumber-0.1.0/tests/test_run_e2e.py +26 -0
  71. ab_plumber-0.1.0/tests/test_snap_shim.py +52 -0
  72. ab_plumber-0.1.0/uv.lock +1549 -0
@@ -0,0 +1,17 @@
1
+ watch_file pyproject.toml
2
+ watch_file uv.lock
3
+ watch_file flake.nix
4
+ watch_file flake.lock
5
+
6
+ use flake
7
+
8
+ # On the first activation, create uv.lock and .venv.
9
+ # After that, require the committed lockfile to remain valid.
10
+ if [[ -f uv.lock ]]; then
11
+ uv sync --locked --extra dev --quiet
12
+ else
13
+ uv sync --extra dev --quiet
14
+ fi
15
+
16
+ export VIRTUAL_ENV="$PWD/.venv"
17
+ PATH_add "$VIRTUAL_ENV/bin"
File without changes
File without changes
@@ -0,0 +1,24 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [master, main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.9", "3.12"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - name: Install
20
+ run: python -m pip install -e ".[dev]"
21
+ - name: Lint
22
+ run: python -m ruff check .
23
+ - name: Test
24
+ run: python -m pytest -q
@@ -0,0 +1,45 @@
1
+ name: publish
2
+
3
+ # Build and publish to PyPI on a GitHub Release, using PyPI Trusted Publishing
4
+ # (OIDC) — no API tokens or secrets required. Configure the trusted publisher at
5
+ # https://pypi.org/manage/project/ab-plumber/settings/publishing/ (or via the
6
+ # "pending publisher" flow before the first release). The publisher must name workflow
7
+ # `publish.yml` and environment `pypi`, or the upload is rejected.
8
+
9
+ on:
10
+ release:
11
+ types: [published]
12
+
13
+ jobs:
14
+ build:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.12"
21
+ - name: Build sdist and wheel
22
+ run: |
23
+ python -m pip install --upgrade build
24
+ python -m build
25
+ - name: Check distributions
26
+ run: |
27
+ python -m pip install --upgrade twine
28
+ python -m twine check dist/*
29
+ - uses: actions/upload-artifact@v4
30
+ with:
31
+ name: dist
32
+ path: dist/
33
+
34
+ publish:
35
+ needs: build
36
+ runs-on: ubuntu-latest
37
+ environment: pypi
38
+ permissions:
39
+ id-token: write # required for OIDC trusted publishing
40
+ steps:
41
+ - uses: actions/download-artifact@v4
42
+ with:
43
+ name: dist
44
+ path: dist/
45
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,13 @@
1
+ *.pyc
2
+ __pycache__/
3
+ .venv/
4
+ .direnv/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ data/raw/*
11
+ data/processed/*
12
+ !data/**/.gitkeep
13
+ .claude/worktrees/
@@ -0,0 +1,27 @@
1
+ # Phase discovery & ordering
2
+
3
+ Type: grilling
4
+ Status: open
5
+ Blocked by: —
6
+
7
+ ## Question
8
+
9
+ How does the `plumber` CLI, run from an arbitrary repo, find that repo's phase
10
+ modules and determine their execution order?
11
+
12
+ Decide between (and/or combine): a conventioned `phases/` directory ordered by
13
+ filename prefix; an explicit ordered list in the config file; a
14
+ registry/decorator the modules opt into. Resolve the design-doc section 7 open
15
+ thread — string names in the pipeline list vs. module-object introspection for
16
+ keying per-phase params. Output: the discovery mechanism, the ordering source of
17
+ truth, and how a phase's identity string is formed and matched to its config
18
+ params.
19
+
20
+ ## Input from research
21
+
22
+ Ticket 09 (`research/pipeline-framework-prior-art.md`) recommends the **Ploomber**
23
+ model: an explicit ordered step list in the config file, each entry a dotted path
24
+ resolved directly to `<module>.run`, no project skeleton or package layout
25
+ required (Kedro/Dagster/Prefect all fail "run from any repo" by requiring one).
26
+ No DAG inference, no named IO ports — the chain is linear, one gdf passed through.
27
+ Per-phase params keyed by the config entry, not by module introspection.
@@ -0,0 +1,32 @@
1
+ # PhaseModule protocol & the phase contract
2
+
3
+ Type: grilling
4
+ Status: open
5
+ Blocked by: —
6
+
7
+ ## Question
8
+
9
+ Pin the phase interface and its written contract.
10
+
11
+ - Module-level `run(gdf, **params) -> gdf` as the single public entry point, with
12
+ a `typing.Protocol` (`PhaseModule`) capturing it — confirm this over a bare
13
+ convention or a registry dict, and confirm no class-based ABC.
14
+ - State the purity / partition contract every executor assumes: `run()` is a pure
15
+ function of its input, sees **one partition** (never assumes it has the whole
16
+ dataset), holds no shared mutable state, does not depend on global phase order.
17
+ - CRS / schema expectations: what a phase may assume about its input and must
18
+ guarantee about its output (design doc: "CRS preserved unless explicitly
19
+ reprojected").
20
+ - Where the `GeoDataFrame`-only assumption lives so a later payload type does not
21
+ break the Protocol (feeds the "payload seam" fog item).
22
+
23
+ Output: the `PhaseModule` Protocol definition and a contract statement suitable
24
+ to drop into `CONTEXT.md` / an ADR.
25
+
26
+ ## Input from research
27
+
28
+ Ticket 08 (`research/beam-spark-partition-contract.md`): the contract must state a
29
+ phase is a **partition-local map** — module-level (not a closure), pure,
30
+ idempotent, order-independent, no shared mutable state, captures no unpicklable
31
+ resources. Include a "non-partitionable phase runs on the driver / single
32
+ partition" escape hatch.
@@ -0,0 +1,31 @@
1
+ # Config file schema & format
2
+
3
+ Type: grilling
4
+ Status: open
5
+ Blocked by: 01, 02
6
+
7
+ ## Question
8
+
9
+ Define the plumber config file — the artifact both a human and the AI skill
10
+ author.
11
+
12
+ - Format: YAML or TOML. Pick one; note why.
13
+ - Schema: pipeline name; input block (format, CRS, geometry type); ordered phase
14
+ list with per-phase params and per-phase constraints; execution block
15
+ (strategy, partition spec — by field(s) / chunk size / worker count).
16
+ - How phase identity in the config maps to discovered modules (depends on 01).
17
+ - What is a CLI flag vs. a config key: executor and scale live on flags and
18
+ override config; params live in config. Nail the override precedence.
19
+ - Validation: is the config schema-checked on load, and by what (pydantic /
20
+ plain dataclass + manual checks)?
21
+
22
+ Output: an annotated example config plus the field list and override rules.
23
+
24
+ ## Input from research
25
+
26
+ Ticket 09 (`research/pipeline-framework-prior-art.md`): adopt Kedro's two-layer
27
+ config with **CLI-wins precedence** (`parameters.yml` + `--params`), merge to one
28
+ dict, pass to the phase as `**params` (Ploomber `PythonCallable(**params)`
29
+ style). Optional typed validation via a **phase-exported Pydantic model** that
30
+ `plumber check` verifies (Dagster `Config` idea) — the entry point stays
31
+ `run(gdf, **params)`, never a class. Explicit ordered `phases:` list, no DAG.
@@ -0,0 +1,36 @@
1
+ # ExecutionStrategy protocol & partition model
2
+
3
+ Type: grilling
4
+ Status: open
5
+ Blocked by: 02
6
+
7
+ ## Question
8
+
9
+ Design the execution-strategy seam so `local` + `localmp` ship now and
10
+ `dataflow` + `pyspark` slot in later without a protocol change.
11
+
12
+ - The `ExecutionStrategy` Protocol: `execute(phases, data, params)` signature —
13
+ what `data` is, what comes back, how per-phase params are threaded through.
14
+ - Partition model: how the config's partition spec (by field(s) / chunk size /
15
+ worker count) is turned into partitions, how the phase chain runs per
16
+ partition, how outputs are reassembled into one `GeoDataFrame`.
17
+ - The uniform-partition question: does `local` also pass a single whole-dataset
18
+ partition so phase code never branches on executor? Decide yes/no.
19
+ - What `LocalSequential` and `LocalMultiprocess` each need from the protocol;
20
+ confirm nothing in the signature blocks a Beam `Map`-chain or a Spark
21
+ `mapPartitions` implementation later (cross-check with ticket 08 findings).
22
+
23
+ Output: the `ExecutionStrategy` Protocol, the partition-spec → partitions
24
+ mapping, and the reassembly contract.
25
+
26
+ ## Input from research
27
+
28
+ Ticket 08 (`research/beam-spark-partition-contract.md`) — lock two costless
29
+ decisions so Beam/Spark stay open: type `execute`'s `data` as a **dataset
30
+ reference** (path + format + opts), not a materialized GeoDataFrame — distributed
31
+ executors do their own IO; type the **return as a result/handle**, not
32
+ necessarily a driver-collected GeoDataFrame. Beam has no first-class per-partition
33
+ transform (carry a partition as one pickled element) and no vector IO in core;
34
+ Spark uses `applyInPandas` (by field) / `mapInPandas` (by size) and effectively
35
+ requires Sedona. Local + localmp can still collect to a GeoDataFrame behind the
36
+ same handle type.
@@ -0,0 +1,25 @@
1
+ # `plumber check` validator design
2
+
3
+ Type: prototype
4
+ Status: open
5
+ Blocked by: 02
6
+
7
+ ## Question
8
+
9
+ Decide how `plumber check` inspects a repo's phase modules for contract
10
+ adherence — this is also the capability the AI skill consumes for its rework
11
+ recommendations.
12
+
13
+ - Inspection method: static (AST) / import-and-introspect signatures / runtime
14
+ probe with a small synthetic `GeoDataFrame` fixture — or a layered combination.
15
+ - What it checks: `run()` present and signature-compatible with `PhaseModule`;
16
+ no obvious shared-state / global-order violations; declared vs. actual dtypes;
17
+ CRS-preservation constraint; params in config match `run()`'s accepted kwargs.
18
+ - Output shape: machine-readable (for the skill) + human-readable; per-phase
19
+ pass/fail with actionable messages.
20
+ - Where it runs: standalone `plumber check`, and/or an automatic preflight before
21
+ `plumber run`.
22
+
23
+ Build a rough prototype against 2–3 stub phase modules (one clean, one with a
24
+ bad signature, one with a dtype/CRS violation) to pressure-test the approach.
25
+ Link the prototype from this ticket.
@@ -0,0 +1,27 @@
1
+ # AI skill boundary
2
+
3
+ Type: grilling
4
+ Status: open
5
+ Blocked by: 03, 05
6
+
7
+ ## Question
8
+
9
+ Fix the boundary of the optional AI skill under `ab_spatial/.claude/` — not its
10
+ conversation flow.
11
+
12
+ - Inputs it reads from plumber: the `PhaseModule` / `ExecutionStrategy`
13
+ Protocols, `plumber check` output, the config schema. How does it get them —
14
+ import plumber, shell out to `plumber check --json`, read a schema file?
15
+ - Outputs it emits: the config file (writes/updates it), and rework
16
+ recommendation text for protocol/partition issues. Anything else? Does it ever
17
+ write into the target repo's phase modules? (Constraint says no — confirm and
18
+ record.)
19
+ - Invocation: slash-command name, and what triggers it (explicit only, or also
20
+ on some condition).
21
+ - Degradation: enumerate exactly what a human does by hand when the skill is
22
+ absent, to confirm nothing is skill-only.
23
+ - Dependency direction: skill depends on plumber; plumber never imports or knows
24
+ about the skill.
25
+
26
+ Output: an inputs/outputs/invocation spec for the skill, ready for its own later
27
+ effort to design the conversation.
@@ -0,0 +1,31 @@
1
+ # Packaging, distribution & "run from any repo"
2
+
3
+ Type: grilling
4
+ Status: open
5
+ Blocked by: —
6
+
7
+ ## Question
8
+
9
+ Decide how plumber is installed and how it locates a target repo's phases and
10
+ config when invoked from that repo's working directory.
11
+
12
+ - Distribution: pipx-installed global tool (like `ruff`/`pytest`) vs. a dev
13
+ dependency each pipeline repo adds vs. both. "Run from any repo" leans global.
14
+ - Discovery root: cwd-relative? a `plumber.toml` / `pyproject.toml [tool.plumber]`
15
+ marker at the repo root? an explicit `--config` path?
16
+ - Console-script entry point name (`plumber`), subcommand surface (`run`,
17
+ `check`, later `new-phase`).
18
+ - Dev environment for the plumber repo itself: match maply's Nix + `uv` +
19
+ `direnv` setup, or simpler? Python version floor (maply targets 3.9–3.12).
20
+ - Packaging backend (hatchling, like maply) and versioning approach.
21
+
22
+ Output: the install story, the discovery rules, and the plumber repo's own dev
23
+ setup.
24
+
25
+ ## Input from research
26
+
27
+ Ticket 09 (`research/pipeline-framework-prior-art.md`): Kedro/Dagster/Prefect all
28
+ require their own project skeleton (`pipeline_registry.py`, `Definitions` code
29
+ location, deployments) — that fails "run from any repo". Ploomber's approach fits:
30
+ resolve a dotted path / file directly, no package layout imposed on the target
31
+ repo. Lean that way for discovery.
@@ -0,0 +1,50 @@
1
+ # Research: Beam & Spark partition/step contract for geospatial data
2
+
3
+ Type: research
4
+ Status: resolved
5
+ Blocked by: —
6
+
7
+ ## Question
8
+
9
+ Surface the facts ticket 04 needs so the `ExecutionStrategy` protocol is designed
10
+ without foreclosing `dataflow` (Apache Beam) or `pyspark`, even though neither is
11
+ implemented now.
12
+
13
+ - Apache Beam: how a per-element / per-partition transform is expressed
14
+ (`beam.Map`, `beam.ParDo`), how bundling/partitioning works, what a step
15
+ function may and may not assume, side-input mechanics, and the state of
16
+ geospatial IO connectors (GeoParquet / vector formats) in Beam's Python SDK.
17
+ - PySpark: `mapPartitions` / pandas-UDF / `applyInPandas` contract for running a
18
+ function over a partition as a (Geo)DataFrame; how partitioning by column vs.
19
+ by size is expressed; Sedona's role for geospatial types.
20
+ - The common shape: what a "run this phase chain over one partition" callable
21
+ must look like to be portable across LocalMultiprocess, Beam, and Spark.
22
+ - Anything that would force a signature change to `execute(phases, data, params)`
23
+ or to `run(gdf, **params) -> gdf`.
24
+
25
+ Capture findings as `.scratch/discovery/research/beam-spark-partition-contract.md`
26
+ and link it here. Primary sources: Beam and Spark official docs, Sedona docs.
27
+
28
+ ## Answer
29
+
30
+ Findings: [`.scratch/discovery/research/beam-spark-partition-contract.md`](../research/beam-spark-partition-contract.md)
31
+
32
+ Nothing forces an arity change to `execute(phases, data, params)` or to
33
+ `run(gdf, **params) -> gdf`. Beam's unit of user code is the element (`beam.Map` /
34
+ `ParDo`; no first-class per-partition transform — you carry a partition as one
35
+ pickled element or rebuild it after `GroupByKey`/`GroupIntoBatches`); bundles are
36
+ opaque, unordered, and retried. Spark's per-partition contract is
37
+ `applyInPandas` (partition-by-field) or `mapInPandas` (by size), each needing a
38
+ declared output schema, with geometry kept typed via Sedona (+ native GeoParquet);
39
+ `repartition`/`repartitionByRange` cover column vs range, target-size is manual
40
+ arithmetic. The portable callable is a module-level (not closure) pure function
41
+ `(partition_gdf, params) -> partition_gdf` that is order-independent, idempotent,
42
+ holds no shared mutable state, and captures no unpicklable resources — the exact
43
+ intersection of `Pool` + Beam `ParDo` + Spark `applyInPandas`. Two decisions to
44
+ make now so Beam/Spark stay open, both costless on the current signature: type
45
+ `data` as a dataset reference (not a materialized GeoDataFrame — distributed
46
+ executors do their own IO) and type `execute`'s return as a result/handle (not
47
+ necessarily a collected GeoDataFrame). Also name in ticket 02 that a phase must be
48
+ a partition-local map, with a "non-partitionable phase runs on the driver" escape
49
+ hatch. Beam core has no GeoParquet/vector IO (plain Parquet only; `geobeam` is the
50
+ third-party option); a Spark executor effectively requires Sedona.
@@ -0,0 +1,59 @@
1
+ # Research: pipeline-framework prior art
2
+
3
+ Type: research
4
+ Status: resolved
5
+ Blocked by: —
6
+
7
+ ## Question
8
+
9
+ Survey how established Python pipeline frameworks handle the three seams plumber
10
+ is designing, so tickets 01/03/04 borrow proven patterns instead of reinventing.
11
+
12
+ - Kedro: node/pipeline discovery, the data catalog + parameters YAML, pluggable
13
+ runners (SequentialRunner / ParallelRunner / ThreadRunner), how it keeps
14
+ node functions ignorant of the runner.
15
+ - Dagster: op/asset definition, config schema (`Config` / run config), executors
16
+ (in-process / multiprocess / k8s), partitions API.
17
+ - Prefect: task/flow discovery, task runners (Concurrent / Dask / Ray),
18
+ parametrization.
19
+ - Optional: Luigi, Snakemake, Ploomber for contrast.
20
+
21
+ For each: how phases are discovered/ordered, how config/params are supplied, how
22
+ the execution backend is swapped, and what the phase function is allowed to
23
+ assume. Call out which patterns fit plumber's "functions over classes" +
24
+ "run from any repo" stance and which don't.
25
+
26
+ Capture findings as `.scratch/discovery/research/pipeline-framework-prior-art.md`
27
+ and link it here.
28
+
29
+ ## Answer
30
+
31
+ Full findings: `.scratch/discovery/research/pipeline-framework-prior-art.md`
32
+ (primary-source citations inline).
33
+
34
+ - **Discovery/ordering**: Kedro, Dagster, Snakemake, Luigi all *derive* order from a
35
+ data-dependency / filename DAG; only **Ploomber** lists steps explicitly in a YAML
36
+ (`pipeline.yaml` tasks with `source`/`upstream`). plumber's chain is linear and passes
37
+ one gdf, so the explicit ordered `phases:` list in the config is the right model — no
38
+ DAG inference, no named IO ports.
39
+ - **Discovery mechanism**: Kedro (`pipeline_registry.py` + `conf/` + `settings.py`),
40
+ Dagster (`Definitions` code location) and Prefect (deployments) all need their own
41
+ project skeleton — fails plumber's "run from any repo". Ploomber resolves a dotted
42
+ path / file directly with no package layout; do that: each config entry -> `<module>.run`.
43
+ - **Config/params**: adopt Kedro's two layers with CLI-wins precedence (`parameters.yml`
44
+ + `--params`); hand the merged dict to the phase as `**params` (Ploomber
45
+ `PythonCallable(**params)`). Optional typed validation via a phase-exported Pydantic
46
+ model checked by `plumber check` (Dagster `Config` idea) — entry point stays
47
+ `run(gdf, **params)`, never a class.
48
+ - **Executor swap**: every framework keeps this orthogonal (Prefect `task_runner=`,
49
+ Dagster `executor_def` / `execution:`, Ploomber `executor:`, Kedro `--runner`). One
50
+ CLI flag / config key picks an `ExecutionStrategy`; phase code never references it.
51
+ Mirror Kedro's ParallelRunner rules as plumber's parallel contract: gdf + params must
52
+ be picklable, no shared mutable state, no cross-phase side effects.
53
+ - **Partitioning**: Dagster's model (same compute logic per partition, key injected at
54
+ runtime, strategy owns fan-out/gather) fits; its `PartitionsDefinition` object graph
55
+ does not — plumber's partitioning is explicit config. Keep phases partition-agnostic:
56
+ one gdf in, one gdf out.
57
+ - **Reject**: DAG inference, mandatory framework skeletons, class-per-phase
58
+ (Luigi), target-file-existence as the ordering primitive, and Prefect's
59
+ config-less imperative flow.
@@ -0,0 +1,115 @@
1
+ <!-- wayfinder:map -->
2
+ # plumber — Discovery map
3
+
4
+ > **Migrated to GitHub Issues** (`austinbreunig/plumber`, label `wayfinder:map`).
5
+ > This file is the charting-session record; the GitHub issue is now the live map.
6
+
7
+ ## Destination
8
+
9
+ A signed-off `plan.md` for **plumber**: a pip/pipx-installable geospatial
10
+ pipeline framework + `plumber` CLI — module-level `run(gdf, **params) -> gdf`
11
+ phase protocol, a builder, pluggable execution strategies, config-file-driven
12
+ phase runs, and a `plumber check` validator — plus a thin **optional** AI skill
13
+ (under `ab_spatial/.claude/`) that only helps author the config file and gives
14
+ protocol/partition rework recommendations by consuming plumber's public API. The
15
+ core is fully usable without the skill.
16
+
17
+ Discovery ends when every decision ticket below is resolved or explicitly
18
+ deferred, enough for Tracer to write a spec. We decide the skill's *boundary*
19
+ (what it reads, what it emits, how it's invoked) — not its conversation flow.
20
+
21
+ ## Notes
22
+
23
+ **Domain:** geospatial data engineering. Phases are algorithm modules; execution
24
+ strategy (local / multiprocess / dataflow / pyspark) is orthogonal and swappable
25
+ without touching phase code. Prototype→production scaling is the core pain point
26
+ this separation targets.
27
+
28
+ **Seed design doc:** the "Geospatial Pipeline Project — Design Doc & Handoff
29
+ Spec" from the charting conversation (sections 1–8). Treat its `core/` reasoning
30
+ as load-bearing; treat its section 6 "agentic pipeline generation" as
31
+ superseded by the constraints below.
32
+
33
+ **Skills every session should consult:** `/grilling` + `/domain-modeling` for any
34
+ decision ticket; `/prototype` for the `check` validator ticket. Wu Wei
35
+ (`.claude/skills/wu-wei/`) — plain functions over classes; a class only for
36
+ genuine persistent state (the builder may qualify, the protocols do not).
37
+
38
+ **Constraints established while charting (not re-open without cause):**
39
+
40
+ - **Skill scope:** config authoring + protocol/partition rework recs only. It
41
+ consumes plumber's protocols + `check` output and drives the public API; it
42
+ never reimplements framework logic and never generates whole phase modules.
43
+ Everything works with the skill removed.
44
+ - **Skill home:** `ab_spatial/.claude/` (root), importing plumber as a dep — not
45
+ in-repo.
46
+ - **Config:** a config file (YAML or TOML — TBD) drives the phase list + per-phase
47
+ params. CLI flags pick executor + scale and override params.
48
+ - **Executors:** design `ExecutionStrategy` to admit all four; implement `local`
49
+ + `localmp` for the tracer. `dataflow` + `pyspark` implementations deferred
50
+ (see Out of scope) — the protocol must not foreclose them.
51
+ - **Payload:** `GeoDataFrame` for v1. Leave a seam for other formats (raster,
52
+ plain DataFrame, arrow) without a protocol break.
53
+ - **Partitioning is explicit config:** by field(s), by chunk size, or (mp) by
54
+ worker count.
55
+ - **Name:** `plumber` (was `geo-plumber`).
56
+
57
+ ## Decisions so far
58
+
59
+ <!-- one line per resolved ticket: gist + link -->
60
+
61
+ - [Research: Beam/Spark partition contract](issues/08-research-beam-spark-partition-contract.md)
62
+ — nothing forces an arity change to `execute(...)` or `run(gdf, **params) -> gdf`.
63
+ The portable per-partition callable is a module-level pure fn
64
+ `(partition_gdf, params) -> partition_gdf` (idempotent, order-independent, no
65
+ shared state, picklable) — plumber's phase contract already meets this if
66
+ phases stay pure. Lock two costless decisions so Beam/Spark aren't foreclosed:
67
+ (1) type `execute`'s `data` as a **dataset reference** (path + format + opts),
68
+ not a materialized GeoDataFrame — distributed executors do their own IO;
69
+ (2) type `execute`'s **return as a result/handle**, not necessarily a
70
+ driver-collected GeoDataFrame. Beam core has no vector/GeoParquet IO
71
+ (`geobeam` is 3rd-party); a Spark executor effectively requires Sedona.
72
+ Full findings: `research/beam-spark-partition-contract.md`.
73
+ - [Research: pipeline-framework prior art](issues/09-research-pipeline-framework-prior-art.md)
74
+ — Kedro/Dagster/Snakemake/Luigi all *derive* order from a DAG and need their
75
+ own project skeleton (fails "run from any repo"). **Ploomber** is the model:
76
+ explicit ordered step list in YAML, each entry a dotted path resolved directly
77
+ to `<module>.run`, no package layout. Config: Kedro's two layers with CLI-wins
78
+ precedence (`parameters.yml` + `--params`) merged and passed as `**params`;
79
+ optional typed validation via a phase-exported Pydantic model checked by
80
+ `plumber check` — entry point stays `run(gdf, **params)`, never a class.
81
+ Executor swap: one flag/key picks an `ExecutionStrategy`, phase code never
82
+ references it; parallel contract = Kedro ParallelRunner rules (picklable, no
83
+ shared state, no cross-phase side effects). Partitioning: Dagster's
84
+ runtime-key-injection model, but explicit in config — phases stay
85
+ partition-agnostic, one gdf in / one gdf out. Reject: DAG inference, mandatory
86
+ skeletons, class-per-phase, target-file-existence ordering.
87
+ Full findings: `research/pipeline-framework-prior-art.md`.
88
+
89
+ ## Not yet specified
90
+
91
+ <!-- in-scope fog; graduates to tickets as the frontier advances -->
92
+
93
+ - **Payload seam for non-GDF formats** — how the GeoDataFrame assumption is
94
+ isolated so raster / DataFrame / arrow can slot in later. Revisit after the
95
+ PhaseModule protocol and ExecutionStrategy tickets resolve.
96
+ - **Tracer fixture / first pipeline** — the synthetic fixture + stub phases (or a
97
+ real pipeline) used to exercise the interface end to end. Revisit after the
98
+ config-schema and ExecutionStrategy tickets.
99
+ - **Skill conversation flow + spec diffing/versioning** — how the interview runs
100
+ and how a later session diffs an existing config instead of starting over.
101
+ Revisit after the skill-boundary ticket. (Its own effort, likely.)
102
+ - **Contract-violation → rework-rec formatting** — how `check` failures are
103
+ phrased back to a human or the skill; any retry semantics. Revisit after the
104
+ `check` validator ticket.
105
+
106
+ ## Out of scope
107
+
108
+ <!-- ruled beyond the destination; never graduates -->
109
+
110
+ - **`dataflow` + `pyspark` executor implementations** — the protocol must admit
111
+ them; building/testing them is a later effort.
112
+ - **Agent-generated phase modules** — the design doc's section 6a/6c codegen. The
113
+ skill authors config + recs only.
114
+ - **The skill's conversation-flow design** — deferred; becomes its own effort
115
+ once the boundary is fixed.