Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/bootstrap/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ The dry run prints the package-directory rename and the list of files that would

Re-run the exact same command **without** `--dry-run`. The script:
- renames `piper/` → `<package>/` (via `git mv`, so history follows). The e2e test file is named after its demo (`tests/e2e/test_extract_entities.py`), not the project, so nothing in `tests/` is renamed — only its contents are edited.
- substitutes the name across `pyproject.toml`, `README.md`, `CLAUDE.md`, the package's `.py`/`.mthds` files, and the tests — using context-aware rules: the dash dist form in CLI-command / distribution positions, the underscore package form in imports and paths, the title in prose. `pyproject.toml` is handled with targeted per-key edits (its package-list arrays need the package form in a bare-string position). It then asserts no placeholder token survived, and aborts with the offending locations if one did.
- substitutes the name across `pyproject.toml`, `README.md`, `CLAUDE.md`, the `Makefile` (the codegen targets point at `<package>/generated` and `<package>/methods` paths), the package's `.py`/`.mthds` files, the tests, and the docs under `docs/` — using context-aware rules: the dash dist form in CLI-command / distribution positions, the underscore package form in imports and paths, the title in prose. `pyproject.toml` is handled with targeted per-key edits (its package-list arrays need the package form in a bare-string position). It then asserts no placeholder token survived, and aborts with the offending locations if one did.
- fills in description, and (if given) author, repo URL
- applies the license choice in all three places: the `LICENSE` body, `license = "..."` in `pyproject.toml`, and the README license line
- strips the README template block
Expand Down
10 changes: 10 additions & 0 deletions .claude/skills/bootstrap/scripts/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ def validate_package(package: str) -> None:
)
if keyword.iskeyword(package):
sys.exit(f"Invalid package name {package!r}: it is a Python keyword.")
# A name like `piper_tools` re-matches the placeholder regex after its own
# insertion (the pyproject transform would corrupt it into `piper_tools_tools`),
# so refuse it up front. Names merely embedding the token (e.g. `sandpiper`)
# have no word boundary before `piper` and stay allowed.
if PIPER_PACKAGE_RE.search(package):
sys.exit(
f"Invalid package name {package!r}: it collides with the template's 'piper' placeholder — pick a name that doesn't start with 'piper_'."
)


def validate_dist(dist: str) -> None:
Expand Down Expand Up @@ -441,12 +449,14 @@ def gather_target_files(root: Path, names: Names) -> list[Path]:
root / "pyproject.toml",
root / "README.md",
root / "CLAUDE.md",
root / "Makefile",
root / "LICENSE",
]
pkg_dir = root / names.package
candidates += sorted(pkg_dir.rglob("*.py"))
candidates += sorted(pkg_dir.rglob("*.mthds"))
candidates += sorted((root / "tests").rglob("*.py"))
candidates += sorted((root / "docs").rglob("*.md"))
seen: set[Path] = set()
files: list[Path] = []
for path in candidates:
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- **Each execution mode is now a self-contained copy-paste unit.** A mode used to be an `ExecutionMode` value threaded through a dispatch chain (CLI command → `_dispatch()` → `match` → `piper/runner.py` → the SDK), so answering "how do I call the API?" meant hopping through four layers, and the `match` existed only to showcase all three lifecycles behind one option — nothing anyone would copy. Now each mode is one file (`piper/blocking/cli.py`, `piper/attended/cli.py`, `piper/detached/cli.py`) holding its commands, its SDK lifecycle, and its progress rendering, with one public lifecycle helper that *is* the mode (`execute_pipe` / `start_and_wait` / `start_pipe`). `piper/runner.py`, `ExecutionMode`, `_dispatch()`, and the `RunResults` adaptation that existed only to give the dispatch one return type are all deleted; `piper/cli.py` shrinks to a `load_dotenv` callback plus three `add_typer` calls. New: `docs/cli-architecture.md`.
- **Only mode-orthogonal code is shared: `piper/inputs.py` (new) and `piper/errors.py`.** `piper/file_input.py` is merged into `piper/inputs.py`, which now also owns the text-or-file input helper (`read_text_input`). Lifecycle code is never shared — the demo commands are deliberately near-duplicated across the three mode files (diff two of them and only the lifecycle helper differs), and `tests/unit/test_mode_symmetry.py` guards that duplication against drift. The hints in `piper/errors.py` now name the mode groups (a blocking run that hits the ~30s cap points at `piper attended`; an interrupted attended run points at `piper detached wait <id>`, which is exactly what it has become).
- **The typed output models are now generated from the bundles, not hand-written.** `pipelex codegen` projects each method's concepts into `piper/generated/<method>/models.py` (a stamped module plus a `codegen.lock` recording the artifact set). `generate-image` parses into the generated built-in `Image` model (natives are materialized into the generated client), replacing the hand-written `GeneratedImage`.
- **Removed the `piper/examples/` layer.** Each demo is now one self-contained command in `piper/cli.py`: bundle path (via a single `METHODS_DIR` constant), pipe code, and inline narrowing into the generated model (`Model.model_validate(results.main_stuff)`) — no more per-demo wrapper module hiding that line behind a `parse()` indirection. The `parse()` unit tests went with it (they only exercised pydantic's `model_validate`); the e2e tests narrow inline the same way.
- **Removed the `piper/examples/` layer.** Each demo is now one self-contained command in its mode file (`piper/<mode>/cli.py`): bundle path (each mode file owns its `METHODS_DIR` constant), pipe code, and inline narrowing into the generated model (`Model.model_validate(main_stuff)`) — no more per-demo wrapper module hiding that line behind a `parse()` indirection. The `parse()` unit tests went with it (they only exercised pydantic's `model_validate`); the e2e tests narrow inline the same way.
- **Fixed the `/bootstrap` pyproject transform.** Its per-key edits had gone stale against the current `pyproject.toml` (multi-line `packages` array with the `piper.generated.*` subpackages, quoted package-data keys, the `piper/generated` ruff exclude), so bootstrapping aborted on the survivor check. Package-name tokens in pyproject are now rewritten generically (quoted-exact + dotted/path positions), which no longer goes stale when the package list changes; the bootstrap test fixture now mirrors the real pyproject shapes.
- **New make targets:** `make codegen` regenerates the typed clients and the runnable `inputs.template.json` scaffolds beside each bundle; `make codegen-check` verifies offline (pure hashing, no network or API key) that the generated clients are current. The `pipelex` CLI is not a starter dependency — point the `PIPELEX` make variable at a pipelex install that ships `codegen`. CI wiring of `codegen-check` lands once a released pipelex ships the command.
- **New offline smoke tests** (`tests/unit/test_generated_clients.py`): the generated modules import, carry the stamp + lock, round-trip their serialization, and each committed input template names exactly the inputs the CLI dispatches.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ This starter calls the **hosted Pipelex API** via the `pipelex-sdk` package (`Pi
- **The execution mode is the command group, not an option.** There are exactly three, each a self-contained Typer sub-package: `piper blocking …` (`client.execute` — one call, dies at the hosted ~30s cap), `piper attended …` (`client.start` + `client.wait_for_result` — durable, you wait), `piper detached …` (`client.start` only — durable, you collect it later with `piper detached status|result|wait <id>`). Attended and detached start the *same* durable run; the axis they name is who waits. There is no default mode and no `--mode` option: `piper/cli.py` is a thin assembler (`load_dotenv` callback + three `add_typer` calls in reading order) and nothing else.
- **Each mode file is a copy-paste unit; lifecycle code is never shared.** `piper/<mode>/cli.py` holds that mode's whole story: its `typer.Typer`, its consoles (results → stdout, progress → stderr), its one public lifecycle helper (`execute_pipe` / `start_and_wait` / `start_pipe`, plus `attend_run` + the fetchers in detached), its demo commands, and a private `_run()` that wraps `asyncio.run` and catches SDK errors once. The **only** shared modules are the two that are orthogonal to execution: `piper/inputs.py` (text-or-file input with a built-in **sample fallback** so every demo runs with zero arguments — `read_text_input` returns `TextInput(text, is_sample)` and the demo prints a stderr notice when the sample was used; plus file → `{"concept": "Document", "content": …}` envelope with the file base64-encoded into a `data:` URL, and the `SAMPLE_*` constants) and `piper/errors.py` (SDK error → message + hint, hints naming the mode groups; it reads the RFC 7807 **problem+json** body off raw protocol-route `httpx.HTTPStatusError`s and branches on the structured `error_type`, e.g. `StartRequiresAsyncOrchestration` → "use `piper blocking`"). Do not introduce a shared runner — the dispatch indirection is exactly what this layout removed. See `docs/cli-architecture.md`.
- **Full demo matrix, guarded.** All three demos exist in all three modes: `extract-entities` (text in), `summarize-pdf` (a *file* in), `generate-image` (prompt in). `generate-image` is the deliberate slow case that overruns the ~30s blocking cap — `piper blocking generate-image` is *expected to fail*, and that is the teaching moment for the durable modes. The near-duplication across mode files is the pedagogy (diff two mode files and only the lifecycle helper differs); `tests/unit/test_mode_symmetry.py` keeps it from drifting. `samples/sample-invoice.pdf` is shipped for `summarize-pdf`.
- The SDK resolves the main output on every path (`client.execute` returns a `PipelexExecuteResult`, the durable path a `RunResults`, both exposing a resolved `.main_stuff`, typed `Any`; a completed run with no main stuff raises `MissingMainStuffError`). So every lifecycle helper returns `main_stuff`, and each demo command narrows it inline — e.g. `ExtractedEntities.model_validate(main_stuff)` — into the generated model. There is no per-example wrapper layer.
- The SDK resolves the main output on every result-producing path (`client.execute` returns a `PipelexExecuteResult`, the durable path a `RunResults`, both exposing a resolved `.main_stuff`, typed `Any`; a completed run with no main stuff raises `MissingMainStuffError`). So the result-producing lifecycle helpers (`execute_pipe`, `start_and_wait`, detached's `attend_run`) return `main_stuff`, and the blocking/attended demo commands narrow it inline — e.g. `ExtractedEntities.model_validate(main_stuff)` — into the generated model. Detached is the exception by design: `start_pipe` returns only the run id (the demos print it bare), and the run-id commands (`wait`/`result`) print the output generically — no model narrowing, since at collection time the command doesn't know which method the run executed. There is no per-example wrapper layer.
- The modes spell out lifecycles the SDK could hide: `client.start_and_wait()` is a self-healing one-liner that picks the path for you (the production shortcut). The starter writes them out because teaching the difference is the point.
- **The typed models are generated, never hand-written.** `pipelex codegen` projects each bundle's concepts into `piper/generated/<method>/models.py` (stamped, locked by a sibling `codegen.lock`); the mode CLIs and the e2e tests import from there. Do NOT edit generated files — edit the bundle, then `make codegen` (regenerates models + `inputs.template.json` scaffolds) and `make codegen-check` (offline drift check). The `pipelex` CLI is not a dependency of this starter; point the `PIPELEX` make variable at a pipelex install that ships `codegen`. `piper/generated` is excluded from ruff (reformatting would trip the drift check) but fully type-checked. See `docs/codegen.md`.

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,10 @@ The other demos run through the exact same path — they differ only in their in
**The mode is the command group, not an option.** There are three, and every demo runs in all three:

```bash
uv run piper blocking extract-entities | summarize-pdf | generate-image
uv run piper attended extract-entities | summarize-pdf | generate-image
uv run piper detached extract-entities | summarize-pdf | generate-image
uv run piper detached wait | status | result <run-id>
uv run piper blocking extract-entities # or summarize-pdf, generate-image
uv run piper attended extract-entities # or summarize-pdf, generate-image
uv run piper detached extract-entities # or summarize-pdf, generate-image
uv run piper detached wait <run-id> # or status, result
```

Read them in that order — each one is a single self-contained file (`piper/<mode>/cli.py`) you can copy straight into your own project.
Expand Down
Loading
Loading