Skip to content

Releases: Query-farm/vgi-rpc-python

v0.20.0

11 Jun 18:31

Choose a tag to compare

v0.19.1

03 Jun 01:40

Choose a tag to compare

Full Changelog: v0.19.0...v0.19.1

v0.19.0

02 Jun 23:14

Choose a tag to compare

Full Changelog: v0.18.3...v0.19.0

v0.18.3

21 May 19:52

Choose a tag to compare

Add serve_named_pipe — Windows named-pipe RPC transport for the launch:/unix:// rendezvous (CPython has no AF_UNIX on Windows). run_server routes --unix to it on win32 with the PIPE: discovery prefix. Adds pywin32 as a Windows-only dependency.

v0.18.2

21 May 18:43

Choose a tag to compare

What's changed

Maintenance release: dependency security patches and CI runtime upgrades. No library API or behavior changes.

Security (Dependabot)

  • Bump idna 3.11 → 3.15 — fixes a bypass of the CVE-2024-3651 fix where specially crafted inputs to idna.encode() could slip through.
  • Bump pymdown-extensions 10.21.2 → 10.21.3 — fixes a regression in pymdownx.snippets that reintroduced a sibling-prefix path-traversal bypass despite restrict_base_path.

CI / workflows

  • Upgrade GitHub Actions to Node 24-compatible runtimes: actions/checkout@v5, astral-sh/setup-uv@v6, actions/attest-build-provenance@v4.

🤖 Generated with Claude Code

v0.18.1

21 May 18:34

Choose a tag to compare

What's changed since v0.17.1

Application protocol_version enforcement (0.18.0)

  • Protocols declaring protocol_version (a ClassVar[str], canonical semver) now have it enforced per-request at the dispatch boundary. The client sends vgi_rpc.protocol_version in custom metadata on every call; the server raises ProtocolVersionError on an exact major+minor mismatch (patch ignored).
  • __describe__ is exempt so a mismatched client can still introspect to discover the server's version.
  • Enforcement applies on both pipe and HTTP dispatch paths, with conformance coverage.

CLI forwards protocol_version (0.18.1)

  • The vgi-rpc CLI now forwards protocol_version on every call.

Conformance / platform

  • Skip http_externalize_always producer-error test on Windows (fixture-level skip).

🤖 Generated with Claude Code

v0.18.0

19 May 01:57

Choose a tag to compare

Application protocol_version enforcement

Introduces a per-Protocol surface version that vgi-rpc validates on every dispatched RPC. A Protocol that declares protocol_version: ClassVar[str] = "X.Y.Z" opts in; the framework emits vgi_rpc.protocol_version on every request batch's custom_metadata; the server enforces exact major+minor match (patch ignored) at the dispatch boundary on every transport (pipe / HTTP / unix / subprocess). Mismatches raise ProtocolVersionError with a directional message telling the reader which side to upgrade.

This is distinct from REQUEST_VERSION (wire framing, vgi-rpc's concern) and from per-catalog data-version semantics. It versions the method-and-schema contract between client and worker — the thing a C++ extension or Rust port has to keep in sync with the Python reference Protocol.

What's new

  • protocol_version ClassVar on Protocol subclasses. Read via vars() (not getattr) so subclasses that don't redeclare get None and opt out cleanly — no inheritance leaks. Validated at server / client construction; malformed semver raises ValueError at construction, not first RPC.
  • vgi_rpc.protocol_version request metadata key. Carried on every request batch from a vgi-rpc RpcClient bound to a Protocol that declares the version. Surfaced in __describe__ response metadata so mismatched clients can introspect the server's expected version.
  • ProtocolVersionError (subclass of VersionError, error_kind="protocol_version_mismatch") with four directional templates: client-too-old, server-too-old, malformed, missing.
  • Dispatch-boundary enforcement on every transport. RpcServer.serve_one for pipe / subprocess / unix; _run_unary_sync and _run_stream_init_sync for HTTP. __describe__ is exempt so version-mismatched clients can still introspect.
  • ConformanceService.protocol_version = "1.0.0" — cross-language ports (Go / TS / Java / Rust) must add the metadata key to claim conformance. Three new conformance tests: describe_protocol_version.surfaces_declared_version, describe_protocol_version.format, protocol_version.matched_dispatch_succeeds.

Breaking changes

  • Operator-supplied RpcServer(protocol_version="...") kwarg removed. This was a free-form access-log label with no enforcement and no observed consumers; the name is reused for the new strict-semver mechanism declared on the Protocol class. Pre-release, no deprecation shim. Operators relying on the access-log protocol_version field must remove it from parsers.
  • protocol_version field removed from access log JSON schema (vgi_rpc/access_log.schema.json, docs/access-log-spec.md). The new mechanism is enforced at the wire layer; access-log emission of the value is no longer load-bearing.

Cross-language source of truth

For ports that can't import vgi_rpc, the canonical version string for ConformanceService ships at https://github.com/Query-farm/vgi-rpc-python/blob/main/vgi_rpc/conformance/_protocol.py — and the Protocol class is the source of truth on the Python side. Bumping the version requires:

  1. Update protocol_version on the Protocol class.
  2. Cross-language ports re-read the metadata key on every request.
  3. Conformance suite enforces parity.

Known issues

  • http_externalize_always conformance variant is skipped on Windows for now. A pre-existing TCP race between waitress and httpx (WinError 10053) surfaces deterministically on Windows when the new metadata key shifts request-batch timing past a margin that v0.17.1 happened to clear by luck. Linux / macOS keep full coverage of this variant. The underlying race needs a framework-side fix (drain WSGI response body before close) and will be addressed in a follow-up.

Files

  • vgi_rpc/metadata.pyPROTOCOL_VERSION_KEY, parse_version, SEMVER_REGEX
  • vgi_rpc/rpc/_server.py_check_protocol_version, dispatch-boundary gate, ProtocolVersionError raise
  • vgi_rpc/rpc/_client.py, vgi_rpc/http/_client.py — request-time emission
  • vgi_rpc/http/server/_app_unary.py, _app_stream.py — HTTP dispatch-boundary gate
  • vgi_rpc/conformance/_protocol.pyprotocol_version = "1.0.0"
  • vgi_rpc/conformance/_runner.py — describe + matched-dispatch tests
  • tests/test_protocol_version.py — 45 unit tests covering pipe and HTTP, comparison matrix, inheritance, malformed, directional error messages

v0.17.1

18 May 14:35

Choose a tag to compare

Sticky streaming conformance coverage

Patch release on top of v0.17.0. The canonical TestSticky conformance group previously only exercised sticky sessions on unary calls — this release extends it with four tests that drive the same _StickyCounter through producer and exchange streams, so cross-language ports implementing sticky must now prove the contract holds across the multi-request shape of streaming RPCs (not just one-shot unary calls).

What's covered

  • TestSticky::test_producer_stream_resumes_session — a producer stream that increments and emits ctx.session.value on every iteration; verifies the session is rebound across the multi-turn producer shape.
  • TestSticky::test_exchange_stream_resumes_session — an exchange stream that mutates the session counter from the input by column on each round-trip; verifies state accumulates across independent HTTP exchange requests.
  • TestSticky::test_stream_without_session_raises — streaming method invoked outside with_session_token() surfaces RpcError with no session bound.
  • TestSticky::test_session_shared_between_unary_and_stream — open via unary, mutate via unary + producer stream + unary, close — all observe the same backing _StickyCounter.

ConformanceService surface additions

Cross-language ports implementing sticky must add these two methods to pass the streaming TestSticky tests:

  • stream_session_counter(count: int) -> Stream[StreamState] — producer stream emitting count increments of the sticky session counter.
  • exchange_session_counter() -> Stream[StreamState] — exchange stream that adds each input by column to the sticky session counter.

Describe method count bumps 79 → 81; runner _EXPECTED_METHODS and _STREAM_METHODS gain the two new names so the describe-conformance suite stays in sync.

Compatibility

  • No behaviour change for existing callers. Non-conformance code paths are byte-identical to 0.17.0.
  • Ports without sticky support skip the entire TestSticky group, including the new streaming tests — no port-side action required if sticky isn't implemented.
  • _StickyCounter moved from vgi_rpc.conformance._impl to vgi_rpc.conformance._types (a private helper; the relocation is mentioned only because in-tree tests imported it directly).

v0.17.0

18 May 14:17

Choose a tag to compare

Features

HTTP sticky sessions (opt-in)

A new opt-in feature for the HTTP transport lets a worker process keep live Python objects — open DuckDB cursors, loaded model handles, streaming LLM clients mid-generation, open file handles — bound to the worker that opened them, keyed by a short-lived AEAD-sealed session token the client echoes on subsequent requests. Non-sticky wire path is byte-identical to 0.16.1; existing callers see no behaviour change. The full cross-language wire spec lives at docs/sticky-sessions-spec.md.

Runtime API

Methods on the server, called from inside an RPC method body:

def open_query(self, sql: str, ctx) -> str:
    cursor = duckdb.connect().execute(sql)
    ctx.open_session(cursor)       # framework mints the token, attaches to response
    return "ok"

def next_rows(self, n: int, ctx) -> bytes:
    return ctx.session.fetch_arrow_table(n).serialize().to_pybytes()

def close_query(self, ctx) -> None:
    ctx.close_session()            # invokes cursor.close(), evicts the registry entry

Eviction is TTL-driven (default 300s, override per-call via ttl=) or explicit. state.close() is invoked on TTL eviction, explicit close, and graceful drain. The framework serializes concurrent calls on the same session via a per-session RLock; different sessions run in parallel. Sticky machinery is HTTP-only — ctx.open_session raises RuntimeError on pipe / subprocess / unix transports.

Client API

from vgi_rpc.http import http_connect

with http_connect(MyService, "http://localhost:8080") as conn, conn.with_session_token() as sess:
    sess.open_query(sql="SELECT * FROM big")
    rows = sess.next_rows(n=1000)
    sess.close_query()

The with conn.with_session_token(): block is the client-side opt-in. Inside the block every request carries VGI-Session-Accept: true and (after open) VGI-Session: <token>. On block exit the view fires a best-effort DELETE /vgi/__session__ to release handle-bearing state promptly. Stash a token across processes via sess.detach() + later conn.with_session_token(token=stashed).

Client-driven routing (Fly.io)

Server can tell the client to replay arbitrary headers on every subsequent request in the session — emitted as VGI-Echo-<name>: <value> on the session-opening response. Used for client-driven routing on platforms where the client knows where to go:

from vgi_rpc.http import make_wsgi_app
from vgi_rpc.http.fly import auto_server_id, fly_sticky_echo_headers

app = make_wsgi_app(
    server,
    enable_sticky=True,
    sticky_echo_headers=fly_sticky_echo_headers(),  # → {"fly-force-instance-id": <id>} on Fly, None elsewhere
)

On Fly the client replays fly-force-instance-id: <machine-id> on every subsequent request and fly-proxy routes directly to the owning Machine. Zero LB config required.

Graceful drain

from vgi_rpc.http import drain_handle, serve_http

# Built-in: serve_http installs SIGTERM/SIGINT handlers automatically
serve_http(server, enable_sticky=True, drain_grace_seconds=30.0)

# Pre-fork (gunicorn): wire your own
# def worker_exit(server, worker):
#     if (h := drain_handle(worker.app.callable)) is not None:
#         h.drain(); time.sleep(30); h.shutdown()

While drained, new ctx.open_session calls raise ServerDrainingError; existing-session calls continue to serve. Double-SIGTERM during grace skips the wait and exits immediately.

Typed errors

  • vgi_rpc.rpc.SessionLostError (error_kind="session_lost") — token decode failure, AAD mismatch, server_id mismatch (wrong worker), registry miss, or TTL expiry.
  • vgi_rpc.rpc.ServerDrainingError (error_kind="server_draining") — ctx.open_session while the server is draining. Existing sessions keep working.

Both follow the vgi_rpc.error_kind metadata convention introduced in 0.16, so client pattern matching against the kind string is wire-stable across language ports.

Access log additions

Records on sticky-touching requests carry two new fields:

  • session_id: 24-char hex (the framework-minted session ID), stable across the lifecycle for a given session.
  • session_action: enum "none" / "open" / "resume" / "close".

Both absent on non-sticky servers. Schema updated in vgi_rpc/access_log.schema.json; spec section at docs/access-log-spec.md §4.7.

Cross-language conformance

New TestSticky group in the canonical conformance suite (vgi_rpc/conformance/_pytest_suite.py) — 11 wire-protocol tests, capability-gated on VGI-Sticky-Enabled: true so ports without sticky support skip the entire group cleanly. Three new ConformanceService methods (open_counter, increment_counter, close_counter) exercise the open / resume / close lifecycle; one further test (test_echo_header_round_trip) is gated on VGI-Sticky-Echo-Headers; another (test_drain_rejects_new_opens) is gated on the conformance server exposing POST/DELETE /__test_drain__. Run against any port: vgi-rpc-test --url http://<server> --filter "Sticky::*".

Porting guide section at docs/porting-guide.md — describes the full contract any port must satisfy to claim sticky support.

Bug fixes

CI Lint unblocked

Two pre-existing CI Lint failures fixed in the same release cycle: pyarrow's stubs declare nulls() overloads only for concrete DataType subtypes, not the abstract base — silenced with a type-ignore on the one call site (vgi_rpc/utils.py:_empty_array). The ty type-checker also flagged drain_handle(app) because the parameter was typed object and isinstance-narrowed to App[Never, Never] — fixed by typing the parameter as falcon.App[falcon.Request, falcon.Response] directly.

Out of scope (deferred follow-ups)

  • Cookie emission for AWS ALB / CloudFront application-cookie stickiness. Header-only multiplexes cleanly across concurrent sessions where cookies cannot. Can be added as an additive operator flag if real demand emerges.
  • Middleware-short-circuit access-log records. Token validation failures (lost / expired / wrong-server) currently bypass the dispatch path where the access log emitter lives. Operators monitoring for misroutes can rely on the typed SessionLostError on the wire surface.
  • Pluggable session store. Sessions hold live Python objects in-process. Redis-style external stores are explicitly excluded — they don't work for the cursor / handle pattern and would compete with the well-defined "TTL eviction + crash = state lost" contract.

v0.16.1

13 May 17:16

Choose a tag to compare

Bug fixes

Linux idle-shutdown hang in serve_unix

_serve_unix_threaded armed an idle timer that closed the listening socket from another thread to interrupt the main accept() loop. On Linux this does not actually wake a blocked accept()close() only decrements the fd refcount, leaving the syscall waiting. macOS happens to wake it, which is why this only manifested on Linux: launcher-spawned workers stayed alive past their idle window and never unlinked their socket file.

The accept loop now runs with a 0.5s socket timeout and an explicit shutdown_requested flag the timer sets instead of closing the listener. Accepted connections are reset to blocking so per-call reads behave unchanged. No public API change; affects only the AF_UNIX (run_server --unix / vgi_rpc.launcher.launch) code path.

Windows mypy errors in launcher tests

tests/test_launcher.py references socket.AF_UNIX and os.geteuid, neither of which appears in Windows type stubs. The tests were already runtime-skipped on Windows but still got parsed by mypy. Added sys.platform != "win32" guards so mypy narrows the POSIX-only attribute access on Windows. Tests remain skipped on Windows.