mirror of
https://github.com/zap-proto/py.git
synced 2026-10-30 15:04:09 +00:00
Make the published zap-proto package contain real code instead of stubs. Wire (the product, was entirely absent): - src/zap/wire.py — pure-stdlib port of the canonical Go runtime (zap.go + builder.py + accessors + list cursors). Message/Object/List zero-copy reads, Builder/ObjectBuilder/ListBuilder writes, 16-byte ZAP\x00 header, Version1+2 accept, reader hardening (reject backward pointers into the header, clamp list length). Byte-faithful to Go — pinned by the shared cross-wire golden vector. RPC (was a sleep-loop server + all-mock client): - src/zap/rpc.py — real TCP request/response transport, Server dispatches the Zap interface method ordinals (@0..@8 from zap.zap), Client calls them. - app.run() now serves real RPC; client.Client is a real RPC client. Crypto (was secrets.token_bytes() fakes whose two sides never agreed and whose verify() accepted any blob): - src/zap/crypto.py — real ML-KEM-768 / ML-DSA-65 (pqcrypto) + X25519 (cryptography), hybrid HKDF-SHA256 KEX. Hard-fails with CryptoError when a backend is missing — never fabricates bytes. Package wiring: - __init__.py exposes the wire API with ZERO third-party dep on the import path; pydantic/httpx are lazy (app.ZAP, Client). All __all__ names resolve on a default install (was 18 of 22 unresolved, swallowed ImportError). - pyproject: drop nonexistent pyzap>=2.0.0; pydantic -> [app]; add [crypto]; fix repo URL to zap-proto/py; target py312. Bug fixes (audit PoCs): - frame.py: reject oversize routing ids cleanly (was u16 struct crash); lenient decode of non-UTF8 ids (was strict-decode crash); encode/decode_cmd type tag so str round-trips to str (was silently bytes). - protocol.py: decode_stream reports bytes consumed (decode dropped 2nd frame). - app.py: _match_uri_template escapes literal template text (was regex over-match). - consensus.py: fix votes int/dict type collision. Quality: ruff clean, ruff format clean, mypy --strict 0 issues, 52 tests pass. Add .github/workflows/ci.yml (ruff + format + mypy + pytest, py3.10-3.13). Rebrand: zap.capnp -> zap.zap, $Go.import -> github.com/zap-proto/go.
75 lines
2.0 KiB
Python
75 lines
2.0 KiB
Python
"""ZAP RPC — real client <-> server over TCP, dispatching method ordinals.
|
|
|
|
These tests stand a real :class:`zap.rpc.Server` on a background thread and drive
|
|
it with a real :class:`zap.rpc.Client`; nothing is mocked.
|
|
"""
|
|
|
|
import threading
|
|
|
|
import pytest
|
|
|
|
from zap.rpc import Client, Method, RpcError, Server
|
|
|
|
|
|
def _serve(server: Server) -> threading.Thread:
|
|
server.bind()
|
|
t = threading.Thread(target=server.serve_forever, daemon=True)
|
|
t.start()
|
|
return t
|
|
|
|
|
|
def test_rpc_call_dispatches_to_handler():
|
|
server = Server()
|
|
server.handle(Method.CALL_TOOL, lambda p: {"sum": p["a"] + p["b"]})
|
|
_serve(server)
|
|
try:
|
|
client = Client.connect("127.0.0.1", server.port)
|
|
assert client.call(Method.CALL_TOOL, {"a": 3, "b": 4}) == {"sum": 7}
|
|
finally:
|
|
client.close()
|
|
server.close()
|
|
|
|
|
|
def test_rpc_multiple_sequential_calls():
|
|
server = Server()
|
|
server.handle(Method.LIST_TOOLS, lambda _p: ["a", "b"])
|
|
server.handle(Method.INIT, lambda p: {"echo": p})
|
|
_serve(server)
|
|
try:
|
|
client = Client.connect("127.0.0.1", server.port)
|
|
assert client.call(Method.LIST_TOOLS) == ["a", "b"]
|
|
assert client.call(Method.INIT, {"x": 1}) == {"echo": {"x": 1}}
|
|
assert client.call(Method.LIST_TOOLS) == ["a", "b"]
|
|
finally:
|
|
client.close()
|
|
server.close()
|
|
|
|
|
|
def test_rpc_handler_error_propagates():
|
|
server = Server()
|
|
|
|
def boom(_p):
|
|
raise ValueError("kaboom")
|
|
|
|
server.handle(Method.CALL_TOOL, boom)
|
|
_serve(server)
|
|
try:
|
|
client = Client.connect("127.0.0.1", server.port)
|
|
with pytest.raises(RpcError, match="kaboom"):
|
|
client.call(Method.CALL_TOOL, {})
|
|
finally:
|
|
client.close()
|
|
server.close()
|
|
|
|
|
|
def test_rpc_unknown_method():
|
|
server = Server()
|
|
_serve(server)
|
|
try:
|
|
client = Client.connect("127.0.0.1", server.port)
|
|
with pytest.raises(RpcError, match="unknown method"):
|
|
client.call(Method.LOG, {})
|
|
finally:
|
|
client.close()
|
|
server.close()
|