weida

GitHub crates.io

weida for Python (the Rust API of `crates/weida`) — API parity

Rendered from docs/libraries/weida-py.md at 5a20f15

Contents
  1. 1. What a row means, and what the other column is
  2. 2. How it is built, tested and packaged
  3. 3. The runtime, the binding and the reactor
  4. 4. The asyncio surface, class by class
    1. 4.1 Req/Rep
    2. 4.2 Push/Pull
    3. 4.3 Pub/Sub
    4. 4.4 PAIR, SURVEY and BUS
    5. 4.5 The streamed transfer classes
    6. 4.6 The cursor surface
  5. 5. The value classes
  6. 6. The sync surface, and exactly how it differs
  7. 7. The exception family
  8. 8. The byte boundary and the explicit receive ceiling
  9. 9. Deliberate deviations from the Rust API
    1. 9.1 No per-call timeouts, anywhere
    2. 9.2 No raw L0 surface: Peer and Acceptor are not bound
    3. 9.3 No streamed form in weida.sync
    4. 9.4 drain takes seconds as a float, and the deadline is mandatory
    5. 9.5 One reply, one finish, and a spent handle says so
    6. 9.6 Each object holds its runtime alive
    7. 9.7 Two text claims this document found, and closed
  10. 10. What is absent, with a reason and where a follow-up adds it
  11. 11. Sources

The parity document of weida-py, the Python binding of weida itself, built on weida-py-core (B-200, B-204, B-205). It answers one question per row: what does a Python caller get of the API this binding binds, and what does that caller not get.

1. What a row means, and what the other column is

Three verdicts and no others:

  • present — implemented, with the module or test that proves it;
  • refused — the call exists and fails with a reason the code gives;
  • absent — not implemented, with what is missing named.

"Partial" is not a verdict (0013 §4.7 clause 6), and neither is "mostly", "planned" or "good coverage" (README.md rule 2).

The one way this document differs from the other five. zmq-py.md, nng-py.md, mqtt-py.md, amqp-py.md and nats-py.md are parity documents against a foreign reference implementation — pyzmq, pynng, paho-mqtt, python-qpid-proton, nats-py — because a Python program that speaks those protocols already has one to compare against. This binding has no foreign reference: weida has no other Python client, and no second implementation of the protocol exists in any language. The reference here is therefore the Rust API this module binds: every public surface of crates/weida is present in Python, refused with a reason, or absent with a reason, and the left-hand column of every table below is a Rust item read out of crates/weida/src/. That makes this the one document in this directory whose two columns are the same protocol at two layers rather than two implementations of one protocol; nothing here is an interop claim about a foreign peer, because there is no foreign peer to make one about (§2, last paragraph).

What each side was read from. The Python column is crates/py/weida-py/src/ — lib.rs, runtime.rs, endpoints.rs, pubsub.rs, patterns.rs, streams.rs, sync.rs, values.rs, errors.rs — and the tests under crates/py/weida-py/tests/. The Rust column is crates/weida/src/: lib.rs's re-exports (lib.rs:67-99), config.rs, runtime.rs, listener.rs, endpoint.rs, transfer.rs, stream.rs and blocking.rs. CPython 3.9 and up through PyO3 0.29 with abi3-py39 (Cargo.toml:110-114 at the workspace root, crates/py/weida-py/pyproject.toml:8), the library entered with the generate and blocking features (crates/py/weida-py/Cargo.toml:34), Linux x86-64.

How the numbers were counted. Every count in this document was counted out of the tree, not estimated:

NumberValueCounted from
#[pyclass]es3823 add_class calls in lib.rs:117-141 plus 15 in sync.rs:919-936
weida.__all__56 names30 literals in lib.rs:150-184 (which include sync, VERSION, ALPN, WeidaError and the five cursor names), the five named levels appended at lib.rs:185, plus the 21 of errors::NAMES
weida.sync.__all__15 namesthe list at sync.rs:937-957, one per class registered above it
Exception classes22the 21 entries of the failures! invocation at errors.rs:72-94, plus the base WeidaError (errors.rs:33), which is not in that list
Test functions328 in tests/test_asyncio.py, 9 in tests/test_patterns.py, 8 in tests/test_sync.py, 7 in tests/test_cursors.py
IncomingMeta attributes11 of the Rust struct's 12 fieldsvalues.rs:147-183 against crates/weida/src/transfer.rs; tracestate is the only one absent (§5)

2. How it is built, tested and packaged

Rust-side factThis bindingVerdict
the crate is weida_py, the module is weidaone #[pymodule] fn weida (lib.rs:101-130); the crate cannot share the name because rustdoc writes one directory per lib namepresent, and the reason is in Cargo.toml:9-15
cargo test links libpython, a wheel must notextension-module is off by default in the crate and turned on by maturin (Cargo.toml:41-46, pyproject.toml:27-32)present
—develop.shpresent: a uv virtualenv at the repository root, maturin and pytest into that virtualenv and nothing into a system interpreter, maturin develop, then pytest with the caller's arguments (develop.sh:23-37)
—package.shpresent: maturin build --release, a wheel whose name is checked to contain abi3 or the script exits 1 (package.sh:40-43), a fresh temporary virtualenv holding only that wheel, and smoke.py run with PATH=/usr/bin:/bin (package.sh:45-52) — so "the wheel needs no Rust toolchain" is established rather than expected
abi3-py39, one wheel for CPython 3.9 and uprequires-python = ">=3.9" (pyproject.toml:8), one abi3 wheelpresent, and the two agree — for GIL-enabled CPython, which is the exception this row used to omit. Read out of PyO3 0.29.2's own selector (pyo3-build-config-0.29.2/src/impl_.rs:96-132): a free-threaded interpreter below 3.15 gets None from applicable_stable_abi and therefore a version-specific build, because the free-threaded stable ABI (abi3t) starts at 3.15 — so cp39-abi3 does not serve 3.13t or 3.14t and a second, version-specific wheel would be needed for them. PyPy and GraalPy take the same branch from the other side: a stable ABI is selected but pinned to the interpreter's own version, so their artifact is their own too (§10)
—what smoke.py claimspresent, and it is four claims: the installed distribution's name and version (smoke.py:20-25), every name of weida.__all__ resolving plus RuntimeFailure under WeidaError and weida.Runtime not a BaseException (smoke.py:28-38), a Req/Rep round trip, a Push/Pull transfer and a Pub/Sub message over real QUIC in one process (smoke.py:43-81), a 10 MiB streamed fan-out against an 8 MiB subscriber budget (smoke.py:83-92), and a wrong fingerprint refused with the key that answered in the message (smoke.py:94-103)
the whole thing under one wall-clock boundasyncio.run(asyncio.wait_for(round_trip(), 30.0)) (smoke.py:109), and each test file's run() helper wraps its coroutine in asyncio.wait_for(..., 15.0) (test_asyncio.py:25-31, test_patterns.py:19-23)present: a binding that deadlocks fails the suite instead of hanging it
a broker, a server or a C library to test againstnone neededpresent: both halves of every test are this library (develop.sh:16-18, test_asyncio.py:3-5)

There is no interop section in this document, and that is not an omission. The other five Python parity documents have one because a foreign implementation of their protocol exists to run against. weida's protocol has exactly one implementation, crates/weida, and this binding is a thin wrapper over it, so a "pairing" here would be this library against itself — which is what every test in crates/py/weida-py/tests/ and smoke.py already is. The claim a real interop section would carry is therefore unavailable, and no number in this document is an interop number. The counts above were counted out of the tree with the sources named in §1 and re-measured by a run when B-244 and B-243 completed the surface: 32 tests passed and smoke.py resolved all 56 names through a freshly built wheel.

3. The runtime, the binding and the reactor

crates/weidaThis bindingVerdict
Runtime::owned(config) (runtime.rs:220)weida.Runtime(worker_threads=1)present — runtime.rs:53-66, used by every test
Runtime::new(config) (needs an ambient Tokio reactor), Runtime::with_handle(handle, config)—absent, and the reason is that they mean nothing here: a plain Python process has no ambient Tokio runtime and no Python caller can hold a tokio::runtime::Handle (runtime.rs:3-11). owned is the only constructor that can serve this surface, so it is the only one offered
RuntimeConfig's thirteen fields (config.rs)worker_threads, and nothing elsepresent for one field — absent for limits, max_connections, max_connections_per_peer, endpoint_queue, max_resolved_addresses, shutdown_timeout, guarantees, connect_attempt_timeout and, since 0031, reconnect, send_timeout, outbox_messages, outbox_bytes and outbox_full: a Python caller gets RuntimeConfig::default() for all of them (runtime.rs:56-59) and cannot change them. The redial therefore runs with its defaults from Python, and the PeerEvent stream of 0031 §4.8 has no Python surface yet. §10
Runtime::exec() (runtime.rs:431)not a method; the bridge is built on itpresent as the reason that accessor exists: Bridge::new(runtime.exec().clone(), ...) at runtime.rs:61 puts this crate's futures on the runtime's own executor, so one process holds one Tokio runtime rather than two (runtime.rs:13-18)
Runtime::listener() then Listener::bind_quic(addr, identity)await runtime.bind("host:port", identity) → Bindingpresent as one call — runtime.rs:78-109. bind is a coroutine because it creates a socket and a QUIC endpoint, and a __new__ that did that would block the event loop (runtime.rs:20-25)
Listener::bind_inproc, bind_unix, bind_pipe (listener.rs:197, 232, 263)—absent: only QUIC can be bound from Python. The dialling side's address grammar does accept weida+unix://, weida+pipe:// and weida+inproc:// (endpoints.rs:59-62), so a Python client can reach a local binding a Rust process made, and a Python server on a local transport is what is missing. §10
Listener::replier/puller/publisher(path)binding.replier(path), .puller(path), .publisher(path)present, synchronous, raising weida.AlreadyRegistered or weida.InvalidEndpointPath — runtime.rs:225-261. pair, respondent and bus join them at runtime.rs:263-311 (§4.4)
Listener::acceptor(path) (listener.rs:350)—absent: the raw L0 surface. §9.2
Binding::local_addr()binding.local_addr() → strpresent — runtime.rs:205-207, and port 0 resolved is what every test dials
Binding::close() (listener.rs:378)—absent: dropping the Python Binding unbinds, because it holds the Arc<weida::Binding> (runtime.rs:193-195). What is missing is an awaited close that waits for the endpoint to go idle
Identity::fingerprint() on the serving sidebinding.fingerprint(), binding.url(path)present, and both are Python-only conveniences with no single Rust counterpart: url formats weida://{fingerprint}@{local}{path}, which is the whole of a client's configuration — runtime.rs:209-218, asserted at test_sync.py:136
Runtime::drain(deadline: Duration) → Drainedawait runtime.drain(seconds: float) → (delivered, outstanding)present — runtime.rs:167-182, test_asyncio.py::test_a_drain_reports_what_it_achieved. Seconds as a float, mandatory and finite. §9.4
Runtime::drain consumes selfthe Python Runtime stays usablepresent, deliberately: weida::Runtime is Clone and a clone shares the pool, the bindings and the drain state, so the binding drains a clone (runtime.rs:174-177)
Runtime::shutdown() (runtime.rs:323)—absent from the asyncio surface: drain is the only end there, and the reactor otherwise dies with the last handle that can cause work on it (endpoints.rs:10-12). weida.sync.Runtime.shutdown() exists (sync.rs:203-209), so the gap is the asyncio surface's alone. §10
Runtime::suppressed_duplicates() (runtime.rs:251), Runtime::config() (runtime.rs:416)—absent: two read-only accessors with no Python spelling, and runtime.rs:47-187 registers neither
Runtime::peer(tls) → Peer (runtime.rs:272)—absent: the raw dialling core. §9.2

4. The asyncio surface, class by class

Twenty-three classes in weida (§1). Every call that waits is a coroutine and the waiting happens on the reactor with the GIL released; the Rust future starts at the first await, not at the call, so a coroutine that is never awaited does nothing at all (crates/py/weida-py-core/src/bridge.rs:131-153).

4.1 Req/Rep

crates/weidaThis bindingVerdict
Runtime::requester(tls) (runtime.rs:282)runtime.requester(trust) → Requesterpresent — runtime.rs:112-118
Requester::connect(url)await requester.connect(url)present — endpoints.rs:79-85; the address forms and the failures are written once at endpoints.rs:53-68
Requester::request(body) / request_with(meta, body)await requester.request(payload, max_reply_bytes) → bytespresent — endpoints.rs:101-119. Two differences: the ceiling is mandatory (§8) and the binding sets content_len for the caller (endpoints.rs:112, asserted as meta.content_len == 4 at test_asyncio.py:48). Proved end to end by test_asyncio.py::test_a_request_is_answered_over_quic
Requester::open(meta) → (OutgoingTransfer, ReplyStream)await requester.open() → (OutgoingStream, Reply)present — endpoints.rs:127-141, test_patterns.py::test_a_streamed_exchange_reads_the_reply_while_writing, which reads the reply's meta.endpoint is None and collects b"streamed request"
Requester::peer_count()—absent on Requester and Pusher; Subscriber.peer_count() has it (pubsub.rs:336-338). The Rust method exists on all three (endpoint.rs:131, 225, 490)
Listener::replier(path), Replier::path()Replier.path()present — endpoints.rs:232-235
Replier::accept() → IncomingRequest (payload still a stream)await replier.accept(max_bytes) → Request (payload already bytes)present, and the shape differs on purpose: the accept and the take_body().collect(max_bytes) are one call (endpoints.rs:247-267), because a Python object is materialized anyway
IncomingRequest::meta(), its bodyRequest.payload → bytes, Request.meta → IncomingMetapresent as getters — endpoints.rs:295-304
IncomingRequest::reply(meta) → OutgoingTransfer (streamed)await request.reply(payload) — whole payload, finish includedpresent — endpoints.rs:314-330
IncomingRequest::refuse(code) for any ErrorCodeawait request.refuse() — always ErrorCode::Rejectedpresent as one refusal, absent as a choice: there is no weida.ErrorCode, so the code is hard-wired (endpoints.rs:341, sync.rs:412) where crates/weida/src/transfer.rs:626 takes one. §10
an exchange carries one replythe second reply raises weida.NoReplypresent, and enforced rather than documented: the request is a Mutex<Option<...>> taken once (endpoints.rs:281-290, 351-369), proved by test_asyncio.py::test_a_request_carries_one_reply
dropping an unanswered request causes ERROR {NO_REPLY}same, inheritedpresent — endpoints.rs:274-279, the rule PROTOCOL.md §9.4 makes mandatory
IncomingRequest::canceled() (transfer.rs:563)—absent: a handler cannot see the requester walk away before it answers. It learns at reply, as weida.Canceled (endpoints.rs:310-311). §10

4.2 Push/Pull

crates/weidaThis bindingVerdict
Runtime::pusher(tls), Pusher::connect(url)runtime.pusher(trust), await pusher.connect(url)present — runtime.rs:121-127, endpoints.rs:157-163
Pusher::send(body) / send_with(meta, body), the receipt through Delivery::delivered()await pusher.send(payload)present as one call — endpoints.rs:179-199: open, write_all, finish, delivered. The receipt is QUIC's fin-acknowledgement and not an application acknowledgement, and the docstring says so (GUARANTEES.md §1)
Pusher::open(meta) → OutgoingTransferawait pusher.open() → OutgoingStreampresent — endpoints.rs:205-216
Listener::puller, Puller::path(), Puller::recv()Puller.path(), await puller.recv(max_bytes) → (bytes, IncomingMeta)present — endpoints.rs:381-399, test_asyncio.py::test_a_push_reaches_a_puller_with_its_metadata, which asserts meta.endpoint == "/ingest" and meta.content_len == 8
—await puller.recv_stream() → (IncomingStream, IncomingMeta)present, and it has no single Rust counterpart because Puller::recv already hands back a stream: this is the call that does not collect (endpoints.rs:403-412)

4.3 Pub/Sub

crates/weidaThis bindingVerdict
Publisher::publish(topic, payload) → usizepublisher.publish(topic, payload) → int, not a coroutinepresent — pubsub.rs:78-81. A publish never waits for a subscriber, so making it awaitable would promise a wait that does not happen (pubsub.rs:68-70); test_patterns.py::test_a_published_message_reaches_the_matching_filter_only asserts 1 for a match and 0 for a non-match
a payload above Limits::subscriber_buffer_bytesweida.LimitExceededrefused at the call — pubsub.rs:74-77, asserted against the 8 MiB budget at test_patterns.py:115-119
Publisher::open(topic) → FanOutpublisher.open(topic) → FanOut, not a coroutinepresent — pubsub.rs:89-96
FanOut::write_within(chunk, limit)await fan.write_within(chunk, seconds) → subscribers leftpresent — pubsub.rs:189-208; the budget bounds a chunk, so the payload has no ceiling (pubsub.rs:186-188)
FanOut::write_now(chunk)await fan.write_now(chunk)present, and a coroutine although it never waits on a subscriber: two coroutines may hold the same FanOut and the lock is the one thing it can wait for (pubsub.rs:24-28, 212-224)
both write forms, neither a defaultboth, and the reason is in the module's own tablepresent — pubsub.rs:4-22: only the first would make a video publisher wait on its slowest viewer, only the second would make a payload larger than the budget undeliverable to everybody
FanOut::subscribers(), FanOut::finish()await fan.subscribers(), await fan.finish()present — pubsub.rs:173-179, 232-238. finish returns how many received all of it "as far as this side can tell", because a fan-out copy carries no receipt
a transfer ends oncea second finish or a later write_now raises weida.NoReplypresent — pubsub.rs:154-160, proved by test_patterns.py::test_a_finished_transfer_is_finished
Publisher::subscriber_count(), filter_count(), dropped(), dropped_on(topic)all fourpresent — pubsub.rs:99-135. dropped_on returns (total, subscriber_budget, subscriber_queue, no_parked_connection): three causes rather than one number, and test_patterns.py::test_a_slow_subscriber_loses_copies_and_the_publisher_says_which_topic asserts they sum to the total and that an untouched topic is None
Publisher::drops() → Vec<TopicDrops> (endpoint.rs:416)—absent: there is no call that enumerates every topic that lost a copy, only dropped_on per topic
Publisher::publish_with_trace, open_with_trace (endpoint.rs:333, 384)—absent: a Python caller cannot propagate an inbound trace into an outbound publish. It can read one, as IncomingMeta.traceparent (§5)
Runtime::subscriber(tls), Subscriber::connect/subscribe/unsubscribe/recvruntime.subscriber(trust), then all fourpresent — runtime.rs:130-136, pubsub.rs:270-317; the segmented filter grammar of PROTOCOL.md §6.4 is the library's, and a filter it rejects is weida.Protocol
—await subscriber.recv_stream()present — pubsub.rs:321-333
Subscriber::filter_count() (endpoint.rs:542)—absent on the Python Subscriber; the publisher side's filter_count is what the tests wait on (test_patterns.py:42-53)

4.4 PAIR, SURVEY and BUS

The three patterns the library gained after this binding was written, added on both surfaces by B-244. crates/py/weida-py/src/patterns.rs, and a weida.Survey in values.rs.

crates/weidaThis bindingVerdict
Runtime::pair(tls), Listener::pair(path) — one type, two rolesruntime.pair(trust) and binding.pair(path), both → Pairedpresent — runtime.rs:139-149, runtime.rs:263-278; what distinguishes the roles is which of them calls connect, as in Rust
Paired::connect(url) refusing a second call with LimitExceededsame, and the class is the library'spresent — patterns.rs:67-73, asserted by test_patterns.py::test_a_pair_carries_both_directions_and_keeps_its_first_peer, which dials twice on one endpoint
a second peer is refused per transfer, and the first keeps workingsame, inheritedpresent, and it is the rule the test spends its bytes on: the newcomer's connect succeeds, its send raises weida.LimitExceeded, and the first peer's next send is still received — written past the 1 MiB stream window so the refusal cannot lose the race to the receipt (0005)
Paired::send/send_with, recv(), path(), peer_count()await paired.send(payload), await paired.recv(max_bytes) → (bytes, IncomingMeta), paired.path(), paired.peer_count()present — patterns.rs:56-133. send sets content_len for the caller, as Req/Rep does
Paired::open(meta) → OutgoingTransfer—absent: a pair has no streamed form here, for the reason §9.3 gives for sync — the whole-payload call is the one a Python caller reaches for, and the Rust API is the streamed one
Runtime::surveyor(tls), Surveyor::connect/peer_countruntime.surveyor(trust), await surveyor.connect(url), surveyor.peer_count()present — runtime.rs:151-158, patterns.rs:142-153; a surveyor accumulates peers on purpose, and the test connects two respondents on one binding
Surveyor::survey(body, deadline) → SurveyRun, then next(max_bytes) per answerawait surveyor.survey(payload, deadline_seconds, max_reply_bytes) → weida.Survey, a valuepresent as a collected value, and this is the one shape that is a decision rather than a translation: the only channel out of a bridged future here is the errno family (crates/py/weida-py-core/src/bridge.rs:154-158), so an async for over answers would need a second error channel invented for StopAsyncIteration alone. The Rust SurveyRun stays the place answers arrive one at a time (values.rs:206-216, patterns.rs:169-206)
SurveyRun::respondents(), late(), and a failed answer among the good onesSurvey.asked, Survey.late, Survey.failed, Survey.replies, Survey.silent()present — values.rs:217-250. silent() is asked minus what answered either way, so nobody answering is a number and never an exception; test_patterns.py::test_a_survey_collects_what_answers_and_counts_the_silence asserts asked == 2, one reply, failed == 0 and silent() == 1 against a respondent that accepts the question and never answers
the deadline is the caller's and nothing on the wire carries itsame, and mandatory: a non-finite or negative float is weida.RuntimeFailurepresent — patterns.rs:35-39. The 0.5 s deadline is load-bearing in the test: with it ignored, the case fails on its own 15 s bound rather than passing
Listener::respondent(path), Respondent::accept()binding.respondent(path), await respondent.accept(max_bytes) → weida.Requestpresent, and the same class a replier hands out — runtime.rs:280-292, patterns.rs:238-246: a survey question is an exchange, byte for byte, so reply, refuse and the answered-once rule are the ones from §4.1 (endpoints.rs:341-366 builds the object for both)
Listener::bus(path, tls) — bound and dialling at oncebinding.bus(path, trust) → BusMember, the one registration that takes a trustpresent — runtime.rs:294-311, and the reason is in the docstring: a member accepts here and joins others with connect
BusMember::send(body) → how many it reached, never the senderawait member.send(payload) → intpresent — patterns.rs:298-315, asserted by test_patterns.py::test_a_bus_message_reaches_every_other_member_and_never_the_sender: send returns 1 for one other member, and a receive on the sender after everything is delivered times out rather than handing back its own message
BusMember::recv(), peer_count(), dropped(), path()all fourpresent — patterns.rs:263-331; dropped is the fan-out's counted loss, and the test asserts it is 0 for a bus both members read
BusMember::send_with(meta, body) as a separate call—absent as a name: the binding sets content_len and there is no weida.TransferMeta to pass, exactly as for Pusher.send

4.5 The streamed transfer classes

crates/weidaThis bindingVerdict
OutgoingTransfer::write_all(buf)await stream.write(chunk)present — streams.rs:84-96
OutgoingTransfer::finish() → Delivery, then Delivery::delivered()await stream.finish() — both stepspresent — streams.rs:105-120; the receipt is the transport's, not the application's
OutgoingTransfer::cancel()await stream.cancel()present — streams.rs:124-135
dropping an unfinished transfer resets itsame, inheritedpresent — streams.rs:26-28
IncomingTransfer as an AsyncReadawait stream.read(max_bytes) → bytes, b"" at the endpresent — streams.rs:175-201, over tokio::io::AsyncReadExt::read precisely so a streaming caller does not go through read_capped; a loop over it is test_patterns.py::test_a_streamed_transfer_is_written_and_read_in_pieces, which asserts more than one piece
IncomingTransfer::collect(max_bytes)await stream.collect(max_bytes)present — streams.rs:205-215
IncomingTransfer::read_capped(max_bytes) (crates/weida/src/transfer.rs:390), non-consuming beside the consuming collect (transfer.rs:417)—absent as a separate name: on a Python IncomingStream, read is the non-consuming call (streams.rs:175-201) and collect the consuming one (streams.rs:205-215), so read_capped's "whole payload, handle still usable" shape has no third spelling
ReplyStream::recv()await reply.recv() → (IncomingStream, IncomingMeta)present — streams.rs:256-266. A lost connection on this half is weida.Indeterminate and never ConnectionLost, because after the request's FIN the outcome is genuinely unknown (FAILURE_MODEL.md)
the request and reply halves are independent streamstwo objects, OutgoingStream and Replypresent, and deliberately two: a caller may read the reply while still writing the request, which is what keeps flow control live in both directions (streams.rs:11-17)

4.6 The cursor surface

How a one-way transfer gets a verdict: a Push has no reply to carry one, so Accepted, Stored and Processed arrive on a stream of their own, after the payload's FIN (0023). Added on both surfaces by B-243; crates/py/weida-py/src/cursors.rs is the asyncio half.

crates/weidaThis bindingVerdict
TransferMeta::with_report(levels), with_report_mode(mode)report= and mode= on pusher.send, pusher.open and paired.sendpresent as keyword arguments — cursors.rs:107-131, endpoints.rs:185-233, patterns.rs:96-116. There is no weida.TransferMeta, so the order rides the call that makes the transfer; the levels reach the DATA header, which is what makes a report the sender's request rather than a convention
CursorLevel::Known(Acknowledgement) and CursorLevel::Application(u64)an integer: weida.TRANSPORT_RECEIPT, ACCEPTED, STORED, REPLICATED, PROCESSED, or any value at or above weida.APPLICATION_FLOORpresent as module constants rather than a class — lib.rs:128-141, cursors.rs:47-58. The level space is open (0023 §4.4), and a class would close what the protocol leaves open. test_cursors.py::test_an_application_level_is_carried_and_never_interpreted orders APPLICATION_FLOOR + 3 and reads back an offset the library cannot check against the payload
an undefined value below the floor is a protocol violationweida.Protocol, naming the floorrefused at the boundary — cursors.rs:60-76, asserted by test_cursors.py::test_a_value_the_protocol_reserves_is_refused for the level 7 and for the mode 9, with APPLICATION_FLOOR itself accepted in the same case so the line is a line
ReportMode::Progress, FinalOnlyweida.PROGRESS, weida.FINAL_ONLYpresent as the same two integers on both surfaces (cursors.rs:60, sync.rs), and Progress is the default because it is the library's
OutgoingTransfer::cursors() → Option<Cursors>, onceawait pusher.send(...) → weida.Cursors or None; await stream.cursors() on the streamed formpresent — endpoints.rs:198-211, streams.rs:122-140. None means nothing was ordered, which is every ordinary send; the second cursors() on one stream is None, asserted by test_cursors.py::test_a_streamed_transfer_orders_a_report_too
Cursors::snapshot(), offset(level), changed()cursors.snapshot() → {level: offset}, cursors.offset(level), await cursors.changed()present — cursors.rs:141-205. A set is a dict rather than a class: a CursorSet is exactly the latest absolute offset per level, and a class would add a vocabulary without adding a fact
Cursors::changed() → None at the endthe same None, and no async forpresent, with the reason weida.Survey gives: the only channel out of a bridged future is the exception family, so StopAsyncIteration would need a second one. The loop is while (set := await cursors.changed()) is not None:
Cursors::changed_within(deadline) → Reportedsync.Cursors.changed(seconds) → dict, None, or TimeoutErrorpresent on the synchronous surface only, and that asymmetry is the point (sync.rs:829-860): asyncio.wait_for bounds the asyncio call and composes, while a parked thread is interrupted by nothing — a peer that never reports opens no stream and a connection both sides keep alive never closes. The deadline is mandatory there for the reason 0009 §4.4 gives for drain, and a deadline that passes raises rather than returning None, because it is not the end of the report
IncomingTransfer::reporter() → Option<Reporter>await puller.recv_reporting(max_bytes) → (payload, meta, reporter), await paired.recv_reporting(...), await stream.reporter()present — endpoints.rs:433-464, patterns.rs:135-157, streams.rs:237-254. recv stays a two-tuple and forgets the transfer: a receiver that reports is a receiver with stages, and it says so at the call it makes
Reporter::levels(), mode(), report(level, offset), finish()all four — reporter.levels, reporter.mode, await reporter.report(...), await reporter.finish()present — cursors.rs:207-293. A level the sender did not order is ignored rather than refused, which test_cursors.py asserts by reporting STORED on a report that ordered only ACCEPTED and PROCESSED and then finding it absent from the sender's snapshot
Reporter::with_granularity(bytes, interval)—absent: the granularity is the reporter's own number and never negotiated (0023 §4.5), and this binding takes the library's default. A Python reporter that wants coarser records reports less often
a cursor on a fan-out or an exchange—absent, and not an omission: a published copy is one transfer per subscriber with no single handle to read, and an exchange's reply is its verdict. The senders that can order are the one-way ones — Pusher and Paired — which is what crates/weida itself allows (BusMember::send_with opens one transfer per member and keeps none)

5. The value classes

crates/weidaThis bindingVerdict
Trust::by_address(), Trust::pin(fingerprint), Trust::anchor_file(path)Trust.by_address(), Trust.pin(text), Trust.anchor_file(path)present as three classmethods with no default — values.rs:29-71. pin parses the sha256: + 64 hex text and raises weida.InvalidFingerprint otherwise (values.rs:47-48)
Trust::anchor(pem_bytes), and_pin, and_anchor, and_anchor_file, is_empty—absent: a Trust holds lists in Rust (config.rs:290-295), and Python can build only the single-entry forms. Two pins, or a pin plus an anchor, has no spelling here
Identity::generate(), generate_for(names), from_pem_file(path), fingerprint()all fourpresent — values.rs:84-136. fingerprint() is a method and not a field because a from_pem_file identity is read lazily, so a corrupt file fails there with the path in hand
Identity::from_pem(chain, key), from_pem_files(chain, key), certificate_pem(), to_pem()—absent: PEM in memory cannot enter (a key from a secret store has no Python route in, config.rs:221-232) and a generated identity cannot be persisted from Python (config.rs:270). §10
Pem, ClientTls, ServerTls as types—absent: Trust and Identity are converted into them by the calls that take them (runtime.rs:92, 112-136), so the two combining types never appear in Python
IncomingMeta's 12 fields11 attributes: endpoint, content_len, content_type, topic, peer, sequence, missed, report, report_mode, report_id, traceparentpresent — values.rs:147-209, and the shape is flattened: gap becomes missed as a number, trace becomes the W3C traceparent string, and report becomes a list of wire values (§4.6)
IncomingMeta::tracestate—absent: forwarded unmodified by the library and not surfaced here. A caller that propagates a full trace context gets the traceparent and loses the vendor state
IncomingMeta::{report, report_mode, report_id}meta.report, meta.report_mode, meta.report_idpresent since B-243 — values.rs:168-179, 200-207. They were absent while the cursor surface was, because the metadata of a report a caller could not act on would have been worse than nothing; now a receiver acts on them (§4.6)
PeerIdentity as a typemeta.peer as str or Nonepresent as text — values.rs:157-161: sha256:… over QUIC, uid=… over a local socket, None for an anonymous or in-process peer. Never from a header, so it can be authorized on rather than claimed (0015); test_asyncio.py:47 asserts the None for a client that presented no identity
a survey's outcome: SurveyRun's answers, respondents() and late()weida.Survey — replies, asked, failed, late, silent()present as a shared value class, on both surfaces (§4.4) — values.rs:206-250
TransferMeta::with_content_lenset by the binding on request, send, replypresent implicitly — endpoints.rs:112, 188, 323
TransferMeta::with_content_type, with_trace (transfer.rs:44, 56)—absent: an outbound transfer cannot declare a content type or carry a trace from Python, although both are readable on arrival (§5 above)
weida::VERSION, weida::ALPNweida.VERSION, weida.ALPNpresent as module constants — lib.rs:102-103, asserted at test_asyncio.py:186-187
Limits, GuaranteeSet, codes, ErrorCode, Address/EndpointAddr, Fingerprint, LossCause, StopReason, Gap, TraceContext as types (lib.rs:67-72)—absent: none of them is a Python class. Where their values matter they arrive as str, int or an exception class

6. The sync surface, and exactly how it differs

weida.sync is a real submodule placed in sys.modules, so import weida.sync works and a traceback names weida.sync.Runtime (sync.rs:916-956, asserted by test_sync.py::test_the_submodule_is_importable_and_shares_its_values). It implements no protocol behaviour: it is weida::blocking — where the block_on, the owned reactor and the refusal of a call from inside a Tokio runtime live (crates/weida/src/blocking.rs:32-47) — with argument conversion around it (sync.rs:17-34). The GIL is released on every waiting call through Python::detach (sync.rs:36-42, and every py.detach(...) in that file), which is what makes one thread per endpoint the shape it is supposed to be (test_sync.py::test_a_request_is_answered_with_no_event_loop runs the serving half on a threading.Thread).

Fifteen classes: Runtime, Binding, Requester, Pusher, Subscriber, Replier, Request, Puller, Publisher, Paired, Surveyor, Respondent, BusMember, Cursors, Reporter (sync.rs:919-936) — every pattern of §4 and the cursor surface of §4.6, because B-244 and B-243 added each to both surfaces at once. The value classes are shared, not copied — weida.Trust, weida.Identity and weida.IncomingMeta are the same objects on both surfaces (sync.rs:50-52, sync.rs:62) — and so is the whole exception family (test_sync.py::test_the_failure_classes_are_the_same_on_both_surfaces).

What differs, row by row:

Asyncio surfaceweida.syncVerdict
every waiting call is a coroutinethe same call, blockingpresent — sync.rs:267-715
Runtime.drain(seconds) leaves the runtime usabledrain consumes the facade's runtimepresent as a difference, and it is the facade's: blocking::Runtime::drain takes self (crates/weida/src/blocking.rs:157), so the second call raises weida.RuntimeFailure — sync.rs:186-200, asserted by test_sync.py::test_a_runtime_is_shut_down_once
no shutdownruntime.shutdown()present here and absent there (§3) — sync.rs:203-209
requester.open(), pusher.open(), publisher.open(topic), puller.recv_stream(), subscriber.recv_stream()—absent from sync, with the reason at sync.rs:44-52: the blocking facade takes whole payloads by design, a synchronous caller that wants to stream wants the asyncio surface or the Rust API, and a sync module that invented its own streaming would be inventing a second facade. §9.3
OutgoingStream, IncomingStream, Reply, FanOut—absent as classes, following from the row above: none of the thirteen classes sync.rs registers is a stream
publisher.filter_count()—absent: the synchronous publisher has path, publish, subscriber_count and dropped (sync.rs:504-529), so test_sync.py:66-72 waits on subscriber_count where the asyncio tests wait on filter_count
publisher.dropped_on(topic)—absent: the per-cause breakdown is asyncio-only, and so it is in the facade (crates/weida/src/blocking.rs:569-606)
subscriber.peer_count() (pubsub.rs:336-338)—absent: sync.rs:352-386 gives the synchronous subscriber connect, subscribe, unsubscribe and recv, and the facade has no such accessor either (crates/weida/src/blocking.rs:383-426)
request.payload / request.meta after answeringweida.RuntimeFailure, naming the spent requestpresent as a refusal — sync.rs:65-72, 422-440: the blocking Request holds the payload inside the request it consumes, so reading it after reply is refused rather than stale
publish is not a coroutinepublish does not block eitherpresent, and it is the one call in the facade that waits for nothing (crates/weida/src/blocking.rs:578-581, sync.rs:512-515)
—blocking::Requester::endpoint() and the other endpoint() accessors, which hand out the async typeabsent from Python: the escape hatch the facade offers a Rust caller (crates/weida/src/blocking.rs:459-462) has no Python spelling, because the asyncio surface is the escape hatch here
await surveyor.survey(...) → weida.Surveysurveyor.survey(...) → the same weida.Surveypresent, and the same value class rather than a synchronous twin of it (sync.rs:594-628): the collected shape is the right one on a surface with no event loop for the reason §4.4 gives, and it happens to be the right one on both
paired.peer_count(), member.dropped(), respondent.accept()all threepresent — sync.rs:532-715; the synchronous Respondent.accept hands back weida.sync.Request, the class Replier.accept hands back, exactly as on the asyncio surface
await cursors.changed(), unboundedcursors.changed(seconds), mandatory deadline, TimeoutError when it passespresent as the one deliberate signature difference in this module (§4.6) — sync.rs:829-860. asyncio.wait_for bounds the asyncio call and composes; a parked thread has nothing, so the facade takes the deadline that the asyncio surface gets from its loop (crates/weida/src/blocking.rs:336-353, crates/weida/src/cursor.rs:280-296)
await stream.reporter(), await stream.cursors()—absent as stream calls, because sync has no streams (§9.3). The handles arrive from pusher.send(report=…) and puller.recv_reporting(...) instead, which is the whole-payload shape of the same thing

7. The exception family

22 classes: 21 named failures plus the base, counted as §1 states. Every failure of the library is a class under weida.WeidaError, each instance carrying errno and cause (errors.rs:1-23), and the base is what lets a caller catch the family (test_asyncio.py:184-185).

RustPythonVerdict
Error::Runtimeweida.RuntimeFailurepresent, and the one rename: weida.Runtime is the runtime, a module cannot have one name for two things, and the module with the collision silently keeps whichever was added last. So the class is RuntimeFailure, its errno says the same, and the variant it comes from is written beside it (errors.rs:45-53, 72-73). Both smoke.py:33-36 and test_sync.py:119-122 assert that weida.Runtime is not a BaseException
the other 20 variants of weida_core::Errorthe same names: InvalidAddress, InvalidEndpointPath, InvalidFingerprint, AlreadyRegistered, NotConnected, ConnectionLost, Negotiation, Protocol, Rejected, UnknownEndpoint, Unsupported, NoParkedConnection, NoReply, Canceled, Indeterminate, LimitExceeded, Tls, Untrusted, Io, Transportpresent — errors.rs:74-94
a variant added to the librarya compile error in this filepresent: name_of is an exhaustive match written by the same macro that writes the name list (errors.rs:54-70), so a new variant cannot silently arrive as the base class
Error::Displaycausepresent, and no second vocabulary: the library's own Display is the wording, because a binding that rephrased it would be a second one to keep in step (errors.rs:101-109)
Error::is_definite_failure() keeping Indeterminate out of the definite setweida.Indeterminate as a sibling of ConnectionLost, not a kind of itpresent — errors.rs:18-23, and it is the one class worth reading twice: the transfer may or may not have arrived, and a caller that treats it as a definite failure is wrong (FAILURE_MODEL.md)
distinguishable failures at a call siteRejected vs UnknownEndpoint vs LimitExceeded vs Untrusted as separate branchespresent — test_asyncio.py::test_an_unknown_path_and_a_refusal_are_distinct_classes, ::test_a_payload_above_the_ceiling_is_refused_rather_than_held, ::test_the_wrong_key_is_untrusted_and_says_which_one_answered, which also asserts the fingerprint that answered is inside cause

8. The byte boundary and the explicit receive ceiling

RuleWhereVerdict
a payload is bytes or bytearray; a str is a TypeErrorpayload_of (crates/py/weida-py-core/src/bytes.rs:64-72)refused by type: guessing an encoding for somebody else's wire format is how mojibake gets sent (bytes.rs:49-55)
one copy in, one copy out, and no second onepayload_of copies straight into the Vec the library keeps; py_bytes uses PyBytes::new (bytes.rs:62-80)present, with the reason zero-copy out is not available: the buffer protocol is not in the limited API before CPython 3.11 and the wheel is abi3 from 3.9, so a zero-copy path would exist only on some interpreters (bytes.rs:29-37)
every receive takes a ceiling in bytesReplier.accept(max_bytes), Requester.request(payload, max_reply_bytes), Puller.recv(max_bytes), Subscriber.recv(max_bytes), IncomingStream.read(max_bytes), IncomingStream.collect(max_bytes), and the same four on weida.syncpresent on all of them — endpoints.rs:247, 101, 391, pubsub.rs:309, streams.rs:175, 205, sync.rs:259, 327, 358, 437
there is no default ceilingnone anywherepresent, and this is the deliberate one: weida's payloads are streams and INVARIANTS.md forbids the core from materializing them, while a Python object is materialized — so the caller who wants the bytes in memory is the one who has to say how many of them there may be. A binding that chose a default would be choosing how much memory a stranger may make a Python process allocate (lib.rs:38-46, endpoints.rs:6-8, tests/test_asyncio.py:19-22)
a payload above the ceilingweida.LimitExceededrefused — test_asyncio.py::test_a_payload_above_the_ceiling_is_refused_rather_than_held, where the reply is 4096 bytes and the caller allowed 16
the ceiling on a stream bounds one piece, not the payloadIncomingStream.read(max)present — streams.rs:22-24, 166-169: a loop over read holds one piece at a time and the payload has no size limit at all

9. Deliberate deviations from the Rust API

9.1 No per-call timeouts, anywhere

No call in this module takes a timeout argument, and the one deadline that exists is Runtime.drain(seconds) (§9.4) and FanOut.write_within(chunk, seconds), which is a subscriber's room rather than a call's patience. The reason is that asyncio.wait_for already is a timeout and a cancelled weida coroutine resets its streams, so the peer learns rather than waits (endpoints.rs:88-91, README.md:87-92). A per-call timeout would be a second cancellation mechanism with the same effect and a worse composition: asyncio.wait_for, asyncio.timeout and a TaskGroup all compose, a keyword argument does not. The synchronous surface has no timeout either, and there weida::blocking's own shape is the limit: a blocked thread is interrupted by nothing.

9.2 No raw L0 surface: Peer and Acceptor are not bound

crates/weida/src/stream.rs is the stream core — the socket replacement, one Peer for dialling and one Acceptor for whatever arrives (stream.rs:117-126, 275-285) — and Runtime::peer(tls) (runtime.rs:272) and Listener::acceptor(path) (listener.rs:350) are the two doors into it. Neither is bound. What a Python caller does not get is the ability to take a stream without a pattern deciding what it is: an Acceptor hands over Incoming::Transfer and Incoming::Exchange and lets the application choose (stream.rs:266-285), where a Replier accepts exchanges only and a Puller one-way transfers only. The binding's own module documentation names this as the absence (lib.rs:55-62), and §10 says where a follow-up would add it.

9.3 No streamed form in weida.sync

The five streaming entry points of the asyncio surface have no synchronous counterpart (§6), because weida::blocking has none to wrap: it takes whole payloads by design and hands out the asynchronous endpoint for anything else (crates/weida/src/blocking.rs:27-30). A sync module that built streaming itself would be the second implementation 0013 §4.4 exists to prevent, and the order the other five bindings used — library facade first, binding sync second — is what avoids it (sync.rs:30-34).

9.4 drain takes seconds as a float, and the deadline is mandatory

weida::Runtime::drain takes a Duration; the Python call takes a float and converts with Duration::try_from_secs_f64, so a negative or non-finite value is weida.RuntimeFailure naming the argument (runtime.rs:145-151, sync.rs:164-169). Seconds as a float is the Python convention — asyncio.wait_for, threading.Event.wait and socket.settimeout all take one — and there is no None: the deadline is mandatory and finite for the reason 0009 §4.4 gives, that waiting on a peer without one is how a process hangs at shutdown. FanOut.write_within(chunk, seconds) converts identically (pubsub.rs:196-201).

9.5 One reply, one finish, and a spent handle says so

Request, FanOut, OutgoingStream, IncomingStream and Reply are all frozen classes two coroutines may hold, so "answered at most once" and "a transfer ends once" are enforced by an Option behind a mutex and a named failure, not by a panic: weida.NoReply with the sentence that says why (endpoints.rs:351-369, pubsub.rs:154-160, streams.rs:42-48). FanOut and the stream classes use a tokio::sync::Mutex because the guard is held across an await, which std's may not be (Cargo.toml:36-39, pubsub.rs:146-149).

9.6 Each object holds its runtime alive

Every endpoint, stream and request holds an Arc<Runtime> (endpoints.rs:37-38, streams.rs:57, pubsub.rs:44-45), so a program that keeps a Replier and drops the Runtime object is a program whose requests keep arriving: the reactor dies with the last handle, not with the first (endpoints.rs:10-12). The Rust API needs no such rule, because a Rust caller holds the Runtime for as long as its endpoints borrow from it.

9.7 Two text claims this document found, and closed

Both were prose that had fallen behind the code, and both were fixed when this document caught them rather than recorded as living defects:

  • lib.rs's module documentation said the synchronous facade "is B-205" — the shape of a sentence written before it landed — while lib.rs:105 registered weida.sync. It now states what the module has on both surfaces and what is absent with the reason.
  • pyproject.toml's and Cargo.toml's descriptions said "Req/Rep and Push/Pull", which was B-200's scope; Pub/Sub and the streamed surface landed in B-204 and PAIR, SURVEY and BUS in B-244, so the wheel's own summary understated the module it ships twice over. Both now name all six patterns and both surfaces.

The Typing :: Typed classifier was the third of the same kind and is handled in §10: it was removed, because a classifier is a claim and no stub file ships.

10. What is absent, with a reason and where a follow-up adds it

AbsentWhat a caller does not getWhere it would be added
the raw L0 surface, Peer and Acceptor (§9.2)taking a stream without a pattern interpreting it; Incoming::Transfer vs Incoming::Exchange as the application's choicea slice binding Runtime.peer(trust) and Binding.acceptor(path) with an Incoming the two arms of which are distinguishable in Python
a streamed form for PAIR (§4.4)writing or reading a pair's payload in pieces; Paired::open(meta) is the Rust callthe same shape Push/Pull has here — one paired.open() returning an OutgoingStream — if a caller ever needs it. PAIR, SURVEY and BUS themselves landed on both surfaces in B-244
Reporter::with_granularity(bytes, interval) (§4.6)choosing how often a reporter's records reach the wire; the library's default standsone method on weida.Reporter taking bytes and seconds, if a Python reporter ever needs coarser records. The cursor surface itself landed on both surfaces in B-243, and nothing is lost by the default: a cursor is absolute, so coarser records end at the same numbers
a Python server on a local transportbind_inproc, bind_unix, bind_pipe (listener.rs:197, 232, 263); the client side already dials all three (endpoints.rs:59-62)three coroutines on Runtime beside bind, each returning the binding kind the library has, plus the LocalPrincipal/WindowsPrincipal reading that makes uid=… useful
eight of RuntimeConfig's nine fields, and all of Limits (§3)every resource ceiling weida bounds — queue depths, connection counts, the subscriber budget, the guarantee set — is the library's default and cannot be moved from Pythona weida.Limits and keyword arguments on Runtime, in the shape mqtt-py.md §5 uses for the two limits that surface there
streaming in weida.sync (§9.3)a synchronous caller cannot write or read a payload in piecesweida::blocking gaining a streamed form first; building it in the binding is what 0013 §4.4 forbids
await runtime.shutdown() on the asyncio surface (§3)a close that does not wait for anything in flight; drain is the only endone coroutine wrapping weida::Runtime::shutdown, beside drain
await binding.close() (§3)an awaited unbind that waits for the endpoint to go idle; dropping the object is the only way to stop acceptingone coroutine wrapping weida::Binding::close (crates/weida/src/listener.rs:378)
weida.ErrorCode (§4.1)refusing a request with any code but Rejectedan ErrorCode enum or a string argument on refuse, with the protocol's codes named
Identity.from_pem, from_pem_files, certificate_pem, to_pem (§5)a key from a secret store entering without touching the filesystem, and a generated identity being persisted so an address survives a restartfour methods on Identity; to_pem returns a private key, so it needs the same "write it owner-only" wording the Rust doc has
multi-entry Trust (§5)two pins, or a pin plus an anchor, on one dialling endpointand_pin/and_anchor_file as chainable methods, or list arguments on the three constructors
IncomingRequest::canceled() (§4.1)seeing the requester walk away before answering, rather than at replyan awaitable on Request, which is what the Rust method is
TransferMeta::with_content_type, with_trace; publish_with_trace, open_with_trace (§4.3, §5)declaring a content type on an outbound payload, and propagating an inbound trace outward — the read side of both already workskeyword arguments on request, send, reply and publish
IncomingMeta.tracestate (§5)the vendor half of a W3C trace contextone more field in PyIncomingMeta::of
Publisher.drops(), Subscriber.filter_count(), Requester/Pusher.peer_count() (§4.3, §4.1)three read-only accessors the Rust API hasone method each
type stubsa .pyi or a py.typed marker: there is none anywhere under crates/py/, so a type checker sees an untyped extension module. The Typing :: Typed classifier was removed from pyproject.toml when this document found it, because a classifier is a claima stub file per module, generated or written, and the classifier back with it
a client for PyPy (B-266)import weida on PyPy, GraalPy, or a free-threaded CPython below 3.15nothing, and the answer is "no" rather than "later" — decided on the number that already exists. The cost of this binding is ~78 µs per operation dominated by one event-loop wakeup per await (IMPLEMENTATION.md §4), not by interpreting bytecode, so the part PyPy replaces is not the part that costs. Against that stands a second artifact per interpreter (§2: PyPy and GraalPy pin the stable ABI to their own version, and a free-threaded build below 3.15 gets no stable ABI at all), plus a second Python surface to keep in step with this one — which is what 0013 §4.4 exists to prevent. If it is ever wanted, the route is a cffi shim over the C ABI of 0030 §3.4, which makes it a consumer of that row rather than a binding of its own: a few hundred lines of Python over a stable header, with the surface list taken from this document. Free-threaded CPython is the case worth revisiting first, and it revisits itself: when abi3t (3.15) is the floor, one wheel serves both builds again
an interop section (§2)nothing measurable is missing: weida has one implementation, so there is no second peer to pair againsta second implementation of the protocol, which is not a binding slice

11. Sources

  • This binding: crates/py/weida-py/src/ (lib.rs, runtime.rs, endpoints.rs, pubsub.rs, patterns.rs, cursors.rs, streams.rs, sync.rs, values.rs, errors.rs), crates/py/weida-py/Cargo.toml, pyproject.toml, develop.sh, package.sh, smoke.py, README.md.
  • The shared foundation: crates/py/weida-py-core/src/ — bridge.rs (the coroutine and its lazy start), bytes.rs (the payload boundary), the exception-family machinery errors.rs builds on.
  • The API this document is the parity table of: crates/weida/src/ — lib.rs:67-99, config.rs, runtime.rs, listener.rs, endpoint.rs, transfer.rs, stream.rs, cursor.rs, blocking.rs.
  • Tests: crates/py/weida-py/tests/ — test_asyncio.py (8), test_patterns.py (9), test_sync.py (8), test_cursors.py (7). Run for B-243: 32 passed, and smoke.py resolving all 56 names through a freshly built wheel.
  • Normative documents the rows cite: PROTOCOL.md §6.4 and §9.4, PATTERNS.md §4.1, GUARANTEES.md §1, INVARIANTS.md, FAILURE_MODEL.md.
  • Decisions: 0014 §2 (one shared PyO3 foundation, asyncio first, the bytes boundary), 0013 §4.4 (nothing decided twice) and §4.7 (the verdicts), 0009 §4.4 (the mandatory finite deadline), 0010 §4.8 (the local address forms), 0015 (the proved peer).
  • Backlog: B-200, B-204, B-205, B-244 and B-243 in BACKLOG.md, whose merge notes are the recorded executions of the suite this document counts.