feat: week-1 scaffold + Lane A (gRPC stubs, go-plugin handshake) - #1
Merged
Conversation
Bootstraps ConduitIO/conduit-connector-sdk-python for the v0.19 Python connector SDK workstream (pulled forward per DeVaris's explicit override, Tier 1 / data path per CLAUDE.md). - Repo scaffold: pyproject.toml (uv/hatchling), ruff+mypy config, CI workflow stubs (lint/test/compat-nightly/release), LICENSE, README, CONTRIBUTING, CHANGELOG. - Design doc landed at docs/design/20260707-python-connector-sdk.md, adapted from the reviewed ConduitIO/conduit design doc with three build-review must-fixes folded in: (1) re-verified the two Go-source citations underpinning the invariant-1/3/4 parity argument (destination.go:345-350, source.go:270-295 — both confirmed correct against conduit-connector-sdk); (2) tightened the Phase-1 shutdown acceptance test from a timing/log heuristic to a deterministic GRPCController.Shutdown RPC-invocation assertion; (3) added the hung/deadlocked-asyncio-event-loop-mid-write failure mode (no Go analog, since goroutines are preemptible and asyncio signal handlers are not), with a bounded SDK-side watchdog force-kill deadline as mitigation. - Lane A: vendored gRPC/protobuf stubs for conduit-connector-protocol v2 (SourcePlugin/DestinationPlugin/SpecifierPlugin) + the opencdc/config message types from conduit-commons, generated via buf against BSR and scoped with --path to exclude the deprecated v1 protocol and unrelated commons surfaces (tools/generate-stubs.sh is the single source of truth for the regen command). - Lane A: go-plugin handshake implementation (_handshake.py) — magic cookie check, protocol version negotiation (v2 only), and the exact pipe-delimited stdout handshake line, unit-tested against the documented wire format independent of a running gRPC server. Gates run locally: package builds/imports, pytest 18/18 green, ruff format+lint clean, mypy --strict clean. Not run: acceptance suite, real Conduit launch, chaos/SIGTERM tests — all later lanes, not week-1 scope. No GitHub repo created — DeVaris to create ConduitIO/conduit-connector-sdk-python. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
devarismeroxa
added a commit
that referenced
this pull request
Jul 23, 2026
DX audit fix #1. `datetime.timedelta`-typed BaseConfig fields now map to config.Parameter.TYPE_DURATION instead of raising NotImplementedError: - New public conduit.config.format_go_duration()/parse_go_duration(): serialize/parse Go's time.Duration.String()/ParseDuration syntax ("5s", "1h30m", "500ms", "1.5h", "-1.5h", ns/us/µs/ms/s/m/h units), using exact fractions.Fraction arithmetic (no float rounding) down to microsecond resolution -- the finest datetime.timedelta supports. - to_parameters() serializes a timedelta field's default via format_go_duration(); gt=/lt=/ge=/le= constraints on duration fields use the same exact (not epsilon-approximated) ±1-microsecond boundary adjustment already used for int fields, since timedelta is likewise a discrete, integer-resolution type. - BaseConfig gained a model_validator(mode="before") that parses a Go-duration string into a real timedelta before pydantic's own validation runs, so the Configure RPC's map<string,string> config values round-trip correctly. Direct construction with a real timedelta still works unchanged. - TYPE_EXCLUSION remains an open, documented A-gap (untouched by this fix) -- still raises NotImplementedError rather than guessing. Tests: tests/test_go_duration.py (known-Go-output pins + Hypothesis round-trip property over arbitrary microsecond counts + malformed-syntax rejection), tests/test_config.py (to_parameters() mapping + Configure-side string parsing + direct-construction-still-works cases). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
devarismeroxa
added a commit
that referenced
this pull request
Jul 23, 2026
…bug found by it DX audit fix #5 (the #1 lever per the audit). New `conduit-connector-sdk` console script, `build` subcommand (_build.py/_cli.py): packages a connector project into one self-contained, directly-executable artifact. Closes the design doc §1.1.6 packaging gap -- Conduit execs a standalone connector subprocess with a clean environment (no inherited PATH), so a `pip install`-then-shebang-script connector cannot launch; this command produces a file whose shebang is an absolute interpreter path resolved at build time. Vendoring strategy: copies files from the *current environment's* already-installed distributions (importlib.metadata: file manifests + transitive Requires-Dist, marker-filtered for extras) rather than a fresh pip install -- this SDK isn't published to PyPI yet, so a fresh resolve would fail outright; this also means `build` needs no network access and runs in ~0.2-0.4s. conduit-connector-sdk itself is vendored by copying its actual installed location directly (works for editable dev installs too, where importlib.metadata's file manifest is just a .pth redirect). Not a plain zipapp: grpcio and pydantic's pydantic-core both ship compiled extensions, which zipimport cannot load from inside a zip archive. Found this the hard way -- an earlier version of this command raised BuildError on any compiled extension, which would have made this SDK's own core dependencies (grpcio, pydantic) impossible to vendor at all. Fixed by generating a small, dependency-free bootstrap __main__.py (the only thing zipimport ever runs directly) that extracts the real payload to a per-build cache directory on first run -- the same fundamental approach shiv/pex use -- then executes the connector's real entry point from those extracted, real files. Verified end-to-end: built the example connector, exec'd the artifact directly (not `python <artifact>`), confirmed grpc/pydantic-core's .so files are present as real extracted files and the handshake completes correctly. Bug fix, found via that same end-to-end testing: sending a real SIGTERM to a running serve() process whose connector's teardown() raised (e.g. HTTPPollSource.teardown() accessing self._client before open() ever ran) silently hung for the full watchdog deadline instead of shutting down promptly. Root cause: _sigterm_shutdown()'s coroutine is scheduled via run_coroutine_threadsafe from a signal handler with its Future never awaited/checked, so an exception inside it was silently swallowed and shutdown_requested was never set. Fixed: shutdown_requested.set() now runs unconditionally in a finally block, with a clear stderr diagnostic distinguishing "teardown raised" from "loop genuinely wedged" -- a buggy teardown() no longer blocks the entire SIGTERM-triggered graceful path. Also hardened the example connector's own teardown() to guard against running before open() ever did. Tests: tests/test_build.py builds the real example connector and execs the resulting artifact directly as a subprocess (mirroring exactly how Conduit's dispenser launches a plugin) -- asserts a valid handshake line, an absolute-path shebang, compiled extensions present as real extracted files, cache reuse on a second launch, and (the regression test for the bug above) that a real SIGTERM triggers prompt graceful shutdown rather than hanging until the watchdog fires. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
devarismeroxa
added a commit
that referenced
this pull request
Jul 24, 2026
…, acceptance harness + example (#2) * feat(record,config): OpenCDC record model + pydantic config introspection Lane C of the v0.19 Python connector SDK workstream (docs/design/20260707-python-connector-sdk.md). - conduit/record.py: Record/Change/Operation/Data (bytes | Mapping) per §2.3, plus Metadata well-known-key constants and a representative set of typed accessors (created_at/read_at/collection). - conduit/config.py: BaseConfig (pydantic v2) + Field re-export + to_parameters() introspecting model_fields into config.Parameter/Validation (no codegen, per §2.2). gt=/lt= map exactly; ge=/le= are approximated as exclusive gt/lt (exact for int fields, epsilon-nudged for float, both documented). Literal[...] -> one TYPE_INCLUSION per value. TYPE_DURATION and TYPE_EXCLUSION explicitly raise NotImplementedError (open A-gaps, not silently guessed). - conduit/errors.py: BackoffRetry/BatchWriteError/ConnectorError. BatchWriteError is the B1 data-loss fix (§2.5): construction requires an exhaustive, disjoint success/failures accounting (or a written= prefix); ValueError at construction time if incomplete -- "ack everything not explicitly marked failed" is structurally unrepresentable, not just documented. - pyproject.toml: mypy_path/overrides extended so the generated connector.v2/config.v1/opencdc.v1 stubs (reached via conduit._grpc's sys.path trick, not conduit._grpc.* dotted imports) resolve under mypy; known-first-party isort config so `import conduit._grpc` always sorts before those stub imports (verified: reordering it breaks the sys.path side effect at runtime, not just a style nit). - tests/test_record_codec.py: Hypothesis round-trip tests, including the B3 google.protobuf.Struct int->float precision-loss case pinned exactly (not papered over with `==` laxity). - tests/test_config.py, tests/test_errors.py: mapping-rule and BatchWriteError construction-validation coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD * feat(source,destination,serve): Source/Destination lifecycle over gRPC v2 + shutdown watchdog Lane B of the v0.19 Python connector SDK workstream (docs/design/20260707-python-connector-sdk.md). - conduit/_dispatch.py: dual sync/async method dispatch (inspect.iscoroutinefunction detection, sync overrides run in the default thread-pool executor) shared by Source and Destination, per §2.1. - conduit/_introspect.py: shared generic-parameter recovery (Source[Config]/Destination[Config] -> Config) used to validate Configure's config map and build Specify's parameter map without author boilerplate. - conduit/source.py: Source ABC (abc.ABC, read() abstract) + _SourceServicer adapting it to SourcePluginServicer. Read loop reuses the Go SDK's exact backoff constants (Factor=2, Min=100ms, Max=5s, source.go:270-295, re-verified MUST-FIX 1) as a single serial loop. Invariant 1: ack() is called only from _consume_acks, driven only by Conduit's ack_positions on the Run request stream -- the read loop itself never calls ack(). - conduit/destination.py: Destination ABC (write() abstract) + _DestinationServicer. _write_batch is the B1 enforcement site: full success only after write() returns cleanly; BatchWriteError's already-validated success/failures accounting drives every ack/nack decision; any other exception nacks the entire batch. No code path acks an index absent from the exhaustive success set. - conduit/_grpc/_controller.py: hand-written (not generated -- go-plugin's own internal proto, outside conduit-connector-protocol) GRPCController.Shutdown service, registered via grpc.method_handlers_generic_handler. - conduit/_grpc/adapters.py: Record/Data/Change <-> proto conversion. The B3 google.protobuf.Struct int->float boundary is documented at its one exact call site (_data_to_proto). - conduit/serve.py: serve() entry point -- handshake validation (reusing _handshake.py, Lane A), grpc.aio server bootstrap, health/specifier/ connector servicer registration, and _ShutdownCoordinator: the hung-event-loop watchdog (MUST-FIX 3). SIGTERM is caught with low-level signal.signal (not loop.add_signal_handler, which cannot fire if the loop is wedged) to start an independent threading.Timer that force-exits after a bounded, configurable deadline if graceful shutdown (teardown() + GRPCController.Shutdown) hasn't confirmed completion. - tests/test_source.py, tests/test_destination.py: ack-ordering and B1 partial-batch-nack coverage (test_destination_partial_write_nacks_all's three cases: incomplete accounting, well-formed written= prefix, non-BatchWriteError exception). - tests/test_serve.py: the deterministic shutdown test (MUST-FIX 2) -- a real grpc.aio server, a real gRPC client calling /plugin.GRPCController/Shutdown, asserting the RPC succeeds and teardown() ran exactly once beforehand via a spy, not a timing race -- plus the hung-loop watchdog tests (MUST-FIX 3), including one that genuinely wedges a real event loop in a background thread and confirms the watchdog still fires within its documented deadline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD * feat(testing): acceptance-test harness + worked http-poll-source example Lane D of the v0.19 Python connector SDK workstream (docs/design/20260707-python-connector-sdk.md). - conduit/testing/acceptance.py: AcceptanceTestDriver Protocol, ConfigurableAcceptanceTestDriver convenience wrapper, and AcceptanceTestSuite -- the versioned (CONTRACT_VERSION = "2026-07.v1") acceptance suite an author subclasses in their own pytest module. Covers every category from the design doc §3: specifier existence/validity, config validation (success + required-param-missing), resume-at-position (snapshot and CDC-equivalent), read/write round trip, read timeout behavior, and partial-batch write correctness (paralleling test_destination_partial_write_nacks_all as an SDK-level guarantee). Exercises connectors in-process via the same servicer adapters serve.py uses -- no real gRPC socket, no real Conduit binary (that's compat-nightly.yml/Conduit-repo scope). - conduit/testing/fixtures.py: golden OpenCDC record-shape factories (snapshot/create/update/delete_record, with_collection). - examples/http-poll-source/main.py: the design doc §2.7 worked example, made fully runnable (httpx-based HTTP polling source, BackoffRetry on empty responses, position-based resume). - examples/http-poll-source/pyproject.toml: standalone packaging, mirroring what `conduit connector new --lang python` will scaffold (Phase 3). - tests/test_acceptance_harness.py: the suite run against a synthetic in-memory driver. - tests/test_example_http_poll_source.py: the suite run against the real, unmodified example file, in-process against a real local HTTP server (stdlib http.server, no httpx mocking) -- this test passes, it is not skipped or stubbed. - README/CHANGELOG updates reflecting Lanes B/C/D landing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD * fix(ci): pin types-protobuf dev dep + reformat design doc's embedded code CI (uv sync --all-extras, ruff/mypy jobs) caught two gaps not visible in my local dev environment, whose packages had drifted from what a fresh `pip install -e '.[dev]'`/`uv sync` resolves: - mypy: `types-protobuf` was installed manually in my local venv while developing but never added to pyproject.toml's dev extras, so a fresh environment (CI, or any contributor running `uv sync`) hit "Library stubs not installed for google.protobuf" in serve.py/_grpc/_controller.py. Added `types-protobuf>=6.30,<7` to `[project.optional-dependencies].dev`. - ruff format: ruff 0.16.0 (resolved by CI's `ruff>=0.14,<1` constraint; 0.15.x, what I had locally, treats Markdown formatting as preview-only and skips it by default) now formats Markdown-embedded Python code fences by default. This caught 3 pre-existing, purely cosmetic whitespace inconsistencies in docs/design/20260707-python-connector-sdk.md's embedded code examples (double-space-before-comment, missing blank lines) -- not present in any file this PR otherwise touches, not a content change, verified via `git diff` to be whitespace-only. Verified by simulating CI exactly: a fresh venv, `pip install -e '.[dev]'` (uv unavailable in this sandbox), then ruff format --check / ruff check / mypy / pytest -- all green, matching CI's actual dependency resolution rather than my possibly-stale local venv. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD * feat(config): Go-duration support for timedelta config fields DX audit fix #1. `datetime.timedelta`-typed BaseConfig fields now map to config.Parameter.TYPE_DURATION instead of raising NotImplementedError: - New public conduit.config.format_go_duration()/parse_go_duration(): serialize/parse Go's time.Duration.String()/ParseDuration syntax ("5s", "1h30m", "500ms", "1.5h", "-1.5h", ns/us/µs/ms/s/m/h units), using exact fractions.Fraction arithmetic (no float rounding) down to microsecond resolution -- the finest datetime.timedelta supports. - to_parameters() serializes a timedelta field's default via format_go_duration(); gt=/lt=/ge=/le= constraints on duration fields use the same exact (not epsilon-approximated) ±1-microsecond boundary adjustment already used for int fields, since timedelta is likewise a discrete, integer-resolution type. - BaseConfig gained a model_validator(mode="before") that parses a Go-duration string into a real timedelta before pydantic's own validation runs, so the Configure RPC's map<string,string> config values round-trip correctly. Direct construction with a real timedelta still works unchanged. - TYPE_EXCLUSION remains an open, documented A-gap (untouched by this fix) -- still raises NotImplementedError rather than guessing. Tests: tests/test_go_duration.py (known-Go-output pins + Hypothesis round-trip property over arbitrary microsecond counts + malformed-syntax rejection), tests/test_config.py (to_parameters() mapping + Configure-side string parsing + direct-construction-still-works cases). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD * feat(source,destination): rename lifecycle hooks, clarify ack(), forward validation detail DX audit fixes #2-#4. - Source.ack()'s default was already a genuine no-op (return None, no log line, no raise) -- verified, and its docstring now says explicitly: "you don't need to override ack() unless you're acknowledging against the source system itself (e.g. committing a Kafka offset, deleting a queue message)." Same note added to the example connector's README. - Renamed lifecycle_on_created/lifecycle_on_updated/lifecycle_on_deleted to on_created/on_updated/on_deleted on both Source and Destination -- the lifecycle_ prefix was redundant (these already live on the connector class). Updated the ABCs, the servicer dispatch code, and tests/test_destination.py. Grepped for every lifecycle_on_ reference (including _dispatch.py/_introspect.py, which had none) to confirm no stragglers. - BatchWriteError.partial(batch_size, written=N, cause=exc): the recommended constructor for the common contiguous-prefix partial-batch case. Every failed index is recorded with the real cause exception instead of a generic "not reached" placeholder, so the ack error detail that reaches Conduit reflects what actually went wrong. Reuses the existing exhaustive-accounting constructor path under the hood -- still no code path that computes "ack everything not explicitly failed." - Configure RPC handlers (Source and Destination) now catch pydantic.ValidationError explicitly and abort with INVALID_ARGUMENT plus a per-field detail message (errors.format_validation_error()), instead of relying on grpc.aio's generic "Unexpected <exception class>: ..." UNKNOWN-status wrapping of an uncaught exception -- per CLAUDE.md's "errors are API, actionable" standard. Tests: tests/test_errors.py (BatchWriteError.partial construction + cause-propagation), tests/test_configure_errors.py (real grpc.aio server + real client, asserting INVALID_ARGUMENT status and that the field name/ message actually appear in the gRPC status detail -- not a mock of pydantic or of the transport). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD * feat(cli): conduit-connector-sdk build + fix a real SIGTERM shutdown bug found by it DX audit fix #5 (the #1 lever per the audit). New `conduit-connector-sdk` console script, `build` subcommand (_build.py/_cli.py): packages a connector project into one self-contained, directly-executable artifact. Closes the design doc §1.1.6 packaging gap -- Conduit execs a standalone connector subprocess with a clean environment (no inherited PATH), so a `pip install`-then-shebang-script connector cannot launch; this command produces a file whose shebang is an absolute interpreter path resolved at build time. Vendoring strategy: copies files from the *current environment's* already-installed distributions (importlib.metadata: file manifests + transitive Requires-Dist, marker-filtered for extras) rather than a fresh pip install -- this SDK isn't published to PyPI yet, so a fresh resolve would fail outright; this also means `build` needs no network access and runs in ~0.2-0.4s. conduit-connector-sdk itself is vendored by copying its actual installed location directly (works for editable dev installs too, where importlib.metadata's file manifest is just a .pth redirect). Not a plain zipapp: grpcio and pydantic's pydantic-core both ship compiled extensions, which zipimport cannot load from inside a zip archive. Found this the hard way -- an earlier version of this command raised BuildError on any compiled extension, which would have made this SDK's own core dependencies (grpcio, pydantic) impossible to vendor at all. Fixed by generating a small, dependency-free bootstrap __main__.py (the only thing zipimport ever runs directly) that extracts the real payload to a per-build cache directory on first run -- the same fundamental approach shiv/pex use -- then executes the connector's real entry point from those extracted, real files. Verified end-to-end: built the example connector, exec'd the artifact directly (not `python <artifact>`), confirmed grpc/pydantic-core's .so files are present as real extracted files and the handshake completes correctly. Bug fix, found via that same end-to-end testing: sending a real SIGTERM to a running serve() process whose connector's teardown() raised (e.g. HTTPPollSource.teardown() accessing self._client before open() ever ran) silently hung for the full watchdog deadline instead of shutting down promptly. Root cause: _sigterm_shutdown()'s coroutine is scheduled via run_coroutine_threadsafe from a signal handler with its Future never awaited/checked, so an exception inside it was silently swallowed and shutdown_requested was never set. Fixed: shutdown_requested.set() now runs unconditionally in a finally block, with a clear stderr diagnostic distinguishing "teardown raised" from "loop genuinely wedged" -- a buggy teardown() no longer blocks the entire SIGTERM-triggered graceful path. Also hardened the example connector's own teardown() to guard against running before open() ever did. Tests: tests/test_build.py builds the real example connector and execs the resulting artifact directly as a subprocess (mirroring exactly how Conduit's dispenser launches a plugin) -- asserts a valid handshake line, an absolute-path shebang, compiled extensions present as real extracted files, cache reuse on a second launch, and (the regression test for the bug above) that a real SIGTERM triggers prompt graceful shutdown rather than hanging until the watchdog fires. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD * fix(tests): make test_build.py's exec-based tests Windows-aware CI's windows-latest jobs failed test_build.py: Windows has neither POSIX executable bits (os.chmod's execute flags are a no-op there for arbitrary extensions) nor shebang-based direct execution (CreateProcess dispatches by file extension, not by parsing a leading `#!` line -- confirmed via the actual CI failure: `OSError: [WinError 193] %1 is not a valid Win32 application` when invoking the bare .pyz path). This is exactly the "Windows subprocess launch specifics ... untested until the CI matrix actually runs it" risk the design doc already flagged as open, now surfaced for real. - The executable-bit assertion is skipped on Windows (nothing meaningful to assert there for a .pyz). - Every subprocess invocation goes through a new `_exec_argv()` helper: the bare artifact path on POSIX (the actual "no `python` prefix" claim this test suite is about), `[sys.executable, artifact]` on Windows, documented as a real, known platform difference rather than silently worked around. - `test_sigterm_triggers_prompt_graceful_shutdown` is skipped on Windows outright: `Popen.send_signal(SIGTERM)` maps to an unconditional `TerminateProcess()` there, not something `conduit.serve`'s SIGTERM handler ever observes, so the test would not exercise the graceful path it exists to pin. Windows-native graceful shutdown is out of scope for this fix (a real, separate feature involving different Windows IPC/ signal-equivalent mechanisms). - The compiled-extension-module check now also looks for `*.pyd` (Windows' extension suffix), not just `*.so`/`*.dylib`. Verified: all 8 tests in tests/test_build.py still pass locally (macOS/POSIX); the Windows-specific branches were validated against the exact CI failure output, not guessed at blind. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD * fix(serve): drain in-flight read/write before SIGTERM-triggered teardown Tier-1 review gap (invariant 7): serve.py's _sigterm_shutdown ran teardown() immediately on SIGTERM with no regard for an actively streaming Run() call, letting teardown() (e.g. closing a DB pool) race a live Source.read()/Destination.write(). Not a data-loss bug (a raced write nacks the whole batch and Conduit redelivers), but a graceful- shutdown gap on a Tier-1 SDK where graceful shutdown is the contract. - _SourceServicer.drain()/_DestinationServicer.drain() factor out the same stop-then-wait ordering Stop() already performs for the deterministic path (Conduit Stop RPC -> Run ends -> Teardown RPC), reusable from serve.py's SIGTERM handler. Destination's Run() gained the same _stop_event/_stopped_event/finally shape Source already had, so drain() waits for the whole Run() generator to finish (including its already-computed ack response), not just for write() to return. - _sigterm_shutdown awaits drain() before teardown(), bounded by the existing hung-loop watchdog deadline (unchanged) so a stuck connector still force-exits on schedule. - Fixed the design doc overclaim (Risks & open questions §3): it asserted the ordinary SIGTERM-mid-write case was "covered by the ordinary SIGTERM-mid-write test," but the only existing SIGTERM test fires before Open(). Added the actual TestSigtermDrainsInFlightOperation::test_sigterm_mid_read_drains_before_teardown and ::test_sigterm_mid_write_drains_before_teardown integration tests (real grpc.aio server + client, deterministic via direct _on_sigterm invocation) and pointed the doc at them. Gates: ruff format --check, ruff check, mypy --strict (18 source files), full pytest (165 passed). Generated _grpc/ dirs unchanged. Tier 1 (data path adjacent: SDK shutdown contract) -- does not merge without DeVaris sign-off. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Initial scaffold of the Python connector SDK — v0.19 anchor build, the strategic #2 SDK (Go → Python → Rust → TS).
Scope (week-1 foundation only)
Repo skeleton + Lane A. The full Source/Destination lifecycle, config/schema, acceptance-test harness,
--lang pythonscaffolding wiring, and the example connector land in follow-up PRs ahead of the week-3 Tier-1 sign-off.What's here
SourcePlugin/DestinationPlugin/SpecifierPlugin+opencdc/configtypes), generated viabufagainst BSR, v1 excluded; the go-plugin handshake (magic-cookie check, v2-only version negotiation, exact pipe-delimited stdout line) — 18 unit tests, green locally.pyproject.toml(uv/hatchling, ruff + mypy strict), CI workflows (lint/test/compat-nightly/release), Apache-2.0, design doc atdocs/design/.Review notes (folded from the plan review)
conduit-connector-sdkGo citations (destination.go:345-350,source.go:270-295) re-verified as-read before the invariant-1/4 parity argument was relied on.Shutdown-RPC-invocation assertion, not a timing/log heuristic.signal-installed watchdog with a bounded force-kill deadline independent of the loop.Open (non-blocking)
sys.pathmodule-name collision in generated stubs (flagged in-file).conduit-connector-sdk— squat-check before first publish.Risk tier
Tier 1 (connector SDK — ack/position semantics cross the gRPC boundary). This scaffold establishes the baseline; the Tier-1 human sign-off gate is the completed SDK core (week 3), not this seed.
🤖 Generated with Claude Code
https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD