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.
- ab_plumber-0.1.0/.envrc +17 -0
- ab_plumber-0.1.0/.github/ISSUE_TEMPLATE/.gitkeep +0 -0
- ab_plumber-0.1.0/.github/workflows/.gitkeep +0 -0
- ab_plumber-0.1.0/.github/workflows/ci.yml +24 -0
- ab_plumber-0.1.0/.github/workflows/publish.yml +45 -0
- ab_plumber-0.1.0/.gitignore +13 -0
- ab_plumber-0.1.0/.scratch/discovery/issues/01-phase-discovery-and-ordering.md +27 -0
- ab_plumber-0.1.0/.scratch/discovery/issues/02-phasemodule-protocol-and-contract.md +32 -0
- ab_plumber-0.1.0/.scratch/discovery/issues/03-config-schema-and-format.md +31 -0
- ab_plumber-0.1.0/.scratch/discovery/issues/04-executionstrategy-protocol-and-partitioning.md +36 -0
- ab_plumber-0.1.0/.scratch/discovery/issues/05-check-validator-design.md +25 -0
- ab_plumber-0.1.0/.scratch/discovery/issues/06-skill-boundary.md +27 -0
- ab_plumber-0.1.0/.scratch/discovery/issues/07-packaging-and-repo-discovery.md +31 -0
- ab_plumber-0.1.0/.scratch/discovery/issues/08-research-beam-spark-partition-contract.md +50 -0
- ab_plumber-0.1.0/.scratch/discovery/issues/09-research-pipeline-framework-prior-art.md +59 -0
- ab_plumber-0.1.0/.scratch/discovery/map.md +115 -0
- ab_plumber-0.1.0/.scratch/discovery/research/beam-spark-partition-contract.md +397 -0
- ab_plumber-0.1.0/.scratch/discovery/research/pipeline-framework-prior-art.md +345 -0
- ab_plumber-0.1.0/CLAUDE.md +30 -0
- ab_plumber-0.1.0/CONTEXT.md +29 -0
- ab_plumber-0.1.0/LICENSE +21 -0
- ab_plumber-0.1.0/PKG-INFO +133 -0
- ab_plumber-0.1.0/README.md +84 -0
- ab_plumber-0.1.0/config/.gitkeep +0 -0
- ab_plumber-0.1.0/data/processed/.gitkeep +0 -0
- ab_plumber-0.1.0/data/raw/.gitkeep +0 -0
- ab_plumber-0.1.0/data/sample/.gitkeep +0 -0
- ab_plumber-0.1.0/docs/adapter-shim.md +59 -0
- ab_plumber-0.1.0/docs/adr/.gitkeep +0 -0
- ab_plumber-0.1.0/docs/agents/domain.md +51 -0
- ab_plumber-0.1.0/docs/agents/issue-tracker.md +45 -0
- ab_plumber-0.1.0/docs/diagrams/.gitkeep +0 -0
- ab_plumber-0.1.0/docs/diagrams/component-readiness.html +294 -0
- ab_plumber-0.1.0/flake.lock +27 -0
- ab_plumber-0.1.0/flake.nix +55 -0
- ab_plumber-0.1.0/plan.md +84 -0
- ab_plumber-0.1.0/pyproject.toml +59 -0
- ab_plumber-0.1.0/scripts/.gitkeep +0 -0
- ab_plumber-0.1.0/src/plumber/__init__.py +3 -0
- ab_plumber-0.1.0/src/plumber/check.py +250 -0
- ab_plumber-0.1.0/src/plumber/checkpoint.py +27 -0
- ab_plumber-0.1.0/src/plumber/cli.py +71 -0
- ab_plumber-0.1.0/src/plumber/entries.py +25 -0
- ab_plumber-0.1.0/src/plumber/examples/__init__.py +0 -0
- ab_plumber-0.1.0/src/plumber/examples/snap_shim.py +28 -0
- ab_plumber-0.1.0/src/plumber/local.py +18 -0
- ab_plumber-0.1.0/src/plumber/localmp.py +73 -0
- ab_plumber-0.1.0/src/plumber/probe.py +57 -0
- ab_plumber-0.1.0/src/plumber/protocols.py +48 -0
- ab_plumber-0.1.0/src/plumber/resolver.py +21 -0
- ab_plumber-0.1.0/src/plumber/run.py +133 -0
- ab_plumber-0.1.0/tests/fixture/myio.py +21 -0
- ab_plumber-0.1.0/tests/fixture/phases/__init__.py +0 -0
- ab_plumber-0.1.0/tests/fixture/phases/counter_state.py +10 -0
- ab_plumber-0.1.0/tests/fixture/phases/crash.py +5 -0
- ab_plumber-0.1.0/tests/fixture/phases/dedupe_zone_leader.py +9 -0
- ab_plumber-0.1.0/tests/fixture/phases/drop_value.py +5 -0
- ab_plumber-0.1.0/tests/fixture/phases/reproject.py +5 -0
- ab_plumber-0.1.0/tests/fixture/phases/scale_value.py +7 -0
- ab_plumber-0.1.0/tests/fixture/phases/strict_by.py +5 -0
- ab_plumber-0.1.0/tests/fixture/phases/tag_percentile.py +7 -0
- ab_plumber-0.1.0/tests/fixture/plumber.yaml +20 -0
- ab_plumber-0.1.0/tests/test_check.py +319 -0
- ab_plumber-0.1.0/tests/test_checkpoint.py +113 -0
- ab_plumber-0.1.0/tests/test_config.py +108 -0
- ab_plumber-0.1.0/tests/test_local_sequential.py +40 -0
- ab_plumber-0.1.0/tests/test_localmp.py +132 -0
- ab_plumber-0.1.0/tests/test_partial_run.py +134 -0
- ab_plumber-0.1.0/tests/test_resolver.py +21 -0
- ab_plumber-0.1.0/tests/test_run_e2e.py +26 -0
- ab_plumber-0.1.0/tests/test_snap_shim.py +52 -0
- ab_plumber-0.1.0/uv.lock +1549 -0
ab_plumber-0.1.0/.envrc
ADDED
|
@@ -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,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.
|