Files
quicproquo/sdks/python/quicproquo/wire.py
Christian Nennemann 49e8e066d7 feat(sdk): add Python SDK with QUIC and FFI transport backends
Implements quicproquo-py with two transport backends:
- Async QUIC transport via aioquic with v2 protobuf wire format
- Synchronous Rust FFI transport via CFFI wrapping libquicproquo_ffi

Includes manual protobuf encode/decode (no codegen), full RPC coverage
(auth, delivery, channels, users, keys, health), PyPI-ready packaging,
async echo bot and FFI demo examples, and 15 passing unit tests.
2026-03-04 20:52:02 +01:00

74 lines
1.7 KiB
Python

"""v2 wire format: ``[method_id:u16][req_id:u32][len:u32][protobuf]``.
Each RPC is sent over its own QUIC stream. The response uses the same
framing on the same stream.
"""
from __future__ import annotations
import struct
# Header: method_id (u16) + req_id (u32) + length (u32) = 10 bytes.
HEADER_FMT = "!HII" # network byte-order: u16 + u32 + u32
HEADER_SIZE = struct.calcsize(HEADER_FMT)
# Method IDs (mirrors quicproquo-proto/src/lib.rs::method_ids).
# Auth (100-103)
OPAQUE_REGISTER_START = 100
OPAQUE_REGISTER_FINISH = 101
OPAQUE_LOGIN_START = 102
OPAQUE_LOGIN_FINISH = 103
# Delivery (200-205)
ENQUEUE = 200
FETCH = 201
FETCH_WAIT = 202
PEEK = 203
ACK = 204
BATCH_ENQUEUE = 205
# Keys (300-304)
UPLOAD_KEY_PACKAGE = 300
FETCH_KEY_PACKAGE = 301
UPLOAD_HYBRID_KEY = 302
FETCH_HYBRID_KEY = 303
FETCH_HYBRID_KEYS = 304
# Channel (400)
CREATE_CHANNEL = 400
# User (500-501)
RESOLVE_USER = 500
RESOLVE_IDENTITY = 501
# Blob (600-601)
UPLOAD_BLOB = 600
DOWNLOAD_BLOB = 601
# Device (700-702)
REGISTER_DEVICE = 700
LIST_DEVICES = 701
REVOKE_DEVICE = 702
# P2P (800-802)
PUBLISH_ENDPOINT = 800
RESOLVE_ENDPOINT = 801
HEALTH = 802
# Delete account (950)
DELETE_ACCOUNT = 950
def encode_frame(method_id: int, req_id: int, payload: bytes) -> bytes:
"""Encode a wire frame: header + protobuf payload."""
header = struct.pack(HEADER_FMT, method_id, req_id, len(payload))
return header + payload
def decode_header(data: bytes) -> tuple[int, int, int]:
"""Decode a wire frame header, returning (method_id, req_id, payload_len)."""
if len(data) < HEADER_SIZE:
raise ValueError(f"header too short: {len(data)} < {HEADER_SIZE}")
method_id, req_id, length = struct.unpack(HEADER_FMT, data[:HEADER_SIZE])
return method_id, req_id, length