Skip to content

feat: Slice 1 — Python client library (define + run pipelines in code, over gRPC) - #1

Merged
devarismeroxa merged 2 commits into
mainfrom
feat/slice1-client-library
Jul 24, 2026
Merged

feat: Slice 1 — Python client library (define + run pipelines in code, over gRPC)#1
devarismeroxa merged 2 commits into
mainfrom
feat/slice1-client-library

Conversation

@devarismeroxa

Copy link
Copy Markdown
Contributor

First slice of the Conduit Python client library — the gRPC embed direction (ADR: embed bindings via gRPC, not a C-ABI). Define a pipeline in code, run it, control its lifecycle — all over Conduit's existing control-plane gRPC API. No core-engine changes.

What's here (Slice 1 / Case A)

  • Pipeline builder: conduit.Pipeline(id).source(plugin, **cfg).destination(plugin, **cfg).process(plugin, **cfg).dlq(...) → a pure BuildPlan.
  • conduit.local(...): spawns + supervises a conduit subprocess (download-on-first-use from GitHub Releases, GoReleaser naming, SHA-256 verified against checksums.txt, cached per-version via platformdirs, never touches PATH), stable state-dir default (./.conduit/state, warns, never ephemeral — at-least-once safety), graceful teardown on exit.
  • conduit.connect(addr): bind to an already-running Conduit (local subprocess or remote/service).
  • run = client.run(pipeline)wait_running() / status() / stop().
  • All gRPC failures wrap into conduit.ConduitError (decodes Conduit's real google.rpc.ErrorInfo — code + message, not raw stack traces).
  • RPCs driven: CreatePipeline/GetPipeline/StartPipeline/StopPipeline/UpdateDLQ, CreateConnector, CreateProcessor, GetInfo.

Verification

  • 45 unit tests (builder→payload mapping, error decoding against a real in-process gRPC server, run() sequencing, provisioning with mocked download).
  • 1 integration test that actually ran (sandbox had network): downloaded Conduit v0.18.0, ran generator→log end-to-end through the client, stopped gracefully.
  • ruff + mypy --strict clean; buf-generated stubs committed + reproducible.

Scope

Slice 1 = Case A (define + run). Out (later slices): the external-connector/inline_source host-as-source (Slice 2, needs the engine-side dial feature), Node (Slice 3), typed connector-config codegen (fast-follow — connector settings are **kwargs dicts for now).

Settled (were flagged open)

  • Distribution name: conduit-client (PyPI-available, confirmed).
  • Release target: v0.20 (pairs with the Python connector SDK GA).

Risk tier

Tier 1 — frozen public API surface (import path + New/Pipeline/local/connect/run shapes). Needs maintainer (DeVaris) sign-off; a fresh-context DX/correctness review runs before merge.

🤖 Generated with Claude Code
https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD

devarismeroxa and others added 2 commits July 24, 2026 11:33
…RPC API

Implements docs/design/20260724-embed-grpc-client-libraries.md's Slice 1:
define-and-run pipelines in code against Conduit's existing control-plane API
(proto/api/v1/api.proto), per the ADR (20260724-embed-bindings-via-grpc.md)
that chose gRPC over a C-ABI. Named connector plugins only (Case A);
inline_source/inline_destination (Case B) is Slice 2, gated on the
external-connector engine feature.

- Pipeline builder (.source/.destination/.process/.dlq) -> exact
  CreatePipeline/CreateConnector/CreateProcessor/UpdateDLQ/StartPipeline
  payloads. Settings accepts both **kwargs (identifier-shaped keys) and an
  explicit settings= dict (for real connector config keys like
  "format.type" that can't be Python keywords).
- conduit.local(): download-on-first-use, checksum-verified conduit binary
  provisioning (GitHub Releases, GoReleaser artifact naming), subprocess
  supervision with a stable (never ephemeral-temp) default state_dir, and
  graceful-then-forced shutdown.
- conduit.connect(addr): thin client for an already-running engine.
- Run handle: wait_running()/status()/stop(), and ConduitError translating
  every grpc.RpcError (including the google.rpc.ErrorInfo detail Conduit's
  server attaches) -- never a raw gRPC stack trace.
- Vendored gRPC/protobuf stubs generated via buf from
  proto/api/v1/api.proto + transitive deps (conduit-commons, googleapis,
  grpc-gateway openapiv2 options); tools/generate-stubs.sh regenerates them.

Gates green locally: ruff format/check, mypy --strict, and pytest (45 unit
tests covering builder->payload mapping, error translation, and provisioning
with a mocked download; 1 integration test that for real downloads a conduit
v0.18.0 binary, runs a generator->log pipeline end-to-end, and gracefully
stops it -- this sandbox has network access, so it ran rather than skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
…tial run() failure

PR #1 review fixes (Slice 1):

- Pipeline.dlq() silently sent window_size=0/window_nack_threshold=0 on the
  wire whenever the caller didn't pass them explicitly. UpdateDLQ takes these
  at face value with no server-side defaulting (unlike pipeline creation's
  DefaultDLQ, window_size=1), so window_size=0 disables the nack-window
  stop-safety -- calling .dlq("builtin:log") was worse than never calling it.
  Now defaults window_size=1/window_nack_threshold=0 to mirror the engine's
  DefaultDLQ, unless the caller passes explicit values (including 0). Added a
  regression test asserting the UpdateDLQ payload's window fields.

- Client.run()'s mid-sequence failure (pipeline created, then a
  connector/processor/DLQ/start call fails) raised a ConduitError with no way
  for the caller to find the already-created pipeline to clean it up. Added
  ConduitError.pipeline_id, set by run() whenever CreatePipeline already
  succeeded before the failing step.

- connect(check_version=True) only checks reachability, not version -- left
  the behavior, added a TODO at the call site and tightened the docstring so
  the flag name doesn't overpromise.

Gates: ruff format --check, ruff check, mypy (strict, packages=["conduit"])
all clean; full pytest (50 tests, including the new DLQ + pipeline_id
regression tests) green; generated _grpc untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
@devarismeroxa

Copy link
Copy Markdown
Contributor Author

Pushed review fixes (5e02df3), not merged — Tier 1, needs DeVaris sign-off.

MUST-FIX — DLQ nack-window (data-integrity). Pipeline.dlq() only put window_size/window_nack_threshold on the wire when the caller passed them explicitly; otherwise UpdateDLQ received the protobuf zero. The engine's UpdateDLQ takes window_size at face value with no defaulting — window_size=0 disables the nack-window stop-safety, and DefaultDLQ (window_size=1) only applies at pipeline creation, never on update. So .dlq("builtin:log") was silently worse than never calling .dlq(). Fixed: .dlq() now defaults window_size=1/window_nack_threshold=0 (mirroring the engine's own DefaultDLQ) unless the caller passes explicit values — including an explicit 0, which is respected as a deliberate opt-out. Added test_dlq_defaults_window_size_to_one_when_not_given and test_dlq_explicit_window_size_zero_is_respected in tests/unit/test_pipeline_builder.py (this bug shipped invisibly — no prior test asserted on the DLQ payload's window fields).

SHOULD-FIX — partial-creation cleanup. Client.run()'s no-rollback design is intentional (documented), but a mid-sequence failure gave the caller no way to find the already-created pipeline. Added ConduitError.pipeline_id, set by run() whenever CreatePipeline already succeeded before the failing step. Regression test: test_run_mid_sequence_failure_carries_pipeline_id (tests/unit/test_client_run.py), plus a unit test on ConduitError.__str__/default in tests/unit/test_errors.py.

TRACK — check_version naming. Left the behavior (reachability-only) unchanged; added a TODO at the connect() call site and tightened the docstring so the flag doesn't overpromise version-checking that isn't implemented.

Gates: ruff format --check + ruff check clean, mypy (strict, packages=["conduit"], matching CI's uv run mypy) clean, full pytest green (50 tests), generated _grpc untouched.

@devarismeroxa
devarismeroxa merged commit 5efa40a into main Jul 24, 2026
9 checks passed
devarismeroxa pushed a commit that referenced this pull request Aug 3, 2026
The pending publisher is configured (project conduit-client, owner ConduitIO,
repo conduit-client-python, workflow release.yml, no environment), so the OIDC
exchange now has a counterpart to match. Adds permissions: id-token: write and
uncomments the pypa/gh-action-pypi-publish step.

Publishing is gated on a tag push, so workflow_dispatch remains a build+check
dry run — the workflow stays usable for verification without risking a publish.

FLAGGED, NOT RESOLVED: README's 'Open questions for DeVaris' #1 lists the
distribution name as unsettled (conduit-client vs conduit-embed vs bare
conduit). Configuring a pending publisher does not claim a name, but the FIRST
PUBLISH does — permanently, since PyPI neither frees nor reuses names. That
question needs answering before the first tag, not after.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant