@signicode/verser2-guest-python
v0.7.1
Published
Python ASGI Guest package for Verser2.
Downloads
808
Maintainers
Readme
@signicode/verser2-guest-python
Python package for verser2 providing Guest and Broker implementations.
This package connects outbound to an existing verser2 Host over TLS HTTP/2.
It is recognized by the repository's npm workspace tooling through
package.json and by Python packaging tooling through pyproject.toml.
Public API
VERSER2_GUEST_PYTHON_PACKAGE_NAMEVerserGuest/create_verser_guest— Python ASGI GuestVerserBroker/create_verser_broker— Python BrokerVerserBrokerResponse— Broker response typeVwsAsgiConnectionandbuild_websocket_scope— ASGI VWS/1 websocket helper types used by the live Python Guest pathdispatch_asgi_websocket— test helper for exercising a synthetic ASGI websocket lifecycle; applications should useVerserGuestinsteadguest.revoke_routes(domains)— revoke advertised route domains viaPOST /verser/guest/revoke; returnsdictwith"status"("ack","partial", or"error")broker.on_route_change(listener)— register a listener for route lifecycle events ("added","removed","changed","degraded") with payload keystype,targetId,domain,reason,generation; returns unsubscribe callable
Commands
npm run build --workspace=@signicode/verser2-guest-python
npm run test --workspace=@signicode/verser2-guest-python
npm run lint --workspace=@signicode/verser2-guest-pythonThe package commands use uv run --project . so Python dependencies such as
h2 are resolved in an isolated project environment.
Python Guest usage
The Guest serves an ASGI 3 app without opening an inbound listening port.
import asyncio
from verser2_guest_python import create_verser_guest
async def app(scope, receive, send):
assert scope["type"] == "http"
body = b""
while True:
event = await receive()
body += event.get("body", b"")
if not event.get("more_body", False):
break
await send({"type": "http.response.start", "status": 200, "headers": []})
await send({"type": "http.response.body", "body": body})
async def main():
guest = create_verser_guest(
host_url="https://localhost:8443",
guest_id="python-guest-a",
app=app,
routed_domains=["python-guest-a.local.test"],
tls_ca_file="/etc/verser/ca.crt",
# For mTLS Hosts, present a client identity as PEM:
# tls_cert_file="/etc/verser/client.crt",
# tls_key_file="/etc/verser/client.key",
# Or as PFX/PKCS12:
# tls_pfx_file="/etc/verser/client.p12",
# tls_pfx_password="...",
)
await guest.connect()
await asyncio.Event().wait()
asyncio.run(main())Domain note: Unlike Node and Bun Guests, the Python Guest does not
default the route domain to the Guest ID. You must provide routed_domains
explicitly.
FastAPI-compatible apps
FastAPI and Starlette applications work because the Guest calls the standard ASGI 3 interface. FastAPI is not a core runtime dependency.
from fastapi import FastAPI
from verser2_guest_python import create_verser_guest
app = FastAPI()
@app.get("/health")
async def health():
return {"ok": True}
guest = create_verser_guest(
host_url="https://localhost:8443",
guest_id="fastapi-guest",
app=app,
routed_domains=["fastapi-guest.local.test"],
tls_ca_file="/etc/verser/ca.crt",
)Python ASGI WebSockets
The Python Guest maps dedicated VWS/1 leases to ASGI websocket scopes:
async def app(scope, receive, send):
if scope["type"] == "websocket":
await receive() # websocket.connect
await send({"type": "websocket.accept"})
event = await receive()
if event["type"] == "websocket.receive":
await send({"type": "websocket.send", "text": "echo"})
returnThis is explicit framing over the existing TLS HTTP/2 transport, not generic
HTTP upgrade forwarding. Python Brokers can also initiate local, direct-remote,
and federated VWS/1 connections with await broker.websocket(url,
protocol=...); web_socket() is an alias. Python Host, fetch, Agent, and
Dispatcher APIs are not implemented.
See VWS/1 WebSockets for a complete Node Broker and Python ASGI example, runtime boundaries, and close/backpressure behavior.
Python Broker usage
The Python Broker connects outbound, registers as broker, and sends requests
to advertised Guest routes.
import asyncio
from verser2_guest_python import create_verser_broker
async def main():
broker = create_verser_broker(
host_url="https://localhost:8443",
broker_id="broker-a",
tls_ca_file="/etc/verser/ca.crt",
)
await broker.connect()
await broker.wait_for_route("python-guest-a.local.test")
response = await broker.get("http://python-guest-a.local.test/health")
print(response.status, response.status_text)
print(response.header_pairs) # ordered, repeated fields preserved
print(await response.text())
asyncio.run(main())The Broker supports request, get, post, put, patch, and delete
helpers. VerserBrokerResponse exposes status, status_text, headers,
header_pairs, request_id, read(), text(), json(), and
aiter_bytes(chunk_size=8192). headers is a last-value-wins compatibility
map; use ordered header_pairs when repeated fields such as set-cookie must
be preserved. status_text is None when the response does not supply one.
Python ASGI Guests do not originate a reason phrase, but Python Brokers expose
one received from a compatible remote response. Response bodies are one-shot.
TLS for Python Broker
broker = create_verser_broker(
host_url="https://localhost:8443",
broker_id="broker-a",
tls_ca_file="/etc/verser/ca.crt",
tls_cert_file="/etc/verser/client.crt",
tls_key_file="/etc/verser/client.key",
# PFX/PKCS12 also supported:
# tls_pfx_file="/etc/verser/client.p12",
# tls_pfx_password="...",
)Python Guests support the same tls_ca_file, PEM client identity, and
PFX/PKCS12 client identity options. PFX/PKCS12 support uses the package's
cryptography dependency.
Streaming behavior
- Guest: routed request body chunks from the Host/Broker lease stream are
delivered as ASGI
http.requestevents withmore_bodycontinuation flags. - Guest: ASGI
http.response.startis converted to the Verser response envelope before response bytes are written. - Guest: ASGI
http.response.bodyevents are written back to the Host lease stream;more_body: falseends the response side of the lease. - Direct
dispatch_routed_request(...)calls are batch-only — they buffer the ASGI response and enforcemax_response_bytesbefore joining chunks. Use leased Host/Broker routing for streaming. - Guest app exceptions before response start are returned as Verser
local-handler-failureerror envelopes with Guest, request, and path context. - Broker response bodies are one-shot;
read(),text(), andjson()consume the body.
Avoid non-terminating async streams
Verser Python transports use async read loops and async body iteration. Any custom async stream, test double, or request-body async iterable must eventually signal completion:
asynciostream readers should returnb""for EOF.- Async request-body iterables should stop iteration when the body is complete.
- Test mocks should not leave
reader.read()as a bareAsyncMock, because each awaited call can produce another truthy mock object forever.
Use an explicit EOF:
reader = AsyncMock()
reader.read = AsyncMock(return_value=b"")or a finite sequence:
reader.read = AsyncMock(side_effect=[b"first-frame", b""])Python Guest route revocation
A connected Python Guest can revoke one or more of its advertised routes:
result = await guest.revoke_routes(["python-guest-a.local.test"])
# result == {"status": "ack"} or {"status": "partial", "failedDomains": [...]}The request is sent to POST /verser/guest/revoke. The Host responds with
"ack" (all domains revoked), "partial" (some failed), or "error" (entire
request rejected). Raises RuntimeError if the Guest is not connected or
domains is empty.
Python Broker route lifecycle observation
Brokers can observe route changes reactively:
def on_change(event: dict):
print(event["type"], event["domain"], event.get("reason"))
unsubscribe = broker.on_route_change(on_change)
# Later, to stop observing:
unsubscribe()The internal route table (get_routes()) is updated before listeners fire. See
the Lifecycle and errors docs for event
types, reasons, and degraded-route behavior.
Known limits
- The first implementation focuses on Python Guest and Broker behavior.
- Python Host, Python-side fetch helper APIs, and Python-side Agent/Dispatcher are not implemented.
- Browser, Rust, Go, and Java WebSocket runtimes, HTTP/3, and Python Host federation endpoints are unsupported roadmap work.
- HTTP/3, complete application authentication, public gateway policy, per-request Broker target authorization, generic upgrades, CONNECT/RFC8441, trailers, Python Host/fetch/Agent/Dispatcher, and advanced ASGI lifespan behavior are not implemented.
- The HTTP transport is intentionally minimal: one outbound TLS HTTP/2 session with a replenished pool of one-use HTTP Guest lease streams. Long-lived VWS/1 WebSocket leases are dedicated streams and are not one-use request leases. VWS frames are limited to 1 MiB, queues are bounded, and transport loss is surfaced as a local abnormal close rather than replay or migration.
